@meistrari/minuta-nuxt 0.1.0

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 (45) hide show
  1. package/README.md +106 -0
  2. package/dist/module.cjs +5 -0
  3. package/dist/module.d.mts +18 -0
  4. package/dist/module.d.ts +18 -0
  5. package/dist/module.json +9 -0
  6. package/dist/module.mjs +45 -0
  7. package/dist/runtime/components/MinutaAnnotationsGutter.vue +425 -0
  8. package/dist/runtime/components/MinutaChatEditCard.vue +124 -0
  9. package/dist/runtime/components/MinutaChromeCanvas.vue +549 -0
  10. package/dist/runtime/components/MinutaChromeEditor.vue +100 -0
  11. package/dist/runtime/components/MinutaDocumentViewer.vue +90 -0
  12. package/dist/runtime/components/MinutaDocxEditor.vue +1158 -0
  13. package/dist/runtime/components/MinutaEmbed.vue +783 -0
  14. package/dist/runtime/components/MinutaIcon.vue +53 -0
  15. package/dist/runtime/composables/annotationAnchors.d.ts +73 -0
  16. package/dist/runtime/composables/annotationAnchors.js +243 -0
  17. package/dist/runtime/composables/chatEditOps.d.ts +37 -0
  18. package/dist/runtime/composables/chatEditOps.js +61 -0
  19. package/dist/runtime/composables/docxExport.d.ts +24 -0
  20. package/dist/runtime/composables/docxExport.js +1292 -0
  21. package/dist/runtime/composables/editorExtensions.d.ts +56 -0
  22. package/dist/runtime/composables/editorExtensions.js +466 -0
  23. package/dist/runtime/composables/paginationPlugin.d.ts +126 -0
  24. package/dist/runtime/composables/paginationPlugin.js +421 -0
  25. package/dist/runtime/composables/useMinutaApi.d.ts +112 -0
  26. package/dist/runtime/composables/useMinutaApi.js +68 -0
  27. package/dist/runtime/composables/vaultImages.d.ts +12 -0
  28. package/dist/runtime/composables/vaultImages.js +85 -0
  29. package/dist/runtime/composables/versionDiff.d.ts +34 -0
  30. package/dist/runtime/composables/versionDiff.js +62 -0
  31. package/dist/runtime/server/api/minuta/[...path].d.ts +2 -0
  32. package/dist/runtime/server/api/minuta/[...path].js +6 -0
  33. package/dist/runtime/server/api/minuta/index.d.ts +2 -0
  34. package/dist/runtime/server/api/minuta/index.js +3 -0
  35. package/dist/runtime/server/utils/minuta-proxy.d.ts +10 -0
  36. package/dist/runtime/server/utils/minuta-proxy.js +63 -0
  37. package/dist/runtime/utils/footnoteHtml.d.ts +26 -0
  38. package/dist/runtime/utils/footnoteHtml.js +117 -0
  39. package/dist/runtime/utils/pocKeepWithNext.d.ts +44 -0
  40. package/dist/runtime/utils/pocKeepWithNext.js +9 -0
  41. package/dist/runtime/utils/sanitizeRichHtml.d.ts +10 -0
  42. package/dist/runtime/utils/sanitizeRichHtml.js +73 -0
  43. package/dist/types.d.mts +7 -0
  44. package/dist/types.d.ts +7 -0
  45. package/package.json +100 -0
