Initial commit (Clean history)

This commit is contained in:
anhduy-tech
2025-12-30 11:27:14 +07:00
commit ef48c93de0
19255 changed files with 3248867 additions and 0 deletions

View File

@@ -0,0 +1,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)

View File

@@ -0,0 +1,61 @@
import cobble
class Node(object):
pass
@cobble.data
class TextNode(Node):
value = cobble.field()
@cobble.data
class Tag(object):
tag_names = cobble.field()
attributes = cobble.field()
collapsible = cobble.field()
separator = cobble.field()
@property
def tag_name(self):
return self.tag_names[0]
@cobble.data
class Element(Node):
tag = cobble.field()
children = cobble.field()
@property
def tag_name(self):
return self.tag.tag_name
@property
def tag_names(self):
return self.tag.tag_names
@property
def attributes(self):
return self.tag.attributes
@property
def collapsible(self):
return self.tag.collapsible
@property
def separator(self):
return self.tag.separator
_VOID_TAG_NAMES = set(["br", "hr", "img", "input"])
def is_void(self):
return not self.children and self.tag_name in self._VOID_TAG_NAMES
@cobble.visitable
class ForceWrite(Node):
pass
NodeVisitor = cobble.visitor(Node)