From 6f882c0658ed31d6580043fd64756150c8a9d09c Mon Sep 17 00:00:00 2001 From: Sean N Date: Mon, 10 Nov 2025 21:45:51 +0200 Subject: [PATCH 1/2] Cleanups --- src/ydnatl/core/element.py | 18 +++--- src/ydnatl/core/fragment.py | 13 +++-- src/ydnatl/tags/tag_factory.py | 1 + tests/test_element.py | 102 +++++++++++++++++---------------- tests/test_fragment.py | 90 +++++++---------------------- 5 files changed, 93 insertions(+), 131 deletions(-) diff --git a/src/ydnatl/core/element.py b/src/ydnatl/core/element.py index fdf97f1..d17a402 100644 --- a/src/ydnatl/core/element.py +++ b/src/ydnatl/core/element.py @@ -5,18 +5,18 @@ import uuid from typing import Callable, Any, Iterator, Union, List, TypeVar -T = TypeVar('T', bound='HTMLElement') +T = TypeVar("T", bound="HTMLElement") class HTMLElement: __slots__ = ["_tag", "_children", "_text", "_attributes", "_self_closing"] def __init__( - self, - *children: Union["HTMLElement", str, List[Any]], - tag: str, - self_closing: bool = False, - **attributes: str, + self, + *children: Union["HTMLElement", str, List[Any]], + tag: str, + self_closing: bool = False, + **attributes: str, ): PRESERVE_UNDERSCORE = {"class_name"} @@ -100,7 +100,7 @@ def append(self, *children: Union["HTMLElement", str, List[Any]]) -> "HTMLElemen return self def filter( - self, condition: Callable[[Any], bool], recursive: bool = False + self, condition: Callable[[Any], bool], recursive: bool = False ) -> Iterator["HTMLElement"]: """Yields children (and optionally descendants) that meet the condition.""" for child in self._children: @@ -288,7 +288,7 @@ def replace_child(self, old_index: int, new_child: "HTMLElement") -> None: self._children[old_index] = new_child def find_by_attribute( - self, attr_name: str, attr_value: Any + self, attr_name: str, attr_value: Any ) -> Union["HTMLElement", None]: """Finds a child by an attribute.""" @@ -470,7 +470,7 @@ def from_dict(cls, data: dict) -> "HTMLElement": element = cls( tag=data["tag"], self_closing=data.get("self_closing", False), - **data.get("attributes", {}) + **data.get("attributes", {}), ) if "text" in data and data["text"]: diff --git a/src/ydnatl/core/fragment.py b/src/ydnatl/core/fragment.py index d779a15..3d21c2f 100644 --- a/src/ydnatl/core/fragment.py +++ b/src/ydnatl/core/fragment.py @@ -16,7 +16,9 @@ class Fragment(HTMLElement): # Instead of:

Title

Content

""" - def __init__(self, *children: Union["HTMLElement", str, List[Any]], **attributes: str): + def __init__( + self, *children: Union["HTMLElement", str, List[Any]], **attributes: str + ): # Initialize with a dummy tag name since we won't render it super().__init__(*children, tag="fragment", **attributes) @@ -35,19 +37,18 @@ def render(self, pretty: bool = False, _indent: int = 0) -> str: # Render only children, not the fragment tag itself if pretty: result = "".join( - child.render(pretty=True, _indent=_indent) - for child in self._children + child.render(pretty=True, _indent=_indent) for child in self._children ) else: result = "".join( - child.render(pretty=False, _indent=_indent) - for child in self._children + child.render(pretty=False, _indent=_indent) for child in self._children ) # Include text content if any if self._text: import html + result = html.escape(self._text) + result self.on_after_render() - return result \ No newline at end of file + return result diff --git a/src/ydnatl/tags/tag_factory.py b/src/ydnatl/tags/tag_factory.py index 7db3142..21855fc 100644 --- a/src/ydnatl/tags/tag_factory.py +++ b/src/ydnatl/tags/tag_factory.py @@ -21,6 +21,7 @@ def __init__(self, *args, **kwargs): **({"self_closing": True} if self_closing else {}), }, ) + # @NOTE __qualname__ for serialization class_name = tag.capitalize() if tag.islower() else tag _Tag.__name__ = class_name diff --git a/tests/test_element.py b/tests/test_element.py index 382cd43..f458381 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -246,6 +246,7 @@ def test_html_escaping_normal_content(self): def test_to_json_simple_element(self): """Test serialization of a simple element to JSON.""" import json + element = HTMLElement("Hello", tag="div", id="test", class_name="container") json_str = element.to_json() self.assertIsInstance(json_str, str) @@ -270,6 +271,7 @@ def test_to_json_nested_elements(self): json_str = parent.to_json() import json + data = json.loads(json_str) self.assertEqual(len(data["children"]), 2) @@ -283,7 +285,7 @@ def test_from_dict_simple_element(self): "self_closing": False, "attributes": {"id": "test", "class": "container"}, "text": "Hello", - "children": [] + "children": [], } element = HTMLElement.from_dict(data) @@ -300,7 +302,7 @@ def test_from_dict_self_closing_element(self): "self_closing": True, "attributes": {}, "text": "", - "children": [] + "children": [], } element = HTMLElement.from_dict(data) @@ -321,16 +323,16 @@ def test_from_dict_nested_elements(self): "self_closing": False, "attributes": {}, "text": "Child 1", - "children": [] + "children": [], }, { "tag": "span", "self_closing": False, "attributes": {}, "text": "Child 2", - "children": [] - } - ] + "children": [], + }, + ], } element = HTMLElement.from_dict(data) @@ -374,12 +376,9 @@ def test_round_trip_serialization(self): section = HTMLElement(tag="section") section.append( HTMLElement("Paragraph 1", tag="p"), - HTMLElement("Paragraph 2", tag="p", class_name="highlight") - ) - original.append( - HTMLElement("Header", tag="h1"), - section + HTMLElement("Paragraph 2", tag="p", class_name="highlight"), ) + original.append(HTMLElement("Header", tag="h1"), section) json_str = original.to_json() @@ -400,7 +399,7 @@ def test_round_trip_with_attributes(self): type="text", name="username", placeholder="Enter username", - data_validation="required" + data_validation="required", ) json_str = original.to_json() @@ -408,8 +407,13 @@ def test_round_trip_with_attributes(self): self.assertEqual(original.get_attribute("type"), restored.get_attribute("type")) self.assertEqual(original.get_attribute("name"), restored.get_attribute("name")) - self.assertEqual(original.get_attribute("placeholder"), restored.get_attribute("placeholder")) - self.assertEqual(original.get_attribute("data-validation"), restored.get_attribute("data-validation")) + self.assertEqual( + original.get_attribute("placeholder"), restored.get_attribute("placeholder") + ) + self.assertEqual( + original.get_attribute("data-validation"), + restored.get_attribute("data-validation"), + ) self.assertEqual(str(original), str(restored)) def test_serialization_deeply_nested(self): @@ -426,22 +430,22 @@ def test_serialization_deeply_nested(self): json_str = root.to_json() restored = HTMLElement.from_json(json_str) - self.assertEqual(restored.children[0].children[0].children[0].text, "Deep content") + self.assertEqual( + restored.children[0].children[0].children[0].text, "Deep content" + ) self.assertEqual(str(root), str(restored)) - def test_render_pretty_simple(self): """Test pretty printing with simple nested structure.""" element = HTMLElement(tag="div", id="container") - element.append( - HTMLElement("Hello", tag="h1"), - HTMLElement("World", tag="p") - ) + element.append(HTMLElement("Hello", tag="h1"), HTMLElement("World", tag="p")) # Test compact (default) compact = element.render(pretty=False) self.assertNotIn("\n", compact) - self.assertEqual(compact, '

