aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--README.md6
-rw-r--r--mammoth/html_paths.py13
-rw-r--r--mammoth/styles/parser/html_path_parser.py74
-rw-r--r--mammoth/styles/parser/token_iterator.py25
-rw-r--r--tests/styles/parser/html_path_parser_tests.py14
6 files changed, 102 insertions, 34 deletions
diff --git a/NEWS b/NEWS
index 8302447..b3ac053 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 f058aeb..2e501e2 100644
--- a/README.md
+++ b/README.md
@@ -649,6 +649,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/mammoth/html_paths.py b/mammoth/html_paths.py
index c91813a..a8fb952 100644
--- a/mammoth/html_paths.py
+++ b/mammoth/html_paths.py
@@ -7,15 +7,16 @@ def path(elements):
return HtmlPath(elements)
-def element(names, class_names=None, fresh=None, separator=None):
+def element(names, attributes=None, class_names=None, fresh=None, separator=None):
+ if attributes is None:
+ attributes = {}
if class_names is None:
class_names = []
if fresh is None:
fresh = False
if class_names:
- attributes = {"class": " ".join(class_names)}
- else:
- attributes = {}
+ attributes["class"] = " ".join(class_names)
+
return HtmlPathElement(html.tag(
tag_names=names,
attributes=attributes,
@@ -27,13 +28,13 @@ def element(names, class_names=None, fresh=None, separator=None):
@cobble.data
class HtmlPath(object):
elements = cobble.field()
-
+
def wrap(self, generate_nodes):
nodes = generate_nodes()
for element in reversed(self.elements):
nodes = element.wrap_nodes(nodes)
-
+
return nodes
diff --git a/mammoth/styles/parser/html_path_parser.py b/mammoth/styles/parser/html_path_parser.py
index 9e155b6..8ed6fed 100644
--- a/mammoth/styles/parser/html_path_parser.py
+++ b/mammoth/styles/parser/html_path_parser.py
@@ -1,6 +1,15 @@
+import cobble
+
from ... import html_paths
from .tokeniser import TokenType
-from .token_parser import parse_identifier, parse_string, try_parse_class_name
+from .token_parser import parse_identifier, parse_string
+
+
+@cobble.data
+class _AttributeOrClassName(object):
+ name = cobble.field()
+ value = cobble.field()
+ append = cobble.field()
def parse_html_path(tokens):
@@ -12,26 +21,33 @@ def parse_html_path(tokens):
def _parse_html_path_elements(tokens):
elements = []
-
+
if tokens.peek_token_type() == TokenType.IDENTIFIER:
elements.append(_parse_element(tokens))
-
+
while tokens.try_skip_many(((TokenType.WHITESPACE, None), (TokenType.SYMBOL, ">"))):
tokens.skip(TokenType.WHITESPACE)
elements.append(_parse_element(tokens))
-
+
return elements
def _parse_element(tokens):
tag_names = _parse_tag_names(tokens)
- class_names = _parse_class_names(tokens)
+ attributes_list = _parse_attribute_or_class_names(tokens)
is_fresh = _parse_is_fresh(tokens)
separator = _parse_separator(tokens)
-
+
+ attributes = {}
+ for attribute in attributes_list:
+ if attribute.append and attributes.get(attribute.name):
+ attributes[attribute.name] += " " + attribute.value
+ else:
+ attributes[attribute.name] = attribute.value
+
return html_paths.element(
tag_names,
- class_names=class_names,
+ attributes=attributes,
fresh=is_fresh,
separator=separator,
)
@@ -39,24 +55,48 @@ def _parse_element(tokens):
def _parse_tag_names(tokens):
tag_names = [parse_identifier(tokens)]
-
+
while tokens.try_skip(TokenType.SYMBOL, "|"):
tag_names.append(parse_identifier(tokens))
-
+
return tag_names
-def _parse_class_names(tokens):
- class_names = []
-
+def _parse_attribute_or_class_names(tokens):
+ attribute_or_class_names = []
+
while True:
- class_name = try_parse_class_name(tokens)
- if class_name is None:
+ attribute_or_class_name = _try_parse_attribute_or_class_name(tokens)
+ if attribute_or_class_name is None:
break
else:
- class_names.append(class_name)
-
- return class_names
+ attribute_or_class_names.append(attribute_or_class_name)
+
+ return attribute_or_class_names
+
+
+def _try_parse_attribute_or_class_name(tokens):
+ if tokens.is_next(TokenType.SYMBOL, "["):
+ return _parse_attribute(tokens)
+ if tokens.is_next(TokenType.SYMBOL, "."):
+ return _parse_class_name(tokens)
+ else:
+ return None
+
+
+def _parse_attribute(tokens):
+ tokens.skip(TokenType.SYMBOL, "[")
+ name = parse_identifier(tokens)
+ tokens.skip(TokenType.SYMBOL, "=")
+ value = parse_string(tokens)
+ tokens.skip(TokenType.SYMBOL, "]")
+ return _AttributeOrClassName(name=name, value=value, append=False)
+
+
+def _parse_class_name(tokens):
+ tokens.skip(TokenType.SYMBOL, ".")
+ class_name = parse_identifier(tokens)
+ return _AttributeOrClassName(name="class", value=class_name, append=True)
def _parse_is_fresh(tokens):
diff --git a/mammoth/styles/parser/token_iterator.py b/mammoth/styles/parser/token_iterator.py
index fded56d..931b419 100644
--- a/mammoth/styles/parser/token_iterator.py
+++ b/mammoth/styles/parser/token_iterator.py
@@ -8,13 +8,13 @@ class TokenIterator(object):
def __init__(self, tokens):
self._tokens = tokens
self._index = 0
-
+
def peek_token_type(self):
return self._tokens[self._index].type
-
+
def next_value(self, token_type=None):
return self._next(token_type).value
-
+
def _next(self, token_type=None):
token = self._tokens[self._index]
if token_type is None or token.type == token_type:
@@ -22,7 +22,7 @@ class TokenIterator(object):
return token
else:
raise self._unexpected_token_type(token_type, token)
-
+
def skip(self, token_type, token_value=None):
token = self._tokens[self._index]
if token.type == token_type and (token_value is None or token.value == token_value):
@@ -30,15 +30,14 @@ class TokenIterator(object):
return True
else:
raise self._unexpected_token_type(token_type, token)
-
+
def try_skip(self, token_type, token_value=None):
- token = self._tokens[self._index]
- if token.type == token_type and (token_value is None or token.value == token_value):
+ if self.is_next(token_type, token_value):
self._index += 1
return True
else:
return False
-
+
def try_skip_many(self, tokens):
start = self._index
for token_type, token_value in tokens:
@@ -48,9 +47,13 @@ class TokenIterator(object):
return False
else:
self._index += 1
-
+
return True
-
+
+ def is_next(self, token_type, token_value=None):
+ token = self._tokens[self._index]
+ return token.type == token_type and (token_value is None or token.value == token_value)
+
def _unexpected_token_type(self, token_type, token):
raise LineParseError()
-
+
diff --git a/tests/styles/parser/html_path_parser_tests.py b/tests/styles/parser/html_path_parser_tests.py
index 0c2ef3a..6dd7032 100644
--- a/tests/styles/parser/html_path_parser_tests.py
+++ b/tests/styles/parser/html_path_parser_tests.py
@@ -53,6 +53,20 @@ def test_can_read_multiple_classes_on_element():
)
+def test_can_read_attribute_on_element():
+ assert_equal(
+ html_paths.path([html_paths.element(["p"], attributes={"lang": "fr"})]),
+ read_html_path("p[lang='fr']")
+ )
+
+
+def test_can_read_multiple_attributes_on_element():
+ assert_equal(
+ html_paths.path([html_paths.element(["p"], attributes={"lang": "fr", "data-x": "y"})]),
+ read_html_path("p[lang='fr'][data-x='y']")
+ )
+
+
def test_can_read_when_element_must_be_fresh():
assert_equal(
html_paths.path([html_paths.element(["p"], fresh=True)]),