Skip to content

Commit d2a30c2

Browse files
committed
DOCSP-4337: parse code-block into a 'code' node
1 parent 189e193 commit d2a30c2

File tree

9 files changed

+132
-20
lines changed

9 files changed

+132
-20
lines changed

snooty.idl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ interface HTML <: Literal {
117117
interface Code <: Literal {
118118
type: "code";
119119
lang: string?;
120-
meta: string?;
120+
copyable: boolean;
121+
emphasize_lines: [(int, int)];
121122
}
122123

123124
interface Definition <: Node {

snooty/gizaparser/steps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def render(self, parse_rst: EmbeddedRstParser) -> List[SerializableType]:
4141
nodes_to_append_children.append({
4242
'type': 'code',
4343
'lang': self.language,
44-
'copyable': False if self.copyable is None else self.copyable,
44+
'copyable': True if self.copyable is None else self.copyable,
4545
'position': {'start': {'line': self.line}},
4646
'value': self.code
4747
})

snooty/gizaparser/test_steps.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def create_page() -> Tuple[Page, EmbeddedRstParser]:
6767
'</heading><paragraph><text>hi</text></paragraph>',
6868
'<paragraph><text>You can install either the latest stable version of MongoDB ',
6969
'or a\nspecific version of MongoDB.</text></paragraph>',
70-
'<directive name="code-block">',
71-
'<text>sh</text><literal></literal></directive><paragraph><text>bye</text></paragraph>',
70+
'<code lang="sh" copyable="True">',
71+
'echo "mongodb-org hold" | sudo dpkg --set-selections',
72+
'</code><paragraph><text>bye</text></paragraph>',
7273
'</section></directive></directive>'
7374
))

snooty/parser.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ def dispatch_visit(self, node: docutils.nodes.Node) -> None:
6767
value = node.children[1].astext()
6868
self.state[-1].setdefault('options', {})[key] = value
6969
raise docutils.nodes.SkipNode()
70+
elif node_name == 'code':
71+
doc['type'] = 'code'
72+
doc['lang'] = node['lang']
73+
doc['copyable'] = node['copyable']
74+
if node['emphasize_lines']:
75+
doc['emphasize_lines'] = node['emphasize_lines']
76+
doc['value'] = node.astext()
77+
self.state[-1]['children'].append(doc)
78+
raise docutils.nodes.SkipNode()
7079

7180
self.state.append(doc)
7281

@@ -75,9 +84,6 @@ def dispatch_visit(self, node: docutils.nodes.Node) -> None:
7584
doc['value'] = str(node)
7685
return
7786

78-
# Most, but not all, nodes have children nodes
79-
make_children = True
80-
8187
if node_name == 'directive':
8288
self.handle_directive(node, doc)
8389
elif node_name == 'role':
@@ -103,13 +109,8 @@ def dispatch_visit(self, node: docutils.nodes.Node) -> None:
103109
doc['type'] = 'listItem'
104110
elif node_name == 'title':
105111
doc['type'] = 'heading'
106-
elif node_name == 'FixedTextElement':
107-
doc['type'] = 'literal'
108-
doc['value'] = node.astext()
109-
make_children = False
110112

111-
if make_children:
112-
doc['children'] = []
113+
doc['children'] = []
113114

114115
def dispatch_departure(self, node: docutils.nodes.Node) -> None:
115116
node_name = node.__class__.__name__

snooty/rstparser.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
PAT_EXPLICIT_TILE = re.compile(r'^(?P<label>.+?)\s*(?<!\x00)<(?P<target>.*?)>$', re.DOTALL)
2121
PAT_WHITESPACE = re.compile(r'^\x20*')
2222
PAT_BLOCK_HAS_ARGUMENT = re.compile(r'^\x20*\.\.\x20[^\s]+::\s*\S+')
23-
SPECIAL_DIRECTIVES = {'code-block', 'include', 'only'}
2423

2524

2625
@checked
@@ -38,6 +37,10 @@ class LegacyTabsDefinition(nodes.Node):
3837
tabs: List[LegacyTabDefinition]
3938

4039

40+
def option_bool(argument: str) -> Any:
41+
return docutils.parsers.rst.directives.choice(argument, ('true', 'false', None))
42+
43+
4144
class directive_argument(docutils.nodes.General, docutils.nodes.TextElement):
4245
pass
4346

@@ -48,6 +51,10 @@ def __init__(self, name: str) -> None:
4851
self['name'] = name
4952

5053

54+
class code(docutils.nodes.General, docutils.nodes.FixedTextElement):
55+
pass
56+
57+
5158
class role(docutils.nodes.General, docutils.nodes.Inline, docutils.nodes.Element):
5259
def __init__(self, name: str, rawtext: str, text: str, lineno: int) -> None:
5360
super(role, self).__init__()
@@ -131,6 +138,22 @@ def parse_options(block_text: str) -> Dict[str, str]:
131138
return kv
132139

133140

141+
def parse_linenos(term: str, max_val: int) -> List[Tuple[int, int]]:
142+
"""Parse a comma-delimited list of line numbers and ranges."""
143+
results: List[Tuple[int, int]] = []
144+
for term in (term for term in term.split(',') if term.strip()):
145+
parts = term.split('-', 1)
146+
lower = int(parts[0])
147+
higher = int(parts[1]) if len(parts) == 2 else lower
148+
149+
if lower < 0 or lower > max_val or higher < 0 or higher > max_val or lower > higher:
150+
raise ValueError(f'Invalid line number specification: {term}')
151+
152+
results.append((lower, higher))
153+
154+
return results
155+
156+
134157
class Directive(docutils.parsers.rst.Directive):
135158
optional_arguments = 1
136159
final_argument_whitespace = True
@@ -163,7 +186,7 @@ def run(self) -> List[docutils.nodes.Node]:
163186
node.append(argument)
164187

