Compare commits

..
11 Commits
Author SHA1 Message Date
truePhilipp 788308cf44 moving along
Stylecheck / stylecheck (push) Failing after 9s
Tests / unittest (3.12) (push) Successful in 15s
Stylecheck / stylecheck-tests (push) Successful in 21s
2026-10-06 15:23:27 +02:00
truePhilipp f61305d1b0 added the new attributes to Attributes.md
Stylecheck / stylecheck (push) Failing after 2m8s
Stylecheck / stylecheck-tests (push) Failing after 2m9s
Tests / unittest (3.12) (push) Successful in 2m10s
2024-05-14 22:35:10 +02:00
truePhilipp 8ac332de49 added more keywords to pyproject.toml 2024-05-14 22:18:16 +02:00
truePhilipp ddc9add551 added assets_formatter and remove_formatter 2024-05-14 22:13:30 +02:00
truePhilipp 8697f3ecb5 added pip installation text 2024-05-11 01:44:10 +02:00
truePhilipp 530bf592a8 corrected environment name 2024-05-11 01:35:29 +02:00
truePhilipp 2dc16547dd enable pypi release 2024-05-11 01:33:06 +02:00
truePhilipp 3546ac40f9 removed main from deploy.yml 2024-05-11 01:19:53 +02:00
truePhilipp a274f53b6e dynamic version test 2024-05-11 01:18:00 +02:00
truePhilipp eef7756e5b added more attribute explanations to Attributes.md 2024-05-11 00:23:07 +02:00
truePhilipp beba63967b created TestDirectory context manager (typing is disabled as i can't find a fix for the generic typing error) 2024-05-10 11:40:30 +00:00
13 changed files with 253 additions and 73 deletions

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
name: Deploy
on:
push:
tags:
- 'v*.*.*'
pull_request:
tags:
- 'v*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Dependencies
run: pip install ".[dev]"
- name: Build on Python 3.12
run: hatch build
- name: Store the distribution packages
uses: actions/upload-artifact@v3
with:
name: python-package-distributions
path: dist/
publish-to-pypi:
needs:
- build
runs-on: ubuntu-latest
environment:
name: Deployment
url: https://pypi.org/p/html_to_django
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
with:
name: python-package-distributions
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+3 -1
View File
@@ -7,7 +7,9 @@ Converts HTML files with special attributes to Django templates.
# Installation
TODO: pip install instructions
```bash
pip install html-to-django
```
# Usage
+63
View File
@@ -1,5 +1,11 @@
# Attributes
| dj-assets | Modify the "href" or "src" attributes of elements with a path prefix and wrap them in a Django static command. |
|----------------|----------------------------------------------------------------------------------------------------------------|
| Syntax | `<html dj-assets="asset_identifier;asset_prefix">...</html>` |
| Example Input | `<html dj-assets="assets;module"><img src="assets/image.png"></html>` |
| Example Output | `<html><img src="{% static 'module/assets/image.png' %}"/></html>` |
| dj-attr | Dynamically assign attributes using Django template variables. |
|----------------|---------------------------------------------------------------------------------------|
| Syntax | `<element dj-attr="attribute1;value1~attribute2;value2~attribute3;value3"></element>` |
@@ -29,3 +35,60 @@
| Syntax | `<element dj-for-wrap="item in list">Content</element>` |
| Example Input | `<div dj-for-wrap="item in list">Content</div>` |
| Example Output | `{% for item in list %}<div>Content</div>{% endfor %}` |
| dj-if, dj-elif, dj-else | Replace the elements with a Django template if-elif-else structure. |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Syntax | `<element dj-if="condition">Content</element>`<br/>`<element dj-elif="condition">Content</element>`<br/>`<element dj-else="condition">Content</element>` |
| Note | The `dj-elif` and `dj-else` attributes are optional. Multiple `dj-elif` attributes are allowed. |
| Example Input | `<div dj-if="condition1">Test</div><div dj-elif="condition2">Test2</div><div dj-else>Test3</div>` |
| Example Output | `{% if condition1 %}Test{% elif condition2 %}Test2{% else %}Test3{% endif %}` |
| dj-include | Replace the content of the element with a Django template include command. |
|----------------|----------------------------------------------------------------------------|
| Syntax | `<element dj-include="template_name"></element>` |
| Example Input | `<div dj-include="template_name.html"></div>` |
| Example Output | `{% include "template_name.html" %}` |
| dj-remove-default | Remove the Doctype, "head" element, and "script" element within the "body" element. It also unwraps the "body" and "html" elements, leaving only the remaining content of the "body" element. |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Syntax | `<html dj-remove-default>...</html>` |
| Note | This is meant to remove all the default html wrapper code most static html generators produce. |
| Example Input | `<!DOCTYPE html><html dj-remove-default><head>...</head><body>Content<script>...</script></body></html>` |
| Example Output | `Content` |
| dj-remove | Remove the element. |
|----------------|---------------------------------|
| Syntax | `<element dj-remove></element>` |
| Example Input | `<div><p dj-remove></p></div>` |
| Example Output | ` ` |
| dj-static | Replace the value of the specified tag with a Django template static command, using the specified static file path. |
|----------------|---------------------------------------------------------------------------------------------------------------------|
| Syntax | `<element dj-static="tag;static_file_path"></element>` |
| Example Input | `<img dj-static="src;images/pic.jpg">` |
| Example Output | `<img src="{% static 'images/pic.jpg' %}"/>` |
| dj-style | Modify the style attribute of a element with django variables. |
|----------------|--------------------------------------------------------------------------------------------|
| Syntax | `<element dj-style="property1;value1;value_type1~property2;value2;value_type2"></element>` |
| Note | The `value_type` is optional. If not included still add the `;` after the value. |
| Example Input | `<div style="color:red" dj-style="background-color;color;~padding;padding;px">` |
| Example Output | `<div style="color:red;background-color:{{ color }};padding:{{ padding }}px">` |
| dj-style-raw | Modify the style attribute of a element. The value will be inserted as is. |
|----------------|----------------------------------------------------------------------------------|
| Syntax | `<element dj-style-raw="property1;value1~property2;value2"></element>` |
| Example Input | `<div style="color:red" dj-style-raw="background-color;blue~color:{{ color }}">` |
| Example Output | `<div style="color:{{ color }};background-color:blue">` |
| dj-unwrap | Remove the element's tags but keeps its content. |
|----------------|--------------------------------------------------|
| Syntax | `<element dj-unwrap>Content</element>` |
| Example Input | `<div dj-unwrap><p>Hello, world!</p></div>` |
| Example Output | `<p>Hello, world!</p>` |
| dj-var | Replace the element's content with a Django template variable. |
|----------------|----------------------------------------------------------------|
| Syntax | `<element dj-var="variable_name">Content</element>` |
| Example Input | `<p dj-var="greeting">Hello, world!</p>` |
| Example Output | `<p>{{ greeting }}</p>` |
-1
View File
@@ -1 +0,0 @@
__version__ = "0.0.1"
+4
View File
@@ -50,6 +50,8 @@ def convert_file(path: str, overwrite: bool = False) -> None:
if not (os.path.exists(path) and os.path.isfile(path)):
raise FileNotFoundError
print("moving the code around")
with open(path, "r") as file:
soup = bs4.BeautifulSoup(file, "html.parser")
@@ -58,6 +60,8 @@ def convert_file(path: str, overwrite: bool = False) -> None:
for formatter in formatters:
formatter.format(soup)
print("even in between")
if overwrite:
out_path = path
else:
+4 -4
View File
@@ -6,10 +6,10 @@ Each formatter module contains a `format` function that performs a specific modi
The `formatters` list in this package contains all the formatter modules for easy access and usage.
"""
from . import (for_formatter, if_formatter, var_formatter, block_formatter, static_formatter, attr_formatter,
for_wrap_formatter, unwrap_formatter, include_formatter, remove_default_formatter, command_formatter,
style_formatter, style_raw_formatter)
for_wrap_formatter, unwrap_formatter, remove_formatter, include_formatter, assets_formatter,
remove_default_formatter, command_formatter, style_formatter, style_raw_formatter)
formatters = [for_formatter, if_formatter, var_formatter, block_formatter, static_formatter, attr_formatter,
for_wrap_formatter, unwrap_formatter, include_formatter, remove_default_formatter, command_formatter,
style_formatter, style_raw_formatter]
for_wrap_formatter, unwrap_formatter, remove_formatter, include_formatter, assets_formatter,
remove_default_formatter, command_formatter, style_formatter, style_raw_formatter]
@@ -0,0 +1,32 @@
"""
This module contains a function `format` that modifies a BeautifulSoup object in place. The function searches for
the "html" element with the attribute "dj-assets". The value of "dj-assets" is expected to be a string with two parts
separated by a semicolon (;), the first part being the asset identifier and the second part being the asset prefix.
For each element in the BeautifulSoup object with a "href" or "src" attribute that starts with the asset identifier,
it adds the asset prefix to the attribute value and wraps it in a Django static command.
After processing all elements, it removes the "dj-assets" attribute from the "html" element.
Example:
If the "html" element is <html dj-assets="assets;module">...</html>,
and there is an element <img src="assets/image.png">,
after processing, the "img" element becomes <img src="{% static 'module/assets/image.png' %}">.
"""
from bs4 import BeautifulSoup, Tag
def format(soup: BeautifulSoup) -> None:
if html := soup.find("html"):
if isinstance(html, Tag):
if html.has_attr("dj-assets"):
if isinstance(html["dj-assets"], str):
asset_identifier, asset_prefix = html["dj-assets"].split(";")
for element in soup.find_all(attrs={"href": True}):
if element["href"].startswith(asset_identifier):
element["href"] = f"{{% static '{asset_prefix}/{element['href']}' %}}"
for element in soup.find_all(attrs={"src": True}):
if element["src"].startswith(asset_identifier):
element["src"] = f"{{% static '{asset_prefix}/{element['src']}' %}}"
del html["dj-assets"]
@@ -0,0 +1,15 @@
"""
This module contains a function `format` that modifies a BeautifulSoup object in place. The function searches for
all elements with the attribute "dj-remove". For each found element, it calls the `decompose` method, which removes
the element from the parse tree. This effectively removes all elements marked with "dj-remove" from the HTML document.
Example:
If an element is <div dj-remove></div>,
after processing, it will be removed from the BeautifulSoup object.
"""
from bs4 import BeautifulSoup
def format(soup: BeautifulSoup) -> None:
for element in soup.find_all(attrs={"dj-remove": True}):
element.decompose()
+5 -4
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
@@ -11,7 +11,7 @@ authors = [
description = "HTML to Django Converter"
readme = {file = "README.md", content-type = "text/markdown"}
license = {file = "LICENSE.txt"}
keywords = ["html", "django"]
keywords = ["html", "converter", "django", "tool", "web development", "static html"]
requires-python = ">= 3.12"
classifiers = [
"Programming Language :: Python :: 3",
@@ -29,7 +29,8 @@ test = [
"pycodestyle==2.11.1"
]
dev = [
"hatch==1.10.0"
"hatch==1.10.0",
"hatch-vcs==0.4.0"
]
[project.urls]
@@ -41,4 +42,4 @@ html_to_django = "html_to_django.converter:command_entry"
html_to_django_r = "html_to_django.converter:command_entry_r"
[tool.hatch.version]
path = "html_to_django/__about__.py"
source = "vcs"
@@ -0,0 +1,31 @@
from unittest import TestCase
from bs4 import BeautifulSoup
from html_to_django.formatters import assets_formatter
class AssetsTests(TestCase):
def test_basic(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"><img src="assets/image.png"></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = '<html><img src="{% static \'module/assets/image.png\' %}"/></html>'
self.assertEqual(expected_result, str(soup))
def test_empty(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = '<html></html>'
self.assertEqual(expected_result, str(soup))
def test_no_assets(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"><p>Test</p></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = '<html><p>Test</p></html>'
self.assertEqual(expected_result, str(soup))
def test_with_multiple_assets(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"><img src="assets/image1.png"><img '
'src="assets/image2.png"></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = ('<html><img src="{% static \'module/assets/image1.png\' %}"/><img src="{% static '
'\'module/assets/image2.png\' %}"/></html>')
self.assertEqual(expected_result, str(soup))
@@ -0,0 +1,23 @@
from unittest import TestCase
from bs4 import BeautifulSoup
from html_to_django.formatters import remove_formatter
class RemoveTests(TestCase):
def test_basic(self) -> None:
soup = BeautifulSoup('<div dj-remove><div>Test</div></div>', "html.parser")
remove_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
def test_empty(self) -> None:
soup = BeautifulSoup('<div dj-remove></div>', "html.parser")
remove_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
def test_with_remove_inside(self) -> None:
soup = BeautifulSoup('<div dj-remove><div dj-remove>Inner Test</div></div>', "html.parser")
remove_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
+3 -3
View File
@@ -5,19 +5,19 @@ from html_to_django.formatters import unwrap_formatter
class UnwrapTests(TestCase):
def test_basic(self) -> None:
soup = BeautifulSoup('<div dj-unwrap="true"><div>Test</div></div>', "html.parser")
soup = BeautifulSoup('<div dj-unwrap><div>Test</div></div>', "html.parser")
unwrap_formatter.format(soup)
expected_result = "<div>Test</div>"
self.assertEqual(expected_result, str(soup))
def test_empty(self) -> None:
soup = BeautifulSoup('<div dj-unwrap="true"></div>', "html.parser")
soup = BeautifulSoup('<div dj-unwrap></div>', "html.parser")
unwrap_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
def test_with_unwrap_inside(self) -> None:
soup = BeautifulSoup('<div dj-unwrap="true"><div dj-unwrap="true">Inner Test</div></div>', "html.parser")
soup = BeautifulSoup('<div dj-unwrap><div dj-unwrap>Inner Test</div></div>', "html.parser")
unwrap_formatter.format(soup)
expected_result = "Inner Test"
self.assertEqual(expected_result, str(soup))
+25 -60
View File
@@ -7,9 +7,24 @@ from io import StringIO
from unittest import TestCase
from bs4 import BeautifulSoup
from glob import glob
from typing import Any
from html_to_django.converter import get_required_libraries, convert_file, convert_dir, convert_path
class TestDirectory(tempfile.TemporaryDirectory): # type: ignore
"""
Context Manager that creates a temporary directory containing the test project.
"""
def __enter__(self) -> Any:
tmpdir = super().__enter__()
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
return tmpdir
class ConverterTests(TestCase):
def test_get_required_libraries(self) -> None:
html = '<div dj-libs="lib1;lib2">Content</div><div dj-libs="lib3"></div>'
@@ -24,12 +39,7 @@ class ConverterTests(TestCase):
self.assertEqual(expected, get_required_libraries(soup))
def test_convert_file(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_file(f"{tmpdir}/test.html")
# verify that the new file exists and has the expected value
self.assertTrue(filecmp.cmp(
@@ -43,12 +53,7 @@ class ConverterTests(TestCase):
))
def test_convert_file_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_file(f"{tmpdir}/test.html", True)
# verify that the source file has been overwriten
self.assertTrue(filecmp.cmp(
@@ -59,12 +64,7 @@ class ConverterTests(TestCase):
self.assertFalse(os.path.exists(f"{tmpdir}/test.n.html"))
def test_convert_file_non_html_extension(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
os.rename(f"{tmpdir}/test.html", f"{tmpdir}/test.txt")
convert_file(f"{tmpdir}/test.txt")
@@ -75,12 +75,7 @@ class ConverterTests(TestCase):
))
def test_convert_file_no_extension(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
os.rename(f"{tmpdir}/test.html", f"{tmpdir}/test")
convert_file(f"{tmpdir}/test")
@@ -99,12 +94,7 @@ class ConverterTests(TestCase):
convert_file("tests/test_project")
def test_convert_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_dir(f"{tmpdir}")
for conversion_result in glob(f"{tmpdir}/**/*.n.html", recursive=True):
@@ -125,12 +115,7 @@ class ConverterTests(TestCase):
))
def test_convert_dir_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_dir(f"{tmpdir}", True)
for conversion_result in glob(f"{tmpdir}/**/*.html", recursive=True):
@@ -153,12 +138,7 @@ class ConverterTests(TestCase):
convert_dir("tests/test_project/test.html")
def test_convert_path_file(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_path(f"{tmpdir}/test.html")
# verify that the new file exists and has the expected value
self.assertTrue(filecmp.cmp(
@@ -172,12 +152,7 @@ class ConverterTests(TestCase):
))
def test_convert_path_file_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_path(f"{tmpdir}/test.html", True)
# verify that the source file has been overwriten
self.assertTrue(filecmp.cmp(
@@ -188,12 +163,7 @@ class ConverterTests(TestCase):
self.assertFalse(os.path.exists(f"{tmpdir}/test.n.html"))
def test_convert_path_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_path(f"{tmpdir}")
for conversion_result in glob(f"{tmpdir}/**/*.n.html", recursive=True):
@@ -214,12 +184,7 @@ class ConverterTests(TestCase):
))
def test_convert_path_dir_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
with TestDirectory() as tmpdir:
convert_path(f"{tmpdir}", True)
for conversion_result in glob(f"{tmpdir}/**/*.html", recursive=True):