@mdzip/editor 1.4.0 → 1.4.5

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.
Files changed (50) hide show
  1. package/README.md +35 -0
  2. package/dist/asset-cache.d.ts +2 -0
  3. package/dist/asset-cache.d.ts.map +1 -1
  4. package/dist/asset-cache.js +12 -0
  5. package/dist/asset-cache.js.map +1 -1
  6. package/dist/front-matter-extension.d.ts +45 -0
  7. package/dist/front-matter-extension.d.ts.map +1 -0
  8. package/dist/front-matter-extension.js +114 -0
  9. package/dist/front-matter-extension.js.map +1 -0
  10. package/dist/front-matter.d.ts +18 -0
  11. package/dist/front-matter.d.ts.map +1 -0
  12. package/dist/front-matter.js +48 -0
  13. package/dist/front-matter.js.map +1 -0
  14. package/dist/highlight-core.d.ts +3 -0
  15. package/dist/highlight-core.d.ts.map +1 -0
  16. package/dist/highlight-core.js +53 -0
  17. package/dist/highlight-core.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/library-info.d.ts +1 -1
  23. package/dist/library-info.js +1 -1
  24. package/dist/mermaid.d.ts.map +1 -1
  25. package/dist/mermaid.js +60 -41
  26. package/dist/mermaid.js.map +1 -1
  27. package/dist/metadata.d.ts +1 -1
  28. package/dist/metadata.d.ts.map +1 -1
  29. package/dist/metadata.js +12 -2
  30. package/dist/metadata.js.map +1 -1
  31. package/dist/preview.d.ts +1 -0
  32. package/dist/preview.d.ts.map +1 -1
  33. package/dist/preview.js +1 -0
  34. package/dist/preview.js.map +1 -1
  35. package/dist/rendering.d.ts +94 -1
  36. package/dist/rendering.d.ts.map +1 -1
  37. package/dist/rendering.js +173 -5
  38. package/dist/rendering.js.map +1 -1
  39. package/dist/view-css.d.ts.map +1 -1
  40. package/dist/view-css.js +67 -0
  41. package/dist/view-css.js.map +1 -1
  42. package/dist/view.d.ts +151 -10
  43. package/dist/view.d.ts.map +1 -1
  44. package/dist/view.js +745 -78
  45. package/dist/view.js.map +1 -1
  46. package/dist/workspace-view.d.ts +1 -0
  47. package/dist/workspace-view.d.ts.map +1 -1
  48. package/dist/workspace-view.js +14 -3
  49. package/dist/workspace-view.js.map +1 -1
  50. package/package.json +3 -1
