@vicacha-devs/astro-tiptap-blocks 1.2.0 → 1.2.1

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 (37) hide show
  1. package/README.md +6 -0
  2. package/dist/lib/html-parser/html-custom-blocks.js +15 -1
  3. package/dist/lib/html-parser/html-inline.d.ts +5 -16
  4. package/dist/lib/html-parser/html-inline.js +52 -103
  5. package/dist/lib/html-parser/html-parser.js +27 -51
  6. package/dist/lib/html-parser/html-tokenizer.d.ts +15 -0
  7. package/dist/lib/html-parser/html-tokenizer.js +91 -0
  8. package/dist/lib/markdown-parser/markdown-inline.d.ts +5 -0
  9. package/dist/lib/markdown-parser/markdown-inline.js +135 -0
  10. package/dist/lib/markdown-parser/markdown-parser.d.ts +3 -3
  11. package/dist/lib/markdown-parser/markdown-parser.js +20 -136
  12. package/dist/lib/parser/index.d.ts +1 -0
  13. package/dist/lib/parser/index.js +1 -0
  14. package/dist/lib/parser/parser-inline.d.ts +14 -0
  15. package/dist/lib/parser/parser-inline.js +122 -0
  16. package/dist/lib/parser/parser-sanitize.d.ts +2 -0
  17. package/dist/lib/parser/parser-sanitize.js +149 -0
  18. package/dist/lib/parser/parser.js +28 -238
  19. package/dist/lib/relations/relations-uri.d.ts +16 -0
  20. package/dist/lib/relations/relations-uri.js +39 -0
  21. package/dist/lib/relations/relations.d.ts +4 -17
  22. package/dist/lib/relations/relations.js +6 -41
  23. package/dist/lib/sanitize/sanitize-format.d.ts +12 -0
  24. package/dist/lib/sanitize/sanitize-format.js +158 -0
  25. package/dist/lib/sanitize/sanitize.d.ts +10 -10
  26. package/dist/lib/sanitize/sanitize.js +48 -118
  27. package/dist/lib/theme/theme-default.d.ts +2 -0
  28. package/dist/lib/theme/theme-default.js +119 -0
  29. package/dist/lib/theme/theme.d.ts +3 -1
  30. package/dist/lib/theme/theme.js +10 -118
  31. package/dist/types/custom.d.ts +19 -0
  32. package/dist/types/tiptap-inline.d.ts +1 -0
  33. package/dist/types/tiptap-media.d.ts +146 -0
  34. package/dist/types/tiptap-media.js +1 -0
  35. package/dist/types/tiptap.d.ts +3 -132
  36. package/dist/types/tiptap.js +1 -0
  37. package/package.json +1 -1
@@ -1,65 +1,18 @@
1
1
  import { parseTiptapHtml } from "../html-parser";
2
- import { sanitizeTiptapIframeUrl, sanitizeTiptapUrl } from "../sanitize";
2
+ import { sanitizeTiptapUrl } from "../sanitize";
3
+ import { isLikelyMarkdown, parseMarkdownInline, parseMediaBlock, parseQuoteChunk, } from "./markdown-inline";
4
+ export { isLikelyMarkdown, parseMarkdownInline };
3
5
  const TIPTAP_META_COMMENT_RE = /\n?<!--\s*tiptap-node:([\s\S]*?)\s*-->$/;
