-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpyrope_highlight.py
More file actions
115 lines (87 loc) · 2.94 KB
/
Copy pathpyrope_highlight.py
File metadata and controls
115 lines (87 loc) · 2.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""MkDocs custom fence formatter for Pyrope code blocks."""
from __future__ import annotations
import html
import os
import re
import subprocess
import tempfile
from pathlib import Path
_LINE_RE = re.compile(r"<td class=line>(.*?)</td>", re.DOTALL)
def _docs_root() -> Path:
return Path(__file__).resolve().parent
def _grammar_path() -> Path | None:
env_path = os.environ.get("PYROPE_TREE_SITTER_GRAMMAR")
candidates = [
Path(env_path) if env_path else None,
_docs_root() / "tree-sitter-pyrope",
_docs_root().parent / "tree-sitter-pyrope",
]
for candidate in candidates:
if candidate and (candidate / "tree-sitter.json").is_file():
return candidate
return None
def _tree_sitter_cli(grammar: Path) -> str:
env_cli = os.environ.get("PYROPE_TREE_SITTER_CLI")
if env_cli:
return env_cli
local_cli = grammar / "node_modules" / ".bin" / "tree-sitter"
if local_cli.is_file():
return str(local_cli)
return "tree-sitter"
def _highlight(source: str) -> str | None:
grammar = _grammar_path()
if grammar is None:
return None
with tempfile.NamedTemporaryFile(
mode="w",
suffix=".prp",
encoding="utf-8",
delete=False,
) as tmp:
tmp.write(source)
tmp_path = Path(tmp.name)
try:
result = subprocess.run(
[
_tree_sitter_cli(grammar),
"highlight",
"--html",
"--css-classes",
"-p",
str(grammar),
str(tmp_path),
],
check=True,
capture_output=True,
text=True,
)
except (OSError, subprocess.CalledProcessError) as exc:
print(f"Pyrope highlighting disabled: {exc}")
return None
finally:
tmp_path.unlink(missing_ok=True)
lines = _LINE_RE.findall(result.stdout)
if not lines:
return None
return "".join(lines)
def _plain_code(source: str) -> str:
return html.escape(source)
def fence_pyrope(source, language, class_name, options, md, **kwargs):
"""Render a ```pyrope fenced block using tree-sitter-pyrope highlights."""
classes = kwargs.get("classes", [])
attrs = kwargs.get("attrs", {})
id_value = kwargs.get("id_value", "")
code = _highlight(source)
if code is None:
code = _plain_code(source)
class_values = ["highlight", "pyrope-highlight"]
if class_name:
class_values.append(class_name)
class_values.extend(classes)
id_attr = f' id="{html.escape(id_value, quote=True)}"' if id_value else ""
class_attr = f' class="{" ".join(html.escape(c, quote=True) for c in class_values)}"'
extra_attrs = "".join(
f' {html.escape(str(k), quote=True)}="{html.escape(str(v), quote=True)}"'
for k, v in attrs.items()
)
return f"<div{class_attr}{id_attr}{extra_attrs}><pre><code>{code}</code></pre></div>"