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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5722f35a Convert non-HTML5-compliant inline attributes to compliant 
ones
5722f35a is described below

commit 5722f35a281b38e8a18d4a82bffb14d0a3c46078
Author: Konrad Windszus <[email protected]>
AuthorDate: Mon Feb 16 12:23:17 2026 +0100

    Convert non-HTML5-compliant inline attributes to compliant ones
    
    Add method to SinkEventAttributes to ease traversing in order without
    additional lookup.
    
    This closes #1034
---
 .../doxia/sink/impl/SinkEventAttributeSet.java     |   9 +
 .../apache/maven/doxia/sink/impl/SinkUtils.java    |  10 +-
 .../maven/doxia/sink/impl/Xhtml5BaseSink.java      | 233 +++++++++++++++++----
 .../maven/doxia/sink/impl/Xhtml5BaseSinkTest.java  |  48 ++++-
 .../maven/doxia/sink/SinkEventAttributes.java      |  22 ++
 5 files changed, 274 insertions(+), 48 deletions(-)

diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkEventAttributeSet.java
 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkEventAttributeSet.java
index 21ba81b4..d135ed39 100644
--- 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkEventAttributeSet.java
+++ 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkEventAttributeSet.java
@@ -24,6 +24,8 @@ import java.util.Collections;
 import java.util.Enumeration;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 
 import org.apache.maven.doxia.sink.SinkEventAttributes;
 
@@ -128,6 +130,8 @@ public class SinkEventAttributeSet implements 
SinkEventAttributes, Cloneable {
     /**
      * Constructs a new SinkEventAttributeSet with the attribute name-value
      * mappings as given by the specified String array.
+     * Each even index of the array is an attribute name, and the following 
odd index is the corresponding attribute value.
+     * This constructor only supports String attribute values.
      *
      * @param attributes the specified String array. If the length of this 
array
      * is not an even number, an IllegalArgumentException is thrown.
@@ -323,6 +327,11 @@ public class SinkEventAttributeSet implements 
SinkEventAttributes, Cloneable {
         this.resolveParent = parent;
     }
 
+    @Override
+    public Set<Entry<String, Object>> entrySet() {
+        return attribs.entrySet();
+    }
+
     @Override
     public Object clone() {
         SinkEventAttributeSet attr = new SinkEventAttributeSet(attribs.size());
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkUtils.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkUtils.java
index 1a47808c..408a5b5a 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkUtils.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/SinkUtils.java
@@ -192,7 +192,7 @@ public class SinkUtils {
         return sb.toString();
     }
 
-    private static String asCssString(AttributeSet att) {
+    public static String asCssString(AttributeSet att) {
         StringBuilder sb = new StringBuilder();
 
         Enumeration<?> names = att.getAttributeNames();
@@ -203,10 +203,7 @@ public class SinkUtils {
 
             // don't go recursive
             if (!(value instanceof AttributeSet)) {
-                sb.append(key.toString())
-                        .append(Markup.COLON)
-                        .append(Markup.SPACE)
-                        .append(value.toString());
+                sb.append(asCssDeclaration(key.toString(), value.toString()));
 
                 if (names.hasMoreElements()) {
                     sb.append(Markup.SEMICOLON).append(Markup.SPACE);
@@ -217,6 +214,9 @@ public class SinkUtils {
         return sb.toString();
     }
 
+    public static String asCssDeclaration(String property, String value) {
+        return property + Markup.COLON + Markup.SPACE + value;
+    }
     /**
      * Filters the given AttributeSet.
      * Removes all attributes whose name (key) is not contained in the sorted 
array valids.
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 27549f5c..bc514ccf 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
@@ -18,17 +18,19 @@
  */
 package org.apache.maven.doxia.sink.impl;
 
+import javax.swing.text.AttributeSet;
 import javax.swing.text.MutableAttributeSet;
 import javax.swing.text.html.HTML.Tag;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.Writer;
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EmptyStackException;
 import java.util.Enumeration;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Stack;
 import java.util.regex.Pattern;
@@ -1245,52 +1247,199 @@ public class Xhtml5BaseSink extends AbstractXmlSink 
implements HtmlMarkup {
         }
     }
 
