1
1
from __future__ import annotations
2
2
3
3
import shutil
4
+ import sys
4
5
import typing as t
5
6
from functools import reduce
6
7
from pathlib import Path
7
8
8
- from .core import Context , Step
9
+ from .core import Step
9
10
from .dependencies import pip_dependency , Dependency
10
11
11
12
if t .TYPE_CHECKING :
13
+ from collections .abc import Sequence
12
14
from jinja2 import Environment
13
-
15
+ from markdown_it .renderer import RendererHTML
16
+ from markdown_it .token import Token
17
+ from markdown_it .utils import EnvType , OptionsDict
14
18
15
19
MDProcessor = t .Callable [[str ], str ]
20
+ MDContainerRenderer = t .Callable [
21
+ ['RendererHTML' , 'Sequence[Token]' , int , 'OptionsDict' , 'EnvType' ],
22
+ str
23
+ ]
16
24
17
25
18
26
class JinjaRenderStep (Step ):
19
27
"""
20
28
Abstract base class for Steps using Jinja rendering.
21
29
"""
22
30
encoding = 'utf-8'
23
- env : Environment
24
31
25
32
@classmethod
26
33
def get_dependencies (cls ):
@@ -31,26 +38,24 @@ def get_dependencies(cls):
31
38
def __init__ (self ,
32
39
env : Environment | None = None ,
33
40
extra_globals : dict [str , t .Any ] | None = None ):
34
- self ._temporary_env = env
41
+ if env and extra_globals :
42
+ env .globals .update (extra_globals )
43
+ self ._env = env
35
44
self ._extra_globals = extra_globals
36
45
37
- def bind (self , context : Context ):
38
- """
39
- Bind this Step to a specific context. Also initializes a Jinja
40
- environment if none is set up already.
41
- """
42
- super ().bind (context )
43
-
44
- if self ._temporary_env :
45
- self .env = self ._temporary_env
46
- else :
47
- from jinja2 import Environment , FileSystemLoader , select_autoescape
48
- self .env = Environment (
49
- loader = FileSystemLoader (context ['input_dir' ]),
50
- autoescape = select_autoescape ()
51
- )
46
+ @property
47
+ def env (self ):
48
+ if self ._env :
49
+ return self ._env
50
+
51
+ from jinja2 import Environment , FileSystemLoader , select_autoescape
52
+ self ._env = Environment (
53
+ loader = FileSystemLoader (self .context ['input_dir' ]),
54
+ autoescape = select_autoescape ()
55
+ )
52
56
if self ._extra_globals :
53
- self .env .globals .update (self ._extra_globals )
57
+ self ._env .globals .update (self ._extra_globals )
58
+ return self ._env
54
59
55
60
def render_template (self , template_name : str , meta : dict [str , t .Any ], output_paths : list [Path ]):
56
61
"""
@@ -184,3 +189,135 @@ def extract_metadata(self, text: str):
184
189
i += 1
185
190
186
191
return meta , '\n ' .join (lines [i :])
192
+
193
+
194
+ class JinjaExtendedMarkdownStep (JinjaRenderStep ):
195
+ encoding = 'utf-8'
196
+
197
+ @classmethod
198
+ def get_dependencies (cls ):
199
+ deps = super ().get_dependencies () | {
200
+ pip_dependency ('markdown-it-py' , check_name = 'markdown_it' ),
201
+ pip_dependency ('mdit_py_plugins' ),
202
+ pip_dependency ('Pygments' , check_name = 'pygments' ),
203
+ }
204
+ if sys .version_info < (3 , 11 ):
205
+ deps .add (pip_dependency ('tomli' ))
206
+ return deps
207
+
208
+ def __init__ (self ,
209
+ default_template : str | None = None ,
210
+ jinja_env : Environment | None = None ,
211
+ jinja_globals : dict [str , t .Any ] | None = None ,
212
+ * ,
213
+ container_types : list [tuple [str | None , list [str ]]] | None = None ,
214
+ container_renderers : dict [str , MDContainerRenderer ] | None = None ,
215
+ substitutions : dict [str , str ] | None = None ,
216
+ auto_anchors : bool = False ,
217
+ auto_typography : bool = True ,
218
+ code_highlighting : bool = True ,
219
+ pygments_params : dict [str , t .Any ] | None = None ,
220
+ wordcount : bool = False ):
221
+ super ().__init__ (jinja_env , jinja_globals )
222
+ self .default_template = default_template
223
+ self .container_types = container_types or []
224
+ self .container_renderers = container_renderers or {}
225
+ self .substitutions = substitutions or {}
226
+ self .auto_anchors = auto_anchors
227
+ self .auto_typography = auto_typography
228
+ self .code_highlighting = code_highlighting
229
+ self .pygments_params = pygments_params or {}
230
+ self .wordcount = wordcount
231
+ self ._md_processor : t .Callable [[str ], tuple [str , dict [str , t .Any ]]] | None = None
232
+
233
+ def __call__ (self , path : Path , output_paths : list [Path ]):
234
+ md , meta = self .md_processor (
235
+ self .apply_substitutions (
236
+ path .read_text (self .encoding ).strip ()
237
+ )
238
+ )
239
+
240
+ meta ['rendered_markdown' ] = md
241
+
242
+ template_path = self .render_template (
243
+ meta .get ('template' , self .default_template ),
244
+ meta ,
245
+ output_paths
246
+ )
247
+ if template_path :
248
+ return [path , Path (template_path )], output_paths
249
+
250
+ @property
251
+ def md_processor (self ):
252
+ if not self ._md_processor :
253
+ self ._md_processor = self ._build_processor ()
254
+ return self ._md_processor
255
+
256
+ def apply_substitutions (self , text : str ):
257
+ for sub , value in self .substitutions .items ():
258
+ text = text .replace ('${{ ' + sub + ' }}' , value )
259
+ return text
260
+
261
+ def highlight_code (self , code : str , lang : str , lang_attrs : str ):
262
+ from pygments import highlight
263
+ from pygments .formatters import HtmlFormatter
264
+ from pygments .lexers import get_lexer_by_name , guess_lexer
265
+ from pygments .util import ClassNotFound
266
+ try :
267
+ lexer = get_lexer_by_name (lang )
268
+ except ClassNotFound :
269
+ try :
270
+ lexer = guess_lexer (code )
271
+ except ClassNotFound :
272
+ return ''
273
+
274
+ return highlight (code , lexer , HtmlFormatter (** self .pygments_params ))
275
+
276
+ def _build_processor (self ):
277
+ import markdown_it
278
+ # TODO Need for pyright suppression will be eliminated in the next
279
+ # release of mdit_py_plugins:
280
+ # https://github.com/executablebooks/mdit-py-plugins/pull/91
281
+ from mdit_py_plugins .anchors import anchors_plugin # type: ignore[reportPrivateImportUsage]
282
+ from mdit_py_plugins .attrs import attrs_block_plugin , attrs_plugin # type: ignore[reportPrivateImportUsage]
283
+ from mdit_py_plugins .container import container_plugin # type: ignore[reportPrivateImportUsage]
284
+ from mdit_py_plugins .front_matter import front_matter_plugin # type: ignore[reportPrivateImportUsage]
285
+ from mdit_py_plugins .wordcount import wordcount_plugin # type: ignore[reportPrivateImportUsage]
286
+ from .components import md_rendering
287
+
288
+ processor = markdown_it .MarkdownIt (
289
+ 'commonmark' ,
290
+ {
291
+ 'typographer' : self .auto_typography ,
292
+ 'highlight' : self .highlight_code if self .code_highlighting else None ,
293
+ },
294
+ renderer_cls = md_rendering .AnchovyRendererHTML
295
+ )
296
+ processor .enable (['strikethrough' , 'table' ])
297
+ if self .auto_typography :
298
+ processor .enable (['smartquotes' , 'replacements' ])
299
+ if self .auto_anchors :
300
+ anchors_plugin (processor )
301
+ attrs_plugin (processor )
302
+ attrs_block_plugin (processor )
303
+ front_matter_plugin (processor )
304
+ if self .wordcount :
305
+ wordcount_plugin (processor )
306
+
307
+ for tag , names in self .container_types :
308
+ for name in names :
309
+ renderer = (
310
+ self .container_renderers .get (name )
311
+ or (md_rendering .get_container_renderer (name , tag ) if tag else None )
312
+ )
313
+ container_plugin (processor , name , render = renderer )
314
+
315
+ def convert (md_string : str ):
316
+ env = {'anchovy_meta' : dict [str , t .Any ]()}
317
+ md : str = processor .render (md_string , env = env )
318
+ meta = env ['anchovy_meta' ]
319
+ if self .wordcount :
320
+ meta ['wordcount' ] = env ['wordcount' ]
321
+ return md , meta
322
+
323
+ return convert
0 commit comments