Skip to content

Commit 76f01cc

Browse files
authored
Indented code blocks surrounded by newlines (#234)
1 parent 7daca94 commit 76f01cc

File tree

3 files changed

+142
-62
lines changed

3 files changed

+142
-62
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
run: |
5353
import codecs, os, sys
5454
env = f"py=py3{sys.version_info[1]}\n"
55-
print(f"Picked {env.split('=')[1].strip()} for {sys.version}")
55+
sys.stdout.write(f"Picked {env.split('=')[1].strip()} for {sys.version}\n")
5656
with codecs.open(os.environ["GITHUB_OUTPUT"], "a", "utf-8") as file_handler:
5757
file_handler.write(env)
5858
- name: Install dependencies

src/mkdocs_include_markdown_plugin/process.py

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
)
115115

116116

117-
def transform_p_by_p_skipping_codeblocks(
117+
def transform_p_by_p_skipping_codeblocks( # noqa: PLR0912, PLR0915
118118
markdown: str,
119119
func: Callable[[str], str],
120120
) -> str:
@@ -128,45 +128,80 @@ def transform_p_by_p_skipping_codeblocks(
128128
_current_fcodeblock_delimiter = ''
129129

130130
# inside indented codeblock
131-
_inside_icodeblock = False
131+
_maybe_icodeblock_lines: list[str] = []
132+
_previous_line_was_empty = False
132133

133134
lines, current_paragraph = ([], '')
134135

135136
def process_current_paragraph() -> None:
136137
lines.extend(func(current_paragraph).splitlines(keepends=True))
137138

139+
# The next implementation takes into account that indented code
140+
# blocks must be surrounded by newlines as per the CommonMark
141+
# specification. See https://spec.commonmark.org/0.28/#indented-code-blocks
142+
#
143+
# However, note that ambiguities with list items are not handled.
144+
138145
for line in io.StringIO(markdown):
139-
if not _current_fcodeblock_delimiter and not _inside_icodeblock:
146+
if not _current_fcodeblock_delimiter:
140147
lstripped_line = line.lstrip()
141148
if (
142149
lstripped_line.startswith('```')
143150
or lstripped_line.startswith('~~~')
144151
):
145152
_current_fcodeblock_delimiter = lstripped_line[:3]
146-
if current_paragraph:
147-
process_current_paragraph()
148-
current_paragraph = ''
153+
process_current_paragraph()
154+
current_paragraph = ''
149155
lines.append(line)
150-
elif (
151-
line.replace('\t', ' ').replace('\r\n', '\n')
152-
== ' \n'
153-
):
154-
_inside_icodeblock = True
155-
if current_paragraph:
156+
elif line.startswith(' '):
157+
if not lstripped_line or _maybe_icodeblock_lines:
158+
# maybe enter indented codeblock
159+
_maybe_icodeblock_lines.append(line)
160+
else:
161+
current_paragraph += line
162+
elif _maybe_icodeblock_lines:
163+
process_current_paragraph()
164+
current_paragraph = ''
165+
if not _previous_line_was_empty:
166+
# wasn't an indented code block
167+
for line_ in _maybe_icodeblock_lines:
168+
current_paragraph += line_
169+
_maybe_icodeblock_lines = []
170+
current_paragraph += line
156171
process_current_paragraph()
157172
current_paragraph = ''
158-
lines.append(line)
173+
else:
174+
# exit indented codeblock
175+
for line_ in _maybe_icodeblock_lines:
176+
lines.append(line_)
177+
_maybe_icodeblock_lines = []
178+
lines.append(line)
159179
else:
160180
current_paragraph += line
181+
_previous_line_was_empty = not lstripped_line
161182
else:
162183
lines.append(line)
163-
if _current_fcodeblock_delimiter:
164-
if line.lstrip().startswith(_current_fcodeblock_delimiter):
165-
_current_fcodeblock_delimiter = ''
166-
elif not line.startswith(' ') and not line.startswith('\t'):
167-
_inside_icodeblock = False
168-
169-
process_current_paragraph()
184+
lstripped_line = line.lstrip()
185+
if lstripped_line.startswith(_current_fcodeblock_delimiter):
186+
_current_fcodeblock_delimiter = ''
187+
_previous_line_was_empty = not lstripped_line
188+
189+
if _maybe_icodeblock_lines:
190+
if not _previous_line_was_empty:
191+
# at EOF
192+
process_current_paragraph()
193+
current_paragraph = ''
194+
for line_ in _maybe_icodeblock_lines:
195+
current_paragraph += line_
196+
process_current_paragraph()
197+
current_paragraph = ''
198+
else:
199+
process_current_paragraph()
200+
current_paragraph = ''
201+
for line_ in _maybe_icodeblock_lines:
202+
lines.append(line_)
203+
else:
204+
process_current_paragraph()
170205

171206
return ''.join(lines)
172207

@@ -180,7 +215,7 @@ def transform_line_by_line_skipping_codeblocks(
180215
Skip fenced codeblock lines, where the transformation never is applied.
181216
182217
Indented codeblocks are not taken into account because in the practice
183-
this function is never used for transformations on indented lines. See
218+
this function is only used for transformations of heading prefixes. See
184219
the PR https://github.com/mondeja/mkdocs-include-markdown-plugin/pull/95
185220
to recover the implementation handling indented codeblocks.
186221
"""
@@ -269,6 +304,7 @@ def transform(paragraph: str) -> str:
269304
functools.partial(found_href, url_group_index=2),
270305
paragraph,
271306
)
307+
272308
return transform_p_by_p_skipping_codeblocks(
273309
markdown,
274310
transform,

tests/test_unit/test_process.py

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,10 @@
1414
('markdown', 'source_path', 'destination_path', 'expected_result'),
1515
(
1616
pytest.param(
17-
'''
18-
Here's a [link](CHANGELOG.md) to the changelog.
19-
''',
17+
"Here's a [link](CHANGELOG.md) to the changelog.",
2018
'README',
2119
'docs/nav.md',
22-
'''
23-
Here's a [link](../CHANGELOG.md) to the changelog.
24-
''',
20+
"Here's a [link](../CHANGELOG.md) to the changelog.",
2521
id='relative-link',
2622
),
2723
pytest.param(
@@ -69,17 +65,17 @@
6965
id='link-reference',
7066
),
7167
pytest.param(
72-
'''Here's a diagram: ![diagram](assets/diagram.png)''',
68+
"Here's a diagram: ![diagram](assets/diagram.png)",
7369
'README',
7470
'docs/home.md',
75-
'''Here's a diagram: ![diagram](../assets/diagram.png)''',
71+
"Here's a diagram: ![diagram](../assets/diagram.png)",
7672
id='image',
7773
),
7874
pytest.param(
79-
'''Build status: [![Build Status](badge.png)](build/)''',
75+
'Build status: [![Build Status](badge.png)](build/)',
8076
'README',
8177
'docs/home.md',
82-
'''Build status: [![Build Status](../badge.png)](../build/)''',
78+
'Build status: [![Build Status](../badge.png)](../build/)',
8379
id='image-inside-link',
8480
),
8581
pytest.param(
@@ -92,10 +88,10 @@
9288
id='absolute-urls',
9389
),
9490
pytest.param(
95-
'''[contact us](mailto:hello@example.com)''',
91+
'[contact us](mailto:hello@example.com)',
9692
'README',
9793
'docs/nav.md',
98-
'''[contact us](mailto:hello@example.com)''',
94+
'[contact us](mailto:hello@example.com)',
9995
id='mailto-urls',
10096
),
10197
pytest.param(
@@ -120,35 +116,33 @@
120116
id='cpp-likelink-fenced-codeblock',
121117
),
122118
pytest.param(
123-
'''Some text before
124-
\t
125-
\tconst auto lambda = []() { .... };
126-
127-
Some text after
128-
''',
119+
(
120+
'Text before\n'
121+
' \n '
122+
'const auto lambda = []() { .... };\n \nText after\n'
123+
),
129124
'README',
130125
'examples/lambda.md',
131-
'''Some text before
132-
\t
133-
\tconst auto lambda = []() { .... };
134-
135-
Some text after
136-
''',
126+
(
127+
'Text before\n'
128+
' \n '
129+
'const auto lambda = []() { .... };\n \nText after\n'
130+
),
137131
id='cpp-likelink-indented-codeblock',
138132
),
139133
pytest.param(
140-
'''Some text before
141-
\t
142-
\tconst auto lambda = []() { .... };\r\n
143-
Some text after
144-
''',
134+
(
135+
'Text before\r\n'
136+
' \r\n '
137+
'const auto lambda = []() { .... };\r\n \r\nText after\r\n'
138+
),
145139
'README',
146140
'examples/lambda.md',
147-
'''Some text before
148-
\t
149-
\tconst auto lambda = []() { .... };\r\n
150-
Some text after
151-
''',
141+
(
142+
'Text before\r\n'
143+
' \r\n '
144+
'const auto lambda = []() { .... };\r\n \r\nText after\r\n'
145+
),
152146
id='cpp-likelink-indented-codeblock-windows-newlines',
153147
),
154148
pytest.param(
@@ -165,16 +159,66 @@
165159
id='exclude-fenced-code-blocks',
166160
),
167161
pytest.param(
168-
' ' * 4 + '''
169-
[link](CHANGELOG.md)
170-
''' + ' ' * 4 + '\n',
162+
(
163+
' \n'
164+
' [link](CHANGELOG.md)\n'
165+
' \n'
166+
),
171167
'README',
172168
'docs/nav.md',
173-
' ' * 4 + '''
174-
[link](CHANGELOG.md)
175-
''' + ' ' * 4 + '\n',
169+
(
170+
' \n'
171+
' [link](CHANGELOG.md)\n'
172+
' \n'
173+
),
176174
id='exclude-indented-code-blocks',
177175
),
176+
pytest.param(
177+
(
178+
' \n'
179+
' [link](CHANGELOG.md)\n'
180+
),
181+
'README',
182+
'docs/nav.md',
183+
# is rewritten because not newline at end of code block
184+
(
185+
' \n'
186+
' [link](../CHANGELOG.md)\n'
187+
),
188+
id='exclude-indented-code-blocks-eof',
189+
),
190+
pytest.param(
191+
(
192+
' [link](CHANGELOG.md)\n'
193+
' \n'
194+
),
195+
'README',
196+
'docs/nav.md',
197+
(
198+
' [link](../CHANGELOG.md)\n'
199+
' \n'
200+
),
201+
# No newline before, is not an indented code block, see:
202+
# https://spec.commonmark.org/0.28/#indented-code-blocks
203+
id='no-exclude-indented-code-blocks-missing-newline-before',
204+
),
205+
pytest.param(
206+
(
207+
' \n'
208+
' [link](CHANGELOG.md)\n'
209+
'Foo\n'
210+
),
211+
'README',
212+
'docs/nav.md',
213+
(
214+
' \n'
215+
' [link](../CHANGELOG.md)\n'
216+
'Foo\n'
217+
),
218+
# No newline after, is not an indented code block, see:
219+
# https://spec.commonmark.org/0.28/#indented-code-blocks
220+
id='no-exclude-indented-code-blocks-missing-newline-after',
221+
),
178222
),
179223
)
180224
def test_rewrite_relative_urls(

0 commit comments

Comments
 (0)