Repository: facelessuser/pymdown-extensions

Issue:
Improvements to snippets with multiple lines from the same file 
### Description

Let's assume, we have a snippet, where we want to "collapse" the middle part
```
// we want this line
    // we don't want this line
    // we want this line
```

Currently, this can only be done with the block format
````markdown
```
--8<--
my_file:1:1
my_file:3:3
--8<--
```
````

### Benefits

It works but it could be improved
* It would be just handy if we could write something like this
````markdown
--8<-- "my_file:1,3"
````
or even something like
````markdown
--8<-- "my_other_file:1:2,4:5"
````

### Solution Idea

* But where it really would be without any alternative, is when using with auto dedention (#1945). Line 3 would always be incorrectly be dedented. This could, if the previous solution is not wanted, be done by taking all snippets from the same file into account but I can see that this is not a very 

Discussion:
facelessuser: @gir-bot lgtm

Patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index d50e6859..fc01d763 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 82923da3..7c4d5aa0 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 e95e86cc..0530e02b 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 @@ class SnippetPreprocessor(Preprocessor):
                     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 @@ class SnippetPreprocessor(Preprocessor):
                         # 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(
                         [


Test patch:
diff --git a/tests/test_extensions/test_snippets.py b/tests/test_extensions/test_snippets.py
index 50c8902b..065d4369 100644
--- a/tests/test_extensions/test_snippets.py
+++ b/tests/test_extensions/test_snippets.py
@@ -236,6 +236,70 @@ class TestSnippets(util.MdCase):
             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."""