-    private void inlineSemantics(SinkEventAttributes attributes, String 
semantic, List<Tag> tags, Tag tag) {
-        if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, 
semantic)) {
-            SinkEventAttributes attributesNoSemantics = (SinkEventAttributes) 
attributes.copyAttributes();
-            
attributesNoSemantics.removeAttribute(SinkEventAttributes.SEMANTICS);
-            writeStartTag(tag, attributesNoSemantics);
-            tags.add(0, tag);
-        }
-    }
-
     @Override
     public void inline(SinkEventAttributes attributes) {
         if (!headFlag) {
-            List<Tag> tags = new ArrayList<>();
-
-            if (attributes != null) {
-                inlineSemantics(attributes, "emphasis", tags, HtmlMarkup.EM);
-                inlineSemantics(attributes, "strong", tags, HtmlMarkup.STRONG);
-                inlineSemantics(attributes, "small", tags, HtmlMarkup.SMALL);
-                inlineSemantics(attributes, "line-through", tags, 
HtmlMarkup.S);
-                inlineSemantics(attributes, "citation", tags, HtmlMarkup.CITE);
-                inlineSemantics(attributes, "quote", tags, HtmlMarkup.Q);
-                inlineSemantics(attributes, "definition", tags, 
HtmlMarkup.DFN);
-                inlineSemantics(attributes, "abbreviation", tags, 
HtmlMarkup.ABBR);
-                inlineSemantics(attributes, "italic", tags, HtmlMarkup.I);
-                inlineSemantics(attributes, "bold", tags, HtmlMarkup.B);
-                inlineSemantics(attributes, "code", tags, HtmlMarkup.CODE);
-                inlineSemantics(attributes, "variable", tags, HtmlMarkup.VAR);
-                inlineSemantics(attributes, "sample", tags, HtmlMarkup.SAMP);
-                inlineSemantics(attributes, "keyboard", tags, HtmlMarkup.KBD);
-                inlineSemantics(attributes, "superscript", tags, 
HtmlMarkup.SUP);
-                inlineSemantics(attributes, "subscript", tags, HtmlMarkup.SUB);
-                inlineSemantics(attributes, "annotation", tags, HtmlMarkup.U);
-                inlineSemantics(attributes, "highlight", tags, 
HtmlMarkup.MARK);
-                inlineSemantics(attributes, "ruby", tags, HtmlMarkup.RUBY);
-                inlineSemantics(attributes, "rubyBase", tags, HtmlMarkup.RB);
-                inlineSemantics(attributes, "rubyText", tags, HtmlMarkup.RT);
-                inlineSemantics(attributes, "rubyTextContainer", tags, 
HtmlMarkup.RTC);
-                inlineSemantics(attributes, "rubyParentheses", tags, 
HtmlMarkup.RP);
-                inlineSemantics(attributes, "bidirectionalIsolation", tags, 
HtmlMarkup.BDI);
-                inlineSemantics(attributes, "bidirectionalOverride", tags, 
HtmlMarkup.BDO);
-                inlineSemantics(attributes, "phrase", tags, HtmlMarkup.SPAN);
-                inlineSemantics(attributes, "insert", tags, HtmlMarkup.INS);
-                inlineSemantics(attributes, "delete", tags, HtmlMarkup.DEL);
+            if (attributes != null && !attributes.entrySet().isEmpty()) {
+                SinkEventAttributes compliantAttributes = 
convertToHtml5CompliantAttributes(attributes);
+                Tag tag = HtmlMarkup.SPAN;
+                // iterates in insertion order
+                for (Map.Entry<String, Object> attribute : 
compliantAttributes.entrySet()) {
+                    if 
(SinkEventAttributes.SEMANTICS.equals(attribute.getKey())) {
+                        switch (attribute.getValue().toString()) {
+                            case "emphasis":
+                                tag = HtmlMarkup.EM;
+                                break;
+                            case "strong":
+                                tag = HtmlMarkup.STRONG;
+                                break;
+                            case "small":
+                                tag = HtmlMarkup.SMALL;
+                                break;
+                            case "line-through":
+                                tag = HtmlMarkup.S;
+                                break;
+                            case "citation":
+                                tag = HtmlMarkup.CITE;
+                                break;
+                            case "quote":
+                                tag = HtmlMarkup.Q;
+                                break;
+                            case "definition":
+                                tag = HtmlMarkup.DFN;
+                                break;
+                            case "abbreviation":
+                                tag = HtmlMarkup.ABBR;
+                                break;
+                            case "italic":
+                                tag = HtmlMarkup.I;
+                                break;
+                            case "bold":
+                                tag = HtmlMarkup.B;
+                                break;
+                            case "code":
+                                tag = HtmlMarkup.CODE;
+                                break;
+                            case "variable":
+                                tag = HtmlMarkup.VAR;
+                                break;
+                            case "sample":
+                                tag = HtmlMarkup.SAMP;
+                                break;
+                            case "keyboard":
+                                tag = HtmlMarkup.KBD;
+                                break;
+                            case "superscript":
+                                tag = HtmlMarkup.SUP;
+                                break;
+                            case "subscript":
+                                tag = HtmlMarkup.SUB;
+                                break;
+                            case "annotation":
+                                tag = HtmlMarkup.U;
+                                break;
+                            case "highlight":
+                                tag = HtmlMarkup.MARK;
+                                break;
+                            case "ruby":
+                                tag = HtmlMarkup.RUBY;
+                                break;
+                            case "rubyBase":
+                                tag = HtmlMarkup.RB;
+                                break;
+                            case "rubyText":
+                                tag = HtmlMarkup.RT;
+                                break;
+                            case "rubyTextContainer":
+                                tag = HtmlMarkup.RTC;
+                                break;
+                            case "rubyParentheses":
+                                tag = HtmlMarkup.RP;
+                                break;
+                            case "bidirectionalIsolation":
+                                tag = HtmlMarkup.BDI;
+                                break;
+                            case "bidirectionalOverride":
+                                tag = HtmlMarkup.BDO;
+                                break;
+                            case "phrase":
+                                tag = HtmlMarkup.SPAN;
+                                break;
+                            case "insert":
+                                tag = HtmlMarkup.INS;
+                                break;
+                            case "delete":
+                                tag = HtmlMarkup.DEL;
+                                break;
+                            default:
+                                LOGGER.warn(
+                                        "{}Skipping unsupported semantic 
attribute '{}'",
+                                        getLocationLogPrefix(),
+                                        attribute.getValue());
+                        }
+                        
compliantAttributes.removeAttribute(SinkEventAttributes.SEMANTICS);
+                    }
+                }
+                writeStartTag(tag, compliantAttributes);
+                inlineStack.push(Collections.singletonList(tag));
+            } else {
+                inlineStack.push(Collections.emptyList());
+            }
+        }
+    }
+
+    /**
+     * Some attributes have generally supported values as defined in {@link 
SinkEventAttributes}.
+     * This method converts them to their HTML5 compliant equivalent, e.g. the 
"underline" value of the "decoration" attribute is converted to a style 
attribute with value "text-decoration-line: underline".
+     *
+     * Other attributes with values outsides of the generally supported ones 
are passed as is (and may not be supported by all HTML output formats).
+     * @param attributes
+     * @return a new set of attributes with HTML5 compliant values for the 
generally supported attribute values
+     */
+    SinkEventAttributes convertToHtml5CompliantAttributes(SinkEventAttributes 
attributes) {
+        SinkEventAttributes compliantAttributes = new SinkEventAttributeSet();
+
+        for (Map.Entry<String, Object> attribute : attributes.entrySet()) {
+            if (attribute.getKey().equals(SinkEventAttributes.DECORATION)) {
+                switch (attribute.getValue().toString()) {
+                    case "underline":
+                        addStyle(compliantAttributes, "text-decoration-line", 
"underline");
+                        break;
+                    case "overline":
+                        addStyle(compliantAttributes, "text-decoration-line", 
"overline");
+                        break;
+                    case "line-through":
+                        addStyle(compliantAttributes, "text-decoration-line", 
"line-through");
+                        break;
+                    case "source":
+                        // potentially overwrites other semantics
+                        
compliantAttributes.addAttributes(SinkEventAttributeSet.Semantics.CODE);
+                        break;
+                    default:
+                        LOGGER.warn(
+                                "{}Skipping unsupported decoration attribute 
'{}'",
+                                getLocationLogPrefix(),
+                                attribute.getValue());
+                }
+            } else if (attribute.getKey().equals(SinkEventAttributes.STYLE)) {
+                switch (attribute.getValue().toString()) {
+                    case "bold":
+                        addStyle(compliantAttributes, "font-weight", "bold");
+                        break;
+                    case "italic":
+                        addStyle(compliantAttributes, "font-style", "italic");
+                        break;
+                    case "monospaced":
+                        addStyle(compliantAttributes, "font-family", 
"monospace");
+                        break;
+                    default:
+                        // everything else is passed as-is, e.g. "color: red" 
or "text-decoration: underline"
+                        
compliantAttributes.addAttribute(SinkEventAttributes.STYLE, 
attribute.getValue());
+                }
+            } else {
+                compliantAttributes.addAttribute(attribute.getKey(), 
attribute.getValue());
             }
+        }
+        return compliantAttributes;
+    }
 
-            inlineStack.push(tags);
+    /**
+     * Adds a style to the given attributes. If the attributes already contain 
a style, the new style value is appended to it.
+     *
+     * @param attributes the attributes to which the style should be added
+     * @param property   the CSS property, e.g. "text-decoration-line"
+     * @param value      the CSS value, e.g. "underline" */
+    static void addStyle(SinkEventAttributes attributes, String property, 
String value) {
+        Object oldStyleValue = 
attributes.getAttribute(SinkEventAttributes.STYLE);
+        // styles may be stored as an AttributeSet or a String
+        if (oldStyleValue instanceof AttributeSet) {
+            SinkEventAttributeSet newStyleValue = new 
SinkEventAttributeSet((AttributeSet) oldStyleValue);
+            newStyleValue.addAttribute(property, value);
+            attributes.addAttribute(SinkEventAttributes.STYLE, newStyleValue);
+        } else {
+            StringBuilder newStyleValue = new StringBuilder();
+            if (oldStyleValue != null) {
+                // if the old style value is not an AttributeSet, we assume it 
is a String and append the new style to
+                // it
+                newStyleValue.append(oldStyleValue.toString());
+                // normalize the old style value by ensuring it ends with a 
semicolon followed by a space, so that the
+                // new style can be appended to it
+                if 
(!newStyleValue.toString().endsWith(Character.toString(Markup.SEMICOLON))) {
+                    
newStyleValue.append(Markup.SEMICOLON).append(Markup.SPACE);
+                }
+            }
+            newStyleValue.append(SinkUtils.asCssDeclaration(property, value));
+            attributes.addAttribute(SinkEventAttributes.STYLE, 
newStyleValue.toString());
         }
     }
 
diff --git 
a/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSinkTest.java
 
b/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSinkTest.java
index 74d12097..d229c23d 100644
--- 
a/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSinkTest.java
+++ 
b/doxia-core/src/test/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSinkTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.maven.doxia.sink.impl;
 
+import javax.swing.text.AttributeSet;
 import javax.swing.text.html.HTML.Attribute;
 
 import java.io.StringWriter;
@@ -1055,7 +1056,7 @@ class Xhtml5BaseSinkTest {
             sink.text(text, attributes);
         }
 
-        assertEquals("a text &amp; &#xc6;", writer.toString());
+        assertEquals("<span style=\"font-weight: bold\">a text &amp; 
&#xc6;</span>", writer.toString());
     }
 
     /**
@@ -1176,4 +1177,49 @@ class Xhtml5BaseSinkTest {
 
         assertTrue(result.contains("&#x2713;"));
     }
+
+    @Test
+    void multipleInlineAttributes() {
+        try (Sink sink = new Xhtml5BaseSink(writer)) {
+            SinkEventAttributeSet attributes = new SinkEventAttributeSet();
+            // set different attributes (semantic, style and decoration)
+            attributes.addAttributes(SinkEventAttributeSet.LINETHROUGH);
+            attributes.addAttributes(SinkEventAttributeSet.BOLD);
+            
attributes.addAttributes(SinkEventAttributeSet.Semantics.SUPERSCRIPT);
+            sink.inline(attributes);
+            sink.text("text");
+            sink.inline_();
+        }
+        // the attribute order should be kept
+        String expected = "<sup style=\"text-decoration-line: line-through; 
font-weight: bold\">text</sup>";
+        assertEquals(expected, writer.toString());
+    }
+
+    @Test
+    void addStyleWithStringValue() {
+        SinkEventAttributes attributes = new SinkEventAttributeSet();
+        Xhtml5BaseSink.addStyle(attributes, "font-weight", "bold");
+        assertEquals("font-weight: bold", 
attributes.getAttribute(SinkEventAttributes.STYLE));
+        Xhtml5BaseSink.addStyle(attributes, "font-size", "12px");
+        assertEquals(1, attributes.getAttributeCount());
+        assertEquals("font-weight: bold; font-size: 12px", 
attributes.getAttribute(SinkEventAttributes.STYLE));
+    }
+
+    @Test
+    void addStyleWithAttributeSetValue() {
+        SinkEventAttributes attributes = new SinkEventAttributeSet();
+        SinkEventAttributeSet styleAttributes = new SinkEventAttributeSet();
+        styleAttributes.addAttribute("font-style", "italic");
+        attributes.addAttribute(SinkEventAttributeSet.STYLE, styleAttributes);
+
+        Xhtml5BaseSink.addStyle(attributes, "font-weight", "bold");
+        assertEquals("font-style: italic; font-weight: bold", 
SinkUtils.asCssString((AttributeSet)
+                attributes.getAttribute(SinkEventAttributes.STYLE)));
+        Xhtml5BaseSink.addStyle(attributes, "font-weight", "lighter");
+        assertEquals("font-style: italic; font-weight: lighter", 
SinkUtils.asCssString((AttributeSet)
+                attributes.getAttribute(SinkEventAttributes.STYLE)));
+        Xhtml5BaseSink.addStyle(attributes, "font-size", "12px");
+        assertEquals("font-style: italic; font-weight: lighter; font-size: 
12px", SinkUtils.asCssString((AttributeSet)
+                attributes.getAttribute(SinkEventAttributes.STYLE)));
+    }
 }
diff --git 
a/doxia-sink-api/src/main/java/org/apache/maven/doxia/sink/SinkEventAttributes.java
 
b/doxia-sink-api/src/main/java/org/apache/maven/doxia/sink/SinkEventAttributes.java
index 38a30e43..ce33e798 100644
--- 
a/doxia-sink-api/src/main/java/org/apache/maven/doxia/sink/SinkEventAttributes.java
+++ 
b/doxia-sink-api/src/main/java/org/apache/maven/doxia/sink/SinkEventAttributes.java
@@ -20,6 +20,9 @@ package org.apache.maven.doxia.sink;
 
 import javax.swing.text.MutableAttributeSet;
 
+import java.util.Map;
+import java.util.Set;
+
 /**
  * A set of attributes for a sink event.
  * <p>
@@ -388,4 +391,23 @@ public interface SinkEventAttributes extends 
MutableAttributeSet {
      * Specifies a machine readable date/time for the time element.
      */
     String DATETIME = "datetime";
+
+    /**
+     * Returns a {@link Set} view of the attributes in form  of {@link 
Map.Entry} items.
+     * The set is backed by the underlying map, so changes to the map are
+     * reflected in the set, and vice-versa.  If the map is modified
+     * while an iteration over the set is in progress (except through
+     * the iterator's own {@code remove} operation, or through the
+     * {@code setValue} operation on a map entry returned by the
+     * iterator) the results of the iteration are undefined.  The set
+     * supports element removal, which removes the corresponding
+     * mapping from the map, via the {@code Iterator.remove},
+     * {@code Set.remove}, {@code removeAll}, {@code retainAll} and
+     * {@code clear} operations.  It does not support the
+     * {@code add} or {@code addAll} operations.
+     *
+     * @return a set view of the attributes
+     * @since 2.1.0
+     */
+    Set<Map.Entry<String, Object>> entrySet();
 }

Reply via email to