Skip to content

Commit 581eef1

Browse files
committed
update renderer
1 parent 765dfd2 commit 581eef1

File tree

2 files changed

+239
-74
lines changed

2 files changed

+239
-74
lines changed

markdown_it/_doc_renderer.py

Lines changed: 150 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
"""NOTE: this will eventually be moved out of core"""
22
from contextlib import contextmanager
33
import json
4+
import sys
45
from typing import List
56

67
import yaml
78

89
from docutils import nodes
910
from docutils.frontend import OptionParser
1011

11-
# from docutils.languages import get_language
12-
# from docutils.parsers.rst import directives, Directive, DirectiveError, roles
12+
from docutils.languages import get_language
13+
from docutils.parsers.rst import roles # directives, Directive, DirectiveError, roles
1314
from docutils.parsers.rst import Parser as RSTParser
1415

1516
# from docutils.parsers.rst.directives.misc import Include
16-
# from docutils.parsers.rst.states import RSTStateMachine, Body, Inliner
17+
from docutils.parsers.rst.states import Inliner # RSTStateMachine, Body
18+
1719
# from docutils.statemachine import StringList
18-
from docutils.utils import new_document, Reporter # noqa
20+
from docutils.utils import new_document, Reporter
1921

2022
from markdown_it.token import Token, nest_tokens
23+
from markdown_it.utils import AttrDict
24+
from markdown_it.common.utils import escapeHtml
2125

2226

2327
def make_document(source_path="notset") -> nodes.document:
@@ -31,20 +35,41 @@ class DocRenderer:
3135

3236
def __init__(self, options=None, env=None):
3337
self.options = options or {}
34-
self.env = env or {}
38+
self.env = env or AttrDict()
3539
self.rules = {
3640
k: v
3741
for k, v in self.__class__.__dict__.items()
3842
if k.startswith("render_") and k != "render_children"
3943
}
4044
self.document = make_document()
45+
self.reporter = self.document.reporter # type: Reporter
4146
self.current_node = self.document
47+
self.language_module = self.document.settings.language_code # type: str
48+
get_language(self.language_module)
49+
# TODO merge these with self.env?
4250
self.config = {}
4351
self._level_to_elem = {0: self.document}
4452

45-
def run_render(self, tokens: List[Token]):
53+
def run_render(self, tokens: List[Token], env: AttrDict):
54+
"""Run the render on a token stream.
55+
56+
:param tokens: the token stream
57+
:param env: the environment sandbox associated with the tokens,
58+
containing additional metadata like reference info
59+
"""
60+
self.env = env
61+
last_map = None
62+
# propagate line number down to inline elements
63+
for token in tokens:
64+
if token.map:
65+
last_map = token.map
66+
elif last_map:
67+
token.meta["parent_line"] = last_map[0]
68+
for child in token.children or []:
69+
child.meta["parent_line"] = last_map[0]
4670
tokens = nest_tokens(tokens)
4771
for i, token in enumerate(tokens):
72+
# skip hidden?
4873
if f"render_{token.type}" in self.rules:
4974
self.rules[f"render_{token.type}"](self, token)
5075
else:
@@ -113,7 +138,7 @@ def renderInlineAsText(self, tokens: List[Token]) -> str:
113138

114139
return result
115140

116-
# ### render methods for tokens
141+
# ### render methods for commonmark tokens
117142

118143
def render_paragraph_open(self, token):
119144
para = nodes.paragraph("")
@@ -133,6 +158,12 @@ def render_bullet_list_open(self, token):
133158
with self.current_node_context(list_node, append=True):
134159
self.render_children(token)
135160

161+
def render_ordered_list_open(self, token):
162+
list_node = nodes.enumerated_list()
163+
self.add_line_and_source_path(list_node, token)
164+
with self.current_node_context(list_node, append=True):
165+
self.render_children(token)
166+
136167
def render_list_item_open(self, token):
137168
item_node = nodes.list_item()
138169
self.add_line_and_source_path(item_node, token)
@@ -220,6 +251,7 @@ def render_heading_open(self, token):
220251
def render_link_open(self, token):
221252
# TODO I think this is maybe already handled at this point?
222253
# refuri = escape_url(http://github.com/token.target)
254+
# TODO identify cross-references
223255
refuri = target = token.attrGet("href")
224256
ref_node = nodes.reference(target, target, refuri=refuri)
225257
self.add_line_and_source_path(ref_node, token)
@@ -240,6 +272,8 @@ def render_image(self, token):
240272

241273
self.current_node.append(img_node)
242274