package/dist/mermaid.js CHANGED
@@ -74,10 +74,68 @@ async function withBodyCleanup(doc, run) {
74
74
  export function mdzipMermaidExtension(options = {}) {
75
75
  const load = options.loadMermaid ?? loadDefaultMermaid;
76
76
  let counter = 0;
77
+ // Split so a chunk with no mermaid blocks returns synchronously instead of
78
+ // through a Promise. Every markdown render extension's transformHtml runs
79
+ // for every chunk regardless of content — declaring this whole thing async
80
+ // (as an earlier version did) meant calling it always returned a Promise,
81
+ // even on this trivial no-op path, forcing the *entire* chunk-render
82
+ // pipeline (marked's `chainRenderStage`) onto a microtask chain for every
83
+ // chunk in every document once mermaid is registered, mermaid or not. That
84
+ // extra always-async hop was implicated in a real, visible per-keystroke
85
+ // flash in mdzip-studio: it delayed a reconciled chunk's replacement DOM
86
+ // landing just long enough, on top of everything else already sharing the
87
+ // event loop, to widen the window where the old chunk's DOM was already
88
+ // torn down but the new one hadn't landed yet.
89
+ async function renderMermaidBlocks(template, blocks, context) {
90
+ const doc = requireDocument();
91
+ const mermaid = await load();
92
+ if (context.signal.aborted) {
93
+ return template.innerHTML;
94
+ }
95
+ mermaid.initialize({
96
+ startOnLoad: false,
97
+ securityLevel: 'strict',
98
+ // Render labels as SVG <text> rather than HTML inside <foreignObject>.
99
+ // The bundled sanitizer keeps the SVG but strips foreignObject HTML, so
100
+ // html labels would otherwise vanish — this keeps the emitted SVG
101
+ // self-consistent with the policy without re-allowing HTML in labels.
102
+ htmlLabels: false,
103
+ flowchart: { htmlLabels: false },
104
+ theme: resolveTheme(options.theme ?? 'auto', context.colorScheme),
105
+ // Stops mermaid from injecting its own error diagram into the DOM on
106
+ // failure; this extension renders its own error block instead.
107
+ suppressErrorRendering: true
108
+ });
109
+ for (const code of blocks) {
110
+ const pre = code.parentElement;
111
+ if (!pre) {
112
+ continue;
113
+ }
114
+ const source = code.textContent ?? '';
115
+ const container = doc.createElement('div');
116
+ try {
117
+ const { svg } = await withBodyCleanup(doc, () => mermaid.render(`mdzip-mermaid-${(counter += 1)}`, source));
118
+ container.className = 'mdzip-mermaid';
119
+ container.innerHTML = sanitizeMdzipHtml(svg, [MERMAID_SANITIZE]);
120
+ }
121
+ catch (error) {
122
+ container.className = 'mdzip-mermaid-error';
123
+ container.textContent = mermaidErrorMessage(error);
124
+ }
125
+ pre.replaceWith(container);
126
+ if (context.signal.aborted) {
127
+ return template.innerHTML;
128
+ }
129
+ }
130
+ return template.innerHTML;
131
+ }
77
132
  return {
78
133
  name: 'mermaid',
79
134
  sanitize: MERMAID_SANITIZE,
80
- async transformHtml(html, context) {
135
+ shouldIsolateChunk(token) {
136
+ return token.type === 'code' && token.lang?.trim().toLowerCase() === 'mermaid';
137
+ },
138
+ transformHtml(html, context) {
81
139
  const doc = requireDocument();
82
140
  const template = doc.createElement('template');
83
141
  template.innerHTML = html;
@@ -85,46 +143,7 @@ export function mdzipMermaidExtension(options = {}) {
85
143
  if (blocks.length === 0) {
86
144
  return html;
87
145
  }
88
- const mermaid = await load();
89
- if (context.signal.aborted) {
90
- return html;
91
- }
92
- mermaid.initialize({
93
- startOnLoad: false,
94
- securityLevel: 'strict',
95
- // Render labels as SVG <text> rather than HTML inside <foreignObject>.
96
- // The bundled sanitizer keeps the SVG but strips foreignObject HTML, so
97
- // html labels would otherwise vanish — this keeps the emitted SVG
98
- // self-consistent with the policy without re-allowing HTML in labels.
99
- htmlLabels: false,
100
- flowchart: { htmlLabels: false },
101
- theme: resolveTheme(options.theme ?? 'auto', context.colorScheme),
102
- // Stops mermaid from injecting its own error diagram into the DOM on
103
- // failure; this extension renders its own error block instead.
104
- suppressErrorRendering: true
105
- });
106
- for (const code of blocks) {
107
- const pre = code.parentElement;
108
- if (!pre) {
109
- continue;
110
- }
111
- const source = code.textContent ?? '';
112
- const container = doc.createElement('div');
113
- try {
114
- const { svg } = await withBodyCleanup(doc, () => mermaid.render(`mdzip-mermaid-${(counter += 1)}`, source));
115
- container.className = 'mdzip-mermaid';
116
- container.innerHTML = sanitizeMdzipHtml(svg, [MERMAID_SANITIZE]);
117
- }
118
- catch (error) {
119
- container.className = 'mdzip-mermaid-error';
120
- container.textContent = mermaidErrorMessage(error);
121
- }
122
- pre.replaceWith(container);
123
- if (context.signal.aborted) {
124
- return html;
125
- }
126
- }
127
- return template.innerHTML;
146
+ return renderMermaidBlocks(template, blocks, context);
128
147
  }
129
148
  };
130
149
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../src/mermaid.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../src/mermaid.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  export declare function firstMarkdownHeading(markdown: string): string | undefined;
3
3
  /** Derive the file base name, without extension, from a path string. */
4
4
  export declare function fileBaseNameFromPath(path: string): string;
5
- /** Resolve a stable title fallback from markdown heading or filename. */
5
+ /** Resolve a stable title fallback from front matter `title:`, markdown heading, or filename. */
6
6
  export declare function suggestedTitleFromMarkdown(markdown: string, fileBaseName: string): string;
7
7
  /** Resolve display title from manifest title with filename fallback. */
8
8
  export declare function displayTitleFromManifest(manifestTitle: string | undefined, fileBaseName: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,yEAAyE;AACzE,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEzF;AAED,wEAAwE;AACxE,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,YAAY,EAAE,MAAM,GACnB,MAAM,CAGR"}
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE;AAYD,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,iGAAiG;AACjG,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEzF;AAED,wEAAwE;AACxE,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,YAAY,EAAE,MAAM,GACnB,MAAM,CAGR"}
package/dist/metadata.js CHANGED
@@ -1,17 +1,27 @@
1
+ import { parseFrontMatter } from './front-matter.js';
1
2
  /** Extract the first ATX heading, for example `# Title`, from markdown text. */
2
3
  export function firstMarkdownHeading(markdown) {
3
4
  const match = markdown.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/m);
4
5
  return match?.[1]?.trim();
5
6
  }
7
+ /** Extract a non-empty string `title:` from a leading front matter block, if any. */
8
+ function frontMatterTitle(markdown) {
9
+ const title = parseFrontMatter(markdown)?.data.title;
10
+ if (typeof title !== 'string') {
11
+ return undefined;
12
+ }
13
+ const trimmed = title.trim();
14
+ return trimmed || undefined;
15
+ }
6
16
  /** Derive the file base name, without extension, from a path string. */
7
17
  export function fileBaseNameFromPath(path) {
8
18
  const slashIndex = path.lastIndexOf('/');
9
19
  const fileName = slashIndex >= 0 ? path.slice(slashIndex + 1) : path;
10
20
  return fileName.replace(/\.[^.]+$/, '') || 'document';
11
21
  }
12
- /** Resolve a stable title fallback from markdown heading or filename. */
22
+ /** Resolve a stable title fallback from front matter `title:`, markdown heading, or filename. */
13
23
  export function suggestedTitleFromMarkdown(markdown, fileBaseName) {
14
- return firstMarkdownHeading(markdown) || fileBaseName;
24
+ return frontMatterTitle(markdown) || firstMarkdownHeading(markdown) || fileBaseName;
15
25
  }
16
26
  /** Resolve display title from manifest title with filename fallback. */
17
27
  export function displayTitleFromManifest(manifestTitle, fileBaseName) {
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACjE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,UAAU,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,OAAO,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,UAAU,CAAC;AACxD,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,0BAA0B,CAAC,QAAgB,EAAE,YAAoB;IAC/E,OAAO,oBAAoB,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC;AACxD,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,wBAAwB,CACtC,aAAiC,EACjC,YAAoB;IAEpB,MAAM,OAAO,GAAG,aAAa,EAAE,IAAI,EAAE,CAAC;IACtC,OAAO,OAAO,IAAI,YAAY,CAAC;AACjC,CAAC"}
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,gFAAgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACjE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,qFAAqF;AACrF,SAAS,gBAAgB,CAAC,QAAgB;IACxC,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,IAAI,SAAS,CAAC;AAC9B,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,UAAU,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,OAAO,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,UAAU,CAAC;AACxD,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,0BAA0B,CAAC,QAAgB,EAAE,YAAoB;IAC/E,OAAO,gBAAgB,CAAC,QAAQ,CAAC,IAAI,oBAAoB,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC;AACtF,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,wBAAwB,CACtC,aAAiC,EACjC,YAAoB;IAEpB,MAAM,OAAO,GAAG,aAAa,EAAE,IAAI,EAAE,CAAC;IACtC,OAAO,OAAO,IAAI,YAAY,CAAC;AACjC,CAAC"}
package/dist/preview.d.ts CHANGED
@@ -2,6 +2,7 @@ export type { MdzWorkspace, MdzWorkspaceAsset } from '@mdzip/core-js';
2
2
  export * from './archive-utils.js';
3
3
  export * from './browser.js';
4
4
  export * from './diff.js';
5
+ export * from './front-matter.js';
5
6
  export * from './metadata.js';
6
7
  export * from './rendering.js';
7
8
  export * from './theme.js';
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC"}
package/dist/preview.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './archive-utils.js';
2
2
  export * from './browser.js';
3
3
  export * from './diff.js';
4
+ export * from './front-matter.js';
4
5
  export * from './metadata.js';
5
6
  export * from './rendering.js';
6
7
  export * from './theme.js';
@@ -1 +1 @@
1
- {"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AACA,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AACA,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC"}
@@ -110,6 +110,19 @@ export interface MdzipMarkdownRenderExtension {
110
110
  transformMarkdown?(markdown: string, context: MdzipMarkdownRenderContext): string | Promise<string>;
111
111
  transformHtml?(html: string, context: MdzipMarkdownRenderContext): string | Promise<string>;
112
112
  mount?(container: HTMLElement, context: MdzipMarkdownRenderContext): void | MdzipRenderHandle | Promise<void | MdzipRenderHandle>;
113
+ /**
114
+ * Marks a top-level block token as expensive enough to isolate into its own
115
+ * chunk for {@link groupTokensIntoChunks}, regardless of the char/token
116
+ * budget — e.g. a mermaid extension claiming its own ` ```mermaid ` fence.
117
+ * Without this, a token this extension will re-render on every mount
118
+ * (diagram layout, not just syntax highlighting) shares a chunk with
119
+ * whatever nearby text the budget happened to group it with; editing any
120
+ * of that unrelated text still invalidates the whole chunk's source key,
121
+ * forcing an expensive re-render the edit had nothing to do with — most
122
+ * visible on a short document that fits within one or two chunks anyway,
123
+ * where *every* edit ends up re-rendering the diagram.
124
+ */
125
+ shouldIsolateChunk?(token: Token): boolean;
113
126
  }
114
127
  /**
115
128
  * Context handed to entry renderers for the currently selected archive entry.
@@ -167,6 +180,29 @@ export interface MdzipRenderRequest {
167
180
  export interface MdzipRenderResult {
168
181
  html: string;
169
182
  }
183
+ /**
184
+ * Rendered heading ids carry this prefix (GitHub does the same) so a heading
185
+ * like "## Images" or "## Title" can never collide with a document/form
186
+ * property, which DOMPurify would otherwise strip the id for. Links written
187
+ * against the bare slug (`#images`) still resolve — see the preview click handler.
188
+ */
189
+ export declare const MDZIP_HEADING_ID_PREFIX = "user-content-";
190
+ /**
191
+ * GitHub-style heading slug: lowercased, everything but letters, marks,
192
+ * numbers, `_`, `-` and spaces removed, spaces turned into hyphens.
193
+ */
194
+ export declare function slugifyMdzipHeading(text: string): string;
195
+ /**
196
+ * Gives every heading token (including ones nested in blockquotes and lists)
197
+ * the id its rendered `<hN>` will carry, in document order, disambiguating
198
+ * repeats GitHub-style (`intro`, `intro-1`, `intro-2`). Has to run over the
199
+ * whole document's tokens before they are split into chunks — a chunk rendered
200
+ * on its own can't know how many earlier chunks already used a slug. A heading
201
+ * whose text slugifies to nothing (e.g. only punctuation) gets no id.
202
+ */
203
+ export declare function assignMdzipHeadingIds(tokens: readonly Token[]): void;
204
+ /** The ids {@link assignMdzipHeadingIds} gave the headings in `tokens`, in order. */
205
+ export declare function collectMdzipHeadingIds(tokens: readonly Token[]): string[];
170
206
  export declare const defaultSafeMarkdownRenderer: MdzipMarkdownRenderer;
171
207
  export declare class MdzipRenderingService {
172
208
  private readonly renderer;
@@ -223,7 +259,45 @@ export interface MdzipChunkOptions {
223
259
  tokenCap?: number;
224
260
  /** Approximate max raw markdown chars per chunk (via each token's `.raw.length`). Default 2000. */
225
261
  charBudget?: number;
262
+ /**
263
+ * When a token matches, it's isolated into its own chunk regardless of the
264
+ * budget: any chunk-in-progress is closed first, the matching token forms
265
+ * a chunk by itself, and accumulation resumes fresh after it. Callers
266
+ * combine every registered extension's {@link MdzipMarkdownRenderExtension.shouldIsolateChunk}
267
+ * into one predicate — see that doc comment for why this matters.
268
+ */
269
+ shouldIsolate?: (token: Token) => boolean;
270
+ /**
271
+ * When a token matches, any chunk-in-progress is closed first and the
272
+ * matching token starts a fresh one (unlike `shouldIsolate`, later tokens
273
+ * keep accumulating into that new chunk under the normal budget — the
274
+ * matching token doesn't get a chunk to itself). Used to keep unrelated
275
+ * sections from sharing a chunk: without this, a document short enough to
276
+ * fit several headings' worth of content under the budget lets an edit in
277
+ * one section's prose invalidate a sibling section's untouched heading and
278
+ * content too, tearing them out of the DOM and back in — a real, visible
279
+ * flash for content that never changed.
280
+ */
281
+ shouldStartChunk?: (token: Token) => boolean;
226
282
  }
283
+ /**
284
+ * True when a token's raw source embeds an image (Markdown `![]()` syntax or
285
+ * a raw `<img>` tag). Used to isolate the token into its own chunk: without
286
+ * this, a short document can put an image in the same budget-sized chunk as
287
+ * unrelated surrounding prose, so an edit anywhere in that prose tears the
288
+ * image out of the DOM and reinserts it — a real, visible flash, since the
289
+ * browser has to redo layout for a freshly-inserted `<img>` even when its
290
+ * source is a cache hit.
291
+ */
292
+ export declare function tokenEmbedsImage(token: Token): boolean;
293
+ /**
294
+ * True for a heading token — used as {@link MdzipChunkOptions.shouldStartChunk}
295
+ * so a heading always begins a fresh chunk instead of merging with whatever
296
+ * content (and sibling headings) happened to precede it under the size
297
+ * budget. See that option's doc comment for why: it keeps an edit in one
298
+ * section from also tearing down an untouched sibling section.
299
+ */
300
+ export declare function tokenIsHeading(token: Token): boolean;
227
301
  /**
228
302
  * Groups top-level block tokens from {@link MdzipRenderingService.tokenizeMarkdown}
229
303
  * into chunks suitable for incremental rendering — a token-count cap and a
@@ -231,7 +305,26 @@ export interface MdzipChunkOptions {
231
305
  * tiny tokens (e.g. a chat export's one-line-paragraph-per-message shape)
232
306
  * doesn't end up with one chunk per token. A single token larger than the
233
307
  * budget still gets its own chunk on its own (a chunk boundary can never
234
- * split a token).
308
+ * split a token). `shouldIsolate` overrides the budget in one direction —
309
+ * forcing a matching token into its own chunk even when the budget would
310
+ * have happily grouped it with neighbors; `shouldStartChunk` overrides it in
311
+ * the other — forcing a boundary before a matching token even when the
312
+ * budget would have merged it with what came before.
235
313
  */
236
314
  export declare function groupTokensIntoChunks(tokens: readonly Token[], options?: MdzipChunkOptions): Token[][];
315
+ /**
316
+ * Stable identity key for a chunk: the exact source text `marked` consumed
317
+ * for it (concatenation of each token's `.raw`). Two chunks with the same key
318
+ * are guaranteed to have identical source text, since `.raw` slices tile the
319
+ * whole input contiguously — safe to use as a plain string equality key (no
320
+ * hashing needed; chunks are budget-capped ~2000 chars, and a render involves
321
+ * at most a few dozen of them).
322
+ *
323
+ * Deliberately keys on SOURCE text only, never on rendered output: an
324
+ * extension's `transformHtml` can be side-effecting per call (e.g. the
325
+ * mermaid extension's per-call SVG id counter), so identical keys must never
326
+ * be treated as license to re-run rendering — only as license to skip it and
327
+ * reuse whatever is already mounted for that chunk.
328
+ */
329
+ export declare function chunkSourceKey(chunk: readonly Token[]): string;
237
330
  //# sourceMappingURL=rendering.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rendering.d.ts","sourceRoot":"","sources":["../src/rendering.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rendering.d.ts","sourceRoot":"","sources":["../src/rendering.ts"],"names":[],"mappings":"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"}
package/dist/rendering.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import DOMPurify from 'dompurify';
2
- import hljs from 'highlight.js';
2
+ import hljs from './highlight-core.js';
3
3
  import { Marked } from 'marked';
4
4
  // In browsers the dompurify default export is already bound to the global
5
5
  // window. In Node it is an unbound factory, so bind it to a host-provided DOM
@@ -67,8 +67,105 @@ export function mdzipExtensionMatcher(...extensions) {
67
67
  return wanted.some((ext) => lower.endsWith(ext));
68
68
  };
69
69
  }
70
+ /**
71
+ * Rendered heading ids carry this prefix (GitHub does the same) so a heading
72
+ * like "## Images" or "## Title" can never collide with a document/form
73
+ * property, which DOMPurify would otherwise strip the id for. Links written
74
+ * against the bare slug (`#images`) still resolve — see the preview click handler.
75
+ */
76
+ export const MDZIP_HEADING_ID_PREFIX = 'user-content-';
77
+ function decodeBasicEntities(text) {
78
+ return text
79
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
80
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)))
81
+ .replace(/&quot;/g, '"')
82
+ .replace(/&#39;|&apos;/g, "'")
83
+ .replace(/&lt;/g, '<')
84
+ .replace(/&gt;/g, '>')
85
+ .replace(/&amp;/g, '&')
86
+ // Any other named entity (&copy; etc.) decodes to a symbol the slug
87
+ // pass would strip anyway.
88
+ .replace(/&[a-z][a-z0-9]*;/gi, '');
89
+ }
90
+ function inlineTokensPlainText(tokens) {
91
+ let text = '';
92
+ for (const token of tokens ?? []) {
93
+ const inline = token;
94
+ if (inline.type === 'br') {
95
+ text += ' ';
96
+ }
97
+ else if (inline.type === 'html') {
98
+ // Tag markers only — their inner text arrives as sibling text tokens.
99
+ }
100
+ else if (inline.tokens) {
101
+ text += inlineTokensPlainText(inline.tokens);
102
+ }
103
+ else if (typeof inline.text === 'string') {
104
+ text += inline.text;
105
+ }
106
+ }
107
+ return decodeBasicEntities(text);
108
+ }
109
+ /**
110
+ * GitHub-style heading slug: lowercased, everything but letters, marks,
111
+ * numbers, `_`, `-` and spaces removed, spaces turned into hyphens.
112
+ */
113
+ export function slugifyMdzipHeading(text) {
114
+ return text.toLowerCase().replace(/[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu, '').replace(/ /g, '-');
115
+ }
116
+ function forEachHeadingToken(tokens, visit) {
117
+ for (const token of tokens) {
118
+ if (token.type === 'heading') {
119
+ visit(token);
120
+ }
121
+ else if (token.type === 'blockquote' || token.type === 'list' || token.type === 'list_item') {
122
+ const container = token;
123
+ forEachHeadingToken(container.tokens ?? container.items ?? [], visit);
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Gives every heading token (including ones nested in blockquotes and lists)
129
+ * the id its rendered `<hN>` will carry, in document order, disambiguating
130
+ * repeats GitHub-style (`intro`, `intro-1`, `intro-2`). Has to run over the
131
+ * whole document's tokens before they are split into chunks — a chunk rendered
132
+ * on its own can't know how many earlier chunks already used a slug. A heading
133
+ * whose text slugifies to nothing (e.g. only punctuation) gets no id.
134
+ */
135
+ export function assignMdzipHeadingIds(tokens) {
136
+ const used = new Map();
137
+ forEachHeadingToken(tokens, (token) => {
138
+ const base = slugifyMdzipHeading(inlineTokensPlainText(token.tokens));
139
+ if (!base) {
140
+ delete token.mdzipAnchorId;
141
+ return;
142
+ }
143
+ let candidate = base;
144
+ while (used.has(candidate)) {
145
+ const next = (used.get(base) ?? 0) + 1;
146
+ used.set(base, next);
147
+ candidate = `${base}-${next}`;
148
+ }
149
+ used.set(candidate, 0);
150
+ token.mdzipAnchorId = candidate;
151
+ });
152
+ }
153
+ /** The ids {@link assignMdzipHeadingIds} gave the headings in `tokens`, in order. */
154
+ export function collectMdzipHeadingIds(tokens) {
155
+ const ids = [];
156
+ forEachHeadingToken(tokens, (token) => {
157
+ if (token.mdzipAnchorId) {
158
+ ids.push(token.mdzipAnchorId);
159
+ }
160
+ });
161
+ return ids;
162
+ }
70
163
  const marked = new Marked({
71
164
  renderer: {
165
+ heading(token) {
166
+ const id = token.mdzipAnchorId ? ` id="${MDZIP_HEADING_ID_PREFIX}${escapeHtml(token.mdzipAnchorId)}"` : '';
167
+ return `<h${token.depth}${id}>${this.parser.parseInline(token.tokens)}</h${token.depth}>\n`;
168
+ },
72
169
  code(token) {
73
170
  const requestedLanguage = token.lang || '';
74
171
  const language = requestedLanguage === 'vue' ? 'html' : requestedLanguage;
@@ -106,7 +203,9 @@ const marked = new Marked({
106
203
  }
107
204
  });
108
205
  function renderDefaultMarkdownUnsanitized(markdown) {
109
- const rendered = marked.parse(markdown, { async: false });
206
+ const tokens = marked.lexer(markdown);
207
+ assignMdzipHeadingIds(tokens);
208
+ const rendered = marked.parser(tokens);
110
209
  return typeof rendered === 'string' ? rendered : escapeHtml(markdown);
111
210
  }
112
211
  export const defaultSafeMarkdownRenderer = {
@@ -228,10 +327,14 @@ export class MdzipRenderingService {
228
327
  if (context.signal.aborted) {
229
328
  throw createAbortError();
230
329
  }
231
- return marked.lexer(resolved);
330
+ const tokens = marked.lexer(resolved);
331
+ assignMdzipHeadingIds(tokens);
332
+ return tokens;
232
333
  });
233
334
  }
234
- return marked.lexer(value);
335
+ const tokens = marked.lexer(value);
336
+ assignMdzipHeadingIds(tokens);
337
+ return tokens;
235
338
  }
236
339
  /**
237
340
  * Renders a subset of tokens from {@link tokenizeMarkdown} through
@@ -261,6 +364,31 @@ export class MdzipRenderingService {
261
364
  }
262
365
  const DEFAULT_CHUNK_TOKEN_CAP = 40;
263
366
  const DEFAULT_CHUNK_CHAR_BUDGET = 2000;
367
+ const IMAGE_MARKDOWN_PATTERN = /!\[[^\]]*\]\([^)]*\)/;
368
+ const IMAGE_HTML_PATTERN = /<img\b/i;
369
+ /**
370
+ * True when a token's raw source embeds an image (Markdown `![]()` syntax or
371
+ * a raw `<img>` tag). Used to isolate the token into its own chunk: without
372
+ * this, a short document can put an image in the same budget-sized chunk as
373
+ * unrelated surrounding prose, so an edit anywhere in that prose tears the
374
+ * image out of the DOM and reinserts it — a real, visible flash, since the
375
+ * browser has to redo layout for a freshly-inserted `<img>` even when its
376
+ * source is a cache hit.
377
+ */
378
+ export function tokenEmbedsImage(token) {
379
+ const raw = token.raw ?? '';
380
+ return IMAGE_MARKDOWN_PATTERN.test(raw) || IMAGE_HTML_PATTERN.test(raw);
381
+ }
382
+ /**
383
+ * True for a heading token — used as {@link MdzipChunkOptions.shouldStartChunk}
384
+ * so a heading always begins a fresh chunk instead of merging with whatever
385
+ * content (and sibling headings) happened to precede it under the size
386
+ * budget. See that option's doc comment for why: it keeps an edit in one
387
+ * section from also tearing down an untouched sibling section.
388
+ */
389
+ export function tokenIsHeading(token) {
390
+ return token.type === 'heading';
391
+ }
264
392
  /**
265
393
  * Groups top-level block tokens from {@link MdzipRenderingService.tokenizeMarkdown}
266
394
  * into chunks suitable for incremental rendering — a token-count cap and a
@@ -268,7 +396,11 @@ const DEFAULT_CHUNK_CHAR_BUDGET = 2000;
268
396
  * tiny tokens (e.g. a chat export's one-line-paragraph-per-message shape)
269
397
  * doesn't end up with one chunk per token. A single token larger than the
270
398
  * budget still gets its own chunk on its own (a chunk boundary can never
271
- * split a token).
399
+ * split a token). `shouldIsolate` overrides the budget in one direction —
400
+ * forcing a matching token into its own chunk even when the budget would
401
+ * have happily grouped it with neighbors; `shouldStartChunk` overrides it in
402
+ * the other — forcing a boundary before a matching token even when the
403
+ * budget would have merged it with what came before.
272
404
  */
273
405
  export function groupTokensIntoChunks(tokens, options = {}) {
274
406
  const tokenCap = options.tokenCap ?? DEFAULT_CHUNK_TOKEN_CAP;
@@ -277,6 +409,20 @@ export function groupTokensIntoChunks(tokens, options = {}) {
277
409
  let current = [];
278
410
  let currentChars = 0;
279
411
  for (const token of tokens) {
412
+ if (options.shouldIsolate?.(token)) {
413
+ if (current.length > 0) {
414
+ chunks.push(current);
415
+ current = [];
416
+ currentChars = 0;
417
+ }
418
+ chunks.push([token]);
419
+ continue;
420
+ }
421
+ if (options.shouldStartChunk?.(token) && current.length > 0) {
422
+ chunks.push(current);
423
+ current = [];
424
+ currentChars = 0;
425
+ }
280
426
  current.push(token);
281
427
  currentChars += token.raw?.length ?? 0;
282
428
  if (current.length >= tokenCap || currentChars >= charBudget) {
@@ -290,6 +436,28 @@ export function groupTokensIntoChunks(tokens, options = {}) {
290
436
  }
291
437
  return chunks;
292
438
  }
439
+ /**
440
+ * Stable identity key for a chunk: the exact source text `marked` consumed
441
+ * for it (concatenation of each token's `.raw`). Two chunks with the same key
442
+ * are guaranteed to have identical source text, since `.raw` slices tile the
443
+ * whole input contiguously — safe to use as a plain string equality key (no
444
+ * hashing needed; chunks are budget-capped ~2000 chars, and a render involves
445
+ * at most a few dozen of them).
446
+ *
447
+ * Deliberately keys on SOURCE text only, never on rendered output: an
448
+ * extension's `transformHtml` can be side-effecting per call (e.g. the
449
+ * mermaid extension's per-call SVG id counter), so identical keys must never
450
+ * be treated as license to re-run rendering — only as license to skip it and
451
+ * reuse whatever is already mounted for that chunk.
452
+ */
453
+ export function chunkSourceKey(chunk) {
454
+ const raw = chunk.map((token) => token.raw ?? '').join('');
455
+ // A heading's id depends on the whole document (an earlier duplicate
456
+ // shifts `intro` to `intro-1`), not just this chunk's own text — so a chunk
457
+ // whose source is unchanged but whose ids moved must not be reused as-is.
458
+ const ids = collectMdzipHeadingIds(chunk);
459
+ return ids.length > 0 ? `${raw}\u0000${ids.join('\u0001')}` : raw;
460
+ }
293
461
  function rewriteAssetSources(markdown, resolver) {
294
462
  return markdown.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (match, alt, rawPath, title) => {
295
463
  const path = rawPath.replace(/^<|>$/g, '');