@meistrari/minuta-editor 1.3.1 → 1.5.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.
@@ -1,11 +1,75 @@
1
- import { f as footnoteHtmlToRuns, p as pocLoneCellIndex, a as pocLooksLikeTitle } from '../shared/minuta-editor.d9d0c0ce.mjs';
2
- import { BorderStyle, TextRun, ExternalHyperlink, Paragraph, Document, Header, Packer, AlignmentType, LineRuleType, Table, TableRow, TableCell, WidthType, TableLayoutType, Footer, FootnoteReferenceRun, VerticalAlign, ImageRun, HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, TextWrappingType, FrameAnchorType, PageNumber, HeadingLevel } from 'docx';
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
+ 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';
5
5
 
6
6
  const A4 = { width: 11906, height: 16838 };
7
7
  const PX_TO_TWIP = 15;
8
8
  const PX_PER_PT = 96 / 72;
9
+ function topicSequenceFor(ctx, group, roman, number) {
10
+ const key = group || TOPIC_DEFAULT_GROUP;
11
+ let seq = ctx.topicSequences.get(key);
12
+ if (!seq || seq.roman !== roman || seq.next !== number) {
13
+ const reference = `cdb-topic-${ctx.topicNumbering.size + 1}`;
14
+ ctx.topicNumbering.set(reference, { roman, start: number });
15
+ seq = { reference, roman, next: number };
16
+ ctx.topicSequences.set(key, seq);
17
+ }
18
+ seq.next = number + 1;
19
+ return seq.reference;
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
+ }
63
+ function leadingTopicNumber(node) {
64
+ for (let i = 0; i < node.childCount; i++) {
65
+ const child = node.child(i);
66
+ if (child.isText && !child.text?.trim())
67
+ continue;
68
+ const number = Number(child.attrs.number);
69
+ return child.type.name === "topicNumber" && !child.attrs.ref && Number.isInteger(number) && number >= 1 ? child : null;
70
+ }
71
+ return null;
72
+ }
9
73
  function getProp(style, prop) {
10
74
  if (!style)
11
75
  return void 0;
@@ -185,9 +249,15 @@ function withMarks(base, marks) {
185
249
  }
186
250
  return info;
187
251
  }
