diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md index d50e6859a..fc01d7630 100644 --- a/docs/src/markdown/about/changelog.md +++ b/docs/src/markdown/about/changelog.md @@ -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. diff --git a/docs/src/markdown/extensions/snippets.md b/docs/src/markdown/extensions/snippets.md index 82923da3c..7c4d5aa07 100644 --- a/docs/src/markdown/extensions/snippets.md +++ b/docs/src/markdown/extensions/snippets.md @@ -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" @@ -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 diff --git a/pymdownx/snippets.py b/pymdownx/snippets.py index e95e86cce..0530e02b4 100644 --- a/pymdownx/snippets.py +++ b/pymdownx/snippets.py @@ -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.""" @@ -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://')) @@ -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( [ diff --git a/tests/test_extensions/test_snippets.py b/tests/test_extensions/test_snippets.py index 50c8902b8..065d4369b 100644 --- a/tests/test_extensions/test_snippets.py +++ b/tests/test_extensions/test_snippets.py @@ -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" + ''', + ''' +
This is a multi-line + snippet. + This is the end of the file. + There is no more.
+ ''', + 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" + ''', + ''' +This is a multi-line + snippet. + This is the end of the file. + There is no more.
+ ''', + 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" + ''', + ''' +This is a multi-line + snippet. + This is a multi-line + snippet.
+Content resides on various lines. + If we use line specifiers, + we can select any number of lines we want.
+This is the end of the file.
+ ''', + 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."""