pantsdown 2.2.8 → 2.3.0
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.
- package/README.md +36 -2
- package/package.json +6 -6
- package/src/css/styles.css +79 -0
- package/src/javascript.ts +356 -1
- package/src/pantsdown.ts +22 -3
- package/src/renderer.ts +9 -0
- package/src/types.ts +34 -0
- package/src/utils.ts +9 -0
package/README.md
CHANGED
|
@@ -148,13 +148,47 @@ const config: PartialPantsdownConfig = {
|
|
|
148
148
|
* false
|
|
149
149
|
*/
|
|
150
150
|
detailsTagDefaultOpen: boolean;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Interactive controls added to rendered mermaid diagrams.
|
|
154
|
+
*/
|
|
155
|
+
mermaid: {
|
|
156
|
+
/**
|
|
157
|
+
* Control buttons overlaid on the diagram.
|
|
158
|
+
* Set individual buttons to `false` to hide them.
|
|
159
|
+
*
|
|
160
|
+
* @default
|
|
161
|
+
* { zoom: true, reset: true, arrows: true, popover: true }
|
|
162
|
+
*/
|
|
163
|
+
buttons: {
|
|
164
|
+
/** Zoom in & zoom out buttons */
|
|
165
|
+
zoom: boolean;
|
|
166
|
+
/** Reset zoom & pan to the initial position */
|
|
167
|
+
reset: boolean;
|
|
168
|
+
/** Directional pan arrows */
|
|
169
|
+
arrows: boolean;
|
|
170
|
+
/** Open the diagram in a fullscreen popover */
|
|
171
|
+
popover: boolean;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Enable cmd/ctrl + drag to pan and cmd/ctrl + scroll to zoom.
|
|
176
|
+
* Plain click & scroll are never captured, so text selection
|
|
177
|
+
* and page scrolling keep working. Inside the popover, plain
|
|
178
|
+
* drag pans and plain scroll zooms regardless of this setting.
|
|
179
|
+
*
|
|
180
|
+
* @default
|
|
181
|
+
* true
|
|
182
|
+
*/
|
|
183
|
+
mouseActions: boolean;
|
|
184
|
+
};
|
|
151
185
|
};
|
|
152
186
|
};
|
|
153
187
|
|
|
154
188
|
const pantsdown = new Pantsdown(config);
|
|
155
|
-
const html = pantsdown.parse(markdown);
|
|
189
|
+
const { html, javascript } = pantsdown.parse(markdown);
|
|
156
190
|
|
|
157
|
-
console.log(html);
|
|
191
|
+
console.log(html, javascript);
|
|
158
192
|
```
|
|
159
193
|
|
|
160
194
|
## 🤝 Acknowledgements
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantsdown",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Markdown to 'GitHub HTML' parser",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wallpants",
|
|
@@ -38,21 +38,21 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"github-slugger": "^2.0.0",
|
|
40
40
|
"highlight.js": "^11.11.1",
|
|
41
|
-
"katex": "^0.18.
|
|
41
|
+
"katex": "^0.18.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@commitlint/cli": "^21.2.1",
|
|
45
45
|
"@commitlint/config-conventional": "^21.2.0",
|
|
46
46
|
"@commitlint/cz-commitlint": "^21.2.0",
|
|
47
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
47
|
+
"@happy-dom/global-registrator": "^20.11.1",
|
|
48
48
|
"@types/bun": "^1.3.14",
|
|
49
49
|
"@types/katex": "^0.16.8",
|
|
50
50
|
"commitizen": "^4.3.2",
|
|
51
51
|
"husky": "^9.1.7",
|
|
52
52
|
"inquirer": "^12.11.1",
|
|
53
|
-
"oxfmt": "^0.
|
|
54
|
-
"oxlint": "^1.
|
|
55
|
-
"oxlint-tsgolint": "^0.
|
|
53
|
+
"oxfmt": "^0.62.0",
|
|
54
|
+
"oxlint": "^1.77.0",
|
|
55
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
56
56
|
"semantic-release": "^25.0.8",
|
|
57
57
|
"typescript": "7.0.2"
|
|
58
58
|
},
|
package/src/css/styles.css
CHANGED
|
@@ -203,6 +203,85 @@
|
|
|
203
203
|
margin-bottom: 16px;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
.pantsdown .mermaid-viewport {
|
|
207
|
+
overflow: hidden;
|
|
208
|
+
}
|
|
209
|
+
/* swallow the pre's padding and restore it as the viewport's own: the resting
|
|
210
|
+
layout is unchanged, but overflow clips at the padding box, so panned
|
|
211
|
+
content reaches the container edge (like github) instead of dying 16px short */
|
|
212
|
+
.pantsdown pre .mermaid-viewport {
|
|
213
|
+
margin: -16px;
|
|
214
|
+
padding: 16px;
|
|
215
|
+
}
|
|
216
|
+
.pantsdown .mermaid-canvas {
|
|
217
|
+
transform-origin: 0 0;
|
|
218
|
+
}
|
|
219
|
+
/* second selector covers consumers where <html> itself is the .pantsdown root */
|
|
220
|
+
html.pantsdown-mermaid-mod .pantsdown .mermaid-viewport,
|
|
221
|
+
html.pantsdown-mermaid-mod.pantsdown .mermaid-viewport {
|
|
222
|
+
cursor: grab;
|
|
223
|
+
}
|
|
224
|
+
.pantsdown .mermaid-viewport.dragging {
|
|
225
|
+
cursor: grabbing;
|
|
226
|
+
user-select: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.pantsdown .mermaid-button {
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
height: 30px;
|
|
232
|
+
width: 30px;
|
|
233
|
+
padding: 0;
|
|
234
|
+
color: var(--color-fg-default);
|
|
235
|
+
background: var(--color-btn-bg);
|
|
236
|
+
border: 1px solid var(--color-btn-border);
|
|
237
|
+
border-radius: 4px;
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
}
|
|
242
|
+
.pantsdown .mermaid-button:hover {
|
|
243
|
+
background: var(--color-btn-hover-bg);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.pantsdown .mermaid-controls {
|
|
247
|
+
position: absolute;
|
|
248
|
+
right: 8px;
|
|
249
|
+
bottom: 8px;
|
|
250
|
+
display: grid;
|
|
251
|
+
gap: 4px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.pantsdown .mermaid-popover-button {
|
|
255
|
+
position: absolute;
|
|
256
|
+
right: 8px;
|
|
257
|
+
top: 8px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.pantsdown.mermaid-overlay {
|
|
261
|
+
position: fixed;
|
|
262
|
+
inset: 0;
|
|
263
|
+
z-index: 100;
|
|
264
|
+
background: rgba(0, 0, 0, 0.5);
|
|
265
|
+
padding: 24px;
|
|
266
|
+
}
|
|
267
|
+
.pantsdown.mermaid-overlay .mermaid-panel {
|
|
268
|
+
position: relative;
|
|
269
|
+
width: 100%;
|
|
270
|
+
height: 100%;
|
|
271
|
+
background: var(--color-canvas-default);
|
|
272
|
+
border: 1px solid var(--color-border-default);
|
|
273
|
+
border-radius: 6px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
}
|
|
276
|
+
.pantsdown.mermaid-overlay .mermaid-viewport {
|
|
277
|
+
width: 100%;
|
|
278
|
+
height: 100%;
|
|
279
|
+
cursor: grab;
|
|
280
|
+
}
|
|
281
|
+
.pantsdown.mermaid-overlay .mermaid-viewport.dragging {
|
|
282
|
+
cursor: grabbing;
|
|
283
|
+
}
|
|
284
|
+
|
|
206
285
|
.pantsdown .octicon {
|
|
207
286
|
display: inline-block;
|
|
208
287
|
fill: currentColor;
|
package/src/javascript.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { type PantsdownConfig } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const copyButtonScript = `
|
|
2
4
|
/** code copy button */
|
|
3
5
|
document.querySelectorAll("pre").forEach((pre) => {
|
|
4
6
|
const firstElement = pre.firstElementChild;
|
|
@@ -33,3 +35,356 @@ export const javascript = `
|
|
|
33
35
|
});
|
|
34
36
|
});
|
|
35
37
|
`;
|
|
38
|
+
|
|
39
|
+
function mermaidScript(config: PantsdownConfig["renderer"]["mermaid"]): string {
|
|
40
|
+
return `
|
|
41
|
+
/** mermaid pan & zoom controls */
|
|
42
|
+
(() => {
|
|
43
|
+
const config = ${JSON.stringify(config)};
|
|
44
|
+
const MIN_SCALE = 0.2;
|
|
45
|
+
const MAX_SCALE = 10;
|
|
46
|
+
const PAN_STEP = 60;
|
|
47
|
+
|
|
48
|
+
// pan/zoom state per diagram, keyed by data-mermaid-key. Lives on
|
|
49
|
+
// window so it survives consumers replacing the whole DOM on every
|
|
50
|
+
// update. Pruning to the keys present in the current document resets
|
|
51
|
+
// views on navigation and evicts stale entries.
|
|
52
|
+
const transforms = (window.pantsdownMermaidTransforms ??= new Map());
|
|
53
|
+
{
|
|
54
|
+
const presentKeys = new Set();
|
|
55
|
+
document.querySelectorAll(".mermaid[data-mermaid-key]").forEach((el) => {
|
|
56
|
+
presentKeys.add(el.getAttribute("data-mermaid-key"));
|
|
57
|
+
});
|
|
58
|
+
transforms.forEach((_, key) => {
|
|
59
|
+
if (!presentKeys.has(key)) transforms.delete(key);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const icon = (paths) =>
|
|
64
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
65
|
+
paths +
|
|
66
|
+
"</svg>";
|
|
67
|
+
|
|
68
|
+
const icons = {
|
|
69
|
+
up: icon('<path d="m18 15-6-6-6 6"/>'),
|
|
70
|
+
down: icon('<path d="m6 9 6 6 6-6"/>'),
|
|
71
|
+
left: icon('<path d="m15 18-6-6 6-6"/>'),
|
|
72
|
+
right: icon('<path d="m9 18 6-6-6-6"/>'),
|
|
73
|
+
zoomIn: icon('<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/>'),
|
|
74
|
+
zoomOut: icon('<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/>'),
|
|
75
|
+
reset: icon('<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>'),
|
|
76
|
+
popover: icon('<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/>'),
|
|
77
|
+
close: icon('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function makeButton(name, title, onClick) {
|
|
81
|
+
const btn = document.createElement("button");
|
|
82
|
+
btn.type = "button";
|
|
83
|
+
btn.className = "mermaid-button";
|
|
84
|
+
btn.innerHTML = icons[name];
|
|
85
|
+
btn.title = title;
|
|
86
|
+
btn.setAttribute("aria-label", title);
|
|
87
|
+
btn.addEventListener("click", onClick);
|
|
88
|
+
return btn;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function attachGestures(viewport, canvas, opts) {
|
|
92
|
+
const state = { x: 0, y: 0, scale: 1 };
|
|
93
|
+
// "reset" returns to this state; the popover overrides it with a
|
|
94
|
+
// fitted & centered transform via setHome
|
|
95
|
+
const home = { x: 0, y: 0, scale: 1 };
|
|
96
|
+
|
|
97
|
+
function apply() {
|
|
98
|
+
canvas.style.transform =
|
|
99
|
+
"translate(" + state.x + "px, " + state.y + "px) scale(" + state.scale + ")";
|
|
100
|
+
if (opts.storeKey) {
|
|
101
|
+
transforms.set(opts.storeKey, {
|
|
102
|
+
x: state.x,
|
|
103
|
+
y: state.y,
|
|
104
|
+
scale: state.scale,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function panBy(dx, dy) {
|
|
110
|
+
state.x += dx;
|
|
111
|
+
state.y += dy;
|
|
112
|
+
apply();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function zoomAt(factor, originX, originY) {
|
|
116
|
+
const scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, state.scale * factor));
|
|
117
|
+
factor = scale / state.scale;
|
|
118
|
+
state.x = originX - (originX - state.x) * factor;
|
|
119
|
+
state.y = originY - (originY - state.y) * factor;
|
|
120
|
+
state.scale = scale;
|
|
121
|
+
apply();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// canvas coordinates are relative to the viewport's content box;
|
|
125
|
+
// the inline viewport carries the pre's padding, so gesture
|
|
126
|
+
// coordinates (relative to the border box) must be shifted by it
|
|
127
|
+
function contentOrigin() {
|
|
128
|
+
const style = window.getComputedStyle(viewport);
|
|
129
|
+
return {
|
|
130
|
+
x: parseFloat(style.paddingLeft) || 0,
|
|
131
|
+
y: parseFloat(style.paddingTop) || 0,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function zoomBy(factor) {
|
|
136
|
+
const rect = viewport.getBoundingClientRect();
|
|
137
|
+
const origin = contentOrigin();
|
|
138
|
+
zoomAt(factor, rect.width / 2 - origin.x, rect.height / 2 - origin.y);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function reset() {
|
|
142
|
+
state.x = home.x;
|
|
143
|
+
state.y = home.y;
|
|
144
|
+
state.scale = home.scale;
|
|
145
|
+
apply();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setHome(x, y, scale) {
|
|
149
|
+
home.x = x;
|
|
150
|
+
home.y = y;
|
|
151
|
+
home.scale = scale;
|
|
152
|
+
reset();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// inline diagrams require cmd/ctrl (and can be disabled via config)
|
|
156
|
+
// so plain click & scroll keep selecting text / scrolling the page;
|
|
157
|
+
// the popover captures plain gestures
|
|
158
|
+
const enabled = opts.requireMod ? config.mouseActions : true;
|
|
159
|
+
const modOk = (e) => !opts.requireMod || e.metaKey || e.ctrlKey;
|
|
160
|
+
|
|
161
|
+
if (enabled) {
|
|
162
|
+
viewport.addEventListener(
|
|
163
|
+
"wheel",
|
|
164
|
+
(e) => {
|
|
165
|
+
if (!modOk(e)) return;
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
const rect = viewport.getBoundingClientRect();
|
|
168
|
+
const origin = contentOrigin();
|
|
169
|
+
const delta = e.deltaY * (e.deltaMode === 1 ? 16 : 1);
|
|
170
|
+
zoomAt(
|
|
171
|
+
Math.exp(-delta * 0.002),
|
|
172
|
+
e.clientX - rect.left - origin.x,
|
|
173
|
+
e.clientY - rect.top - origin.y,
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
{ passive: false },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
viewport.addEventListener("pointerdown", (e) => {
|
|
180
|
+
if (e.button !== 0 || !modOk(e)) return;
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
viewport.setPointerCapture(e.pointerId);
|
|
183
|
+
viewport.classList.add("dragging");
|
|
184
|
+
let lastX = e.clientX;
|
|
185
|
+
let lastY = e.clientY;
|
|
186
|
+
const onMove = (ev) => {
|
|
187
|
+
panBy(ev.clientX - lastX, ev.clientY - lastY);
|
|
188
|
+
lastX = ev.clientX;
|
|
189
|
+
lastY = ev.clientY;
|
|
190
|
+
};
|
|
191
|
+
const onUp = () => {
|
|
192
|
+
viewport.classList.remove("dragging");
|
|
193
|
+
viewport.removeEventListener("pointermove", onMove);
|
|
194
|
+
viewport.removeEventListener("pointerup", onUp);
|
|
195
|
+
viewport.removeEventListener("pointercancel", onUp);
|
|
196
|
+
};
|
|
197
|
+
viewport.addEventListener("pointermove", onMove);
|
|
198
|
+
viewport.addEventListener("pointerup", onUp);
|
|
199
|
+
viewport.addEventListener("pointercancel", onUp);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// restore the stashed transform so re-renders don't visually
|
|
204
|
+
// move the diagram; home stays identity, so reset still returns
|
|
205
|
+
// to the natural view
|
|
206
|
+
const stored = opts.storeKey ? transforms.get(opts.storeKey) : undefined;
|
|
207
|
+
if (stored) {
|
|
208
|
+
state.x = stored.x;
|
|
209
|
+
state.y = stored.y;
|
|
210
|
+
state.scale = stored.scale;
|
|
211
|
+
apply();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { panBy, zoomBy, reset, setHome };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildCluster(controller) {
|
|
218
|
+
const b = config.buttons;
|
|
219
|
+
if (!b.arrows && !b.zoom && !b.reset) return null;
|
|
220
|
+
|
|
221
|
+
const cluster = document.createElement("div");
|
|
222
|
+
cluster.className = "mermaid-controls";
|
|
223
|
+
cluster.style.gridTemplateAreas = b.arrows
|
|
224
|
+
? '". up zin" "left reset right" ". down zout"'
|
|
225
|
+
: '"zin" "reset" "zout"';
|
|
226
|
+
|
|
227
|
+
const place = (btn, area) => {
|
|
228
|
+
btn.style.gridArea = area;
|
|
229
|
+
cluster.appendChild(btn);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (b.arrows) {
|
|
233
|
+
place(makeButton("up", "Pan up", () => controller.panBy(0, PAN_STEP)), "up");
|
|
234
|
+
place(makeButton("down", "Pan down", () => controller.panBy(0, -PAN_STEP)), "down");
|
|
235
|
+
place(makeButton("left", "Pan left", () => controller.panBy(PAN_STEP, 0)), "left");
|
|
236
|
+
place(makeButton("right", "Pan right", () => controller.panBy(-PAN_STEP, 0)), "right");
|
|
237
|
+
}
|
|
238
|
+
if (b.zoom) {
|
|
239
|
+
place(makeButton("zoomIn", "Zoom in", () => controller.zoomBy(1.25)), "zin");
|
|
240
|
+
place(makeButton("zoomOut", "Zoom out", () => controller.zoomBy(0.8)), "zout");
|
|
241
|
+
}
|
|
242
|
+
if (b.reset) place(makeButton("reset", "Reset view", () => controller.reset()), "reset");
|
|
243
|
+
|
|
244
|
+
return cluster;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function openPopover(el, svg) {
|
|
248
|
+
const root = el.closest(".pantsdown");
|
|
249
|
+
const overlay = document.createElement("div");
|
|
250
|
+
overlay.className = (root ? root.className : "pantsdown") + " mermaid-overlay";
|
|
251
|
+
|
|
252
|
+
const panel = document.createElement("div");
|
|
253
|
+
panel.className = "mermaid-panel";
|
|
254
|
+
const viewport = document.createElement("div");
|
|
255
|
+
viewport.className = "mermaid-viewport";
|
|
256
|
+
const canvas = document.createElement("div");
|
|
257
|
+
canvas.className = "mermaid-canvas";
|
|
258
|
+
const clone = svg.cloneNode(true);
|
|
259
|
+
canvas.appendChild(clone);
|
|
260
|
+
viewport.appendChild(canvas);
|
|
261
|
+
panel.appendChild(viewport);
|
|
262
|
+
overlay.appendChild(panel);
|
|
263
|
+
|
|
264
|
+
const controller = attachGestures(viewport, canvas, { requireMod: false });
|
|
265
|
+
const cluster = buildCluster(controller);
|
|
266
|
+
if (cluster) panel.appendChild(cluster);
|
|
267
|
+
|
|
268
|
+
function onKeydown(e) {
|
|
269
|
+
if (e.key === "Escape") close();
|
|
270
|
+
}
|
|
271
|
+
function close() {
|
|
272
|
+
overlay.remove();
|
|
273
|
+
window.removeEventListener("keydown", onKeydown);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const closeButton = makeButton("close", "Close", close);
|
|
277
|
+
closeButton.classList.add("mermaid-popover-button");
|
|
278
|
+
panel.appendChild(closeButton);
|
|
279
|
+
|
|
280
|
+
window.addEventListener("keydown", onKeydown);
|
|
281
|
+
overlay.addEventListener("pointerdown", (e) => {
|
|
282
|
+
if (e.target === overlay) close();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
document.body.appendChild(overlay);
|
|
286
|
+
|
|
287
|
+
// fit the whole diagram inside the viewport (never upscaling past
|
|
288
|
+
// its natural size) and center it; reset returns to this state
|
|
289
|
+
const vp = viewport.getBoundingClientRect();
|
|
290
|
+
const content = clone.getBoundingClientRect();
|
|
291
|
+
if (vp.width > 0 && content.width > 0 && content.height > 0) {
|
|
292
|
+
const pad = 32;
|
|
293
|
+
const scale = Math.max(
|
|
294
|
+
MIN_SCALE,
|
|
295
|
+
Math.min(
|
|
296
|
+
1,
|
|
297
|
+
(vp.width - pad) / content.width,
|
|
298
|
+
(vp.height - pad) / content.height,
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
const x = (vp.width - content.width * scale) / 2 - (content.left - vp.left) * scale;
|
|
302
|
+
const y = (vp.height - content.height * scale) / 2 - (content.top - vp.top) * scale;
|
|
303
|
+
controller.setHome(x, y, scale);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function setup(el) {
|
|
308
|
+
// prefer the canvas svg: if a consumer re-injects html that was
|
|
309
|
+
// serialized after a previous setup, the diagram svg sits inside
|
|
310
|
+
// the old wrapper and stale button icons are svgs too
|
|
311
|
+
const svg = el.querySelector(".mermaid-canvas svg") ?? el.querySelector("svg");
|
|
312
|
+
if (!svg) return;
|
|
313
|
+
|
|
314
|
+
// serialized html loses event listeners, so stale wrappers and
|
|
315
|
+
// controls would be dead and duplicated — remove them and rebuild
|
|
316
|
+
el.querySelectorAll(
|
|
317
|
+
".mermaid-viewport, .mermaid-controls, .mermaid-popover-button",
|
|
318
|
+
).forEach((stale) => {
|
|
319
|
+
stale.remove();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const viewport = document.createElement("div");
|
|
323
|
+
viewport.className = "mermaid-viewport";
|
|
324
|
+
const canvas = document.createElement("div");
|
|
325
|
+
canvas.className = "mermaid-canvas";
|
|
326
|
+
canvas.appendChild(svg);
|
|
327
|
+
viewport.appendChild(canvas);
|
|
328
|
+
el.appendChild(viewport);
|
|
329
|
+
|
|
330
|
+
const controller = attachGestures(viewport, canvas, {
|
|
331
|
+
requireMod: true,
|
|
332
|
+
storeKey: el.getAttribute("data-mermaid-key"),
|
|
333
|
+
});
|
|
334
|
+
const cluster = buildCluster(controller);
|
|
335
|
+
if (cluster) el.appendChild(cluster);
|
|
336
|
+
|
|
337
|
+
if (config.buttons.popover) {
|
|
338
|
+
const btn = makeButton("popover", "Open in popover", () => openPopover(el, svg));
|
|
339
|
+
btn.classList.add("mermaid-popover-button");
|
|
340
|
+
el.appendChild(btn);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// mermaid renders asynchronously (the page loads mermaid.js separately),
|
|
345
|
+
// so wait for the svg to replace the diagram source
|
|
346
|
+
function whenRendered(el, cb) {
|
|
347
|
+
if (el.querySelector("svg")) {
|
|
348
|
+
cb();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const observer = new MutationObserver(() => {
|
|
352
|
+
if (el.querySelector("svg")) {
|
|
353
|
+
observer.disconnect();
|
|
354
|
+
cb();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
observer.observe(el, { childList: true, subtree: true });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
document.querySelectorAll(".mermaid").forEach((el) => {
|
|
361
|
+
if (el.hasAttribute("data-pantsdown-mermaid")) return;
|
|
362
|
+
el.setAttribute("data-pantsdown-mermaid", "");
|
|
363
|
+
whenRendered(el, () => {
|
|
364
|
+
setup(el);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// grab cursor hint while cmd/ctrl is held over a diagram
|
|
369
|
+
if (config.mouseActions && !window.pantsdownMermaidModListener) {
|
|
370
|
+
window.pantsdownMermaidModListener = true;
|
|
371
|
+
const setMod = (on) => {
|
|
372
|
+
document.documentElement.classList.toggle("pantsdown-mermaid-mod", on);
|
|
373
|
+
};
|
|
374
|
+
window.addEventListener("keydown", (e) => {
|
|
375
|
+
if (e.key === "Meta" || e.key === "Control") setMod(true);
|
|
376
|
+
});
|
|
377
|
+
window.addEventListener("keyup", (e) => {
|
|
378
|
+
if (e.key === "Meta" || e.key === "Control") setMod(false);
|
|
379
|
+
});
|
|
380
|
+
window.addEventListener("blur", () => {
|
|
381
|
+
setMod(false);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
})();
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function getJavascript(config: PantsdownConfig): string {
|
|
389
|
+
return copyButtonScript + mermaidScript(config.renderer.mermaid);
|
|
390
|
+
}
|
package/src/pantsdown.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import GithubSlugger from "github-slugger";
|
|
2
|
-
import {
|
|
2
|
+
import { getJavascript } from "./javascript.ts";
|
|
3
3
|
import { Lexer } from "./lexer.ts";
|
|
4
4
|
import { Parser } from "./parser.ts";
|
|
5
5
|
import { type PantsdownConfig, type PartialPantsdownConfig } from "./types.ts";
|
|
@@ -9,6 +9,15 @@ const defaultConfig: PantsdownConfig = {
|
|
|
9
9
|
relativeImageUrlPrefix: "",
|
|
10
10
|
absoluteImageUrlPrefix: undefined,
|
|
11
11
|
detailsTagDefaultOpen: false,
|
|
12
|
+
mermaid: {
|
|
13
|
+
buttons: {
|
|
14
|
+
zoom: true,
|
|
15
|
+
reset: true,
|
|
16
|
+
arrows: true,
|
|
17
|
+
popover: true,
|
|
18
|
+
},
|
|
19
|
+
mouseActions: true,
|
|
20
|
+
},
|
|
12
21
|
},
|
|
13
22
|
};
|
|
14
23
|
|
|
@@ -28,10 +37,19 @@ export class Pantsdown {
|
|
|
28
37
|
* The object you provide will be deeply merged into current config.
|
|
29
38
|
*/
|
|
30
39
|
setConfig(config: PartialPantsdownConfig) {
|
|
40
|
+
const { mermaid, ...renderer } = config.renderer ?? {};
|
|
31
41
|
this.config = {
|
|
32
42
|
renderer: {
|
|
33
43
|
...this.config.renderer,
|
|
34
|
-
...
|
|
44
|
+
...renderer,
|
|
45
|
+
mermaid: {
|
|
46
|
+
...this.config.renderer.mermaid,
|
|
47
|
+
...mermaid,
|
|
48
|
+
buttons: {
|
|
49
|
+
...this.config.renderer.mermaid.buttons,
|
|
50
|
+
...mermaid?.buttons,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
35
53
|
},
|
|
36
54
|
};
|
|
37
55
|
}
|
|
@@ -43,12 +61,13 @@ export class Pantsdown {
|
|
|
43
61
|
// re-init slugger to avoid slug count from incorrectly incrementing
|
|
44
62
|
// from previosly slugged headings
|
|
45
63
|
this.parser.renderer.slugger = new GithubSlugger();
|
|
64
|
+
this.parser.renderer.mermaidCounts = new Map();
|
|
46
65
|
|
|
47
66
|
const tokens = this.lexer.lex(src);
|
|
48
67
|
const html = this.parser.parse(tokens);
|
|
49
68
|
return {
|
|
50
69
|
html,
|
|
51
|
-
javascript,
|
|
70
|
+
javascript: getJavascript(this.config),
|
|
52
71
|
};
|
|
53
72
|
}
|
|
54
73
|
}
|
package/src/renderer.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
fixHtmlLocalImageHref,
|
|
14
14
|
fixLocalImageHref,
|
|
15
15
|
getHtmlElementText,
|
|
16
|
+
hashString,
|
|
16
17
|
injectHtmlAttributes,
|
|
17
18
|
} from "./utils.ts";
|
|
18
19
|
|
|
@@ -23,6 +24,7 @@ export class Renderer {
|
|
|
23
24
|
private pantsdown: Pantsdown;
|
|
24
25
|
parser!: Parser; // set by the parser
|
|
25
26
|
slugger = new GithubSlugger();
|
|
27
|
+
mermaidCounts = new Map<string, number>();
|
|
26
28
|
|
|
27
29
|
constructor(pantsdown: Pantsdown) {
|
|
28
30
|
this.pantsdown = pantsdown;
|
|
@@ -40,6 +42,13 @@ export class Renderer {
|
|
|
40
42
|
|
|
41
43
|
if (lang === "mermaid") {
|
|
42
44
|
attrs.push(["class", "mermaid-container mermaid"]);
|
|
45
|
+
// stable identity across re-renders — the pan/zoom script keys
|
|
46
|
+
// stashed transforms on it; the occurrence index disambiguates
|
|
47
|
+
// byte-identical diagrams in the same document
|
|
48
|
+
const hash = hashString(code);
|
|
49
|
+
const count = this.mermaidCounts.get(hash) ?? 0;
|
|
50
|
+
this.mermaidCounts.set(hash, count + 1);
|
|
51
|
+
attrs.push(["data-mermaid-key", `${hash}-${count}`]);
|
|
43
52
|
// escape so diagram source can't be parsed as HTML;
|
|
44
53
|
// mermaid reads textContent, which undoes the escaping
|
|
45
54
|
const result = `<pre style="position: relative;">` + escape(code, true) + "\n" + `</pre>`;
|
package/src/types.ts
CHANGED
|
@@ -285,6 +285,40 @@ export type PantsdownConfig = {
|
|
|
285
285
|
* false
|
|
286
286
|
*/
|
|
287
287
|
detailsTagDefaultOpen: boolean;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Interactive controls added to rendered mermaid diagrams.
|
|
291
|
+
*/
|
|
292
|
+
mermaid: {
|
|
293
|
+
/**
|
|
294
|
+
* Control buttons overlaid on the diagram.
|
|
295
|
+
* Set individual buttons to `false` to hide them.
|
|
296
|
+
*
|
|
297
|
+
* @default
|
|
298
|
+
* { zoom: true, reset: true, arrows: true, popover: true }
|
|
299
|
+
*/
|
|
300
|
+
buttons: {
|
|
301
|
+
/** Zoom in & zoom out buttons */
|
|
302
|
+
zoom: boolean;
|
|
303
|
+
/** Reset zoom & pan to the initial position */
|
|
304
|
+
reset: boolean;
|
|
305
|
+
/** Directional pan arrows */
|
|
306
|
+
arrows: boolean;
|
|
307
|
+
/** Open the diagram in a fullscreen popover */
|
|
308
|
+
popover: boolean;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Enable cmd/ctrl + drag to pan and cmd/ctrl + scroll to zoom.
|
|
313
|
+
* Plain click & scroll are never captured, so text selection
|
|
314
|
+
* and page scrolling keep working. Inside the popover, plain
|
|
315
|
+
* drag pans and plain scroll zooms regardless of this setting.
|
|
316
|
+
*
|
|
317
|
+
* @default
|
|
318
|
+
* true
|
|
319
|
+
*/
|
|
320
|
+
mouseActions: boolean;
|
|
321
|
+
};
|
|
288
322
|
};
|
|
289
323
|
};
|
|
290
324
|
|
package/src/utils.ts
CHANGED
|
@@ -30,6 +30,15 @@ export function escape(html: string, encode?: boolean) {
|
|
|
30
30
|
return html;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** djb2 — stable, collision-tolerant key for identifying content across parses */
|
|
34
|
+
export function hashString(str: string): string {
|
|
35
|
+
let hash = 5381;
|
|
36
|
+
for (let i = 0; i < str.length; i++) {
|
|
37
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
38
|
+
}
|
|
39
|
+
return (hash >>> 0).toString(36);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function getHtmlElementText(html: string) {
|
|
34
43
|
try {
|
|
35
44
|
const parser = new DOMParser();
|