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.
@@ -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,3 @@
1
+ [pygments.lexers]
2
+ citry = pygments_citry.lexers:CitryPythonLexer
3
+ citry-html = pygments_citry.citry_html:CitryHtmlLexer
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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