diff --git a/README.md b/README.md
index 36c3709..c0730be 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,12 @@
# simple_html
-### Template-less. Type-safe. Minified by default.
+### Template-less. Type-safe. Minified by default. Fast.
-simple_html is built to simplify HTML rendering in Python. No templates needed. Just create HTML in
-normal Python. In most cases, the code will be more concise than standard HTML. Other benefits include:
-- typically renders fewer bytes than template-based rendering
+simple_html allows you to create HTML in standard Python. Benefits include:
+- it's typically faster than jinja2 -- up to 20x faster
+- it typically renders fewer bytes than template-based rendering
- types mean your editor and tools can help you write correct code faster
-- no framework needed
-- lightweight
+- lightweight and framework agnostic
### Installation
@@ -17,82 +16,97 @@ normal Python. In most cases, the code will be more concise than standard HTML.
### Usage
```python
-from simple_html.nodes import body, head, html, p
-from simple_html.render import render
-
-node = html(
- head,
- body(
- p.attrs(id="hello")(
- "Hello World!"
- )
- )
-)
+from simple_html import div, h1, render
+
+node = div({},
+ h1({"id": "hello"},
+ "Hello World!"
+ )
+ )
+
+render(node)
+#
Hello World!
+```
+
+There are several ways to render nodes:
+```python
+from simple_html import br, div, h1, img, render, span
+
+# raw node
+render(br)
+#
+# node with attributes only
+render(img({"src": "/some/image/url.jpg", "alt": "a great picture"}))
+#
+
+# node with children
render(
- node) # returns: Hello World!
+ div({},
+ h1({},
+ "something"))
+)
+# something
'
```
+Node attributes with `None` as the value will only render the key
+```python
+from simple_html import div, render
+
+render(div({"empty-str-attribute": "", "key-only-attr": None}))
+#
+```
Strings are escaped by default, but you can pass in `SafeString`s to avoid escaping.
```python
-from simple_html.nodes import br, p, safe_string
-from simple_html.render import render
+from simple_html import br, p, safe_string, render
-node = p(
- "Escaped & stuff",
- br,
- safe_string("Not escaped & stuff")
-)
+node = p({},
+ "Escaped & stuff",
+ br,
+ safe_string("Not escaped & stuff")
+ )
render(node) # returns: Escaped & stuff
Not escaped & stuff
```
-For convenience, many tags are provided, but you can create your own as well:
-
+Lists and generators are both valid collections of nodes:
```python
-from simple_html.nodes import Tag
-from simple_html.render import render
+from typing import Generator
+from simple_html import div, render, Node, br
-custom_elem = Tag("custom-elem")
-render(
- custom_elem.attrs(id="some-custom-elem-id")(
- "Wow"
- )
-) # returns: Wow
-```
+def list_of_nodes_function() -> list[Node]:
+ return ["neat", br]
-Likewise, some attributes have been created as type-safe presets. Note that there are multiple ways to create attributes.
-The examples below are all equivalent:
-```python
-from simple_html.attributes import height, id_
-from simple_html.nodes import div
+render(div({}, list_of_nodes_function()))
+# neat
-# **kwargs: recommended for most cases
-div.attrs(id="some-id", height="100")
+def node_generator() -> Generator[Node, None, None]:
+ yield "neat"
+ yield br
-# *args: useful for attributes that may be reserved keywords or when type constraints are desired.
-# Presets, raw tuples, and kwargs can be used interchangeably.
-div.attrs(id_("some-id"),
- height(100),
- ("class", "abc"),
- width="100")
-# renders to:
+render(div({}, node_generator()))
+# neat
```
-You can build your own presets, using `str_attr`, `int_attr`, or `bool_attr`. For instance, here are
-several of the attribute preset definitions
+
+For convenience, many tags are provided, but you can create your own as well:
```python
-from simple_html.attributes import bool_attr, int_attr, str_attr
+from simple_html import Tag, render
+
+custom_elem = Tag("custom-elem")
-checked = bool_attr('checked')
-class_ = str_attr('class')
-cols = int_attr('cols')
+render(
+ custom_elem({"id": "some-custom-elem-id"},
+ "Wow"
+ )
+) # returns: Wow
```
-But anything that renders to the type of `Attribute` will work.
\ No newline at end of file
+
+Likewise, some attributes have been created as type-safe presets. Note that there are multiple ways to create attributes.
\ No newline at end of file
diff --git a/bench/simple.py b/bench/simple.py
index 3cb6710..68904ff 100644
--- a/bench/simple.py
+++ b/bench/simple.py
@@ -1,6 +1,6 @@
from typing import List, Tuple
-from simple_html.nodes import (
+from simple_html import (
h1,
html,
title,
@@ -12,9 +12,10 @@
li,
safe_string,
br,
- meta, DOCTYPE_HTML5,
+ meta,
+ DOCTYPE_HTML5,
+ render
)
-from simple_html.render import render
def hello_world_empty(objs: List[None]) -> None:
diff --git a/poetry.lock b/poetry.lock
index c620312..2d5e592 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -508,4 +508,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.8.1"
-content-hash = "4159ff97884acb4fb7805f2f27ecf57124dc82ef56b0cc4cff04f53b542fa746"
+content-hash = "f5d4f84d632e8050ac4920f79b0186a6e61ab162c4f229a5b1a0b3378aceaa09"
diff --git a/pyproject.toml b/pyproject.toml
index 4d22d99..bb54ebf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "simple-html"
-version = "0.7.0"
+version = "1.0.0"
readme = "README.md"
description = "Template-less html rendering in Python"
authors = ["Keith Philpott "]
@@ -19,9 +19,9 @@ mypy = "1.6.0"
pytest = "7.4.2"
setuptools = "68.2.2"
black = "23.9.1"
-django = "^4.2.6"
-fast-html = "^1.0.3"
-dominate = "^2.8.0"
+django = "4.2.6"
+fast-html = "1.0.3"
+dominate = "2.8.0"
[build-system]
diff --git a/simple_html/__init__.py b/simple_html/__init__.py
index e69de29..53d21bf 100644
--- a/simple_html/__init__.py
+++ b/simple_html/__init__.py
@@ -0,0 +1,204 @@
+from html import escape
+from types import GeneratorType
+from typing import Tuple, Union, Dict, List, Generator, Optional, TYPE_CHECKING, cast
+
+SafeString = Tuple[str]
+
+
+def safe_string(x: str) -> SafeString:
+ return (x,)
+
+
+Node = Union[
+ str,
+ SafeString,
+ "Tag",
+ "TagTuple",
+ List["Node"],
+ Generator["Node", None, None],
+]
+
+TagTuple = Tuple[str, Tuple[Node, ...], str]
+
+
+class Tag:
+ __slots__ = ('tag_start', 'rendered', "closing_tag", "no_children_close")
+
+ def __init__(self, name: str, self_closing: bool = False) -> None:
+ self.tag_start = f"<{name}"
+ if self_closing:
+ self.closing_tag = ""
+ self.no_children_close = "/>"
+ else:
+ self.closing_tag = f"{name}>"
+ self.no_children_close = f">{self.closing_tag}"
+ self.rendered = f"{self.tag_start}{self.no_children_close}"
+
+ def __call__(
+ self, attributes: Dict[str, Optional[str]], *children: Node
+ ) -> Union[TagTuple, SafeString]:
+ if attributes:
+ # in this case this is faster than attrs = "".join([...])
+ attrs = ""
+ for key, val in attributes.items():
+ attrs += (" " + key if val is None else f' {key}="{val}"')
+
+ if children:
+ return f"{self.tag_start}{attrs}>", children, self.closing_tag
+ else:
+ return f"{self.tag_start}{attrs}{self.no_children_close}",
+ return f"{self.tag_start}>", children, self.closing_tag
+
+
+DOCTYPE_HTML5 = safe_string("")
+
+a = Tag("a")
+abbr = Tag("abbr")
+address = Tag("address")
+area = Tag("area", True)
+article = Tag("article")
+aside = Tag("aside")
+audio = Tag("audio")
+b = Tag("b")
+base = Tag("base", True)
+bdi = Tag("bdi")
+bdo = Tag("bdo")
+blockquote = Tag("blockquote")
+body = Tag("body")
+br = Tag("br", True)
+button = Tag("button")
+canvas = Tag("canvas")
+center = Tag("center")
+caption = Tag("caption")
+cite = Tag("cite")
+code = Tag("code")
+col = Tag("col")
+colgroup = Tag("colgroup")
+datalist = Tag("datalist")
+dd = Tag("dd")
+details = Tag("details")
+del_ = Tag("del")
+dfn = Tag("dfn")
+div = Tag("div")
+dl = Tag("dl")
+dt = Tag("dt")
+em = Tag("em")
+embed = Tag("embed", True)
+fieldset = Tag("fieldset")
+figure = Tag("figure")
+figcaption = Tag("figcaption")
+footer = Tag("footer")
+font = Tag("font")
+form = Tag("form")
+head = Tag("head")
+header = Tag("header")
+h1 = Tag("h1")
+h2 = Tag("h2")
+h3 = Tag("h3")
+h4 = Tag("h4")
+h5 = Tag("h5")
+h6 = Tag("h6")
+hr = Tag("hr", True)
+html = Tag("html")
+i = Tag("i")
+iframe = Tag("iframe", True)
+img = Tag("img", True)
+input_ = Tag("input", True)
+ins = Tag("ins")
+kbd = Tag("kbd")
+label = Tag("label")
+legend = Tag("legend")
+li = Tag("li")
+link = Tag("link", True)
+main = Tag("main")
+mark = Tag("mark")
+marquee = Tag("marquee")
+math = Tag("math")
+menu = Tag("menu")
+menuitem = Tag("menuitem")
+meta = Tag("meta", True)
+meter = Tag("meter")
+nav = Tag("nav")
+object_ = Tag("object")
+noscript = Tag("noscript")
+ol = Tag("ol")
+optgroup = Tag("optgroup")
+option = Tag("option")
+p = Tag("p")
+param = Tag("param", True)
+picture = Tag("picture")
+pre = Tag("pre")
+progress = Tag("progress")
+q = Tag("q")
+rp = Tag("rp")
+rt = Tag("rt")
+ruby = Tag("ruby")
+s = Tag("s")
+samp = Tag("samp")
+script = Tag("script")
+section = Tag("section")
+select = Tag("select")
+small = Tag("small")
+source = Tag("source", True)
+span = Tag("span")
+strike = Tag("strike")
+strong = Tag("strong")
+style = Tag("style")
+sub = Tag("sub")
+summary = Tag("summary")
+sup = Tag("sup")
+svg = Tag("svg")
+table = Tag("table")
+tbody = Tag("tbody")
+template = Tag("template")
+textarea = Tag("textarea")
+td = Tag("td")
+th = Tag("th")
+thead = Tag("thead")
+time = Tag("time")
+title = Tag("title")
+tr = Tag("tr")
+track = Tag("track", True)
+u = Tag("u")
+ul = Tag("ul")
+var = Tag("var")
+video = Tag("video")
+wbr = Tag("wbr")
+
+
+def _render(node: Node, strs: List[str]) -> None:
+ """
+ mutate a list instead of constantly rendering strings
+ """
+ if type(node) is tuple:
+ if len(node) == 3:
+ if TYPE_CHECKING:
+ node = cast(TagTuple, node)
+ strs.append(node[0])
+ for child in node[1]:
+ _render(child, strs)
+ strs.append(node[2])
+ else:
+ if TYPE_CHECKING:
+ node = cast(SafeString, node)
+ strs.append(node[0])
+ elif isinstance(node, str):
+ strs.append(escape(node))
+ elif isinstance(node, Tag):
+ strs.append(node.rendered)
+ elif isinstance(node, list):
+ for n in node:
+ _render(n, strs)
+ elif isinstance(node, GeneratorType):
+ for n in node:
+ _render(n, strs)
+ else:
+ raise TypeError(f"Got unknown type: {type(node)}")
+
+
+def render(*nodes: Node) -> str:
+ results: List[str] = []
+ for node in nodes:
+ _render(node, results)
+
+ return "".join(results)
diff --git a/simple_html/nodes.py b/simple_html/nodes.py
deleted file mode 100644
index b0d5ec9..0000000
--- a/simple_html/nodes.py
+++ /dev/null
@@ -1,164 +0,0 @@
-from typing import Tuple, Union, Dict, List, Generator, Optional
-
-SafeString = Tuple[str]
-
-
-def safe_string(x: str) -> SafeString:
- return (x,)
-
-
-Node = Union[
- str,
- SafeString,
- "Tag",
- "TagTuple",
- List["Node"],
- Generator["Node", None, None],
-]
-
-TagTuple = Tuple[str, Tuple[Node, ...], str]
-
-
-class Tag:
- __slots__ = ('tag_start', 'rendered', "closing_tag", "no_children_close")
-
- def __init__(self, name: str, self_closing: bool = False) -> None:
- self.tag_start = f"<{name}"
- if self_closing:
- self.closing_tag = ""
- self.no_children_close = "/>"
- else:
- self.closing_tag = f"{name}>"
- self.no_children_close = f">{self.closing_tag}"
- self.rendered = f"{self.tag_start}{self.no_children_close}"
-
- def __call__(
- self, attributes: Dict[str, Optional[str]], *children: Node
- ) -> Union[TagTuple, SafeString]:
- if attributes:
- # in this case this is faster than attrs = "".join([...])
- attrs = ""
- for key, val in attributes.items():
- attrs += (" " + key if val is None else f' {key}="{val}"')
-
- if children:
- return f"{self.tag_start}{attrs}>", children, self.closing_tag
- else:
- return f"{self.tag_start}{attrs}{self.no_children_close}",
- return f"{self.tag_start}>", children, self.closing_tag
-
-
-DOCTYPE_HTML5 = safe_string("")
-
-a = Tag("a")
-abbr = Tag("abbr")
-address = Tag("address")
-area = Tag("area", True)
-article = Tag("article")
-aside = Tag("aside")
-audio = Tag("audio")
-b = Tag("b")
-base = Tag("base", True)
-bdi = Tag("bdi")
-bdo = Tag("bdo")
-blockquote = Tag("blockquote")
-body = Tag("body")
-br = Tag("br", True)
-button = Tag("button")
-canvas = Tag("canvas")
-center = Tag("center")
-caption = Tag("caption")
-cite = Tag("cite")
-code = Tag("code")
-col = Tag("col")
-colgroup = Tag("colgroup")
-datalist = Tag("datalist")
-dd = Tag("dd")
-details = Tag("details")
-del_ = Tag("del")
-dfn = Tag("dfn")
-div = Tag("div")
-dl = Tag("dl")
-dt = Tag("dt")
-em = Tag("em")
-embed = Tag("embed", True)
-fieldset = Tag("fieldset")
-figure = Tag("figure")
-figcaption = Tag("figcaption")
-footer = Tag("footer")
-font = Tag("font")
-form = Tag("form")
-head = Tag("head")
-header = Tag("header")
-h1 = Tag("h1")
-h2 = Tag("h2")
-h3 = Tag("h3")
-h4 = Tag("h4")
-h5 = Tag("h5")
-h6 = Tag("h6")
-hr = Tag("hr", True)
-html = Tag("html")
-i = Tag("i")
-iframe = Tag("iframe", True)
-img = Tag("img", True)
-input_ = Tag("input", True)
-ins = Tag("ins")
-kbd = Tag("kbd")
-label = Tag("label")
-legend = Tag("legend")
-li = Tag("li")
-link = Tag("link", True)
-main = Tag("main")
-mark = Tag("mark")
-marquee = Tag("marquee")
-math = Tag("math")
-menu = Tag("menu")
-menuitem = Tag("menuitem")
-meta = Tag("meta", True)
-meter = Tag("meter")
-nav = Tag("nav")
-object_ = Tag("object")
-noscript = Tag("noscript")
-ol = Tag("ol")
-optgroup = Tag("optgroup")
-option = Tag("option")
-p = Tag("p")
-param = Tag("param", True)
-picture = Tag("picture")
-pre = Tag("pre")
-progress = Tag("progress")
-q = Tag("q")
-rp = Tag("rp")
-rt = Tag("rt")
-ruby = Tag("ruby")
-s = Tag("s")
-samp = Tag("samp")
-script = Tag("script")
-section = Tag("section")
-select = Tag("select")
-small = Tag("small")
-source = Tag("source", True)
-span = Tag("span")
-strike = Tag("strike")
-strong = Tag("strong")
-style = Tag("style")
-sub = Tag("sub")
-summary = Tag("summary")
-sup = Tag("sup")
-svg = Tag("svg")
-table = Tag("table")
-tbody = Tag("tbody")
-template = Tag("template")
-textarea = Tag("textarea")
-td = Tag("td")
-th = Tag("th")
-thead = Tag("thead")
-time = Tag("time")
-title = Tag("title")
-tr = Tag("tr")
-track = Tag("track", True)
-u = Tag("u")
-ul = Tag("ul")
-var = Tag("var")
-video = Tag("video")
-wbr = Tag("wbr")
diff --git a/simple_html/render.py b/simple_html/render.py
deleted file mode 100644
index aeda80b..0000000
--- a/simple_html/render.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from html import escape
-from types import GeneratorType
-from typing import TYPE_CHECKING, cast, List
-
-from simple_html.nodes import Node, TagTuple, SafeString, Tag
-
-
-def _render(node: Node, strs: List[str]) -> None:
- """
- mutate a list instead of constantly rendering strings
- """
- if type(node) is tuple:
- if len(node) == 3:
- if TYPE_CHECKING:
- node = cast(TagTuple, node)
- strs.append(node[0])
- for child in node[1]:
- _render(child, strs)
- strs.append(node[2])
- else:
- if TYPE_CHECKING:
- node = cast(SafeString, node)
- strs.append(node[0])
- elif isinstance(node, str):
- strs.append(escape(node))
- elif isinstance(node, Tag):
- strs.append(node.rendered)
- elif isinstance(node, list):
- for n in node:
- _render(n, strs)
- elif isinstance(node, GeneratorType):
- for n in node:
- _render(n, strs)
- else:
- raise TypeError(f"Got unknown type: {type(node)}")
-
-
-def render(*nodes: Node) -> str:
- results: List[str] = []
- for node in nodes:
- _render(node, results)
-
- return "".join(results)
diff --git a/tests/test_render.py b/tests/test_render.py
index 2a473a7..f5be2e4 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -1,7 +1,7 @@
import json
from typing import Generator
-from simple_html.nodes import (
+from simple_html import (
safe_string,
a,
body,
@@ -16,9 +16,10 @@
p,
script,
span,
- Node, DOCTYPE_HTML5,
+ Node,
+ DOCTYPE_HTML5,
+ render
)
-from simple_html.render import render
def test_renders_no_children() -> None: