pantsdown 2.2.7 → 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 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
@@ -165,3 +199,5 @@ console.log(html);
165
199
 
166
200
  Pantsdown is based on [Marked](https://github.com/markedjs/marked). Without their hard work,
167
201
  Pantsdown would not exist.
202
+
203
+ Last synced with Marked [v18.0.7](https://github.com/markedjs/marked/releases/tag/v18.0.7).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantsdown",
3
- "version": "2.2.7",
3
+ "version": "2.3.0",
4
4
  "description": "Markdown to 'GitHub HTML' parser",
5
5
  "license": "MIT",
6
6
  "author": "wallpants",
@@ -32,25 +32,27 @@
32
32
  "typecheck": "tsc",
33
33
  "lint": "oxlint .",
34
34
  "check": "bun run typecheck && bun run lint",
35
- "docs:build": "bun run docs/build.ts"
35
+ "docs:build": "bun run docs/build.ts",
36
+ "prepare": "husky"
36
37
  },
37
38
  "dependencies": {
38
39
  "github-slugger": "^2.0.0",
39
40
  "highlight.js": "^11.11.1",
40
- "katex": "^0.18.0"
41
+ "katex": "^0.18.1"
41
42
  },
42
43
  "devDependencies": {
44
+ "@commitlint/cli": "^21.2.1",
43
45
  "@commitlint/config-conventional": "^21.2.0",
44
46
  "@commitlint/cz-commitlint": "^21.2.0",
45
- "@happy-dom/global-registrator": "^20.11.0",
47
+ "@happy-dom/global-registrator": "^20.11.1",
46
48
  "@types/bun": "^1.3.14",
47
49
  "@types/katex": "^0.16.8",
48
50
  "commitizen": "^4.3.2",
49
- "commitlint": "^21.2.1",
51
+ "husky": "^9.1.7",
50
52
  "inquirer": "^12.11.1",
51
- "oxfmt": "^0.59.0",
52
- "oxlint": "^1.73.0",
53
- "oxlint-tsgolint": "^0.25.0",
53
+ "oxfmt": "^0.62.0",
54
+ "oxlint": "^1.77.0",
55
+ "oxlint-tsgolint": "^7.0.2001",
54
56
  "semantic-release": "^25.0.8",
55
57
  "typescript": "7.0.2"
56
58
  },
@@ -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
- export const javascript = `
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
+ }