From 06e7f1d167424db161d8b574cba9d64b51cc2306 Mon Sep 17 00:00:00 2001 From: Abel Salgado Romero Date: Sat, 3 Feb 2024 11:28:15 +0100 Subject: [PATCH] Add support for description lists in parser-doxia-module * Checkpoint: tested all description lists from docs * Update docs * Update IT Closes #751 --- .../src/site/asciidoc/sample.adoc | 13 ++ .../src/it/maven-site-plugin/validate.groovy | 19 ++ .../maven/site/ast/NodesSinker.java | 32 ++-- .../DescriptionListNodeProcessor.java | 77 ++++++++ .../ast/processors/ListItemNodeProcessor.java | 43 +++-- .../DescriptionListNodeProcessorTest.java | 176 ++++++++++++++++++ .../maven/site/ast/processors/test/Html.java | 49 +++++ ...parser-module-setup-and-configuration.adoc | 8 +- 8 files changed, 390 insertions(+), 27 deletions(-) create mode 100644 asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessor.java create mode 100644 asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessorTest.java create mode 100644 asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/test/Html.java diff --git a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc index 63b3da47..364d2c9d 100644 --- a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc +++ b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc @@ -75,3 +75,16 @@ public class HelloWorld { . Protons . Electrons . Neutrons + +==== Description list + +Operating Systems:: +Linux::: +. Fedora +* Desktop +. Ubuntu +* Desktop +* Server +BSD::: +. FreeBSD +. NetBSD diff --git a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy index 7cc629ed..49cbe17b 100644 --- a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy +++ b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy @@ -42,6 +42,15 @@ new HtmlAsserter(htmlContent).with { asserter -> asserter.containsSectionTitle("Ordered list", 4) asserter.containsOrderedList("Protons", "Electrons", "Neutrons") + asserter.containsSectionTitle("Description list", 4) + asserter.descriptionListTerm("Operating Systems") + asserter.descriptionListTerm("Linux") + asserter.contains("
  • Fedora") + asserter.containsUnorderedList("Desktop") + asserter.contains("
  • Ubuntu") + asserter.containsUnorderedList("Desktop", "Server") + asserter.descriptionListTerm("BSD") + asserter.containsOrderedList("FreeBSD", "NetBSD") } String strong(String text) { @@ -111,6 +120,11 @@ class HtmlAsserter { return content.indexOf(value, lastAssertionCursor) } + void contains(String text) { + def found = find(text) + assertFound("HTML text", text, found) + } + void containsDocumentTitle(String value) { def found = find("

    $value

    ") assertFound("Document Title", value, found) @@ -160,6 +174,11 @@ class HtmlAsserter { assertFound("Ordered list", values.join(','), found) } + void descriptionListTerm(String term) { + def found = find("
    ${term}
    ") + assertFound("Description list", term, found) + } + void containsTable(int columns, int rows, List headers, String caption) { def start = content.indexOf("", lastAssertionCursor) + "".length() diff --git a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/NodesSinker.java b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/NodesSinker.java index 6571cb54..31db76a1 100644 --- a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/NodesSinker.java +++ b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/NodesSinker.java @@ -6,6 +6,7 @@ import org.apache.maven.doxia.sink.Sink; import org.asciidoctor.ast.StructuralNode; +import org.asciidoctor.maven.site.ast.processors.DescriptionListNodeProcessor; import org.asciidoctor.maven.site.ast.processors.DocumentNodeProcessor; import org.asciidoctor.maven.site.ast.processors.ImageNodeProcessor; import org.asciidoctor.maven.site.ast.processors.ListItemNodeProcessor; @@ -39,23 +40,26 @@ public NodesSinker(Sink sink) { UnorderedListNodeProcessor unorderedListNodeProcessor = new UnorderedListNodeProcessor(sink); OrderedListNodeProcessor orderedListNodeProcessor = new OrderedListNodeProcessor(sink); + DescriptionListNodeProcessor descriptionListNodeProcessor = new DescriptionListNodeProcessor(sink); ListItemNodeProcessor listItemNodeProcessor = new ListItemNodeProcessor(sink); - listItemNodeProcessor.setNodeProcessors(Arrays.asList(unorderedListNodeProcessor, orderedListNodeProcessor)); + listItemNodeProcessor.setNodeProcessors(Arrays.asList(unorderedListNodeProcessor, orderedListNodeProcessor, descriptionListNodeProcessor)); unorderedListNodeProcessor.setItemNodeProcessor(listItemNodeProcessor); orderedListNodeProcessor.setItemNodeProcessor(listItemNodeProcessor); + descriptionListNodeProcessor.setItemNodeProcessor(listItemNodeProcessor); nodeProcessors = Arrays.asList( - new DocumentNodeProcessor(sink), - new ImageNodeProcessor(sink), - new ListingNodeProcessor(sink), - new LiteralNodeProcessor(sink), - new ParagraphNodeProcessor(sink), - new PreambleNodeProcessor(sink), - new SectionNodeProcessor(sink), - new TableNodeProcessor(sink), - orderedListNodeProcessor, - unorderedListNodeProcessor + new DocumentNodeProcessor(sink), + new ImageNodeProcessor(sink), + new ListingNodeProcessor(sink), + new LiteralNodeProcessor(sink), + new ParagraphNodeProcessor(sink), + new PreambleNodeProcessor(sink), + new SectionNodeProcessor(sink), + new TableNodeProcessor(sink), + descriptionListNodeProcessor, + orderedListNodeProcessor, + unorderedListNodeProcessor ); } @@ -72,8 +76,8 @@ private void processNode(StructuralNode node, int depth) { try { // Only one matches in current NodeProcessors implementation Optional nodeProcessor = nodeProcessors.stream() - .filter(np -> np.applies(node)) - .findFirst(); + .filter(np -> np.applies(node)) + .findFirst(); if (nodeProcessor.isPresent()) { NodeProcessor processor = nodeProcessor.get(); processor.process(node); @@ -90,6 +94,6 @@ private void processNode(StructuralNode node, int depth) { private void traverse(StructuralNode node, int depth) { node.getBlocks() - .forEach(b -> processNode(b, depth + 1)); + .forEach(b -> processNode(b, depth + 1)); } } diff --git a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessor.java b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessor.java new file mode 100644 index 00000000..5073cc6a --- /dev/null +++ b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessor.java @@ -0,0 +1,77 @@ +package org.asciidoctor.maven.site.ast.processors; + +import java.util.List; + +import org.apache.maven.doxia.sink.Sink; +import org.asciidoctor.ast.DescriptionList; +import org.asciidoctor.ast.DescriptionListEntry; +import org.asciidoctor.ast.ListItem; +import org.asciidoctor.ast.StructuralNode; +import org.asciidoctor.maven.site.ast.NodeProcessor; + +/** + * Description list processor. + * + * @author abelsromero + * @since 3.0.0 + */ +public class DescriptionListNodeProcessor extends AbstractSinkNodeProcessor implements NodeProcessor { + + private ListItemNodeProcessor itemNodeProcessor; + + /** + * Constructor. + * + * @param sink Doxia {@link Sink} + */ + public DescriptionListNodeProcessor(Sink sink) { + super(sink); + } + + /** + * Inject a {@link ListItemNodeProcessor}. + * + * @param nodeProcessor {@link ListItemNodeProcessor} + */ + public void setItemNodeProcessor(ListItemNodeProcessor nodeProcessor) { + this.itemNodeProcessor = nodeProcessor; + } + + @Override + public boolean applies(StructuralNode node) { + return "dlist".equals(node.getNodeName()); + } + + @Override + public boolean isTerminal(StructuralNode node) { + return true; + } + + @Override + public void process(StructuralNode node) { + + final List items = ((DescriptionList) node).getItems(); + final Sink sink = getSink(); + + if (!items.isEmpty()) { + sink.definitionList(); + for (DescriptionListEntry item : items) { + // About the model, see https://asciidoctor.zulipchat.com/#narrow/stream/279642-users/topic/.E2.9C.94.20Description.20List.20AST.20structure/near/419353063 + final ListItem term = item.getTerms().get(0); + sink.definedTerm(); + sink.rawText(term.getText()); + sink.definedTerm_(); + + final ListItem description = item.getDescription(); + sink.definition(); + if (description.getBlocks().isEmpty()) { + sink.rawText(description.getText()); + } else { + itemNodeProcessor.process(description); + } + sink.definition_(); + } + sink.definitionList_(); + } + } +} diff --git a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/ListItemNodeProcessor.java b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/ListItemNodeProcessor.java index 2a42d6f2..945fabd9 100644 --- a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/ListItemNodeProcessor.java +++ b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/ast/processors/ListItemNodeProcessor.java @@ -45,12 +45,19 @@ public boolean applies(StructuralNode node) { public void process(StructuralNode node) { final ListItem item = (ListItem) node; final Sink sink = getSink(); - if (isUnorderedListItem(item)) - sink.listItem(); - else - sink.numberedListItem(); + final ListType listType = getListType(item); - String text = item.getText(); + // description type does not require any action + switch (listType) { + case ordered: + sink.numberedListItem(); + break; + case unordered: + sink.listItem(); + break; + } + + final String text = item.getText(); sink.rawText(text == null ? "" : text); for (StructuralNode subNode : node.getBlocks()) { @@ -61,14 +68,28 @@ public void process(StructuralNode node) { } } - if (isUnorderedListItem(item)) - sink.listItem_(); - else - sink.numberedListItem_(); + switch (listType) { + case ordered: + sink.numberedListItem_(); + break; + case unordered: + sink.listItem_(); + break; + } } - private static boolean isUnorderedListItem(ListItem item) { + private static ListType getListType(ListItem item) { final String marker = item.getMarker(); - return marker.startsWith("*") || marker.startsWith("-"); + if (marker == null) { + return ListType.description; + } else if (marker.startsWith("*") || marker.startsWith("-")) { + return ListType.ordered; + } else { + return ListType.unordered; + } + } + + enum ListType { + ordered, unordered, description } } diff --git a/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessorTest.java b/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessorTest.java new file mode 100644 index 00000000..8a3864c4 --- /dev/null +++ b/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/DescriptionListNodeProcessorTest.java @@ -0,0 +1,176 @@ +package org.asciidoctor.maven.site.ast.processors; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; + +import org.apache.maven.doxia.sink.Sink; +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Options; +import org.asciidoctor.ast.StructuralNode; +import org.asciidoctor.maven.site.ast.NodeProcessor; +import org.asciidoctor.maven.site.ast.processors.test.NodeProcessorTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.asciidoctor.maven.site.ast.processors.test.Html.LIST_STYLE_TYPE_DECIMAL; +import static org.asciidoctor.maven.site.ast.processors.test.Html.dd; +import static org.asciidoctor.maven.site.ast.processors.test.Html.dt; +import static org.asciidoctor.maven.site.ast.processors.test.Html.italics; +import static org.asciidoctor.maven.site.ast.processors.test.Html.li; +import static org.asciidoctor.maven.site.ast.processors.test.Html.monospace; +import static org.asciidoctor.maven.site.ast.processors.test.Html.ol; +import static org.asciidoctor.maven.site.ast.processors.test.Html.strong; +import static org.asciidoctor.maven.site.ast.processors.test.Html.ul; +import static org.asciidoctor.maven.site.ast.processors.test.StringTestUtils.clean; +import static org.assertj.core.api.Assertions.assertThat; + +@NodeProcessorTest(DescriptionListNodeProcessor.class) +class DescriptionListNodeProcessorTest { + + private Asciidoctor asciidoctor; + private NodeProcessor nodeProcessor; + private Sink sink; + private StringWriter sinkWriter; + + + @BeforeEach + void setup() { + ListItemNodeProcessor listItemNodeProcessor = new ListItemNodeProcessor(sink); + ((DescriptionListNodeProcessor) nodeProcessor).setItemNodeProcessor(listItemNodeProcessor); + + OrderedListNodeProcessor oListNodeProcessor = new OrderedListNodeProcessor(sink); + oListNodeProcessor.setItemNodeProcessor(listItemNodeProcessor); + + UnorderedListNodeProcessor uListNodeProcessor = new UnorderedListNodeProcessor(sink); + uListNodeProcessor.setItemNodeProcessor(listItemNodeProcessor); + + listItemNodeProcessor.setNodeProcessors(Arrays.asList(nodeProcessor, oListNodeProcessor, uListNodeProcessor)); + } + + @Test + void should_convert_simple_list() { + String content = buildDocumentWithSimpleList(); + + String html = process(content); + + // TODO: document We are not adding additional
    /

    , unlike Asciidoctor + assertThat(html) + .isEqualTo("

    " + + dt("CPU") + dd("The brain of the computer.") + + dt("RAM") + dd("Temporarily stores information the CPU uses during operation.") + + "
    "); + } + + @Test + void should_convert_simple_list_with_formatting() { + String content = buildDocumentWithSimpleListWithFormatting(); + + String html = process(content); + + assertThat(html) + .isEqualTo("
    " + + dt(strong("CPU")) + dd("The brain of " + italics("the computer") + ".") + + dt(monospace("RAM")) + dd(strong("Temporarily stores information") + " the CPU uses during operation.") + + "
    "); + } + + @Test + void should_convert_simple_list_with_nested_list() { + String content = buildDocumentWithNestedLists(); + + String html = process(content); + + assertThat(html) + .isEqualTo("
    " + + dt("Dairy") + + "
    " + + ul(li("Milk"), li("Eggs")) + + "
    " + + dt("Bakery") + + "
    " + + ol(LIST_STYLE_TYPE_DECIMAL, li("Bread")) + + "
    " + + "
    " + ); + } + + @Test + void should_convert_nested_description_lists() { + String content = buildDocumentWithNestedDescriptionLists(); + + String html = process(content); + + assertThat(html) + .isEqualTo("
    " + + dt("Operating Systems") + + "
    " + + "
    " + + + dt("Linux") + + "
    " + + ol(LIST_STYLE_TYPE_DECIMAL, + li("Fedora" + ul(li("Desktop"))), + li("Ubuntu" + ul(li("Desktop"), li("Server"))) + ) + + "
    " + + dt("BSD") + + "
    " + + ol(LIST_STYLE_TYPE_DECIMAL, li("FreeBSD"), li("NetBSD")) + + "
    " + + "
    " + + + "
    " + + "
    " + ); + } + + private static String buildDocumentWithSimpleList() { + return "= Document tile\n\n" + + "== Section\n\n" + + "CPU:: The brain of the computer.\n" + + "RAM:: Temporarily stores information the CPU uses during operation.\n"; + } + + private static String buildDocumentWithSimpleListWithFormatting() { + return "= Document tile\n\n" + + "== Section\n\n" + + "*CPU*:: The brain of _the computer_.\n" + + "`RAM`:: *Temporarily stores information* the CPU uses during operation.\n"; + } + + private static String buildDocumentWithNestedLists() { + return "= Document tile\n\n" + + "== Section\n\n" + + "Dairy::\n" + + "* Milk\n" + + "* Eggs\n" + + "Bakery::\n" + + ". Bread\n"; + } + + private static String buildDocumentWithNestedDescriptionLists() { + return "= Document tile\n\n" + + "== Section\n\n" + + "Operating Systems::\n" + + " Linux:::\n" + + " . Fedora\n" + + " * Desktop\n" + + " . Ubuntu\n" + + " * Desktop\n" + + " * Server\n" + + " BSD:::\n" + + " . FreeBSD\n" + + " . NetBSD\n"; + } + + private String process(String content) { + StructuralNode node = asciidoctor.load(content, Options.builder().build()) + .findBy(Collections.singletonMap("context", ":dlist")) + .get(0); + + nodeProcessor.process(node); + + return clean(sinkWriter.toString()); + } +} diff --git a/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/test/Html.java b/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/test/Html.java new file mode 100644 index 00000000..d3e9abf6 --- /dev/null +++ b/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/ast/processors/test/Html.java @@ -0,0 +1,49 @@ +package org.asciidoctor.maven.site.ast.processors.test; + +public class Html { + + public static final String LIST_STYLE_TYPE_DECIMAL = "list-style-type: decimal"; + + public static String strong(String text) { + return htmlElement("strong", text); + } + + public static String italics(String text) { + return htmlElement("em", text); + } + + public static String monospace(String text) { + return htmlElement("code", text); + } + + public static String ul(String... elements) { + return htmlElement("ul", String.join("", elements)); + } + + public static String ol(String style, String... elements) { + return htmlElement("ol", style, String.join("", elements)); + } + + public static String li(String text) { + return htmlElement("li", text); + } + + public static String dt(String text) { + return htmlElement("dt", text); + } + + public static String dd(String text) { + return htmlElement("dd", text); + } + + static String htmlElement(String element, String text) { + return htmlElement(element, null, text); + } + + static String htmlElement(String element, String style, String text) { + if (style == null) { + return String.format("<%1$s>%2$s", element, text).trim(); + } + return String.format("<%1$s style=\"%3$s\">%2$s", element, text, style).trim(); + } +} diff --git a/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc b/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc index 870693d2..154f5750 100644 --- a/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc +++ b/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc @@ -71,7 +71,8 @@ Make sure you add a `menu` item for each page so you can access it from the site Given the modules implements a custom converter, the following features are limited: * `templateDirs` configurations are not supported in this module. -* Extensions injected with `requires` can only modify the AST. Modifications of output won't be applied. +* Extensions injected with `requires` can only modify the AST. +Modifications of output won't be applied. ==== As of version 1.5.3 of the plugin, you can configure Asciidoctor by specifying configuration properties in the plugin declaration, just like with the other goals of the plugin. @@ -171,13 +172,16 @@ This module is still under development, here is a summary of supported features: ** Support for `sectnums` and `sectnumlevels` * Paragraphs -** Basic formatting (bold, italics, etc.) +** Basic formatting (bold, italics, monospace, etc.) ** Attributes substitutions * Lists ** Unordered, for `*` and `-` markers ** Ordered, only arabic numerals +** Description lists, with nested ordered, unordered and description lists ** Formatted text in list items ++ +NOTE: Unlike in Asciidoctor lists, descriptions are not surrounded by `

    ` and list themselves are not surrounded by `

    ` elements. * Code blocks with source-highlighting using https://maven.apache.org/skins/maven-fluido-skin/#source-code-line-numbers[Fluido Skin Pretiffy]. ** Support for numbered lines with `linenums`