Skip to content

Commit

Permalink
Support alternate fenced header form (#2470)
Browse files Browse the repository at this point in the history
 Support alternate fenced header form

Based off of @dmadisetti's code in PR #2468
  • Loading branch information
facelessuser authored Sep 27, 2024
1 parent 8355b45 commit 4b54d02
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 36 deletions.
4 changes: 4 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 10.11

- **NEW**: SuperFences: Allow fenced code to be parsed in the form ` ```lang {.class #id} `.

## 10.10.2

- **FIX**: BetterEm: Add better support for `*em, **em,strong***` and `_em, __em,strong___` cases.
Expand Down
67 changes: 41 additions & 26 deletions docs/src/markdown/extensions/superfences.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,44 @@ md = markdown.Markdown(extensions=['pymdownx.superfences'])

## Injecting Classes, IDs, and Attributes

You can use the brace format to specify classes and IDs. The first provided class is always used as the language class.
IDs (`#id`) can also be inserted as well. Arbitrary attributes in the form `key="value"` can be inserted as well if
the [`attr_list`][attr-list] extension is enabled, but when using Pygments, only `key="value"` attributes that start
with the `data-` prefix will be recognized, all others will be treated as options for Pygments and will be rejected if
not valid.
You can use the brace format to specify classes and IDs where IDs use the form `#id` and classes use the form `.class`.
Arbitrary attributes in the form `key="value"` can be inserted as well if the [`attr_list`][attr-list] extension is
enabled, but when using Pygments, only `key="value"` attributes that start with the `data-` prefix will be recognized,
all others will be treated as options for Pygments and will be rejected if not valid.

````text title="Injecting Classes"
```{.python .extra-class linenums="1"}
```python {.extra-class #id linenums="1"}
import hello_world
```
````

/// html | div.result
```html
<div class="extra-class highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span>1</pre></div></td><td class="code"><div><pre><span></span><code><span cv></td><td class="code"><div><pre><span></span><code><span class="kn">import</span> <spanlass="kn">import</span> <span class="nn">hello_world</span>\n</code></pre></div>\n</td></tr></table></div>
<div id="id" class="extra-class highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">1</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="kn">import</span> <span class="nn">hello_world</span>
</code></pre></div></td></tr></table></div>
```
///

Alternatively, if a language is not provided, the first class is assumed to specify the language.

````text title="Injecting Classes"
```{.python .extra-class #id linenums="1"}
import hello_world
```
````

/// html | div.result
```html
<div id="id" class="extra-class highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">1</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="kn">import</span> <span class="nn">hello_world</span>
</code></pre></div></td></tr></table></div>
```
///

When generating additional classes on a JavaScript style code block (non-Pygments code blocks), classes are injected in
the `#!html code` block.

````text title="Non-Pygments Injecting Classes"
```{.python .extra-class #id linenums="1"}
```python {.extra-class #id linenums="1"}
import hello_world
```
````
Expand Down Expand Up @@ -206,13 +221,13 @@ sign and the value must be quoted. Valid line numbers are n > 0. If `linenums`
control the starting line shown in the block.

````text title="Line Numbers"
```{.python linenums="1"}
```python {linenums="1"}
import foo.bar
```
````

/// html | div.result
```{.python linenums="1"}
```python {linenums="1"}
import foo.bar
```
///
Expand All @@ -221,13 +236,13 @@ import foo.bar
And, if we wanted to start with a different starting line number, we would just specify something other than `1`.

````text title="Custom Line Number Start"
```{.python linenums="2"}
```python {linenums="2"}
import foo.bar
```
````

/// html | div.result
```{.python linenums="2"}
```python {linenums="2"}
import foo.bar
```
///
Expand All @@ -241,7 +256,7 @@ So to set showing only every other line number, we could do the following. Line
"line step" is always the second option, so you must specify line start before line step.

````text title="N<sup>th</sup> Line"
``` {.python linenums="1 2"}
```python {linenums="1 2"}
"""Some file."""
import foo.bar
import boo.baz
Expand All @@ -250,7 +265,7 @@ import foo.bar.baz
````

/// html | div.result
``` {.python linenums="1 2"}
```python {linenums="1 2"}
"""Some file."""
import foo.bar
import boo.baz
Expand Down Expand Up @@ -291,7 +306,7 @@ after the opening tokens (and language if present). The setting is named `hl_li
targeted line numbers separated by spaces.

````text title="Highlight Lines"
```{.python hl_lines="1 3"}
```python {hl_lines="1 3"}
"""Some file."""
import foo.bar
import boo.baz
Expand All @@ -300,7 +315,7 @@ import foo.bar.baz
````

/// html | div.result
```{.python hl_lines="1 3"}
```python {hl_lines="1 3"}
"""Some file."""
import foo.bar
import boo.baz
Expand All @@ -312,7 +327,7 @@ import foo.bar.baz
Line numbers are always referenced starting at 1 ignoring what the line number is labeled as when showing line numbers.

````text title="Highlight Lines with Line Numbers"
```{.py3 hl_lines="1 3" linenums="2"}
```python {hl_lines="1 3" linenums="2"}
"""Some file."""
import foo.bar
import boo.baz
Expand All @@ -321,7 +336,7 @@ import foo.bar.baz
````

/// html | div.result
```{.py3 hl_lines="1 3" linenums="2"}
```python {hl_lines="1 3" linenums="2"}
"""Some file."""
import foo.bar
import boo.baz
Expand All @@ -333,7 +348,7 @@ If you'd like to do a range of lines, you can use the notation `x-y` where `x` i
ending line. You can do multiple ranges and even mix them with non ranges.

````text title="Highlight Ranges"
```{.py3 hl_lines="1-2 5 7-8"}
```python {hl_lines="1-2 5 7-8"}
import foo
import boo.baz
import foo.bar.baz
Expand All @@ -347,7 +362,7 @@ class Foo:
````

/// html | div.result
```{.py3 hl_lines="1-2 5 7-8"}
```python {hl_lines="1-2 5 7-8"}
import foo
import boo.baz
import foo.bar.baz
Expand Down Expand Up @@ -398,15 +413,15 @@ element at the start of the table set to span both the line number column and th
```

````text title="Adding Titles"
```{.py3 title="My Cool Header"}
```python {title="My Cool Header"}
import foo.bar
import boo.baz
import foo.bar.baz
```
````

/// html | div.result
```{.py3 title="My Cool Header"}
```python {title="My Cool Header"}
import foo.bar
import boo.baz
import foo.bar.baz
Expand Down Expand Up @@ -535,7 +550,7 @@ extension_configs = {

/// tab | Markdown
````
```{.python linenums="1 1" }
```python {linenums="1 1" }
import foo
```
````
Expand Down Expand Up @@ -576,7 +591,7 @@ extension_configs = {

/// tab | Markdown
````
```{.python linenums="1 1" }
```python {linenums="1 1" }
import foo
```
````
Expand Down Expand Up @@ -705,7 +720,7 @@ conditionally control logic within the formatter. If no validator is defined, th
inputs to `attrs`.

SuperFences will only pass `attrs` to a formatter if an attribute style header is used for a fenced block
(` ``` {.lang attr="value"}`) and the [`attr_list`][attr-list] extension is enabled. Attribute are not supported in the
(` ```lang {attr="value"}`) and the [`attr_list`][attr-list] extension is enabled. Attribute are not supported in the
form (` ```lang attr=value`) and will cause the parsing of the fenced block to abort.

Custom options can be used as keys with quoted values (`key="value"`), or as keys with no value (`key`). If a key is
Expand Down Expand Up @@ -753,7 +768,7 @@ def custom_format(source, language, class_name, options, md, **kwargs):
This would allow us to use the following custom fence:

````
```{.test opt="A"}
```test {opt="A"}
test
```
````
Expand Down
2 changes: 1 addition & 1 deletion docs/src/markdown/extras/mermaid.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ window.mermaidConfig = {
///

/// tab | JS
```{.js .md-max-height}
```js {.md-max-height}
const uml = async className => {

// Custom element to encapsulate Mermaid content.
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, 10, 2, "final")
__version_info__ = Version(10, 11, 0, "final")
__version__ = __version_info__._get_canonical()
18 changes: 11 additions & 7 deletions pymdownx/superfences.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@

RE_NESTED_FENCE_START = re.compile(
r'''(?x)
(?P<fence>~{3,}|`{3,})[ \t]* # Fence opening
(?:(\{(?P<attrs>[^\n]*)\})?| # Optional attributes or
(?:\.?(?P<lang>[\w#.+-]*))?[ \t]* # Language
(?P<fence>~{3,}|`{3,})
(?:[ \t]*\.?(?P<lang>[\w#.+-]+))? # Language
(?:
[ \t]*(\{(?P<attrs>[^\n]*)\}) | # Optional attributes or
(?P<options>
(?:
(?:\b[a-zA-Z][a-zA-Z0-9_]*(?:=(?P<quot>"|').*?(?P=quot))?[ \t]*) | # Options
)*
(?:[ \t]*[a-zA-Z][a-zA-Z0-9_]*(?:=(?P<quot>"|').*?(?P=quot))?) # Options
)+
)
)[ \t]*$
)?[ \t]*$
'''
)

Expand Down Expand Up @@ -639,7 +640,10 @@ def handle_attrs(self, m):
else:
values[k] = v

self.lang = self.classes.pop(0) if self.classes else ''
if m.group('lang'):
self.lang = m.group('lang')
else:
self.lang = self.classes.pop(0) if self.classes else ''

# Run per language validator
for entry in reversed(self.extension.superfences):
Expand Down
49 changes: 48 additions & 1 deletion tests/test_extensions/test_superfences.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,22 @@ def test_attr(self):
True
)

def test_attrs_alternate_form(self):
"""Test attributes with new alternate form."""

self.check_markdown(
r'''
```python {.test .class #class}
import test
```
''',
r'''
<div id="class" class="test class highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">test</span>
</code></pre></div>
''', # noqa: E501
True
)


class TestSuperFencesClassesIdsAttrList(util.MdCase):
"""Test fence ids and classes with attribute lists."""
Expand Down Expand Up @@ -767,6 +783,22 @@ def test_data_attr_linenums(self):
True
)

def test_attrs_alternate_form(self):
"""Test attributes with new alternate form."""

self.check_markdown(
r'''
```python {.test .class #id data-attr="test" linenums="1"}
import test
```
''',
r'''
<div id="id" class="test class highlight" data-attr="test"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">1</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="kn">import</span> <span class="nn">test</span>
</code></pre></div></td></tr></table></div>
''', # noqa: E501
True
)


class TestSuperFencesClassesIdsAttrListNoPygments(util.MdCase):
"""Test fence ids and classes with attribute lists and with no Pygments."""
Expand Down Expand Up @@ -929,7 +961,7 @@ def test_bad_option_value(self):
class TestSuperFencesCustom(util.MdCase):
"""Test custom validator and format."""

extension = ['pymdownx.superfences']
extension = ['pymdownx.superfences', 'attr_list']
extension_configs = {
'pymdownx.superfences': {
'custom_fences': [
Expand Down Expand Up @@ -1026,6 +1058,21 @@ def test_custom_options(self):
True
)

def test_custom_options_attr_list(self):
"""Test option with correct value."""

self.check_markdown(
r'''
```test {opt="A"}
test
```
''',
r'''
<div lang="test" class_name="class-test", option="A">test</div>
''',
True
)


class TestSuperFencesCustomException(util.MdCase):
"""Test custom validator and format."""
Expand Down

0 comments on commit 4b54d02

Please sign in to comment.