@meistrari/minuta-editor 1.4.0 → 1.6.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.
package/README.md CHANGED
@@ -61,6 +61,13 @@ editor.setImageFallback('dataUri' | 'none')
61
61
  editor.variablesInDocument() // → string[]
62
62
  editor.refreshVaultImages(vaultUrl => Promise<string>) // renova o src das imagens do vault (data-vault-src ou src="vault://"); sem undo/onChange
63
63
 
64
+ // checklist junto da edição (o documento gerado vem do GET /data/case/:id/html?source=blocks)
65
+ editor.stampGenerated() // ao CRIAR a edição a partir do HTML do checklist: marca os blocos como "gerados"
66
+ editor.applyChecklist(htmlGerado, { toggled? }) // → { inserted, removed, regenerated, variablesUpdated, conflicts, … }
67
+ editor.checklistConflicts() // → [{ blockId, blockTitle, kind: 'replace' | 'remove' }]
68
+ editor.resolveChecklistConflict(blockId, 'keep' | 'apply')
69
+ editor.documentBlockIds() // → blocos no texto, na ordem
70
+
64
71
  editor.schedulePaginate() // repagina (após mudar algo que afeta altura por fora)
65
72
  editor.totalPages()
66
73
  editor.destroy()
@@ -1,4 +1,4 @@
1
- import { f as footnoteHtmlToRuns, p as pocLoneCellIndex, a as formatTopicNumber, b as pocLooksLikeTitle, T as TOPIC_DEFAULT_GROUP } from '../shared/minuta-editor.b3351035.mjs';
1
+ import { f as footnoteHtmlToRuns, p as pocLoneCellIndex, a as formatTopicNumber, b as pocLooksLikeTitle, c as pocNodeHasVisibleContent, d as pocNodePaintsBox, T as TOPIC_DEFAULT_GROUP } from '../shared/minuta-editor.f1239e0b.mjs';
2
2
  import { BorderStyle, TextRun, ExternalHyperlink, Paragraph, Document, LevelFormat, AlignmentType, LevelSuffix, Header, Packer, LineRuleType, Table, TableRow, TableCell, WidthType, TableLayoutType, Footer, FootnoteReferenceRun, VerticalAlign, ImageRun, HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, TextWrappingType, FrameAnchorType, PageNumber, HeadingLevel } from 'docx';
3
3
  import '@tiptap/pm/state';
4
4
  import '@tiptap/pm/view';
@@ -18,6 +18,48 @@ function topicSequenceFor(ctx, group, roman, number) {
18
18
  seq.next = number + 1;
19
19
  return seq.reference;
20
20
  }
