@meistrari/minuta-editor 1.4.0 → 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,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.mjs CHANGED
@@ -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, 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
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 }) {
@@ -2163,4 +2186,4 @@ function createMinutaEditor(element, opts) {
2163
2186
  return api;
2164
2187
  }
2165
2188
 
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 };
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 };
@@ -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.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",