@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
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
- attrs[propKey] = v;
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
- export interface HtmlToken {
3
- type: "open" | "close" | "self" | "text";
4
- tag?: string;
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
- 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;
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
- if (node.tag === "strong" || node.tag === "b")
131
- nextMarks.push({ type: "bold" });
132
- else if (node.tag === "em" || node.tag === "i")
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, sanitizeCssColor, sanitizeTiptapUrl } from "../sanitize";
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" && dataType === "taskList") {
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
- const hasBlock = li.children.some((c) => typeof c !== "string" && ["p", "ul", "ol", "blockquote"].includes(c.tag));
74
- return {
75
- type: "listItem",
76
- content: hasBlock
77
- ? convertTreeToBlocks(li.children)
78
- : [{ type: "paragraph", content: collectInlineNodes(li.children) }],
79
- };
80
- });
81
- return [{ type: el.tag === "ol" ? "orderedList" : "bulletList", content: items }];
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
- const rows = [];
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, TiptapInlineNode } from "../../types";
2
- export declare const parseMarkdownInline: (text: string) => TiptapInlineNode[];
3
- export declare const isLikelyMarkdown: (text: string) => boolean;
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;