You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/03 22:51:31 UTC

(maven-doxia) 01/01: Add HtmlTag, a Swing-free tag type for the sink writers

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

slachiewicz pushed a commit to branch doxia-1092-htmltag
in repository https://gitbox.apache.org/repos/asf/maven-doxia.git

commit c2fa8d9f7480038d0810b848458065fa7dd63e91
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Sep 4 00:51:24 2026 +0200

    Add HtmlTag, a Swing-free tag type for the sink writers
    
    The writers read exactly one thing from javax.swing HTML.Tag: isBlock(), which
    decides whether a start tag is preceded by a newline. HtmlTag carries the name
    and that flag, with the values the HtmlMarkup constants report today copied
    unchanged, so no generated site's whitespace moves; several of them are wrong for
    HTML5 and correcting them is a separate change with its own site comparison.
    
    The Tag-typed writeStartTag/writeEndTag/writeSimpleTag overloads are deprecated
    and delegate, keeping a tag Doxia does not declare on its own block value.
    Step 3 of #1092.
---
 .../org/apache/maven/doxia/markup/HtmlTag.java     | 234 +++++++++++++++++++++
 .../maven/doxia/sink/impl/AbstractXmlSink.java     | 119 ++++++++++-
 .../org/apache/maven/doxia/markup/HtmlTagTest.java |  76 +++++++
 .../maven/doxia/sink/impl/AbstractXmlSinkTest.java |  44 +++-
 4 files changed, 467 insertions(+), 6 deletions(-)

