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 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
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.16.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ rustgments=rustgments.mkdocs_plugin:RustgmentsPlugin