Hello

World

') + self.assertEqual( + compact, '

Hello

World

' + ) # Test pretty pretty = element.render(pretty=True) @@ -482,11 +486,7 @@ def test_add_style_single(self): def test_add_styles_multiple(self): """Test adding multiple CSS styles at once.""" element = HTMLElement(tag="div") - element.add_styles({ - "color": "blue", - "font-size": "14px", - "margin": "10px" - }) + element.add_styles({"color": "blue", "font-size": "14px", "margin": "10px"}) style = element.get_attribute("style") self.assertIn("color: blue", style) @@ -514,11 +514,7 @@ def test_get_style(self): def test_remove_style(self): """Test removing CSS styles.""" element = HTMLElement(tag="div") - element.add_styles({ - "color": "red", - "font-size": "14px", - "margin": "10px" - }) + element.add_styles({"color": "red", "font-size": "14px", "margin": "10px"}) element.remove_style("margin") @@ -540,7 +536,9 @@ def test_remove_style_last_one(self): def test_parse_styles(self): """Test the _parse_styles static method.""" - styles_dict = HTMLElement._parse_styles("color: red; font-size: 14px; margin: 10px") + styles_dict = HTMLElement._parse_styles( + "color: red; font-size: 14px; margin: 10px" + ) self.assertEqual(styles_dict["color"], "red") self.assertEqual(styles_dict["font-size"], "14px") @@ -586,9 +584,11 @@ def test_method_chaining_append(self): self.assertIs(result, element) # Test actual chaining - element = (HTMLElement(tag="div") - .append(HTMLElement(tag="h1")) - .append(HTMLElement(tag="p"))) + element = ( + HTMLElement(tag="div") + .append(HTMLElement(tag="h1")) + .append(HTMLElement(tag="p")) + ) self.assertEqual(element.count_children(), 2) @@ -607,9 +607,11 @@ def test_method_chaining_add_attribute(self): self.assertIs(result, element) # Test actual chaining - element = (HTMLElement(tag="div") - .add_attribute("id", "main") - .add_attribute("class", "container")) + element = ( + HTMLElement(tag="div") + .add_attribute("id", "main") + .add_attribute("class", "container") + ) self.assertEqual(element.get_attribute("id"), "main") self.assertEqual(element.get_attribute("class"), "container") @@ -636,9 +638,11 @@ def test_method_chaining_add_style(self): self.assertIs(result, element) # Test actual chaining - element = (HTMLElement(tag="div") - .add_style("color", "blue") - .add_style("font-size", "14px")) + element = ( + HTMLElement(tag="div") + .add_style("color", "blue") + .add_style("font-size", "14px") + ) self.assertEqual(element.get_style("color"), "blue") self.assertEqual(element.get_style("font-size"), "14px") @@ -676,13 +680,15 @@ def test_method_chaining_remove_all(self): def test_method_chaining_complex(self): """Test complex method chaining scenario.""" - element = (HTMLElement(tag="div") - .add_attribute("id", "container") - .add_attribute("class", "wrapper") - .add_style("background", "#f0f0f0") - .add_styles({"padding": "20px", "margin": "10px"}) - .append(HTMLElement("Title", tag="h1")) - .append(HTMLElement("Content", tag="p"))) + element = ( + HTMLElement(tag="div") + .add_attribute("id", "container") + .add_attribute("class", "wrapper") + .add_style("background", "#f0f0f0") + .add_styles({"padding": "20px", "margin": "10px"}) + .append(HTMLElement("Title", tag="h1")) + .append(HTMLElement("Content", tag="p")) + ) self.assertEqual(element.get_attribute("id"), "container") self.assertEqual(element.get_attribute("class"), "wrapper") diff --git a/tests/test_fragment.py b/tests/test_fragment.py index 35f1461..821032e 100644 --- a/tests/test_fragment.py +++ b/tests/test_fragment.py @@ -10,10 +10,7 @@ class TestFragment(unittest.TestCase): def test_fragment_basic(self): """Test basic Fragment rendering without wrapper tag.""" - fragment = Fragment( - H1("Title"), - Paragraph("Content") - ) + fragment = Fragment(H1("Title"), Paragraph("Content")) rendered = str(fragment) self.assertEqual(rendered, "