275+
# ### render methods for plugin tokens
276+
243277
def render_front_matter(self, token):
244278
"""Pass document front matter data
245279
@@ -267,6 +301,45 @@ def render_front_matter(self, token):
267301
docinfo = dict_to_docinfo(data)
268302
self.current_node.append(docinfo)
269303

304+
def render_math_inline(self, token):
305+
content = token.content
306+
node = nodes.math(content, content)
307+
self.add_line_and_source_path(node, token)
308+
self.current_node.append(node)
309+
310+
def render_math_block(self, token):
311+
content = token.content
312+
node = nodes.math_block(content, content, nowrap=False, number=None)
313+
self.add_line_and_source_path(node, token)
314+
self.current_node.append(node)
315+
316+
def render_footnote_ref(self, token):
317+
"""Footnote references are added as auto-numbered,
318+
.i.e. `[^a]` is read as rST `[#a]_`
319+
"""
320+
# TODO we now also have ^[a] the inline version (currently disabled)
321+
# that would be rendered here
322+
target = token.meta["label"]
323+
refnode = nodes.footnote_reference("[^{}]".format(target))
324+
self.add_line_and_source_path(refnode, token)
325+
refnode["auto"] = 1
326+
refnode["refname"] = target
327+
# refnode += nodes.Text(token.target)
328+
self.document.note_autofootnote_ref(refnode)
329+
self.document.note_footnote_ref(refnode)
330+
self.current_node.append(refnode)
331+
332+
def render_footnote_reference_open(self, token):
333+
target = token.meta["label"]
334+
footnote = nodes.footnote()
335+
self.add_line_and_source_path(footnote, token)
336+
footnote["names"].append(target)
337+
footnote["auto"] = 1
338+
self.document.note_autofootnote(footnote)
339+
self.document.note_explicit_target(footnote, footnote)
340+
with self.current_node_context(footnote, append=True):
341+
self.render_children(token)
342+
270343
def render_myst_block_break(self, token):
271344
block_break = nodes.comment(token.content, token.content)
272345
block_break["classes"] += ["block_break"]
@@ -282,14 +355,33 @@ def render_myst_target(self, token):
282355
self.document.note_explicit_target(target, self.current_node)
283356
self.current_node.append(target)
284357

285-
def render_myst_role(self, token):
358+
def render_myst_line_comment(self, token):
359+
self.current_node.append(nodes.comment(token.content, token.content))
286360

361+
def render_myst_role(self, token):
287362
name = token.meta["name"]
288-
# TODO representing as literal for place-holder
289-
content = f":{name}:`{token.content}`"
290-
node = nodes.literal(content, content)
291-
self.add_line_and_source_path(node, token)
292-
self.current_node.append(node)
363+
text = escapeHtml(token.content) # TODO check this
364+
rawsource = f":{name}:`{token.content}`"
365+
lineno = token.meta.get("parent_line", 0)
366+
role_func, messages = roles.role(
367+
name, self.language_module, lineno, self.reporter
368+
)
369+
inliner = MockInliner(self, lineno)
370+
if role_func:
371+
nodes, messages2 = role_func(name, rawsource, text, lineno, inliner)
372+
# return nodes, messages + messages2
373+
self.current_node += nodes
374+
else:
375+
message = self.reporter.error(
376+
'Unknown interpreted text role "{}".'.format(name), line=lineno
377+
)
378+
problematic = inliner.problematic(text, rawsource, message)
379+
self.current_node += problematic
380+
381+
# # TODO representing as literal for place-holder
382+
# node = nodes.literal(rawsource, rawsource)
383+
# self.add_line_and_source_path(node, token)
384+
# self.current_node.append(node)
293385

294386
# def render_table_open(self, token):
295387
# # print(token)
@@ -326,3 +418,48 @@ def dict_to_docinfo(data):
326418
field_node += nodes.field_body(value, nodes.Text(value, value))
327419
docinfo += field_node
328420
return docinfo
421+
422+
423+
class MockingError(Exception):
424+
"""An exception to signal an error during mocking of docutils components."""
425+
426+
427+
class MockInliner:
428+
"""A mock version of `docutils.parsers.rst.states.Inliner`.
429+
430+
This is parsed to role functions.
431+
"""
432+
433+
def __init__(self, renderer: DocRenderer, lineno: int):
434+
self._renderer = renderer
435+
self.document = renderer.document
436+
self.reporter = renderer.document.reporter
437+
if not hasattr(self.reporter, "get_source_and_line"):
438+
# TODO this is called by some roles,
439+
# but I can't see how that would work in RST?
440+
self.reporter.get_source_and_line = lambda l: (self.document["source"], l)
441+
self.parent = renderer.current_node
442+
self.language = renderer.language_module
443+
self.rfc_url = "rfc%d.html"
444+
445+
def problematic(self, text: str, rawsource: str, message: nodes.system_message):
446+
msgid = self.document.set_id(message, self.parent)
447+
problematic = nodes.problematic(rawsource, rawsource, refid=msgid)
448+
prbid = self.document.set_id(problematic)
449+
message.add_backref(prbid)
450+
return problematic
451+
452+
# TODO add parse method
453+
454+
def __getattr__(self, name):
455+
"""This method is only be called if the attribute requested has not
456+
been defined. Defined attributes will not be overridden.
457+
"""
458+
# TODO use document.reporter mechanism?
459+
if hasattr(Inliner, name):
460+
msg = "{cls} has not yet implemented attribute '{name}'".format(
461+
cls=type(self).__name__, name=name
462+
)
463+
raise MockingError(msg).with_traceback(sys.exc_info()[2])
464+
msg = "{cls} has no attribute {name}".format(cls=type(self).__name__, name=name)
465+
raise MockingError(msg).with_traceback(sys.exc_info()[2])

0 commit comments

Comments
 (0)