@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
package/README.md
CHANGED
|
@@ -163,6 +163,12 @@ pnpm check && pnpm build
|
|
|
163
163
|
|
|
164
164
|
---
|
|
165
165
|
|
|
166
|
+
## TODO
|
|
167
|
+
|
|
168
|
+
- [ ] Remove the `conventional-changelog-writer` override from `pnpm-workspace.yaml` once `semantic-release` ships `@semantic-release/release-notes-generator@15` (the `conventionalcommits@10` preset needs writer 9, but `release-notes-generator@14` bundles writer 8).
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
166
172
|
## License
|
|
167
173
|
|
|
168
174
|
MIT © Vicacha Devs
|
|
@@ -154,7 +154,21 @@ export const convertCustomHtmlElement = (el, convertTree) => {
|
|
|
154
154
|
const propKey = k
|
|
155
155
|
.replace(/^data-/, "")
|
|
156
156
|
.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
157
|
-
|
|
157
|
+
if (propKey === "__proto__" ||
|
|
158
|
+
propKey === "constructor" ||
|
|
159
|
+
propKey === "prototype" ||
|
|
160
|
+
/^on[a-z]/i.test(propKey)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (/^(?:src|href|url|action|formaction)$|(?:Url|Href|Src|Uri)$/.test(propKey)) {
|
|
164
|
+
const safeVal = sanitizeTiptapUrl(v);
|
|
165
|
+
if (safeVal === "#")
|
|
166
|
+
return [];
|
|
167
|
+
attrs[propKey] = safeVal;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
attrs[propKey] = v;
|
|
171
|
+
}
|
|
158
172
|
}
|
|
159
173
|
return [{ type: customNodeType, attrs }];
|
|
160
174
|
}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
import type { TiptapInlineNode, TiptapMark } from "../../types";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
attrs?: Record<string, string>;
|
|
6
|
-
text?: string;
|
|
7
|
-
}
|
|
8
|
-
export interface SimpleElement {
|
|
9
|
-
tag: string;
|
|
10
|
-
attrs: Record<string, string>;
|
|
11
|
-
children: Array<SimpleElement | string>;
|
|
12
|
-
}
|
|
13
|
-
export declare const tokenizeHtml: (html: string) => HtmlToken[];
|
|
14
|
-
export declare const buildElementTree: (tokens: HtmlToken[]) => Array<SimpleElement | string>;
|
|
15
|
-
export declare const extractRawText: (nodes: Array<SimpleElement | string>) => string;
|
|
16
|
-
export declare const parseInlineStyleProps: (style?: string) => Record<string, string>;
|
|
1
|
+
import type { TiptapBlockNode, TiptapInlineNode, TiptapMark } from "../../types";
|
|
2
|
+
import { type SimpleElement } from "./html-tokenizer";
|
|
3
|
+
export * from "./html-tokenizer";
|
|
4
|
+
export declare const BLOCK_CHILD_TAGS: Set<string>;
|
|
17
5
|
export declare const collectInlineNodes: (nodes: Array<SimpleElement | string>, activeMarks?: TiptapMark[]) => TiptapInlineNode[];
|
|
6
|
+
export declare const convertTableElement: (el: SimpleElement, convertTree: (nodes: Array<SimpleElement | string>) => TiptapBlockNode[]) => TiptapBlockNode[];
|
|
@@ -1,93 +1,19 @@
|
|
|
1
1
|
import { decodeHtmlEntities, sanitizeCssColor, sanitizeCssDimension, sanitizeCssFontFamily, sanitizeTiptapUrl, } from "../sanitize";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.replace(/<!--[\s\S]*?-->/g, "")
|
|
18
|
-
.replace(/<(script|style|object|embed)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
19
|
-
const tokens = [];
|
|
20
|
-
const tagRegex = /<\/?([a-zA-Z0-9-]+)([^>]*)>/g;
|
|
21
|
-
let lastIndex = 0;
|
|
22
|
-
let match;
|
|
23
|
-
while ((match = tagRegex.exec(sanitized)) !== null) {
|
|
24
|
-
if (match.index > lastIndex) {
|
|
25
|
-
tokens.push({ type: "text", text: sanitized.slice(lastIndex, match.index) });
|
|
26
|
-
}
|
|
27
|
-
const fullMatch = match[0];
|
|
28
|
-
const tag = match[1].toLowerCase();
|
|
29
|
-
const rawAttrs = match[2] || "";
|
|
30
|
-
const isClosing = fullMatch.startsWith("</");
|
|
31
|
-
const isSelfClosing = fullMatch.endsWith("/>") || SELF_CLOSING_TAGS.has(tag);
|
|
32
|
-
if (!IGNORED_CONTAINERS.has(tag)) {
|
|
33
|
-
if (isClosing)
|
|
34
|
-
tokens.push({ type: "close", tag });
|
|
35
|
-
else if (isSelfClosing)
|
|
36
|
-
tokens.push({ type: "self", tag, attrs: parseAttributes(rawAttrs) });
|
|
37
|
-
else
|
|
38
|
-
tokens.push({ type: "open", tag, attrs: parseAttributes(rawAttrs) });
|
|
39
|
-
}
|
|
40
|
-
lastIndex = tagRegex.lastIndex;
|
|
41
|
-
}
|
|
42
|
-
if (lastIndex < sanitized.length) {
|
|
43
|
-
tokens.push({ type: "text", text: sanitized.slice(lastIndex) });
|
|
44
|
-
}
|
|
45
|
-
return tokens;
|
|
46
|
-
};
|
|
47
|
-
export const buildElementTree = (tokens) => {
|
|
48
|
-
const root = { tag: "root", attrs: {}, children: [] };
|
|
49
|
-
const stack = [root];
|
|
50
|
-
for (const token of tokens) {
|
|
51
|
-
const current = stack[stack.length - 1];
|
|
52
|
-
if (token.type === "text" && token.text !== undefined) {
|
|
53
|
-
current.children.push(token.text);
|
|
54
|
-
}
|
|
55
|
-
else if (token.type === "self" && token.tag) {
|
|
56
|
-
current.children.push({ tag: token.tag, attrs: token.attrs || {}, children: [] });
|
|
57
|
-
}
|
|
58
|
-
else if (token.type === "open" && token.tag) {
|
|
59
|
-
const el = { tag: token.tag, attrs: token.attrs || {}, children: [] };
|
|
60
|
-
current.children.push(el);
|
|
61
|
-
stack.push(el);
|
|
62
|
-
}
|
|
63
|
-
else if (token.type === "close" && token.tag) {
|
|
64
|
-
for (let i = stack.length - 1; i >= 1; i--) {
|
|
65
|
-
if (stack[i].tag === token.tag) {
|
|
66
|
-
stack.length = i;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return root.children;
|
|
73
|
-
};
|
|
74
|
-
export const extractRawText = (nodes) => nodes
|
|
75
|
-
.map((n) => typeof n === "string"
|
|
76
|
-
? decodeHtmlEntities(n)
|
|
77
|
-
: n.tag === "br"
|
|
78
|
-
? "\n"
|
|
79
|
-
: extractRawText(n.children))
|
|
80
|
-
.join("");
|
|
81
|
-
export const parseInlineStyleProps = (style) => {
|
|
82
|
-
const map = {};
|
|
83
|
-
if (!style)
|
|
84
|
-
return map;
|
|
85
|
-
for (const part of style.split(";")) {
|
|
86
|
-
const idx = part.indexOf(":");
|
|
87
|
-
if (idx > 0)
|
|
88
|
-
map[part.slice(0, idx).trim().toLowerCase()] = part.slice(idx + 1).trim();
|
|
89
|
-
}
|
|
90
|
-
return map;
|
|
2
|
+
import { extractRawText, parseInlineStyleProps } from "./html-tokenizer";
|
|
3
|
+
export * from "./html-tokenizer";
|
|
4
|
+
export const BLOCK_CHILD_TAGS = new Set(["p", "ul", "ol", "blockquote", "pre", "table", "details"]);
|
|
5
|
+
const TAG_TO_MARK = {
|
|
6
|
+
strong: "bold",
|
|
7
|
+
b: "bold",
|
|
8
|
+
em: "italic",
|
|
9
|
+
i: "italic",
|
|
10
|
+
u: "underline",
|
|
11
|
+
s: "strike",
|
|
12
|
+
strike: "strike",
|
|
13
|
+
del: "strike",
|
|
14
|
+
code: "code",
|
|
15
|
+
sub: "subscript",
|
|
16
|
+
sup: "superscript",
|
|
91
17
|
};
|
|
92
18
|
export const collectInlineNodes = (nodes, activeMarks = []) => {
|
|
93
19
|
const result = [];
|
|
@@ -127,20 +53,9 @@ export const collectInlineNodes = (nodes, activeMarks = []) => {
|
|
|
127
53
|
}
|
|
128
54
|
const nextMarks = [...activeMarks];
|
|
129
55
|
const styles = parseInlineStyleProps(node.attrs.style);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
nextMarks.push({ type: "italic" });
|
|
134
|
-
else if (node.tag === "u")
|
|
135
|
-
nextMarks.push({ type: "underline" });
|
|
136
|
-
else if (node.tag === "s" || node.tag === "strike" || node.tag === "del")
|
|
137
|
-
nextMarks.push({ type: "strike" });
|
|
138
|
-
else if (node.tag === "code")
|
|
139
|
-
nextMarks.push({ type: "code" });
|
|
140
|
-
else if (node.tag === "sub")
|
|
141
|
-
nextMarks.push({ type: "subscript" });
|
|
142
|
-
else if (node.tag === "sup")
|
|
143
|
-
nextMarks.push({ type: "superscript" });
|
|
56
|
+
const simpleMarkType = TAG_TO_MARK[node.tag];
|
|
57
|
+
if (simpleMarkType)
|
|
58
|
+
nextMarks.push({ type: simpleMarkType });
|
|
144
59
|
else if (node.tag === "abbr")
|
|
145
60
|
nextMarks.push({ type: "abbr", attrs: { title: node.attrs.title || null } });
|
|
146
61
|
else if (node.tag === "mark") {
|
|
@@ -179,3 +94,37 @@ export const collectInlineNodes = (nodes, activeMarks = []) => {
|
|
|
179
94
|
}
|
|
180
95
|
return result;
|
|
181
96
|
};
|
|
97
|
+
export const convertTableElement = (el, convertTree) => {
|
|
98
|
+
const rows = [];
|
|
99
|
+
const collectRows = (children) => {
|
|
100
|
+
for (const child of children) {
|
|
101
|
+
if (typeof child === "string")
|
|
102
|
+
continue;
|
|
103
|
+
if (child.tag === "tr") {
|
|
104
|
+
const cells = child.children
|
|
105
|
+
.filter((c) => typeof c !== "string" && (c.tag === "td" || c.tag === "th"))
|
|
106
|
+
.map((cell) => {
|
|
107
|
+
const styles = parseInlineStyleProps(cell.attrs.style);
|
|
108
|
+
const bg = sanitizeCssColor(cell.attrs["data-background-color"] || styles["background-color"]);
|
|
109
|
+
const hasBlocks = cell.children.some((c) => typeof c !== "string" && BLOCK_CHILD_TAGS.has(c.tag));
|
|
110
|
+
return {
|
|
111
|
+
type: cell.tag === "th" ? "tableHeader" : "tableCell",
|
|
112
|
+
attrs: {
|
|
113
|
+
...(cell.attrs.colspan ? { colspan: Number(cell.attrs.colspan) } : {}),
|
|
114
|
+
...(cell.attrs.rowspan ? { rowspan: Number(cell.attrs.rowspan) } : {}),
|
|
115
|
+
...(bg ? { backgroundColor: bg } : {}),
|
|
116
|
+
},
|
|
117
|
+
content: hasBlocks
|
|
118
|
+
? convertTree(cell.children)
|
|
119
|
+
: [{ type: "paragraph", content: collectInlineNodes(cell.children) }],
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
rows.push({ type: "tableRow", content: cells });
|
|
123
|
+
}
|
|
124
|
+
else if (["thead", "tbody", "tfoot"].includes(child.tag))
|
|
125
|
+
collectRows(child.children);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
collectRows(el.children);
|
|
129
|
+
return [{ type: "table", content: rows }];
|
|
130
|
+
};
|
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
import { decodeHtmlEntities,
|
|
1
|
+
import { decodeHtmlEntities, sanitizeTiptapSrcset, sanitizeTiptapUrl } from "../sanitize";
|
|
2
2
|
import { convertCustomHtmlElement, findChildByTag } from "./html-custom-blocks";
|
|
3
|
-
import { buildElementTree, collectInlineNodes, extractRawText, parseInlineStyleProps, tokenizeHtml, } from "./html-inline";
|
|
3
|
+
import { BLOCK_CHILD_TAGS, buildElementTree, collectInlineNodes, convertTableElement, extractRawText, parseInlineStyleProps, tokenizeHtml, } from "./html-inline";
|
|
4
4
|
const extractBaseBlockAttrs = (el) => {
|
|
5
5
|
const styles = parseInlineStyleProps(el.attrs.style);
|
|
6
6
|
const textAlign = (el.attrs["data-text-align"] ||
|
|
7
7
|
styles["text-align"] ||
|
|
8
8
|
null);
|
|
9
9
|
const dir = (el.attrs.dir || null);
|
|
10
|
-
return {
|
|
11
|
-
...(textAlign ? { textAlign } : {}),
|
|
12
|
-
...(dir ? { dir } : {}),
|
|
13
|
-
};
|
|
10
|
+
return { ...(textAlign ? { textAlign } : {}), ...(dir ? { dir } : {}) };
|
|
14
11
|
};
|
|
15
12
|
const convertElementToBlocks = (el) => {
|
|
16
13
|
const custom = convertCustomHtmlElement(el, convertTreeToBlocks);
|
|
17
|
-
if (custom !== null)
|
|
14
|
+
if (custom !== null)
|
|
18
15
|
return custom;
|
|
19
|
-
}
|
|
20
|
-
const dataType = el.attrs["data-type"] || "";
|
|
21
16
|
if (/^h[1-6]$/.test(el.tag)) {
|
|
22
17
|
const level = Number(el.attrs["data-level"] || el.tag[1]);
|
|
23
18
|
const seoTag = (el.attrs["data-seo-tag"] || null);
|
|
@@ -56,7 +51,7 @@ const convertElementToBlocks = (el) => {
|
|
|
56
51
|
},
|
|
57
52
|
];
|
|
58
53
|
}
|
|
59
|
-
if (el.tag === "ul" &&
|
|
54
|
+
if (el.tag === "ul" && el.attrs["data-type"] === "taskList") {
|
|
60
55
|
const items = el.children
|
|
61
56
|
.filter((c) => typeof c !== "string" && c.tag === "li")
|
|
62
57
|
.map((li) => ({
|
|
@@ -69,16 +64,23 @@ const convertElementToBlocks = (el) => {
|
|
|
69
64
|
if (el.tag === "ul" || el.tag === "ol") {
|
|
70
65
|
const items = el.children
|
|
71
66
|
.filter((c) => typeof c !== "string" && c.tag === "li")
|
|
72
|
-
.map((li) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
content:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
.map((li) => ({
|
|
68
|
+
type: "listItem",
|
|
69
|
+
content: li.children.some((c) => typeof c !== "string" && BLOCK_CHILD_TAGS.has(c.tag))
|
|
70
|
+
? convertTreeToBlocks(li.children)
|
|
71
|
+
: [{ type: "paragraph", content: collectInlineNodes(li.children) }],
|
|
72
|
+
}));
|
|
73
|
+
if (el.tag === "ol") {
|
|
74
|
+
const startAttr = el.attrs.start ? Number(el.attrs.start) : undefined;
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
type: "orderedList",
|
|
78
|
+
...(startAttr && startAttr > 1 ? { attrs: { start: startAttr } } : {}),
|
|
79
|
+
content: items,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
return [{ type: "bulletList", content: items }];
|
|
82
84
|
}
|
|
83
85
|
if (el.tag === "pre") {
|
|
84
86
|
const codeEl = findChildByTag(el, "code");
|
|
@@ -108,6 +110,7 @@ const convertElementToBlocks = (el) => {
|
|
|
108
110
|
const src = sanitizeTiptapUrl(el.attrs.src);
|
|
109
111
|
if (src === "#")
|
|
110
112
|
return [];
|
|
113
|
+
const safeSrcset = sanitizeTiptapSrcset(el.attrs.srcset);
|
|
111
114
|
return [
|
|
112
115
|
{
|
|
113
116
|
type: "image",
|
|
@@ -118,6 +121,8 @@ const convertElementToBlocks = (el) => {
|
|
|
118
121
|
"caption": el.attrs["data-caption"] || null,
|
|
119
122
|
"width": el.attrs.width || null,
|
|
120
123
|
"height": el.attrs.height || null,
|
|
124
|
+
...(safeSrcset ? { srcset: safeSrcset } : {}),
|
|
125
|
+
...(el.attrs.sizes ? { sizes: el.attrs.sizes } : {}),
|
|
121
126
|
"data-align": el.attrs["data-align"] || null,
|
|
122
127
|
"data-asset-id": el.attrs["data-asset-id"] || null,
|
|
123
128
|
"data-document-id": el.attrs["data-document-id"] || null,
|
|
@@ -127,37 +132,8 @@ const convertElementToBlocks = (el) => {
|
|
|
127
132
|
}
|
|
128
133
|
if (el.tag === "hr")
|
|
129
134
|
return [{ type: "horizontalRule" }];
|
|
130
|
-
if (el.tag === "table")
|
|
131
|
-
|
|
132
|
-
const collectRows = (children) => {
|
|
133
|
-
for (const child of children) {
|
|
134
|
-
if (typeof child === "string")
|
|
135
|
-
continue;
|
|
136
|
-
if (child.tag === "tr") {
|
|
137
|
-
const cells = child.children
|
|
138
|
-
.filter((c) => typeof c !== "string" && (c.tag === "td" || c.tag === "th"))
|
|
139
|
-
.map((cell) => {
|
|
140
|
-
const styles = parseInlineStyleProps(cell.attrs.style);
|
|
141
|
-
const bg = sanitizeCssColor(cell.attrs["data-background-color"] || styles["background-color"]);
|
|
142
|
-
return {
|
|
143
|
-
type: cell.tag === "th" ? "tableHeader" : "tableCell",
|
|
144
|
-
attrs: {
|
|
145
|
-
...(cell.attrs.colspan ? { colspan: Number(cell.attrs.colspan) } : {}),
|
|
146
|
-
...(cell.attrs.rowspan ? { rowspan: Number(cell.attrs.rowspan) } : {}),
|
|
147
|
-
...(bg ? { backgroundColor: bg } : {}),
|
|
148
|
-
},
|
|
149
|
-
content: [{ type: "paragraph", content: collectInlineNodes(cell.children) }],
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
rows.push({ type: "tableRow", content: cells });
|
|
153
|
-
}
|
|
154
|
-
else if (["thead", "tbody", "tfoot"].includes(child.tag))
|
|
155
|
-
collectRows(child.children);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
collectRows(el.children);
|
|
159
|
-
return [{ type: "table", content: rows }];
|
|
160
|
-
}
|
|
135
|
+
if (el.tag === "table")
|
|
136
|
+
return convertTableElement(el, convertTreeToBlocks);
|
|
161
137
|
return convertTreeToBlocks(el.children);
|
|
162
138
|
};
|
|
163
139
|
export const convertTreeToBlocks = (nodes) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface HtmlToken {
|
|
2
|
+
type: "open" | "close" | "self" | "text";
|
|
3
|
+
tag?: string;
|
|
4
|
+
attrs?: Record<string, string>;
|
|
5
|
+
text?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SimpleElement {
|
|
8
|
+
tag: string;
|
|
9
|
+
attrs: Record<string, string>;
|
|
10
|
+
children: Array<SimpleElement | string>;
|
|
11
|
+
}
|
|
12
|
+
export declare const tokenizeHtml: (html: string) => HtmlToken[];
|
|
13
|
+
export declare const buildElementTree: (tokens: HtmlToken[]) => Array<SimpleElement | string>;
|
|
14
|
+
export declare const extractRawText: (nodes: Array<SimpleElement | string>) => string;
|
|
15
|
+
export declare const parseInlineStyleProps: (style?: string) => Record<string, string>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { decodeHtmlEntities } from "../sanitize";
|
|
2
|
+
const SELF_CLOSING_TAGS = new Set(["br", "hr", "img", "input", "meta", "link", "source"]);
|
|
3
|
+
const IGNORED_CONTAINERS = new Set(["script", "style", "object", "embed"]);
|
|
4
|
+
const parseAttributes = (attrString) => {
|
|
5
|
+
const attrs = {};
|
|
6
|
+
const attrRegex = /([a-zA-Z0-9_:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = attrRegex.exec(attrString)) !== null) {
|
|
9
|
+
const key = match[1].toLowerCase();
|
|
10
|
+
const val = match[2] ?? match[3] ?? match[4] ?? "";
|
|
11
|
+
attrs[key] = decodeHtmlEntities(val);
|
|
12
|
+
}
|
|
13
|
+
return attrs;
|
|
14
|
+
};
|
|
15
|
+
export const tokenizeHtml = (html) => {
|
|
16
|
+
const sanitized = html
|
|
17
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
18
|
+
.replace(/<(script|style|object|embed)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
19
|
+
const tokens = [];
|
|
20
|
+
const tagRegex = /<\/?([a-zA-Z0-9-]+)((?:"[^"]*"|'[^']*'|[^>"'])*)>/g;
|
|
21
|
+
let lastIndex = 0;
|
|
22
|
+
let match;
|
|
23
|
+
while ((match = tagRegex.exec(sanitized)) !== null) {
|
|
24
|
+
if (match.index > lastIndex) {
|
|
25
|
+
tokens.push({ type: "text", text: sanitized.slice(lastIndex, match.index) });
|
|
26
|
+
}
|
|
27
|
+
const fullMatch = match[0];
|
|
28
|
+
const tag = match[1].toLowerCase();
|
|
29
|
+
const rawAttrs = match[2] || "";
|
|
30
|
+
const isClosing = fullMatch.startsWith("</");
|
|
31
|
+
const isSelfClosing = fullMatch.endsWith("/>") || SELF_CLOSING_TAGS.has(tag);
|
|
32
|
+
if (!IGNORED_CONTAINERS.has(tag)) {
|
|
33
|
+
if (isClosing)
|
|
34
|
+
tokens.push({ type: "close", tag });
|
|
35
|
+
else if (isSelfClosing)
|
|
36
|
+
tokens.push({ type: "self", tag, attrs: parseAttributes(rawAttrs) });
|
|
37
|
+
else
|
|
38
|
+
tokens.push({ type: "open", tag, attrs: parseAttributes(rawAttrs) });
|
|
39
|
+
}
|
|
40
|
+
lastIndex = tagRegex.lastIndex;
|
|
41
|
+
}
|
|
42
|
+
if (lastIndex < sanitized.length) {
|
|
43
|
+
tokens.push({ type: "text", text: sanitized.slice(lastIndex) });
|
|
44
|
+
}
|
|
45
|
+
return tokens;
|
|
46
|
+
};
|
|
47
|
+
export const buildElementTree = (tokens) => {
|
|
48
|
+
const root = { tag: "root", attrs: {}, children: [] };
|
|
49
|
+
const stack = [root];
|
|
50
|
+
for (const token of tokens) {
|
|
51
|
+
const current = stack[stack.length - 1];
|
|
52
|
+
if (token.type === "text" && token.text !== undefined) {
|
|
53
|
+
current.children.push(token.text);
|
|
54
|
+
}
|
|
55
|
+
else if (token.type === "self" && token.tag) {
|
|
56
|
+
current.children.push({ tag: token.tag, attrs: token.attrs || {}, children: [] });
|
|
57
|
+
}
|
|
58
|
+
else if (token.type === "open" && token.tag) {
|
|
59
|
+
const el = { tag: token.tag, attrs: token.attrs || {}, children: [] };
|
|
60
|
+
current.children.push(el);
|
|
61
|
+
stack.push(el);
|
|
62
|
+
}
|
|
63
|
+
else if (token.type === "close" && token.tag) {
|
|
64
|
+
for (let i = stack.length - 1; i >= 1; i--) {
|
|
65
|
+
if (stack[i].tag === token.tag) {
|
|
66
|
+
stack.length = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return root.children;
|
|
73
|
+
};
|
|
74
|
+
export const extractRawText = (nodes) => nodes
|
|
75
|
+
.map((n) => typeof n === "string"
|
|
76
|
+
? decodeHtmlEntities(n)
|
|
77
|
+
: n.tag === "br"
|
|
78
|
+
? "\n"
|
|
79
|
+
: extractRawText(n.children))
|
|
80
|
+
.join("");
|
|
81
|
+
export const parseInlineStyleProps = (style) => {
|
|
82
|
+
const map = {};
|
|
83
|
+
if (!style)
|
|
84
|
+
return map;
|
|
85
|
+
for (const part of style.split(";")) {
|
|
86
|
+
const idx = part.indexOf(":");
|
|
87
|
+
if (idx > 0)
|
|
88
|
+
map[part.slice(0, idx).trim().toLowerCase()] = part.slice(idx + 1).trim();
|
|
89
|
+
}
|
|
90
|
+
return map;
|
|
91
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TiptapBlockNode, TiptapInlineNode } from "../../types";
|
|
2
|
+
export declare const parseMarkdownInline: (text: string) => TiptapInlineNode[];
|
|
3
|
+
export declare const parseMediaBlock: (kind: string, label: string, rawUrl: string) => TiptapBlockNode[];
|
|
4
|
+
export declare const parseQuoteChunk: (chunk: string, parseBody: (md: string) => TiptapBlockNode[]) => TiptapBlockNode[];
|
|
5
|
+
export declare const isLikelyMarkdown: (text: string) => boolean;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { parseTiptapHtml } from "../html-parser";
|
|
2
|
+
import { sanitizeTiptapIframeUrl, sanitizeTiptapUrl } from "../sanitize";
|
|
3
|
+
export const parseMarkdownInline = (text) => {
|
|
4
|
+
if (!text)
|
|
5
|
+
return [];
|
|
6
|
+
const nodes = [];
|
|
7
|
+
const tokenRe = /(\$(?!\$)([^$\n]+)\$|!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)|\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)|`([^`]+)`|\*\*([^*]+)\*\*|==([^=]+)==|~~([^~]+)~~|\*([^*]+)\*|<([a-zA-Z0-9-]+)(?:\s[^>]*)?>[\s\S]*?<\/\14>)/g;
|
|
8
|
+
let lastIdx = 0;
|
|
9
|
+
let match;
|
|
10
|
+
while ((match = tokenRe.exec(text)) !== null) {
|
|
11
|
+
if (match.index > lastIdx)
|
|
12
|
+
nodes.push({ type: "text", text: text.slice(lastIdx, match.index) });
|
|
13
|
+
if (match[2] !== undefined) {
|
|
14
|
+
nodes.push({ type: "mathematics", attrs: { latex: match[2].trim(), displayMode: "inline" } });
|
|
15
|
+
}
|
|
16
|
+
else if (match[4] !== undefined) {
|
|
17
|
+
const safeHref = sanitizeTiptapUrl(match[4]);
|
|
18
|
+
if (safeHref !== "#") {
|
|
19
|
+
nodes.push({
|
|
20
|
+
type: "text",
|
|
21
|
+
text: match[3] || "Image",
|
|
22
|
+
marks: [{ type: "link", attrs: { href: safeHref, title: match[5] || null } }],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (match[6] !== undefined && match[7] !== undefined) {
|
|
27
|
+
const label = match[6];
|
|
28
|
+
const rawHref = match[7];
|
|
29
|
+
if (/^(?:tiptap|internal|cms|doc):\/\//.test(rawHref)) {
|
|
30
|
+
const parts = rawHref.replace(/^[a-z]+:\/\//i, "").split("/");
|
|
31
|
+
nodes.push({
|
|
32
|
+
type: "linker",
|
|
33
|
+
attrs: {
|
|
34
|
+
collectionName: parts[0] || null,
|
|
35
|
+
documentId: parts[1] || null,
|
|
36
|
+
label,
|
|
37
|
+
href: rawHref,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
nodes.push({
|
|
43
|
+
type: "text",
|
|
44
|
+
text: label,
|
|
45
|
+
marks: [
|
|
46
|
+
{ type: "link", attrs: { href: sanitizeTiptapUrl(rawHref), title: match[8] || null } },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (match[9] !== undefined)
|
|
52
|
+
nodes.push({ type: "text", text: match[9], marks: [{ type: "code" }] });
|
|
53
|
+
else if (match[10] !== undefined)
|
|
54
|
+
nodes.push({ type: "text", text: match[10], marks: [{ type: "bold" }] });
|
|
55
|
+
else if (match[11] !== undefined)
|
|
56
|
+
nodes.push({ type: "text", text: match[11], marks: [{ type: "highlight" }] });
|
|
57
|
+
else if (match[12] !== undefined)
|
|
58
|
+
nodes.push({ type: "text", text: match[12], marks: [{ type: "strike" }] });
|
|
59
|
+
else if (match[13] !== undefined)
|
|
60
|
+
nodes.push({ type: "text", text: match[13], marks: [{ type: "italic" }] });
|
|
61
|
+
else if (match[14] !== undefined) {
|
|
62
|
+
const first = parseTiptapHtml(`<p>${match[0]}</p>`).content[0];
|
|
63
|
+
if (first && "content" in first && Array.isArray(first.content))
|
|
64
|
+
nodes.push(...first.content);
|
|
65
|
+
}
|
|
66
|
+
lastIdx = tokenRe.lastIndex;
|
|
67
|
+
}
|
|
68
|
+
if (lastIdx < text.length)
|
|
69
|
+
nodes.push({ type: "text", text: text.slice(lastIdx) });
|
|
70
|
+
return nodes;
|
|
71
|
+
};
|
|
72
|
+
export const parseMediaBlock = (kind, label, rawUrl) => {
|
|
73
|
+
if (kind === "video") {
|
|
74
|
+
const src = sanitizeTiptapUrl(rawUrl);
|
|
75
|
+
const isEmbed = /youtube\.com|youtu\.be|vimeo\.com/i.test(src);
|
|
76
|
+
return src === "#"
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
{
|
|
80
|
+
type: "video",
|
|
81
|
+
attrs: { src, title: label, caption: label, embedType: isEmbed ? "embed" : "file" },
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
if (kind === "audio") {
|
|
86
|
+
const src = sanitizeTiptapUrl(rawUrl);
|
|
87
|
+
return src === "#" ? [] : [{ type: "audio", attrs: { src, title: label, caption: label } }];
|
|
88
|
+
}
|
|
89
|
+
if (kind === "attachment") {
|
|
90
|
+
const href = sanitizeTiptapUrl(rawUrl);
|
|
91
|
+
return href === "#"
|
|
92
|
+
? []
|
|
93
|
+
: [{ type: "attachment", attrs: { href, filename: label, title: label } }];
|
|
94
|
+
}
|
|
95
|
+
const src = sanitizeTiptapIframeUrl(rawUrl);
|
|
96
|
+
return src === "#" ? [] : [{ type: "iframe", attrs: { src, title: label } }];
|
|
97
|
+
};
|
|
98
|
+
export const parseQuoteChunk = (chunk, parseBody) => {
|
|
99
|
+
const stripped = chunk
|
|
100
|
+
.split("\n")
|
|
101
|
+
.map((l) => l.replace(/^>\s?/, ""))
|
|
102
|
+
.join("\n")
|
|
103
|
+
.trim();
|
|
104
|
+
if (stripped.startsWith("**Lead:**")) {
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
type: "leadParagraph",
|
|
108
|
+
content: parseMarkdownInline(stripped.replace(/^\*\*Lead:\*\*\s*/, "")),
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
const calloutMatch = /^\[!(INFO|SUCCESS|WARNING|DANGER|NEUTRAL|TIP)\](?:\s+([^\n]*))?\n?([\s\S]*)$/i.exec(stripped);
|
|
113
|
+
if (calloutMatch) {
|
|
114
|
+
const variant = calloutMatch[1].toLowerCase();
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
type: "informationPane",
|
|
118
|
+
attrs: { variant, title: calloutMatch[2]?.trim() || null },
|
|
119
|
+
content: parseBody(calloutMatch[3] || ""),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
const convMatch = /^\*\*Conversation History\*\*(?:\s+\*\(([^)]+)\)\*)?\n?([\s\S]*)$/i.exec(stripped);
|
|
124
|
+
if (convMatch) {
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
type: "conversationHistory",
|
|
128
|
+
attrs: { author: convMatch[1] || null },
|
|
129
|
+
content: parseBody(convMatch[2] || ""),
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
return [{ type: "blockquote", content: parseBody(stripped) }];
|
|
134
|
+
};
|
|
135
|
+
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);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TiptapDocNode
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
import type { TiptapDocNode } from "../../types";
|
|
2
|
+
import { isLikelyMarkdown, parseMarkdownInline } from "./markdown-inline";
|
|
3
|
+
export { isLikelyMarkdown, parseMarkdownInline };
|
|
4
4
|
export declare const parseTiptapMarkdown: (markdown: string) => TiptapDocNode;
|