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 + +[![PyPI - Version](https://img.shields.io/pypi/v/pygments-djc)](https://pypi.org/project/pygments-djc/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pygments-djc)](https://pypi.org/project/pygments-djc/) [![PyPI - License](https://img.shields.io/pypi/l/pygments-djc)](https://github.com/django-components/pygments-djc/blob/main/LICENSE) [![PyPI - Downloads](https://img.shields.io/pypi/dm/pygments-djc)](https://pypistats.org/packages/pygments-djc) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/django-components/pygments-djc/tests.yml)](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: + +![Django Components Python Lexer Example](./assets/demo.png) + +## 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