Skip to content

Commit

Permalink
Support attributes in HTML paths in style mappings
Browse files Browse the repository at this point in the history
  • Loading branch information
mwilliamson committed Feb 17, 2024
1 parent af2f17f commit 9bb9e64
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 15 deletions.
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
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<TokenType> tokens) {
Expand All @@ -22,6 +19,18 @@ public static HtmlPath parse(TokenIterator<TokenType> tokens) {
}
}

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<TokenType> tokens) {
List<HtmlPathElement> elements = new ArrayList<>();

Expand All @@ -41,10 +50,20 @@ private static HtmlPath parseHtmlPathElements(TokenIterator<TokenType> tokens) {

private static HtmlPathElement parseElement(TokenIterator<TokenType> tokens) {
List<String> tagNames = parseTagNames(tokens);
List<String> classNames = parseClassNames(tokens);
Map<String, String> attributes = classNames.isEmpty()
? map()
: map("class", String.join(" ", classNames));

List<Attribute> attributeList = parseAttributeOrClassNames(tokens);
Map<String, String> 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));
Expand All @@ -59,18 +78,43 @@ private static List<String> parseTagNames(TokenIterator<TokenType> tokens) {
return tagNames;
}

private static List<String> parseClassNames(TokenIterator<TokenType> tokens) {
List<String> classNames = new ArrayList<>();
private static List<Attribute> parseAttributeOrClassNames(TokenIterator<TokenType> tokens) {
List<Attribute> attributes = new ArrayList<>();
while (true) {
Optional<String> className = TokenParser.parseClassName(tokens);
if (className.isPresent()) {
classNames.add(className.get());
Optional<Attribute> attribute = parseAttributeOrClassName(tokens);
if (attribute.isPresent()) {
attributes.add(attribute.get());
} else {
return classNames;
return attributes;
}
}
}

private static Optional<Attribute> parseAttributeOrClassName(TokenIterator<TokenType> 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<TokenType> 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<TokenType> tokens) {
tokens.skip(TokenType.SYMBOL, ".");
String className = parseIdentifier(tokens);
return new Attribute("class", className, true);
}

private static boolean parseIsFresh(TokenIterator<TokenType> tokens) {
return tokens.tryParse(() -> {
tokens.skip(TokenType.SYMBOL, ":");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ public void canReadMultipleClassesOnElement() {
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(
Expand Down

0 comments on commit 9bb9e64

Please sign in to comment.