This is an automated email from the ASF dual-hosted git repository. kwin pushed a commit to branch bugfix/fix-code-link in repository https://gitbox.apache.org/repos/asf/maven-doxia.git
commit d5ec544fec1b0df412d571292c0119a239338fa1 Author: Konrad Windszus <k...@apache.org> AuthorDate: Sat Oct 19 11:29:47 2024 +0200 [DOXIA-751] Linked inline code must be emitted in right order Introduce buffer stack to be able to buffer for each context separately. Refactoring of buffer handling --- .../maven/doxia/module/markdown/MarkdownSink.java | 178 ++++++++++++--------- .../doxia/module/markdown/MarkdownSinkTest.java | 15 ++ 2 files changed, 119 insertions(+), 74 deletions(-) diff --git a/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/MarkdownSink.java b/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/MarkdownSink.java index 91dceae9..eedfa17c 100644 --- a/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/MarkdownSink.java +++ b/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/MarkdownSink.java @@ -51,9 +51,11 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { // Instance fields // ---------------------------------------------------------------------- - /** A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>. - * The content of this buffer is already escaped. */ - private StringBuilder buffer; + /** + * A buffer that holds the current text when the current context requires buffering. + * The content of this buffer is already escaped. + */ + private Queue<StringBuilder> bufferStack = Collections.asLifoQueue(new LinkedList<>()); /** author. */ private Collection<String> authors; @@ -95,23 +97,21 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { /** Most important contextual metadata (of the surrounding element) */ enum ElementContext { - HEAD("head", Type.GENERIC_CONTAINER, null, true), - BODY("body", Type.GENERIC_CONTAINER, MarkdownSink::escapeMarkdown), + HEAD(Type.GENERIC_CONTAINER, null, true), + BODY(Type.GENERIC_CONTAINER, MarkdownSink::escapeMarkdown), // only the elements, which affect rendering of children and are different from BODY or HEAD are listed here - FIGURE("", Type.INLINE, MarkdownSink::escapeMarkdown, true), - CODE_BLOCK("code block", Type.LEAF_BLOCK, null, false), - CODE_SPAN("code span", Type.INLINE, null), - TABLE_CAPTION("table caption", Type.INLINE, MarkdownSink::escapeMarkdown), + FIGURE(Type.INLINE, MarkdownSink::escapeMarkdown, true), + CODE_BLOCK(Type.LEAF_BLOCK, null, false), + CODE_SPAN(Type.INLINE, null, true), + TABLE_CAPTION(Type.INLINE, MarkdownSink::escapeMarkdown), + TABLE_ROW(Type.CONTAINER_BLOCK, null, true), TABLE_CELL( - "table cell", Type.LEAF_BLOCK, MarkdownSink::escapeForTableCell, - true), // special type, as allows containing inlines, but not starting on a separate line + false), // special type, as allows containing inlines, but not starting on a separate line // same parameters as BODY but paragraphs inside list items are handled differently - LIST_ITEM("list item", Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, INDENT), - BLOCKQUOTE("blockquote", Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP); - - final String name; + LIST_ITEM(Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, INDENT), + BLOCKQUOTE(Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP); /** * @see <a href="https://spec.commonmark.org/0.30/#blocks-and-inlines">CommonMark, 3 Blocks and inlines</a> @@ -159,31 +159,24 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { */ final boolean requiresSurroundingByBlankLines; - ElementContext(String name, Type type, UnaryOperator<String> escapeFunction) { - this(name, type, escapeFunction, false); + ElementContext(Type type, UnaryOperator<String> escapeFunction) { + this(type, escapeFunction, false); } - ElementContext(String name, Type type, UnaryOperator<String> escapeFunction, boolean requiresBuffering) { - this(name, type, escapeFunction, requiresBuffering, ""); + ElementContext(Type type, UnaryOperator<String> escapeFunction, boolean requiresBuffering) { + this(type, escapeFunction, requiresBuffering, ""); } - ElementContext( - String name, - Type type, - UnaryOperator<String> escapeFunction, - boolean requiresBuffering, - String prefix) { - this(name, type, escapeFunction, requiresBuffering, prefix, false); + ElementContext(Type type, UnaryOperator<String> escapeFunction, boolean requiresBuffering, String prefix) { + this(type, escapeFunction, requiresBuffering, prefix, false); } ElementContext( - String name, Type type, UnaryOperator<String> escapeFunction, boolean requiresBuffering, String prefix, boolean requiresSurroundingByBlankLines) { - this.name = name; this.type = type; this.escapeFunction = escapeFunction; this.requiresBuffering = requiresBuffering; @@ -248,9 +241,16 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { if (removedContext.isBlock()) { endBlock(removedContext.requiresSurroundingByBlankLines); } + if (removedContext.requiresBuffering) { + // remove buffer from stack (assume it has been evaluated already) + bufferStack.remove(); + } } private void startContext(ElementContext newContext) { + if (newContext.requiresBuffering) { + bufferStack.add(new StringBuilder()); + } if (newContext.isBlock()) { startBlock(newContext.requiresSurroundingByBlankLines); } @@ -307,20 +307,34 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { } /** - * Returns the buffer that holds the current text. - * - * @return A StringBuffer. + * Returns the buffer that holds the text of the current context (or the closest container context with a buffer). + * @return The StringBuilder representing the current buffer, never {@code null} + * @throws NoSuchElementException if no buffer is available */ - protected StringBuilder getBuffer() { - return buffer; + protected StringBuilder getCurrentBuffer() { + return bufferStack.element(); + } + + /** + * Returns the content of the buffer of the current context (or the closest container context with a buffer). + * The buffer is reset to an empty string in this method. + * @return the content of the buffer as a string or {@code null} if no buffer is available + */ + protected String consumeBuffer() { + StringBuilder buffer = bufferStack.peek(); + if (buffer == null) { + return null; + } else { + String content = buffer.toString(); + buffer.setLength(0); + return content; + } } @Override protected void init() { super.init(); - resetBuffer(); - this.authors = new LinkedList<>(); this.title = null; this.date = null; @@ -334,19 +348,12 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { elementContextStack.add(ElementContext.BODY); } - /** - * Reset the StringBuilder. - */ - protected void resetBuffer() { - buffer = new StringBuilder(); - } - @Override public void head(SinkEventAttributes attributes) { init(); // remove default body context here endContext(ElementContext.BODY); - elementContextStack.add(ElementContext.HEAD); + startContext(ElementContext.HEAD); } @Override @@ -374,6 +381,7 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { @Override public void body(SinkEventAttributes attributes) { + startContext(ElementContext.BODY); elementContextStack.add(ElementContext.BODY); } @@ -384,25 +392,25 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { @Override public void title_() { - if (buffer.length() > 0) { - title = buffer.toString(); - resetBuffer(); + String buffer = consumeBuffer(); + if (buffer != null && !buffer.isEmpty()) { + this.title = buffer.toString(); } } @Override public void author_() { - if (buffer.length() > 0) { - authors.add(buffer.toString()); - resetBuffer(); + String buffer = consumeBuffer(); + if (buffer != null && !buffer.isEmpty()) { + authors.add(buffer); } } @Override public void date_() { - if (buffer.length() > 0) { + String buffer = consumeBuffer(); + if (buffer != null && !buffer.isEmpty()) { date = buffer.toString(); - resetBuffer(); } } @@ -579,11 +587,14 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { @Override public void tableRow(SinkEventAttributes attributes) { + startContext(ElementContext.TABLE_ROW); cellCount = 0; } @Override public void tableRow_() { + String buffer = consumeBuffer(); + endContext(ElementContext.TABLE_ROW); if (isFirstTableRow && !tableHeaderCellFlag) { // emit empty table header as this is mandatory for GFM table extension // (https://stackoverflow.com/a/17543474/5155923) @@ -596,10 +607,8 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { writeUnescaped(TABLE_ROW_PREFIX); - writeUnescaped(buffer.toString()); - - resetBuffer(); - + // this must bypass the buffer + writeUnescaped(buffer); writeUnescaped(EOL); if (isFirstTableRow) { @@ -648,6 +657,7 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { @Override public void tableCell(SinkEventAttributes attributes) { + startContext(ElementContext.TABLE_CELL); if (attributes != null) { // evaluate alignment attributes final int cellJustification; @@ -674,7 +684,6 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { } } } - elementContextStack.add(ElementContext.TABLE_CELL); } @Override @@ -698,7 +707,7 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { */ private void endTableCell() { endContext(ElementContext.TABLE_CELL); - buffer.append(TABLE_CELL_SEPARATOR_MARKUP); + writeUnescaped(TABLE_CELL_SEPARATOR_MARKUP); cellCount++; } @@ -715,7 +724,7 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { @Override public void figure(SinkEventAttributes attributes) { figureSrc = null; - elementContextStack.add(ElementContext.FIGURE); + startContext(ElementContext.FIGURE); } @Override @@ -733,8 +742,13 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { @Override public void figure_() { + StringBuilder buffer = getCurrentBuffer(); + String label = ""; + if (buffer != null) { + label = buffer.toString(); + } endContext(ElementContext.FIGURE); - writeImage(buffer.toString(), figureSrc); + writeImage(label, figureSrc); } private void writeImage(String alt, String src) { @@ -756,15 +770,36 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { /** {@inheritDoc} */ public void link(String name, SinkEventAttributes attributes) { - writeUnescaped(LINK_START_1_MARKUP); - linkName = name; + if (elementContextStack.element() == ElementContext.CODE_BLOCK) { + LOGGER.warn("{}Ignoring unsupported link inside code block", getLocationLogPrefix()); + } else if (elementContextStack.element() == ElementContext.CODE_SPAN) { + // emit link outside the code span, i.e. insert at the beginning of the buffer + getCurrentBuffer().insert(0, LINK_START_1_MARKUP); + linkName = name; + } else { + writeUnescaped(LINK_START_1_MARKUP); + linkName = name; + } } @Override public void link_() { - writeUnescaped(LINK_START_2_MARKUP); - text(linkName.startsWith("#") ? linkName.substring(1) : linkName); - writeUnescaped(LINK_END_MARKUP); + if (elementContextStack.element() == ElementContext.CODE_BLOCK) { + return; + } else if (elementContextStack.element() == ElementContext.CODE_SPAN) { + // defer emitting link end markup until inline_() is called + StringBuilder linkEndMarkup = new StringBuilder(); + linkEndMarkup.append(LINK_START_2_MARKUP); + linkEndMarkup.append(escapeMarkdown(linkName.startsWith("#") ? linkName.substring(1) : linkName)); + linkEndMarkup.append(LINK_END_MARKUP); + Queue<String> endMarkups = new LinkedList<>(inlineStack.poll()); + endMarkups.add(linkEndMarkup.toString()); + inlineStack.add(endMarkups); + } else { + writeUnescaped(LINK_START_2_MARKUP); + text(linkName.startsWith("#") ? linkName.substring(1) : linkName); + writeUnescaped(LINK_END_MARKUP); + } linkName = null; } @@ -779,9 +814,9 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "code") || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "monospaced") || attributes.containsAttribute(SinkEventAttributes.STYLE, "monospaced")) { + startContext(ElementContext.CODE_SPAN); writeUnescaped(MONOSPACED_START_MARKUP); endMarkups.add(MONOSPACED_END_MARKUP); - elementContextStack.add(ElementContext.CODE_SPAN); } else { // in XHTML "<em>" is used, but some tests still rely on the outdated "<italic>" if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "em") @@ -806,7 +841,9 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { public void inline_() { for (String endMarkup : inlineStack.remove()) { if (endMarkup.equals(MONOSPACED_END_MARKUP)) { + String buffer = getCurrentBuffer().toString(); endContext(ElementContext.CODE_SPAN); + writeUnescaped(buffer); } writeUnescaped(endMarkup); } @@ -896,16 +933,9 @@ public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { LOGGER.warn("{}Unknown Sink event '" + name + "', ignoring!", getLocationLogPrefix()); } - /** - * - * @return {@code true} if any of the parent contexts require buffering - */ - private boolean requiresBuffering() { - return elementContextStack.stream().anyMatch(c -> c.requiresBuffering); - } - protected void writeUnescaped(String text) { - if (requiresBuffering()) { + StringBuilder buffer = bufferStack.peek(); + if (buffer != null) { buffer.append(text); } else { writer.write(text); diff --git a/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/MarkdownSinkTest.java b/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/MarkdownSinkTest.java index 44d1162e..9308449d 100644 --- a/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/MarkdownSinkTest.java +++ b/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/MarkdownSinkTest.java @@ -31,6 +31,7 @@ import org.apache.maven.doxia.parser.Parser; import org.apache.maven.doxia.sink.Sink; import org.apache.maven.doxia.sink.impl.AbstractSinkTest; import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; +import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics; import org.apache.maven.doxia.sink.impl.SinkEventTestingSink; import org.apache.maven.doxia.util.HtmlTools; import org.hamcrest.MatcherAssert; @@ -513,4 +514,18 @@ public class MarkdownSinkTest extends AbstractSinkTest { String expected = "Text" + EOL + "# Section1" + EOL + EOL; assertEquals(expected, getSinkContent(), "Wrong heading after inline element!"); } + + @Test + public void testCodeLink() { + try (final Sink sink = getSink()) { + sink.inline(Semantics.CODE); + sink.link("http://example.com"); + sink.text("label"); + sink.link_(); + sink.inline_(); + } + // heading must be on a new line + String expected = "[`label`](http://example\\.com)"; + assertEquals(expected, getSinkContent(), "Wrong link on code!"); + } }