aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2016-11-24 21:55:13 +0000
committerMichael Williamson <mike@zwobble.org>2016-11-24 21:55:13 +0000
commit7e3ef0ebd0c3453ce351af70ccdb25e93de1d7a9 (patch)
treebaa24495768580b2a70bcd181ef1a398c435bc6d
parentcfc0df82457acff6d8ac1abf930901b8baafbe17 (diff)
Add HTML path parser
-rw-r--r--mammoth/styles/parser/html_path_parser.py64
-rw-r--r--mammoth/styles/parser/token_iterator.py22
-rw-r--r--tests/styles/parser/html_path_parser_tests.py80
3 files changed, 164 insertions, 2 deletions
diff --git a/mammoth/styles/parser/html_path_parser.py b/mammoth/styles/parser/html_path_parser.py
new file mode 100644
index 0000000..e8f448e
--- /dev/null
+++ b/mammoth/styles/parser/html_path_parser.py
@@ -0,0 +1,64 @@
+from ... import html_paths
+from .tokeniser import TokenType
+from .token_parser import parse_identifier, try_parse_class_name
+
+
+def parse_html_path(tokens):
+ if tokens.try_skip(TokenType.SYMBOL, "!"):
+ return html_paths.ignore
+ else:
+ return html_paths.path(_parse_html_path_elements(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)
+ is_fresh = _parse_is_fresh(tokens)
+
+ return html_paths.element(
+ tag_names,
+ class_names=class_names,
+ fresh=is_fresh,
+ )
+
+
+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 = []
+
+ while True:
+ class_name = try_parse_class_name(tokens)
+ if class_name is None:
+ break
+ else:
+ class_names.append(class_name)
+
+ return class_names
+
+
+def _parse_is_fresh(tokens):
+ return tokens.try_skip_many((
+ (TokenType.SYMBOL, ":"),
+ (TokenType.IDENTIFIER, "fresh"),
+ ))
diff --git a/mammoth/styles/parser/token_iterator.py b/mammoth/styles/parser/token_iterator.py
index 810e50b..c6876e9 100644
--- a/mammoth/styles/parser/token_iterator.py
+++ b/mammoth/styles/parser/token_iterator.py
@@ -1,8 +1,13 @@
+# TODO: check indices
+
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):
return self._next(token_type).value
@@ -14,9 +19,9 @@ class TokenIterator(object):
else:
raise self._unexpected_token_type(token_type, token)
- def skip(self, token_type, token_value):
+ def skip(self, token_type, token_value=None):
token = self._tokens[self._index]
- if token.type == token_type and token.value == token_value:
+ if token.type == token_type and (token_value is None or token.value == token_value):
self._index += 1
return True
else:
@@ -29,3 +34,16 @@ class TokenIterator(object):
return True
else:
return False
+
+ def try_skip_many(self, tokens):
+ start = self._index
+ for token_type, token_value in tokens:
+ token = self._tokens[self._index]
+ if not (token.type == token_type and (token_value is None or token.value == token_value)):
+ self._index = start
+ return False
+ else:
+ self._index += 1
+
+ return True
+
diff --git a/tests/styles/parser/html_path_parser_tests.py b/tests/styles/parser/html_path_parser_tests.py
new file mode 100644
index 0000000..feaa343
--- /dev/null
+++ b/tests/styles/parser/html_path_parser_tests.py
@@ -0,0 +1,80 @@
+from nose.tools import istest, assert_equal
+
+from mammoth import html_paths
+from mammoth.styles.parser.html_path_parser import parse_html_path
+from mammoth.styles.parser.tokeniser import tokenise
+from mammoth.styles.parser.token_iterator import TokenIterator
+
+
+@istest
+def can_read_empty_path():
+ assert_equal(
+ html_paths.empty,
+ read_html_path("")
+ )
+
+@istest
+def can_read_single_element():
+ assert_equal(
+ html_paths.path([html_paths.element(["p"])]),
+ read_html_path("p")
+ )
+
+
+@istest
+def can_read_choice_of_two_elements():
+ assert_equal(
+ html_paths.path([html_paths.element(["ul", "ol"])]),
+ read_html_path("ul|ol")
+ )
+
+
+@istest
+def can_read_choice_of_three_elements():
+ assert_equal(
+ html_paths.path([html_paths.element(["ul", "ol", "p"])]),
+ read_html_path("ul|ol|p")
+ )
+
+
+@istest
+def can_read_nested_elements():
+ assert_equal(
+ html_paths.path([html_paths.element(["ul"]), html_paths.element(["li"])]),
+ read_html_path("ul > li")
+ )
+
+
+@istest
+def can_read_class_on_element():
+ assert_equal(
+ html_paths.path([html_paths.element(["p"], class_names=["tip"])]),
+ read_html_path("p.tip")
+ )
+
+
+@istest
+def can_read_multiple_classes_on_element():
+ assert_equal(
+ html_paths.path([html_paths.element(["p"], class_names=["tip", "help"])]),
+ read_html_path("p.tip.help")
+ )
+
+
+@istest
+def can_read_when_element_must_be_fresh():
+ assert_equal(
+ html_paths.path([html_paths.element(["p"], fresh=True)]),
+ read_html_path("p:fresh")
+ )
+
+
+@istest
+def can_read_ignore_element():
+ assert_equal(
+ html_paths.ignore,
+ read_html_path("!")
+ )
+
+def read_html_path(string):
+ return parse_html_path(TokenIterator(tokenise(string)))