Skip to content

Commit 143bcdf

Browse files
committed
✨ Add sphinxcontrib-tree-sitter
1 parent c5590cd commit 143bcdf

12 files changed

Lines changed: 449 additions & 20 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,40 @@ This project provides:
1212
[texcat](https://texrocks.readthedocs.io/en/latest/topics/texcat.md.html)
1313
- [LaTeX](https://www.latex-project.org/): also
1414
[texcat](https://texrocks.readthedocs.io/en/latest/topics/texcat.md.html)
15+
- [jekyll](https://github.com/jekyll/jekyll): TODO
1516
- [pandoc](http://github.com/pandoc/pandoc): TODO
1617
- [typst](https://github.com/typst/typst): TODO
18+
19+
## Bench
20+
21+
<!-- markdownlint-disable MD013 -->
22+
23+
```bash
24+
$ hyperfine -Nw10 'python -m sphinxcontrib.tree_sitter pyproject.toml' 'texcat pyproject.toml' 'tree-sitter highlight pyproject.toml' 'pygmentize pyproject.toml' 'bat pyproject.toml'
25+
Benchmark 1: python -m sphinxcontrib.tree_sitter pyproject.toml
26+
Time (mean ± σ): 367.2 ms ± 25.7 ms [User: 307.1 ms, System: 51.4 ms]
27+
Range (min … max): 345.6 ms … 414.5 ms 10 runs
28+
29+
Benchmark 2: texcat pyproject.toml
30+
Time (mean ± σ): 70.3 ms ± 3.0 ms [User: 61.0 ms, System: 8.3 ms]
31+
Range (min … max): 67.2 ms … 81.6 ms 42 runs
32+
33+
Benchmark 3: tree-sitter highlight pyproject.toml
34+
Time (mean ± σ): 5.8 ms ± 0.3 ms [User: 2.6 ms, System: 2.9 ms]
35+
Range (min … max): 5.2 ms … 8.1 ms 531 runs
36+
37+
Benchmark 4: pygmentize pyproject.toml
38+
Time (mean ± σ): 250.5 ms ± 13.3 ms [User: 221.1 ms, System: 24.3 ms]
39+
Range (min … max): 238.9 ms … 274.7 ms 10 runs
40+
41+
Benchmark 5: bat pyproject.toml
42+
Time (mean ± σ): 15.0 ms ± 3.4 ms [User: 11.8 ms, System: 6.6 ms]
43+
Range (min … max): 11.7 ms … 29.4 ms 252 runs
44+
45+
Summary
46+
tree-sitter highlight pyproject.toml ran
47+
2.60 ± 0.60 times faster than bat pyproject.toml
48+
12.20 ± 0.86 times faster than texcat pyproject.toml
49+
43.47 ± 3.35 times faster than pygmentize pyproject.toml
50+
63.73 ± 5.71 times faster than python -m sphinxcontrib.tree_sitter pyproject.toml
51+
```

crates/py-tree-sitter-highlight/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ from tree_sitter_highlight import highlight, search_parsers
88

99
parsers = search_parsers(
1010
tree_sitter_python,
11-
# same as:
11+
# NOTE: same as
1212
# python=tree_sitter_python,
1313
sh = tree_sitter_bash,
1414
)

crates/py-tree-sitter-highlight/pyproject.toml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@ dev = [
2020
"pytest-cov>=7",
2121
"tree-sitter-python>=0.25.0",
2222
]
23-
docs = [
24-
"myst-parser>=4",
25-
"tree-sitter-bash>=0.25",
26-
"tree-sitter-comment>=0.3.0",
27-
"tree-sitter-lua>=0.5.0",
28-
"tree-sitter-python>=0.25.0",
29-
"tree-sitter-toml>=0.7.0",
30-
]
3123

3224
[build-system]
3325
requires = ["maturin>=1.0,<2.0"]

crates/py-tree-sitter-highlight/tree_sitter_highlight/tree_sitter_highlight.pyi

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
from __future__ import annotations
22

3-
import sys
43
from typing import Any, Literal, TypedDict
54

6-
if sys.version_info >= (3, 11):
7-
from typing import NotRequired
8-
else:
9-
from typing_extensions import NotRequired
10-
115
# An imported ``tree_sitter_*`` grammar package module object, e.g.
126
# ``import tree_sitter_bash``. The package exposes a ``language()`` function
137
# returning a pointer (as an ``int``) plus on-disk query files; this binding
@@ -20,7 +14,6 @@ Style = Literal["classes", "inline", "minimal"]
2014

2115
__all__ = ["search_parsers", "highlight"]
2216

23-
2417
class ParserEntry(TypedDict):
2518
"""The per-language entry stored in the ``parsers`` mapping."""
2619

@@ -36,14 +29,12 @@ class ParserEntry(TypedDict):
3629
locals: str
3730
"""The text of the language's ``locals.scm`` query (may be empty)."""
3831

39-
4032
# Mapping of language name -> ParserEntry.
4133
Parsers = dict[str, ParserEntry]
4234

4335
# A theme mapping highlight scope names to ``{color, bold, italic, ...}``.
4436
Theme = dict[str, Any]
4537

46-
4738
def search_parsers(
4839
*modules: GrammarModule,
4940
**named_modules: GrammarModule,
@@ -62,7 +53,6 @@ def search_parsers(
6253
``LOCALS_QUERY`` module attributes).
6354
"""
6455

65-
6656
def highlight(
6757
*,
6858
source: str | None = None,

docs/conf.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
copyright = "2026-" + str(datetime.now().year)
2626

2727
PROJECT_FILE = os.path.join(
28-
os.path.dirname(os.path.dirname(__file__)), "pyproject.toml"
28+
os.path.dirname(os.path.dirname(__file__)),
29+
"packages",
30+
"sphinxcontrib-tree-sitter",
31+
"pyproject.toml",
2932
)
3033

3134
with open(PROJECT_FILE, "rb") as f:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# sphinxcontrib-tree-sitter
2+
3+
Use tree-sitter to highlight code blocks in Sphinx.
4+
5+
## Usage
6+
7+
`docs/conf.py`:
8+
9+
```python
10+
extensions = [
11+
"sphinxcontrib.tree_sitter",
12+
]
13+
```
14+
15+
`pyproject.toml`:
16+
17+
```toml
18+
# ...
19+
[project.optional-dependencies]
20+
dev = [
21+
"sphinx",
22+
"tree-sitter-XXX",
23+
]
24+
```
25+
26+
```bash
27+
uv sync
28+
source .venv/bin/activate
29+
sphinx-build docs _readthedocs/html
30+
xdg-open _readthedocs/html/index.html
31+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package = "bench"
2+
version = "0.0.1"
3+
lua = ">=5.1"
4+
5+
[description]
6+
summary = ""
7+
maintainer = "Wu"
8+
labels = [ "" ]
9+
10+
[test_dependencies]
11+
texcat = ">=0.0.1"
12+
13+
[build]
14+
type = "builtin"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[project]
2+
name = "sphinxcontrib-tree-sitter"
3+
version = "0.0.1"
4+
description = "Use tree-sitter to highlight code blocks in Sphinx"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
license = "GPL-3.0-or-later"
8+
authors = [{ name = "Wu, Zhenyu", email = "wuzhenyu@ustc.edu" }]
9+
dependencies = [
10+
"identify>=2",
11+
"pygments>=2",
12+
"tree-sitter-highlight",
13+
]
14+
15+
[project.urls]
16+
homepage = "https://github.com/sphinx-contrib/tree-sitter-highlight"
17+
18+
[project.optional-dependencies]
19+
dev = [
20+
"myst-parser>=4",
21+
"tree-sitter-bash>=0.25",
22+
"tree-sitter-comment>=0.3.0",
23+
"tree-sitter-lua>=0.5.0",
24+
"tree-sitter-python>=0.25.0",
25+
"tree-sitter-toml>=0.7.0",
26+
]
27+
28+
[build-system]
29+
requires = ["uv_build<0.12.0"]
30+
build-backend = "uv_build"
31+
32+
[tool.uv.build-backend]
33+
module-name = "sphinxcontrib.tree_sitter"
34+
35+
[tool.uv.sources]
36+
tree-sitter-highlight = { workspace = true }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import TYPE_CHECKING
2+
3+
if TYPE_CHECKING:
4+
from sphinx.application import Sphinx
5+
6+
__version__ = "0.0.1"
7+
8+
9+
def setup(_: "Sphinx") -> dict[str, str | bool]:
10+
from sphinx.highlighting import PygmentsBridge
11+
12+
from .highlighting import TreeSitterBridge
13+
14+
PygmentsBridge.highlight_block = TreeSitterBridge.highlight_block # ty:ignore[invalid-assignment]
15+
PygmentsBridge.get_stylesheet = TreeSitterBridge.get_stylesheet # ty:ignore[invalid-assignment]
16+
17+
return {
18+
"version": __version__,
19+
"parallel_read_safe": True,
20+
"parallel_write_safe": True,
21+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
r"""This module can be called by
2+
`python -m <https://docs.python.org/3/library/__main__.html>`_.
3+
"""
4+
5+
from argparse import ArgumentParser
6+
7+
from pygments.styles import STYLE_MAP, get_style_by_name
8+
from tree_sitter_highlight import highlight
9+
10+
from . import __version__
11+
from .highlighting import TreeSitterBridge
12+
13+
14+
def get_parser(parsers) -> ArgumentParser:
15+
r"""Get a parser for unit test."""
16+
parser = ArgumentParser()
17+
parser.add_argument("--version", version=__version__, action="version")
18+
19+
parser.add_argument("files", nargs="*", help="file name")
20+
parser.add_argument(
21+
"--language",
22+
choices=parsers,
23+
help="set language",
24+
)
25+
parser.add_argument(
26+
"--theme",
27+
choices=STYLE_MAP,
28+
help="set theme",
29+
)
30+
parser.add_argument(
31+
"--format",
32+
default="terminal",
33+
choices=["latex", "html", "terminal"],
34+
help="output format (default: terminal)",
35+
)
36+
parser.add_argument(
37+
"--layout",
38+
default="document",
39+
choices=["fragment", "document", "line-numbers"],
40+
help="layout (default: document)",
41+
)
42+
parser.add_argument(
43+
"--style",
44+
default="classes",
45+
choices=["minimal", "classes", "inline"],
46+
help="style (default: classes)",
47+
)
48+
parser.add_argument(
49+
"--prefix", default="TS", help="command prefix for TeX (default: TS)"
50+
)
51+
parser.add_argument(
52+
"--math-escape",
53+
nargs="*",
54+
default=[],
55+
help="the scope to escape $math TeX code$",
56+
)
57+
parser.add_argument(
58+
"--injections",
59+
nargs="*",
60+
default=[],
61+
help="injection languages, all means all",
62+
)
63+
return parser
64+
65+
66+
def main() -> None:
67+
r"""Parse arguments and provide shell completions."""
68+
parsers = TreeSitterBridge.get_parsers()
69+
parser = get_parser(parsers)
70+
args = parser.parse_args()
71+
72+
from identify import identify
73+
74+
theme = None
75+
if args.theme:
76+
style = get_style_by_name(args.theme)
77+
theme = TreeSitterBridge.get_theme(style)
78+
for file in args.files:
79+
language = args.language
80+
if language is None:
81+
tags = identify.tags_from_path(file)
82+
for tag in tags:
83+
if tag in parsers:
84+
language = tag
85+
break
86+
if language is None:
87+
language = "text"
88+
if language == "text":
89+
with open(file) as f:
90+
text = f.read()
91+
print(text)
92+
elif language in parsers:
93+
hlsource = highlight(
94+
file=file,
95+
language=language,
96+
parsers=parsers,
97+
theme=theme,
98+
layout=args.layout,
99+
style=args.style,
100+
prefix=args.prefix,
101+
math_escape=args.math_escape,
102+
)
103+
print(hlsource)
104+
else:
105+
print(f"No parser found for {language}")
106+
107+
108+
if __name__ == "__main__":
109+
main()

0 commit comments

Comments
 (0)