diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/HeadingProps.java b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/HeadingProps.java index 85934afd3..9cdb05240 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/HeadingProps.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/HeadingProps.java @@ -24,12 +24,10 @@ * Sections do not support bold text, images, bullet points, etc. * To customize headings style you can pass JSON block at the end of section text */ -public class HeadingProps { +public record HeadingProps(Map props) { public static HeadingProps EMPTY = new HeadingProps(Collections.emptyMap()); public static HeadingProps STYLE_API = new HeadingProps(Collections.singletonMap("style", "api")); - private final Map props; - public static HeadingProps styleApiWithBadge(String badgeText) { Map props = new HashMap<>(); props.put("badge", badgeText); @@ -38,11 +36,8 @@ public static HeadingProps styleApiWithBadge(String badgeText) { return new HeadingProps(props); } - public HeadingProps(Map props) { - this.props = props; - } - - public Map getProps() { + @Override + public Map props() { return Collections.unmodifiableMap(props); } diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/MarkdownVisitor.java b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/MarkdownVisitor.java index 78f5a9f48..45abb2a56 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/MarkdownVisitor.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/MarkdownVisitor.java @@ -35,9 +35,7 @@ import org.testingisdocumenting.znai.utils.JsonUtils; import java.nio.file.Path; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; public class MarkdownVisitor extends AbstractVisitor { private final ComponentsRegistry componentsRegistry; @@ -293,28 +291,29 @@ private void handleParamsValidationResult(PluginParamValidationResult validation private HeadingTextAndProps extractHeadingTextAndProps(Heading heading) { heading.accept(ValidateNoExtraSyntaxExceptInlineCodeInHeadingVisitor.INSTANCE); - Node firstChild = heading.getFirstChild(); - if (firstChild == null) { - return new HeadingTextAndProps("", HeadingProps.EMPTY); - } - - String extractedText = extractText(firstChild); + List textParts = new ArrayList<>(); + heading.accept(new AbstractVisitor() { + @Override + public void visit(Text text) { + HeadingTextAndProps headingTextAndProps = HeadingTextAndProps.extractTextAndProps(text.getLiteral()); + textParts.add(headingTextAndProps.text()); + } - int startOfCurlyIdx = extractedText.indexOf('{'); - if (startOfCurlyIdx == -1) { - return new HeadingTextAndProps(extractedText, HeadingProps.EMPTY); - } + @Override + public void visit(Code code) { + textParts.add(code.getLiteral()); + } + }); + String combinedText = String.join("", textParts).trim(); - try { - String jsonStart = extractedText.substring(startOfCurlyIdx); - - Map props = JsonUtils.deserializeAsMap(jsonStart); - String headingTextOnly = extractedText.substring(0, startOfCurlyIdx).trim(); - return new HeadingTextAndProps(headingTextOnly, new HeadingProps(props)); - } catch (JsonParseException e) { - throw new RuntimeException("Can't parse props of heading: " + extractedText, e); + Node lastChild = heading.getLastChild(); + if (lastChild instanceof Text textNode) { + HeadingTextAndProps headingTextAndProps = HeadingTextAndProps.extractTextAndProps(textNode.getLiteral()); + return new HeadingTextAndProps(combinedText, headingTextAndProps.props); + } else { + return new HeadingTextAndProps(combinedText, HeadingProps.EMPTY); } } @@ -326,13 +325,22 @@ private String extractText(Node node) { return ((Text) node).getLiteral().trim(); } - private static class HeadingTextAndProps { - private final String text; - private final HeadingProps props; + private record HeadingTextAndProps(String text, HeadingProps props) { + public static HeadingTextAndProps extractTextAndProps(String text) { + int startOfCurlyIdx = text.indexOf('{'); + if (startOfCurlyIdx == -1) { + return new HeadingTextAndProps(text, HeadingProps.EMPTY); + } + + try { + String jsonStart = text.substring(startOfCurlyIdx); - public HeadingTextAndProps(String text, HeadingProps props) { - this.text = text; - this.props = props; + Map props = JsonUtils.deserializeAsMap(jsonStart); + String headingTextOnly = text.substring(0, startOfCurlyIdx); + return new HeadingTextAndProps(headingTextOnly, new HeadingProps(props)); + } catch (JsonParseException e) { + throw new RuntimeException("Can't parse props of heading: " + text, e); + } + } } - } } diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/ValidateNoExtraSyntaxExceptInlineCodeInHeadingVisitor.java b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/ValidateNoExtraSyntaxExceptInlineCodeInHeadingVisitor.java index 242cb67e8..27ecc4540 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/ValidateNoExtraSyntaxExceptInlineCodeInHeadingVisitor.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/ValidateNoExtraSyntaxExceptInlineCodeInHeadingVisitor.java @@ -33,7 +33,6 @@ public void visit(BulletList bulletList) { @Override public void visit(Code code) { - onlyRegularTextAllowed(); } @Override diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/docelement/DocElementCreationParserHandler.java b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/docelement/DocElementCreationParserHandler.java index 4d0a20d0d..ec928cfaa 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/parser/docelement/DocElementCreationParserHandler.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/parser/docelement/DocElementCreationParserHandler.java @@ -100,7 +100,7 @@ public void onSectionStart(String title, HeadingProps headingProps) { onSectionEnd(); } - Map headingPropsMap = headingProps.getProps(); + Map headingPropsMap = headingProps.props(); String id = new PageSectionIdTitle(title, headingPropsMap).getId(); Map props = new LinkedHashMap<>(headingPropsMap); @@ -132,7 +132,7 @@ public void onSectionEnd() { @Override public void onSubHeading(int level, String title, HeadingProps headingProps) { - Map headingPropsMap = headingProps.getProps(); + Map headingPropsMap = headingProps.props(); String idByTitle = new PageSectionIdTitle(title, headingPropsMap).getId(); diff --git a/znai-core/src/test/groovy/org/testingisdocumenting/znai/parser/MarkdownParserTest.groovy b/znai-core/src/test/groovy/org/testingisdocumenting/znai/parser/MarkdownParserTest.groovy index f9b333394..b303184ae 100644 --- a/znai-core/src/test/groovy/org/testingisdocumenting/znai/parser/MarkdownParserTest.groovy +++ b/znai-core/src/test/groovy/org/testingisdocumenting/znai/parser/MarkdownParserTest.groovy @@ -242,170 +242,20 @@ world""") @Test void "second level section without text"() { - parse("## ") + parse("## ", Paths.get("empty-header.md")) content.should == [[type: 'SubHeading', level: 2, title: '', id: '']] } @Test - void "second level section with payload"() { - parse('## Secondary Section {badge: "v2.0"}" \ntext text') - content.should == [[type: 'SubHeading', level: 2, title: 'Secondary Section', id: 'secondary-section', badge: 'v2.0'], - [type: 'Paragraph', content: [[type: 'SimpleText', text: 'text text']]]] - } - - @Test - void "repeating sub headings within different parent sections"() { - parse(""" -# top level section -## example -#### java -### java -## constraint -### java -#### java -#### java -# another top level -## example -## example - """) - - content.should == [[title: 'top level section', id: 'top-level-section', type: 'Section', - content: [[level: 2, title: 'example', id: 'top-level-section-example', type: 'SubHeading'], - [level: 4, title: 'java', id: 'top-level-section-example-java', type: 'SubHeading'], - [level: 3, title: 'java', id: 'top-level-section-example-java-2', type: 'SubHeading'], - [level: 2, title: 'constraint', id: 'top-level-section-constraint', type: 'SubHeading'], - [level: 3, title: 'java', id: 'top-level-section-constraint-java', type: 'SubHeading'], - [level: 4, title: 'java', id: 'top-level-section-constraint-java-java', type: 'SubHeading'], - [level: 4, title: 'java', id: 'top-level-section-constraint-java-java-2', type: 'SubHeading']]], - [title: 'another top level', id: 'another-top-level', type: 'Section', - content: [[level: 2, title: 'example', id: 'another-top-level-example', type: 'SubHeading'], - [level: 2, title: 'example', id: 'another-top-level-example-2', type: 'SubHeading']]]] - } - - @Test - void "top level section with styles"() { - code { - parse("# title with **text**") - } should throwException("only regular text is supported in headings") - - code { - parse("# title *with*") - } should throwException("only regular text is supported in headings") - } - - @Test - void "second level section with styles"() { - code { - parse("## title with **text**") - } should throwException("only regular text is supported in headings") - - code { - parse("## title *with*") - } should throwException("only regular text is supported in headings") - } - - @Test - void "inlined image"() { - TEST_COMPONENTS_REGISTRY.timeService().fakedFileTime = 300000 - - parse("text ![alt text](images/png-test.png \"custom title\") another text") - content.should == [[type: 'Paragraph', content:[ - [text: "text " , type: "SimpleText"], - [title: "custom title", destination: '/test-doc/png-test.png', alt: 'alt text', type: 'Image', inlined: true, - width:762, height:581, timestamp: 300000], - [text: " another text" , type: "SimpleText"]]]] - } - - @Test - void "standalone image"() { - TEST_COMPONENTS_REGISTRY.timeService().fakedFileTime = 200000 - - parse("![alt text](images/png-test.png \"custom title\")") - content.should == [[title: "custom title", destination: '/test-doc/png-test.png', - alt: 'alt text', inlined: false, - width:762, height:581, - timestamp: 200000, - type: 'Image']] - } - - @Test - void "standalone svg image"() { - TEST_COMPONENTS_REGISTRY.timeService().fakedFileTime = 200000 - - parse("![alt text](images/test.svg \"custom title\")") - content.should == [[title: "custom title", destination: '/test-doc/test.svg', - alt: 'alt text', inlined: false, - timestamp: 200000, - type: 'Image']] + void "header inline code text is allowed"() { + parse('# my header about `thing` here {badge: "v3.4"}') + content.should == [[title: 'my header about thing here', id: 'my-header-about-thing-here', badge: 'v3.4', type: 'Section']] } @Test - void "image with external ref"() { - TEST_COMPONENTS_REGISTRY.timeService().fakedFileTime = 200000 - - parse("![alt text](https://host/images/png-test.png \"custom title\")") - content.should == [[title: "custom title", destination: 'https://host/images/png-test.png', - alt: 'alt text', inlined: false, - type: 'Image']] - } - - @Test - void "image with no alt text"() { - TEST_COMPONENTS_REGISTRY.timeService().fakedFileTime = 200000 - - parse("![](images/png-test.png \"custom title\")") - content.should == [[title: "custom title", destination: '/test-doc/png-test.png', - alt: 'image', inlined: false, - width:762, height:581, - timestamp: 200000, - type: 'Image']] - } - - @Test - void "include plugin"() { - parse(":include-dummy: free-form text {param1: 'v1', param2: 'v2'}") - content.should == [[type: 'IncludeDummy', ff: 'free-form text', opts: [param1: 'v1', param2: 'v2']]] - } - - @Test - void "include plugin with any number of spaces in front"() { - parse(" :include-dummy: free-form text {param1: 'v1', param2: 'v2'}") - def expected = [[type: 'IncludeDummy', ff: 'free-form text', opts: [param1: 'v1', param2: 'v2']]] - content.should == expected - - parse(" :include-dummy: free-form text {param1: 'v1', param2: 'v2'}") - content.should == expected - } - - @Test - void "include plugin right after paragraph of text"() { - parse("hello text\n:include-dummy: free-form text {param1: 'v1', param2: 'v2'}") - - content.should == [[type: 'Paragraph', content: [[text: 'hello text', type: 'SimpleText']]], - [ff: 'free-form text', opts: [param1: 'v1', param2: 'v2'], type: 'IncludeDummy']] - } - - @Test - void "include plugin inside nested code block"() { - parse(" :include-dummy: free-form text {param1: 'v1', param2: 'v2'}") - content.should == [[lang: "", - snippet: ":include-dummy: free-form text {param1: 'v1', param2: 'v2'}\n", - lineNumber: "", type: "Snippet"]] - } - - @Test - void "include plugin inside numbered list"() { - parse("1. step one\n\n" + - " :include-dummy: free-form text1 {param1: 'v1', param2: 'v2'}\n" + - "2. step two\n\n" + - " :include-dummy: free-form text2 {param1: 'v3', param2: 'v4'}\n") - - content.should == [[delimiter: '.', startNumber: 1, type: 'OrderedList', - content: [[type: 'ListItem', content: [[type: 'Paragraph', - content: [[text: 'step one', type: 'SimpleText']]], - [ff: 'free-form text1', opts: [param1: 'v1', param2: 'v2'], type: 'IncludeDummy']]], - [type: 'ListItem', content: [[type: 'Paragraph', content: [[text: 'step two', type: 'SimpleText']]], - [ff: 'free-form text2', opts: [param1: 'v3', param2: 'v4'], type: 'IncludeDummy']]]]]] + void "sub-header inline code text is allowed"() { + parse('## my header about `thing` here {badge: "v3.4"}', Paths.get("sub-header.md")) + content.should == [[title: 'my header about thing here', id: 'my-header-about-thing-here', badge: 'v3.4', type: 'SubHeading', level: 2]] } @Test @@ -603,6 +453,7 @@ world""") } private void parse(String markdown, Path path = Paths.get("test.md")) { + // use different path names if you use `sub headings` as the heading states is maintained per file/parsing parseResult = parser.parse(path, markdown) content = parseResult.docElement().getContent().collect { it.toMap() } } diff --git a/znai-docs/znai/release-notes/1.69/add-2024-05-06-initial-inline-code-in-header.md b/znai-docs/znai/release-notes/1.69/add-2024-05-06-initial-inline-code-in-header.md new file mode 100644 index 000000000..c26ad569b --- /dev/null +++ b/znai-docs/znai/release-notes/1.69/add-2024-05-06-initial-inline-code-in-header.md @@ -0,0 +1 @@ +* Add: Initial support for `inlined-code` in headers. For now it displays it as a regular text. \ No newline at end of file