From 5b148c38948dc685f1d5c0c0de4913d2689bd85a Mon Sep 17 00:00:00 2001
From: keithasaurus <592217+keithasaurus@users.noreply.github.com>
Date: Thu, 7 Dec 2023 21:26:30 -0800
Subject: [PATCH] helper to render inline css styles (#10)
* wip
* tests and docs
* wip
* fix
* fix Dict type
* update docs
---
README.md | 23 ++++
pyproject.toml | 2 +-
simple_html/__init__.py | 243 +++++++++++++++++++++++++++++++++++++-
tests/test_simple_html.py | 56 ++++++---
4 files changed, 307 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index f3a3c50..d589b16 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,29 @@ render(
#
```
+You can render inline css styles with `render_styles`:
+```python
+from simple_html import div, render, render_styles
+
+styles = render_styles({"min-width": "25px"})
+
+render(
+ div({"style": styles},
+ "cool")
+)
+# cool
+
+
+# ints and floats are legal values
+styles = render_styles({"padding": 0, "flex-grow": 0.6})
+
+render(
+ div({"style": styles},
+ "wow")
+)
+# wow
+```
+
Lists and generators are both valid collections of nodes:
```python
diff --git a/pyproject.toml b/pyproject.toml
index 284162d..e8a126e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "simple-html"
-version = "1.1.1"
+version = "1.2.0"
readme = "README.md"
description = "Template-less html rendering in Python"
authors = ["Keith Philpott "]
diff --git a/simple_html/__init__.py b/simple_html/__init__.py
index fbae2c4..b36e311 100644
--- a/simple_html/__init__.py
+++ b/simple_html/__init__.py
@@ -1,6 +1,6 @@
from html import escape
from types import GeneratorType
-from typing import Tuple, Union, Dict, List, FrozenSet, Generator, Iterable
+from typing import Tuple, Union, Dict, List, FrozenSet, Generator, Iterable, Any
class SafeString:
@@ -12,6 +12,12 @@ def __init__(self, safe_str: str) -> None:
def __hash__(self) -> int:
return hash(f"SafeString__{self.safe_str}")
+ def __eq__(self, other: Any) -> bool:
+ return isinstance(other, SafeString) and other.safe_str == self.safe_str
+
+ def __repr__(self) -> str:
+ return f"SafeString(safe_str='{self.safe_str}')"
+
Node = Union[
str,
@@ -273,6 +279,241 @@ def _render(nodes: Iterable[Node], strs: List[str]) -> None:
raise TypeError(f"Got unknown type: {type(node)}")
+_common_safe_css_props = frozenset(
+ {
+ "color",
+ "border",
+ "margin",
+ "font-style",
+ "transform",
+ "background-color",
+ "align-content",
+ "align-items",
+ "align-self",
+ "all",
+ "animation",
+ "animation-delay",
+ "animation-direction",
+ "animation-duration",
+ "animation-fill-mode",
+ "animation-iteration-count",
+ "animation-name",
+ "animation-play-state",
+ "animation-timing-function",
+ "backface-visibility",
+ "background",
+ "background-attachment",
+ "background-blend-mode",
+ "background-clip",
+ "background-color",
+ "background-image",
+ "background-origin",
+ "background-position",
+ "background-repeat",
+ "background-size",
+ "border",
+ "border-bottom",
+ "border-bottom-color",
+ "border-bottom-left-radius",
+ "border-bottom-right-radius",
+ "border-bottom-style",
+ "border-bottom-width",
+ "border-collapse",
+ "border-color",
+ "border-image",
+ "border-image-outset",
+ "border-image-repeat",
+ "border-image-slice",
+ "border-image-source",
+ "border-image-width",
+ "border-left",
+ "border-left-color",
+ "border-left-style",
+ "border-left-width",
+ "border-radius",
+ "border-right",
+ "border-right-color",
+ "border-right-style",
+ "border-right-width",
+ "border-spacing",
+ "border-style",
+ "border-top",
+ "border-top-color",
+ "border-top-left-radius",
+ "border-top-right-radius",
+ "border-top-style",
+ "border-top-width",
+ "border-width",
+ "bottom",
+ "box-shadow",
+ "box-sizing",
+ "caption-side",
+ "caret-color",
+ "@charset",
+ "clear",
+ "clip",
+ "clip-path",
+ "color",
+ "column-count",
+ "column-fill",
+ "column-gap",
+ "column-rule",
+ "column-rule-color",
+ "column-rule-style",
+ "column-rule-width",
+ "column-span",
+ "column-width",
+ "columns",
+ "content",
+ "counter-increment",
+ "counter-reset",
+ "cursor",
+ "direction",
+ "display",
+ "empty-cells",
+ "filter",
+ "flex",
+ "flex-basis",
+ "flex-direction",
+ "flex-flow",
+ "flex-grow",
+ "flex-shrink",
+ "flex-wrap",
+ "float",
+ "font",
+ "@font-face",
+ "font-family",
+ "font-kerning",
+ "font-size",
+ "font-size-adjust",
+ "font-stretch",
+ "font-style",
+ "font-variant",
+ "font-weight",
+ "grid",
+ "grid-area",
+ "grid-auto-columns",
+ "grid-auto-flow",
+ "grid-auto-rows",
+ "grid-column",
+ "grid-column-end",
+ "grid-column-gap",
+ "grid-column-start",
+ "grid-gap",
+ "grid-row",
+ "grid-row-end",
+ "grid-row-gap",
+ "grid-row-start",
+ "grid-template",
+ "grid-template-areas",
+ "grid-template-columns",
+ "grid-template-rows",
+ "height",
+ "hyphens",
+ "@import",
+ "justify-content",
+ "@keyframes",
+ "left",
+ "letter-spacing",
+ "line-height",
+ "list-style",
+ "list-style-image",
+ "list-style-position",
+ "list-style-type",
+ "margin",
+ "margin-bottom",
+ "margin-left",
+ "margin-right",
+ "margin-top",
+ "max-height",
+ "max-width",
+ "@media",
+ "min-height",
+ "min-width",
+ "object-fit",
+ "object-position",
+ "opacity",
+ "order",
+ "outline",
+ "outline-color",
+ "outline-offset",
+ "outline-style",
+ "outline-width",
+ "overflow",
+ "overflow-x",
+ "overflow-y",
+ "padding",
+ "padding-bottom",
+ "padding-left",
+ "padding-right",
+ "padding-top",
+ "page-break-after",
+ "page-break-before",
+ "page-break-inside",
+ "perspective",
+ "perspective-origin",
+ "pointer-events",
+ "position",
+ "quotes",
+ "right",
+ "scroll-behavior",
+ "table-layout",
+ "text-align",
+ "text-align-last",
+ "text-decoration",
+ "text-decoration-color",
+ "text-decoration-line",
+ "text-decoration-style",
+ "text-indent",
+ "text-justify",
+ "text-overflow",
+ "text-shadow",
+ "text-transform",
+ "top",
+ "transform",
+ "transform-origin",
+ "transform-style",
+ "transition",
+ "transition-delay",
+ "transition-duration",
+ "transition-property",
+ "transition-timing-function",
+ "user-select",
+ "vertical-align",
+ "visibility",
+ "white-space",
+ "width",
+ "word-break",
+ "word-spacing",
+ "word-wrap",
+ "writing-mode",
+ "z-index",
+ }
+)
+
+
+def render_styles(
+ styles: Dict[Union[str, SafeString], Union[str, int, float, SafeString]]
+) -> SafeString:
+ ret = ""
+ for k, v in styles.items():
+ if k not in _common_safe_css_props:
+ if isinstance(k, SafeString):
+ k = k.safe_str
+ else:
+ k = escape(k, True)
+
+ if isinstance(v, SafeString):
+ v = v.safe_str
+ elif isinstance(v, str):
+ v = escape(v, True)
+ # note that ints and floats pass through these condition checks
+
+ ret += f"{k}:{v};"
+
+ return SafeString(ret)
+
+
def render(*nodes: Node) -> str:
results: List[str] = []
_render(nodes, results)
diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py
index 0d29484..c75178f 100644
--- a/tests/test_simple_html.py
+++ b/tests/test_simple_html.py
@@ -19,7 +19,7 @@
Node,
DOCTYPE_HTML5,
render,
- escape_attribute_key,
+ escape_attribute_key, render_styles,
)
@@ -114,8 +114,8 @@ def test_kw_attributes() -> None:
node = div({"class": "first", "name": "some_name", "style": "color:blue;"}, "okok")
assert (
- render(node)
- == 'okok
'
+ render(node)
+ == 'okok
'
)
@@ -156,8 +156,8 @@ def test_render_kw_attribute_with_none() -> None:
def test_can_render_empty() -> None:
assert render([]) == ""
assert (
- render(div({}, [], "hello ", [], span({}, "World!"), []))
- == "hello World!
"
+ render(div({}, [], "hello ", [], span({}, "World!"), []))
+ == "hello World!
"
)
@@ -175,24 +175,24 @@ def test_escape_key() -> None:
assert escape_attribute_key("=") == "="
assert escape_attribute_key("`") == "`"
assert (
- escape_attribute_key("something with spaces")
- == "something with spaces"
+ escape_attribute_key("something with spaces")
+ == "something with spaces"
)
def test_render_with_escaped_attributes() -> None:
assert (
- render(div({'onmousenter="alert(1)" noop': "1"}))
- == ''
+ render(div({'onmousenter="alert(1)" noop': "1"}))
+ == ''
)
assert (
- render(span({"": "\">"}))
- == ''
+ render(span({"": "\">"}))
+ == ''
)
# vals and keys escape slightly differently
assert (
- render(div({'onmousenter="alert(1)" noop': 'onmousenter="alert(1)" noop'}))
- == ''
+ render(div({'onmousenter="alert(1)" noop': 'onmousenter="alert(1)" noop'}))
+ == ''
)
@@ -200,6 +200,32 @@ def test_render_with_safestring_attributes() -> None:
bad_key = 'onmousenter="alert(1)" noop'
bad_val = ""
assert (
- render(div({SafeString(bad_key): SafeString(bad_val)}))
- == f''
+ render(div({SafeString(bad_key): SafeString(bad_val)}))
+ == f''
+ )
+
+
+def test_safestring_repr() -> None:
+ assert repr(SafeString("abc123")) == "SafeString(safe_str='abc123')"
+
+
+def test_safe_string_eq() -> None:
+ assert "abc123" != SafeString("abc123")
+ assert SafeString("a") != SafeString("abc123")
+ assert SafeString("abc123") == SafeString("abc123")
+
+
+def test_render_styles() -> None:
+ assert render_styles({}) == SafeString("")
+ assert render_styles({"abc": 123.45}) == SafeString("abc:123.45;")
+ assert render_styles({"padding": 0,
+ "margin": "0 10"}) == SafeString("padding:0;margin:0 10;")
+
+ assert render(div({"style": render_styles({"min-width": "25px"})},
+ "cool")) == 'cool
'
+
+
+def test_render_styles_escapes() -> None:
+ assert render_styles({'"><': "><>\""}) == SafeString(
+ safe_str='"><:><>";'
)