@@ -0,0 +1,34 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ import type { Node as PMNode } from '@tiptap/pm/model';
3
+ import type { Change } from '@tiptap/pm/changeset';
4
+ import { PluginKey } from '@tiptap/pm/state';
5
+ import { DecorationSet } from '@tiptap/pm/view';
6
+ /**
7
+ * Comparação read-only entre uma versão do histórico e o documento atual
8
+ * (estilo histórico do Google Docs): verde = inserido, vermelho tachado =
9
+ * removido. O diff roda sobre os DOCS ProseMirror — nunca sobre o HTML cru,
10
+ * que quebraria em chips de variável, html islands e atributos vault://.
11
+ *
12
+ * Diferente do usePocTrackChanges (tracking incremental de edições ao vivo,
13
+ * órfão), aqui o changeset é computado UMA vez entre dois estados arbitrários
14
+ * e vira um DecorationSet estático num plugin efêmero — registrado ao entrar
15
+ * no modo comparação e removido ao sair, fora do buildPocExtensions.
16
+ */
17
+ export declare const pocVersionDiffKey: PluginKey<DecorationSet>;
18
+ /**
19
+ * Doc ProseMirror a partir do html salvo, com o MESMO schema do editor vivo —
20
+ * qualquer divergência de parse apareceria como mudança fantasma.
21
+ */
22
+ export declare function pocDocFromHtml(editor: Editor, html: string): PMNode;
23
+ /**
24
+ * Mudanças mínimas entre oldDoc e o doc ATUAL do editor: uma Transform
25
+ * substitui o oldDoc inteiro pelo atual e o ChangeSet reduz aos ranges reais
26
+ * (computeDiff interno); simplifyChanges agrupa em fronteiras de palavra.
27
+ * Posições resultantes (fromB/toB) são no doc atual.
28
+ */
29
+ export declare function pocComputeVersionDiff(editor: Editor, oldDoc: PMNode): Change[];
30
+ /** Liga a comparação: computa o diff contra `oldHtml` e decora o doc atual. */
31
+ export declare function pocEnterVersionDiff(editor: Editor, oldHtml: string): {
32
+ changeCount: number;
33
+ };
34
+ export declare function pocExitVersionDiff(editor: Editor): void;
@@ -0,0 +1,62 @@
1
+ import { createNodeFromContent } from "@tiptap/core";
2
+ import { ChangeSet, simplifyChanges } from "@tiptap/pm/changeset";
3
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
4
+ import { Transform } from "@tiptap/pm/transform";
5
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
6
+ export const pocVersionDiffKey = new PluginKey("pocVersionDiff");
7
+ const DELETED_PREVIEW_MAX = 500;
8
+ export function pocDocFromHtml(editor, html) {
9
+ return createNodeFromContent(html, editor.schema, { slice: false });
10
+ }
11
+ export function pocComputeVersionDiff(editor, oldDoc) {
12
+ const newDoc = editor.state.doc;
13
+ const tr = new Transform(oldDoc);
14
+ tr.replaceWith(0, oldDoc.content.size, newDoc.content);
15
+ const changeset = ChangeSet.create(oldDoc).addSteps(tr.doc, tr.mapping.maps, {});
16
+ return simplifyChanges(changeset.changes, tr.doc);
17
+ }
18
+ function buildDecorations(doc, oldDoc, changes) {
19
+ const decorations = [];
20
+ for (const change of changes) {
21
+ if (change.toB > change.fromB)
22
+ decorations.push(Decoration.inline(change.fromB, change.toB, { class: "poc-ins" }));
23
+ if (change.toA > change.fromA) {
24
+ const deleted = oldDoc.textBetween(change.fromA, change.toA, " ");
25
+ if (deleted.trim()) {
26
+ decorations.push(Decoration.widget(change.fromB, () => {
27
+ const span = document.createElement("span");
28
+ span.className = "poc-del";
29
+ span.textContent = deleted.length > DELETED_PREVIEW_MAX ? `${deleted.slice(0, DELETED_PREVIEW_MAX)}\u2026` : deleted;
30
+ return span;
31
+ }, { side: -1 }));
32
+ }
33
+ }
34
+ }
35
+ return DecorationSet.create(doc, decorations);
36
+ }
37
+ export function pocEnterVersionDiff(editor, oldHtml) {
38
+ pocExitVersionDiff(editor);
39
+ const oldDoc = pocDocFromHtml(editor, oldHtml);
40
+ const changes = pocComputeVersionDiff(editor, oldDoc);
41
+ const decorations = buildDecorations(editor.state.doc, oldDoc, changes);
42
+ editor.registerPlugin(new Plugin({
43
+ key: pocVersionDiffKey,
44
+ state: {
45
+ init: () => decorations,
46
+ // o editor está read-only no modo comparação, mas troca de imagem
47
+ // fria ainda gera transação — mapear mantém as marcas no lugar
48
+ apply: (tr, value) => tr.docChanged ? value.map(tr.mapping, tr.doc) : value
49
+ },
50
+ props: {
51
+ decorations(state) {
52
+ return pocVersionDiffKey.getState(state);
53
+ }
54
+ }
55
+ }));
56
+ return { changeCount: changes.length };
57
+ }
58
+ export function pocExitVersionDiff(editor) {
59
+ if (editor.isDestroyed)
60
+ return;
61
+ editor.unregisterPlugin(pocVersionDiffKey);
62
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { defineEventHandler, getRouterParam } from "h3";
2
+ import { proxyToMinutaApi } from "../../utils/minuta-proxy.js";
3
+ export default defineEventHandler((event) => {
4
+ const path = getRouterParam(event, "_") ?? getRouterParam(event, "path") ?? "";
5
+ return proxyToMinutaApi(event, path);
6
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { proxyToMinutaApi } from "../../utils/minuta-proxy.js";
3
+ export default defineEventHandler((event) => proxyToMinutaApi(event, ""));
@@ -0,0 +1,10 @@
1
+ import type { H3Event } from 'h3';
2
+ /**
3
+ * Resolve o JWT Tela do usuário logado no app hospedeiro (mesma receita do
4
+ * chat-nuxt): contexto de auth do host → cookie configurado → cookie padrão.
5
+ * O token nunca chega ao browser do embed — só trafega server-side.
6
+ */
7
+ export declare function resolveMinutaAuthToken(event: H3Event): string | undefined;
8
+ /** Valida segmentos do path (sem traversal) e monta a URL de destino no CDB. */
9
+ export declare function buildMinutaTargetUrl(event: H3Event, path: string): string;
10
+ export declare function proxyToMinutaApi(event: H3Event, path: string): Promise<any>;
@@ -0,0 +1,63 @@
1
+ import { createError, getCookie, proxyRequest } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ const DEFAULT_JWT_COOKIES = ["tela-access-token", "tela-jwt"];
4
+ export function resolveMinutaAuthToken(event) {
5
+ const auth = event.context.auth;
6
+ if (auth?.token)
7
+ return auth.token;
8
+ const config = useRuntimeConfig(event);
9
+ const cookieName = config.minutaNuxt?.authTokenCookie;
10
+ if (cookieName)
11
+ return getCookie(event, cookieName) || void 0;
12
+ for (const name of DEFAULT_JWT_COOKIES) {
13
+ const token = getCookie(event, name);
14
+ if (token)
15
+ return token;
16
+ }
17
+ return void 0;
18
+ }
19
+ function resolveCdbApiUrl(event) {
20
+ const config = useRuntimeConfig(event);
21
+ const url = config.minutaNuxt?.cdbApiUrl;
22
+ if (!url) {
23
+ throw createError({
24
+ statusCode: 500,
25
+ statusMessage: "minutaNuxt.cdbApiUrl is not configured"
26
+ });
27
+ }
28
+ return url.replace(/\/$/, "");
29
+ }
30
+ export function buildMinutaTargetUrl(event, path) {
31
+ const segments = path.split("/").map((s) => s.trim()).filter(Boolean);
32
+ for (const segment of segments) {
33
+ let decoded = "";
34
+ try {
35
+ decoded = decodeURIComponent(segment);
36
+ } catch {
37
+ throw createError({ statusCode: 400, statusMessage: "Invalid proxy path" });
38
+ }
39
+ if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\"))
40
+ throw createError({ statusCode: 400, statusMessage: "Invalid proxy path" });
41
+ }
42
+ const base = resolveCdbApiUrl(event);
43
+ return segments.length ? `${base}/standalone-minuta/${segments.join("/")}` : `${base}/standalone-minuta`;
44
+ }
45
+ export function proxyToMinutaApi(event, path) {
46
+ const token = resolveMinutaAuthToken(event);
47
+ if (!token) {
48
+ throw createError({
49
+ statusCode: 401,
50
+ statusMessage: "Authentication token is required"
51
+ });
52
+ }
53
+ return proxyRequest(event, buildMinutaTargetUrl(event, path), {
54
+ headers: {
55
+ authorization: `Bearer ${token}`,
56
+ // o h3 repassa TODOS os headers do request menos os hop-by-hop —
57
+ // inclusive o Cookie do app hospedeiro. A API do CDB se autentica
58
+ // pelo Bearer acima e não precisa (nem deve receber) a sessão do
59
+ // host: zera o header em vez de vazá-la para outro serviço.
60
+ cookie: ""
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Conteúdo de nota de rodapé: o template escreve HTML dentro de `<footnote>`
3
+ * (negrito, itálico, links) e declara o corpo da nota em `style="font-size: 9pt"`.
4
+ * O marcador do preview carrega esse conteúdo num atributo, então ele precisa
5
+ * de um subconjunto seguro — atributo nenhum sobrevive, só as tags de ênfase.
6
+ */
7
+ /** HTML da nota reduzido a ênfase inline + links (o resto vira texto) */
8
+ export declare function sanitizeFootnoteHtml(html: string): string;
9
+ export interface FootnoteRun {
10
+ text: string;
11
+ bold?: boolean;
12
+ italics?: boolean;
13
+ underline?: boolean;
14
+ break?: boolean;
15
+ /** quando presente, o run vira ExternalHyperlink no .docx */
16
+ href?: string;
17
+ /** personalização por trecho (spans do popover) */
18
+ font?: string;
19
+ sizePt?: number;
20
+ /** hex SEM '#' (formato do docx) */
21
+ color?: string;
22
+ }
23
+ /** o mesmo HTML fatiado em runs — é assim que o .docx representa ênfase */
24
+ export declare function footnoteHtmlToRuns(html: string): FootnoteRun[];
25
+ /** tamanho declarado na nota (`font-size: 9pt`) em pontos */
26
+ export declare function footnoteSizePt(style: string | null | undefined): number | null;
@@ -0,0 +1,117 @@
1
+ const INLINE_TAGS = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "SUP", "SUB", "BR"]);
2
+ function safeHref(value) {
3
+ const href = (value ?? "").trim();
4
+ return /^https?:\/\//i.test(href) ? href : null;
5
+ }
6
+ function rgbToHex(value) {
7
+ const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
8
+ if (!match)
9
+ return null;
10
+ return `#${match.slice(1, 4).map((n) => Number(n).toString(16).padStart(2, "0")).join("")}`;
11
+ }
12
+ function safeSpanStyle(style) {
13
+ if (!style)
14
+ return null;
15
+ const out = [];
16
+ const family = style.match(/font-family\s*:\s*([^;]+)/i)?.[1]?.trim();
17
+ if (family && /^[\w\s,'"-]+$/.test(family))
18
+ out.push(`font-family: ${family.replace(/"/g, "'")}`);
19
+ const size = style.match(/font-size\s*:\s*([\d.]+)\s*pt/i)?.[1];
20
+ if (size)
21
+ out.push(`font-size: ${size}pt`);
22
+ const rawColor = style.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i)?.[1]?.trim();
23
+ if (rawColor) {
24
+ const hex = rawColor.startsWith("#") ? /^#[0-9a-f]{6}$/i.test(rawColor) ? rawColor : null : rgbToHex(rawColor);
25
+ if (hex)
26
+ out.push(`color: ${hex}`);
27
+ }
28
+ return out.length ? out.join("; ") : null;
29
+ }
30
+ export function sanitizeFootnoteHtml(html) {
31
+ if (!html)
32
+ return "";
33
+ const doc = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
34
+ const root = doc.body.firstElementChild;
35
+ if (!root)
36
+ return "";
37
+ const walk = (node) => {
38
+ if (node.nodeType === Node.TEXT_NODE)
39
+ return (node.textContent ?? "").replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c]);
40
+ if (!(node instanceof Element))
41
+ return "";
42
+ if (node.tagName === "SCRIPT" || node.tagName === "STYLE")
43
+ return "";
44
+ const inner = Array.from(node.childNodes).map(walk).join("");
45
+ if (node.tagName === "BR")
46
+ return "<br>";
47
+ if (node.tagName === "A") {
48
+ const href = safeHref(node.getAttribute("href"));
49
+ return href ? `<a href="${href.replace(/"/g, "&quot;")}">${inner}</a>` : inner;
50
+ }
51
+ if (node.tagName === "SPAN" || node.tagName === "FONT") {
52
+ const style = safeSpanStyle(node.getAttribute("style"));
53
+ return style ? `<span style="${style}">${inner}</span>` : inner;
54
+ }
55
+ if (node.tagName === "DIV" || node.tagName === "P")
56
+ return inner ? `${inner}<br>` : "";
57
+ if (INLINE_TAGS.has(node.tagName))
58
+ return `<${node.tagName.toLowerCase()}>${inner}</${node.tagName.toLowerCase()}>`;
59
+ return inner;
60
+ };
61
+ return Array.from(root.childNodes).map(walk).join("").replace(/\s+/g, " ").replace(/(<br>\s*)+$/i, "").trim();
62
+ }
63
+ export function footnoteHtmlToRuns(html) {
64
+ const runs = [];
65
+ if (!html)
66
+ return runs;
67
+ const doc = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
68
+ const root = doc.body.firstElementChild;
69
+ if (!root)
70
+ return runs;
71
+ const walk = (node, style) => {
72
+ if (node.nodeType === Node.TEXT_NODE) {
73
+ const text = node.textContent ?? "";
74
+ if (text)
75
+ runs.push({ ...style, text });
76
+ return;
77
+ }
78
+ if (!(node instanceof Element))
79
+ return;
80
+ if (node.tagName === "BR") {
81
+ runs.push({ text: "", break: true });
82
+ return;
83
+ }
84
+ const next = { ...style };
85
+ if (node.tagName === "B" || node.tagName === "STRONG")
86
+ next.bold = true;
87
+ if (node.tagName === "I" || node.tagName === "EM")
88
+ next.italics = true;
89
+ if (node.tagName === "U")
90
+ next.underline = true;
91
+ if (node.tagName === "A") {
92
+ const href = safeHref(node.getAttribute("href"));
93
+ if (href)
94
+ next.href = href;
95
+ }
96
+ if (node.tagName === "SPAN") {
97
+ const spanStyle = node.getAttribute("style") ?? "";
98
+ const family = spanStyle.match(/font-family\s*:\s*([^;]+)/i)?.[1]?.split(",")[0]?.trim().replace(/['"]/g, "");
99
+ if (family)
100
+ next.font = family;
101
+ const size = spanStyle.match(/font-size\s*:\s*([\d.]+)\s*pt/i)?.[1];
102
+ if (size)
103
+ next.sizePt = Number.parseFloat(size);
104
+ const color = spanStyle.match(/color\s*:\s*#([0-9a-f]{6})/i)?.[1];
105
+ if (color)
106
+ next.color = color.toUpperCase();
107
+ }
108
+ node.childNodes.forEach((child) => walk(child, next));
109
+ };
110
+ root.childNodes.forEach((child) => walk(child, {}));
111
+ return runs;
112
+ }
113
+ export function footnoteSizePt(style) {
114
+ const match = style?.match(/font-size\s*:\s*([\d.]+)\s*pt/i);
115
+ const value = match ? Number.parseFloat(match[1]) : Number.NaN;
116
+ return Number.isFinite(value) ? value : null;
117
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Regra de *keep-with-next* do motor de paginação: um título não pode ficar
3
+ * órfão no pé da página, então a quebra sobe para antes dele.
4
+ *
5
+ * Vive separada de usePocPaginationPlugin porque o laço que a usa tem
6
+ * RETROCESSO — ele volta o índice para reavaliar o item depois de mover a
7
+ * quebra — e uma decisão que não avança trava o laço para sempre. Sendo pura,
8
+ * a regra pode ser testada sem DOM nem ProseMirror.
9
+ */
10
+ export interface PocKeepWithNextItem {
11
+ /** topo do item no fluxo normalizado (sem chrome), em px */
12
+ top: number;
13
+ /** altura do item em px */
14
+ height: number;
15
+ /** margem superior, que conta como parte da página que o item abre */
16
+ marginTop: number;
17
+ /** é um título (h1–h4)? */
18
+ heading: boolean;
19
+ /** é o parágrafo-marcador de quebra manual do template? */
20
+ marker: boolean;
21
+ }
22
+ /**
23
+ * A quebra deve subir do item para o título imediatamente anterior?
24
+ *
25
+ * Duas condições, e as duas são necessárias para o laço TERMINAR:
26
+ *
27
+ * 1. o título ainda não abre a página corrente — comparado pelo topo COM
28
+ * MARGEM, que é exatamente o valor que vira o novo `pageStart`. Comparar
29
+ * `prev.top` puro faz um título com `margin-top > 0` satisfazer a condição
30
+ * para sempre: `pageStart` vira `prev.top - prev.marginTop`, que é sempre
31
+ * menor que `prev.top`, e a mesma quebra é reemitida em loop;
32
+ *
33
+ * 2. o par título+item precisa CABER na página nova. Se o item sozinho já
34
+ * estoura a página (uma tabela ou imagem grande vinda de DOCX), descer o
35
+ * título não resolve — a quebra não avança e o laço volta ao mesmo ponto.
36
+ */
37
+ export declare function shouldMoveBreakToHeading(params: {
38
+ heading: PocKeepWithNextItem | undefined;
39
+ item: PocKeepWithNextItem;
40
+ /** topo da página corrente no fluxo normalizado */
41
+ pageStart: number;
42
+ /** altura útil disponível, já descontadas as notas de rodapé */
43
+ capacity: number;
44
+ }): boolean;
@@ -0,0 +1,9 @@
1
+ export function shouldMoveBreakToHeading(params) {
2
+ const { heading, item, pageStart, capacity } = params;
3
+ if (!heading || !heading.heading || heading.marker)
4
+ return false;
5
+ const headingStart = heading.top - heading.marginTop;
6
+ if (headingStart <= pageStart)
7
+ return false;
8
+ return item.top + item.height - headingStart <= capacity;
9
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Sanitização do HTML rico de cabeçalho/rodapé (chrome) — conteúdo criado em
3
+ * contenteditable e PERSISTIDO (template.headers, case_edition.chrome). Sem
4
+ * isso, um usuário do workspace poderia armazenar markup executável que roda
5
+ * no browser de quem abrir o caso/preview (stored XSS).
6
+ *
7
+ * Aplicar em TODO sink (v-html / innerHTML) e ao persistir. Mantém a
8
+ * formatação suportada (b/i/u, spans com estilo, imagens data:/blob:/https).
9
+ */
10
+ export declare function sanitizeRichHtml(html: string | null | undefined): string;
@@ -0,0 +1,73 @@
1
+ const BLOCKED_TAGS = /* @__PURE__ */ new Set([
2
+ "SCRIPT",
3
+ "STYLE",
4
+ "IFRAME",
5
+ "OBJECT",
6
+ "EMBED",
7
+ "LINK",
8
+ "META",
9
+ "BASE",
10
+ "FORM",
11
+ "INPUT",
12
+ "BUTTON",
13
+ "TEXTAREA",
14
+ "SELECT",
15
+ "TEMPLATE",
16
+ "DIALOG",
17
+ // SMIL: anima/troca atributo de outro elemento DEPOIS da sanitização —
18
+ // <a><animate attributeName="href" to="javascript:…"> devolve o href
19
+ // proibido no clique, com a URL nunca passando pelo allowlist abaixo
20
+ "ANIMATE",
21
+ "ANIMATEMOTION",
22
+ "ANIMATETRANSFORM",
23
+ "SET",
24
+ "DISCARD",
25
+ // hospedam markup de outro namespace (HTML dentro de SVG, MathML) — o
26
+ // parser reinterpreta na 2ª passada e é o vetor clássico de mXSS
27
+ "FOREIGNOBJECT",
28
+ "ANNOTATION-XML",
29
+ "NOSCRIPT",
30
+ "NOEMBED",
31
+ "NOFRAMES",
32
+ "XMP",
33
+ "PLAINTEXT",
34
+ // <use> puxa e clona um nó por referência (inclusive de fora do documento)
35
+ "USE"
36
+ ]);
37
+ const BLOCKED_ATTRS = /* @__PURE__ */ new Set(["attributename", "attributetype", "to", "values", "from", "by"]);
38
+ const SAFE_URL = /^(?:https?:|data:image\/|blob:|vault:)/i;
39
+ function scrubElement(el) {
40
+ for (const child of [...el.children]) {
41
+ if (BLOCKED_TAGS.has(child.tagName.toUpperCase())) {
42
+ child.remove();
43
+ continue;
44
+ }
45
+ for (const attr of [...child.attributes]) {
46
+ const name = attr.name.toLowerCase();
47
+ if (name.startsWith("on")) {
48
+ child.removeAttribute(attr.name);
49
+ continue;
50
+ }
51
+ if (BLOCKED_ATTRS.has(name)) {
52
+ child.removeAttribute(attr.name);
53
+ continue;
54
+ }
55
+ if ((name === "src" || name === "href" || name === "xlink:href" || name === "formaction" || name === "srcdoc") && (name === "srcdoc" || !SAFE_URL.test(attr.value.trim()))) {
56
+ child.removeAttribute(attr.name);
57
+ continue;
58
+ }
59
+ if (name === "style" && /url\s*\(/i.test(attr.value) && !/url\s*\(\s*['"]?(?:https?:|data:image\/|blob:)/i.test(attr.value)) {
60
+ child.removeAttribute(attr.name);
61
+ }
62
+ }
63
+ scrubElement(child);
64
+ }
65
+ }
66
+ export function sanitizeRichHtml(html) {
67
+ if (!html)
68
+ return "";
69
+ const tpl = document.createElement("template");
70
+ tpl.innerHTML = html;
71
+ scrubElement(tpl.content);
72
+ return tpl.innerHTML;
73
+ }
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.js'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.js'
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module'
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@meistrari/minuta-nuxt",
3
+ "version": "0.1.0",
4
+ "description": "Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/meistrari/case-defense-builder.git",
8
+ "directory": "packages/minuta-nuxt"
9
+ },
10
+ "license": "UNLICENSED",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types.d.mts",
15
+ "import": "./dist/module.mjs"
16
+ },
17
+ "./runtime/*": {
18
+ "types": "./dist/runtime/*.d.ts",
19
+ "import": "./dist/runtime/*.js"
20
+ }
21
+ },
22
+ "main": "./dist/module.mjs",
23
+ "typesVersions": {
24
+ "*": {
25
+ ".": [
26
+ "./dist/types.d.mts"
27
+ ]
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "prepack": "nuxt-module-build build",
35
+ "build": "nuxt-module-build build",
36
+ "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
37
+ "dev": "nuxi dev playground"
38
+ },
39
+ "dependencies": {
40
+ "@fontsource-variable/inter": "5.3.0",
41
+ "@nuxt/kit": "^3.17.7",
42
+ "@phosphor-icons/vue": "2.2.1",
43
+ "@tiptap/core": "2.11.9",
44
+ "@tiptap/extension-table": "2.11.9",
45
+ "@tiptap/extension-table-cell": "2.11.9",
46
+ "@tiptap/extension-table-header": "2.11.9",
47
+ "@tiptap/extension-table-row": "2.11.9",
48
+ "@tiptap/pm": "2.11.9",
49
+ "@tiptap/starter-kit": "2.11.9",
50
+ "@tiptap/vue-3": "2.11.9",
51
+ "defu": "^6.1.4",
52
+ "docx": "9.7.1",
53
+ "h3": "^1.15.0",
54
+ "ofetch": "^1.4.1"
55
+ },
56
+ "overrides": {
57
+ "@tiptap/core": "2.11.9",
58
+ "@tiptap/pm": "2.11.9",
59
+ "@tiptap/extension-blockquote": "2.11.9",
60
+ "@tiptap/extension-bold": "2.11.9",
61
+ "@tiptap/extension-bullet-list": "2.11.9",
62
+ "@tiptap/extension-code": "2.11.9",
63
+ "@tiptap/extension-code-block": "2.11.9",
64
+ "@tiptap/extension-document": "2.11.9",
65
+ "@tiptap/extension-dropcursor": "2.11.9",
66
+ "@tiptap/extension-gapcursor": "2.11.9",
67
+ "@tiptap/extension-hard-break": "2.11.9",
68
+ "@tiptap/extension-heading": "2.11.9",
69
+ "@tiptap/extension-history": "2.11.9",
70
+ "@tiptap/extension-horizontal-rule": "2.11.9",
71
+ "@tiptap/extension-italic": "2.11.9",
72
+ "@tiptap/extension-list-item": "2.11.9",
73
+ "@tiptap/extension-ordered-list": "2.11.9",
74
+ "@tiptap/extension-paragraph": "2.11.9",
75
+ "@tiptap/extension-strike": "2.11.9",
76
+ "@tiptap/extension-text": "2.11.9",
77
+ "@tiptap/extension-text-style": "2.11.9",
78
+ "@tiptap/extension-bubble-menu": "2.11.9",
79
+ "@tiptap/extension-floating-menu": "2.11.9"
80
+ },
81
+ "devDependencies": {
82
+ "@nuxt/module-builder": "^0.8.4",
83
+ "nuxt": "~3.17.7",
84
+ "pino-pretty": "^13.1.3",
85
+ "typescript": "^5.8.2",
86
+ "vue": "^3.5.13",
87
+ "@meistrari/chat-nuxt": "3.14.0-rc.2"
88
+ },
89
+ "publishConfig": {
90
+ "access": "public"
91
+ },
92
+ "peerDependencies": {
93
+ "@meistrari/chat-nuxt": ">=3.13.0"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "@meistrari/chat-nuxt": {
97
+ "optional": true
98
+ }
99
+ }
100
+ }