markwright 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markwright/__init__.py +0 -0
- markwright/_util.py +31 -0
- markwright/cli.py +147 -0
- markwright/codepen.py +206 -0
- markwright/fence.py +483 -0
- markwright/highlight.py +189 -0
- markwright/image_compare.py +128 -0
- markwright/instagram.py +193 -0
- markwright/py.typed +0 -0
- markwright/registry.py +133 -0
- markwright/slideshow.py +150 -0
- markwright/twitter.py +187 -0
- markwright/youtube.py +106 -0
- markwright-0.1.0.dist-info/METADATA +194 -0
- markwright-0.1.0.dist-info/RECORD +20 -0
- markwright-0.1.0.dist-info/WHEEL +4 -0
- markwright-0.1.0.dist-info/entry_points.txt +3 -0
- markwright-0.1.0.dist-info/licenses/LICENSE +21 -0
- markwright-0.1.0.dist-info/licenses/LICENSE-Apache-2.0 +191 -0
- markwright-0.1.0.dist-info/licenses/NOTICE +27 -0
markwright/twitter.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# ABOUTME: Twitter embed extension for Python-Markdown.
|
|
2
|
+
# Converts [twitter URL flags...] syntax to Twitter blockquote embeds with script injection.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import html
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from markdown import Markdown
|
|
10
|
+
from markdown.extensions import Extension
|
|
11
|
+
from markdown.postprocessors import Postprocessor
|
|
12
|
+
from markdown.preprocessors import Preprocessor
|
|
13
|
+
|
|
14
|
+
from markwright._util import URL_SCHEME_WWW_PREFIX
|
|
15
|
+
|
|
16
|
+
# Scheme, "www.", and the twitter.com/x.com host are all optional, and when
|
|
17
|
+
# present must be followed by a "/", so inputs from a bare "user/status/id"
|
|
18
|
+
# up to a full "https://www.twitter.com/user/status/id" all match. Ported
|
|
19
|
+
# verbatim from upstream twitter.js's URL-matching grammar.
|
|
20
|
+
TWITTER_RE = re.compile(
|
|
21
|
+
r"^\[twitter\s+(?:(?:" + URL_SCHEME_WWW_PREFIX + r"(?:twitter|x)\.com)?/)?(\w+)/status/(\d+)"
|
|
22
|
+
r"((?:\s+(?:light|dark|left|center|right|\d+))*)\]$"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
TWITTER_MIN_WIDTH = 250
|
|
26
|
+
TWITTER_MAX_WIDTH = 550
|
|
27
|
+
TWITTER_DEFAULT_WIDTH = 550
|
|
28
|
+
|
|
29
|
+
TWITTER_SIGNATURE = 'class="twitter-tweet"'
|
|
30
|
+
|
|
31
|
+
TWITTER_SCRIPT = '<script async defer src="https://platform.twitter.com/widgets.js"></script>'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_twitter_flags(raw_flags: str) -> dict[str, str | int]:
|
|
35
|
+
"""Parse space-separated Twitter flags into a settings dictionary.
|
|
36
|
+
|
|
37
|
+
:param raw_flags: Raw space-separated flags string.
|
|
38
|
+
:returns: Dictionary with theme, alignment, and width settings.
|
|
39
|
+
"""
|
|
40
|
+
flags = raw_flags.split()
|
|
41
|
+
|
|
42
|
+
theme = "dark" if "dark" in flags else "light"
|
|
43
|
+
|
|
44
|
+
alignment = "center"
|
|
45
|
+
for align_option in ("left", "right", "center"):
|
|
46
|
+
if align_option in flags:
|
|
47
|
+
alignment = align_option
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
width = TWITTER_DEFAULT_WIDTH
|
|
51
|
+
for flag in flags:
|
|
52
|
+
if flag.isdigit():
|
|
53
|
+
width = max(TWITTER_MIN_WIDTH, min(TWITTER_MAX_WIDTH, int(flag)))
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"theme": theme,
|
|
58
|
+
"alignment": alignment,
|
|
59
|
+
"width": width,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _render_match(line: str) -> str | None:
|
|
64
|
+
"""Build the Twitter embed HTML for a standalone embed line.
|
|
65
|
+
|
|
66
|
+
:param line: A single source line.
|
|
67
|
+
:returns: The embed HTML if the line is a Twitter embed, else ``None``.
|
|
68
|
+
"""
|
|
69
|
+
twitter_match = TWITTER_RE.match(line.strip())
|
|
70
|
+
if not twitter_match:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
user = twitter_match.group(1)
|
|
74
|
+
status_id = twitter_match.group(2)
|
|
75
|
+
raw_flags = twitter_match.group(3).strip()
|
|
76
|
+
settings = _parse_twitter_flags(raw_flags)
|
|
77
|
+
|
|
78
|
+
# Canonicalize to twitter.com
|
|
79
|
+
canonical_url = f"https://twitter.com/{user}/status/{status_id}"
|
|
80
|
+
escaped_url = html.escape(canonical_url)
|
|
81
|
+
escaped_user = html.escape(user)
|
|
82
|
+
|
|
83
|
+
theme = settings["theme"]
|
|
84
|
+
alignment = settings["alignment"]
|
|
85
|
+
width = settings["width"]
|
|
86
|
+
|
|
87
|
+
align_attr = f' align="{alignment}"' if alignment != "center" else ""
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
f'<div class="twitter"{align_attr}>\n'
|
|
91
|
+
f' <blockquote class="twitter-tweet" data-dnt="true"'
|
|
92
|
+
f' data-width="{width}" data-theme="{theme}">\n'
|
|
93
|
+
f' <a href="{escaped_url}">View tweet by @{escaped_user}</a>\n'
|
|
94
|
+
f" </blockquote>\n"
|
|
95
|
+
f"</div>"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def expand_source(text: str) -> str:
|
|
100
|
+
"""Expand standalone Twitter embed lines to embed HTML in raw source.
|
|
101
|
+
|
|
102
|
+
Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
|
|
103
|
+
HTML inline without any Python-Markdown stash placeholder.
|
|
104
|
+
|
|
105
|
+
:param text: The source text.
|
|
106
|
+
:returns: The text with standalone Twitter embeds replaced by HTML.
|
|
107
|
+
"""
|
|
108
|
+
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def apply_html(rendered_html: str, warnings: list[str] | None = None) -> str:
|
|
112
|
+
"""Inject the Twitter widgets script once if a Twitter embed is present.
|
|
113
|
+
|
|
114
|
+
Used by the ``mw post`` CLI stage and the in-process postprocessor. The
|
|
115
|
+
script is appended only when the Twitter class signature is present and the
|
|
116
|
+
script is not already injected, so the transform is idempotent.
|
|
117
|
+
|
|
118
|
+
:param rendered_html: Rendered HTML content.
|
|
119
|
+
:param warnings: Optional warnings list; unused (signature injection never warns).
|
|
120
|
+
:returns: HTML with the Twitter script appended if needed.
|
|
121
|
+
"""
|
|
122
|
+
if TWITTER_SIGNATURE in rendered_html and TWITTER_SCRIPT not in rendered_html:
|
|
123
|
+
return rendered_html + "\n" + TWITTER_SCRIPT
|
|
124
|
+
return rendered_html
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TwitterPreprocessor(Preprocessor):
|
|
128
|
+
"""Replace [twitter ...] lines with Twitter embed HTML.
|
|
129
|
+
|
|
130
|
+
:param md: The Markdown instance.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def run(self, lines: list[str]) -> list[str]:
|
|
134
|
+
"""Process lines, replacing Twitter embed syntax with HTML.
|
|
135
|
+
|
|
136
|
+
:param lines: Source lines to process.
|
|
137
|
+
:returns: Modified lines with Twitter embeds replaced by HTML.
|
|
138
|
+
"""
|
|
139
|
+
output: list[str] = []
|
|
140
|
+
for line in lines:
|
|
141
|
+
embed_html = _render_match(line)
|
|
142
|
+
if embed_html is not None:
|
|
143
|
+
output.append(self.md.htmlStash.store(embed_html))
|
|
144
|
+
else:
|
|
145
|
+
output.append(line)
|
|
146
|
+
return output
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TwitterPostprocessor(Postprocessor):
|
|
150
|
+
"""Append the Twitter widgets script tag once if any embeds are present.
|
|
151
|
+
|
|
152
|
+
:param md: The Markdown instance.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def run(self, text: str) -> str:
|
|
156
|
+
"""Append the script tag if a Twitter embed signature is present.
|
|
157
|
+
|
|
158
|
+
:param text: Rendered HTML content.
|
|
159
|
+
:returns: HTML with Twitter script appended if needed.
|
|
160
|
+
"""
|
|
161
|
+
return apply_html(text)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TwitterExtension(Extension):
|
|
165
|
+
"""Python-Markdown extension for Twitter embeds.
|
|
166
|
+
|
|
167
|
+
:param \\*\\*kwargs: Configuration options passed to the extension.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def extendMarkdown(self, md: Markdown) -> None:
|
|
171
|
+
"""Register the Twitter preprocessor and postprocessor.
|
|
172
|
+
|
|
173
|
+
:param md: The Markdown instance to extend.
|
|
174
|
+
"""
|
|
175
|
+
preprocessor = TwitterPreprocessor(md)
|
|
176
|
+
postprocessor = TwitterPostprocessor(md)
|
|
177
|
+
md.preprocessors.register(preprocessor, "do-twitter", 20)
|
|
178
|
+
md.postprocessors.register(postprocessor, "do-twitter-script", 15)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def makeExtension(**kwargs: object) -> TwitterExtension:
|
|
182
|
+
"""Create and return the TwitterExtension instance.
|
|
183
|
+
|
|
184
|
+
:param \\*\\*kwargs: Configuration options.
|
|
185
|
+
:returns: A configured TwitterExtension.
|
|
186
|
+
"""
|
|
187
|
+
return TwitterExtension(**kwargs)
|
markwright/youtube.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ABOUTME: YouTube embed extension for Python-Markdown.
|
|
2
|
+
# Converts [youtube ID height width] syntax to responsive iframe embeds.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import urllib.parse
|
|
8
|
+
|
|
9
|
+
from markdown import Markdown
|
|
10
|
+
from markdown.extensions import Extension
|
|
11
|
+
from markdown.preprocessors import Preprocessor
|
|
12
|
+
|
|
13
|
+
from markwright._util import reduce_fraction
|
|
14
|
+
|
|
15
|
+
YOUTUBE_RE = re.compile(r"^\[youtube (\S+?)(?:\s+(-?\d+))?(?:\s+(-?\d+))?\]$")
|
|
16
|
+
|
|
17
|
+
DEFAULT_HEIGHT = 270
|
|
18
|
+
DEFAULT_WIDTH = 480
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _render_match(line: str) -> str | None:
|
|
22
|
+
"""Build the iframe HTML for a standalone YouTube embed line.
|
|
23
|
+
|
|
24
|
+
:param line: A single source line.
|
|
25
|
+
:returns: The iframe HTML if the line is a YouTube embed, else ``None``.
|
|
26
|
+
"""
|
|
27
|
+
youtube_match = YOUTUBE_RE.match(line.strip())
|
|
28
|
+
if not youtube_match:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
video_id = youtube_match.group(1)
|
|
32
|
+
height = int(youtube_match.group(2)) if youtube_match.group(2) else DEFAULT_HEIGHT
|
|
33
|
+
width = int(youtube_match.group(3)) if youtube_match.group(3) else DEFAULT_WIDTH
|
|
34
|
+
if height <= 0 or width <= 0:
|
|
35
|
+
height, width = DEFAULT_HEIGHT, DEFAULT_WIDTH
|
|
36
|
+
|
|
37
|
+
encoded_id = urllib.parse.quote(video_id, safe="")
|
|
38
|
+
aspect_width, aspect_height = reduce_fraction(width, height)
|
|
39
|
+
aspect_ratio = f"{aspect_width}/{aspect_height}"
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
f'<iframe src="https://www.youtube.com/embed/{encoded_id}"'
|
|
43
|
+
f' class="youtube" height="{height}" width="{width}"'
|
|
44
|
+
f' style="aspect-ratio: {aspect_ratio}" frameborder="0" allowfullscreen>\n'
|
|
45
|
+
f' <a href="https://www.youtube.com/watch?v={encoded_id}"'
|
|
46
|
+
f' target="_blank">View YouTube video</a>\n'
|
|
47
|
+
f"</iframe>"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def expand_source(text: str) -> str:
|
|
52
|
+
"""Expand standalone YouTube embed lines to iframe HTML in raw source.
|
|
53
|
+
|
|
54
|
+
Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
|
|
55
|
+
HTML inline without any Python-Markdown stash placeholder.
|
|
56
|
+
|
|
57
|
+
:param text: The source text.
|
|
58
|
+
:returns: The text with standalone YouTube embeds replaced by iframe HTML.
|
|
59
|
+
"""
|
|
60
|
+
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class YouTubePreprocessor(Preprocessor):
|
|
64
|
+
"""Replace [youtube ...] lines with responsive iframe HTML.
|
|
65
|
+
|
|
66
|
+
:param md: The Markdown instance.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def run(self, lines: list[str]) -> list[str]:
|
|
70
|
+
"""Process lines, replacing YouTube embed syntax with iframe HTML.
|
|
71
|
+
|
|
72
|
+
:param lines: Source lines to process.
|
|
73
|
+
:returns: Modified lines with YouTube embeds replaced by HTML.
|
|
74
|
+
"""
|
|
75
|
+
output: list[str] = []
|
|
76
|
+
for line in lines:
|
|
77
|
+
iframe_html = _render_match(line)
|
|
78
|
+
if iframe_html is not None:
|
|
79
|
+
output.append(self.md.htmlStash.store(iframe_html))
|
|
80
|
+
else:
|
|
81
|
+
output.append(line)
|
|
82
|
+
return output
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class YouTubeExtension(Extension):
|
|
86
|
+
"""Python-Markdown extension for YouTube video embeds.
|
|
87
|
+
|
|
88
|
+
:param \\*\\*kwargs: Configuration options passed to the extension.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def extendMarkdown(self, md: Markdown) -> None:
|
|
92
|
+
"""Register the YouTube preprocessor.
|
|
93
|
+
|
|
94
|
+
:param md: The Markdown instance to extend.
|
|
95
|
+
"""
|
|
96
|
+
preprocessor = YouTubePreprocessor(md)
|
|
97
|
+
md.preprocessors.register(preprocessor, "do-youtube", 20)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def makeExtension(**kwargs: object) -> YouTubeExtension:
|
|
101
|
+
"""Create and return the YouTubeExtension instance.
|
|
102
|
+
|
|
103
|
+
:param \\*\\*kwargs: Configuration options.
|
|
104
|
+
:returns: A configured YouTubeExtension.
|
|
105
|
+
"""
|
|
106
|
+
return YouTubeExtension(**kwargs)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markwright
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python-Markdown extensions ported from DigitalOcean's do-markdownit
|
|
5
|
+
Keywords: markdown,python-markdown,do-markdownit,mkdocs,hugo
|
|
6
|
+
Author: Mason Egger
|
|
7
|
+
Author-email: Mason Egger <mason@masonegger.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: LICENSE-Apache-2.0
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
19
|
+
Requires-Dist: markdown>=3.4
|
|
20
|
+
Requires-Dist: pymdown-extensions>=10.5
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Project-URL: Homepage, https://github.com/MasonEgger/markwright
|
|
23
|
+
Project-URL: Repository, https://github.com/MasonEgger/markwright
|
|
24
|
+
Project-URL: Documentation, https://masonegger.github.io/markwright/
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# markwright
|
|
28
|
+
|
|
29
|
+
A Python port of DigitalOcean's [`do-markdownit`](https://github.com/digitalocean/do-markdownit) JavaScript library.
|
|
30
|
+
|
|
31
|
+
These are [Python-Markdown](https://python-markdown.github.io/) extensions that bring DigitalOcean's markdown syntax (highlighted text, labeled code fences, and a set of media embeds) to any tool built on Python-Markdown, including [MkDocs](https://www.mkdocs.org/), Flask, or a plain script.
|
|
32
|
+
The port maintains compatibility with the original `do-markdownit` HTML output, so rendered pages match what DigitalOcean produces.
|
|
33
|
+
A bundled command-line tool, `mw`, runs the same extensions as pre and post filters around any renderer, so the syntax also works in toolchains that are not built on Python-Markdown, like Hugo.
|
|
34
|
+
|
|
35
|
+
Full documentation and a live demo: https://masonegger.github.io/markwright/
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
Each extension is standalone and can be loaded on its own or alongside the others.
|
|
40
|
+
|
|
41
|
+
- **Highlight** wraps `<^>text<^>` in `<mark>`, and works inside inline code and fenced code blocks where `==text==` cannot reach.
|
|
42
|
+
- **Fence** adds labels, secondary labels, environment classes, and command-line prefixes (line numbers, `$`/`#` prompts, custom prefixes) to code blocks.
|
|
43
|
+
- **YouTube** turns `[youtube ID]` into a responsive iframe embed.
|
|
44
|
+
- **CodePen** embeds a pen with theme, default-tab, and layout options.
|
|
45
|
+
- **Twitter** embeds a tweet with theme and alignment options.
|
|
46
|
+
- **Instagram** embeds a post with caption and alignment options.
|
|
47
|
+
- **Slideshow** builds an image slideshow with navigation arrows.
|
|
48
|
+
- **Image Compare** renders two images with a draggable before/after slider.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv add markwright
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
or
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install markwright
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
To work against a local checkout, install it editable:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add --editable ../markwright
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### With MkDocs
|
|
71
|
+
|
|
72
|
+
Add the extensions to `mkdocs.yml`.
|
|
73
|
+
Load `markwright.fence` after `pymdownx.superfences` and `pymdownx.highlight` so the code-block HTML is in the shape the fence postprocessor expects.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
markdown_extensions:
|
|
77
|
+
- pymdownx.superfences
|
|
78
|
+
- pymdownx.highlight:
|
|
79
|
+
pygments_lang_class: true
|
|
80
|
+
- markwright.highlight
|
|
81
|
+
- markwright.fence:
|
|
82
|
+
allowed_environments:
|
|
83
|
+
- local
|
|
84
|
+
- second
|
|
85
|
+
- third
|
|
86
|
+
- markwright.youtube
|
|
87
|
+
- markwright.codepen
|
|
88
|
+
- markwright.twitter
|
|
89
|
+
- markwright.instagram
|
|
90
|
+
- markwright.slideshow
|
|
91
|
+
- markwright.image_compare
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Standalone
|
|
95
|
+
|
|
96
|
+
The extensions work with Python-Markdown directly, so you can use them in any Python program.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import markdown
|
|
100
|
+
|
|
101
|
+
md = markdown.Markdown(extensions=[
|
|
102
|
+
"pymdownx.superfences",
|
|
103
|
+
"pymdownx.highlight",
|
|
104
|
+
"markwright.highlight",
|
|
105
|
+
"markwright.fence",
|
|
106
|
+
"markwright.youtube",
|
|
107
|
+
"markwright.codepen",
|
|
108
|
+
"markwright.twitter",
|
|
109
|
+
"markwright.instagram",
|
|
110
|
+
"markwright.slideshow",
|
|
111
|
+
"markwright.image_compare",
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
html = md.convert("This has a <^>highlighted word<^> in it.")
|
|
115
|
+
print(html)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Load only what you need:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import markdown
|
|
122
|
+
|
|
123
|
+
# Just YouTube embeds
|
|
124
|
+
md = markdown.Markdown(extensions=["markwright.youtube"])
|
|
125
|
+
html = md.convert("[youtube dQw4w9WgXcQ]")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The fence extension expects `pymdownx.superfences` and `pymdownx.highlight` to be loaded too.
|
|
129
|
+
Without them the preprocessor still extracts directives, but the code-block HTML may not match what the postprocessor rewrites.
|
|
130
|
+
|
|
131
|
+
## Command-Line Interface (`mw`)
|
|
132
|
+
|
|
133
|
+
For a toolchain that is not built on Python-Markdown, the `mw` command runs the same extensions as a pair of Unix filters around any renderer: a pre stage on the Markdown source, and a post stage on the rendered HTML.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mw pre < in.md | your-renderer | mw post > out.html
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `mw pre` expands the embeds and extracts fence directives into an `mw-fence` comment.
|
|
140
|
+
- `mw post` applies the fence styling, resolves the highlights, and injects each embed script once.
|
|
141
|
+
- `mw render` runs the full pipeline in one shot for callers without their own renderer.
|
|
142
|
+
- `mw list` prints every extension and the stages it provides.
|
|
143
|
+
|
|
144
|
+
Your renderer must pass raw HTML and HTML comments through.
|
|
145
|
+
Hugo is a worked, tested example: see the [Hugo guide](https://masonegger.github.io/markwright/integrations/hugo/).
|
|
146
|
+
The full command and flag reference is in the [CLI docs](https://masonegger.github.io/markwright/cli/).
|
|
147
|
+
|
|
148
|
+
## Syntax at a Glance
|
|
149
|
+
|
|
150
|
+
````text
|
|
151
|
+
Highlight: <^>important<^>
|
|
152
|
+
|
|
153
|
+
Fenced code with a label and command prompts:
|
|
154
|
+
```command
|
|
155
|
+
[label deploy.sh]
|
|
156
|
+
ssh root@server
|
|
157
|
+
apt update
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
YouTube: [youtube dQw4w9WgXcQ]
|
|
161
|
+
[youtube dQw4w9WgXcQ 360 640]
|
|
162
|
+
CodePen: [codepen MattCowley vwPzeX]
|
|
163
|
+
[codepen MattCowley vwPzeX dark css]
|
|
164
|
+
Twitter: [twitter https://twitter.com/github/status/1234567890]
|
|
165
|
+
Instagram: [instagram https://www.instagram.com/p/CkQuv3_LRgS]
|
|
166
|
+
Slideshow: [slideshow https://picsum.photos/id/10/480/270 https://picsum.photos/id/20/480/270]
|
|
167
|
+
Image Compare: [compare before.jpg after.jpg]
|
|
168
|
+
````
|
|
169
|
+
|
|
170
|
+
See the [extension docs](https://masonegger.github.io/markwright/) for every flag and the exact HTML each one produces.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
This project uses [`uv`](https://docs.astral.sh/uv/) and [`just`](https://github.com/casey/just).
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
just install # uv sync
|
|
178
|
+
just test # unit tests, 100% line + branch coverage
|
|
179
|
+
just test-integration # end-to-end Hugo pipeline test (needs hugo)
|
|
180
|
+
just lint # ruff check + ruff format --check
|
|
181
|
+
just typecheck # mypy --strict
|
|
182
|
+
just check # test + lint + typecheck
|
|
183
|
+
just docs-serve # serve the docs site at localhost:8000
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`just check` must pass before a change is complete.
|
|
187
|
+
The tooling bar is strict: `mypy --strict`, a clean `ruff`, and full test coverage.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
markwright is released under the [MIT License](LICENSE).
|
|
192
|
+
|
|
193
|
+
It is a port of [`do-markdownit`](https://github.com/digitalocean/do-markdownit) by DigitalOcean, which is licensed under the Apache License 2.0.
|
|
194
|
+
See the [NOTICE](NOTICE) file for attribution details.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
markwright/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
markwright/_util.py,sha256=BI3Ams_h52bPjUnDWicSeacXULmxpNZmQIDj9st9ZQU,1279
|
|
3
|
+
markwright/cli.py,sha256=u-xX6_hiVh_NjNyuEhBiH8b0uftw4gRynluA_lOY4Fo,5565
|
|
4
|
+
markwright/codepen.py,sha256=R4Wo8y2znGUZuREUo-VZVIxJ2xePk30vqi1vs6lV92c,6815
|
|
5
|
+
markwright/fence.py,sha256=MQVYAFM-alFIJAEnRflyxs5HeKFoV73ZHrDIIzrUuF4,19768
|
|
6
|
+
markwright/highlight.py,sha256=Bzgz8-RFmUQ2EIEwJiDQ9n80yzRqwMZNqHh_DnhWzbs,7648
|
|
7
|
+
markwright/image_compare.py,sha256=EID0oa8T-uNs35KBq5WwKhS640GJmUs3AM2CTLM9Xzo,4810
|
|
8
|
+
markwright/instagram.py,sha256=2rJwUpqejJdOoXopLvCTATiSOXrFDPbPGaX_qIF11ZU,6644
|
|
9
|
+
markwright/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
markwright/registry.py,sha256=6meSIct5DdkVV_t3cX0waYit_YmJ-IRh2jJiOIgaLKc,5935
|
|
11
|
+
markwright/slideshow.py,sha256=rv25PkhoYTADLuIAJ_UWU3bMm1_kFrQermAbPxI7iWg,4841
|
|
12
|
+
markwright/twitter.py,sha256=t8yzuF5rr_mz4fDmDok0v2FMlFb6_yeocR1eNbwM8qc,6261
|
|
13
|
+
markwright/youtube.py,sha256=-GaDUZneA1Jji66yZvc1HmK58VhHwBxv_O00Xrkd8i0,3535
|
|
14
|
+
markwright-0.1.0.dist-info/licenses/LICENSE,sha256=2lCfnjZ3jo5wTPiXG_2XesGsPbfJyhAYlCd3kTmYCio,1068
|
|
15
|
+
markwright-0.1.0.dist-info/licenses/LICENSE-Apache-2.0,sha256=DlD_JqT3u3kroA5EzJNq_Bl5b7hSImEASqHAVdBIckg,10760
|
|
16
|
+
markwright-0.1.0.dist-info/licenses/NOTICE,sha256=iANynyRpH3OWLtlIHOFDUxn304VgEobUKKexJy4kYBs,1204
|
|
17
|
+
markwright-0.1.0.dist-info/WHEEL,sha256=_d8F1e7SqtoW6CDj4Gi8lFC26a_7I17R7zPLCKTp4Fg,81
|
|
18
|
+
markwright-0.1.0.dist-info/entry_points.txt,sha256=b2GIDio68udBvjKs86sxzirrQlwaCxPm6jWAk7yMMR0,44
|
|
19
|
+
markwright-0.1.0.dist-info/METADATA,sha256=9QXUMFUPnBINSRsF-KlX8cvxLZ2Ajpoh1wtwFMzeuRs,7058
|
|
20
|
+
markwright-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mason Egger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|