21
+ function listFormatFor(type) {
22
+ switch (type) {
23
+ case "a":
24
+ return LevelFormat.LOWER_LETTER;
25
+ case "A":
26
+ return LevelFormat.UPPER_LETTER;
27
+ case "i":
28
+ return LevelFormat.LOWER_ROMAN;
29
+ case "I":
30
+ return LevelFormat.UPPER_ROMAN;
31
+ default:
32
+ return LevelFormat.DECIMAL;
33
+ }
34
+ }
35
+ function listFormatFromCss(listStyleType) {
36
+ switch (listStyleType) {
37
+ case "lower-alpha":
38
+ case "lower-latin":
39
+ return LevelFormat.LOWER_LETTER;
40
+ case "upper-alpha":
41
+ case "upper-latin":
42
+ return LevelFormat.UPPER_LETTER;
43
+ case "lower-roman":
44
+ return LevelFormat.LOWER_ROMAN;
45
+ case "upper-roman":
46
+ return LevelFormat.UPPER_ROMAN;
47
+ default:
48
+ return LevelFormat.DECIMAL;
49
+ }
50
+ }
51
+ function listSequenceFor(ctx, list, run) {
52
+ const reference = `cdb-list-${ctx.listNumbering.size + 1}`;
53
+ const start = Number.parseInt(String(list.start ?? 1), 10);
54
+ ctx.listNumbering.set(reference, {
55
+ format: list.format,
56
+ // `start="0"` é válido (o Word aceita w:start=0); só o ausente/inválido vira 1
57
+ start: Number.isFinite(start) && start >= 0 ? start : 1,
58
+ suffix: list.suffix || ".",
59
+ run
60
+ });
61
+ return reference;
62
+ }
21
63
  function leadingTopicNumber(node) {
22
64
  for (let i = 0; i < node.childCount; i++) {
23
65
  const child = node.child(i);
@@ -466,20 +508,28 @@ async function islandToDocx(node, pos, contentWidthPx, ctx) {
466
508
  const paragraphs = [];
467
509
  const liveLis = islandEl ? [...islandEl.querySelectorAll(":scope > ul > li, :scope > ol > li, :scope > li")] : [];
468
510
  const parsedLis = [...el.querySelectorAll(":scope > li")];
469
- let n = 1;
511
+ let reference = null;
470
512
  for (let i = 0; i < parsedLis.length; i++) {
471
513
  const live = liveLis[i] ?? null;
472
514
  const source = live ?? parsedLis[i];
473
515
  const m = live ? measureEl(live) : null;
474
- const marker = tag === "ol" ? `${n++}. ` : "\u2022 ";
475
- const markerColor = m?.color;
516
+ const markerRun = { color: m?.color, size: m?.sizeHalf, font: m ? primaryFont(m.cs) : void 0 };
517
+ if (tag === "ol" && !reference) {
518
+ const liveStyleType = liveLis[0] ? getComputedStyle(liveLis[0]).listStyleType : "";
519
+ reference = listSequenceFor(ctx, {
520
+ format: el.hasAttribute("type") ? listFormatFor(el.getAttribute("type")) : listFormatFromCss(liveStyleType),
521
+ start: el.getAttribute("start"),
522
+ suffix: el.getAttribute("data-list-suffix")
523
+ }, markerRun);
524
+ }
476
525
  const runs = live ? domInlineToRuns(live) : [new TextRun({ text: source.textContent?.trim() || "", ...runOptions(parseStyle(source.getAttribute("style"))) })];
477
526
  const indentPx = live && islandEl ? live.offsetLeft : 24;
478
527
  paragraphs.push(new Paragraph({
479
528
  children: [
480
- new TextRun({ text: marker, color: markerColor, size: m?.sizeHalf, font: m ? primaryFont(m.cs) : void 0 }),
529
+ ...tag === "ul" ? [new TextRun({ text: "\u2022 ", ...markerRun })] : [],
481
530
  ...runs
482
531
  ],
532
+ numbering: reference ? { reference, level: 0 } : void 0,
483
533
  spacing: m ? { after: m.afterTw, before: m.beforeTw, ...m.lineExactTw ? { line: m.lineExactTw, lineRule: LineRuleType.EXACT } : {} } : { after: 80 },
484
534
  indent: { left: Math.round(indentPx * PX_TO_TWIP) },
485
535
  alignment: m?.align,
@@ -694,6 +744,27 @@ async function flexRowToDocx(node, pos, contentWidthPx, ctx) {
694
744
  }
695
745
  return tables;
696
746
  }
747
+ const INVISIBLE_NODE_TYPES = /* @__PURE__ */ new Set(["paragraph", "styledDiv", "missingImage", "hardBreak", "text"]);
748
+ function rowPaintsOrShowsAnything(row, rowPos, ctx) {
749
+ const paintsAt = (node, pos) => {
750
+ if (pocNodePaintsBox(node))
751
+ return true;
752
+ const dom = domAt(ctx, pos);
753
+ return Boolean(bordersFrom(measureAt(ctx, pos), node.attrs?.style)) || Boolean(cellShading(dom ? getComputedStyle(dom) : null));
754
+ };
755
+ if (paintsAt(row, rowPos))
756
+ return true;
757
+ let found = false;
758
+ row.descendants((node, offset) => {
759
+ if (found)
760
+ return false;
761
+ const pos = rowPos + 1 + offset;
762
+ if (!INVISIBLE_NODE_TYPES.has(node.type.name) || pocNodeHasVisibleContent(node) || paintsAt(node, pos))
763
+ found = true;
764
+ return !found;
765
+ });
766
+ return found;
767
+ }
697
768
  function isPlainContainer(node) {
698
769
  return node.type.name === "styledDiv" && !isFlexRow(node);
699
770
  }
@@ -930,6 +1001,18 @@ async function nodeToDocx(node, pos, contentWidthPx, ctx) {
930
1001
  }
931
1002
  case "styledDiv": {
932
1003
  if (isFlexRow(node)) {
1004
+ if (!rowPaintsOrShowsAnything(node, pos, ctx)) {
1005
+ const rowM2 = measureAt(ctx, pos);
1006
+ const breakBefore = consumeBreak(ctx);
1007
+ const before = breakBefore ? rowM2?.beforeTw ?? 0 : Math.max(0, (rowM2?.beforeTw ?? 0) - ctx.lastAfterTw);
1008
+ const lugar = new Paragraph({
1009
+ children: [],
1010
+ spacing: { after: rowM2?.afterTw ?? 0, before },
1011
+ pageBreakBefore: breakBefore
1012
+ });
1013
+ ctx.lastAfterTw = rowM2?.afterTw ?? 0;
1014
+ return [lugar];
1015
+ }
933
1016
  const loneIndex = pocLoneCellIndex(node);
934
1017
  if (loneIndex != null) {
935
1018
  let childPos = pos + 1;
@@ -1012,31 +1095,41 @@ async function nodeToDocx(node, pos, contentWidthPx, ctx) {
1012
1095
  case "bulletList":
1013
1096
  case "orderedList": {
1014
1097
  const paragraphs = [];
1015
- let n = 1;
1098
+ const ordered = node.type.name === "orderedList";
1099
+ let reference = null;
1016
1100
  let liPos = pos + 1;
1017
1101
  for (let i = 0; i < node.childCount; i++) {
1018
1102
  const li = node.child(i);
1019
1103
  let childPos = liPos + 1;
1104
+ let marked = false;
1020
1105
  li.forEach((child) => {
1021
1106
  if (child.isTextblock) {
1022
1107
  const m = measureAt(ctx, childPos);
1023
1108
  const base = m ? { bold: m.bold, italics: m.italics, color: m.color, size: m.sizeHalf } : parseStyle(child.attrs.style);
1024
- const marker = node.type.name === "orderedList" ? `${n}. ` : "\u2022 ";
1025
1109
  const liDom = domAt(ctx, liPos);
1026
1110
  const indentPx = liDom?.offsetLeft ?? 48;
1027
1111
  const liBreak = consumeBreak(ctx);
1112
+ if (ordered && !reference) {
1113
+ reference = listSequenceFor(
1114
+ ctx,
1115
+ { format: listFormatFor(node.attrs.type), start: node.attrs.start },
1116
+ { bold: base.bold, italics: base.italics, color: base.color, size: base.size, font: base.font }
1117
+ );
1118
+ }
1119
+ const bullet = !ordered && !marked ? [new TextRun({ text: "\u2022 ", ...runOptions(base) })] : [];
1028
1120
  const liParagraph = new Paragraph({
1029
- children: [new TextRun({ text: marker, ...runOptions(base) }), ...inlineToRuns(child, base, ctx)],
1121
+ children: [...bullet, ...inlineToRuns(child, base, ctx)],
1122
+ numbering: ordered && !marked && reference ? { reference, level: 0 } : void 0,
1030
1123
  spacing: spacingFrom(m, child.attrs.style),
1031
1124
  indent: { left: Math.round(indentPx * PX_TO_TWIP) },
1032
1125
  pageBreakBefore: liBreak
1033
1126
  });
1127
+ marked = true;
1034
1128
  ctx.meta.set(liParagraph, { breakBefore: liBreak });
1035
1129
  paragraphs.push(liParagraph);
1036
1130
  }
1037
1131
  childPos += child.nodeSize;
1038
1132
  });
1039
- n++;
1040
1133
  liPos += li.nodeSize;
1041
1134
  }
1042
1135
  return paragraphs;
@@ -1355,7 +1448,8 @@ async function pocExportDocx(editor, margins, chrome, options = {}) {
1355
1448
  meta: /* @__PURE__ */ new WeakMap(),
1356
1449
  footnotes: [],
1357
1450
  topicNumbering: /* @__PURE__ */ new Map(),
1358
- topicSequences: /* @__PURE__ */ new Map()
1451
+ topicSequences: /* @__PURE__ */ new Map(),
1452
+ listNumbering: /* @__PURE__ */ new Map()
1359
1453
  };
1360
1454
  const children = [];
1361
1455
  let pos = 0;
@@ -1404,21 +1498,38 @@ async function pocExportDocx(editor, margins, chrome, options = {}) {
1404
1498
  }
1405
1499
  },
1406
1500
  footnotes: Object.keys(footnotes).length ? footnotes : void 0,
1407
- numbering: ctx.topicNumbering.size ? {
1408
- config: [...ctx.topicNumbering].map(([reference, { roman, start }]) => ({
1409
- reference,
1410
- levels: [{
1411
- level: 0,
1412
- format: roman ? LevelFormat.UPPER_ROMAN : LevelFormat.DECIMAL,
1413
- text: "%1",
1414
- start,
1415
- alignment: AlignmentType.START,
1416
- // o ". " depois do número é texto do template; e sem
1417
- // recuo/tab o parágrafo fica igual ao preview
1418
- suffix: LevelSuffix.NOTHING,
1419
- style: { paragraph: { indent: { left: 0, hanging: 0 } } }
1420
- }]
1421
- }))
1501
+ numbering: ctx.topicNumbering.size || ctx.listNumbering.size ? {
1502
+ config: [
1503
+ ...[...ctx.topicNumbering].map(([reference, { roman, start }]) => ({
1504
+ reference,
1505
+ levels: [{
1506
+ level: 0,
1507
+ format: roman ? LevelFormat.UPPER_ROMAN : LevelFormat.DECIMAL,
1508
+ text: "%1",
1509
+ start,
1510
+ alignment: AlignmentType.START,
1511
+ // o ". " depois do número é texto do template; e sem
1512
+ // recuo/tab o parágrafo fica igual ao preview
1513
+ suffix: LevelSuffix.NOTHING,
1514
+ style: { paragraph: { indent: { left: 0, hanging: 0 } } }
1515
+ }]
1516
+ })),
1517
+ ...[...ctx.listNumbering].map(([reference, { format, start, suffix, run }]) => ({
1518
+ reference,
1519
+ levels: [{
1520
+ level: 0,
1521
+ format,
1522
+ text: `%1${suffix}`,
1523
+ start,
1524
+ alignment: AlignmentType.LEFT,
1525
+ // "a. texto": marcador colado ao item com um espaço,
1526
+ // como o preview (list-style-position: inside); o
1527
+ // recuo é o do parágrafo, não da sequência
1528
+ suffix: LevelSuffix.SPACE,
1529
+ style: { paragraph: { indent: { left: 0, hanging: 0 } }, run }
1530
+ }]
1531
+ }))
1532
+ ]
1422
1533
  } : void 0,
1423
1534
  sections: [{
1424
1535
  properties: {
package/dist/docx.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { pocExportDocx } from './chunks/docxExport.mjs';
2
- import './shared/minuta-editor.b3351035.mjs';
2
+ import './shared/minuta-editor.f1239e0b.mjs';
3
3
  import '@tiptap/pm/state';
4
4
  import '@tiptap/pm/view';
5
5
  import 'docx';
package/dist/index.d.mts CHANGED
@@ -22,7 +22,7 @@ import { Transaction } from '@tiptap/pm/state';
22
22
  import { EditorView } from '@tiptap/pm/view';
23
23
  import { P as PocPageConfig } from './shared/minuta-editor.9e65d163.mjs';
24
24
  export { e as PAGINATE_META, a as PocDocxChrome, b as PocDocxExportOptions, s as PocLoneRow, d as PocPageBreak, c as PocPageNote, v as PocPaginationPlugin, u as breaksEqual, h as buildPocFooterEl, g as buildPocHeaderEl, j as buildPocNotesEl, l as computePocBreaks, t as computePocLoneRows, i as isPocChromeCanvas, n as isPocFlexRow, f as pocChromeFlowHeight, m as pocDescendsInto, r as pocLoneCellIndex, k as pocLooksLikeTitle, o as pocNodeHasVisibleContent, q as pocNodePaintsBox, p as pocPageKey } from './shared/minuta-editor.9e65d163.mjs';
25
- import '@tiptap/pm/model';
25
+ import { Node as Node$1 } from '@tiptap/pm/model';
26
26
 
27
27
  /**
28
28
  * Variáveis de template da minuta — o formato do placeholder e a resolução dele.
@@ -206,6 +206,84 @@ declare function buildPocExtensions(pageConfig?: PocPageConfig, options?: {
206
206
  handleKeyDown?: (view: EditorView, event: KeyboardEvent) => boolean;
207
207
  }): (Node<any, any> | Extension<any, any> | Mark<any, any>)[];
208
208
 
209
+ /**
210
+ * Impressão digital de um bloco da minuta (nó `minutaBlock` do editor) — base
211
+ * de "o advogado mexeu neste trecho?" quando o checklist muda com uma edição
212
+ * viva. O bloco nasce com `genHash` = impressão do texto gerado; enquanto a
213
+ * impressão atual bater com ela, o trecho é do checklist e pode ser
214
+ * re-gerado sem perder nada.
215
+ *
216
+ * Fica de fora o que muda SEM o advogado editar:
217
+ * - URL descartável de imagem (blob:/placeholder do load progressivo/vault:// do
218
+ * save): a imagem entra pela identidade DURÁVEL (`imageIdentity`), então
219
+ * trocar a imagem de verdade muda a impressão;
220
+ * - número de tópico e de nota (re-sequenciam quando outro trecho muda);
221
+ * - valor das variáveis (o chip é atualizado no lugar, em qualquer bloco —
222
+ * inclusive dentro de lista/tabela guardada como HTML cru).
223
+ *
224
+ * Mesma regra em `client/utils/minutaBlockFingerprint.ts` (o app ainda mantém a
225
+ * cópia dele) — a impressão gravada por um tem que bater com a do outro.
226
+ *
227
+ * Recebe o JSON do nó (`node.toJSON()`), então é pura e não depende de DOM.
228
+ */
229
+ interface NodeJson {
230
+ type: string;
231
+ attrs?: Record<string, any>;
232
+ content?: NodeJson[];
233
+ marks?: {
234
+ type: string;
235
+ attrs?: Record<string, any>;
236
+ }[];
237
+ text?: string;
238
+ }
239
+ /**
240
+ * Identidade durável de uma imagem a partir do `src` (e do `data-vault-src` do
241
+ * placeholder do load progressivo) — no app, blob: do cache → vault://.
242
+ */
243
+ type ImageIdentity = (src: string) => string;
244
+ declare function minutaBlockFingerprint(blockJson: NodeJson, identity?: ImageIdentity): string;
245
+
246
+ /**
247
+ * Checklist × edição viva, para quem usa o editor fora do defesa.ai.
248
+ *
249
+ * O documento editado continua dividido em `minutaBlock` (um por bloco do
250
+ * template, `isolating`), e cada bloco guarda em `genHash` a impressão digital
251
+ * do texto que o checklist gerou. Com isso, quando o checklist muda (tese que
252
+ * passou a valer, variável alterada), dá para aplicar a mudança SÓ onde ela
253
+ * cai, sem passar por cima do que o advogado escreveu:
254
+ *
255
+ * - tese que passou a gerar texto → o bloco entra na posição do template;
256
+ * - tese que deixou de gerar → o bloco sai (se foi editado, vira conflito);
257
+ * - variável alterada → o chip atualiza no lugar, em qualquer bloco;
258
+ * - bloco cuja ESTRUTURA mudou → re-gerado se ninguém o editou, conflito se sim.
259
+ *
260
+ * Mesma lógica de `client/composables/poc/pocEditionSync.ts` + o
261
+ * `syncEditionWithChecklist` do app; aqui o documento gerado chega pronto (o
262
+ * `GET /data/case/:id/html?source=blocks`), em vez de renderizado no browser.
263
+ */
264
+ interface MinutaChecklistConflict {
265
+ blockId: string;
266
+ blockTitle: string | null;
267
+ /** `replace`: o checklist mudou o texto de um trecho editado; `remove`: desligou uma tese editada */
268
+ kind: 'replace' | 'remove';
269
+ }
270
+ interface MinutaChecklistResult {
271
+ inserted: string[];
272
+ removed: string[];
273
+ regenerated: string[];
274
+ /** quantos chips de variável mudaram de valor */
275
+ variablesUpdated: number;
276
+ /** conflitos abertos depois desta aplicação (inclui os que já estavam) */
277
+ conflicts: MinutaChecklistConflict[];
278
+ /** a tese pedida em `toggled.show` não gera texto (condições internas) */
279
+ toggledWithoutContent: boolean;
280
+ /** a remoção não aconteceu porque o documento ficaria sem nenhum bloco */
281
+ lastBlockKept: boolean;
282
+ }
283
+ declare function blockHash(node: Node$1, identity?: ImageIdentity): string;
284
+ /** o conteúdo ainda é o que o checklist gerou (sem `genHash` = editado, por segurança) */
285
+ declare function isBlockUntouched(node: Node$1, identity?: ImageIdentity): boolean;
286
+
209
287
  /**
210
288
  * O editor de minuta como coisa montável em qualquer lugar: `createMinutaEditor(el, opts)`.
211
289
  *
@@ -374,6 +452,35 @@ interface MinutaEditor {
374
452
  * guarda uma cópia do documento.
375
453
  */
376
454
  refreshVaultImages: (resolveUrl: (vaultUrl: string) => Promise<string>) => Promise<number>;
455
+ /**
456
+ * Checklist junto da edição: aplica o documento GERADO pelo checklist agora
457
+ * (o `GET /data/case/:id/html?source=blocks`) sobre o documento editado,
458
+ * bloco a bloco — tese que passou a valer entra na posição do template, a
459
+ * que deixou de valer sai, variáveis atualizam no lugar e trecho cuja
460
+ * estrutura mudou é re-gerado só se ninguém o editou (senão vira conflito).
461
+ * `toggled` força a decisão de UMA tese (o host ligou/desligou na mão).
462
+ * Uma transação só: o desfazer volta tudo. Blocos novos (e os aceitos em
463
+ * `resolveChecklistConflict`) podem trazer imagem `vault://` — chame
464
+ * `refreshVaultImages` depois de aplicar E de resolver os conflitos.
465
+ */
466
+ applyChecklist: (generatedHtml: string, options?: {
467
+ toggled?: {
468
+ blockId: string;
469
+ show: boolean;
470
+ };
471
+ }) => MinutaChecklistResult;
472
+ /** trechos editados que o checklist quer mudar e aguardam decisão do advogado */
473
+ checklistConflicts: () => MinutaChecklistConflict[];
474
+ /** `apply` usa o texto novo (ou remove); `keep` mantém o editado e não pergunta de novo pela mesma mudança */
475
+ resolveChecklistConflict: (blockId: string, resolution: 'keep' | 'apply') => boolean;
476
+ /**
477
+ * Marca todos os blocos como "texto gerado". Chame UMA vez ao criar a edição
478
+ * a partir do HTML do checklist — sem a marca, todo bloco conta como editado
479
+ * e o `applyChecklist` pergunta em vez de re-gerar. Devolve quantos marcou.
480
+ */
481
+ stampGenerated: () => number;
482
+ /** blocos (`data-block-id`) no documento, na ordem — para o checklist mostrar o que está no texto */
483
+ documentBlockIds: () => string[];
377
484
  /** repagina (o host chama após mudar algo que afeta altura fora do editor) */
378
485
  schedulePaginate: () => void;
379
486
  /** reconstrói cabeçalhos/rodapés com o chrome atual, sem repaginar */
@@ -561,5 +668,5 @@ declare function collectVaultRefs(html: string): string[];
561
668
  /** aplica as URLs resolvidas às `<img>` do trecho; tags sem resolução ficam como estão */
562
669
  declare function renewVaultImageTags(html: string, resolved: Map<string, string>): string;
563
670
 
564
- export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocTopicNumber, PocTopicRenumber, PocUnderline, PocVariableChip, TOPIC_DEFAULT_GROUP, TOPIC_GROUP_ATTR, TOPIC_NUMBER_ATTR, TOPIC_REF_ATTR, TOPIC_ROMAN_ATTR, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, formatTopicNumber, fromRoman, iconSvg, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, toRoman, topicNumberHtml, variableChipHtml, vaultImageHtml, vaultRefOf };
565
- export type { FootnoteRun, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, TopicNumberInfo, VariableClickKind };
671
+ export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocTopicNumber, PocTopicRenumber, PocUnderline, PocVariableChip, TOPIC_DEFAULT_GROUP, TOPIC_GROUP_ATTR, TOPIC_NUMBER_ATTR, TOPIC_REF_ATTR, TOPIC_ROMAN_ATTR, blockHash, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, formatTopicNumber, fromRoman, iconSvg, isBlockUntouched, minutaBlockFingerprint, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, toRoman, topicNumberHtml, variableChipHtml, vaultImageHtml, vaultRefOf };
672
+ export type { FootnoteRun, ImageIdentity, MinutaChecklistConflict, MinutaChecklistResult, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, TopicNumberInfo, VariableClickKind };
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ import { Transaction } from '@tiptap/pm/state';
22
22
  import { EditorView } from '@tiptap/pm/view';
23
23
  import { P as PocPageConfig } from './shared/minuta-editor.9e65d163.js';
24
24
  export { e as PAGINATE_META, a as PocDocxChrome, b as PocDocxExportOptions, s as PocLoneRow, d as PocPageBreak, c as PocPageNote, v as PocPaginationPlugin, u as breaksEqual, h as buildPocFooterEl, g as buildPocHeaderEl, j as buildPocNotesEl, l as computePocBreaks, t as computePocLoneRows, i as isPocChromeCanvas, n as isPocFlexRow, f as pocChromeFlowHeight, m as pocDescendsInto, r as pocLoneCellIndex, k as pocLooksLikeTitle, o as pocNodeHasVisibleContent, q as pocNodePaintsBox, p as pocPageKey } from './shared/minuta-editor.9e65d163.js';
25
- import '@tiptap/pm/model';
25
+ import { Node as Node$1 } from '@tiptap/pm/model';
26
26
 
27
27
  /**
28
28
  * Variáveis de template da minuta — o formato do placeholder e a resolução dele.
@@ -206,6 +206,84 @@ declare function buildPocExtensions(pageConfig?: PocPageConfig, options?: {
206
206
  handleKeyDown?: (view: EditorView, event: KeyboardEvent) => boolean;
207
207
  }): (Node<any, any> | Extension<any, any> | Mark<any, any>)[];
208
208
 
209
+ /**
210
+ * Impressão digital de um bloco da minuta (nó `minutaBlock` do editor) — base
211
+ * de "o advogado mexeu neste trecho?" quando o checklist muda com uma edição
212
+ * viva. O bloco nasce com `genHash` = impressão do texto gerado; enquanto a
213
+ * impressão atual bater com ela, o trecho é do checklist e pode ser
214
+ * re-gerado sem perder nada.
215
+ *
216
+ * Fica de fora o que muda SEM o advogado editar:
217
+ * - URL descartável de imagem (blob:/placeholder do load progressivo/vault:// do
218
+ * save): a imagem entra pela identidade DURÁVEL (`imageIdentity`), então
219
+ * trocar a imagem de verdade muda a impressão;
220
+ * - número de tópico e de nota (re-sequenciam quando outro trecho muda);
221
+ * - valor das variáveis (o chip é atualizado no lugar, em qualquer bloco —
222
+ * inclusive dentro de lista/tabela guardada como HTML cru).
223
+ *
224
+ * Mesma regra em `client/utils/minutaBlockFingerprint.ts` (o app ainda mantém a
225
+ * cópia dele) — a impressão gravada por um tem que bater com a do outro.
226
+ *
227
+ * Recebe o JSON do nó (`node.toJSON()`), então é pura e não depende de DOM.
228
+ */
229
+ interface NodeJson {
230
+ type: string;
231
+ attrs?: Record<string, any>;
232
+ content?: NodeJson[];
233
+ marks?: {
234
+ type: string;
235
+ attrs?: Record<string, any>;
236
+ }[];
237
+ text?: string;
238
+ }
239
+ /**
240
+ * Identidade durável de uma imagem a partir do `src` (e do `data-vault-src` do
241
+ * placeholder do load progressivo) — no app, blob: do cache → vault://.
242
+ */
243
+ type ImageIdentity = (src: string) => string;
244
+ declare function minutaBlockFingerprint(blockJson: NodeJson, identity?: ImageIdentity): string;
245
+
246
+ /**
247
+ * Checklist × edição viva, para quem usa o editor fora do defesa.ai.
248
+ *
249
+ * O documento editado continua dividido em `minutaBlock` (um por bloco do
250
+ * template, `isolating`), e cada bloco guarda em `genHash` a impressão digital
251
+ * do texto que o checklist gerou. Com isso, quando o checklist muda (tese que
252
+ * passou a valer, variável alterada), dá para aplicar a mudança SÓ onde ela
253
+ * cai, sem passar por cima do que o advogado escreveu:
254
+ *
255
+ * - tese que passou a gerar texto → o bloco entra na posição do template;
256
+ * - tese que deixou de gerar → o bloco sai (se foi editado, vira conflito);
257
+ * - variável alterada → o chip atualiza no lugar, em qualquer bloco;
258
+ * - bloco cuja ESTRUTURA mudou → re-gerado se ninguém o editou, conflito se sim.
259
+ *
260
+ * Mesma lógica de `client/composables/poc/pocEditionSync.ts` + o
261
+ * `syncEditionWithChecklist` do app; aqui o documento gerado chega pronto (o
262
+ * `GET /data/case/:id/html?source=blocks`), em vez de renderizado no browser.
263
+ */
264
+ interface MinutaChecklistConflict {
265
+ blockId: string;
266
+ blockTitle: string | null;
267
+ /** `replace`: o checklist mudou o texto de um trecho editado; `remove`: desligou uma tese editada */
268
+ kind: 'replace' | 'remove';
269
+ }
270
+ interface MinutaChecklistResult {
271
+ inserted: string[];
272
+ removed: string[];
273
+ regenerated: string[];
274
+ /** quantos chips de variável mudaram de valor */
275
+ variablesUpdated: number;
276
+ /** conflitos abertos depois desta aplicação (inclui os que já estavam) */
277
+ conflicts: MinutaChecklistConflict[];
278
+ /** a tese pedida em `toggled.show` não gera texto (condições internas) */
279
+ toggledWithoutContent: boolean;
280
+ /** a remoção não aconteceu porque o documento ficaria sem nenhum bloco */
281
+ lastBlockKept: boolean;
282
+ }
283
+ declare function blockHash(node: Node$1, identity?: ImageIdentity): string;
284
+ /** o conteúdo ainda é o que o checklist gerou (sem `genHash` = editado, por segurança) */
285
+ declare function isBlockUntouched(node: Node$1, identity?: ImageIdentity): boolean;
286
+
209
287
  /**
210
288
  * O editor de minuta como coisa montável em qualquer lugar: `createMinutaEditor(el, opts)`.
211
289
  *
@@ -374,6 +452,35 @@ interface MinutaEditor {
374
452
  * guarda uma cópia do documento.
375
453
  */
376
454
  refreshVaultImages: (resolveUrl: (vaultUrl: string) => Promise<string>) => Promise<number>;
455
+ /**
456
+ * Checklist junto da edição: aplica o documento GERADO pelo checklist agora
457
+ * (o `GET /data/case/:id/html?source=blocks`) sobre o documento editado,
458
+ * bloco a bloco — tese que passou a valer entra na posição do template, a
459
+ * que deixou de valer sai, variáveis atualizam no lugar e trecho cuja
460
+ * estrutura mudou é re-gerado só se ninguém o editou (senão vira conflito).
461
+ * `toggled` força a decisão de UMA tese (o host ligou/desligou na mão).
462
+ * Uma transação só: o desfazer volta tudo. Blocos novos (e os aceitos em
463
+ * `resolveChecklistConflict`) podem trazer imagem `vault://` — chame
464
+ * `refreshVaultImages` depois de aplicar E de resolver os conflitos.
465
+ */
466
+ applyChecklist: (generatedHtml: string, options?: {
467
+ toggled?: {
468
+ blockId: string;
469
+ show: boolean;
470
+ };
471
+ }) => MinutaChecklistResult;
472
+ /** trechos editados que o checklist quer mudar e aguardam decisão do advogado */
473
+ checklistConflicts: () => MinutaChecklistConflict[];
474
+ /** `apply` usa o texto novo (ou remove); `keep` mantém o editado e não pergunta de novo pela mesma mudança */
475
+ resolveChecklistConflict: (blockId: string, resolution: 'keep' | 'apply') => boolean;
476
+ /**
477
+ * Marca todos os blocos como "texto gerado". Chame UMA vez ao criar a edição
478
+ * a partir do HTML do checklist — sem a marca, todo bloco conta como editado
479
+ * e o `applyChecklist` pergunta em vez de re-gerar. Devolve quantos marcou.
480
+ */
481
+ stampGenerated: () => number;
482
+ /** blocos (`data-block-id`) no documento, na ordem — para o checklist mostrar o que está no texto */
483
+ documentBlockIds: () => string[];
377
484
  /** repagina (o host chama após mudar algo que afeta altura fora do editor) */
378
485
  schedulePaginate: () => void;
379
486
  /** reconstrói cabeçalhos/rodapés com o chrome atual, sem repaginar */
@@ -561,5 +668,5 @@ declare function collectVaultRefs(html: string): string[];
561
668
  /** aplica as URLs resolvidas às `<img>` do trecho; tags sem resolução ficam como estão */
562
669
  declare function renewVaultImageTags(html: string, resolved: Map<string, string>): string;
563
670
 
564
- export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocTopicNumber, PocTopicRenumber, PocUnderline, PocVariableChip, TOPIC_DEFAULT_GROUP, TOPIC_GROUP_ATTR, TOPIC_NUMBER_ATTR, TOPIC_REF_ATTR, TOPIC_ROMAN_ATTR, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, formatTopicNumber, fromRoman, iconSvg, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, toRoman, topicNumberHtml, variableChipHtml, vaultImageHtml, vaultRefOf };
565
- export type { FootnoteRun, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, TopicNumberInfo, VariableClickKind };
671
+ export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocTopicNumber, PocTopicRenumber, PocUnderline, PocVariableChip, TOPIC_DEFAULT_GROUP, TOPIC_GROUP_ATTR, TOPIC_NUMBER_ATTR, TOPIC_REF_ATTR, TOPIC_ROMAN_ATTR, blockHash, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, formatTopicNumber, fromRoman, iconSvg, isBlockUntouched, minutaBlockFingerprint, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, toRoman, topicNumberHtml, variableChipHtml, vaultImageHtml, vaultRefOf };
672
+ export type { FootnoteRun, ImageIdentity, MinutaChecklistConflict, MinutaChecklistResult, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, TopicNumberInfo, VariableClickKind };
package/dist/index.mjs CHANGED
@@ -22,8 +22,10 @@ import TableCell from '@tiptap/extension-table-cell';
22
22
  import TableHeader from '@tiptap/extension-table-header';
23
23
  import TableRow from '@tiptap/extension-table-row';
24
24
  import { Plugin } from '@tiptap/pm/state';
25
- import { s as sanitizeRichHtml, T as TOPIC_DEFAULT_GROUP, c as TOPIC_NUMBER_ATTR, r as readTopicNumber, d as TOPIC_GROUP_ATTR, e as TOPIC_ROMAN_ATTR, g as TOPIC_REF_ATTR, a as formatTopicNumber, P as PocPaginationPlugin, h as sanitizeFootnoteHtml, i as computePocBreaks, j as pocPageKey, k as breaksEqual, l as PAGINATE_META } from './shared/minuta-editor.b3351035.mjs';
26
- export { q as buildPocFooterEl, o as buildPocHeaderEl, t as buildPocNotesEl, y as computePocLoneRows, f as footnoteHtmlToRuns, z as footnoteSizePt, C as fromRoman, n as isPocChromeCanvas, v as isPocFlexRow, m as pocChromeFlowHeight, u as pocDescendsInto, p as pocLoneCellIndex, b as pocLooksLikeTitle, w as pocNodeHasVisibleContent, x as pocNodePaintsBox, A as shouldMoveBreakToHeading, B as toRoman, D as topicNumberHtml } from './shared/minuta-editor.b3351035.mjs';
25
+ import { s as sanitizeRichHtml, e as sanitizeFootnoteHtml, g as footnoteSizePt, T as TOPIC_DEFAULT_GROUP, h as TOPIC_NUMBER_ATTR, r as readTopicNumber, i as TOPIC_GROUP_ATTR, j as TOPIC_ROMAN_ATTR, k as TOPIC_REF_ATTR, a as formatTopicNumber, P as PocPaginationPlugin, l as computePocBreaks, m as pocPageKey, n as breaksEqual, o as PAGINATE_META } from './shared/minuta-editor.f1239e0b.mjs';
26
+ export { v as buildPocFooterEl, u as buildPocHeaderEl, w as buildPocNotesEl, z as computePocLoneRows, f as footnoteHtmlToRuns, C as fromRoman, t as isPocChromeCanvas, y as isPocFlexRow, q as pocChromeFlowHeight, x as pocDescendsInto, p as pocLoneCellIndex, b as pocLooksLikeTitle, c as pocNodeHasVisibleContent, d as pocNodePaintsBox, A as shouldMoveBreakToHeading, B as toRoman, D as topicNumberHtml } from './shared/minuta-editor.f1239e0b.mjs';
27
+ import { closeHistory } from '@tiptap/pm/history';
28
+ import { DOMParser } from '@tiptap/pm/model';
27
29
  import '@tiptap/pm/view';
28
30
 
29
31
  const POC_ASSET_SWAP = "pocAssetSwap";
@@ -35,7 +37,18 @@ function htmlToDom(html) {
35
37
  const PocDocument = Node$1.create({
36
38
  name: "doc",
37
39
  topNode: true,
38
- content: "minutaBlock+"
40
+ content: "minutaBlock+",
41
+ addAttributes() {
42
+ return {
43
+ /**
44
+ * Estado do checklist × edição (engine/checklistSync): teses que o
45
+ * checklist queria no texto na última aplicação + conflitos abertos.
46
+ * Vive NO documento para o desfazer do editor voltar junto com o
47
+ * texto; não vai para o HTML (sem documento salvo, nasce do texto).
48
+ */
49
+ checklist: { default: null, rendered: false, parseHTML: () => null }
50
+ };
51
+ }
39
52
  });
40
53
  const PocMinutaBlock = Node$1.create({
41
54
  name: "minutaBlock",
@@ -46,7 +59,17 @@ const PocMinutaBlock = Node$1.create({
46
59
  addAttributes() {
47
60
  return {
48
61
  blockId: { default: null },
49
- blockTitle: { default: null }
62
+ blockTitle: { default: null },
63
+ /**
64
+ * impressão digital do texto GERADO pelo checklist (ver
65
+ * utils/minutaBlockFingerprint no app): enquanto o conteúdo bater
66
+ * com ela o trecho não foi editado e o checklist pode re-gerá-lo
67
+ */
68
+ genHash: {
69
+ default: null,
70
+ parseHTML: (el) => el.getAttribute("data-gen-hash"),
71
+ renderHTML: (attrs) => attrs.genHash ? { "data-gen-hash": attrs.genHash } : {}
72
+ }
50
73
  };
51
74
  },
52
75
  parseHTML() {
@@ -281,6 +304,14 @@ const PocVariableChip = Node$1.create({
281
304
  return {
282
305
  variableId: { default: null },
283
306
  parentBlockId: { default: null },
307
+ /**
308
+ * linha do `<for>` — distingue a mesma variável em linhas diferentes
309
+ * (`data-loop-index` do app; `data-variable-index` da Data API)
310
+ */
311
+ loopIndex: {
312
+ default: null,
313
+ parseHTML: (el) => el.getAttribute("data-loop-index") ?? el.getAttribute("data-variable-index")
314
+ },
284
315
  html: { default: "" },
285
316
  text: { default: "" }
286
317
  };
@@ -292,6 +323,7 @@ const PocVariableChip = Node$1.create({
292
323
  getAttrs: (el) => ({
293
324
  variableId: el.getAttribute("data-variable-id"),
294
325
  parentBlockId: el.getAttribute("data-parent-block-id"),
326
+ loopIndex: el.getAttribute("data-loop-index") ?? el.getAttribute("data-variable-index"),
295
327
  html: el.outerHTML,
296
328
  text: el.textContent ?? ""
297
329
  })
@@ -408,7 +440,9 @@ const PocFootnoteRef = Node$1.create({
408
440
  selectable: false,
409
441
  addAttributes() {
410
442
  return {
411
- number: { default: "" },
443
+ // o Tiptap relê `number="1"` do `<footnote>` e o converte em número —
444
+ // e o render do ProseMirror só aceita texto como conteúdo
445
+ number: { default: "", parseHTML: (el) => el.getAttribute("number") ?? el.textContent ?? "" },
412
446
  /** conteúdo da nota em HTML de ênfase (b/i/u/br) */
413
447
  text: { default: "" },
414
448
  /** corpo declarado no template (`font-size: 9pt`), em pontos */
@@ -424,6 +458,17 @@ const PocFootnoteRef = Node$1.create({
424
458
  text: el.getAttribute("data-footnote") ?? "",
425
459
  size: Number.parseFloat(el.getAttribute("data-footnote-size") ?? "") || null
426
460
  })
461
+ }, {
462
+ // `<footnote>` cru do template (HTML de servidor antigo ou de
463
+ // outra origem): sem esta regra a tag some e o texto da nota
464
+ // cai no meio do parágrafo
465
+ tag: "footnote",
466
+ priority: 110,
467
+ getAttrs: (el) => ({
468
+ number: el.getAttribute("number") ?? "",
469
+ text: sanitizeFootnoteHtml(el.innerHTML.replace(/&nbsp;/g, " ")),
470
+ size: footnoteSizePt(el.getAttribute("style"))
471
+ })
427
472
  }];
428
473
  },
429
474
  renderHTML({ node }) {
@@ -705,7 +750,7 @@ const VARIABLE_TAG_RE = /<span\b[^>]*\bdata-minuta-var="([^"]*)"[^>]*>[^<]*<\/sp
705
750
  function escapeHtml(value) {
706
751
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
707
752
  }
708
- function decodeAttr(value) {
753
+ function decodeAttr$1(value) {
709
754
  return value.replace(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
710
755
  }
711
756
  function extractTemplateVariables(html) {
@@ -713,7 +758,7 @@ function extractTemplateVariables(html) {
713
758
  return [];
714
759
  const keys = [];
715
760
  for (const match of html.matchAll(VARIABLE_TAG_RE)) {
716
- const key = decodeAttr(match[1] ?? "").trim();
761
+ const key = decodeAttr$1(match[1] ?? "").trim();
717
762
  if (key && !keys.includes(key))
718
763
  keys.push(key);
719
764
  }
@@ -724,7 +769,7 @@ function resolveTemplateVariables(html, values) {
724
769
  return { html: "", missing: [] };
725
770
  const missing = [];
726
771
  const resolved = html.replace(VARIABLE_TAG_RE, (tag, rawKey) => {
727
- const key = decodeAttr(rawKey ?? "").trim();
772
+ const key = decodeAttr$1(rawKey ?? "").trim();
728
773
  if (!key)
729
774
  return tag;
730
775
  const value = values?.[key];
@@ -841,6 +886,362 @@ async function refreshVaultImages(editor, resolveUrl, options = {}) {
841
886
  return touched;
842
887
  }
843
888
 
889
+ const IMG_TAG = /<img\b[^>]*>/g;
890
+ const ATTR = (name) => new RegExp(`\\s${name}="([^"]*)"`);
891
+ function decodeAttr(value) {
892
+ return value.replace(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
893
+ }
894
+ function normalizeImgTags(html, identity) {
895
+ if (!html.includes("<img"))
896
+ return html;
897
+ return html.replace(IMG_TAG, (tag) => {
898
+ const vault = tag.match(ATTR("data-vault-src"))?.[1];
899
+ const src = tag.match(ATTR("src"))?.[1] ?? "";
900
+ const id = vault ? decodeAttr(vault) : identity(decodeAttr(src));
901
+ return tag.replace(/\s(?:src|data-vault-src|data-download-url)="[^"]*"/g, "").replace(/^<img\b/, `<img data-img="${id}"`);
902
+ });
903
+ }
904
+ const CHIP_SPAN = /<span\b([^>]*\sdata-variable-id="[^"]*"[^>]*)>[\s\S]*?<\/span>/g;
905
+ function normalizeChipSpans(html) {
906
+ if (!html.includes("data-variable-id"))
907
+ return html;
908
+ return html.replace(CHIP_SPAN, (_all, attrs) => {
909
+ const id = attrs.match(/\sdata-variable-id="([^"]*)"/)?.[1] ?? "";
910
+ const index = attrs.match(/\sdata-(?:loop|variable)-index="([^"]*)"/)?.[1] ?? "";
911
+ return `<var:${id}:${index}>`;
912
+ });
913
+ }
914
+ function normalizeAttrs(attrs, identity, chipSpans = true) {
915
+ if (!attrs)
916
+ return void 0;
917
+ const out = {};
918
+ for (const key of Object.keys(attrs).sort()) {
919
+ if (key === "genHash" || key === "uid")
920
+ continue;
921
+ const value = attrs[key];
922
+ if (key === "src" && typeof value === "string") {
923
+ out[key] = identity(value);
924
+ } else {
925
+ out[key] = typeof value === "string" ? chipSpans ? normalizeChipSpans(normalizeImgTags(value, identity)) : normalizeImgTags(value, identity) : value;
926
+ }
927
+ }
928
+ return out;
929
+ }
930
+ function normalize(node, identity, chipSpans = true) {
931
+ switch (node.type) {
932
+ case "variableChip":
933
+ return ["var", node.attrs?.variableId ?? null];
934
+ case "topicNumber":
935
+ return ["topic", node.attrs?.group ?? null, !!node.attrs?.ref];
936
+ case "footnoteRef":
937
+ return ["note", node.attrs?.text ?? "", node.attrs?.size ?? null];
938
+ case "text":
939
+ return ["text", node.text ?? "", (node.marks ?? []).map((mark) => [mark.type, normalizeAttrs(mark.attrs, identity, chipSpans)])];
940
+ }
941
+ return [node.type, normalizeAttrs(node.attrs, identity, chipSpans), (node.content ?? []).map((child) => normalize(child, identity, chipSpans))];
942
+ }
943
+ function fnv1a(input) {
944
+ let hash = 2166136261;
945
+ for (let i = 0; i < input.length; i++) {
946
+ hash ^= input.charCodeAt(i);
947
+ hash = Math.imul(hash, 16777619);
948
+ }
949
+ return (hash >>> 0).toString(16).padStart(8, "0");
950
+ }
951
+ function minutaBlockFingerprint(blockJson, identity = (src) => src) {
952
+ const body = JSON.stringify((blockJson.content ?? []).map((child) => normalize(child, identity)));
953
+ return `${fnv1a(body)}${body.length.toString(16)}`;
954
+ }
955
+ function minutaBlockFingerprintV1(blockJson, identity = (src) => src) {
956
+ const body = JSON.stringify((blockJson.content ?? []).map((child) => normalize(child, identity, false)));
957
+ return `${fnv1a(body)}${body.length.toString(16)}`;
958
+ }
959
+
960
+ function readChecklist(doc) {
961
+ const stored = doc.attrs.checklist;
962
+ return stored ?? { wanted: [...docBlocks(doc).keys()], pending: [] };
963
+ }
964
+ const defaultImageIdentity = (src) => src;
965
+ function blockHash(node, identity = defaultImageIdentity) {
966
+ return minutaBlockFingerprint(node.toJSON(), identity);
967
+ }
968
+ function isBlockUntouched(node, identity = defaultImageIdentity) {
969
+ const genHash = node.attrs.genHash;
970
+ if (!genHash)
971
+ return false;
972
+ return genHash === blockHash(node, identity) || genHash === minutaBlockFingerprintV1(node.toJSON(), identity);
973
+ }
974
+ function docBlocks(doc) {
975
+ const map = /* @__PURE__ */ new Map();
976
+ doc.forEach((node, offset) => {
977
+ const id = node.attrs.blockId;
978
+ if (node.type.name === "minutaBlock" && id && !map.has(id))
979
+ map.set(id, { pos: offset, node });
980
+ });
981
+ return map;
982
+ }
983
+ function parseGeneratedBlocks(schema, html, identity = defaultImageIdentity) {
984
+ const wrapper = document.createElement("div");
985
+ wrapper.innerHTML = html;
986
+ const doc = DOMParser.fromSchema(schema).parse(wrapper);
987
+ const blocks = [];
988
+ doc.forEach((node) => {
989
+ if (node.type.name === "minutaBlock" && node.attrs.blockId)
990
+ blocks.push(node.type.create({ ...node.attrs, genHash: blockHash(node, identity) }, node.content, node.marks));
991
+ });
992
+ return blocks;
993
+ }
994
+ function stampGeneratedBlocks(editor, identity = defaultImageIdentity) {
995
+ const tr = editor.state.tr;
996
+ let stamped = 0;
997
+ editor.state.doc.forEach((node, offset) => {
998
+ if (node.type.name !== "minutaBlock")
999
+ return;
1000
+ const genHash = blockHash(node, identity);
1001
+ if (node.attrs.genHash !== genHash) {
1002
+ tr.setNodeMarkup(offset, void 0, { ...node.attrs, genHash });
1003
+ stamped++;
1004
+ }
1005
+ });
1006
+ if (tr.docChanged)
1007
+ editor.view.dispatch(tr);
1008
+ return stamped;
1009
+ }
1010
+ function baseChipKey(variableId, parentBlockId) {
1011
+ return `${parentBlockId ?? ""}|${variableId ?? ""}`;
1012
+ }
1013
+ function chipKey(variableId, parentBlockId, loopIndex) {
1014
+ return `${baseChipKey(variableId, parentBlockId)}|${loopIndex ?? ""}`;
1015
+ }
1016
+ function collectChips(html) {
1017
+ const wrapper = document.createElement("div");
1018
+ wrapper.innerHTML = html;
1019
+ const byKey = /* @__PURE__ */ new Map();
1020
+ const byBase = /* @__PURE__ */ new Map();
1021
+ wrapper.querySelectorAll("span[data-variable-id]").forEach((el) => {
1022
+ const variableId = el.getAttribute("data-variable-id");
1023
+ const parentBlockId = el.getAttribute("data-parent-block-id");
1024
+ const loopIndex = el.getAttribute("data-loop-index") ?? el.getAttribute("data-variable-index");
1025
+ const value = { html: el.outerHTML, text: el.textContent ?? "", loopIndex };
1026
+ for (const [map, key] of [[byKey, chipKey(variableId, parentBlockId, loopIndex)], [byBase, baseChipKey(variableId, parentBlockId)]]) {
1027
+ const list = map.get(key) ?? [];
1028
+ list.push(value);
1029
+ map.set(key, list);
1030
+ }
1031
+ });
1032
+ return { byKey, byBase };
1033
+ }
1034
+ function matchByValue(docChips, generated) {
1035
+ const pairs = [];
1036
+ const count = (list, text) => list.filter((t) => t === text).length;
1037
+ const docTexts = docChips.map((chip) => chip.node.attrs.text);
1038
+ const genTexts = generated.map((value) => value.text);
1039
+ const freeDoc = new Set(docChips.keys());
1040
+ const freeGen = new Set(generated.keys());
1041
+ docChips.forEach((chip, i) => {
1042
+ const text = docTexts[i];
1043
+ if (count(docTexts, text) !== 1 || count(genTexts, text) !== 1)
1044
+ return;
1045
+ const j = genTexts.indexOf(text);
1046
+ pairs.push({ ...chip, next: generated[j] });
1047
+ freeDoc.delete(i);
1048
+ freeGen.delete(j);
1049
+ });
1050
+ if (freeDoc.size === 1 && freeGen.size === 1) {
1051
+ const [i] = freeDoc;
1052
+ const [j] = freeGen;
1053
+ pairs.push({ ...docChips[i], next: generated[j] });
1054
+ }
1055
+ return pairs;
1056
+ }
1057
+ function refreshChips(tr, chips) {
1058
+ const inDoc = /* @__PURE__ */ new Map();
1059
+ tr.doc.descendants((node, pos) => {
1060
+ if (node.type.name !== "variableChip")
1061
+ return;
1062
+ const key = chipKey(node.attrs.variableId, node.attrs.parentBlockId, node.attrs.loopIndex);
1063
+ const list = inDoc.get(key) ?? [];
1064
+ list.push({ pos, node });
1065
+ inDoc.set(key, list);
1066
+ });
1067
+ let changed = 0;
1068
+ for (const [key, docChips] of inDoc) {
1069
+ let generated = chips.byKey.get(key);
1070
+ if (!generated?.length && docChips[0].node.attrs.loopIndex == null) {
1071
+ const { variableId, parentBlockId } = docChips[0].node.attrs;
1072
+ generated = chips.byBase.get(baseChipKey(variableId, parentBlockId));
1073
+ }
1074
+ if (!generated?.length)
1075
+ continue;
1076
+ const values = generated;
1077
+ const uniform = values.every((value) => value.html === values[0].html);
1078
+ const pairs = uniform ? docChips.map((chip) => ({ ...chip, next: values[0] })) : matchByValue(docChips, values);
1079
+ for (const { pos, node, next } of pairs) {
1080
+ const loopIndex = uniform ? node.attrs.loopIndex : next.loopIndex ?? node.attrs.loopIndex;
1081
+ if (next.html === node.attrs.html && loopIndex === node.attrs.loopIndex)
1082
+ continue;
1083
+ tr.setNodeMarkup(pos, void 0, { ...node.attrs, html: next.html, text: next.text, loopIndex });
1084
+ if (next.text !== node.attrs.text)
1085
+ changed++;
1086
+ }
1087
+ }
1088
+ return changed;
1089
+ }
1090
+ function refreshIslandChips(tr, chips) {
1091
+ let changed = 0;
1092
+ tr.doc.descendants((node, pos) => {
1093
+ if (node.type.name !== "htmlIsland" || !String(node.attrs.html ?? "").includes("data-variable-id"))
1094
+ return;
1095
+ const tpl = document.createElement("template");
1096
+ tpl.innerHTML = node.attrs.html;
1097
+ let touched = false;
1098
+ tpl.content.querySelectorAll("span[data-variable-id]").forEach((el) => {
1099
+ const key = chipKey(
1100
+ el.getAttribute("data-variable-id"),
1101
+ el.getAttribute("data-parent-block-id"),
1102
+ el.getAttribute("data-loop-index") ?? el.getAttribute("data-variable-index")
1103
+ );
1104
+ const values = chips.byKey.get(key);
1105
+ if (!values?.length || values.some((value) => value.html !== values[0].html))
1106
+ return;
1107
+ if (el.outerHTML !== values[0].html) {
1108
+ if (el.textContent !== values[0].text)
1109
+ changed++;
1110
+ el.outerHTML = values[0].html;
1111
+ touched = true;
1112
+ }
1113
+ });
1114
+ if (touched)
1115
+ tr.setNodeMarkup(pos, void 0, { ...node.attrs, html: tpl.innerHTML });
1116
+ return false;
1117
+ });
1118
+ return changed;
1119
+ }
1120
+ function insertPos(doc, blockId, order) {
1121
+ const present = docBlocks(doc);
1122
+ const index = order.indexOf(blockId);
1123
+ for (let i = index - 1; i >= 0; i--) {
1124
+ const prev = present.get(order[i]);
1125
+ if (prev)
1126
+ return prev.pos + prev.node.nodeSize;
1127
+ }
1128
+ return 0;
1129
+ }
1130
+ function listConflicts(doc) {
1131
+ return readChecklist(doc).pending.map(({ blockId, kind, title }) => ({ blockId, blockTitle: title, kind }));
1132
+ }
1133
+ function applyChecklistToDocument(editor, generatedHtml, options = {}) {
1134
+ const identity = options.imageIdentity ?? defaultImageIdentity;
1135
+ const generated = parseGeneratedBlocks(editor.schema, generatedHtml, identity);
1136
+ const genById = new Map(generated.map((node) => [node.attrs.blockId, node]));
1137
+ const order = generated.map((node) => node.attrs.blockId);
1138
+ const wanted = new Set(order);
1139
+ const stored = readChecklist(editor.state.doc);
1140
+ const before = new Set(stored.wanted);
1141
+ const previousPending = new Map(stored.pending.map((entry) => [entry.blockId, entry]));
1142
+ const pending = /* @__PURE__ */ new Map();
1143
+ const toggled = options.toggled;
1144
+ const tr = editor.state.tr;
1145
+ const result = {
1146
+ inserted: [],
1147
+ removed: [],
1148
+ regenerated: [],
1149
+ variablesUpdated: 0,
1150
+ conflicts: [],
1151
+ toggledWithoutContent: false,
1152
+ lastBlockKept: false
1153
+ };
1154
+ const setConflict = (id, kind, node, title) => {
1155
+ pending.set(id, { blockId: id, kind, title, node: node ? node.toJSON() : null });
1156
+ };
1157
+ const ids = [...order, ...[...docBlocks(tr.doc).keys()].filter((id) => !wanted.has(id))];
1158
+ if (toggled && !ids.includes(toggled.blockId))
1159
+ ids.push(toggled.blockId);
1160
+ for (const id of ids) {
1161
+ const fresh = genById.get(id) ?? null;
1162
+ const explicit = toggled?.blockId === id;
1163
+ const turnedOn = explicit ? toggled.show : !before.has(id) && !!fresh;
1164
+ const turnedOff = explicit ? !toggled.show : before.has(id) && !fresh;
1165
+ const present = docBlocks(tr.doc).get(id);
1166
+ if (!present) {
1167
+ if (turnedOn && fresh) {
1168
+ tr.insert(insertPos(tr.doc, id, order), fresh);
1169
+ result.inserted.push(id);
1170
+ } else if (explicit && toggled.show && !fresh) {
1171
+ result.toggledWithoutContent = true;
1172
+ }
1173
+ continue;
1174
+ }
1175
+ const untouched = isBlockUntouched(present.node, identity);
1176
+ const title = present.node.attrs.blockTitle ?? null;
1177
+ if (!fresh || turnedOff) {
1178
+ if (!turnedOff) {
1179
+ const open = previousPending.get(id);
1180
+ if (!untouched && open?.kind === "remove")
1181
+ pending.set(id, open);
1182
+ continue;
1183
+ }
1184
+ if (tr.doc.childCount <= 1) {
1185
+ result.lastBlockKept = true;
1186
+ continue;
1187
+ }
1188
+ if (untouched) {
1189
+ tr.delete(present.pos, present.pos + present.node.nodeSize);
1190
+ result.removed.push(id);
1191
+ } else {
1192
+ setConflict(id, "remove", null, title);
1193
+ }
1194
+ continue;
1195
+ }
1196
+ if (fresh.attrs.genHash === present.node.attrs.genHash)
1197
+ continue;
1198
+ if (untouched) {
1199
+ tr.replaceWith(present.pos, present.pos + present.node.nodeSize, fresh);
1200
+ result.regenerated.push(id);
1201
+ } else {
1202
+ setConflict(id, "replace", fresh, title);
1203
+ }
1204
+ }
1205
+ const chips = collectChips(generatedHtml);
1206
+ result.variablesUpdated = refreshChips(tr, chips) + refreshIslandChips(tr, chips);
1207
+ const next = { wanted: [...wanted], pending: [...pending.values()] };
1208
+ if (JSON.stringify(next) !== JSON.stringify(stored) || tr.docChanged)
1209
+ tr.setDocAttribute("checklist", next);
1210
+ if (tr.docChanged) {
1211
+ editor.view.dispatch(closeHistory(tr));
1212
+ }
1213
+ result.conflicts = next.pending.map(({ blockId, kind, title }) => ({ blockId, blockTitle: title, kind }));
1214
+ return result;
1215
+ }
1216
+ function resolveChecklistConflict(editor, blockId, resolution) {
1217
+ const stored = readChecklist(editor.state.doc);
1218
+ const entry = stored.pending.find((item) => item.blockId === blockId);
1219
+ const present = docBlocks(editor.state.doc).get(blockId);
1220
+ const tr = editor.state.tr;
1221
+ const rest = { ...stored, pending: stored.pending.filter((item) => item.blockId !== blockId) };
1222
+ if (!entry || !present) {
1223
+ if (entry) {
1224
+ tr.setDocAttribute("checklist", rest);
1225
+ editor.view.dispatch(tr);
1226
+ }
1227
+ return false;
1228
+ }
1229
+ if (resolution === "apply" && entry.kind === "remove" && editor.state.doc.childCount <= 1)
1230
+ return false;
1231
+ const fresh = entry.node ? editor.schema.nodeFromJSON(entry.node) : null;
1232
+ if (resolution === "apply") {
1233
+ if (entry.kind === "remove")
1234
+ tr.delete(present.pos, present.pos + present.node.nodeSize);
1235
+ else if (fresh)
1236
+ tr.replaceWith(present.pos, present.pos + present.node.nodeSize, fresh);
1237
+ } else if (entry.kind === "replace" && fresh) {
1238
+ tr.setNodeMarkup(present.pos, void 0, { ...present.node.attrs, genHash: fresh.attrs.genHash });
1239
+ }
1240
+ tr.setDocAttribute("checklist", rest);
1241
+ editor.view.dispatch(closeHistory(tr));
1242
+ return true;
1243
+ }
1244
+
844
1245
  function h(tag, attrs = {}, children = []) {
845
1246
  const el = document.createElement(tag);
846
1247
  if (attrs.class)
@@ -2144,6 +2545,32 @@ function createMinutaEditor(element, opts) {
2144
2545
  return Promise.resolve(0);
2145
2546
  return refreshVaultImages(ed, resolveUrl, { meta: { [POC_ASSET_SWAP]: true } });
2146
2547
  },
2548
+ applyChecklist(generatedHtml, options) {
2549
+ const ed = tiptap;
2550
+ if (!ed || ed.isDestroyed)
2551
+ throw new Error("editor n\xE3o montado");
2552
+ return applyChecklistToDocument(ed, generatedHtml, options ?? {});
2553
+ },
2554
+ checklistConflicts() {
2555
+ const ed = tiptap;
2556
+ return ed && !ed.isDestroyed ? listConflicts(ed.state.doc) : [];
2557
+ },
2558
+ resolveChecklistConflict(blockId, resolution) {
2559
+ const ed = tiptap;
2560
+ if (!ed || ed.isDestroyed)
2561
+ return false;
2562
+ return resolveChecklistConflict(ed, blockId, resolution);
2563
+ },
2564
+ stampGenerated() {
2565
+ const ed = tiptap;
2566
+ if (!ed || ed.isDestroyed)
2567
+ return 0;
2568
+ return stampGeneratedBlocks(ed);
2569
+ },
2570
+ documentBlockIds() {
2571
+ const ed = tiptap;
2572
+ return ed && !ed.isDestroyed ? [...docBlocks(ed.state.doc).keys()] : [];
2573
+ },
2147
2574
  schedulePaginate,
2148
2575
  refreshChrome,
2149
2576
  totalPages: () => totalPages,
@@ -2163,4 +2590,4 @@ function createMinutaEditor(element, opts) {
2163
2590
  return api;
2164
2591
  }
2165
2592
 
2166
- export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, PAGINATE_META, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPaginationPlugin, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocTopicNumber, PocTopicRenumber, PocUnderline, PocVariableChip, TOPIC_DEFAULT_GROUP, TOPIC_GROUP_ATTR, TOPIC_NUMBER_ATTR, TOPIC_REF_ATTR, TOPIC_ROMAN_ATTR, breaksEqual, buildPocExtensions, collectVaultRefs, computePocBreaks, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, formatTopicNumber, iconSvg, pocPageKey, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
2593
+ export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, PAGINATE_META, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPaginationPlugin, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocTopicNumber, PocTopicRenumber, PocUnderline, PocVariableChip, TOPIC_DEFAULT_GROUP, TOPIC_GROUP_ATTR, TOPIC_NUMBER_ATTR, TOPIC_REF_ATTR, TOPIC_ROMAN_ATTR, blockHash, breaksEqual, buildPocExtensions, collectVaultRefs, computePocBreaks, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteSizePt, formatTopicNumber, iconSvg, isBlockUntouched, minutaBlockFingerprint, pocPageKey, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
@@ -75,84 +75,6 @@ function sanitizeRichHtml(html) {
75
75
  return tpl.innerHTML;
76
76
  }
77
77
 
78
- const TOPIC_NUMBER_ATTR = "data-topic-number";
79
- const TOPIC_GROUP_ATTR = "data-topic-group";
80
- const TOPIC_ROMAN_ATTR = "data-topic-roman";
81
- const TOPIC_REF_ATTR = "data-topic-ref";
82
- const TOPIC_DEFAULT_GROUP = "default";
83
- function toRoman(num) {
84
- if (!Number.isFinite(num) || num <= 0)
85
- return "";
86
- const numerals = [
87
- [1e3, "M"],
88
- [900, "CM"],
89
- [500, "D"],
90
- [400, "CD"],
91
- [100, "C"],
92
- [90, "XC"],
93
- [50, "L"],
94
- [40, "XL"],
95
- [10, "X"],
96
- [9, "IX"],
97
- [5, "V"],
98
- [4, "IV"],
99
- [1, "I"]
100
- ];
101
- let result = "";
102
- let rest = Math.floor(num);
103
- for (const [value, numeral] of numerals) {
104
- while (rest >= value) {
105
- result += numeral;
106
- rest -= value;
107
- }
108
- }
109
- return result;
110
- }
111
- function fromRoman(text) {
112
- const upper = text.trim().toUpperCase();
113
- if (!upper || !/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/.test(upper))
114
- return null;
115
- const values = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1e3 };
116
- let total = 0;
117
- for (let i = 0; i < upper.length; i++) {
118
- const current = values[upper[i]];
119
- const next = values[upper[i + 1] ?? ""] ?? 0;
120
- total += current < next ? -current : current;
121
- }
122
- return total;
123
- }
124
- function formatTopicNumber(number, roman) {
125
- return roman ? toRoman(number) : String(number);
126
- }
127
- function escapeAttr(value) {
128
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
129
- }
130
- function topicNumberHtml(info) {
131
- let attrs = `${TOPIC_NUMBER_ATTR}="${info.number}" ${TOPIC_GROUP_ATTR}="${escapeAttr(info.group || TOPIC_DEFAULT_GROUP)}"`;
132
- if (info.roman)
133
- attrs += ` ${TOPIC_ROMAN_ATTR}="true"`;
134
- if (info.ref)
135
- attrs += ` ${TOPIC_REF_ATTR}="true"`;
136
- return `<span ${attrs}>${formatTopicNumber(info.number, info.roman)}</span>`;
137
- }
138
- function readTopicNumber(el) {
139
- const roman = el.getAttribute(TOPIC_ROMAN_ATTR) === "true";
140
- const raw = el.getAttribute(TOPIC_NUMBER_ATTR) ?? "";
141
- let number = Number.parseInt(raw, 10);
142
- if (!Number.isFinite(number)) {
143
- const text = el.textContent?.trim() ?? "";
144
- number = Number.parseInt(text, 10);
145
- if (!Number.isFinite(number))
146
- number = fromRoman(text) ?? 1;
147
- }
148
- return {
149
- number,
150
- group: el.getAttribute(TOPIC_GROUP_ATTR) || TOPIC_DEFAULT_GROUP,
151
- roman,
152
- ref: el.getAttribute(TOPIC_REF_ATTR) === "true"
153
- };
154
- }
155
-
156
78
  const INLINE_TAGS = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "SUP", "SUB", "BR"]);
157
79
  function safeHref(value) {
158
80
  const href = (value ?? "").trim();
@@ -208,12 +130,12 @@ function sanitizeFootnoteHtml(html) {
208
130
  return style ? `<span style="${style}">${inner}</span>` : inner;
209
131
  }
210
132
  if (node.tagName === "DIV" || node.tagName === "P")
211
- return inner ? `${inner}<br>` : "";
133
+ return inner.trim() ? `${inner}<br>` : "";
212
134
  if (INLINE_TAGS.has(node.tagName))
213
135
  return `<${node.tagName.toLowerCase()}>${inner}</${node.tagName.toLowerCase()}>`;
214
136
  return inner;
215
137
  };
216
- return Array.from(root.childNodes).map(walk).join("").replace(/\s+/g, " ").replace(/(<br>\s*)+$/i, "").trim();
138
+ return Array.from(root.childNodes).map(walk).join("").replace(/\s+/g, " ").replace(/^(\s*<br>)+/i, "").replace(/(<br>\s*)+$/i, "").trim();
217
139
  }
218
140
  function footnoteHtmlToRuns(html) {
219
141
  const runs = [];
@@ -271,6 +193,84 @@ function footnoteSizePt(style) {
271
193
  return Number.isFinite(value) ? value : null;
272
194
  }
273
195
 
196
+ const TOPIC_NUMBER_ATTR = "data-topic-number";
197
+ const TOPIC_GROUP_ATTR = "data-topic-group";
198
+ const TOPIC_ROMAN_ATTR = "data-topic-roman";
199
+ const TOPIC_REF_ATTR = "data-topic-ref";
200
+ const TOPIC_DEFAULT_GROUP = "default";
201
+ function toRoman(num) {
202
+ if (!Number.isFinite(num) || num <= 0)
203
+ return "";
204
+ const numerals = [
205
+ [1e3, "M"],
206
+ [900, "CM"],
207
+ [500, "D"],
208
+ [400, "CD"],
209
+ [100, "C"],
210
+ [90, "XC"],
211
+ [50, "L"],
212
+ [40, "XL"],
213
+ [10, "X"],
214
+ [9, "IX"],
215
+ [5, "V"],
216
+ [4, "IV"],
217
+ [1, "I"]
218
+ ];
219
+ let result = "";
220
+ let rest = Math.floor(num);
221
+ for (const [value, numeral] of numerals) {
222
+ while (rest >= value) {
223
+ result += numeral;
224
+ rest -= value;
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ function fromRoman(text) {
230
+ const upper = text.trim().toUpperCase();
231
+ if (!upper || !/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/.test(upper))
232
+ return null;
233
+ const values = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1e3 };
234
+ let total = 0;
235
+ for (let i = 0; i < upper.length; i++) {
236
+ const current = values[upper[i]];
237
+ const next = values[upper[i + 1] ?? ""] ?? 0;
238
+ total += current < next ? -current : current;
239
+ }
240
+ return total;
241
+ }
242
+ function formatTopicNumber(number, roman) {
243
+ return roman ? toRoman(number) : String(number);
244
+ }
245
+ function escapeAttr(value) {
246
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
247
+ }
248
+ function topicNumberHtml(info) {
249
+ let attrs = `${TOPIC_NUMBER_ATTR}="${info.number}" ${TOPIC_GROUP_ATTR}="${escapeAttr(info.group || TOPIC_DEFAULT_GROUP)}"`;
250
+ if (info.roman)
251
+ attrs += ` ${TOPIC_ROMAN_ATTR}="true"`;
252
+ if (info.ref)
253
+ attrs += ` ${TOPIC_REF_ATTR}="true"`;
254
+ return `<span ${attrs}>${formatTopicNumber(info.number, info.roman)}</span>`;
255
+ }
256
+ function readTopicNumber(el) {
257
+ const roman = el.getAttribute(TOPIC_ROMAN_ATTR) === "true";
258
+ const raw = el.getAttribute(TOPIC_NUMBER_ATTR) ?? "";
259
+ let number = Number.parseInt(raw, 10);
260
+ if (!Number.isFinite(number)) {
261
+ const text = el.textContent?.trim() ?? "";
262
+ number = Number.parseInt(text, 10);
263
+ if (!Number.isFinite(number))
264
+ number = fromRoman(text) ?? 1;
265
+ }
266
+ return {
267
+ number,
268
+ group: el.getAttribute(TOPIC_GROUP_ATTR) || TOPIC_DEFAULT_GROUP,
269
+ roman,
270
+ ref: el.getAttribute(TOPIC_REF_ATTR) === "true"
271
+ };
272
+ }
273
+
274
274
  function shouldMoveBreakToHeading(params) {
275
275
  const { heading, item, pageStart, capacity } = params;
276
276
  if (!heading || !heading.heading || heading.marker)
@@ -897,4 +897,4 @@ function PocPaginationPlugin(cfg) {
897
897
  });
898
898
  }
899
899
 
900
- export { shouldMoveBreakToHeading as A, toRoman as B, fromRoman as C, topicNumberHtml as D, PocPaginationPlugin as P, TOPIC_DEFAULT_GROUP as T, formatTopicNumber as a, pocLooksLikeTitle as b, TOPIC_NUMBER_ATTR as c, TOPIC_GROUP_ATTR as d, TOPIC_ROMAN_ATTR as e, footnoteHtmlToRuns as f, TOPIC_REF_ATTR as g, sanitizeFootnoteHtml as h, computePocBreaks as i, pocPageKey as j, breaksEqual as k, PAGINATE_META as l, pocChromeFlowHeight as m, isPocChromeCanvas as n, buildPocHeaderEl as o, pocLoneCellIndex as p, buildPocFooterEl as q, readTopicNumber as r, sanitizeRichHtml as s, buildPocNotesEl as t, pocDescendsInto as u, isPocFlexRow as v, pocNodeHasVisibleContent as w, pocNodePaintsBox as x, computePocLoneRows as y, footnoteSizePt as z };
900
+ export { shouldMoveBreakToHeading as A, toRoman as B, fromRoman as C, topicNumberHtml as D, PocPaginationPlugin as P, TOPIC_DEFAULT_GROUP as T, formatTopicNumber as a, pocLooksLikeTitle as b, pocNodeHasVisibleContent as c, pocNodePaintsBox as d, sanitizeFootnoteHtml as e, footnoteHtmlToRuns as f, footnoteSizePt as g, TOPIC_NUMBER_ATTR as h, TOPIC_GROUP_ATTR as i, TOPIC_ROMAN_ATTR as j, TOPIC_REF_ATTR as k, computePocBreaks as l, pocPageKey as m, breaksEqual as n, PAGINATE_META as o, pocLoneCellIndex as p, pocChromeFlowHeight as q, readTopicNumber as r, sanitizeRichHtml as s, isPocChromeCanvas as t, buildPocHeaderEl as u, buildPocFooterEl as v, buildPocNotesEl as w, pocDescendsInto as x, isPocFlexRow as y, computePocLoneRows as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-editor",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Editor de minuta do defesa.ai sem framework: folha A4 paginada, edição rica, placeholders de variável e export DOCX fiel ao preview — monta em qualquer app (React, Vue, vanilla)",
5
5
  "repository": {
6
6
  "type": "git",