sphinx-gp-highlighting 0.0.1a33__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,233 @@
1
+ # Node
2
+ node_modules/
3
+ *.tsbuildinfo
4
+ .vitest-cache/
5
+
6
+ # Byte-compiled / optimized / DLL files
7
+ __pycache__/
8
+ *.py[codz]
9
+ *$py.class
10
+
11
+ # C extensions
12
+ *.so
13
+
14
+ # Distribution / packaging
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ share/python-wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+ MANIFEST
33
+
34
+ # PyInstaller
35
+ # Usually these files are written by a python script from a template
36
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
37
+ *.manifest
38
+ *.spec
39
+
40
+ # Installer logs
41
+ pip-log.txt
42
+ pip-delete-this-directory.txt
43
+
44
+ # Unit test / coverage reports
45
+ htmlcov/
46
+ .tox/
47
+ .nox/
48
+ .coverage
49
+ .coverage.*
50
+ .cache
51
+ nosetests.xml
52
+ coverage.xml
53
+ *.cover
54
+ *.py.cover
55
+ .hypothesis/
56
+ .pytest_cache/
57
+ cover/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+
69
+ # Flask stuff:
70
+ instance/
71
+ .webassets-cache
72
+
73
+ # Scrapy stuff:
74
+ .scrapy
75
+
76
+ # Sphinx documentation
77
+ docs/_build/
78
+
79
+ # PyBuilder
80
+ .pybuilder/
81
+ target/
82
+
83
+ # Jupyter Notebook
84
+ .ipynb_checkpoints
85
+
86
+ # IPython
87
+ profile_default/
88
+ ipython_config.py
89
+
90
+ # pyenv
91
+ # For a library or package, you might want to ignore these files since the code is
92
+ # intended to run in multiple environments; otherwise, check them in:
93
+ # .python-version
94
+
95
+ # pipenv
96
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
98
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
99
+ # install all needed dependencies.
100
+ #Pipfile.lock
101
+
102
+ # UV
103
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
104
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
105
+ # commonly ignored for libraries.
106
+ #uv.lock
107
+
108
+ # poetry
109
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
110
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
111
+ # commonly ignored for libraries.
112
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
113
+ #poetry.lock
114
+ #poetry.toml
115
+
116
+ # pdm
117
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
118
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
119
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
120
+ #pdm.lock
121
+ #pdm.toml
122
+ .pdm-python
123
+ .pdm-build/
124
+
125
+ # pixi
126
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
127
+ #pixi.lock
128
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
129
+ # in the .venv directory. It is recommended not to include this directory in version control.
130
+ .pixi
131
+
132
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
133
+ __pypackages__/
134
+
135
+ # Celery stuff
136
+ celerybeat-schedule
137
+ celerybeat.pid
138
+
139
+ # SageMath parsed files
140
+ *.sage.py
141
+
142
+ # Environments
143
+ .env
144
+ .envrc
145
+ .venv
146
+ env/
147
+ venv/
148
+ ENV/
149
+ env.bak/
150
+ venv.bak/
151
+
152
+ # Spyder project settings
153
+ .spyderproject
154
+ .spyproject
155
+
156
+ # Rope project settings
157
+ .ropeproject
158
+
159
+ # mkdocs documentation
160
+ /site
161
+
162
+ # mypy
163
+ .mypy_cache/
164
+ .dmypy.json
165
+ dmypy.json
166
+
167
+ # Pyre type checker
168
+ .pyre/
169
+
170
+ # pytype static type analyzer
171
+ .pytype/
172
+
173
+ # Cython debug symbols
174
+ cython_debug/
175
+
176
+ # PyCharm
177
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
178
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
179
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
180
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
181
+ #.idea/
182
+
183
+ # Abstra
184
+ # Abstra is an AI-powered process automation framework.
185
+ # Ignore directories containing user credentials, local state, and settings.
186
+ # Learn more at https://abstra.io/docs
187
+ .abstra/
188
+
189
+ # Visual Studio Code
190
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
191
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
192
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
193
+ # you could uncomment the following to ignore the entire vscode folder
194
+ # .vscode/
195
+
196
+ # Ruff stuff:
197
+ .ruff_cache/
198
+
199
+ # PyPI configuration file
200
+ .pypirc
201
+
202
+ # Cursor
203
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
204
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
205
+ # refer to https://docs.cursor.com/context/ignore-files
206
+ .cursorignore
207
+ .cursorindexingignore
208
+
209
+ # Marimo
210
+ marimo/_static/
211
+ marimo/_lsp/
212
+ __marimo__/
213
+
214
+ # Generated by sphinx_fonts extension (downloaded at build time)
215
+ docs/_static/fonts/
216
+ docs/_static/css/fonts.css
217
+
218
+ # Claude Code
219
+ **/CLAUDE.local.md
220
+ **/CLAUDE.*.md
221
+ **/.claude/settings.local.json
222
+
223
+ # Playwright MCP
224
+ .playwright-mcp/
225
+
226
+ # Repo-local pytest mirror (do not track — validator-only)
227
+ out/
228
+
229
+ # Misc
230
+ .vim/
231
+ *.lprof
232
+ pip-wheel-metadata/
233
+ monkeytype.sqlite3
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: sphinx-gp-highlighting
3
+ Version: 0.0.1a33
4
+ Summary: Reusable Pygments lexers and inline highlighting for Sphinx
5
+ Project-URL: Repository, https://github.com/git-pull/gp-sphinx
6
+ Author-email: Tony Narlock <tony@git-pull.com>
7
+ License: MIT
8
+ Keywords: documentation,highlighting,pygments,sphinx
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: Sphinx
11
+ Classifier: Framework :: Sphinx :: Extension
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Documentation
21
+ Classifier: Topic :: Documentation :: Sphinx
22
+ Classifier: Topic :: Software Development :: Documentation
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: <4.0,>=3.10
25
+ Requires-Dist: docutils
26
+ Requires-Dist: pygments
27
+ Requires-Dist: sphinx>=8.1
28
+ Description-Content-Type: text/markdown
29
+
30
+ # sphinx-gp-highlighting
31
+
32
+ Reusable Sphinx highlighting helpers for git-pull documentation. The package
33
+ ships Pygments lexers for documentation-oriented blocks and Sphinx inline
34
+ literal helpers for commands, paths, and directories.
35
+
36
+ ## Install
37
+
38
+ ```console
39
+ $ pip install sphinx-gp-highlighting
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Enable the extension in `conf.py`:
45
+
46
+ ```python
47
+ extensions = ["sphinx_gp_highlighting"]
48
+ ```
49
+
50
+ Use the `tree` lexer for directory layouts:
51
+
52
+ ````markdown
53
+ ```tree
54
+ python_module
55
+ ├── package
56
+ │ └── __init__.py
57
+ └── pyproject.toml
58
+ ```
59
+ ````
@@ -0,0 +1,30 @@
1
+ # sphinx-gp-highlighting
2
+
3
+ Reusable Sphinx highlighting helpers for git-pull documentation. The package
4
+ ships Pygments lexers for documentation-oriented blocks and Sphinx inline
5
+ literal helpers for commands, paths, and directories.
6
+
7
+ ## Install
8
+
9
+ ```console
10
+ $ pip install sphinx-gp-highlighting
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Enable the extension in `conf.py`:
16
+
17
+ ```python
18
+ extensions = ["sphinx_gp_highlighting"]
19
+ ```
20
+
21
+ Use the `tree` lexer for directory layouts:
22
+
23
+ ````markdown
24
+ ```tree
25
+ python_module
26
+ ├── package
27
+ │ └── __init__.py
28
+ └── pyproject.toml
29
+ ```
30
+ ````
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "sphinx-gp-highlighting"
3
+ version = "0.0.1a33"
4
+ description = "Reusable Pygments lexers and inline highlighting for Sphinx"
5
+ requires-python = ">=3.10,<4.0"
6
+ authors = [
7
+ {name = "Tony Narlock", email = "tony@git-pull.com"}
8
+ ]
9
+ license = { text = "MIT" }
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Framework :: Sphinx",
14
+ "Framework :: Sphinx :: Extension",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Topic :: Documentation",
23
+ "Topic :: Documentation :: Sphinx",
24
+ "Topic :: Software Development :: Documentation",
25
+ "Typing :: Typed",
26
+ ]
27
+ readme = "README.md"
28
+ keywords = ["sphinx", "pygments", "highlighting", "documentation"]
29
+ dependencies = [
30
+ "docutils",
31
+ "pygments",
32
+ "sphinx>=8.1",
33
+ ]
34
+
35
+ [project.entry-points."pygments.lexers"]
36
+ "directory-tree" = "sphinx_gp_highlighting.lexers:DirectoryTreeLexer"
37
+
38
+ [project.urls]
39
+ Repository = "https://github.com/git-pull/gp-sphinx"
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/sphinx_gp_highlighting"]
47
+
48
+ [tool.gp-sphinx.docs]
49
+ showcase = ["examples"]
@@ -0,0 +1,100 @@
1
+ """Reusable Sphinx highlighting helpers.
2
+
3
+ Registers documentation-oriented Pygments lexers and inline literal
4
+ helpers for Sphinx projects.
5
+
6
+ Examples
7
+ --------
8
+ >>> from sphinx_gp_highlighting import setup
9
+ >>> callable(setup)
10
+ True
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import pathlib
17
+ import typing as t
18
+
19
+ from sphinx.util.logging import getLogger
20
+
21
+ from sphinx_gp_highlighting.inline import (
22
+ HighlightingInlineTransform,
23
+ cmd_role,
24
+ dir_role,
25
+ path_role,
26
+ )
27
+ from sphinx_gp_highlighting.lexers import DirectoryTreeLexer
28
+
29
+ if t.TYPE_CHECKING:
30
+ from sphinx.application import Sphinx
31
+ from sphinx.util.typing import ExtensionMetadata
32
+
33
+ _EXTENSION_VERSION = "0.0.1a33"
34
+ _TREE_ALIASES: tuple[str, ...] = ("tree", "directory-tree", "dir-tree")
35
+
36
+ logger = getLogger(__name__)
37
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
38
+
39
+ __all__ = ["DirectoryTreeLexer", "HighlightingInlineTransform", "setup"]
40
+
41
+
42
+ def setup(app: Sphinx) -> ExtensionMetadata:
43
+ """Register the package lexers with Sphinx.
44
+
45
+ Parameters
46
+ ----------
47
+ app : Sphinx
48
+ Sphinx application instance.
49
+
50
+ Returns
51
+ -------
52
+ ExtensionMetadata
53
+ Extension metadata with version and parallel-build flags.
54
+
55
+ Examples
56
+ --------
57
+ >>> from sphinx_gp_highlighting import setup
58
+ >>> callable(setup)
59
+ True
60
+ """
61
+ for alias in _TREE_ALIASES:
62
+ app.add_lexer(alias, DirectoryTreeLexer)
63
+ app.add_config_value(
64
+ "gp_highlighting_inline_literals",
65
+ default="off",
66
+ rebuild="env",
67
+ types=frozenset({str}),
68
+ description=(
69
+ "Inline literal auto-highlighting mode. Use ``'safe'`` to "
70
+ "highlight configured commands and clear path/directory literals."
71
+ ),
72
+ )
73
+ app.add_config_value(
74
+ "gp_highlighting_inline_commands",
75
+ default=[],
76
+ rebuild="env",
77
+ types=frozenset({list, tuple}),
78
+ description=(
79
+ "Command names eligible for safe inline literal highlighting "
80
+ "when ``gp_highlighting_inline_literals`` is ``'safe'``."
81
+ ),
82
+ )
83
+ app.add_role("cmd", cmd_role)
84
+ app.add_role("path", path_role)
85
+ app.add_role("dir", dir_role)
86
+ app.add_transform(HighlightingInlineTransform)
87
+
88
+ static_dir = str(pathlib.Path(__file__).parent / "_static")
89
+
90
+ def _add_static_path(app: Sphinx) -> None:
91
+ if static_dir not in app.config.html_static_path:
92
+ app.config.html_static_path.append(static_dir)
93
+
94
+ app.connect("builder-inited", _add_static_path)
95
+ app.add_css_file("css/sphinx_gp_highlighting.css")
96
+ return {
97
+ "version": _EXTENSION_VERSION,
98
+ "parallel_read_safe": True,
99
+ "parallel_write_safe": True,
100
+ }
@@ -0,0 +1,17 @@
1
+ .gp-sphinx-highlighting-inline {
2
+ white-space: nowrap;
3
+ }
4
+
5
+ .gp-sphinx-highlighting-inline--kind-command,
6
+ .gp-sphinx-highlighting-inline--kind-shell-session {
7
+ color: var(--color-code-foreground, inherit);
8
+ }
9
+
10
+ .gp-sphinx-highlighting-inline--kind-path,
11
+ .gp-sphinx-highlighting-inline--kind-dir {
12
+ color: var(--color-api-name, inherit);
13
+ }
14
+
15
+ .gp-sphinx-highlighting-inline--kind-dir::after {
16
+ color: var(--color-foreground-muted, currentColor);
17
+ }
@@ -0,0 +1,350 @@
1
+ """Inline literal roles and transforms for Sphinx highlighting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import typing as t
7
+
8
+ from docutils import nodes
9
+ from docutils.transforms import Transform
10
+
11
+ if t.TYPE_CHECKING:
12
+ from docutils.parsers.rst.states import Inliner
13
+
14
+ InlineLiteralKind: t.TypeAlias = t.Literal["command", "shell-session", "path", "dir"]
15
+
16
+ _BASE_CLASS = "gp-sphinx-highlighting-inline"
17
+ _COMMAND_LANGUAGE = "bash"
18
+ _SHELL_SESSION_LANGUAGE = "console"
19
+ _SHELL_SESSION_RE = re.compile(r"^\$ \S")
20
+ _HOME_OR_DOT_PATH_RE = re.compile(r"^(?:~|\.{1,2}/)[^\s`]*$")
21
+ _WINDOWS_PATH_RE = re.compile(r"^[A-Za-z]:[\\/][^\s`]+$")
22
+ _RELATIVE_DIR_RE = re.compile(r"^(?:[\w.-]+/)+$")
23
+ _RELATIVE_FILE_RE = re.compile(r"^(?:[\w.-]+/)+[\w.-]+\.[A-Za-z0-9][\w.-]*$")
24
+ _ABSOLUTE_PATH_ROOTS = frozenset(
25
+ {
26
+ "Applications",
27
+ "Library",
28
+ "Users",
29
+ "bin",
30
+ "dev",
31
+ "etc",
32
+ "home",
33
+ "lib",
34
+ "lib64",
35
+ "mnt",
36
+ "opt",
37
+ "proc",
38
+ "root",
39
+ "run",
40
+ "sbin",
41
+ "srv",
42
+ "sys",
43
+ "tmp",
44
+ "usr",
45
+ "var",
46
+ "Volumes",
47
+ }
48
+ )
49
+
50
+
51
+ def classify_inline_literal(
52
+ text: str,
53
+ *,
54
+ commands: t.Iterable[str] = (),
55
+ ) -> InlineLiteralKind | None:
56
+ """Classify a literal that is safe to highlight automatically.
57
+
58
+ Parameters
59
+ ----------
60
+ text : str
61
+ Literal text.
62
+ commands : iterable of str
63
+ Command names eligible for bare command highlighting.
64
+
65
+ Returns
66
+ -------
67
+ InlineLiteralKind | None
68
+ The safe literal kind, or ``None`` when the text should remain
69
+ an ordinary inline literal.
70
+
71
+ Examples
72
+ --------
73
+ >>> classify_inline_literal("tmuxp freeze my-session", commands=["tmuxp"])
74
+ 'command'
75
+ >>> classify_inline_literal("~/.config/tmuxp/")
76
+ 'dir'
77
+ >>> classify_inline_literal("module_name") is None
78
+ True
79
+ """
80
+ candidate = text.strip()
81
+ if not candidate or "\n" in candidate:
82
+ return None
83
+ if _SHELL_SESSION_RE.match(candidate):
84
+ return "shell-session"
85
+ first_word = candidate.split(maxsplit=1)[0]
86
+ if first_word in set(commands) and " " in candidate:
87
+ return "command"
88
+ if _is_path_like(candidate):
89
+ return "dir" if candidate.endswith("/") else "path"
90
+ return None
91
+
92
+
93
+ def build_highlighted_literal(text: str, kind: InlineLiteralKind) -> nodes.literal:
94
+ """Build a literal node with package highlighting classes.
95
+
96
+ Parameters
97
+ ----------
98
+ text : str
99
+ Literal text.
100
+ kind : InlineLiteralKind
101
+ Highlighting kind.
102
+
103
+ Returns
104
+ -------
105
+ nodes.literal
106
+ Literal node ready for the Sphinx HTML writer.
107
+
108
+ Examples
109
+ --------
110
+ >>> node = build_highlighted_literal("tmuxp freeze my-session", "command")
111
+ >>> node["language"]
112
+ 'bash'
113
+ >>> "gp-sphinx-highlighting-inline--kind-command" in node["classes"]
114
+ True
115
+ >>> "language" in build_highlighted_literal("~/.config/tmuxp/", "dir")
116
+ False
117
+ """
118
+ node = nodes.literal(
119
+ text,
120
+ text,
121
+ classes=[_BASE_CLASS, f"{_BASE_CLASS}--kind-{kind}"],
122
+ )
123
+ if kind in {"command", "shell-session"}:
124
+ node["classes"].extend(["code", "highlight"])
125
+ node["language"] = (
126
+ _SHELL_SESSION_LANGUAGE if kind == "shell-session" else _COMMAND_LANGUAGE
127
+ )
128
+ return node
129
+
130
+
131
+ def cmd_role(
132
+ name: str,
133
+ rawtext: str,
134
+ text: str,
135
+ lineno: int,
136
+ inliner: Inliner | None,
137
+ options: dict[str, t.Any] | None = None,
138
+ content: list[str] | None = None,
139
+ ) -> tuple[list[nodes.Node], list[nodes.system_message]]:
140
+ """Role for inline shell commands.
141
+
142
+ Parameters
143
+ ----------
144
+ name : str
145
+ Role name.
146
+ rawtext : str
147
+ Full role markup.
148
+ text : str
149
+ Role content.
150
+ lineno : int
151
+ Source line number.
152
+ inliner : Inliner | None
153
+ Docutils inliner.
154
+ options : dict | None
155
+ Role options.
156
+ content : list | None
157
+ Role content lines.
158
+
159
+ Returns
160
+ -------
161
+ tuple[list[nodes.Node], list[nodes.system_message]]
162
+ Role output nodes and messages.
163
+
164
+ Examples
165
+ --------
166
+ >>> nodes_, messages = cmd_role(
167
+ ... "cmd", "{cmd}`tmuxp freeze x`", "tmuxp freeze x", 1, None
168
+ ... )
169
+ >>> messages
170
+ []
171
+ >>> nodes_[0]["language"]
172
+ 'bash'
173
+ """
174
+ return [build_highlighted_literal(text, "command")], []
175
+
176
+
177
+ def path_role(
178
+ name: str,
179
+ rawtext: str,
180
+ text: str,
181
+ lineno: int,
182
+ inliner: Inliner | None,
183
+ options: dict[str, t.Any] | None = None,
184
+ content: list[str] | None = None,
185
+ ) -> tuple[list[nodes.Node], list[nodes.system_message]]:
186
+ """Role for inline filesystem paths.
187
+
188
+ Parameters
189
+ ----------
190
+ name : str
191
+ Role name.
192
+ rawtext : str
193
+ Full role markup.
194
+ text : str
195
+ Role content.
196
+ lineno : int
197
+ Source line number.
198
+ inliner : Inliner | None
199
+ Docutils inliner.
200
+ options : dict | None
201
+ Role options.
202
+ content : list | None
203
+ Role content lines.
204
+
205
+ Returns
206
+ -------
207
+ tuple[list[nodes.Node], list[nodes.system_message]]
208
+ Role output nodes and messages.
209
+
210
+ Examples
211
+ --------
212
+ >>> nodes_, messages = path_role("path", "{path}`~/x.py`", "~/x.py", 1, None)
213
+ >>> messages
214
+ []
215
+ >>> "gp-sphinx-highlighting-inline--kind-path" in nodes_[0]["classes"]
216
+ True
217
+ """
218
+ return [build_highlighted_literal(text, "path")], []
219
+
220
+
221
+ def dir_role(
222
+ name: str,
223
+ rawtext: str,
224
+ text: str,
225
+ lineno: int,
226
+ inliner: Inliner | None,
227
+ options: dict[str, t.Any] | None = None,
228
+ content: list[str] | None = None,
229
+ ) -> tuple[list[nodes.Node], list[nodes.system_message]]:
230
+ """Role for inline filesystem directories.
231
+
232
+ Parameters
233
+ ----------
234
+ name : str
235
+ Role name.
236
+ rawtext : str
237
+ Full role markup.
238
+ text : str
239
+ Role content.
240
+ lineno : int
241
+ Source line number.
242
+ inliner : Inliner | None
243
+ Docutils inliner.
244
+ options : dict | None
245
+ Role options.
246
+ content : list | None
247
+ Role content lines.
248
+
249
+ Returns
250
+ -------
251
+ tuple[list[nodes.Node], list[nodes.system_message]]
252
+ Role output nodes and messages.
253
+
254
+ Examples
255
+ --------
256
+ >>> nodes_, messages = dir_role("dir", "{dir}`~/x/`", "~/x/", 1, None)
257
+ >>> messages
258
+ []
259
+ >>> "gp-sphinx-highlighting-inline--kind-dir" in nodes_[0]["classes"]
260
+ True
261
+ """
262
+ return [build_highlighted_literal(text, "dir")], []
263
+
264
+
265
+ class HighlightingInlineTransform(Transform):
266
+ """Highlight safe inline literals when configured."""
267
+
268
+ default_priority = 120
269
+
270
+ def apply(self) -> None:
271
+ """Rewrite safe inline literals to highlighted literal nodes."""
272
+ env = getattr(self.document.settings, "env", None)
273
+ if env is None:
274
+ return
275
+ config = env.app.config
276
+ if getattr(config, "gp_highlighting_inline_literals", "off") != "safe":
277
+ return
278
+ commands = tuple(getattr(config, "gp_highlighting_inline_commands", ()))
279
+ for literal in list(self.document.findall(nodes.literal)):
280
+ if _should_skip_literal(literal):
281
+ continue
282
+ kind = classify_inline_literal(literal.astext(), commands=commands)
283
+ if kind is None:
284
+ continue
285
+ replacement = build_highlighted_literal(literal.astext(), kind)
286
+ replacement.source = literal.source
287
+ replacement.line = literal.line
288
+ literal.replace_self(replacement)
289
+
290
+
291
+ def _is_path_like(text: str) -> bool:
292
+ """Return whether ``text`` looks like a filesystem path.
293
+
294
+ Examples
295
+ --------
296
+ >>> _is_path_like("~/.config/tmuxp/")
297
+ True
298
+ >>> _is_path_like("/mnt/c/Users/example")
299
+ True
300
+ >>> _is_path_like("application/json")
301
+ False
302
+ """
303
+ return bool(
304
+ _HOME_OR_DOT_PATH_RE.match(text)
305
+ or _WINDOWS_PATH_RE.match(text)
306
+ or _is_absolute_filesystem_path(text)
307
+ or _is_relative_filesystem_path(text)
308
+ )
309
+
310
+
311
+ def _is_absolute_filesystem_path(text: str) -> bool:
312
+ """Return whether ``text`` starts with a common absolute path root.
313
+
314
+ Examples
315
+ --------
316
+ >>> _is_absolute_filesystem_path("/proc/version")
317
+ True
318
+ >>> _is_absolute_filesystem_path("/api/pull")
319
+ False
320
+ """
321
+ if not text.startswith("/"):
322
+ return False
323
+ first_component = text.removeprefix("/").split("/", maxsplit=1)[0]
324
+ return first_component in _ABSOLUTE_PATH_ROOTS
325
+
326
+
327
+ def _is_relative_filesystem_path(text: str) -> bool:
328
+ """Return whether ``text`` is a conservative bare relative path.
329
+
330
+ Examples
331
+ --------
332
+ >>> _is_relative_filesystem_path("docs/conf.py")
333
+ True
334
+ >>> _is_relative_filesystem_path("resources/read")
335
+ False
336
+ """
337
+ if text.startswith(("/", "~", "./", "../")):
338
+ return False
339
+ first_component = text.split("/", maxsplit=1)[0]
340
+ if "." in first_component and not first_component.startswith("."):
341
+ return False
342
+ return bool(_RELATIVE_DIR_RE.match(text) or _RELATIVE_FILE_RE.match(text))
343
+
344
+
345
+ def _should_skip_literal(node: nodes.literal) -> bool:
346
+ """Return whether a literal should not be rewritten."""
347
+ classes: set[str] = set(node.get("classes", ()))
348
+ if _BASE_CLASS in classes:
349
+ return True
350
+ return bool("language" in node or "code" in classes)
@@ -0,0 +1,82 @@
1
+ """Pygments lexers for documentation-focused code blocks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import typing as t
7
+
8
+ from pygments.lexer import RegexLexer
9
+ from pygments.token import Comment, Name, Punctuation, Text, Whitespace
10
+
11
+
12
+ class DirectoryTreeLexer(RegexLexer):
13
+ r"""Lexer for ``tree(1)``-style directory listings.
14
+
15
+ The lexer targets documentation snippets that show project layouts
16
+ with box-drawing connectors. It deliberately uses standard Pygments
17
+ token types so existing Sphinx styles can colour the output without
18
+ package-specific token CSS.
19
+
20
+ Examples
21
+ --------
22
+ >>> from pygments.token import Token
23
+ >>> lexer = DirectoryTreeLexer()
24
+ >>> source = '''\
25
+ ... root
26
+ ... └── pyproject.toml # config
27
+ ... '''
28
+ >>> tokens = list(lexer.get_tokens(source))
29
+ >>> (Token.Punctuation, '└──') in tokens
30
+ True
31
+ >>> (Token.Name, 'pyproject.toml') in tokens
32
+ True
33
+ >>> (Token.Comment.Single, '# config') in tokens
34
+ True
35
+ """
36
+
37
+ name = "Directory tree"
38
+ aliases = ["tree", "directory-tree", "dir-tree"] # noqa: RUF012
39
+ filenames: t.ClassVar[list[str]] = []
40
+ mimetypes = ["text/x-directory-tree"] # noqa: RUF012
41
+
42
+ tokens = { # noqa: RUF012
43
+ "root": [
44
+ (r"\n", Whitespace),
45
+ (r"[ \t]+", Whitespace),
46
+ (r"(?:├──|└──|│)", Punctuation),
47
+ (r"#.*$", Comment.Single),
48
+ (r"[^/\s#]+/", Name.Namespace),
49
+ (r"[^/\s#.]+\.[^/\s#]+", Name),
50
+ (r"[^/\s#]+", Name.Namespace),
51
+ (r".", Text),
52
+ ],
53
+ }
54
+
55
+ @staticmethod
56
+ def analyse_text(text: str) -> float:
57
+ r"""Return a confidence score for tree-like text.
58
+
59
+ Parameters
60
+ ----------
61
+ text : str
62
+ Source text to inspect.
63
+
64
+ Returns
65
+ -------
66
+ float
67
+ ``0.7`` when box-drawing tree connectors are present,
68
+ otherwise ``0.0``.
69
+
70
+ Examples
71
+ --------
72
+ >>> DirectoryTreeLexer.analyse_text('''\
73
+ ... root
74
+ ... └── pyproject.toml
75
+ ... ''')
76
+ 0.7
77
+ >>> DirectoryTreeLexer.analyse_text("plain text")
78
+ 0.0
79
+ """
80
+ if re.search(r"(?m)^[│ \t]*(?:├──|└──)\s+\S", text):
81
+ return 0.7
82
+ return 0.0