4
- export const parseMarkdownInline = (text) => {
5
- if (!text)
6
- return [];
7
- const nodes = [];
8
- const tokenRe = /(\$(?!\$)([^$\n]+)\$|!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)|\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)|`([^`]+)`|\*\*([^*]+)\*\*|==([^=]+)==|~~([^~]+)~~|\*([^*]+)\*|<([a-zA-Z0-9-]+)(?:\s[^>]*)?>[\s\S]*?<\/\14>)/g;
9
- let lastIdx = 0;
10
- let match;
11
- while ((match = tokenRe.exec(text)) !== null) {
12
- if (match.index > lastIdx)
13
- nodes.push({ type: "text", text: text.slice(lastIdx, match.index) });
14
- if (match[2] !== undefined) {
15
- nodes.push({ type: "mathematics", attrs: { latex: match[2].trim(), displayMode: "inline" } });
16
- }
17
- else if (match[6] !== undefined && match[7] !== undefined) {
18
- const label = match[6];
19
- const rawHref = match[7];
20
- if (/^(?:tiptap|internal|cms|doc):\/\//.test(rawHref)) {
21
- const parts = rawHref.replace(/^[a-z]+:\/\//i, "").split("/");
22
- nodes.push({
23
- type: "linker",
24
- attrs: {
25
- collectionName: parts[0] || null,
26
- documentId: parts[1] || null,
27
- label,
28
- href: rawHref,
29
- },
30
- });
31
- }
32
- else {
33
- nodes.push({
34
- type: "text",
35
- text: label,
36
- marks: [
37
- { type: "link", attrs: { href: sanitizeTiptapUrl(rawHref), title: match[8] || null } },
38
- ],
39
- });
40
- }
41
- }
42
- else if (match[9] !== undefined)
43
- nodes.push({ type: "text", text: match[9], marks: [{ type: "code" }] });
44
- else if (match[10] !== undefined)
45
- nodes.push({ type: "text", text: match[10], marks: [{ type: "bold" }] });
46
- else if (match[11] !== undefined)
47
- nodes.push({ type: "text", text: match[11], marks: [{ type: "highlight" }] });
48
- else if (match[12] !== undefined)
49
- nodes.push({ type: "text", text: match[12], marks: [{ type: "strike" }] });
50
- else if (match[13] !== undefined)
51
- nodes.push({ type: "text", text: match[13], marks: [{ type: "italic" }] });
52
- else if (match[14] !== undefined) {
53
- const first = parseTiptapHtml(`<p>${match[0]}</p>`).content[0];
54
- if (first && "content" in first && Array.isArray(first.content))
55
- nodes.push(...first.content);
56
- }
57
- lastIdx = tokenRe.lastIndex;
58
- }
59
- if (lastIdx < text.length)
60
- nodes.push({ type: "text", text: text.slice(lastIdx) });
61
- return nodes;
62
- };
6
+ const parseTableCells = (rowStr, isHeader) => rowStr
7
+ .trim()
8
+ .replace(/^\||\|$/g, "")
9
+ .split(/(?<!\\)\|/)
10
+ .map((c) => ({
11
+ type: isHeader ? "tableHeader" : "tableCell",
12
+ content: [
13
+ { type: "paragraph", content: parseMarkdownInline(c.replace(/\\\|/g, "|").trim()) },
14
+ ],
15
+ }));
63
16
  const parseMarkdownChunk = (rawChunk) => {
64
17
  const metaMatch = TIPTAP_META_COMMENT_RE.exec(rawChunk);
65
18
  if (metaMatch) {
@@ -69,7 +22,7 @@ const parseMarkdownChunk = (rawChunk) => {
69
22
  return [parsed];
70
23
  }
71
24
  catch {
72
- // Fallback to markdown parsing
25
+ /* fallback to markdown parsing */
73
26
  }
74
27
  }
75
28
  const chunk = rawChunk.replace(TIPTAP_META_COMMENT_RE, "").trim();
@@ -112,35 +65,7 @@ const parseMarkdownChunk = (rawChunk) => {
112
65
  }
113
66
  const mediaMatch = /^\[(?:(Video|Audio|Attachment|Embed):\s*([^\]]*))\]\(([^)\s]+)\)$/.exec(chunk);
114
67
  if (mediaMatch) {
115
- const kind = mediaMatch[1].toLowerCase();
116
- const label = mediaMatch[2].trim();
117
- const rawUrl = mediaMatch[3];
118
- if (kind === "video") {
119
- const src = sanitizeTiptapUrl(rawUrl);
120
- const isEmbed = /youtube\.com|youtu\.be|vimeo\.com/i.test(src);
121
- return src === "#"
122
- ? []
123
- : [
124
- {
125
- type: "video",
126
- attrs: { src, title: label, caption: label, embedType: isEmbed ? "embed" : "file" },
127
- },
128
- ];
129
- }
130
- if (kind === "audio") {
131
- const src = sanitizeTiptapUrl(rawUrl);
132
- return src === "#" ? [] : [{ type: "audio", attrs: { src, title: label, caption: label } }];
133
- }
134
- if (kind === "attachment") {
135
- const href = sanitizeTiptapUrl(rawUrl);
136
- return href === "#"
137
- ? []
138
- : [{ type: "attachment", attrs: { href, filename: label, title: label } }];
139
- }
140
- if (kind === "embed") {
141
- const src = sanitizeTiptapIframeUrl(rawUrl);
142
- return src === "#" ? [] : [{ type: "iframe", attrs: { src, title: label } }];
143
- }
68
+ return parseMediaBlock(mediaMatch[1].toLowerCase(), mediaMatch[2].trim(), mediaMatch[3]);
144
69
  }
145
70
  const imgMatch = /^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)$/.exec(chunk);
146
71
  if (imgMatch) {
@@ -150,41 +75,7 @@ const parseMarkdownChunk = (rawChunk) => {
150
75
  : [{ type: "image", attrs: { src, alt: imgMatch[1] || "", title: imgMatch[3] || null } }];
151
76
  }
152
77
  if (chunk.startsWith(">")) {
153
- const stripped = chunk
154
- .split("\n")
155
- .map((l) => l.replace(/^>\s?/, ""))
156
- .join("\n")
157
- .trim();
158
- if (stripped.startsWith("**Lead:**")) {
159
- return [
160
- {
161
- type: "leadParagraph",
162
- content: parseMarkdownInline(stripped.replace(/^\*\*Lead:\*\*\s*/, "")),
163
- },
164
- ];
165
- }
166
- const calloutMatch = /^\[!(INFO|SUCCESS|WARNING|DANGER|NEUTRAL|TIP)\](?:\s+([^\n]*))?\n?([\s\S]*)$/i.exec(stripped);
167
- if (calloutMatch) {
168
- const variant = calloutMatch[1].toLowerCase();
169
- return [
170
- {
171
- type: "informationPane",
172
- attrs: { variant, title: calloutMatch[2]?.trim() || null },
173
- content: parseTiptapMarkdown(calloutMatch[3] || "").content,
174
- },
175
- ];
176
- }
177
- const convMatch = /^\*\*Conversation History\*\*(?:\s+\*\(([^)]+)\)\*)?\n?([\s\S]*)$/i.exec(stripped);
178
- if (convMatch) {
179
- return [
180
- {
181
- type: "conversationHistory",
182
- attrs: { author: convMatch[1] || null },
183
- content: parseTiptapMarkdown(convMatch[2] || "").content,
184
- },
185
- ];
186
- }
187
- return [{ type: "blockquote", content: parseTiptapMarkdown(stripped).content }];
78
+ return parseQuoteChunk(chunk, (md) => parseTiptapMarkdown(md).content);
188
79
  }
189
80
  const lines = chunk.split("\n");
190
81
  if (lines.every((l) => /^[-*]\s+\[([ xX])\]\s+/.test(l.trim()))) {
@@ -216,9 +107,11 @@ const parseMarkdownChunk = (rawChunk) => {
216
107
  ];
217
108
  }
218
109
  if (lines.every((l) => /^\d+\.\s+/.test(l.trim()))) {
110
+ const firstStart = Number(/^(\d+)\.\s+/.exec(lines[0].trim())?.[1] ?? 1);
219
111
  return [
220
112
  {
221
113
  type: "orderedList",
114
+ ...(firstStart > 1 ? { attrs: { start: firstStart } } : {}),
222
115
  content: lines.map((l) => ({
223
116
  type: "listItem",
224
117
  content: [
@@ -229,20 +122,12 @@ const parseMarkdownChunk = (rawChunk) => {
229
122
  ];
230
123
  }
231
124
  if (lines.length >= 2 && lines[0].includes("|") && /^\s*\|?[\s:-]+\|/.test(lines[1])) {
232
- const parseCells = (rowStr, isHeader) => rowStr
233
- .trim()
234
- .replace(/^\||\|$/g, "")
235
- .split("|")
236
- .map((c) => ({
237
- type: isHeader ? "tableHeader" : "tableCell",
238
- content: [{ type: "paragraph", content: parseMarkdownInline(c.trim()) }],
239
- }));
240
125
  const rows = [
241
- { type: "tableRow", content: parseCells(lines[0], true) },
126
+ { type: "tableRow", content: parseTableCells(lines[0], true) },
242
127
  ...lines
243
128
  .slice(2)
244
129
  .filter((l) => l.includes("|"))
245
- .map((l) => ({ type: "tableRow", content: parseCells(l, false) })),
130
+ .map((l) => ({ type: "tableRow", content: parseTableCells(l, false) })),
246
131
  ];
247
132
  return [{ type: "table", content: rows }];
248
133
  }
@@ -250,7 +135,6 @@ const parseMarkdownChunk = (rawChunk) => {
250
135
  return parseTiptapHtml(chunk).content;
251
136
  return [{ type: "paragraph", content: parseMarkdownInline(chunk) }];
252
137
  };
253
- export const isLikelyMarkdown = (text) => /(?:^|\n)(?:#{1,6}\s|>\s|[-*]\s+\[[ xX]\]\s|[-*]\s+\S|\d+\.\s+\S|```|\$\$|<!--\s*(?:tiptap-node:|pagebreak|toc)|\[(?:Video|Audio|Attachment|Embed):|!\[[^\]]*\]\([^)]+\)|\|.+\|\n\|[\s:-]+\|)/.test(text) || /(\*\*[^*]+\*\*|==[^=]+==|~~[^~]+~~|\[[^\]]+\]\([^)]+\)|\$(?!\$)[^$\n]+\$)/.test(text);
254
138
  export const parseTiptapMarkdown = (markdown) => {
255
139
  const chunks = markdown.trim().split(/\n{2,}(?!(?:[^`]*```[^`]*```)*[^`]*```)/);
256
140
  const content = [];
@@ -1 +1,2 @@
1
1
  export * from "./parser";
2
+ export { extractDetailsParts } from "./parser-inline";
@@ -1 +1,2 @@
1
1
  export * from "./parser";
2
+ export { extractDetailsParts } from "./parser-inline";
@@ -0,0 +1,14 @@
1
+ import type { TiptapBlockNode, TiptapDetailsNode, TiptapInlineNode, TiptapMark, TiptapParserOptions } from "../../types";
2
+ export declare const extractInlineText: (nodes?: TiptapInlineNode[]) => string;
3
+ export declare const extractDetailsParts: (content?: TiptapDetailsNode["content"]) => {
4
+ summary: TiptapInlineNode[];
5
+ body: TiptapBlockNode[];
6
+ };
7
+ export declare const sanitizeMarks: (marks?: TiptapMark[], options?: TiptapParserOptions) => TiptapMark[] | undefined;
8
+ export declare const sanitizeInlineNodes: (nodes?: TiptapInlineNode[], options?: TiptapParserOptions) => TiptapInlineNode[];
9
+ export declare const allocateHeadingId: (explicitId: string | null | undefined, rawText: string, usedIds: Map<string, number>) => string | null;
10
+ export declare const sanitizeCustomBlock: (custom: {
11
+ type?: string;
12
+ attrs?: Record<string, unknown>;
13
+ content?: TiptapBlockNode[];
14
+ }, options: TiptapParserOptions | undefined, recurse: (children?: TiptapBlockNode[]) => TiptapBlockNode[]) => TiptapBlockNode | null;
@@ -0,0 +1,122 @@
1
+ import { resolveInternalLinkHref } from "../relations/relations-uri";
2
+ import { resolveTiptapMediaUrl, sanitizeCssColor, sanitizeTextStyleAttrs, sanitizeTiptapSrcset, slugifyHeadingText, } from "../sanitize";
3
+ const BLOCKED_CUSTOM_ATTR_KEYS = new Set(["__proto__", "constructor", "prototype"]);
4
+ const URL_ATTR_KEY_REGEX = /^(?:src|href|url|action|formaction)$|(?:Url|Href|Src|Uri|_url|_href|_src|-url|-href|-src)$/;
5
+ export const extractInlineText = (nodes) => (nodes ?? [])
6
+ .map((n) => (n.type === "text" ? n.text : n.type === "linker" ? n.attrs.label || "" : ""))
7
+ .join("");
8
+ export const extractDetailsParts = (content = []) => {
9
+ const summaryNode = content.find((child) => child.type === "detailsSummary");
10
+ const detailsContentNode = content.find((child) => child.type === "detailsContent");
11
+ const body = detailsContentNode
12
+ ? detailsContentNode.content || []
13
+ : content.filter((child) => child.type !== "detailsSummary" && child.type !== "detailsContent");
14
+ return { summary: summaryNode?.content || [], body };
15
+ };
16
+ export const sanitizeMarks = (marks, options) => {
17
+ if (!Array.isArray(marks) || marks.length === 0)
18
+ return undefined;
19
+ const result = [];
20
+ for (const mark of marks) {
21
+ if (!mark || typeof mark !== "object" || !("type" in mark))
22
+ continue;
23
+ if (mark.type === "link") {
24
+ result.push({
25
+ ...mark,
26
+ attrs: {
27
+ ...mark.attrs,
28
+ href: resolveInternalLinkHref({ href: mark.attrs?.href }, options?.resolveInternalLink),
29
+ },
30
+ });
31
+ }
32
+ else if (mark.type === "highlight") {
33
+ const color = sanitizeCssColor(mark.attrs?.color);
34
+ result.push({ type: "highlight", ...(color ? { attrs: { color } } : {}) });
35
+ }
36
+ else if (mark.type === "textStyle") {
37
+ const safeAttrs = sanitizeTextStyleAttrs(mark.attrs);
38
+ if (safeAttrs)
39
+ result.push({ type: "textStyle", attrs: safeAttrs });
40
+ }
41
+ else {
42
+ result.push(mark);
43
+ }
44
+ }
45
+ return result.length > 0 ? result : undefined;
46
+ };
47
+ export const sanitizeInlineNodes = (nodes, options) => {
48
+ if (!Array.isArray(nodes))
49
+ return [];
50
+ const result = [];
51
+ for (const node of nodes) {
52
+ if (!node || typeof node !== "object" || !("type" in node))
53
+ continue;
54
+ if (node.type === "text") {
55
+ const marks = sanitizeMarks(node.marks, options);
56
+ result.push({
57
+ type: "text",
58
+ text: typeof node.text === "string" ? node.text : "",
59
+ ...(marks ? { marks } : {}),
60
+ });
61
+ }
62
+ else if (node.type === "hardBreak") {
63
+ result.push({ type: "hardBreak" });
64
+ }
65
+ else if (node.type === "linker") {
66
+ const href = resolveInternalLinkHref(node.attrs || {}, options?.resolveInternalLink);
67
+ result.push({ ...node, attrs: { ...node.attrs, href } });
68
+ }
69
+ else if (node.type === "mathematics") {
70
+ result.push({
71
+ type: "mathematics",
72
+ attrs: {
73
+ latex: String(node.attrs?.latex ?? ""),
74
+ displayMode: node.attrs?.displayMode ?? "inline",
75
+ },
76
+ });
77
+ }
78
+ }
79
+ return result;
80
+ };
81
+ export const allocateHeadingId = (explicitId, rawText, usedIds) => {
82
+ if (explicitId && explicitId.trim()) {
83
+ const clean = explicitId.trim();
84
+ usedIds.set(clean, (usedIds.get(clean) ?? 0) + 1);
85
+ return clean;
86
+ }
87
+ if (!rawText.trim())
88
+ return null;
89
+ const base = slugifyHeadingText(rawText);
90
+ const count = (usedIds.get(base) ?? 0) + 1;
91
+ usedIds.set(base, count);
92
+ return count === 1 ? base : `${base}-${count}`;
93
+ };
94
+ export const sanitizeCustomBlock = (custom, options, recurse) => {
95
+ if (typeof custom.type !== "string" || !custom.type.trim())
96
+ return null;
97
+ const safeAttrs = {};
98
+ for (const [k, v] of Object.entries(custom.attrs ?? {})) {
99
+ if (BLOCKED_CUSTOM_ATTR_KEYS.has(k) || /^on[a-z]/i.test(k))
100
+ continue;
101
+ if (typeof v === "string" && /srcset$/i.test(k)) {
102
+ const safeSrcset = sanitizeTiptapSrcset(v, options);
103
+ if (!safeSrcset)
104
+ return null;
105
+ safeAttrs[k] = safeSrcset;
106
+ }
107
+ else if (typeof v === "string" && URL_ATTR_KEY_REGEX.test(k)) {
108
+ const resolved = resolveTiptapMediaUrl(v, options);
109
+ if (resolved === "#")
110
+ return null;
111
+ safeAttrs[k] = resolved;
112
+ }
113
+ else {
114
+ safeAttrs[k] = v;
115
+ }
116
+ }
117
+ return {
118
+ type: custom.type,
119
+ ...(custom.attrs ? { attrs: safeAttrs } : {}),
120
+ ...(Array.isArray(custom.content) ? { content: recurse(custom.content) } : {}),
121
+ };
122
+ };
@@ -0,0 +1,2 @@
1
+ import type { TiptapBlockNode, TiptapParserOptions } from "../../types";
2
+ export declare const sanitizeBlockNodes: (nodes?: TiptapBlockNode[], options?: TiptapParserOptions, usedHeadingIds?: Map<string, number>) => TiptapBlockNode[];
@@ -0,0 +1,149 @@
1
+ import { resolveTiptapMediaUrl, sanitizeCssColor, sanitizeTiptapIframeUrl, sanitizeTiptapSrcset, } from "../sanitize";
2
+ import { allocateHeadingId, extractInlineText, sanitizeCustomBlock, sanitizeInlineNodes, } from "./parser-inline";
3
+ export const sanitizeBlockNodes = (nodes, options, usedHeadingIds = new Map()) => {
4
+ if (!Array.isArray(nodes))
5
+ return [];
6
+ const recurse = (children) => sanitizeBlockNodes(children, options, usedHeadingIds);
7
+ const result = [];
8
+ for (const node of nodes) {
9
+ if (!node || typeof node !== "object" || !("type" in node))
10
+ continue;
11
+ switch (node.type) {
12
+ case "heading": {
13
+ const content = sanitizeInlineNodes(node.content, options);
14
+ const level = (node.attrs?.level ?? 2);
15
+ const id = allocateHeadingId(node.attrs?.id, extractInlineText(content), usedHeadingIds);
16
+ result.push({ ...node, attrs: { ...node.attrs, level, ...(id ? { id } : {}) }, content });
17
+ break;
18
+ }
19
+ case "paragraph":
20
+ case "leadParagraph":
21
+ result.push({ ...node, content: sanitizeInlineNodes(node.content, options) });
22
+ break;
23
+ case "blockquote":
24
+ case "informationPane":
25
+ case "conversationHistory":
26
+ result.push({ ...node, content: recurse(node.content) });
27
+ break;
28
+ case "details":
29
+ result.push({
30
+ ...node,
31
+ content: (node.content ?? [])
32
+ .map((child) => child.type === "detailsSummary"
33
+ ? {
34
+ type: "detailsSummary",
35
+ content: sanitizeInlineNodes(child.content, options),
36
+ }
37
+ : child.type === "detailsContent"
38
+ ? { type: "detailsContent", content: recurse(child.content) }
39
+ : recurse([child])[0])
40
+ .filter(Boolean),
41
+ });
42
+ break;
43
+ case "layoutContainer":
44
+ result.push({
45
+ ...node,
46
+ content: (node.content ?? []).map((col) => ({
47
+ type: "layoutColumn",
48
+ content: recurse(col.content),
49
+ })),
50
+ });
51
+ break;
52
+ case "bulletList":
53
+ case "orderedList":
54
+ result.push({
55
+ ...node,
56
+ content: (node.content ?? []).map((item) => ({
57
+ type: "listItem",
58
+ content: recurse(item.content),
59
+ })),
60
+ });
61
+ break;
62
+ case "taskList":
63
+ result.push({
64
+ ...node,
65
+ content: (node.content ?? []).map((item) => ({
66
+ type: "taskItem",
67
+ attrs: { checked: Boolean(item.attrs?.checked) },
68
+ content: recurse(item.content),
69
+ })),
70
+ });
71
+ break;
72
+ case "codeBlock":
73
+ result.push({
74
+ ...node,
75
+ content: sanitizeInlineNodes(node.content, options).filter((n) => n.type === "text"),
76
+ });
77
+ break;
78
+ case "image": {
79
+ const safeSrc = resolveTiptapMediaUrl(node.attrs?.src, options);
80
+ if (safeSrc !== "#") {
81
+ const safeSrcset = sanitizeTiptapSrcset(node.attrs?.srcset, options);
82
+ result.push({
83
+ ...node,
84
+ attrs: { ...node.attrs, src: safeSrc, ...(safeSrcset ? { srcset: safeSrcset } : {}) },
85
+ });
86
+ }
87
+ break;
88
+ }
89
+ case "video":
90
+ case "audio": {
91
+ const safeSrc = resolveTiptapMediaUrl(node.attrs?.src, options);
92
+ if (safeSrc !== "#")
93
+ result.push({ ...node, attrs: { ...node.attrs, src: safeSrc } });
94
+ break;
95
+ }
96
+ case "attachment": {
97
+ const safeHref = resolveTiptapMediaUrl(node.attrs?.href, options);
98
+ if (safeHref !== "#")
99
+ result.push({ ...node, attrs: { ...node.attrs, href: safeHref } });
100
+ break;
101
+ }
102
+ case "iframe": {
103
+ const safeSrc = sanitizeTiptapIframeUrl(node.attrs?.src, options?.allowedIframeDomains);
104
+ if (safeSrc !== "#")
105
+ result.push({ ...node, attrs: { ...node.attrs, src: safeSrc } });
106
+ break;
107
+ }
108
+ case "mathematics":
109
+ result.push({
110
+ type: "mathematics",
111
+ attrs: {
112
+ latex: String(node.attrs?.latex ?? ""),
113
+ displayMode: node.attrs?.displayMode ?? "block",
114
+ },
115
+ });
116
+ break;
117
+ case "tableOfContents":
118
+ result.push({ ...node });
119
+ break;
120
+ case "pageBreak":
121
+ case "horizontalRule":
122
+ result.push({ type: node.type });
123
+ break;
124
+ case "table":
125
+ result.push({
126
+ ...node,
127
+ content: (node.content ?? []).map((row) => ({
128
+ type: "tableRow",
129
+ content: (row.content ?? []).map((cell) => ({
130
+ ...cell,
131
+ attrs: {
132
+ ...cell.attrs,
133
+ backgroundColor: sanitizeCssColor(cell.attrs?.backgroundColor),
134
+ },
135
+ content: recurse(cell.content),
136
+ })),
137
+ })),
138
+ });
139
+ break;
140
+ default: {
141
+ const safeCustom = sanitizeCustomBlock(node, options, recurse);
142
+ if (safeCustom)
143
+ result.push(safeCustom);
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ return result;
149
+ };