rustgments 0.1.0__cp39-abi3-win_amd64.whl
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.
- rustgments/__init__.py +203 -0
- rustgments/_dropin.py +279 -0
- rustgments/_native.pyd +0 -0
- rustgments/mkdocs_plugin.py +15 -0
- rustgments/sphinxext.py +12 -0
- rustgments-0.1.0.dist-info/METADATA +123 -0
- rustgments-0.1.0.dist-info/RECORD +14 -0
- rustgments-0.1.0.dist-info/WHEEL +4 -0
- rustgments-0.1.0.dist-info/entry_points.txt +2 -0
- rustgments-0.1.0.dist-info/licenses/LICENSE +25 -0
- rustgments-0.1.0.dist-info/licenses/LICENSE-pygments +25 -0
- rustgments-0.1.0.dist-info/licenses/LICENSE-third-party.txt +566 -0
- rustgments-0.1.0.dist-info/sboms/rustgments.cyclonedx.json +1401 -0
- rustgments-autoinstall.pth +1 -0
rustgments/__init__.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""rustgments: Pygments-compatible highlighting with a Rust engine.
|
|
2
|
+
|
|
3
|
+
The token tables are generated from Pygments 2.21.0 (BSD-2-Clause, see
|
|
4
|
+
``rustgments.PYGMENTS_NOTICE``). Lexers that the Rust engine cannot reproduce
|
|
5
|
+
exactly fall back to Pygments itself; ``Lexer.backend`` says which one is used.
|
|
6
|
+
|
|
7
|
+
Pygments submodules are imported lazily: the native highlight path never
|
|
8
|
+
needs them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pygments
|
|
12
|
+
|
|
13
|
+
from . import _native
|
|
14
|
+
|
|
15
|
+
__version__ = '0.1.0'
|
|
16
|
+
|
|
17
|
+
__all__ = ['get_lexer_by_name', 'highlight', 'HtmlFormatter', 'Lexer', 'backend_of',
|
|
18
|
+
'EngineFailure', 'PYGMENTS_NOTICE', 'install', 'uninstall']
|
|
19
|
+
|
|
20
|
+
EngineFailure = _native.EngineFailure
|
|
21
|
+
# Native tables are only valid for the Pygments version they were built from,
|
|
22
|
+
# and for the interpreter they were built with: Python's \w, \d, \s and its
|
|
23
|
+
# case-insensitive matching follow the interpreter's Unicode database and
|
|
24
|
+
# `re` module. The IR carries one table variant per supported Python
|
|
25
|
+
# version; pick ours before anything else touches the engine.
|
|
26
|
+
import sys as _sys
|
|
27
|
+
import unicodedata as _ud
|
|
28
|
+
_PYTHON_TAG = '%d.%d' % _sys.version_info[:2]
|
|
29
|
+
_VARIANT_OK = _native.select_variant(_PYTHON_TAG, _ud.unidata_version)
|
|
30
|
+
_INFO = _native.info()
|
|
31
|
+
PYGMENTS_NOTICE = _INFO['notice']
|
|
32
|
+
PYGMENTS_VERSION = _INFO['pygments_version']
|
|
33
|
+
_VERSION_OK = pygments.__version__ == PYGMENTS_VERSION and _VARIANT_OK
|
|
34
|
+
|
|
35
|
+
_LEXERS = {}
|
|
36
|
+
_ALIASES = {}
|
|
37
|
+
for _cls, _name, _aliases, _filenames, _backend, _reason in _native.lexers():
|
|
38
|
+
_LEXERS[_cls] = (_name, tuple(_aliases), tuple(_filenames), _backend, _reason)
|
|
39
|
+
for _a in _aliases:
|
|
40
|
+
_ALIASES[_a] = _cls
|
|
41
|
+
|
|
42
|
+
_NATIVE_BACKENDS = ('native', 'handwritten')
|
|
43
|
+
_TOKENS = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _token_types():
|
|
47
|
+
global _TOKENS
|
|
48
|
+
if _TOKENS is None:
|
|
49
|
+
from pygments.token import string_to_tokentype
|
|
50
|
+
_TOKENS = [string_to_tokentype(n) for n in _native.token_names()]
|
|
51
|
+
return _TOKENS
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _pyg_lexers():
|
|
55
|
+
import pygments.lexers
|
|
56
|
+
return pygments.lexers
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Lexer:
|
|
60
|
+
"""A lexer that runs natively when it can, else wraps the Pygments lexer."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, class_name, **options):
|
|
63
|
+
self.class_name = class_name
|
|
64
|
+
self.options = options
|
|
65
|
+
self._pyg = None
|
|
66
|
+
info = _LEXERS.get(class_name)
|
|
67
|
+
if info is None:
|
|
68
|
+
self.backend = 'fallback'
|
|
69
|
+
self.reason = 'not in the rustgments native set'
|
|
70
|
+
elif not _VERSION_OK:
|
|
71
|
+
self.backend = 'fallback'
|
|
72
|
+
self.reason = (f'no tables for Pygments {pygments.__version__} on Python {_PYTHON_TAG} '
|
|
73
|
+
f'(Unicode {_ud.unidata_version}); tables are for Pygments {PYGMENTS_VERSION}')
|
|
74
|
+
elif options:
|
|
75
|
+
self.backend = 'fallback'
|
|
76
|
+
self.reason = f'non-default lexer options {sorted(options)}'
|
|
77
|
+
elif info[3] in _NATIVE_BACKENDS:
|
|
78
|
+
self.backend = 'native'
|
|
79
|
+
self.reason = info[4] if info[3] == 'handwritten' else None
|
|
80
|
+
else:
|
|
81
|
+
self.backend = 'fallback'
|
|
82
|
+
self.reason = info[4]
|
|
83
|
+
self.name = info[0] if info else self.pygments_lexer.name
|
|
84
|
+
|
|
85
|
+
def __repr__(self):
|
|
86
|
+
return f'<rustgments.Lexer {self.class_name} backend={self.backend}>'
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def pygments_lexer(self):
|
|
90
|
+
if self._pyg is None:
|
|
91
|
+
self._pyg = getattr(_pyg_lexers(), self.class_name)(**self.options)
|
|
92
|
+
return self._pyg
|
|
93
|
+
|
|
94
|
+
def get_tokens_unprocessed(self, text):
|
|
95
|
+
if self.backend == 'native' and isinstance(text, str):
|
|
96
|
+
try:
|
|
97
|
+
raw = _native.get_tokens_unprocessed(self.class_name, text)
|
|
98
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
toks = _token_types()
|
|
102
|
+
return [(i, toks[t], v) for i, t, v in raw]
|
|
103
|
+
return list(self.pygments_lexer.get_tokens_unprocessed(text))
|
|
104
|
+
|
|
105
|
+
def get_tokens(self, text):
|
|
106
|
+
if self.backend == 'native' and isinstance(text, str):
|
|
107
|
+
try:
|
|
108
|
+
pre = _native.preprocess(text)
|
|
109
|
+
raw = _native.get_tokens_unprocessed(self.class_name, pre)
|
|
110
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
111
|
+
pass
|
|
112
|
+
else:
|
|
113
|
+
toks = _token_types()
|
|
114
|
+
return [(toks[t], v) for _, t, v in raw]
|
|
115
|
+
return list(self.pygments_lexer.get_tokens(text))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_lexer_by_name(alias, **options):
|
|
119
|
+
cls = _ALIASES.get(alias)
|
|
120
|
+
if cls is None:
|
|
121
|
+
cls = type(_pyg_lexers().get_lexer_by_name(alias)).__name__
|
|
122
|
+
return Lexer(cls, **options)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def backend_of(alias):
|
|
126
|
+
return get_lexer_by_name(alias).backend
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class HtmlFormatter:
|
|
130
|
+
"""HtmlFormatter; native only with default options."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, **options):
|
|
133
|
+
self.options = options
|
|
134
|
+
self.native = not options
|
|
135
|
+
self._pyg = None
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def pygments_formatter(self):
|
|
139
|
+
if self._pyg is None:
|
|
140
|
+
from pygments.formatters.html import HtmlFormatter as PygHtmlFormatter
|
|
141
|
+
self._pyg = PygHtmlFormatter(**self.options)
|
|
142
|
+
return self._pyg
|
|
143
|
+
|
|
144
|
+
def get_style_defs(self, arg=None):
|
|
145
|
+
return self.pygments_formatter.get_style_defs(arg)
|
|
146
|
+
|
|
147
|
+
def format(self, tokensource, outfile):
|
|
148
|
+
return self.pygments_formatter.format(tokensource, outfile)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _native_lexer_class(lexer):
|
|
152
|
+
if isinstance(lexer, Lexer):
|
|
153
|
+
return lexer.class_name if lexer.backend == 'native' else None
|
|
154
|
+
# a plain Pygments lexer instance of a native class with default options
|
|
155
|
+
name = type(lexer).__name__
|
|
156
|
+
info = _LEXERS.get(name)
|
|
157
|
+
if (info and _VERSION_OK and info[3] in _NATIVE_BACKENDS
|
|
158
|
+
and type(lexer) is getattr(_pyg_lexers(), name, None)
|
|
159
|
+
and not lexer.options and not lexer.filters):
|
|
160
|
+
return name
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _native_formatter(formatter):
|
|
165
|
+
if isinstance(formatter, HtmlFormatter):
|
|
166
|
+
return formatter.native
|
|
167
|
+
from pygments.formatters.html import HtmlFormatter as PygHtmlFormatter
|
|
168
|
+
return type(formatter) is PygHtmlFormatter and not formatter.options
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def highlight(code, lexer, formatter, outfile=None):
|
|
172
|
+
"""Like ``pygments.highlight``; native for native lexers + default HtmlFormatter."""
|
|
173
|
+
cls = _native_lexer_class(lexer)
|
|
174
|
+
if cls is not None and isinstance(code, str) and _native_formatter(formatter):
|
|
175
|
+
try:
|
|
176
|
+
html = _native.highlight_html(cls, code)
|
|
177
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
178
|
+
html = None
|
|
179
|
+
if html is not None:
|
|
180
|
+
if outfile is not None:
|
|
181
|
+
outfile.write(html)
|
|
182
|
+
return None
|
|
183
|
+
return html
|
|
184
|
+
from pygments import highlight as pyg_highlight
|
|
185
|
+
pyg_lexer = lexer.pygments_lexer if isinstance(lexer, Lexer) else lexer
|
|
186
|
+
pyg_fmt = formatter.pygments_formatter if isinstance(formatter, HtmlFormatter) else formatter
|
|
187
|
+
return pyg_highlight(code, pyg_lexer, pyg_fmt, outfile)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def install():
|
|
191
|
+
"""Drop-in mode: make Pygments' own ``highlight``, ``Lexer.get_tokens``,
|
|
192
|
+
``Lexer.get_tokens_unprocessed`` and ``HtmlFormatter`` use the native
|
|
193
|
+
engine for native lexers wherever the output is identical (see
|
|
194
|
+
``rustgments._dropin``). Returns False, changing nothing, when this
|
|
195
|
+
interpreter or Pygments version has no native tables."""
|
|
196
|
+
from ._dropin import install as _install
|
|
197
|
+
return _install()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def uninstall():
|
|
201
|
+
"""Undo ``install()``."""
|
|
202
|
+
from ._dropin import uninstall as _uninstall
|
|
203
|
+
_uninstall()
|
rustgments/_dropin.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Drop-in mode: Pygments itself uses the native engine where it is exact.
|
|
2
|
+
|
|
3
|
+
``install()`` patches, for every lexer class rustgments runs natively:
|
|
4
|
+
|
|
5
|
+
* ``get_tokens_unprocessed``: native tokens for instances of exactly that
|
|
6
|
+
class (subclasses, other start states and non-str input keep Pygments' own
|
|
7
|
+
method). Code that only drives lexers, such as ``DelegatingLexer`` or
|
|
8
|
+
``using(...)`` callbacks of other lexers, picks this up too.
|
|
9
|
+
* ``get_tokens``: Pygments' own preprocessing (``_preprocess_lexer_input``
|
|
10
|
+
with the lexer's options), then a token stream that lexes natively when it
|
|
11
|
+
is first read and runs the lexer's filters on it in Python.
|
|
12
|
+
|
|
13
|
+
and ``HtmlFormatter.format_unencoded``: when it receives such a stream
|
|
14
|
+
untouched, ``_format_lines`` (the per-token loop) runs natively with span
|
|
15
|
+
openers computed by the formatter's own code; every per-line step (line
|
|
16
|
+
numbers, ``hl_lines``, anchors, ``wrap``, ``<div>``, ``full``) still runs
|
|
17
|
+
the formatter's own methods. A stream with a token of type ``Error`` and a
|
|
18
|
+
``raiseonerror`` filter takes Pygments' path, so the error is raised at the
|
|
19
|
+
same point with the same partial output.
|
|
20
|
+
|
|
21
|
+
Everything else (other lexers, other formatters, unknown filters, overridden
|
|
22
|
+
``_format_lines``, ``tagsfile``) runs Pygments untouched. ``uninstall()``
|
|
23
|
+
restores the original methods.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import threading
|
|
27
|
+
|
|
28
|
+
from . import _native, EngineFailure
|
|
29
|
+
|
|
30
|
+
_LOCK = threading.Lock()
|
|
31
|
+
_SAVED = [] # (owner, attribute, original value or _MISSING)
|
|
32
|
+
_MISSING = object()
|
|
33
|
+
_INSTALLED = False
|
|
34
|
+
|
|
35
|
+
_HANDLES = {} # opener configuration key -> native handle
|
|
36
|
+
|
|
37
|
+
# How often each path ran (for tests and benchmarks; plain ints, no locking).
|
|
38
|
+
STATS = {'lex_native': 0, 'lex_pygments': 0, 'html_native': 0, 'html_pygments': 0}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save(owner, name, value):
|
|
42
|
+
_SAVED.append((owner, name, owner.__dict__.get(name, _MISSING)))
|
|
43
|
+
setattr(owner, name, value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NativeTokens:
|
|
47
|
+
"""The token stream of ``Lexer.get_tokens`` for a native lexer.
|
|
48
|
+
|
|
49
|
+
It behaves like the generator Pygments returns: an iterator of
|
|
50
|
+
``(tokentype, value)`` pairs, lexed on first use, with the lexer's filters
|
|
51
|
+
applied. The HTML formatter recognises an untouched stream and formats it
|
|
52
|
+
natively.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
__slots__ = ('lexer', 'class_name', 'text', 'filters', '_it')
|
|
56
|
+
|
|
57
|
+
def __init__(self, lexer, class_name, text, filters):
|
|
58
|
+
self.lexer = lexer
|
|
59
|
+
self.class_name = class_name
|
|
60
|
+
self.text = text
|
|
61
|
+
self.filters = filters
|
|
62
|
+
self._it = None
|
|
63
|
+
|
|
64
|
+
def __iter__(self):
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __next__(self):
|
|
68
|
+
if self._it is None:
|
|
69
|
+
self._it = self._stream()
|
|
70
|
+
return next(self._it)
|
|
71
|
+
|
|
72
|
+
def close(self):
|
|
73
|
+
self._it = iter(())
|
|
74
|
+
|
|
75
|
+
def _stream(self):
|
|
76
|
+
from pygments.filter import apply_filters
|
|
77
|
+
toks = None
|
|
78
|
+
if type(self.text) is str:
|
|
79
|
+
try:
|
|
80
|
+
toks = _native.tokens(self.class_name, self.text)
|
|
81
|
+
STATS['lex_native'] += 1
|
|
82
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
83
|
+
toks = None
|
|
84
|
+
if toks is None:
|
|
85
|
+
toks = ((t, v) for _, t, v in self.lexer.get_tokens_unprocessed(self.text))
|
|
86
|
+
stream = iter(toks)
|
|
87
|
+
if self.filters:
|
|
88
|
+
stream = apply_filters(stream, self.filters, self.lexer)
|
|
89
|
+
return stream
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _patch_lexer(cls, class_name):
|
|
93
|
+
orig_gtu = cls.get_tokens_unprocessed
|
|
94
|
+
orig_gt = cls.get_tokens
|
|
95
|
+
from pygments.lexer import RegexLexer
|
|
96
|
+
regex = issubclass(cls, RegexLexer)
|
|
97
|
+
|
|
98
|
+
if regex:
|
|
99
|
+
def get_tokens_unprocessed(self, text, stack=('root',)):
|
|
100
|
+
if type(self) is cls and type(text) is str and stack == ('root',):
|
|
101
|
+
try:
|
|
102
|
+
toks = _native.tokens_with_offsets(class_name, text)
|
|
103
|
+
STATS['lex_native'] += 1
|
|
104
|
+
return iter(toks)
|
|
105
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
106
|
+
pass
|
|
107
|
+
STATS['lex_pygments'] += 1
|
|
108
|
+
return orig_gtu(self, text, stack)
|
|
109
|
+
else:
|
|
110
|
+
def get_tokens_unprocessed(self, text):
|
|
111
|
+
if type(self) is cls and type(text) is str:
|
|
112
|
+
try:
|
|
113
|
+
toks = _native.tokens_with_offsets(class_name, text)
|
|
114
|
+
STATS['lex_native'] += 1
|
|
115
|
+
return iter(toks)
|
|
116
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
117
|
+
pass
|
|
118
|
+
STATS['lex_pygments'] += 1
|
|
119
|
+
return orig_gtu(self, text)
|
|
120
|
+
|
|
121
|
+
def get_tokens(self, text, unfiltered=False):
|
|
122
|
+
if type(self) is not cls:
|
|
123
|
+
return orig_gt(self, text, unfiltered)
|
|
124
|
+
# Lexer.get_tokens: preprocess eagerly, lex lazily, then filters
|
|
125
|
+
text = self._preprocess_lexer_input(text)
|
|
126
|
+
return NativeTokens(self, class_name, text, () if unfiltered else self.filters)
|
|
127
|
+
|
|
128
|
+
get_tokens_unprocessed.__doc__ = orig_gtu.__doc__
|
|
129
|
+
get_tokens.__doc__ = orig_gt.__doc__
|
|
130
|
+
_save(cls, 'get_tokens_unprocessed', get_tokens_unprocessed)
|
|
131
|
+
_save(cls, 'get_tokens', get_tokens)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _opener_handle(fmt):
|
|
135
|
+
"""Native handle for the span openers and line separator of `fmt`.
|
|
136
|
+
|
|
137
|
+
The openers come from the formatter's own `_format_lines` (run once over
|
|
138
|
+
every token type), so `noclasses`, styles, `classprefix` and
|
|
139
|
+
`debug_token_types` behave exactly as in Pygments. A fresh formatter of a
|
|
140
|
+
class that does not override the CSS helpers depends only on the key
|
|
141
|
+
below, so its handle is shared; anything else is computed per instance.
|
|
142
|
+
"""
|
|
143
|
+
from pygments.formatters.html import HtmlFormatter
|
|
144
|
+
from . import _token_types
|
|
145
|
+
cached = fmt.__dict__.get('_rustgments_handle')
|
|
146
|
+
if cached is not None:
|
|
147
|
+
return cached
|
|
148
|
+
t = type(fmt)
|
|
149
|
+
shareable = (not fmt.span_element_openers
|
|
150
|
+
and t._get_css_class is HtmlFormatter._get_css_class
|
|
151
|
+
and t._get_css_classes is HtmlFormatter._get_css_classes
|
|
152
|
+
and t._get_css_inline_styles is HtmlFormatter._get_css_inline_styles)
|
|
153
|
+
key = None
|
|
154
|
+
if shareable:
|
|
155
|
+
key = (t, bool(fmt.noclasses), fmt.classprefix, bool(fmt.debug_token_types),
|
|
156
|
+
fmt.lineseparator, fmt.style if fmt.noclasses else None)
|
|
157
|
+
h = _HANDLES.get(key)
|
|
158
|
+
if h is not None:
|
|
159
|
+
return h
|
|
160
|
+
tts = _token_types()
|
|
161
|
+
for _ in fmt._format_lines([(tt, 'x') for tt in tts]):
|
|
162
|
+
pass
|
|
163
|
+
openers = [fmt.span_element_openers[tt] for tt in tts]
|
|
164
|
+
h = _native.register_openers(openers, fmt.lineseparator)
|
|
165
|
+
if key is not None:
|
|
166
|
+
_HANDLES[key] = h
|
|
167
|
+
else:
|
|
168
|
+
fmt.__dict__['_rustgments_handle'] = h
|
|
169
|
+
return h
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _patch_html_formatter():
|
|
173
|
+
from pygments.filters import RaiseOnErrorTokenFilter
|
|
174
|
+
from pygments.formatters.html import HtmlFormatter
|
|
175
|
+
orig = HtmlFormatter.format_unencoded
|
|
176
|
+
orig_lines = HtmlFormatter._format_lines
|
|
177
|
+
orig_translate = HtmlFormatter._translate_parts
|
|
178
|
+
|
|
179
|
+
def native_lines(self, ts):
|
|
180
|
+
t = type(self)
|
|
181
|
+
if (t._format_lines is not orig_lines or t._translate_parts is not orig_translate
|
|
182
|
+
or self.tagsfile or type(ts.text) is not str):
|
|
183
|
+
return None
|
|
184
|
+
if any(type(f) is not RaiseOnErrorTokenFilter for f in ts.filters):
|
|
185
|
+
return None
|
|
186
|
+
try:
|
|
187
|
+
return _native.format_lines(ts.class_name, ts.text, _opener_handle(self), bool(ts.filters))
|
|
188
|
+
except (EngineFailure, UnicodeEncodeError):
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def format_unencoded(self, tokensource, outfile):
|
|
192
|
+
if type(tokensource) is NativeTokens and tokensource._it is None:
|
|
193
|
+
lines = native_lines(self, tokensource)
|
|
194
|
+
if lines is not None:
|
|
195
|
+
STATS['html_native'] += 1
|
|
196
|
+
tokensource._it = iter(())
|
|
197
|
+
source = ((1, line) for line in lines)
|
|
198
|
+
# The rest is HtmlFormatter.format_unencoded (Pygments 2.21.0)
|
|
199
|
+
# after `source = self._format_lines(tokensource)`.
|
|
200
|
+
if not self.nowrap and self.linenos == 2:
|
|
201
|
+
source = self._wrap_inlinelinenos(source)
|
|
202
|
+
if self.hl_lines:
|
|
203
|
+
source = self._highlight_lines(source)
|
|
204
|
+
if not self.nowrap:
|
|
205
|
+
if self.lineanchors:
|
|
206
|
+
source = self._wrap_lineanchors(source)
|
|
207
|
+
if self.linespans:
|
|
208
|
+
source = self._wrap_linespans(source)
|
|
209
|
+
source = self.wrap(source)
|
|
210
|
+
if self.linenos == 1:
|
|
211
|
+
source = self._wrap_tablelinenos(source)
|
|
212
|
+
source = self._wrap_div(source)
|
|
213
|
+
if self.full:
|
|
214
|
+
source = self._wrap_full(source, outfile)
|
|
215
|
+
for t, piece in source:
|
|
216
|
+
outfile.write(piece)
|
|
217
|
+
return None
|
|
218
|
+
STATS['html_pygments'] += 1
|
|
219
|
+
return orig(self, tokensource, outfile)
|
|
220
|
+
|
|
221
|
+
format_unencoded.__doc__ = orig.__doc__
|
|
222
|
+
_save(HtmlFormatter, 'format_unencoded', format_unencoded)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# sha256 of the source of HtmlFormatter.format_unencoded in Pygments 2.21.0,
|
|
226
|
+
# whose tail is copied above; install() refuses to patch anything else.
|
|
227
|
+
_FORMAT_UNENCODED_SHA256 = '70460d4f03e958a4052d16e84b873aaa1b60d8d90af84c0ef2e81e93377d860a'
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _formatter_source_ok():
|
|
231
|
+
import hashlib
|
|
232
|
+
import inspect
|
|
233
|
+
from pygments.formatters.html import HtmlFormatter
|
|
234
|
+
try:
|
|
235
|
+
src = inspect.getsource(HtmlFormatter.format_unencoded)
|
|
236
|
+
except (OSError, TypeError):
|
|
237
|
+
return True # no source shipped; the Pygments version pin still holds
|
|
238
|
+
return hashlib.sha256(src.encode()).hexdigest() == _FORMAT_UNENCODED_SHA256
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def install():
|
|
242
|
+
"""Route Pygments' own lexers and HtmlFormatter through rustgments where
|
|
243
|
+
the output is identical. Returns True if installed, False when this
|
|
244
|
+
interpreter / Pygments version has no native tables (Pygments then runs
|
|
245
|
+
unchanged)."""
|
|
246
|
+
global _INSTALLED
|
|
247
|
+
from . import _VERSION_OK, _LEXERS, _NATIVE_BACKENDS, _token_types
|
|
248
|
+
with _LOCK:
|
|
249
|
+
if _INSTALLED:
|
|
250
|
+
return True
|
|
251
|
+
if not _VERSION_OK:
|
|
252
|
+
return False
|
|
253
|
+
import pygments.lexers
|
|
254
|
+
if not _formatter_source_ok():
|
|
255
|
+
return False
|
|
256
|
+
_native.set_token_types(_token_types())
|
|
257
|
+
for name, info in _LEXERS.items():
|
|
258
|
+
if info[3] in _NATIVE_BACKENDS:
|
|
259
|
+
_patch_lexer(getattr(pygments.lexers, name), name)
|
|
260
|
+
_patch_html_formatter()
|
|
261
|
+
_INSTALLED = True
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def uninstall():
|
|
266
|
+
"""Restore every method install() replaced."""
|
|
267
|
+
global _INSTALLED
|
|
268
|
+
with _LOCK:
|
|
269
|
+
while _SAVED:
|
|
270
|
+
owner, name, value = _SAVED.pop()
|
|
271
|
+
if value is _MISSING:
|
|
272
|
+
delattr(owner, name)
|
|
273
|
+
else:
|
|
274
|
+
setattr(owner, name, value)
|
|
275
|
+
_INSTALLED = False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def installed():
|
|
279
|
+
return _INSTALLED
|
rustgments/_native.pyd
ADDED
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""MkDocs plugin (``plugins: [rustgments]``): calls ``rustgments.install()``.
|
|
2
|
+
|
|
3
|
+
Pygments calls made by Markdown extensions (codehilite, pymdownx.highlight)
|
|
4
|
+
then use the native engine where the output is identical.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from mkdocs.plugins import BasePlugin
|
|
8
|
+
|
|
9
|
+
import rustgments
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RustgmentsPlugin(BasePlugin):
|
|
13
|
+
def on_config(self, config):
|
|
14
|
+
rustgments.install()
|
|
15
|
+
return config
|
rustgments/sphinxext.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Sphinx extension: ``extensions = [..., 'rustgments.sphinxext']``.
|
|
2
|
+
|
|
3
|
+
Calls ``rustgments.install()`` so that Sphinx's Pygments calls use the native
|
|
4
|
+
engine where the output is identical. Output files do not change.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import rustgments
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def setup(app):
|
|
11
|
+
rustgments.install()
|
|
12
|
+
return {'version': '0.1.0', 'parallel_read_safe': True, 'parallel_write_safe': True}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rustgments
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Rust
|
|
7
|
+
Classifier: Topic :: Text Processing :: Filters
|
|
8
|
+
Requires-Dist: pygments>=2.21
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: LICENSE-pygments
|
|
11
|
+
License-File: LICENSE-third-party.txt
|
|
12
|
+
Summary: A Rust engine for Pygments: byte-identical output, much faster when you highlight a lot
|
|
13
|
+
Keywords: pygments,syntax-highlighting,rust
|
|
14
|
+
License: BSD-2-Clause
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
17
|
+
Project-URL: Repository, https://github.com/b84330808/rustgments
|
|
18
|
+
|
|
19
|
+
# rustgments
|
|
20
|
+
|
|
21
|
+
A Rust engine for [Pygments](https://pygments.org/). It runs Pygments' own lexer tables and produces the same tokens and the same HTML, byte for byte. A lexer or option it can't reproduce exactly goes to Pygments itself, so output never changes.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import rustgments
|
|
25
|
+
|
|
26
|
+
lexer = rustgments.get_lexer_by_name('python')
|
|
27
|
+
html = rustgments.highlight(code, lexer, rustgments.HtmlFormatter())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or leave your code alone and make Pygments itself use it:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import rustgments
|
|
34
|
+
rustgments.install() # pygments.highlight() and friends now use the native path
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## When it helps
|
|
38
|
+
|
|
39
|
+
It helps when one process highlights a lot of code. Python startup and imports are the same either way, so a one-shot script on a small file gains little.
|
|
40
|
+
|
|
41
|
+
| Workload | Pygments | rustgments | |
|
|
42
|
+
|---|---:|---:|---:|
|
|
43
|
+
| `highlight()` called again in the same process, 20 KB - 1.2 MB files | 5 - 1,600 ms | 0.2 - 53 ms | 12 - 75x |
|
|
44
|
+
| First `highlight()` of a process, same files | 14 - 1,588 ms | 0.3 - 52 ms | 7 - 54x |
|
|
45
|
+
| 4,226 files up to 20 KB through `pygments.highlight` after `install()`, highlight loop | 32.55 s | 2.23 s | 14.6x |
|
|
46
|
+
| Same, whole process | 33.41 s | 3.31 s | 10.1x |
|
|
47
|
+
| One file, whole process from start to exit, 20 KB - 1.2 MB | 0.14 - 1.7 s | 0.09 - 0.18 s | 1.1 - 9.5x |
|
|
48
|
+
| Django docs with Sphinx (4,199 code blocks) | 109.5 s | 100.7 s | within noise |
|
|
49
|
+
|
|
50
|
+
The Sphinx row is the honest one for documentation builds: highlighting was 3.8 s of a 113 s build, and it dropped to 1.8 s. Sphinx spends its time elsewhere.
|
|
51
|
+
|
|
52
|
+
The first use of each lexer costs a little more than in Pygments (median 3.8 ms against 2.7 ms over 361 lexers; a few very large lexers take 100-300 ms), so a short run that touches many languages a few times each can come out slower.
|
|
53
|
+
|
|
54
|
+
Windows 11, i7-9700, CPython 3.11-3.12, base and rustgments alternating. Raw results are in [`bench/results`](bench/results).
|
|
55
|
+
|
|
56
|
+
## What runs natively
|
|
57
|
+
|
|
58
|
+
363 of Pygments' 602 lexers, including Python, JavaScript, TypeScript, JSON, Bash, Rust, Go, Java, Kotlin, HTML, CSS, XML, TOML, INI, Dockerfile, PowerShell, Perl and diff. **C, C++, YAML, Markdown, SQL, Ruby, PHP and Swift still run on Pygments** for now.
|
|
59
|
+
|
|
60
|
+
A lexer stays on Pygments when it isn't a plain `RegexLexer` table (hand-written lexers, custom callbacks, overridden tokenizing), when Pygments has no test files to check it against, or when lexer options change its behaviour. `lexer.backend` tells you which one you got, and `lexer.reason` says why:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
>>> rustgments.get_lexer_by_name('c').backend
|
|
64
|
+
'fallback'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`HtmlFormatter` runs natively with the options Sphinx and pymdownx use (`cssclass`, `nowrap`, `linenos` variants, `hl_lines`, `wrapcode`, `linespans`, `lineanchors`, `noclasses`, `style`, ...). Other formatters, and `tagsfile`, are plain Pygments.
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
Python 3.9 - 3.14 and **Pygments 2.21.0**. The tables are generated from that exact release, and Python's `\w`, `\d`, `\s` follow the interpreter's Unicode version, so there is one table set per Unicode version. With any other Pygments, or a Python it has no tables for, everything runs on Pygments: correct, just not faster.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
pip install rustgments
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Wheels are built for Windows, Linux (glibc and musl, x86_64 and aarch64) and macOS (x86_64 and arm64), for any Python from 3.9. Elsewhere pip builds from source, which needs a Rust toolchain.
|
|
80
|
+
|
|
81
|
+
## Using it with existing tools
|
|
82
|
+
|
|
83
|
+
| | |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Your own code | `rustgments.install()`; `rustgments.uninstall()` restores Pygments |
|
|
86
|
+
| Sphinx | `extensions = ['rustgments.sphinxext']` |
|
|
87
|
+
| MkDocs | `plugins: [rustgments]` |
|
|
88
|
+
| Anything else | set `RUSTGMENTS_INSTALL=1` |
|
|
89
|
+
|
|
90
|
+
The environment variable works through a one-line `.pth` file in the wheel. It runs at every Python startup, and all it does is check that variable. Delete `rustgments-autoinstall.pth` from `site-packages` if you'd rather not have it.
|
|
91
|
+
|
|
92
|
+
`install()` patches `get_tokens`, `get_tokens_unprocessed` of the native lexers and the per-token loop of `HtmlFormatter`. Line numbers, highlighted lines, anchors and wrapping still run Pygments' own code.
|
|
93
|
+
|
|
94
|
+
## How parity is checked
|
|
95
|
+
|
|
96
|
+
Parity is the point of this project, so a lexer only runs natively after passing a gate on every supported Python:
|
|
97
|
+
|
|
98
|
+
- `tests/gate.py`: each lexer's own Pygments test files (`tests/examplefiles`, `tests/snippets`) plus 30 generated mutants, comparing preprocessing, every `(offset, token, value)` and the final HTML. Any mismatch, engine error or timeout keeps that lexer on Pygments. The result is [`build_ir/native_gate.json`](build_ir/native_gate.json).
|
|
99
|
+
- `tests/parity.py`: the same comparisons over a corpus of real code (5,171 files, 88 MB, for the five core lexers).
|
|
100
|
+
- `tests/independent_check.py`: a second checker using only the public API, on other files and random mutants.
|
|
101
|
+
- `tests/dropin_check.py`: Pygments' API before and after `install()`, with 23 formatter and lexer call shapes.
|
|
102
|
+
- `tests/unicode_check.py`: the 9,917 characters whose classes changed between Unicode versions, inserted into real code.
|
|
103
|
+
|
|
104
|
+
The corpus isn't in this repository. Point `RUSTGMENTS_CORPUS` at a directory with `python/`, `json/`, `js/`, `bash/` and `rust/` subdirectories, and `RUSTGMENTS_PYGMENTS_SRC` at a checkout of Pygments 2.21.0.
|
|
105
|
+
|
|
106
|
+
CI builds every wheel and runs Pygments' own test inputs against it.
|
|
107
|
+
|
|
108
|
+
## How it works
|
|
109
|
+
|
|
110
|
+
A build step imports Pygments and serializes each lexer's processed token tables, so `include`, `inherit`, `words()` and friends are already resolved. Every pattern is re-emitted from Python's own regex parse tree into Rust syntax, with Python's semantics spelled out (`\w`, `\d`, `\s`, `\Z`, `$`, flags). At run time the engine matches with DFAs where it can and falls back to [fancy-regex](https://github.com/fancy-regex/fancy-regex) for look-around and back-references, following `RegexLexer.get_tokens_unprocessed` step by step.
|
|
111
|
+
|
|
112
|
+
`build_ir/` regenerates the tables in `ir/`, which are checked in.
|
|
113
|
+
|
|
114
|
+
## Status
|
|
115
|
+
|
|
116
|
+
Early. Version 0.1.0 tracks Pygments 2.21.0 only, and the API is the small part of Pygments listed above. Please open an issue if the output ever differs from Pygments: that's the bug this project cares about most.
|
|
117
|
+
|
|
118
|
+
Prior art: the `pygmentsrs` crate inside [westurner/dsport](https://github.com/westurner/dsport) takes a similar approach for a docutils port. No code from it is used here.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
BSD-2-Clause, see [LICENSE](LICENSE). The generated tables are derived from Pygments ([LICENSE-pygments](LICENSE-pygments)). Rust crate licenses are in [LICENSE-third-party.txt](LICENSE-third-party.txt).
|
|
123
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
rustgments/__init__.py,sha256=cZbmRC8KbgZ-fCs0FWahZUS0Bixc6i-FFx-K2TGVZLo,7559
|
|
2
|
+
rustgments/_dropin.py,sha256=-6dncL7O_xh_cG8H7NtqlXV2yD_S2SzLb3usM-ZQRz8,11065
|
|
3
|
+
rustgments/_native.pyd,sha256=l8QcSmwalN3F4hlu8OjLO5lkmFVzMCxgHZQyoVnQfTk,33523712
|
|
4
|
+
rustgments/mkdocs_plugin.py,sha256=FCNEVYoKyVkyIxl8SOg1StCdR_wQ58kj3KB-meJkkU8,412
|
|
5
|
+
rustgments/sphinxext.py,sha256=P9evfxdGeq-W37FVIQATxIwwJLzJCuTNBm_Ji3y2uU8,382
|
|
6
|
+
rustgments-0.1.0.dist-info/METADATA,sha256=mSnBwBVc44KyVO3rMZILqFfO51u36vSIKcoF7-nJdJ0,7358
|
|
7
|
+
rustgments-0.1.0.dist-info/WHEEL,sha256=xw8F9xBWUOgm5ym2S9tihj_bLJOb723bjhA0kdANo2I,95
|
|
8
|
+
rustgments-0.1.0.dist-info/entry_points.txt,sha256=wSditpSfhXlH_zcvi9_Cz0jTU1N2vO7HI3nDeodPTj0,70
|
|
9
|
+
rustgments-0.1.0.dist-info/licenses/LICENSE,sha256=AXgZCU-0YJGXNge_PnwcDXRg8SLI6P5cq12PjX9KbOw,1358
|
|
10
|
+
rustgments-0.1.0.dist-info/licenses/LICENSE-pygments,sha256=-BhIrkxwyHKSLs6c7ElBh-T83z7MIECFfWXgljWtsCo,1356
|
|
11
|
+
rustgments-0.1.0.dist-info/licenses/LICENSE-third-party.txt,sha256=yddUo8trEbt_V0ji3TEdhD_kgZuzoTgMzTUD4BkdQ00,29194
|
|
12
|
+
rustgments-0.1.0.dist-info/sboms/rustgments.cyclonedx.json,sha256=OWgOZkBy2etjCvQlpw_qC9OoTRYEKPKVhg9K8Dum22c,43111
|
|
13
|
+
rustgments-autoinstall.pth,sha256=DhAh9CaC5dXH5YaLEM8ID9gJowSpU_tS-oJfpMartKw,95
|
|
14
|
+
rustgments-0.1.0.dist-info/RECORD,,
|