diff --git a/docs/html2htpy.md b/docs/html2htpy.md index 1ffcea6..0ef3644 100644 --- a/docs/html2htpy.md +++ b/docs/html2htpy.md @@ -7,17 +7,20 @@ html into Python code (htpy!). ``` $ html2htpy -h -usage: html2htpy [-h] [-e] [-f {auto,ruff,black,none}] [-i] [input] +usage: html2htpy [-h] [-e] [-f {auto,ruff,black,none}] [-i {yes,h,no}] [input] positional arguments: input input HTML from file or stdin options: -h, --help show this help message and exit - -e, --explicit Use explicit `id` and `class_` kwargs instead of the shorthand #id.class syntax + -e, --explicit Use explicit `id` and `class_` kwargs instead of the shorthand + #id.class syntax -f {auto,ruff,black,none}, --format {auto,ruff,black,none} - Select one of the following formatting options: auto, ruff, black or none - -i, --imports Output imports for htpy elements found + Select one of the following formatting options: auto, ruff, black + or none + -i {yes,h,no}, --imports {yes,h,no} + Output mode for imports of found htpy elements ``` @@ -56,6 +59,8 @@ $ html2htpy index.html ``` ```py +from htpy import body, div, h1, h2, head, html, meta, p, span, title + html(lang="en")[ head[ meta(charset="UTF-8"), @@ -106,7 +111,7 @@ powershell Get-Clipboard | html2htpy > output.py Select the preferred formatter with the `-f`/`--format` flag. Options are `auto`, `ruff`, `black` and `none`. By default, the selection will be `auto`, formatting if it finds a formatter on path, prefering `ruff` if it's available. -If no formatters are available on path, the output not be formatted. +If no formatters are available on path, the output will not be formatted. ## Explicit id and class kwargs @@ -122,6 +127,8 @@ If you prefer the explicit `id="id", class_="class"` kwargs syntax over the defa #### Default shorthand `#id.class` ```py title="$ html2htpy example.html" +from htpy import p, section + section("#main-section.hero.is-link")[ p(".subtitle.is-3.is-spaced")["Welcome"] ] @@ -129,36 +136,40 @@ section("#main-section.hero.is-link")[ #### Explicit kwargs `id`, `class_` ```py title="$ html2htpy --explicit example.html" +from htpy import p, section + section(id="main-section", class_="hero is-link")[ p(class_="subtitle is-3 is-spaced")["Welcome"] ] ``` -## Detect htpy imports +## Import options -If you pass the `-i`/`--imports` flag, htpy elements detected will be included as -imports in the output. For example: +You have a couple of options regarding imports with the `-i`/`--imports` flag. +Options are `yes` (default), `h`, `no`. -```py title="$ html2htpy --imports example.html" -from htpy import p, section +#### Module import of htpy: `--imports=h` -section("#main-section.hero.is-link")[ - p(".subtitle.is-3.is-spaced")["Welcome"] +Some people prefer to `import htpy as h` instead of importing individual elements from htpy. +If this is you, you can use the `--imports=h` option to get corresponding output when using `html2htpy`. + +```py title="$ html2htpy --imports=h example.html" +import htpy as h + +h.section("#main-section.hero.is-link")[ + h.p(".subtitle.is-3.is-spaced")["Welcome"] ] ``` - ## Template interpolation to f-strings -You might have some templates laying around after using jinja or some other templating language. - -`html2htpy` will try to convert the `template {{ variables }}`... +`html2htpy` will try to convert template variables to pythonic f-strings: -...to pythonic f-strings: `f"template { variables }"` +`template {{ variables }}` -> `f"template { variables }"` -Note that other template template syntax, such as loops `{% for x in y %}` can not be transformed at -this time, so you will often have to clean up a bit after `html2htpy` is done with its thing. +Note that other typical template syntax, such as loops `{% for x in y %}`, can not be transformed this way, +so you will often have to clean up a bit after `html2htpy` is done with its thing. See the example below: @@ -180,6 +191,8 @@ See the example below: ``` ```py title="$ html2htpy jinja.html" +from htpy import body, h1, h2, h3, li, ol, p + body[ h1[f"{ heading }"], p[f"Welcome to our cooking site, { user.name }!"], diff --git a/htpy/html2htpy.py b/htpy/html2htpy.py index 6147228..7ef4b3d 100644 --- a/htpy/html2htpy.py +++ b/htpy/html2htpy.py @@ -43,7 +43,7 @@ def __init__( self.parent = parent self.children: list[Any | str] = [] - def serialize(self, shorthand_id_class: bool = False) -> str: + def serialize(self, shorthand_id_class: bool, use_h_prefix: bool) -> str: _positional_attrs: dict[str, str | None] = {} _attrs = "" _kwattrs: list[tuple[str, str | None]] = [] @@ -110,7 +110,7 @@ def serialize(self, shorthand_id_class: bool = False) -> str: _children += "[" for c in self.children: if isinstance(c, Tag): - _children += c.serialize(shorthand_id_class=shorthand_id_class) + _children += c.serialize(shorthand_id_class, use_h_prefix) else: _children += str(c) @@ -118,6 +118,9 @@ def serialize(self, shorthand_id_class: bool = False) -> str: _children = _children[:-1] + "]" + if use_h_prefix: + return f"h.{self.python_type}{_attrs}{_children}" + return f"{self.python_type}{_attrs}{_children}" @@ -196,12 +199,14 @@ def handle_data(self, data: str) -> None: def serialize_python( self, shorthand_id_class: bool = False, - include_imports: bool = False, + import_mode: Literal["yes", "h", "no"] = "yes", formatter: Formatter | None = None, ) -> str: o = "" - if include_imports: + use_h_prefix = False + + if import_mode == "yes": unique_tags: set[str] = set() def _tags_from_children(parent: Tag) -> None: @@ -220,13 +225,17 @@ def _tags_from_children(parent: Tag) -> None: o += f'from htpy import {", ".join(sorted_tags)}\n' + elif import_mode == "h": + o += "import htpy as h\n" + use_h_prefix = True + if len(self._collected) == 1: - o += _serialize(self._collected[0], shorthand_id_class) + o += _serialize(self._collected[0], shorthand_id_class, use_h_prefix) else: o += "[" for t in self._collected: - o += _serialize(t, shorthand_id_class) + "," + o += _serialize(t, shorthand_id_class, use_h_prefix) + "," o = o[:-1] + "]" if formatter: @@ -238,13 +247,13 @@ def _tags_from_children(parent: Tag) -> None: def html2htpy( html: str, shorthand_id_class: bool = True, - include_imports: bool = False, + import_mode: Literal["yes", "h", "no"] = "yes", formatter: Formatter | None = None, ) -> str: parser = HTPYParser() parser.feed(html) - return parser.serialize_python(shorthand_id_class, include_imports, formatter) + return parser.serialize_python(shorthand_id_class, import_mode, formatter) def _convert_data_to_string(data: str) -> str: @@ -294,9 +303,9 @@ def replacer(match: re.Match[str]) -> str: return _data -def _serialize(el: Tag | str, shorthand_id_class: bool) -> str: +def _serialize(el: Tag | str, shorthand_id_class: bool, use_h_prefix: bool) -> str: if isinstance(el, Tag): - return el.serialize(shorthand_id_class=shorthand_id_class) + return el.serialize(shorthand_id_class, use_h_prefix) else: return str(el) @@ -356,8 +365,9 @@ def main() -> None: parser.add_argument( "-i", "--imports", - help="Output imports for htpy elements found", - action="store_true", + choices=["yes", "h", "no"], + help="Output mode for imports of found htpy elements", + default="yes", ) parser.add_argument( "input", @@ -388,7 +398,7 @@ def main() -> None: sys.exit(1) shorthand: bool = False if args.explicit else True - imports: bool = args.imports + imports: Literal["yes", "h", "no"] = args.imports formatter = _get_formatter(args.format) diff --git a/tests/test_html2htpy.py b/tests/test_html2htpy.py index f57d392..cfd0737 100644 --- a/tests/test_html2htpy.py +++ b/tests/test_html2htpy.py @@ -12,7 +12,7 @@ def test_convert_default_shorthand_id_and_class() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") expected = 'div("#div-id.some-class.other-class")[p["This is a paragraph."]]' assert actual == expected @@ -25,7 +25,7 @@ def test_convert_explicit_id_class_syntas() -> None: """ - actual = html2htpy(input, shorthand_id_class=False) + actual = html2htpy(input, shorthand_id_class=False, import_mode="no") expected = 'div(id="div-id",class_="some-class other-class")[p["This is a paragraph."]]' assert actual == expected @@ -40,7 +40,7 @@ def test_convert_explicit_id_class_syntas() -> None: def test_convert_nested_element_without_formatting() -> None: - actual = html2htpy(nested_html, formatter=None) + actual = html2htpy(nested_html, formatter=None, import_mode="no") expected = ( "div[" @@ -53,7 +53,7 @@ def test_convert_nested_element_without_formatting() -> None: def test_convert_nested_element_ruff_formatting() -> None: - actual = html2htpy(nested_html, formatter=RuffFormatter()) + actual = html2htpy(nested_html, formatter=RuffFormatter(), import_mode="no") assert actual == textwrap.dedent( """\ div[ @@ -65,7 +65,7 @@ def test_convert_nested_element_ruff_formatting() -> None: def test_convert_nested_element_black_formatting() -> None: - actual = html2htpy(nested_html, formatter=BlackFormatter()) + actual = html2htpy(nested_html, formatter=BlackFormatter(), import_mode="no") assert actual == textwrap.dedent( """\ div[ @@ -76,8 +76,8 @@ def test_convert_nested_element_black_formatting() -> None: ) -def test_convert_nested_element_include_imports() -> None: - actual = html2htpy(nested_html, include_imports=True) +def test_convert_nested_element___import_mode_yes() -> None: + actual = html2htpy(nested_html, import_mode="yes") assert actual == ( "from htpy import a, div, p, span, strong\n" "div[" @@ -87,9 +87,20 @@ def test_convert_nested_element_include_imports() -> None: ) +def test_convert_nested_element___import_mode_h() -> None: + actual = html2htpy(nested_html, import_mode="h") + assert actual == ( + "import htpy as h\n" + "h.div[" + 'h.p["This is a ",h.span["nested"]," element."],' + 'h.p["Another ",h.a(href="#")["nested ",h.strong["tag"]],"."]' + "]" + ) + + def test_convert_custom_element_include_imports() -> None: input = 'Custom content' - actual = html2htpy(input, include_imports=True) + actual = html2htpy(input, import_mode="yes") assert actual == ( "from htpy import custom_element\n" 'custom_element(attribute="value")["Custom content"]' @@ -103,14 +114,14 @@ def test_convert_self_closing_tags() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == '[img(src="image.jpg",alt="An image"),br,input(type="text")]' def test_convert_attribute_with_special_characters() -> None: input = """A <test> & 'image'""" - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == """img(src="path/to/image.jpg",alt="A & 'image'")""" @@ -119,7 +130,7 @@ def test_convert_ignores_comments() -> None:
Content inside
""" - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == 'div["Content "," inside"]' @@ -128,7 +139,7 @@ def test_convert_special_characters() -> None:

Special characters: & < > " ' ©

""" - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == 'p["Special characters: & < > \\" \' ©"]' @@ -137,7 +148,7 @@ def test_convert_f_string_escaping() -> None:

{{ variable }} is "a" { paragraph }.

""" - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") expected = r'p[f"{ variable } is \"a\" {{ paragraph }}."]' assert actual == expected @@ -161,7 +172,7 @@ def test_convert_f_string_escaping_complex() -> None: """ - actual = html2htpy(input, formatter=RuffFormatter()) + actual = html2htpy(input, formatter=RuffFormatter(), import_mode="no") expected = textwrap.dedent( """\ body[ @@ -187,7 +198,7 @@ def test_convert_script_tag() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == """script(type="text/javascript")["alert('This is a script');"]""" @@ -195,7 +206,7 @@ def test_convert_style_tag() -> None: input = """ """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == """style["body { background-color: #fff; }"]""" @@ -213,7 +224,7 @@ def test_convert_html_doctype() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") expected = """html[head[title["Test Document"]],body[h1["Header"],p["Paragraph"]]]""" assert actual == expected @@ -226,7 +237,7 @@ def test_convert_empty_elements() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == "[div,p,span]" @@ -243,7 +254,7 @@ def test_convert_void_elements() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == 'div[div[input(type="text")],div[input(type="text")]]' @@ -252,7 +263,7 @@ def test_convert_custom_tag() -> None: Custom content """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == """custom_element(attribute="value")["Custom content"]""" @@ -275,7 +286,7 @@ def test_convert_attributes_without_values() -> None: """ - actual = html2htpy(input) + actual = html2htpy(input, import_mode="no") assert actual == """[input(type="checkbox",checked=True),option(selected=True)["Option"]]""" @@ -291,7 +302,7 @@ def test_convert_complex_section() -> None: """ - actual = html2htpy(input, shorthand_id_class=False) + actual = html2htpy(input, shorthand_id_class=False, import_mode="no") expected = ( 'section(class_="hero is-fullheight is-link")[' 'div(class_="hero-body")[' @@ -331,7 +342,7 @@ def test_convert_complex_svg() -> None: """ - actual_output = html2htpy(input, formatter=BlackFormatter()) + actual_output = html2htpy(input, formatter=BlackFormatter(), import_mode="no") expected_output = textwrap.dedent( f"""\