diff --git a/EMBEDDING.md b/EMBEDDING.md index 4b091d8..f4d7edc 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -2,6 +2,11 @@ The `embed-code` utility uses a custom `` tag to insert code snippets from source files into Markdown documentation. +For executable examples of the embedding features described here, see the +[embed-code showcase](examples/showcase/README.md). The showcase uses paired +instruction tags and dedicated source fixtures so it can double as a guide and +an opt-in end-to-end test. + ## Embedding options There are two ways to specify which code fragment to embed: @@ -209,18 +214,18 @@ Not all languages distinguish documentation from regular comments or inline from The table below lists the supported languages and supported `comments` modes for them: -| Language | Extensions | Supported `comments` modes | -|------------------------|---------------------------------------------------------|--------------------------------------------------------------| -| Java, Kotlin, Groovy | `.java`, `.kt`, `.kts`, `.groovy` | `all`, `none`, `documentation`, `regular`, `inline`, `block` | -| C# | `.cs` | `all`, `none`, `documentation`, `regular`, `inline`, `block` | +| Language | Extensions | Supported `comments` modes | +|------------------------|----------------------------------------------------------|--------------------------------------------------------------| +| Java, Kotlin, Groovy | `.java`, `.kt`, `.kts`, `.groovy` | `all`, `none`, `documentation`, `regular`, `inline`, `block` | +| C# | `.cs` | `all`, `none`, `documentation`, `regular`, `inline`, `block` | | C, C++ | `.c`, `.h`, `.cc`, `.cpp`, `.cxx`, `.hh`, `.hpp`, `.hxx` | `all`, `none`, `inline`, `block` | -| JavaScript, TypeScript | `.js`, `.jsx`, `.ts`, `.tsx` | `all`, `none`, `documentation`, `regular`, `inline`, `block` | -| Go | `.go` | `all`, `none`, `inline`, `block` | -| Protobuf | `.proto` | `all`, `none`, `inline`, `block` | -| Python | `.py`, `.pyi`, `.pyw` | `all`, `none` | -| YAML | `.yml`, `.yaml` | `all`, `none` | -| XML, HTML | `.xml`, `.html`, `.htm` | `all`, `none` | -| Visual Basic | `.vb`, `.bas`, `.vbs`, `.vbscript` | `all`, `none`, `documentation`, `regular` | +| JavaScript, TypeScript | `.js`, `.jsx`, `.ts`, `.tsx` | `all`, `none`, `documentation`, `regular`, `inline`, `block` | +| Go | `.go` | `all`, `none`, `inline`, `block` | +| Protobuf | `.proto` | `all`, `none`, `inline`, `block` | +| Python | `.py`, `.pyi`, `.pyw` | `all`, `none` | +| YAML | `.yml`, `.yaml` | `all`, `none` | +| XML, HTML | `.xml`, `.html`, `.htm` | `all`, `none` | +| Visual Basic | `.vb`, `.bas`, `.vbs`, `.vbscript` | `all`, `none`, `documentation`, `regular` | ## Advanced use cases diff --git a/README.md b/README.md index bf33995..7265c99 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ This project is the implementation of `embed-code` utility written in Go. For the details of the usage in the documentation and the code, please refer to the [EMBEDDING.md](EMBEDDING.md). +For a runnable guide with positive examples, negative examples, and YAML +configuration shapes, see the [embed-code showcase](examples/showcase/README.md). +The showcase is an opt-in end-to-end test and is not part of the normal +`go test ./...` flow. + ## Running Embed Code operates in two modes: diff --git a/examples/showcase/README.md b/examples/showcase/README.md new file mode 100644 index 0000000..843e943 --- /dev/null +++ b/examples/showcase/README.md @@ -0,0 +1,42 @@ +# Embed Code Showcase + +This is an executable showcase guide to `embed-code-go` and the end-to-end tests. + +## How To Use This Guide + +Guide is divided on two categories: + +1. [Configuration](configuration/README.md) - describes how to configure the whole embed-code application. +2. [Embedding](embedding/README.md) - describes how to work with the embedding instructions. + +## How To Run Tests + +Run commands from the repository root. + +Run the opt-in end-to-end test with the `showcase` build tag: + +```bash +go test -tags showcase ./examples/showcase +``` + +Verify the positive embedding examples: + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/embedding/embed-code.yml +``` + +Verify the configuration examples: + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/configuration/single-source.yml +go run ./main.go -mode=check -config-path=examples/showcase/configuration/named-sources.yml +go run ./main.go -mode=check -config-path=examples/showcase/configuration/include-exclude.yml +go run ./main.go -mode=check -config-path=examples/showcase/configuration/multiple-embeddings.yml +``` + +The negative examples are intentionally broken, so these commands should fail: + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/embedding/negative/processing-errors.yml +go run ./main.go -mode=check -config-path=examples/showcase/embedding/negative/stale.yml +``` diff --git a/examples/showcase/code/java/org/showcase/CommentModes.java b/examples/showcase/code/java/org/showcase/CommentModes.java new file mode 100644 index 0000000..3cbf65f --- /dev/null +++ b/examples/showcase/code/java/org/showcase/CommentModes.java @@ -0,0 +1,14 @@ +package org.showcase; + +/** + * Creates public greetings. + */ +public interface CommentModes { + /* + * Internal implementation note. + */ + String URL = "http://example.org/*not-comment*/"; + + // Regular inline comment. + String greet(String name); // trailing inline comment. +} diff --git a/examples/showcase/code/java/org/showcase/Greeting.java b/examples/showcase/code/java/org/showcase/Greeting.java new file mode 100644 index 0000000..60ab7c9 --- /dev/null +++ b/examples/showcase/code/java/org/showcase/Greeting.java @@ -0,0 +1,17 @@ +package org.showcase; + +// #docfragment "Greeter class" +public final class Greeting { + private Greeting() {} + + // #docfragment "main()" + public static void main(String[] args) { + System.out.println(greeting("Ada")); + } + // #enddocfragment "main()" + + public static String greeting(String name) { + return "Hello, " + name + "!"; + } +} +// #enddocfragment "Greeter class" diff --git a/examples/showcase/code/java/org/showcase/MultiPartWorkflow.java b/examples/showcase/code/java/org/showcase/MultiPartWorkflow.java new file mode 100644 index 0000000..71c4ba3 --- /dev/null +++ b/examples/showcase/code/java/org/showcase/MultiPartWorkflow.java @@ -0,0 +1,23 @@ +package org.showcase; + +// #docfragment "Workflow" +public final class MultiPartWorkflow { + // #enddocfragment "Workflow" + private MultiPartWorkflow() {} + + // #docfragment "Workflow" + public static void start() { + // #enddocfragment "Workflow" + System.out.println("Internal setup is hidden."); + // #docfragment "Workflow" + System.out.println("Start workflow"); + } + // #enddocfragment "Workflow" + + public static void finish() { + System.out.println("Finish workflow"); + } + +// #docfragment "Workflow" +} +// #enddocfragment "Workflow" diff --git a/examples/showcase/code/java/org/showcase/OverlappingFragments.java b/examples/showcase/code/java/org/showcase/OverlappingFragments.java new file mode 100644 index 0000000..602ec4a --- /dev/null +++ b/examples/showcase/code/java/org/showcase/OverlappingFragments.java @@ -0,0 +1,28 @@ +package org.showcase; + +// #docfragment "Class wrapper", "Greeting method" +public final class OverlappingFragments { + // #enddocfragment "Class wrapper", "Greeting method" + private OverlappingFragments() {} + + // #docfragment "Class wrapper" + public static void boot() { + // #enddocfragment "Class wrapper" + System.out.println("Boot details are hidden."); + // #docfragment "Class wrapper" + System.out.println("Boot complete"); + } + // #enddocfragment "Class wrapper" + + // #docfragment "Greeting method" + public static String greeting(String name) { + // #enddocfragment "Greeting method" + var normalized = name.trim(); + // #docfragment "Greeting method" + return "Hello, " + normalized + "!"; + } + // #enddocfragment "Greeting method" + +// #docfragment "Class wrapper", "Greeting method" +} +// #enddocfragment "Class wrapper", "Greeting method" diff --git a/examples/showcase/code/java/org/showcase/PatternSamples.java b/examples/showcase/code/java/org/showcase/PatternSamples.java new file mode 100644 index 0000000..8d429f8 --- /dev/null +++ b/examples/showcase/code/java/org/showcase/PatternSamples.java @@ -0,0 +1,22 @@ +package org.showcase; + +class PatternSamples { + + private static final String ESCAPED_NEWLINE = "\n"; + + @Scenario + @Name("adds two numbers") + void addsTwoNumbers() { + int total = 1 + 1; + + assertEquals(2, total); + } + + @Scenario + @Name("subtracts two numbers") + void subtractsTwoNumbers() { + int total = 2 - 1; + + assertEquals(1, total); + } +} diff --git a/examples/showcase/code/kotlin/org/showcase/KotlinGreeting.kt b/examples/showcase/code/kotlin/org/showcase/KotlinGreeting.kt new file mode 100644 index 0000000..5b06ade --- /dev/null +++ b/examples/showcase/code/kotlin/org/showcase/KotlinGreeting.kt @@ -0,0 +1,11 @@ +package org.showcase + +object KotlinGreeting { + + // #docfragment "main()" + @JvmStatic + fun main(args: Array) { + println("Hello from Kotlin") + } + // #enddocfragment "main()" +} diff --git a/examples/showcase/code/text/glob-patterns.txt b/examples/showcase/code/text/glob-patterns.txt new file mode 100644 index 0000000..d7529af --- /dev/null +++ b/examples/showcase/code/text/glob-patterns.txt @@ -0,0 +1,5 @@ + padded text +Use * to multiply +The total is $5 +The value ends with $ +^ starts with caret diff --git a/examples/showcase/configuration/README.md b/examples/showcase/configuration/README.md new file mode 100644 index 0000000..6aabe27 --- /dev/null +++ b/examples/showcase/configuration/README.md @@ -0,0 +1,121 @@ +# Configuration Examples + +This folder is a runnable guide to YAML configuration. Start with the smallest +working config, then add only the options your documentation needs. + +## Minimal Config + +A configuration needs one source root and one documentation root: + +```yaml +code-path: examples/showcase/code/java +docs-path: examples/showcase/configuration/docs/single-source +``` + +This config is shown by [single-source.yml](single-source.yml). + +The application scans files under `docs-path`, finds `` instructions, +and resolves each instruction's `file` path from `code-path`. For example, see +instruction in [docs/single-source/greeting.md](docs/single-source/greeting.md). + +Relative paths in `code-path` and `docs-path` are resolved +from the command's current working directory. + +Run this example (from the project root): + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/configuration/single-source.yml +``` + +## Add Document Selection + +Add `doc-includes` when only some files under `docs-path` should be scanned. +Add `doc-excludes` when selected files should be skipped: + +```yaml +code-path: examples/showcase/code/java +docs-path: examples/showcase/configuration/docs/include-exclude +doc-includes: + - "**/*.md" +doc-excludes: + - excluded.md +``` + +This shape is shown by [include-exclude.yml](include-exclude.yml). It processes +[docs/include-exclude/included.md](docs/include-exclude/included.md) and skips +[docs/include-exclude/excluded.md](docs/include-exclude/excluded.md). + +Use include and exclude patterns to skip drafts, generated docs, deprecated +pages, or any file that should not be scanned for active instructions. + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/configuration/include-exclude.yml +``` + +## Add Named Source Roots + +Use named source roots when one documentation tree embeds snippets from several +source trees: + +```yaml +code-path: + - name: java + path: examples/showcase/code/java + - name: kotlin + path: examples/showcase/code/kotlin + - name: text + path: examples/showcase/code/text +docs-path: examples/showcase/configuration/docs/named-sources +``` + +This shape is shown by [named-sources.yml](named-sources.yml). Its docs live in +[docs/named-sources](docs/named-sources/). + +Instructions choose a source root with the `$name` prefix: + +```markdown + +``` + +Run the named-source example: + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/configuration/named-sources.yml +``` + +## Add Multiple Documentation Targets + +Use `embeddings` when one command should process several independent +documentation targets. Each entry has its own `name`, `code-path`, `docs-path`, +and optional settings: + +```yaml +embeddings: + - name: java-guide + code-path: examples/showcase/code/java + docs-path: examples/showcase/configuration/docs/multiple/java + - name: kotlin-guide + code-path: + - name: kotlin + path: examples/showcase/code/kotlin + docs-path: examples/showcase/configuration/docs/multiple/kotlin +``` + +This shape is shown by [multiple-embeddings.yml](multiple-embeddings.yml). It +processes [docs/multiple/java](docs/multiple/java/) and +[docs/multiple/kotlin](docs/multiple/kotlin/) in one run. + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/configuration/multiple-embeddings.yml +``` + +## All Configuration Checks + +Run commands from the project root. + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/configuration/single-source.yml +go run ./main.go -mode=check -config-path=examples/showcase/configuration/named-sources.yml +go run ./main.go -mode=check -config-path=examples/showcase/configuration/include-exclude.yml +go run ./main.go -mode=check -config-path=examples/showcase/configuration/multiple-embeddings.yml +``` diff --git a/examples/showcase/configuration/docs/include-exclude/excluded.md b/examples/showcase/configuration/docs/include-exclude/excluded.md new file mode 100644 index 0000000..991f2a3 --- /dev/null +++ b/examples/showcase/configuration/docs/include-exclude/excluded.md @@ -0,0 +1,14 @@ +# Excluded Document + +The config excludes this file. The intentionally missing source file proves +that excluded documents are not processed. + +## How It Works + +[../../include-exclude.yml](../../include-exclude.yml) lists this file in +`doc-excludes`. The instruction points at a missing source file, but the command +still succeeds because excluded files are skipped before instruction parsing. + + +```java +``` diff --git a/examples/showcase/configuration/docs/include-exclude/included.md b/examples/showcase/configuration/docs/include-exclude/included.md new file mode 100644 index 0000000..a7dd199 --- /dev/null +++ b/examples/showcase/configuration/docs/include-exclude/included.md @@ -0,0 +1,18 @@ +# Included Document + +The `doc-includes` pattern selects this Markdown file, so the instruction is +processed normally. + +## How It Works + +[../../include-exclude.yml](../../include-exclude.yml) includes Markdown files +under this docs root. Because this file is not listed in `doc-excludes`, check +mode resolves the instruction and compares the rendered fence with the Java +source fragment. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` diff --git a/examples/showcase/configuration/docs/multiple/java/greeting.md b/examples/showcase/configuration/docs/multiple/java/greeting.md new file mode 100644 index 0000000..727e27d --- /dev/null +++ b/examples/showcase/configuration/docs/multiple/java/greeting.md @@ -0,0 +1,17 @@ +# Java Embedding Entry + +This document is processed by the `java-guide` entry in an `embeddings` config. + +## How It Works + +[../../../multiple-embeddings.yml](../../../multiple-embeddings.yml) contains a +`java-guide` entry with its own `code-path` and `docs-path`. This document lives +under that entry's docs root, so the unprefixed source path resolves against the +Java source tree. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` diff --git a/examples/showcase/configuration/docs/multiple/kotlin/greeting.md b/examples/showcase/configuration/docs/multiple/kotlin/greeting.md new file mode 100644 index 0000000..6a78a25 --- /dev/null +++ b/examples/showcase/configuration/docs/multiple/kotlin/greeting.md @@ -0,0 +1,19 @@ +# Kotlin Embedding Entry + +This document is processed by the `kotlin-guide` entry in the same +`embeddings` config. + +## How It Works + +[../../../multiple-embeddings.yml](../../../multiple-embeddings.yml) contains a +separate `kotlin-guide` entry. That entry defines a named `kotlin` source root, +so this instruction uses `$kotlin` even though it is processed by the same +top-level command as the Java entry. + + +```kotlin +@JvmStatic +fun main(args: Array) { + println("Hello from Kotlin") +} +``` diff --git a/examples/showcase/configuration/docs/named-sources/java-greeting.md b/examples/showcase/configuration/docs/named-sources/java-greeting.md new file mode 100644 index 0000000..655e392 --- /dev/null +++ b/examples/showcase/configuration/docs/named-sources/java-greeting.md @@ -0,0 +1,17 @@ +# Named Java Source + +This config has multiple named source roots. The `$java` prefix chooses the Java +root before resolving the relative path. + +## How It Works + +[../../named-sources.yml](../../named-sources.yml) defines a source root named +`java`. The instruction must include `$java` so the resolver knows which source +tree owns `org/showcase/Greeting.java`. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` diff --git a/examples/showcase/configuration/docs/named-sources/kotlin-greeting.md b/examples/showcase/configuration/docs/named-sources/kotlin-greeting.md new file mode 100644 index 0000000..9cb7f17 --- /dev/null +++ b/examples/showcase/configuration/docs/named-sources/kotlin-greeting.md @@ -0,0 +1,18 @@ +# Named Kotlin Source + +The same config can embed from a different root by changing the source-root +prefix in the instruction. + +## How It Works + +[../../named-sources.yml](../../named-sources.yml) also defines a source root +named `kotlin`. Changing the prefix to `$kotlin` resolves the same relative +package path under the Kotlin source tree. + + +```kotlin +@JvmStatic +fun main(args: Array) { + println("Hello from Kotlin") +} +``` diff --git a/examples/showcase/configuration/docs/named-sources/text-line.md b/examples/showcase/configuration/docs/named-sources/text-line.md new file mode 100644 index 0000000..38d679f --- /dev/null +++ b/examples/showcase/configuration/docs/named-sources/text-line.md @@ -0,0 +1,16 @@ +# Named Text Source + +Named roots do not need to be language-specific. This example embeds one line +from the text source root. + +## How It Works + +[../../named-sources.yml](../../named-sources.yml) defines a source root named +`text`. The `line` pattern matches one plain-text line from +`glob-patterns.txt`, showing that source roots can point at any supported text +fixture, not only programming language files. + + +```text +The total is $5 +``` diff --git a/examples/showcase/configuration/docs/single-source/greeting.md b/examples/showcase/configuration/docs/single-source/greeting.md new file mode 100644 index 0000000..0fbb97f --- /dev/null +++ b/examples/showcase/configuration/docs/single-source/greeting.md @@ -0,0 +1,16 @@ +# Single Source Root + +This config uses one unnamed `code-path`. + +## How It Works + +[../../single-source.yml](../../single-source.yml) points `code-path` directly +at `examples/showcase/code/java`. The instruction therefore uses +`org/showcase/Greeting.java` instead of `$java/org/showcase/Greeting.java`. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` diff --git a/examples/showcase/configuration/include-exclude.yml b/examples/showcase/configuration/include-exclude.yml new file mode 100644 index 0000000..d165aaf --- /dev/null +++ b/examples/showcase/configuration/include-exclude.yml @@ -0,0 +1,6 @@ +code-path: examples/showcase/code/java +docs-path: examples/showcase/configuration/docs/include-exclude +doc-includes: + - "**/*.md" +doc-excludes: + - excluded.md diff --git a/examples/showcase/configuration/multiple-embeddings.yml b/examples/showcase/configuration/multiple-embeddings.yml new file mode 100644 index 0000000..0316823 --- /dev/null +++ b/examples/showcase/configuration/multiple-embeddings.yml @@ -0,0 +1,13 @@ +embeddings: + - name: java-guide + code-path: examples/showcase/code/java + docs-path: examples/showcase/configuration/docs/multiple/java + doc-includes: + - "**/*.md" + - name: kotlin-guide + code-path: + - name: kotlin + path: examples/showcase/code/kotlin + docs-path: examples/showcase/configuration/docs/multiple/kotlin + doc-includes: + - "**/*.md" diff --git a/examples/showcase/configuration/named-sources.yml b/examples/showcase/configuration/named-sources.yml new file mode 100644 index 0000000..a624a75 --- /dev/null +++ b/examples/showcase/configuration/named-sources.yml @@ -0,0 +1,10 @@ +code-path: + - name: java + path: examples/showcase/code/java + - name: kotlin + path: examples/showcase/code/kotlin + - name: text + path: examples/showcase/code/text +docs-path: examples/showcase/configuration/docs/named-sources +doc-includes: + - "**/*.md" diff --git a/examples/showcase/configuration/single-source.yml b/examples/showcase/configuration/single-source.yml new file mode 100644 index 0000000..bf2e503 --- /dev/null +++ b/examples/showcase/configuration/single-source.yml @@ -0,0 +1,2 @@ +code-path: examples/showcase/code/java +docs-path: examples/showcase/configuration/docs/single-source diff --git a/examples/showcase/embedding/README.md b/examples/showcase/embedding/README.md new file mode 100644 index 0000000..5085494 --- /dev/null +++ b/examples/showcase/embedding/README.md @@ -0,0 +1,63 @@ +# Embedding Examples + +This folder is a runnable guide to embedding instructions. The positive +examples show supported features, and the negative examples show the failures a +user should expect when an instruction is malformed or stale. + +Run the positive examples from the repository root: + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/embedding/embed-code.yml +``` + +## Feature Examples + +### Simple Embedding + +- [Whole file source](positive/whole-file-source.md) + shows how to embed the whole source file. +- [Instruction tag](positive/instruction-tag.md) + shows the preferred way to use `` tag. +- [Named source root](positive/named-source-root.md) + shows how to use different configured source trees. + +### Line, Range, And Glob Matching + +- [Source line pattern](positive/source-line-pattern.md) + embeds the first source line that matches a `line` pattern. +- [Start and end patterns](positive/start-end-pattern.md) + embeds an inclusive source range selected by `start` and `end`. +- [Multi-line patterns](positive/multi-line-pattern.md) + uses `\n` to match consecutive source lines. +- [Pattern escaping](positive/pattern-escaping.md) + shows how to escape special characters. + +### Fragments + +- [Named fragment](positive/named-fragment.md) + embeds a region wrapped with `#docfragment` and `#enddocfragment` markers. +- [Multi-part fragment separator](positive/multi-part-fragment-separator.md) + joins repeated fragment parts with the configured separator. +- [Overlapping fragments](positive/overlapping-fragments.md) + shows fragment markers that share source lines. + +### Rendered Content And Documents + +- [Comment filtering](positive/comment-filtering.md) + shows how to omit comments in the source code. +- [Markdown fence shielding](positive/markdown-fence-shielding.md) + shows that instruction-looking text inside ordinary code fences is inert. +- [HTML showcase](positive/html-showcase.html) + shows that HTML documents can be processed when the include patterns allow them. + +## Negative Examples + +The negative examples are intentionally broken and should fail. +Use them to recognize common diagnostics: + +```bash +go run ./main.go -mode=check -config-path=examples/showcase/embedding/negative/processing-errors.yml +go run ./main.go -mode=check -config-path=examples/showcase/embedding/negative/stale.yml +``` + +The cases live in [negative/docs](negative/docs/). diff --git a/examples/showcase/embedding/embed-code.yml b/examples/showcase/embedding/embed-code.yml new file mode 100644 index 0000000..1f88c8a --- /dev/null +++ b/examples/showcase/embedding/embed-code.yml @@ -0,0 +1,12 @@ +code-path: + - name: java + path: examples/showcase/code/java + - name: kotlin + path: examples/showcase/code/kotlin + - name: text + path: examples/showcase/code/text +docs-path: examples/showcase/embedding/positive +doc-includes: + - "**/*.md" + - "**/*.html" +separator: "// ..." diff --git a/examples/showcase/embedding/negative/docs/invalid-attributes.md b/examples/showcase/embedding/negative/docs/invalid-attributes.md new file mode 100644 index 0000000..eb7b17d --- /dev/null +++ b/examples/showcase/embedding/negative/docs/invalid-attributes.md @@ -0,0 +1,14 @@ +# Invalid Attributes + +This scenario shows a structurally invalid instruction. + +## How It Fails + +`fragment` selects a named source region, while `line`, `start`, and `end` +select by pattern. The instruction combines `fragment` and `line`, so the tool +rejects it before reading the source file. In a real guide, choose one selection +style for each instruction. + + +```java +``` diff --git a/examples/showcase/embedding/negative/docs/missing-code-fence.md b/examples/showcase/embedding/negative/docs/missing-code-fence.md new file mode 100644 index 0000000..1e51cde --- /dev/null +++ b/examples/showcase/embedding/negative/docs/missing-code-fence.md @@ -0,0 +1,14 @@ +# Missing Code Fence + +This scenario shows what happens when an active instruction has no owned code +fence. + +## How It Fails + +Every instruction must be followed immediately by a Markdown code fence. The +plain text line below is not a fence, so the parser reports the missing fence at +the instruction. In a real guide, add an opening and closing fence after the +instruction, even if the fence starts empty. + + +This line is not a code fence. diff --git a/examples/showcase/embedding/negative/docs/missing-fragment.md b/examples/showcase/embedding/negative/docs/missing-fragment.md new file mode 100644 index 0000000..63ea22a --- /dev/null +++ b/examples/showcase/embedding/negative/docs/missing-fragment.md @@ -0,0 +1,15 @@ +# Missing Fragment + +This scenario shows what happens when the source file exists but the named +fragment does not. + +## How It Fails + +[../../../code/java/org/showcase/Greeting.java](../../../code/java/org/showcase/Greeting.java) +contains fragments such as `main()`, but it does not contain `does not exist`. +Check mode reports the missing fragment. In a real guide, use an existing +fragment name or add matching source markers. + + +```java +``` diff --git a/examples/showcase/embedding/negative/docs/missing-pattern.md b/examples/showcase/embedding/negative/docs/missing-pattern.md new file mode 100644 index 0000000..a107ef8 --- /dev/null +++ b/examples/showcase/embedding/negative/docs/missing-pattern.md @@ -0,0 +1,15 @@ +# Missing Pattern + +This scenario shows what happens when a line pattern matches nothing. + +## How It Fails + +The source file is found, but no line in +[../../../code/java/org/showcase/Greeting.java](../../../code/java/org/showcase/Greeting.java) +matches `doesNotExistPattern`. Check mode reports the unmatched pattern. In a +real guide, loosen the glob pattern, add anchors only where needed, or point the +instruction at the intended source file. + + +```java +``` diff --git a/examples/showcase/embedding/negative/docs/missing-source.md b/examples/showcase/embedding/negative/docs/missing-source.md new file mode 100644 index 0000000..a2ac946 --- /dev/null +++ b/examples/showcase/embedding/negative/docs/missing-source.md @@ -0,0 +1,14 @@ +# Missing Source + +This scenario shows what happens when the `file` attribute cannot be resolved. + +## How It Fails + +The `$java` root exists, but `org/showcase/DoesNotExist.java` is not present +under [../../../code/java](../../../code/java/). Check mode reports the missing source +file and leaves the document unchanged. In a real guide, fix the path or add the +missing source file. + + +```java +``` diff --git a/examples/showcase/embedding/negative/docs/stale-snippet.md b/examples/showcase/embedding/negative/docs/stale-snippet.md new file mode 100644 index 0000000..f4b28cc --- /dev/null +++ b/examples/showcase/embedding/negative/docs/stale-snippet.md @@ -0,0 +1,18 @@ +# Stale Snippet + +This scenario is syntactically valid, but the rendered code is out of date. + +## How It Fails + +Check mode resolves the `main()` fragment from +[../../../code/java/org/showcase/Greeting.java](../../../code/java/org/showcase/Greeting.java) +and compares it with the existing fence. The fence contains different text, so +check mode reports the document as stale without rewriting it. Embed mode would +replace the fence with the current source fragment. + + +```java +public static void main(String[] args) { + System.out.println("Out of date"); +} +``` diff --git a/examples/showcase/embedding/negative/docs/unclosed-code-fence.md b/examples/showcase/embedding/negative/docs/unclosed-code-fence.md new file mode 100644 index 0000000..35315de --- /dev/null +++ b/examples/showcase/embedding/negative/docs/unclosed-code-fence.md @@ -0,0 +1,15 @@ +# Unclosed Code Fence + +This scenario shows what happens when the instruction has an opening fence but +no closing fence. + +## How It Fails + +The parser finds the opening fence after the instruction and then reaches the +end of the file before seeing a matching closing fence. In a real guide, close +the fence with the same marker style and at least the same marker length. + + +```java +public static void main(String[] args) { +} diff --git a/examples/showcase/embedding/negative/processing-errors.yml b/examples/showcase/embedding/negative/processing-errors.yml new file mode 100644 index 0000000..e04d3a5 --- /dev/null +++ b/examples/showcase/embedding/negative/processing-errors.yml @@ -0,0 +1,13 @@ +code-path: + - name: repo + path: . + - name: java + path: examples/showcase/code/java +docs-path: examples/showcase/embedding/negative/docs +doc-includes: + - missing-source.md + - missing-fragment.md + - missing-pattern.md + - invalid-attributes.md + - missing-code-fence.md + - unclosed-code-fence.md diff --git a/examples/showcase/embedding/negative/stale.yml b/examples/showcase/embedding/negative/stale.yml new file mode 100644 index 0000000..1347787 --- /dev/null +++ b/examples/showcase/embedding/negative/stale.yml @@ -0,0 +1,6 @@ +code-path: + - name: java + path: examples/showcase/code/java +docs-path: examples/showcase/embedding/negative/docs +doc-includes: + - stale-snippet.md diff --git a/examples/showcase/embedding/positive/comment-filtering.md b/examples/showcase/embedding/positive/comment-filtering.md new file mode 100644 index 0000000..e1b43bc --- /dev/null +++ b/examples/showcase/embedding/positive/comment-filtering.md @@ -0,0 +1,144 @@ +# Comment Filtering + +Use `comments` when examples should keep useful API documentation but omit +implementation notes. + +## How It Works + +The instruction first resolves the requested source content using `file` plus +any `fragment`, `start`, `end`, or `line` selection. It then applies comment +filtering before comparing or rendering the code fence. If `comments` is +omitted, the default is `all`, so every recognized comment remains in the snippet. + +Supported modes are: + +- `all` keeps every comment. +- `none` removes every recognized comment. +- `documentation` keeps documentation comments, such as Javadoc. +- `regular` keeps non-documentation line and block comments. +- `inline` keeps non-documentation line comments, such as `//`. +- `block` keeps non-documentation block comments, such as `/* */`. + +Comment support depends on the source file extension. Unknown extensions are +embedded unchanged. Not every supported language distinguishes documentation, +regular, inline, and block comments, so unsupported categories simply have no +comments to keep. See +[Comment filtering](../../../../EMBEDDING.md#comment-filtering) for the full +language matrix. + +The examples below embed the same Java file with different `comments` modes so +the rendered output can be compared directly. + +## All Comments + +`comments="all"` keeps every recognized comment. Omitting `comments` would +produce the same result because `all` is the default. + + +```java +package org.showcase; + +/** + * Creates public greetings. + */ +public interface CommentModes { + /* + * Internal implementation note. + */ + String URL = "http://example.org/*not-comment*/"; + + // Regular inline comment. + String greet(String name); // trailing inline comment. +} +``` + +## No Comments + +`comments="none"` removes every recognized comment. + + +```java +package org.showcase; + +public interface CommentModes { + String URL = "http://example.org/*not-comment*/"; + + String greet(String name); +} +``` + +## Documentation Comments + +`comments="documentation"` keeps Javadoc and removes regular block and inline +comments. Comment-like text inside string literals stays unchanged because it is +not a real comment. + + +```java +package org.showcase; + +/** + * Creates public greetings. + */ +public interface CommentModes { + String URL = "http://example.org/*not-comment*/"; + + String greet(String name); +} +``` + +## Regular Comments + +`comments="regular"` keeps non-documentation line and block comments and removes +documentation comments. + + +```java +package org.showcase; + +public interface CommentModes { + /* + * Internal implementation note. + */ + String URL = "http://example.org/*not-comment*/"; + + // Regular inline comment. + String greet(String name); // trailing inline comment. +} +``` + +## Inline Comments + +`comments="inline"` keeps non-documentation line comments and removes block and +documentation comments. + + +```java +package org.showcase; + +public interface CommentModes { + String URL = "http://example.org/*not-comment*/"; + + // Regular inline comment. + String greet(String name); // trailing inline comment. +} +``` + +## Block Comments + +`comments="block"` keeps non-documentation block comments and removes inline and +documentation comments. + + +```java +package org.showcase; + +public interface CommentModes { + /* + * Internal implementation note. + */ + String URL = "http://example.org/*not-comment*/"; + + String greet(String name); +} +``` diff --git a/examples/showcase/embedding/positive/html-showcase.html b/examples/showcase/embedding/positive/html-showcase.html new file mode 100644 index 0000000..af867f9 --- /dev/null +++ b/examples/showcase/embedding/positive/html-showcase.html @@ -0,0 +1,23 @@ + + + +

