@wrrdev/fenix-ui 1.1.14 → 1.1.16

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 (81) hide show
  1. package/README.md +5 -4
  2. package/dist/components/editor/commands.d.ts +27 -0
  3. package/dist/components/editor/commands.d.ts.map +1 -0
  4. package/dist/components/editor/commands.js +235 -0
  5. package/dist/components/editor/commands.js.map +1 -0
  6. package/dist/components/editor/editor.d.ts +83 -0
  7. package/dist/components/editor/editor.d.ts.map +1 -0
  8. package/dist/components/editor/editor.js +1951 -0
  9. package/dist/components/editor/editor.js.map +1 -0
  10. package/dist/components/editor/index.d.ts +12 -0
  11. package/dist/components/editor/index.d.ts.map +1 -0
  12. package/dist/components/editor/index.js +7 -0
  13. package/dist/components/editor/index.js.map +1 -0
  14. package/dist/components/editor/normalize-html.d.ts +33 -0
  15. package/dist/components/editor/normalize-html.d.ts.map +1 -0
  16. package/dist/components/editor/normalize-html.js +316 -0
  17. package/dist/components/editor/normalize-html.js.map +1 -0
  18. package/dist/components/editor/sanitize-html.d.ts +37 -0
  19. package/dist/components/editor/sanitize-html.d.ts.map +1 -0
  20. package/dist/components/editor/sanitize-html.js +247 -0
  21. package/dist/components/editor/sanitize-html.js.map +1 -0
  22. package/dist/components/editor/schema.d.ts +36 -0
  23. package/dist/components/editor/schema.d.ts.map +1 -0
  24. package/dist/components/editor/schema.js +232 -0
  25. package/dist/components/editor/schema.js.map +1 -0
  26. package/dist/components/editor/selection.d.ts +22 -0
  27. package/dist/components/editor/selection.d.ts.map +1 -0
  28. package/dist/components/editor/selection.js +97 -0
  29. package/dist/components/editor/selection.js.map +1 -0
  30. package/dist/components/editor/styles.d.ts +10 -0
  31. package/dist/components/editor/styles.d.ts.map +1 -0
  32. package/dist/components/editor/styles.js +446 -0
  33. package/dist/components/editor/styles.js.map +1 -0
  34. package/dist/components/editor/table-utils.d.ts +33 -0
  35. package/dist/components/editor/table-utils.d.ts.map +1 -0
  36. package/dist/components/editor/table-utils.js +150 -0
  37. package/dist/components/editor/table-utils.js.map +1 -0
  38. package/dist/components/input/input.d.ts +5 -1
  39. package/dist/components/input/input.d.ts.map +1 -1
  40. package/dist/components/input/input.js +15 -1
  41. package/dist/components/input/input.js.map +1 -1
  42. package/dist/components/pagination/pagination.d.ts +3 -0
  43. package/dist/components/pagination/pagination.d.ts.map +1 -1
  44. package/dist/components/pagination/pagination.js +19 -9
  45. package/dist/components/pagination/pagination.js.map +1 -1
  46. package/dist/components/table/table.d.ts.map +1 -1
  47. package/dist/components/table/table.js +20 -3
  48. package/dist/components/table/table.js.map +1 -1
  49. package/dist/components/toast/toast.d.ts +10 -10
  50. package/dist/components/toast/toast.d.ts.map +1 -1
  51. package/dist/components/toast/toast.js +46 -13
  52. package/dist/components/toast/toast.js.map +1 -1
  53. package/dist/core/jsx.d.ts +25 -1
  54. package/dist/core/jsx.d.ts.map +1 -1
  55. package/dist/core/jsx.js.map +1 -1
  56. package/dist/core/page-range.d.ts +31 -0
  57. package/dist/core/page-range.d.ts.map +1 -0
  58. package/dist/core/page-range.js +41 -0
  59. package/dist/core/page-range.js.map +1 -0
  60. package/dist/core/vue.d.ts +3 -1
  61. package/dist/core/vue.d.ts.map +1 -1
  62. package/dist/core/vue.js.map +1 -1
  63. package/dist/docs/componentes/editor.doc.d.ts +6 -0
  64. package/dist/docs/componentes/editor.doc.d.ts.map +1 -0
  65. package/dist/docs/componentes/index.d.ts.map +1 -1
  66. package/dist/docs/componentes/input.doc.d.ts.map +1 -1
  67. package/dist/docs/componentes/table.doc.d.ts.map +1 -1
  68. package/dist/fenix-ui.umd.min.js +774 -256
  69. package/dist/fenix-ui2.esm.js.map +1 -1
  70. package/dist/fenix-ui4.esm.js +3877 -181
  71. package/dist/fenix-ui4.esm.js.map +1 -1
  72. package/dist/fenix-ui5.esm.js +1 -0
  73. package/dist/fenix-ui5.esm.js.map +1 -1
  74. package/dist/index.d.ts +1 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +14 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/plugins/auto-import.d.ts.map +1 -1
  79. package/dist/plugins/auto-import.js +1 -0
  80. package/dist/plugins/auto-import.js.map +1 -1
  81. package/package.json +5 -1