Title

Content

") @@ -28,9 +25,7 @@ def test_fragment_empty(self): def test_fragment_single_child(self): """Test Fragment with single child.""" - fragment = Fragment( - Paragraph("Single child") - ) + fragment = Fragment(Paragraph("Single child")) rendered = str(fragment) self.assertEqual(rendered, "

Single child

") @@ -41,7 +36,7 @@ def test_fragment_multiple_children(self): H1("Header"), Paragraph("Paragraph 1"), Paragraph("Paragraph 2"), - Span("Inline text") + Span("Inline text"), ) rendered = str(fragment) @@ -51,13 +46,7 @@ def test_fragment_multiple_children(self): def test_fragment_nested_elements(self): """Test Fragment with nested HTML elements.""" fragment = Fragment( - Div( - H1("Title"), - Paragraph("Content") - ), - Section( - Paragraph("More content") - ) + Div(H1("Title"), Paragraph("Content")), Section(Paragraph("More content")) ) rendered = str(fragment) @@ -75,11 +64,7 @@ def test_fragment_with_text(self): def test_fragment_mixed_text_and_elements(self): """Test Fragment with both text and elements.""" - fragment = Fragment( - "Text before", - Paragraph("Middle"), - "Text after" - ) + fragment = Fragment("Text before", Paragraph("Middle"), "Text after") # Note: text is handled differently, need to test actual output rendered = str(fragment) @@ -88,11 +73,7 @@ def test_fragment_mixed_text_and_elements(self): def test_fragment_pretty_printing(self): """Test Fragment with pretty printing enabled.""" fragment = Fragment( - Div( - H1("Title"), - Paragraph("Content") - ), - Paragraph("Outside") + Div(H1("Title"), Paragraph("Content")), Paragraph("Outside") ) pretty = fragment.render(pretty=True) @@ -111,9 +92,7 @@ def test_fragment_append_children(self): def test_fragment_prepend_children(self): """Test prepending children to Fragment.""" - fragment = Fragment( - Paragraph("Second") - ) + fragment = Fragment(Paragraph("Second")) fragment.prepend(H1("First")) rendered = str(fragment) @@ -121,10 +100,7 @@ def test_fragment_prepend_children(self): def test_fragment_clear(self): """Test clearing Fragment children.""" - fragment = Fragment( - H1("Title"), - Paragraph("Content") - ) + fragment = Fragment(H1("Title"), Paragraph("Content")) fragment.clear() rendered = str(fragment) @@ -132,20 +108,13 @@ def test_fragment_clear(self): def test_fragment_count_children(self): """Test counting Fragment children.""" - fragment = Fragment( - H1("Title"), - Paragraph("Content 1"), - Paragraph("Content 2") - ) + fragment = Fragment(H1("Title"), Paragraph("Content 1"), Paragraph("Content 2")) self.assertEqual(fragment.count_children(), 3) def test_fragment_nested_in_div(self): """Test using Fragment inside another element.""" - fragment = Fragment( - H1("Title"), - Paragraph("Content") - ) + fragment = Fragment(H1("Title"), Paragraph("Content")) div = Div() div.append(fragment) @@ -187,11 +156,7 @@ def test_fragment_list_composition(self): def test_fragment_with_attributes(self): """Test that Fragment ignores attributes (doesn't render them).""" - fragment = Fragment( - H1("Title"), - id="ignored", - class_name="also-ignored" - ) + fragment = Fragment(H1("Title"), id="ignored", class_name="also-ignored") rendered = str(fragment) self.assertNotIn("id", rendered) @@ -200,10 +165,12 @@ def test_fragment_with_attributes(self): def test_fragment_method_chaining(self): """Test method chaining with Fragment.""" - fragment = (Fragment() - .append(H1("Title")) - .append(Paragraph("Content 1")) - .append(Paragraph("Content 2"))) + fragment = ( + Fragment() + .append(H1("Title")) + .append(Paragraph("Content 1")) + .append(Paragraph("Content 2")) + ) self.assertEqual(fragment.count_children(), 3) rendered = str(fragment) @@ -226,10 +193,7 @@ def test_fragment_context_manager(self): def test_fragment_clone(self): """Test cloning a Fragment.""" - original = Fragment( - H1("Title"), - Paragraph("Content") - ) + original = Fragment(H1("Title"), Paragraph("Content")) cloned = original.clone() @@ -240,10 +204,7 @@ def test_fragment_clone(self): def test_fragment_filter(self): """Test filtering Fragment children.""" fragment = Fragment( - H1("Title"), - Paragraph("Para 1"), - Span("Span"), - Paragraph("Para 2") + H1("Title"), Paragraph("Para 1"), Span("Span"), Paragraph("Para 2") ) paragraphs = list(fragment.filter(lambda x: x.tag == "p")) @@ -252,9 +213,7 @@ def test_fragment_filter(self): def test_fragment_find_by_attribute(self): """Test finding elements by attribute in Fragment.""" fragment = Fragment( - H1("Title", id="header"), - Paragraph("Content", id="main"), - Span("Text") + H1("Title", id="header"), Paragraph("Content", id="main"), Span("Text") ) found = fragment.find_by_attribute("id", "main") @@ -263,12 +222,7 @@ def test_fragment_find_by_attribute(self): def test_fragment_compact_vs_pretty(self): """Test compact vs pretty rendering of Fragment.""" - fragment = Fragment( - Div( - H1("Title"), - Paragraph("Content") - ) - ) + fragment = Fragment(Div(H1("Title"), Paragraph("Content"))) # Compact compact = fragment.render(pretty=False) @@ -281,4 +235,4 @@ def test_fragment_compact_vs_pretty(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 31bc7305240cd7d838c5f5f4fedbe4e35a6c2de7 Mon Sep 17 00:00:00 2001 From: Sean N Date: Tue, 11 Nov 2025 11:06:47 +0200 Subject: [PATCH 2/2] Added: Styling engine, LLM_GUIDE and additional documentation --- LLM_GUIDE.md | 2138 +++++++++++++++++++++++++++++++ README.md | 226 +++- examples/stylesheet_example.py | 295 +++++ pyproject.toml | 2 +- src/ydnatl/__init__.py | 11 + src/ydnatl/core/fragment.py | 1 + src/ydnatl/core/parser.py | 134 ++ src/ydnatl/styles/__init__.py | 11 + src/ydnatl/styles/style.py | 201 +++ src/ydnatl/styles/stylesheet.py | 316 +++++ src/ydnatl/styles/theme.py | 428 +++++++ tests/test_fragment.py | 4 +- tests/test_parser.py | 357 ++++++ tests/test_styles.py | 408 ++++++ tests/test_text.py | 2 +- 15 files changed, 4529 insertions(+), 5 deletions(-) create mode 100644 LLM_GUIDE.md create mode 100644 examples/stylesheet_example.py create mode 100644 src/ydnatl/core/parser.py create mode 100644 src/ydnatl/styles/__init__.py create mode 100644 src/ydnatl/styles/style.py create mode 100644 src/ydnatl/styles/stylesheet.py create mode 100644 src/ydnatl/styles/theme.py create mode 100644 tests/test_parser.py create mode 100644 tests/test_styles.py diff --git a/LLM_GUIDE.md b/LLM_GUIDE.md new file mode 100644 index 0000000..5376cad --- /dev/null +++ b/LLM_GUIDE.md @@ -0,0 +1,2138 @@ +# YDNATL - LLM Developer Guide + +**Version:** 1.0.x +**Language:** Python 3.8+ +**Dependencies:** None (pure Python, uses only stdlib) + +## Overview + +YDNATL (You Don't Need Another Template Language) is a Python library for programmatic HTML generation. It uses a declarative, class-based API similar to Flutter's widget system. All elements are subclasses of `HTMLElement` and support method chaining, context managers, and serialization. + +## Core Concepts + +### 1. Element Hierarchy +- All HTML elements inherit from `HTMLElement` +- Elements can have children (other elements) +- Elements have attributes (HTML attributes like id, class, etc.) +- Elements have text content +- Some elements are self-closing (br, img, hr, input, etc.) + +### 2. Rendering +- `render()` method converts element tree to HTML string +- Supports compact (default) and pretty (indented) output +- Automatic HTML entity escaping for security + +### 3. Serialization +- Elements can be serialized to JSON (`to_json()`) +- Elements can be deserialized from JSON (`from_json()`) +- Elements can be parsed from HTML strings (`from_html()`) + +--- + +## Import Patterns + +### Import Everything (Quick Start) +```python +from ydnatl import * +``` + +### Selective Imports (Recommended for Production) +```python +from ydnatl import HTMLElement, Fragment, from_html +from ydnatl.tags.text import H1, Paragraph, Span +from ydnatl.tags.layout import Div, Section +from ydnatl.tags.form import Form, Input, Button +``` + +### Import from Core +```python +from ydnatl.core.element import HTMLElement +from ydnatl.core.fragment import Fragment +from ydnatl.core.parser import from_html +``` + +--- + +## HTMLElement Class + +**Location:** `ydnatl.core.element.HTMLElement` + +### Constructor + +```python +HTMLElement( + *children, # Variable number of child elements or text + tag: str = "div", # HTML tag name + text: str = "", # Text content + self_closing: bool = False, # Whether element is self-closing + **attributes # HTML attributes as keyword arguments +) +``` + +**Special Attribute Handling:** +- `class_name` → renders as `class` attribute +- `data_*` attributes → preserved with hyphens (data-value) +- Boolean attributes (checked, disabled) → rendered without values when True +- `style` → can be string or managed via style helper methods + +**Example:** +```python +div = HTMLElement( + H1("Title"), + Paragraph("Content"), + tag="div", + id="main", + class_name="container", + data_value="123" +) +``` + +### Properties + +| Property | Type | Description | Mutable | +|----------------|--------------------------|-------------------|-----------------------| +| `tag` | str | HTML tag name | Yes | +| `children` | List[HTMLElement \| str] | Child elements | Yes (but use methods) | +| `text` | str | Text content | Yes | +| `attributes` | Dict[str, str] | HTML attributes | Yes (but use methods) | +| `self_closing` | bool | Self-closing flag | Yes | + +--- + +## Element Manipulation Methods + +### Adding Children + +#### append(*children) → self +Adds children to end of children list. + +```python +div = Div() +div.append(H1("Title")) +div.append(Paragraph("Para 1"), Paragraph("Para 2")) +``` + +**Parameters:** +- `*children`: Variable number of HTMLElement or str + +**Returns:** self (for chaining) + +#### prepend(*children) → self +Adds children to beginning of children list. + +```python +div = Div(Paragraph("Second")) +div.prepend(H1("First")) # H1 now comes before Paragraph +``` + +**Parameters:** +- `*children`: Variable number of HTMLElement or str + +**Returns:** self (for chaining) + +### Removing Children + +#### clear() → self +Removes all children. + +```python +div.clear() # Now has no children +``` + +**Returns:** self (for chaining) + +#### pop(index: int = -1) → HTMLElement +Removes and returns child at index. + +```python +last_child = div.pop() # Remove last child +first_child = div.pop(0) # Remove first child +``` + +**Parameters:** +- `index`: int (default: -1) - Index of child to remove + +**Returns:** Removed HTMLElement + +#### remove_all(condition: Callable) → self +Removes all children matching condition. + +```python +# Remove all paragraphs +div.remove_all(lambda el: el.tag == "p") + +# Remove elements with specific class +div.remove_all(lambda el: el.get_attribute("class_name") == "hidden") +``` + +**Parameters:** +- `condition`: Callable[[HTMLElement], bool] - Function that returns True for elements to remove + +**Returns:** self (for chaining) + +### Querying Children + +#### filter(condition: Callable) → Iterator[HTMLElement] +Returns iterator of children matching condition. + +```python +# Get all paragraphs +paragraphs = list(div.filter(lambda el: el.tag == "p")) + +# Get elements with specific attribute +highlighted = list(div.filter(lambda el: el.has_attribute("highlight"))) +``` + +**Parameters:** +- `condition`: Callable[[HTMLElement], bool] + +**Returns:** Iterator[HTMLElement] + +#### find_by_attribute(key: str, value: str) → Optional[HTMLElement] +Finds first child with matching attribute. + +```python +main = div.find_by_attribute("id", "main") +container = div.find_by_attribute("class_name", "container") +``` + +**Parameters:** +- `key`: str - Attribute name +- `value`: str - Attribute value to match + +**Returns:** HTMLElement or None + +#### first() → Optional[HTMLElement] +Returns first child. + +```python +first = div.first() +``` + +**Returns:** HTMLElement or None + +#### last() → Optional[HTMLElement] +Returns last child. + +```python +last = div.last() +``` + +**Returns:** HTMLElement or None + +#### count_children() → int +Returns number of direct children. + +```python +count = div.count_children() +``` + +**Returns:** int + +### Modifying Children + +#### replace_child(index: int, new_child: HTMLElement) → self +Replaces child at index with new child. + +```python +div.replace_child(0, H1("New Title")) +``` + +**Parameters:** +- `index`: int - Index of child to replace +- `new_child`: HTMLElement - New child element + +**Returns:** self (for chaining) + +--- + +## Attribute Management Methods + +### add_attribute(key: str, value: str) → self +Adds or updates single attribute. + +```python +div.add_attribute("id", "main") +div.add_attribute("data-value", "123") +``` + +**Parameters:** +- `key`: str - Attribute name +- `value`: str - Attribute value + +**Returns:** self (for chaining) + +### add_attributes(attributes: List[Tuple[str, str]]) → self +Adds multiple attributes at once. + +```python +div.add_attributes([ + ("id", "main"), + ("class", "container"), + ("role", "main") +]) +``` + +**Parameters:** +- `attributes`: List[Tuple[str, str]] - List of (key, value) tuples + +**Returns:** self (for chaining) + +### remove_attribute(key: str) → self +Removes an attribute. + +```python +div.remove_attribute("data-old") +``` + +**Parameters:** +- `key`: str - Attribute name to remove + +**Returns:** self (for chaining) + +### get_attribute(key: str) → Optional[str] +Gets attribute value. + +```python +id_value = div.get_attribute("id") +class_name = div.get_attribute("class_name") +``` + +**Parameters:** +- `key`: str - Attribute name + +**Returns:** str or None + +### has_attribute(key: str) → bool +Checks if attribute exists. + +```python +if div.has_attribute("id"): + print("Has ID") +``` + +**Parameters:** +- `key`: str - Attribute name + +**Returns:** bool + +### get_attributes(*keys: str) → Dict[str, str] +Gets multiple attributes or all attributes. + +```python +all_attrs = div.get_attributes() +some_attrs = div.get_attributes("id", "class_name") +``` + +**Parameters:** +- `*keys`: str - Attribute names (if empty, returns all) + +**Returns:** Dict[str, str] + +### generate_id() → str +Generates unique ID if element doesn't have one. + +```python +element_id = div.generate_id() # Returns existing or generates new +``` + +**Returns:** str - The element's ID + +--- + +## CSS Style Management Methods + +### add_style(key: str, value: str) → self +Adds or updates single inline style. + +```python +div.add_style("color", "red") +div.add_style("font-size", "16px") +``` + +**Parameters:** +- `key`: str - CSS property name (kebab-case or camelCase) +- `value`: str - CSS property value + +**Returns:** self (for chaining) + +### add_styles(styles: Dict[str, str]) → self +Adds multiple inline styles at once. + +```python +div.add_styles({ + "color": "blue", + "background-color": "#f0f0f0", + "padding": "20px", + "margin": "10px" +}) +``` + +**Parameters:** +- `styles`: Dict[str, str] - Dictionary of CSS property: value pairs + +**Returns:** self (for chaining) + +### get_style(key: str) → Optional[str] +Gets value of specific inline style. + +```python +color = div.get_style("color") +padding = div.get_style("padding") +``` + +**Parameters:** +- `key`: str - CSS property name + +**Returns:** str or None + +### remove_style(key: str) → self +Removes specific inline style. + +```python +div.remove_style("margin") +``` + +**Parameters:** +- `key`: str - CSS property name to remove + +**Returns:** self (for chaining) + +--- + +## Rendering Methods + +### render(pretty: bool = False, _indent: int = 0) → str +Renders element to HTML string. + +```python +# Compact (default) +html = div.render() +#

