Skip to content

Commit

Permalink
Allow specifying a section in a snippet
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser committed Oct 1, 2022
1 parent a1c2727 commit e8eb7b6
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 7 deletions.
56 changes: 52 additions & 4 deletions pymdownx/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,16 @@ class SnippetPreprocessor(Preprocessor):
'''
)

RE_SNIPPET_FILE = re.compile(r'(.*?)(:[0-9]*)?(:[0-9]*)?$')
RE_SNIPPET_SECTION = re.compile(
r'''(?xi)
^.*?
(?P<inline_marker>-{2,}8<-{2,}[ \t]+)
\[[ \t]*(?P<type>start|end)[ \t]*:[ \t]*(?P<name>[a-z][0-9a-z]*)[ \t]*\]
.*?$
'''
)

RE_SNIPPET_FILE = re.compile(r'(?i)(.*?)(?:(:[0-9]*)?(:[0-9]*)?|(:[a-z][0-9a-z]*)?)$')

def __init__(self, config, md):
"""Initialize."""
Expand All @@ -80,6 +89,37 @@ def __init__(self, config, md):
self.tab_length = md.tab_length
super(SnippetPreprocessor, self).__init__()

def extract_section(self, section, lines):
"""Extract the specified section from the lines."""

new_lines = []
start = False
for l in lines:

# Found a snippet section marker with our specified name
m = self.RE_SNIPPET_SECTION.match(l)
if m is not None and m.group('name') == section:

# We found the start
if not start and m.group('type') == 'start':
start = True
continue

# We found the end
elif start and m.group('type') == 'end':
start = False
break

# We found an end, but no start
else:
return []

# We are currently in a section, so append the line
if start:
new_lines.append(l)

return new_lines

def get_snippet_path(self, path):
"""Get snippet path."""

Expand Down Expand Up @@ -132,7 +172,7 @@ def download(self, url):
return ['']

# Process lines
return [l.decode(self.encoding) for l in response.readlines()]
return [l.decode(self.encoding).rstrip('\r\n') for l in response.readlines()]

def parse_snippets(self, lines, file_name=None, is_url=False):
"""Parse snippets snippet."""
Expand Down Expand Up @@ -198,6 +238,7 @@ def parse_snippets(self, lines, file_name=None, is_url=False):
# Get line numbers (if specified)
end = None
start = None
section = None
m = self.RE_SNIPPET_FILE.match(path)
path = m.group(1).strip()
# Looks like we have an empty file and only lines specified
Expand All @@ -212,6 +253,9 @@ def parse_snippets(self, lines, file_name=None, is_url=False):
starting = m.group(2)
if starting and len(starting) > 1:
start = max(1, int(starting[1:]) - 1)
section_name = m.group(4)
if section_name:
section = section_name[1:]

# Ignore path links if we are in external, downloaded content
is_link = path.lower().startswith(('https://', 'http://'))
Expand All @@ -232,17 +276,21 @@ def parse_snippets(self, lines, file_name=None, is_url=False):
if not url:
# Read file content
with codecs.open(snippet, 'r', encoding=self.encoding) as f:
s_lines = [l for l in 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 = 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 = s_lines[s]
elif section:
s_lines = self.extract_section(section, s_lines)
except SnippetMissingError:
if self.check_paths:
raise
Expand All @@ -252,7 +300,7 @@ def parse_snippets(self, lines, file_name=None, is_url=False):
new_lines.extend(
[
space + l2 for l2 in self.parse_snippets(
[l.rstrip('\r\n') for l in s_lines],
s_lines,
snippet,
is_url=url
)
Expand Down
19 changes: 19 additions & 0 deletions tests/test_extensions/_snippets/section.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* --8<-- [start: cssSection] */
div {
color: red;
}
/* --8<-- [end: cssSection] */

<!-- --8<-- [start: htmlSection] -->
<div><p>content</p></div>
<!-- --8<-- [end: htmlSection] -->

/* --8<-- [end: cssSection2] */
/* --8<-- [start: cssSection2] */
div {
color: red;
}
/* --8<-- [end: cssSection2] */

<!-- --8<-- [start: htmlSection2] -->
<div><p>content</p></div>
97 changes: 94 additions & 3 deletions tests/test_extensions/test_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class TestSnippets(util.MdCase):
"""Test snippet cases."""

extension = [
'pymdownx.snippets',
'pymdownx.snippets', 'pymdownx.superfences'
]

extension_configs = {
Expand Down Expand Up @@ -223,6 +223,68 @@ def test_start_end_line_block(self):
True
)

def test_section_inline(self):
"""Test section partial in inline snippet."""

self.check_markdown(
R'''
```
--8<-- "section.txt:cssSection"
```
''',
'''
<div class="highlight"><pre><span></span><code>div {
color: red;
}
</code></pre></div>
''',
True
)

def test_section_block(self):
"""Test section partial in inline snippet."""

self.check_markdown(
R'''
--8<--
section.txt:htmlSection
--8<--
''',
'''
<div><p>content</p></div>
''',
True
)

def test_section_end_first(self):
"""Test section when the end is specified first."""

self.check_markdown(
R'''
--8<--
section.txt:cssSection2
--8<--
''',
'''
''',
True
)

def test_section_no_end(self):
"""Test section when the end is not specified."""

self.check_markdown(
R'''
--8<--
section.txt:htmlSection2
--8<--
''',
'''
<div><p>content</p></div>
''',
True
)


class TestSnippetsFile(util.MdCase):
"""Test snippet file case."""
Expand Down Expand Up @@ -502,7 +564,7 @@ def test_url_nested_file(self, mock_urlopen):

@patch('urllib.request.urlopen')
def test_url_lines(self, mock_urlopen):
"""Test nested file in URL."""
"""Test specifying specific lines in a URL."""

content = []
length = 0
Expand All @@ -515,7 +577,7 @@ def test_url_lines(self, mock_urlopen):
cm.status = 200
cm.code = 200
cm.readlines.return_value = content
cm.headers = {'content-length': '183'}
cm.headers = {'content-length': length}
cm.__enter__.return_value = cm
mock_urlopen.return_value = cm

Expand Down Expand Up @@ -615,6 +677,35 @@ def test_content_length_zero(self, mock_urlopen):
True
)

@patch('urllib.request.urlopen')
def test_url_sections(self, mock_urlopen):
"""Test specifying a section in a URL."""

content = []
length = 0
with open('tests/test_extensions/_snippets/section.txt', 'rb') as f:
for l in f:
length += len(l)
content.append(l)

cm = MagicMock()
cm.status = 200
cm.code = 200
cm.readlines.return_value = content
cm.headers = {'content-length': length}
cm.__enter__.return_value = cm
mock_urlopen.return_value = cm

self.check_markdown(
R'''
--8<-- "https://test.com/myfile.md:htmlSection"
''',
'''
<div><p>content</p></div>
''',
True
)


class TestURLSnippetsNoMax(util.MdCase):
"""Test snippet URL cases no max size."""
Expand Down

0 comments on commit e8eb7b6

Please sign in to comment.