165188
# Parse the content
166-
if self.name in SPECIAL_DIRECTIVES:
189+
if self.name in {'include', 'only'}:
167190
raw = docutils.nodes.FixedTextElement()
168191
raw.document = self.state.document
169192
raw.source, raw.line = source, line
@@ -195,9 +218,8 @@ def prepare_viewlist(text: str, ignore: int = 1) -> List[str]:
195218
class TabsDirective(Directive):
196219
option_spec = {
197220
'tabset': str,
198-
'hidden': bool
221+
'hidden': option_bool
199222
}
200-
has_content = True
201223

202224
def run(self) -> List[docutils.nodes.Node]:
203225
# Transform the old YAML-based syntax into the new pure-rst syntax.
@@ -262,6 +284,38 @@ def make_tab_node(self, source: str, child: LegacyTabDefinition) -> docutils.nod
262284
return node
263285

264286

287+
class CodeDirective(Directive):
288+
required_arguments = 1
289+
optional_arguments = 0
290+
option_spec = {
291+
'copyable': option_bool,
292+
'emphasize-lines': str
293+
}
294+
295+
def run(self) -> List[docutils.nodes.Node]:
296+
source, line = self.state_machine.get_source_and_line(self.lineno)
297+
copyable = 'copyable' not in self.options or self.options['copyable'] == 'true'
298+
299+
try:
300+
n_lines = len(self.content)
301+
emphasize_lines = parse_linenos(self.options.get('emphasize-lines', ''), n_lines)
302+
except ValueError as err:
303+
error_node = self.state.document.reporter.error(
304+
str(err),
305+
line=self.lineno)
306+
return [error_node]
307+
308+
value = '\n'.join(self.content)
309+
node = code(value, value)
310+
node['name'] = 'code'
311+
node['lang'] = self.arguments[0]
312+
node['copyable'] = copyable
313+
node['emphasize_lines'] = emphasize_lines
314+
node.document = self.state.document
315+
node.source, node.line = source, line
316+
return [node]
317+
318+
265319
def handle_role(typ: str, rawtext: str, text: str,
266320
lineno: int, inliner: object,
267321
options: Dict[str, object] = {},
@@ -275,6 +329,9 @@ def lookup_directive(directive_name: str, language_module: object,
275329
if directive_name.startswith('tabs'):
276330
return TabsDirective, []
277331

332+
if directive_name in {'code-block', 'sourcecode'}:
333+
return CodeDirective, []
334+
278335
return Directive, []
279336

280337

snooty/test_parser.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pathlib import Path
22
from . import rstparser
33
from .util import ast_to_testing_string
4-
from .types import ProjectConfig
4+
from .types import Diagnostic, ProjectConfig
55
from .parser import parse_rst, JSONVisitor
66

77

@@ -36,3 +36,48 @@ def test_tabs() -> None:
3636
assert len(diagnostics) == 1 and \
3737
diagnostics[0].message.startswith('Unexpected field') and \
3838
diagnostics[0].start[0] == 36
39+
40+
41+
def test_codeblock() -> None:
42+
root = Path('test_data')
43+
tabs_path = Path(root).joinpath(Path('test.rst'))
44+
project_config = ProjectConfig(root, '')
45+
parser = rstparser.Parser(project_config, JSONVisitor)
46+
47+
# Test a simple code-block
48+
page, diagnostics = parse_rst(parser, tabs_path, '''
49+
.. code-block:: sh
50+
51+
foo bar
52+
indented
53+
end''')
54+
assert diagnostics == []
55+
assert ast_to_testing_string(page.ast) == ''.join((
56+
'<root>',
57+
'<code lang="sh" copyable="True">foo bar\n indented\nend</code>'
58+
'</root>'
59+
))
60+
61+
# Test parsing of emphasize-lines
62+
page, diagnostics = parse_rst(parser, tabs_path, '''
63+
.. code-block:: sh
64+
:copyable: false
65+
:emphasize-lines: 1, 2-3
66+
67+
foo
68+
bar
69+
baz''')
70+
assert diagnostics == []
71+
assert ast_to_testing_string(page.ast) == ''.join((
72+
'<root>',
73+
'<code lang="sh" emphasize_lines="[(1, 1), (2, 3)]">foo\nbar\nbaz</code>'
74+
'</root>'
75+
))
76+
77+
# Test handling of out-of-range lines
78+
page, diagnostics = parse_rst(parser, tabs_path, '''
79+
.. code-block:: sh
80+
:emphasize-lines: 10
81+
82+
foo''')
83+
assert diagnostics[0].severity == Diagnostic.Level.warning

stubs/docutils/nodes.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ class SkipNode(TreePruningException): ...
6363

6464

6565
class system_message(Element): ...
66+
67+
68+
class Text(Node):
69+
def __init__(self, data: object, rawsource: str=''): ...
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from typing import Any, List, Type, Tuple
1+
from typing import Any, Collection, List, Type, Tuple
22
import docutils.nodes
33

44

55
def directive(directive_name: str, language_module: object, document: docutils.nodes.document) -> Tuple[Type[Any], List[object]]: ...
6+
7+
def choice(argument: str, values: Collection[object]) -> Any: ...

stubs/docutils/statemachine.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class ViewList:
99
def __iter__(self) -> Iterator[str]: ...
1010

1111

12-
class StringList(ViewList): ...
12+
class StringList(ViewList):
13+
def __len__(self) -> int: ...
1314

1415

1516
class State:

0 commit comments

Comments
 (0)