pygments-citry 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygments_citry-0.1.0/LICENSE +22 -0
- pygments_citry-0.1.0/PKG-INFO +126 -0
- pygments_citry-0.1.0/README.md +96 -0
- pygments_citry-0.1.0/pygments_citry/__init__.py +42 -0
- pygments_citry-0.1.0/pygments_citry/citry_html.py +195 -0
- pygments_citry-0.1.0/pygments_citry/lexers.py +79 -0
- pygments_citry-0.1.0/pygments_citry/py.typed +0 -0
- pygments_citry-0.1.0/pygments_citry.egg-info/PKG-INFO +126 -0
- pygments_citry-0.1.0/pygments_citry.egg-info/SOURCES.txt +16 -0
- pygments_citry-0.1.0/pygments_citry.egg-info/dependency_links.txt +1 -0
- pygments_citry-0.1.0/pygments_citry.egg-info/entry_points.txt +3 -0
- pygments_citry-0.1.0/pygments_citry.egg-info/requires.txt +1 -0
- pygments_citry-0.1.0/pygments_citry.egg-info/top_level.txt +1 -0
- pygments_citry-0.1.0/pyproject.toml +68 -0
- pygments_citry-0.1.0/setup.cfg +4 -0
- pygments_citry-0.1.0/tests/test_citry_html.py +231 -0
- pygments_citry-0.1.0/tests/test_lexers.py +68 -0
- pygments_citry-0.1.0/tests/test_registration.py +34 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 citry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pygments-citry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pygments lexer for Citry components: highlights the HTML, JS, and CSS embedded in a component's template/js/css strings
|
|
5
|
+
Author-email: Juro Oravec <juraj.oravec.josefson@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://citry.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/citry-dev/citry
|
|
9
|
+
Project-URL: Changelog, https://github.com/citry-dev/citry/blob/main/packages/py/pygments_citry/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/citry-dev/citry/issues
|
|
11
|
+
Keywords: citry,pygments,lexer,syntax highlighting,components,html
|
|
12
|
+
Classifier: Environment :: Plugins
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
23
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: <4.0,>=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: pygments>=2.15
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# pygments-citry
|
|
32
|
+
|
|
33
|
+
A [Pygments](https://pygments.org/) lexer for [Citry](https://citry.dev)
|
|
34
|
+
components. It highlights a component the way an editor would: the Python class,
|
|
35
|
+
plus the HTML, JavaScript, and CSS embedded in its `template`, `js`, and `css`
|
|
36
|
+
string attributes, each in its own language.
|
|
37
|
+
|
|
38
|
+
Without it, a Citry component in a ` ```python ` block renders the template,
|
|
39
|
+
JS script, and CSS stylesheet as flat string literals. With it, a ` ```citry `
|
|
40
|
+
block colours the `<c-*>` tags, the `{{ ... }}` interpolation, the embedded
|
|
41
|
+
stylesheet, and the embedded script.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pygments-citry
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package registers a `citry` lexer as a Pygments plugin, so any tool that
|
|
50
|
+
resolves lexers by name picks it up once it is installed.
|
|
51
|
+
|
|
52
|
+
## Use it
|
|
53
|
+
|
|
54
|
+
On the command line:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pygmentize -l citry my_component.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
In Python:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pygments import highlight
|
|
64
|
+
from pygments.formatters import HtmlFormatter
|
|
65
|
+
from pygments.lexers import get_lexer_by_name
|
|
66
|
+
|
|
67
|
+
lexer = get_lexer_by_name("citry")
|
|
68
|
+
html = highlight(source, lexer, HtmlFormatter())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In a Markdown site (mkdocs with `pymdownx.highlight` / `pymdownx.superfences`,
|
|
72
|
+
or any python-markdown setup that resolves Pygments lexers by name), tag the
|
|
73
|
+
fence with `citry`:
|
|
74
|
+
|
|
75
|
+
````markdown
|
|
76
|
+
```citry
|
|
77
|
+
from citry import Component
|
|
78
|
+
|
|
79
|
+
class Welcome(Component):
|
|
80
|
+
template = """
|
|
81
|
+
<div class="card">
|
|
82
|
+
<h1>{{ title }}</h1>
|
|
83
|
+
<p c-if="count">You have {{ count }} new messages.</p>
|
|
84
|
+
</div>
|
|
85
|
+
"""
|
|
86
|
+
```
|
|
87
|
+
````
|
|
88
|
+
|
|
89
|
+
No superfences `custom_fences` entry is needed: once the package is installed,
|
|
90
|
+
`citry` resolves like any other Pygments lexer.
|
|
91
|
+
|
|
92
|
+
## What it highlights
|
|
93
|
+
|
|
94
|
+
A `citry` block is highlighted as Python, and the following Citry constructs
|
|
95
|
+
inside the `template` / `js` / `css` attributes are highlighted in their own
|
|
96
|
+
language:
|
|
97
|
+
|
|
98
|
+
- The HTML in `template`, including the `<c-*>` component and control-flow tags
|
|
99
|
+
(the built-ins `c-if`, `c-for`, `c-slot`, `<c-raw>`, and so on) and `c-*`
|
|
100
|
+
dynamic attributes.
|
|
101
|
+
- `{{ ... }}` interpolation, whose body is a Python expression.
|
|
102
|
+
- `{# ... #}` template comments.
|
|
103
|
+
- The JavaScript in `js` and the CSS in `css`.
|
|
104
|
+
|
|
105
|
+
The `<c-raw>...</c-raw>` element is treated as verbatim text, matching the
|
|
106
|
+
engine: `{{ }}` and tags inside it are not interpreted.
|
|
107
|
+
|
|
108
|
+
## Template-only blocks: `citry-html`
|
|
109
|
+
|
|
110
|
+
For a fenced block that shows only a template, with no surrounding Python (just
|
|
111
|
+
`<c-*>` tags, `{{ ... }}`, and `{# ... #}`), tag it `citry-html`:
|
|
112
|
+
|
|
113
|
+
````markdown
|
|
114
|
+
```citry-html
|
|
115
|
+
<c-for each="item in items">
|
|
116
|
+
<li c-if="item.visible">{{ item.label }}</li>
|
|
117
|
+
</c-for>
|
|
118
|
+
```
|
|
119
|
+
````
|
|
120
|
+
|
|
121
|
+
`citry` expects full component code (Python with embedded strings), so a bare
|
|
122
|
+
template belongs in a `citry-html` block instead.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT. Part of the [Citry](https://github.com/citry-dev/citry) project.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# pygments-citry
|
|
2
|
+
|
|
3
|
+
A [Pygments](https://pygments.org/) lexer for [Citry](https://citry.dev)
|
|
4
|
+
components. It highlights a component the way an editor would: the Python class,
|
|
5
|
+
plus the HTML, JavaScript, and CSS embedded in its `template`, `js`, and `css`
|
|
6
|
+
string attributes, each in its own language.
|
|
7
|
+
|
|
8
|
+
Without it, a Citry component in a ` ```python ` block renders the template,
|
|
9
|
+
JS script, and CSS stylesheet as flat string literals. With it, a ` ```citry `
|
|
10
|
+
block colours the `<c-*>` tags, the `{{ ... }}` interpolation, the embedded
|
|
11
|
+
stylesheet, and the embedded script.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pygments-citry
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The package registers a `citry` lexer as a Pygments plugin, so any tool that
|
|
20
|
+
resolves lexers by name picks it up once it is installed.
|
|
21
|
+
|
|
22
|
+
## Use it
|
|
23
|
+
|
|
24
|
+
On the command line:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pygmentize -l citry my_component.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In Python:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pygments import highlight
|
|
34
|
+
from pygments.formatters import HtmlFormatter
|
|
35
|
+
from pygments.lexers import get_lexer_by_name
|
|
36
|
+
|
|
37
|
+
lexer = get_lexer_by_name("citry")
|
|
38
|
+
html = highlight(source, lexer, HtmlFormatter())
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In a Markdown site (mkdocs with `pymdownx.highlight` / `pymdownx.superfences`,
|
|
42
|
+
or any python-markdown setup that resolves Pygments lexers by name), tag the
|
|
43
|
+
fence with `citry`:
|
|
44
|
+
|
|
45
|
+
````markdown
|
|
46
|
+
```citry
|
|
47
|
+
from citry import Component
|
|
48
|
+
|
|
49
|
+
class Welcome(Component):
|
|
50
|
+
template = """
|
|
51
|
+
<div class="card">
|
|
52
|
+
<h1>{{ title }}</h1>
|
|
53
|
+
<p c-if="count">You have {{ count }} new messages.</p>
|
|
54
|
+
</div>
|
|
55
|
+
"""
|
|
56
|
+
```
|
|
57
|
+
````
|
|
58
|
+
|
|
59
|
+
No superfences `custom_fences` entry is needed: once the package is installed,
|
|
60
|
+
`citry` resolves like any other Pygments lexer.
|
|
61
|
+
|
|
62
|
+
## What it highlights
|
|
63
|
+
|
|
64
|
+
A `citry` block is highlighted as Python, and the following Citry constructs
|
|
65
|
+
inside the `template` / `js` / `css` attributes are highlighted in their own
|
|
66
|
+
language:
|
|
67
|
+
|
|
68
|
+
- The HTML in `template`, including the `<c-*>` component and control-flow tags
|
|
69
|
+
(the built-ins `c-if`, `c-for`, `c-slot`, `<c-raw>`, and so on) and `c-*`
|
|
70
|
+
dynamic attributes.
|
|
71
|
+
- `{{ ... }}` interpolation, whose body is a Python expression.
|
|
72
|
+
- `{# ... #}` template comments.
|
|
73
|
+
- The JavaScript in `js` and the CSS in `css`.
|
|
74
|
+
|
|
75
|
+
The `<c-raw>...</c-raw>` element is treated as verbatim text, matching the
|
|
76
|
+
engine: `{{ }}` and tags inside it are not interpreted.
|
|
77
|
+
|
|
78
|
+
## Template-only blocks: `citry-html`
|
|
79
|
+
|
|
80
|
+
For a fenced block that shows only a template, with no surrounding Python (just
|
|
81
|
+
`<c-*>` tags, `{{ ... }}`, and `{# ... #}`), tag it `citry-html`:
|
|
82
|
+
|
|
83
|
+
````markdown
|
|
84
|
+
```citry-html
|
|
85
|
+
<c-for each="item in items">
|
|
86
|
+
<li c-if="item.visible">{{ item.label }}</li>
|
|
87
|
+
</c-for>
|
|
88
|
+
```
|
|
89
|
+
````
|
|
90
|
+
|
|
91
|
+
`citry` expects full component code (Python with embedded strings), so a bare
|
|
92
|
+
template belongs in a `citry-html` block instead.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT. Part of the [Citry](https://github.com/citry-dev/citry) project.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pygments-citry: a Pygments lexer for Citry components.
|
|
3
|
+
|
|
4
|
+
Importing this package registers the ``citry`` lexer with Pygments, so
|
|
5
|
+
``get_lexer_by_name("citry")`` resolves even in an environment where the plugin
|
|
6
|
+
entry point was not picked up (for example an editable checkout whose plugin
|
|
7
|
+
metadata is not written). For an installed package, the ``pygments.lexers``
|
|
8
|
+
entry point in ``pyproject.toml`` is the primary registration and needs no
|
|
9
|
+
import.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pygments.lexers import LEXERS
|
|
15
|
+
|
|
16
|
+
from pygments_citry.citry_html import CitryHtmlLexer
|
|
17
|
+
from pygments_citry.lexers import CitryPythonLexer
|
|
18
|
+
|
|
19
|
+
__all__ = ["CitryHtmlLexer", "CitryPythonLexer"]
|
|
20
|
+
|
|
21
|
+
# Register in Pygments' builtin lexer table. get_lexer_by_name scans this table
|
|
22
|
+
# before the entry-point plugins, so an explicit `import pygments_citry` becomes
|
|
23
|
+
# a deterministic loader. Each tuple is (module, name, aliases, filenames,
|
|
24
|
+
# mimetypes); _load_lexers imports the module and finds the class via __all__.
|
|
25
|
+
#
|
|
26
|
+
# `citry` is the full component lexer (Python + embedded template/js/css);
|
|
27
|
+
# `citry-html` is the template lexer alone, for a fence that shows only a
|
|
28
|
+
# template (`<c-*>` tags, `{{ }}`, `{# #}`) with no surrounding Python.
|
|
29
|
+
LEXERS["CitryPythonLexer"] = (
|
|
30
|
+
"pygments_citry",
|
|
31
|
+
"Citry Python",
|
|
32
|
+
("citry",),
|
|
33
|
+
(),
|
|
34
|
+
(),
|
|
35
|
+
)
|
|
36
|
+
LEXERS["CitryHtmlLexer"] = (
|
|
37
|
+
"pygments_citry",
|
|
38
|
+
"Citry HTML",
|
|
39
|
+
("citry-html",),
|
|
40
|
+
(),
|
|
41
|
+
(),
|
|
42
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The lexer for the HTML that lives inside a Citry component's ``template``.
|
|
3
|
+
|
|
4
|
+
Citry templates are HTML with a few additions: ``<c-*>`` component and
|
|
5
|
+
control-flow tags, ``c-*`` dynamic attributes whose values are Python
|
|
6
|
+
expressions, ``{{ python }}`` interpolation, ``{# comment #}``, and a verbatim
|
|
7
|
+
``<c-raw>`` element. This lexer extends Pygments' plain ``HtmlLexer`` with those,
|
|
8
|
+
so a Citry template highlights as HTML with the Citry bits called out, and the
|
|
9
|
+
Python inside ``{{ }}`` and ``c-*`` values is handed to the Python lexer.
|
|
10
|
+
|
|
11
|
+
It is registered as ``citry-html``, both on its own (for a fenced block that
|
|
12
|
+
shows only a template) and embedded by ``CitryPythonLexer`` to highlight the
|
|
13
|
+
``template`` string of a component.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
19
|
+
|
|
20
|
+
from pygments.lexer import ExtendedRegexLexer, bygroups, using
|
|
21
|
+
from pygments.lexers.html import HtmlLexer
|
|
22
|
+
from pygments.lexers.javascript import JavascriptLexer
|
|
23
|
+
from pygments.lexers.python import PythonLexer
|
|
24
|
+
from pygments.token import Comment, Name, Operator, Punctuation, Text
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
import re
|
|
28
|
+
from collections.abc import Iterator
|
|
29
|
+
|
|
30
|
+
# An exact Citry tag name ends where the template grammar's tag name ends. The
|
|
31
|
+
# explicit lookahead stops a built-in prefix such as ``c-slot`` from stealing
|
|
32
|
+
# the start of a user component name such as ``c-slot-panel``.
|
|
33
|
+
_TAG_END = r"(?=\s|/?>|\{#)"
|
|
34
|
+
|
|
35
|
+
# One Python lexer, reused for the bodies of {{ ... }} interpolations.
|
|
36
|
+
_PYTHON = PythonLexer()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _interpolation(_lexer: Any, match: re.Match[str], ctx: Any) -> Iterator[tuple[int, Any, str]]:
|
|
40
|
+
"""
|
|
41
|
+
Tokenise a ``{{ ... }}`` interpolation, handing the expression to Python.
|
|
42
|
+
|
|
43
|
+
The closing ``}}`` is found by scanning for one at brace depth zero and
|
|
44
|
+
skipping Python string literals along the way, so a ``}}`` inside a string
|
|
45
|
+
(``{{ "}}" }}``) or a nested dict literal (``{{ {"a": {1: 2}} }}``) does not
|
|
46
|
+
end the block early. This mirrors how the engine's grammar finds the
|
|
47
|
+
boundary; a plain regex cannot, because it cannot count nesting.
|
|
48
|
+
|
|
49
|
+
Surrounding whitespace stays plain text and only the expression in the
|
|
50
|
+
middle is highlighted, matching how the rest of the lexer treats a value.
|
|
51
|
+
"""
|
|
52
|
+
text = match.string
|
|
53
|
+
yield match.start(), Punctuation, "{{"
|
|
54
|
+
start = match.end()
|
|
55
|
+
length = len(text)
|
|
56
|
+
i = start
|
|
57
|
+
depth = 0
|
|
58
|
+
close = None
|
|
59
|
+
while i < length:
|
|
60
|
+
char = text[i]
|
|
61
|
+
if char in "\"'": # skip a Python string literal, honouring backslash escapes
|
|
62
|
+
quote = char
|
|
63
|
+
i += 1
|
|
64
|
+
while i < length:
|
|
65
|
+
if text[i] == "\\":
|
|
66
|
+
i += 2
|
|
67
|
+
continue
|
|
68
|
+
if text[i] == quote:
|
|
69
|
+
i += 1
|
|
70
|
+
break
|
|
71
|
+
i += 1
|
|
72
|
+
continue
|
|
73
|
+
if char == "{":
|
|
74
|
+
depth += 1
|
|
75
|
+
elif char == "}":
|
|
76
|
+
if depth:
|
|
77
|
+
depth -= 1
|
|
78
|
+
elif i + 1 < length and text[i + 1] == "}":
|
|
79
|
+
close = i
|
|
80
|
+
break
|
|
81
|
+
i += 1
|
|
82
|
+
|
|
83
|
+
body = text[start : close if close is not None else length]
|
|
84
|
+
lead = body[: len(body) - len(body.lstrip())]
|
|
85
|
+
expr = body.strip()
|
|
86
|
+
if lead:
|
|
87
|
+
yield start, Text, lead
|
|
88
|
+
for offset, token, value in _PYTHON.get_tokens_unprocessed(expr):
|
|
89
|
+
yield start + len(lead) + offset, token, value
|
|
90
|
+
trail = body[len(lead) + len(expr) :]
|
|
91
|
+
if trail:
|
|
92
|
+
yield start + len(lead) + len(expr), Text, trail
|
|
93
|
+
|
|
94
|
+
if close is not None:
|
|
95
|
+
yield close, Punctuation, "}}"
|
|
96
|
+
ctx.pos = close + 2
|
|
97
|
+
else:
|
|
98
|
+
ctx.pos = length
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# The value of a c-* (or built-in) attribute is a Python expression, quoted or bare.
|
|
102
|
+
_PY_ATTR_STATE = "python-attr"
|
|
103
|
+
_JS_ATTR_STATE = "javascript-attr"
|
|
104
|
+
# Attribute rules shared by ordinary and built-in tags: framework passthrough
|
|
105
|
+
# names plain HtmlLexer cannot tokenise (@click, [style], (click)), ordinary
|
|
106
|
+
# attributes (covers :class and v-model, which HtmlLexer handles), and the close.
|
|
107
|
+
_COMMON_ATTR_RULES = [
|
|
108
|
+
(r"([@\[(][\w:@.()\[\]-]*)(\s*)(=)(\s*)", bygroups(Name.Attribute, Text, Operator, Text), "attr"),
|
|
109
|
+
(r"[@\[(][\w:@.()\[\]-]*", Name.Attribute),
|
|
110
|
+
(r"([\w:-]+)(\s*)(=)(\s*)", bygroups(Name.Attribute, Text, Operator, Text), "attr"),
|
|
111
|
+
(r"[\w:-]+", Name.Attribute),
|
|
112
|
+
(r"(/?)(\s*)(>)", bygroups(Punctuation, Text, Punctuation), "#pop"),
|
|
113
|
+
]
|
|
114
|
+
_WS_RULE = (r"\s+", Text)
|
|
115
|
+
# `$c-props` is evaluated by the client runtime, so its value is JavaScript.
|
|
116
|
+
_CLIENT_PROPS_ATTR_RULE = (
|
|
117
|
+
r"(\$c-props)(\s*)(=)(\s*)",
|
|
118
|
+
bygroups(Name.Attribute, Text, Operator, Text),
|
|
119
|
+
_JS_ATTR_STATE,
|
|
120
|
+
)
|
|
121
|
+
# A c-* dynamic attribute: the value is a Python expression.
|
|
122
|
+
_C_ATTR_RULE = (r"(c-[\w$:@.-]*)(\s*)(=)(\s*)", bygroups(Name.Attribute, Text, Operator, Text), _PY_ATTR_STATE)
|
|
123
|
+
# Structural control-flow tags carry Python-valued attributes that are not
|
|
124
|
+
# c-*-prefixed. These rules live in tag-specific states because the same names
|
|
125
|
+
# on other elements are ordinary static HTML attributes.
|
|
126
|
+
_COND_ATTR_RULE = (r"(cond)(\s*)(=)(\s*)", bygroups(Name.Attribute, Text, Operator, Text), _PY_ATTR_STATE)
|
|
127
|
+
_EACH_ATTR_RULE = (r"(each)(\s*)(=)(\s*)", bygroups(Name.Attribute, Text, Operator, Text), _PY_ATTR_STATE)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CitryHtmlLexer(ExtendedRegexLexer, HtmlLexer):
|
|
131
|
+
"""Highlights the HTML (plus Citry tags and interpolation) of a Citry template."""
|
|
132
|
+
|
|
133
|
+
name = "Citry HTML"
|
|
134
|
+
aliases: ClassVar[list[str]] = ["citry-html"]
|
|
135
|
+
|
|
136
|
+
tokens: ClassVar[dict[str, Any]] = {
|
|
137
|
+
**HtmlLexer.tokens,
|
|
138
|
+
"root": [
|
|
139
|
+
# {# template comment #}
|
|
140
|
+
(r"\{#.*?#\}", Comment),
|
|
141
|
+
# {{ python-expression }}: a callback scans to the balanced }}
|
|
142
|
+
(r"\{\{", _interpolation),
|
|
143
|
+
# a lone { that is not part of {{ or {#
|
|
144
|
+
(r"\{", Text),
|
|
145
|
+
# <c-raw> ... </c-raw>: the body is verbatim (not interpreted)
|
|
146
|
+
(
|
|
147
|
+
r"(<)(\s*)(c-raw)" + _TAG_END,
|
|
148
|
+
bygroups(Punctuation, Text, Name.Tag),
|
|
149
|
+
("raw-content", "tag"),
|
|
150
|
+
),
|
|
151
|
+
# Only the structural tags with unprefixed Python attributes need
|
|
152
|
+
# special states. Every other c-* name falls through to HtmlLexer.
|
|
153
|
+
(
|
|
154
|
+
r"(</?)(\s*)(c-(?:if|elif))" + _TAG_END,
|
|
155
|
+
bygroups(Punctuation, Text, Name.Tag),
|
|
156
|
+
"condition-tag",
|
|
157
|
+
),
|
|
158
|
+
(
|
|
159
|
+
r"(</?)(\s*)(c-for)" + _TAG_END,
|
|
160
|
+
bygroups(Punctuation, Text, Name.Tag),
|
|
161
|
+
"for-tag",
|
|
162
|
+
),
|
|
163
|
+
# ordinary text, stopped at { so the interpolation/comment rules fire;
|
|
164
|
+
# everything after this is HtmlLexer's own root (its [^<&]+ text rule,
|
|
165
|
+
# which does not stop at {, is dropped by the [1:] slice)
|
|
166
|
+
(r"[^<&{]+", Text),
|
|
167
|
+
*HtmlLexer.tokens["root"][1:],
|
|
168
|
+
],
|
|
169
|
+
# An ordinary element's attributes.
|
|
170
|
+
"tag": [_WS_RULE, _CLIENT_PROPS_ATTR_RULE, _C_ATTR_RULE, *_COMMON_ATTR_RULES],
|
|
171
|
+
"condition-tag": [_WS_RULE, _CLIENT_PROPS_ATTR_RULE, _C_ATTR_RULE, _COND_ATTR_RULE, *_COMMON_ATTR_RULES],
|
|
172
|
+
"for-tag": [_WS_RULE, _CLIENT_PROPS_ATTR_RULE, _C_ATTR_RULE, _EACH_ATTR_RULE, *_COMMON_ATTR_RULES],
|
|
173
|
+
# The value of a c-* (or built-in) attribute: a Python expression.
|
|
174
|
+
"python-attr": [
|
|
175
|
+
(r'(")((?:[^"\\]|\\.)*)(")', bygroups(Punctuation, using(PythonLexer), Punctuation), "#pop"),
|
|
176
|
+
(r"(')((?:[^'\\]|\\.)*)(')", bygroups(Punctuation, using(PythonLexer), Punctuation), "#pop"),
|
|
177
|
+
(r"[^\s>]+", using(PythonLexer), "#pop"),
|
|
178
|
+
],
|
|
179
|
+
# The direct client props value is an Alpine/JavaScript expression.
|
|
180
|
+
"javascript-attr": [
|
|
181
|
+
(r'(")((?:[^"\\]|\\.)*)(")', bygroups(Punctuation, using(JavascriptLexer), Punctuation), "#pop"),
|
|
182
|
+
(r"(')((?:[^'\\]|\\.)*)(')", bygroups(Punctuation, using(JavascriptLexer), Punctuation), "#pop"),
|
|
183
|
+
(r"[^\s>]+", using(JavascriptLexer), "#pop"),
|
|
184
|
+
],
|
|
185
|
+
# The verbatim body of <c-raw>...</c-raw>: plain text until the close tag.
|
|
186
|
+
"raw-content": [
|
|
187
|
+
(
|
|
188
|
+
r"(<)(\s*)(/)(\s*)(c-raw)(\s*)(>)",
|
|
189
|
+
bygroups(Punctuation, Text, Punctuation, Text, Name.Tag, Text, Punctuation),
|
|
190
|
+
"#pop",
|
|
191
|
+
),
|
|
192
|
+
(r"[^<]+", Text),
|
|
193
|
+
(r"<", Text),
|
|
194
|
+
],
|
|
195
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The ``citry`` lexer: Python, with the embedded ``template``/``js``/``css`` highlighted.
|
|
3
|
+
|
|
4
|
+
``CitryPythonLexer`` behaves like Pygments' ``PythonLexer`` but recognises the
|
|
5
|
+
three multiline string attributes a Citry component uses and hands each body to
|
|
6
|
+
the right language:
|
|
7
|
+
|
|
8
|
+
- ``template = \"\"\"...\"\"\"`` to the Citry HTML lexer (``<c-*>`` tags, ``{{ }}``, ...),
|
|
9
|
+
- ``js = \"\"\"...\"\"\"`` to the JavaScript lexer,
|
|
10
|
+
- ``css = \"\"\"...\"\"\"`` to the CSS lexer.
|
|
11
|
+
|
|
12
|
+
The detection is deliberately simple, matching the upstream ``pygments-djc``: it
|
|
13
|
+
fires on any ``template``/``js``/``css`` triple-quoted assignment, even one not
|
|
14
|
+
on a component class. That is fine for documentation, where these names almost
|
|
15
|
+
always mean a component.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, ClassVar
|
|
21
|
+
|
|
22
|
+
from pygments.lexer import bygroups, using
|
|
23
|
+
from pygments.lexers.css import CssLexer
|
|
24
|
+
from pygments.lexers.javascript import JavascriptLexer
|
|
25
|
+
from pygments.lexers.python import PythonLexer
|
|
26
|
+
from pygments.token import Name, Operator, Punctuation, String, Text
|
|
27
|
+
|
|
28
|
+
from pygments_citry.citry_html import CitryHtmlLexer
|
|
29
|
+
|
|
30
|
+
# Matches `template = """` or the typed `template: some.Type = """` (and the ''' form).
|
|
31
|
+
# The nine capture groups are highlighted individually by the bygroups action below.
|
|
32
|
+
_CAPTURE = r'({name})(\s*)(?:(:)(\s*)([^\s=]+))?(\s*)(=)(\s*)((?:"""|\'\'\'))'
|
|
33
|
+
|
|
34
|
+
# Highlights the nine groups of _CAPTURE. Reused for all three openers, which
|
|
35
|
+
# differ only in the attribute name and the state they push.
|
|
36
|
+
_OPENER = bygroups(
|
|
37
|
+
Name.Variable, # the attribute name (template / js / css)
|
|
38
|
+
Text,
|
|
39
|
+
Punctuation, # the ':' of an optional type annotation
|
|
40
|
+
Text,
|
|
41
|
+
Name.Class, # the annotation type, if present
|
|
42
|
+
Text,
|
|
43
|
+
Operator, # the '='
|
|
44
|
+
Text,
|
|
45
|
+
String.Doc, # the opening triple quote
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CitryPythonLexer(PythonLexer):
|
|
50
|
+
"""Lexer for Citry component code: Python plus the embedded template/js/css."""
|
|
51
|
+
|
|
52
|
+
name = "Citry Python"
|
|
53
|
+
aliases: ClassVar[list[str]] = ["citry"]
|
|
54
|
+
|
|
55
|
+
tokens: ClassVar[dict[str, Any]] = {
|
|
56
|
+
**PythonLexer.tokens,
|
|
57
|
+
# The three openers are prepended to Python's root so they win over its
|
|
58
|
+
# own string handling; each pushes a state that re-lexes the body.
|
|
59
|
+
"root": [
|
|
60
|
+
(_CAPTURE.format(name="template"), _OPENER, "template_string"),
|
|
61
|
+
(_CAPTURE.format(name="js"), _OPENER, "js_string"),
|
|
62
|
+
(_CAPTURE.format(name="css"), _OPENER, "css_string"),
|
|
63
|
+
*PythonLexer.tokens["root"],
|
|
64
|
+
],
|
|
65
|
+
# Inside a body: the closing triple quote pops (first, so it wins);
|
|
66
|
+
# otherwise the run up to the close is handed to the embedded lexer.
|
|
67
|
+
"template_string": [
|
|
68
|
+
(r'(?:"""|\'\'\')', String.Doc, "#pop"),
|
|
69
|
+
(r'((?!"""|\'\'\')(?:.|\n))+', using(CitryHtmlLexer)),
|
|
70
|
+
],
|
|
71
|
+
"js_string": [
|
|
72
|
+
(r'(?:"""|\'\'\')', String.Doc, "#pop"),
|
|
73
|
+
(r'((?!"""|\'\'\')(?:.|\n))+', using(JavascriptLexer)),
|
|
74
|
+
],
|
|
75
|
+
"css_string": [
|
|
76
|
+
(r'(?:"""|\'\'\')', String.Doc, "#pop"),
|
|
77
|
+
(r'((?!"""|\'\'\')(?:.|\n))+', using(CssLexer)),
|
|
78
|
+
],
|
|
79
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pygments-citry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pygments lexer for Citry components: highlights the HTML, JS, and CSS embedded in a component's template/js/css strings
|
|
5
|
+
Author-email: Juro Oravec <juraj.oravec.josefson@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://citry.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/citry-dev/citry
|
|
9
|
+
Project-URL: Changelog, https://github.com/citry-dev/citry/blob/main/packages/py/pygments_citry/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/citry-dev/citry/issues
|
|
11
|
+
Keywords: citry,pygments,lexer,syntax highlighting,components,html
|
|
12
|
+
Classifier: Environment :: Plugins
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
23
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: <4.0,>=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: pygments>=2.15
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# pygments-citry
|
|
32
|
+
|
|
33
|
+
A [Pygments](https://pygments.org/) lexer for [Citry](https://citry.dev)
|
|
34
|
+
components. It highlights a component the way an editor would: the Python class,
|
|
35
|
+
plus the HTML, JavaScript, and CSS embedded in its `template`, `js`, and `css`
|
|
36
|
+
string attributes, each in its own language.
|
|
37
|
+
|
|
38
|
+
Without it, a Citry component in a ` ```python ` block renders the template,
|
|
39
|
+
JS script, and CSS stylesheet as flat string literals. With it, a ` ```citry `
|
|
40
|
+
block colours the `<c-*>` tags, the `{{ ... }}` interpolation, the embedded
|
|
41
|
+
stylesheet, and the embedded script.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pygments-citry
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package registers a `citry` lexer as a Pygments plugin, so any tool that
|
|
50
|
+
resolves lexers by name picks it up once it is installed.
|
|
51
|
+
|
|
52
|
+
## Use it
|
|
53
|
+
|
|
54
|
+
On the command line:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pygmentize -l citry my_component.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
In Python:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pygments import highlight
|
|
64
|
+
from pygments.formatters import HtmlFormatter
|
|
65
|
+
from pygments.lexers import get_lexer_by_name
|
|
66
|
+
|
|
67
|
+
lexer = get_lexer_by_name("citry")
|
|
68
|
+
html = highlight(source, lexer, HtmlFormatter())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In a Markdown site (mkdocs with `pymdownx.highlight` / `pymdownx.superfences`,
|
|
72
|
+
or any python-markdown setup that resolves Pygments lexers by name), tag the
|
|
73
|
+
fence with `citry`:
|
|
74
|
+
|
|
75
|
+
````markdown
|
|
76
|
+
```citry
|
|
77
|
+
from citry import Component
|
|
78
|
+
|
|
79
|
+
class Welcome(Component):
|
|
80
|
+
template = """
|
|
81
|
+
<div class="card">
|
|
82
|
+
<h1>{{ title }}</h1>
|
|
83
|
+
<p c-if="count">You have {{ count }} new messages.</p>
|
|
84
|
+
</div>
|
|
85
|
+
"""
|
|
86
|
+
```
|
|
87
|
+
````
|
|
88
|
+
|
|
89
|
+
No superfences `custom_fences` entry is needed: once the package is installed,
|
|
90
|
+
`citry` resolves like any other Pygments lexer.
|
|
91
|
+
|
|
92
|
+
## What it highlights
|
|
93
|
+
|
|
94
|
+
A `citry` block is highlighted as Python, and the following Citry constructs
|
|
95
|
+
inside the `template` / `js` / `css` attributes are highlighted in their own
|
|
96
|
+
language:
|
|
97
|
+
|
|
98
|
+
- The HTML in `template`, including the `<c-*>` component and control-flow tags
|
|
99
|
+
(the built-ins `c-if`, `c-for`, `c-slot`, `<c-raw>`, and so on) and `c-*`
|
|
100
|
+
dynamic attributes.
|
|
101
|
+
- `{{ ... }}` interpolation, whose body is a Python expression.
|
|
102
|
+
- `{# ... #}` template comments.
|
|
103
|
+
- The JavaScript in `js` and the CSS in `css`.
|
|
104
|
+
|
|
105
|
+
The `<c-raw>...</c-raw>` element is treated as verbatim text, matching the
|
|
106
|
+
engine: `{{ }}` and tags inside it are not interpreted.
|
|
107
|
+
|
|
108
|
+
## Template-only blocks: `citry-html`
|
|
109
|
+
|
|
110
|
+
For a fenced block that shows only a template, with no surrounding Python (just
|
|
111
|
+
`<c-*>` tags, `{{ ... }}`, and `{# ... #}`), tag it `citry-html`:
|
|
112
|
+
|
|
113
|
+
````markdown
|
|
114
|
+
```citry-html
|
|
115
|
+
<c-for each="item in items">
|
|
116
|
+
<li c-if="item.visible">{{ item.label }}</li>
|
|
117
|
+
</c-for>
|
|
118
|
+
```
|
|
119
|
+
````
|
|
120
|
+
|
|
121
|
+
`citry` expects full component code (Python with embedded strings), so a bare
|
|
122
|
+
template belongs in a `citry-html` block instead.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT. Part of the [Citry](https://github.com/citry-dev/citry) project.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pygments_citry/__init__.py
|
|
5
|
+
pygments_citry/citry_html.py
|
|
6
|
+
pygments_citry/lexers.py
|
|
7
|
+
pygments_citry/py.typed
|
|
8
|
+
pygments_citry.egg-info/PKG-INFO
|
|
9
|
+
pygments_citry.egg-info/SOURCES.txt
|
|
10
|
+
pygments_citry.egg-info/dependency_links.txt
|
|
11
|
+
pygments_citry.egg-info/entry_points.txt
|
|
12
|
+
pygments_citry.egg-info/requires.txt
|
|
13
|
+
pygments_citry.egg-info/top_level.txt
|
|
14
|
+
tests/test_citry_html.py
|
|
15
|
+
tests/test_lexers.py
|
|
16
|
+
tests/test_registration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pygments>=2.15
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pygments_citry
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# The `license-files` field below needs setuptools 77 or newer.
|
|
3
|
+
requires = ["setuptools>=77"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "pygments-citry"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
requires-python = ">=3.10, <4.0"
|
|
10
|
+
description = "Pygments lexer for Citry components: highlights the HTML, JS, and CSS embedded in a component's template/js/css strings"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
keywords = [
|
|
13
|
+
"citry",
|
|
14
|
+
"pygments",
|
|
15
|
+
"lexer",
|
|
16
|
+
"syntax highlighting",
|
|
17
|
+
"components",
|
|
18
|
+
"html",
|
|
19
|
+
]
|
|
20
|
+
authors = [{ name = "Juro Oravec", email = "juraj.oravec.josefson@gmail.com" }]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Environment :: Plugins",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
33
|
+
"Topic :: Software Development :: Documentation",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
license = "MIT"
|
|
37
|
+
license-files = ["LICENSE"]
|
|
38
|
+
# Only Pygments. The floor is where the plugin lexer API we rely on is stable;
|
|
39
|
+
# the docs environment keeps its own upper pin (see the root pyproject `docs`
|
|
40
|
+
# extra), which resolves against this floor.
|
|
41
|
+
dependencies = ["pygments>=2.15"]
|
|
42
|
+
|
|
43
|
+
# Registers the lexers as Pygments plugins: any environment with this package
|
|
44
|
+
# installed resolves ```citry (full component) and ```citry-html (template only)
|
|
45
|
+
# through get_lexer_by_name with no explicit import.
|
|
46
|
+
[project.entry-points."pygments.lexers"]
|
|
47
|
+
citry = "pygments_citry.lexers:CitryPythonLexer"
|
|
48
|
+
citry-html = "pygments_citry.citry_html:CitryHtmlLexer"
|
|
49
|
+
|
|
50
|
+
# The single declaration of this package's test deps. The uv workspace installs
|
|
51
|
+
# them on `uv sync --all-packages`, so the shared venv and CI get them with no
|
|
52
|
+
# separate root-level copy to keep in step.
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = ["pytest>=8.3.5"]
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://citry.dev"
|
|
58
|
+
Repository = "https://github.com/citry-dev/citry"
|
|
59
|
+
Changelog = "https://github.com/citry-dev/citry/blob/main/packages/py/pygments_citry/CHANGELOG.md"
|
|
60
|
+
Issues = "https://github.com/citry-dev/citry/issues"
|
|
61
|
+
|
|
62
|
+
# Ship only the lexer package; the flat-layout auto-discovery would otherwise
|
|
63
|
+
# also try to package the tests directory.
|
|
64
|
+
[tool.setuptools.packages.find]
|
|
65
|
+
include = ["pygments_citry*"]
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.package-data]
|
|
68
|
+
pygments_citry = ["py.typed"]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Token-stream tests for the Citry template constructs inside a ``template`` string."""
|
|
2
|
+
|
|
3
|
+
from pygments.lexers import get_lexer_by_name
|
|
4
|
+
from pygments.token import Comment, Error, Keyword, Name, Operator, Punctuation, String, Text
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def lex_template(body):
|
|
8
|
+
"""Lex a component whose ``template`` is `body`, returning its non-blank tokens."""
|
|
9
|
+
lexer = get_lexer_by_name("citry")
|
|
10
|
+
src = 'class C(Component):\n template = """' + body + '"""\n'
|
|
11
|
+
return [(tok, val) for tok, val in lexer.get_tokens(src) if val.strip()]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def lex_html(body):
|
|
15
|
+
"""Lex `body` directly with the standalone ``citry-html`` lexer (a bare template)."""
|
|
16
|
+
lexer = get_lexer_by_name("citry-html")
|
|
17
|
+
return [(tok, val) for tok, val in lexer.get_tokens(body) if val.strip()]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_interpolation_body_is_python():
|
|
21
|
+
toks = lex_template("<p>{{ count }}</p>")
|
|
22
|
+
assert (Punctuation, "{{") in toks
|
|
23
|
+
assert (Punctuation, "}}") in toks
|
|
24
|
+
assert (Name, "count") in toks
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_interpolation_boundary_skips_string_literals():
|
|
28
|
+
# the "}}" inside the Python string must not end the block early
|
|
29
|
+
toks = lex_template('{{ "}}" }}')
|
|
30
|
+
# exactly one closing }} punctuation, the real end (not the one in the string)
|
|
31
|
+
assert [v for tok, v in toks if (tok, v) == (Punctuation, "}}")] == ["}}"]
|
|
32
|
+
assert (String.Double, "}}") in toks # the "}}" is part of a Python string
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_template_comment():
|
|
36
|
+
toks = lex_template("<i>{# note #}</i>")
|
|
37
|
+
assert (Comment, "{# note #}") in toks
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_lone_brace_is_text_not_interpolation():
|
|
41
|
+
toks = lex_template("a { b")
|
|
42
|
+
assert (Text, "{") in toks
|
|
43
|
+
assert (Punctuation, "{{") not in toks
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_builtin_and_user_component_names_are_html_tag_tokens():
|
|
47
|
+
toks = lex_template("<c-slot></c-slot><c-Card></c-Card>")
|
|
48
|
+
assert (Name.Tag, "c-slot") in toks
|
|
49
|
+
assert (Name.Tag, "c-Card") in toks
|
|
50
|
+
assert (Name.Builtin, "c-slot") not in toks
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_dynamic_attribute_value_is_python():
|
|
54
|
+
toks = lex_template('<li c-for="item in items"></li>')
|
|
55
|
+
assert (Name.Attribute, "c-for") in toks
|
|
56
|
+
assert (Operator.Word, "in") in toks # `in` from the Python expression
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_dynamic_attribute_single_quoted_and_bare():
|
|
60
|
+
toks = lex_template("<b c-if='a and b' c-x=y></b>")
|
|
61
|
+
assert (Operator.Word, "and") in toks
|
|
62
|
+
assert (Name.Attribute, "c-x") in toks
|
|
63
|
+
assert (Name, "y") in toks
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_direct_client_props_value_is_javascript():
|
|
67
|
+
toks = lex_template('<c-child $c-props="{ enabled: true, count: localCount }" />')
|
|
68
|
+
assert (Name.Attribute, "$c-props") in toks
|
|
69
|
+
assert (Keyword.Constant, "true") in toks
|
|
70
|
+
assert (Name.Other, "localCount") in toks
|
|
71
|
+
assert not any(token is Error for token, _ in toks)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_server_dynamic_client_props_value_is_python():
|
|
75
|
+
toks = lex_template('<c-child c-$c-props="primary if enabled else fallback" />')
|
|
76
|
+
assert (Name.Attribute, "c-$c-props") in toks
|
|
77
|
+
assert (Keyword, "if") in toks
|
|
78
|
+
assert (Keyword, "else") in toks
|
|
79
|
+
assert (Name, "fallback") in toks
|
|
80
|
+
assert not any(token is Error for token, _ in toks)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_only_exact_client_props_name_gets_special_highlighting():
|
|
84
|
+
toks = lex_html('<div $other="true" $c-props-extra="true"></div>')
|
|
85
|
+
assert (Error, "$") in toks
|
|
86
|
+
assert (Name.Attribute, "$c-props") not in toks
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_c_raw_body_is_verbatim():
|
|
90
|
+
toks = lex_template("<c-raw>a < b {{ z }}</c-raw>")
|
|
91
|
+
assert (Name.Tag, "c-raw") in toks
|
|
92
|
+
# nothing inside c-raw is interpreted
|
|
93
|
+
assert (Punctuation, "{{") not in toks
|
|
94
|
+
assert (Name, "z") not in toks
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_framework_attributes_are_accepted():
|
|
98
|
+
toks = lex_template('<div @click="f" :class="c" [style]="s" (tap)="t"></div>')
|
|
99
|
+
assert (Name.Attribute, "@click") in toks
|
|
100
|
+
assert (Name.Attribute, "[style]") in toks
|
|
101
|
+
assert (Name.Attribute, "(tap)") in toks
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_boolean_attributes():
|
|
105
|
+
toks = lex_template("<input disabled [hidden]>")
|
|
106
|
+
assert (Name.Attribute, "disabled") in toks
|
|
107
|
+
assert (Name.Attribute, "[hidden]") in toks
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_interpolation_boundary_counts_nested_braces():
|
|
111
|
+
# the dict's closing }} must not end the block; the real end is the last }}
|
|
112
|
+
toks = lex_template('{{ {"a": {1: 2}} }}')
|
|
113
|
+
assert [v for tok, v in toks if (tok, v) == (Punctuation, "}}")] == ["}}"]
|
|
114
|
+
assert (Punctuation, "{") in toks # the dict braces are highlighted as Python
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_control_flow_tag_attributes_are_python():
|
|
118
|
+
toks = lex_template('<c-if cond="a and b"></c-if><c-elif cond="c or d"></c-elif><c-for each="x in xs" />')
|
|
119
|
+
assert (Name.Attribute, "cond") in toks
|
|
120
|
+
assert (Name.Attribute, "each") in toks
|
|
121
|
+
assert (Operator.Word, "and") in toks # cond value lexed as Python
|
|
122
|
+
assert (Operator.Word, "or") in toks # c-elif uses the same condition state
|
|
123
|
+
assert (Operator.Word, "in") in toks # each value lexed as Python
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_dynamic_component_static_and_python_targets_are_distinct():
|
|
127
|
+
toks = lex_template('<c-component is="widget" /><c-element c-is="widget" />')
|
|
128
|
+
|
|
129
|
+
assert (Name.Attribute, "is") in toks
|
|
130
|
+
assert (String, '"widget"') in toks
|
|
131
|
+
assert (Name.Attribute, "c-is") in toks
|
|
132
|
+
assert (Name, "widget") in toks
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_control_flow_attribute_names_on_other_tags_stay_strings():
|
|
136
|
+
toks = lex_template('<c-slot cond="a and b" each="x in xs" is="widget" />')
|
|
137
|
+
|
|
138
|
+
assert toks.count((String, '"a and b"')) == 1
|
|
139
|
+
assert toks.count((String, '"x in xs"')) == 1
|
|
140
|
+
assert toks.count((String, '"widget"')) == 1
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_is_on_an_ordinary_tag_stays_a_string():
|
|
144
|
+
# `is` is a real HTML attribute; on a plain element its value is a string
|
|
145
|
+
toks = lex_template('<button is="my-btn">x</button>')
|
|
146
|
+
assert (Name.Attribute, "is") in toks
|
|
147
|
+
assert (String, '"my-btn"') in toks
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_interpolation_skips_escaped_quote_in_string():
|
|
151
|
+
# a backslash-escaped quote must not end the string mid-scan
|
|
152
|
+
toks = lex_template(r'{{ "a\"}}b" + c }}')
|
|
153
|
+
assert [v for tok, v in toks if (tok, v) == (Punctuation, "}}")] == ["}}"]
|
|
154
|
+
assert (Name, "c") in toks
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_interpolation_without_surrounding_whitespace():
|
|
158
|
+
toks = lex_template("{{x}}")
|
|
159
|
+
assert (Punctuation, "{{") in toks
|
|
160
|
+
assert (Punctuation, "}}") in toks
|
|
161
|
+
assert (Name, "x") in toks
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_unclosed_interpolation_degrades_to_body():
|
|
165
|
+
# no closing }}: the rest is lexed as the expression, and nothing crashes
|
|
166
|
+
toks = lex_html("{{ oops")
|
|
167
|
+
assert (Punctuation, "{{") in toks
|
|
168
|
+
assert (Name, "oops") in toks
|
|
169
|
+
assert (Punctuation, "}}") not in toks
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_malformed_interpolations_do_not_crash():
|
|
173
|
+
# an unterminated string and a lone trailing } are degenerate but must not
|
|
174
|
+
# raise; the scanner just runs to the end of the text
|
|
175
|
+
for body in ('{{ "unterminated', "{{ a}"):
|
|
176
|
+
toks = lex_html(body)
|
|
177
|
+
assert (Punctuation, "{{") in toks
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_standalone_citry_html_lexer_on_a_bare_template():
|
|
181
|
+
toks = lex_html('<c-if cond="ok">{# note #}<c-slot />{{ user.name }}</c-if>')
|
|
182
|
+
assert (Name.Tag, "c-if") in toks
|
|
183
|
+
assert (Name.Tag, "c-slot") in toks
|
|
184
|
+
assert (Comment, "{# note #}") in toks
|
|
185
|
+
assert (Name.Attribute, "cond") in toks
|
|
186
|
+
assert (Name, "ok") in toks # cond value is Python
|
|
187
|
+
assert (Name, "user") in toks # {{ }} body is Python
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_every_citry_tag_name_uses_the_html_tag_token():
|
|
191
|
+
tags = (
|
|
192
|
+
"c-if",
|
|
193
|
+
"c-elif",
|
|
194
|
+
"c-else",
|
|
195
|
+
"c-for",
|
|
196
|
+
"c-empty",
|
|
197
|
+
"c-slot",
|
|
198
|
+
"c-fill",
|
|
199
|
+
"c-component",
|
|
200
|
+
"c-element",
|
|
201
|
+
"c-provide",
|
|
202
|
+
"c-css",
|
|
203
|
+
"c-js",
|
|
204
|
+
"c-raw",
|
|
205
|
+
"c-cache",
|
|
206
|
+
"c-error-fallback",
|
|
207
|
+
"c-template",
|
|
208
|
+
"c-Card",
|
|
209
|
+
)
|
|
210
|
+
toks = lex_html("".join(f"<{tag}></{tag}>" for tag in tags))
|
|
211
|
+
|
|
212
|
+
for tag in tags:
|
|
213
|
+
assert toks.count((Name.Tag, tag)) == 2
|
|
214
|
+
assert (Name.Builtin, tag) not in toks
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_builtin_prefixes_do_not_steal_user_component_names():
|
|
218
|
+
tags = (
|
|
219
|
+
"c-if-panel",
|
|
220
|
+
"c-elif-panel",
|
|
221
|
+
"c-for-panel",
|
|
222
|
+
"c-slot-panel",
|
|
223
|
+
"c-component-card",
|
|
224
|
+
"c-raw-data",
|
|
225
|
+
)
|
|
226
|
+
toks = lex_html("".join(f"<{tag}>{{{{ value }}}}</{tag}>" for tag in tags))
|
|
227
|
+
|
|
228
|
+
for tag in tags:
|
|
229
|
+
assert toks.count((Name.Tag, tag)) == 2
|
|
230
|
+
assert toks.count((Punctuation, "{{")) == len(tags)
|
|
231
|
+
assert toks.count((Punctuation, "}}")) == len(tags)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Token-stream tests for the top-level ``citry`` lexer (Python plus the embeds)."""
|
|
2
|
+
|
|
3
|
+
from pygments.lexers import get_lexer_by_name
|
|
4
|
+
from pygments.token import Keyword, Name, String
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def lex(src):
|
|
8
|
+
"""Lex `src` with the citry lexer, dropping whitespace-only tokens."""
|
|
9
|
+
lexer = get_lexer_by_name("citry")
|
|
10
|
+
return [(tok, val) for tok, val in lexer.get_tokens(src) if val.strip()]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_alias_and_name():
|
|
14
|
+
lexer = get_lexer_by_name("citry")
|
|
15
|
+
assert lexer.name == "Citry Python"
|
|
16
|
+
assert "citry" in lexer.aliases
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_template_opener_and_html_embed():
|
|
20
|
+
toks = lex('class C(Component):\n template = """<div>{{ x }}</div>"""\n')
|
|
21
|
+
assert (Name.Variable, "template") in toks
|
|
22
|
+
assert (String.Doc, '"""') in toks
|
|
23
|
+
assert (Name.Tag, "div") in toks
|
|
24
|
+
# the {{ }} body is handed to the Python lexer
|
|
25
|
+
assert (Name, "x") in toks
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_js_opener_embeds_javascript():
|
|
29
|
+
toks = lex('class C(Component):\n js = """const x = `a`;"""\n')
|
|
30
|
+
assert (Name.Variable, "js") in toks
|
|
31
|
+
# `const` and a backtick template literal are JavaScript, not Python
|
|
32
|
+
assert (Keyword.Declaration, "const") in toks
|
|
33
|
+
assert (String.Backtick, "`") in toks
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_css_opener_embeds_css():
|
|
37
|
+
toks = lex('class C(Component):\n css = """a { color: red; }"""\n')
|
|
38
|
+
assert (Name.Variable, "css") in toks
|
|
39
|
+
assert (Keyword, "color") in toks
|
|
40
|
+
assert (Keyword.Constant, "red") in toks
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_typed_opener_keeps_annotation_and_embeds_html():
|
|
44
|
+
toks = lex("class C(Component):\n template: types.html = '''<br/>'''\n")
|
|
45
|
+
assert (Name.Variable, "template") in toks
|
|
46
|
+
assert (Name.Class, "types.html") in toks
|
|
47
|
+
assert (Name.Tag, "br") in toks
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_triple_single_quote_body():
|
|
51
|
+
toks = lex("class C(Component):\n css = '''a{}'''\n")
|
|
52
|
+
assert (String.Doc, "'''") in toks
|
|
53
|
+
assert (Name.Tag, "a") in toks
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_full_component_highlights_all_three_and_stays_python():
|
|
57
|
+
src = (
|
|
58
|
+
"class Card(Component):\n"
|
|
59
|
+
' template = """<c-slot></c-slot>{{ title }}"""\n'
|
|
60
|
+
' js = """const n = 1;"""\n'
|
|
61
|
+
' css = """.card { color: red; }"""\n'
|
|
62
|
+
)
|
|
63
|
+
toks = lex(src)
|
|
64
|
+
assert (Name.Variable, "template") in toks
|
|
65
|
+
assert (Name.Variable, "js") in toks
|
|
66
|
+
assert (Name.Variable, "css") in toks
|
|
67
|
+
# the class itself is still ordinary Python
|
|
68
|
+
assert (Name.Class, "Card") in toks
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The ``citry`` lexer registers with Pygments as a plugin and on import."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
|
+
|
|
5
|
+
from pygments.lexers import LEXERS, get_lexer_by_name
|
|
6
|
+
|
|
7
|
+
import pygments_citry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_resolves_by_alias():
|
|
11
|
+
lexer = get_lexer_by_name("citry")
|
|
12
|
+
assert type(lexer).__name__ == "CitryPythonLexer"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_plugin_entry_point_declared():
|
|
16
|
+
values = {ep.value for ep in entry_points(group="pygments.lexers") if ep.name == "citry"}
|
|
17
|
+
assert "pygments_citry.lexers:CitryPythonLexer" in values
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_import_registers_in_builtin_table():
|
|
21
|
+
assert "CitryPythonLexer" in LEXERS
|
|
22
|
+
assert "CitryHtmlLexer" in LEXERS
|
|
23
|
+
assert pygments_citry.CitryPythonLexer.name == "Citry Python"
|
|
24
|
+
assert pygments_citry.CitryHtmlLexer.name == "Citry HTML"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_citry_html_resolves_by_alias():
|
|
28
|
+
lexer = get_lexer_by_name("citry-html")
|
|
29
|
+
assert type(lexer).__name__ == "CitryHtmlLexer"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_citry_html_entry_point_declared():
|
|
33
|
+
values = {ep.value for ep in entry_points(group="pygments.lexers") if ep.name == "citry-html"}
|
|
34
|
+
assert "pygments_citry.citry_html:CitryHtmlLexer" in values
|