@markdstage/markdstage 2.5.0 → 2.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdstage/markdstage",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
5
5
  "license": "MIT",
6
6
  "author": "runceel",
@@ -940,6 +940,60 @@ function textContentBounds(element, deck) {
940
940
  };
941
941
  }
942
942
 
943
+ function textInsetsFor(element) {
944
+ const style = getComputedStyle(element);
945
+ const metric = (padding, border) => {
946
+ const paddingValue = Number.parseFloat(style[padding]);
947
+ const borderValue = Number.parseFloat(style[border]);
948
+ return roundedMetric(
949
+ Math.max(0, Number.isFinite(paddingValue) ? paddingValue : 0) +
950
+ Math.max(0, Number.isFinite(borderValue) ? borderValue : 0),
951
+ );
952
+ };
953
+ const insets = {
954
+ left: metric("paddingLeft", "borderLeftWidth"),
955
+ top: metric("paddingTop", "borderTopWidth"),
956
+ right: metric("paddingRight", "borderRightWidth"),
957
+ bottom: metric("paddingBottom", "borderBottomWidth"),
958
+ };
959
+ return Object.values(insets).some((value) => value > 0) ? insets : null;
960
+ }
961
+
962
+ function singleLineTextLayout(element, deck, bounds, textInsets, alignment) {
963
+ const content = textContentBounds(element, deck);
964
+ const fontSize = Number.parseFloat(getComputedStyle(element).fontSize);
965
+ const widthBuffer = Math.max(2, (Number.isFinite(fontSize) ? fontSize : 0) * 0.5);
966
+ const fittedInsets = textInsets ? { ...textInsets } : null;
967
+ const requiredWidth = () =>
968
+ content.width +
969
+ (fittedInsets?.left || 0) +
970
+ (fittedInsets?.right || 0) +
971
+ widthBuffer;
972
+ const resolvedAlignment =
973
+ alignment || pptxAlignment(getComputedStyle(element).textAlign);
974
+ // Keep the leading CSS inset, but release a trailing inset when browser and
975
+ // PowerPoint font metrics would otherwise force a visually single line to wrap.
976
+ if (fittedInsets && requiredWidth() > bounds.width) {
977
+ if (resolvedAlignment === "right") fittedInsets.left = 0;
978
+ if (resolvedAlignment === "left") fittedInsets.right = 0;
979
+ }
980
+ const extraWidth = Math.max(0, requiredWidth() - bounds.width);
981
+ const offset =
982
+ resolvedAlignment === "right"
983
+ ? extraWidth
984
+ : resolvedAlignment === "center"
985
+ ? extraWidth / 2
986
+ : 0;
987
+ return {
988
+ bounds: {
989
+ ...bounds,
990
+ x: roundedMetric(bounds.x - offset),
991
+ width: roundedMetric(bounds.width + extraWidth),
992
+ },
993
+ textInsets: fittedInsets,
994
+ };
995
+ }
996
+
943
997
  function rasterImageSupported(image) {
944
998
  const source = image.currentSrc || image.getAttribute("src") || "";
945
999
  if (["cover", "none"].includes(getComputedStyle(image).objectFit)) return false;
@@ -1028,12 +1082,22 @@ function paragraphFor(element, options = {}) {
1028
1082
  function renderedTextLineCount(element) {
1029
1083
  const range = document.createRange();
1030
1084
  range.selectNodeContents(element);
1031
- const tops = [];
1085
+ const lines = [];
1032
1086
  for (const rect of range.getClientRects()) {
1033
1087
  if (rect.width <= 0 || rect.height <= 0) continue;
1034
- if (!tops.some((top) => Math.abs(top - rect.top) < 1)) tops.push(rect.top);
1088
+ const line = lines.find(
1089
+ (candidate) =>
1090
+ rect.top < candidate.bottom - 1 &&
1091
+ rect.bottom > candidate.top + 1,
1092
+ );
1093
+ if (line) {
1094
+ line.top = Math.min(line.top, rect.top);
1095
+ line.bottom = Math.max(line.bottom, rect.bottom);
1096
+ } else {
1097
+ lines.push({ top: rect.top, bottom: rect.bottom });
1098
+ }
1035
1099
  }
1036
- return Math.max(1, tops.length);
1100
+ return Math.max(1, lines.length);
1037
1101
  }
1038
1102
 
1039
1103
  function unsupportedEffects(element) {
@@ -1587,17 +1651,29 @@ async function collectPptxSlide(slide, index) {
1587
1651
  : undefined,
1588
1652
  });
1589
1653
  if (!paragraph.runs.some((run) => run.text.trim())) continue;
1590
- const disableTextWrap =
1591
- element.classList.contains("kicker") ||
1592
- (element.matches("h1, h2, .slide-title") && renderedTextLineCount(element) === 1);
1654
+ const disableTextWrap = renderedTextLineCount(element) === 1;
1655
+ const textInsets = textInsetsFor(element);
1656
+ const baseBounds = element.classList.contains("kicker")
1657
+ ? textContentBounds(element, deck)
1658
+ : relativeBounds(element, deck);
1659
+ const singleLineLayout = disableTextWrap
1660
+ ? singleLineTextLayout(
1661
+ element,
1662
+ deck,
1663
+ baseBounds,
1664
+ textInsets,
1665
+ element.classList.contains("kicker") ? "left" : undefined,
1666
+ )
1667
+ : null;
1668
+ const bounds = singleLineLayout?.bounds || baseBounds;
1669
+ const fittedTextInsets = singleLineLayout?.textInsets || textInsets;
1593
1670
  elements.push({
1594
1671
  type: "text",
1595
1672
  path: elementPath(element, deck),
1596
- ...(element.classList.contains("kicker")
1597
- ? textContentBounds(element, deck)
1598
- : relativeBounds(element, deck)),
1673
+ ...bounds,
1599
1674
  paragraphs: [paragraph],
1600
1675
  opacity: Number(getComputedStyle(element).opacity) || 1,
1676
+ ...(fittedTextInsets ? { textInsets: fittedTextInsets } : {}),
1601
1677
  ...(disableTextWrap ? { textWrap: "none" } : {}),
1602
1678
  });
1603
1679
  element.setAttribute("data-pptx-native", "text");