This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch bugfix/emit-metadata-first
in repository https://gitbox.apache.org/repos/asf/maven-doxia.git

commit 36b1b7b9cadd5ac4e9ed10fc771c0f8b44b42fe0
Author: Konrad Windszus <[email protected]>
AuthorDate: Fri Mar 6 19:55:09 2026 +0100

    Make sure to emit metadata prior everything else
    
    Buffer everything until either body or head context is determined.
    Don't use init() from constructor to prevent inheritance related issues.
    Extract bufferStack into dedicated BufferingStackWriter.
    
    This closes #1041
---
 .../maven/doxia/sink/impl/Xhtml5BaseSink.java      |  22 +-
 .../module/markdown/BufferingStackWriter.java      | 102 +++++++
 ...ingWriter.java => LastTwoLinesAwareWriter.java} |   6 +-
 .../maven/doxia/module/markdown/MarkdownSink.java  | 321 ++++++++++-----------
 ...rTest.java => LastTwoLinesAwareWriterTest.java} |   8 +-
 .../doxia/module/markdown/MarkdownSinkTest.java    |  34 ++-
 6 files changed, 303 insertions(+), 190 deletions(-)

diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSink.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSink.java
index c4c66afa..14ff201f 100644
--- 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSink.java
+++ 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSink.java
@@ -135,9 +135,20 @@ public class Xhtml5BaseSink extends AbstractXmlSink 
implements HtmlMarkup {
         this.tableCaptionXMLWriterStack = new LinkedList<>();
         this.tableCaptionStack = new LinkedList<>();
 
-        init();
+        doInit();
     }
 
+    /**
+     * Called from constructor and from {@link #init()} to initialize certain 
instance fields.
+     */
+    private void doInit() {
+        this.headFlag = false;
+        this.paragraphFlag = false;
+        this.verbatimMode = VerbatimMode.OFF;
+
+        this.evenTableRow = true;
+        this.tableAttributes = null;
+    }
     // ----------------------------------------------------------------------
     // Accessor methods
     // ----------------------------------------------------------------------
