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!");
+    }
 }

Reply via email to