@@ -0,0 +1,316 @@
1
+ const BLOCK_TAGS = /* @__PURE__ */ new Set([
2
+ "p",
3
+ "h1",
4
+ "h2",
5
+ "h3",
6
+ "h4",
7
+ "h5",
8
+ "h6",
9
+ "blockquote",
10
+ "pre",
11
+ "ul",
12
+ "ol",
13
+ "li",
14
+ "table",
15
+ "figure",
16
+ "hr",
17
+ "div"
18
+ ]);
19
+ const INLINE_TAGS = /* @__PURE__ */ new Set([
20
+ "strong",
21
+ "b",
22
+ "em",
23
+ "i",
24
+ "u",
25
+ "s",
26
+ "del",
27
+ "ins",
28
+ "sub",
29
+ "sup",
30
+ "code",
31
+ "a",
32
+ "span",
33
+ "small",
34
+ "mark",
35
+ "br"
36
+ ]);
37
+ function isElement(node) {
38
+ return node.nodeType === Node.ELEMENT_NODE;
39
+ }
40
+ function tagOf(node) {
41
+ return isElement(node) ? node.tagName.toLowerCase() : "";
42
+ }
43
+ function hasBlockChild(el) {
44
+ return Array.from(el.children).some((child) => BLOCK_TAGS.has(tagOf(child)) || tagOf(child) === "figure");
45
+ }
46
+ function unwrap(el) {
47
+ const parent = el.parentNode;
48
+ if (!parent) {
49
+ el.remove();
50
+ return;
51
+ }
52
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
53
+ el.remove();
54
+ }
55
+ function rename(el, tag) {
56
+ const doc = el.ownerDocument;
57
+ const next = doc.createElement(tag);
58
+ for (const attr of Array.from(el.attributes)) next.setAttribute(attr.name, attr.value);
59
+ while (el.firstChild) next.appendChild(el.firstChild);
60
+ el.replaceWith(next);
61
+ return next;
62
+ }
63
+ function normalizeChildren(parent, options) {
64
+ for (const node of Array.from(parent.childNodes)) {
65
+ if (node.nodeType === Node.COMMENT_NODE) {
66
+ node.remove();
67
+ continue;
68
+ }
69
+ if (node.nodeType === Node.TEXT_NODE) {
70
+ node.nodeValue = (node.nodeValue ?? "").replace(/\u00a0/g, " ");
71
+ continue;
72
+ }
73
+ if (!isElement(node)) continue;
74
+ let el = node;
75
+ let tag = tagOf(el);
76
+ if (tag === "b") el = rename(el, "strong");
77
+ else if (tag === "i") el = rename(el, "em");
78
+ else if (tag === "strike" || tag === "del") el = rename(el, "s");
79
+ tag = tagOf(el);
80
+ if (tag === "font") {
81
+ const span = rename(el, "span");
82
+ const styles = [];
83
+ const color = el.getAttribute("color");
84
+ const face = el.getAttribute("face");
85
+ if (color) styles.push(`color: ${color}`);
86
+ if (face) styles.push(`font-family: ${face}`);
87
+ if (styles.length) span.setAttribute("style", styles.join("; "));
88
+ el = span;
89
+ tag = "span";
90
+ }
91
+ if (tag === "div" || tag === "section" || tag === "article" || tag === "header" || tag === "footer" || tag === "main" || tag === "aside") {
92
+ if (hasBlockChild(el)) {
93
+ unwrap(el);
94
+ continue;
95
+ }
96
+ el = rename(el, "p");
97
+ tag = "p";
98
+ }
99
+ if (tag === "p" && hasBlockChild(el)) {
100
+ unwrap(el);
101
+ continue;
102
+ }
103
+ if (tag === "span" && !el.hasAttribute("style") && !el.hasAttribute("class")) {
104
+ unwrap(el);
105
+ continue;
106
+ }
107
+ if (!options.forEditor) {
108
+ el.removeAttribute("contenteditable");
109
+ el.removeAttribute("spellcheck");
110
+ el.removeAttribute("tabindex");
111
+ }
112
+ if (tag === "a") {
113
+ if (!el.getAttribute("href")) {
114
+ el.removeAttribute("target");
115
+ el.removeAttribute("rel");
116
+ } else if (el.getAttribute("target") === "_blank") {
117
+ el.setAttribute("rel", "noopener noreferrer");
118
+ }
119
+ }
120
+ normalizeChildren(el, options);
121
+ }
122
+ }
123
+ function wrapInlineChildren(cell, doc) {
124
+ if (Array.from(cell.children).some((child) => BLOCK_TAGS.has(tagOf(child)))) return;
125
+ const p = doc.createElement("p");
126
+ while (cell.firstChild) p.appendChild(cell.firstChild);
127
+ if (!p.textContent?.trim() && !p.querySelector("img,video,iframe,br")) {
128
+ p.innerHTML = "<br>";
129
+ }
130
+ cell.appendChild(p);
131
+ }
132
+ function normalizeFigures(body, options) {
133
+ const doc = body.ownerDocument;
134
+ for (const media of Array.from(body.querySelectorAll("img, video, iframe"))) {
135
+ if (media.closest("figure")) continue;
136
+ const anchor = media.closest("a");
137
+ const wrapper = anchor && anchor.querySelectorAll("*").length === 1 ? anchor : media;
138
+ const figure = doc.createElement("figure");
139
+ const kind = tagOf(media);
140
+ figure.className = kind === "img" ? "fx-image" : kind === "video" ? "fx-video" : "fx-embed";
141
+ if (kind === "img") figure.setAttribute("data-align", "center");
142
+ const p = wrapper.closest("p");
143
+ if (p) p.after(figure);
144
+ else wrapper.parentNode?.insertBefore(figure, wrapper);
145
+ figure.appendChild(wrapper);
146
+ if (options.forEditor) figure.setAttribute("contenteditable", "false");
147
+ }
148
+ for (const figure of Array.from(body.querySelectorAll("figure"))) {
149
+ if (figure.classList.contains("fx-table") || figure.querySelector("table")) {
150
+ figure.classList.add("fx-table");
151
+ figure.removeAttribute("contenteditable");
152
+ continue;
153
+ }
154
+ const hasImg = Boolean(figure.querySelector("img"));
155
+ const hasVideo = Boolean(figure.querySelector("video"));
156
+ const hasIframe = Boolean(figure.querySelector("iframe"));
157
+ if (!hasImg && !hasVideo && !hasIframe) {
158
+ figure.remove();
159
+ continue;
160
+ }
161
+ const classes = new Set((figure.getAttribute("class") ?? "").split(/\s+/).filter(Boolean));
162
+ if (hasImg) classes.add("fx-image");
163
+ if (hasVideo) classes.add("fx-video");
164
+ if (hasIframe) classes.add("fx-embed");
165
+ figure.setAttribute("class", Array.from(classes).join(" "));
166
+ if (!figure.hasAttribute("data-align")) figure.setAttribute("data-align", "center");
167
+ if (options.forEditor) figure.setAttribute("contenteditable", "false");
168
+ else figure.removeAttribute("contenteditable");
169
+ }
170
+ }
171
+ function normalizeTables(body) {
172
+ const doc = body.ownerDocument;
173
+ for (const table of Array.from(body.querySelectorAll("table"))) {
174
+ const isGrid = table.classList.contains("fx-grid") || table.getAttribute("role") === "presentation";
175
+ if (isGrid) {
176
+ table.classList.add("fx-grid");
177
+ table.setAttribute("role", "presentation");
178
+ const firstRow = table.querySelector("tr");
179
+ const cols = firstRow ? firstRow.children.length : 0;
180
+ if (cols) table.setAttribute("data-cols", String(cols));
181
+ } else {
182
+ table.removeAttribute("role");
183
+ const parentFigure = table.closest("figure");
184
+ if (!parentFigure || !parentFigure.classList.contains("fx-table")) {
185
+ const figure = doc.createElement("figure");
186
+ figure.className = "fx-table";
187
+ table.parentNode?.insertBefore(figure, table);
188
+ figure.appendChild(table);
189
+ }
190
+ }
191
+ let tbody = Array.from(table.children).find((child) => tagOf(child) === "tbody");
192
+ const looseRows = Array.from(table.children).filter((child) => tagOf(child) === "tr");
193
+ if (looseRows.length) {
194
+ if (!tbody) {
195
+ tbody = doc.createElement("tbody");
196
+ table.appendChild(tbody);
197
+ }
198
+ for (const row of looseRows) tbody.appendChild(row);
199
+ }
200
+ if (!Array.from(table.children).some((child) => tagOf(child) === "colgroup")) {
201
+ const firstRow = table.querySelector("tr");
202
+ const count = firstRow ? Array.from(firstRow.children).filter((c) => tagOf(c) === "td" || tagOf(c) === "th").length : 0;
203
+ if (count > 0) {
204
+ const colgroup = doc.createElement("colgroup");
205
+ for (let i = 0; i < count; i++) {
206
+ const col = doc.createElement("col");
207
+ col.style.width = `${(100 / count).toFixed(4).replace(/\.?0+$/, "")}%`;
208
+ colgroup.appendChild(col);
209
+ }
210
+ table.insertBefore(colgroup, table.firstChild);
211
+ }
212
+ }
213
+ for (const th of Array.from(table.querySelectorAll("th"))) {
214
+ if (!th.getAttribute("scope")) th.setAttribute("scope", "col");
215
+ }
216
+ for (const cell of Array.from(table.querySelectorAll("th, td"))) {
217
+ wrapInlineChildren(cell, doc);
218
+ }
219
+ }
220
+ }
221
+ function normalizeRoot(body, options) {
222
+ const doc = body.ownerDocument;
223
+ let buffer = [];
224
+ const flush = (before) => {
225
+ if (!buffer.length) return;
226
+ const p = doc.createElement("p");
227
+ body.insertBefore(p, before);
228
+ for (const node of buffer) p.appendChild(node);
229
+ buffer = [];
230
+ if (!p.textContent?.trim() && !p.querySelector("img,video,iframe,br")) p.innerHTML = "<br>";
231
+ };
232
+ for (const node of Array.from(body.childNodes)) {
233
+ if (node.nodeType === Node.TEXT_NODE) {
234
+ if ((node.nodeValue ?? "").trim()) buffer.push(node);
235
+ else node.remove();
236
+ continue;
237
+ }
238
+ if (!isElement(node)) {
239
+ node.remove();
240
+ continue;
241
+ }
242
+ const tag = tagOf(node);
243
+ if (BLOCK_TAGS.has(tag) || tag === "figure") {
244
+ flush(node);
245
+ continue;
246
+ }
247
+ if (INLINE_TAGS.has(tag) || tag === "img" || tag === "video" || tag === "iframe") {
248
+ buffer.push(node);
249
+ continue;
250
+ }
251
+ while (node.firstChild) buffer.push(node.firstChild), body.insertBefore(node.firstChild, node);
252
+ node.remove();
253
+ }
254
+ flush(null);
255
+ if (options.forEditor && !body.querySelector("p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table,figure,hr")) {
256
+ body.innerHTML = "<p><br></p>";
257
+ }
258
+ }
259
+ function stripEmpty(body, forEditor) {
260
+ for (const span of Array.from(body.querySelectorAll("span"))) {
261
+ if (!span.textContent?.trim() && !span.querySelector("img,video,iframe,br") && !span.hasAttribute("style")) {
262
+ span.remove();
263
+ }
264
+ }
265
+ for (const p of Array.from(body.querySelectorAll("p"))) {
266
+ if (p.querySelector("img,video,iframe,br,table,figure")) continue;
267
+ if (p.textContent?.trim()) continue;
268
+ if (forEditor && p === body.lastElementChild) continue;
269
+ p.remove();
270
+ }
271
+ for (const heading of Array.from(body.querySelectorAll("h1,h2,h3,h4,h5,h6,blockquote"))) {
272
+ if (!heading.textContent?.trim() && !heading.querySelector("img,video,iframe,br")) heading.remove();
273
+ }
274
+ for (const figure of Array.from(body.querySelectorAll("figure"))) {
275
+ if (!figure.querySelector("img,video,iframe,table")) figure.remove();
276
+ }
277
+ for (const table of Array.from(body.querySelectorAll("table"))) {
278
+ if (!table.querySelector("td,th")) table.remove();
279
+ }
280
+ if (forEditor && !body.querySelector("p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table,figure,hr")) {
281
+ body.innerHTML = "<p><br></p>";
282
+ }
283
+ }
284
+ function normalizeElement(body, options = {}) {
285
+ normalizeChildren(body, options);
286
+ normalizeFigures(body, options);
287
+ normalizeTables(body);
288
+ normalizeChildren(body, options);
289
+ normalizeRoot(body, options);
290
+ stripEmpty(body, Boolean(options.forEditor));
291
+ const result = body.innerHTML.trim();
292
+ if (!result) return options.forEditor ? "<p><br></p>" : "";
293
+ return result;
294
+ }
295
+ function normalizeHtml(html, options = {}) {
296
+ if (!html) return options.forEditor ? "<p><br></p>" : "";
297
+ if (typeof DOMParser === "undefined") return html;
298
+ const doc = new DOMParser().parseFromString(
299
+ `<!doctype html><html><body>${html}</body></html>`,
300
+ "text/html"
301
+ );
302
+ return normalizeElement(doc.body, options);
303
+ }
304
+ function normalizeForStorage(html) {
305
+ return normalizeHtml(html, { forEditor: false });
306
+ }
307
+ function normalizeForEditor(html) {
308
+ return normalizeHtml(html, { forEditor: true });
309
+ }
310
+ export {
311
+ normalizeElement,
312
+ normalizeForEditor,
313
+ normalizeForStorage,
314
+ normalizeHtml
315
+ };
316
+ //# sourceMappingURL=normalize-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-html.js","sources":["../../../src/components/editor/normalize-html.ts"],"sourcesContent":["/**\n * Normalizador de HTML do <fx-editor>.\n *\n * Ele garante um contrato estável para o dado salvo:\n * - apenas blocos no nível raiz (texto/inline é embrulhado em <p>);\n * - tags legadas convertidas (b→strong, i→em, strike→s);\n * - mídia sempre em <figure> atômico;\n * - tabelas com thead/tbody e colgroup;\n * - grid como layout table com role=presentation;\n * - sem spans/parágrafos vazios.\n */\n\nexport interface NormalizeOptions {\n /** Adiciona `contenteditable=\"false\"` nas figuras de mídia (uso no editor). */\n forEditor?: boolean;\n}\n\nconst BLOCK_TAGS = new Set<string>([\n 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'ol',\n 'li', 'table', 'figure', 'hr', 'div',\n]);\n\nconst INLINE_TAGS = new Set<string>([\n 'strong', 'b', 'em', 'i', 'u', 's', 'del', 'ins', 'sub', 'sup', 'code',\n 'a', 'span', 'small', 'mark', 'br',\n]);\n\nfunction isElement(node: Node): node is Element {\n return node.nodeType === Node.ELEMENT_NODE;\n}\n\nfunction tagOf(node: Node): string {\n return isElement(node) ? node.tagName.toLowerCase() : '';\n}\n\nfunction hasBlockChild(el: Element): boolean {\n return Array.from(el.children).some((child) => BLOCK_TAGS.has(tagOf(child)) || tagOf(child) === 'figure');\n}\n\nfunction unwrap(el: Element): void {\n const parent = el.parentNode;\n if (!parent) {\n el.remove();\n return;\n }\n while (el.firstChild) parent.insertBefore(el.firstChild, el);\n el.remove();\n}\n\nfunction rename(el: Element, tag: string): Element {\n const doc = el.ownerDocument;\n const next = doc.createElement(tag);\n for (const attr of Array.from(el.attributes)) next.setAttribute(attr.name, attr.value);\n while (el.firstChild) next.appendChild(el.firstChild);\n el.replaceWith(next);\n return next;\n}\n\nfunction normalizeChildren(parent: Element, options: NormalizeOptions): void {\n for (const node of Array.from(parent.childNodes)) {\n if (node.nodeType === Node.COMMENT_NODE) {\n node.remove();\n continue;\n }\n if (node.nodeType === Node.TEXT_NODE) {\n node.nodeValue = (node.nodeValue ?? '').replace(/\\u00a0/g, ' ');\n continue;\n }\n if (!isElement(node)) continue;\n\n let el = node as Element;\n let tag = tagOf(el);\n\n if (tag === 'b') el = rename(el, 'strong');\n else if (tag === 'i') el = rename(el, 'em');\n else if (tag === 'strike' || tag === 'del') el = rename(el, 's');\n tag = tagOf(el);\n\n if (tag === 'font') {\n const span = rename(el, 'span');\n const styles: string[] = [];\n const color = el.getAttribute('color');\n const face = el.getAttribute('face');\n if (color) styles.push(`color: ${color}`);\n if (face) styles.push(`font-family: ${face}`);\n if (styles.length) span.setAttribute('style', styles.join('; '));\n el = span;\n tag = 'span';\n }\n\n // div/section usados como container viram parágrafo (ou são desembrulhados).\n if (tag === 'div' || tag === 'section' || tag === 'article' || tag === 'header' || tag === 'footer' || tag === 'main' || tag === 'aside') {\n if (hasBlockChild(el)) {\n unwrap(el);\n continue;\n }\n el = rename(el, 'p');\n tag = 'p';\n }\n\n // <p> não pode conter blocos.\n if (tag === 'p' && hasBlockChild(el)) {\n unwrap(el);\n continue;\n }\n\n // Span vazio sem atributos é ruído de paste.\n if (tag === 'span' && !el.hasAttribute('style') && !el.hasAttribute('class')) {\n unwrap(el);\n continue;\n }\n\n if (!options.forEditor) {\n el.removeAttribute('contenteditable');\n el.removeAttribute('spellcheck');\n el.removeAttribute('tabindex');\n }\n\n if (tag === 'a') {\n if (!el.getAttribute('href')) {\n el.removeAttribute('target');\n el.removeAttribute('rel');\n } else if (el.getAttribute('target') === '_blank') {\n el.setAttribute('rel', 'noopener noreferrer');\n }\n }\n\n normalizeChildren(el, options);\n }\n}\n\nfunction wrapInlineChildren(cell: Element, doc: Document): void {\n if (Array.from(cell.children).some((child) => BLOCK_TAGS.has(tagOf(child)))) return;\n const p = doc.createElement('p');\n while (cell.firstChild) p.appendChild(cell.firstChild);\n if (!p.textContent?.trim() && !p.querySelector('img,video,iframe,br')) {\n p.innerHTML = '<br>';\n }\n cell.appendChild(p);\n}\n\nfunction normalizeFigures(body: Element, options: NormalizeOptions): void {\n const doc = body.ownerDocument;\n\n for (const media of Array.from(body.querySelectorAll('img, video, iframe'))) {\n if (media.closest('figure')) continue;\n\n const anchor = media.closest('a');\n const wrapper: Element = anchor && anchor.querySelectorAll('*').length === 1 ? anchor : media;\n const figure = doc.createElement('figure');\n const kind = tagOf(media);\n figure.className = kind === 'img' ? 'fx-image' : kind === 'video' ? 'fx-video' : 'fx-embed';\n if (kind === 'img') figure.setAttribute('data-align', 'center');\n\n const p = wrapper.closest('p');\n if (p) p.after(figure);\n else wrapper.parentNode?.insertBefore(figure, wrapper);\n figure.appendChild(wrapper);\n\n if (options.forEditor) figure.setAttribute('contenteditable', 'false');\n }\n\n for (const figure of Array.from(body.querySelectorAll('figure'))) {\n // Tabela de conteúdo/grid: o wrapper é estrutural, não uma figura de mídia.\n if (figure.classList.contains('fx-table') || figure.querySelector('table')) {\n figure.classList.add('fx-table');\n figure.removeAttribute('contenteditable');\n continue;\n }\n\n const hasImg = Boolean(figure.querySelector('img'));\n const hasVideo = Boolean(figure.querySelector('video'));\n const hasIframe = Boolean(figure.querySelector('iframe'));\n\n if (!hasImg && !hasVideo && !hasIframe) {\n figure.remove();\n continue;\n }\n\n const classes = new Set((figure.getAttribute('class') ?? '').split(/\\s+/).filter(Boolean));\n if (hasImg) classes.add('fx-image');\n if (hasVideo) classes.add('fx-video');\n if (hasIframe) classes.add('fx-embed');\n figure.setAttribute('class', Array.from(classes).join(' '));\n\n if (!figure.hasAttribute('data-align')) figure.setAttribute('data-align', 'center');\n if (options.forEditor) figure.setAttribute('contenteditable', 'false');\n else figure.removeAttribute('contenteditable');\n }\n}\n\nfunction normalizeTables(body: Element): void {\n const doc = body.ownerDocument;\n\n for (const table of Array.from(body.querySelectorAll('table'))) {\n const isGrid =\n table.classList.contains('fx-grid') ||\n table.getAttribute('role') === 'presentation';\n\n if (isGrid) {\n table.classList.add('fx-grid');\n table.setAttribute('role', 'presentation');\n const firstRow = table.querySelector('tr');\n const cols = firstRow ? firstRow.children.length : 0;\n if (cols) table.setAttribute('data-cols', String(cols));\n } else {\n table.removeAttribute('role');\n const parentFigure = table.closest('figure');\n if (!parentFigure || !parentFigure.classList.contains('fx-table')) {\n const figure = doc.createElement('figure');\n figure.className = 'fx-table';\n table.parentNode?.insertBefore(figure, table);\n figure.appendChild(table);\n }\n }\n\n let tbody = Array.from(table.children).find((child) => tagOf(child) === 'tbody') as Element | undefined;\n const looseRows = Array.from(table.children).filter((child) => tagOf(child) === 'tr');\n if (looseRows.length) {\n if (!tbody) {\n tbody = doc.createElement('tbody');\n table.appendChild(tbody);\n }\n for (const row of looseRows) tbody.appendChild(row);\n }\n\n if (!Array.from(table.children).some((child) => tagOf(child) === 'colgroup')) {\n const firstRow = table.querySelector('tr');\n const count = firstRow ? Array.from(firstRow.children).filter((c) => tagOf(c) === 'td' || tagOf(c) === 'th').length : 0;\n if (count > 0) {\n const colgroup = doc.createElement('colgroup');\n for (let i = 0; i < count; i++) {\n const col = doc.createElement('col');\n col.style.width = `${(100 / count).toFixed(4).replace(/\\.?0+$/, '')}%`;\n colgroup.appendChild(col);\n }\n table.insertBefore(colgroup, table.firstChild);\n }\n }\n\n for (const th of Array.from(table.querySelectorAll('th'))) {\n if (!th.getAttribute('scope')) th.setAttribute('scope', 'col');\n }\n\n for (const cell of Array.from(table.querySelectorAll('th, td'))) {\n wrapInlineChildren(cell, doc);\n }\n }\n}\n\nfunction normalizeRoot(body: Element, options: NormalizeOptions): void {\n const doc = body.ownerDocument;\n let buffer: Node[] = [];\n\n const flush = (before: Node | null): void => {\n if (!buffer.length) return;\n const p = doc.createElement('p');\n body.insertBefore(p, before);\n for (const node of buffer) p.appendChild(node);\n buffer = [];\n if (!p.textContent?.trim() && !p.querySelector('img,video,iframe,br')) p.innerHTML = '<br>';\n };\n\n for (const node of Array.from(body.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n if ((node.nodeValue ?? '').trim()) buffer.push(node);\n else node.remove();\n continue;\n }\n\n if (!isElement(node)) {\n node.remove();\n continue;\n }\n\n const tag = tagOf(node);\n if (BLOCK_TAGS.has(tag) || tag === 'figure') {\n flush(node);\n continue;\n }\n\n if (INLINE_TAGS.has(tag) || tag === 'img' || tag === 'video' || tag === 'iframe') {\n buffer.push(node);\n continue;\n }\n\n // Elemento desconhecido no nível raiz: preserva os filhos como inline.\n while (node.firstChild) buffer.push(node.firstChild), body.insertBefore(node.firstChild, node);\n node.remove();\n }\n\n flush(null);\n if (options.forEditor && !body.querySelector('p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table,figure,hr')) {\n body.innerHTML = '<p><br></p>';\n }\n}\n\nfunction stripEmpty(body: Element, forEditor: boolean): void {\n for (const span of Array.from(body.querySelectorAll('span'))) {\n if (!span.textContent?.trim() && !span.querySelector('img,video,iframe,br') && !span.hasAttribute('style')) {\n span.remove();\n }\n }\n\n for (const p of Array.from(body.querySelectorAll('p'))) {\n if (p.querySelector('img,video,iframe,br,table,figure')) continue;\n if (p.textContent?.trim()) continue;\n if (forEditor && p === body.lastElementChild) continue;\n p.remove();\n }\n\n for (const heading of Array.from(body.querySelectorAll('h1,h2,h3,h4,h5,h6,blockquote'))) {\n if (!heading.textContent?.trim() && !heading.querySelector('img,video,iframe,br')) heading.remove();\n }\n\n for (const figure of Array.from(body.querySelectorAll('figure'))) {\n if (!figure.querySelector('img,video,iframe,table')) figure.remove();\n }\n\n for (const table of Array.from(body.querySelectorAll('table'))) {\n if (!table.querySelector('td,th')) table.remove();\n }\n\n if (forEditor && !body.querySelector('p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table,figure,hr')) {\n body.innerHTML = '<p><br></p>';\n }\n}\n\n/**\n * Normaliza um `<body>` já parseado/sanitizado e devolve o HTML final.\n * Evita uma segunda parseada quando o chamador já tem o DOM em mãos.\n */\nexport function normalizeElement(body: Element, options: NormalizeOptions = {}): string {\n normalizeChildren(body, options);\n normalizeFigures(body, options);\n normalizeTables(body);\n normalizeChildren(body, options);\n normalizeRoot(body, options);\n stripEmpty(body, Boolean(options.forEditor));\n\n const result = body.innerHTML.trim();\n if (!result) return options.forEditor ? '<p><br></p>' : '';\n return result;\n}\n\n/**\n * Canonicaliza o HTML do editor.\n *\n * @example\n * normalizeHtml('<div>Oi <b>mundo</b></div>')\n * // => '<p>Oi <strong>mundo</strong></p>'\n */\nexport function normalizeHtml(html: string, options: NormalizeOptions = {}): string {\n if (!html) return options.forEditor ? '<p><br></p>' : '';\n if (typeof DOMParser === 'undefined') return html;\n\n const doc = new DOMParser().parseFromString(\n `<!doctype html><html><body>${html}</body></html>`,\n 'text/html',\n );\n\n return normalizeElement(doc.body, options);\n}\n\n/** Remove atributos exclusivos do editor que não devem ir para o dado salvo. */\nexport function normalizeForStorage(html: string): string {\n return normalizeHtml(html, { forEditor: false });\n}\n\n/** Prepara o HTML para entrar no contenteditable (figuras atômicas). */\nexport function normalizeForEditor(html: string): string {\n return normalizeHtml(html, { forEditor: true });\n}\n\n\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Sanitizador de HTML do <fx-editor>.
3
+ *
4
+ * Estratégia: allowlist + DOMParser inerte + serialização única.
5
+ * Nada de regex para decidir o que é seguro: a árvore é percorrida e cada
6
+ * nó/atributo é validado individualmente.
7
+ *
8
+ * O resultado é DEFENSE IN DEPTH. O HTML salvo também deve ser sanitizado
9
+ * no backend antes de ser renderizado em outro contexto.
10
+ */
11
+ export interface SanitizeOptions {
12
+ allowBase64Images?: boolean;
13
+ allowBase64Videos?: boolean;
14
+ allowEmbeds?: boolean;
15
+ maxDataImageBytes?: number;
16
+ maxDataVideoBytes?: number;
17
+ }
18
+ /** Valida um link (href). Retorna '' quando não é seguro. */
19
+ export declare function sanitizeLinkUrl(value: string): string;
20
+ /** Valida src/poster de mídia. */
21
+ export declare function sanitizeMediaUrl(value: string, tag: string, options?: SanitizeOptions): string;
22
+ /** Valida e canonicaliza um iframe de embed. */
23
+ export declare function sanitizeEmbedUrl(value: string): string;
24
+ /**
25
+ * Sanitiza um fragmento HTML e devolve o HTML seguro.
26
+ *
27
+ * @example
28
+ * sanitizeHtml('<p onclick="x()">Oi<script>alert(1)</script></p>')
29
+ * // => '<p>Oi</p>'
30
+ */
31
+ /**
32
+ * Sanitiza o HTML e devolve o `<body>` já filtrado, sem serializar.
33
+ * Permite encadear a normalização no MESMO DOM (uma única parseada).
34
+ */
35
+ export declare function sanitizeBody(html: string, options?: SanitizeOptions): HTMLElement | null;
36
+ export declare function sanitizeHtml(html: string, options?: SanitizeOptions): string;
37
+ //# sourceMappingURL=sanitize-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-html.d.ts","sourceRoot":"","sources":["../../../src/components/editor/sanitize-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAkBH,MAAM,WAAW,eAAe;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAuBD,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAuBrD;AAED,kCAAkC;AAClC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAiClG;AAED,gDAAgD;AAChD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBtD;AAyLD;;;;;;GAMG;AACH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,WAAW,GAAG,IAAI,CAY5F;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAGhF"}
@@ -0,0 +1,247 @@
1
+ import { SAFE_LINK_PROTOCOLS, DEFAULT_MAX_DATA_VIDEO_BYTES, DEFAULT_MAX_DATA_IMAGE_BYTES, SAFE_IMAGE_DATA_RE, SAFE_MEDIA_PROTOCOLS, ALLOWED_EMBED_HOSTS, DROP_CONTENT_TAGS, ALLOWED_TAGS, FORBIDDEN_ATTRS, GLOBAL_ATTRS, TAG_ATTRS, ALLOWED_CLASS_RE, ALLOWED_STYLE_PROPS } from "./schema.js";
2
+ const DEFAULTS = {
3
+ allowBase64Images: true,
4
+ allowBase64Videos: false,
5
+ allowEmbeds: true,
6
+ maxDataImageBytes: DEFAULT_MAX_DATA_IMAGE_BYTES,
7
+ maxDataVideoBytes: DEFAULT_MAX_DATA_VIDEO_BYTES
8
+ };
9
+ const SAFE_DATA_VIDEO_RE = /^data:video\/(?:mp4|webm);base64,[a-z0-9+/=\s]+$/i;
10
+ function cleanRawUrl(value) {
11
+ return value.replace(/[\u0000-\u001F\u007F\s]+/g, "").trim();
12
+ }
13
+ function isRelativeUrl(value) {
14
+ return !value.startsWith("//") && /^(?:#|\/|\.{1,2}\/)/.test(value);
15
+ }
16
+ function sanitizeLinkUrl(value) {
17
+ const raw = cleanRawUrl(value);
18
+ if (!raw) return "";
19
+ if (isRelativeUrl(raw)) return raw;
20
+ const lower = raw.toLowerCase();
21
+ if (lower.startsWith("javascript:") || lower.startsWith("vbscript:") || lower.startsWith("data:") || lower.startsWith("file:") || lower.startsWith("blob:")) {
22
+ return "";
23
+ }
24
+ try {
25
+ const url = new URL(raw);
26
+ return SAFE_LINK_PROTOCOLS.has(url.protocol) ? raw : "";
27
+ } catch {
28
+ return /^[a-z0-9][a-z0-9._~!$&'()*+,;=:@%/?#-]*$/i.test(raw) ? raw : "";
29
+ }
30
+ }
31
+ function sanitizeMediaUrl(value, tag, options = {}) {
32
+ const opts = { ...DEFAULTS, ...options };
33
+ const raw = cleanRawUrl(value);
34
+ if (!raw) return "";
35
+ if (raw.toLowerCase().startsWith("data:")) {
36
+ if (tag === "img" && opts.allowBase64Images) {
37
+ return SAFE_IMAGE_DATA_RE.test(raw) && raw.length <= opts.maxDataImageBytes ? raw : "";
38
+ }
39
+ if ((tag === "video" || tag === "source") && opts.allowBase64Videos) {
40
+ return SAFE_DATA_VIDEO_RE.test(raw) && raw.length <= opts.maxDataVideoBytes ? raw : "";
41
+ }
42
+ return "";
43
+ }
44
+ if (isRelativeUrl(raw)) return raw;
45
+ const lower = raw.toLowerCase();
46
+ if (lower.startsWith("javascript:") || lower.startsWith("vbscript:") || lower.startsWith("file:") || lower.startsWith("blob:")) {
47
+ return "";
48
+ }
49
+ try {
50
+ const url = new URL(raw);
51
+ return SAFE_MEDIA_PROTOCOLS.has(url.protocol) ? raw : "";
52
+ } catch {
53
+ return "";
54
+ }
55
+ }
56
+ function sanitizeEmbedUrl(value) {
57
+ let raw = cleanRawUrl(value);
58
+ if (!raw) return "";
59
+ if (raw.startsWith("//")) raw = `https:${raw}`;
60
+ let url;
61
+ try {
62
+ url = new URL(raw);
63
+ } catch {
64
+ return "";
65
+ }
66
+ if (url.protocol !== "https:") return "";
67
+ if (!ALLOWED_EMBED_HOSTS.has(url.hostname.toLowerCase())) return "";
68
+ const path = url.pathname.toLowerCase();
69
+ const isValidPath = url.hostname.includes("youtube") && path.startsWith("/embed/") || url.hostname.includes("vimeo") && path.startsWith("/video/");
70
+ if (!isValidPath) return "";
71
+ return url.toString();
72
+ }
73
+ function sanitizeStyle(value) {
74
+ if (!value) return "";
75
+ const probe = document.createElement("span");
76
+ probe.setAttribute("style", value);
77
+ const out = [];
78
+ for (let i = 0; i < probe.style.length; i++) {
79
+ const prop = probe.style.item(i);
80
+ if (!ALLOWED_STYLE_PROPS.has(prop)) continue;
81
+ const val = probe.style.getPropertyValue(prop).trim();
82
+ if (!val) continue;
83
+ if (/\\/.test(val)) continue;
84
+ if (/url\s*\(|expression\s*\(|javascript\s*:|@import|behavior\s*:|-moz-binding/i.test(val)) continue;
85
+ out.push(`${prop}: ${val}`);
86
+ }
87
+ return out.join("; ");
88
+ }
89
+ function sanitizeAttributes(el, tag, opts) {
90
+ for (const attr of Array.from(el.attributes)) {
91
+ const name = attr.name.toLowerCase();
92
+ const value = attr.value;
93
+ if (FORBIDDEN_ATTRS.has(name) || name.startsWith("on")) {
94
+ el.removeAttribute(attr.name);
95
+ continue;
96
+ }
97
+ const allowed = GLOBAL_ATTRS.has(name) || TAG_ATTRS[tag]?.has(name);
98
+ if (!allowed) {
99
+ el.removeAttribute(attr.name);
100
+ continue;
101
+ }
102
+ if (name === "style") {
103
+ const safe = sanitizeStyle(value);
104
+ if (safe) el.setAttribute("style", safe);
105
+ else el.removeAttribute("style");
106
+ continue;
107
+ }
108
+ if (name === "class") {
109
+ const classes = value.split(/\s+/).filter((c) => ALLOWED_CLASS_RE.test(c));
110
+ if (classes.length) el.setAttribute("class", classes.join(" "));
111
+ else el.removeAttribute("class");
112
+ continue;
113
+ }
114
+ if (name === "href") {
115
+ const safe = sanitizeLinkUrl(value);
116
+ if (safe) el.setAttribute("href", safe);
117
+ else el.removeAttribute("href");
118
+ continue;
119
+ }
120
+ if (name === "src") {
121
+ const safe = sanitizeMediaUrl(value, tag, opts);
122
+ if (safe) el.setAttribute("src", safe);
123
+ else el.removeAttribute("src");
124
+ continue;
125
+ }
126
+ if (name === "poster") {
127
+ const safe = sanitizeMediaUrl(value, "img", opts);
128
+ if (safe) el.setAttribute("poster", safe);
129
+ else el.removeAttribute("poster");
130
+ continue;
131
+ }
132
+ if (name === "target") {
133
+ const safe = value === "_self" || value === "_blank" ? value : "_blank";
134
+ el.setAttribute("target", safe);
135
+ continue;
136
+ }
137
+ if (name === "colspan" || name === "rowspan" || name === "span" || name === "start") {
138
+ const n = Number(value);
139
+ if (Number.isInteger(n) && n > 0 && n <= 1e3) el.setAttribute(name, String(n));
140
+ else el.removeAttribute(name);
141
+ continue;
142
+ }
143
+ if (name === "width" || name === "height") {
144
+ if (/^\d{1,4}(?:px|%)?$/i.test(value.trim())) el.setAttribute(name, value.trim());
145
+ else el.removeAttribute(name);
146
+ continue;
147
+ }
148
+ }
149
+ if (tag === "a") {
150
+ const href = el.getAttribute("href");
151
+ if (!href) {
152
+ el.removeAttribute("target");
153
+ el.removeAttribute("rel");
154
+ } else if (el.getAttribute("target") === "_blank") {
155
+ el.setAttribute("rel", "noopener noreferrer");
156
+ }
157
+ }
158
+ if (tag === "iframe") {
159
+ el.setAttribute("sandbox", "allow-scripts allow-same-origin allow-presentation");
160
+ el.setAttribute("allowfullscreen", "");
161
+ el.setAttribute("loading", "lazy");
162
+ el.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
163
+ }
164
+ if (tag === "img") {
165
+ if (!el.hasAttribute("alt")) el.setAttribute("alt", "");
166
+ el.setAttribute("loading", "lazy");
167
+ el.setAttribute("decoding", "async");
168
+ }
169
+ if (tag === "video") {
170
+ el.setAttribute("controls", "");
171
+ el.setAttribute("preload", "metadata");
172
+ }
173
+ }
174
+ function sanitizeIframe(el) {
175
+ const src = el.getAttribute("src") ?? "";
176
+ const safe = sanitizeEmbedUrl(src);
177
+ if (!safe) return false;
178
+ el.setAttribute("src", safe);
179
+ return true;
180
+ }
181
+ function unwrap(el, opts) {
182
+ const parent = el.parentNode;
183
+ if (!parent) {
184
+ el.remove();
185
+ return;
186
+ }
187
+ for (const child of Array.from(el.childNodes)) {
188
+ sanitizeNode(child, opts);
189
+ parent.insertBefore(child, el);
190
+ }
191
+ el.remove();
192
+ }
193
+ function sanitizeNode(node, opts) {
194
+ if (node.nodeType === Node.TEXT_NODE) return;
195
+ if (node.nodeType === Node.COMMENT_NODE) {
196
+ node.parentNode?.removeChild(node);
197
+ return;
198
+ }
199
+ if (node.nodeType !== Node.ELEMENT_NODE) {
200
+ node.parentNode?.removeChild(node);
201
+ return;
202
+ }
203
+ const el = node;
204
+ const tag = el.tagName.toLowerCase();
205
+ if (DROP_CONTENT_TAGS.has(tag)) {
206
+ el.remove();
207
+ return;
208
+ }
209
+ if (tag === "iframe") {
210
+ if (!opts.allowEmbeds || !sanitizeIframe(el)) el.remove();
211
+ else sanitizeAttributes(el, tag, opts);
212
+ return;
213
+ }
214
+ if (!ALLOWED_TAGS.has(tag)) {
215
+ unwrap(el, opts);
216
+ return;
217
+ }
218
+ sanitizeAttributes(el, tag, opts);
219
+ if ((tag === "img" || tag === "video" || tag === "source") && !el.hasAttribute("src")) {
220
+ el.remove();
221
+ return;
222
+ }
223
+ for (const child of Array.from(el.childNodes)) sanitizeNode(child, opts);
224
+ }
225
+ function sanitizeBody(html, options = {}) {
226
+ const opts = { ...DEFAULTS, ...options };
227
+ if (!html) return null;
228
+ if (typeof DOMParser === "undefined" || typeof document === "undefined") return null;
229
+ const doc = new DOMParser().parseFromString(
230
+ `<!doctype html><html><body>${html}</body></html>`,
231
+ "text/html"
232
+ );
233
+ for (const child of Array.from(doc.body.childNodes)) sanitizeNode(child, opts);
234
+ return doc.body;
235
+ }
236
+ function sanitizeHtml(html, options = {}) {
237
+ const body = sanitizeBody(html, options);
238
+ return body ? body.innerHTML : "";
239
+ }
240
+ export {
241
+ sanitizeBody,
242
+ sanitizeEmbedUrl,
243
+ sanitizeHtml,
244
+ sanitizeLinkUrl,
245
+ sanitizeMediaUrl
246
+ };
247
+ //# sourceMappingURL=sanitize-html.js.map