@@ -246,12 +257,7 @@ public class Xhtml5BaseSink extends AbstractXmlSink 
implements HtmlMarkup {
         this.tableCaptionStack.clear();
         this.inlineStack.clear();
 
-        this.headFlag = false;
-        this.paragraphFlag = false;
-        this.verbatimMode = VerbatimMode.OFF;
-
-        this.evenTableRow = true;
-        this.tableAttributes = null;
+        doInit();
     }
 
     /**
@@ -943,7 +949,7 @@ public class Xhtml5BaseSink extends AbstractXmlSink 
implements HtmlMarkup {
     }
 
     /**
-     * The default style class is <code>bodyTable</code>.
+     * The default style class is <code>Table</code>.
      *
      * @param grid if {@code true} the style class {@code bodyTableBorder} 
will be added
      *
diff --git 
a/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/BufferingStackWriter.java
 
b/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/BufferingStackWriter.java
new file mode 100644
index 00000000..b4859465
--- /dev/null
+++ 
b/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/BufferingStackWriter.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.doxia.module.markdown;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Queue;
+
+public class BufferingStackWriter extends Writer {
+
+    /**
+     * A buffer stack that holds the output when the current context requires 
buffering.
+     * The content of this buffer is supposed to be already escaped.
+     */
+    private final Queue<StringBuilder> bufferStack;
+
+    private final Writer out;
+
+    public BufferingStackWriter(Writer out) {
+        this.out = out;
+        this.bufferStack = Collections.asLifoQueue(new LinkedList<>());
+    }
+
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        if (bufferStack.isEmpty()) {
+            out.write(cbuf, off, len);
+        } else {
+            bufferStack.element().append(cbuf, off, len);
+        }
+    }
+
+    /**
+     * Adds another buffer to the stack. The content of the current buffer is 
not affected, but the new content will
+     * be written to the new buffer until it is polled.
+     */
+    public void addBuffer() {
+        StringBuilder sb = new StringBuilder();
+        bufferStack.add(sb);
+    }
+
+    /**
+     * Retrieves the content of the current buffer without removing it from 
the stack.
+     * Also writing to the StringBuffer returned by this method will affect 
the content of the current buffer.
+     */
+    public StringBuilder getCurrentBuffer() {
+        return bufferStack.element();
+    }
+
+    /**
+     * Retrieves the content of the current buffer without removing it from 
the stack.
+     * In contrast to {@link #getCurrentBuffer()} the current buffer is 
cleared.
+     */
+    public String getAndClearCurrentBuffer() {
+        String buffer = bufferStack.remove().toString();
+        addBuffer();
+        return buffer;
+    }
+
+    /**
+     * Retrieves the content of the current buffer and removes it from the 
stack. Returns null if the stack is empty.
+     * @return the content of the current buffer, or null if the stack is empty
+     */
+    public void removeBuffer() {
+        bufferStack.remove();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        // do nothing
+        if (bufferStack.isEmpty()) {
+            out.flush();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (!bufferStack.isEmpty()) {
+            throw new IllegalStateException(
+                    "Cannot close BufferingStackWriter while there are still 
buffers in the stack.");
+        }
+        out.close();
+    }
+}
diff --git 
a/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/LastTwoLinesBufferingWriter.java
 
b/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/LastTwoLinesAwareWriter.java
similarity index 95%
rename from 
doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/LastTwoLinesBufferingWriter.java
rename to 
doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/LastTwoLinesAwareWriter.java
index bc2f0d79..1a109821 100644
--- 
a/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/LastTwoLinesBufferingWriter.java
+++ 
b/doxia-modules/doxia-module-markdown/src/main/java/org/apache/maven/doxia/module/markdown/LastTwoLinesAwareWriter.java
@@ -28,19 +28,19 @@ import org.apache.maven.doxia.util.DoxiaStringUtils;
  * Useful to collapse subsequent new lines or blank lines by evaluating {@link 
#isWriterAfterBlankLine()} and {@link #isWriterAfterBlankLine()}.
  * The buffering does not affect or defer delegation to the underlying writer, 
though.
  */
-public class LastTwoLinesBufferingWriter extends Writer {
+public class LastTwoLinesAwareWriter extends Writer {
 
     private final Writer out;
     private String previousLine;
     private StringBuilder currentLine;
     private final String lineSeparator;
 
-    public LastTwoLinesBufferingWriter(Writer out) {
+    public LastTwoLinesAwareWriter(Writer out) {
         // don't use System.lineSeparator, as overwritten in AbstractModuleTest
         this(out, System.getProperty("line.separator"));
     }
 
-    LastTwoLinesBufferingWriter(Writer out, String lineSeparator) {
+    LastTwoLinesAwareWriter(Writer out, String lineSeparator) {
         super();
         this.out = out;
         this.previousLine = "";
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 88c987a0..82bbf137 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
@@ -21,7 +21,6 @@ package org.apache.maven.doxia.module.markdown;
 import javax.swing.text.AttributeSet;
 import javax.swing.text.MutableAttributeSet;
 
-import java.io.PrintWriter;
 import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -29,7 +28,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.NoSuchElementException;
 import java.util.Queue;
 import java.util.stream.Collectors;
 
@@ -55,12 +53,6 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     // Instance fields
     // ----------------------------------------------------------------------
 
-    /**
-     * 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;
 
@@ -85,11 +77,11 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     /** is header row */
     private boolean isFirstTableRow;
 
-    /** The writer to use. */
-    private final PrintWriter writer;
+    /** The inner decorated writer to buffer the text of contexts requiring 
buffering. Writing to this and {@code bufferingWriter} has the same effect. */
+    private final BufferingStackWriter bufferingStackWriter;
 
-    /** A temporary writer used to buffer the last two lines */
-    private final LastTwoLinesBufferingWriter bufferingWriter;
+    /** The outer decorated writer taking care of remembering the last two 
written lines. Writing to this and {@code writer} has the same effect. */
+    private final LastTwoLinesAwareWriter lineAwareWriter;
 
     private static final String USE_XHTML_SINK = "XhtmlSink";
 
@@ -103,10 +95,16 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
 
     @FunctionalInterface
     interface TextEscapeFunction {
-        String escape(ElementContext context, LastTwoLinesBufferingWriter 
writer, String text);
+        String escape(ElementContext context, LastTwoLinesAwareWriter writer, 
String text);
     }
     /** Most important contextual metadata (of elements). This contains 
information about necessary escaping rules, potential prefixes and newlines */
     enum ElementContext {
+        ROOT_WITH_BUFFERING(
+                Type.GENERIC_CONTAINER,
+                ElementContext::escapeMarkdown,
+                true), // only needs buffering until head()_ is called to make 
sure to emit metadata first
+        ROOT_WITHOUT_BUFFERING(
+                Type.GENERIC_CONTAINER, null, false), // used after 
head()_/body() to prevent unnecessary buffering
         HEAD(Type.GENERIC_CONTAINER, null, true),
         BODY(Type.GENERIC_CONTAINER, ElementContext::escapeMarkdown),
         // only the elements, which affect rendering of children and are 
different from BODY or HEAD are listed here
@@ -117,7 +115,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
         TABLE_CAPTION(Type.INLINE, ElementContext::escapeMarkdown),
         TABLE_ROW(Type.CONTAINER_BLOCK, null, true),
         TABLE_CELL(
-                Type.LEAF_BLOCK,
+                Type.INLINE,
                 ElementContext::escapeForTableCell,
                 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
@@ -204,7 +202,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
          * @param text
          * @return the escaped text (may be same as {@code text} when no 
escaping is necessary)
          */
-        String escape(LastTwoLinesBufferingWriter writer, String text) {
+        String escape(LastTwoLinesAwareWriter writer, String text) {
             // is escaping necessary at all?
             if (escapeFunction == null) {
                 return text;
@@ -248,7 +246,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
          * @return the text escaped, "" if null String input
          * @see <a 
href="https://daringfireball.net/projects/markdown/syntax#backslash";>Backslash 
Escapes</a>
          */
-        private String escapeMarkdown(LastTwoLinesBufferingWriter writer, 
String text) {
+        private String escapeMarkdown(LastTwoLinesAwareWriter writer, String 
text) {
             if (text == null) {
                 return "";
             }
@@ -302,7 +300,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
             return buffer.toString();
         }
 
-        private static boolean isAfterDigit(StringBuilder buffer, 
LastTwoLinesBufferingWriter writer) {
+        private static boolean isAfterDigit(StringBuilder buffer, 
LastTwoLinesAwareWriter writer) {
             if (buffer.length() > 0) {
                 return Character.isDigit(buffer.charAt(buffer.length() - 1));
             } else {
@@ -310,7 +308,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
             }
         }
 
-        private static boolean isInBlankLine(StringBuilder buffer, 
LastTwoLinesBufferingWriter writer) {
+        private static boolean isInBlankLine(StringBuilder buffer, 
LastTwoLinesAwareWriter writer) {
             if (DoxiaStringUtils.isBlank(buffer.toString())) {
                 return writer.isInBlankLine();
             }
@@ -321,7 +319,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
             return "\\" + c;
         }
 
-        private String escapeHtml(LastTwoLinesBufferingWriter writer, String 
text) {
+        private String escapeHtml(LastTwoLinesAwareWriter writer, String text) 
{
             return HtmlTools.escapeHTML(text, true);
         }
 
@@ -332,7 +330,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
          * @return the escaped text
          * @see {@link #escapeMarkdown(String)
          */
-        private String escapeForTableCell(LastTwoLinesBufferingWriter writer, 
String text) {
+        private String escapeForTableCell(LastTwoLinesAwareWriter writer, 
String text) {
             return escapeMarkdown(writer, text).replace("|", "\\|");
         }
     }
@@ -341,8 +339,9 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     // ----------------------------------------------------------------------
 
     protected static MarkdownSink newInstance(Writer writer) {
-        LastTwoLinesBufferingWriter bufferingWriter = new 
LastTwoLinesBufferingWriter(writer);
-        return new MarkdownSink(bufferingWriter, new 
PrintWriter(bufferingWriter));
+        BufferingStackWriter bufferingStackWriter = new 
BufferingStackWriter(writer);
+        LastTwoLinesAwareWriter bufferingWriter = new 
LastTwoLinesAwareWriter(bufferingStackWriter);
+        return new MarkdownSink(bufferingWriter, bufferingStackWriter);
     }
 
     /**
@@ -350,12 +349,24 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
      *
      * @param writer not null writer to write the result. <b>Should</b> be an 
UTF-8 Writer.
      */
-    private MarkdownSink(LastTwoLinesBufferingWriter bufferingWriter, 
PrintWriter writer) {
-        super(writer);
-        this.bufferingWriter = bufferingWriter;
-        this.writer = writer;
+    private MarkdownSink(LastTwoLinesAwareWriter lineAwareWriter, 
BufferingStackWriter bufferingStackWriter) {
+        super(lineAwareWriter);
+        this.lineAwareWriter = lineAwareWriter;
+        this.bufferingStackWriter = bufferingStackWriter;
+        doInit();
+    }
 
-        init();
+    private void doInit() {
+        this.authors = new LinkedList<>();
+        this.title = null;
+        this.date = null;
+        this.linkName = null;
+        this.tableHeaderCellFlag = false;
+        this.cellCount = 0;
+        this.cellJustif = null;
+        this.elementContextStack = Collections.asLifoQueue(new LinkedList<>());
+        this.inlineStack = Collections.asLifoQueue(new LinkedList<>());
+        startContext(ElementContext.ROOT_WITH_BUFFERING);
     }
 
     private void endContext(ElementContext expectedContext) {
@@ -370,13 +381,13 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
         }
         if (removedContext.requiresBuffering) {
             // remove buffer from stack (assume it has been evaluated already)
-            bufferStack.remove();
+            bufferingStackWriter.removeBuffer();
         }
     }
 
     private void startContext(ElementContext newContext) {
         if (newContext.requiresBuffering) {
-            bufferStack.add(new StringBuilder());
+            bufferingStackWriter.addBuffer();
         }
         if (newContext.isBlock()) {
             // every block element within a list item must be surrounded by 
blank lines
@@ -387,14 +398,31 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
         elementContextStack.add(newContext);
     }
 
+    private String toogleToRootContextWithoutBuffering(boolean dumpBuffer) {
+        final String buffer;
+        if (elementContextStack.element() == 
ElementContext.ROOT_WITH_BUFFERING) {
+            buffer = bufferingStackWriter.getCurrentBuffer().toString();
+            endContext(ElementContext.ROOT_WITH_BUFFERING);
+            if (dumpBuffer) {
+                write(buffer);
+            }
+            startContext(ElementContext.ROOT_WITHOUT_BUFFERING);
+        } else if (elementContextStack.element() != 
ElementContext.ROOT_WITHOUT_BUFFERING) {
+            throw new IllegalStateException("Unexpected context " + 
elementContextStack.element()
+                    + ", expected ROOT_WITH_BUFFERING or 
ROOT_WITHOUT_BUFFERING");
+        } else {
+            buffer = "";
+        }
+        return buffer;
+    }
     /**
      * Ensures that the {@link #writer} is currently at the beginning of a new 
line.
      * Optionally writes a line separator to ensure that.
      */
     private void ensureBeginningOfLine() {
         // make sure that we are at the start of a line without adding 
unnecessary blank lines
-        if (!bufferingWriter.isWriterAtStartOfNewLine()) {
-            writeUnescaped(EOL);
+        if (!lineAwareWriter.isWriterAtStartOfNewLine()) {
+            write(EOL);
         }
     }
 
@@ -404,11 +432,11 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
      */
     private void ensureBlankLine() {
         // prevent duplicate blank lines
-        if (!bufferingWriter.isWriterAfterBlankLine()) {
-            if (bufferingWriter.isWriterAtStartOfNewLine()) {
-                writeUnescaped(EOL);
+        if (!lineAwareWriter.isWriterAfterBlankLine()) {
+            if (lineAwareWriter.isWriterAtStartOfNewLine()) {
+                write(EOL);
             } else {
-                writeUnescaped(BLANK_LINE);
+                write(BLANK_LINE);
             }
         }
     }
@@ -419,7 +447,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
         } else {
             ensureBeginningOfLine();
         }
-        writeUnescaped(getLinePrefix());
+        write(getLinePrefix());
     }
 
     private void endBlock(boolean requireBlankLine) {
@@ -445,83 +473,51 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                 .findFirst()
                 .isPresent();
     }
-    /**
-     * 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 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();
 
-        this.authors = new LinkedList<>();
-        this.title = null;
-        this.date = null;
-        this.linkName = null;
-        this.tableHeaderCellFlag = false;
-        this.cellCount = 0;
-        this.cellJustif = null;
-        this.elementContextStack = Collections.asLifoQueue(new LinkedList<>());
-        this.inlineStack = Collections.asLifoQueue(new LinkedList<>());
-        // always set a default context (at least for tests not emitting a 
body)
-        elementContextStack.add(ElementContext.BODY);
+        doInit();
     }
 
     @Override
     public void head(SinkEventAttributes attributes) {
-        init();
+        // init();
         // remove default body context here
-        endContext(ElementContext.BODY);
+        // endContext(ElementContext.BODY);
         startContext(ElementContext.HEAD);
     }
 
     @Override
     public void head_() {
         endContext(ElementContext.HEAD);
+        String priorHeadBuffer = toogleToRootContextWithoutBuffering(false);
         // only write head block if really necessary
         if (title == null && authors.isEmpty() && date == null) {
             return;
         }
-        writeUnescaped(METADATA_MARKUP + EOL);
+        write(METADATA_MARKUP + EOL);
         if (title != null) {
-            writeUnescaped("title: " + title + EOL);
+            write("title: " + title + EOL);
         }
         if (!authors.isEmpty()) {
-            writeUnescaped("author: " + EOL);
+            write("author: " + EOL);
             for (String author : authors) {
-                writeUnescaped("  - " + author + EOL);
+                write("  - " + author + EOL);
             }
         }
         if (date != null) {
-            writeUnescaped("date: " + date + EOL);
+            write("date: " + date + EOL);
         }
-        writeUnescaped(METADATA_MARKUP + BLANK_LINE);
+        write(METADATA_MARKUP + BLANK_LINE);
+        write(priorHeadBuffer);
     }
 
     @Override
     public void body(SinkEventAttributes attributes) {
+        toogleToRootContextWithoutBuffering(true);
         startContext(ElementContext.BODY);
-        elementContextStack.add(ElementContext.BODY);
     }
 
     @Override
@@ -531,25 +527,25 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
 
     @Override
     public void title_() {
-        String buffer = consumeBuffer();
-        if (buffer != null && !buffer.isEmpty()) {
-            this.title = buffer.toString();
+        String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
+        if (!buffer.isEmpty()) {
+            this.title = buffer;
         }
     }
 
     @Override
     public void author_() {
-        String buffer = consumeBuffer();
-        if (buffer != null && !buffer.isEmpty()) {
+        String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
+        if (!buffer.isEmpty()) {
             authors.add(buffer);
         }
     }
 
     @Override
     public void date_() {
-        String buffer = consumeBuffer();
-        if (buffer != null && !buffer.isEmpty()) {
-            date = buffer.toString();
+        String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
+        if (!buffer.isEmpty()) {
+            date = buffer;
         }
     }
 
@@ -577,7 +573,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     public void sectionTitle(int level, SinkEventAttributes attributes) {
         startContext(ElementContext.HEADING);
         if (level > 0) {
-            writeUnescaped(DoxiaStringUtils.repeat(SECTION_TITLE_START_MARKUP, 
level) + SPACE);
+            write(DoxiaStringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + 
SPACE);
         }
     }
 
@@ -605,7 +601,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     @Override
     public void listItem(SinkEventAttributes attributes) {
         startContext(ElementContext.LIST_ITEM);
-        writeUnescaped(LIST_UNORDERED_ITEM_START_MARKUP);
+        write(LIST_UNORDERED_ITEM_START_MARKUP);
     }
 
     @Override
@@ -633,7 +629,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     @Override
     public void numberedListItem(SinkEventAttributes attributes) {
         startContext(ElementContext.LIST_ITEM);
-        writeUnescaped(LIST_ORDERED_ITEM_START_MARKUP);
+        write(LIST_ORDERED_ITEM_START_MARKUP);
     }
 
     @Override
@@ -646,33 +642,33 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
         LOGGER.warn(
                 "{}Definition list not natively supported in Markdown, 
rendering HTML instead", getLocationLogPrefix());
         startContext(ElementContext.HTML_BLOCK);
-        writeUnescaped("<dl>" + EOL);
+        write("<dl>" + EOL);
     }
 
     @Override
     public void definitionList_() {
-        writeUnescaped("</dl>");
+        write("</dl>");
         endContext(ElementContext.HTML_BLOCK);
     }
 
     @Override
     public void definedTerm(SinkEventAttributes attributes) {
-        writeUnescaped("<dt>");
+        write("<dt>");
     }
 
     @Override
     public void definedTerm_() {
-        writeUnescaped("</dt>" + EOL);
+        write("</dt>" + EOL);
     }
 
     @Override
     public void definition(SinkEventAttributes attributes) {
-        writeUnescaped("<dd>");
+        write("<dd>");
     }
 
     @Override
     public void definition_() {
-        writeUnescaped("</dd>" + EOL);
+        write("</dd>" + EOL);
     }
 
     @Override
@@ -682,10 +678,10 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
 
     @Override
     public void paragraph(SinkEventAttributes attributes) {
-        ensureBlankLine();
         // ignore paragraphs outside container contexts
         if (elementContextStack.element().isContainer()) {
-            writeUnescaped(getLinePrefix());
+            ensureBlankLine();
+            write(getLinePrefix());
         }
     }
 
@@ -704,12 +700,12 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
         } else {
             // if no source attribute, then don't emit an info string
             startContext(ElementContext.CODE_BLOCK);
-            writeUnescaped(VERBATIM_START_MARKUP);
+            write(VERBATIM_START_MARKUP);
             if (attributes != null && 
attributes.containsAttributes(SinkEventAttributeSet.SOURCE)) {
-                writeUnescaped("unknown"); // unknown language
+                write("unknown"); // unknown language
             }
-            writeUnescaped(EOL);
-            writeUnescaped(getLinePrefix());
+            write(EOL);
+            write(getLinePrefix());
         }
     }
 
@@ -719,8 +715,8 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
             super.verbatim_();
         } else {
             ensureBeginningOfLine();
-            writeUnescaped(getLinePrefix());
-            writeUnescaped(VERBATIM_END_MARKUP + BLANK_LINE);
+            write(getLinePrefix());
+            write(VERBATIM_END_MARKUP + BLANK_LINE);
             endContext(ElementContext.CODE_BLOCK);
         }
     }
@@ -731,7 +727,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
             super.blockquote(attributes);
         } else {
             startContext(ElementContext.BLOCKQUOTE);
-            writeUnescaped(BLOCKQUOTE_START_MARKUP);
+            write(BLOCKQUOTE_START_MARKUP);
         }
     }
 
@@ -747,8 +743,8 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
     @Override
     public void horizontalRule(SinkEventAttributes attributes) {
         ensureBeginningOfLine();
-        writeUnescaped(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
-        writeUnescaped(getLinePrefix());
+        write(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
+        write(getLinePrefix());
     }
 
     @Override
@@ -757,7 +753,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
             super.table(attributes);
         } else {
             ensureBlankLine();
-            writeUnescaped(getLinePrefix());
+            write(getLinePrefix());
         }
     }
 
@@ -807,7 +803,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
         if (elementContextStack.element().isHtml()) {
             super.tableRow_();
         } else {
-            String buffer = consumeBuffer();
+            String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
             endContext(ElementContext.TABLE_ROW);
             if (isFirstTableRow && !tableHeaderCellFlag) {
                 // emit empty table header as this is mandatory for GFM table 
extension
@@ -818,9 +814,9 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
                 isFirstTableRow = false;
                 // afterwards emit the first row
             }
-            writeUnescaped(TABLE_ROW_PREFIX);
-            writeUnescaped(buffer);
-            writeUnescaped(EOL);
+            write(TABLE_ROW_PREFIX);
+            write(buffer);
+            write(EOL);
             if (isFirstTableRow) {
                 // emit delimiter row
                 writeTableDelimiterRow();
@@ -832,17 +828,17 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
     }
 
     private void writeEmptyTableHeader() {
-        writeUnescaped(TABLE_ROW_PREFIX);
+        write(TABLE_ROW_PREFIX);
         for (int i = 0; i < cellCount; i++) {
-            writeUnescaped(DoxiaStringUtils.repeat(String.valueOf(SPACE), 3) + 
TABLE_CELL_SEPARATOR_MARKUP);
+            write(DoxiaStringUtils.repeat(String.valueOf(SPACE), 3) + 
TABLE_CELL_SEPARATOR_MARKUP);
         }
-        writeUnescaped(EOL);
-        writeUnescaped(getLinePrefix());
+        write(EOL);
+        write(getLinePrefix());
     }
 
     /** Emit the delimiter row which determines the alignment */
     private void writeTableDelimiterRow() {
-        writeUnescaped(TABLE_ROW_PREFIX);
+        write(TABLE_ROW_PREFIX);
         int justification = Sink.JUSTIFY_DEFAULT;
         for (int i = 0; i < cellCount; i++) {
             // keep previous column's alignment in case too few are specified
@@ -851,21 +847,21 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
             }
             switch (justification) {
                 case Sink.JUSTIFY_RIGHT:
-                    writeUnescaped(TABLE_COL_RIGHT_ALIGNED_MARKUP);
+                    write(TABLE_COL_RIGHT_ALIGNED_MARKUP);
                     break;
                 case Sink.JUSTIFY_CENTER:
-                    writeUnescaped(TABLE_COL_CENTER_ALIGNED_MARKUP);
+                    write(TABLE_COL_CENTER_ALIGNED_MARKUP);
                     break;
                 case Sink.JUSTIFY_LEFT:
-                    writeUnescaped(TABLE_COL_LEFT_ALIGNED_MARKUP);
+                    write(TABLE_COL_LEFT_ALIGNED_MARKUP);
                     break;
                 default:
-                    writeUnescaped(TABLE_COL_DEFAULT_ALIGNED_MARKUP);
+                    write(TABLE_COL_DEFAULT_ALIGNED_MARKUP);
                     break;
             }
-            writeUnescaped(TABLE_CELL_SEPARATOR_MARKUP);
+            write(TABLE_CELL_SEPARATOR_MARKUP);
         }
-        writeUnescaped(EOL);
+        write(EOL);
     }
 
     @Override
@@ -936,7 +932,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
      */
     private void endTableCell() {
         endContext(ElementContext.TABLE_CELL);
-        writeUnescaped(TABLE_CELL_SEPARATOR_MARKUP);
+        write(TABLE_CELL_SEPARATOR_MARKUP);
         cellCount++;
     }
 
@@ -994,7 +990,7 @@ public class MarkdownSink extends Xhtml5BaseSink implements 
MarkdownMarkup {
                 if (alt == null) {
                     alt = "";
                 }
-                
writeImage(elementContextStack.element().escape(bufferingWriter, 
alt.toString()), name);
+                
writeImage(elementContextStack.element().escape(lineAwareWriter, 
alt.toString()), name);
             }
         }
     }
@@ -1004,27 +1000,23 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
         if (elementContextStack.element().isHtml()) {
             super.figure_();
         } else {
-            StringBuilder buffer = getCurrentBuffer();
-            String label = "";
-            if (buffer != null) {
-                label = buffer.toString();
-            }
+            String label = bufferingStackWriter.getCurrentBuffer().toString();
             endContext(ElementContext.FIGURE);
             writeImage(label, figureSrc);
         }
     }
 
     private void writeImage(String alt, String src) {
-        writeUnescaped("![");
-        writeUnescaped(alt);
-        writeUnescaped("](" + src + ")");
+        write("![");
+        write(alt);
+        write("](" + src + ")");
     }
 
     public void anchor(String name, SinkEventAttributes attributes) {
         super.anchor(name, attributes);
         if (!elementContextStack.element().isHtml()) {
             // close anchor tag immediately otherwise markdown would not be 
allowed afterwards
-            writeUnescaped("</a>");
+            write("</a>");
         }
     }
 
@@ -1045,10 +1037,10 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                 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);
+                bufferingStackWriter.getCurrentBuffer().insert(0, 
LINK_START_1_MARKUP);
                 linkName = name;
             } else {
-                writeUnescaped(LINK_START_1_MARKUP);
+                write(LINK_START_1_MARKUP);
                 linkName = name;
             }
         }
@@ -1071,7 +1063,7 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                 endMarkups.add(linkEndMarkup.toString());
                 inlineStack.add(endMarkups);
             } else {
-                writeUnescaped(LINK_START_2_MARKUP + linkName + 
LINK_END_MARKUP);
+                write(LINK_START_2_MARKUP + linkName + LINK_END_MARKUP);
             }
             linkName = null;
         }
@@ -1093,11 +1085,11 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                         || 
attributes.containsAttributes(SinkEventAttributeSet.Semantics.MONOSPACED)
                         || 
attributes.containsAttributes(SinkEventAttributeSet.MONOSPACED)) {
                     if (requiresHtml) {
-                        writeUnescaped("<code>");
+                        write("<code>");
                         endMarkups.add("</code>");
                     } else {
                         startContext(ElementContext.CODE_SPAN);
-                        writeUnescaped(MONOSPACED_START_MARKUP);
+                        write(MONOSPACED_START_MARKUP);
                         endMarkups.add(MONOSPACED_END_MARKUP);
                     }
                 } else {
@@ -1109,10 +1101,10 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                             SinkEventAttributeSet.Semantics.ITALIC,
                             SinkEventAttributeSet.ITALIC)) {
                         if (requiresHtml) {
-                            writeUnescaped("<em>");
+                            write("<em>");
                             endMarkups.add("</em>");
                         } else {
-                            writeUnescaped(ITALIC_START_MARKUP);
+                            write(ITALIC_START_MARKUP);
                             endMarkups.add(ITALIC_END_MARKUP);
                         }
                     }
@@ -1123,20 +1115,20 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                             SinkEventAttributeSet.Semantics.BOLD,
                             SinkEventAttributeSet.BOLD)) {
                         if (requiresHtml) {
-                            writeUnescaped("<strong>");
+                            write("<strong>");
                             endMarkups.add("</strong>");
                         } else {
-                            writeUnescaped(BOLD_START_MARKUP);
+                            write(BOLD_START_MARKUP);
                             endMarkups.add(BOLD_END_MARKUP);
                         }
                     }
                     // <del> is supported via GFM strikethrough extension
                     if (filterAttributes(remainingAttributes, 
SinkEventAttributeSet.Semantics.DELETE)) {
                         if (requiresHtml) {
-                            writeUnescaped("<del>");
+                            write("<del>");
                             endMarkups.add("</del>");
                         } else {
-                            writeUnescaped(STRIKETHROUGH_START_MARKUP);
+                            write(STRIKETHROUGH_START_MARKUP);
                             endMarkups.add(STRIKETHROUGH_END_MARKUP);
                         }
                     }
@@ -1170,11 +1162,11 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                 super.inline_();
             } else {
                 if (endMarkup.equals(MONOSPACED_END_MARKUP)) {
-                    String buffer = getCurrentBuffer().toString();
+                    String buffer = 
bufferingStackWriter.getCurrentBuffer().toString();
                     endContext(ElementContext.CODE_SPAN);
-                    writeUnescaped(buffer);
+                    write(buffer);
                 }
-                writeUnescaped(endMarkup);
+                write(endMarkup);
             }
         }
     }
@@ -1212,16 +1204,16 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
     @Override
     public void lineBreak(SinkEventAttributes attributes) {
         if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
-            writeUnescaped(EOL);
+            write(EOL);
         } else {
-            writeUnescaped("" + SPACE + SPACE + EOL);
+            write("" + SPACE + SPACE + EOL);
         }
-        writeUnescaped(getLinePrefix());
+        write(getLinePrefix());
     }
 
     @Override
     public void nonBreakingSpace() {
-        writeUnescaped(NON_BREAKING_SPACE_MARKUP);
+        write(NON_BREAKING_SPACE_MARKUP);
     }
 
     @Override
@@ -1237,7 +1229,7 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                 // table caption cannot even be emitted via XHTML in markdown 
as there is no suitable location
                 LOGGER.warn("{}Ignoring unsupported table caption in 
Markdown", getLocationLogPrefix());
             } else {
-                String unifiedText = currentContext.escape(bufferingWriter, 
unifyEOLs(text));
+                String unifiedText = currentContext.escape(lineAwareWriter, 
unifyEOLs(text));
                 // ignore newlines only, because those are emitted often 
coming from linebreaks in HTML with no
                 // semantical
                 // meaning
@@ -1247,7 +1239,7 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
                         unifiedText = unifiedText.replaceAll(EOL, EOL + 
prefix);
                     }
                 }
-                writeUnescaped(unifiedText);
+                write(unifiedText);
             }
             if (attributes != null) {
                 inline_();
@@ -1257,7 +1249,7 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
 
     @Override
     public void rawText(String text) {
-        writeUnescaped(text);
+        write(text);
     }
 
     /**
@@ -1271,23 +1263,10 @@ public class MarkdownSink extends Xhtml5BaseSink 
implements MarkdownMarkup {
         LOGGER.warn("{}Unknown Sink event '" + name + "', ignoring!", 
getLocationLogPrefix());
     }
 
-    protected void writeUnescaped(String text) {
-        StringBuilder buffer = bufferStack.peek();
-        if (buffer != null) {
-            buffer.append(text);
-        } else {
-            writer.write(text);
-        }
-    }
-
-    @Override
-    public void flush() {
-        writer.flush();
-    }
-
     @Override
     public void close() {
-        writer.close();
+        toogleToRootContextWithoutBuffering(true);
+        super.close();
 
         init();
     }
diff --git 
a/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/LastTwoLinesBufferingWriterTest.java
 
b/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/LastTwoLinesAwareWriterTest.java
similarity index 93%
rename from 
doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/LastTwoLinesBufferingWriterTest.java
rename to 
doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/LastTwoLinesAwareWriterTest.java
index be3f8e6b..c0e79378 100644
--- 
a/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/LastTwoLinesBufferingWriterTest.java
+++ 
b/doxia-modules/doxia-module-markdown/src/test/java/org/apache/maven/doxia/module/markdown/LastTwoLinesAwareWriterTest.java
@@ -27,13 +27,13 @@ import org.junit.jupiter.params.provider.ValueSource;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-class LastTwoLinesBufferingWriterTest {
+class LastTwoLinesAwareWriterTest {
 
-    private LastTwoLinesBufferingWriter writer;
+    private LastTwoLinesAwareWriter writer;
 
     @BeforeEach
     void setup() {
-        writer = new LastTwoLinesBufferingWriter(NullWriter.INSTANCE);
+        writer = new LastTwoLinesAwareWriter(NullWriter.INSTANCE);
     }
 
     @Test
@@ -91,7 +91,7 @@ class LastTwoLinesBufferingWriterTest {
     @ParameterizedTest()
     @ValueSource(strings = {"\n", "\r\n"})
     void differentLineSeparators(String lineSeparator) throws Exception {
-        writer = new LastTwoLinesBufferingWriter(NullWriter.INSTANCE, 
lineSeparator);
+        writer = new LastTwoLinesAwareWriter(NullWriter.INSTANCE, 
lineSeparator);
         writer.write("text" + lineSeparator);
         assertFalse(writer.isWriterAfterBlankLine());
         assertTrue(writer.isWriterAtStartOfNewLine());
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 95c06f95..1e88f084 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
@@ -59,15 +59,18 @@ class MarkdownSinkTest extends AbstractSinkTest {
     }
 
     protected String getTitleBlock(String title) {
-        return title;
+        // only written inside metadata section once head()_ is called
+        return "";
     }
 
     protected String getAuthorBlock(String author) {
-        return getEscapedText(author);
+        // only written inside metadata section once head()_ is called
+        return "";
     }
 
     protected String getDateBlock(String date) {
-        return date;
+        // only written inside metadata section once head()_ is called
+        return "";
     }
 
     protected String getHeadBlock() {
@@ -300,7 +303,7 @@ class MarkdownSinkTest extends AbstractSinkTest {
      * @return the text with all special characters escaped
      */
     private String getEscapedText(String text) {
-        return MarkdownSink.ElementContext.BODY.escape(new 
LastTwoLinesBufferingWriter(new StringWriter()), text);
+        return MarkdownSink.ElementContext.BODY.escape(new 
LastTwoLinesAwareWriter(new StringWriter()), text);
     }
 
     @Override
@@ -595,4 +598,27 @@ class MarkdownSinkTest extends AbstractSinkTest {
                 + EOL;
         assertEquals(expected, getSinkContent());
     }
+
+    @Test
+    void commentPriorHead() {
+        try (Sink sink = getSink()) {
+            sink.comment("This is a comment");
+            sink.head();
+            sink.title();
+            sink.text("Title");
+            sink.title_();
+            sink.author();
+            sink.text("author1");
+            sink.author_();
+            sink.head_();
+        }
+        String expected = "---" + EOL
+                + "title: Title" + EOL
+                + "author: " + EOL
+                + "  - author1" + EOL
+                + "---" + EOL
+                + EOL
+                + "<!--This is a comment-->";
+        assertEquals(expected, getSinkContent(), "Wrong metadata section");
+    }
 }


Reply via email to