@meistrari/minuta-editor 1.0.1 → 1.0.2

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.
@@ -70,6 +70,8 @@ function parseStyle(style) {
70
70
  if (bg && bg.toLowerCase() !== "ffffff")
71
71
  info.highlight = bg;
72
72
  info.size = cssSizeToHalfPoints(getProp(style, "font-size"));
73
+ if (getProp(style, "text-transform") === "uppercase")
74
+ info.caps = true;
73
75
  const family = getProp(style, "font-family");
74
76
  if (family)
75
77
  info.font = family.split(",")[0].trim().replace(/["']/g, "");
@@ -123,6 +125,7 @@ function measureEl(el) {
123
125
  color: cssColorToHex(cs.color),
124
126
  bold: (Number.parseInt(cs.fontWeight) || 400) >= 600,
125
127
  italics: cs.fontStyle.includes("italic"),
128
+ caps: cs.textTransform === "uppercase",
126
129
  align: cssAlignToDocx(cs.textAlign)
127
130
  };
128
131
  }
@@ -145,6 +148,7 @@ function runOptions(info) {
145
148
  italics: info.italics,
146
149
  underline: info.underline ? {} : void 0,
147
150
  strike: info.strike,
151
+ allCaps: info.caps || void 0,
148
152
  color: info.color,
149
153
  size: info.size,
150
154
  font: info.font,
@@ -165,6 +169,22 @@ function primaryFont(cs) {
165
169
  const family = cs.fontFamily?.split(",")[0]?.trim().replace(/["']/g, "");
166
170
  return family || void 0;
167
171
  }
172
+ function withMarks(base, marks) {
173
+ const info = { ...base };
174
+ for (const mark of marks) {
175
+ if (mark.type.name === "bold")
176
+ info.bold = true;
177
+ if (mark.type.name === "italic")
178
+ info.italics = true;
179
+ if (mark.type.name === "underline")
180
+ info.underline = true;
181
+ if (mark.type.name === "strike")
182
+ info.strike = true;
183
+ if (mark.type.name === "inlineStyle")
184
+ Object.assign(info, cleanUndefined(parseStyle(mark.attrs.style)));
185
+ }
186
+ return info;
187
+ }
168
188
  function inlineToRuns(node, base, ctx) {
169
189
  const runs = [];
170
190
  node.forEach((child) => {
@@ -186,7 +206,7 @@ function inlineToRuns(node, base, ctx) {
186
206
  }
187
207
  if (child.type.name === "variableChip") {
188
208
  if (child.attrs.text)
189
- runs.push(new TextRun({ text: child.attrs.text, ...runOptions(base) }));
209
+ runs.push(new TextRun({ text: child.attrs.text, ...runOptions(withMarks(base, child.marks)) }));
190
210
  return;
191
211
  }
192
212
  if (child.type.name === "hardBreak") {
@@ -245,6 +265,7 @@ function domInlineToRuns(root, fallback = {}) {
245
265
  italics: cs.fontStyle.includes("italic") || void 0,
246
266
  underline: cs.textDecorationLine?.includes("underline") ? {} : void 0,
247
267
  strike: cs.textDecorationLine?.includes("line-through") || void 0,
268
+ allCaps: cs.textTransform === "uppercase" || void 0,
248
269
  characterSpacing: TRACKING_COMPENSATION_TW
249
270
  }));
250
271
  }
@@ -296,7 +317,7 @@ function spacingFrom(m, style) {
296
317
  function textBlockToParagraph(node, pos, ctx, opts = {}) {
297
318
  const m = measureAt(ctx, pos);
298
319
  const style = node.attrs.style;
299
- const base = m ? { bold: m.bold, italics: m.italics, color: m.color, size: m.sizeHalf, font: primaryFont(m.cs) } : parseStyle(style);
320
+ const base = m ? { bold: m.bold, italics: m.italics, caps: m.caps, color: m.color, size: m.sizeHalf, font: primaryFont(m.cs) } : parseStyle(style);
300
321
  if (ctx.extraTrackingTw)
301
322
  base.trackingTw = ctx.extraTrackingTw;
302
323
  const ownIndent = m ? m.marginLeftTw + Math.round((Number.parseFloat(m.cs.paddingLeft) || 0) * PX_TO_TWIP) : cssLengthToTwips(getProp(style, "margin-left")) ?? 0;
@@ -652,6 +673,66 @@ function isFlexRow(node) {
652
673
  return false;
653
674
  return node.childCount > 1;
654
675
  }
676
+ function islandImage(node) {
677
+ if (node.type.name !== "htmlIsland")
678
+ return null;
679
+ const tpl = document.createElement("template");
680
+ tpl.innerHTML = node.attrs.html || "";
681
+ const el = tpl.content.firstElementChild;
682
+ if (!el)
683
+ return null;
684
+ const img = el.tagName === "IMG" ? el : el.querySelector("img");
685
+ return img instanceof HTMLImageElement ? img : null;
686
+ }
687
+ function collectImageIslands(node, pos) {
688
+ if (islandImage(node))
689
+ return [{ node, pos }];
690
+ if (node.type.name !== "styledDiv")
691
+ return null;
692
+ const out = [];
693
+ let childPos = pos + 1;
694
+ for (let i = 0; i < node.childCount; i++) {
695
+ const child = node.child(i);
696
+ if (!(child.isTextblock && child.content.size === 0)) {
697
+ const inner = collectImageIslands(child, childPos);
698
+ if (!inner)
699
+ return null;
700
+ out.push(...inner);
701
+ }
702
+ childPos += child.nodeSize;
703
+ }
704
+ return out.length ? out : null;
705
+ }
706
+ async function inlineImagesParagraph(islands, innerPx, ctx) {
707
+ const GAP_PX = 8;
708
+ const items = [];
709
+ for (const { node, pos } of islands) {
710
+ const img = islandImage(node);
711
+ const src = await srcToDataUrl(img.getAttribute("src") || "");
712
+ if (!src)
713
+ continue;
714
+ const live = domAt(ctx, pos)?.querySelector("img");
715
+ const styleW = Number.parseFloat(getProp(img.getAttribute("style"), "width") ?? "");
716
+ const widthPx = live?.offsetWidth || 0 || (Number.isFinite(styleW) && styleW > 0 ? styleW : 0) || Math.min(innerPx, 200);
717
+ items.push({ src, widthPx });
718
+ }
719
+ if (!items.length)
720
+ return null;
721
+ const total = items.reduce((a, b) => a + b.widthPx, 0) + GAP_PX * (items.length - 1);
722
+ const scale = innerPx > 0 && total > innerPx ? (innerPx - GAP_PX * (items.length - 1)) / (total - GAP_PX * (items.length - 1)) : 1;
723
+ const runs = [];
724
+ for (let i = 0; i < items.length; i++) {
725
+ const run = await dataUrlToImageRun(items[i].src, Math.max(1, Math.round(items[i].widthPx * scale)));
726
+ if (!run)
727
+ continue;
728
+ if (runs.length)
729
+ runs.push(new TextRun({ text: " " }));
730
+ runs.push(run);
731
+ }
732
+ if (!runs.length)
733
+ return null;
734
+ return new Paragraph({ children: runs, alignment: AlignmentType.CENTER, spacing: { after: 0, before: 0 } });
735
+ }
655
736
  async function tableToDocx(node, pos, contentWidthPx, ctx) {
656
737
  const tableDomRaw = domAt(ctx, pos);
657
738
  const tableEl = tableDomRaw?.tagName === "TABLE" ? tableDomRaw : tableDomRaw?.querySelector("table") ?? null;
@@ -681,9 +762,36 @@ async function tableToDocx(node, pos, contentWidthPx, ctx) {
681
762
  }
682
763
  }
683
764
  const cellCtx = { ...ctx, insideCell: true, indentLeft: 0, lastAfterTw: 0, pending: { breakBefore: false }, extraTrackingTw: extraTrackingTw || void 0 };
765
+ const cellInnerPx = cellM?.el ? cellM.el.clientWidth - (Number.parseFloat(cellM.cs.paddingLeft) || 0) - (Number.parseFloat(cellM.cs.paddingRight) || 0) : contentWidthPx / 2;
766
+ const isEmptyBlock = (n) => n.isTextblock && n.content.size === 0;
767
+ const besideImage = (k) => k > 0 && !!collectImageIslands(cellNode.child(k - 1), 0) || k + 1 < cellNode.childCount && !!collectImageIslands(cellNode.child(k + 1), 0);
684
768
  let childPos = cellPos + 1;
685
769
  for (let k = 0; k < cellNode.childCount; k++) {
686
770
  const child = cellNode.child(k);
771
+ if (isEmptyBlock(child) && besideImage(k)) {
772
+ childPos += child.nodeSize;
773
+ continue;
774
+ }
775
+ const islands = collectImageIslands(child, childPos);
776
+ if (islands?.length) {
777
+ let end = k + 1;
778
+ let endPos = childPos + child.nodeSize;
779
+ while (end < cellNode.childCount) {
780
+ const next = cellNode.child(end);
781
+ const more = collectImageIslands(next, endPos);
782
+ if (!more?.length)
783
+ break;
784
+ islands.push(...more);
785
+ endPos += next.nodeSize;
786
+ end++;
787
+ }
788
+ const para = await inlineImagesParagraph(islands, cellInnerPx, cellCtx);
789
+ if (para)
790
+ cellContent.push(para);
791
+ childPos = endPos;
792
+ k = end - 1;
793
+ continue;
794
+ }
687
795
  if (child.isTextblock)
688
796
  cellContent.push(textBlockToParagraph(child, childPos, cellCtx));
689
797
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-editor",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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",