Skip to content

Commit 2703d81

Browse files
authored
Add custom blockquote extension (#2817)
Resolves #2814
1 parent 8c7393a commit 2703d81

File tree

20 files changed

+646
-58
lines changed

20 files changed

+646
-58
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ jobs:
4949
allow-prereleases: true
5050
- name: Install dependencies
5151
run: |
52-
python -m pip install --upgrade pip build tox coverage codecov
52+
python -m pip install --upgrade pip build tox coverage
5353
- name: Test
5454
run: |
5555
python -m tox
5656
- name: Upload Results
5757
if: success()
58-
uses: codecov/codecov-action@v4
58+
uses: codecov/codecov-action@v5
5959
with:
60-
file: ./coverage.xml
60+
files: ./coverage.xml
6161
flags: unittests
6262
name: ${{ matrix.platform }}-${{ matrix.tox-env }}
6363
token: ${{ secrets.CODECOV_TOKEN }} # required

docs/src/dictionary/en-custom.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ CDN
1212
CDNs
1313
CSS
1414
CVE
15+
Callout
16+
Callouts
1517
Changelog
1618
Cloudflare
1719
CodeHilite
@@ -44,6 +46,7 @@ GitHub
4446
GitHubEmoji
4547
GitLab
4648
Gitter
49+
Gruber
4750
HeaderAnchor
4851
Hunspell
4952
Inline
@@ -84,13 +87,11 @@ PlainHTML
8487
Pomaceous
8588
Postprocessor
8689
ProgressBar
87-
EmojiOne
8890
PyMdown
8991
PyPI
9092
PyYAML
9193
PyYaml
9294
Pygments
93-
Gruber
9495
Pymdownx
9596
Reindent
9697
Rosaceae
@@ -134,6 +135,8 @@ blockquotes
134135
bool
135136
boolean
136137
builtins
138+
callout
139+
callouts
137140
checkbox
138141
checkboxes
139142
cmd

docs/src/markdown/.snippets/links.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
[_mark]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/mark.py "Source"
2121
[_pathconverter]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/pathconverter.py "Source"
2222
[_progressbar]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/progressbar.py "Source"
23+
[_quotes]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/quotes.py "Source"
2324
[_saneheaders]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/saneheaders.py "Source"
2425
[_slugs]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/slugs.py "Source"
2526
[_smartsymbols]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/smartsymbols.py "Source"
@@ -36,6 +37,7 @@
3637
[atomic]: https://python-markdown.github.io/extensions/api/#working_with_et
3738
[attr-list]: https://python-markdown.github.io/extensions/attr_list/
3839
[bleach]: https://pypi.python.org/pypi/bleach
40+
[blockquotes]: https://daringfireball.net/projects/markdown/syntax#blockquote
3941
[codehilite]: https://python-markdown.github.io/extensions/code_hilite/
4042
[coverage]: https://coverage.readthedocs.io
4143
[critic-markup]: http://criticmarkup.com/
@@ -50,6 +52,7 @@
5052
[footnotes]: https://python-markdown.github.io/extensions/footnotes/
5153
[gemoji-index]: https://github.com/facelessuser/pymdown-extensions/blob/master/pymdownx/gemoji_db.py
5254
[gemoji]: https://github.com/github/gemoji
55+
[github-alerts]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
5356
[github-api-emoji]: https://developer.github.com/v3/emojis/
5457
[github-api-oauth]: https://developer.github.com/v3/oauth/
5558
[highlightjs]: https://highlightjs.org/
@@ -64,6 +67,7 @@
6467
[mkdocs-config]: https://github.com/facelessuser/pymdown-extensions/blob/master/mkdocs.yml
6568
[mkdocs-material]: https://github.com/squidfunk/mkdocs-material
6669
[mkdocs]: https://github.com/mkdocs/mkdocs
70+
[multimarkdown-critic]: https://fletcher.github.io/MultiMarkdown-6/syntax/critic.html
6771
[nested-markdown]: https://python-markdown.github.io/extensions/extra/#nested-markdown-inside-html-blocks
6872
[nl2br]: https://python-markdown.github.io/extensions/nl2br/
6973
[octicons]: https://octicons.github.com/

docs/src/markdown/about/changelog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ icon: lucide/scroll-text
33
---
44
# Changelog
55

6+
## 10.20
7+
8+
- **NEW**: Quotes: New blockquotes extension added that uses a more modern approach when compared to Python Markdown's
9+
default. Quotes specifically will not group consecutive blockquotes together in the same lazy fashion that the
10+
default Python Markdown does which follows a more modern trend to how parsers these days handle block quotes.
11+
12+
In addition, Quotes also provides an optional feature to enable specifying callouts/alerts in the style used by
13+
GitHub and Obsidian.
14+
615
## 10.19.1
716

817
- **FIX**: Arithmatex: Fix issue where block `$$` math used inline within a paragraph could result in nested math

docs/src/markdown/extensions/betterem.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import markdown
2424
md = markdown.Markdown(extensions=['pymdownx.betterem'])
2525
```
2626

27-
/// danger | Reminder
27+
/// important | Reminder
2828
Remember to read the [Usage Notes](../usage_notes.md) for information that may be relevant when using this
2929
extension!
3030
///

docs/src/markdown/extensions/critic.md

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,24 +194,19 @@ will work quite well. But when trying to render the edits visually **and** tryi
194194
things can get ugly. I think this is the one unfortunate problem with CriticMarkup. The existence of the critic edits
195195
can alter the actual source. Its a fantastic idea, but it should be understood that when using CriticMarkup beyond
196196
inline or block paragraphs, there is a possibility that invalid HTML will be created for the preview (especially in
197-
relation to lists or if breaking up Markdown syntax). I think Fletcher of [MultiMarkdown][multi-markdown] said it best
198-
here:
197+
relation to lists or if breaking up Markdown syntax). I think Fletcher of [MultiMarkdown][multimarkdown-critic] said it
198+
best.
199199

200-
> I view CriticMarkup as two things:
200+
/// quote
201+
I view CriticMarkup as two things (in addition to the actual tools that implement these concepts):
201202

202-
> 1. A syntax for documenting editing notes and changes, and for collaborating amongst coauthors.
203+
1. A syntax for documenting editing notes and changes, and for collaborating amongst coauthors.
203204

204-
> 2. A means to display those notes/changes in the HTML output.
205+
2. A means to display those notes/changes in the HTML output.
205206

206-
> I believe that \#1 is a really great idea, and well implemented. \#2 is not so well implemented, largely due to the
207-
"orthogonal" nature of CriticMarkup and the underlying Markdown syntax.
208-
209-
> CM is designed as a separate layer on top of Markdown/MultiMarkdown. This means that a Markdown span could, for
210-
example, start in the middle of a CriticMarkup structure, but end outside of it. This means that an algorithm to
211-
properly convert a CM/Markdown document to HTML would be quite complex, with a huge number of edge cases to consider.
212-
I've tried a few (fairly creative, in my opinion) approaches, but they didn't work. Perhaps someone else will come up
213-
with a better solution, or will be so interested that they put the work in to create the complex algorithm. I have no
214-
current plans to do so.
207+
I believe that \#1 is a really great idea, and well implemented. \#2 is not so well implemented, largely due to the
208+
"orthogonal" nature of CriticMarkup and the underlying Markdown syntax.
209+
///
215210

216211
The Critic extension does its best by employing a preprocessor to inject the critic tags before all other parsing and a
217212
post-processor to clean up some of the weird side effects of the injection (only selected odd cases as others are more
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
icon: lucide/quote
3+
---
4+
[:octicons-file-code-24:][_quotes]{: .source-link }
5+
6+
# Quotes
7+
8+
## Overview
9+
10+
Quotes is an extension that offers a more modern take on blockquotes compared to the one that ships with Python Markdown
11+
by default.
12+
13+
Python Markdown is an old-school parser that aims to follow the original Markdown specification. When implementing
14+
blockquotes, it follows the guidance that describes the ability to lazily specify content under a blockquote as shown
15+
below. Notice that two blocks of text can be specified without having to specify `>` on the empty lines between the
16+
blocks.
17+
18+
```text title="Lazy Blockquote Handling"
19+
> This is a paragraph
20+
21+
> This is another paragraph.
22+
```
23+
24+
/// html | div.result
25+
```md-render
26+
> This is a paragraph
27+
28+
> This is another paragraph.
29+
```
30+
///
31+
32+
The problem with this approach is that if you have a need to have two separate blockquotes, this because difficult.
33+
34+
The Quotes extensions follows logic that is seen in more modern approaches to Markdown.
35+
36+
```text title="Separate Blockquotes"
37+
> This is a paragraph
38+
39+
> This is another paragraph.
40+
```
41+
42+
/// html | div.result
43+
> This is a paragraph
44+
45+
> This is another paragraph.
46+
///
47+
48+
It should be noted that since this extension does not use lazy formatting, you should be stricter with your leading
49+
blockquote `>`. For instance, when using [SuperFences](./superfences.md), you will find that these must be fully
50+
contained in the blockquotes. In the past, the lazy handling would have allowed for
51+
52+
````text title="Blockquotes and SuperFences"
53+
> ```
54+
> Test
55+
>
56+
> Test
57+
> ```
58+
59+
This will not.
60+
61+
> ```
62+
> Test
63+
64+
> Test
65+
> ```
66+
````
67+
68+
/// html | div.result
69+
This will work.
70+
71+
> ```
72+
> Test
73+
>
74+
> Test
75+
> ```
76+
77+
This will not.
78+
79+
> ```
80+
> Test
81+
82+
> Test
83+
> ```
84+
///
85+
86+
## Callouts
87+
88+
[GitHub][github-alerts] and [Obsidian](https://help.obsidian.md/callouts) allow for specifying admonitions (also known
89+
as alerts or callouts) by using a special syntax at the start of a blockquote. While Obsidian does this with a more
90+
feature rich syntax, GitHub offers a more stripped down version.
91+
92+
The Quotes extensions allows you to opt-in approach to specifying admonitions in a similar approach that is compatible
93+
with Obsidian and GitHub if usage is limited to what they offer.
94+
95+
Quotes will take the special blockquote syntax and output HTML that matches the output of [Admonitions][admonition] or
96+
[Details](./details.md).
97+
98+
To specify normal callouts, simply ensure the that the first line of a blockquote contains a class name in within
99+
`[!...]`.
100+
101+
```text title="Callout"
102+
> [!note]
103+
> Here is a note
104+
```
105+
106+
/// html | div.result
107+
> [!note]
108+
> Here is a note
109+
///
110+
111+
/// note
112+
GitHub only supports the simple class notation, and only recognizes the classes: note, tip, important, warning, caution.
113+
114+
There are no restrictions as to which classes you can specify, but if cross compliance is desired, take note of what
115+
GitHub supports. Learn more [here][github-alerts].
116+
///
117+
118+
Borrowing from Obsidian, Quotes also enables a few more advanced features. If you'd like a custom title, simply provide
119+
one on the same line as the class specifier. If one is not provided, a title is sourced from the first the class.
120+
121+
```text title="Callout with Custom Title"
122+
> [!tip] Here's a tip
123+
> Use a custom title!
124+
```
125+
126+
/// html | div.result
127+
> [!tip] Here's a tip
128+
> Use a custom title!
129+
///
130+
131+
To make a collapsible callout, just add `+` (open) or `-` (closed) immediately after the class specifier.
132+
133+
```text title="Collapsible Callout"
134+
> [!danger]- Click to see more
135+
> I'm collapsible.
136+
```
137+
138+
/// html | div.result
139+
> [!danger]- Click to see more
140+
> I'm collapsible.
141+
///
142+
143+
Lastly, if you need to specify more multiple classes, you can utilize the Obsidian approach and and separate each class
144+
with `|`. The first class will be used as the main title if no title is provided, but all classes will be applied to the
145+
callout.
146+
147+
```text title="Collapsible Callout"
148+
> [!warning | inline | end]
149+
> Make inline and set at the end.
150+
151+
A paragraph.
152+
```
153+
154+
/// html | div.result
155+
> [!warning | inline | end]
156+
> Make inline and set at the end.
157+
158+
A paragraph.
159+
///
160+
161+
## Options
162+
163+
Option | Type | Default | Description
164+
-------------------- | ------- | ------------ |------------
165+
`callouts` | bool | `#!py3 False`| Enable the ability to create special callouts using blockquotes.

docs/src/markdown/extensions/superfences.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ md = markdown.Markdown(extensions=['pymdownx.superfences'])
4242
exempted.
4343

4444
5. If you are using a fenced block inside a blockquote, at the very least, the first line of the fenced block needs to
45-
have the appropriate number of `>` characters signifying the quote depth.
45+
have the appropriate number of `>` characters signifying the quote depth. This i
4646

4747
````
4848
> ```
4949
a fenced block
5050
```
5151
````
5252
53-
6. Too many blank lines will cause a blockquote to terminate, so remember to use `>` markers accordingly if not marking
53+
Too many blank lines will cause a blockquote to terminate, so remember to use `>` markers accordingly if not marking
5454
every line.
5555
5656
````
@@ -61,7 +61,22 @@ md = markdown.Markdown(extensions=['pymdownx.superfences'])
6161
```
6262
````
6363
64-
7. If using a fenced block as the first line of a list, you will have to leave the first line blank, but remember that
64+
If you are using the [Quotes](./quotes.md) extension, you must include the entire fenced code block within the
65+
blockquote as Quotes purposely does not use lazy blockquote logic.
66+
67+
````
68+
> ```
69+
> a fenced block
70+
> ```
71+
````
72+
73+
/// tip
74+
If you always include the entire code block within the blockquote, you will always get good results and is generally
75+
the recommended approach regardless if you are using the default lazy blockquote logic that comes stock with Python
76+
Markdown or using the [Quotes](./quotes.md) extension.
77+
///
78+
79+
6. If using a fenced block as the first line of a list, you will have to leave the first line blank, but remember that
6580
the list marker must be immediately followed by at least one space. To avoid accidentally deleting the space and to
6681
make your intentions clear, you might want to also add an explicit unicode space (` `) as shown here:
6782
@@ -78,7 +93,7 @@ md = markdown.Markdown(extensions=['pymdownx.superfences'])
7893
```
7994
````
8095
81-
8. Fenced blocks should be separated from other blocks by an empty line.
96+
7. Fenced blocks should be separated from other blocks by an empty line.
8297
8398
````
8499
Paragraph.

docs/src/markdown/faq.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ extensions = [
7676
'pymdownx.emoji',
7777
'pymdownx.tasklist',
7878
'pymdownx.superfences',
79-
'pymdownx.saneheaders'
79+
'pymdownx.saneheaders',
80+
'pymdownx.quotes'
8081
]
8182
8283
extension_configs = {
@@ -103,6 +104,9 @@ extension_configs = {
103104
"image_path": "https://github.githubassets.com/images/icons/emoji/unicode/",
104105
"non_standard_image_path": "https://github.githubassets.com/images/icons/emoji/"
105106
}
107+
},
108+
"pymdownx.quotes": {
109+
"callouts": True
106110
}
107111
}
108112
```

0 commit comments

Comments
 (0)