Skip to content

Commit

Permalink
Allow negative indexes
Browse files Browse the repository at this point in the history
Resolves #2517
  • Loading branch information
facelessuser committed Dec 22, 2024
1 parent 1789d60 commit 4e2bf00
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 12 deletions.
5 changes: 4 additions & 1 deletion docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Changelog

## 10.12.1
## 10.13

- **NEW**: Snippets: Allow multiple line numbers or line number blocks separated by `,`.
- **NEW**: Snippets: Allow using a negative index for number start indexes and end indexes. Negative indexes are
converted to positive indexes based on the number of lines in the snippet.
- **FIX**: Snippets: Properly capture empty newline at end of file.
- **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
2 changes: 1 addition & 1 deletion pymdownx/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,5 @@ def parse_version(ver, pre=False):
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(10, 12, 1, "final")
__version_info__ = Version(10, 13, 0, "final")
__version__ = __version_info__._get_canonical()
29 changes: 22 additions & 7 deletions pymdownx/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class SnippetPreprocessor(Preprocessor):
)

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

def __init__(self, config, md):
Expand Down Expand Up @@ -222,7 +222,11 @@ def download(self, url):
content = response.read()

# Process lines
return [l.decode(self.encoding) for l in content.splitlines()]
last = content.endswith((b'\r', b'\n'))
s_lines = [l.decode(self.encoding) for l in content.splitlines()]
if last:
s_lines.append('')
return s_lines

def parse_snippets(self, lines, file_name=None, is_url=False, is_section=False):
"""Parse snippets snippet."""
Expand Down Expand Up @@ -314,8 +318,10 @@ def parse_snippets(self, lines, file_name=None, is_url=False, is_section=False):
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)
st = int(span[0]) if span[0] else None
start.append(st if st is None or st < 0 else max(0, st - 1))
en = int(span[1]) if len(span) > 1 and span[1] else None
end.append(en if en is None or en >= 0 else en)
elif m.group(3):
section = m.group(3)[1:]

Expand All @@ -338,7 +344,13 @@ def parse_snippets(self, lines, file_name=None, is_url=False, is_section=False):
if not url:
# Read file content
with codecs.open(snippet, 'r', encoding=self.encoding) as f:
s_lines = [l.rstrip('\r\n') for l in f]
last = False
s_lines = []
for l in f:
last = l.endswith(('\r', '\n'))
s_lines.append(l.strip('\r\n'))
if last:
s_lines.append('')
else:
# Read URL content
try:
Expand All @@ -349,10 +361,13 @@ def parse_snippets(self, lines, file_name=None, is_url=False, is_section=False):
s_lines = []

if s_lines:
total = len(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)])
for sel in zip(start, end):
s_start = util.clamp(total + sel[0], 0, total) if sel[0] and sel[0] < 0 else sel[0]
s_end = util.clamp(total + 1 + sel[1], 0, total) if sel[1] and sel[1] < 0 else sel[1]
final_lines.extend(s_lines[slice(s_start, s_end, None)])
s_lines = self.dedent(final_lines) if self.dedent_subsections else final_lines
elif section:
s_lines = self.extract_section(section, s_lines)
Expand Down
13 changes: 13 additions & 0 deletions pymdownx/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@
PY39 = (3, 9) <= sys.version_info


def clamp(value, mn, mx):
"""Clamp the value to the given minimum and maximum."""

if mn is not None and mx is not None:
return max(min(value, mx), mn)
elif mn is not None:
return max(value, mn)
elif mx is not None:
return min(value, mx)
else:
return value


def is_win(): # pragma: no cover
"""Is Windows."""

Expand Down
67 changes: 64 additions & 3 deletions tests/test_extensions/test_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ def test_inline(self):
---8<--- "b.txt"
''',
R'''
<p>Snippet
Snippet
---8&lt;--- "b.txt"</p>
<p>Snippet</p>
<p>Snippet</p>
<p>---8&lt;--- "b.txt"</p>
<ul>
<li>
<p>Testing indentation</p>
Expand Down Expand Up @@ -300,6 +300,67 @@ def test_start_multi_hanging_comma(self):
True
)

def test_negative_range(self):
"""Test negative indexing range."""

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

def test_negative_single(self):
"""Test negative indexing single line."""

self.check_markdown(
R'''
---8<--- "lines.txt:-2:-2"
''',
'''
<p>There is no more.</p>
''',
True
)

def test_mixed_negative(self):
"""Test negative indexing single line."""

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

def test_start_negative_multi(self):
"""Test multiple line specifiers with negative indexes."""

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

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

Expand Down

0 comments on commit 4e2bf00

Please sign in to comment.