Skip to content

Commit

Permalink
Update Formatting, -f auto/ruff/black/none from path
Browse files Browse the repository at this point in the history
  • Loading branch information
OleJoik committed Jun 12, 2024
1 parent 731c97e commit c60dd1e
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 43 deletions.
23 changes: 12 additions & 11 deletions docs/html2htpy.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ html into python code (htpy!).

```
$ html2htpy -h
usage: html2htpy [-h] [-s] [-f] [input]
usage: html2htpy [-h] [-s] [-f {auto,ruff,black,none}] [input]
positional arguments:
input input html from file or stdin
input input html from file or stdin
options:
-h, --help show this help message and exit
-s, --shorthand Use shorthand syntax for class and id attributes
-f, --format Format output code (requires black installed)
-h, --help show this help message and exit
-s, --shorthand Use shorthand syntax for class and id attributes
-f {auto,ruff,black,none}, --format {auto,ruff,black,none}
Select one of the following formatting options: auto, ruff, black or none
```


Expand Down Expand Up @@ -50,7 +51,7 @@ Lets say you have an existing html file:
Now, if you run the command, it outputs the corresponding python code (htpy).

```
$ html2htpy -f index.html
$ html2htpy index.html
```

```py
Expand Down Expand Up @@ -104,11 +105,11 @@ powershell Get-Clipboard | html2htpy > output.py


## Formatting the output
`html2htpy` can format the output python code using `black`. It needs to be available in your python environment
when you run `html2htpy` with the `-f`/`--format` flag. You might have it in your environment already, or you can install it
as part of the htpy extras: `pip install htpy[extras]`.
`html2htpy` can format the output python code using `black` or `ruff`.
Select the preferred formatter with the `-f`/`--format` flag. Options are `auto`, `ruff`, `black` and `none`.

By default, output code is not formatted.
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.


## Shorthand syntax
Expand Down Expand Up @@ -170,7 +171,7 @@ See the example below:
```

