Skip to content

Commit

Permalink
Allow specifying multiple line selection blocks (#2551)
Browse files Browse the repository at this point in the history
* Allow specifying multiple line selection blocks
* Simplify some areas of code

Resolves #2385
  • Loading branch information
facelessuser authored Dec 22, 2024
1 parent f6102ac commit 55ccf79
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 24 deletions.
1 change: 1 addition & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 10.12.1

- **NEW**: Snippets: Allow multiple line numbers or line number blocks separated by `,`.
- **FIX**: Snippets: Fix issue where when non sections of files are included, section labels are not stripped.
- **FIX**: BetterEm: Fixes for complex cases.

Expand Down
11 changes: 10 additions & 1 deletion docs/src/markdown/extensions/snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ numbers, simply append the start and/or end to the end of the file name with eac
- To specify extraction of content to start at a specific line number, simply use `file.md:3`.
- To extract all content up to a specific line, use `file.md::3`. This will extract lines 1 - 3.
- To extract all content starting at a specific line up to another line, use `file.md:4:6`. This will extract lines
4 - 6.
4 - 6.
- If you'd like to specify multiple blocks of extraction, separate each one with a `,`: `file.md:1:3,5:6`.

Each line selection is evaluated independently of previous selections. Selections will be performed in the order
they are specified. No additional separators (empty lines or otherwise) are inserted between selections, they are
inserted exactly as specified.

```
;--8<-- "file.md:4:6"
Expand All @@ -126,6 +131,10 @@ include.md::3
;--8<--
```

/// new | 10.13
Specifying multiple line selections separated with `,` was added in 10.13.
///

### Snippet Sections

/// new | New 9.7
Expand Down
45 changes: 22 additions & 23 deletions pymdownx/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ class SnippetPreprocessor(Preprocessor):
'''
)

RE_SNIPPET_FILE = re.compile(r'(?i)(.*?)(?:(:[0-9]*)?(:[0-9]*)?|(:[a-z][-_0-9a-z]*)?)$')
RE_SNIPPET_FILE = re.compile(
r'(?i)(.*?)(?:((?::[0-9]*){1,2}(?:(?:,(?=[0-9:])[0-9]*)(?::[0-9]*)?)*)|(:[a-z][-_0-9a-z]*))?$'
)

def __init__(self, config, md):
"""Initialize."""
Expand Down Expand Up @@ -298,26 +300,24 @@ def parse_snippets(self, lines, file_name=None, is_url=False, is_section=False):
continue

# Get line numbers (if specified)
end = None
start = None
end = []
start = []
section = None
m = self.RE_SNIPPET_FILE.match(path)
path = m.group(1).strip()
path = '' if m is None else m.group(1).strip()
# Looks like we have an empty file and only lines specified
if not path:
if self.check_paths:
raise SnippetMissingError(f"Snippet at path '{path}' could not be found")
else:
continue
ending = m.group(3)
if ending and len(ending) > 1:
end = int(ending[1:])
starting = m.group(2)
if starting and len(starting) > 1:
start = max(0, int(starting[1:]) - 1)
section_name = m.group(4)
if section_name:
section = section_name[1:]
if m.group(2):
for nums in m.group(2)[1:].split(','):
span = nums.split(':')
start.append(max(0, int(span[0]) - 1) if span[0] else None)
end.append(int(span[1]) if len(span) > 1 and span[1] else None)
elif m.group(3):
section = m.group(3)[1:]

# Ignore path links if we are in external, downloaded content
is_link = path.lower().startswith(('https://', 'http://'))
Expand All @@ -339,25 +339,24 @@ def parse_snippets(self, lines, file_name=None, is_url=False, is_section=False):
# Read file content
with codecs.open(snippet, 'r', encoding=self.encoding) as f:
s_lines = [l.rstrip('\r\n') for l in f]
if start is not None or end is not None:
s = slice(start, end)
s_lines = self.dedent(s_lines[s]) if self.dedent_subsections else s_lines[s]
elif section:
s_lines = self.extract_section(section, s_lines)
else:
# Read URL content
try:
s_lines = self.download(snippet)
if start is not None or end is not None:
s = slice(start, end)
s_lines = self.dedent(s_lines[s]) if self.dedent_subsections else s_lines[s]
elif section:
s_lines = self.extract_section(section, s_lines)
except SnippetMissingError:
if self.check_paths:
raise
s_lines = []

if s_lines:
if start and end:
final_lines = []
for entry in zip(start, end):
final_lines.extend(s_lines[slice(entry[0], entry[1], None)])
s_lines = self.dedent(final_lines) if self.dedent_subsections else final_lines
elif section:
s_lines = self.extract_section(section, s_lines)

# Process lines looking for more snippets
new_lines.extend(
[
Expand Down
64 changes: 64 additions & 0 deletions tests/test_extensions/test_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,70 @@ def test_start_1_2(self):
True
)

def test_start_multi(self):
"""Test multiple line specifiers."""

self.check_markdown(
R'''
---8<--- "lines.txt:1:2,8:9"
''',
'''
<p>This is a multi-line
snippet.
This is the end of the file.
There is no more.</p>
''',
True
)

def test_start_multi_no_end(self):
"""Test multiple line specifiers but the last has no end."""

self.check_markdown(
R'''
---8<--- "lines.txt:1:2,8"
''',
'''
<p>This is a multi-line
snippet.
This is the end of the file.
There is no more.</p>
''',
True
)

def test_start_multi_no_start(self):
"""Test multiple line specifiers but the last has no start."""

self.check_markdown(
R'''
---8<--- "lines.txt:1:2,:8"
''',
'''
<p>This is a multi-line
snippet.
This is a multi-line
snippet.</p>
<p>Content resides on various lines.
If we use line specifiers,
we can select any number of lines we want.</p>
<p>This is the end of the file.</p>
''',
True
)

def test_start_multi_hanging_comma(self):
"""Test multiple line specifiers but there is a hanging comma."""

self.check_markdown(
R'''
---8<--- "lines.txt:1:2,"
''',
'''
''',
True
)

def test_end_line_inline(self):
"""Test ending line with inline syntax."""

Expand Down

0 comments on commit 55ccf79

Please sign in to comment.