diff --git a/EMBEDDING.md b/EMBEDDING.md
index 458528a..a83082b 100644
--- a/EMBEDDING.md
+++ b/EMBEDDING.md
@@ -70,6 +70,7 @@ To embed a named fragment, add the following to your Markdown file:
- **`file`**: The path to the source file relative to the `code-path` defined in your configuration.
- **`fragment`**: The name of the fragment to embed. If omitted, the entire file will be embedded.
+- **`comments`**: Optional comment filtering mode. If omitted, all comments are retained.
Fragment names can be any string, but avoid using double quotes (`"`) or characters reserved by XML.
@@ -103,6 +104,48 @@ Use `^` and `$` to disable this behavior and match the exact line start or end.
If you need to match a literal `^` at the start of a line, use `^^`.
Similarly, use `$$` to match a literal `$` at the end of a line.
+## Comment filtering
+
+Use the optional `comments` attribute to reduce comment noise in the embedded snippet:
+
+````markdown
+
+```java
+```
+````
+
+Supported values:
+
+- `all` — retain all comments. This is the default.
+- `none` — strip all recognized comments.
+- `documentation` — retain documentation comments such as Javadoc.
+- `regular` — retain non-documentation line and block comments.
+- `inline` — retain non-documentation line comments such as `//`.
+- `block` — retain non-documentation block comments such as `/* */`.
+
+Unknown extensions are embedded unchanged.
+
+Not all languages has difference between documentation/regular or inline/block comments.
+
+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` |
+| 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` |
+
## Advanced use cases
### Joining several parts of code into one fragment
diff --git a/embedding/commentfilter/config.go b/embedding/commentfilter/config.go
new file mode 100644
index 0000000..913a63d
--- /dev/null
+++ b/embedding/commentfilter/config.go
@@ -0,0 +1,182 @@
+// 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 commentfilter
+
+// filtersByExtension is a mapping of the file extension to its comment filter.
+var filtersByExtension = map[string]filterEntry{
+ // Java/Kotlin
+ ".java": filterConfig(MarkerCommentFilter{Syntax: javaSyntax}, allModes),
+ ".kt": filterConfig(MarkerCommentFilter{Syntax: javaSyntax}, allModes),
+ ".kts": filterConfig(MarkerCommentFilter{Syntax: javaSyntax}, allModes),
+ ".groovy": filterConfig(MarkerCommentFilter{Syntax: javaSyntax}, allModes),
+
+ // C#
+ ".cs": filterConfig(MarkerCommentFilter{Syntax: csharpSyntax}, allModes),
+
+ // C/C++
+ ".c": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".h": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".cc": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".cpp": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".cxx": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".hh": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".hpp": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+ ".hxx": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+
+ // JavaScript
+ ".js": filterConfig(MarkerCommentFilter{Syntax: jsSyntax}, allModes),
+ ".jsx": filterConfig(MarkerCommentFilter{Syntax: jsSyntax}, allModes),
+ ".ts": filterConfig(MarkerCommentFilter{Syntax: jsSyntax}, allModes),
+ ".tsx": filterConfig(MarkerCommentFilter{Syntax: jsSyntax}, allModes),
+
+ // Go
+ ".go": filterConfig(MarkerCommentFilter{Syntax: goSyntax}, regularModes),
+
+ // Protobuf
+ ".proto": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes),
+
+ // Python
+ ".py": filterConfig(MarkerCommentFilter{Syntax: hashLineSyntax}, noneMode),
+ ".pyi": filterConfig(MarkerCommentFilter{Syntax: hashLineSyntax}, noneMode),
+ ".pyw": filterConfig(MarkerCommentFilter{Syntax: hashLineSyntax}, noneMode),
+
+ // YAML
+ ".yml": filterConfig(MarkerCommentFilter{Syntax: hashLineSyntax}, noneMode),
+ ".yaml": filterConfig(MarkerCommentFilter{Syntax: hashLineSyntax}, noneMode),
+
+ // XML
+ ".xml": filterConfig(MarkerCommentFilter{Syntax: xmlSyntax}, noneMode),
+
+ // HTML
+ ".html": filterConfig(MarkerCommentFilter{Syntax: xmlSyntax}, noneMode),
+ ".htm": filterConfig(MarkerCommentFilter{Syntax: xmlSyntax}, noneMode),
+
+ // Visual Basic
+ ".vb": filterConfig(VisualBasicCommentFilter{}, documentationModes),
+ ".bas": filterConfig(VisualBasicCommentFilter{}, documentationModes),
+ ".vbs": filterConfig(VisualBasicCommentFilter{}, documentationModes),
+ ".vbscript": filterConfig(VisualBasicCommentFilter{}, documentationModes),
+}
+
+// filterEntry stores a comment filter and supported modes for its language.
+type filterEntry struct {
+ filter CommentFilter
+ supportedModes []Mode
+}
+
+var javaSyntax = CommentMarker{
+ Inline: []string{"//"},
+ Block: []BlockMarker{
+ {Start: "/*", End: "*/"},
+ },
+ Documentation: DocumentationMarker{
+ Block: []BlockMarker{{Start: "/**", End: "*/"}},
+ },
+ QuoteChars: "\"'",
+}
+
+var jsSyntax = CommentMarker{
+ Inline: []string{"//"},
+ Block: []BlockMarker{
+ {Start: "/*", End: "*/"},
+ },
+ Documentation: DocumentationMarker{
+ Block: []BlockMarker{{Start: "/**", End: "*/"}},
+ },
+ QuoteChars: "\"'`",
+}
+
+var csharpSyntax = CommentMarker{
+ Inline: []string{"//"},
+ Block: []BlockMarker{
+ {Start: "/*", End: "*/"},
+ },
+ Documentation: DocumentationMarker{
+ Inline: []string{"///"},
+ Block: []BlockMarker{{Start: "/**", End: "*/"}},
+ },
+ QuoteChars: "\"'`",
+}
+
+var cStyleSyntax = CommentMarker{
+ Inline: []string{"//"},
+ Block: []BlockMarker{
+ {Start: "/*", End: "*/"},
+ },
+ QuoteChars: "\"'",
+}
+
+var goSyntax = CommentMarker{
+ Inline: []string{"//"},
+ Block: []BlockMarker{
+ {Start: "/*", End: "*/"},
+ },
+ QuoteChars: "\"'`",
+}
+
+var hashLineSyntax = CommentMarker{
+ Inline: []string{"#"},
+ QuoteChars: "\"'",
+}
+
+var xmlSyntax = CommentMarker{
+ Block: []BlockMarker{
+ {Start: ""},
+ },
+ QuoteChars: "\"'",
+}
+
+// allModes lists all comment filtering modes.
+var allModes = []Mode{
+ RetainAll,
+ RetainNone,
+ RetainDocumentation,
+ RetainRegular,
+ RetainInline,
+ RetainBlock,
+}
+
+// noneMode lists modes for languages whose comments are not separated into supported subtypes.
+var noneMode = []Mode{RetainAll, RetainNone}
+
+// regularModes lists modes for languages that distinguish inline and block comments,
+// but do not expose documentation comments as a separate supported type.
+var regularModes = []Mode{
+ RetainAll,
+ RetainNone,
+ RetainInline,
+ RetainBlock,
+}
+
+// documentationModes lists modes for languages that distinguish documentation and regular comments,
+// but do not expose inline and block comments as separate supported types.
+var documentationModes = []Mode{
+ RetainAll,
+ RetainNone,
+ RetainDocumentation,
+ RetainRegular,
+}
+
+// filterConfig creates a filter registry entry.
+func filterConfig(filter CommentFilter, supportedModes []Mode) filterEntry {
+ return filterEntry{
+ filter: filter,
+ supportedModes: supportedModes,
+ }
+}
diff --git a/embedding/commentfilter/filter.go b/embedding/commentfilter/filter.go
new file mode 100644
index 0000000..39544db
--- /dev/null
+++ b/embedding/commentfilter/filter.go
@@ -0,0 +1,171 @@
+// 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 commentfilter
+
+import (
+ "fmt"
+ "log/slog"
+ "path/filepath"
+ "strings"
+)
+
+// EmbeddingCommentFilter filters comments for one embed-code instruction.
+type EmbeddingCommentFilter struct {
+ filePath string
+ embeddingDocPath string
+ embeddingLine int
+}
+
+// CommentFilter strips source comments according to the requested mode.
+type CommentFilter interface {
+ Filter(lines []string, mode Mode) []string
+}
+
+// Filter returns source lines with comments stripped according to the requested mode.
+func Filter(
+ lines []string,
+ filePath string,
+ mode Mode,
+ embeddingDocPath string,
+ embeddingLine int,
+) []string {
+ filter := EmbeddingCommentFilter{
+ filePath: filePath,
+ embeddingDocPath: embeddingDocPath,
+ embeddingLine: embeddingLine,
+ }
+
+ return filter.Filter(lines, mode)
+}
+
+// Filter strips comments using the filter registered in the filtersByExtension.
+func (f EmbeddingCommentFilter) Filter(lines []string, mode Mode) []string {
+ if mode == RetainAll {
+ return lines
+ }
+ filter, found := filterFor(f.filePath, mode, f.embeddingDocPath, f.embeddingLine)
+ if !found {
+ return lines
+ }
+
+ return filter.Filter(lines, mode)
+}
+
+// filterFor returns the comment filter registered for the given file path and warns on odd modes.
+func filterFor(
+ filePath string,
+ mode Mode,
+ embeddingDocPath string,
+ embeddingLine int,
+) (CommentFilter, bool) {
+ extension := normalizeExtension(filepath.Ext(filePath))
+ entry, found := filtersByExtension[extension]
+ if !found {
+ warnUnsupportedFileType(filePath, mode, embeddingDocPath, embeddingLine)
+ return nil, false
+ }
+ warnUnsupportedCommentsMode(filePath, mode, embeddingDocPath, embeddingLine, entry.supportedModes)
+
+ return entry.filter, true
+}
+
+// normalizeExtension returns a lowercase file extension with a leading dot.
+func normalizeExtension(extension string) string {
+ normalized := strings.ToLower(extension)
+ if normalized == "" || strings.HasPrefix(normalized, ".") {
+ return normalized
+ }
+
+ return "." + normalized
+}
+
+// warnUnsupportedFileType logs when comments filtering is requested for an unsupported file.
+func warnUnsupportedFileType(
+ filePath string,
+ mode Mode,
+ embeddingDocPath string,
+ embeddingLine int,
+) {
+ if mode == RetainAll {
+ return
+ }
+ slog.Warn(
+ fmt.Sprintf(
+ "`comments=\"%s\"` was requested in `%s` for `%s`, "+
+ "but comment filtering is not supported for this file extension.",
+ mode,
+ fileURL(embeddingDocPath, embeddingLine),
+ filePath,
+ ),
+ )
+}
+
+// warnUnsupportedCommentsMode logs when the selected mode is not supported for a file.
+func warnUnsupportedCommentsMode(
+ filePath string,
+ mode Mode,
+ embeddingDocPath string,
+ embeddingLine int,
+ supportedModes []Mode,
+) {
+ if containsMode(supportedModes, mode) {
+ return
+ }
+ var wrappedModes []string
+ for _, mode := range supportedModes {
+ wrappedModes = append(wrappedModes, fmt.Sprintf("`%s`", mode))
+ }
+
+ slog.Warn(
+ fmt.Sprintf(
+ "`comments=\"%s\"` was requested in `%s` for `%s`, but this mode does not have "+
+ "a distinct meaning for this file type. Supported modes are: %s.",
+ mode,
+ fileURL(embeddingDocPath, embeddingLine),
+ filePath,
+ strings.Join(wrappedModes, ", "),
+ ),
+ )
+}
+
+// fileURL returns an absolute file URL for a local path and line.
+func fileURL(path string, line int) string {
+ absolutePath, err := filepath.Abs(path)
+ if err != nil {
+ return "file://" + path
+ }
+
+ url := "file://" + absolutePath
+ if line > 0 {
+ url = fmt.Sprintf("%s:%d", url, line)
+ }
+
+ return url
+}
+
+// containsMode reports whether the list includes the given mode.
+func containsMode(modes []Mode, mode Mode) bool {
+ for _, supportedMode := range modes {
+ if supportedMode == mode {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/embedding/commentfilter/filter_test.go b/embedding/commentfilter/filter_test.go
new file mode 100644
index 0000000..b2c0638
--- /dev/null
+++ b/embedding/commentfilter/filter_test.go
@@ -0,0 +1,481 @@
+// 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 commentfilter
+
+import (
+ "bytes"
+ "log/slog"
+ "testing"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+// TestCommentFilter runs the comment filter test suite.
+func TestCommentFilter(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Comment Filter Suite")
+}
+
+var _ = Describe("Comment filter", func() {
+ Describe("YAML", func() {
+ It("should strip all comments", func() {
+ lines := []string{
+ "name: test # inline",
+ "# standalone",
+ "value: \"# literal\"",
+ }
+
+ expected := []string{
+ "name: test ",
+ "value: \"# literal\"",
+ }
+
+ assertFiltered("config.yml", RetainNone, lines, expected)
+ })
+ })
+
+ Describe("XML", func() {
+ It("should strip all comments", func() {
+ lines := []string{
+ "",
+ " ",
+ " - \"/>",
+ "
",
+ }
+
+ expected := []string{
+ "",
+ " - \"/>",
+ "
",
+ }
+
+ assertFiltered("layout.xml", RetainNone, lines, expected)
+ })
+ })
+
+ Describe("Java-style languages", func() {
+ It("should keep documentation comments", func() {
+ lines := []string{
+ "/** API docs. */",
+ "// implementation note",
+ "fun call() = \"// literal\"",
+ }
+
+ expected := []string{
+ "/** API docs. */",
+ "fun call() = \"// literal\"",
+ }
+
+ assertFiltered("api.kt", RetainDocumentation, lines, expected)
+ })
+
+ It("should keep block comments", func() {
+ lines := []string{
+ "/** API docs. */",
+ "/* implementation note */",
+ "String create();",
+ }
+
+ expected := []string{
+ "/* implementation note */",
+ "String create();",
+ }
+
+ assertFiltered("Api.java", RetainBlock, lines, expected)
+ })
+
+ It("should keep regular comments", func() {
+ lines := []string{
+ "/** API docs. */",
+ "/* implementation note */",
+ "String create(); // inline note",
+ }
+
+ expected := []string{
+ "/* implementation note */",
+ "String create(); // inline note",
+ }
+
+ assertFiltered("Api.java", RetainRegular, lines, expected)
+ })
+ })
+
+ Describe("JavaScript and TypeScript", func() {
+ It("should strip comments without treating template literals as comments", func() {
+ lines := []string{
+ "// module comment",
+ "const url = `http://example.org/*not-comment*/`;",
+ "const value = 42; // inline comment",
+ }
+
+ expected := []string{
+ "const url = `http://example.org/*not-comment*/`;",
+ "const value = 42; ",
+ }
+
+ assertFiltered("sample.ts", RetainNone, lines, expected)
+ })
+ })
+
+ Describe("C#", func() {
+ It("should keep XML documentation comments", func() {
+ lines := []string{
+ "/// Creates a value.",
+ "// implementation note",
+ "public string Create() => \"// literal\";",
+ }
+
+ expected := []string{
+ "/// Creates a value.",
+ "public string Create() => \"// literal\";",
+ }
+
+ assertFiltered("Api.cs", RetainDocumentation, lines, expected)
+ })
+
+ It("should keep inline comments", func() {
+ lines := []string{
+ "/// Creates a value.",
+ "// implementation note",
+ "public string Create() => \"// literal\";",
+ }
+
+ expected := []string{
+ "// implementation note",
+ "public string Create() => \"// literal\";",
+ }
+
+ assertFiltered("Api.cs", RetainInline, lines, expected)
+ })
+ })
+
+ Describe("C and C++", func() {
+ It("should strip all comments without treating literals as comments", func() {
+ lines := []string{
+ "// header comment",
+ "#include ",
+ "",
+ "/* block comment */",
+ "const char slash = '/';",
+ "const char* url = \"http://example.org\";",
+ "int create() { return 1; } // inline comment",
+ }
+
+ expected := []string{
+ "#include ",
+ "",
+ "const char slash = '/';",
+ "const char* url = \"http://example.org\";",
+ "int create() { return 1; } ",
+ }
+
+ assertFiltered("sample.cpp", RetainNone, lines, expected)
+ })
+
+ It("should keep inline comments", func() {
+ lines := []string{
+ "// header comment",
+ "int create();",
+ "/* block comment */",
+ "int count(); // inline comment",
+ }
+
+ expected := []string{
+ "// header comment",
+ "int create();",
+ "int count(); // inline comment",
+ }
+
+ assertFiltered("sample.cpp", RetainInline, lines, expected)
+ })
+
+ It("should keep block comments", func() {
+ lines := []string{
+ "// header comment",
+ "int create();",
+ "/* block comment */",
+ "int count(); // inline comment",
+ }
+
+ expected := []string{
+ "int create();",
+ "/* block comment */",
+ "int count(); ",
+ }
+
+ assertFiltered("sample.hpp", RetainBlock, lines, expected)
+ })
+ })
+
+ Describe("Go", func() {
+ It("should strip all comments without treating literals as comments", func() {
+ lines := []string{
+ "// package comment",
+ "package sample",
+ "",
+ "/* block comment */",
+ "const slash = '/'",
+ "const url = \"http://example.org\"",
+ "const raw = `/* not a comment */`",
+ "func create() {} // inline comment",
+ }
+
+ expected := []string{
+ "package sample",
+ "",
+ "const slash = '/'",
+ "const url = \"http://example.org\"",
+ "const raw = `/* not a comment */`",
+ "func create() {} ",
+ }
+
+ assertFiltered("sample.go", RetainNone, lines, expected)
+ })
+
+ It("should keep inline comments", func() {
+ lines := []string{
+ "// package comment",
+ "package sample",
+ "/* block comment */",
+ "func create() {} // inline comment",
+ }
+
+ expected := []string{
+ "// package comment",
+ "package sample",
+ "func create() {} // inline comment",
+ }
+
+ assertFiltered("sample.go", RetainInline, lines, expected)
+ })
+
+ It("should keep block comments", func() {
+ lines := []string{
+ "// package comment",
+ "package sample",
+ "/* block comment */",
+ "func create() {} // inline comment",
+ }
+
+ expected := []string{
+ "package sample",
+ "/* block comment */",
+ "func create() {} ",
+ }
+
+ assertFiltered("sample.go", RetainBlock, lines, expected)
+ })
+ })
+
+ Describe("Protobuf", func() {
+ It("should strip all comments without treating literals as comments", func() {
+ lines := []string{
+ "// file comment",
+ "syntax = \"proto3\";",
+ "",
+ "/* message comment */",
+ "message Sample {",
+ " string url = 1 [default = 'http://example.org'];",
+ " int32 count = 2; // inline comment",
+ "}",
+ }
+
+ expected := []string{
+ "syntax = \"proto3\";",
+ "",
+ "message Sample {",
+ " string url = 1 [default = 'http://example.org'];",
+ " int32 count = 2; ",
+ "}",
+ }
+
+ assertFiltered("sample.proto", RetainNone, lines, expected)
+ })
+
+ It("should keep inline comments", func() {
+ lines := []string{
+ "// file comment",
+ "syntax = \"proto3\";",
+ "/* message comment */",
+ "message Sample {} // inline comment",
+ }
+
+ expected := []string{
+ "// file comment",
+ "syntax = \"proto3\";",
+ "message Sample {} // inline comment",
+ }
+
+ assertFiltered("sample.proto", RetainInline, lines, expected)
+ })
+
+ It("should keep block comments", func() {
+ lines := []string{
+ "// file comment",
+ "syntax = \"proto3\";",
+ "/* message comment */",
+ "message Sample {} // inline comment",
+ }
+
+ expected := []string{
+ "syntax = \"proto3\";",
+ "/* message comment */",
+ "message Sample {} ",
+ }
+
+ assertFiltered("sample.proto", RetainBlock, lines, expected)
+ })
+ })
+
+ Describe("Python", func() {
+ It("should strip all comments", func() {
+ lines := []string{
+ "# module comment",
+ "name = 'hash # literal'",
+ "value = 1 # inline comment",
+ }
+
+ expected := []string{
+ "name = 'hash # literal'",
+ "value = 1 ",
+ }
+
+ assertFiltered("module.py", RetainNone, lines, expected)
+ })
+ })
+
+ Describe("Visual Basic", func() {
+ It("should strip all comments", func() {
+ lines := []string{
+ "' file comment",
+ "REM module comment",
+ "Dim text = \"REM not a comment\"",
+ "Dim value = 1 ' inline",
+ "Dim ready = True : Rem after statement separator",
+ "Dim reminder = 1",
+ }
+
+ expected := []string{
+ "Dim text = \"REM not a comment\"",
+ "Dim value = 1 ",
+ "Dim ready = True : ",
+ "Dim reminder = 1",
+ }
+
+ assertFiltered("Module.vb", RetainNone, lines, expected)
+ })
+
+ It("should keep regular comments", func() {
+ lines := []string{
+ "''' Creates a value.",
+ "' file comment",
+ "REM module comment",
+ "Dim value = 1 ' inline",
+ }
+
+ expected := []string{
+ "' file comment",
+ "REM module comment",
+ "Dim value = 1 ' inline",
+ }
+
+ assertFiltered("Module.vb", RetainRegular, lines, expected)
+ })
+
+ It("should keep documentation comments", func() {
+ lines := []string{
+ "''' Creates a value.",
+ "' implementation note",
+ "REM module comment",
+ "Public Function Create() As String",
+ }
+
+ expected := []string{
+ "''' Creates a value.",
+ "Public Function Create() As String",
+ }
+
+ assertFiltered("Module.vb", RetainDocumentation, lines, expected)
+ })
+ })
+
+ Describe("unsupported extensions", func() {
+ It("should return unsupported files unchanged", func() {
+ lines := []string{
+ "# docs",
+ "sub call { } # inline",
+ }
+
+ assertFiltered("service.pl", RetainAll, lines, lines)
+ })
+
+ It("should warn about unsupported comment modes", func() {
+ output := captureWarnings(func() {
+ Filter([]string{"# comment"}, "service.pl", RetainNone, "docs/guide.md", 12)
+ })
+
+ Expect(output).Should(ContainSubstring(
+ "comment filtering is not supported for this file extension",
+ ))
+ Expect(output).Should(ContainSubstring("file://"))
+ Expect(output).Should(ContainSubstring("guide.md:12"))
+ })
+ })
+
+ Describe("warnings", func() {
+ It("should warn about modes without language-specific meaning", func() {
+ output := captureWarnings(func() {
+ Filter([]string{""}, "layout.xml", RetainDocumentation, "docs/guide.md", 12)
+ })
+
+ Expect(output).Should(ContainSubstring("documentation"))
+ Expect(output).Should(ContainSubstring("layout.xml"))
+ Expect(output).Should(ContainSubstring("file://"))
+ Expect(output).Should(ContainSubstring("guide.md:12"))
+ Expect(output).Should(ContainSubstring("does not have a distinct meaning"))
+ })
+ })
+})
+
+// assertFiltered verifies filtering output for one file path and mode.
+func assertFiltered(
+ filePath string,
+ mode Mode,
+ lines []string,
+ expected []string,
+) {
+ got := Filter(lines, filePath, mode, "docs/guide.md", 12)
+
+ Expect(got).Should(Equal(expected))
+}
+
+// captureWarnings runs action and returns slog warning output.
+func captureWarnings(action func()) string {
+ var output bytes.Buffer
+ previous := slog.Default()
+ slog.SetDefault(slog.New(slog.NewTextHandler(&output, &slog.HandlerOptions{
+ Level: slog.LevelWarn,
+ })))
+ defer slog.SetDefault(previous)
+
+ action()
+
+ return output.String()
+}
diff --git a/embedding/commentfilter/marker_comment_filter.go b/embedding/commentfilter/marker_comment_filter.go
new file mode 100644
index 0000000..75a4b3c
--- /dev/null
+++ b/embedding/commentfilter/marker_comment_filter.go
@@ -0,0 +1,238 @@
+// 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 commentfilter
+
+import "strings"
+
+// BlockMarker describes a block comment marker pair.
+type BlockMarker struct {
+ Start string
+ End string
+}
+
+// DocumentationMarker describes API documentation comment markers.
+type DocumentationMarker struct {
+ Inline []string
+ Block []BlockMarker
+}
+
+// CommentMarker describes lexical comment markers and string delimiters for a language family.
+type CommentMarker struct {
+ Inline []string
+ Block []BlockMarker
+ Documentation DocumentationMarker
+ QuoteChars string
+}
+
+// MarkerCommentFilter removes comments using lexical markers declared in CommentMarker.
+type MarkerCommentFilter struct {
+ Syntax CommentMarker
+}
+
+type blockState struct {
+ active bool
+ block BlockMarker
+ keep bool
+}
+
+type markerLineFilter struct {
+ filter MarkerCommentFilter
+ line string
+ mode Mode
+ state *blockState
+ result strings.Builder
+ position int
+ hadComment bool
+}
+
+// Filter removes or preserves recognized comments across all lines.
+func (f MarkerCommentFilter) Filter(lines []string, mode Mode) []string {
+ var filtered []string
+ state := blockState{}
+ for _, line := range lines {
+ filteredLine, hadComment := f.filterLine(line, mode, &state)
+ if hadComment && strings.TrimSpace(filteredLine) == "" {
+ continue
+ }
+ filtered = append(filtered, filteredLine)
+ }
+
+ return filtered
+}
+
+// filterLine removes or preserves recognized comments from a single source line.
+func (f MarkerCommentFilter) filterLine(
+ line string,
+ mode Mode,
+ state *blockState,
+) (string, bool) {
+ filter := markerLineFilter{
+ filter: f,
+ line: line,
+ mode: mode,
+ state: state,
+ }
+
+ return filter.filterLine()
+}
+
+// filterLine walks the current line until it reaches its end or a line comment.
+func (f *markerLineFilter) filterLine() (string, bool) {
+ for f.position < len(f.line) {
+ if f.consumeActiveBlock() {
+ continue
+ }
+ if f.consumeQuotedSegment() {
+ continue
+ }
+ if consumed, stop := f.consumeComment(); consumed {
+ if stop {
+ break
+ }
+ continue
+ }
+ f.consumeCodeByte()
+ }
+
+ return f.result.String(), f.hadComment
+}
+
+// consumeActiveBlock consumes text while the scanner is inside a block comment.
+func (f *markerLineFilter) consumeActiveBlock() bool {
+ if !f.state.active {
+ return false
+ }
+ f.hadComment = true
+ end := strings.Index(f.line[f.position:], f.state.block.End)
+ if end < 0 {
+ if f.state.keep {
+ f.result.WriteString(f.line[f.position:])
+ }
+ f.position = len(f.line)
+ return true
+ }
+ endPosition := f.position + end + len(f.state.block.End)
+ if f.state.keep {
+ f.result.WriteString(f.line[f.position:endPosition])
+ }
+ f.position = endPosition
+ f.state.active = false
+
+ return true
+}
+
+// consumeQuotedSegment copies a quoted segment without scanning comment markers inside it.
+func (f *markerLineFilter) consumeQuotedSegment() bool {
+ quoteEnd := quotedSegmentEnd(f.line, f.position, f.filter.Syntax.QuoteChars)
+ if quoteEnd <= f.position {
+ return false
+ }
+ f.result.WriteString(f.line[f.position:quoteEnd])
+ f.position = quoteEnd
+
+ return true
+}
+
+// quotedSegmentEnd returns the end offset of a quoted string starting at position.
+func quotedSegmentEnd(line string, position int, quoteChars string) int {
+ if position >= len(line) || !strings.ContainsRune(quoteChars, rune(line[position])) {
+ return position
+ }
+ quote := line[position]
+ cursor := position + 1
+ for cursor < len(line) {
+ if line[cursor] == '\\' {
+ cursor += 2
+ continue
+ }
+ if line[cursor] == quote {
+ return cursor + 1
+ }
+ cursor++
+ }
+
+ return len(line)
+}
+
+// consumeComment consumes a comment and reports whether it consumed input and ended the line.
+func (f *markerLineFilter) consumeComment() (bool, bool) {
+ if _, found := prefixAt(f.line, f.position, f.filter.Syntax.Documentation.Inline); found {
+ f.consumeInlineComment(f.mode == RetainDocumentation)
+ return true, true
+ }
+ if block, found := blockAt(f.line, f.position, f.filter.Syntax.Documentation.Block); found {
+ f.startBlockComment(block, f.mode == RetainDocumentation)
+ return true, false
+ }
+ if _, found := prefixAt(f.line, f.position, f.filter.Syntax.Inline); found {
+ f.consumeInlineComment(f.mode == RetainInline || f.mode == RetainRegular)
+ return true, true
+ }
+ if block, found := blockAt(f.line, f.position, f.filter.Syntax.Block); found {
+ f.startBlockComment(block, f.mode == RetainBlock || f.mode == RetainRegular)
+ return true, false
+ }
+
+ return false, false
+}
+
+// consumeInlineComment consumes the rest of the line as a line comment.
+func (f *markerLineFilter) consumeInlineComment(keep bool) {
+ f.hadComment = true
+ if keep {
+ f.result.WriteString(f.line[f.position:])
+ }
+ f.position = len(f.line)
+}
+
+// startBlockComment records the active block comment markers and whether to keep them.
+func (f *markerLineFilter) startBlockComment(block BlockMarker, keep bool) {
+ f.hadComment = true
+ f.state.active = true
+ f.state.block = block
+ f.state.keep = keep
+}
+
+// consumeCodeByte copies one source byte that does not belong to a recognized comment.
+func (f *markerLineFilter) consumeCodeByte() {
+ f.result.WriteByte(f.line[f.position])
+ f.position++
+}
+
+// prefixAt reports whether one of the given prefixes starts at the position.
+func prefixAt(line string, position int, prefixes []string) (string, bool) {
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(line[position:], prefix) {
+ return prefix, true
+ }
+ }
+
+ return "", false
+}
+
+// blockAt reports whether one of the given block markers starts at the position.
+func blockAt(line string, position int, blocks []BlockMarker) (BlockMarker, bool) {
+ for _, block := range blocks {
+ if strings.HasPrefix(line[position:], block.Start) {
+ return block, true
+ }
+ }
+
+ return BlockMarker{}, false
+}
diff --git a/embedding/commentfilter/mode.go b/embedding/commentfilter/mode.go
new file mode 100644
index 0000000..9986471
--- /dev/null
+++ b/embedding/commentfilter/mode.go
@@ -0,0 +1,52 @@
+// 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 commentfilter
+
+import "fmt"
+
+// Mode controls which source comments are affected by the comment filter.
+type Mode string
+
+const (
+ // RetainAll keeps all comments in the embedded source.
+ RetainAll Mode = "all"
+ // RetainNone removes all comments recognized for the source language.
+ RetainNone Mode = "none"
+ // RetainDocumentation keeps only API documentation comments.
+ RetainDocumentation Mode = "documentation"
+ // RetainRegular keeps inline and block comments that are not documentation comments.
+ RetainRegular Mode = "regular"
+ // RetainInline keeps only inline comments such as `//` and `#`.
+ RetainInline Mode = "inline"
+ // RetainBlock keeps only block comments such as `/* */`.
+ RetainBlock Mode = "block"
+)
+
+// ParseMode converts an embed-code `comments` attribute value into a comment filter Mode.
+func ParseMode(value string) (Mode, error) {
+ switch Mode(value) {
+ case "":
+ return RetainAll, nil
+ case RetainAll, RetainNone, RetainDocumentation, RetainRegular, RetainInline, RetainBlock:
+ return Mode(value), nil
+ default:
+ return "", fmt.Errorf("unsupported comments value `%s`; expected one of "+
+ "`all`, `none`, `documentation`, `regular`, `inline`, or `block`", value)
+ }
+}
diff --git a/embedding/commentfilter/visual_basic.go b/embedding/commentfilter/visual_basic.go
new file mode 100644
index 0000000..645d61c
--- /dev/null
+++ b/embedding/commentfilter/visual_basic.go
@@ -0,0 +1,109 @@
+// 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 commentfilter
+
+import (
+ "strings"
+ "unicode"
+)
+
+const (
+ commentPrefix = '\''
+ docPrefix = "'''"
+ rem = "rem"
+)
+
+// VisualBasicCommentFilter filters the Visual Basic comment forms:
+// - documentation comments starting with `”'`;
+// - apostrophe comments starting with `'`;
+// - REM comments starting with `REM`.
+type VisualBasicCommentFilter struct{}
+
+// Filter removes or preserves Visual Basic comments according to mode.
+func (VisualBasicCommentFilter) Filter(lines []string, mode Mode) []string {
+ var filtered []string
+ for _, line := range lines {
+ filteredLine, hadComment := filterVisualBasicLine(line, mode)
+ if hadComment && strings.TrimSpace(filteredLine) == "" {
+ continue
+ }
+ filtered = append(filtered, filteredLine)
+ }
+
+ return filtered
+}
+
+// filterVisualBasicLine removes or preserves one Visual Basic comment.
+func filterVisualBasicLine(line string, mode Mode) (string, bool) {
+ var result strings.Builder
+ position := 0
+ for position < len(line) {
+ if quoteEnd := quotedSegmentEnd(line, position, "\""); quoteEnd > position {
+ result.WriteString(line[position:quoteEnd])
+ position = quoteEnd
+ continue
+ }
+ if strings.HasPrefix(line[position:], docPrefix) {
+ if mode == RetainDocumentation {
+ result.WriteString(line[position:])
+ }
+ return result.String(), true
+ }
+ if line[position] == commentPrefix || remCommentAt(line, position) {
+ if mode == RetainInline || mode == RetainRegular {
+ result.WriteString(line[position:])
+ }
+ return result.String(), true
+ }
+ result.WriteByte(line[position])
+ position++
+ }
+
+ return result.String(), false
+}
+
+// remCommentAt reports whether a Visual Basic REM comment starts at position.
+func remCommentAt(line string, position int) bool {
+ if len(line[position:]) < len(rem) ||
+ !strings.EqualFold(
+ line[position:position+len(rem)],
+ rem,
+ ) {
+ return false
+ }
+ return remPrefixBoundary(line, position) &&
+ remSuffixBoundary(line, position+len(rem))
+}
+
+// remPrefixBoundary reports whether REM appears where a statement can start.
+func remPrefixBoundary(line string, position int) bool {
+ for cursor := position - 1; cursor >= 0; cursor-- {
+ if unicode.IsSpace(rune(line[cursor])) {
+ continue
+ }
+ return line[cursor] == ':'
+ }
+
+ return true
+}
+
+// remSuffixBoundary reports whether REM is followed by whitespace or the end of line.
+func remSuffixBoundary(line string, position int) bool {
+ return position >= len(line) || unicode.IsSpace(rune(line[position]))
+}
diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go
index 76cc87f..13e6089 100644
--- a/embedding/parsing/instruction.go
+++ b/embedding/parsing/instruction.go
@@ -22,6 +22,7 @@ import (
"fmt"
"embed-code/embed-code-go/configuration"
+ "embed-code/embed-code-go/embedding/commentfilter"
"embed-code/embed-code-go/fragmentation"
"embed-code/embed-code-go/indent"
)
@@ -42,13 +43,22 @@ import (
// EndPattern — an optional glob-like pattern. If specified, lines after the matching one
// are excluded.
//
+// CommentMode — specifies which comments are retained in the embedded code.
+//
+// DocumentationFile — a documentation file containing the instruction.
+//
+// DocumentationLine — a line containing the start of the instruction.
+//
// Configuration — a Configuration with all embed-code settings.
type Instruction struct {
- CodeFile string
- Fragment string
- StartPattern *Pattern
- EndPattern *Pattern
- Configuration configuration.Configuration
+ CodeFile string
+ Fragment string
+ StartPattern *Pattern
+ EndPattern *Pattern
+ CommentMode commentfilter.Mode
+ DocumentationFile string
+ DocumentationLine int
+ Configuration configuration.Configuration
}
// NewInstruction creates an Instruction based on provided attributes and configuration.
@@ -60,6 +70,7 @@ type Instruction struct {
// - start — an optional glob-like pattern. If specified, lines before the matching one
// are excluded;
// - end — an optional glob-like pattern. If specified, lines after the matching one are excluded.
+// - comments — an optional comment filtering mode. If omitted, all comments are retained.
//
// config — a Configuration with all embed-code settings.
//
@@ -70,6 +81,10 @@ func NewInstruction(
fragment := attributes["fragment"]
startValue := attributes["start"]
endValue := attributes["end"]
+ commentMode, err := commentfilter.ParseMode(attributes["comments"])
+ if err != nil {
+ return Instruction{}, err
+ }
if fragment != "" && (startValue != "" || endValue != "") {
return Instruction{},
@@ -92,6 +107,7 @@ func NewInstruction(
Fragment: fragment,
StartPattern: start,
EndPattern: end,
+ CommentMode: commentMode,
Configuration: config,
}, nil
}
@@ -105,16 +121,24 @@ func (e Instruction) Content() ([]string, error) {
return nil, err
}
if e.StartPattern != nil || e.EndPattern != nil {
- return e.matchingLines(fileContent), nil
+ fileContent = e.matchingLines(fileContent)
}
- return fileContent, nil
+ return commentfilter.Filter(
+ fileContent,
+ e.CodeFile,
+ e.CommentMode,
+ e.DocumentationFile,
+ e.DocumentationLine,
+ ), nil
}
// Returns string representation of Instruction.
func (e Instruction) String() string {
- return fmt.Sprintf("EmbeddingInstruction[file=`%s`, fragment=`%s`, start=`%s`, end=`%s`]",
- e.CodeFile, e.Fragment, e.StartPattern, e.EndPattern)
+ return fmt.Sprintf(
+ "EmbeddingInstruction[file=`%s`, fragment=`%s`, start=`%s`, end=`%s`, comments=`%s`]",
+ e.CodeFile, e.Fragment, e.StartPattern, e.EndPattern, e.CommentMode,
+ )
}
// Filters and returns a subset of input lines based on start and end patterns.
diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go
index c4183f6..9676f44 100644
--- a/embedding/parsing/instruction_test.go
+++ b/embedding/parsing/instruction_test.go
@@ -36,6 +36,7 @@ type TestInstructionParams struct {
fragment string
startGlob string
endGlob string
+ comments string
closeTag bool
}
@@ -82,6 +83,15 @@ var _ = Describe("Instruction", func() {
Expect(parsing.FromXML(xmlString, config)).Error().ShouldNot(HaveOccurred())
})
+ It("should have an error for unsupported comments mode", func() {
+ instructionParams := TestInstructionParams{
+ comments: "summary",
+ }
+ xmlString := buildInstruction("org/example/Comments.java", instructionParams)
+
+ Expect(parsing.FromXML(xmlString, config)).Error().Should(HaveOccurred())
+ })
+
It("should successfully read source content", func() {
instructionParams := TestInstructionParams{
closeTag: true,
@@ -98,6 +108,82 @@ var _ = Describe("Instruction", func() {
Expect(actualLines[checkedLine]).Should(Equal(expectedLine))
})
+ It("should strip all recognized comments", func() {
+ instructionParams := TestInstructionParams{
+ comments: "none",
+ }
+
+ actualLines := getXMLExtractionContent(
+ "org/example/Comments.java", instructionParams, config)
+
+ Expect(actualLines).Should(Equal([]string{
+ "package org.example;",
+ "",
+ "public interface Comments {",
+ " String marker = \"http://example.org/*not-comment*/\";",
+ "",
+ " String create(String name); ",
+ "}",
+ }))
+ })
+
+ It("should keep documentation comments only", func() {
+ instructionParams := TestInstructionParams{
+ comments: "documentation",
+ }
+
+ actualLines := getXMLExtractionContent(
+ "org/example/Comments.java", instructionParams, config)
+
+ Expect(actualLines).Should(ContainElement("/**"))
+ Expect(actualLines).Should(ContainElement(" * Documents the public API."))
+ Expect(actualLines).ShouldNot(ContainElement(" * The block comment."))
+ Expect(actualLines).ShouldNot(ContainElement(" // Full-line inline comment."))
+ })
+
+ It("should keep inline comments only", func() {
+ instructionParams := TestInstructionParams{
+ comments: "inline",
+ }
+
+ actualLines := getXMLExtractionContent(
+ "org/example/Comments.java", instructionParams, config)
+
+ Expect(actualLines).Should(ContainElement(" // Full-line inline comment."))
+ Expect(actualLines).Should(ContainElement(" String create(String name); // end-of-line inline comment."))
+ Expect(actualLines).ShouldNot(ContainElement("/**"))
+ Expect(actualLines).ShouldNot(ContainElement(" * The block comment."))
+ })
+
+ It("should keep block comments only", func() {
+ instructionParams := TestInstructionParams{
+ comments: "block",
+ }
+
+ actualLines := getXMLExtractionContent(
+ "org/example/Comments.java", instructionParams, config)
+
+ Expect(actualLines).ShouldNot(ContainElement("/**"))
+ Expect(actualLines).ShouldNot(ContainElement(" * Documents the public API."))
+ Expect(actualLines).Should(ContainElement(" * The block comment."))
+ Expect(actualLines).ShouldNot(ContainElement(" // Full-line inline comment."))
+ })
+
+ It("should keep regular comments only", func() {
+ instructionParams := TestInstructionParams{
+ comments: "regular",
+ }
+
+ actualLines := getXMLExtractionContent(
+ "org/example/Comments.java", instructionParams, config)
+
+ Expect(actualLines).ShouldNot(ContainElement("/**"))
+ Expect(actualLines).ShouldNot(ContainElement(" * Documents the public API."))
+ Expect(actualLines).Should(ContainElement(" * The block comment."))
+ Expect(actualLines).Should(ContainElement(" // Full-line inline comment."))
+ Expect(actualLines).Should(ContainElement(" String create(String name); // end-of-line inline comment."))
+ })
+
It("should have an error when parsing fragment with start glob", func() {
instructionParams := TestInstructionParams{
fragment: "fragment",
@@ -318,6 +404,10 @@ func buildInstruction(fileName string, params TestInstructionParams) string {
endAttr := xmlAttribute("end", params.endGlob)
instructionLine += " " + endAttr
}
+ if len(params.comments) > 0 {
+ commentsAttr := xmlAttribute("comments", params.comments)
+ instructionLine += " " + commentsAttr
+ }
if params.closeTag {
instructionLine += ">"
} else {
diff --git a/embedding/parsing/instruction_token.go b/embedding/parsing/instruction_token.go
index b3234de..d98fa6d 100644
--- a/embedding/parsing/instruction_token.go
+++ b/embedding/parsing/instruction_token.go
@@ -78,6 +78,8 @@ func (e EmbedInstructionTokenState) Accept(context *Context,
instruction, err := FromXML(strings.Join(instructionBody, " "), config)
if err == nil {
+ instruction.DocumentationFile = context.MarkdownFilePath
+ instruction.DocumentationLine = startLine
context.SetEmbedding(&instruction)
} else {
parseErr = err
diff --git a/embedding/parsing/xml_parse.go b/embedding/parsing/xml_parse.go
index 4fe51b1..330d87e 100644
--- a/embedding/parsing/xml_parse.go
+++ b/embedding/parsing/xml_parse.go
@@ -47,6 +47,7 @@ type Item struct {
// - start — an optional glob-like pattern. If specified, lines before the matching one
// are excluded;
// - end — an optional glob-like pattern. If specified, lines after the matching one are excluded.
+// - comments — an optional comment filtering mode. If omitted, all comments are retained.
//
// config — a Configuration with all embed-code settings.
//
diff --git a/logging/logger.go b/logging/logger.go
index a8086b6..a05e9a1 100644
--- a/logging/logger.go
+++ b/logging/logger.go
@@ -94,10 +94,34 @@ func (h *Handler) WithGroup(name string) slog.Handler {
// defer HandlePanic(withStacktrace)
func HandlePanic(withStacktrace bool) {
if r := recover(); r != nil {
- fmt.Printf("Panic: %v\n", r)
+ fmt.Println(formatPanicMessage(r))
if withStacktrace {
debug.PrintStack()
}
os.Exit(1)
}
}
+
+// formatPanicMessage formats panic values for console output.
+func formatPanicMessage(recovered any) string {
+ err, ok := recovered.(error)
+ if !ok {
+ return fmt.Sprintf("panic: %v", recovered)
+ }
+
+ joined, ok := err.(interface {
+ Unwrap() []error
+ })
+ if !ok || len(joined.Unwrap()) <= 1 {
+ return fmt.Sprintf("panic: %v", err)
+ }
+
+ var builder strings.Builder
+ builder.WriteString("panic:")
+ for _, wrappedErr := range joined.Unwrap() {
+ builder.WriteString("\n- ")
+ builder.WriteString(wrappedErr.Error())
+ }
+
+ return builder.String()
+}
diff --git a/main.go b/main.go
index 98c7615..f742bbd 100644
--- a/main.go
+++ b/main.go
@@ -134,7 +134,7 @@ func logError(message string, err error) {
slog.Error(fmt.Sprintf("%s: %v", message, err))
}
-// checkByConfigs runs check for all configs and logs outdated documentation files.
+// checkByConfigs runs check for all configs and panics if documentation files are outdated.
func checkByConfigs(configs []configuration.Configuration) {
var totalOutdatedFiles []string
for _, config := range configs {
@@ -146,7 +146,8 @@ func checkByConfigs(configs []configuration.Configuration) {
return
}
- printFiles("File outdated:", "Files outdated:", totalOutdatedFiles)
+ printFiles("File to update:", "Files to update:", totalOutdatedFiles)
+ panic("the documentation files are not up-to-date with code files")
}
// embedByConfig runs the embedByConfig for all configs and logs the results.
diff --git a/test/resources/code/java/org/example/Comments.java b/test/resources/code/java/org/example/Comments.java
new file mode 100644
index 0000000..11934c8
--- /dev/null
+++ b/test/resources/code/java/org/example/Comments.java
@@ -0,0 +1,14 @@
+package org.example;
+
+/**
+ * Documents the public API.
+ */
+public interface Comments {
+ /*
+ * The block comment.
+ */
+ String marker = "http://example.org/*not-comment*/";
+
+ // Full-line inline comment.
+ String create(String name); // end-of-line inline comment.
+}