Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add type annotations #2

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 18 additions & 17 deletions html5tagger/builder.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations
from typing import Any, Dict, List, Union

from .html5 import omit_endtag
from .util import mangle, escape, escape_special, esc_script, esc_style, attributes

Expand All @@ -11,19 +14,19 @@ class Builder:
E.g. Document("page title", lang="en").div(id="main")("Hello World!")
"""

def __init__(self, name):
def __init__(self, name: str):
self.name = name
self._clear()

def _clear(self):
self._pieces = [] # Document content
self._templates = {} # Template builders
self._pieces: List[Union[str, Builder]] = [] # Document content
self._templates: Dict[str, Builder] = {} # Template builders
self._endtag = ""
self._stack = []
self._stack: List[str] = []

@property
def _allpieces(self):
retval = []
retval: List[Union[str, Builder]] = []
retval.extend(self._pieces)
retval.append(self._endtag)
retval.extend(self._stack[::-1])
Expand Down Expand Up @@ -61,7 +64,7 @@ def __str__(self):
def __iter__(self):
return str(self).__iter__()

def __getattr__(self, name):
def __getattr__(self, name: str):
"""Names that don't begin with underscore are HTML tag names or template blocks."""
if name[0] == "_":
return object.__getattribute__(self, name)
Expand All @@ -88,15 +91,15 @@ def __getattr__(self, name):
self._endtag = f"</{tagname}>"
return self

def __setattr__(self, name, value):
def __setattr__(self, name: str, value: Any):
if not name[0].isupper():
return object.__setattr__(self, name, value)
# Set the value of a Template placeholder
template = self._templates[name]
template._clear()
template(value)

def __call__(self, *_inner_content, **_attrs):
def __call__(self, *_inner_content: Any, **_attrs: Any):
"""Add attributes and content to the current tag, or append to the document."""
# Template placeholder just added
if self._pieces and isinstance(self._pieces[-1], Builder):
Expand All @@ -115,7 +118,7 @@ def __call__(self, *_inner_content, **_attrs):
self._endtag_close()
return self

def _(self, *_content):
def _(self, *_content: Any):
"""Append new content without closing the current tag."""
for c in _content:
if c is None:
Expand All @@ -130,16 +133,14 @@ def _(self, *_content):
self._pieces += c._pieces
# Other type of data, convert to HTML str
else:
self._pieces.append(str(
c.__html__() if hasattr(c, "__html__") else escape(c)
))
self._pieces.append(str(c.__html__() if hasattr(c, "__html__") else escape(c)))
return self

def _optimize(self):
"""Join adjacent text fragments."""
print("optimize")
newfrags = []
strfrags = []
newfrags: List[Union[str, Builder]] = []
strfrags: List[str] = []
for frag in self._pieces:
if isinstance(frag, str) or frag.name not in self._templates:
print("str", frag)
Expand Down Expand Up @@ -168,20 +169,20 @@ def __exit__(self, w, t, f):

## HTML5 elements and comments special methods

def _comment(self, text):
def _comment(self, text: str):
"""Add an HTML comment."""
text = str(text).replace("-->", "‒‒>")
self._pieces.append(f"<!--{text}-->")
return self

def _script(self, code: str, **attrs):
def _script(self, code: str, **attrs: Any):
"""Add inline JavaScript correctly escaped."""
self._endtag_close()
code = escape_special(esc_script, code)
self._pieces.append(f"<script{attributes(attrs)}>{code}</script>")
return self

def _style(self, code: str, **attrs):
def _style(self, code: str, **attrs: Any):
"""Add inline CSS correctly escaped."""
self._endtag_close()
code = escape_special(esc_style, code)
Expand Down
4 changes: 3 additions & 1 deletion html5tagger/document.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Any, List, Optional
from .builder import Builder
from .util import HTML


def Document(*title, _urls=None, _viewport=False, **html_attrs) -> Builder:
def Document(*title: str, _urls: Optional[List[str]] = None, _viewport: Union[bool, str] = False, **html_attrs: Any) -> Builder:
"""Construct a new document with a DOCTYPE and minimal structure.

The html tag is added if any attributes are provided for it.
Expand Down Expand Up @@ -39,6 +40,7 @@ def Document(*title, _urls=None, _viewport=False, **html_attrs) -> Builder:
raise ValueError("Unknown extension in " + fn)
return doc


# Arguments for link elements by filename/extension
linkarg = {
"manifest.json": dict(rel="manifest"),
Expand Down
2 changes: 2 additions & 0 deletions html5tagger/html5.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# HTML5 makes many end tags optional or forbidden (void elements)
void = set("area base br col embed hr img input keygen link menuitem meta param source track wbr".split())

optional = set("html head body p colgroup thead tbody tfoot tr th td li dt dd optgroup option".split())

omit_endtag = void | optional
assert not void & optional

Expand Down
5 changes: 3 additions & 2 deletions html5tagger/makebuilder.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from typing import Any
from .builder import Builder


class MakeBuilder:
"""Use E.elemname or E(content) to create initially empty snippets."""

def __getattr__(self, name):
def __getattr__(self, name: str):
return getattr(Builder("E Builder"), name)

def __call__(self, *args, **kwargs):
def __call__(self, *args: Any, **kwargs: Any):
return Builder("E Builder")(*args, **kwargs)


Expand Down
14 changes: 9 additions & 5 deletions html5tagger/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import re
from typing import Any, Dict, Pattern


class HTML(str):
"""A HTML string that will not be escaped."""
Expand All @@ -9,18 +11,20 @@ def __repr__(self):
return f"HTML({super().__repr__()})"


def escape(text):
def escape(text: str):
return HTML(str(text).replace("&", "&amp;").replace("<", "&lt;"))


# Inline styles and scripts only escape the specific end tag
esc_style = re.compile("</(style>)", re.IGNORECASE)
esc_script = re.compile("</(script>)", re.IGNORECASE)

def escape_special(tag: re, text):
return HTML(tag.sub(r'<\\/\1', text))

def escape_special(tag: Pattern[str], text: str):
return HTML(tag.sub(r"<\\/\1", text))


def attributes(attrs):
def attributes(attrs: Dict[str, Any]):
ret = ""
for k, v in attrs.items():
k = mangle(k)
Expand All @@ -36,7 +40,7 @@ def attributes(attrs):
return ret


def mangle(name):
def mangle(name: str):
"""Mangle Python identifiers into HTML tag/attribute names.

Underscores are converted into hyphens. Underscore at end is removed."""
Expand Down