@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.
- package/README.md +6 -0
- package/dist/lib/html-parser/html-custom-blocks.js +15 -1
- package/dist/lib/html-parser/html-inline.d.ts +5 -16
- package/dist/lib/html-parser/html-inline.js +52 -103
- package/dist/lib/html-parser/html-parser.js +27 -51
- package/dist/lib/html-parser/html-tokenizer.d.ts +15 -0
- package/dist/lib/html-parser/html-tokenizer.js +91 -0
- package/dist/lib/markdown-parser/markdown-inline.d.ts +5 -0
- package/dist/lib/markdown-parser/markdown-inline.js +135 -0
- package/dist/lib/markdown-parser/markdown-parser.d.ts +3 -3
- package/dist/lib/markdown-parser/markdown-parser.js +20 -136
- package/dist/lib/parser/index.d.ts +1 -0
- package/dist/lib/parser/index.js +1 -0
- package/dist/lib/parser/parser-inline.d.ts +14 -0
- package/dist/lib/parser/parser-inline.js +122 -0
- package/dist/lib/parser/parser-sanitize.d.ts +2 -0
- package/dist/lib/parser/parser-sanitize.js +149 -0
- package/dist/lib/parser/parser.js +28 -238
- package/dist/lib/relations/relations-uri.d.ts +16 -0
- package/dist/lib/relations/relations-uri.js +39 -0
- package/dist/lib/relations/relations.d.ts +4 -17
- package/dist/lib/relations/relations.js +6 -41
- package/dist/lib/sanitize/sanitize-format.d.ts +12 -0
- package/dist/lib/sanitize/sanitize-format.js +158 -0
- package/dist/lib/sanitize/sanitize.d.ts +10 -10
- package/dist/lib/sanitize/sanitize.js +48 -118
- package/dist/lib/theme/theme-default.d.ts +2 -0
- package/dist/lib/theme/theme-default.js +119 -0
- package/dist/lib/theme/theme.d.ts +3 -1
- package/dist/lib/theme/theme.js +10 -118
- package/dist/types/custom.d.ts +19 -0
- package/dist/types/tiptap-inline.d.ts +1 -0
- package/dist/types/tiptap-media.d.ts +146 -0
- package/dist/types/tiptap-media.js +1 -0
- package/dist/types/tiptap.d.ts +3 -132
- package/dist/types/tiptap.js +1 -0
- package/package.json +1 -1
|
@@ -1,65 +1,18 @@
|
|
|
1
1
|
import { parseTiptapHtml } from "../html-parser";
|
|
2
|
-
import {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 = [];
|
package/dist/lib/parser/index.js
CHANGED
|
@@ -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,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
|
+
};
|