```py
$ html2htpy -f -s jinja.html
$ html2htpy -s jinja.html
body[
h1[f"{ heading }"],
p[f"Welcome to our cooking site, { user.name }!"],
Expand Down
105 changes: 86 additions & 19 deletions htpy/html2htpy.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from abc import ABC, abstractmethod
import sys
import re
import subprocess
import argparse
import shutil
from dataclasses import dataclass
from typing import Self
from typing import Literal, Self
from html.parser import HTMLParser

__all__ = ["html2htpy"]
Expand Down Expand Up @@ -102,6 +105,32 @@ def serialize(self, shorthand_id_class: bool = False):
return f"{_type}{_attrs}{_children}"


class Formatter(ABC):
@abstractmethod
def format(self, s: str) -> str:
raise NotImplementedError()


class BlackFormatter(Formatter):
def format(self, s: str) -> str:
result = subprocess.run(
["black", "-q", "-"],
input=s.encode("utf8"),
stdout=subprocess.PIPE,
)
return result.stdout.decode("utf8")


class RuffFormatter(Formatter):
def format(self, s: str) -> str:
result = subprocess.run(
["ruff", "format", "-"],
input=s.encode("utf8"),
stdout=subprocess.PIPE,
)
return result.stdout.decode("utf8")


class HTPYParser(HTMLParser):
def __init__(self):
self._collected: list[Tag | str] = []
Expand Down Expand Up @@ -148,7 +177,9 @@ def handle_data(self, data: str):
else:
self._collected.append(stringified_data)

def serialize_python(self, shorthand_id_class: bool = False, format: bool = False):
def serialize_python(
self, shorthand_id_class: bool = False, formatter: Formatter | None = None
):
o = ""

if len(self._collected) == 1:
Expand All @@ -160,26 +191,21 @@ def serialize_python(self, shorthand_id_class: bool = False, format: bool = Fals
o += _serialize(t, shorthand_id_class) + ","
o = o[:-1] + "]"

if format:
try:
import black
except:
raise Exception(
"Cannot import formatter. Please ensure black is installed."
)

return black.format_str(
o, mode=black.FileMode(line_length=80, magic_trailing_comma=False)
)
if formatter:
return formatter.format(o)
else:
return o


def html2htpy(html: str, shorthand_id_class: bool = False, format: bool = False):
def html2htpy(
html: str,
shorthand_id_class: bool = False,
formatter: Formatter | None = None,
):
parser = HTPYParser()
parser.feed(html)

return parser.serialize_python(shorthand_id_class, format)
return parser.serialize_python(shorthand_id_class, formatter)


def _convert_data_to_string(data: str):
Expand Down Expand Up @@ -236,6 +262,45 @@ def _serialize(el: Tag | str, shorthand_id_class: bool):
return str(el)


def _get_formatter(
format: Literal["auto", "ruff", "black", "none"]
) -> Formatter | None:
formatter: Formatter | None = None
if format == "ruff":
if _is_package_installed("ruff"):
formatter = RuffFormatter()
else:
_printerr(
"Selected formatter (ruff) is not installed.",
)
_printerr("Please install it or select another formatter.")
_printerr("`html2htpy -h` for help")
sys.exit(1)

if format == "black":
if _is_package_installed("black"):
formatter = BlackFormatter()
else:
_printerr(
"Selected formatter (black) is not installed.",
)
_printerr("Please install it or select another formatter.")
_printerr("`html2htpy -h` for help")
sys.exit(1)

elif format == "auto":
if _is_package_installed("ruff"):
formatter = RuffFormatter()
elif _is_package_installed("black"):
formatter = BlackFormatter()

return formatter


def _is_package_installed(package_name: str):
return shutil.which(package_name) is not None


@dataclass
class ConvertArgs:
shorthand: bool
Expand All @@ -254,8 +319,9 @@ def main():
parser.add_argument(
"-f",
"--format",
help="Format output code (requires black installed)",
action="store_true",
choices=["auto", "ruff", "black", "none"],
default="auto",
help="Select one of the following formatting options: auto, ruff, black or none",
)
parser.add_argument(
"input",
Expand Down Expand Up @@ -286,9 +352,10 @@ def main():
sys.exit(1)

shorthand: bool = args.shorthand
format: bool = args.format

print(html2htpy(input, shorthand, format))
formatter = _get_formatter(args.format)

print(html2htpy(input, shorthand, formatter))


def _printerr(value: str):
Expand Down
3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ optional-dependencies.dev = [
"django-stubs",
"jinja2",
]
optional-dependencies.extras = [
"black"
]
optional-dependencies.docs = [
"mkdocs-material==9.5.12",
]
Expand Down
20 changes: 10 additions & 10 deletions tests/test_html2htpy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import textwrap
import pytest
from htpy.html2htpy import html2htpy
from htpy.html2htpy import BlackFormatter, html2htpy


def test_convert_shorthand_id_and_class():
Expand All @@ -10,7 +10,7 @@ def test_convert_shorthand_id_and_class():
</div>
"""

actual = html2htpy(input, shorthand_id_class=True, format=True)
actual = html2htpy(input, shorthand_id_class=True, formatter=BlackFormatter())
expected = 'div("#div-id.some-class.other-class")[p["This is a paragraph."]]\n'

assert actual == expected
Expand All @@ -24,7 +24,7 @@ def test_convert_nested_element():
</div>
"""

actual = html2htpy(input, format=True)
actual = html2htpy(input, formatter=BlackFormatter())
expected = textwrap.dedent(
"""\
div[
Expand Down Expand Up @@ -78,7 +78,7 @@ def test_convert_f_string_escaping():
<p>{{ variable }} is "a" { paragraph }.</p>
"""

actual = html2htpy(input, format=False)
actual = html2htpy(input)
expected = r'p[f"{ variable } is \"a\" {{ paragraph }}."]'

assert actual == expected
Expand All @@ -102,7 +102,7 @@ def test_convert_f_string_escaping_complex():
</body>
"""

actual = html2htpy(input, format=True)
actual = html2htpy(input, formatter=BlackFormatter())
expected = textwrap.dedent(
"""\
body[
Expand All @@ -129,7 +129,7 @@ def test_convert_script_style_tags():
<style>body { background-color: #fff; }</style>
"""

actual = html2htpy(input, format=True)
actual = html2htpy(input, formatter=BlackFormatter())
assert actual == textwrap.dedent(
"""\
[
Expand Down Expand Up @@ -220,7 +220,7 @@ def test_convert_section_regular():
</section>
"""

actual = html2htpy(input, shorthand_id_class=False, format=True)
actual = html2htpy(input, shorthand_id_class=False, formatter=BlackFormatter())
expected = textwrap.dedent(
"""\
section(class_="hero is-fullheight is-link")[
Expand Down Expand Up @@ -249,7 +249,7 @@ def test_convert_section_shorthand_id_class():
</section>
"""

actual = html2htpy(input, shorthand_id_class=True, format=True)
actual = html2htpy(input, shorthand_id_class=True, formatter=BlackFormatter())

assert actual == textwrap.dedent(
"""\
Expand All @@ -273,7 +273,7 @@ def test_convert_nested_element_without_formatting():
</div>
"""

actual = html2htpy(input, format=False)
actual = html2htpy(input, formatter=None)

expected = 'div[p["This is a ",span["nested"]," element."],p["Another ",a(href="#")["nested ",strong["tag"]],"."]]'

Expand All @@ -287,7 +287,7 @@ def test_convert_html_to_htpy_svg():
</svg>
"""

actual_output = html2htpy(input, format=True)
actual_output = html2htpy(input, formatter=BlackFormatter())

expected_output = textwrap.dedent(
"""\
Expand Down

0 comments on commit c60dd1e

Please sign in to comment.