188
- function inlineToRuns(node, base, ctx) {
252
+ function inlineToRuns(node, base, ctx, skip) {
189
253
  const runs = [];
190
254
  node.forEach((child) => {
255
+ if (skip && child === skip)
256
+ return;
257
+ if (child.type.name === "topicNumber") {
258
+ runs.push(new TextRun({ text: formatTopicNumber(child.attrs.number, child.attrs.roman), ...runOptions(withMarks(base, child.marks)) }));
259
+ return;
260
+ }
191
261
  if (child.type.name === "footnoteRef") {
192
262
  const text2 = String(child.attrs.text ?? "").trim();
193
263
  if (ctx && text2) {
@@ -327,10 +397,18 @@ function textBlockToParagraph(node, pos, ctx, opts = {}) {
327
397
  if (spacing.before && !breakBefore)
328
398
  spacing.before = Math.max(0, spacing.before - ctx.lastAfterTw);
329
399
  ctx.lastAfterTw = spacing.after ?? 0;
330
- const runs = inlineToRuns(node, base, ctx);
400
+ const topic = leadingTopicNumber(node);
401
+ let numbering;
402
+ if (topic) {
403
+ const reference = topicSequenceFor(ctx, topic.attrs.group, Boolean(topic.attrs.roman), Number(topic.attrs.number));
404
+ numbering = { reference, level: 0, custom: true };
405
+ }
406
+ const runs = inlineToRuns(node, base, ctx, topic);
331
407
  const keepNext = Boolean(opts.heading || m && pocLooksLikeTitle(m.el)) || void 0;
332
408
  const paragraph = new Paragraph({
333
409
  children: runs,
410
+ numbering,
411
+ run: numbering ? runOptions(topic ? withMarks(base, topic.marks) : base) : void 0,
334
412
  // sem alinhamento explícito, corpo de peça vai justificado (o Word
335
413
  // usaria esquerda e o .docx sairia diferente da praxe e do preview)
336
414
  alignment: m?.align ?? parseAlignment(style) ?? (opts.heading ? void 0 : AlignmentType.JUSTIFIED),
@@ -340,7 +418,7 @@ function textBlockToParagraph(node, pos, ctx, opts = {}) {
340
418
  pageBreakBefore: breakBefore,
341
419
  keepNext
342
420
  });
343
- ctx.meta.set(paragraph, { spacer: runs.length === 0 && !opts.heading, breakBefore });
421
+ ctx.meta.set(paragraph, { spacer: runs.length === 0 && !numbering && !opts.heading, breakBefore });
344
422
  return paragraph;
345
423
  }
346
424
  async function srcToDataUrl(src) {
@@ -430,20 +508,28 @@ async function islandToDocx(node, pos, contentWidthPx, ctx) {
430
508
  const paragraphs = [];
431
509
  const liveLis = islandEl ? [...islandEl.querySelectorAll(":scope > ul > li, :scope > ol > li, :scope > li")] : [];
432
510
  const parsedLis = [...el.querySelectorAll(":scope > li")];
433
- let n = 1;
511
+ let reference = null;
434
512
  for (let i = 0; i < parsedLis.length; i++) {
435
513
  const live = liveLis[i] ?? null;
436
514
  const source = live ?? parsedLis[i];
437
515
  const m = live ? measureEl(live) : null;
438
- const marker = tag === "ol" ? `${n++}. ` : "\u2022 ";
439
- 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
+ }
440
525
  const runs = live ? domInlineToRuns(live) : [new TextRun({ text: source.textContent?.trim() || "", ...runOptions(parseStyle(source.getAttribute("style"))) })];
441
526
  const indentPx = live && islandEl ? live.offsetLeft : 24;
442
527
  paragraphs.push(new Paragraph({
443
528
  children: [
444
- new TextRun({ text: marker, color: markerColor, size: m?.sizeHalf, font: m ? primaryFont(m.cs) : void 0 }),
529
+ ...tag === "ul" ? [new TextRun({ text: "\u2022 ", ...markerRun })] : [],
445
530
  ...runs
446
531
  ],
532
+ numbering: reference ? { reference, level: 0 } : void 0,
447
533
  spacing: m ? { after: m.afterTw, before: m.beforeTw, ...m.lineExactTw ? { line: m.lineExactTw, lineRule: LineRuleType.EXACT } : {} } : { after: 80 },
448
534
  indent: { left: Math.round(indentPx * PX_TO_TWIP) },
449
535
  alignment: m?.align,
@@ -658,6 +744,27 @@ async function flexRowToDocx(node, pos, contentWidthPx, ctx) {
658
744
  }
659
745
  return tables;
660
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
+ }
661
768
  function isPlainContainer(node) {
662
769
  return node.type.name === "styledDiv" && !isFlexRow(node);
663
770
  }
@@ -894,6 +1001,18 @@ async function nodeToDocx(node, pos, contentWidthPx, ctx) {
894
1001
  }
895
1002
  case "styledDiv": {
896
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
+ }
897
1016
  const loneIndex = pocLoneCellIndex(node);
898
1017
  if (loneIndex != null) {
899
1018
  let childPos = pos + 1;
@@ -976,31 +1095,41 @@ async function nodeToDocx(node, pos, contentWidthPx, ctx) {
976
1095
  case "bulletList":
977
1096
  case "orderedList": {
978
1097
  const paragraphs = [];
979
- let n = 1;
1098
+ const ordered = node.type.name === "orderedList";
1099
+ let reference = null;
980
1100
  let liPos = pos + 1;
981
1101
  for (let i = 0; i < node.childCount; i++) {
982
1102
  const li = node.child(i);
983
1103
  let childPos = liPos + 1;
1104
+ let marked = false;
984
1105
  li.forEach((child) => {
985
1106
  if (child.isTextblock) {
986
1107
  const m = measureAt(ctx, childPos);
987
1108
  const base = m ? { bold: m.bold, italics: m.italics, color: m.color, size: m.sizeHalf } : parseStyle(child.attrs.style);
988
- const marker = node.type.name === "orderedList" ? `${n}. ` : "\u2022 ";
989
1109
  const liDom = domAt(ctx, liPos);
990
1110
  const indentPx = liDom?.offsetLeft ?? 48;
991
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) })] : [];
992
1120
  const liParagraph = new Paragraph({
993
- 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,
994
1123
  spacing: spacingFrom(m, child.attrs.style),
995
1124
  indent: { left: Math.round(indentPx * PX_TO_TWIP) },
996
1125
  pageBreakBefore: liBreak
997
1126
  });
1127
+ marked = true;
998
1128
  ctx.meta.set(liParagraph, { breakBefore: liBreak });
999
1129
  paragraphs.push(liParagraph);
1000
1130
  }
1001
1131
  childPos += child.nodeSize;
1002
1132
  });
1003
- n++;
1004
1133
  liPos += li.nodeSize;
1005
1134
  }
1006
1135
  return paragraphs;
@@ -1317,7 +1446,10 @@ async function pocExportDocx(editor, margins, chrome, options = {}) {
1317
1446
  pending: { breakBefore: false },
1318
1447
  lastAfterTw: 0,
1319
1448
  meta: /* @__PURE__ */ new WeakMap(),
1320
- footnotes: []
1449
+ footnotes: [],
1450
+ topicNumbering: /* @__PURE__ */ new Map(),
1451
+ topicSequences: /* @__PURE__ */ new Map(),
1452
+ listNumbering: /* @__PURE__ */ new Map()
1321
1453
  };
1322
1454
  const children = [];
1323
1455
  let pos = 0;
@@ -1366,6 +1498,39 @@ async function pocExportDocx(editor, margins, chrome, options = {}) {
1366
1498
  }
1367
1499
  },
1368
1500
  footnotes: Object.keys(footnotes).length ? footnotes : void 0,
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
+ ]
1533
+ } : void 0,
1369
1534
  sections: [{
1370
1535
  properties: {
1371
1536
  page: {
package/dist/docx.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { pocExportDocx } from './chunks/docxExport.mjs';
2
- import './shared/minuta-editor.d9d0c0ce.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
@@ -17,12 +17,12 @@
17
17
  /// <reference types="@tiptap/extension-text" />
18
18
  /// <reference types="@tiptap/extension-table" />
19
19
  /// <reference types="@tiptap/extension-bubble-menu" />
20
- import { Node, Editor, Mark, Extension } from '@tiptap/core';
20
+ import { Node, Editor, Extension, Mark } from '@tiptap/core';
21
+ import { Transaction } from '@tiptap/pm/state';
21
22
  import { EditorView } from '@tiptap/pm/view';
22
23
  import { P as PocPageConfig } from './shared/minuta-editor.9e65d163.mjs';
23
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';
24
25
  import '@tiptap/pm/model';
25
- import '@tiptap/pm/state';
26
26
 
27
27
  /**
28
28
  * Variáveis de template da minuta — o formato do placeholder e a resolução dele.
@@ -162,6 +162,29 @@ declare const PocTemplateVariable: Node<{
162
162
  declare const PocFootnoteRef: Node<any, any>;
163
163
  /** re-sequencia a numeração das chamadas de nota na ordem do documento */
164
164
  declare function renumberPocFootnotes(editor: Editor): void;
165
+ /**
166
+ * Número de tópico/parágrafo — o `<topic group="paragrafo"/>` do template já
167
+ * resolvido pelo render. É NÓ, e não texto, por dois motivos: quem edita a
168
+ * minuta apaga, move ou insere um parágrafo numerado e a sequência do grupo se
169
+ * refaz sozinha (`PocTopicRenumber`), e o export leva o número como numeração
170
+ * nativa do Word, que também se refaz quando o advogado apaga um parágrafo lá.
171
+ * Contrato do marcador em `utils/topicNumber.ts`.
172
+ */
173
+ declare const PocTopicNumber: Node<any, any>;
174
+ /**
175
+ * Re-sequencia os números de tópico na ordem do documento, por grupo, dentro
176
+ * da transação dada. Devolve se mudou algo. Um nó `ref` (latest) recebe o
177
+ * último número já contado do grupo — a mesma regra do render.
178
+ */
179
+ declare function renumberPocTopicsIn(tr: Transaction): boolean;
180
+ /** re-sequencia a numeração dos tópicos no editor dado (uso pontual; o plugin faz isso a cada mudança) */
181
+ declare function renumberPocTopics(editor: Editor): void;
182
+ /**
183
+ * A cada transação que muda o documento, refaz a sequência dos tópicos. Só
184
+ * despacha quando algum número de fato mudou, então digitar dentro de um
185
+ * parágrafo não custa nada além da varredura.
186
+ */
187
+ declare const PocTopicRenumber: Extension<any, any>;
165
188
  declare const PocStyledDiv: Node<any, any>;
166
189
  declare const PocInlineStyle: Mark<any, any>;
167
190
  declare const PocUnderline: Mark<any, any>;
@@ -181,7 +204,7 @@ declare function buildPocExtensions(pageConfig?: PocPageConfig, options?: {
181
204
  * ainda no editor — o seletor de variável — depende disso.
182
205
  */
183
206
  handleKeyDown?: (view: EditorView, event: KeyboardEvent) => boolean;
184
- }): (Node<any, any> | Mark<any, any> | Extension<any, any>)[];
207
+ }): (Node<any, any> | Extension<any, any> | Mark<any, any>)[];
185
208
 
186
209
  /**
187
210
  * O editor de minuta como coisa montável em qualquer lugar: `createMinutaEditor(el, opts)`.
@@ -480,6 +503,40 @@ declare function refreshVaultImages(editor: Editor, resolveUrl: (vaultUrl: strin
480
503
  */
481
504
  declare function sanitizeRichHtml(html: string | null | undefined): string;
482
505
 
506
+ /**
507
+ * Número de tópico/parágrafo — o `<topic group="paragrafo"/>` dos templates.
508
+ *
509
+ * O render resolve o número, mas em vez de achatá-lo em texto ("12") o guarda
510
+ * num `<span data-topic-number>`: no editor ele vira o nó `topicNumber`, que se
511
+ * re-sequencia sozinho quando um parágrafo é apagado, movido ou inserido, e no
512
+ * DOCX vira numeração nativa do Word (que também se refaz quando o advogado
513
+ * apaga um parágrafo lá). Este é o contrato do marcador — server, client e
514
+ * pacote falam por ele.
515
+ */
516
+ declare const TOPIC_NUMBER_ATTR = "data-topic-number";
517
+ declare const TOPIC_GROUP_ATTR = "data-topic-group";
518
+ declare const TOPIC_ROMAN_ATTR = "data-topic-roman";
519
+ declare const TOPIC_REF_ATTR = "data-topic-ref";
520
+ declare const TOPIC_DEFAULT_GROUP = "default";
521
+ interface TopicNumberInfo {
522
+ /** valor numérico (o que se conta) */
523
+ number: number;
524
+ /** grupo do contador (`group` da tag); sem grupo = `default` */
525
+ group: string;
526
+ /** exibido em algarismos romanos maiúsculos */
527
+ roman: boolean;
528
+ /** `<topic latest="true">`: repete o último número do grupo em vez de avançar */
529
+ ref: boolean;
530
+ }
531
+ declare function toRoman(num: number): string;
532
+ declare function fromRoman(text: string): number | null;
533
+ /** texto exibido para o número (decimal ou romano) */
534
+ declare function formatTopicNumber(number: number, roman: boolean): string;
535
+ /** o marcador em HTML — é o que o render emite e o que o editor lê e grava */
536
+ declare function topicNumberHtml(info: TopicNumberInfo): string;
537
+ /** lê o marcador de um elemento (`span[data-topic-number]`) */
538
+ declare function readTopicNumber(el: Element): TopicNumberInfo;
539
+
483
540
  /**
484
541
  * A tag de imagem que o editor insere após o upload: guarda o `src` presignado
485
542
  * (funciona agora) E o `data-vault-src` durável (`vault://…`). Quem re-resolve
@@ -504,5 +561,5 @@ declare function collectVaultRefs(html: string): string[];
504
561
  /** aplica as URLs resolvidas às `<img>` do trecho; tags sem resolução ficam como estão */
505
562
  declare function renewVaultImageTags(html: string, resolved: Map<string, string>): string;
506
563
 
507
- 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, PocUnderline, PocVariableChip, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, iconSvg, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
508
- export type { FootnoteRun, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, VariableClickKind };
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 };
package/dist/index.d.ts CHANGED
@@ -17,12 +17,12 @@
17
17
  /// <reference types="@tiptap/extension-text" />
18
18
  /// <reference types="@tiptap/extension-table" />
19
19
  /// <reference types="@tiptap/extension-bubble-menu" />
20
- import { Node, Editor, Mark, Extension } from '@tiptap/core';
20
+ import { Node, Editor, Extension, Mark } from '@tiptap/core';
21
+ import { Transaction } from '@tiptap/pm/state';
21
22
  import { EditorView } from '@tiptap/pm/view';
22
23
  import { P as PocPageConfig } from './shared/minuta-editor.9e65d163.js';
23
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';
24
25
  import '@tiptap/pm/model';
25
- import '@tiptap/pm/state';
26
26
 
27
27
  /**
28
28
  * Variáveis de template da minuta — o formato do placeholder e a resolução dele.
@@ -162,6 +162,29 @@ declare const PocTemplateVariable: Node<{
162
162
  declare const PocFootnoteRef: Node<any, any>;
163
163
  /** re-sequencia a numeração das chamadas de nota na ordem do documento */
164
164
  declare function renumberPocFootnotes(editor: Editor): void;
165
+ /**
166
+ * Número de tópico/parágrafo — o `<topic group="paragrafo"/>` do template já
167
+ * resolvido pelo render. É NÓ, e não texto, por dois motivos: quem edita a
168
+ * minuta apaga, move ou insere um parágrafo numerado e a sequência do grupo se
169
+ * refaz sozinha (`PocTopicRenumber`), e o export leva o número como numeração
170
+ * nativa do Word, que também se refaz quando o advogado apaga um parágrafo lá.
171
+ * Contrato do marcador em `utils/topicNumber.ts`.
172
+ */
173
+ declare const PocTopicNumber: Node<any, any>;
174
+ /**
175
+ * Re-sequencia os números de tópico na ordem do documento, por grupo, dentro
176
+ * da transação dada. Devolve se mudou algo. Um nó `ref` (latest) recebe o
177
+ * último número já contado do grupo — a mesma regra do render.
178
+ */
179
+ declare function renumberPocTopicsIn(tr: Transaction): boolean;
180
+ /** re-sequencia a numeração dos tópicos no editor dado (uso pontual; o plugin faz isso a cada mudança) */
181
+ declare function renumberPocTopics(editor: Editor): void;
182
+ /**
183
+ * A cada transação que muda o documento, refaz a sequência dos tópicos. Só
184
+ * despacha quando algum número de fato mudou, então digitar dentro de um
185
+ * parágrafo não custa nada além da varredura.
186
+ */
187
+ declare const PocTopicRenumber: Extension<any, any>;
165
188
  declare const PocStyledDiv: Node<any, any>;
166
189
  declare const PocInlineStyle: Mark<any, any>;
167
190
  declare const PocUnderline: Mark<any, any>;
@@ -181,7 +204,7 @@ declare function buildPocExtensions(pageConfig?: PocPageConfig, options?: {
181
204
  * ainda no editor — o seletor de variável — depende disso.
182
205
  */
183
206
  handleKeyDown?: (view: EditorView, event: KeyboardEvent) => boolean;
184
- }): (Node<any, any> | Mark<any, any> | Extension<any, any>)[];
207
+ }): (Node<any, any> | Extension<any, any> | Mark<any, any>)[];
185
208
 
186
209
  /**
187
210
  * O editor de minuta como coisa montável em qualquer lugar: `createMinutaEditor(el, opts)`.
@@ -480,6 +503,40 @@ declare function refreshVaultImages(editor: Editor, resolveUrl: (vaultUrl: strin
480
503
  */
481
504
  declare function sanitizeRichHtml(html: string | null | undefined): string;
482
505
 
506
+ /**
507
+ * Número de tópico/parágrafo — o `<topic group="paragrafo"/>` dos templates.
508
+ *
509
+ * O render resolve o número, mas em vez de achatá-lo em texto ("12") o guarda
510
+ * num `<span data-topic-number>`: no editor ele vira o nó `topicNumber`, que se
511
+ * re-sequencia sozinho quando um parágrafo é apagado, movido ou inserido, e no
512
+ * DOCX vira numeração nativa do Word (que também se refaz quando o advogado
513
+ * apaga um parágrafo lá). Este é o contrato do marcador — server, client e
514
+ * pacote falam por ele.
515
+ */
516
+ declare const TOPIC_NUMBER_ATTR = "data-topic-number";
517
+ declare const TOPIC_GROUP_ATTR = "data-topic-group";
518
+ declare const TOPIC_ROMAN_ATTR = "data-topic-roman";
519
+ declare const TOPIC_REF_ATTR = "data-topic-ref";
520
+ declare const TOPIC_DEFAULT_GROUP = "default";
521
+ interface TopicNumberInfo {
522
+ /** valor numérico (o que se conta) */
523
+ number: number;
524
+ /** grupo do contador (`group` da tag); sem grupo = `default` */
525
+ group: string;
526
+ /** exibido em algarismos romanos maiúsculos */
527
+ roman: boolean;
528
+ /** `<topic latest="true">`: repete o último número do grupo em vez de avançar */
529
+ ref: boolean;
530
+ }
531
+ declare function toRoman(num: number): string;
532
+ declare function fromRoman(text: string): number | null;
533
+ /** texto exibido para o número (decimal ou romano) */
534
+ declare function formatTopicNumber(number: number, roman: boolean): string;
535
+ /** o marcador em HTML — é o que o render emite e o que o editor lê e grava */
536
+ declare function topicNumberHtml(info: TopicNumberInfo): string;
537
+ /** lê o marcador de um elemento (`span[data-topic-number]`) */
538
+ declare function readTopicNumber(el: Element): TopicNumberInfo;
539
+
483
540
  /**
484
541
  * A tag de imagem que o editor insere após o upload: guarda o `src` presignado
485
542
  * (funciona agora) E o `data-vault-src` durável (`vault://…`). Quem re-resolve
@@ -504,5 +561,5 @@ declare function collectVaultRefs(html: string): string[];
504
561
  /** aplica as URLs resolvidas às `<img>` do trecho; tags sem resolução ficam como estão */
505
562
  declare function renewVaultImageTags(html: string, resolved: Map<string, string>): string;
506
563
 
507
- 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, PocUnderline, PocVariableChip, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, iconSvg, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
508
- export type { FootnoteRun, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, VariableClickKind };
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 };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Node as Node$1, mergeAttributes, Mark, Extension, Editor } from '@tiptap/core';
1
+ import { Node as Node$1, mergeAttributes, Extension, Mark, Editor } from '@tiptap/core';
2
2
  import BubbleMenu from '@tiptap/extension-bubble-menu';
3
3
  import Bold from '@tiptap/extension-bold';
4
4
  import Blockquote from '@tiptap/extension-blockquote';
@@ -22,8 +22,8 @@ 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, P as PocPaginationPlugin, b as sanitizeFootnoteHtml, c as computePocBreaks, d as pocPageKey, e as breaksEqual, g as PAGINATE_META } from './shared/minuta-editor.d9d0c0ce.mjs';
26
- export { k as buildPocFooterEl, j as buildPocHeaderEl, l as buildPocNotesEl, r as computePocLoneRows, f as footnoteHtmlToRuns, t as footnoteSizePt, i as isPocChromeCanvas, n as isPocFlexRow, h as pocChromeFlowHeight, m as pocDescendsInto, p as pocLoneCellIndex, a as pocLooksLikeTitle, o as pocNodeHasVisibleContent, q as pocNodePaintsBox, u as shouldMoveBreakToHeading } from './shared/minuta-editor.d9d0c0ce.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
27
  import '@tiptap/pm/view';
28
28
 
29
29
  const POC_ASSET_SWAP = "pocAssetSwap";
@@ -46,7 +46,17 @@ const PocMinutaBlock = Node$1.create({
46
46
  addAttributes() {
47
47
  return {
48
48
  blockId: { default: null },
49
- blockTitle: { default: null }
49
+ blockTitle: { default: null },
50
+ /**
51
+ * impressão digital do texto GERADO pelo checklist (ver
52
+ * utils/minutaBlockFingerprint no app): enquanto o conteúdo bater
53
+ * com ela o trecho não foi editado e o checklist pode re-gerá-lo
54
+ */
55
+ genHash: {
56
+ default: null,
57
+ parseHTML: (el) => el.getAttribute("data-gen-hash"),
58
+ renderHTML: (attrs) => attrs.genHash ? { "data-gen-hash": attrs.genHash } : {}
59
+ }
50
60
  };
51
61
  },
52
62
  parseHTML() {
@@ -408,7 +418,9 @@ const PocFootnoteRef = Node$1.create({
408
418
  selectable: false,
409
419
  addAttributes() {
410
420
  return {
411
- number: { default: "" },
421
+ // o Tiptap relê `number="1"` do `<footnote>` e o converte em número —
422
+ // e o render do ProseMirror só aceita texto como conteúdo
423
+ number: { default: "", parseHTML: (el) => el.getAttribute("number") ?? el.textContent ?? "" },
412
424
  /** conteúdo da nota em HTML de ênfase (b/i/u/br) */
413
425
  text: { default: "" },
414
426
  /** corpo declarado no template (`font-size: 9pt`), em pontos */
@@ -424,6 +436,17 @@ const PocFootnoteRef = Node$1.create({
424
436
  text: el.getAttribute("data-footnote") ?? "",
425
437
  size: Number.parseFloat(el.getAttribute("data-footnote-size") ?? "") || null
426
438
  })
439
+ }, {
440
+ // `<footnote>` cru do template (HTML de servidor antigo ou de
441
+ // outra origem): sem esta regra a tag some e o texto da nota
442
+ // cai no meio do parágrafo
443
+ tag: "footnote",
444
+ priority: 110,
445
+ getAttrs: (el) => ({
446
+ number: el.getAttribute("number") ?? "",
447
+ text: sanitizeFootnoteHtml(el.innerHTML.replace(/&nbsp;/g, " ")),
448
+ size: footnoteSizePt(el.getAttribute("style"))
449
+ })
427
450
  }];
428
451
  },
429
452
  renderHTML({ node }) {
@@ -472,6 +495,76 @@ function renumberPocFootnotes(editor) {
472
495
  if (touched)
473
496
  editor.view.dispatch(tr);
474
497
  }
498
+ const PocTopicNumber = Node$1.create({
499
+ name: "topicNumber",
500
+ inline: true,
501
+ group: "inline",
502
+ atom: true,
503
+ selectable: false,
504
+ addAttributes() {
505
+ return {
506
+ number: { default: 1 },
507
+ group: { default: TOPIC_DEFAULT_GROUP },
508
+ roman: { default: false },
509
+ /** `<topic latest="true">`: repete o último número do grupo em vez de avançar */
510
+ ref: { default: false }
511
+ };
512
+ },
513
+ parseHTML() {
514
+ return [{
515
+ tag: `span[${TOPIC_NUMBER_ATTR}]`,
516
+ priority: 110,
517
+ getAttrs: (el) => readTopicNumber(el)
518
+ }];
519
+ },
520
+ renderHTML({ node }) {
521
+ const attrs = {
522
+ [TOPIC_NUMBER_ATTR]: String(node.attrs.number),
523
+ [TOPIC_GROUP_ATTR]: node.attrs.group || TOPIC_DEFAULT_GROUP,
524
+ class: "poc-topic-number"
525
+ };
526
+ if (node.attrs.roman)
527
+ attrs[TOPIC_ROMAN_ATTR] = "true";
528
+ if (node.attrs.ref)
529
+ attrs[TOPIC_REF_ATTR] = "true";
530
+ return ["span", attrs, formatTopicNumber(node.attrs.number, node.attrs.roman)];
531
+ }
532
+ });
533
+ function renumberPocTopicsIn(tr) {
534
+ const counters = {};
535
+ let touched = false;
536
+ tr.doc.descendants((node, pos) => {
537
+ if (node.type.name !== "topicNumber")
538
+ return;
539
+ const group = node.attrs.group || TOPIC_DEFAULT_GROUP;
540
+ const number = node.attrs.ref ? counters[group] ?? 0 : counters[group] = (counters[group] ?? 0) + 1;
541
+ if (node.attrs.number !== number) {
542
+ tr.setNodeMarkup(pos, void 0, { ...node.attrs, number });
543
+ touched = true;
544
+ }
545
+ });
546
+ return touched;
547
+ }
548
+ function renumberPocTopics(editor) {
549
+ if (!editor || editor.isDestroyed)
550
+ return;
551
+ const tr = editor.state.tr;
552
+ if (renumberPocTopicsIn(tr))
553
+ editor.view.dispatch(tr);
554
+ }
555
+ const PocTopicRenumber = Extension.create({
556
+ name: "pocTopicRenumber",
557
+ addProseMirrorPlugins() {
558
+ return [new Plugin({
559
+ appendTransaction(transactions, _oldState, newState) {
560
+ if (!transactions.some((t) => t.docChanged))
561
+ return null;
562
+ const tr = newState.tr;
563
+ return renumberPocTopicsIn(tr) ? tr : null;
564
+ }
565
+ })];
566
+ }
567
+ });
475
568
  const PocStyledDiv = Node$1.create({
476
569
  name: "styledDiv",
477
570
  group: "block",
@@ -601,6 +694,8 @@ function buildPocExtensions(pageConfig, options) {
601
694
  PocVariableChip,
602
695
  PocTemplateVariable.configure({ catalog: options?.variables ?? [] }),
603
696
  PocFootnoteRef,
697
+ PocTopicNumber,
698
+ PocTopicRenumber,
604
699
  PocStyledDiv,
605
700
  PocInlineStyle,
606
701
  PocUnderline,
@@ -2091,4 +2186,4 @@ function createMinutaEditor(element, opts) {
2091
2186
  return api;
2092
2187
  }
2093
2188
 
2094
- 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, PocUnderline, PocVariableChip, breaksEqual, buildPocExtensions, collectVaultRefs, computePocBreaks, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, iconSvg, pocPageKey, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
2189
+ 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, footnoteSizePt, formatTopicNumber, iconSvg, pocPageKey, readTopicNumber, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, renumberPocTopics, renumberPocTopicsIn, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
@@ -130,12 +130,12 @@ function sanitizeFootnoteHtml(html) {
130
130
  return style ? `<span style="${style}">${inner}</span>` : inner;
131
131
  }
132
132
  if (node.tagName === "DIV" || node.tagName === "P")
133
- return inner ? `${inner}<br>` : "";
133
+ return inner.trim() ? `${inner}<br>` : "";
134
134
  if (INLINE_TAGS.has(node.tagName))
135
135
  return `<${node.tagName.toLowerCase()}>${inner}</${node.tagName.toLowerCase()}>`;
136
136
  return inner;
137
137
  };
138
- 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();
139
139
  }
140
140
  function footnoteHtmlToRuns(html) {
141
141
  const runs = [];
@@ -193,6 +193,84 @@ function footnoteSizePt(style) {
193
193
  return Number.isFinite(value) ? value : null;
194
194
  }
195
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
+
196
274
  function shouldMoveBreakToHeading(params) {
197
275
  const { heading, item, pageStart, capacity } = params;
198
276
  if (!heading || !heading.heading || heading.marker)
@@ -819,4 +897,4 @@ function PocPaginationPlugin(cfg) {
819
897
  });
820
898
  }
821
899
 
822
- export { PocPaginationPlugin as P, pocLooksLikeTitle as a, sanitizeFootnoteHtml as b, computePocBreaks as c, pocPageKey as d, breaksEqual as e, footnoteHtmlToRuns as f, PAGINATE_META as g, pocChromeFlowHeight as h, isPocChromeCanvas as i, buildPocHeaderEl as j, buildPocFooterEl as k, buildPocNotesEl as l, pocDescendsInto as m, isPocFlexRow as n, pocNodeHasVisibleContent as o, pocLoneCellIndex as p, pocNodePaintsBox as q, computePocLoneRows as r, sanitizeRichHtml as s, footnoteSizePt as t, shouldMoveBreakToHeading as u };
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.3.1",
3
+ "version": "1.5.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",