1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
from ..lists import flat_map
from .nodes import TextNode, Tag, Element, ForceWrite, NodeVisitor
def text(value):
return TextNode(value)
def tag(tag_names, attributes=None, collapsible=None, separator=None):
if not isinstance(tag_names, list):
tag_names = [tag_names]
if attributes is None:
attributes = {}
return Tag(tag_names=tag_names, attributes=attributes, collapsible=bool(collapsible), separator=separator)
def element(tag_names, attributes=None, children=None, collapsible=None, separator=None):
if children is None:
children = []
element_tag = tag(tag_names=tag_names, attributes=attributes, collapsible=collapsible, separator=separator)
return Element(element_tag, children)
def collapsible_element(tag_names, attributes=None, children=None):
return element(tag_names, attributes, children, collapsible=True)
force_write = ForceWrite()
def strip_empty(nodes):
return flat_map(_strip_empty_node, nodes)
def _strip_empty_node(node):
return StripEmpty().visit(node)
class StripEmpty(NodeVisitor):
def visit_text_node(self, node):
if node.value:
return [node]
else:
return []
def visit_element(self, element):
children = strip_empty(element.children)
if len(children) == 0 and not element.is_void():
return []
else:
return [Element(element.tag, children)]
def visit_force_write(self, node):
return [node]
def collapse(nodes):
collapsed = []
for node in nodes:
_collapsing_add(collapsed, node)
return collapsed
class _CollapseNode(NodeVisitor):
def visit_text_node(self, node):
return node
def visit_element(self, element):
return Element(element.tag, collapse(element.children))
def visit_force_write(self, node):
return node
_collapse_node = _CollapseNode().visit
def _collapsing_add(collapsed, node):
collapsed_node = _collapse_node(node)
if not _try_collapse(collapsed, collapsed_node):
collapsed.append(collapsed_node)
def _try_collapse(collapsed, node):
if not collapsed:
return False
last = collapsed[-1]
if not isinstance(last, Element) or not isinstance(node, Element):
return False
if not node.collapsible:
return False
if not _is_match(last, node):
return False
if node.separator:
last.children.append(text(node.separator))
for child in node.children:
_collapsing_add(last.children, child)
return True
def _is_match(first, second):
return first.tag_name in second.tag_names and first.attributes == second.attributes
def write(writer, nodes):
visitor = _NodeWriter(writer)
visitor.visit_all(nodes)
class _NodeWriter(NodeVisitor):
def __init__(self, writer):
self._writer = writer
def visit_text_node(self, node):
self._writer.text(node.value)
def visit_element(self, element):
if element.is_void():
self._writer.self_closing(element.tag_name, element.attributes)
else:
self._writer.start(element.tag_name, element.attributes)
self.visit_all(element.children)
self._writer.end(element.tag_name)
def visit_force_write(self, element):
pass
def visit_all(self, nodes):
for node in nodes:
self.visit(node)
|