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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Python Hiccup

This project started out as a fun challenge, and now evolving into something that could be useful.

Current status: _experimental_
Python Hiccup is a library for representing HTML using plain Python data structures.

[![CircleCI](https://dl.circleci.com/status-badge/img/gh/DavidVujic/python-hiccup/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DavidVujic/python-hiccup/tree/main)

Expand All @@ -14,6 +12,8 @@ Current status: _experimental_
This is a Python implementation of the Hiccup syntax. Python Hiccup is a library for representing HTML in Python.
Using `list` or `tuple` to represent HTML elements, and `dict` to represent the element attributes.

_This project started out as a fun coding challenge, and now evolving into something useful for Python Dev teams._

## Usage
Create server side HTML using plain Python data structures.
This library should also be possible to combine with PyScript, but I haven't tested that out yet.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "python-hiccup"
version = "0.2.0"
description = "Add your description here"
version = "0.3.0"
description = "Python Hiccup is a library for representing HTML using plain Python data structures"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
Expand Down
19 changes: 12 additions & 7 deletions src/python_hiccup/html/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Mapping, Sequence
from functools import reduce

from python_hiccup.transform import transform
from python_hiccup.transform import CONTENT_TAG, transform


def _element_allows_raw_content(element: str) -> bool:
Expand Down Expand Up @@ -56,23 +56,28 @@ def _suffix(element_data: str) -> str:
return "" if any(s in normalized for s in specials) else " /"


def _to_html(tag: Mapping) -> list:
def _is_content(element: str) -> bool:
return element == CONTENT_TAG


def _to_html(tag: Mapping, parent: str = "") -> list:
element = next(iter(tag.keys()))
child = next(iter(tag.values()))

if _is_content(element):
return [_escape(str(child), parent)]

attributes = reduce(_to_attributes, tag.get("attributes", []), "")
bool_attributes = reduce(_to_bool_attributes, tag.get("boolean_attributes", []), "")
element_attributes = attributes + bool_attributes

content = [_escape(str(c), element) for c in tag.get("content", [])]

matrix = [_to_html(c) for c in child]
matrix = [_to_html(c, element) for c in child]
flattened: list = reduce(operator.iadd, matrix, [])

begin = f"{element}{element_attributes}" if element_attributes else element

if flattened or content:
return [f"<{begin}>", *flattened, *content, f"</{element}>"]
if flattened:
return [f"<{begin}>", *flattened, f"</{element}>"]

if _closing_tag(element):
return [f"<{begin}>", f"</{element}>"]
Expand Down
4 changes: 2 additions & 2 deletions src/python_hiccup/transform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Transform Hiccup syntax."""

from python_hiccup.transform.core import transform
from python_hiccup.transform.core import CONTENT_TAG, transform

__all__ = ["transform"]
__all__ = ["CONTENT_TAG", "transform"]
14 changes: 5 additions & 9 deletions src/python_hiccup/transform/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
ATTRIBUTES = "attributes"
BOOLEAN_ATTRIBUTES = "boolean_attributes"
CHILDREN = "children"
CONTENT = "content"

CONTENT_TAG = "<::HICCUP_CONTENT::>"


def _is_attribute(item: Item) -> bool:
Expand All @@ -25,12 +26,6 @@ def _is_child(item: Item) -> bool:
return isinstance(item, list | tuple)


def _is_content(item: Item) -> bool:
pipeline = [_is_attribute, _is_boolean_attribute, _is_child]

return not any(fn(item) for fn in pipeline)


def _is_sibling(item: Item) -> bool:
return _is_child(item)

Expand All @@ -40,8 +35,6 @@ def _key_for_group(item: Item) -> str:
return ATTRIBUTES
if _is_boolean_attribute(item):
return BOOLEAN_ATTRIBUTES
if _is_content(item):
return CONTENT

return CHILDREN

Expand All @@ -67,6 +60,9 @@ def _extract_from_tag(tag: str) -> tuple[str, dict]:


def _transform_tags(tags: Sequence) -> dict:
if not isinstance(tags, list | tuple):
return {CONTENT_TAG: tags}

first, *rest = tags

element, extracted = _extract_from_tag(first)
Expand Down
11 changes: 9 additions & 2 deletions test/test_render_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ def test_generates_an_element_with_children() -> None:

def test_allows_numeric_values_in_content() -> None:
"""Assert that numeric values are allowed as the content of an element."""
data = ["ul", ["li", 1]]
data = ["ul", ["li", 1], ["li", 2.2]]

assert render(data) == "<ul><li>1</li></ul>"
assert render(data) == "<ul><li>1</li><li>2.2</li></ul>"


def test_order_of_items() -> None:
"""Assert that items of different types are ordered as expected."""
data = ["h1", "some ", ["span.pys", "<py>"]]

assert render(data) == '<h1>some <span class="pys">&lt;py&gt;</span></h1>'
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.