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 & Æ", writer.toString());
+ assertEquals("<span style=\"font-weight: bold\">a text &
Æ</span>", writer.toString());
}
/**
@@ -1176,4 +1177,49 @@ class Xhtml5BaseSinkTest {
assertTrue(result.contains("✓"));
}
+
+ @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();
}