Title

Content

+ +# Pretty (indented) +html = div.render(pretty=True) +#
+#

Title

+#

Content

+#
+``` + +**Parameters:** +- `pretty`: bool (default: False) - Enable pretty printing with indentation +- `_indent`: int (default: 0) - Internal parameter for indentation level + +**Returns:** str - HTML string + +**Note:** `_indent` is for internal use. Don't set it manually. + +### __str__() → str +String representation (calls render() with pretty=False). + +```python +html = str(div) +# Same as: html = div.render() +``` + +**Returns:** str - Compact HTML string + +--- + +## Serialization Methods + +### to_dict() → Dict +Converts element to dictionary representation. + +```python +data = div.to_dict() +# { +# "tag": "div", +# "self_closing": False, +# "attributes": {"id": "main"}, +# "text": "", +# "children": [...] +# } +``` + +**Returns:** Dict with keys: +- `tag`: str +- `self_closing`: bool +- `attributes`: Dict[str, str] +- `text`: str +- `children`: List[Dict] + +### to_json(indent: Optional[int] = None) → str +Converts element to JSON string. + +```python +# Compact JSON +json_str = div.to_json() + +# Pretty JSON +json_str = div.to_json(indent=2) +``` + +**Parameters:** +- `indent`: Optional[int] - Number of spaces for indentation (None = compact) + +**Returns:** str - JSON string + +### from_dict(data: Dict) → HTMLElement (classmethod) +Creates element from dictionary. + +```python +data = { + "tag": "div", + "self_closing": False, + "attributes": {"id": "main"}, + "text": "Content", + "children": [] +} +element = HTMLElement.from_dict(data) +``` + +**Parameters:** +- `data`: Dict - Dictionary with element data + +**Returns:** HTMLElement + +**Raises:** +- `ValueError` if data is invalid or missing required fields + +### from_json(json_str: str) → HTMLElement (classmethod) +Creates element from JSON string. + +```python +json_str = '{"tag": "div", "text": "Hello", "children": []}' +element = HTMLElement.from_json(json_str) +``` + +**Parameters:** +- `json_str`: str - JSON string + +**Returns:** HTMLElement + +**Raises:** +- `ValueError` if JSON is invalid + +### from_html(html_str: str, fragment: bool = False) → Union[HTMLElement, List[HTMLElement]] (classmethod) +Parses HTML string to YDNATL element(s). + +```python +# Single element +element = HTMLElement.from_html('
Content
') + +# Multiple root elements (fragment) +elements = HTMLElement.from_html('

