Skip to content

Commit 06e409d

Browse files
committed
🐛 FIX: Source line reporting for nested parsing
Previously, nested parsing of content within directives would not correctly propagate the starting line number of the content.
1 parent 0f2902a commit 06e409d

9 files changed

Lines changed: 61 additions & 16 deletions

File tree

myst_parser/docutils_renderer.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,19 @@ def add_document_wordcount(self) -> None:
262262
self.document.note_substitution_def(substitution_node, f"wordcount-{key}")
263263

264264
def nested_render_text(self, text: str, lineno: int) -> None:
265-
"""Render unparsed text."""
265+
"""Render unparsed text.
266+
267+
:param text: the text to render
268+
:param lineno: the starting line number of the text, within the full source
269+
"""
266270

267271
tokens = self.md.parse(text + "\n", self.md_env)
268272

273+
# update the line numbers
274+
for token in tokens:
275+
if token.map:
276+
token.map = [token.map[0] + lineno, token.map[1] + lineno]
277+
269278
# remove front matter
270279
if tokens and tokens[0].type == "front_matter":
271280
tokens.pop(0)
@@ -1139,7 +1148,7 @@ def run_directive(
11391148
directive_class.option_spec["relative-docs"] = directives.path
11401149

11411150
try:
1142-
arguments, options, body_lines = parse_directive_text(
1151+
arguments, options, body_lines, content_offset = parse_directive_text(
11431152
directive_class, first_line, content
11441153
)
11451154
except DirectiveParsingError as error:
@@ -1175,7 +1184,7 @@ def run_directive(
11751184
# the absolute line number of the first line of the directive
11761185
lineno=position,
11771186
# the line offset of the first line of the content
1178-
content_offset=0, # TODO get content offset from `parse_directive_text`
1187+
content_offset=content_offset,
11791188
# a string containing the entire directive
11801189
block_text="\n".join(body_lines),
11811190
state=state,

myst_parser/mocking.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,14 @@ def parse_directive_block(
114114
if option_presets:
115115
raise MockingError("parse_directive_block: option_presets not implemented")
116116
# TODO should argument_str always be ""?
117-
arguments, options, body_lines = parse_directive_text(
117+
arguments, options, body_lines, content_offset = parse_directive_text(
118118
directive, "", "\n".join(content)
119119
)
120120
return (
121121
arguments,
122122
options,
123123
StringList(body_lines, source=content.source),
124-
line_offset + len(content) - len(body_lines),
124+
line_offset + content_offset,
125125
)
126126

127127
def nested_parse(

myst_parser/parse_directives.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import datetime
3737
import re
3838
from textwrap import dedent
39-
from typing import Any, Callable, Dict, Type
39+
from typing import Any, Callable, Dict, List, Tuple, Type
4040

4141
import yaml
4242
from docutils.parsers.rst import Directive
@@ -54,25 +54,27 @@ def parse_directive_text(
5454
first_line: str,
5555
content: str,
5656
validate_options: bool = True,
57-
):
57+
) -> Tuple[List[str], dict, List[str], int]:
5858
"""Parse (and validate) the full directive text.
5959
6060
:param first_line: The text on the same line as the directive name.
6161
May be an argument or body text, dependent on the directive
6262
:param content: All text after the first line. Can include options.
6363
:param validate_options: Whether to validate the values of options
6464
65+
:returns: (arguments, options, body_lines, content_offset)
6566
"""
6667
if directive_class.option_spec:
6768
body, options = parse_directive_options(
6869
content, directive_class, validate=validate_options
6970
)
71+
body_lines = body.splitlines()
72+
content_offset = len(content.splitlines()) - len(body_lines)
7073
else:
7174
# If there are no possible options, we do not look for a YAML block
7275
options = {}
73-
body = content
74-
75-
body_lines = body.splitlines()
76+
body_lines = content.splitlines()
77+
content_offset = 0
7678

7779
if not (
7880
directive_class.required_arguments
@@ -91,12 +93,13 @@ def parse_directive_text(
9193
# this is to allow space between the options and the content
9294
if body_lines and not body_lines[0].strip():
9395
body_lines = body_lines[1:]
96+
content_offset += 1
9497

9598
# check for body content
9699
if body_lines and not directive_class.has_content:
97100
raise DirectiveParsingError("No content permitted")
98101

99-
return arguments, options, body_lines
102+
return arguments, options, body_lines, content_offset
100103

101104

102105
def parse_directive_options(

tests/test_renderers/fixtures/mock_include_errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ Error in include file:
2020
```{include} bad.md
2121
```
2222
.
23-
tmpdir/bad.md:1: (ERROR/3) Unknown interpreted text role "a".
23+
tmpdir/bad.md:2: (ERROR/3) Unknown interpreted text role "a".
2424
.

tests/test_renderers/fixtures/reporter_warnings.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,19 @@ header nested in admonition
136136
# Header
137137
```
138138
.
139-
<string>:1: (WARNING/2) Header nested in this element can lead to unexpected outcomes
139+
<string>:2: (WARNING/2) Header nested in this element can lead to unexpected outcomes
140+
.
141+
142+
nested parse warning
143+
.
144+
Paragraph
145+
146+
```{note}
147+
:class: abc
148+
:name: xyz
149+
150+
{unknown}`a`
151+
```
152+
.
153+
<string>:7: (ERROR/3) Unknown interpreted text role "unknown".
140154
.

tests/test_renderers/test_parse_directives.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66
from myst_parser.parse_directives import DirectiveParsingError, parse_directive_text
77

88

9-
@pytest.mark.parametrize("klass,arguments,content", [(Note, "", "a"), (Note, "a", "")])
9+
@pytest.mark.parametrize(
10+
"klass,arguments,content",
11+
[(Note, "", "a"), (Note, "a", ""), (Note, "", ":class: name\n\na")],
12+
)
1013
def test_parsing(klass, arguments, content, data_regression):
11-
arguments, options, body_lines = parse_directive_text(klass, arguments, content)
14+
arguments, options, body_lines, content_offset = parse_directive_text(
15+
klass, arguments, content
16+
)
1217
data_regression.check(
13-
{"arguments": arguments, "options": options, "body": body_lines}
18+
{
19+
"arguments": arguments,
20+
"options": options,
21+
"body": body_lines,
22+
"content_offset": content_offset,
23+
}
1424
)
1525

1626

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
arguments: []
2+
body:
3+
- a
4+
content_offset: 2
5+
options:
6+
class:
7+
- name
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
arguments: []
22
body:
33
- a
4+
content_offset: 0
45
options: {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
arguments: []
22
body:
33
- a
4+
content_offset: 0
45
options: {}

0 commit comments

Comments
 (0)