@markdstage/markdstage 3.3.0 → 3.8.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.
@@ -19,6 +19,9 @@ const NS_REL =
19
19
  const JAPANESE_FONT_FACE = "Yu Gothic";
20
20
  const HUNDREDTH_POINTS_PER_PIXEL = 75;
21
21
  const SLIDE_LAYOUT_ID_BASE = 2147500000;
22
+ const DRAWINGML_ANGLE_UNITS_PER_DEGREE = 60000;
23
+ const DRAWINGML_HALF_TURN = 180 * DRAWINGML_ANGLE_UNITS_PER_DEGREE;
24
+ const DRAWINGML_FULL_TURN = 360 * DRAWINGML_ANGLE_UNITS_PER_DEGREE;
22
25
 
23
26
  const REL = {
24
27
  officeDocument: `${NS_R}/officeDocument`,
@@ -111,6 +114,33 @@ function optionalUnitInterval(value, path, fallback = 1) {
111
114
  return number;
112
115
  }
113
116
 
117
+ function optionalOwnUnitInterval(value, key, path, fallback = 1) {
118
+ if (!Object.hasOwn(value, key)) {
119
+ if (key in value) fail(`${path}.${key} must be an own property`);
120
+ return fallback;
121
+ }
122
+ return optionalUnitInterval(value[key], `${path}.${key}`, fallback);
123
+ }
124
+
125
+ function optionalOwnRotationUnits(value, path) {
126
+ if (!Object.hasOwn(value, "rotation")) {
127
+ if ("rotation" in value) fail(`${path}.rotation must be an own property`);
128
+ return 0;
129
+ }
130
+ const rotation = finiteNumber(value.rotation, `${path}.rotation`);
131
+ let units = Math.round((((rotation % 360) + 360) % 360) * DRAWINGML_ANGLE_UNITS_PER_DEGREE);
132
+ if (units >= DRAWINGML_HALF_TURN) units -= DRAWINGML_FULL_TURN;
133
+ return units === 0 ? 0 : units;
134
+ }
135
+
136
+ function paintOpacities(element, path) {
137
+ return {
138
+ opacity: optionalOwnUnitInterval(element, "opacity", path),
139
+ fillOpacity: optionalOwnUnitInterval(element, "fillOpacity", path),
140
+ strokeOpacity: optionalOwnUnitInterval(element, "strokeOpacity", path),
141
+ };
142
+ }
143
+
114
144
  function boundsOf(value, path) {
115
145
  if (!value || typeof value !== "object" || Array.isArray(value)) {
116
146
  fail(`${path} must be an object`);
@@ -127,8 +157,9 @@ function emu(value) {
127
157
  return Math.round(value * PPTX_DIMENSIONS.emusPerPx);
128
158
  }
129
159
 
130
- function xfrmXml(bounds, tag = "a:xfrm") {
131
- return `<${tag}><a:off x="${emu(bounds.x)}" y="${emu(bounds.y)}"/><a:ext cx="${emu(bounds.width)}" cy="${emu(bounds.height)}"/></${tag}>`;
160
+ function xfrmXml(bounds, tag = "a:xfrm", rotationUnits = 0) {
161
+ const rotation = rotationUnits ? ` rot="${rotationUnits}"` : "";
162
+ return `<${tag}${rotation}><a:off x="${emu(bounds.x)}" y="${emu(bounds.y)}"/><a:ext cx="${emu(bounds.width)}" cy="${emu(bounds.height)}"/></${tag}>`;
132
163
  }
133
164
 
134
165
  function parseChannel(value, path) {
@@ -210,16 +241,25 @@ function colorXml(value, path, opacity = 1) {
210
241
  return color ? `<a:solidFill>${color}</a:solidFill>` : "<a:noFill/>";
211
242
  }
212
243
 
213
- function lineXml(element, path) {
244
+ function lineCapXml(value, path) {
245
+ if (value === undefined) return "";
246
+ const caps = { butt: "flat", round: "rnd", square: "sq" };
247
+ if (typeof value !== "string" || !Object.hasOwn(caps, value)) fail(`${path} is not a supported line cap`);
248
+ return ` cap="${caps[value]}"`;
249
+ }
250
+
251
+ function lineXml(element, path, opacities = paintOpacities(element, path)) {
214
252
  const width = element.strokeWidth === undefined
215
253
  ? 1
216
254
  : positiveNumber(element.strokeWidth, `${path}.strokeWidth`);
217
255
  const color = colorOf(element.stroke, `${path}.stroke`);
218
- if (!color) return `<a:ln w="${emu(width)}"><a:noFill/></a:ln>`;
219
- const opacity = optionalUnitInterval(element.opacity, `${path}.opacity`);
220
- const alpha = Math.round(color.alpha * opacity * 100000);
256
+ const cap = lineCapXml(element.lineCap, `${path}.lineCap`);
257
+ if (!color) return `<a:ln w="${emu(width)}"${cap}><a:noFill/></a:ln>`;
258
+ const alpha = Math.round(
259
+ color.alpha * opacities.opacity * opacities.strokeOpacity * 100000,
260
+ );
221
261
  const dash = dashXml(element.dash, `${path}.dash`);
222
- return `<a:ln w="${emu(width)}"><a:solidFill><a:srgbClr val="${color.hex}">${
262
+ return `<a:ln w="${emu(width)}"${cap}><a:solidFill><a:srgbClr val="${color.hex}">${
223
263
  alpha < 100000 ? `<a:alpha val="${alpha}"/>` : ""
224
264
  }</a:srgbClr></a:solidFill>${dash}</a:ln>`;
225
265
  }
@@ -468,9 +508,9 @@ function runXml(run, path, relationships) {
468
508
  .filter(Boolean)
469
509
  .join(" ");
470
510
  let properties = colorXml(
471
- run.color ?? "#000000",
511
+ run.color === undefined ? "#000000" : run.color,
472
512
  `${path}.color`,
473
- optionalUnitInterval(run.opacity, `${path}.opacity`),
513
+ optionalOwnUnitInterval(run, "opacity", path),
474
514
  );
475
515
  if (run.fontFace !== undefined) {
476
516
  if (typeof run.fontFace !== "string" || !run.fontFace) {
@@ -548,12 +588,13 @@ function textBodyXml(
548
588
  return `<${tag}>${textBodyPropertiesXml(bodyOptions, bodyPath)}<a:lstStyle/>${paragraphs}</${tag}>`;
549
589
  }
550
590
 
551
- function shapeBase(id, name, bounds, properties, text = "") {
552
- return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${xmlEscape(name)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>${xfrmXml(bounds)}${properties}</p:spPr>${text}</p:sp>`;
591
+ function shapeBase(id, name, bounds, properties, text = "", rotationUnits = 0) {
592
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${xmlEscape(name)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>${xfrmXml(bounds, "a:xfrm", rotationUnits)}${properties}</p:spPr>${text}</p:sp>`;
553
593
  }
554
594
 
555
595
  function textShapeXml(element, path, id, relationships) {
556
596
  const bounds = boundsOf(element, path);
597
+ const rotationUnits = optionalOwnRotationUnits(element, path);
557
598
  const text = { paragraphs: element.paragraphs };
558
599
  const paragraphs = Array.isArray(text.paragraphs) ? text.paragraphs : [];
559
600
  const bulletInsetPx = Math.max(
@@ -584,6 +625,7 @@ function textShapeXml(element, path, id, relationships) {
584
625
  path,
585
626
  bulletInsetPx,
586
627
  ),
628
+ rotationUnits,
587
629
  );
588
630
  }
589
631
 
@@ -604,7 +646,8 @@ function shapeTextOf(element, path) {
604
646
 
605
647
  function nativeShapeXml(element, path, id, relationships) {
606
648
  const bounds = boundsOf(element, path);
607
- const preset = {
649
+ const rotationUnits = optionalOwnRotationUnits(element, path);
650
+ const presets = {
608
651
  rect: "rect",
609
652
  roundedRect: "roundRect",
610
653
  ellipse: "ellipse",
@@ -612,18 +655,53 @@ function nativeShapeXml(element, path, id, relationships) {
612
655
  triangle: "triangle",
613
656
  hexagon: "hexagon",
614
657
  parallelogram: "parallelogram",
615
- }[element.shape];
616
- if (!preset) {
658
+ };
659
+ const customGeometries = {
660
+ quarterHeightHexagon: '<a:custGeom><a:avLst/><a:gdLst><a:gd name="dx" fmla="*/ h 1 4"/><a:gd name="rx" fmla="+- w 0 dx"/><a:gd name="cy" fmla="*/ h 1 2"/></a:gdLst><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path><a:moveTo><a:pt x="dx" y="h"/></a:moveTo><a:lnTo><a:pt x="rx" y="h"/></a:lnTo><a:lnTo><a:pt x="w" y="cy"/></a:lnTo><a:lnTo><a:pt x="rx" y="0"/></a:lnTo><a:lnTo><a:pt x="dx" y="0"/></a:lnTo><a:lnTo><a:pt x="0" y="cy"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom>',
661
+ sequenceTab: '<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path w="50000" h="20000"><a:moveTo><a:pt x="0" y="0"/></a:moveTo><a:lnTo><a:pt x="50000" y="0"/></a:lnTo><a:lnTo><a:pt x="50000" y="13000"/></a:lnTo><a:lnTo><a:pt x="41600" y="20000"/></a:lnTo><a:lnTo><a:pt x="0" y="20000"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom>',
662
+ reverseParallelogram: '<a:custGeom><a:avLst/><a:gdLst><a:gd name="dx" fmla="*/ h 1 2"/><a:gd name="rx" fmla="+- w 0 dx"/></a:gdLst><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path><a:moveTo><a:pt x="dx" y="h"/></a:moveTo><a:lnTo><a:pt x="w" y="h"/></a:lnTo><a:lnTo><a:pt x="rx" y="0"/></a:lnTo><a:lnTo><a:pt x="0" y="0"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom>',
663
+ trapezoid: '<a:custGeom><a:avLst/><a:gdLst><a:gd name="dx" fmla="*/ h 1 2"/><a:gd name="rx" fmla="+- w 0 dx"/></a:gdLst><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path><a:moveTo><a:pt x="0" y="h"/></a:moveTo><a:lnTo><a:pt x="w" y="h"/></a:lnTo><a:lnTo><a:pt x="rx" y="0"/></a:lnTo><a:lnTo><a:pt x="dx" y="0"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom>',
664
+ invertedTrapezoid: '<a:custGeom><a:avLst/><a:gdLst><a:gd name="dx" fmla="*/ h 1 2"/><a:gd name="rx" fmla="+- w 0 dx"/></a:gdLst><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path><a:moveTo><a:pt x="dx" y="h"/></a:moveTo><a:lnTo><a:pt x="rx" y="h"/></a:lnTo><a:lnTo><a:pt x="w" y="0"/></a:lnTo><a:lnTo><a:pt x="0" y="0"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom>',
665
+ };
666
+ const adjustedGeometries = {
667
+ stadium: '<a:prstGeom prst="roundRect"><a:avLst><a:gd name="adj" fmla="val 50000"/></a:avLst></a:prstGeom>',
668
+ };
669
+ if (element.shape === "topRoundedRect") {
670
+ const radius = Math.min(nonNegativeNumber(element.cornerRadius === undefined ? 5 : element.cornerRadius, `${path}.cornerRadius`),
671
+ bounds.width / 2, bounds.height / 2);
672
+ const r = Math.round(radius * 9525);
673
+ // This bounded preset retains Mermaid's two quadratic top corners and square bottom.
674
+ customGeometries.topRoundedRect = `<a:custGeom><a:avLst/><a:gdLst><a:gd name="r" fmla="val ${r}"/><a:gd name="rx" fmla="+- w 0 r"/></a:gdLst><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path><a:moveTo><a:pt x="0" y="h"/></a:moveTo><a:lnTo><a:pt x="0" y="r"/></a:lnTo><a:quadBezTo><a:pt x="0" y="0"/><a:pt x="r" y="0"/></a:quadBezTo><a:lnTo><a:pt x="rx" y="0"/></a:lnTo><a:quadBezTo><a:pt x="w" y="0"/><a:pt x="w" y="r"/></a:quadBezTo><a:lnTo><a:pt x="w" y="h"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom>`;
675
+ }
676
+ if (element.shape === "roundedRect" && element.cornerRadius !== undefined) {
677
+ const radius = nonNegativeNumber(element.cornerRadius, `${path}.cornerRadius`);
678
+ const adjustment = Math.round(Math.min(50000,
679
+ radius / Math.min(bounds.width, bounds.height) * 100000));
680
+ adjustedGeometries.roundedRect =
681
+ `<a:prstGeom prst="roundRect"><a:avLst><a:gd name="adj" fmla="val ${adjustment}"/></a:avLst></a:prstGeom>`;
682
+ }
683
+ const shape = Object.hasOwn(element, "shape") ? element.shape : undefined;
684
+ const preset = typeof shape === "string" && Object.hasOwn(presets, shape) ? presets[shape] : "";
685
+ const customGeometry = typeof shape === "string" && Object.hasOwn(customGeometries, shape)
686
+ ? customGeometries[shape]
687
+ : "";
688
+ const adjustedGeometry = typeof shape === "string" && Object.hasOwn(adjustedGeometries, shape)
689
+ ? adjustedGeometries[shape]
690
+ : "";
691
+ const geometry = adjustedGeometry || customGeometry || (preset
692
+ ? `<a:prstGeom prst="${preset}"><a:avLst/></a:prstGeom>`
693
+ : "");
694
+ if (!geometry) {
617
695
  fail(
618
- `${path}.shape must be rect, roundedRect, ellipse, diamond, triangle, hexagon, or parallelogram`,
696
+ `${path}.shape must be rect, roundedRect, topRoundedRect, stadium, ellipse, diamond, triangle, hexagon, quarterHeightHexagon, parallelogram, reverseParallelogram, trapezoid, invertedTrapezoid, or sequenceTab`,
619
697
  );
620
698
  }
621
- const opacity = optionalUnitInterval(element.opacity, `${path}.opacity`);
622
- const properties = `${xfrmXml(bounds)}<a:prstGeom prst="${preset}"><a:avLst/></a:prstGeom>${colorXml(
699
+ const opacities = paintOpacities(element, path);
700
+ const properties = `${xfrmXml(bounds, "a:xfrm", rotationUnits)}${geometry}${colorXml(
623
701
  element.fill,
624
702
  `${path}.fill`,
625
- opacity,
626
- )}${lineXml(element, path)}`;
703
+ opacities.opacity * opacities.fillOpacity,
704
+ )}${lineXml(element, path, opacities)}`;
627
705
  const shapeText = shapeTextOf(element, path);
628
706
  if (!shapeText) textBodyPropertiesXml(element, path);
629
707
  const text = shapeText
@@ -741,9 +819,12 @@ function connectorXml(element, path, nextId, relationships) {
741
819
  : positiveNumber(element.strokeWidth, `${path}.strokeWidth`);
742
820
  const color = colorOf(element.stroke ?? "#000000", `${path}.stroke`);
743
821
  if (!color) fail(`${path}.stroke cannot be null`);
744
- const opacity = optionalUnitInterval(element.opacity, `${path}.opacity`);
745
- const alpha = Math.round(color.alpha * opacity * 100000);
822
+ const opacities = paintOpacities(element, path);
823
+ const alpha = Math.round(
824
+ color.alpha * opacities.opacity * opacities.strokeOpacity * 100000,
825
+ );
746
826
  const dash = dashXml(element.dash, `${path}.dash`);
827
+ const cap = lineCapXml(element.lineCap, `${path}.lineCap`);
747
828
  const shapes = [];
748
829
  for (let index = 0; index < points.length - 1; index += 1) {
749
830
  const start = points[index];
@@ -762,7 +843,7 @@ function connectorXml(element, path, nextId, relationships) {
762
843
  ? arrowXml(element.arrowEnd, `${path}.arrowEnd`)
763
844
  : "";
764
845
  shapes.push(
765
- `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="Connector ${id}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm${flipH}${flipV}><a:off x="${emu(x)}" y="${emu(y)}"/><a:ext cx="${emu(Math.abs(end.x - start.x))}" cy="${emu(Math.abs(end.y - start.y))}"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:ln w="${emu(width)}"><a:solidFill><a:srgbClr val="${color.hex}">${
846
+ `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="Connector ${id}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm${flipH}${flipV}><a:off x="${emu(x)}" y="${emu(y)}"/><a:ext cx="${emu(Math.abs(end.x - start.x))}" cy="${emu(Math.abs(end.y - start.y))}"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:ln w="${emu(width)}"${cap}><a:solidFill><a:srgbClr val="${color.hex}">${
766
847
  alpha < 100000 ? `<a:alpha val="${alpha}"/>` : ""
767
848
  }</a:srgbClr></a:solidFill>${dash}${head}${tail}</a:ln></p:spPr></p:sp>`,
768
849
  );