Title

Text

', fragment=True) +``` + +**Parameters:** +- `html_str`: str - HTML string to parse +- `fragment`: bool (default: False) - If True, returns list of elements + +**Returns:** +- If `fragment=False`: HTMLElement or None +- If `fragment=True`: List[HTMLElement] + +--- + +## Parser Function + +### from_html(html_str: str, fragment: bool = False) → Union[HTMLElement, List[HTMLElement], None] + +**Location:** `ydnatl.core.parser.from_html` or `ydnatl.from_html` + +Standalone function for parsing HTML strings. + +```python +from ydnatl import from_html + +# Parse single element +element = from_html('
Content
') + +# Parse fragment +elements = from_html('

One

Two

', fragment=True) +``` + +**Parameters:** +- `html_str`: str - HTML string to parse +- `fragment`: bool (default: False) - If True, returns list of elements + +**Returns:** +- If `fragment=False`: HTMLElement or None +- If `fragment=True`: List[HTMLElement] + +**Features:** +- Handles all HTML5 elements +- Converts `class` attribute to `class_name` +- Preserves all attributes including data-* +- Handles self-closing tags (br, img, hr, input, etc.) +- Handles HTML entities (&, <, etc.) +- Strips whitespace-only text nodes + +--- + +## Utility Methods + +### clone() → HTMLElement +Creates deep copy of element. + +```python +original = Div(H1("Title")) +copy = original.clone() +# Modifying copy won't affect original +``` + +**Returns:** HTMLElement - Deep copy + +### __enter__() → self +Enables use as context manager. + +```python +with Div(id="container") as div: + div.append(H1("Title")) + div.append(Paragraph("Content")) +``` + +**Returns:** self + +### __exit__(exc_type, exc_val, exc_tb) → None +Exits context manager. + +**Parameters:** Standard context manager parameters + +--- + +## Event Callbacks + +### on_load() → None +Called when element is loaded. Override in subclass. + +```python +class MyElement(HTMLElement): + def on_load(self): + print("Element loaded") + # Custom initialization logic +``` + +### on_before_render() → None +Called before element is rendered. Override in subclass. + +```python +class MyElement(HTMLElement): + def on_before_render(self): + print("About to render") + # Pre-render logic +``` + +### on_after_render() → None +Called after element is rendered. Override in subclass. + +```python +class MyElement(HTMLElement): + def on_after_render(self): + print("Rendered") + # Post-render logic +``` + +### on_unload() → None +Called when element is unloaded. Override in subclass. + +```python +class MyElement(HTMLElement): + def on_unload(self): + print("Element unloaded") + # Cleanup logic +``` + +--- + +## Fragment Class + +**Location:** `ydnatl.core.fragment.Fragment` + +Special element that renders only its children without a wrapper tag. + +### Constructor + +```python +Fragment(*children, **kwargs) +``` + +**Note:** Any attributes passed are ignored (Fragment doesn't render a tag). + +### Usage + +```python +from ydnatl import Fragment, H1, Paragraph + +# Without Fragment - adds wrapper +content = Div(H1("Title"), Paragraph("Text")) +# Output:

Title

Text

+ +# With Fragment - no wrapper +content = Fragment(H1("Title"), Paragraph("Text")) +# Output:

Title

Text

+``` + +### Methods +Inherits all methods from HTMLElement. + +**Important:** `render()` outputs only children without wrapper tags. + +--- + +## Tag Classes by Module + +All tag classes follow the same constructor pattern as HTMLElement: + +```python +TagName(*children, **attributes) +``` + +### ydnatl.tags.text + +**Headings:** +- `H1(*children, **attributes)` +- `H2(*children, **attributes)` +- `H3(*children, **attributes)` +- `H4(*children, **attributes)` +- `H5(*children, **attributes)` +- `H6(*children, **attributes)` + +**Text Elements:** +- `Paragraph(*children, **attributes)` - `

` +- `Span(*children, **attributes)` - `` +- `Bold(*children, **attributes)` - `` +- `Strong(*children, **attributes)` - `` +- `Italic(*children, **attributes)` - `` +- `Em(*children, **attributes)` - `` +- `Underline(*children, **attributes)` - `` +- `Strikethrough(*children, **attributes)` - `` +- `Small(*children, **attributes)` - `` +- `Mark(*children, **attributes)` - `` +- `Del(*children, **attributes)` - `` +- `Ins(*children, **attributes)` - `` +- `Subscript(*children, **attributes)` - `` +- `Superscript(*children, **attributes)` - `` + +**Code/Technical:** +- `Code(*children, **attributes)` - `` +- `Pre(*children, **attributes)` - `

`
+- `Kbd(*children, **attributes)` - ``
+- `Samp(*children, **attributes)` - ``
+- `Var(*children, **attributes)` - ``
+
+**Semantic:**
+- `Blockquote(*children, **attributes)` - `
` +- `Quote(*children, **attributes)` - `` +- `Cite(*children, **attributes)` - `` +- `Abbr(*children, **attributes)` - `` +- `Dfn(*children, **attributes)` - `` +- `Time(*children, **attributes)` - `