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. +}