mkdocs-markdoc 0.1.0__tar.gz
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.
- mkdocs_markdoc-0.1.0/.gitignore +24 -0
- mkdocs_markdoc-0.1.0/PKG-INFO +174 -0
- mkdocs_markdoc-0.1.0/README.md +151 -0
- mkdocs_markdoc-0.1.0/mkdocs_markdoc/__init__.py +1 -0
- mkdocs_markdoc-0.1.0/mkdocs_markdoc/assets/markdoc.css +95 -0
- mkdocs_markdoc-0.1.0/mkdocs_markdoc/markdoc_runner.js +137 -0
- mkdocs_markdoc-0.1.0/mkdocs_markdoc/plugin.py +246 -0
- mkdocs_markdoc-0.1.0/pyproject.toml +58 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Node
|
|
15
|
+
node_modules/
|
|
16
|
+
|
|
17
|
+
# MkDocs built output
|
|
18
|
+
example/site/
|
|
19
|
+
|
|
20
|
+
# Editor / OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
.idea/
|
|
23
|
+
.vscode/
|
|
24
|
+
*.swp
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mkdocs-markdoc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MkDocs plugin that renders pages with Stripe's Markdoc instead of Python-Markdown
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: documentation,markdoc,markdown,mkdocs,plugin
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Documentation
|
|
16
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: mkdocs>=1.5
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest-mock>=3; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# mkdocs-markdoc
|
|
25
|
+
|
|
26
|
+
An MkDocs plugin that completely replaces the default Python-Markdown renderer
|
|
27
|
+
with **[Stripe's Markdoc](https://markdoc.dev)**. Every `.md` page in your
|
|
28
|
+
docs site is parsed and rendered by Markdoc's HTML renderer — no React
|
|
29
|
+
required.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
MkDocs ──on_page_markdown──▶ plugin.py ──stdin──▶ markdoc_runner.js
|
|
37
|
+
│
|
|
38
|
+
HTML string ◀──stdout──────────────┘
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The Python plugin hooks into `on_page_markdown`, pipes the raw Markdown to a
|
|
42
|
+
bundled Node.js script via `subprocess`, and returns the HTML string that
|
|
43
|
+
MkDocs injects into the theme template.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Prerequisites
|
|
48
|
+
|
|
49
|
+
| Requirement | Minimum version |
|
|
50
|
+
|-------------|----------------|
|
|
51
|
+
| Python | 3.9 |
|
|
52
|
+
| MkDocs | 1.5 |
|
|
53
|
+
| Node.js | 18 LTS |
|
|
54
|
+
| npm | 8 |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
### 1 — Install the Python package
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# From the repo root (editable/development install)
|
|
64
|
+
pip install -e .
|
|
65
|
+
|
|
66
|
+
# Or once published to PyPI
|
|
67
|
+
pip install mkdocs-markdoc
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2 — Install the Node.js Markdoc library
|
|
71
|
+
|
|
72
|
+
The `@markdoc/markdoc` npm package must be resolvable by Node.js when it runs
|
|
73
|
+
the bundled `markdoc_runner.js` script. Install it in **one** of these
|
|
74
|
+
locations (Node's module resolution will find it):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Option A – global install (simplest for local dev / CI)
|
|
78
|
+
npm install -g @markdoc/markdoc
|
|
79
|
+
|
|
80
|
+
# Option B – local install in your docs project root
|
|
81
|
+
cd /path/to/your/docs-project
|
|
82
|
+
npm install @markdoc/markdoc
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3 — Enable the plugin in `mkdocs.yml`
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
# mkdocs.yml
|
|
89
|
+
site_name: My Docs
|
|
90
|
+
|
|
91
|
+
plugins:
|
|
92
|
+
- markdoc # ← add this; remove or comment out the default 'search'
|
|
93
|
+
# plugin only if you no longer need it
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> **Note:** MkDocs' built-in `search` plugin is independent of the Markdown
|
|
97
|
+
> renderer and can be kept alongside `markdoc`:
|
|
98
|
+
> ```yaml
|
|
99
|
+
> plugins:
|
|
100
|
+
> - search
|
|
101
|
+
> - markdoc
|
|
102
|
+
> ```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Configuration options
|
|
107
|
+
|
|
108
|
+
All options are optional.
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
plugins:
|
|
112
|
+
- markdoc:
|
|
113
|
+
# Path to the Node.js executable.
|
|
114
|
+
# Default: "node" (resolved via $PATH)
|
|
115
|
+
node_path: /usr/local/bin/node
|
|
116
|
+
|
|
117
|
+
# Path to a JS or JSON file that exports a Markdoc config object.
|
|
118
|
+
# When omitted, Markdoc uses its built-in defaults (standard Markdown
|
|
119
|
+
# nodes, no custom tags or functions).
|
|
120
|
+
markdoc_config: docs/markdoc.config.js
|
|
121
|
+
|
|
122
|
+
# Milliseconds to wait for the Node subprocess before raising an error.
|
|
123
|
+
# Default: 30000 (30 seconds)
|
|
124
|
+
timeout: 30000
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Example `markdoc.config.js`
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// docs/markdoc.config.js
|
|
131
|
+
const { nodes, Tag } = require("@markdoc/markdoc");
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
tags: {
|
|
135
|
+
callout: {
|
|
136
|
+
render: "div",
|
|
137
|
+
attributes: {
|
|
138
|
+
type: { type: String, default: "note" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
nodes: {
|
|
143
|
+
// Override the default heading to add anchor IDs
|
|
144
|
+
heading: {
|
|
145
|
+
...nodes.heading,
|
|
146
|
+
render: "h1",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Running the docs locally
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
mkdocs serve
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Caveats & trade-offs
|
|
163
|
+
|
|
164
|
+
* **Markdoc syntax differs from CommonMark.** Markdoc is a superset of
|
|
165
|
+
Markdown, but some edge-cases render differently. Review the
|
|
166
|
+
[Markdoc syntax reference](https://markdoc.dev/docs/syntax) when migrating
|
|
167
|
+
an existing docs site.
|
|
168
|
+
* **Node.js subprocess overhead.** Each page spawns (and immediately exits) one
|
|
169
|
+
Node process. For large sites with hundreds of pages the build time will
|
|
170
|
+
increase compared to the native Python-Markdown renderer. If this becomes a
|
|
171
|
+
bottleneck, consider batching pages in a future version.
|
|
172
|
+
* **MkDocs extensions are bypassed.** Because we skip Python-Markdown entirely,
|
|
173
|
+
any `markdown_extensions:` listed in `mkdocs.yml` will have no effect.
|
|
174
|
+
Equivalent behaviour must be implemented via Markdoc tags/nodes/functions.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# mkdocs-markdoc
|
|
2
|
+
|
|
3
|
+
An MkDocs plugin that completely replaces the default Python-Markdown renderer
|
|
4
|
+
with **[Stripe's Markdoc](https://markdoc.dev)**. Every `.md` page in your
|
|
5
|
+
docs site is parsed and rendered by Markdoc's HTML renderer — no React
|
|
6
|
+
required.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
MkDocs ──on_page_markdown──▶ plugin.py ──stdin──▶ markdoc_runner.js
|
|
14
|
+
│
|
|
15
|
+
HTML string ◀──stdout──────────────┘
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The Python plugin hooks into `on_page_markdown`, pipes the raw Markdown to a
|
|
19
|
+
bundled Node.js script via `subprocess`, and returns the HTML string that
|
|
20
|
+
MkDocs injects into the theme template.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
| Requirement | Minimum version |
|
|
27
|
+
|-------------|----------------|
|
|
28
|
+
| Python | 3.9 |
|
|
29
|
+
| MkDocs | 1.5 |
|
|
30
|
+
| Node.js | 18 LTS |
|
|
31
|
+
| npm | 8 |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### 1 — Install the Python package
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# From the repo root (editable/development install)
|
|
41
|
+
pip install -e .
|
|
42
|
+
|
|
43
|
+
# Or once published to PyPI
|
|
44
|
+
pip install mkdocs-markdoc
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2 — Install the Node.js Markdoc library
|
|
48
|
+
|
|
49
|
+
The `@markdoc/markdoc` npm package must be resolvable by Node.js when it runs
|
|
50
|
+
the bundled `markdoc_runner.js` script. Install it in **one** of these
|
|
51
|
+
locations (Node's module resolution will find it):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Option A – global install (simplest for local dev / CI)
|
|
55
|
+
npm install -g @markdoc/markdoc
|
|
56
|
+
|
|
57
|
+
# Option B – local install in your docs project root
|
|
58
|
+
cd /path/to/your/docs-project
|
|
59
|
+
npm install @markdoc/markdoc
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3 — Enable the plugin in `mkdocs.yml`
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
# mkdocs.yml
|
|
66
|
+
site_name: My Docs
|
|
67
|
+
|
|
68
|
+
plugins:
|
|
69
|
+
- markdoc # ← add this; remove or comment out the default 'search'
|
|
70
|
+
# plugin only if you no longer need it
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Note:** MkDocs' built-in `search` plugin is independent of the Markdown
|
|
74
|
+
> renderer and can be kept alongside `markdoc`:
|
|
75
|
+
> ```yaml
|
|
76
|
+
> plugins:
|
|
77
|
+
> - search
|
|
78
|
+
> - markdoc
|
|
79
|
+
> ```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Configuration options
|
|
84
|
+
|
|
85
|
+
All options are optional.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
plugins:
|
|
89
|
+
- markdoc:
|
|
90
|
+
# Path to the Node.js executable.
|
|
91
|
+
# Default: "node" (resolved via $PATH)
|
|
92
|
+
node_path: /usr/local/bin/node
|
|
93
|
+
|
|
94
|
+
# Path to a JS or JSON file that exports a Markdoc config object.
|
|
95
|
+
# When omitted, Markdoc uses its built-in defaults (standard Markdown
|
|
96
|
+
# nodes, no custom tags or functions).
|
|
97
|
+
markdoc_config: docs/markdoc.config.js
|
|
98
|
+
|
|
99
|
+
# Milliseconds to wait for the Node subprocess before raising an error.
|
|
100
|
+
# Default: 30000 (30 seconds)
|
|
101
|
+
timeout: 30000
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Example `markdoc.config.js`
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// docs/markdoc.config.js
|
|
108
|
+
const { nodes, Tag } = require("@markdoc/markdoc");
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
tags: {
|
|
112
|
+
callout: {
|
|
113
|
+
render: "div",
|
|
114
|
+
attributes: {
|
|
115
|
+
type: { type: String, default: "note" },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
nodes: {
|
|
120
|
+
// Override the default heading to add anchor IDs
|
|
121
|
+
heading: {
|
|
122
|
+
...nodes.heading,
|
|
123
|
+
render: "h1",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Running the docs locally
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
mkdocs serve
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Caveats & trade-offs
|
|
140
|
+
|
|
141
|
+
* **Markdoc syntax differs from CommonMark.** Markdoc is a superset of
|
|
142
|
+
Markdown, but some edge-cases render differently. Review the
|
|
143
|
+
[Markdoc syntax reference](https://markdoc.dev/docs/syntax) when migrating
|
|
144
|
+
an existing docs site.
|
|
145
|
+
* **Node.js subprocess overhead.** Each page spawns (and immediately exits) one
|
|
146
|
+
Node process. For large sites with hundreds of pages the build time will
|
|
147
|
+
increase compared to the native Python-Markdown renderer. If this becomes a
|
|
148
|
+
bottleneck, consider batching pages in a future version.
|
|
149
|
+
* **MkDocs extensions are bypassed.** Because we skip Python-Markdown entirely,
|
|
150
|
+
any `markdown_extensions:` listed in `mkdocs.yml` will have no effect.
|
|
151
|
+
Equivalent behaviour must be implemented via Markdoc tags/nodes/functions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# mkdocs-markdoc: MkDocs plugin that renders pages with Stripe's Markdoc
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* markdoc.css — styling for elements that Markdoc emits but Material doesn't
|
|
3
|
+
* cover on its own (badges, details, code block tweaks).
|
|
4
|
+
*
|
|
5
|
+
* Most standard HTML elements (headings, tables, blockquotes, lists, etc.)
|
|
6
|
+
* are already styled by Material's .md-typeset rules because our plugin output
|
|
7
|
+
* lands inside that wrapper.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* -------------------------------------------------------------------------
|
|
11
|
+
* Code blocks
|
|
12
|
+
* Our fence override renders <pre><code class="language-X">.
|
|
13
|
+
* highlight.js adds the `hljs` class; we normalise padding to match Material.
|
|
14
|
+
* ---------------------------------------------------------------------- */
|
|
15
|
+
|
|
16
|
+
.md-typeset pre > code.hljs {
|
|
17
|
+
padding: 1em 1.2em;
|
|
18
|
+
border-radius: 0.2rem;
|
|
19
|
+
font-size: 0.85em;
|
|
20
|
+
line-height: 1.6;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.md-typeset pre {
|
|
24
|
+
position: relative;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* -------------------------------------------------------------------------
|
|
28
|
+
* Admonitions (callout → Material .admonition markup, styled for free)
|
|
29
|
+
* ---------------------------------------------------------------------- */
|
|
30
|
+
|
|
31
|
+
.md-typeset .admonition {
|
|
32
|
+
margin: 1.5em 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* -------------------------------------------------------------------------
|
|
36
|
+
* Badge (inline colored chips)
|
|
37
|
+
* ---------------------------------------------------------------------- */
|
|
38
|
+
|
|
39
|
+
.md-typeset .mkd-badge {
|
|
40
|
+
display: inline-block;
|
|
41
|
+
padding: 0.1em 0.55em;
|
|
42
|
+
border-radius: 0.25rem;
|
|
43
|
+
font-size: 0.72em;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
letter-spacing: 0.02em;
|
|
46
|
+
line-height: 1.6;
|
|
47
|
+
vertical-align: middle;
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.md-typeset .mkd-badge--blue { background-color: #1976d2; color: #fff; }
|
|
52
|
+
.md-typeset .mkd-badge--green { background-color: #388e3c; color: #fff; }
|
|
53
|
+
.md-typeset .mkd-badge--red { background-color: #d32f2f; color: #fff; }
|
|
54
|
+
.md-typeset .mkd-badge--orange { background-color: #e65100; color: #fff; }
|
|
55
|
+
.md-typeset .mkd-badge--grey { background-color: #616161; color: #fff; }
|
|
56
|
+
.md-typeset .mkd-badge--purple { background-color: #6a1b9a; color: #fff; }
|
|
57
|
+
|
|
58
|
+
/* -------------------------------------------------------------------------
|
|
59
|
+
* Details (collapsible <details>/<summary>)
|
|
60
|
+
*
|
|
61
|
+
* Material for MkDocs already styles all `details > summary` elements with
|
|
62
|
+
* its own disclosure icon (the right-side chevron) and flex layout. Adding
|
|
63
|
+
* our own ::before marker on top produces a double-icon overlap and pushes
|
|
64
|
+
* text mid-word. We therefore only set box-level styles here and let
|
|
65
|
+
* Material's built-in `details` CSS handle the indicator entirely.
|
|
66
|
+
* ---------------------------------------------------------------------- */
|
|
67
|
+
|
|
68
|
+
.md-typeset details.mkd-details {
|
|
69
|
+
border-left: 4px solid var(--md-primary-fg-color);
|
|
70
|
+
background-color: var(--md-admonition-bg-color, rgba(68, 138, 255, .05));
|
|
71
|
+
border-radius: 0.2rem;
|
|
72
|
+
padding: 0 1em;
|
|
73
|
+
margin: 1.25em 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Tighten summary text weight to make it feel like a heading */
|
|
77
|
+
.md-typeset details.mkd-details > summary {
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Separate summary from body when open */
|
|
82
|
+
.md-typeset details.mkd-details[open] > summary {
|
|
83
|
+
border-bottom: 1px solid var(--md-default-fg-color--lightest);
|
|
84
|
+
margin-bottom: 0.25em;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* -------------------------------------------------------------------------
|
|
88
|
+
* Tables
|
|
89
|
+
* Markdoc renders plain <table> elements; Material styles them inside
|
|
90
|
+
* .md-typeset automatically. Enforce full width for consistency.
|
|
91
|
+
* ---------------------------------------------------------------------- */
|
|
92
|
+
|
|
93
|
+
.md-typeset table:not([class]) {
|
|
94
|
+
width: 100%;
|
|
95
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* markdoc_runner.js
|
|
4
|
+
*
|
|
5
|
+
* Reads raw Markdown from stdin, renders it to HTML with @markdoc/markdoc,
|
|
6
|
+
* and writes the HTML to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Usage (invoked by the Python plugin):
|
|
9
|
+
* echo "# Hello" | node markdoc_runner.js [--config <path>]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 success – HTML written to stdout
|
|
13
|
+
* 1 error – message written to stderr
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const path = require("path");
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Argument parsing (minimal – only --config <path> is supported)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const args = { configPath: null };
|
|
27
|
+
for (let i = 2; i < argv.length; i++) {
|
|
28
|
+
if (argv[i] === "--config" && argv[i + 1]) {
|
|
29
|
+
args.configPath = path.resolve(argv[++i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Load optional Markdoc config
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function loadMarkdocConfig(configPath) {
|
|
40
|
+
if (!configPath) return {};
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(configPath)) {
|
|
43
|
+
throw new Error(`Markdoc config file not found: ${configPath}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ext = path.extname(configPath).toLowerCase();
|
|
47
|
+
|
|
48
|
+
if (ext === ".json") {
|
|
49
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Treat .js / .cjs as a CommonJS module exporting a config object.
|
|
53
|
+
// The module may export the config directly or as `module.exports.default`.
|
|
54
|
+
const mod = require(configPath);
|
|
55
|
+
return mod.default ?? mod;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Render pipeline
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function renderMarkdoc(source, markdocConfig) {
|
|
63
|
+
// Lazy-require so a missing package produces a clear error message.
|
|
64
|
+
let Markdoc;
|
|
65
|
+
try {
|
|
66
|
+
Markdoc = require("@markdoc/markdoc");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"@markdoc/markdoc is not installed. " +
|
|
70
|
+
"Run `npm install @markdoc/markdoc` in the plugin directory or globally.\n" +
|
|
71
|
+
err.message
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 1. Parse – produces an AST.
|
|
76
|
+
const ast = Markdoc.parse(source);
|
|
77
|
+
|
|
78
|
+
// 2. Validate – surface any Markdoc schema errors before transforming.
|
|
79
|
+
const errors = Markdoc.validate(ast, markdocConfig);
|
|
80
|
+
if (errors.length > 0) {
|
|
81
|
+
const messages = errors
|
|
82
|
+
.map((e) => ` [${e.error.level}] ${e.error.message} (line ${e.lines?.[0] ?? "?"})`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
|
|
85
|
+
// Only hard-fail on actual errors; warnings/hints are logged to stderr.
|
|
86
|
+
const fatal = errors.filter((e) => e.error.level === "error");
|
|
87
|
+
if (fatal.length > 0) {
|
|
88
|
+
throw new Error(`Markdoc validation errors:\n${messages}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process.stderr.write(`mkdocs-markdoc: Markdoc warnings:\n${messages}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Transform – converts AST to a renderable tree using the config.
|
|
95
|
+
const renderableTree = Markdoc.transform(ast, markdocConfig);
|
|
96
|
+
|
|
97
|
+
// 4. Render to plain HTML (no React dependency).
|
|
98
|
+
return Markdoc.renderers.html(renderableTree);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Main – read stdin, render, write stdout
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
const args = parseArgs(process.argv);
|
|
107
|
+
|
|
108
|
+
let markdocConfig;
|
|
109
|
+
try {
|
|
110
|
+
markdocConfig = loadMarkdocConfig(args.configPath);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
process.stderr.write(`mkdocs-markdoc: failed to load config: ${err.message}\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Collect stdin into a single string.
|
|
117
|
+
const chunks = [];
|
|
118
|
+
for await (const chunk of process.stdin) {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
}
|
|
121
|
+
const source = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
|
|
122
|
+
|
|
123
|
+
let html;
|
|
124
|
+
try {
|
|
125
|
+
html = renderMarkdoc(source, markdocConfig);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
process.stderr.write(`mkdocs-markdoc: render error: ${err.message}\n`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.stdout.write(html);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main().catch((err) => {
|
|
135
|
+
process.stderr.write(`mkdocs-markdoc: unexpected error: ${err.message}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MkDocs plugin that replaces the default Python-Markdown renderer with
|
|
3
|
+
Stripe's Markdoc, via a bundled Node.js subprocess.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from html.parser import HTMLParser
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mkdocs.config import config_options
|
|
16
|
+
from mkdocs.config.base import Config
|
|
17
|
+
from mkdocs.plugins import BasePlugin
|
|
18
|
+
from mkdocs.structure.files import File, Files
|
|
19
|
+
from mkdocs.structure.pages import Page
|
|
20
|
+
from mkdocs.structure.toc import AnchorLink, TableOfContents
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger("mkdocs.plugins.markdoc")
|
|
23
|
+
|
|
24
|
+
# Absolute path to the bundled Node.js runner so it works regardless of cwd.
|
|
25
|
+
_RUNNER_PATH = Path(__file__).parent / "markdoc_runner.js"
|
|
26
|
+
|
|
27
|
+
_HEADING_TAGS = frozenset({"h1", "h2", "h3", "h4", "h5", "h6"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _HeadingParser(HTMLParser):
|
|
31
|
+
"""Extract heading text + existing id attributes from rendered HTML."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.headings: list[dict] = []
|
|
36
|
+
self._tag: str = ""
|
|
37
|
+
self._id: str = ""
|
|
38
|
+
self._text: list[str] = []
|
|
39
|
+
|
|
40
|
+
def handle_starttag(self, tag: str, attrs: list) -> None:
|
|
41
|
+
if tag in _HEADING_TAGS:
|
|
42
|
+
self._tag = tag
|
|
43
|
+
self._id = dict(attrs).get("id", "")
|
|
44
|
+
self._text = []
|
|
45
|
+
|
|
46
|
+
def handle_endtag(self, tag: str) -> None:
|
|
47
|
+
if tag == self._tag and tag in _HEADING_TAGS:
|
|
48
|
+
self.headings.append({
|
|
49
|
+
"level": int(self._tag[1]),
|
|
50
|
+
"id": self._id,
|
|
51
|
+
"text": "".join(self._text).strip(),
|
|
52
|
+
})
|
|
53
|
+
self._tag = ""
|
|
54
|
+
|
|
55
|
+
def handle_data(self, data: str) -> None:
|
|
56
|
+
if self._tag:
|
|
57
|
+
self._text.append(data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _toc_from_html(html: str) -> TableOfContents:
|
|
61
|
+
"""
|
|
62
|
+
Build a MkDocs TableOfContents from <hN id="..."> elements in the HTML.
|
|
63
|
+
|
|
64
|
+
The heading node override in markdoc.config.js guarantees every heading
|
|
65
|
+
already carries an id attribute, so no HTML modification is needed here.
|
|
66
|
+
Headings without an id (e.g. those inside custom tag blocks that swallow
|
|
67
|
+
them) are silently skipped.
|
|
68
|
+
"""
|
|
69
|
+
parser = _HeadingParser()
|
|
70
|
+
parser.feed(html)
|
|
71
|
+
|
|
72
|
+
top: list[AnchorLink] = []
|
|
73
|
+
stack: list[AnchorLink] = []
|
|
74
|
+
|
|
75
|
+
for h in parser.headings:
|
|
76
|
+
if not h["id"]:
|
|
77
|
+
continue
|
|
78
|
+
link = AnchorLink(title=h["text"], id=h["id"], level=h["level"])
|
|
79
|
+
while stack and stack[-1].level >= h["level"]:
|
|
80
|
+
stack.pop()
|
|
81
|
+
if stack:
|
|
82
|
+
stack[-1].children.append(link)
|
|
83
|
+
else:
|
|
84
|
+
top.append(link)
|
|
85
|
+
stack.append(link)
|
|
86
|
+
|
|
87
|
+
return TableOfContents(top)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MarkdocPluginConfig(Config):
|
|
91
|
+
# Path to the `node` executable. Defaults to whatever is on $PATH.
|
|
92
|
+
node_path = config_options.Type(str, default="node")
|
|
93
|
+
|
|
94
|
+
# Optional: path to a Markdoc config JS/JSON file that the runner will
|
|
95
|
+
# `require()`. When empty the runner uses bare Markdoc defaults.
|
|
96
|
+
markdoc_config = config_options.Optional(config_options.File(exists=True))
|
|
97
|
+
|
|
98
|
+
# Milliseconds before the Node subprocess is killed and an error raised.
|
|
99
|
+
timeout = config_options.Type(int, default=30_000)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MarkdocPlugin(BasePlugin[MarkdocPluginConfig]):
|
|
103
|
+
"""
|
|
104
|
+
Intercepts raw Markdown on every page and hands it to the Node.js Markdoc
|
|
105
|
+
runner. The runner returns a plain HTML string that MkDocs then injects
|
|
106
|
+
into its theme template exactly as it would with the normal renderer.
|
|
107
|
+
|
|
108
|
+
Lifecycle
|
|
109
|
+
---------
|
|
110
|
+
on_config – validate that Node.js is available once, up front.
|
|
111
|
+
on_page_markdown – convert each page's Markdown to HTML via subprocess.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def on_config(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
115
|
+
node_exec = self.config["node_path"]
|
|
116
|
+
|
|
117
|
+
# Resolve "node" to a full path so the error message is unambiguous.
|
|
118
|
+
resolved = shutil.which(node_exec)
|
|
119
|
+
if resolved is None:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"mkdocs-markdoc: Node.js executable '{node_exec}' not found. "
|
|
122
|
+
"Install Node.js (https://nodejs.org) or set the `node_path` "
|
|
123
|
+
"option in your mkdocs.yml plugin configuration."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self._node_exec = resolved
|
|
127
|
+
log.debug("mkdocs-markdoc: using Node.js at %s", resolved)
|
|
128
|
+
|
|
129
|
+
# Inject the bundled stylesheet so it loads before any user extra_css.
|
|
130
|
+
config.setdefault("extra_css", []).insert(0, "assets/markdoc.css")
|
|
131
|
+
|
|
132
|
+
# Verify @markdoc/markdoc is installed where the runner can reach it.
|
|
133
|
+
check = self._run_node(
|
|
134
|
+
"require('@markdoc/markdoc'); process.stdout.write('ok');"
|
|
135
|
+
)
|
|
136
|
+
if check.returncode != 0 or check.stdout.strip() != "ok":
|
|
137
|
+
stderr = check.stderr.strip()
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
"mkdocs-markdoc: @markdoc/markdoc is not importable from the "
|
|
140
|
+
"Node.js runner. Run `npm install @markdoc/markdoc` (globally "
|
|
141
|
+
f"or in the project directory).\nNode stderr: {stderr}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return config
|
|
145
|
+
|
|
146
|
+
def on_files(self, files: Files, config: dict[str, Any], **kwargs: Any) -> Files:
|
|
147
|
+
"""Inject the bundled markdoc.css into the MkDocs file collection."""
|
|
148
|
+
files.append(File(
|
|
149
|
+
path="assets/markdoc.css",
|
|
150
|
+
src_dir=str(Path(__file__).parent),
|
|
151
|
+
dest_dir=config["site_dir"],
|
|
152
|
+
use_directory_urls=config["use_directory_urls"],
|
|
153
|
+
))
|
|
154
|
+
return files
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# Core hook
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def on_page_markdown(
|
|
161
|
+
self,
|
|
162
|
+
markdown: str,
|
|
163
|
+
page: Page,
|
|
164
|
+
config: dict[str, Any],
|
|
165
|
+
**kwargs: Any,
|
|
166
|
+
) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Called by MkDocs with the raw Markdown string for every page.
|
|
169
|
+
Returns the rendered HTML string.
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
result = self._run_node_runner(markdown)
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
# Node executable disappeared between on_config and now.
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
f"mkdocs-markdoc: Node.js executable '{self._node_exec}' "
|
|
177
|
+
"disappeared during the build."
|
|
178
|
+
)
|
|
179
|
+
except subprocess.TimeoutExpired:
|
|
180
|
+
raise RuntimeError(
|
|
181
|
+
f"mkdocs-markdoc: Node.js subprocess timed out after "
|
|
182
|
+
f"{self.config['timeout']} ms while processing "
|
|
183
|
+
f"'{page.file.src_path}'."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if result.returncode != 0:
|
|
187
|
+
stderr = result.stderr.strip()
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
f"mkdocs-markdoc: Markdoc rendering failed for "
|
|
190
|
+
f"'{page.file.src_path}'.\nNode stderr: {stderr}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
html = result.stdout
|
|
194
|
+
if not html:
|
|
195
|
+
log.warning(
|
|
196
|
+
"mkdocs-markdoc: empty HTML output for '%s' – "
|
|
197
|
+
"returning empty string.",
|
|
198
|
+
page.file.src_path,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return html
|
|
202
|
+
|
|
203
|
+
def on_page_content(
|
|
204
|
+
self,
|
|
205
|
+
html: str,
|
|
206
|
+
page: Page,
|
|
207
|
+
config: dict[str, Any],
|
|
208
|
+
**kwargs: Any,
|
|
209
|
+
) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Called by MkDocs after Python-Markdown has processed the page.
|
|
212
|
+
|
|
213
|
+
Because we bypass Python-Markdown entirely, page.toc is empty after
|
|
214
|
+
page.render() — the toc extension never sees any Markdown headings.
|
|
215
|
+
We rebuild it here by parsing the id-annotated <hN> elements that
|
|
216
|
+
the heading node override in markdoc.config.js already produced.
|
|
217
|
+
"""
|
|
218
|
+
page.toc = _toc_from_html(html)
|
|
219
|
+
return html
|
|
220
|
+
|
|
221
|
+
# ------------------------------------------------------------------
|
|
222
|
+
# Helpers
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
def _run_node_runner(self, markdown: str) -> subprocess.CompletedProcess:
|
|
226
|
+
"""Pipe *markdown* into markdoc_runner.js and return the result."""
|
|
227
|
+
cmd = [self._node_exec, str(_RUNNER_PATH)]
|
|
228
|
+
if self.config["markdoc_config"]:
|
|
229
|
+
cmd += ["--config", self.config["markdoc_config"]]
|
|
230
|
+
|
|
231
|
+
return subprocess.run(
|
|
232
|
+
cmd,
|
|
233
|
+
input=markdown,
|
|
234
|
+
capture_output=True,
|
|
235
|
+
text=True,
|
|
236
|
+
timeout=self.config["timeout"] / 1000, # subprocess uses seconds
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _run_node(self, script: str) -> subprocess.CompletedProcess:
|
|
240
|
+
"""Run an inline Node.js *script* string for quick checks."""
|
|
241
|
+
return subprocess.run(
|
|
242
|
+
[self._node_exec, "-e", script],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
timeout=10,
|
|
246
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mkdocs-markdoc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MkDocs plugin that renders pages with Stripe's Markdoc instead of Python-Markdown"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["mkdocs", "markdoc", "markdown", "plugin", "documentation"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Documentation",
|
|
23
|
+
"Topic :: Software Development :: Documentation",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"mkdocs>=1.5",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=7",
|
|
32
|
+
"pytest-mock>=3",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.entry-points."mkdocs.plugins"]
|
|
36
|
+
markdoc = "mkdocs_markdoc.plugin:MarkdocPlugin"
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Hatch build – make sure the JS runner is included in the wheel/sdist.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["mkdocs_markdoc"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = [
|
|
47
|
+
"mkdocs_markdoc/**",
|
|
48
|
+
"README.md",
|
|
49
|
+
"pyproject.toml",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Ruff (optional – only applied when ruff is installed)
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py39"
|