├── py.typed ├── tests ├── __init__.py ├── dj_angles │ ├── __init__.py │ ├── attributes │ │ ├── __init__.py │ │ └── test_attributes.py │ ├── evaluator │ │ ├── __init__.py │ │ └── test_evaluated_function.py │ ├── filters │ │ ├── __init__.py │ │ └── test_dateformat.py │ ├── mappers │ │ ├── __init__.py │ │ ├── angles │ │ │ ├── __init__.py │ │ │ ├── test_map_call.py │ │ │ ├── test_map_model.py │ │ │ └── test_map_form.py │ │ ├── django │ │ │ ├── __init__.py │ │ │ ├── test_map_extends.py │ │ │ ├── test_map_image.py │ │ │ ├── test_map_css.py │ │ │ └── test_map_block.py │ │ ├── mapper │ │ │ ├── __init__.py │ │ │ └── test_get_tag_map.py │ │ ├── thirdparty │ │ │ ├── __init__.py │ │ │ ├── test_map_partialdef.py │ │ │ └── test_map_bird.py │ │ └── include │ │ │ ├── test_get_include_template_file.py │ │ │ └── test_map_include.py │ ├── middleware │ │ ├── __init__.py │ │ ├── test_request_ajax.py │ │ └── test_request_method.py │ ├── strings │ │ ├── __init__.py │ │ └── test_replace_newlines.py │ ├── tokenizer │ │ ├── __init__.py │ │ └── test_yield_tokens.py │ ├── caseconverters │ │ ├── __init__.py │ │ └── test_kebabify.py │ ├── templatetags │ │ ├── __init__.py │ │ ├── call │ │ │ ├── __init__.py │ │ │ └── test_do_call.py │ │ ├── model │ │ │ ├── __init__.py │ │ │ ├── test_do_model.py │ │ │ └── test_render.py │ │ ├── test_dj_angles.py │ │ └── template │ │ │ └── test_render.py │ ├── tags │ │ ├── __init__.py │ │ └── test_tag.py │ ├── regex_replacer │ │ ├── test_get_attribute_replacements.py │ │ └── test_django_tag_replacer.py │ └── test_template_loader.py └── templates │ ├── _underscore.html │ ├── components │ └── _underscore.html │ └── slot.html ├── example ├── __init__.py ├── www │ ├── __init__.py │ ├── templates │ │ ├── _partial.html │ │ └── www │ │ │ ├── components │ │ │ ├── include.html │ │ │ ├── include-slot.html │ │ │ └── include-shadow.html │ │ │ ├── index.html │ │ │ ├── bird.html │ │ │ ├── base.html │ │ │ └── include.html │ ├── urls.py │ └── views.py ├── book │ ├── __init__.py │ ├── apps.py │ └── models.py ├── project │ ├── __init__.py │ ├── urls.py │ ├── wsgi.py │ └── settings.py ├── templates │ └── bird │ │ └── button.html └── manage.py ├── src └── dj_angles │ ├── templatetags │ ├── __init__.py │ ├── dj_angles.py │ ├── model.py │ └── template.py │ ├── caseconverter │ ├── __init__.py │ ├── kebab.py │ ├── LICENSE │ ├── boundaries.py │ ├── README.md │ └── caseconverter.py │ ├── __init__.py │ ├── templates │ └── dj_angles │ │ └── scripts.html │ ├── modules.py │ ├── regex_replacer │ ├── objects.py │ ├── __init__.py │ ├── django_tag_replacer.py │ └── tag_replacer.py │ ├── mappers │ ├── __init__.py │ ├── thirdparty.py │ ├── mapper.py │ ├── include.py │ ├── django.py │ └── angles.py │ ├── strings.py │ ├── middleware.py │ ├── settings.py │ ├── exceptions.py │ ├── template_loader.py │ ├── tokenizer.py │ ├── templates.py │ ├── htmls.py │ ├── static │ └── dj_angles │ │ └── js │ │ └── ajax-form.js │ └── evaluator.py ├── .github ├── FUNDING.yml └── workflows │ ├── publish.yml │ └── checks.yml ├── .gitignore ├── justfile ├── DEVELOPING.md ├── .all-contributorsrc ├── docs └── source │ ├── template-tags │ ├── model.md │ └── template.md │ ├── middlewares │ ├── request-method.md │ └── request-ajax.md │ ├── inline-expressions.md │ ├── tag-attributes.md │ ├── conf.py │ ├── settings.md │ ├── examples.md │ ├── mappers.md │ ├── installation.md │ ├── integrations │ ├── django-template-partials.md │ └── django-bird.md │ ├── custom-elements │ └── ajax-form.md │ ├── changelog.md │ ├── index.md │ └── tag-elements.md ├── .readthedocs.yml ├── LICENSE ├── conftest.py ├── pyproject.toml └── CHANGELOG.md /py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/www/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/book/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dj_angles/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/attributes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/evaluator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/mappers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/strings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/tokenizer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/caseconverters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/mappers/angles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/mappers/django/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/mappers/mapper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/templates/_underscore.html: -------------------------------------------------------------------------------- 1 | _underscore -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adamghill 2 | 3 | -------------------------------------------------------------------------------- /tests/dj_angles/mappers/thirdparty/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/templatetags/call/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dj_angles/templatetags/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/templates/components/_underscore.html: -------------------------------------------------------------------------------- 1 | components/_underscore -------------------------------------------------------------------------------- /example/www/templates/_partial.html: -------------------------------------------------------------------------------- 1 |
3 | This is a regular include. 4 |
5 |3 | This is a slot include. 4 |
5 | 6 |
5 | This is an example Django site that shows how dj-angles works.
6 |
3 | This is a shadow include. 4 |
5 |
",
19 | replacement="{% if True %}
{% endif %}",
20 | ),
21 | ),
22 | )
23 | def test_if(original, replacement):
24 | actual = get_attribute_replacements(original)
25 |
26 | for tag_replacement in actual:
27 | assert tag_replacement.original == original
28 | assert tag_replacement.replacement == replacement
29 |
--------------------------------------------------------------------------------
/tests/dj_angles/templatetags/model/test_do_model.py:
--------------------------------------------------------------------------------
1 | from django.template.base import Token, TokenType
2 |
3 | from dj_angles.templatetags.model import do_model
4 |
5 |
6 | def test_model():
7 | token = Token(TokenType.BLOCK, contents="model Book.objects.filter(id=1).first() as books")
8 | actual = do_model(None, token)
9 |
10 | assert actual.parsed_function.portions[0].name == "__dj_angles_models"
11 | assert actual.parsed_function.portions[1].name == "Book"
12 | assert actual.parsed_function.portions[2].name == "objects"
13 | assert actual.parsed_function.portions[3].name == "filter"
14 | assert actual.parsed_function.portions[3].args == []
15 | assert actual.parsed_function.portions[3].kwargs == {"id": 1}
16 | assert actual.parsed_function.portions[4].name == "first"
17 | assert actual.context_variable_name == "books"
18 |
--------------------------------------------------------------------------------
/src/dj_angles/caseconverter/kebab.py:
--------------------------------------------------------------------------------
1 | from dj_angles.caseconverter.boundaries import OnDelimeterLowercaseNext, OnUpperPrecededByLowerAppendLower
2 | from dj_angles.caseconverter.caseconverter import CaseConverter
3 |
4 |
5 | class Kebab(CaseConverter):
6 | JOIN_CHAR = "-"
7 |
8 | def define_boundaries(self):
9 | self.add_boundary_handler(OnDelimeterLowercaseNext(self.delimiters(), self.JOIN_CHAR))
10 | self.add_boundary_handler(OnUpperPrecededByLowerAppendLower(self.JOIN_CHAR))
11 |
12 | def prepare_string(self, s):
13 | if s.isupper():
14 | return s.lower()
15 |
16 | return s
17 |
18 | def mutate(self, c):
19 | return c.lower()
20 |
21 |
22 | def kebabify(s, **kwargs):
23 | """Convert a string to kebab case
24 |
25 | Example
26 |
27 | Hello World => hello-world
28 |
29 | """
30 | return Kebab(s, **kwargs).convert()
31 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 | version: 2
5 |
6 | sphinx:
7 | configuration: docs/source/conf.py
8 | fail_on_warning: true
9 | builder: dirhtml
10 |
11 | build:
12 | os: ubuntu-lts-latest
13 | tools:
14 | python: latest
15 | jobs:
16 | pre_create_environment:
17 | - asdf plugin add uv
18 | - asdf install uv latest
19 | - asdf global uv latest
20 | create_environment:
21 | - uv venv $READTHEDOCS_VIRTUALENV_PATH
22 | install:
23 | # Use a cache dir in the same mount to halve the install time
24 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH uv sync --cache-dir $READTHEDOCS_VIRTUALENV_PATH/../../uv_cache --group docs
25 | build:
26 | html:
27 | - uv run sphinx-build -T -b dirhtml docs/source $READTHEDOCS_OUTPUT/html
28 |
--------------------------------------------------------------------------------
/src/dj_angles/strings.py:
--------------------------------------------------------------------------------
1 | def dequotify(s: str) -> str:
2 | """Removes single or double quotes from a string.
3 |
4 | Args:
5 | param s: The string to remove quotes from.
6 |
7 | Returns:
8 | A new string without the quotes or the original if there were no quotes.
9 | """
10 |
11 | if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
12 | return s[1:-1]
13 |
14 | return s
15 |
16 |
17 | def replace_newlines(s: str, replacement: str = "") -> str:
18 | """Replaces newlines with the given replacement string.
19 |
20 | Args:
21 | param s: The string to replace newlines in.
22 | param replacement: The string to replace the newlines with.
23 |
24 | Returns:
25 | A new string with the newlines replaced.
26 | """
27 |
28 | return s.replace("\r\n", replacement).replace("\n", replacement).replace("\r", replacement)
29 |
--------------------------------------------------------------------------------
/docs/source/middlewares/request-method.md:
--------------------------------------------------------------------------------
1 | # RequestMethodMiddleware
2 |
3 | Adds the request's method as boolean properties to the `request` object.
4 |
5 | ```{note}
6 | Make sure to [install `dj_angles` middleware](../installation.md#middleware) to access this functionality.
7 | ```
8 |
9 | ## Example
10 |
11 | ```python
12 | # views.py
13 | from django.shortcuts import render
14 | from book.models import Book
15 |
16 | def book(request, book_id):
17 | book = Book.objects.filter(id=book_id).first()
18 |
19 | if request.is_post:
20 | book.name = request.POST.get('name')
21 | book.save()
22 |
23 | return redirect('book', id=book.id)
24 |
25 | return render(request, 'book.html', {'book': book})
26 | ```
27 |
28 | ## Properties
29 |
30 | - `request.is_post`
31 | - `request.is_get`
32 | - `request.is_patch`
33 | - `request.is_head`
34 | - `request.is_put`
35 | - `request.is_delete`
36 | - `request.is_trace`
--------------------------------------------------------------------------------
/docs/source/inline-expressions.md:
--------------------------------------------------------------------------------
1 | # Inline Expressions
2 |
3 | The `dj-angles` approach is shown first and then the equivalent Django Template Language is second.
4 |
5 | ## `or`
6 |
7 | Similar to the [`default` filter](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#default), but feels a little more Pythonic.
8 |
9 | ```html
10 | {{ request.user.username or request.user.email }}
11 | ```
12 |
13 | ```html
14 | {% if request.user.username %}{{ request.user.username }}{% else %}{{ request.user.email }}{% endif %}
15 | ```
16 |
17 | ## `if`
18 |
19 | Python ternary operator where the first part is a conditional, the part after the " if " is the true value, and the part after the " else " is the false value.
20 |
21 | ```html
22 | {{ request.user.username if request.user.is_authenticated else 'Unknown' }}
23 | ```
24 |
25 | ```html
26 | {% if request.user.is_authenticated %}{{ request.user.username }}{% else %}Unknown{% endif %}
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/source/middlewares/request-ajax.md:
--------------------------------------------------------------------------------
1 | # RequestAJAXMiddleware
2 |
3 | Adds whether the request is AJAX as a boolean property to the `request` object. Useful to check when rendering a view's HTML via AJAX, i.e. using the [`form` tag](../tag-elements.md#form) or [HTMX](https://htmx.org/).
4 |
5 | ```{note}
6 | Make sure to [install `dj_angles` middleware](../installation.md#middleware) to access this functionality.
7 | ```
8 |
9 | ## Example
10 |
11 | ```python
12 | # views.py
13 | from django.shortcuts import render
14 | from book.models import Book
15 |
16 | def book(request, book_id):
17 | book = Book.objects.filter(id=book_id).first()
18 |
19 | if request.is_post:
20 | book.title = request.POST.get('title')
21 | book.save()
22 |
23 | if not request.is_ajax:
24 | return redirect('book', id=book.id)
25 |
26 | return render(request, 'book.html', {'book': book})
27 | ```
28 |
29 | ## Properties
30 |
31 | - `request.is_ajax`
32 |
--------------------------------------------------------------------------------
/tests/dj_angles/test_template_loader.py:
--------------------------------------------------------------------------------
1 | from types import SimpleNamespace
2 |
3 | from django.template import Engine
4 |
5 | from dj_angles.template_loader import Loader
6 |
7 |
8 | def test_get_contents_returns_original_when_no_angles(tmp_path):
9 | html = "19 | https://github.com/adamghill/dj-angles 20 |
21 | 29 |
220 | ```
221 |
222 | ## [`model`](template-tags/model.md)
223 |
224 | ```
225 |