HTML Embedding Showcase

+

+ HTML files are scanned when the include patterns allow them. The instruction + below uses the same tag form as the Markdown examples and still needs a + Markdown code fence immediately after it. +

+

+ The source root comes from embed-code.yml, and + the rendered snippet is checked the same way as a Markdown document. Embed + mode can update the fenced snippet in an HTML file. +

+ + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` + + diff --git a/examples/showcase/embedding/positive/instruction-tag.md b/examples/showcase/embedding/positive/instruction-tag.md new file mode 100644 index 0000000..71b09a4 --- /dev/null +++ b/examples/showcase/embedding/positive/instruction-tag.md @@ -0,0 +1,33 @@ +# Instruction Tag + +An embedding instruction is an XML-like tag placed immediately before the code +fence that embed-code should manage. The tag contains source-selection +attributes such as `file`, `fragment`, `line`, etc. + +The following Markdown fences keeps the language label used by renderers for +syntax highlighting. + +## Paired Tag + +The paired form is preferred in Markdown because it is displayed consistently +by most renderers. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` + +## Self-Closing Tag + +The self-closing form is supported and resolves the same source content, +but it is preferred to use paired tags elsewhere because they tend to look better +in Markdown previews. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` diff --git a/examples/showcase/embedding/positive/markdown-fence-shielding.md b/examples/showcase/embedding/positive/markdown-fence-shielding.md new file mode 100644 index 0000000..4638674 --- /dev/null +++ b/examples/showcase/embedding/positive/markdown-fence-shielding.md @@ -0,0 +1,23 @@ +# Instructions Inside Markdown Fences + +Use a normal Markdown fence when documentation needs to show an embedding +instruction as plain text. This lets guides explain the syntax without causing +the example instruction to run. + +## How It Works + +The parser tracks ordinary code fences before it looks for active +`` instructions. Instruction-looking text inside a fence is +therefore preserved as documentation content, not treated as a real instruction. + +An active instruction must appear outside a fence and must be followed by its +own managed code fence. The nested fence in this example is only part of the +displayed Markdown snippet. + +## Shielded Example + +````markdown + +```go +``` +```` diff --git a/examples/showcase/embedding/positive/multi-line-pattern.md b/examples/showcase/embedding/positive/multi-line-pattern.md new file mode 100644 index 0000000..eab3dfb --- /dev/null +++ b/examples/showcase/embedding/positive/multi-line-pattern.md @@ -0,0 +1,50 @@ +# Multi-Line Patterns + +Use `\n` inside a pattern when one source line is not specific enough to select +the right source range. + +## How It Works + +A multi-line pattern is a sequence of ordinary line patterns separated by `\n`. +The match succeeds only when those patterns match neighboring source lines in +the same order. This works for `start`, `end`, and `line` patterns. + +Spaces around `\n` are ignored, so `Scenario \n adds two numbers` is treated as +two line patterns: `Scenario` and `adds two numbers`. Each part keeps the same +glob behavior as a one-line pattern. When a part does not start with `^`, it may +begin anywhere in the source line. When it does not end with `$`, it may stop +before the source line ends. Add `^`, `$`, or both to the individual part that +needs a stricter boundary. + +## Start And End Pattern + +Here the `start` value matches the `@Scenario` line followed immediately by the +display-name line. The `end` value does the same for the assertion line and the +closing brace. + + +```java +@Scenario +@Name("adds two numbers") +void addsTwoNumbers() { + int total = 1 + 1; + + assertEquals(2, total); +} +``` + +## Line Pattern + +The `line` attribute also accepts `\n`. In this case the matched consecutive +source lines become the rendered snippet instead of a single source line. + + +```java +@Scenario +@Name("adds two numbers") +``` diff --git a/examples/showcase/embedding/positive/multi-part-fragment-separator.md b/examples/showcase/embedding/positive/multi-part-fragment-separator.md new file mode 100644 index 0000000..43c01b1 --- /dev/null +++ b/examples/showcase/embedding/positive/multi-part-fragment-separator.md @@ -0,0 +1,36 @@ +# Multi-Part Fragment Separator + +Use a multi-part fragment when one documentation example should show several +non-adjacent pieces of a source file as one snippet. This is useful for keeping +the public shape of an example while hiding setup, implementation details, or +unrelated branches between the selected parts. + +## How It Works + +A fragment becomes multi-part when the same `#docfragment "name"` marker is +opened and closed more than once in the same source file. Embed-code collects +the selected parts in source order, normalizes common indentation across all of +them, and inserts the configured `separator` between neighboring parts. + +The default separator is `...`. This showcase uses `// ...` in +[../embed-code.yml](../embed-code.yml) so the separator is valid inside Java +snippets. Separator indentation follows the surrounding rendered code, which +keeps skipped sections readable inside classes and methods. + +## Embedding Instruction + +[../../code/java/org/showcase/MultiPartWorkflow.java](../../code/java/org/showcase/MultiPartWorkflow.java) +opens and closes the `Workflow` fragment several times. The instruction below +renders those selected parts as one snippet. + + +```java +public final class MultiPartWorkflow { + // ... + public static void start() { + // ... + System.out.println("Start workflow"); + } +// ... +} +``` diff --git a/examples/showcase/embedding/positive/named-fragment.md b/examples/showcase/embedding/positive/named-fragment.md new file mode 100644 index 0000000..dbcc4e4 --- /dev/null +++ b/examples/showcase/embedding/positive/named-fragment.md @@ -0,0 +1,45 @@ +# Named Fragment + +Use `fragment` when the source file can mark a stable region that documentation +should reuse. Named fragments are usually easier to maintain than line patterns +when the example has a clear semantic boundary, such as a method, class, or +configuration block. + +## How It Works + +A named fragment is declared in the source file with `#docfragment "name"` +before the first line to include and `#enddocfragment "name"` after the last +line to include. The marker text can sit inside the comment syntax of the source +language, so Java uses `//`, Kotlin uses `//`, and HTML can use ``. + +The `fragment` value in the embedding instruction must match the source marker +name exactly. During embed mode or check mode, embed-code resolves the named +region, removes the marker lines, normalizes common indentation, and compares or +updates the following code fence. If the fragment name is not present in the +source file, the run reports the missing fragment. + +## Source Markers + +[../../code/java/org/showcase/Greeting.java](../../code/java/org/showcase/Greeting.java) +declares the `main()` fragment like this: + +```java +// #docfragment "main()" +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +// #enddocfragment "main()" +``` + +## Embedding Instruction + +The `file` attribute points to the source file, and `fragment` selects the +named region inside that file. A named fragment cannot be combined with +`start`, `end`, or `line`; use one source-selection method per instruction. + + +```java +public static void main(String[] args) { + System.out.println(greeting("Ada")); +} +``` diff --git a/examples/showcase/embedding/positive/named-source-root.md b/examples/showcase/embedding/positive/named-source-root.md new file mode 100644 index 0000000..febeaee --- /dev/null +++ b/examples/showcase/embedding/positive/named-source-root.md @@ -0,0 +1,25 @@ +# Named Source Roots + +Use named source roots when one documentation set needs snippets from several +source trees. Names keep instructions explicit and avoid relying on whichever +configured root happens to contain a matching relative path. + +## How It Works + +In config file, each entry in `code-path` can have a `name` and a `path`. +When an instruction starts its `file` value with `$name/`, embed-code selects +only that named root and then resolves the remaining relative path inside it. + +[../embed-code.yml](../embed-code.yml) defines `java`, `kotlin`, and `text` +source roots. The `$kotlin` prefix below chooses the Kotlin root before +resolving `org/showcase/KotlinGreeting.kt`. + +## Embedding Instruction + + +```kotlin +@JvmStatic +fun main(args: Array) { + println("Hello from Kotlin") +} +``` diff --git a/examples/showcase/embedding/positive/overlapping-fragments.md b/examples/showcase/embedding/positive/overlapping-fragments.md new file mode 100644 index 0000000..6276541 --- /dev/null +++ b/examples/showcase/embedding/positive/overlapping-fragments.md @@ -0,0 +1,30 @@ +# Overlapping Fragments + +Use overlapping fragments when different documentation examples need to share +some source lines but hide different details. One marker line can name several +fragments, and each named fragment is resolved independently. + +## How It Works + +A marker can open or close multiple fragments by listing several quoted names: +`#docfragment "Class wrapper", "Greeting method"`. + +## Embedding Instruction + +[../../code/java/org/showcase/OverlappingFragments.java](../../code/java/org/showcase/OverlappingFragments.java) +uses marker lines that name both `Class wrapper` and `Greeting method`. The +instruction asks only for `Greeting method`, so the rendered snippet keeps the +shared class wrapper and the greeting method while replacing skipped details +with the configured separator. + + +```java +public final class OverlappingFragments { + // ... + public static String greeting(String name) { + // ... + return "Hello, " + normalized + "!"; + } +// ... +} +``` diff --git a/examples/showcase/embedding/positive/pattern-escaping.md b/examples/showcase/embedding/positive/pattern-escaping.md new file mode 100644 index 0000000..adf9e06 --- /dev/null +++ b/examples/showcase/embedding/positive/pattern-escaping.md @@ -0,0 +1,58 @@ +# Pattern Escaping + +Pattern escaping distinguishes glob syntax from source text that happens to use +the same characters. + +## How It Works + +Patterns use glob control characters, so `*`, `?`, and character classes have +special meaning unless they are escaped with a backslash. The anchors `^` and +`$` are special only at the beginning and end of a pattern part. Use `^^` at the +beginning to match a literal caret and `$$` at the end to match a literal dollar +sign. + +The sequence `\n` separates consecutive pattern lines. Use `\\n` when the source +line contains the literal characters `\n`. + +## Literal Asterisk + +The pattern `Use \* to multiply` treats `*` as source text instead of a +wildcard. It matches a line in +[../../code/text/glob-patterns.txt](../../code/text/glob-patterns.txt). + + +```text +Use * to multiply +``` + +## Literal Dollar At The End + +`$` is an end anchor only at the end of a pattern. Use `$$` there when the +source line itself ends with a dollar sign. + + +```text +The value ends with $ +``` + +## Literal Caret At The Start + +`^` is a start anchor only at the start of a pattern. Use `^^` there when the +source line itself starts with a caret. + + +```text +^ starts with caret +``` + +## Literal Backslash-N Text + +Use `\\n` when the source line contains the characters backslash and `n`. The +quote characters are written as `\"` so the instruction remains valid XML. + + +```java +private static final String ESCAPED_NEWLINE = "\n"; +``` diff --git a/examples/showcase/embedding/positive/source-line-pattern.md b/examples/showcase/embedding/positive/source-line-pattern.md new file mode 100644 index 0000000..21e4ca0 --- /dev/null +++ b/examples/showcase/embedding/positive/source-line-pattern.md @@ -0,0 +1,25 @@ +# Source Line Pattern + +Use `line` when the documentation needs one source line instead of a whole +fragment or range. + +## How It Works + +The `line` attribute uses the same glob-style pattern syntax as `start` and`end`. +By default, embed-code behaves as if `*` exists at the beginning and end +of the pattern, so `Hello` can match any source line that contains `Hello`. +Use `^` or `$` when the match must start or end at a line boundary. + +Only the first matching source line is rendered into the fence. A `line` pattern +cannot be combined with `fragment`, `start`, or `end`. + +## Embedding Instruction + +The instruction below searches +[../../code/java/org/showcase/Greeting.java](../../code/java/org/showcase/Greeting.java) +and renders the first line that contains `Hello`. + + +```java +return "Hello, " + name + "!"; +``` diff --git a/examples/showcase/embedding/positive/start-end-pattern.md b/examples/showcase/embedding/positive/start-end-pattern.md new file mode 100644 index 0000000..0468243 --- /dev/null +++ b/examples/showcase/embedding/positive/start-end-pattern.md @@ -0,0 +1,36 @@ +# Start And End Patterns + +Use `start` and `end` patterns to select the code snippet. + +## How It Works + +`start` and `end` select an inclusive source range. Embed-code first searches +for the `start` pattern, then searches for the `end` pattern after that match. +Both matched boundary lines are included in the rendered snippet. + +By default, embed-code behaves as if `*` exists at the beginning and end +of the pattern, so `Hello` can match any source line that contains `Hello`. +Use `^` or `$` when the match must start or end at a line boundary. + +If `start` is omitted, the range starts at the beginning of the file. +If `end` is omitted, it continues to the end of the file. + +## Embedding Instruction + +The instruction below finds the first `@Scenario` in +[../../code/java/org/showcase/PatternSamples.java](../../code/java/org/showcase/PatternSamples.java). +It then stops at the next line that is exactly four spaces followed by `}`. + + +```java +@Scenario +@Name("adds two numbers") +void addsTwoNumbers() { + int total = 1 + 1; + + assertEquals(2, total); +} +``` diff --git a/examples/showcase/embedding/positive/whole-file-source.md b/examples/showcase/embedding/positive/whole-file-source.md new file mode 100644 index 0000000..9c42108 --- /dev/null +++ b/examples/showcase/embedding/positive/whole-file-source.md @@ -0,0 +1,35 @@ +# Whole File Source + +Use a whole-file embedding when the documentation should mirror a complete +small source file. This is the smallest useful instruction: it only needs +`file`, followed by the code fence. + +## How It Works + +The `file` attribute is resolved from the configured source roots. + +In this example, the `$java` prefix selects the Java source root from +[../embed-code.yml](../embed-code.yml) before resolving `org/showcase/Greeting.java`. + +## Embedding Instruction + +The instruction below embeds the contents of +[../../code/java/org/showcase/Greeting.java](../../code/java/org/showcase/Greeting.java) +into the following code fence, without the embed-code-related instructions. + + +```java +package org.showcase; + +public final class Greeting { + private Greeting() {} + + public static void main(String[] args) { + System.out.println(greeting("Ada")); + } + + public static String greeting(String name) { + return "Hello, " + name + "!"; + } +} +``` diff --git a/examples/showcase/showcase_test.go b/examples/showcase/showcase_test.go new file mode 100644 index 0000000..2133368 --- /dev/null +++ b/examples/showcase/showcase_test.go @@ -0,0 +1,331 @@ +//go:build showcase + +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package showcase_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// TestShowcase runs the showcase example suite. +func TestShowcase(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Showcase Suite") +} + +var _ = Describe("Showcase", func() { + var repoRoot string + + BeforeEach(func() { + repoRoot = findRepoRoot() + }) + + Describe("embedding examples", func() { + It("should check, detect staleness, embed, and recheck positive examples", func() { + docsRoot := copyShowcaseDocs(repoRoot, filepath.Join("embedding", "positive")) + configPath := writeShowcaseConfig(repoRoot, docsRoot) + + checkOutput, err := runEmbedCode(repoRoot, "check", configPath) + Expect(err).ShouldNot(HaveOccurred(), "expected positive showcase check to pass:\n%s", checkOutput) + + staleDoc := filepath.Join(docsRoot, "whole-file-source.md") + replaceInFile(staleDoc, "package org.showcase;", "package stale.showcase;") + + staleOutput, err := runEmbedCode(repoRoot, "check", configPath) + Expect(err).Should(HaveOccurred(), "expected stale showcase check to fail:\n%s", staleOutput) + Expect(staleOutput).Should(ContainSubstring("File to update:")) + Expect(staleOutput).Should(ContainSubstring("whole-file-source.md")) + + embedOutput, err := runEmbedCode(repoRoot, "embed", configPath) + Expect(err).ShouldNot(HaveOccurred(), "expected positive showcase embed to repair stale doc:\n%s", embedOutput) + Expect(embedOutput).Should(ContainSubstring("Embedding process finished.")) + + finalOutput, err := runEmbedCode(repoRoot, "check", configPath) + Expect(err).ShouldNot(HaveOccurred(), "expected positive showcase check to pass after embed:\n%s", finalOutput) + }) + + Describe("negative examples", func() { + for _, tc := range negativeShowcaseCases() { + tc := tc + + It("should report "+tc.name, func() { + docsRoot := copyShowcaseDocs(repoRoot, filepath.Join("embedding", "negative", "docs")) + configPath := writeSingleDocConfig( + docsRoot, + []namedSource{javaSource(repoRoot)}, + tc.doc, + ) + + output, err := runEmbedCode(repoRoot, "check", configPath) + Expect(err).Should(HaveOccurred(), "expected negative scenario to fail:\n%s", output) + for _, expected := range tc.expected { + Expect(output).Should(ContainSubstring(expected)) + } + }) + } + }) + }) + + Describe("configuration examples", func() { + for _, config := range []string{ + "single-source.yml", + "named-sources.yml", + "include-exclude.yml", + "multiple-embeddings.yml", + } { + config := config + + It("should check "+config, func() { + configPath := filepath.Join("examples", "showcase", "configuration", config) + + output, err := runEmbedCode(repoRoot, "check", configPath) + Expect(err).ShouldNot(HaveOccurred(), "expected configuration example to pass:\n%s", output) + }) + } + }) +}) + +// negativeShowcaseCase describes one intentionally broken showcase document. +type negativeShowcaseCase struct { + name string + doc string + expected []string +} + +// namedSource is the named code source path. +type namedSource struct { + name string + path string +} + +// negativeShowcaseCases returns the expected failures for the broken embedding examples. +func negativeShowcaseCases() []negativeShowcaseCase { + return []negativeShowcaseCase{ + { + name: "missing source", + doc: "missing-source.md", + expected: []string{ + "code file `$java/org/showcase/DoesNotExist.java", + "not found", + }, + }, + { + name: "missing fragment", + doc: "missing-fragment.md", + expected: []string{ + "fragment `does not exist`", + "not found", + }, + }, + { + name: "missing pattern", + doc: "missing-pattern.md", + expected: []string{ + "matches the line pattern", + "doesNotExistPattern", + }, + }, + { + name: "invalid attributes", + doc: "invalid-attributes.md", + expected: []string{ + "must NOT specify both a fragment name and start/end/line patterns", + }, + }, + { + name: "missing code fence", + doc: "missing-code-fence.md", + expected: []string{ + "expected a markdown code fence after the embedding instruction", + }, + }, + { + name: "unclosed code fence", + doc: "unclosed-code-fence.md", + expected: []string{ + "the markdown code fence after the embedding instruction is not closed", + }, + }, + { + name: "stale snippet", + doc: "stale-snippet.md", + expected: []string{ + "File to update:", + "stale-snippet.md", + "the documentation files are not up-to-date with code files", + }, + }, + } +} + +// javaSource returns the Java showcase source root. +func javaSource(repoRoot string) namedSource { + return namedSource{ + name: "java", + path: filepath.Join(repoRoot, "examples", "showcase", "code", "java"), + } +} + +// findRepoRoot returns the repository root by walking up from this test file. +func findRepoRoot() string { + GinkgoHelper() + + _, filePath, _, ok := runtime.Caller(0) + Expect(ok).Should(BeTrue(), "could not locate showcase test file") + + return filepath.Clean(filepath.Join(filepath.Dir(filePath), "..", "..")) +} + +// copyShowcaseDocs copies one showcase documentation folder to a temporary test directory. +func copyShowcaseDocs(repoRoot string, relativeSource string) string { + GinkgoHelper() + + sourceRoot := filepath.Join(repoRoot, "examples", "showcase", relativeSource) + tempRoot, err := os.MkdirTemp("", "embed-code-showcase-docs-*") + Expect(err).ShouldNot(HaveOccurred()) + DeferCleanup(os.RemoveAll, tempRoot) + + targetRoot := filepath.Join(tempRoot, "docs") + copyDir(sourceRoot, targetRoot) + + return targetRoot +} + +// copyDir recursively copies a directory tree while preserving regular file permissions. +func copyDir(sourceRoot string, targetRoot string) { + GinkgoHelper() + + err := filepath.WalkDir(sourceRoot, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + relativePath, err := filepath.Rel(sourceRoot, path) + if err != nil { + return err + } + targetPath := filepath.Join(targetRoot, relativePath) + if entry.IsDir() { + return os.MkdirAll(targetPath, 0o755) + } + if !entry.Type().IsRegular() { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + + return os.WriteFile(targetPath, data, info.Mode()) + }) + Expect(err).ShouldNot(HaveOccurred(), "failed to copy showcase docs") +} + +// writeShowcaseConfig creates a temp config that points at copied positive docs. +func writeShowcaseConfig(repoRoot string, docsRoot string) string { + GinkgoHelper() + + return writeConfig(docsRoot, []namedSource{ + javaSource(repoRoot), + {name: "kotlin", path: filepath.Join(repoRoot, "examples", "showcase", "code", "kotlin")}, + {name: "text", path: filepath.Join(repoRoot, "examples", "showcase", "code", "text")}, + }, []string{"**/*.md", "**/*.html"}) +} + +// writeSingleDocConfig creates a temp config for one negative showcase document. +func writeSingleDocConfig( + docsRoot string, + sources []namedSource, + docInclude string, +) string { + GinkgoHelper() + + return writeConfig(docsRoot, sources, []string{docInclude}) +} + +// writeConfig writes a YAML config with absolute source and documentation paths. +func writeConfig( + docsRoot string, + sources []namedSource, + includes []string, +) string { + GinkgoHelper() + + var builder strings.Builder + builder.WriteString("code-path:\n") + for _, source := range sources { + builder.WriteString(fmt.Sprintf(" - name: %s\n", source.name)) + builder.WriteString(fmt.Sprintf(" path: %s\n", filepath.ToSlash(source.path))) + } + builder.WriteString(fmt.Sprintf("docs-path: %s\n", filepath.ToSlash(docsRoot))) + builder.WriteString("doc-includes:\n") + for _, include := range includes { + builder.WriteString(fmt.Sprintf(" - %q\n", include)) + } + builder.WriteString("separator: \"// ...\"\n") + + tempRoot, err := os.MkdirTemp("", "embed-code-showcase-config-*") + Expect(err).ShouldNot(HaveOccurred()) + DeferCleanup(os.RemoveAll, tempRoot) + + configPath := filepath.Join(tempRoot, "embed-code.yml") + Expect(os.WriteFile(configPath, []byte(builder.String()), 0o644)). + Should(Succeed(), "failed to write temp config") + + return configPath +} + +// runEmbedCode executes the CLI through `go run` and returns combined output. +func runEmbedCode(repoRoot string, mode string, configPath string) (string, error) { + GinkgoHelper() + + cmd := exec.Command("go", "run", "./main.go", "-mode="+mode, "-config-path="+configPath) + cmd.Dir = repoRoot + output, err := cmd.CombinedOutput() + + return string(output), err +} + +// replaceInFile replaces one expected substring in a copied documentation file. +func replaceInFile(path string, oldText string, newText string) { + GinkgoHelper() + + data, err := os.ReadFile(path) + Expect(err).ShouldNot(HaveOccurred(), "failed to read %s", path) + content := string(data) + Expect(content).Should(ContainSubstring(oldText), "expected %s to contain %q", path, oldText) + content = strings.Replace(content, oldText, newText, 1) + Expect(os.WriteFile(path, []byte(content), 0o644)). + Should(Succeed(), "failed to write %s", path) +} diff --git a/go.mod b/go.mod index 637cf52..0f202b0 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -// Copyright 2024, TeamDev. All rights reserved. +// Copyright 2026, TeamDev. All rights reserved. // // Redistribution and use in source and/or binary forms, with or without // modification, must retain the above copyright notice and the following