Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,57 @@ a([tab]
## Combine the two
div(attrs=[div.class_("flex")], class_={"dark-mode": True})
# <div class="flex dark-mode"></div>

# Style produces basic css statements
# but does no special quoting
div(style={
"flex-direction": "row",
"background": "indigo"
})
# <div style="flex-direction: row; background: indigo">

class Flex:
"""
Flexbox helper class with potentially
clearer language
"""
flow_from = Literal
["start",
"end",
"center",
"between",
"around",
"evenly"
]
across_items = Literal[
"start",
"end",
"center",
"stretch",
"baseline",
]

@staticmethod
def row(
flow_from: flow_from | None = None,
cross: across_items | None = None,
gap: str | int = 0,
size_basis: str | int | None = None,
) -> str:
"""
Standard row flexbox - left to right

:param flow_from: item positioning rule (justify-content)
:param cross: cross axis positioning rule (align-items)
:param gap: spacing between items (px if int, otherwise raw CSS)
:param size_basis: flex-basis value - initial size. px if int, otherwise raw CSS
:return: CSS style string
"""
return Flex._impl("row", flow_from, cross, gap, size_basis)

div(style=Flex.row(flow_from"center", cross="stretch"))

# <div style="flex-direction: row; justify-content: center; align-items: stretch;"></div>
```

* 🎭 Type hints for the editor generated from WhatWG spec
Expand Down
7 changes: 7 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 0.10.1
* Style parameter: when input is a dict[str, str], assume the user wants simple f"{key}: {value}" css statements
* Type hints: Cleanup type hints to all be 3.10 style
* Fix pretty print not accepting custom beautifulsoup encoder
* [breaking] Fix base_attribute "delimiter" argument spelling
This is considered a minor but potentially breaking change.

# 0.10.0
* onclick/onaction attrs are now generated as kwargs for elements.
* Improve typing: class and style params now correctly type hint, as do other
Expand Down
2 changes: 1 addition & 1 deletion doc/ideas/02_base_element.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The same rule is applied anywhere else a name conflicts with a keyword i.e. the

### Repeat Attributes

In the event `class` or `style` occur multiple times, they are concatenated with the correct delimeter in the order they're received.
In the event `class` or `style` occur multiple times, they are concatenated with the correct delimiter in the order they're received.

Because there's no clear way to concat other attributes, an exception is raised.

Expand Down
28 changes: 28 additions & 0 deletions doc/ideas/04_attrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ div._.class_("red")
# "red"
```

An easy mistake is getting caught assuming the dictionary will resolve the value
```python
from html_compose import div

# This is NOT the correct way to use a dictionary
div.hint.class_({
'color': "red", # ❌ Incorrect use
}
)
# "color" ❌ is likely not what you wanted

```

An exception to the rule is `style`
```python
from html_compose import div

# the style attribute has special handling.
div.hint.style({
'background': "red", # OK
"flow-direction": "row"
}
)
"background: red; flow-directionn: row"
```
The implementation is the simplest `<key>: <value>.
User is therefore responsible for quoting.

## `attrs=` parameter syntax

In the constructor for any element, you can specify the `attrs` parameter.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "html-compose"
version = "0.10.0"
version = "0.10.1"
description = "Composable HTML generation in python"
authors = [
{ name = "jealouscloud", email = "github@noaha.org" }
Expand Down
6 changes: 2 additions & 4 deletions src/html_compose/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@
.. include:: ../../doc/ideas/05_livereload.md
"""

from typing import Union

from markupsafe import Markup, escape


Expand All @@ -95,7 +93,7 @@ def escape_text(value) -> Markup:
return escape(str(value))


def unsafe_text(value: Union[str, Markup]) -> Markup:
def unsafe_text(value: str | Markup) -> Markup:
"""
Return input string as Markup

Expand All @@ -121,7 +119,7 @@ def pretty_print(html_str: str, features="html.parser") -> str:
# so we lazy load bs4
from bs4 import BeautifulSoup # type: ignore[import-untyped]

return BeautifulSoup(html_str, features="html.parser").prettify(
return BeautifulSoup(html_str, features=features).prettify(
formatter="html5"
)

Expand Down
8 changes: 4 additions & 4 deletions src/html_compose/attributes/global_attrs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from . import BaseAttribute
from typing import Literal
from typing import Literal, Iterable, Mapping
from ..base_types import Resolvable, StrLike


Expand Down Expand Up @@ -60,7 +60,7 @@ def autofocus(value: bool) -> BaseAttribute:
return BaseAttribute("autofocus", value)

@staticmethod
def class_(value: Resolvable) -> BaseAttribute:
def class_(value: StrLike | Iterable[StrLike]) -> BaseAttribute:
"""
"global" attribute: class
Classes to which the element belongs
Expand Down Expand Up @@ -317,7 +317,7 @@ def spellcheck(value: Literal["true", "false", ""]) -> BaseAttribute:
return BaseAttribute("spellcheck", value)

@staticmethod
def style(value: Resolvable) -> BaseAttribute:
def style(value: Resolvable | Mapping[StrLike, StrLike]) -> BaseAttribute:
"""
"global" attribute: style
Presentational and formatting instructions
Expand All @@ -326,7 +326,7 @@ def style(value: Resolvable) -> BaseAttribute:
:return: An style attribute to be added to your element
""" # fmt: skip

return BaseAttribute("style", value)
return BaseAttribute("style", value, delimiter="; ")

@staticmethod
def tabindex(value: int) -> BaseAttribute:
Expand Down
36 changes: 26 additions & 10 deletions src/html_compose/base_attribute.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable, Tuple, Union
from typing import Iterable, Tuple

from markupsafe import Markup

Expand All @@ -16,21 +16,21 @@ class BaseAttribute:
Attribute Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
"""

__slots__ = ("name", "data", "delimeter")
__slots__ = ("name", "data", "delimiter")

def __init__(
self, name: str, data: Resolvable = None, delimeter: str = " "
self, name: str, data: Resolvable = None, delimiter: str = " "
):
self.name = name
self.data = data
self.delimeter = delimeter
self.delimiter = delimiter

def resolve_join(self, input_data: Iterable):
"""
Join a list of strings
Split out for implementors to override
"""
return self.delimeter.join(
return self.delimiter.join(
x if isinstance(x, str) else str(x) for x in input_data
)

Expand Down Expand Up @@ -58,7 +58,18 @@ def dict_string_generator(self, data):
continue
yield key

def resolve_data(self) -> Union[None, str]:
def dict_style_string_generator(self, data):
"""
Resolve dictionary key, value into css statement pairs


The implementation is the simplest `<key>: <value>.
User is therefore responsible for quoting.
"""
for key, value in data.items():
yield f"{key}: {value}"

def resolve_data(self) -> str | None:
"""
Resolve right half of attribute into a string

Expand Down Expand Up @@ -89,13 +100,18 @@ def resolve_data(self) -> Union[None, str]:
_resolved = self.list_string_generator(data)
# dictionary of key value pairs
elif isinstance(data, dict):
_resolved = self.dict_string_generator(data)
if self.name == "style":
# Magic: Style attribute with key-value pairs procudes
# basic css statements.
_resolved = self.dict_style_string_generator(data)
else:
_resolved = self.dict_string_generator(data)
else:
raise ValueError(f"Input data type {data} not supported")

return self.resolve_join(_resolved)

def evaluate(self) -> Union[None, Tuple[str, str]]:
def evaluate(self) -> Tuple[str, str] | None:
"""
Evaluate attribute, return key, value as tuple
or None if attribute is falsey
Expand All @@ -110,8 +126,8 @@ def evaluate(self) -> Union[None, Tuple[str, str]]:
return (self.name, resolved)

def __repr__(self):
if self.delimeter != " ":
return f"BaseAttribute{{name={repr(self.name)}, data={repr(self.data)}, delimeter={repr(self.data)}}}"
if self.delimiter != " ":
return f"BaseAttribute{{name={repr(self.name)}, data={repr(self.data)}, delimiter={repr(self.data)}}}"

return (
f"BaseAttribute{{name={repr(self.name)}, data={repr(self.data)}}}"
Expand Down
12 changes: 7 additions & 5 deletions src/html_compose/base_element.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable, Generator, Iterable, Mapping, Optional, Union
from typing import Callable, Generator, Iterable, Mapping

from . import escape_text, unsafe_text, util_funcs
from .attributes import BaseAttribute, GlobalAttrs
Expand Down Expand Up @@ -58,7 +58,7 @@ def __init__(
BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable]
]
| None = None,
children: Optional[list] = None,
children: list | None = None,
) -> None:
"""
Initialize an HTML element
Expand All @@ -84,15 +84,17 @@ def __eq__(self, other):

return False

def _process_attr(self, attr_name, attr_data):
def _process_attr(
self, attr_name: str, attr_data: str | Resolvable | BaseAttribute | None
):
"""
Add an attribute for the element to the internal _attrs dict
We technically allow stacking for supported attributes.
This allows us to support (combine) attributes like "class" and "style".

Args:
attr_name (str): The name of the attribute.
attr_data (Union[str, BaseAttribute]): The data for the attribute.
attr_data (str | Resolvable): The data for the attribute.
"""
if attr_data is None or attr_data is False:
return # noop
Expand Down Expand Up @@ -277,7 +279,7 @@ def _resolve_child(

def _resolve_tree(
self, parent=None
) -> Generator[Union[str, Callable], None, None]:
) -> Generator[str | Callable, None, None]:
"""
Walk html element tree and yield all resolved children

Expand Down
18 changes: 11 additions & 7 deletions src/html_compose/custom_element.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Optional, Union
from typing import Iterable, Mapping

from .attributes import BaseAttribute, GlobalAttrs
from .base_element import BaseElement
from .base_types import Resolvable
from .util_funcs import safe_name


Expand All @@ -15,12 +16,15 @@ class CustomElement(BaseElement):

def __init__(
self,
attrs: Optional[
Union[dict[str, Union[str, dict, list]], list[BaseAttribute]]
] = None,
id: Optional[str] = None,
class_: Optional[Union[str, list]] = None,
children: Optional[list] = None,
attrs: Iterable[BaseAttribute]
| Mapping[str, Resolvable]
| Iterable[
BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable]
]
| None = None,
id: str | None = None,
class_: str | list | None = None,
children: list | None = None,
):
"""
Initialize a custom HTML element
Expand Down
16 changes: 8 additions & 8 deletions src/html_compose/document.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from typing import Optional, Union

from . import base_types, doctype, pretty_print, unsafe_text
from . import elements as el
from .util_funcs import get_livereload_env


def HTML5Document(
title: Optional[str] = None,
lang: Optional[str] = None,
head: Optional[list] = None,
body: Union[list[base_types.Node], el.body, None] = None,
prettify: Union[bool, str] = False,
title: str | None = None,
lang: str | None = None,
head: list | None = None,
body: list[base_types.Node] | el.body | None = None,
prettify: bool | str = False,
) -> str:
"""
Return an HTML5 document with the given title and content.
Expand Down Expand Up @@ -63,7 +61,9 @@ def HTML5Document(
html = el.html(lang=lang)[head_el, body_el]
result = f"{header}\n{html.render()}"
if prettify:
return pretty_print(result)
if prettify is True:
return pretty_print(result)
return pretty_print(result, features=prettify)
else:
return result

Expand Down
6 changes: 3 additions & 3 deletions src/html_compose/elements/a_element.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Optional, Iterable, Mapping
from typing import Literal, Iterable, Mapping

from ..attributes import GlobalAttrs, AnchorAttrs
from ..base_attribute import BaseAttribute
Expand Down Expand Up @@ -164,14 +164,14 @@ def __init__(
popover: Literal["auto", "manual"] | StrLike | None = None,
slot: StrLike | None = None,
spellcheck: Literal["true", "false", ""] | StrLike | None = None,
style: Resolvable | None = None,
style: Resolvable | Mapping[StrLike, StrLike] | None = None,
tabindex: int | StrLike | None = None,
title: StrLike | None = None,
translate: Literal["yes", "no"] | StrLike | None = None,
writingsuggestions: Literal["true", "false", ""]
| StrLike
| None = None,
children: Optional[list] = None,
children: list | None = None,
) -> None:
"""
Initialize 'a' (Hyperlink) element.
Expand Down
Loading