diff --git a/doxia-core/src/main/java/org/apache/maven/doxia/markup/HtmlTag.java b/doxia-core/src/main/java/org/apache/maven/doxia/markup/HtmlTag.java
new file mode 100644
index 00000000..dd906c36
--- /dev/null
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/markup/HtmlTag.java
@@ -0,0 +1,234 @@
+/*
+ * 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.markup;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * An HTML tag name together with the one property Doxia reads from it: whether a start tag may be
+ * preceded by a line break. It replaces {@code javax.swing.text.html.HTML.Tag} in the sink API,
+ * which carries an HTML 4 table Doxia has outgrown.
+ * <p>
+ * The block values of the {@linkplain #valueOf(String) known tags} are those the {@link HtmlMarkup}
+ * constants report today, copied unchanged so that no generated site's whitespace moves. Several of
+ * them are wrong for HTML5 &#x2014; {@code section}, {@code article} and {@code figure} are not
+ * treated as block, while {@code menu} and {@code title} are &#x2014; and correcting them is a
+ * separate change with its own before-and-after site comparison.
+ * </p>
+ *
+ * @since 2.2.0
+ */
+public final class HtmlTag {
+
+    private static final Map<String, HtmlTag> KNOWN_TAGS = knownTags();
+
+    private final String name;
+
+    private final boolean block;
+
+    /**
+     * Constructs a tag.
+     *
+     * @param name the tag name, not null.
+     * @param block whether a start tag may be preceded by a line break.
+     */
+    public HtmlTag(String name, boolean block) {
+        this.name = Objects.requireNonNull(name, "name cannot be null");
+        this.block = block;
+    }
+
+    /**
+     * Returns the tag of the given name known to Doxia.
+     *
+     * @param name a tag name, not null.
+     * @return the tag, or null if Doxia declares no tag of that name.
+     */
+    public static HtmlTag valueOf(String name) {
+        return KNOWN_TAGS.get(Objects.requireNonNull(name, "name cannot be null"));
+    }
+
+    /**
+     * Returns the tag name.
+     *
+     * @return the tag name, never null.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns whether a start tag may be preceded by a line break.
+     *
+     * @return true if the tag is a block-level tag.
+     */
+    public boolean isBlock() {
+        return block;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof HtmlTag)) {
+            return false;
+        }
+        HtmlTag that = (HtmlTag) other;
+        return block == that.block && name.equals(that.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, block);
+    }
+
+    private static void register(Map<String, HtmlTag> tags, String name, boolean block) {
+        tags.put(name, new HtmlTag(name, block));
+    }
+
+    private static Map<String, HtmlTag> knownTags() {
+        Map<String, HtmlTag> tags = new LinkedHashMap<>();
+        register(tags, "a", false);
+        register(tags, "abbr", false);
+        register(tags, "address", false);
+        register(tags, "area", false);
+        register(tags, "article", false);
+        register(tags, "aside", false);
+        register(tags, "audio", false);
+        register(tags, "b", false);
+        register(tags, "base", false);
+        register(tags, "bdi", false);
+        register(tags, "bdo", false);
+        register(tags, "blockquote", true);
+        register(tags, "body", true);
+        register(tags, "br", false);
+        register(tags, "button", false);
+        register(tags, "canvas", false);
+        register(tags, "caption", false);
+        register(tags, "cite", false);
+        register(tags, "code", false);
+        register(tags, "col", false);
+        register(tags, "colgroup", false);
+        register(tags, "command", false);
+        register(tags, "data", false);
+        register(tags, "datalist", false);
+        register(tags, "dd", true);
+        register(tags, "del", false);
+        register(tags, "details", false);
+        register(tags, "dfn", false);
+        register(tags, "dialog", false);
+        register(tags, "div", true);
+        register(tags, "dl", true);
+        register(tags, "dt", true);
+        register(tags, "em", false);
+        register(tags, "embed", false);
+        register(tags, "fieldset", false);
+        register(tags, "figcaption", false);
+        register(tags, "figure", false);
+        register(tags, "footer", false);
+        register(tags, "form", false);
+        register(tags, "h1", true);
+        register(tags, "h2", true);
+        register(tags, "h3", true);
+        register(tags, "h4", true);
+        register(tags, "h5", true);
+        register(tags, "h6", true);
+        register(tags, "head", true);
+        register(tags, "header", false);
+        register(tags, "hgroup", false);
+        register(tags, "hr", false);
+        register(tags, "html", false);
+        register(tags, "i", false);
+        register(tags, "iframe", false);
+        register(tags, "img", false);
+        register(tags, "input", false);
+        register(tags, "ins", false);
+        register(tags, "kbd", false);
+        register(tags, "keygen", false);
+        register(tags, "label", false);
+        register(tags, "legend", false);
+        register(tags, "li", true);
+        register(tags, "link", false);
+        register(tags, "main", false);
+        register(tags, "map", false);
+        register(tags, "mark", false);
+        register(tags, "menu", true);
+        register(tags, "menuitem", false);
+        register(tags, "meta", false);
+        register(tags, "meter", false);
+        register(tags, "nav", false);
+        register(tags, "noscript", false);
+        register(tags, "object", false);
+        register(tags, "ol", true);
+        register(tags, "optgroup", false);
+        register(tags, "option", false);
+        register(tags, "output", false);
+        register(tags, "p", true);
+        register(tags, "param", false);
+        register(tags, "picture", false);
+        register(tags, "pre", true);
+        register(tags, "progress", false);
+        register(tags, "q", false);
+        register(tags, "rb", false);
+        register(tags, "rp", false);
+        register(tags, "rt", false);
+        register(tags, "rtc", false);
+        register(tags, "ruby", false);
+        register(tags, "s", false);
+        register(tags, "samp", false);
+        register(tags, "script", false);
+        register(tags, "section", false);
+        register(tags, "select", false);
+        register(tags, "small", false);
+        register(tags, "source", false);
+        register(tags, "span", false);
+        register(tags, "strong", false);
+        register(tags, "style", false);
+        register(tags, "sub", false);
+        register(tags, "summary", false);
+        register(tags, "sup", false);
+        register(tags, "svg", false);
+        register(tags, "table", true);
+        register(tags, "tbody", false);
+        register(tags, "td", true);
+        register(tags, "template", false);
+        register(tags, "textarea", false);
+        register(tags, "tfoot", false);
+        register(tags, "th", true);
+        register(tags, "thead", false);
+        register(tags, "time", false);
+        register(tags, "title", true);
+        register(tags, "tr", true);
+        register(tags, "track", false);
+        register(tags, "u", false);
+        register(tags, "ul", true);
+        register(tags, "var", false);
+        register(tags, "video", false);
+        register(tags, "wbr", false);
+        return Collections.unmodifiableMap(tags);
+    }
+}
diff --git a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/AbstractXmlSink.java b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/AbstractXmlSink.java
index cc67ce52..15529f5b 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/AbstractXmlSink.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/AbstractXmlSink.java
@@ -23,6 +23,7 @@ import javax.swing.text.html.HTML.Tag;
 
 import java.util.Objects;
 
