From 41b856689ad08bc6d08de452b60c88fc6984084a Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sat, 6 Dec 2025 12:16:49 -0500 Subject: [PATCH 1/2] ocaml: include-ocaml-comments indentation fix --- .../ocaml/OcamlCommentExtractor.java | 19 ++++++-- .../ocaml/OcamlCommentExtractorTest.groovy | 43 ++++++++++++++++--- ...25-12-06-ocaml-comments-fix-indentation.md | 1 + .../znai/utils/StringUtils.java | 21 +++++++++ .../znai/utils/StringUtilsTest.groovy | 25 +++++++++++ 5 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 znai-docs/znai/release-notes/1.84/fix-2025-12-06-ocaml-comments-fix-indentation.md diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java index 6dd2dc549..3f698f215 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java @@ -45,14 +45,25 @@ MarkupParserResult extractCommentBlockAsDocElements(ComponentsRegistry component private String removeCommentPrefixAndSuffix(String commentBlock) { String trimmed = commentBlock.trim(); - + // Remove opening delimiter - either (** or (* int startIndex = trimmed.startsWith("(**") ? 3 : 2; - + // Remove closing delimiter - always *) String withoutDelimiters = trimmed.substring(startIndex, trimmed.length() - 2); - - return withoutDelimiters.trim(); + + // Check if first line has content (text on same line as opening delimiter) + String[] lines = withoutDelimiters.split("\n", 2); + boolean firstLineHasContent = lines.length > 0 && !lines[0].trim().isEmpty(); + + if (firstLineHasContent) { + // First line has text right after delimiter - skip it when calculating indentation + // This removes both source code indentation and comment alignment indentation + return StringUtils.stripIndentationSkipFirstLine(withoutDelimiters); + } else { + // First line is empty - content starts on next line, use regular strip + return StringUtils.stripIndentation(withoutDelimiters); + } } private String extractBlock(int startBlockIdx, int endBlockIdx) { diff --git a/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy b/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy index 52f01f97e..b940f5cb0 100644 --- a/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy +++ b/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy @@ -36,16 +36,16 @@ let x = 5 @Test void "extract multi line comment block"() { def content = """ -(* This is a +(* This is a multi-line comment *) let y = 10 """ def extractor = new OcamlCommentExtractor(content) def result = extractor.extractCommentBlock("let y") - result.should == """This is a - multi-line - comment""" + result.should == """This is a +multi-line +comment""" } @Test @@ -174,9 +174,42 @@ let transform lst = List.map (fun x -> x + 1) lst def extractor = new OcamlCommentExtractor(content) def elements = extractor.extractCommentBlockAsDocElements(TestComponentsRegistry.TEST_COMPONENTS_REGISTRY, Paths.get("test.ml"), "transform").contentToListOfMaps() - + elements.size().should == 1 elements[0].type.should == 'TestMarkup' elements[0].markup.should == 'Use `List.map` to transform elements' } + + @Test + void "indented multi-line comment should not create code blocks in markdown"() { + def content = """ + (* This function does something important. + It takes an argument and returns a result. + The algorithm is efficient. *) + let myFunc x = x + 1 +""" + def extractor = new OcamlCommentExtractor(content) + def elements = extractor.extractCommentBlockAsDocElements(TestComponentsRegistry.TEST_COMPONENTS_REGISTRY, + Paths.get("test.ml"), "myFunc").contentToListOfMaps() + + elements.size().should == 1 + elements[0].type.should == 'TestMarkup' + elements[0].markup.should == 'This function does something important.\nIt takes an argument and returns a result.\nThe algorithm is efficient.' + } + + @Test + void "comment with empty first line after delimiter"() { + def content = """ +(* + This is the first line of content + This is the second line +*) +let x = 5 +""" + def extractor = new OcamlCommentExtractor(content) + def result = extractor.extractCommentBlock("let x") + + result.should == """This is the first line of content +This is the second line""" + } } \ No newline at end of file diff --git a/znai-docs/znai/release-notes/1.84/fix-2025-12-06-ocaml-comments-fix-indentation.md b/znai-docs/znai/release-notes/1.84/fix-2025-12-06-ocaml-comments-fix-indentation.md new file mode 100644 index 000000000..d56662486 --- /dev/null +++ b/znai-docs/znai/release-notes/1.84/fix-2025-12-06-ocaml-comments-fix-indentation.md @@ -0,0 +1 @@ +* Fix: `:include-ocaml-comment:` properly strips indentations instead of creating a code block \ No newline at end of file diff --git a/znai-utils/src/main/java/org/testingisdocumenting/znai/utils/StringUtils.java b/znai-utils/src/main/java/org/testingisdocumenting/znai/utils/StringUtils.java index dbb139775..ad1056842 100644 --- a/znai-utils/src/main/java/org/testingisdocumenting/znai/utils/StringUtils.java +++ b/znai-utils/src/main/java/org/testingisdocumenting/znai/utils/StringUtils.java @@ -49,6 +49,27 @@ public static String stripIndentation(String text) { return lines.stream().map(l -> removeIndentation(l, indentation)).collect(Collectors.joining("\n")); } + /** + * Strips common indentation from a multi-line text, but skips the first line when calculating + * the minimum indentation. Useful when the first line has different formatting (e.g., starts + * on the same line as an opening delimiter). + * + * @param text the text to process + * @return text with indentation stripped from all lines except the first (which is trimmed) + */ + public static String stripIndentationSkipFirstLine(String text) { + String[] lines = text.split("\n", -1); + if (lines.length <= 1) { + return text.trim(); + } + + // Strip indentation from lines after the first + String restOfLines = String.join("\n", Arrays.copyOfRange(lines, 1, lines.length)); + String strippedRest = stripIndentation(restOfLines).trim(); + + return lines[0].trim() + (strippedRest.isEmpty() ? "" : "\n" + strippedRest); + } + public static String extractInsideCurlyBraces(String code) { int startIdx = code.indexOf('{'); if (startIdx == -1) { diff --git a/znai-utils/src/test/groovy/org/testingisdocumenting/znai/utils/StringUtilsTest.groovy b/znai-utils/src/test/groovy/org/testingisdocumenting/znai/utils/StringUtilsTest.groovy index ff9c761ce..256490a16 100644 --- a/znai-utils/src/test/groovy/org/testingisdocumenting/znai/utils/StringUtilsTest.groovy +++ b/znai-utils/src/test/groovy/org/testingisdocumenting/znai/utils/StringUtilsTest.groovy @@ -45,6 +45,31 @@ line #_3\r""") stripped.should == "int a = 2;\nint b = 3;" } + @Test + void "strip indentation but skip first line"() { + // First line has no indentation, continuation lines have 3 spaces + def text = "first line\n second line\n third line" + def stripped = StringUtils.stripIndentationSkipFirstLine(text) + // First line is trimmed, continuation lines have indentation stripped + stripped.should == "first line\nsecond line\nthird line" + } + + @Test + void "strip indentation skip first line with single line"() { + def text = " single line " + def stripped = StringUtils.stripIndentationSkipFirstLine(text) + // Single line is just trimmed + stripped.should == "single line" + } + + @Test + void "strip indentation skip first line with first line having leading space"() { + def text = " first line\n second line\n third line" + def stripped = StringUtils.stripIndentationSkipFirstLine(text) + // First line is trimmed, continuation lines have 4-space indentation stripped + stripped.should == "first line\nsecond line\nthird line" + } + @Test void "extracts inside curly braces"() { def code = "{\n statement1;\n statement2}" From 183209f2e019724a8c6cd9c5da8b3407df698d5b Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sat, 6 Dec 2025 12:31:25 -0500 Subject: [PATCH 2/2] ocaml: include-ocaml-comments indentation fix --- .../ocaml/OcamlCommentExtractor.java | 2 -- .../ocaml/OcamlCommentExtractorTest.groovy | 29 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java index 3f698f215..8822b3181 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractor.java @@ -48,8 +48,6 @@ private String removeCommentPrefixAndSuffix(String commentBlock) { // Remove opening delimiter - either (** or (* int startIndex = trimmed.startsWith("(**") ? 3 : 2; - - // Remove closing delimiter - always *) String withoutDelimiters = trimmed.substring(startIndex, trimmed.length() - 2); // Check if first line has content (text on same line as opening delimiter) diff --git a/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy b/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy index b940f5cb0..b539b67ab 100644 --- a/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy +++ b/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/ocaml/OcamlCommentExtractorTest.groovy @@ -21,6 +21,8 @@ import org.testingisdocumenting.znai.parser.TestComponentsRegistry import java.nio.file.Paths +import static org.testingisdocumenting.webtau.Matchers.* + class OcamlCommentExtractorTest { @Test void "extract single line comment block"() { @@ -72,24 +74,43 @@ let docFunc x = x * 2 result.should == "This is a documentation comment" } - @Test(expected = IllegalArgumentException) + @Test void "throw exception when text not found"() { def content = """ (* Some comment *) let x = 5 """ def extractor = new OcamlCommentExtractor(content) - extractor.extractCommentBlock("nonexistent") + + code { + extractor.extractCommentBlock("nonexistent") + }.should throwException(IllegalArgumentException, contain("can't find text: nonexistent")) } - @Test(expected = IllegalArgumentException) + @Test void "throw exception when no comment block found before match"() { def content = """ let x = 5 let y = 10 """ def extractor = new OcamlCommentExtractor(content) - extractor.extractCommentBlock("let x") + + code { + extractor.extractCommentBlock("let x") + }.should throwException(IllegalArgumentException, contain("can't find comment block start")) + } + + @Test + void "throw exception when comment block is not properly closed"() { + def content = """ +(* This comment is not closed +let x = 5 +""" + def extractor = new OcamlCommentExtractor(content) + + code { + extractor.extractCommentBlock("let x") + }.should throwException(IllegalArgumentException, contain("can't find comment block end")) } @Test