From 9bb9e645808bb8adcd3179bb688c3d70755a62fd Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sat, 17 Feb 2024 14:18:13 +0000 Subject: Support attributes in HTML paths in style mappings --- NEWS | 4 ++ README.md | 6 ++ .../internal/styles/parsing/HtmlPathParser.java | 74 +++++++++++++++++----- .../tests/styles/parsing/HtmlPathParsingTests.java | 14 ++++ 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/NEWS b/NEWS index 3c24378..9977f85 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ +# 1.7.0 + +* Support attributes in HTML paths in style mappings. + # 1.6.0 * Support merged paragraphs when revisions are tracked. diff --git a/README.md b/README.md index 7097a1b..05d8c0e 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,12 @@ append a dot followed by the name of the class: h1.section-title ``` +To add an attribute, use square brackets similarly to a CSS attribute selector: + +``` +p[lang='fr'] +``` + To require that an element is fresh, use `:fresh`: ``` diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/parsing/HtmlPathParser.java b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/HtmlPathParser.java index a52ed64..ec615f8 100644 --- a/src/main/java/org/zwobble/mammoth/internal/styles/parsing/HtmlPathParser.java +++ b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/HtmlPathParser.java @@ -5,13 +5,10 @@ import org.zwobble.mammoth.internal.styles.HtmlPath; import org.zwobble.mammoth.internal.styles.HtmlPathElement; import org.zwobble.mammoth.internal.styles.HtmlPathElements; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import static org.zwobble.mammoth.internal.styles.parsing.TokenParser.parseIdentifier; -import static org.zwobble.mammoth.internal.util.Maps.map; +import static org.zwobble.mammoth.internal.styles.parsing.TokenParser.parseString; public class HtmlPathParser { public static HtmlPath parse(TokenIterator tokens) { @@ -22,6 +19,18 @@ public class HtmlPathParser { } } + private static class Attribute { + private final String name; + private final String value; + private final boolean append; + + private Attribute(String name, String value, boolean append) { + this.name = name; + this.value = value; + this.append = append; + } + } + private static HtmlPath parseHtmlPathElements(TokenIterator tokens) { List elements = new ArrayList<>(); @@ -41,10 +50,20 @@ public class HtmlPathParser { private static HtmlPathElement parseElement(TokenIterator tokens) { List tagNames = parseTagNames(tokens); - List classNames = parseClassNames(tokens); - Map attributes = classNames.isEmpty() - ? map() - : map("class", String.join(" ", classNames)); + + List attributeList = parseAttributeOrClassNames(tokens); + Map attributes = new HashMap<>(); + for (Attribute attribute : attributeList) { + if (attribute.append && attributes.containsKey(attribute.name)) { + attributes.put( + attribute.name, + attributes.get(attribute.name) + " " + attribute.value + ); + } else { + attributes.put(attribute.name, attribute.value); + } + } + boolean isFresh = parseIsFresh(tokens); String separator = parseSeparator(tokens); return new HtmlPathElement(new HtmlTag(tagNames, attributes, !isFresh, separator)); @@ -59,18 +78,43 @@ public class HtmlPathParser { return tagNames; } - private static List parseClassNames(TokenIterator tokens) { - List classNames = new ArrayList<>(); + private static List parseAttributeOrClassNames(TokenIterator tokens) { + List attributes = new ArrayList<>(); while (true) { - Optional className = TokenParser.parseClassName(tokens); - if (className.isPresent()) { - classNames.add(className.get()); + Optional attribute = parseAttributeOrClassName(tokens); + if (attribute.isPresent()) { + attributes.add(attribute.get()); } else { - return classNames; + return attributes; } } } + private static Optional parseAttributeOrClassName(TokenIterator tokens) { + if (tokens.isNext(TokenType.SYMBOL, "[")) { + return Optional.of(parseAttribute(tokens)); + } else if (tokens.isNext(TokenType.SYMBOL, ".")) { + return Optional.of(parseClassName(tokens)); + } else { + return Optional.empty(); + } + } + + private static Attribute parseAttribute(TokenIterator tokens) { + tokens.skip(TokenType.SYMBOL, "["); + String name = parseIdentifier(tokens); + tokens.skip(TokenType.SYMBOL, "="); + String value = parseString(tokens); + tokens.skip(TokenType.SYMBOL, "]"); + return new Attribute(name, value, true); + } + + private static Attribute parseClassName(TokenIterator tokens) { + tokens.skip(TokenType.SYMBOL, "."); + String className = parseIdentifier(tokens); + return new Attribute("class", className, true); + } + private static boolean parseIsFresh(TokenIterator tokens) { return tokens.tryParse(() -> { tokens.skip(TokenType.SYMBOL, ":"); diff --git a/src/test/java/org/zwobble/mammoth/tests/styles/parsing/HtmlPathParsingTests.java b/src/test/java/org/zwobble/mammoth/tests/styles/parsing/HtmlPathParsingTests.java index 23cc730..85591ad 100644 --- a/src/test/java/org/zwobble/mammoth/tests/styles/parsing/HtmlPathParsingTests.java +++ b/src/test/java/org/zwobble/mammoth/tests/styles/parsing/HtmlPathParsingTests.java @@ -60,6 +60,20 @@ public class HtmlPathParsingTests { deepEquals(HtmlPath.collapsibleElement("p", map("class", "tip help")))); } + @Test + public void canReadAttributeOnElement() { + assertThat( + parseHtmlPath("p[lang='fr']"), + deepEquals(HtmlPath.collapsibleElement("p", map("lang", "fr")))); + } + + @Test + public void canReadMultipleAttributesOnElement() { + assertThat( + parseHtmlPath("p[lang='fr'][data-x='y']"), + deepEquals(HtmlPath.collapsibleElement("p", map("lang", "fr", "data-x", "y")))); + } + @Test public void canReadWhenElementMustBeFresh() { assertThat( -- cgit v1.2.3