+import org.apache.maven.doxia.markup.HtmlTag;
 import org.apache.maven.doxia.markup.XmlMarkup;
 
 /**
@@ -75,8 +76,10 @@ public abstract class AbstractXmlSink extends SinkAdapter implements XmlMarkup {
      * </pre>
      *
      * @param t a non null tag
-     * @see #writeStartTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet)
+     * @see #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet)
+     * @deprecated use {@link #writeStartTag(HtmlTag)}; the {@code javax.swing} tag type goes in 2.3.0.
      */
+    @Deprecated
     protected void writeStartTag(Tag t) {
         writeStartTag(t, null);
     }
@@ -89,8 +92,11 @@ public abstract class AbstractXmlSink extends SinkAdapter implements XmlMarkup {
      *
      * @param t a non null tag.
      * @param att a set of attributes. May be null.
-     * @see #writeStartTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet, boolean)
+     * @see #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet, boolean)
+     * @deprecated use {@link #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet)}; the
+     *      {@code javax.swing} tag type goes in 2.3.0.
      */
+    @Deprecated
     protected void writeStartTag(Tag t, MutableAttributeSet att) {
         writeStartTag(t, att, false);
     }
@@ -104,8 +110,55 @@ public abstract class AbstractXmlSink extends SinkAdapter implements XmlMarkup {
      * @param t a non null tag.
      * @param att a set of attributes. May be null.
      * @param isEmptyElement boolean to write as tag for an <a href="https://www.w3.org/TR/xml/#d0e2480">empty element</a>.
+     * @deprecated use {@link #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet, boolean)}; the
+     *      {@code javax.swing} tag type goes in 2.3.0.
      */
+    @Deprecated
     protected void writeStartTag(Tag t, MutableAttributeSet att, boolean isEmptyElement) {
+        writeStartTag(toHtmlTag(t), att, isEmptyElement);
+    }
+
+    /**
+     * Starts a Tag. For instance:
+     * <pre>
+     * &lt;tag&gt;
+     * </pre>
+     *
+     * @param t a non null tag
+     * @see #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet)
+     * @since 2.2.0
+     */
+    protected void writeStartTag(HtmlTag t) {
+        writeStartTag(t, null);
+    }
+
+    /**
+     * Starts a Tag with attributes. For instance:
+     * <pre>
+     * &lt;tag attName="attValue"&gt;
+     * </pre>
+     *
+     * @param t a non null tag.
+     * @param att a set of attributes. May be null.
+     * @see #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet, boolean)
+     * @since 2.2.0
+     */
+    protected void writeStartTag(HtmlTag t, MutableAttributeSet att) {
+        writeStartTag(t, att, false);
+    }
+
+    /**
+     * Starts a Tag with attributes. For instance:
+     * <pre>
+     * &lt;tag attName="attValue"&gt;
+     * </pre>
+     *
+     * @param t a non null tag.
+     * @param att a set of attributes. May be null.
+     * @param isEmptyElement boolean to write as tag for an <a href="https://www.w3.org/TR/xml/#d0e2480">empty element</a>.
+     * @since 2.2.0
+     */
+    protected void writeStartTag(HtmlTag t, MutableAttributeSet att, boolean isEmptyElement) {
         Objects.requireNonNull(t, "t cannot be null");
 
         StringBuilder sb = new StringBuilder();
@@ -147,8 +200,20 @@ public abstract class AbstractXmlSink extends SinkAdapter implements XmlMarkup {
      * Ends a Tag without writing an EOL. For instance: <pre>&lt;/tag&gt;</pre>.
      *
      * @param t a tag.
+     * @deprecated use {@link #writeEndTag(HtmlTag)}; the {@code javax.swing} tag type goes in 2.3.0.
      */
+    @Deprecated
     protected void writeEndTag(Tag t) {
+        writeEndTag(toHtmlTag(t));
+    }
+
+    /**
+     * Ends a Tag without writing an EOL. For instance: <pre>&lt;/tag&gt;</pre>.
+     *
+     * @param t a tag.
+     * @since 2.2.0
+     */
+    protected void writeEndTag(HtmlTag t) {
         Objects.requireNonNull(t, "t cannot be null");
 
         StringBuilder sb = new StringBuilder();
@@ -172,8 +237,10 @@ public abstract class AbstractXmlSink extends SinkAdapter implements XmlMarkup {
      * </pre>
      *
      * @param t a non null tag
-     * @see #writeSimpleTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet)
+     * @see #writeSimpleTag(HtmlTag, javax.swing.text.MutableAttributeSet)
+     * @deprecated use {@link #writeSimpleTag(HtmlTag)}; the {@code javax.swing} tag type goes in 2.3.0.
      */
+    @Deprecated
     protected void writeSimpleTag(Tag t) {
         writeSimpleTag(t, null);
     }
@@ -186,12 +253,56 @@ public abstract class AbstractXmlSink extends SinkAdapter implements XmlMarkup {
      *
      * @param t a non null tag.
      * @param att a set of attributes. May be null.
-     * @see #writeStartTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet, boolean)
+     * @see #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet, boolean)
+     * @deprecated use {@link #writeSimpleTag(HtmlTag, javax.swing.text.MutableAttributeSet)}; the
+     *      {@code javax.swing} tag type goes in 2.3.0.
      */
+    @Deprecated
     protected void writeSimpleTag(Tag t, MutableAttributeSet att) {
         writeStartTag(t, att, true);
     }
 
+    /**
+     * Starts a simple Tag. For instance:
+     * <pre>
+     * &lt;tag /&gt;
+     * </pre>
+     *
+     * @param t a non null tag
+     * @see #writeSimpleTag(HtmlTag, javax.swing.text.MutableAttributeSet)
+     * @since 2.2.0
+     */
+    protected void writeSimpleTag(HtmlTag t) {
+        writeSimpleTag(t, null);
+    }
+
+    /**
+     * Starts a simple Tag with attributes. For instance:
+     * <pre>
+     * &lt;tag attName="attValue" /&gt;
+     * </pre>
+     *
+     * @param t a non null tag.
+     * @param att a set of attributes. May be null.
+     * @see #writeStartTag(HtmlTag, javax.swing.text.MutableAttributeSet, boolean)
+     * @since 2.2.0
+     */
+    protected void writeSimpleTag(HtmlTag t, MutableAttributeSet att) {
+        writeStartTag(t, att, true);
+    }
+
+    /**
+     * Returns the Doxia tag of the same name, falling back to a tag that answers exactly as the
+     * given one does, so that a tag Doxia does not declare keeps its own block value.
+     */
+    private static HtmlTag toHtmlTag(Tag t) {
+        Objects.requireNonNull(t, "t cannot be null");
+
+        HtmlTag known = HtmlTag.valueOf(t.toString());
+
+        return known != null ? known : new HtmlTag(t.toString(), t.isBlock());
+    }
+
     /**
      * Write a text to the sink.
      *
diff --git a/doxia-core/src/test/java/org/apache/maven/doxia/markup/HtmlTagTest.java b/doxia-core/src/test/java/org/apache/maven/doxia/markup/HtmlTagTest.java
new file mode 100644
index 00000000..a7828d7e
--- /dev/null
+++ b/doxia-core/src/test/java/org/apache/maven/doxia/markup/HtmlTagTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.markup;
+
+import javax.swing.text.html.HTML.Tag;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests {@link HtmlTag} against the {@link HtmlMarkup} constants it will replace.
+ */
+class HtmlTagTest {
+
+    /**
+     * Every tag Doxia declares must be known to HtmlTag and must answer isBlock() exactly as it
+     * does today. A tag whose block value moves changes the whitespace of every generated site.
+     */
+    @Test
+    void knownTagsAnswerAsHtmlMarkupDoes() throws IllegalAccessException {
+        List<Field> tagFields = new ArrayList<>();
+
+        for (Field field : HtmlMarkup.class.getDeclaredFields()) {
+            if (field.getType() == Tag.class) {
+                tagFields.add(field);
+            }
+        }
+
+        assertTrue(tagFields.size() > 100, "expected HtmlMarkup to declare the HTML5 tags");
+
+        for (Field field : tagFields) {
+            Tag tag = (Tag) field.get(null);
+            HtmlTag htmlTag = HtmlTag.valueOf(tag.toString());
+
+            assertNotNull(htmlTag, "HtmlTag does not know " + field.getName());
+            assertEquals(tag.toString(), htmlTag.getName());
+            assertEquals(tag.isBlock(), htmlTag.isBlock(), "block value moved for " + field.getName());
+        }
+    }
+
+    @Test
+    void unknownTagName() {
+        assertNull(HtmlTag.valueOf("no-such-tag"));
+    }
+
+    @Test
+    void equality() {
+        assertEquals(HtmlTag.valueOf("p"), new HtmlTag("p", true));
+        assertEquals(HtmlTag.valueOf("p").hashCode(), new HtmlTag("p", true).hashCode());
+        assertEquals("p", HtmlTag.valueOf("p").toString());
+    }
+}
diff --git a/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/AbstractXmlSinkTest.java b/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/AbstractXmlSinkTest.java
index 909eef8f..8f196c30 100644
--- a/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/AbstractXmlSinkTest.java
+++ b/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/AbstractXmlSinkTest.java
@@ -20,6 +20,8 @@ package org.apache.maven.doxia.sink.impl;
 
 import javax.swing.text.html.HTML.Tag;
 
+import org.apache.maven.doxia.markup.HtmlMarkup;
+import org.apache.maven.doxia.markup.HtmlTag;
 import org.apache.maven.doxia.markup.Markup;
 import org.apache.maven.doxia.sink.SinkEventAttributeSet;
 import org.apache.maven.doxia.sink.SinkEventAttributes;
@@ -27,6 +29,7 @@ import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.fail;
 
 /**
@@ -62,14 +65,14 @@ class AbstractXmlSinkTest {
         assertEquals(ns, instance.getNameSpace());
 
         try {
-            instance.writeStartTag(null);
+            instance.writeStartTag((HtmlTag) null);
             fail("null tag should fail!");
         } catch (NullPointerException e) {
             assertNotNull(e);
         }
 
         try {
-            instance.writeEndTag(null);
+            instance.writeEndTag((HtmlTag) null);
             fail("null tag should fail!");
         } catch (NullPointerException e) {
             assertNotNull(e);
@@ -98,6 +101,43 @@ class AbstractXmlSinkTest {
         assertEquals("<a style=\"bold\" />", instance.getText());
     }
 
+    /**
+     * The HtmlTag overloads must write exactly what the deprecated Tag ones write.
+     */
+    @Test
+    void htmlTagOverloadsMatchTagOverloads() {
+        final Tag t = Tag.A;
+        final HtmlTag htmlTag = HtmlTag.valueOf("a");
+        final SinkEventAttributes att = new SinkEventAttributeSet(SinkEventAttributeSet.BOLD);
+        final XmlTestSink instance = new XmlTestSink();
+
+        instance.writeStartTag(t, att, true);
+        final String fromTag = instance.getText();
+
+        instance.writeStartTag(htmlTag, att, true);
+        assertEquals(fromTag, instance.getText());
+
+        instance.writeSimpleTag(htmlTag);
+        assertEquals("<a />", instance.getText());
+
+        instance.writeEndTag(htmlTag);
+        assertEquals("</a>", instance.getText());
+    }
+
+    /**
+     * A tag Doxia does not declare keeps the block value of the tag it was written with.
+     */
+    @Test
+    void unknownTagKeepsItsOwnBlockValue() {
+        final XmlTestSink instance = new XmlTestSink();
+
+        assertNull(HtmlTag.valueOf("dir"), "dir is expected to be unknown to Doxia");
+
+        instance.writeStartTag(HtmlMarkup.P);
+        instance.writeStartTag(Tag.DIR);
+        assertEquals("<p>" + Markup.EOL + "<dir>", instance.getText());
+    }
+
     /**
      * Test of writeEOL method, of class AbstractXmlSink.
      */