diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..0f935c1
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: ["JuroOravec"]
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..17c5ae2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "pip" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/automate-dependabot.yml b/.github/workflows/automate-dependabot.yml
new file mode 100644
index 0000000..5b0e03c
--- /dev/null
+++ b/.github/workflows/automate-dependabot.yml
@@ -0,0 +1,22 @@
+name: Dependabot auto-merge
+on: pull_request
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: github.actor == 'dependabot[bot]'
+ steps:
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v2
+ with:
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Enable auto-merge for Dependabot PRs
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
new file mode 100644
index 0000000..cf2f2da
--- /dev/null
+++ b/.github/workflows/publish-to-pypi.yml
@@ -0,0 +1,45 @@
+name: Publish to PyPI
+
+on:
+ push:
+ tags:
+ - '*'
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ if: github.repository == 'django-components/pygments-djc'
+ steps:
+ - name: Checkout the repo
+ uses: actions/checkout@v2
+
+ - name: Setup python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.9'
+
+ - name: Install pypa/build
+ run: >-
+ python -m
+ pip install
+ build
+ --user
+
+ - name: Build a binary wheel and a source tarball
+ run: >-
+ python -m
+ build
+ --sdist
+ --wheel
+ --outdir dist/
+ .
+
+ - name: Publish distribution to PyPI
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..4fd3ace
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,31 @@
+name: Run tests
+
+on:
+ push:
+ branches:
+ - 'main'
+ - 'dev'
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: "pip"
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements-ci.txt
+ - name: Run tests
+ run: tox
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1d20d10
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,80 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+*.sqlite3
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# VSCode
+.vscode
+
+# Poetry
+# lock file is not needed for development
+# as project supports variety of Django versions
+poetry.lock
+
+# PyCharm
+.idea/
+
+# Python environment
+.venv/
+.DS_Store
+.python-version
+site
+
+# JS, NPM Dependency directories
+node_modules/
+jspm_packages/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..1854167
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,14 @@
+repos:
+ - repo: https://github.com/pycqa/isort
+ rev: 5.13.2
+ hooks:
+ - id: isort
+ - repo: https://github.com/psf/black
+ rev: 24.10.0
+ hooks:
+ - id: black
+ - repo: https://github.com/pycqa/flake8
+ rev: 7.1.1
+ hooks:
+ - id: flake8
+ additional_dependencies: [flake8-pyproject]
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a731127
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Release notes
+
+## v1.0.0 - First release
+
+_11 Feb 2025_
+
+### Feat
+
+- `djc_py` and `djc_python` code block - Syntax highlighting for Python code that contains Component classes with HTML / CSS / JS inlined blocks.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..a83376d
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at juraj.oravec.josefson@gmail.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c0ebbd8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Juro Oravec
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..fa3e700
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+# MANIFEST.in is defined so we can include non-Python (e.g. JS) files
+# in the built distribution.
+# See https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html
+graft src/pygments_djc
+prune tests
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d647b68
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# pygments-djc
+
+[](https://pypi.org/project/pygments-djc/) [](https://pypi.org/project/pygments-djc/) [](https://github.com/django-components/pygments-djc/blob/main/LICENSE) [](https://pypistats.org/packages/pygments-djc) [](https://github.com/django-components/pygments-djc/actions/workflows/tests.yml)
+
+_[Pygments](https://pygments.org/) Lexers for [django-components](https://pypi.org/project/django-components/)._
+
+## Installation
+
+1. Install the package:
+ ```bash
+ pip install pygments-djc
+ ```
+
+2. Add the lexers to your Pygments configuration by simply importing `pygments_djc`
+ ```python
+ import pygments_djc
+ ```
+
+## Lexers
+
+### `DjangoComponentsPythonLexer`
+
+Code blocks: `djc_py` / `djc_python`
+
+This is the same as Python3 Lexer, but also highlights nested JS / CSS / HTML code blocks within `Component` classes:
+
+```python
+class MyComponent(Component):
+ template = """
+
Hello World
+ """
+```
+
+The syntax highlight then looks like this:
+
+
+
+## Release notes
+
+Read the [Release Notes](https://github.com/django-components/pygments-djc/tree/main/CHANGELOG.md)
+to see the latest features and fixes.
+
+## Development
+
+### Tests
+
+To run tests, use:
+
+```bash
+pytest
+```
diff --git a/assets/demo.png b/assets/demo.png
new file mode 100644
index 0000000..6dd8270
Binary files /dev/null and b/assets/demo.png differ
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..812d14d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,94 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "pygments_djc"
+version = "1.0.0"
+requires-python = ">=3.8, <4.0"
+description = "Pygments Lexers for django-components"
+keywords = ["django", "components", "pygments", "lexer", "html", "js", "css"]
+readme = "README.md"
+authors = [
+ {name = "Juro Oravec", email = "juraj.oravec.josefson@gmail.com"},
+]
+classifiers = [
+ "Framework :: Django",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+ 'pygments>=2.15',
+]
+license = {text = "MIT"}
+
+[project.urls]
+Homepage = "https://github.com/django-components/pygments-djc/"
+
+
+[tool.setuptools.packages.find]
+where = ["src"]
+include = ["pygments_djc*"]
+exclude = ["pygments_djc.tests*"]
+namespaces = false
+
+[tool.black]
+line-length = 119
+include = '\.pyi?$'
+exclude = '''
+/(
+ \.git
+ | \.hg
+ | \.mypy_cache
+ | \.tox
+ | \.venv
+ | activate
+ | _build
+ | buck-out
+ | build
+ | dist
+)/
+'''
+
+[tool.isort]
+profile = "black"
+line_length = 119
+multi_line_output = 3
+include_trailing_comma = "True"
+known_first_party = "pygments_djc"
+
+[tool.flake8]
+ignore = ['E302', 'W503']
+max-line-length = 119
+exclude = [
+ '__pycache__',
+ 'env',
+ '.env',
+ '.venv',
+ '.tox',
+ 'build',
+]
+
+[tool.mypy]
+check_untyped_defs = true
+ignore_missing_imports = true
+exclude = [
+ 'build',
+]
+
+[[tool.mypy.overrides]]
+module = "pygments_djc.*"
+disallow_untyped_defs = true
+
+
+[tool.pytest.ini_options]
+testpaths = [
+ "tests"
+]
diff --git a/requirements-ci.in b/requirements-ci.in
new file mode 100644
index 0000000..4cc37a7
--- /dev/null
+++ b/requirements-ci.in
@@ -0,0 +1,3 @@
+tox
+tox-gh-actions
+pygments
\ No newline at end of file
diff --git a/requirements-ci.txt b/requirements-ci.txt
new file mode 100644
index 0000000..0cb9d51
--- /dev/null
+++ b/requirements-ci.txt
@@ -0,0 +1,40 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile requirements-ci.in
+#
+cachetools==5.5.1
+ # via tox
+chardet==5.2.0
+ # via tox
+colorama==0.4.6
+ # via tox
+distlib==0.3.9
+ # via virtualenv
+filelock==3.16.1
+ # via
+ # tox
+ # virtualenv
+packaging==24.2
+ # via
+ # pyproject-api
+ # tox
+platformdirs==4.3.6
+ # via
+ # tox
+ # virtualenv
+pluggy==1.5.0
+ # via tox
+pygments==2.19.1
+ # via -r requirements-ci.in
+pyproject-api==1.8.0
+ # via tox
+tox==4.24.1
+ # via
+ # -r requirements-ci.in
+ # tox-gh-actions
+tox-gh-actions==3.2.0
+ # via -r requirements-ci.in
+virtualenv==20.29.2
+ # via tox
diff --git a/requirements-dev.in b/requirements-dev.in
new file mode 100644
index 0000000..82de4d4
--- /dev/null
+++ b/requirements-dev.in
@@ -0,0 +1,9 @@
+tox
+pytest
+flake8
+flake8-pyproject
+isort
+pre-commit
+black
+mypy
+pygments
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..913d1fc
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,85 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile requirements-dev.in
+#
+black==25.1.0
+ # via -r requirements-dev.in
+cachetools==5.5.1
+ # via tox
+cfgv==3.4.0
+ # via pre-commit
+chardet==5.2.0
+ # via tox
+click==8.1.8
+ # via black
+colorama==0.4.6
+ # via tox
+distlib==0.3.9
+ # via virtualenv
+filelock==3.16.1
+ # via
+ # tox
+ # virtualenv
+flake8==7.1.1
+ # via
+ # -r requirements-dev.in
+ # flake8-pyproject
+flake8-pyproject==1.2.3
+ # via -r requirements-dev.in
+identify==2.6.7
+ # via pre-commit
+iniconfig==2.0.0
+ # via pytest
+isort==6.0.0
+ # via -r requirements-dev.in
+mccabe==0.7.0
+ # via flake8
+mypy==1.15.0
+ # via -r requirements-dev.in
+mypy-extensions==1.0.0
+ # via
+ # black
+ # mypy
+nodeenv==1.9.1
+ # via pre-commit
+packaging==24.2
+ # via
+ # black
+ # pyproject-api
+ # pytest
+ # tox
+pathspec==0.12.1
+ # via black
+platformdirs==4.3.6
+ # via
+ # black
+ # tox
+ # virtualenv
+pluggy==1.5.0
+ # via
+ # pytest
+ # tox
+pre-commit==4.1.0
+ # via -r requirements-dev.in
+pycodestyle==2.12.1
+ # via flake8
+pyflakes==3.2.0
+ # via flake8
+pygments==2.19.1
+ # via -r requirements-dev.in
+pyproject-api==1.8.0
+ # via tox
+pytest==8.3.4
+ # via -r requirements-dev.in
+pyyaml==6.0.2
+ # via pre-commit
+tox==4.24.1
+ # via -r requirements-dev.in
+typing-extensions==4.12.2
+ # via mypy
+virtualenv==20.29.2
+ # via
+ # pre-commit
+ # tox
diff --git a/src/pygments_djc/__init__.py b/src/pygments_djc/__init__.py
new file mode 100644
index 0000000..a0004af
--- /dev/null
+++ b/src/pygments_djc/__init__.py
@@ -0,0 +1,22 @@
+from pygments.lexers import LEXERS
+
+# Public API
+from pygments_djc.lexers import DjangoComponentsPythonLexer
+
+__all__ = ["DjangoComponentsPythonLexer"]
+
+
+# Register the Lexer. Unfortunately Pygments doesn't support registering a Lexer
+# without it living in the pygments codebase.
+# See https://github.com/pygments/pygments/issues/1096#issuecomment-1821807464
+LEXERS["DjangoComponentsPythonLexer"] = (
+ "pygments_djc",
+ "Django Components Python",
+ # The aliases of the Lexer - This means that code blocks like
+ # ```djc_py
+ # ```
+ # will be highlighted by this Lexer.
+ ["djc_py", "djc_python"],
+ [],
+ [],
+)
diff --git a/src/pygments_djc/lexers.py b/src/pygments_djc/lexers.py
new file mode 100644
index 0000000..24134db
--- /dev/null
+++ b/src/pygments_djc/lexers.py
@@ -0,0 +1,119 @@
+from typing import List, Tuple
+
+from pygments.lexer import Lexer, bygroups, using
+from pygments.lexers.css import CssLexer
+from pygments.lexers.javascript import JavascriptLexer
+from pygments.lexers.python import PythonLexer
+from pygments.lexers.templates import HtmlDjangoLexer
+from pygments.token import Name, Operator, Punctuation, String, Text
+
+
+# Since this Lexer will be used only in documentation, it's a naive implementation
+# that detects the nested JS / CSS / HTML blocks by searching for following patterns:
+# - `js = """`
+# - `js: some.Type = """`
+# - `css = """`
+# - `css: some.Type = """`
+# - `template = """`
+# - `template: some.Type = """`
+#
+# However, this Lexer is NOT sensitive to where the variable is defined. So this highlighting rule
+# will be triggered even when the variable is NOT defined on the Component class.
+#
+# In other words, we want to highlight cases like this:
+# ```py
+# class MyComponent(Component):
+# template = """
+# Hello World
+# """
+# ```
+#
+# But NOT cases like this:
+# ```py
+# js = """
+# ```
+#
+# But our implementation still highlights the latter.
+def _gen_code_block_capture_rule(var_name: str, next_state: str) -> Tuple[str, str, str]:
+ # In Pygments, capture rules are defined as a tuple of 3 elements:
+ # - The pattern to capture
+ # - The token to highlight
+ # - The next state
+ return (
+ # Captures patterns like `template = """` or `template: some.Type = """`
+ rf'({var_name})(\s*)(?:(:)(\s*)([^\s=]+))?(\s*)(=)(\s*)((?:"""|\'\'\'))',
+ # ^ ^ ^ ^ ^ ^ ^ ^ ^
+ # 1 2 3 4 5 6 7 8 9
+ #
+ # The way Pygments Lexers work, when we match something, we have to define how to highlight it.
+ # Since the match pattern contains complex structures, we use `bygroups` to highlight individual
+ # parts of the match.
+ # fmt: off
+ bygroups(
+ Name.Variable, # 1
+ Text, # 2
+ Punctuation, # 3
+ Text, # 4
+ Name.Class, # 5
+ Text, # 6
+ Operator, # 7
+ Text, # 8
+ String.Doc, # 9
+ ),
+ # fmt: on
+ # Lastly, we tell the Lexer what the next state should be
+ next_state,
+ )
+
+
+# This generates capture rules for when we are already inside the code block
+def _gen_code_block_rules(lexer: Lexer) -> List[Tuple[str, str, str]]:
+ return [
+ # We're inside the code block and we came across a """ or ''',
+ # so the code block ends.
+ # This is the FIRST item in the list, so it takes precedence over the other rules.
+ # `#pop` tells the Lexer to go back to the previous state.
+ (r'(?:"""|\'\'\')', String.Doc, "#pop"),
+ # Take everything until """ or ''', and pass it to corresponding lexer (e.g. JS / CSS / HTML Lexers)
+ (r'((?!"""|\'\'\')(?:.|\n))+', using(lexer)), # type: ignore
+ ]
+
+
+class DjangoComponentsPythonLexer(PythonLexer):
+ """
+ Lexer for Django Components Python code blocks.
+
+ This Lexer behaves like a normal Python Lexer, but also highlights
+ nested JS / CSS / HTML code blocks within Component classes:
+
+ ```py
+ class MyComponent(Component):
+ template = \"\"\"
+ Hello World
+ \"\"\"
+ js = \"\"\"
+ console.log("Hello World")
+ \"\"\"
+ css = \"\"\"
+ .my-component {
+ color: red;
+ }
+ \"\"\"
+ ```
+ """
+
+ name = "Django Components Python"
+ aliases = ["djc_py", "djc_python"]
+
+ tokens = {
+ **PythonLexer.tokens,
+ "root": [
+ _gen_code_block_capture_rule("template", "template_string"),
+ _gen_code_block_capture_rule("js", "js_string"),
+ _gen_code_block_capture_rule("css", "css_string"),
+ *PythonLexer.tokens["root"],
+ ],
+ "template_string": _gen_code_block_rules(HtmlDjangoLexer),
+ "js_string": _gen_code_block_rules(JavascriptLexer),
+ "css_string": _gen_code_block_rules(CssLexer),
+ }
diff --git a/src/pygments_djc/py.typed b/src/pygments_djc/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lexers.py b/tests/test_lexers.py
new file mode 100644
index 0000000..b3f2223
--- /dev/null
+++ b/tests/test_lexers.py
@@ -0,0 +1,396 @@
+from pygments.token import Keyword, Literal, Name, Operator, Punctuation, Text
+from pygments.lexers.css import CssLexer
+from pygments.lexers.javascript import JavascriptLexer
+from pygments.lexers.templates import HtmlDjangoLexer
+
+from pygments_djc import DjangoComponentsPythonLexer
+
+
+def test_djcpy_html():
+ code = '''
+class Calendar(Component):
+ template = """
+ Hello
+ """
+ '''
+ lexer = DjangoComponentsPythonLexer()
+ tokens = list(lexer.get_tokens(code))
+
+ html_tokens = list(HtmlDjangoLexer().get_tokens("Hello
"))
+ assert html_tokens == [
+ (Punctuation, "<"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "Hello"),
+ (Punctuation, "<"),
+ (Punctuation, "/"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "\n"),
+ ]
+
+ assert tokens == [
+ (Keyword, "class"),
+ (Text.Whitespace, " "),
+ (Name.Class, "Calendar"),
+ (Punctuation, "("),
+ (Name, "Component"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "template"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ (Text, "\n "),
+ # Syntax highlighting for the HTML
+ *html_tokens[:-1],
+ # End of the HTML
+ (Text, "\n "),
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Text.Whitespace, "\n"),
+ ]
+
+
+def test_djcpy_js():
+ code = '''
+class Calendar(Component):
+ js = """
+ console.log("Hello");
+ """
+ '''
+ lexer = DjangoComponentsPythonLexer()
+ tokens = list(lexer.get_tokens(code))
+
+ js_tokens = list(JavascriptLexer().get_tokens('console.log("Hello");'))
+
+ assert js_tokens == [
+ (Name.Other, "console"),
+ (Punctuation, "."),
+ (Name.Other, "log"),
+ (Punctuation, "("),
+ (Literal.String.Double, '"Hello"'),
+ (Punctuation, ")"),
+ (Punctuation, ";"),
+ (Text.Whitespace, "\n"),
+ ]
+
+ assert tokens == [
+ (Keyword, "class"),
+ (Text.Whitespace, " "),
+ (Name.Class, "Calendar"),
+ (Punctuation, "("),
+ (Name, "Component"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "js"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ (Text, ""),
+ (Text.Whitespace, "\n "),
+ # Syntax highlighting for the JS
+ *js_tokens[:-1],
+ # End of the JS
+ (Text.Whitespace, "\n "),
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Text.Whitespace, "\n"),
+ ]
+
+
+def test_djcpy_css():
+ code = '''
+class Calendar(Component):
+ css = """
+ .calendar {
+ background-color: red;
+ }
+ """
+ '''
+ lexer = DjangoComponentsPythonLexer()
+ tokens = list(lexer.get_tokens(code))
+
+ css_tokens = list(CssLexer().get_tokens(".calendar { background-color: red; }"))
+
+ assert css_tokens == [
+ (Punctuation, "."),
+ (Name.Class, "calendar"),
+ (Text.Whitespace, " "),
+ (Punctuation, "{"),
+ (Text.Whitespace, " "),
+ (Keyword, "background-color"),
+ (Punctuation, ":"),
+ (Text.Whitespace, " "),
+ (Keyword.Constant, "red"),
+ (Punctuation, ";"),
+ (Text.Whitespace, " "),
+ (Punctuation, "}"),
+ (Text.Whitespace, "\n"),
+ ]
+
+ assert tokens == [
+ (Keyword, "class"),
+ (Text.Whitespace, " "),
+ (Name.Class, "Calendar"),
+ (Punctuation, "("),
+ (Name, "Component"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "css"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n "),
+ # Syntax highlighting for the CSS
+ (Punctuation, "."),
+ (Name.Class, "calendar"),
+ (Text.Whitespace, " "),
+ (Punctuation, "{"),
+ (Text.Whitespace, "\n "),
+ (Keyword, "background-color"),
+ (Punctuation, ":"),
+ (Text.Whitespace, " "),
+ (Keyword.Constant, "red"),
+ (Punctuation, ";"),
+ (Text.Whitespace, "\n "),
+ (Punctuation, "}"),
+ (Text.Whitespace, "\n "),
+ # End of the CSS
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Text.Whitespace, "\n"),
+ ]
+
+
+def test_djcpy_combined():
+ code = '''
+class Calendar(Component):
+ template = """
+ Hello
+ """
+ js = """
+ console.log("Hello");
+ """
+ css = """
+ .calendar {
+ background-color: red;
+ }
+ """
+
+ def get_context_data(self):
+ return {
+ "hello": "world",
+ }
+ '''
+ lexer = DjangoComponentsPythonLexer()
+ tokens = list(lexer.get_tokens(code))
+
+ assert tokens == [
+ (Keyword, "class"),
+ (Text.Whitespace, " "),
+ (Name.Class, "Calendar"),
+ (Punctuation, "("),
+ (Name, "Component"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "template"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ # HTML
+ (Text, "\n "),
+ (Punctuation, "<"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "Hello"),
+ (Punctuation, "<"),
+ (Punctuation, "/"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "\n "),
+ # End of the HTML
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "js"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ # JS
+ (Text, ""),
+ (Text.Whitespace, "\n "),
+ (Name.Other, "console"),
+ (Punctuation, "."),
+ (Name.Other, "log"),
+ (Punctuation, "("),
+ (Literal.String.Double, '"Hello"'),
+ (Punctuation, ")"),
+ (Punctuation, ";"),
+ (Text.Whitespace, "\n "),
+ # End of the JS
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "css"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n "),
+ # CSS
+ (Punctuation, "."),
+ (Name.Class, "calendar"),
+ (Text.Whitespace, " "),
+ (Punctuation, "{"),
+ (Text.Whitespace, "\n "),
+ (Keyword, "background-color"),
+ (Punctuation, ":"),
+ (Text.Whitespace, " "),
+ (Keyword.Constant, "red"),
+ (Punctuation, ";"),
+ (Text.Whitespace, "\n "),
+ (Punctuation, "}"),
+ (Text.Whitespace, "\n "),
+ (Literal.String.Doc, '"""'),
+ # End of the CSS
+ (Text.Whitespace, "\n"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ # get_context_data
+ (Keyword, "def"),
+ (Text.Whitespace, " "),
+ (Name.Function, "get_context_data"),
+ (Punctuation, "("),
+ (Name.Builtin.Pseudo, "self"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Keyword, "return"),
+ (Text, " "),
+ (Punctuation, "{"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Literal.String.Double, '"'),
+ (Literal.String.Double, "hello"),
+ (Literal.String.Double, '"'),
+ (Punctuation, ":"),
+ (Text, " "),
+ (Literal.String.Double, '"'),
+ (Literal.String.Double, "world"),
+ (Literal.String.Double, '"'),
+ (Punctuation, ","),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Punctuation, "}"),
+ (Text.Whitespace, "\n"),
+ # End of the get_context_data
+ (Text, " "),
+ (Text.Whitespace, "\n"),
+ ]
+
+
+def test_djcpy_type_hints():
+ code = '''
+class Calendar(Component):
+ template: types.html = """
+ Hello
+ """
+ '''
+ lexer = DjangoComponentsPythonLexer()
+ tokens = list(lexer.get_tokens(code))
+
+ assert tokens == [
+ (Keyword, "class"),
+ (Text.Whitespace, " "),
+ (Name.Class, "Calendar"),
+ (Punctuation, "("),
+ (Name, "Component"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "template"),
+ (Punctuation, ":"),
+ (Text, " "),
+ (Name.Class, "types.html"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, '"""'),
+ (Text, "\n "),
+ # Syntax highlighting for the HTML
+ (Punctuation, "<"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "Hello"),
+ (Punctuation, "<"),
+ (Punctuation, "/"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ # End of the HTML
+ (Text, "\n "),
+ (Literal.String.Doc, '"""'),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Text.Whitespace, "\n"),
+ ]
+
+
+def test_djcpy_triple_single_quotes():
+ code = """
+class Calendar(Component):
+ template = '''
+ Hello
+ '''
+ """
+ lexer = DjangoComponentsPythonLexer()
+ tokens = list(lexer.get_tokens(code))
+
+ assert tokens == [
+ (Keyword, "class"),
+ (Text.Whitespace, " "),
+ (Name.Class, "Calendar"),
+ (Punctuation, "("),
+ (Name, "Component"),
+ (Punctuation, ")"),
+ (Punctuation, ":"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Name.Variable, "template"),
+ (Text, " "),
+ (Operator, "="),
+ (Text, " "),
+ (Literal.String.Doc, "'''"),
+ (Text, "\n "),
+ (Punctuation, "<"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "Hello"),
+ (Punctuation, "<"),
+ (Punctuation, "/"),
+ (Name.Tag, "div"),
+ (Punctuation, ">"),
+ (Text, "\n "),
+ (Literal.String.Doc, "'''"),
+ (Text.Whitespace, "\n"),
+ (Text, " "),
+ (Text.Whitespace, "\n"),
+ ]
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..d78583b
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,54 @@
+[tox]
+envlist =
+ py{38,39,310,311,312,313}
+ flake8
+ isort
+ coverage
+ mypy
+ black
+
+[gh-actions]
+python =
+ 3.8: py38
+ 3.9: py39
+ 3.10: py310
+ 3.11: py311
+ 3.12: py312
+ 3.13: py313, flake8, isort, coverage, mypy, black
+
+isolated_build = true
+
+[testenv]
+package = wheel
+wheel_build_env = .pkg
+deps =
+ pygments
+ pytest
+ pytest-xdist
+commands = pytest {posargs}
+
+[testenv:flake8]
+deps = flake8
+ flake8-pyproject
+commands = flake8 .
+
+[testenv:isort]
+deps = isort
+commands = isort --check-only --diff src/pygments_djc
+
+[testenv:coverage]
+deps =
+ pytest-coverage
+commands =
+ coverage run --branch -m pytest
+ coverage report -m --fail-under=97
+
+[testenv:mypy]
+deps =
+ mypy
+ types-requests
+commands = mypy .
+
+[testenv:black]
+deps = black
+commands = black --check src/pygments_djc