@bendyline/squisq 2.13.14 → 2.13.16
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/dist/{Doc-BhvotZx_.d.ts → Doc-DLfTam0R.d.ts} +82 -6
- package/dist/{ImageEditDoc-D8hYio6b.d.ts → ImageEditDoc-DdSHb6sj.d.ts} +1 -1
- package/dist/{chunk-YQQWE4HG.js → chunk-33CAD575.js} +200 -262
- package/dist/{chunk-L7QZORLB.js → chunk-37RLFDIT.js} +11 -4
- package/dist/chunk-4R3YZTPX.js +42 -0
- package/dist/{chunk-DBVCSVNV.js → chunk-A4ZFYGU6.js} +13 -38
- package/dist/{chunk-CTT4XAGO.js → chunk-BG2OYBGC.js} +2534 -737
- package/dist/{chunk-2SRF5III.js → chunk-BVL3F64S.js} +1 -1
- package/dist/chunk-C7DKMO4H.js +88 -0
- package/dist/{chunk-SGNWO7OF.js → chunk-MA7XUCSQ.js} +517 -30
- package/dist/{chunk-JSGIWW35.js → chunk-MTIR5PWF.js} +1 -1
- package/dist/{chunk-C4SB3NN4.js → chunk-NBDF6SQ5.js} +22 -2
- package/dist/chunk-TOPEP5B7.js +515 -0
- package/dist/{chunk-OIQ2GO2O.js → chunk-TXN23SG2.js} +1 -1
- package/dist/{chunk-7MS26OXD.js → chunk-UDPPWYYM.js} +6 -0
- package/dist/{chunk-J2G6EOEJ.js → chunk-XY5Z23C4.js} +75 -16
- package/dist/{chunk-5DFFVW75.js → chunk-Z73DG6IL.js} +1 -1
- package/dist/doc/index.d.ts +412 -26
- package/dist/doc/index.js +66 -11
- package/dist/fence/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/imageEdit/index.d.ts +3 -3
- package/dist/index.d.ts +9 -9
- package/dist/index.js +147 -29
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/jsonForm/index.js +5 -4
- package/dist/markdown/index.js +3 -3
- package/dist/{materializePageSection-CWxou4mX.d.ts → materializePageSection-HIoRYiZu.d.ts} +121 -8
- package/dist/mediaEdit/index.d.ts +154 -3
- package/dist/mediaEdit/index.js +241 -2
- package/dist/narration/index.d.ts +1 -1
- package/dist/narration/index.js +7 -5
- package/dist/recommend/index.d.ts +129 -2
- package/dist/recommend/index.js +13 -4
- package/dist/schemas/index.d.ts +26 -7
- package/dist/schemas/index.js +14 -8
- package/dist/{themeLibrary-BIYE6aLw.d.ts → themeLibrary-B2K-aAOy.d.ts} +1 -1
- package/dist/timing/index.d.ts +66 -2
- package/dist/timing/index.js +12 -0
- package/dist/transform/index.d.ts +173 -16
- package/dist/transform/index.js +58 -17
- package/package.json +1 -1
- package/dist/chunk-FCB6JLWB.js +0 -125
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
detectAsciiDiagram,
|
|
8
8
|
detectAsciiTimeline,
|
|
9
9
|
detectTree,
|
|
10
|
+
extractConceptItems,
|
|
10
11
|
extractEmbeddedVideos,
|
|
11
12
|
extractHostedVideos,
|
|
12
13
|
extractImages,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
isPictureFence,
|
|
25
26
|
isPictureParagraph,
|
|
26
27
|
markdownBlockSeparatorLines,
|
|
28
|
+
matchNumberHighlight,
|
|
27
29
|
parseAnnotationInner,
|
|
28
30
|
parseAsciiDiagram,
|
|
29
31
|
parseAsciiTimeline,
|
|
@@ -31,8 +33,12 @@ import {
|
|
|
31
33
|
pickAutoTemplate,
|
|
32
34
|
profileBlockContents,
|
|
33
35
|
renderMarkdownBlocksHtml,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
splitConceptItem,
|
|
37
|
+
splitStatItem,
|
|
38
|
+
timelineItems,
|
|
39
|
+
treeToTemplateData,
|
|
40
|
+
vennOverlapIndex
|
|
41
|
+
} from "./chunk-MA7XUCSQ.js";
|
|
36
42
|
import {
|
|
37
43
|
anchorPoint,
|
|
38
44
|
base64ToUtf8,
|
|
@@ -51,6 +57,8 @@ import {
|
|
|
51
57
|
createTemplateContext,
|
|
52
58
|
getTwoColumnPositions,
|
|
53
59
|
getViewportOrientation,
|
|
60
|
+
hexHueDegrees,
|
|
61
|
+
isHex,
|
|
54
62
|
isPersistentLayerTemplate,
|
|
55
63
|
isTemplateBlock,
|
|
56
64
|
oklchDarken,
|
|
@@ -63,11 +71,20 @@ import {
|
|
|
63
71
|
validateTheme,
|
|
64
72
|
withAlpha
|
|
65
73
|
} from "./chunk-JNUNNQ74.js";
|
|
74
|
+
import {
|
|
75
|
+
taskMarker
|
|
76
|
+
} from "./chunk-4R3YZTPX.js";
|
|
77
|
+
import {
|
|
78
|
+
createMediaTimeMap
|
|
79
|
+
} from "./chunk-MTIR5PWF.js";
|
|
80
|
+
import {
|
|
81
|
+
normalizeMediaCuts
|
|
82
|
+
} from "./chunk-37RLFDIT.js";
|
|
66
83
|
import {
|
|
67
84
|
TABLE_FED_TEMPLATES,
|
|
68
85
|
isContainerTemplate,
|
|
69
86
|
resolveTemplateName
|
|
70
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-UDPPWYYM.js";
|
|
71
88
|
import {
|
|
72
89
|
KNOWN_BLOCK_META_KEYS,
|
|
73
90
|
coerceAnnotationValues,
|
|
@@ -629,8 +646,8 @@ var FALLBACK_COLOR_SCHEME = {
|
|
|
629
646
|
function resolveColorScheme(context, name) {
|
|
630
647
|
const schemes = context.theme.colorSchemes;
|
|
631
648
|
if (name && name in schemes) return schemes[name];
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
649
|
+
const keys2 = Object.keys(schemes);
|
|
650
|
+
if (keys2.length > 0) return schemes[keys2[0]];
|
|
634
651
|
return FALLBACK_COLOR_SCHEME;
|
|
635
652
|
}
|
|
636
653
|
var FALLBACK_BODY = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
@@ -722,8 +739,8 @@ function getTemplateHint(context, templateName, key, fallback) {
|
|
|
722
739
|
// src/doc/templates/captionUtils.ts
|
|
723
740
|
var TRAILING_PUNCT_RE = /[.;:!?]+$/;
|
|
724
741
|
var CAPTION_BASELINE_SHIFT_EM = 0.055;
|
|
725
|
-
function cleanCaption(
|
|
726
|
-
return
|
|
742
|
+
function cleanCaption(text2) {
|
|
743
|
+
return text2.replace(TRAILING_PUNCT_RE, "").trim();
|
|
727
744
|
}
|
|
728
745
|
function createBackgroundLayer(id, fill) {
|
|
729
746
|
return {
|
|
@@ -733,19 +750,19 @@ function createBackgroundLayer(id, fill) {
|
|
|
733
750
|
position: { x: 0, y: 0, width: "100%", height: "100%" }
|
|
734
751
|
};
|
|
735
752
|
}
|
|
736
|
-
function estimateTextHeight(
|
|
753
|
+
function estimateTextHeight(text2, fontSizePx, maxWidthPx, lineHeight) {
|
|
737
754
|
const avgCharWidth = fontSizePx * 0.52;
|
|
738
755
|
const charsPerLine = Math.max(8, Math.floor(maxWidthPx / avgCharWidth));
|
|
739
|
-
const lines = Math.max(1, Math.ceil(
|
|
756
|
+
const lines = Math.max(1, Math.ceil(text2.length / charsPerLine));
|
|
740
757
|
return lines * fontSizePx * lineHeight;
|
|
741
758
|
}
|
|
742
|
-
function estimateWrappedLineCount(
|
|
743
|
-
if (!
|
|
759
|
+
function estimateWrappedLineCount(text2, fontSize, maxWidth) {
|
|
760
|
+
if (!text2.trim()) return 1;
|
|
744
761
|
const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
|
|
745
762
|
if (charsPerLine <= 0) return 1;
|
|
746
763
|
let lineCount = 0;
|
|
747
764
|
let currentLineLength = 0;
|
|
748
|
-
for (const word of
|
|
765
|
+
for (const word of text2.split(/\s+/)) {
|
|
749
766
|
const testLineLength = currentLineLength ? currentLineLength + 1 + word.length : word.length;
|
|
750
767
|
if (testLineLength <= charsPerLine) {
|
|
751
768
|
currentLineLength = testLineLength;
|
|
@@ -761,11 +778,11 @@ function estimateWrappedLineCount(text, fontSize, maxWidth) {
|
|
|
761
778
|
}
|
|
762
779
|
return Math.max(1, lineCount + (currentLineLength ? 1 : 0));
|
|
763
780
|
}
|
|
764
|
-
function estimateProseLineCount(
|
|
765
|
-
return
|
|
781
|
+
function estimateProseLineCount(text2, fontSize, maxWidthPx) {
|
|
782
|
+
return text2.split("\n").reduce((sum, line) => sum + estimateWrappedLineCount(line, fontSize, maxWidthPx), 0);
|
|
766
783
|
}
|
|
767
784
|
function fitProse(options) {
|
|
768
|
-
const { text, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
|
|
785
|
+
const { text: text2, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
|
|
769
786
|
const step2 = Math.max(0.5, options.step ?? 2);
|
|
770
787
|
const floor = Math.min(baseFontSize, minFontSize);
|
|
771
788
|
const heightAt = (lines, size) => lines * size * lineHeight;
|
|
@@ -773,7 +790,7 @@ function fitProse(options) {
|
|
|
773
790
|
for (let size = baseFontSize; size > floor; size -= step2) candidates.push(size);
|
|
774
791
|
candidates.push(floor);
|
|
775
792
|
for (const size of candidates) {
|
|
776
|
-
const lines = estimateProseLineCount(
|
|
793
|
+
const lines = estimateProseLineCount(text2, size, maxWidthPx);
|
|
777
794
|
const heightPx = heightAt(lines, size);
|
|
778
795
|
if (heightPx <= maxHeightPx) return { fontSize: size, lines, heightPx };
|
|
779
796
|
}
|
|
@@ -1292,8 +1309,8 @@ function sectionHeader(input, context) {
|
|
|
1292
1309
|
// src/doc/utils/textMetrics.ts
|
|
1293
1310
|
var AVERAGE_CHAR_WIDTH_EM = 0.62;
|
|
1294
1311
|
var BOLD_CHAR_WIDTH_EM = 0.66;
|
|
1295
|
-
function estimateTextWidth(
|
|
1296
|
-
return
|
|
1312
|
+
function estimateTextWidth(text2, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
|
|
1313
|
+
return text2.length * fontSize * charWidthEm;
|
|
1297
1314
|
}
|
|
1298
1315
|
var DISPLAY_CHAR_EM = {
|
|
1299
1316
|
space: 0.27,
|
|
@@ -1319,9 +1336,9 @@ function displayCharEm(ch) {
|
|
|
1319
1336
|
if (".,:;-/'\u2019!?()".includes(ch)) return DISPLAY_CHAR_EM.punct;
|
|
1320
1337
|
return DISPLAY_CHAR_EM.other;
|
|
1321
1338
|
}
|
|
1322
|
-
function estimateDisplayTextWidth(
|
|
1339
|
+
function estimateDisplayTextWidth(text2, fontSize) {
|
|
1323
1340
|
let em = 0;
|
|
1324
|
-
for (const ch of
|
|
1341
|
+
for (const ch of text2.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
|
|
1325
1342
|
return em * fontSize;
|
|
1326
1343
|
}
|
|
1327
1344
|
|
|
@@ -1658,8 +1675,8 @@ function collectListItems(block) {
|
|
|
1658
1675
|
for (const node of block.contents) {
|
|
1659
1676
|
walkMarkdownTree(node, (n) => {
|
|
1660
1677
|
if (n.type === "listItem") {
|
|
1661
|
-
const
|
|
1662
|
-
if (
|
|
1678
|
+
const text2 = extractPlainText(n).replace(/\s+/g, " ").trim();
|
|
1679
|
+
if (text2) out.push(text2);
|
|
1663
1680
|
return true;
|
|
1664
1681
|
}
|
|
1665
1682
|
return;
|
|
@@ -1675,14 +1692,14 @@ function getChildrenSummary(block) {
|
|
|
1675
1692
|
if (!block.children || block.children.length === 0) return "";
|
|
1676
1693
|
return block.children.map((c) => c.title?.trim()).filter((t) => Boolean(t)).join(", ");
|
|
1677
1694
|
}
|
|
1678
|
-
function escapeBraces(
|
|
1679
|
-
return
|
|
1695
|
+
function escapeBraces(text2) {
|
|
1696
|
+
return text2.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
|
|
1680
1697
|
}
|
|
1681
|
-
function unescapeBraces(
|
|
1682
|
-
return
|
|
1698
|
+
function unescapeBraces(text2) {
|
|
1699
|
+
return text2.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
|
|
1683
1700
|
}
|
|
1684
|
-
function substitute(
|
|
1685
|
-
const escaped = escapeBraces(
|
|
1701
|
+
function substitute(text2, resolve) {
|
|
1702
|
+
const escaped = escapeBraces(text2);
|
|
1686
1703
|
const tokenized = escaped.replace(
|
|
1687
1704
|
TOKEN_RE,
|
|
1688
1705
|
(match, name, arg, def) => {
|
|
@@ -1725,8 +1742,8 @@ function resolveToken(name, arg, block, images, item) {
|
|
|
1725
1742
|
return null;
|
|
1726
1743
|
}
|
|
1727
1744
|
}
|
|
1728
|
-
function substituteText(
|
|
1729
|
-
return substitute(
|
|
1745
|
+
function substituteText(text2, block, images, item) {
|
|
1746
|
+
return substitute(text2, (name, arg) => resolveToken(name, arg, block, images, item));
|
|
1730
1747
|
}
|
|
1731
1748
|
function resolveImageSrc(src, block, images, item) {
|
|
1732
1749
|
if (!src.includes("{")) return src;
|
|
@@ -1786,7 +1803,7 @@ function collectRepeatItems(source, block, images) {
|
|
|
1786
1803
|
index: i + 1
|
|
1787
1804
|
}));
|
|
1788
1805
|
case "listItems":
|
|
1789
|
-
return collectListItems(block).map((
|
|
1806
|
+
return collectListItems(block).map((text2, i) => ({ text: text2, label: text2, index: i + 1 }));
|
|
1790
1807
|
default:
|
|
1791
1808
|
return [];
|
|
1792
1809
|
}
|
|
@@ -1873,21 +1890,22 @@ function makeCustomTemplateFn(def) {
|
|
|
1873
1890
|
|
|
1874
1891
|
// src/doc/utils/countUp.ts
|
|
1875
1892
|
var STAT_PATTERN = /^([^\d]*?)([-−]?\d[\d,]*(?:\.\d+)?)(.*)$/s;
|
|
1876
|
-
|
|
1877
|
-
|
|
1893
|
+
var MEASURE_UNIT = /^\s?(?:feet|foot|ft|metres?|meters?|m|km|kilomet(?:re|er)s?|miles?|mi|yards?|inch(?:es)?|cm|mm|acres?|hectares?|ha|kg|kilograms?|g|grams?|lbs?|pounds?|tons?|tonnes?|litres?|liters?|gallons?|people|residents|inhabitants|visitors|species|steps|pages|words|°[CF]?|degrees|mph|km\/h|knots|watts?|kW|MW|calories|dollars|euros)\b/i;
|
|
1894
|
+
function parseStatNumber(text2) {
|
|
1895
|
+
const match = STAT_PATTERN.exec(text2.trim());
|
|
1878
1896
|
if (!match) return null;
|
|
1879
1897
|
const [, prefix, numeric, suffix] = match;
|
|
1880
1898
|
const normalized = numeric.replace(/,/g, "").replace("\u2212", "-");
|
|
1881
1899
|
const value = Number(normalized);
|
|
1882
1900
|
if (!Number.isFinite(value)) return null;
|
|
1883
1901
|
const decimals = normalized.includes(".") ? normalized.split(".")[1].length : 0;
|
|
1884
|
-
const bare = /^-?\d{4}$/.test(
|
|
1902
|
+
const bare = /^-?\d{4}$/.test(numeric.replace("\u2212", "-"));
|
|
1885
1903
|
const looksLikeYear = bare && Math.abs(value) >= 1e3 && Math.abs(value) <= 2999 && !/^[$€£¥]/.test(prefix);
|
|
1886
|
-
if (looksLikeYear && !/^[%KMB]/i.test(suffix.trim())) return null;
|
|
1904
|
+
if (looksLikeYear && !/^[%KMB]/i.test(suffix.trim()) && !MEASURE_UNIT.test(suffix)) return null;
|
|
1887
1905
|
return { prefix, value, decimals, grouping: numeric.includes(","), suffix };
|
|
1888
1906
|
}
|
|
1889
|
-
function countUpSpecFor(
|
|
1890
|
-
const parsed = parseStatNumber(
|
|
1907
|
+
function countUpSpecFor(text2) {
|
|
1908
|
+
const parsed = parseStatNumber(text2);
|
|
1891
1909
|
if (!parsed) return null;
|
|
1892
1910
|
return {
|
|
1893
1911
|
from: 0,
|
|
@@ -1909,13 +1927,13 @@ function formatCountValue(value, decimals, grouping) {
|
|
|
1909
1927
|
const sign = value < 0 && Number(fixed) !== 0 ? "-" : "";
|
|
1910
1928
|
return `${sign}${grouped}${fraction ? `.${fraction}` : ""}`;
|
|
1911
1929
|
}
|
|
1912
|
-
function formatCountUp(
|
|
1913
|
-
const count = animation.count ?? countUpSpecFor(
|
|
1914
|
-
if (!count) return
|
|
1930
|
+
function formatCountUp(text2, animation, blockTime) {
|
|
1931
|
+
const count = animation.count ?? countUpSpecFor(text2);
|
|
1932
|
+
if (!count) return text2;
|
|
1915
1933
|
const delay = animation.delay ?? 0;
|
|
1916
1934
|
const duration = animation.duration ?? 1.5;
|
|
1917
1935
|
const progress = duration <= 0 ? 1 : Math.min(1, Math.max(0, (blockTime - delay) / duration));
|
|
1918
|
-
if (progress >= 1) return animation.count ? formatFinal(count) :
|
|
1936
|
+
if (progress >= 1) return animation.count ? formatFinal(count) : text2;
|
|
1919
1937
|
const from = count.from ?? 0;
|
|
1920
1938
|
const value = from + (count.to - from) * easeOutCubic(progress);
|
|
1921
1939
|
return `${count.prefix ?? ""}${formatCountValue(value, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
|
|
@@ -1924,183 +1942,570 @@ function formatFinal(count) {
|
|
|
1924
1942
|
return `${count.prefix ?? ""}${formatCountValue(count.to, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
|
|
1925
1943
|
}
|
|
1926
1944
|
|
|
1927
|
-
// src/doc/templates/
|
|
1928
|
-
var
|
|
1929
|
-
var
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
var
|
|
1939
|
-
var
|
|
1940
|
-
var
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
var MIN_SCALE = 0.62;
|
|
1947
|
-
var MAX_SCALE = 1.18;
|
|
1948
|
-
var GROW_SHARE = 0.9;
|
|
1949
|
-
function ramp(context, scale) {
|
|
1945
|
+
// src/doc/templates/listBlock.ts
|
|
1946
|
+
var LIST_ITEMS_FLOW = "list-items";
|
|
1947
|
+
var LIST_ITEM_LINE_HEIGHT = 1.2;
|
|
1948
|
+
var LIST_ITEM_BASE_FONT_PX = 34;
|
|
1949
|
+
var LIST_ITEM_GAP_PX = 18;
|
|
1950
|
+
var LIST_ITEM_MIN_FONT_PX = 16;
|
|
1951
|
+
var LIST_BOTTOM_MARGIN_PCT = 8;
|
|
1952
|
+
var LIST_STRIP_CLEARANCE_PCT = 2;
|
|
1953
|
+
var LIST_RELAXED_TITLE_TOP_PCT = 20;
|
|
1954
|
+
var LIST_RELAXED_ITEMS_TOP_PCT = 34;
|
|
1955
|
+
var LIST_RELAXED_UNTITLED_TOP_PCT = 26;
|
|
1956
|
+
var LIST_CONDENSED_TOP_PCT = 8;
|
|
1957
|
+
var LIST_CONDENSED_TITLE_GAP_PX = 22;
|
|
1958
|
+
var LIST_TITLE_LINE_HEIGHT = 1.15;
|
|
1959
|
+
function listItemGeometry(fontSize, baseFontSize, itemCount, textWidthPx, ordered = true) {
|
|
1960
|
+
const markerText = ordered ? `${itemCount}.` : "\u2022";
|
|
1961
|
+
const markerWidthPx = Math.max(fontSize, markerText.length * fontSize * 0.5);
|
|
1962
|
+
const markerGapPx = fontSize * 0.35;
|
|
1963
|
+
const bodyIndentPx = markerWidthPx + markerGapPx;
|
|
1950
1964
|
return {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1965
|
+
markerWidthPx,
|
|
1966
|
+
bodyIndentPx,
|
|
1967
|
+
bodyWidthPx: Math.max(fontSize, textWidthPx - bodyIndentPx),
|
|
1968
|
+
gapPx: LIST_ITEM_GAP_PX * (fontSize / baseFontSize)
|
|
1954
1969
|
};
|
|
1955
1970
|
}
|
|
1956
|
-
function
|
|
1957
|
-
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const widest = (text, size2, em) => Math.max(0, ...text.split(/\s+/u).map((word) => word.length)) * size2 * em;
|
|
1962
|
-
const overflows = widest(item.label, sizes.label, 0.62) > width || widest(item.detail, size, 0.56) > width;
|
|
1963
|
-
return { labelH, gap, detailH, height: overflows ? Infinity : labelH + gap + detailH };
|
|
1964
|
-
}
|
|
1965
|
-
var SCALES = [MAX_SCALE, 1, 0.82, MIN_SCALE];
|
|
1966
|
-
function fitRamp(items, context, width, height) {
|
|
1967
|
-
for (const scale of SCALES) {
|
|
1968
|
-
const sizes = ramp(context, scale);
|
|
1969
|
-
const room = height(scale) * (scale > 1 ? GROW_SHARE : 1);
|
|
1970
|
-
if (items.every((item) => measureItem(item, sizes, width(scale)).height <= room))
|
|
1971
|
-
return { sizes, scale };
|
|
1971
|
+
function measureListHeightPx(items, fontSize, geometry) {
|
|
1972
|
+
let heightPx = 0;
|
|
1973
|
+
for (const itemText2 of items) {
|
|
1974
|
+
const lineCount = estimateProseLineCount(itemText2, fontSize, geometry.bodyWidthPx);
|
|
1975
|
+
heightPx += lineCount * fontSize * LIST_ITEM_LINE_HEIGHT + geometry.gapPx;
|
|
1972
1976
|
}
|
|
1973
|
-
return
|
|
1974
|
-
}
|
|
1975
|
-
function accentColor(input, context) {
|
|
1976
|
-
return input.colorScheme ? resolveColorScheme(context, input.colorScheme).accent : context.theme.colors.highlight || context.theme.colors.primary;
|
|
1977
|
+
return heightPx;
|
|
1977
1978
|
}
|
|
1978
|
-
function
|
|
1979
|
-
const {
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
1979
|
+
function listBlock(input, context) {
|
|
1980
|
+
const { title } = input;
|
|
1981
|
+
const items = Array.isArray(input.items) ? input.items : [];
|
|
1982
|
+
if (items.length === 0 && !context.block?.summaryMode) return [];
|
|
1983
|
+
const build = input.build ?? context.block?.build;
|
|
1984
|
+
if (build && build.mode !== "off") {
|
|
1985
|
+
const rich = extractRichListItems(context.block?.contents);
|
|
1986
|
+
const matches = rich.length === items.length && rich.every((item, i) => item.text === items[i]);
|
|
1987
|
+
const contents = matches ? context.block?.contents ?? [] : [
|
|
1988
|
+
{
|
|
1989
|
+
type: "list",
|
|
1990
|
+
ordered: input.ordered !== false,
|
|
1991
|
+
children: items.map((text2) => ({
|
|
1992
|
+
type: "listItem",
|
|
1993
|
+
children: [{ type: "paragraph", children: [{ type: "text", value: text2 }] }]
|
|
1994
|
+
}))
|
|
1995
|
+
}
|
|
1996
|
+
];
|
|
1997
|
+
const layers2 = contentBlock(
|
|
1998
|
+
{ ...input, template: "content", title: input.title ?? "", build },
|
|
1999
|
+
{
|
|
2000
|
+
...context,
|
|
2001
|
+
block: {
|
|
2002
|
+
id: input.id,
|
|
2003
|
+
duration: input.duration,
|
|
2004
|
+
startTime: 0,
|
|
2005
|
+
audioSegment: input.audioSegment,
|
|
2006
|
+
...context.block,
|
|
2007
|
+
summaryMode: void 0,
|
|
2008
|
+
contents,
|
|
2009
|
+
build
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
);
|
|
2013
|
+
const accent = accentForViewport(input.accentImage, context.viewport);
|
|
2014
|
+
if (accent) {
|
|
2015
|
+
const layout = getAccentLayout(accent.position);
|
|
2016
|
+
layers2.splice(
|
|
2017
|
+
1,
|
|
2018
|
+
0,
|
|
2019
|
+
...createAccentLayers(
|
|
2020
|
+
accent,
|
|
2021
|
+
input.id,
|
|
2022
|
+
themedImageTreatment(context, input.imageTreatment)
|
|
2023
|
+
)
|
|
2024
|
+
);
|
|
2025
|
+
for (const layer of layers2) {
|
|
2026
|
+
if (layer.type !== "text" || layer.id !== "title" && layer.id !== "body") continue;
|
|
2027
|
+
layer.position.x = `${parseFloat(layout.textCenterX) - parseFloat(layout.textWidth) / 2}%`;
|
|
2028
|
+
layer.position.width = layout.textWidth;
|
|
2029
|
+
layer.position.y = adjustY(String(layer.position.y), layout);
|
|
2030
|
+
if (layer.id === "body" && accent.position === "bottom-strip")
|
|
2031
|
+
layer.position.height = `${72 - ACCENT_STRIP_SIZE}%`;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return layers2;
|
|
2035
|
+
}
|
|
2036
|
+
const accentImage = accentForViewport(input.accentImage, context.viewport);
|
|
2037
|
+
const ordered = input.ordered !== false;
|
|
2038
|
+
const richItems = extractRichListItems(context.block?.contents);
|
|
2039
|
+
const { theme } = context;
|
|
2040
|
+
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
2041
|
+
const titleFontSize = themedFontSize(44, context, true);
|
|
2042
|
+
const baseItemFontSize = themedFontSize(LIST_ITEM_BASE_FONT_PX, context, false);
|
|
2043
|
+
const minItemFontSize = Math.min(
|
|
2044
|
+
baseItemFontSize,
|
|
2045
|
+
themedFontSize(LIST_ITEM_MIN_FONT_PX, context, false)
|
|
2046
|
+
);
|
|
2047
|
+
const layers = [
|
|
2048
|
+
// Background — gradient
|
|
2049
|
+
{
|
|
2000
2050
|
type: "shape",
|
|
2001
|
-
id: "
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
width: W * 0.045,
|
|
2006
|
-
height: ruleH
|
|
2051
|
+
id: "bg",
|
|
2052
|
+
content: {
|
|
2053
|
+
shape: "rect",
|
|
2054
|
+
fill: `linear-gradient(155deg, ${theme.colors.backgroundLight} 0%, ${theme.colors.background} 100%)`
|
|
2007
2055
|
},
|
|
2008
|
-
|
|
2009
|
-
animation: entrance(context, "rule", 0)
|
|
2010
|
-
});
|
|
2011
|
-
layers.push({
|
|
2012
|
-
...textLayer("title", title, box(margin, headlineTop, W - margin * 2, fit.heightPx), {
|
|
2013
|
-
fontSize: fit.fontSize,
|
|
2014
|
-
fontFamily: getThemeFont(context, "title"),
|
|
2015
|
-
fontWeight: "bold",
|
|
2016
|
-
color: theme.colors.text,
|
|
2017
|
-
lineHeight: HEADLINE_LINE_HEIGHT,
|
|
2018
|
-
shadow: shouldUseShadow(context),
|
|
2019
|
-
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
2020
|
-
}),
|
|
2021
|
-
animation: entrance(context, "text", 0.1)
|
|
2022
|
-
});
|
|
2023
|
-
top = headlineTop + fit.heightPx + H * (portrait ? 0.05 : 0.07);
|
|
2024
|
-
}
|
|
2025
|
-
return {
|
|
2026
|
-
layers,
|
|
2027
|
-
accent,
|
|
2028
|
-
left: margin,
|
|
2029
|
-
right: W - margin,
|
|
2030
|
-
top,
|
|
2031
|
-
bottom: H * (portrait ? 0.92 : 0.9),
|
|
2032
|
-
portrait
|
|
2033
|
-
};
|
|
2034
|
-
}
|
|
2035
|
-
function box(left, top, width, height) {
|
|
2036
|
-
return { x: left + width / 2, y: top + height / 2, width, height, anchor: "center" };
|
|
2037
|
-
}
|
|
2038
|
-
function textLayer(id, text, position, style) {
|
|
2039
|
-
return {
|
|
2040
|
-
type: "text",
|
|
2041
|
-
id,
|
|
2042
|
-
position,
|
|
2043
|
-
content: {
|
|
2044
|
-
text,
|
|
2045
|
-
style: { textAlign: "left", verticalAlign: "top", shrinkToFit: true, ...style }
|
|
2056
|
+
position: { x: 0, y: 0, width: "100%", height: "100%" }
|
|
2046
2057
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
function itemText(id, item, measure, sizes, left, top, width, align, context) {
|
|
2050
|
-
const { theme } = context;
|
|
2051
|
-
const slack = (size) => size * 0.4;
|
|
2052
|
-
const layers = [];
|
|
2053
|
-
if (item.label)
|
|
2054
|
-
layers.push(
|
|
2055
|
-
textLayer(
|
|
2056
|
-
`${id}-label`,
|
|
2057
|
-
item.label,
|
|
2058
|
-
box(left, top, width, measure.labelH + slack(sizes.label)),
|
|
2059
|
-
{
|
|
2060
|
-
fontSize: sizes.label,
|
|
2061
|
-
fontFamily: getThemeFont(context, "title"),
|
|
2062
|
-
fontWeight: "bold",
|
|
2063
|
-
color: theme.colors.text,
|
|
2064
|
-
lineHeight: LABEL_LINE_HEIGHT,
|
|
2065
|
-
textAlign: align
|
|
2066
|
-
}
|
|
2067
|
-
)
|
|
2068
|
-
);
|
|
2069
|
-
if (item.detail) {
|
|
2070
|
-
const size = item.label ? sizes.detail : sizes.plain;
|
|
2058
|
+
];
|
|
2059
|
+
if (accentImage) {
|
|
2071
2060
|
layers.push(
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
{
|
|
2077
|
-
fontSize: size,
|
|
2078
|
-
fontFamily: getThemeFont(context, "body"),
|
|
2079
|
-
// Details read as secondary by weight and a little transparency, not a
|
|
2080
|
-
// muted hue that loses contrast on dark themes.
|
|
2081
|
-
color: item.label ? withAlpha(theme.colors.text, 0.8) : theme.colors.text,
|
|
2082
|
-
lineHeight: DETAIL_LINE_HEIGHT,
|
|
2083
|
-
textAlign: align
|
|
2084
|
-
}
|
|
2061
|
+
...createAccentLayers(
|
|
2062
|
+
accentImage,
|
|
2063
|
+
input.id,
|
|
2064
|
+
themedImageTreatment(context, input.imageTreatment)
|
|
2085
2065
|
)
|
|
2086
2066
|
);
|
|
2087
2067
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2068
|
+
const centerX = parseFloat(accentLayout.textCenterX);
|
|
2069
|
+
const widthPct = parseFloat(accentLayout.textWidth);
|
|
2070
|
+
const leftPct = Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
|
|
2071
|
+
const leftX = `${leftPct}%`;
|
|
2072
|
+
const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
|
|
2073
|
+
const bottomLimitPct = (accentImage?.position === "bottom-strip" ? 100 - ACCENT_STRIP_SIZE - LIST_STRIP_CLEARANCE_PCT : 100 - LIST_BOTTOM_MARGIN_PCT) - accentLayout.textYAdjust;
|
|
2074
|
+
const baseGeometry = listItemGeometry(
|
|
2075
|
+
baseItemFontSize,
|
|
2076
|
+
baseItemFontSize,
|
|
2077
|
+
items.length,
|
|
2078
|
+
textWidthPx,
|
|
2079
|
+
ordered
|
|
2080
|
+
);
|
|
2081
|
+
const relaxedStartY = title ? LIST_RELAXED_ITEMS_TOP_PCT : LIST_RELAXED_UNTITLED_TOP_PCT;
|
|
2082
|
+
const relaxedBottomPct = relaxedStartY + measureListHeightPx(items, baseItemFontSize, baseGeometry) / context.viewport.height * 100;
|
|
2083
|
+
const condensed = relaxedBottomPct > bottomLimitPct;
|
|
2084
|
+
const condensedTopPct = LIST_CONDENSED_TOP_PCT - accentLayout.textYAdjust;
|
|
2085
|
+
const titleTopPct = condensed ? condensedTopPct : LIST_RELAXED_TITLE_TOP_PCT;
|
|
2086
|
+
let startY = relaxedStartY;
|
|
2087
|
+
if (condensed) {
|
|
2088
|
+
const titleHeightPx = title ? estimateWrappedLineCount(title, titleFontSize, textWidthPx) * titleFontSize * LIST_TITLE_LINE_HEIGHT + LIST_CONDENSED_TITLE_GAP_PX : 0;
|
|
2089
|
+
startY = condensedTopPct + titleHeightPx / context.viewport.height * 100;
|
|
2090
|
+
}
|
|
2091
|
+
if (title) {
|
|
2092
|
+
layers.push({
|
|
2093
|
+
type: "text",
|
|
2094
|
+
id: "list-title",
|
|
2095
|
+
content: {
|
|
2096
|
+
text: title,
|
|
2097
|
+
style: {
|
|
2098
|
+
fontSize: titleFontSize,
|
|
2099
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2100
|
+
fontWeight: "bold",
|
|
2101
|
+
color: theme.colors.text,
|
|
2102
|
+
textAlign: "left",
|
|
2103
|
+
shadow: shouldUseShadow(context)
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
position: {
|
|
2107
|
+
x: leftX,
|
|
2108
|
+
y: adjustY(`${titleTopPct}%`, accentLayout),
|
|
2109
|
+
width: accentLayout.textWidth
|
|
2110
|
+
},
|
|
2111
|
+
animation: motionEntrance(context, { type: "fadeIn", duration: 1 })
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
const availablePx = Math.max(0, (bottomLimitPct - startY) / 100 * context.viewport.height);
|
|
2115
|
+
let itemFontSize = baseItemFontSize;
|
|
2116
|
+
let geometry = baseGeometry;
|
|
2117
|
+
while (itemFontSize > minItemFontSize && measureListHeightPx(items, itemFontSize, geometry) > availablePx) {
|
|
2118
|
+
itemFontSize -= 1;
|
|
2119
|
+
geometry = listItemGeometry(itemFontSize, baseItemFontSize, items.length, textWidthPx, ordered);
|
|
2120
|
+
}
|
|
2121
|
+
const { markerWidthPx, bodyIndentPx, bodyWidthPx, gapPx } = geometry;
|
|
2122
|
+
const bodyLeftX = `${leftPct + bodyIndentPx / context.viewport.width * 100}%`;
|
|
2123
|
+
let itemY = startY;
|
|
2124
|
+
for (let i = 0; i < items.length; i++) {
|
|
2125
|
+
const itemText2 = items[i];
|
|
2126
|
+
const itemHtml = richItems[i]?.text === itemText2 ? richItems[i]?.html : void 0;
|
|
2127
|
+
const lineCount = estimateProseLineCount(itemText2, itemFontSize, bodyWidthPx);
|
|
2128
|
+
const animation = motionEntrance(context, {
|
|
2129
|
+
type: "fadeIn",
|
|
2130
|
+
duration: 0.8,
|
|
2131
|
+
delay: 0.3 + 0.3 * i
|
|
2132
|
+
});
|
|
2133
|
+
layers.push({
|
|
2134
|
+
type: "text",
|
|
2135
|
+
id: `item-${i}-marker`,
|
|
2136
|
+
content: {
|
|
2137
|
+
// Bullets stay bullets; only ordered (or legacy, unflagged) lists count.
|
|
2138
|
+
text: ordered ? `${i + 1}.` : "\u2022",
|
|
2139
|
+
flow: { id: LIST_ITEMS_FLOW, role: "marker" },
|
|
2140
|
+
style: {
|
|
2141
|
+
fontSize: itemFontSize,
|
|
2142
|
+
fontFamily: getThemeFont(context, "body"),
|
|
2143
|
+
color: theme.colors.text,
|
|
2144
|
+
textAlign: "right",
|
|
2145
|
+
lineHeight: LIST_ITEM_LINE_HEIGHT,
|
|
2146
|
+
shadow: shouldUseShadow(context)
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
position: {
|
|
2150
|
+
x: leftX,
|
|
2151
|
+
y: adjustY(`${itemY}%`, accentLayout),
|
|
2152
|
+
width: markerWidthPx
|
|
2153
|
+
},
|
|
2154
|
+
animation
|
|
2155
|
+
});
|
|
2156
|
+
layers.push({
|
|
2157
|
+
type: "text",
|
|
2158
|
+
id: `item-${i}`,
|
|
2159
|
+
content: {
|
|
2160
|
+
text: itemText2,
|
|
2161
|
+
...itemHtml ? { html: itemHtml } : {},
|
|
2162
|
+
paragraphs: [
|
|
2163
|
+
{
|
|
2164
|
+
text: itemText2,
|
|
2165
|
+
list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
|
|
2166
|
+
}
|
|
2167
|
+
],
|
|
2168
|
+
flow: { id: LIST_ITEMS_FLOW },
|
|
2169
|
+
style: {
|
|
2170
|
+
fontSize: itemFontSize,
|
|
2171
|
+
fontFamily: getThemeFont(context, "body"),
|
|
2172
|
+
color: theme.colors.text,
|
|
2173
|
+
textAlign: "left",
|
|
2174
|
+
lineHeight: LIST_ITEM_LINE_HEIGHT,
|
|
2175
|
+
shadow: shouldUseShadow(context)
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
position: {
|
|
2179
|
+
x: bodyLeftX,
|
|
2180
|
+
y: adjustY(`${itemY}%`, accentLayout),
|
|
2181
|
+
width: bodyWidthPx,
|
|
2182
|
+
// Rich (HTML) items render in a hidden-overflow box the renderer
|
|
2183
|
+
// sizes from the plain text; size it here from the same line count
|
|
2184
|
+
// that reserved the vertical space so nested sub-items are not cut off.
|
|
2185
|
+
height: lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT + itemFontSize * 0.5
|
|
2186
|
+
},
|
|
2187
|
+
animation
|
|
2188
|
+
});
|
|
2189
|
+
const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
|
|
2190
|
+
itemY += (itemHeightPx + gapPx) / context.viewport.height * 100;
|
|
2191
|
+
}
|
|
2192
|
+
return layers;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/doc/templates/conceptSpecs.ts
|
|
2196
|
+
var CONCEPT_TEMPLATES = [
|
|
2197
|
+
"cards",
|
|
2198
|
+
"process",
|
|
2199
|
+
"cycle",
|
|
2200
|
+
"layers",
|
|
2201
|
+
"stats",
|
|
2202
|
+
"pyramid",
|
|
2203
|
+
"funnel",
|
|
2204
|
+
"hub",
|
|
2205
|
+
"venn",
|
|
2206
|
+
"matrix"
|
|
2207
|
+
];
|
|
2208
|
+
var CONCEPT_MAX_DRAWN = 6;
|
|
2209
|
+
var CYCLE_STAGE_CHARACTERS = 170;
|
|
2210
|
+
var CONCEPT_SPECS = {
|
|
2211
|
+
cards: {
|
|
2212
|
+
items: [2, 4],
|
|
2213
|
+
itemForm: "label-detail",
|
|
2214
|
+
inputs: [{ key: "versus", guide: "versus: true for exactly two set against each other" }]
|
|
2215
|
+
},
|
|
2216
|
+
process: { items: [2, 4], itemForm: "label-detail", order: "in order" },
|
|
2217
|
+
cycle: { items: [3, 5], itemForm: "label-detail" },
|
|
2218
|
+
layers: { items: [3, 5], itemForm: "label-detail", order: "innermost first" },
|
|
2219
|
+
stats: { items: [2, 4], itemForm: "figure-caption" },
|
|
2220
|
+
pyramid: { items: [3, 5], itemForm: "label-detail", order: "from the top down" },
|
|
2221
|
+
funnel: { items: [3, 5], itemForm: "label-detail", order: "widest first" },
|
|
2222
|
+
hub: {
|
|
2223
|
+
items: [3, 6],
|
|
2224
|
+
itemForm: "label-detail",
|
|
2225
|
+
inputs: [{ key: "center", guide: "center \u2264 3 words" }]
|
|
2226
|
+
},
|
|
2227
|
+
// Two sets and the item that says what they share.
|
|
2228
|
+
venn: { items: [3, 3], itemForm: "label-detail" },
|
|
2229
|
+
matrix: {
|
|
2230
|
+
items: [4, 4],
|
|
2231
|
+
itemForm: "label-detail",
|
|
2232
|
+
order: "in reading order (top left, top right, bottom left, bottom right)",
|
|
2233
|
+
inputs: [
|
|
2234
|
+
{ key: "xAxis", guide: "xAxis" },
|
|
2235
|
+
{ key: "yAxis", guide: "yAxis" }
|
|
2236
|
+
]
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
function conceptTemplate(template) {
|
|
2240
|
+
return CONCEPT_TEMPLATES.find((name) => name === template);
|
|
2241
|
+
}
|
|
2242
|
+
function range(template, options) {
|
|
2243
|
+
const [min, most] = CONCEPT_SPECS[template].items;
|
|
2244
|
+
return [min, Math.max(min, Math.min(most, options.max ?? most))];
|
|
2245
|
+
}
|
|
2246
|
+
function conceptItemCount(template, options = {}) {
|
|
2247
|
+
const [min, max] = range(template, options);
|
|
2248
|
+
return min === max ? `exactly ${min}` : `${min}\u2013${max}`;
|
|
2249
|
+
}
|
|
2250
|
+
function itemTexts(items) {
|
|
2251
|
+
return (Array.isArray(items) ? items : []).map(
|
|
2252
|
+
(item) => String(item ?? "").replace(/\s+/gu, " ").trim()
|
|
2253
|
+
).filter(Boolean);
|
|
2254
|
+
}
|
|
2255
|
+
var quoted = (text2) => `"${text2.length > 48 ? `${text2.slice(0, 47)}\u2026` : text2}"`;
|
|
2256
|
+
function conceptItemsProblem(template, items, options = {}) {
|
|
2257
|
+
const texts = itemTexts(items);
|
|
2258
|
+
const [min, max] = range(template, options);
|
|
2259
|
+
if (texts.length < min || texts.length > max)
|
|
2260
|
+
return `${template} takes ${conceptItemCount(template, options)} items and this has ${texts.length}`;
|
|
2261
|
+
if (template === "stats") {
|
|
2262
|
+
const bare = texts.find((text2) => !splitStatItem(text2).figure);
|
|
2263
|
+
if (bare !== void 0)
|
|
2264
|
+
return `every stats item is "figure: what it measures" and ${quoted(bare)} states no figure`;
|
|
2265
|
+
}
|
|
2266
|
+
if (template === "venn" && vennOverlapIndex(texts.map(splitConceptItem)) < 0)
|
|
2267
|
+
return 'venn needs one item labelled "Both:" that says what the two share';
|
|
2268
|
+
return void 0;
|
|
2269
|
+
}
|
|
2270
|
+
function cycleDrawsAsProcess(items) {
|
|
2271
|
+
return items.length < 3 || items.some((item) => item.detail.length > CYCLE_STAGE_CHARACTERS);
|
|
2272
|
+
}
|
|
2273
|
+
function conceptDowngrade(template, items) {
|
|
2274
|
+
const texts = itemTexts(items);
|
|
2275
|
+
if (texts.length > CONCEPT_MAX_DRAWN)
|
|
2276
|
+
return {
|
|
2277
|
+
drawsAs: "list",
|
|
2278
|
+
reason: `a drawing holds ${CONCEPT_MAX_DRAWN} items and this has ${texts.length}`
|
|
2279
|
+
};
|
|
2280
|
+
const parsed = texts.map(splitConceptItem);
|
|
2281
|
+
const n = parsed.length;
|
|
2282
|
+
switch (template) {
|
|
2283
|
+
case "cycle":
|
|
2284
|
+
if (n < 3)
|
|
2285
|
+
return {
|
|
2286
|
+
drawsAs: "process",
|
|
2287
|
+
reason: `a ring needs at least three stages and this has ${n}`
|
|
2288
|
+
};
|
|
2289
|
+
return cycleDrawsAsProcess(parsed) ? { drawsAs: "process", reason: "a stage runs too long to sit beside a ring" } : void 0;
|
|
2290
|
+
case "layers":
|
|
2291
|
+
case "pyramid":
|
|
2292
|
+
case "funnel":
|
|
2293
|
+
case "hub":
|
|
2294
|
+
return n < 2 ? { drawsAs: "cards", reason: `it needs at least two items and this has ${n}` } : void 0;
|
|
2295
|
+
case "stats":
|
|
2296
|
+
return texts.some((text2) => splitStatItem(text2).figure) ? void 0 : { drawsAs: "cards", reason: "no item states a figure" };
|
|
2297
|
+
case "venn": {
|
|
2298
|
+
const sets = n - (vennOverlapIndex(parsed) >= 0 ? 1 : 0);
|
|
2299
|
+
return sets === 2 ? void 0 : {
|
|
2300
|
+
drawsAs: "cards",
|
|
2301
|
+
reason: `a Venn diagram draws two sets (and one item labelled "Both:") and this has ${sets}`
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
case "matrix":
|
|
2305
|
+
return n === 4 ? void 0 : { drawsAs: "cards", reason: `a matrix places exactly four items and this has ${n}` };
|
|
2306
|
+
default:
|
|
2307
|
+
return void 0;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function conceptDrawsAs(template, items) {
|
|
2311
|
+
return conceptDowngrade(template, items)?.drawsAs ?? template;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/doc/templates/conceptBlocks.ts
|
|
2315
|
+
var MAX_ITEMS = CONCEPT_MAX_DRAWN;
|
|
2316
|
+
var HEADLINE_BASE = 60;
|
|
2317
|
+
var HEADLINE_MIN = 40;
|
|
2318
|
+
var HEADLINE_LINE_HEIGHT = 1.15;
|
|
2319
|
+
var LABEL_BASE = 44;
|
|
2320
|
+
var DETAIL_BASE = 33;
|
|
2321
|
+
var PLAIN_BASE = 38;
|
|
2322
|
+
var LABEL_LINE_HEIGHT = 1.2;
|
|
2323
|
+
var DETAIL_LINE_HEIGHT = 1.4;
|
|
2324
|
+
var MIN_SCALE = 0.62;
|
|
2325
|
+
var MAX_SCALE = 1.18;
|
|
2326
|
+
var GROW_SHARE = 0.9;
|
|
2327
|
+
function ramp(context, scale) {
|
|
2328
|
+
return {
|
|
2329
|
+
label: Math.round(themedFontSize(LABEL_BASE, context, false) * scale),
|
|
2330
|
+
detail: Math.round(themedFontSize(DETAIL_BASE, context, false) * scale),
|
|
2331
|
+
plain: Math.round(themedFontSize(PLAIN_BASE, context, false) * scale)
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function measureItem(item, sizes, width) {
|
|
2335
|
+
const labelH = item.label ? estimateProseLineCount(item.label, sizes.label, width) * sizes.label * LABEL_LINE_HEIGHT : 0;
|
|
2336
|
+
const size = item.label ? sizes.detail : sizes.plain;
|
|
2337
|
+
const detailH = item.detail ? estimateProseLineCount(item.detail, size, width) * size * DETAIL_LINE_HEIGHT : 0;
|
|
2338
|
+
const gap = item.label && item.detail ? sizes.label * 0.35 : 0;
|
|
2339
|
+
const widest = (text2, size2, em) => Math.max(0, ...text2.split(/\s+/u).map((word) => word.length)) * size2 * em;
|
|
2340
|
+
const overflows = widest(item.label, sizes.label, 0.62) > width || widest(item.detail, size, 0.56) > width;
|
|
2341
|
+
return { labelH, gap, detailH, height: overflows ? Infinity : labelH + gap + detailH };
|
|
2342
|
+
}
|
|
2343
|
+
var SCALES = [MAX_SCALE, 1, 0.82, MIN_SCALE];
|
|
2344
|
+
function fitRamp(items, context, width, height) {
|
|
2345
|
+
for (const scale of SCALES) {
|
|
2346
|
+
const sizes = ramp(context, scale);
|
|
2347
|
+
const room = height(scale) * (scale > 1 ? GROW_SHARE : 1);
|
|
2348
|
+
if (items.every((item) => measureItem(item, sizes, width(scale)).height <= room))
|
|
2349
|
+
return { sizes, scale };
|
|
2350
|
+
}
|
|
2351
|
+
return { sizes: ramp(context, MIN_SCALE), scale: MIN_SCALE };
|
|
2352
|
+
}
|
|
2353
|
+
function accentColor(input, context) {
|
|
2354
|
+
return input.colorScheme ? resolveColorScheme(context, input.colorScheme).accent : context.theme.colors.highlight || context.theme.colors.primary;
|
|
2355
|
+
}
|
|
2356
|
+
function frame(input, context) {
|
|
2357
|
+
const { theme, viewport } = context;
|
|
2358
|
+
const { width: W, height: H } = viewport;
|
|
2359
|
+
const portrait = H > W;
|
|
2360
|
+
const accent = accentColor(input, context);
|
|
2361
|
+
const margin = Math.round(W * (portrait ? 0.08 : 0.07));
|
|
2362
|
+
const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
|
|
2363
|
+
const title = (input.title ?? "").trim();
|
|
2364
|
+
let top = H * 0.1;
|
|
2365
|
+
if (title) {
|
|
2366
|
+
const headlineTop = H * (portrait ? 0.08 : 0.11);
|
|
2367
|
+
const base = themedFontSize(HEADLINE_BASE, context, true);
|
|
2368
|
+
const fit = fitProse({
|
|
2369
|
+
text: title,
|
|
2370
|
+
baseFontSize: base,
|
|
2371
|
+
minFontSize: Math.min(base, themedFontSize(HEADLINE_MIN, context, true)),
|
|
2372
|
+
maxWidthPx: W - margin * 2,
|
|
2373
|
+
maxHeightPx: base * HEADLINE_LINE_HEIGHT * 2,
|
|
2374
|
+
lineHeight: HEADLINE_LINE_HEIGHT
|
|
2375
|
+
});
|
|
2376
|
+
const ruleH = Math.max(4, Math.round(H * 6e-3));
|
|
2377
|
+
layers.push({
|
|
2378
|
+
type: "shape",
|
|
2379
|
+
id: "accent-rule",
|
|
2380
|
+
position: {
|
|
2381
|
+
x: margin,
|
|
2382
|
+
y: headlineTop - ruleH - fit.fontSize * 0.55,
|
|
2383
|
+
width: W * 0.045,
|
|
2384
|
+
height: ruleH
|
|
2385
|
+
},
|
|
2386
|
+
content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 },
|
|
2387
|
+
animation: entrance(context, "rule", 0)
|
|
2388
|
+
});
|
|
2389
|
+
layers.push({
|
|
2390
|
+
...textLayer("title", title, box(margin, headlineTop, W - margin * 2, fit.heightPx), {
|
|
2391
|
+
fontSize: fit.fontSize,
|
|
2392
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2393
|
+
fontWeight: "bold",
|
|
2394
|
+
color: theme.colors.text,
|
|
2395
|
+
lineHeight: HEADLINE_LINE_HEIGHT,
|
|
2396
|
+
shadow: shouldUseShadow(context),
|
|
2397
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
2398
|
+
}),
|
|
2399
|
+
animation: entrance(context, "text", 0.1)
|
|
2400
|
+
});
|
|
2401
|
+
top = headlineTop + fit.heightPx + H * (portrait ? 0.05 : 0.07);
|
|
2402
|
+
}
|
|
2403
|
+
return {
|
|
2404
|
+
layers,
|
|
2405
|
+
accent,
|
|
2406
|
+
left: margin,
|
|
2407
|
+
right: W - margin,
|
|
2408
|
+
top,
|
|
2409
|
+
bottom: H * (portrait ? 0.92 : 0.9),
|
|
2410
|
+
portrait
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function box(left, top, width, height) {
|
|
2414
|
+
return { x: left + width / 2, y: top + height / 2, width, height, anchor: "center" };
|
|
2415
|
+
}
|
|
2416
|
+
function textLayer(id, text2, position, style) {
|
|
2417
|
+
return {
|
|
2418
|
+
type: "text",
|
|
2419
|
+
id,
|
|
2420
|
+
position,
|
|
2421
|
+
content: {
|
|
2422
|
+
text: text2,
|
|
2423
|
+
style: { textAlign: "left", verticalAlign: "top", shrinkToFit: true, ...style }
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
function itemText(id, item, measure, sizes, left, top, width, align, context) {
|
|
2428
|
+
const { theme } = context;
|
|
2429
|
+
const slack = (size) => size * 0.4;
|
|
2430
|
+
const layers = [];
|
|
2431
|
+
if (item.label)
|
|
2432
|
+
layers.push(
|
|
2433
|
+
textLayer(
|
|
2434
|
+
`${id}-label`,
|
|
2435
|
+
item.label,
|
|
2436
|
+
box(left, top, width, measure.labelH + slack(sizes.label)),
|
|
2437
|
+
{
|
|
2438
|
+
fontSize: sizes.label,
|
|
2439
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2440
|
+
fontWeight: "bold",
|
|
2441
|
+
color: theme.colors.text,
|
|
2442
|
+
lineHeight: LABEL_LINE_HEIGHT,
|
|
2443
|
+
textAlign: align
|
|
2444
|
+
}
|
|
2445
|
+
)
|
|
2446
|
+
);
|
|
2447
|
+
if (item.detail) {
|
|
2448
|
+
const size = item.label ? sizes.detail : sizes.plain;
|
|
2449
|
+
layers.push(
|
|
2450
|
+
textLayer(
|
|
2451
|
+
`${id}-detail`,
|
|
2452
|
+
item.detail,
|
|
2453
|
+
box(left, top + measure.labelH + measure.gap, width, measure.detailH + slack(size)),
|
|
2454
|
+
{
|
|
2455
|
+
fontSize: size,
|
|
2456
|
+
fontFamily: getThemeFont(context, "body"),
|
|
2457
|
+
// Details read as secondary by weight and a little transparency, not a
|
|
2458
|
+
// muted hue that loses contrast on dark themes.
|
|
2459
|
+
color: item.label ? withAlpha(theme.colors.text, 0.8) : theme.colors.text,
|
|
2460
|
+
lineHeight: DETAIL_LINE_HEIGHT,
|
|
2461
|
+
textAlign: align
|
|
2462
|
+
}
|
|
2463
|
+
)
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
return layers;
|
|
2467
|
+
}
|
|
2468
|
+
function itemTexts2(input) {
|
|
2469
|
+
return (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, MAX_ITEMS);
|
|
2470
|
+
}
|
|
2471
|
+
function itemsOf(input) {
|
|
2472
|
+
return itemTexts2(input).map(splitConceptItem);
|
|
2473
|
+
}
|
|
2474
|
+
function withoutIdeas(area, context) {
|
|
2475
|
+
return context.block?.contents?.length ? [] : area.layers;
|
|
2476
|
+
}
|
|
2477
|
+
function asListWhenCrowded(input, context) {
|
|
2478
|
+
const items = (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
2479
|
+
if (items.length <= MAX_ITEMS) return void 0;
|
|
2480
|
+
return listBlock(
|
|
2481
|
+
{
|
|
2482
|
+
...input,
|
|
2483
|
+
template: "list",
|
|
2484
|
+
items,
|
|
2485
|
+
ordered: input.template === "process"
|
|
2486
|
+
},
|
|
2487
|
+
context
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
function statItemsOf(input) {
|
|
2491
|
+
return itemTexts2(input).map(splitStatItem);
|
|
2492
|
+
}
|
|
2493
|
+
function conceptBuildStepTexts(template, items) {
|
|
2494
|
+
const shown = itemsOf({ items: [...items] });
|
|
2495
|
+
const stats = template === "stats" ? statItemsOf({ items: [...items] }) : [];
|
|
2496
|
+
if (stats.some((stat) => stat.figure))
|
|
2497
|
+
return stats.map((stat) => {
|
|
2498
|
+
const { number, unit } = splitFigure(stat.figure);
|
|
2499
|
+
return [number, unit, stat.caption].filter(Boolean).join(" ");
|
|
2500
|
+
});
|
|
2501
|
+
if (template !== "process" && template !== "cycle")
|
|
2502
|
+
return shown.map((item) => [item.label, item.detail].filter(Boolean).join(" "));
|
|
2503
|
+
const asProcess = template === "process" || cycleDrawsAsProcess(shown);
|
|
2504
|
+
const dated = asProcess ? timelineItems(shown) : void 0;
|
|
2505
|
+
if (dated)
|
|
2097
2506
|
return dated.map((item) => [item.when, item.label, item.detail].filter(Boolean).join(" "));
|
|
2098
2507
|
return shown.map(
|
|
2099
|
-
(item, index) => [
|
|
2100
|
-
template === "cards" || template === "layers" ? "" : String(index + 1),
|
|
2101
|
-
item.label,
|
|
2102
|
-
item.detail
|
|
2103
|
-
].filter(Boolean).join(" ")
|
|
2508
|
+
(item, index) => [String(index + 1), item.label, item.detail].filter(Boolean).join(" ")
|
|
2104
2509
|
);
|
|
2105
2510
|
}
|
|
2106
2511
|
var FIRST_TURN = 0.6;
|
|
@@ -2149,18 +2554,18 @@ function words(layers, at) {
|
|
|
2149
2554
|
var LEAD_BASE = 44;
|
|
2150
2555
|
var LEAD_MIN = 28;
|
|
2151
2556
|
var LEAD_LINE_HEIGHT = 1.4;
|
|
2152
|
-
function lead(area,
|
|
2557
|
+
function lead(area, text2, context) {
|
|
2153
2558
|
const { theme, viewport } = context;
|
|
2154
2559
|
const barW = Math.max(5, Math.round(viewport.width * 4e-3));
|
|
2155
2560
|
const indent = barW + Math.round(viewport.width * 0.018);
|
|
2156
2561
|
const width = (area.right - area.left) * (area.portrait ? 1 : 0.8) - indent;
|
|
2157
2562
|
const base = themedFontSize(
|
|
2158
|
-
|
|
2563
|
+
text2.length <= 90 ? LEAD_BASE * 1.3 : text2.length <= 160 ? LEAD_BASE * 1.15 : LEAD_BASE,
|
|
2159
2564
|
context,
|
|
2160
2565
|
false
|
|
2161
2566
|
);
|
|
2162
2567
|
const fit = fitProse({
|
|
2163
|
-
text,
|
|
2568
|
+
text: text2,
|
|
2164
2569
|
baseFontSize: base,
|
|
2165
2570
|
minFontSize: Math.min(base, themedFontSize(LEAD_MIN, context, false)),
|
|
2166
2571
|
maxWidthPx: width,
|
|
@@ -2190,7 +2595,7 @@ function lead(area, text, context) {
|
|
|
2190
2595
|
[
|
|
2191
2596
|
textLayer(
|
|
2192
2597
|
"lead",
|
|
2193
|
-
|
|
2598
|
+
text2,
|
|
2194
2599
|
box(area.left + indent, top, width, fit.heightPx + fit.fontSize * 0.4),
|
|
2195
2600
|
{
|
|
2196
2601
|
fontSize: fit.fontSize,
|
|
@@ -2212,13 +2617,13 @@ function lead(area, text, context) {
|
|
|
2212
2617
|
}
|
|
2213
2618
|
var STATEMENT_BASE = 84;
|
|
2214
2619
|
var STATEMENT_MIN = 48;
|
|
2215
|
-
function statement(area,
|
|
2620
|
+
function statement(area, text2, context) {
|
|
2216
2621
|
const { theme, viewport } = context;
|
|
2217
2622
|
const W = area.right - area.left;
|
|
2218
2623
|
const H = area.bottom - area.top;
|
|
2219
2624
|
const base = themedFontSize(STATEMENT_BASE, context, true);
|
|
2220
2625
|
const fit = fitProse({
|
|
2221
|
-
text,
|
|
2626
|
+
text: text2,
|
|
2222
2627
|
baseFontSize: base,
|
|
2223
2628
|
minFontSize: Math.min(base, themedFontSize(STATEMENT_MIN, context, true)),
|
|
2224
2629
|
maxWidthPx: W * (area.portrait ? 1 : 0.86),
|
|
@@ -2249,7 +2654,7 @@ function statement(area, text, context) {
|
|
|
2249
2654
|
[
|
|
2250
2655
|
textLayer(
|
|
2251
2656
|
"statement",
|
|
2252
|
-
|
|
2657
|
+
text2,
|
|
2253
2658
|
box(area.left, top, W * (area.portrait ? 1 : 0.86), fit.heightPx + fit.fontSize * 0.3),
|
|
2254
2659
|
{
|
|
2255
2660
|
fontSize: fit.fontSize,
|
|
@@ -2271,10 +2676,12 @@ function statement(area, text, context) {
|
|
|
2271
2676
|
];
|
|
2272
2677
|
}
|
|
2273
2678
|
function cardsBlock(input, context) {
|
|
2679
|
+
const crowded = asListWhenCrowded(input, context);
|
|
2680
|
+
if (crowded) return crowded;
|
|
2274
2681
|
const items = itemsOf(input);
|
|
2275
2682
|
const area = frame(input, context);
|
|
2276
2683
|
const { theme, viewport } = context;
|
|
2277
|
-
if (!items.length) return area
|
|
2684
|
+
if (!items.length) return withoutIdeas(area, context);
|
|
2278
2685
|
if (items.length === 1 && !items[0].label)
|
|
2279
2686
|
return (input.title?.trim() ? lead : statement)(area, items[0].detail, context);
|
|
2280
2687
|
const contrast = String(input.versus) === "true";
|
|
@@ -2506,10 +2913,12 @@ function connector(id, d, color, width, arrow = true) {
|
|
|
2506
2913
|
};
|
|
2507
2914
|
}
|
|
2508
2915
|
function processBlock(input, context) {
|
|
2916
|
+
const crowded = asListWhenCrowded(input, context);
|
|
2917
|
+
if (crowded) return crowded;
|
|
2509
2918
|
const items = itemsOf(input);
|
|
2510
2919
|
const area = frame(input, context);
|
|
2511
2920
|
const { viewport } = context;
|
|
2512
|
-
if (!items.length) return area
|
|
2921
|
+
if (!items.length) return withoutIdeas(area, context);
|
|
2513
2922
|
const dated = timelineItems(items);
|
|
2514
2923
|
if (dated) return timeline(area, dated, context);
|
|
2515
2924
|
const W = area.right - area.left;
|
|
@@ -2638,30 +3047,6 @@ function processBlock(input, context) {
|
|
|
2638
3047
|
});
|
|
2639
3048
|
return layers;
|
|
2640
3049
|
}
|
|
2641
|
-
var MONTH = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\\.?\\s+";
|
|
2642
|
-
var LEADING_DATE = new RegExp(
|
|
2643
|
-
`^((?:${MONTH}(?:\\d{1,2},?\\s+)?)?(?:\\d{4}s?|\\d{1,4}\\s?(?:BCE?|AD|CE))(?![.,]?\\d))[\\s,:;\u2014\u2013-]*(.*)$`,
|
|
2644
|
-
"u"
|
|
2645
|
-
);
|
|
2646
|
-
function timelineItems(items) {
|
|
2647
|
-
if (items.length < 2) return void 0;
|
|
2648
|
-
let last = -Infinity;
|
|
2649
|
-
const dated = [];
|
|
2650
|
-
for (const item of items) {
|
|
2651
|
-
const match = (item.label || item.detail).match(LEADING_DATE);
|
|
2652
|
-
if (!match) return void 0;
|
|
2653
|
-
const when = match[1];
|
|
2654
|
-
const year = Number(when.match(/\d{1,4}(?=s?\s?(?:BCE?|AD|CE)?$)/u)?.[0] ?? NaN);
|
|
2655
|
-
const signed = /BCE?$/u.test(when) ? -year : year;
|
|
2656
|
-
if (!Number.isFinite(signed) || signed < last) return void 0;
|
|
2657
|
-
last = signed;
|
|
2658
|
-
const rest = match[2].trim();
|
|
2659
|
-
dated.push(
|
|
2660
|
-
item.label ? { when, label: rest, detail: item.detail } : { when, label: "", detail: rest }
|
|
2661
|
-
);
|
|
2662
|
-
}
|
|
2663
|
-
return dated;
|
|
2664
|
-
}
|
|
2665
3050
|
function timeline(area, items, context) {
|
|
2666
3051
|
const { viewport } = context;
|
|
2667
3052
|
const W = area.right - area.left;
|
|
@@ -2824,10 +3209,12 @@ function timeline(area, items, context) {
|
|
|
2824
3209
|
return layers;
|
|
2825
3210
|
}
|
|
2826
3211
|
function cycleBlock(input, context) {
|
|
3212
|
+
const crowded = asListWhenCrowded(input, context);
|
|
3213
|
+
if (crowded) return crowded;
|
|
2827
3214
|
const items = itemsOf(input);
|
|
2828
3215
|
const area = frame(input, context);
|
|
2829
3216
|
const { viewport } = context;
|
|
2830
|
-
if (items
|
|
3217
|
+
if (cycleDrawsAsProcess(items)) return processBlock({ ...input, template: "process" }, context);
|
|
2831
3218
|
const H = area.bottom - area.top;
|
|
2832
3219
|
const n = items.length;
|
|
2833
3220
|
const cx = (area.left + area.right) / 2;
|
|
@@ -2912,8 +3299,11 @@ function cycleBlock(input, context) {
|
|
|
2912
3299
|
return layers;
|
|
2913
3300
|
}
|
|
2914
3301
|
function layersBlock(input, context) {
|
|
3302
|
+
const crowded = asListWhenCrowded(input, context);
|
|
3303
|
+
if (crowded) return crowded;
|
|
2915
3304
|
const items = itemsOf(input);
|
|
2916
|
-
if (
|
|
3305
|
+
if (conceptDrawsAs("layers", input.items) === "cards")
|
|
3306
|
+
return cardsBlock({ ...input, template: "cards" }, context);
|
|
2917
3307
|
const area = frame(input, context);
|
|
2918
3308
|
const { theme, viewport } = context;
|
|
2919
3309
|
const W = area.right - area.left;
|
|
@@ -4241,10 +4631,10 @@ function estimateLineEm(line, mono) {
|
|
|
4241
4631
|
}
|
|
4242
4632
|
return em;
|
|
4243
4633
|
}
|
|
4244
|
-
function simulateRendererLines(
|
|
4634
|
+
function simulateRendererLines(text2, fontSize, boxWidthPx) {
|
|
4245
4635
|
const charsPerLine = Math.floor(boxWidthPx / (fontSize * RENDERER_CHAR_EM));
|
|
4246
4636
|
const lines = [];
|
|
4247
|
-
for (const segment of
|
|
4637
|
+
for (const segment of text2.split("\n")) {
|
|
4248
4638
|
if (!segment.trim() || charsPerLine <= 0) {
|
|
4249
4639
|
lines.push(segment);
|
|
4250
4640
|
continue;
|
|
@@ -4268,14 +4658,14 @@ function simulateRendererLines(text, fontSize, boxWidthPx) {
|
|
|
4268
4658
|
}
|
|
4269
4659
|
return lines;
|
|
4270
4660
|
}
|
|
4271
|
-
function fitWrapWidth(
|
|
4661
|
+
function fitWrapWidth(text2, fontSize, columnWidthPx, fontFamily) {
|
|
4272
4662
|
const mono = isMonoFamily(fontFamily);
|
|
4273
|
-
if (!mono && !WIDE_GLYPH_RE.test(
|
|
4663
|
+
if (!mono && !WIDE_GLYPH_RE.test(text2)) return columnWidthPx;
|
|
4274
4664
|
const floor = columnWidthPx * 0.5;
|
|
4275
4665
|
const step2 = columnWidthPx * 0.02;
|
|
4276
4666
|
let width = columnWidthPx;
|
|
4277
4667
|
while (width > floor) {
|
|
4278
|
-
const lines = simulateRendererLines(
|
|
4668
|
+
const lines = simulateRendererLines(text2, fontSize, width);
|
|
4279
4669
|
const widest = Math.max(...lines.map((line) => estimateLineEm(line, mono))) * fontSize;
|
|
4280
4670
|
if (widest <= columnWidthPx) return width;
|
|
4281
4671
|
width -= step2;
|
|
@@ -4542,328 +4932,903 @@ var LINE_HEIGHT = 1.2;
|
|
|
4542
4932
|
var FULL_SIZE_CHARS = 40;
|
|
4543
4933
|
var MIN_SIZE_CHARS = 160;
|
|
4544
4934
|
var MIN_LENGTH_FACTOR = 0.6;
|
|
4545
|
-
function lengthFactor(
|
|
4546
|
-
const length =
|
|
4935
|
+
function lengthFactor(text2) {
|
|
4936
|
+
const length = text2.trim().length;
|
|
4547
4937
|
if (length <= FULL_SIZE_CHARS) return 1;
|
|
4548
4938
|
const t = Math.min(1, (length - FULL_SIZE_CHARS) / (MIN_SIZE_CHARS - FULL_SIZE_CHARS));
|
|
4549
4939
|
return 1 - t * (1 - MIN_LENGTH_FACTOR);
|
|
4550
4940
|
}
|
|
4551
4941
|
function fullBleedQuote(input, context) {
|
|
4552
|
-
const { text, colorScheme = "blue" } = input;
|
|
4942
|
+
const { text: text2, colorScheme = "blue" } = input;
|
|
4553
4943
|
const { viewport } = context;
|
|
4554
4944
|
const colors = resolveColorScheme(context, colorScheme);
|
|
4555
4945
|
const H = viewport.height;
|
|
4556
4946
|
const W = viewport.width;
|
|
4557
4947
|
const pct2 = (px) => `${Number((px / H * 100).toFixed(3))}%`;
|
|
4558
4948
|
const impactFontSize = themedFontSize(120, context, true);
|
|
4559
|
-
const baseFontSize = Math.round(impactFontSize * lengthFactor(
|
|
4949
|
+
const baseFontSize = Math.round(impactFontSize * lengthFactor(text2));
|
|
4560
4950
|
const bandHeightPx = (BAND_BOTTOM3 - BAND_TOP3) * H;
|
|
4561
4951
|
const fit = fitProse({
|
|
4562
|
-
text,
|
|
4952
|
+
text: text2,
|
|
4563
4953
|
baseFontSize,
|
|
4564
4954
|
minFontSize: Math.max(28, impactFontSize / 3),
|
|
4565
4955
|
maxWidthPx: COLUMN_WIDTH * W,
|
|
4566
|
-
maxHeightPx: bandHeightPx * BAND_FILL,
|
|
4567
|
-
lineHeight: LINE_HEIGHT
|
|
4568
|
-
});
|
|
4569
|
-
const topPx = Math.max(
|
|
4570
|
-
BAND_TOP3 * H,
|
|
4571
|
-
(BAND_TOP3 + (BAND_BOTTOM3 - BAND_TOP3) / 2) * H - fit.heightPx / 2
|
|
4572
|
-
);
|
|
4573
|
-
return [
|
|
4574
|
-
// Background — radial vignette built from the color scheme's own
|
|
4575
|
-
// surface, so the card takes on the scheme (and the theme behind it)
|
|
4576
|
-
// instead of vignetting every theme into hard black.
|
|
4577
|
-
{
|
|
4578
|
-
type: "shape",
|
|
4579
|
-
id: "bg",
|
|
4580
|
-
content: {
|
|
4581
|
-
shape: "rect",
|
|
4582
|
-
fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
|
|
4583
|
-
},
|
|
4584
|
-
position: { x: 0, y: 0, width: "100%", height: "100%" }
|
|
4585
|
-
},
|
|
4586
|
-
// The text — massive, centered, boxed so it can never leave the frame.
|
|
4587
|
-
{
|
|
4588
|
-
type: "text",
|
|
4589
|
-
id: "impact-text",
|
|
4590
|
-
content: {
|
|
4591
|
-
text,
|
|
4592
|
-
style: {
|
|
4593
|
-
fontSize: fit.fontSize,
|
|
4594
|
-
fontFamily: getThemeFont(context, "title"),
|
|
4595
|
-
fontWeight: "bold",
|
|
4596
|
-
color: colors.text,
|
|
4597
|
-
textAlign: "center",
|
|
4598
|
-
lineHeight: LINE_HEIGHT,
|
|
4599
|
-
shadow: shouldUseShadow(context),
|
|
4600
|
-
// Real glyphs of wide display / mono / CJK faces exceed the 0.5em
|
|
4601
|
-
// wrap estimate; the renderer measures and scales into the box.
|
|
4602
|
-
shrinkToFit: true,
|
|
4603
|
-
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
4604
|
-
}
|
|
4605
|
-
},
|
|
4606
|
-
position: {
|
|
4607
|
-
x: `${COLUMN_LEFT * 100}%`,
|
|
4608
|
-
y: pct2(topPx),
|
|
4609
|
-
width: `${COLUMN_WIDTH * 100}%`,
|
|
4610
|
-
height: pct2(fit.heightPx),
|
|
4611
|
-
anchor: "top-left"
|
|
4612
|
-
},
|
|
4613
|
-
animation: motionEntrance(
|
|
4614
|
-
context,
|
|
4615
|
-
getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
|
|
4616
|
-
)
|
|
4617
|
-
}
|
|
4618
|
-
];
|
|
4619
|
-
}
|
|
4620
|
-
|
|
4621
|
-
// src/doc/templates/listBlock.ts
|
|
4622
|
-
var LIST_ITEMS_FLOW = "list-items";
|
|
4623
|
-
var LIST_ITEM_LINE_HEIGHT = 1.2;
|
|
4624
|
-
var LIST_ITEM_BASE_FONT_PX = 34;
|
|
4625
|
-
var LIST_ITEM_GAP_PX = 18;
|
|
4626
|
-
var LIST_ITEM_MIN_FONT_PX = 16;
|
|
4627
|
-
var LIST_BOTTOM_MARGIN_PCT = 8;
|
|
4628
|
-
var LIST_STRIP_CLEARANCE_PCT = 2;
|
|
4629
|
-
var LIST_RELAXED_TITLE_TOP_PCT = 20;
|
|
4630
|
-
var LIST_RELAXED_ITEMS_TOP_PCT = 34;
|
|
4631
|
-
var LIST_RELAXED_UNTITLED_TOP_PCT = 26;
|
|
4632
|
-
var LIST_CONDENSED_TOP_PCT = 8;
|
|
4633
|
-
var LIST_CONDENSED_TITLE_GAP_PX = 22;
|
|
4634
|
-
var LIST_TITLE_LINE_HEIGHT = 1.15;
|
|
4635
|
-
function listItemGeometry(fontSize, baseFontSize, itemCount, textWidthPx, ordered = true) {
|
|
4636
|
-
const markerText = ordered ? `${itemCount}.` : "\u2022";
|
|
4637
|
-
const markerWidthPx = Math.max(fontSize, markerText.length * fontSize * 0.5);
|
|
4638
|
-
const markerGapPx = fontSize * 0.35;
|
|
4639
|
-
const bodyIndentPx = markerWidthPx + markerGapPx;
|
|
4640
|
-
return {
|
|
4641
|
-
markerWidthPx,
|
|
4642
|
-
bodyIndentPx,
|
|
4643
|
-
bodyWidthPx: Math.max(fontSize, textWidthPx - bodyIndentPx),
|
|
4644
|
-
gapPx: LIST_ITEM_GAP_PX * (fontSize / baseFontSize)
|
|
4645
|
-
};
|
|
4646
|
-
}
|
|
4647
|
-
function measureListHeightPx(items, fontSize, geometry) {
|
|
4648
|
-
let heightPx = 0;
|
|
4649
|
-
for (const itemText2 of items) {
|
|
4650
|
-
const lineCount = estimateProseLineCount(itemText2, fontSize, geometry.bodyWidthPx);
|
|
4651
|
-
heightPx += lineCount * fontSize * LIST_ITEM_LINE_HEIGHT + geometry.gapPx;
|
|
4652
|
-
}
|
|
4653
|
-
return heightPx;
|
|
4654
|
-
}
|
|
4655
|
-
function listBlock(input, context) {
|
|
4656
|
-
const { title } = input;
|
|
4657
|
-
const items = Array.isArray(input.items) ? input.items : [];
|
|
4658
|
-
const build = input.build ?? context.block?.build;
|
|
4659
|
-
if (build && build.mode !== "off") {
|
|
4660
|
-
const rich = extractRichListItems(context.block?.contents);
|
|
4661
|
-
const matches = rich.length === items.length && rich.every((item, i) => item.text === items[i]);
|
|
4662
|
-
const contents = matches ? context.block?.contents ?? [] : [
|
|
4663
|
-
{
|
|
4664
|
-
type: "list",
|
|
4665
|
-
ordered: input.ordered !== false,
|
|
4666
|
-
children: items.map((text) => ({
|
|
4667
|
-
type: "listItem",
|
|
4668
|
-
children: [{ type: "paragraph", children: [{ type: "text", value: text }] }]
|
|
4669
|
-
}))
|
|
4670
|
-
}
|
|
4671
|
-
];
|
|
4672
|
-
const layers2 = contentBlock(
|
|
4673
|
-
{ ...input, template: "content", title: input.title ?? "", build },
|
|
4674
|
-
{
|
|
4675
|
-
...context,
|
|
4676
|
-
block: {
|
|
4677
|
-
id: input.id,
|
|
4678
|
-
duration: input.duration,
|
|
4679
|
-
startTime: 0,
|
|
4680
|
-
audioSegment: input.audioSegment,
|
|
4681
|
-
...context.block,
|
|
4682
|
-
summaryMode: void 0,
|
|
4683
|
-
contents,
|
|
4684
|
-
build
|
|
4685
|
-
}
|
|
4686
|
-
}
|
|
4687
|
-
);
|
|
4688
|
-
const accent = accentForViewport(input.accentImage, context.viewport);
|
|
4689
|
-
if (accent) {
|
|
4690
|
-
const layout = getAccentLayout(accent.position);
|
|
4691
|
-
layers2.splice(
|
|
4692
|
-
1,
|
|
4693
|
-
0,
|
|
4694
|
-
...createAccentLayers(
|
|
4695
|
-
accent,
|
|
4696
|
-
input.id,
|
|
4697
|
-
themedImageTreatment(context, input.imageTreatment)
|
|
4698
|
-
)
|
|
4699
|
-
);
|
|
4700
|
-
for (const layer of layers2) {
|
|
4701
|
-
if (layer.type !== "text" || layer.id !== "title" && layer.id !== "body") continue;
|
|
4702
|
-
layer.position.x = `${parseFloat(layout.textCenterX) - parseFloat(layout.textWidth) / 2}%`;
|
|
4703
|
-
layer.position.width = layout.textWidth;
|
|
4704
|
-
layer.position.y = adjustY(String(layer.position.y), layout);
|
|
4705
|
-
if (layer.id === "body" && accent.position === "bottom-strip")
|
|
4706
|
-
layer.position.height = `${72 - ACCENT_STRIP_SIZE}%`;
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
return layers2;
|
|
4710
|
-
}
|
|
4711
|
-
const accentImage = accentForViewport(input.accentImage, context.viewport);
|
|
4712
|
-
const ordered = input.ordered !== false;
|
|
4713
|
-
const richItems = extractRichListItems(context.block?.contents);
|
|
4714
|
-
const { theme } = context;
|
|
4715
|
-
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
4716
|
-
const titleFontSize = themedFontSize(44, context, true);
|
|
4717
|
-
const baseItemFontSize = themedFontSize(LIST_ITEM_BASE_FONT_PX, context, false);
|
|
4718
|
-
const minItemFontSize = Math.min(
|
|
4719
|
-
baseItemFontSize,
|
|
4720
|
-
themedFontSize(LIST_ITEM_MIN_FONT_PX, context, false)
|
|
4721
|
-
);
|
|
4722
|
-
const layers = [
|
|
4723
|
-
// Background — gradient
|
|
4724
|
-
{
|
|
4725
|
-
type: "shape",
|
|
4726
|
-
id: "bg",
|
|
4727
|
-
content: {
|
|
4728
|
-
shape: "rect",
|
|
4729
|
-
fill: `linear-gradient(155deg, ${theme.colors.backgroundLight} 0%, ${theme.colors.background} 100%)`
|
|
4730
|
-
},
|
|
4731
|
-
position: { x: 0, y: 0, width: "100%", height: "100%" }
|
|
4732
|
-
}
|
|
4733
|
-
];
|
|
4734
|
-
if (accentImage) {
|
|
4735
|
-
layers.push(
|
|
4736
|
-
...createAccentLayers(
|
|
4737
|
-
accentImage,
|
|
4738
|
-
input.id,
|
|
4739
|
-
themedImageTreatment(context, input.imageTreatment)
|
|
4740
|
-
)
|
|
4741
|
-
);
|
|
4742
|
-
}
|
|
4743
|
-
const centerX = parseFloat(accentLayout.textCenterX);
|
|
4744
|
-
const widthPct = parseFloat(accentLayout.textWidth);
|
|
4745
|
-
const leftPct = Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
|
|
4746
|
-
const leftX = `${leftPct}%`;
|
|
4747
|
-
const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
|
|
4748
|
-
const bottomLimitPct = (accentImage?.position === "bottom-strip" ? 100 - ACCENT_STRIP_SIZE - LIST_STRIP_CLEARANCE_PCT : 100 - LIST_BOTTOM_MARGIN_PCT) - accentLayout.textYAdjust;
|
|
4749
|
-
const baseGeometry = listItemGeometry(
|
|
4750
|
-
baseItemFontSize,
|
|
4751
|
-
baseItemFontSize,
|
|
4752
|
-
items.length,
|
|
4753
|
-
textWidthPx,
|
|
4754
|
-
ordered
|
|
4956
|
+
maxHeightPx: bandHeightPx * BAND_FILL,
|
|
4957
|
+
lineHeight: LINE_HEIGHT
|
|
4958
|
+
});
|
|
4959
|
+
const topPx = Math.max(
|
|
4960
|
+
BAND_TOP3 * H,
|
|
4961
|
+
(BAND_TOP3 + (BAND_BOTTOM3 - BAND_TOP3) / 2) * H - fit.heightPx / 2
|
|
4755
4962
|
);
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4963
|
+
return [
|
|
4964
|
+
// Background — radial vignette built from the color scheme's own
|
|
4965
|
+
// surface, so the card takes on the scheme (and the theme behind it)
|
|
4966
|
+
// instead of vignetting every theme into hard black.
|
|
4967
|
+
{
|
|
4968
|
+
type: "shape",
|
|
4969
|
+
id: "bg",
|
|
4970
|
+
content: {
|
|
4971
|
+
shape: "rect",
|
|
4972
|
+
fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
|
|
4973
|
+
},
|
|
4974
|
+
position: { x: 0, y: 0, width: "100%", height: "100%" }
|
|
4975
|
+
},
|
|
4976
|
+
// The text — massive, centered, boxed so it can never leave the frame.
|
|
4977
|
+
{
|
|
4768
4978
|
type: "text",
|
|
4769
|
-
id: "
|
|
4979
|
+
id: "impact-text",
|
|
4770
4980
|
content: {
|
|
4771
|
-
text:
|
|
4981
|
+
text: text2,
|
|
4772
4982
|
style: {
|
|
4773
|
-
fontSize:
|
|
4983
|
+
fontSize: fit.fontSize,
|
|
4774
4984
|
fontFamily: getThemeFont(context, "title"),
|
|
4775
4985
|
fontWeight: "bold",
|
|
4776
|
-
color:
|
|
4777
|
-
textAlign: "
|
|
4778
|
-
|
|
4986
|
+
color: colors.text,
|
|
4987
|
+
textAlign: "center",
|
|
4988
|
+
lineHeight: LINE_HEIGHT,
|
|
4989
|
+
shadow: shouldUseShadow(context),
|
|
4990
|
+
// Real glyphs of wide display / mono / CJK faces exceed the 0.5em
|
|
4991
|
+
// wrap estimate; the renderer measures and scales into the box.
|
|
4992
|
+
shrinkToFit: true,
|
|
4993
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
4779
4994
|
}
|
|
4780
4995
|
},
|
|
4781
4996
|
position: {
|
|
4782
|
-
x:
|
|
4783
|
-
y:
|
|
4784
|
-
width:
|
|
4997
|
+
x: `${COLUMN_LEFT * 100}%`,
|
|
4998
|
+
y: pct2(topPx),
|
|
4999
|
+
width: `${COLUMN_WIDTH * 100}%`,
|
|
5000
|
+
height: pct2(fit.heightPx),
|
|
5001
|
+
anchor: "top-left"
|
|
4785
5002
|
},
|
|
4786
|
-
animation: motionEntrance(
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
}
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
5003
|
+
animation: motionEntrance(
|
|
5004
|
+
context,
|
|
5005
|
+
getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
|
|
5006
|
+
)
|
|
5007
|
+
}
|
|
5008
|
+
];
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
// src/doc/templates/conceptShapes.ts
|
|
5012
|
+
function region(id, d, fill, stroke, strokeWidth) {
|
|
5013
|
+
return {
|
|
5014
|
+
type: "path",
|
|
5015
|
+
id,
|
|
5016
|
+
position: { x: 0, y: 0, width: "100%", height: "100%" },
|
|
5017
|
+
content: { d, fill, stroke, strokeWidth }
|
|
5018
|
+
};
|
|
5019
|
+
}
|
|
5020
|
+
function disc(id, cx, cy, r, fill) {
|
|
5021
|
+
return {
|
|
5022
|
+
type: "shape",
|
|
5023
|
+
id,
|
|
5024
|
+
position: { x: cx - r, y: cy - r, width: r * 2, height: r * 2 },
|
|
5025
|
+
content: { shape: "circle", fill }
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
function unbrokenSize(text2, size, width, floor) {
|
|
5029
|
+
const longest = Math.max(1, ...text2.split(/\s+/u).map((word) => word.length));
|
|
5030
|
+
return Math.max(Math.min(size, floor), Math.min(size, Math.floor(width / (longest * 0.64))));
|
|
5031
|
+
}
|
|
5032
|
+
function panel(id, left, top, width, height, pad, ruleH, accent, context) {
|
|
5033
|
+
const { theme } = context;
|
|
5034
|
+
return [
|
|
5035
|
+
[
|
|
5036
|
+
{
|
|
5037
|
+
type: "shape",
|
|
5038
|
+
id,
|
|
5039
|
+
position: { x: left, y: top, width, height },
|
|
5040
|
+
content: {
|
|
5041
|
+
shape: "rect",
|
|
5042
|
+
fill: withAlpha(theme.colors.text, 0.06),
|
|
5043
|
+
stroke: withAlpha(theme.colors.text, 0.12),
|
|
5044
|
+
strokeWidth: 2,
|
|
5045
|
+
borderRadius: Math.round(Math.min(width, height) * 0.06)
|
|
4822
5046
|
}
|
|
4823
5047
|
},
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
5048
|
+
"surface",
|
|
5049
|
+
0
|
|
5050
|
+
],
|
|
5051
|
+
[
|
|
5052
|
+
{
|
|
5053
|
+
type: "shape",
|
|
5054
|
+
id: `${id}-rule`,
|
|
5055
|
+
position: { x: left + pad, y: top + pad, width: Math.min(width * 0.22, 64), height: ruleH },
|
|
5056
|
+
content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 }
|
|
4828
5057
|
},
|
|
4829
|
-
|
|
5058
|
+
"rule",
|
|
5059
|
+
0.2
|
|
5060
|
+
]
|
|
5061
|
+
];
|
|
5062
|
+
}
|
|
5063
|
+
var FIGURE_LINE_HEIGHT2 = 1.05;
|
|
5064
|
+
function statsBlock(input, context) {
|
|
5065
|
+
const crowded = asListWhenCrowded(input, context);
|
|
5066
|
+
if (crowded) return crowded;
|
|
5067
|
+
const stats = statItemsOf(input);
|
|
5068
|
+
if (conceptDrawsAs("stats", input.items) === "cards")
|
|
5069
|
+
return cardsBlock({ ...input, template: "cards" }, context);
|
|
5070
|
+
const area = frame(input, context);
|
|
5071
|
+
const { theme, viewport } = context;
|
|
5072
|
+
const W = area.right - area.left;
|
|
5073
|
+
const H = area.bottom - area.top;
|
|
5074
|
+
const n = stats.length;
|
|
5075
|
+
const columns2 = area.portrait ? n <= 3 ? 1 : 2 : n <= 4 ? n : 3;
|
|
5076
|
+
const rows = Math.ceil(n / columns2);
|
|
5077
|
+
const gutter = Math.round(viewport.width * (area.portrait ? 0.07 : 0.045));
|
|
5078
|
+
const rowGap = Math.round(viewport.height * 0.055);
|
|
5079
|
+
const cellW = (W - gutter * (columns2 - 1)) / columns2;
|
|
5080
|
+
const cellH = (H - rowGap * (rows - 1)) / rows;
|
|
5081
|
+
const figures = stats.map((stat) => splitFigure(stat.figure));
|
|
5082
|
+
const base = themedFontSize(
|
|
5083
|
+
rows > 1 && !area.portrait ? 124 : columns2 >= 4 ? 132 : columns2 === 3 ? 156 : 176,
|
|
5084
|
+
context,
|
|
5085
|
+
true
|
|
5086
|
+
);
|
|
5087
|
+
const floor = Math.min(base, themedFontSize(64, context, true));
|
|
5088
|
+
const figureSize = Math.min(
|
|
5089
|
+
...figures.map(
|
|
5090
|
+
({ number }) => number ? fitProse({
|
|
5091
|
+
text: number,
|
|
5092
|
+
baseFontSize: base,
|
|
5093
|
+
minFontSize: floor,
|
|
5094
|
+
maxWidthPx: cellW * 0.98,
|
|
5095
|
+
maxHeightPx: base * FIGURE_LINE_HEIGHT2,
|
|
5096
|
+
lineHeight: FIGURE_LINE_HEIGHT2
|
|
5097
|
+
}).fontSize : base
|
|
5098
|
+
)
|
|
5099
|
+
);
|
|
5100
|
+
const figureH = figureSize * FIGURE_LINE_HEIGHT2;
|
|
5101
|
+
const unitSize = Math.round(themedFontSize(LABEL_BASE, context, true) * (figureSize / base));
|
|
5102
|
+
const unitH = figures.some((figure) => figure.unit) ? unitSize * LABEL_LINE_HEIGHT : 0;
|
|
5103
|
+
const captionGap = Math.round(figureSize * 0.16);
|
|
5104
|
+
const captions = stats.map((stat) => ({ label: "", detail: stat.caption }));
|
|
5105
|
+
const { sizes } = fitRamp(
|
|
5106
|
+
captions,
|
|
5107
|
+
context,
|
|
5108
|
+
() => cellW,
|
|
5109
|
+
() => Math.max(0, cellH - figureH - unitH - captionGap)
|
|
5110
|
+
);
|
|
5111
|
+
const measures = captions.map((caption) => measureItem(caption, sizes, cellW));
|
|
5112
|
+
const captionH = Math.max(0, ...measures.map((m) => Number.isFinite(m.height) ? m.height : 0));
|
|
5113
|
+
const groupH = Math.min(cellH, figureH + unitH + captionGap + captionH);
|
|
5114
|
+
const gridH = rows * groupH + (rows - 1) * rowGap;
|
|
5115
|
+
const gridTop = area.top + Math.max(0, (H - gridH) * 0.38);
|
|
5116
|
+
const motion = getMotionProfile(context);
|
|
5117
|
+
const ruleW = Math.max(2, Math.round(viewport.width * 12e-4));
|
|
5118
|
+
const layers = [...area.layers];
|
|
5119
|
+
stats.forEach((stat, index) => {
|
|
5120
|
+
const row = Math.floor(index / columns2);
|
|
5121
|
+
const column = index % columns2;
|
|
5122
|
+
const inRow = Math.min(columns2, n - row * columns2);
|
|
5123
|
+
const rowLeft = area.left + (columns2 - inRow) * (cellW + gutter) / 2;
|
|
5124
|
+
const left = rowLeft + column * (cellW + gutter);
|
|
5125
|
+
const top = gridTop + row * (groupH + rowGap);
|
|
5126
|
+
const turn = FIRST_TURN + index * TURN_STAGGER;
|
|
5127
|
+
const { number, unit } = figures[index];
|
|
5128
|
+
const parts = [];
|
|
5129
|
+
if (column > 0)
|
|
5130
|
+
parts.push([
|
|
5131
|
+
{
|
|
5132
|
+
type: "shape",
|
|
5133
|
+
id: `stat-${index}-divider`,
|
|
5134
|
+
position: { x: left - gutter / 2 - ruleW / 2, y: top, width: ruleW, height: groupH },
|
|
5135
|
+
content: { shape: "rect", fill: withAlpha(theme.colors.text, 0.18) }
|
|
5136
|
+
},
|
|
5137
|
+
"bar",
|
|
5138
|
+
0
|
|
5139
|
+
]);
|
|
5140
|
+
const built = step(parts, index, context);
|
|
5141
|
+
if (number) {
|
|
5142
|
+
const count = motion.countUp ? countUpSpecFor(number) : null;
|
|
5143
|
+
built.push({
|
|
5144
|
+
...textLayer(`stat-${index}`, number, box(left, top, cellW, figureH), {
|
|
5145
|
+
fontSize: figureSize,
|
|
5146
|
+
fontFamily: getThemeFont(context, "title"),
|
|
5147
|
+
fontWeight: "bold",
|
|
5148
|
+
color: area.accent,
|
|
5149
|
+
lineHeight: FIGURE_LINE_HEIGHT2,
|
|
5150
|
+
shadow: shouldUseShadow(context)
|
|
5151
|
+
}),
|
|
5152
|
+
buildStep: index,
|
|
5153
|
+
animation: count ? {
|
|
5154
|
+
type: "countUp",
|
|
5155
|
+
duration: motion.countUpDuration,
|
|
5156
|
+
delay: motionDelay(context, turn + 0.1),
|
|
5157
|
+
count
|
|
5158
|
+
} : entrance(context, "pop", turn + 0.1)
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
const rest = [];
|
|
5162
|
+
if (unit)
|
|
5163
|
+
rest.push([
|
|
5164
|
+
textLayer(`stat-${index}-unit`, unit, box(left, top + figureH, cellW, unitH), {
|
|
5165
|
+
fontSize: unitSize,
|
|
5166
|
+
fontFamily: getThemeFont(context, "title"),
|
|
5167
|
+
fontWeight: "bold",
|
|
5168
|
+
color: area.accent,
|
|
5169
|
+
lineHeight: LABEL_LINE_HEIGHT
|
|
5170
|
+
}),
|
|
5171
|
+
"text",
|
|
5172
|
+
0.25
|
|
5173
|
+
]);
|
|
5174
|
+
rest.push(
|
|
5175
|
+
...words(
|
|
5176
|
+
itemText(
|
|
5177
|
+
`stat-${index}`,
|
|
5178
|
+
captions[index],
|
|
5179
|
+
measures[index],
|
|
5180
|
+
sizes,
|
|
5181
|
+
left,
|
|
5182
|
+
top + (number ? figureH + unitH + captionGap : 0),
|
|
5183
|
+
cellW,
|
|
5184
|
+
"left",
|
|
5185
|
+
context
|
|
5186
|
+
),
|
|
5187
|
+
0.35
|
|
5188
|
+
)
|
|
5189
|
+
);
|
|
5190
|
+
layers.push(...built, ...step(rest, index, context));
|
|
5191
|
+
});
|
|
5192
|
+
return layers;
|
|
5193
|
+
}
|
|
5194
|
+
var FUNNEL_STEM = 0.3;
|
|
5195
|
+
function tiers(kind, input, context) {
|
|
5196
|
+
const crowded = asListWhenCrowded(input, context);
|
|
5197
|
+
if (crowded) return crowded;
|
|
5198
|
+
const items = itemsOf(input);
|
|
5199
|
+
if (conceptDrawsAs(kind, input.items) === "cards")
|
|
5200
|
+
return cardsBlock({ ...input, template: "cards" }, context);
|
|
5201
|
+
const area = frame(input, context);
|
|
5202
|
+
const { theme, viewport } = context;
|
|
5203
|
+
const W = area.right - area.left;
|
|
5204
|
+
const H = area.bottom - area.top;
|
|
5205
|
+
const n = items.length;
|
|
5206
|
+
const portrait = area.portrait;
|
|
5207
|
+
const gap = Math.round(viewport.width * (portrait ? 0.02 : 0.045));
|
|
5208
|
+
const rowGap = Math.round(viewport.height * 0.02);
|
|
5209
|
+
const listGap = viewport.height * 0.045;
|
|
5210
|
+
const top = area.top;
|
|
5211
|
+
const listW = W - gap * 1.5;
|
|
5212
|
+
const listed = portrait ? fitRamp(
|
|
5213
|
+
items,
|
|
5214
|
+
context,
|
|
5215
|
+
() => listW,
|
|
5216
|
+
() => (H * 0.64 - listGap - rowGap * (n - 1)) / n
|
|
5217
|
+
).sizes : void 0;
|
|
5218
|
+
const listH = listed ? items.reduce(
|
|
5219
|
+
(sum, item) => {
|
|
5220
|
+
const height = measureItem(item, listed, listW).height;
|
|
5221
|
+
return sum + (Number.isFinite(height) ? height : 0);
|
|
5222
|
+
},
|
|
5223
|
+
rowGap * (n - 1)
|
|
5224
|
+
) : 0;
|
|
5225
|
+
const shapeH = Math.round(
|
|
5226
|
+
portrait ? Math.max(H * 0.36, Math.min(H * 0.5, H - listH - listGap)) : H
|
|
5227
|
+
);
|
|
5228
|
+
const shapeW = Math.round(
|
|
5229
|
+
portrait ? Math.min(W * 0.78, shapeH * 1.5) : Math.min(W * 0.4, H * 1.1)
|
|
5230
|
+
);
|
|
5231
|
+
const shapeLeft = portrait ? area.left + (W - shapeW) / 2 : area.left;
|
|
5232
|
+
const cx = shapeLeft + shapeW / 2;
|
|
5233
|
+
const slit = Math.max(3, Math.round(viewport.height * 7e-3));
|
|
5234
|
+
const tierH = shapeH / n;
|
|
5235
|
+
const half = (y) => kind === "pyramid" ? shapeW / 2 * (y / shapeH) : shapeW / 2 * (1 - (1 - FUNNEL_STEM) * (y / shapeH));
|
|
5236
|
+
const tint = (index) => {
|
|
5237
|
+
const depth = n > 1 ? index / (n - 1) : 0;
|
|
5238
|
+
return withAlpha(area.accent, kind === "pyramid" ? 0.92 - 0.6 * depth : 0.32 + 0.6 * depth);
|
|
5239
|
+
};
|
|
5240
|
+
const textLeft = portrait ? area.left + gap * 1.5 : shapeLeft + shapeW + gap;
|
|
5241
|
+
const textW = area.right - textLeft;
|
|
5242
|
+
const textTop = portrait ? top + shapeH + listGap : top;
|
|
5243
|
+
const sizes = listed ?? fitRamp(
|
|
5244
|
+
items,
|
|
5245
|
+
context,
|
|
5246
|
+
() => textW,
|
|
5247
|
+
() => tierH - rowGap
|
|
5248
|
+
).sizes;
|
|
5249
|
+
const measures = items.map((item) => measureItem(item, sizes, textW));
|
|
5250
|
+
const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
|
|
5251
|
+
const shapes = [];
|
|
5252
|
+
const callouts = [];
|
|
5253
|
+
let rowTop = textTop;
|
|
5254
|
+
items.forEach((item, index) => {
|
|
5255
|
+
const y0 = index * tierH;
|
|
5256
|
+
const y1 = y0 + tierH - (index < n - 1 ? slit : 0);
|
|
5257
|
+
const [a, b] = [half(y0), half(y1)];
|
|
5258
|
+
shapes.push(
|
|
5259
|
+
...step(
|
|
5260
|
+
[
|
|
5261
|
+
[
|
|
5262
|
+
region(
|
|
5263
|
+
`tier-${index}`,
|
|
5264
|
+
`M ${cx - a} ${top + y0} L ${cx + a} ${top + y0} L ${cx + b} ${top + y1} L ${cx - b} ${top + y1} Z`,
|
|
5265
|
+
tint(index),
|
|
5266
|
+
"none",
|
|
5267
|
+
0
|
|
5268
|
+
),
|
|
5269
|
+
"surface",
|
|
5270
|
+
0
|
|
5271
|
+
]
|
|
5272
|
+
],
|
|
5273
|
+
index,
|
|
5274
|
+
context
|
|
5275
|
+
)
|
|
5276
|
+
);
|
|
5277
|
+
const m = measures[index];
|
|
5278
|
+
const height = Number.isFinite(m.height) ? m.height : tierH;
|
|
5279
|
+
const middle = top + (y0 + y1) / 2;
|
|
5280
|
+
const parts = [];
|
|
5281
|
+
let blockTop;
|
|
5282
|
+
if (portrait) {
|
|
5283
|
+
const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
|
|
5284
|
+
const swatch = Math.round(sizes.label * 0.55);
|
|
5285
|
+
parts.push([
|
|
5286
|
+
{
|
|
5287
|
+
type: "shape",
|
|
5288
|
+
id: `tier-${index}-swatch`,
|
|
5289
|
+
position: {
|
|
5290
|
+
x: area.left,
|
|
5291
|
+
y: rowTop + firstLine / 2 - swatch / 2,
|
|
5292
|
+
width: swatch,
|
|
5293
|
+
height: swatch
|
|
5294
|
+
},
|
|
5295
|
+
content: { shape: "rect", fill: tint(index), borderRadius: Math.round(swatch * 0.2) }
|
|
5296
|
+
},
|
|
5297
|
+
"pop",
|
|
5298
|
+
0.2
|
|
5299
|
+
]);
|
|
5300
|
+
blockTop = rowTop;
|
|
5301
|
+
rowTop += height + rowGap;
|
|
5302
|
+
} else {
|
|
5303
|
+
const edge = cx + half((y0 + y1) / 2);
|
|
5304
|
+
const dot = Math.max(8, Math.round(viewport.height * 9e-3));
|
|
5305
|
+
parts.push(
|
|
5306
|
+
[
|
|
5307
|
+
connector(
|
|
5308
|
+
`tier-${index}-callout`,
|
|
5309
|
+
`M ${edge + dot} ${middle} L ${textLeft - gap * 0.25} ${middle}`,
|
|
5310
|
+
withAlpha(theme.colors.text, 0.45),
|
|
5311
|
+
stroke,
|
|
5312
|
+
false
|
|
5313
|
+
),
|
|
5314
|
+
"draw",
|
|
5315
|
+
0.2
|
|
5316
|
+
],
|
|
5317
|
+
[
|
|
5318
|
+
disc(`tier-${index}-pin`, edge + dot / 2, middle, dot / 2, theme.colors.text),
|
|
5319
|
+
"pop",
|
|
5320
|
+
0.15
|
|
5321
|
+
]
|
|
5322
|
+
);
|
|
5323
|
+
blockTop = Math.max(area.top, middle - height / 2);
|
|
5324
|
+
}
|
|
5325
|
+
parts.push(
|
|
5326
|
+
...words(
|
|
5327
|
+
itemText(`tier-${index}`, item, m, sizes, textLeft, blockTop, textW, "left", context),
|
|
5328
|
+
0.35
|
|
5329
|
+
)
|
|
5330
|
+
);
|
|
5331
|
+
callouts.push(...step(parts, index, context));
|
|
5332
|
+
});
|
|
5333
|
+
return [...area.layers, ...shapes, ...callouts];
|
|
5334
|
+
}
|
|
5335
|
+
function pyramidBlock(input, context) {
|
|
5336
|
+
return tiers("pyramid", input, context);
|
|
5337
|
+
}
|
|
5338
|
+
function funnelBlock(input, context) {
|
|
5339
|
+
return tiers("funnel", input, context);
|
|
5340
|
+
}
|
|
5341
|
+
function hubBlock(input, context) {
|
|
5342
|
+
const crowded = asListWhenCrowded(input, context);
|
|
5343
|
+
if (crowded) return crowded;
|
|
5344
|
+
const items = itemsOf(input);
|
|
5345
|
+
if (conceptDrawsAs("hub", input.items) === "cards")
|
|
5346
|
+
return cardsBlock({ ...input, template: "cards" }, context);
|
|
5347
|
+
const area = frame(input, context);
|
|
5348
|
+
const { viewport } = context;
|
|
5349
|
+
const W = area.right - area.left;
|
|
5350
|
+
const H = area.bottom - area.top;
|
|
5351
|
+
const n = items.length;
|
|
5352
|
+
const portrait = area.portrait;
|
|
5353
|
+
const center = String(input.center ?? "").replace(/\s+/gu, " ").trim();
|
|
5354
|
+
const hubR = Math.round(
|
|
5355
|
+
Math.min(viewport.height, viewport.width) * (center ? portrait ? 0.09 : 0.115 : 0.055)
|
|
5356
|
+
);
|
|
5357
|
+
const dot = Math.round(Math.min(viewport.height, viewport.width) * 0.034);
|
|
5358
|
+
const gap = Math.round(dot * 0.6);
|
|
5359
|
+
const cx = portrait ? area.left + hubR : (area.left + area.right) / 2;
|
|
5360
|
+
const cy = area.top + H / 2;
|
|
5361
|
+
const rx = portrait ? W * 0.4 : Math.min(H * 0.42, W * 0.2);
|
|
5362
|
+
const ry = portrait ? H * 0.42 : rx;
|
|
5363
|
+
const leftCount = portrait ? 0 : Math.ceil(n / 2);
|
|
5364
|
+
const spots = items.map((_, index) => {
|
|
5365
|
+
const onLeft = index < leftCount;
|
|
5366
|
+
const count = onLeft ? leftCount : n - leftCount;
|
|
5367
|
+
const at = onLeft ? index : index - leftCount;
|
|
5368
|
+
const spread = portrait ? 124 : count <= 1 ? 0 : count === 2 ? 70 : 110;
|
|
5369
|
+
const degrees = count <= 1 ? 0 : -spread / 2 + spread * at / (count - 1);
|
|
5370
|
+
const theta = degrees * Math.PI / 180;
|
|
5371
|
+
return {
|
|
5372
|
+
onLeft,
|
|
5373
|
+
x: cx + (onLeft ? -1 : 1) * rx * Math.cos(theta),
|
|
5374
|
+
y: cy + ry * Math.sin(theta)
|
|
5375
|
+
};
|
|
5376
|
+
});
|
|
5377
|
+
const labelW = (index) => spots[index].onLeft ? spots[index].x - dot / 2 - gap - area.left : area.right - (spots[index].x + dot / 2 + gap);
|
|
5378
|
+
const rowH = (index) => {
|
|
5379
|
+
const side = spots.filter((spot) => spot.onLeft === spots[index].onLeft);
|
|
5380
|
+
if (side.length < 2) return H * 0.8;
|
|
5381
|
+
return (side[side.length - 1].y - side[0].y) / (side.length - 1) * 0.9;
|
|
5382
|
+
};
|
|
5383
|
+
const scale = Math.min(
|
|
5384
|
+
...items.map(
|
|
5385
|
+
(item, index) => fitRamp(
|
|
5386
|
+
[item],
|
|
5387
|
+
context,
|
|
5388
|
+
() => labelW(index),
|
|
5389
|
+
() => rowH(index)
|
|
5390
|
+
).scale
|
|
5391
|
+
)
|
|
5392
|
+
);
|
|
5393
|
+
const sizes = ramp(context, scale);
|
|
5394
|
+
const measures = items.map((item, index) => measureItem(item, sizes, labelW(index)));
|
|
5395
|
+
const line = withAlpha(area.accent, 0.6);
|
|
5396
|
+
const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
|
|
5397
|
+
const layers = [...area.layers];
|
|
5398
|
+
layers.push({
|
|
5399
|
+
...disc("hub", cx, cy, hubR, area.accent),
|
|
5400
|
+
animation: entrance(context, "pop", 0.35)
|
|
5401
|
+
});
|
|
5402
|
+
if (center) {
|
|
5403
|
+
const side = hubR * 1.5;
|
|
5404
|
+
const floor = themedFontSize(18, context, true);
|
|
5405
|
+
const size = unbrokenSize(center, themedFontSize(36, context, true), side, floor);
|
|
5406
|
+
const fit = fitProse({
|
|
5407
|
+
text: center,
|
|
5408
|
+
baseFontSize: size,
|
|
5409
|
+
minFontSize: Math.min(size, floor),
|
|
5410
|
+
maxWidthPx: side,
|
|
5411
|
+
maxHeightPx: side,
|
|
5412
|
+
lineHeight: 1.15
|
|
4830
5413
|
});
|
|
4831
5414
|
layers.push({
|
|
4832
5415
|
type: "text",
|
|
4833
|
-
id:
|
|
5416
|
+
id: "hub-center",
|
|
5417
|
+
position: { x: cx, y: cy, width: side, height: side, anchor: "center" },
|
|
4834
5418
|
content: {
|
|
4835
|
-
text:
|
|
4836
|
-
...itemHtml ? { html: itemHtml } : {},
|
|
4837
|
-
paragraphs: [
|
|
4838
|
-
{
|
|
4839
|
-
text: itemText2,
|
|
4840
|
-
list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
|
|
4841
|
-
}
|
|
4842
|
-
],
|
|
4843
|
-
flow: { id: LIST_ITEMS_FLOW },
|
|
5419
|
+
text: center,
|
|
4844
5420
|
style: {
|
|
4845
|
-
fontSize:
|
|
4846
|
-
fontFamily: getThemeFont(context, "
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
5421
|
+
fontSize: fit.fontSize,
|
|
5422
|
+
fontFamily: getThemeFont(context, "title"),
|
|
5423
|
+
fontWeight: "bold",
|
|
5424
|
+
color: pickContrastingText(area.accent),
|
|
5425
|
+
textAlign: "center",
|
|
5426
|
+
verticalAlign: "middle",
|
|
5427
|
+
lineHeight: 1.15,
|
|
5428
|
+
shrinkToFit: true,
|
|
5429
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
4851
5430
|
}
|
|
4852
5431
|
},
|
|
4853
|
-
|
|
4854
|
-
x: bodyLeftX,
|
|
4855
|
-
y: adjustY(`${itemY}%`, accentLayout),
|
|
4856
|
-
width: bodyWidthPx,
|
|
4857
|
-
// Rich (HTML) items render in a hidden-overflow box the renderer
|
|
4858
|
-
// sizes from the plain text; size it here from the same line count
|
|
4859
|
-
// that reserved the vertical space so nested sub-items are not cut off.
|
|
4860
|
-
height: lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT + itemFontSize * 0.5
|
|
4861
|
-
},
|
|
4862
|
-
animation
|
|
5432
|
+
animation: entrance(context, "pop", 0.35)
|
|
4863
5433
|
});
|
|
4864
|
-
const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
|
|
4865
|
-
itemY += (itemHeightPx + gapPx) / context.viewport.height * 100;
|
|
4866
5434
|
}
|
|
5435
|
+
items.forEach((item, index) => {
|
|
5436
|
+
const { x, y, onLeft } = spots[index];
|
|
5437
|
+
const length = Math.hypot(x - cx, y - cy) || 1;
|
|
5438
|
+
const [ux, uy] = [(x - cx) / length, (y - cy) / length];
|
|
5439
|
+
const from = hubR + dot * 0.35;
|
|
5440
|
+
const to = length - dot / 2 - dot * 0.15;
|
|
5441
|
+
const m = measures[index];
|
|
5442
|
+
const height = Number.isFinite(m.height) ? m.height : 0;
|
|
5443
|
+
const width = labelW(index);
|
|
5444
|
+
const left = onLeft ? x - dot / 2 - gap - width : x + dot / 2 + gap;
|
|
5445
|
+
const top = Math.min(
|
|
5446
|
+
Math.max(area.top, y - height / 2),
|
|
5447
|
+
Math.max(area.top, area.bottom - height)
|
|
5448
|
+
);
|
|
5449
|
+
layers.push(
|
|
5450
|
+
...step(
|
|
5451
|
+
[
|
|
5452
|
+
[
|
|
5453
|
+
connector(
|
|
5454
|
+
`spoke-${index}`,
|
|
5455
|
+
`M ${cx + from * ux} ${cy + from * uy} L ${cx + to * ux} ${cy + to * uy}`,
|
|
5456
|
+
line,
|
|
5457
|
+
stroke,
|
|
5458
|
+
false
|
|
5459
|
+
),
|
|
5460
|
+
"draw",
|
|
5461
|
+
0
|
|
5462
|
+
],
|
|
5463
|
+
[disc(`spoke-${index}-end`, x, y, dot / 2, area.accent), "pop", 0.3],
|
|
5464
|
+
...words(
|
|
5465
|
+
itemText(
|
|
5466
|
+
`spoke-${index}`,
|
|
5467
|
+
item,
|
|
5468
|
+
m,
|
|
5469
|
+
sizes,
|
|
5470
|
+
left,
|
|
5471
|
+
top,
|
|
5472
|
+
width,
|
|
5473
|
+
onLeft ? "right" : "left",
|
|
5474
|
+
context
|
|
5475
|
+
),
|
|
5476
|
+
0.45
|
|
5477
|
+
)
|
|
5478
|
+
],
|
|
5479
|
+
index,
|
|
5480
|
+
context
|
|
5481
|
+
)
|
|
5482
|
+
);
|
|
5483
|
+
});
|
|
5484
|
+
return layers;
|
|
5485
|
+
}
|
|
5486
|
+
function secondColor(accent, context) {
|
|
5487
|
+
const { colors, colorSchemes } = context.theme;
|
|
5488
|
+
const hue = (color) => isHex(color) ? hexHueDegrees(color) : NaN;
|
|
5489
|
+
const apart = (color) => {
|
|
5490
|
+
const turn = Math.abs(hue(color) - hue(accent)) % 360;
|
|
5491
|
+
return Math.min(turn, 360 - turn);
|
|
5492
|
+
};
|
|
5493
|
+
const candidates = [
|
|
5494
|
+
...Object.values(colorSchemes ?? {}).map((scheme) => scheme.accent),
|
|
5495
|
+
colors.secondary,
|
|
5496
|
+
colors.primary,
|
|
5497
|
+
colors.highlight
|
|
5498
|
+
];
|
|
5499
|
+
return candidates.find((color) => !!color && apart(color) >= 50) ?? colors.text;
|
|
5500
|
+
}
|
|
5501
|
+
function vennBlock(input, context) {
|
|
5502
|
+
const crowded = asListWhenCrowded(input, context);
|
|
5503
|
+
if (crowded) return crowded;
|
|
5504
|
+
const all = itemsOf(input);
|
|
5505
|
+
const sharedAt = vennOverlapIndex(all);
|
|
5506
|
+
const sets = all.map((item, index) => ({ item, index })).filter(({ index }) => index !== sharedAt);
|
|
5507
|
+
if (conceptDrawsAs("venn", input.items) === "cards")
|
|
5508
|
+
return cardsBlock({ ...input, template: "cards" }, context);
|
|
5509
|
+
const shared = sharedAt >= 0 ? all[sharedAt] : void 0;
|
|
5510
|
+
const area = frame(input, context);
|
|
5511
|
+
const { theme, viewport } = context;
|
|
5512
|
+
const W = area.right - area.left;
|
|
5513
|
+
const H = area.bottom - area.top;
|
|
5514
|
+
const portrait = area.portrait;
|
|
5515
|
+
const colors = [area.accent, secondColor(area.accent, context)];
|
|
5516
|
+
const mid = (area.left + area.right) / 2;
|
|
5517
|
+
const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
|
|
5518
|
+
const gap = Math.round(viewport.width * 0.025);
|
|
5519
|
+
const layers = [...area.layers];
|
|
5520
|
+
const captionSize = themedFontSize(DETAIL_BASE * 1.1, context, false);
|
|
5521
|
+
const captionW = portrait ? W : W * 0.62;
|
|
5522
|
+
const captionLines = shared?.detail ? Math.min(3, estimateProseLineCount(shared.detail, captionSize, captionW)) : 0;
|
|
5523
|
+
const sharedLabelH = shared?.label && !portrait ? captionSize * 1.5 : 0;
|
|
5524
|
+
const band = shared && !portrait ? sharedLabelH + captionLines * captionSize * DETAIL_LINE_HEIGHT + viewport.height * 0.04 : 0;
|
|
5525
|
+
const circlesH = portrait ? H * 0.34 : H - band;
|
|
5526
|
+
const R = Math.round(Math.min(circlesH / 2, (portrait ? W : W * 0.34) / 2));
|
|
5527
|
+
const distance = R * 1.12;
|
|
5528
|
+
const cy = area.top + circlesH / 2;
|
|
5529
|
+
const centres = [mid - distance / 2, mid + distance / 2];
|
|
5530
|
+
const lensHalf = Math.sqrt(Math.max(0, R * R - (distance / 2) ** 2));
|
|
5531
|
+
const sideW = portrait ? W - gap * 1.5 : mid - distance / 2 - R - gap - area.left;
|
|
5532
|
+
const rowGap = Math.round(viewport.height * 0.025);
|
|
5533
|
+
const listTop = cy + R + viewport.height * 0.045;
|
|
5534
|
+
const listed = portrait ? [...sets.map(({ item }) => item), ...shared ? [shared] : []] : [];
|
|
5535
|
+
const { sizes } = fitRamp(
|
|
5536
|
+
portrait ? listed : sets.map(({ item }) => ({ label: "", detail: item.detail })),
|
|
5537
|
+
context,
|
|
5538
|
+
() => sideW,
|
|
5539
|
+
() => portrait ? (area.bottom - listTop - rowGap * (listed.length - 1)) / Math.max(1, listed.length) : circlesH * 0.9
|
|
5540
|
+
);
|
|
5541
|
+
let rowTop = listTop;
|
|
5542
|
+
const row = (id, item, fill) => {
|
|
5543
|
+
const m = measureItem(item, sizes, sideW);
|
|
5544
|
+
const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
|
|
5545
|
+
const swatch = Math.round(sizes.label * 0.5);
|
|
5546
|
+
const parts = [
|
|
5547
|
+
[
|
|
5548
|
+
disc(`${id}-swatch`, area.left + swatch / 2, rowTop + firstLine / 2, swatch / 2, fill),
|
|
5549
|
+
"pop",
|
|
5550
|
+
0.2
|
|
5551
|
+
],
|
|
5552
|
+
...words(
|
|
5553
|
+
itemText(id, item, m, sizes, area.left + gap * 1.5, rowTop, sideW, "left", context),
|
|
5554
|
+
0.3
|
|
5555
|
+
)
|
|
5556
|
+
];
|
|
5557
|
+
rowTop += (Number.isFinite(m.height) ? m.height : firstLine) + rowGap;
|
|
5558
|
+
return parts;
|
|
5559
|
+
};
|
|
5560
|
+
sets.forEach(({ item, index }, side) => {
|
|
5561
|
+
const cx = centres[side];
|
|
5562
|
+
const fill = colors[side];
|
|
5563
|
+
const parts = [
|
|
5564
|
+
[
|
|
5565
|
+
{
|
|
5566
|
+
type: "shape",
|
|
5567
|
+
id: `set-${side}`,
|
|
5568
|
+
position: { x: cx - R, y: cy - R, width: R * 2, height: R * 2 },
|
|
5569
|
+
content: {
|
|
5570
|
+
shape: "circle",
|
|
5571
|
+
fill: withAlpha(fill, 0.3),
|
|
5572
|
+
stroke: withAlpha(fill, 0.9),
|
|
5573
|
+
strokeWidth: stroke
|
|
5574
|
+
}
|
|
5575
|
+
},
|
|
5576
|
+
"pop",
|
|
5577
|
+
0
|
|
5578
|
+
]
|
|
5579
|
+
];
|
|
5580
|
+
if (portrait) {
|
|
5581
|
+
parts.push(...row(`set-${side}`, item, withAlpha(fill, 0.75)));
|
|
5582
|
+
} else {
|
|
5583
|
+
const own = R * 2 - (R * 2 - distance);
|
|
5584
|
+
const lobeW = own * 0.74;
|
|
5585
|
+
const lobeX = side === 0 ? cx - R + own * 0.52 : cx + R - own * 0.52;
|
|
5586
|
+
if (item.label) {
|
|
5587
|
+
const floor = themedFontSize(20, context, true);
|
|
5588
|
+
const size = unbrokenSize(
|
|
5589
|
+
item.label,
|
|
5590
|
+
themedFontSize(LABEL_BASE, context, true),
|
|
5591
|
+
lobeW,
|
|
5592
|
+
floor
|
|
5593
|
+
);
|
|
5594
|
+
const fit = fitProse({
|
|
5595
|
+
text: item.label,
|
|
5596
|
+
baseFontSize: size,
|
|
5597
|
+
minFontSize: Math.min(size, floor),
|
|
5598
|
+
maxWidthPx: lobeW,
|
|
5599
|
+
maxHeightPx: R * 0.9,
|
|
5600
|
+
lineHeight: LABEL_LINE_HEIGHT
|
|
5601
|
+
});
|
|
5602
|
+
parts.push([
|
|
5603
|
+
{
|
|
5604
|
+
type: "text",
|
|
5605
|
+
id: `set-${side}-label`,
|
|
5606
|
+
position: { x: lobeX, y: cy, width: lobeW, height: R * 0.9, anchor: "center" },
|
|
5607
|
+
content: {
|
|
5608
|
+
text: item.label,
|
|
5609
|
+
style: {
|
|
5610
|
+
fontSize: fit.fontSize,
|
|
5611
|
+
fontFamily: getThemeFont(context, "title"),
|
|
5612
|
+
fontWeight: "bold",
|
|
5613
|
+
color: theme.colors.text,
|
|
5614
|
+
textAlign: "center",
|
|
5615
|
+
verticalAlign: "middle",
|
|
5616
|
+
lineHeight: LABEL_LINE_HEIGHT,
|
|
5617
|
+
shrinkToFit: true,
|
|
5618
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
},
|
|
5622
|
+
"text",
|
|
5623
|
+
0.25
|
|
5624
|
+
]);
|
|
5625
|
+
}
|
|
5626
|
+
if (item.detail) {
|
|
5627
|
+
const detail = { label: "", detail: item.detail };
|
|
5628
|
+
const m = measureItem(detail, sizes, sideW);
|
|
5629
|
+
const height = Number.isFinite(m.height) ? m.height : 0;
|
|
5630
|
+
const left = side === 0 ? area.left : area.right - sideW;
|
|
5631
|
+
parts.push(
|
|
5632
|
+
...words(
|
|
5633
|
+
itemText(
|
|
5634
|
+
`set-${side}`,
|
|
5635
|
+
detail,
|
|
5636
|
+
m,
|
|
5637
|
+
sizes,
|
|
5638
|
+
left,
|
|
5639
|
+
Math.max(area.top, cy - height / 2),
|
|
5640
|
+
sideW,
|
|
5641
|
+
side === 0 ? "right" : "left",
|
|
5642
|
+
context
|
|
5643
|
+
),
|
|
5644
|
+
0.4
|
|
5645
|
+
)
|
|
5646
|
+
);
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
layers.push(...step(parts, index, context));
|
|
5650
|
+
});
|
|
5651
|
+
if (shared) {
|
|
5652
|
+
const lens = region(
|
|
5653
|
+
"overlap",
|
|
5654
|
+
`M ${mid} ${cy - lensHalf} A ${R} ${R} 0 0 1 ${mid} ${cy + lensHalf} A ${R} ${R} 0 0 1 ${mid} ${cy - lensHalf} Z`,
|
|
5655
|
+
withAlpha(theme.colors.text, 0.16),
|
|
5656
|
+
withAlpha(theme.colors.text, 0.5),
|
|
5657
|
+
stroke
|
|
5658
|
+
);
|
|
5659
|
+
const parts = [[lens, "surface", 0]];
|
|
5660
|
+
if (portrait) {
|
|
5661
|
+
parts.push(...row("overlap", shared, withAlpha(theme.colors.text, 0.5)));
|
|
5662
|
+
} else {
|
|
5663
|
+
const top = cy + R + viewport.height * 0.02;
|
|
5664
|
+
parts.push([
|
|
5665
|
+
connector(
|
|
5666
|
+
"overlap-callout",
|
|
5667
|
+
`M ${mid} ${cy + lensHalf * 0.35} L ${mid} ${top}`,
|
|
5668
|
+
withAlpha(theme.colors.text, 0.5),
|
|
5669
|
+
stroke,
|
|
5670
|
+
false
|
|
5671
|
+
),
|
|
5672
|
+
"draw",
|
|
5673
|
+
0.2
|
|
5674
|
+
]);
|
|
5675
|
+
if (shared.label)
|
|
5676
|
+
parts.push([
|
|
5677
|
+
textLayer(
|
|
5678
|
+
"overlap-label",
|
|
5679
|
+
shared.label,
|
|
5680
|
+
box(mid - captionW / 2, top + viewport.height * 0.012, captionW, sharedLabelH),
|
|
5681
|
+
{
|
|
5682
|
+
fontSize: captionSize,
|
|
5683
|
+
fontFamily: getThemeFont(context, "title"),
|
|
5684
|
+
fontWeight: "bold",
|
|
5685
|
+
color: theme.colors.text,
|
|
5686
|
+
lineHeight: LABEL_LINE_HEIGHT,
|
|
5687
|
+
textAlign: "center"
|
|
5688
|
+
}
|
|
5689
|
+
),
|
|
5690
|
+
"text",
|
|
5691
|
+
0.3
|
|
5692
|
+
]);
|
|
5693
|
+
if (shared.detail)
|
|
5694
|
+
parts.push([
|
|
5695
|
+
textLayer(
|
|
5696
|
+
"overlap-detail",
|
|
5697
|
+
shared.detail,
|
|
5698
|
+
box(
|
|
5699
|
+
mid - captionW / 2,
|
|
5700
|
+
top + viewport.height * 0.012 + sharedLabelH,
|
|
5701
|
+
captionW,
|
|
5702
|
+
captionLines * captionSize * DETAIL_LINE_HEIGHT + captionSize * 0.4
|
|
5703
|
+
),
|
|
5704
|
+
{
|
|
5705
|
+
fontSize: captionSize,
|
|
5706
|
+
fontFamily: getThemeFont(context, "body"),
|
|
5707
|
+
color: theme.colors.text,
|
|
5708
|
+
lineHeight: DETAIL_LINE_HEIGHT,
|
|
5709
|
+
textAlign: "center",
|
|
5710
|
+
maxLines: 3
|
|
5711
|
+
}
|
|
5712
|
+
),
|
|
5713
|
+
"text",
|
|
5714
|
+
0.4
|
|
5715
|
+
]);
|
|
5716
|
+
}
|
|
5717
|
+
layers.push(...step(parts, sharedAt, context));
|
|
5718
|
+
}
|
|
5719
|
+
return layers;
|
|
5720
|
+
}
|
|
5721
|
+
var MATRIX_SCALES = [1.5, 1.32];
|
|
5722
|
+
function matrixBlock(input, context) {
|
|
5723
|
+
const crowded = asListWhenCrowded(input, context);
|
|
5724
|
+
if (crowded) return crowded;
|
|
5725
|
+
const items = itemsOf(input);
|
|
5726
|
+
if (conceptDrawsAs("matrix", input.items) === "cards")
|
|
5727
|
+
return cardsBlock({ ...input, template: "cards" }, context);
|
|
5728
|
+
const area = frame(input, context);
|
|
5729
|
+
const { theme, viewport } = context;
|
|
5730
|
+
const xAxis = String(input.xAxis ?? "").trim();
|
|
5731
|
+
const yAxis = String(input.yAxis ?? "").trim();
|
|
5732
|
+
const axisSize = themedFontSize(DETAIL_BASE * 1.05, context, false);
|
|
5733
|
+
const axisRoom = Math.round(axisSize * 1.9);
|
|
5734
|
+
const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
|
|
5735
|
+
const left = area.left + (yAxis ? axisRoom * 0.7 : 0);
|
|
5736
|
+
const top = area.top + (yAxis ? axisRoom : 0);
|
|
5737
|
+
const right = area.right;
|
|
5738
|
+
const bottom = area.bottom - (xAxis ? axisRoom * 1.15 : 0);
|
|
5739
|
+
const gap = Math.round(viewport.width * 0.014);
|
|
5740
|
+
const cellW = (right - left - gap) / 2;
|
|
5741
|
+
const cellH = (bottom - top - gap) / 2;
|
|
5742
|
+
const pad = Math.round(Math.min(40, Math.max(22, cellW * 0.07)));
|
|
5743
|
+
const ruleH = Math.max(4, Math.round(viewport.height * 6e-3));
|
|
5744
|
+
const ruleGap = Math.round(viewport.height * 0.02);
|
|
5745
|
+
const chrome = pad * 2 + ruleH + ruleGap;
|
|
5746
|
+
const roomy = MATRIX_SCALES.find(
|
|
5747
|
+
(scale) => items.every(
|
|
5748
|
+
(item) => measureItem(item, ramp(context, scale), cellW - pad * 2).height <= (cellH - chrome) * 0.7
|
|
5749
|
+
)
|
|
5750
|
+
);
|
|
5751
|
+
const sizes = roomy ? ramp(context, roomy) : fitRamp(
|
|
5752
|
+
items,
|
|
5753
|
+
context,
|
|
5754
|
+
() => cellW - pad * 2,
|
|
5755
|
+
() => cellH - chrome
|
|
5756
|
+
).sizes;
|
|
5757
|
+
const measures = items.map((item) => measureItem(item, sizes, cellW - pad * 2));
|
|
5758
|
+
const layers = [...area.layers];
|
|
5759
|
+
const axisColor = withAlpha(theme.colors.text, 0.6);
|
|
5760
|
+
const axisText = (id, text2, x, y, align) => textLayer(id, text2, box(x, y, (right - left) * 0.6, axisSize * 1.4), {
|
|
5761
|
+
fontSize: axisSize,
|
|
5762
|
+
fontFamily: getThemeFont(context, "title"),
|
|
5763
|
+
fontWeight: "bold",
|
|
5764
|
+
color: axisColor,
|
|
5765
|
+
lineHeight: 1.2,
|
|
5766
|
+
textAlign: align
|
|
5767
|
+
});
|
|
5768
|
+
if (yAxis) {
|
|
5769
|
+
const x = area.left + stroke;
|
|
5770
|
+
layers.push(
|
|
5771
|
+
{
|
|
5772
|
+
...connector(
|
|
5773
|
+
"axis-y",
|
|
5774
|
+
`M ${x} ${bottom} L ${x} ${top + axisRoom * 0.1}`,
|
|
5775
|
+
axisColor,
|
|
5776
|
+
stroke
|
|
5777
|
+
),
|
|
5778
|
+
animation: entrance(context, "draw", 0.3)
|
|
5779
|
+
},
|
|
5780
|
+
{
|
|
5781
|
+
...axisText("axis-y-label", yAxis, area.left, area.top, "left"),
|
|
5782
|
+
animation: entrance(context, "text", 0.45)
|
|
5783
|
+
}
|
|
5784
|
+
);
|
|
5785
|
+
}
|
|
5786
|
+
if (xAxis) {
|
|
5787
|
+
const y = bottom + axisRoom * 0.35;
|
|
5788
|
+
layers.push(
|
|
5789
|
+
{
|
|
5790
|
+
...connector("axis-x", `M ${left} ${y} L ${right - stroke} ${y}`, axisColor, stroke),
|
|
5791
|
+
animation: entrance(context, "draw", 0.3)
|
|
5792
|
+
},
|
|
5793
|
+
{
|
|
5794
|
+
...axisText(
|
|
5795
|
+
"axis-x-label",
|
|
5796
|
+
xAxis,
|
|
5797
|
+
right - (right - left) * 0.6,
|
|
5798
|
+
y + axisRoom * 0.2,
|
|
5799
|
+
"right"
|
|
5800
|
+
),
|
|
5801
|
+
animation: entrance(context, "text", 0.45)
|
|
5802
|
+
}
|
|
5803
|
+
);
|
|
5804
|
+
}
|
|
5805
|
+
items.forEach((item, index) => {
|
|
5806
|
+
const x = left + index % 2 * (cellW + gap);
|
|
5807
|
+
const y = top + Math.floor(index / 2) * (cellH + gap);
|
|
5808
|
+
layers.push(
|
|
5809
|
+
...step(
|
|
5810
|
+
[
|
|
5811
|
+
...panel(`cell-${index}`, x, y, cellW, cellH, pad, ruleH, area.accent, context),
|
|
5812
|
+
...words(
|
|
5813
|
+
itemText(
|
|
5814
|
+
`cell-${index}`,
|
|
5815
|
+
item,
|
|
5816
|
+
measures[index],
|
|
5817
|
+
sizes,
|
|
5818
|
+
x + pad,
|
|
5819
|
+
y + pad + ruleH + ruleGap,
|
|
5820
|
+
cellW - pad * 2,
|
|
5821
|
+
"left",
|
|
5822
|
+
context
|
|
5823
|
+
),
|
|
5824
|
+
0.22
|
|
5825
|
+
)
|
|
5826
|
+
],
|
|
5827
|
+
index,
|
|
5828
|
+
context
|
|
5829
|
+
)
|
|
5830
|
+
);
|
|
5831
|
+
});
|
|
4867
5832
|
return layers;
|
|
4868
5833
|
}
|
|
4869
5834
|
|
|
@@ -5176,6 +6141,8 @@ function definitionCard(input, context) {
|
|
|
5176
6141
|
// src/doc/templates/comparisonBar.ts
|
|
5177
6142
|
function comparisonBar(input, context) {
|
|
5178
6143
|
const { leftLabel, rightLabel, unit, colorScheme = "blue" } = input;
|
|
6144
|
+
if (!leftLabel && !rightLabel && !Number.isFinite(input.leftValue) && !Number.isFinite(input.rightValue))
|
|
6145
|
+
return [];
|
|
5179
6146
|
const { theme, viewport } = context;
|
|
5180
6147
|
const colors = resolveColorScheme(context, colorScheme);
|
|
5181
6148
|
const leftValue = Number.isFinite(input.leftValue) ? input.leftValue : 0;
|
|
@@ -5345,7 +6312,7 @@ var FIT_SLACK = 0.93;
|
|
|
5345
6312
|
var QUOTE_LINE_HEIGHT = 1.35;
|
|
5346
6313
|
var ATTR_LINE_HEIGHT = 1.4;
|
|
5347
6314
|
function pullQuoteLockup(options, context) {
|
|
5348
|
-
const { text, attribution, overMedia } = options;
|
|
6315
|
+
const { text: text2, attribution, overMedia } = options;
|
|
5349
6316
|
const { theme, viewport } = context;
|
|
5350
6317
|
const pct2 = (px) => px / viewport.height * 100;
|
|
5351
6318
|
const quoteBaseSize = themedFontSize(52, context, true);
|
|
@@ -5364,7 +6331,7 @@ function pullQuoteLockup(options, context) {
|
|
|
5364
6331
|
(frameHeightPx - attrHeightPx - gapReservePx) * FIT_SLACK
|
|
5365
6332
|
);
|
|
5366
6333
|
const fit = fitProse({
|
|
5367
|
-
text,
|
|
6334
|
+
text: text2,
|
|
5368
6335
|
baseFontSize: quoteBaseSize,
|
|
5369
6336
|
minFontSize: quoteMinSize,
|
|
5370
6337
|
maxWidthPx: columnWidthPx,
|
|
@@ -5388,7 +6355,7 @@ function pullQuoteLockup(options, context) {
|
|
|
5388
6355
|
type: "text",
|
|
5389
6356
|
id: "quote-text",
|
|
5390
6357
|
content: {
|
|
5391
|
-
text,
|
|
6358
|
+
text: text2,
|
|
5392
6359
|
style: {
|
|
5393
6360
|
fontSize: fit.fontSize,
|
|
5394
6361
|
fontFamily: getThemeFont(context, "title"),
|
|
@@ -5466,7 +6433,7 @@ function pullQuoteOverlayLayer() {
|
|
|
5466
6433
|
return createBackgroundLayer("overlay", "rgba(0, 0, 0, 0.55)");
|
|
5467
6434
|
}
|
|
5468
6435
|
function pullQuote(input, context) {
|
|
5469
|
-
const { text, attribution, backgroundImage, ambientMotion } = input;
|
|
6436
|
+
const { text: text2, attribution, backgroundImage, ambientMotion } = input;
|
|
5470
6437
|
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
5471
6438
|
const layers = [];
|
|
5472
6439
|
if (backgroundImage?.src) {
|
|
@@ -5496,7 +6463,7 @@ function pullQuote(input, context) {
|
|
|
5496
6463
|
layers.push(createBackgroundLayer("bg", themedSurfaceGradient(context, 160)));
|
|
5497
6464
|
}
|
|
5498
6465
|
layers.push(
|
|
5499
|
-
...pullQuoteLockup({ text, attribution, overMedia: Boolean(backgroundImage?.src) }, context)
|
|
6466
|
+
...pullQuoteLockup({ text: text2, attribution, overMedia: Boolean(backgroundImage?.src) }, context)
|
|
5500
6467
|
);
|
|
5501
6468
|
return layers;
|
|
5502
6469
|
}
|
|
@@ -5639,7 +6606,7 @@ function videoWithCaption(input, context) {
|
|
|
5639
6606
|
|
|
5640
6607
|
// src/doc/templates/videoPullQuote.ts
|
|
5641
6608
|
function videoPullQuote(input, context) {
|
|
5642
|
-
const { text, attribution, backgroundVideo } = input;
|
|
6609
|
+
const { text: text2, attribution, backgroundVideo } = input;
|
|
5643
6610
|
const layers = [];
|
|
5644
6611
|
if (backgroundVideo?.src) {
|
|
5645
6612
|
layers.push(
|
|
@@ -5667,7 +6634,7 @@ function videoPullQuote(input, context) {
|
|
|
5667
6634
|
layers.push(createBackgroundLayer("bg", themedSurfaceGradient(context, 160)));
|
|
5668
6635
|
}
|
|
5669
6636
|
layers.push(
|
|
5670
|
-
...pullQuoteLockup({ text, attribution, overMedia: Boolean(backgroundVideo?.src) }, context)
|
|
6637
|
+
...pullQuoteLockup({ text: text2, attribution, overMedia: Boolean(backgroundVideo?.src) }, context)
|
|
5671
6638
|
);
|
|
5672
6639
|
return layers;
|
|
5673
6640
|
}
|
|
@@ -5763,22 +6730,22 @@ function normalizeChartTable(table) {
|
|
|
5763
6730
|
}
|
|
5764
6731
|
function parseCellNumber(raw) {
|
|
5765
6732
|
if (typeof raw !== "string") return null;
|
|
5766
|
-
let
|
|
5767
|
-
if (!
|
|
6733
|
+
let text2 = raw.trim();
|
|
6734
|
+
if (!text2) return null;
|
|
5768
6735
|
let negative = false;
|
|
5769
|
-
const accounting = /^\((.*)\)$/.exec(
|
|
6736
|
+
const accounting = /^\((.*)\)$/.exec(text2);
|
|
5770
6737
|
if (accounting) {
|
|
5771
6738
|
negative = true;
|
|
5772
|
-
|
|
6739
|
+
text2 = accounting[1].trim();
|
|
5773
6740
|
}
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(
|
|
5778
|
-
|
|
6741
|
+
text2 = text2.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
|
|
6742
|
+
text2 = text2.replace(/%$/, "").trim();
|
|
6743
|
+
text2 = text2.replace(/,/g, "");
|
|
6744
|
+
if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text2)) {
|
|
6745
|
+
text2 = text2.replace(/ /g, "");
|
|
5779
6746
|
}
|
|
5780
|
-
if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(
|
|
5781
|
-
const value = Number(
|
|
6747
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text2)) return null;
|
|
6748
|
+
const value = Number(text2);
|
|
5782
6749
|
if (!Number.isFinite(value)) return null;
|
|
5783
6750
|
return negative ? -value : value;
|
|
5784
6751
|
}
|
|
@@ -5794,13 +6761,13 @@ function isNumericColumn(rows, colIndex) {
|
|
|
5794
6761
|
return numeric > 0 && numeric / Math.max(1, nonEmpty) >= NUMERIC_COLUMN_THRESHOLD;
|
|
5795
6762
|
}
|
|
5796
6763
|
function resolveColumnRef(ref, headers) {
|
|
5797
|
-
const
|
|
5798
|
-
if (!
|
|
5799
|
-
const lower =
|
|
6764
|
+
const text2 = String(ref).trim();
|
|
6765
|
+
if (!text2) return null;
|
|
6766
|
+
const lower = text2.toLowerCase();
|
|
5800
6767
|
const byName = headers.findIndex((h) => h.trim().toLowerCase() === lower);
|
|
5801
6768
|
if (byName >= 0) return byName;
|
|
5802
|
-
if (/^\d+$/.test(
|
|
5803
|
-
const index = Number(
|
|
6769
|
+
if (/^\d+$/.test(text2)) {
|
|
6770
|
+
const index = Number(text2);
|
|
5804
6771
|
if (index < headers.length) return index;
|
|
5805
6772
|
}
|
|
5806
6773
|
return null;
|
|
@@ -5848,9 +6815,9 @@ function buildChartData(table, options = {}) {
|
|
|
5848
6815
|
}
|
|
5849
6816
|
|
|
5850
6817
|
// src/doc/templates/chart/scale.ts
|
|
5851
|
-
function niceNum(
|
|
5852
|
-
const exponent = Math.floor(Math.log10(
|
|
5853
|
-
const fraction =
|
|
6818
|
+
function niceNum(range2, round4) {
|
|
6819
|
+
const exponent = Math.floor(Math.log10(range2));
|
|
6820
|
+
const fraction = range2 / 10 ** exponent;
|
|
5854
6821
|
let niceFraction;
|
|
5855
6822
|
if (round4) {
|
|
5856
6823
|
if (fraction < 1.5) niceFraction = 1;
|
|
@@ -5876,8 +6843,8 @@ function niceTicks(min, max, targetCount = 5) {
|
|
|
5876
6843
|
if (min >= 0 && lo < 0) lo = 0;
|
|
5877
6844
|
}
|
|
5878
6845
|
const count = Math.max(2, targetCount);
|
|
5879
|
-
const
|
|
5880
|
-
const step2 = niceNum(
|
|
6846
|
+
const range2 = niceNum(hi - lo, false);
|
|
6847
|
+
const step2 = niceNum(range2 / (count - 1), true);
|
|
5881
6848
|
const niceMin = Math.floor(lo / step2) * step2;
|
|
5882
6849
|
const niceMax = Math.ceil(hi / step2) * step2;
|
|
5883
6850
|
const ticks = [];
|
|
@@ -5890,24 +6857,24 @@ function formatChartValue(value, unit) {
|
|
|
5890
6857
|
if (!Number.isFinite(value)) return "\u2014";
|
|
5891
6858
|
const sign = value < 0 ? "-" : "";
|
|
5892
6859
|
const abs = Math.abs(value);
|
|
5893
|
-
let
|
|
6860
|
+
let text2;
|
|
5894
6861
|
if (abs >= 1e6) {
|
|
5895
|
-
|
|
6862
|
+
text2 = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
|
|
5896
6863
|
} else if (abs >= 1e3) {
|
|
5897
|
-
|
|
6864
|
+
text2 = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
|
|
5898
6865
|
} else {
|
|
5899
|
-
|
|
6866
|
+
text2 = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
5900
6867
|
}
|
|
5901
|
-
return unit ? `${
|
|
6868
|
+
return unit ? `${text2} ${unit}` : text2;
|
|
5902
6869
|
}
|
|
5903
6870
|
function formatChartDataValue(value, unit) {
|
|
5904
6871
|
if (!Number.isFinite(value) || Math.abs(value) >= 1e4) return formatChartValue(value, unit);
|
|
5905
6872
|
const sign = value < 0 ? "-" : "";
|
|
5906
|
-
const
|
|
5907
|
-
return unit ? `${
|
|
6873
|
+
const text2 = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
6874
|
+
return unit ? `${text2} ${unit}` : text2;
|
|
5908
6875
|
}
|
|
5909
|
-
function trimTrailingZero(
|
|
5910
|
-
return
|
|
6876
|
+
function trimTrailingZero(text2) {
|
|
6877
|
+
return text2.replace(/\.0$/, "");
|
|
5911
6878
|
}
|
|
5912
6879
|
|
|
5913
6880
|
// src/doc/templates/chart/geometry.ts
|
|
@@ -5944,15 +6911,15 @@ function arcPath(cx, cy, rOuter, rInner, startDeg, endDeg) {
|
|
|
5944
6911
|
"Z"
|
|
5945
6912
|
].join(" ");
|
|
5946
6913
|
}
|
|
5947
|
-
function polylinePath(
|
|
5948
|
-
if (
|
|
5949
|
-
return
|
|
6914
|
+
function polylinePath(points2) {
|
|
6915
|
+
if (points2.length === 0) return "";
|
|
6916
|
+
return points2.map((p, i) => `${i === 0 ? "M" : "L"} ${round(p.x)} ${round(p.y)}`).join(" ");
|
|
5950
6917
|
}
|
|
5951
|
-
function areaPath(
|
|
5952
|
-
if (
|
|
5953
|
-
const open = polylinePath(
|
|
5954
|
-
const first =
|
|
5955
|
-
const last =
|
|
6918
|
+
function areaPath(points2, baselineY) {
|
|
6919
|
+
if (points2.length === 0) return "";
|
|
6920
|
+
const open = polylinePath(points2);
|
|
6921
|
+
const first = points2[0];
|
|
6922
|
+
const last = points2[points2.length - 1];
|
|
5956
6923
|
return `${open} L ${round(last.x)} ${round(baselineY)} L ${round(first.x)} ${round(baselineY)} Z`;
|
|
5957
6924
|
}
|
|
5958
6925
|
|
|
@@ -7064,34 +8031,76 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
|
|
|
7064
8031
|
unconsumedMedia: "reserve-when-no-native-media",
|
|
7065
8032
|
variants: TEXT_VARIANTS
|
|
7066
8033
|
},
|
|
7067
|
-
cards: {
|
|
7068
|
-
summary: "Text-only cards by default; supplemental media is paired with the cards.",
|
|
8034
|
+
cards: {
|
|
8035
|
+
summary: "Text-only cards by default; supplemental media is paired with the cards.",
|
|
8036
|
+
noMedia: "template-default",
|
|
8037
|
+
ownership: "supplemental",
|
|
8038
|
+
unconsumedMedia: "reserved-slot",
|
|
8039
|
+
variants: DENSE_VARIANTS
|
|
8040
|
+
},
|
|
8041
|
+
process: {
|
|
8042
|
+
summary: "A drawn process by default; supplemental media is paired with the stages.",
|
|
8043
|
+
noMedia: "template-default",
|
|
8044
|
+
ownership: "supplemental",
|
|
8045
|
+
unconsumedMedia: "reserved-slot",
|
|
8046
|
+
variants: DENSE_VARIANTS
|
|
8047
|
+
},
|
|
8048
|
+
cycle: {
|
|
8049
|
+
summary: "A drawn cycle by default; supplemental media is paired with the ring.",
|
|
8050
|
+
noMedia: "template-default",
|
|
8051
|
+
ownership: "supplemental",
|
|
8052
|
+
unconsumedMedia: "reserved-slot",
|
|
8053
|
+
variants: TEXT_VARIANTS
|
|
8054
|
+
},
|
|
8055
|
+
layers: {
|
|
8056
|
+
summary: "Drawn concentric layers by default; supplemental media is paired with the rings.",
|
|
8057
|
+
noMedia: "template-default",
|
|
8058
|
+
ownership: "supplemental",
|
|
8059
|
+
unconsumedMedia: "reserved-slot",
|
|
8060
|
+
variants: TEXT_VARIANTS
|
|
8061
|
+
},
|
|
8062
|
+
stats: {
|
|
8063
|
+
summary: "Key figures by default; supplemental media is paired with the figures.",
|
|
8064
|
+
noMedia: "template-default",
|
|
8065
|
+
ownership: "supplemental",
|
|
8066
|
+
unconsumedMedia: "reserved-slot",
|
|
8067
|
+
variants: TEXT_VARIANTS
|
|
8068
|
+
},
|
|
8069
|
+
pyramid: {
|
|
8070
|
+
summary: "A drawn pyramid by default; supplemental media is paired with the tiers.",
|
|
7069
8071
|
noMedia: "template-default",
|
|
7070
8072
|
ownership: "supplemental",
|
|
7071
8073
|
unconsumedMedia: "reserved-slot",
|
|
7072
|
-
variants:
|
|
8074
|
+
variants: TEXT_VARIANTS
|
|
7073
8075
|
},
|
|
7074
|
-
|
|
7075
|
-
summary: "A drawn
|
|
8076
|
+
funnel: {
|
|
8077
|
+
summary: "A drawn funnel by default; supplemental media is paired with the stages.",
|
|
7076
8078
|
noMedia: "template-default",
|
|
7077
8079
|
ownership: "supplemental",
|
|
7078
8080
|
unconsumedMedia: "reserved-slot",
|
|
7079
|
-
variants:
|
|
8081
|
+
variants: TEXT_VARIANTS
|
|
7080
8082
|
},
|
|
7081
|
-
|
|
7082
|
-
summary: "A drawn
|
|
8083
|
+
hub: {
|
|
8084
|
+
summary: "A drawn hub and spokes by default; supplemental media is paired with the ideas.",
|
|
7083
8085
|
noMedia: "template-default",
|
|
7084
8086
|
ownership: "supplemental",
|
|
7085
8087
|
unconsumedMedia: "reserved-slot",
|
|
7086
8088
|
variants: TEXT_VARIANTS
|
|
7087
8089
|
},
|
|
7088
|
-
|
|
7089
|
-
summary: "Drawn
|
|
8090
|
+
venn: {
|
|
8091
|
+
summary: "Drawn overlapping circles by default; supplemental media is paired with the sets.",
|
|
7090
8092
|
noMedia: "template-default",
|
|
7091
8093
|
ownership: "supplemental",
|
|
7092
8094
|
unconsumedMedia: "reserved-slot",
|
|
7093
8095
|
variants: TEXT_VARIANTS
|
|
7094
8096
|
},
|
|
8097
|
+
matrix: {
|
|
8098
|
+
summary: "A two-by-two grid by default; supplemental media is paired with the grid.",
|
|
8099
|
+
noMedia: "template-default",
|
|
8100
|
+
ownership: "supplemental",
|
|
8101
|
+
unconsumedMedia: "reserved-slot",
|
|
8102
|
+
variants: DENSE_VARIANTS
|
|
8103
|
+
},
|
|
7095
8104
|
photoGrid: {
|
|
7096
8105
|
summary: "Requires a primary image collection and owns the complete tiled media field.",
|
|
7097
8106
|
noMedia: "unsupported",
|
|
@@ -7243,15 +8252,15 @@ function getBlockMediaLayoutPolicy(template) {
|
|
|
7243
8252
|
}
|
|
7244
8253
|
|
|
7245
8254
|
// src/doc/richMediaLayout.ts
|
|
7246
|
-
function containedFeatureArea(
|
|
8255
|
+
function containedFeatureArea(region2, aspects) {
|
|
7247
8256
|
const count = aspects.length;
|
|
7248
8257
|
const columns2 = Math.min(
|
|
7249
8258
|
count,
|
|
7250
|
-
Math.max(1, Math.ceil(Math.sqrt(count *
|
|
8259
|
+
Math.max(1, Math.ceil(Math.sqrt(count * region2.width / region2.height)))
|
|
7251
8260
|
);
|
|
7252
8261
|
const rows = Math.ceil(count / columns2);
|
|
7253
|
-
const width =
|
|
7254
|
-
const height =
|
|
8262
|
+
const width = region2.width / columns2;
|
|
8263
|
+
const height = region2.height / rows;
|
|
7255
8264
|
return aspects.reduce((area, aspect) => {
|
|
7256
8265
|
const ratio = aspect && Number.isFinite(aspect) && aspect > 0 ? aspect : 16 / 9;
|
|
7257
8266
|
const containedWidth = Math.min(width, height * ratio);
|
|
@@ -7954,15 +8963,15 @@ var DIAGRAM_LABEL_LINE_HEIGHT = 1.25;
|
|
|
7954
8963
|
var DIAGRAM_LABEL_HORIZONTAL_PADDING = 24;
|
|
7955
8964
|
var DIAGRAM_LABEL_VERTICAL_PADDING = 16;
|
|
7956
8965
|
var DIAGRAM_LABEL_MIN_FONT_SIZE = 10;
|
|
7957
|
-
function fitDiagramLabel(
|
|
8966
|
+
function fitDiagramLabel(text2, boxWidth, boxHeight, preferredFontSize) {
|
|
7958
8967
|
const textWidth = Math.max(1, boxWidth - DIAGRAM_LABEL_HORIZONTAL_PADDING);
|
|
7959
8968
|
const textHeight = Math.max(1, boxHeight - DIAGRAM_LABEL_VERTICAL_PADDING);
|
|
7960
8969
|
const preferred = Math.max(DIAGRAM_LABEL_MIN_FONT_SIZE, Math.floor(preferredFontSize));
|
|
7961
8970
|
let fontSize = preferred;
|
|
7962
|
-
let lineCount = countWrappedLines(
|
|
8971
|
+
let lineCount = countWrappedLines(text2, fontSize, textWidth);
|
|
7963
8972
|
while (fontSize > DIAGRAM_LABEL_MIN_FONT_SIZE && lineCount * fontSize * DIAGRAM_LABEL_LINE_HEIGHT > textHeight) {
|
|
7964
8973
|
fontSize--;
|
|
7965
|
-
lineCount = countWrappedLines(
|
|
8974
|
+
lineCount = countWrappedLines(text2, fontSize, textWidth);
|
|
7966
8975
|
}
|
|
7967
8976
|
return {
|
|
7968
8977
|
fontSize,
|
|
@@ -7971,16 +8980,16 @@ function fitDiagramLabel(text, boxWidth, boxHeight, preferredFontSize) {
|
|
|
7971
8980
|
textWidth
|
|
7972
8981
|
};
|
|
7973
8982
|
}
|
|
7974
|
-
function countWrappedLines(
|
|
7975
|
-
return
|
|
8983
|
+
function countWrappedLines(text2, fontSize, maxWidth) {
|
|
8984
|
+
return text2.split("\n").reduce((count, line) => count + wrappedLineCount2(line, fontSize, maxWidth), 0);
|
|
7976
8985
|
}
|
|
7977
|
-
function wrappedLineCount2(
|
|
7978
|
-
if (!
|
|
8986
|
+
function wrappedLineCount2(text2, fontSize, maxWidth) {
|
|
8987
|
+
if (!text2.trim()) return 1;
|
|
7979
8988
|
const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
|
|
7980
8989
|
if (charsPerLine <= 0) return 1;
|
|
7981
8990
|
let lines = 0;
|
|
7982
8991
|
let currentLength = 0;
|
|
7983
|
-
for (const word of
|
|
8992
|
+
for (const word of text2.split(/\s+/)) {
|
|
7984
8993
|
const testLength = currentLength > 0 ? currentLength + 1 + word.length : word.length;
|
|
7985
8994
|
if (testLength <= charsPerLine) {
|
|
7986
8995
|
currentLength = testLength;
|
|
@@ -7999,6 +9008,7 @@ function wrappedLineCount2(text, fontSize, maxWidth) {
|
|
|
7999
9008
|
}
|
|
8000
9009
|
|
|
8001
9010
|
// src/doc/templates/diagramBlock.ts
|
|
9011
|
+
import { layoutGraph } from "@bendyline/squisq-graph";
|
|
8002
9012
|
var NODE_WIDTH = 180;
|
|
8003
9013
|
var NODE_HEIGHT = 64;
|
|
8004
9014
|
var PADDING = 80;
|
|
@@ -8075,8 +9085,8 @@ function planCanvasFrame(template, title, context) {
|
|
|
8075
9085
|
const { viewport } = context;
|
|
8076
9086
|
const media = unconsumedMedia(context.block);
|
|
8077
9087
|
const mediaRect = media.count > 0 ? resolveSupplementalMediaLayout([], template, viewport, media.count, media.aspectRatios).mediaRect : void 0;
|
|
8078
|
-
const
|
|
8079
|
-
const innerWidth = Math.max(1,
|
|
9088
|
+
const region2 = freeRegion(viewport, mediaRect);
|
|
9089
|
+
const innerWidth = Math.max(1, region2.width - PADDING * 2);
|
|
8080
9090
|
let titleFit;
|
|
8081
9091
|
if (title) {
|
|
8082
9092
|
const baseFontSize = themedFontSize(40, context, true);
|
|
@@ -8103,7 +9113,7 @@ function planCanvasFrame(template, title, context) {
|
|
|
8103
9113
|
maxWidthPx: widthPx,
|
|
8104
9114
|
maxHeightPx: Math.min(
|
|
8105
9115
|
CANVAS_DESCRIPTION_MAX_LINES * baseFontSize * CANVAS_DESCRIPTION_LINE_HEIGHT,
|
|
8106
|
-
|
|
9116
|
+
region2.height * CANVAS_DESCRIPTION_MAX_HEIGHT_RATIO
|
|
8107
9117
|
),
|
|
8108
9118
|
lineHeight: CANVAS_DESCRIPTION_LINE_HEIGHT
|
|
8109
9119
|
});
|
|
@@ -8112,13 +9122,13 @@ function planCanvasFrame(template, title, context) {
|
|
|
8112
9122
|
const titleBlockPx = titleFit ? titleFit.heightPx + CANVAS_TITLE_GAP_PX : 0;
|
|
8113
9123
|
const descriptionBlockPx = description ? CANVAS_DESCRIPTION_GAP_PX + description.fit.heightPx : 0;
|
|
8114
9124
|
return {
|
|
8115
|
-
region,
|
|
9125
|
+
region: region2,
|
|
8116
9126
|
...titleFit ? { title: titleFit } : {},
|
|
8117
9127
|
...description ? { description } : {},
|
|
8118
9128
|
titleBlockPx,
|
|
8119
9129
|
descriptionBlockPx,
|
|
8120
9130
|
availW: innerWidth,
|
|
8121
|
-
availH: Math.max(1,
|
|
9131
|
+
availH: Math.max(1, region2.height - PADDING * 2 - titleBlockPx - descriptionBlockPx),
|
|
8122
9132
|
...mediaRect ? { mediaRect } : {}
|
|
8123
9133
|
};
|
|
8124
9134
|
}
|
|
@@ -8140,14 +9150,14 @@ function placeCanvasGroup(frame2, contentW, contentH) {
|
|
|
8140
9150
|
descriptionY: offsetY + scaledH + CANVAS_DESCRIPTION_GAP_PX
|
|
8141
9151
|
};
|
|
8142
9152
|
}
|
|
8143
|
-
function canvasTitleLayer(id,
|
|
9153
|
+
function canvasTitleLayer(id, text2, frame2, placement, context) {
|
|
8144
9154
|
const fit = frame2.title;
|
|
8145
9155
|
if (!fit) return void 0;
|
|
8146
9156
|
return {
|
|
8147
9157
|
type: "text",
|
|
8148
9158
|
id,
|
|
8149
9159
|
content: {
|
|
8150
|
-
text,
|
|
9160
|
+
text: text2,
|
|
8151
9161
|
style: {
|
|
8152
9162
|
fontSize: fit.fontSize,
|
|
8153
9163
|
fontFamily: getThemeFont(context, "title"),
|
|
@@ -8215,11 +9225,40 @@ function nodesFromChildren(context) {
|
|
|
8215
9225
|
}))
|
|
8216
9226
|
};
|
|
8217
9227
|
}
|
|
8218
|
-
function
|
|
9228
|
+
function placeDiagramNodes(nodes, rawEdges, direction) {
|
|
9229
|
+
if (nodes.length === 0 || nodes.some((node) => node.container)) return [];
|
|
9230
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
9231
|
+
const edges = (Array.isArray(rawEdges) ? rawEdges : []).filter(
|
|
9232
|
+
(edge) => typeof edge === "object" && edge !== null && ids.has(edge.source) && ids.has(edge.target) && edge.source !== edge.target
|
|
9233
|
+
).map((edge, index) => ({ id: String(index), source: edge.source, target: edge.target }));
|
|
9234
|
+
const size = (node) => ({
|
|
9235
|
+
width: typeof node.w === "number" && node.w > 0 ? node.w : NODE_WIDTH,
|
|
9236
|
+
height: typeof node.h === "number" && node.h > 0 ? node.h : NODE_HEIGHT
|
|
9237
|
+
});
|
|
9238
|
+
const result = layoutGraph(
|
|
9239
|
+
{ nodes: nodes.map((node) => ({ id: node.id, ...size(node) })), edges },
|
|
9240
|
+
{
|
|
9241
|
+
direction,
|
|
9242
|
+
layerSpacing: direction === "right" ? 100 : 96,
|
|
9243
|
+
nodeSpacing: direction === "right" ? 96 : 100,
|
|
9244
|
+
componentSpacing: 100,
|
|
9245
|
+
// The template draws its own connectors; only the positions are used.
|
|
9246
|
+
edgeSpacing: 1
|
|
9247
|
+
}
|
|
9248
|
+
);
|
|
9249
|
+
return nodes.map((node, index) => ({
|
|
9250
|
+
...node,
|
|
9251
|
+
x: Math.round(result.nodes[index].x),
|
|
9252
|
+
y: Math.round(result.nodes[index].y)
|
|
9253
|
+
}));
|
|
9254
|
+
}
|
|
9255
|
+
function nodesFromTemplateData(input, direction) {
|
|
8219
9256
|
const rawNodes = Array.isArray(input.nodes) ? input.nodes : [];
|
|
8220
|
-
const
|
|
8221
|
-
(n) => typeof n === "object" && n !== null && typeof n.id === "string" && n.id.length > 0 && typeof n.label === "string"
|
|
9257
|
+
const named = rawNodes.filter(
|
|
9258
|
+
(n) => typeof n === "object" && n !== null && typeof n.id === "string" && n.id.length > 0 && typeof n.label === "string"
|
|
8222
9259
|
);
|
|
9260
|
+
const positioned = (n) => typeof n.x === "number" && Number.isFinite(n.x) && typeof n.y === "number" && Number.isFinite(n.y);
|
|
9261
|
+
const valid = named.some(positioned) ? named.filter(positioned) : placeDiagramNodes(named, input.edges, direction);
|
|
8223
9262
|
const ids = new Set(valid.map((n) => n.id));
|
|
8224
9263
|
const containerIds = new Set(
|
|
8225
9264
|
valid.map((n) => n.container).filter((c) => typeof c === "string" && ids.has(c))
|
|
@@ -8264,7 +9303,7 @@ function diagramBlock(input, context) {
|
|
|
8264
9303
|
const { theme, viewport, children = [] } = context;
|
|
8265
9304
|
const colors = input.colorScheme ? resolveColorScheme(context, input.colorScheme) : void 0;
|
|
8266
9305
|
const background = input.background === "surface" ? [createBackgroundLayer("diagram-bg", themedSurfaceGradient(context, 170))] : [];
|
|
8267
|
-
const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input) : { nodes: [], edges: [] };
|
|
9306
|
+
const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input, viewport.height > viewport.width ? "down" : "right") : { nodes: [], edges: [] };
|
|
8268
9307
|
if (resolved.nodes.length === 0) {
|
|
8269
9308
|
return [
|
|
8270
9309
|
...background,
|
|
@@ -8584,11 +9623,11 @@ function coerceTracks(raw) {
|
|
|
8584
9623
|
const tracks = [];
|
|
8585
9624
|
raw.forEach((candidate, trackIndex) => {
|
|
8586
9625
|
if (typeof candidate !== "object" || candidate === null) return;
|
|
8587
|
-
const
|
|
8588
|
-
const id = typeof
|
|
9626
|
+
const record2 = candidate;
|
|
9627
|
+
const id = typeof record2.id === "string" && record2.id.trim() ? record2.id.trim() : `track-${trackIndex + 1}`;
|
|
8589
9628
|
const events = [];
|
|
8590
|
-
if (Array.isArray(
|
|
8591
|
-
|
|
9629
|
+
if (Array.isArray(record2.events)) {
|
|
9630
|
+
record2.events.forEach((eventCandidate, eventIndex) => {
|
|
8592
9631
|
if (typeof eventCandidate !== "object" || eventCandidate === null) return;
|
|
8593
9632
|
const event = eventCandidate;
|
|
8594
9633
|
if (typeof event.label !== "string") return;
|
|
@@ -8612,8 +9651,8 @@ function coerceTracks(raw) {
|
|
|
8612
9651
|
}
|
|
8613
9652
|
tracks.push({
|
|
8614
9653
|
id,
|
|
8615
|
-
...typeof
|
|
8616
|
-
...typeof
|
|
9654
|
+
...typeof record2.label === "string" && record2.label.trim() ? { label: record2.label.trim() } : {},
|
|
9655
|
+
...typeof record2.endLabel === "string" && record2.endLabel.trim() ? { endLabel: record2.endLabel.trim() } : {},
|
|
8617
9656
|
events
|
|
8618
9657
|
});
|
|
8619
9658
|
});
|
|
@@ -9389,8 +10428,9 @@ function drawingBlock(input, context) {
|
|
|
9389
10428
|
const defaultStroke = input.stroke ?? colors.text ?? theme.colors.text;
|
|
9390
10429
|
const defaultFill = input.fill ?? "none";
|
|
9391
10430
|
const layout = computeDrawingLayout(children);
|
|
10431
|
+
const background = createBackgroundLayer("bg", theme.colors.background);
|
|
9392
10432
|
if (layout.shapes.length === 0 && layout.connectors.length === 0) {
|
|
9393
|
-
return [emptyHint(input, context)];
|
|
10433
|
+
return [background, emptyHint(input, context)];
|
|
9394
10434
|
}
|
|
9395
10435
|
const minX = Math.min(...layout.shapes.map((s) => s.x));
|
|
9396
10436
|
const maxX = Math.max(...layout.shapes.map((s) => s.x + s.width));
|
|
@@ -9404,7 +10444,7 @@ function drawingBlock(input, context) {
|
|
|
9404
10444
|
const tx = (x) => offsetX + (x - minX) * scale;
|
|
9405
10445
|
const ty = (y) => offsetY + (y - minY) * scale;
|
|
9406
10446
|
const fontAdj = Math.min(Math.max(scale, 1), 1.5);
|
|
9407
|
-
const layers = [];
|
|
10447
|
+
const layers = [background];
|
|
9408
10448
|
if (input.title) {
|
|
9409
10449
|
const titleLayer = canvasTitleLayer("drawing-title", input.title, frame2, placement, context);
|
|
9410
10450
|
if (titleLayer) layers.push(titleLayer);
|
|
@@ -9550,12 +10590,12 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
|
|
|
9550
10590
|
}
|
|
9551
10591
|
return out;
|
|
9552
10592
|
}
|
|
9553
|
-
function textLayer3(id,
|
|
10593
|
+
function textLayer3(id, text2, cx, cy, width, context, color, bold, fontAdj) {
|
|
9554
10594
|
return {
|
|
9555
10595
|
type: "text",
|
|
9556
10596
|
id,
|
|
9557
10597
|
content: {
|
|
9558
|
-
text,
|
|
10598
|
+
text: text2,
|
|
9559
10599
|
style: {
|
|
9560
10600
|
fontSize: themedFontSize(Math.round((bold ? 22 : 18) * fontAdj), context, false),
|
|
9561
10601
|
fontFamily: getThemeFont(context, "body"),
|
|
@@ -9660,7 +10700,7 @@ function computeLayoutLayers(children, _viewport, defaults = {}) {
|
|
|
9660
10700
|
continue;
|
|
9661
10701
|
}
|
|
9662
10702
|
if (kind === "text") {
|
|
9663
|
-
const
|
|
10703
|
+
const text2 = bodyText2(child) || child.title || "";
|
|
9664
10704
|
const layer = {
|
|
9665
10705
|
id: child.id,
|
|
9666
10706
|
type: "text",
|
|
@@ -9670,7 +10710,7 @@ function computeLayoutLayers(children, _viewport, defaults = {}) {
|
|
|
9670
10710
|
width: numParam2(params, "width") ?? DEFAULT_TEXT.w,
|
|
9671
10711
|
height: numParam2(params, "height") ?? DEFAULT_TEXT.h
|
|
9672
10712
|
},
|
|
9673
|
-
content: { text, style: textStyleFromParams(params, ink) }
|
|
10713
|
+
content: { text: text2, style: textStyleFromParams(params, ink) }
|
|
9674
10714
|
};
|
|
9675
10715
|
layers.push(layer);
|
|
9676
10716
|
continue;
|
|
@@ -9771,7 +10811,8 @@ function round3(n) {
|
|
|
9771
10811
|
function layoutBlock(input, context) {
|
|
9772
10812
|
const { theme, viewport, children = [] } = context;
|
|
9773
10813
|
const { layers } = computeLayoutLayers(children, viewport, { ink: theme.colors.text });
|
|
9774
|
-
|
|
10814
|
+
const background = createBackgroundLayer("bg", theme.colors.background);
|
|
10815
|
+
return [background, ...layers.length > 0 ? layers : [emptyHint2(input, context)]];
|
|
9775
10816
|
}
|
|
9776
10817
|
function emptyHint2(input, context) {
|
|
9777
10818
|
return {
|
|
@@ -10006,12 +11047,12 @@ function handoffLayers(input, context, accent, textColor, bubbleFill) {
|
|
|
10006
11047
|
}
|
|
10007
11048
|
return layers;
|
|
10008
11049
|
}
|
|
10009
|
-
function nameLayer(id,
|
|
11050
|
+
function nameLayer(id, text2, x, fontSize, fontFamily, color) {
|
|
10010
11051
|
return {
|
|
10011
11052
|
type: "text",
|
|
10012
11053
|
id,
|
|
10013
11054
|
content: {
|
|
10014
|
-
text,
|
|
11055
|
+
text: text2,
|
|
10015
11056
|
style: { fontSize, fontFamily, fontWeight: "bold", color, textAlign: "center" }
|
|
10016
11057
|
},
|
|
10017
11058
|
position: { x, y: "42%", anchor: "center" }
|
|
@@ -10105,6 +11146,12 @@ var templateRegistry = {
|
|
|
10105
11146
|
process: processBlock,
|
|
10106
11147
|
cycle: cycleBlock,
|
|
10107
11148
|
layers: layersBlock,
|
|
11149
|
+
stats: statsBlock,
|
|
11150
|
+
pyramid: pyramidBlock,
|
|
11151
|
+
funnel: funnelBlock,
|
|
11152
|
+
hub: hubBlock,
|
|
11153
|
+
venn: vennBlock,
|
|
11154
|
+
matrix: matrixBlock,
|
|
10108
11155
|
photoGrid,
|
|
10109
11156
|
definitionCard,
|
|
10110
11157
|
comparisonBar,
|
|
@@ -10510,6 +11557,27 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
|
|
|
10510
11557
|
{ key: "tracks", description: "Timeline tracks and events (supply via template data)" },
|
|
10511
11558
|
{ key: "links", description: "Branch links between event ids (supply via template data)" }
|
|
10512
11559
|
],
|
|
11560
|
+
cards: CONCEPT_DESCRIPTORS([
|
|
11561
|
+
{
|
|
11562
|
+
key: "versus",
|
|
11563
|
+
description: 'Set exactly two ideas against each other, with a "vs" mark between them',
|
|
11564
|
+
coerce: "boolean"
|
|
11565
|
+
}
|
|
11566
|
+
]),
|
|
11567
|
+
process: CONCEPT_DESCRIPTORS(),
|
|
11568
|
+
cycle: CONCEPT_DESCRIPTORS(),
|
|
11569
|
+
layers: CONCEPT_DESCRIPTORS(),
|
|
11570
|
+
stats: CONCEPT_DESCRIPTORS(),
|
|
11571
|
+
pyramid: CONCEPT_DESCRIPTORS(),
|
|
11572
|
+
funnel: CONCEPT_DESCRIPTORS(),
|
|
11573
|
+
hub: CONCEPT_DESCRIPTORS([
|
|
11574
|
+
{ key: "center", description: "The one to three words written inside the hub" }
|
|
11575
|
+
]),
|
|
11576
|
+
venn: CONCEPT_DESCRIPTORS(),
|
|
11577
|
+
matrix: CONCEPT_DESCRIPTORS([
|
|
11578
|
+
{ key: "xAxis", description: "What grows from left to right, named along the foot" },
|
|
11579
|
+
{ key: "yAxis", description: "What grows from bottom to top, named up the side" }
|
|
11580
|
+
]),
|
|
10513
11581
|
videoWithCaption: [
|
|
10514
11582
|
{ key: "videoSrc", description: "Path to the video file", required: true },
|
|
10515
11583
|
{ key: "posterSrc", description: "Poster frame path" },
|
|
@@ -10522,6 +11590,17 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
|
|
|
10522
11590
|
{ key: "videoLicense", description: "License identifier" }
|
|
10523
11591
|
]
|
|
10524
11592
|
};
|
|
11593
|
+
function CONCEPT_DESCRIPTORS(extra = []) {
|
|
11594
|
+
return [
|
|
11595
|
+
{ key: "title", description: "Headline above the ideas (defaults to the heading)" },
|
|
11596
|
+
{
|
|
11597
|
+
key: "colorScheme",
|
|
11598
|
+
description: "Accent color (defaults to the theme highlight)",
|
|
11599
|
+
valueHint: COLOR_SCHEME_HINT
|
|
11600
|
+
},
|
|
11601
|
+
...extra
|
|
11602
|
+
];
|
|
11603
|
+
}
|
|
10525
11604
|
function FEATURE_DESCRIPTORS() {
|
|
10526
11605
|
return [
|
|
10527
11606
|
{ key: "imageSrc", description: "Path to the feature image", required: true },
|
|
@@ -10726,9 +11805,9 @@ function fallbackBlockLayers(block, context, notice) {
|
|
|
10726
11805
|
}
|
|
10727
11806
|
function extractBodyText(block) {
|
|
10728
11807
|
if (!block.contents || block.contents.length === 0) return "";
|
|
10729
|
-
const
|
|
10730
|
-
if (
|
|
10731
|
-
return `${
|
|
11808
|
+
const text2 = block.contents.filter((node) => !isPictureFence(node)).map((node) => extractPlainText(node)).join("\n").trim();
|
|
11809
|
+
if (text2.length <= MAX_BODY_CHARS) return text2;
|
|
11810
|
+
return `${text2.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
|
|
10732
11811
|
}
|
|
10733
11812
|
|
|
10734
11813
|
// src/doc/structuredData.ts
|
|
@@ -11056,6 +12135,21 @@ function materializeBlockLayersWithRuntime(block, options, runtime) {
|
|
|
11056
12135
|
const context = runtime.templateContext ? cloneData(runtime.templateContext) : createTemplateContext(theme, blockIndex, totalBlocks, viewport, { motion: options.motion });
|
|
11057
12136
|
context.block = cloneData(block);
|
|
11058
12137
|
const execution = executeTemplateMaterialization(block, context, runtime.registry);
|
|
12138
|
+
if (execution.status === "ok" && drawsNothing(execution.layers) && hasContentToShow(block)) {
|
|
12139
|
+
const asContent = materializeBlockLayersWithRuntime(
|
|
12140
|
+
{ ...block, template: "content" },
|
|
12141
|
+
options,
|
|
12142
|
+
runtime
|
|
12143
|
+
);
|
|
12144
|
+
return {
|
|
12145
|
+
...asContent,
|
|
12146
|
+
diagnostic: {
|
|
12147
|
+
code: "template-drew-nothing",
|
|
12148
|
+
template: block.template,
|
|
12149
|
+
message: `Template "${block.template}" has nothing to draw here; the block is shown as content.`
|
|
12150
|
+
}
|
|
12151
|
+
};
|
|
12152
|
+
}
|
|
11059
12153
|
if (execution.status === "ok") {
|
|
11060
12154
|
const styledLayers = runtime.applyRenderStyle !== false ? applyRenderStyleToLayers(execution.layers, block, theme) : execution.layers;
|
|
11061
12155
|
const layers = appendRichContentLayers(
|
|
@@ -11116,6 +12210,16 @@ function materializeBlockLayersWithRuntime(block, options, runtime) {
|
|
|
11116
12210
|
diagnostic
|
|
11117
12211
|
};
|
|
11118
12212
|
}
|
|
12213
|
+
function drawsNothing(layers) {
|
|
12214
|
+
return layers.every(
|
|
12215
|
+
(layer) => (layer.type === "image" || layer.type === "video") && !layer.content.src
|
|
12216
|
+
);
|
|
12217
|
+
}
|
|
12218
|
+
function hasContentToShow(block) {
|
|
12219
|
+
if (resolveTemplateName(block.template) === "content") return false;
|
|
12220
|
+
const { title, contents } = block;
|
|
12221
|
+
return !!title?.trim() || (contents?.length ?? 0) > 0;
|
|
12222
|
+
}
|
|
11119
12223
|
function mermaidSources(block) {
|
|
11120
12224
|
return (block.contents ?? []).filter(
|
|
11121
12225
|
(node) => node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid"
|
|
@@ -11249,15 +12353,15 @@ function collectRichMediaItems(layers, block) {
|
|
|
11249
12353
|
...embeddedSpatialMedia(block)
|
|
11250
12354
|
];
|
|
11251
12355
|
}
|
|
11252
|
-
function gridCells(
|
|
12356
|
+
function gridCells(region2, count, viewport, inset) {
|
|
11253
12357
|
const unit = Math.min(viewport.width, viewport.height);
|
|
11254
12358
|
const padding = inset ? unit * 0.025 : 0;
|
|
11255
12359
|
const gap = count > 1 ? unit * 0.018 : 0;
|
|
11256
12360
|
const inner = {
|
|
11257
|
-
x:
|
|
11258
|
-
y:
|
|
11259
|
-
width: Math.max(0,
|
|
11260
|
-
height: Math.max(0,
|
|
12361
|
+
x: region2.x + padding,
|
|
12362
|
+
y: region2.y + padding,
|
|
12363
|
+
width: Math.max(0, region2.width - padding * 2),
|
|
12364
|
+
height: Math.max(0, region2.height - padding * 2)
|
|
11261
12365
|
};
|
|
11262
12366
|
const columns2 = Math.min(
|
|
11263
12367
|
count,
|
|
@@ -11407,7 +12511,42 @@ function applyEmbeddedVideoTiming(layers, block) {
|
|
|
11407
12511
|
};
|
|
11408
12512
|
});
|
|
11409
12513
|
}
|
|
12514
|
+
function applyEmbeddedVideoEdits(layers, block) {
|
|
12515
|
+
const editedBySrc = new Map(
|
|
12516
|
+
extractEmbeddedVideos(block.contents).filter((video) => video.edits).map((video) => [video.src, video])
|
|
12517
|
+
);
|
|
12518
|
+
if (editedBySrc.size === 0) return layers;
|
|
12519
|
+
return layers.map((layer) => {
|
|
12520
|
+
if (layer.type !== "video") return layer;
|
|
12521
|
+
const video = editedBySrc.get(layer.content.src);
|
|
12522
|
+
if (!video?.edits) return layer;
|
|
12523
|
+
const { fx, gain, fadeIn, fadeOut, crop } = video.edits;
|
|
12524
|
+
const edits = {
|
|
12525
|
+
...fx ? { fx } : {},
|
|
12526
|
+
...gain ? { gain } : {},
|
|
12527
|
+
...fadeIn ? { fadeIn } : {},
|
|
12528
|
+
...fadeOut ? { fadeOut } : {},
|
|
12529
|
+
...crop ? { crop } : {}
|
|
12530
|
+
};
|
|
12531
|
+
const hasEdits = Object.keys(edits).length > 0;
|
|
12532
|
+
const withEdits = (content) => hasEdits ? { ...layer, content: { ...content, edits } } : content === layer.content ? layer : { ...layer, content };
|
|
12533
|
+
if (!video.cuts?.length) return withEdits(layer.content);
|
|
12534
|
+
const { clipStart } = layer.content;
|
|
12535
|
+
const clipEnd = video.clipEnd != null ? layer.content.clipEnd : createMediaTimeMap({ clipStart, cuts: video.cuts }).playedToSource(
|
|
12536
|
+
Math.max(0, layer.content.clipEnd - clipStart)
|
|
12537
|
+
);
|
|
12538
|
+
const cuts = normalizeMediaCuts(video.cuts, clipStart, clipEnd);
|
|
12539
|
+
if (cuts.length === 0) return withEdits(layer.content);
|
|
12540
|
+
return withEdits({ ...layer.content, clipEnd, cuts });
|
|
12541
|
+
});
|
|
12542
|
+
}
|
|
11410
12543
|
function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRatios) {
|
|
12544
|
+
return applyEmbeddedVideoEdits(
|
|
12545
|
+
placeRichContentLayers(layers, block, theme, viewport, mediaAspectRatios),
|
|
12546
|
+
block
|
|
12547
|
+
);
|
|
12548
|
+
}
|
|
12549
|
+
function placeRichContentLayers(layers, block, theme, viewport, mediaAspectRatios) {
|
|
11411
12550
|
const timedLayers = applyEmbeddedVideoTiming(layers, block);
|
|
11412
12551
|
const items = collectRichMediaItems(timedLayers, block);
|
|
11413
12552
|
if (items.length === 0) return timedLayers;
|
|
@@ -11631,6 +12770,30 @@ var TEMPLATE_METADATA = {
|
|
|
11631
12770
|
label: "Layers",
|
|
11632
12771
|
description: "Draws nested parts as concentric rings, innermost first, each one called out."
|
|
11633
12772
|
},
|
|
12773
|
+
stats: {
|
|
12774
|
+
label: "Key Numbers",
|
|
12775
|
+
description: "Sets two to six key figures side by side, each counting up over what it measures."
|
|
12776
|
+
},
|
|
12777
|
+
pyramid: {
|
|
12778
|
+
label: "Pyramid",
|
|
12779
|
+
description: "Stacks a hierarchy as a pyramid, the apex first and the base last."
|
|
12780
|
+
},
|
|
12781
|
+
funnel: {
|
|
12782
|
+
label: "Funnel",
|
|
12783
|
+
description: "Draws stages that narrow as a funnel, the widest first and the outcome last."
|
|
12784
|
+
},
|
|
12785
|
+
hub: {
|
|
12786
|
+
label: "Hub",
|
|
12787
|
+
description: "Arranges ideas around the one thing they share, each joined to it by a spoke."
|
|
12788
|
+
},
|
|
12789
|
+
venn: {
|
|
12790
|
+
label: "Venn",
|
|
12791
|
+
description: "Draws two sets as overlapping circles, with what they share in the overlap."
|
|
12792
|
+
},
|
|
12793
|
+
matrix: {
|
|
12794
|
+
label: "Matrix",
|
|
12795
|
+
description: "Places four ideas on two axes as a two-by-two grid."
|
|
12796
|
+
},
|
|
11634
12797
|
photoGrid: {
|
|
11635
12798
|
label: "Photo Grid",
|
|
11636
12799
|
description: "Arranges up to four photos in a mosaic grid."
|
|
@@ -11844,6 +13007,42 @@ var TEMPLATE_AUTHORING_METADATA = {
|
|
|
11844
13007
|
safeForContentFirst: false,
|
|
11845
13008
|
placement: "heading"
|
|
11846
13009
|
},
|
|
13010
|
+
stats: {
|
|
13011
|
+
role: "highlight",
|
|
13012
|
+
bodyPolicy: "derived",
|
|
13013
|
+
safeForContentFirst: false,
|
|
13014
|
+
placement: "heading"
|
|
13015
|
+
},
|
|
13016
|
+
pyramid: {
|
|
13017
|
+
role: "content",
|
|
13018
|
+
bodyPolicy: "derived",
|
|
13019
|
+
safeForContentFirst: false,
|
|
13020
|
+
placement: "heading"
|
|
13021
|
+
},
|
|
13022
|
+
funnel: {
|
|
13023
|
+
role: "content",
|
|
13024
|
+
bodyPolicy: "derived",
|
|
13025
|
+
safeForContentFirst: false,
|
|
13026
|
+
placement: "heading"
|
|
13027
|
+
},
|
|
13028
|
+
hub: {
|
|
13029
|
+
role: "content",
|
|
13030
|
+
bodyPolicy: "derived",
|
|
13031
|
+
safeForContentFirst: false,
|
|
13032
|
+
placement: "heading"
|
|
13033
|
+
},
|
|
13034
|
+
venn: {
|
|
13035
|
+
role: "comparison",
|
|
13036
|
+
bodyPolicy: "derived",
|
|
13037
|
+
safeForContentFirst: false,
|
|
13038
|
+
placement: "heading"
|
|
13039
|
+
},
|
|
13040
|
+
matrix: {
|
|
13041
|
+
role: "comparison",
|
|
13042
|
+
bodyPolicy: "derived",
|
|
13043
|
+
safeForContentFirst: false,
|
|
13044
|
+
placement: "heading"
|
|
13045
|
+
},
|
|
11847
13046
|
photoGrid: {
|
|
11848
13047
|
role: "media",
|
|
11849
13048
|
bodyPolicy: "structured",
|
|
@@ -11960,12 +13159,217 @@ var TEMPLATE_AUTHORING_METADATA = {
|
|
|
11960
13159
|
}
|
|
11961
13160
|
};
|
|
11962
13161
|
|
|
13162
|
+
// src/transform/presentationPlan.ts
|
|
13163
|
+
var PRESENTATION_KEY = "squisq-presentation";
|
|
13164
|
+
var PRESENTATION_LAYOUTS = [
|
|
13165
|
+
"title",
|
|
13166
|
+
"divider",
|
|
13167
|
+
"statement",
|
|
13168
|
+
"emphasis",
|
|
13169
|
+
"quote",
|
|
13170
|
+
"stat",
|
|
13171
|
+
"definition",
|
|
13172
|
+
"list",
|
|
13173
|
+
"comparison",
|
|
13174
|
+
"steps",
|
|
13175
|
+
"image",
|
|
13176
|
+
"cards",
|
|
13177
|
+
"process",
|
|
13178
|
+
"cycle",
|
|
13179
|
+
"layers",
|
|
13180
|
+
"stats",
|
|
13181
|
+
"pyramid",
|
|
13182
|
+
"funnel",
|
|
13183
|
+
"hub",
|
|
13184
|
+
"venn",
|
|
13185
|
+
"matrix"
|
|
13186
|
+
];
|
|
13187
|
+
var AUTHORED_PRESENTATION_LAYOUT = "authored";
|
|
13188
|
+
var EMPHASIS_CHARACTERS = 60;
|
|
13189
|
+
var spaced = (text2) => text2.replace(/\s+/gu, " ").trim();
|
|
13190
|
+
function presentationStat(headline, points2) {
|
|
13191
|
+
for (const [index, line] of [headline, ...points2].entries()) {
|
|
13192
|
+
const match = matchNumberHighlight(line);
|
|
13193
|
+
if (match) return { match, line, index: index - 1 };
|
|
13194
|
+
}
|
|
13195
|
+
return null;
|
|
13196
|
+
}
|
|
13197
|
+
function presentationLayoutFits(layout, headline, points2, passage) {
|
|
13198
|
+
return presentationLayoutProblem(layout, headline, points2, passage) === void 0;
|
|
13199
|
+
}
|
|
13200
|
+
function presentationLayoutProblem(layout, headline, points2, passage) {
|
|
13201
|
+
const concept = conceptTemplate(layout);
|
|
13202
|
+
if (concept) return conceptItemsProblem(concept, points2, { max: PRESENTATION_LIMITS.points });
|
|
13203
|
+
switch (layout) {
|
|
13204
|
+
case "list":
|
|
13205
|
+
case "steps":
|
|
13206
|
+
return points2.length >= 2 ? void 0 : `${layout} takes at least 2 points and this has ${points2.length}`;
|
|
13207
|
+
case "comparison":
|
|
13208
|
+
return points2.length === 2 ? void 0 : `comparison takes exactly 2 points and this has ${points2.length}`;
|
|
13209
|
+
case "definition":
|
|
13210
|
+
return points2.length >= 1 ? void 0 : "definition takes a point that defines its headline";
|
|
13211
|
+
case "stat":
|
|
13212
|
+
return presentationStat(headline, points2) !== null ? void 0 : "stat shows a prominent figure, such as 40% or $3 million, and neither the headline nor a point states one";
|
|
13213
|
+
case "emphasis":
|
|
13214
|
+
return headline.length <= EMPHASIS_CHARACTERS ? void 0 : `emphasis takes a headline of at most ${EMPHASIS_CHARACTERS} characters`;
|
|
13215
|
+
case "quote":
|
|
13216
|
+
return spaced(passage).includes(spaced(headline)) ? void 0 : "quote takes a headline copied word for word from the passage";
|
|
13217
|
+
default:
|
|
13218
|
+
return void 0;
|
|
13219
|
+
}
|
|
13220
|
+
}
|
|
13221
|
+
function presentationLayoutNeeds(layout, wording = {}) {
|
|
13222
|
+
const points2 = wording.points ?? "points";
|
|
13223
|
+
const concept = conceptTemplate(layout);
|
|
13224
|
+
if (concept) {
|
|
13225
|
+
const count = `${conceptItemCount(concept, { max: PRESENTATION_LIMITS.points })} ${points2}`;
|
|
13226
|
+
if (concept === "stats") return `${count}, each stating a figure`;
|
|
13227
|
+
if (concept === "venn") return `${count}, one of them labelled "Both:"`;
|
|
13228
|
+
return count;
|
|
13229
|
+
}
|
|
13230
|
+
switch (layout) {
|
|
13231
|
+
case "list":
|
|
13232
|
+
case "steps":
|
|
13233
|
+
return `at least 2 ${points2}`;
|
|
13234
|
+
case "comparison":
|
|
13235
|
+
return `exactly 2 ${points2}`;
|
|
13236
|
+
case "stat":
|
|
13237
|
+
return "a prominent figure, such as 40% or $3 million";
|
|
13238
|
+
case "emphasis":
|
|
13239
|
+
return `a headline of ${EMPHASIS_CHARACTERS} characters or fewer`;
|
|
13240
|
+
case "quote":
|
|
13241
|
+
return "a sentence of 110 characters or fewer to quote";
|
|
13242
|
+
case "image":
|
|
13243
|
+
return "a picture";
|
|
13244
|
+
default:
|
|
13245
|
+
return void 0;
|
|
13246
|
+
}
|
|
13247
|
+
}
|
|
13248
|
+
var BEAT_EXTRAS = ["center", "xAxis", "yAxis"];
|
|
13249
|
+
var LAYOUT_EXTRAS = {
|
|
13250
|
+
hub: ["center"],
|
|
13251
|
+
matrix: ["xAxis", "yAxis"]
|
|
13252
|
+
};
|
|
13253
|
+
function presentationBeatExtras(layout, source) {
|
|
13254
|
+
const extras = {};
|
|
13255
|
+
if (!record(source)) return extras;
|
|
13256
|
+
for (const key of LAYOUT_EXTRAS[layout] ?? []) {
|
|
13257
|
+
const value = source[key];
|
|
13258
|
+
const clean = typeof value === "string" ? value.replace(/\s+/gu, " ").trim() : "";
|
|
13259
|
+
if (clean && text(clean, PRESENTATION_LIMITS.extraCharacters)) extras[key] = clean;
|
|
13260
|
+
}
|
|
13261
|
+
return extras;
|
|
13262
|
+
}
|
|
13263
|
+
var PRESENTATION_LIMITS = {
|
|
13264
|
+
sourceCharacters: 4e4,
|
|
13265
|
+
beats: 80,
|
|
13266
|
+
serializedCharacters: 16e4,
|
|
13267
|
+
mediaPerBeat: 4,
|
|
13268
|
+
/** The most points a summary slide holds. */
|
|
13269
|
+
points: 4,
|
|
13270
|
+
/** The longest a hub's centre or a matrix's axis name may be. */
|
|
13271
|
+
extraCharacters: 40
|
|
13272
|
+
};
|
|
13273
|
+
function record(value) {
|
|
13274
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
13275
|
+
}
|
|
13276
|
+
function keys(value, required, optional = []) {
|
|
13277
|
+
return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
13278
|
+
}
|
|
13279
|
+
function text(value, max) {
|
|
13280
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !Array.from(value).some((character) => {
|
|
13281
|
+
const code = character.charCodeAt(0);
|
|
13282
|
+
return code < 32 && code !== 9 && code !== 10 && code !== 13;
|
|
13283
|
+
});
|
|
13284
|
+
}
|
|
13285
|
+
function parsePresentationPlan(input) {
|
|
13286
|
+
let value = input;
|
|
13287
|
+
if (typeof value === "string") {
|
|
13288
|
+
if (value.length > PRESENTATION_LIMITS.serializedCharacters) return null;
|
|
13289
|
+
try {
|
|
13290
|
+
value = JSON.parse(value);
|
|
13291
|
+
} catch {
|
|
13292
|
+
return null;
|
|
13293
|
+
}
|
|
13294
|
+
}
|
|
13295
|
+
if (!record(value) || !keys(value, ["version", "sourceText", "origin", "beats"]) || value.version !== 1 || !text(value.sourceText, PRESENTATION_LIMITS.sourceCharacters) || typeof value.origin !== "string" || !["automatic", "ai"].includes(value.origin) || !Array.isArray(value.beats) || !value.beats.length || value.beats.length > PRESENTATION_LIMITS.beats)
|
|
13296
|
+
return null;
|
|
13297
|
+
const beats = [];
|
|
13298
|
+
const ids = /* @__PURE__ */ new Set();
|
|
13299
|
+
let end = 0;
|
|
13300
|
+
for (const beat of value.beats) {
|
|
13301
|
+
if (!record(beat) || !keys(
|
|
13302
|
+
beat,
|
|
13303
|
+
["id", "sourceStart", "sourceEnd", "layout", "headline", "points"],
|
|
13304
|
+
["imageSrc", "media", "hidden", ...BEAT_EXTRAS]
|
|
13305
|
+
) || "hidden" in beat && beat.hidden !== true || !text(beat.id, 80) || ids.has(beat.id) || !Number.isInteger(beat.sourceStart) || !Number.isInteger(beat.sourceEnd) || typeof beat.sourceStart !== "number" || typeof beat.sourceEnd !== "number" || beat.sourceStart < end || beat.sourceEnd <= beat.sourceStart || beat.sourceEnd > value.sourceText.length || value.sourceText.slice(end, beat.sourceStart).trim() || !text(beat.headline, 110) || !Array.isArray(beat.points) || beat.points.length > PRESENTATION_LIMITS.points || !beat.points.every((point) => text(point, 180)))
|
|
13306
|
+
return null;
|
|
13307
|
+
const extras = presentationBeatExtras(String(beat.layout), beat);
|
|
13308
|
+
if (BEAT_EXTRAS.some((key) => key in beat && beat[key] !== extras[key])) return null;
|
|
13309
|
+
if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) {
|
|
13310
|
+
if (beat.points.length || "media" in beat) return null;
|
|
13311
|
+
} else if (!PRESENTATION_LAYOUTS.includes(beat.layout) || !presentationLayoutFits(
|
|
13312
|
+
beat.layout,
|
|
13313
|
+
beat.headline,
|
|
13314
|
+
beat.points,
|
|
13315
|
+
value.sourceText.slice(beat.sourceStart, beat.sourceEnd)
|
|
13316
|
+
))
|
|
13317
|
+
return null;
|
|
13318
|
+
if (beat.layout === "image" ? !text(beat.imageSrc, 2048) : "imageSrc" in beat) return null;
|
|
13319
|
+
if ("media" in beat && (!Array.isArray(beat.media) || !beat.media.length || beat.media.length > PRESENTATION_LIMITS.mediaPerBeat || new Set(beat.media).size !== beat.media.length || !beat.media.every((ref) => text(ref, 1100))))
|
|
13320
|
+
return null;
|
|
13321
|
+
ids.add(beat.id);
|
|
13322
|
+
end = beat.sourceEnd;
|
|
13323
|
+
beats.push({
|
|
13324
|
+
id: beat.id,
|
|
13325
|
+
sourceStart: beat.sourceStart,
|
|
13326
|
+
sourceEnd: beat.sourceEnd,
|
|
13327
|
+
layout: beat.layout,
|
|
13328
|
+
headline: beat.headline,
|
|
13329
|
+
points: [...beat.points],
|
|
13330
|
+
...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {},
|
|
13331
|
+
..."media" in beat ? { media: [...beat.media] } : {},
|
|
13332
|
+
...beat.hidden === true ? { hidden: true } : {},
|
|
13333
|
+
...extras
|
|
13334
|
+
});
|
|
13335
|
+
}
|
|
13336
|
+
if (value.sourceText.slice(end).trim()) return null;
|
|
13337
|
+
return {
|
|
13338
|
+
version: 1,
|
|
13339
|
+
sourceText: value.sourceText,
|
|
13340
|
+
origin: value.origin,
|
|
13341
|
+
beats
|
|
13342
|
+
};
|
|
13343
|
+
}
|
|
13344
|
+
function serializePresentationPlan(plan) {
|
|
13345
|
+
const parsed = parsePresentationPlan(plan);
|
|
13346
|
+
if (!parsed) throw new Error("The presentation plan is invalid.");
|
|
13347
|
+
const result = JSON.stringify(parsed);
|
|
13348
|
+
if (result.length > PRESENTATION_LIMITS.serializedCharacters)
|
|
13349
|
+
throw new Error("The presentation plan is too large.");
|
|
13350
|
+
return result;
|
|
13351
|
+
}
|
|
13352
|
+
|
|
11963
13353
|
// src/doc/templates/authoringGuide.ts
|
|
11964
13354
|
var CHART = {
|
|
11965
13355
|
fields: "title, headers [label, value], rows [[label, value], \u2026] 2\u20136 rows, showValues: true",
|
|
11966
13356
|
use: "only values the {source} states"
|
|
11967
13357
|
};
|
|
11968
13358
|
var CONCEPT_BUILD = 'build: {mode: "items"} to reveal them as they are spoken';
|
|
13359
|
+
function conceptFields(template, items) {
|
|
13360
|
+
const spec = CONCEPT_SPECS[template];
|
|
13361
|
+
const inputs = spec.inputs ?? [];
|
|
13362
|
+
const form = spec.itemForm === "figure-caption" ? '"Figure: what it measures"' : '"Label: detail"';
|
|
13363
|
+
return [
|
|
13364
|
+
"title",
|
|
13365
|
+
// What names the picture comes before its items; a switch comes after.
|
|
13366
|
+
...inputs.filter((input) => input.key !== "versus").map((input) => input.guide),
|
|
13367
|
+
items ?? `items: [${form}, \u2026] ${conceptItemCount(template)}${spec.order ? ` ${spec.order}` : ""}`,
|
|
13368
|
+
...inputs.filter((input) => input.key === "versus").map((input) => input.guide),
|
|
13369
|
+
CONCEPT_BUILD
|
|
13370
|
+
].join(", ");
|
|
13371
|
+
}
|
|
13372
|
+
var points = (template) => conceptItemCount(template, { max: PRESENTATION_LIMITS.points });
|
|
11969
13373
|
var TEMPLATE_GUIDES = {
|
|
11970
13374
|
imageWithCaption: {
|
|
11971
13375
|
fields: "imageSrc, imageAlt, caption \u2264 90 chars, ambientMotion: zoomIn|zoomOut|panLeft|panRight",
|
|
@@ -12002,7 +13406,7 @@ var TEMPLATE_GUIDES = {
|
|
|
12002
13406
|
use: "3\u20137 dated events the {source} states"
|
|
12003
13407
|
},
|
|
12004
13408
|
diagram: {
|
|
12005
|
-
fields: "title, nodeShape: 'pill', nodes: [{ id, label \u2264 3 words
|
|
13409
|
+
fields: "title, nodeShape: 'pill', nodes: [{ id, label \u2264 3 words }] 3\u20136 nodes (no x or y: the diagram places them from the edges), edges: [{ source, target, label \u2264 3 words }]",
|
|
12006
13410
|
use: "relationships the {source} describes"
|
|
12007
13411
|
},
|
|
12008
13412
|
list: {
|
|
@@ -12021,24 +13425,54 @@ var TEMPLATE_GUIDES = {
|
|
|
12021
13425
|
use: "opens a section on a photograph"
|
|
12022
13426
|
},
|
|
12023
13427
|
cards: {
|
|
12024
|
-
fields:
|
|
13428
|
+
fields: conceptFields("cards"),
|
|
12025
13429
|
use: "parallel ideas side by side, such as kinds, causes or features"
|
|
12026
13430
|
},
|
|
12027
13431
|
process: {
|
|
12028
|
-
fields:
|
|
13432
|
+
fields: conceptFields("process"),
|
|
12029
13433
|
use: "ordered stages or dated events, drawn as numbered steps joined by arrows"
|
|
12030
13434
|
},
|
|
12031
13435
|
cycle: {
|
|
12032
|
-
fields:
|
|
13436
|
+
fields: conceptFields("cycle"),
|
|
12033
13437
|
use: "stages that repeat in a loop, drawn on a ring"
|
|
12034
13438
|
},
|
|
12035
13439
|
layers: {
|
|
12036
|
-
fields:
|
|
13440
|
+
fields: conceptFields("layers"),
|
|
12037
13441
|
use: "parts nested inside one another, drawn as concentric rings"
|
|
13442
|
+
},
|
|
13443
|
+
stats: {
|
|
13444
|
+
fields: conceptFields(
|
|
13445
|
+
"stats",
|
|
13446
|
+
`items: ["Figure: what it measures", \u2026] ${conceptItemCount("stats")} such as "40%: of trips made on foot"`
|
|
13447
|
+
),
|
|
13448
|
+
use: "several key numbers the {source} states, side by side; each counts up"
|
|
13449
|
+
},
|
|
13450
|
+
pyramid: {
|
|
13451
|
+
fields: conceptFields("pyramid"),
|
|
13452
|
+
use: "a hierarchy, or levels that rest on one another, drawn as a pyramid"
|
|
13453
|
+
},
|
|
13454
|
+
funnel: {
|
|
13455
|
+
fields: conceptFields("funnel"),
|
|
13456
|
+
use: "stages that narrow from many to few, drawn as a funnel"
|
|
13457
|
+
},
|
|
13458
|
+
hub: {
|
|
13459
|
+
fields: conceptFields("hub"),
|
|
13460
|
+
use: "factors, causes or parts of the one thing `center` names, drawn around a hub"
|
|
13461
|
+
},
|
|
13462
|
+
venn: {
|
|
13463
|
+
fields: conceptFields(
|
|
13464
|
+
"venn",
|
|
13465
|
+
'items: ["A: what only A has", "B: what only B has", "Both: what they share"]'
|
|
13466
|
+
),
|
|
13467
|
+
use: "two things compared by what each has alone and what they share, drawn as overlapping circles"
|
|
13468
|
+
},
|
|
13469
|
+
matrix: {
|
|
13470
|
+
fields: conceptFields("matrix"),
|
|
13471
|
+
use: "four ideas placed on two axes, such as effort against impact"
|
|
12038
13472
|
}
|
|
12039
13473
|
};
|
|
12040
|
-
function guideText(
|
|
12041
|
-
return
|
|
13474
|
+
function guideText(text2, wording = {}) {
|
|
13475
|
+
return text2.replace(/\{source\}/gu, wording.source ?? "source");
|
|
12042
13476
|
}
|
|
12043
13477
|
function templateCatalog(entries, wording = {}) {
|
|
12044
13478
|
return entries.map((entry) => {
|
|
@@ -12059,13 +13493,51 @@ var PRESENTATION_LAYOUT_GUIDE = {
|
|
|
12059
13493
|
comparison: "exactly 2 genuinely contrasting points",
|
|
12060
13494
|
steps: "2\u20133 ordered actions",
|
|
12061
13495
|
image: 'only when the current layout is "image"; the headline is then its caption',
|
|
12062
|
-
cards: "
|
|
12063
|
-
process: "
|
|
12064
|
-
cycle: "
|
|
12065
|
-
layers: "
|
|
13496
|
+
cards: `${points("cards")} parallel ideas side by side, such as kinds, causes or features`,
|
|
13497
|
+
process: `${points("process")} ordered stages or dated events, drawn as numbered steps joined by arrows`,
|
|
13498
|
+
cycle: `${points("cycle")} stages that repeat in a loop, drawn on a ring`,
|
|
13499
|
+
layers: `${points("layers")} parts nested inside one another, innermost first, such as a core and the layers around it, drawn as concentric rings`,
|
|
13500
|
+
stats: `${points("stats")} key figures the passage states, each point the number itself, then a colon and what it measures, such as "40%: of trips made on foot" or "$3 million: raised"; they count up side by side`,
|
|
13501
|
+
pyramid: `${points("pyramid")} levels of a hierarchy, the top first and the base last, drawn as a pyramid`,
|
|
13502
|
+
funnel: `${points("funnel")} stages that narrow from many to few, the widest first, drawn as a funnel`,
|
|
13503
|
+
hub: `${points("hub")} factors, causes or parts of one thing, drawn around a hub; add "center", that thing in 1\u20133 words`,
|
|
13504
|
+
venn: `${points("venn")} points: two things compared, each "Name: what only it has", then "Both: what they share"; drawn as overlapping circles`,
|
|
13505
|
+
matrix: `${points("matrix")} points placed on two axes, in reading order: top left, top right, bottom left, bottom right; add "xAxis" and "yAxis", what grows from left to right and from bottom to top`
|
|
12066
13506
|
};
|
|
12067
|
-
var
|
|
12068
|
-
|
|
13507
|
+
var PRESENTATION_LAYOUT_LABELS = {
|
|
13508
|
+
title: { label: "Title", description: "Opens the deck or closes it." },
|
|
13509
|
+
divider: { label: "Section break", description: "A new topic begins; only the headline." },
|
|
13510
|
+
statement: { label: "Statement", description: "One idea under the headline." },
|
|
13511
|
+
emphasis: { label: "Big statement", description: "One short takeaway in display type." },
|
|
13512
|
+
quote: { label: "Quote", description: "One sentence in the passage\u2019s own words." },
|
|
13513
|
+
stat: { label: "Key number", description: "One figure, large, beside what it means." },
|
|
13514
|
+
definition: { label: "Definition", description: "A term and what it means." },
|
|
13515
|
+
list: { label: "Points", description: "Two or three points side by side." },
|
|
13516
|
+
comparison: { label: "Comparison", description: "Two sides set against each other." },
|
|
13517
|
+
steps: { label: "Steps", description: "Ordered actions, numbered and joined by arrows." },
|
|
13518
|
+
image: { label: "Picture", description: "A picture with the headline as its caption." },
|
|
13519
|
+
cards: { label: "Cards", description: "Parallel ideas side by side." },
|
|
13520
|
+
process: { label: "Process", description: "Ordered stages; dated ones draw as a timeline." },
|
|
13521
|
+
cycle: { label: "Cycle", description: "Stages on a ring that comes back to its start." },
|
|
13522
|
+
layers: { label: "Layers", description: "Nested parts as concentric rings." },
|
|
13523
|
+
stats: { label: "Key numbers", description: "Several figures side by side, counting up." },
|
|
13524
|
+
pyramid: { label: "Pyramid", description: "Levels of a hierarchy, the top first." },
|
|
13525
|
+
funnel: { label: "Funnel", description: "Stages that narrow from many to few." },
|
|
13526
|
+
hub: { label: "Hub", description: "Ideas around the one thing they share." },
|
|
13527
|
+
venn: { label: "Venn", description: "Two sets and what they share." },
|
|
13528
|
+
matrix: { label: "Matrix", description: "Four ideas on two axes." }
|
|
13529
|
+
};
|
|
13530
|
+
var DRAWN_CORE = ["cards", "process", "cycle", "layers"];
|
|
13531
|
+
var DRAWN_SHAPES = ["pyramid", "funnel", "hub", "venn", "matrix"];
|
|
13532
|
+
var DRAWN = [...DRAWN_CORE, ...DRAWN_SHAPES];
|
|
13533
|
+
var NUMBERS = [
|
|
13534
|
+
"statHighlight",
|
|
13535
|
+
"stats",
|
|
13536
|
+
"comparisonBar",
|
|
13537
|
+
"columnChart",
|
|
13538
|
+
"barChart",
|
|
13539
|
+
"pieChart"
|
|
13540
|
+
];
|
|
12069
13541
|
var ACCENTED = [
|
|
12070
13542
|
"factCard",
|
|
12071
13543
|
"statHighlight",
|
|
@@ -12081,7 +13553,9 @@ var AUTHORING_TRICKS = [
|
|
|
12081
13553
|
},
|
|
12082
13554
|
{
|
|
12083
13555
|
id: "numbers-as-numbers",
|
|
12084
|
-
text: "Draw numbers as numbers: one striking quantity \u2192 statHighlight (it counts up); two quantities in one unit \u2192 comparisonBar; three or more values in one unit \u2192 a chart; three to seven dated events \u2192 timeline.",
|
|
13556
|
+
text: "Draw numbers as numbers: one striking quantity \u2192 statHighlight (it counts up); two to four key quantities in different units \u2192 stats; two quantities in one unit \u2192 comparisonBar; three or more values in one unit \u2192 a chart; three to seven dated events \u2192 timeline.",
|
|
13557
|
+
summary: 'Draw numbers as numbers: when the passage turns on one figure choose "stat"; when it gives two to four key figures choose "stats" and start each point with its number, as in "40%: of trips made on foot".',
|
|
13558
|
+
storyboard: 'Draw numbers as numbers: a passage that turns on one figure is a "stat"; one that gives two to four key figures is "stats".',
|
|
12085
13559
|
templates: [...NUMBERS, "timeline"]
|
|
12086
13560
|
},
|
|
12087
13561
|
{
|
|
@@ -12092,8 +13566,16 @@ var AUTHORING_TRICKS = [
|
|
|
12092
13566
|
{
|
|
12093
13567
|
id: "draw-ideas",
|
|
12094
13568
|
text: `Slides that draw their ideas explain best: several parallel ideas \u2192 cards, ordered stages \u2192 process, a loop that comes back to its start \u2192 cycle, parts nested inside one another \u2192 layers; prefer them to a list. Write each item as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters, in the {source}'s own words. For dated events, start each label with its date: a process of dated stages draws as a timeline.`,
|
|
12095
|
-
summary: `Slides that draw their ideas explain best: when the passage has several parallel ideas, ordered stages, a repeating loop or nested parts, choose ${
|
|
12096
|
-
|
|
13569
|
+
summary: `Slides that draw their ideas explain best: when the passage has several parallel ideas, ordered stages, a repeating loop or nested parts, choose ${DRAWN_CORE.map((layout) => `"${layout}"`).join(", ")} rather than "statement" or "list", and write each of their points (up to 4) as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters. For dated events, start each label with its date.`,
|
|
13570
|
+
storyboard: 'Draw ideas rather than list them: several parallel ideas are "cards", ordered stages or dated events a "process", a loop that comes back to its start a "cycle", and parts nested inside one another "layers".',
|
|
13571
|
+
templates: DRAWN_CORE
|
|
13572
|
+
},
|
|
13573
|
+
{
|
|
13574
|
+
id: "draw-shapes",
|
|
13575
|
+
text: 'Some relationships have a shape of their own: a hierarchy or levels that rest on one another \u2192 pyramid (top first); stages that narrow from many to few \u2192 funnel (widest first); factors, causes or parts of one thing \u2192 hub, with that thing as `center`; two things and what they share \u2192 venn, its last item "Both: \u2026"; four ideas on two axes \u2192 matrix. Choose one only when the {source} states that relationship; a shape the ideas do not have misleads.',
|
|
13576
|
+
summary: 'Some relationships have a shape of their own: a hierarchy \u2192 "pyramid", stages that narrow from many to few \u2192 "funnel", factors or causes of one thing \u2192 "hub", two things and what they share \u2192 "venn" (its third point "Both: \u2026"), four ideas on two axes \u2192 "matrix". Choose one only when the passage states that relationship; a shape the ideas do not have misleads.',
|
|
13577
|
+
storyboard: 'Some relationships have a shape of their own: a hierarchy is a "pyramid", stages that narrow from many to few a "funnel", factors or causes of one thing a "hub", two things and what they share a "venn", and four ideas on two axes a "matrix". Choose one only when the passage states that relationship.',
|
|
13578
|
+
templates: DRAWN_SHAPES
|
|
12097
13579
|
},
|
|
12098
13580
|
{
|
|
12099
13581
|
id: "reveal-as-spoken",
|
|
@@ -12117,7 +13599,8 @@ var AUTHORING_TRICKS = [
|
|
|
12117
13599
|
},
|
|
12118
13600
|
{
|
|
12119
13601
|
id: "vary",
|
|
12120
|
-
text: "Vary the slides: when two templates fit equally well, choose the one not just shown. A long run of photo-and-caption slides is where a narrated story feels static."
|
|
13602
|
+
text: "Vary the slides: when two templates fit equally well, choose the one not just shown. A long run of photo-and-caption slides is where a narrated story feels static.",
|
|
13603
|
+
storyboard: "Vary the deck: no layout on more than a third of the slides, and no two neighbouring slides alike when another layout fits as well."
|
|
12121
13604
|
}
|
|
12122
13605
|
];
|
|
12123
13606
|
function authoringTricks(options = {}) {
|
|
@@ -12126,8 +13609,8 @@ function authoringTricks(options = {}) {
|
|
|
12126
13609
|
return selected.filter(
|
|
12127
13610
|
(trick) => !offered || !trick.templates || trick.templates.some((template) => offered.has(template))
|
|
12128
13611
|
).flatMap((trick) => {
|
|
12129
|
-
const
|
|
12130
|
-
return
|
|
13612
|
+
const text2 = options.surface === "summary" ? trick.summary : options.surface === "storyboard" ? trick.storyboard : trick.text;
|
|
13613
|
+
return text2 ? [guideText(text2, options.wording)] : [];
|
|
12131
13614
|
});
|
|
12132
13615
|
}
|
|
12133
13616
|
|
|
@@ -12157,10 +13640,10 @@ function coverScrimGradient(background) {
|
|
|
12157
13640
|
const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
|
|
12158
13641
|
return `linear-gradient(0deg, ${stops.join(", ")})`;
|
|
12159
13642
|
}
|
|
12160
|
-
function fitFrostedText(
|
|
13643
|
+
function fitFrostedText(text2, size, minSize, maxPlateWidth, maxLines) {
|
|
12161
13644
|
const wrapAt = (s) => {
|
|
12162
13645
|
const column = maxPlateWidth - 2 * FROSTED_PAD_X_EM * s;
|
|
12163
|
-
const lines =
|
|
13646
|
+
const lines = text2.split("\n").flatMap((line) => balancedDisplayLines(line, s, column));
|
|
12164
13647
|
const width = Math.max(...lines.map((line) => estimateDisplayTextWidth(line, s)));
|
|
12165
13648
|
return { lines, width, column };
|
|
12166
13649
|
};
|
|
@@ -12658,6 +14141,166 @@ function expandCoverBlock(config, context, template = "cover") {
|
|
|
12658
14141
|
return coverBlock(input, context);
|
|
12659
14142
|
}
|
|
12660
14143
|
|
|
14144
|
+
// src/doc/templates/markdownShapeGuide.ts
|
|
14145
|
+
var MARKDOWN_SHAPE_EXAMPLES = [
|
|
14146
|
+
{
|
|
14147
|
+
template: "process",
|
|
14148
|
+
cue: "a numbered list",
|
|
14149
|
+
brief: ["a numbered list", "steps"],
|
|
14150
|
+
drawn: "numbered steps joined by arrows",
|
|
14151
|
+
heading: "How a release ships",
|
|
14152
|
+
ordered: true,
|
|
14153
|
+
items: ["Draft the release notes", "Tag and test the build", "Roll out to everyone"]
|
|
14154
|
+
},
|
|
14155
|
+
{
|
|
14156
|
+
template: "cards",
|
|
14157
|
+
cue: "items written `Label: detail`",
|
|
14158
|
+
brief: ["`Label: detail` items", "cards"],
|
|
14159
|
+
drawn: "cards side by side",
|
|
14160
|
+
heading: "Three choices for next quarter",
|
|
14161
|
+
items: [
|
|
14162
|
+
"Fund: redesign permissions setup",
|
|
14163
|
+
"Protect: keep the activation gains",
|
|
14164
|
+
"Defer: advanced mobile editing"
|
|
14165
|
+
]
|
|
14166
|
+
},
|
|
14167
|
+
{
|
|
14168
|
+
template: "stats",
|
|
14169
|
+
cue: "items written `Figure: what it measures`",
|
|
14170
|
+
brief: ["`Figure: what it measures` items", "key numbers"],
|
|
14171
|
+
drawn: "key numbers that count up",
|
|
14172
|
+
heading: "The quarter in numbers",
|
|
14173
|
+
items: ["18,420: active teams", "71%: weekly retained teams", "$3.8M: expansion revenue"]
|
|
14174
|
+
},
|
|
14175
|
+
{
|
|
14176
|
+
template: "process",
|
|
14177
|
+
cue: "items that open with dates in order",
|
|
14178
|
+
brief: ["items that open with dates", "a timeline"],
|
|
14179
|
+
drawn: "a timeline",
|
|
14180
|
+
heading: "Our story",
|
|
14181
|
+
items: [
|
|
14182
|
+
"2016: founded in a Portland garage",
|
|
14183
|
+
"2018: first product ships",
|
|
14184
|
+
"2022: Berlin office opens"
|
|
14185
|
+
]
|
|
14186
|
+
},
|
|
14187
|
+
{
|
|
14188
|
+
template: "cycle",
|
|
14189
|
+
cue: "a heading that says cycle or loop",
|
|
14190
|
+
brief: ["a cycle or loop heading", "a ring"],
|
|
14191
|
+
drawn: "stages on a ring",
|
|
14192
|
+
heading: "The water cycle",
|
|
14193
|
+
items: [
|
|
14194
|
+
"Evaporation: the sun lifts water from the sea",
|
|
14195
|
+
"Condensation: vapour cools into cloud",
|
|
14196
|
+
"Precipitation: rain and snow fall"
|
|
14197
|
+
]
|
|
14198
|
+
},
|
|
14199
|
+
{
|
|
14200
|
+
template: "layers",
|
|
14201
|
+
cue: "a heading that says layers or anatomy, with the innermost item first",
|
|
14202
|
+
brief: ["a layers or anatomy heading (innermost first)", "rings"],
|
|
14203
|
+
drawn: "concentric rings",
|
|
14204
|
+
heading: "Layers of the Earth",
|
|
14205
|
+
items: [
|
|
14206
|
+
"Inner core: solid iron",
|
|
14207
|
+
"Outer core: liquid iron",
|
|
14208
|
+
"Mantle: slowly flowing rock",
|
|
14209
|
+
"Crust: the thin outer layer"
|
|
14210
|
+
]
|
|
14211
|
+
},
|
|
14212
|
+
{
|
|
14213
|
+
template: "pyramid",
|
|
14214
|
+
cue: "a heading that says hierarchy, pyramid, tiers or chain of command, with the top item first",
|
|
14215
|
+
brief: ["a hierarchy or pyramid heading (top first)", "a pyramid"],
|
|
14216
|
+
drawn: "a pyramid",
|
|
14217
|
+
heading: "The chain of command",
|
|
14218
|
+
items: [
|
|
14219
|
+
"Chief executive: sets the direction",
|
|
14220
|
+
"Vice presidents: turn it into plans",
|
|
14221
|
+
"Team leads: run the daily work",
|
|
14222
|
+
"Engineers: build the product"
|
|
14223
|
+
]
|
|
14224
|
+
},
|
|
14225
|
+
{
|
|
14226
|
+
template: "funnel",
|
|
14227
|
+
cue: "a heading that says funnel or conversion, with the widest item first",
|
|
14228
|
+
brief: ["a funnel or conversion heading (widest first)", "a funnel"],
|
|
14229
|
+
drawn: "a funnel",
|
|
14230
|
+
heading: "The sign-up funnel",
|
|
14231
|
+
items: ["48,000: visit the site", "6,200: start a trial", "640: become customers"]
|
|
14232
|
+
},
|
|
14233
|
+
{
|
|
14234
|
+
template: "hub",
|
|
14235
|
+
cue: "a heading that names factors, causes, drivers or pillars over `Label: detail` items",
|
|
14236
|
+
brief: ["a factors, causes or drivers heading", "a hub"],
|
|
14237
|
+
drawn: "ideas around a hub",
|
|
14238
|
+
heading: "Three factors behind every delay",
|
|
14239
|
+
items: [
|
|
14240
|
+
"Scope: requirements keep changing",
|
|
14241
|
+
"Staffing: too few senior engineers",
|
|
14242
|
+
"Funding: released two months late"
|
|
14243
|
+
],
|
|
14244
|
+
inputs: { center: "Delays" }
|
|
14245
|
+
},
|
|
14246
|
+
{
|
|
14247
|
+
template: "venn",
|
|
14248
|
+
cue: "two `Name: \u2026` items and a third `Both: \u2026`",
|
|
14249
|
+
brief: ["two items and a `Both: \u2026` item", "a Venn diagram"],
|
|
14250
|
+
drawn: "a Venn diagram",
|
|
14251
|
+
heading: "Designers and engineers",
|
|
14252
|
+
items: [
|
|
14253
|
+
"Designers: research and the interface",
|
|
14254
|
+
"Engineers: architecture and the code",
|
|
14255
|
+
"Both: accessibility and testing"
|
|
14256
|
+
]
|
|
14257
|
+
},
|
|
14258
|
+
{
|
|
14259
|
+
template: "matrix",
|
|
14260
|
+
cue: "exactly four items under a heading that says matrix or quadrants (top left, top right, bottom left, bottom right)",
|
|
14261
|
+
brief: ["four items under a matrix or quadrants heading", "a two-by-two"],
|
|
14262
|
+
drawn: "a two-by-two",
|
|
14263
|
+
heading: "The effort and impact matrix",
|
|
14264
|
+
items: [
|
|
14265
|
+
"Quick wins: little effort, high impact",
|
|
14266
|
+
"Big bets: high effort, high impact",
|
|
14267
|
+
"Fill-ins: little effort, low impact",
|
|
14268
|
+
"Time sinks: high effort, low impact"
|
|
14269
|
+
],
|
|
14270
|
+
inputs: { xAxis: "Effort", yAxis: "Impact" }
|
|
14271
|
+
},
|
|
14272
|
+
{
|
|
14273
|
+
template: "cards",
|
|
14274
|
+
cue: "two items under an `A vs B` heading",
|
|
14275
|
+
brief: ["two items under an `A vs B` heading", "two panels"],
|
|
14276
|
+
drawn: "two panels set against each other",
|
|
14277
|
+
heading: "Self-hosting vs a managed service",
|
|
14278
|
+
items: ["Self-hosting: costs less, needs an engineer", "Managed: costs more, includes backups"]
|
|
14279
|
+
}
|
|
14280
|
+
];
|
|
14281
|
+
function markdownShapeExample(template, options = {}) {
|
|
14282
|
+
const example = MARKDOWN_SHAPE_EXAMPLES.find((entry) => entry.template === template);
|
|
14283
|
+
const hashes = "#".repeat(Math.min(6, Math.max(1, options.headingLevel ?? 2)));
|
|
14284
|
+
const inputs = Object.entries(example.inputs ?? {}).map(([key, value]) => ` ${key}="${value}"`).join("");
|
|
14285
|
+
return [
|
|
14286
|
+
`${hashes} ${example.heading} {[${template}${inputs}]}`,
|
|
14287
|
+
"",
|
|
14288
|
+
...example.items.map((item, index) => `${example.ordered ? `${index + 1}.` : "-"} ${item}`)
|
|
14289
|
+
].join("\n");
|
|
14290
|
+
}
|
|
14291
|
+
function markdownShapeGuide() {
|
|
14292
|
+
return [
|
|
14293
|
+
"A section whose body is one list of 2\u20136 short items is drawn as a picture of how they relate, with no annotation. What decides the picture:",
|
|
14294
|
+
...MARKDOWN_SHAPE_EXAMPLES.map((example) => `- ${example.cue} \u2192 ${example.drawn}`),
|
|
14295
|
+
`To choose a picture yourself, annotate the heading: \`## Title {[funnel]}\`. A hub takes \`center="\u2026"\` (1\u20133 words) and a matrix \`xAxis="\u2026" yAxis="\u2026"\`; a venn is exactly two sets and one \`Both: \u2026\` item, and a matrix ${conceptItemCount("matrix")} items.`,
|
|
14296
|
+
"Only a list that is the whole body is drawn: a paragraph beside it, a link, an image or a seventh item keeps the section as written. Put an introduction in the heading or on the slide before. Name a shape only when the items really have it; a plain list is better than a misleading drawing."
|
|
14297
|
+
];
|
|
14298
|
+
}
|
|
14299
|
+
function markdownShapeSummary() {
|
|
14300
|
+
const rules = MARKDOWN_SHAPE_EXAMPLES.map(({ brief }) => `${brief[0]} \u2192 ${brief[1]}`).join("; ");
|
|
14301
|
+
return `A section whose body is one list of 2\u20136 short items is drawn as a picture of how they relate, with no annotation: ${rules}. Only a list that is the whole body is drawn, so keep any introduction in the heading or on the slide before; to choose a picture yourself, annotate the heading (\`# Title {[funnel]}\`).`;
|
|
14302
|
+
}
|
|
14303
|
+
|
|
12661
14304
|
// src/doc/templates/index.ts
|
|
12662
14305
|
function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme, viewport, persistentLayers, expandedPersistentLayers, registry, failureMode, onDiagnostic, motion, mediaAspectRatios) {
|
|
12663
14306
|
const materialized = materializeBlockLayersWithRuntime(
|
|
@@ -12684,7 +14327,11 @@ function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme,
|
|
|
12684
14327
|
audioSegment: sourceBlock.audioSegment,
|
|
12685
14328
|
...materialized.layers.length > 0 ? { layers: materialized.layers } : {},
|
|
12686
14329
|
transition: sourceBlock.transition,
|
|
12687
|
-
template
|
|
14330
|
+
// A block its template could not draw was drawn as content. Name what
|
|
14331
|
+
// drew it, so renderers size its text as a content slide's: they skip
|
|
14332
|
+
// grow-to-fit by template name, and "comparisonBar" would keep a
|
|
14333
|
+
// paragraph at chart-label size.
|
|
14334
|
+
template: materialized.diagnostic?.code === "template-drew-nothing" ? "content" : sourceBlock.template,
|
|
12688
14335
|
build: sourceBlock.build,
|
|
12689
14336
|
buildTiming: sourceBlock.buildTiming,
|
|
12690
14337
|
buildSource: sourceBlock.buildSource,
|
|
@@ -12896,11 +14543,12 @@ function expandDocBlocks(blocks, options = {}) {
|
|
|
12896
14543
|
}
|
|
12897
14544
|
}
|
|
12898
14545
|
const MIN_TRANSITION_GAP = 5;
|
|
14546
|
+
const GAP_TOLERANCE = 1e-3;
|
|
12899
14547
|
const segmentEnd = audioSegment.startTime + audioSegment.duration;
|
|
12900
14548
|
while (segmentExpandedBlocks.length > 1) {
|
|
12901
14549
|
const lastBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 1];
|
|
12902
14550
|
const timeFromLastToEnd = segmentEnd - lastBlock.startTime;
|
|
12903
|
-
if (mergeShortBlocks && timeFromLastToEnd < MIN_TRANSITION_GAP && lastBlock.template !== "sectionHeader") {
|
|
14551
|
+
if (mergeShortBlocks && timeFromLastToEnd < MIN_TRANSITION_GAP - GAP_TOLERANCE && lastBlock.template !== "sectionHeader") {
|
|
12904
14552
|
const prevBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 2];
|
|
12905
14553
|
prevBlock.duration = segmentEnd - prevBlock.startTime;
|
|
12906
14554
|
lastBlock.duration = 0;
|
|
@@ -12924,7 +14572,7 @@ function expandDocBlocks(blocks, options = {}) {
|
|
|
12924
14572
|
changed = false;
|
|
12925
14573
|
for (let i = segmentExpandedBlocks.length - 1; i >= 1; i--) {
|
|
12926
14574
|
const block = segmentExpandedBlocks[i];
|
|
12927
|
-
if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP) {
|
|
14575
|
+
if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP - GAP_TOLERANCE) {
|
|
12928
14576
|
const prev = segmentExpandedBlocks[i - 1];
|
|
12929
14577
|
const blockEnd = block.startTime + block.duration;
|
|
12930
14578
|
prev.duration = blockEnd - prev.startTime;
|
|
@@ -12943,7 +14591,7 @@ function expandDocBlocks(blocks, options = {}) {
|
|
|
12943
14591
|
if (block.duration > MAX_BLOCK_DURATION) {
|
|
12944
14592
|
const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
|
|
12945
14593
|
const partDuration = block.duration / numParts;
|
|
12946
|
-
if (partDuration >= MIN_TRANSITION_GAP) {
|
|
14594
|
+
if (partDuration >= MIN_TRANSITION_GAP - GAP_TOLERANCE) {
|
|
12947
14595
|
block.duration = partDuration;
|
|
12948
14596
|
const parts = [];
|
|
12949
14597
|
for (let p = 1; p < numParts; p++) {
|
|
@@ -13112,20 +14760,78 @@ function detectPromotableBodyAnnotation(contents) {
|
|
|
13112
14760
|
return { data, strippedContents };
|
|
13113
14761
|
}
|
|
13114
14762
|
|
|
14763
|
+
// src/doc/videoTiming.ts
|
|
14764
|
+
function clipEndsWithBlock(clip) {
|
|
14765
|
+
return clip.kind === "video" && clip.anchor === "block" && clip.spillover !== true && clip.lockToBlock !== false;
|
|
14766
|
+
}
|
|
14767
|
+
function blockVideos(block) {
|
|
14768
|
+
const body = extractEmbeddedVideos(block.contents).map((video) => ({
|
|
14769
|
+
src: video.src,
|
|
14770
|
+
startAt: video.startAt,
|
|
14771
|
+
clipStart: video.clipStart,
|
|
14772
|
+
clipEnd: video.clipEnd,
|
|
14773
|
+
cuts: video.cuts
|
|
14774
|
+
}));
|
|
14775
|
+
const clips = (block.media ?? []).filter(clipEndsWithBlock).map((clip) => ({
|
|
14776
|
+
src: clip.src,
|
|
14777
|
+
startAt: clip.startAt,
|
|
14778
|
+
clipStart: clip.clipStart,
|
|
14779
|
+
clipEnd: clip.clipEnd,
|
|
14780
|
+
cuts: clip.edits?.cuts
|
|
14781
|
+
}));
|
|
14782
|
+
return [...body, ...clips];
|
|
14783
|
+
}
|
|
14784
|
+
function knownLength(seconds) {
|
|
14785
|
+
return seconds != null && Number.isFinite(seconds) && seconds > 0 ? seconds : void 0;
|
|
14786
|
+
}
|
|
14787
|
+
function videoEnd(video, lookup) {
|
|
14788
|
+
const sourceDuration = knownLength(lookup?.(video.src));
|
|
14789
|
+
if (video.clipEnd == null && sourceDuration == null) return null;
|
|
14790
|
+
const played = createMediaTimeMap({
|
|
14791
|
+
clipStart: video.clipStart,
|
|
14792
|
+
clipEnd: video.clipEnd,
|
|
14793
|
+
cuts: video.cuts,
|
|
14794
|
+
sourceDuration
|
|
14795
|
+
}).playedDuration;
|
|
14796
|
+
if (!Number.isFinite(played) || played <= 0) return null;
|
|
14797
|
+
return Math.max(0, video.startAt ?? 0) + played;
|
|
14798
|
+
}
|
|
14799
|
+
function blockVideoLength(block, lookup) {
|
|
14800
|
+
let longest = null;
|
|
14801
|
+
for (const video of blockVideos(block)) {
|
|
14802
|
+
const end = videoEnd(video, lookup);
|
|
14803
|
+
if (end != null && (longest == null || end > longest)) longest = end;
|
|
14804
|
+
}
|
|
14805
|
+
return longest;
|
|
14806
|
+
}
|
|
14807
|
+
function collectVideoDurationSources(doc) {
|
|
14808
|
+
const sources = /* @__PURE__ */ new Set();
|
|
14809
|
+
const visit = (blocks) => {
|
|
14810
|
+
for (const block of blocks) {
|
|
14811
|
+
for (const video of blockVideos(block)) {
|
|
14812
|
+
if (video.clipEnd == null && video.src) sources.add(video.src);
|
|
14813
|
+
}
|
|
14814
|
+
if (block.children?.length) visit(block.children);
|
|
14815
|
+
}
|
|
14816
|
+
};
|
|
14817
|
+
visit(doc.blocks);
|
|
14818
|
+
return [...sources];
|
|
14819
|
+
}
|
|
14820
|
+
|
|
13115
14821
|
// src/doc/markdownToDoc.ts
|
|
13116
14822
|
var SLUG_NON_WORD_RE = /[^\w\s-]/g;
|
|
13117
14823
|
var SLUG_SPACES_RE = /[\s_]+/g;
|
|
13118
14824
|
var SLUG_MULTI_HYPHEN_RE = /-+/g;
|
|
13119
14825
|
var SLUG_TRIM_HYPHEN_RE = /^-|-$/g;
|
|
13120
|
-
function slugify(
|
|
13121
|
-
return
|
|
14826
|
+
function slugify(text2) {
|
|
14827
|
+
return text2.toLowerCase().replace(SLUG_NON_WORD_RE, "").replace(SLUG_SPACES_RE, "-").replace(SLUG_MULTI_HYPHEN_RE, "-").replace(SLUG_TRIM_HYPHEN_RE, "") || // Trim leading/trailing hyphens
|
|
13122
14828
|
"block";
|
|
13123
14829
|
}
|
|
13124
14830
|
function createIdGenerator() {
|
|
13125
14831
|
const used = /* @__PURE__ */ new Map();
|
|
13126
14832
|
const generate = (heading, _index) => {
|
|
13127
|
-
const
|
|
13128
|
-
const base = slugify(
|
|
14833
|
+
const text2 = extractPlainText(heading);
|
|
14834
|
+
const base = slugify(text2);
|
|
13129
14835
|
const count = used.get(base) ?? 0;
|
|
13130
14836
|
used.set(base, count + 1);
|
|
13131
14837
|
if (count === 0) return base;
|
|
@@ -13134,8 +14840,8 @@ function createIdGenerator() {
|
|
|
13134
14840
|
generate.reserve = (id) => {
|
|
13135
14841
|
used.set(id, Math.max(used.get(id) ?? 0, 1));
|
|
13136
14842
|
};
|
|
13137
|
-
generate.fromText = (
|
|
13138
|
-
const base = slugify(
|
|
14843
|
+
generate.fromText = (text2) => {
|
|
14844
|
+
const base = slugify(text2);
|
|
13139
14845
|
const count = used.get(base) ?? 0;
|
|
13140
14846
|
used.set(base, count + 1);
|
|
13141
14847
|
return count === 0 ? base : `${base}-${count + 1}`;
|
|
@@ -13340,10 +15046,15 @@ function markdownToDoc(markdownDoc, options) {
|
|
|
13340
15046
|
applyTimelineData(block, diagnostics);
|
|
13341
15047
|
applyTreeData(block, diagnostics);
|
|
13342
15048
|
}
|
|
13343
|
-
|
|
13344
|
-
|
|
15049
|
+
const autoLevel = frontmatterAutoTemplates(markdownDoc, options?.autoTemplates ?? true);
|
|
15050
|
+
if (autoLevel) {
|
|
15051
|
+
applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), {
|
|
15052
|
+
featureIndex: 0,
|
|
15053
|
+
concepts: autoLevel !== "structural"
|
|
15054
|
+
});
|
|
13345
15055
|
}
|
|
13346
15056
|
for (const block of allBlocks) collectTimelineWarnings(block, diagnostics);
|
|
15057
|
+
for (const block of allBlocks) collectShapeWarning(block, diagnostics);
|
|
13347
15058
|
const seenIds = /* @__PURE__ */ new Map();
|
|
13348
15059
|
for (const block of allBlocks) {
|
|
13349
15060
|
if (seenIds.has(block.id)) {
|
|
@@ -13372,46 +15083,13 @@ function markdownToDoc(markdownDoc, options) {
|
|
|
13372
15083
|
}
|
|
13373
15084
|
}
|
|
13374
15085
|
const minDuration = 3;
|
|
13375
|
-
const phrases = [];
|
|
13376
15086
|
for (const block of allBlocks) {
|
|
13377
15087
|
if (pinnedMeta(block).duration != null) continue;
|
|
13378
15088
|
const bodyText3 = getBlockBodyText2(block);
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
block.duration = Math.max(minDuration, estimate.seconds);
|
|
13382
|
-
} else {
|
|
13383
|
-
block.duration = defaultDuration;
|
|
13384
|
-
}
|
|
13385
|
-
}
|
|
13386
|
-
let currentTime = 0;
|
|
13387
|
-
for (const block of allBlocks) {
|
|
13388
|
-
const explicitStart = pinnedMeta(block).startTime;
|
|
13389
|
-
if (explicitStart == null) {
|
|
13390
|
-
block.startTime = currentTime;
|
|
13391
|
-
}
|
|
13392
|
-
const bodyText3 = getBlockBodyText2(block);
|
|
13393
|
-
if (bodyText3.length > 0) {
|
|
13394
|
-
const sentences = splitIntoSentences(bodyText3);
|
|
13395
|
-
if (sentences.length > 0) {
|
|
13396
|
-
const timePerSentence = block.duration / sentences.length;
|
|
13397
|
-
for (let i = 0; i < sentences.length; i++) {
|
|
13398
|
-
phrases.push({
|
|
13399
|
-
text: sentences[i],
|
|
13400
|
-
startTime: currentTime + i * timePerSentence,
|
|
13401
|
-
endTime: currentTime + (i + 1) * timePerSentence,
|
|
13402
|
-
audioSegment: 0
|
|
13403
|
-
});
|
|
13404
|
-
}
|
|
13405
|
-
}
|
|
13406
|
-
}
|
|
13407
|
-
currentTime += block.duration;
|
|
13408
|
-
}
|
|
13409
|
-
const blockById = new Map(allBlocks.map((block) => [block.id, block]));
|
|
13410
|
-
for (const clip of documentMedia) {
|
|
13411
|
-
if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
|
|
13412
|
-
clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
|
|
13413
|
-
}
|
|
15089
|
+
const estimate = bodyText3.length > 0 ? Math.max(minDuration, estimateReadingTime(bodyText3).seconds) : defaultDuration;
|
|
15090
|
+
block.duration = Math.max(estimate, blockVideoLength(block) ?? 0);
|
|
13414
15091
|
}
|
|
15092
|
+
const { phrases, duration: totalDuration } = sequenceBlockTimeline(allBlocks, documentMedia);
|
|
13415
15093
|
const captions = phrases.length > 0 ? {
|
|
13416
15094
|
phrases,
|
|
13417
15095
|
...options?.captionsGeneratedAt ? { generatedAt: options.captionsGeneratedAt } : {},
|
|
@@ -13423,7 +15101,7 @@ function markdownToDoc(markdownDoc, options) {
|
|
|
13423
15101
|
const motion = readFrontmatterMotion(markdownDoc.frontmatter);
|
|
13424
15102
|
const doc = {
|
|
13425
15103
|
articleId,
|
|
13426
|
-
duration:
|
|
15104
|
+
duration: totalDuration,
|
|
13427
15105
|
blocks: rootBlocks,
|
|
13428
15106
|
audio: {
|
|
13429
15107
|
segments: []
|
|
@@ -13445,6 +15123,64 @@ function markdownToDoc(markdownDoc, options) {
|
|
|
13445
15123
|
}
|
|
13446
15124
|
return doc;
|
|
13447
15125
|
}
|
|
15126
|
+
function sequenceBlockTimeline(blocks, documentMedia) {
|
|
15127
|
+
const phrases = [];
|
|
15128
|
+
let currentTime = 0;
|
|
15129
|
+
for (const block of blocks) {
|
|
15130
|
+
const explicitStart = pinnedMeta(block).startTime;
|
|
15131
|
+
if (explicitStart == null) {
|
|
15132
|
+
block.startTime = currentTime;
|
|
15133
|
+
}
|
|
15134
|
+
const bodyText3 = getBlockBodyText2(block);
|
|
15135
|
+
if (bodyText3.length > 0) {
|
|
15136
|
+
const sentences = splitIntoSentences(bodyText3);
|
|
15137
|
+
if (sentences.length > 0) {
|
|
15138
|
+
const timePerSentence = block.duration / sentences.length;
|
|
15139
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
15140
|
+
phrases.push({
|
|
15141
|
+
text: sentences[i],
|
|
15142
|
+
startTime: currentTime + i * timePerSentence,
|
|
15143
|
+
endTime: currentTime + (i + 1) * timePerSentence,
|
|
15144
|
+
audioSegment: 0
|
|
15145
|
+
});
|
|
15146
|
+
}
|
|
15147
|
+
}
|
|
15148
|
+
}
|
|
15149
|
+
currentTime += block.duration;
|
|
15150
|
+
}
|
|
15151
|
+
const blockById = new Map(blocks.map((block) => [block.id, block]));
|
|
15152
|
+
for (const clip of documentMedia) {
|
|
15153
|
+
if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
|
|
15154
|
+
clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
|
|
15155
|
+
}
|
|
15156
|
+
}
|
|
15157
|
+
return { phrases, duration: currentTime };
|
|
15158
|
+
}
|
|
15159
|
+
var VIDEO_FLOOR_EPSILON = 1e-3;
|
|
15160
|
+
function applyVideoDurations(doc, lookup) {
|
|
15161
|
+
const grownBy = /* @__PURE__ */ new Map();
|
|
15162
|
+
for (const block of flattenBlocks(doc.blocks)) {
|
|
15163
|
+
if (pinnedMeta(block).duration != null) continue;
|
|
15164
|
+
const floor = blockVideoLength(block, lookup);
|
|
15165
|
+
if (floor != null && floor > block.duration + VIDEO_FLOOR_EPSILON) grownBy.set(block, floor);
|
|
15166
|
+
}
|
|
15167
|
+
if (grownBy.size === 0) return doc;
|
|
15168
|
+
const copy = (blocks2) => blocks2.map((block) => ({
|
|
15169
|
+
...block,
|
|
15170
|
+
...grownBy.has(block) ? { duration: grownBy.get(block) } : {},
|
|
15171
|
+
...block.children ? { children: copy(block.children) } : {}
|
|
15172
|
+
}));
|
|
15173
|
+
const blocks = copy(doc.blocks);
|
|
15174
|
+
const documentMedia = doc.documentMedia?.map((clip) => ({ ...clip }));
|
|
15175
|
+
const { phrases, duration } = sequenceBlockTimeline(flattenBlocks(blocks), documentMedia ?? []);
|
|
15176
|
+
return {
|
|
15177
|
+
...doc,
|
|
15178
|
+
blocks,
|
|
15179
|
+
duration,
|
|
15180
|
+
...doc.captions ? { captions: { ...doc.captions, phrases } } : {},
|
|
15181
|
+
...documentMedia ? { documentMedia } : {}
|
|
15182
|
+
};
|
|
15183
|
+
}
|
|
13448
15184
|
function flattenBlocks(blocks) {
|
|
13449
15185
|
const result = [];
|
|
13450
15186
|
for (const block of blocks) {
|
|
@@ -13561,6 +15297,23 @@ function applyTimelineData(block, diagnostics) {
|
|
|
13561
15297
|
}
|
|
13562
15298
|
block.templateData = { ...derived, ...block.templateData };
|
|
13563
15299
|
}
|
|
15300
|
+
function collectShapeWarning(block, diagnostics) {
|
|
15301
|
+
if (!block.sourceHeading?.templateAnnotation?.template && !block.promotedBodyAnnotation) return;
|
|
15302
|
+
const template = conceptTemplate(resolveTemplateName(block.template ?? ""));
|
|
15303
|
+
if (!template) return;
|
|
15304
|
+
const given = block.templateData?.items;
|
|
15305
|
+
const items = Array.isArray(given) ? given : extractConceptItems(block.contents);
|
|
15306
|
+
if (!items.length) return;
|
|
15307
|
+
const downgrade = conceptDowngrade(template, items);
|
|
15308
|
+
if (!downgrade) return;
|
|
15309
|
+
diagnostics.push({
|
|
15310
|
+
severity: "warning",
|
|
15311
|
+
code: "template-shape-unmet",
|
|
15312
|
+
message: `Block "${block.id}" asks for ${template} but is drawn as ${downgrade.drawsAs === "list" ? "a list" : downgrade.drawsAs}: ${downgrade.reason}`,
|
|
15313
|
+
blockId: block.id,
|
|
15314
|
+
...lineOf(block)
|
|
15315
|
+
});
|
|
15316
|
+
}
|
|
13564
15317
|
function collectTimelineWarnings(block, diagnostics) {
|
|
13565
15318
|
if (resolveTemplateName(block.template ?? "") !== "timeline") return;
|
|
13566
15319
|
if (!block.templateData || !("tracks" in block.templateData)) return;
|
|
@@ -13578,20 +15331,26 @@ function collectTimelineWarnings(block, diagnostics) {
|
|
|
13578
15331
|
});
|
|
13579
15332
|
}
|
|
13580
15333
|
}
|
|
13581
|
-
function
|
|
15334
|
+
function frontmatterAutoTemplates(markdownDoc, option) {
|
|
15335
|
+
if (!option) return false;
|
|
13582
15336
|
const v = markdownDoc.frontmatter?.["squisq-auto-templates"];
|
|
13583
|
-
|
|
15337
|
+
if (v === false || v === "false" || v === "off" || v === "no" || v === 0) return false;
|
|
15338
|
+
if (v === "structural" || v === "basic") return "structural";
|
|
15339
|
+
return option;
|
|
13584
15340
|
}
|
|
13585
15341
|
function applyAutoTemplates(blocks, resolvedDefault, state) {
|
|
13586
15342
|
for (const block of blocks) {
|
|
13587
15343
|
const annotated = !!block.sourceHeading?.templateAnnotation?.template || !!block.promotedBodyAnnotation;
|
|
13588
15344
|
if (block.sourceHeading && !annotated && block.template === resolvedDefault) {
|
|
13589
|
-
const profile = profileBlockContents(block.contents ?? []);
|
|
13590
|
-
|
|
15345
|
+
const profile = profileBlockContents(block.contents ?? [], { heading: block.title });
|
|
15346
|
+
let picked = pickAutoTemplate(profile, state.featureIndex, { concepts: state.concepts });
|
|
15347
|
+
const drawn = !!picked && picked !== "list" && profile.listShape?.template === picked;
|
|
15348
|
+
if (drawn && !autoTemplatePreservesContent(picked, block.contents)) picked = "list";
|
|
13591
15349
|
if (picked && autoTemplatePreservesContent(picked, block.contents)) {
|
|
13592
|
-
const
|
|
15350
|
+
const derived = deriveTemplateInputs(picked, block.title ?? "", block.contents, {
|
|
13593
15351
|
preserveSourceHeading: true
|
|
13594
15352
|
});
|
|
15353
|
+
const inputs = derived && drawn && picked !== "list" ? { ...derived, ...profile.listShape?.inputs } : derived;
|
|
13595
15354
|
if (inputs) {
|
|
13596
15355
|
if (picked === "leftFeature" || picked === "rightFeature") state.featureIndex += 1;
|
|
13597
15356
|
block.template = picked;
|
|
@@ -13612,8 +15371,8 @@ function getBlockBodyText2(block) {
|
|
|
13612
15371
|
return block.contents.filter((node) => !(consumedFence && node.type === "code")).filter((node) => !isMaterializedMediaNode(node)).map((node) => extractPlainText(node)).join("\n").trim();
|
|
13613
15372
|
}
|
|
13614
15373
|
var MAX_PHRASE_WORDS = 12;
|
|
13615
|
-
function splitIntoSentences(
|
|
13616
|
-
const lines =
|
|
15374
|
+
function splitIntoSentences(text2) {
|
|
15375
|
+
const lines = text2.split(/\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
13617
15376
|
const sentenceRe = /(?<=[.!?])\s+/;
|
|
13618
15377
|
const fragments = [];
|
|
13619
15378
|
for (const line of lines) {
|
|
@@ -13785,12 +15544,12 @@ function buildNarrationScript(doc, options) {
|
|
|
13785
15544
|
const separator = options?.blockSeparator ?? "\n\n";
|
|
13786
15545
|
const pieces = [];
|
|
13787
15546
|
for (const block of flattenRenderableBlocks(doc.blocks)) {
|
|
13788
|
-
const
|
|
13789
|
-
if (!
|
|
15547
|
+
const text2 = blockSpokenText(block, includeTitles);
|
|
15548
|
+
if (!text2) continue;
|
|
13790
15549
|
pieces.push({
|
|
13791
15550
|
blockId: block.id,
|
|
13792
15551
|
heading: block.title || void 0,
|
|
13793
|
-
text
|
|
15552
|
+
text: text2
|
|
13794
15553
|
});
|
|
13795
15554
|
}
|
|
13796
15555
|
let sourceText = "";
|
|
@@ -13831,20 +15590,20 @@ function buildNarrationScript(doc, options) {
|
|
|
13831
15590
|
spoken
|
|
13832
15591
|
});
|
|
13833
15592
|
}
|
|
13834
|
-
for (const
|
|
13835
|
-
|
|
13836
|
-
|
|
15593
|
+
for (const range2 of blocks) {
|
|
15594
|
+
range2.tokenStart = tokens.length;
|
|
15595
|
+
range2.tokenEnd = 0;
|
|
13837
15596
|
}
|
|
13838
15597
|
for (let i = 0; i < tokens.length; i++) {
|
|
13839
|
-
const
|
|
13840
|
-
if (
|
|
13841
|
-
if (i <
|
|
13842
|
-
|
|
15598
|
+
const range2 = blocks[tokens[i].blockIndex];
|
|
15599
|
+
if (range2) {
|
|
15600
|
+
if (i < range2.tokenStart) range2.tokenStart = i;
|
|
15601
|
+
range2.tokenEnd = i + 1;
|
|
13843
15602
|
}
|
|
13844
15603
|
}
|
|
13845
|
-
for (const
|
|
13846
|
-
if (
|
|
13847
|
-
|
|
15604
|
+
for (const range2 of blocks) {
|
|
15605
|
+
if (range2.tokenStart > range2.tokenEnd) {
|
|
15606
|
+
range2.tokenStart = range2.tokenEnd;
|
|
13848
15607
|
}
|
|
13849
15608
|
}
|
|
13850
15609
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -13943,9 +15702,9 @@ function planContentBuilds(nodes, spec) {
|
|
|
13943
15702
|
const steps = [];
|
|
13944
15703
|
const targets = /* @__PURE__ */ new Map();
|
|
13945
15704
|
if (!spec || spec.mode === "off") return { steps, targets };
|
|
13946
|
-
const add = (node,
|
|
15705
|
+
const add = (node, text2, ancestors) => {
|
|
13947
15706
|
const index = steps.length;
|
|
13948
|
-
steps.push({ id: `build-${index}`, text, ...ancestors.length ? { ancestors } : {} });
|
|
15707
|
+
steps.push({ id: `build-${index}`, text: text2, ...ancestors.length ? { ancestors } : {} });
|
|
13949
15708
|
targets.set(node, index);
|
|
13950
15709
|
return index;
|
|
13951
15710
|
};
|
|
@@ -14067,26 +15826,26 @@ function resolveDocBuildTiming(doc, blocks) {
|
|
|
14067
15826
|
});
|
|
14068
15827
|
return { ...block, buildTiming: { sourceText: buildSourceText(steps), starts: starts2 } };
|
|
14069
15828
|
}
|
|
14070
|
-
const
|
|
14071
|
-
(
|
|
15829
|
+
const range2 = script?.blocks.find(
|
|
15830
|
+
(range3) => range3.blockId === (block.sourceBlockId ?? block.id)
|
|
14072
15831
|
);
|
|
14073
|
-
const original =
|
|
15832
|
+
const original = range2 ? flattenRenderableBlocks(doc.blocks).find((source2) => source2.id === range2.blockId) : void 0;
|
|
14074
15833
|
const body = original ? getBlockBodyText2(original) : "";
|
|
14075
|
-
const charStart =
|
|
14076
|
-
const source = block.buildSource ?? (
|
|
15834
|
+
const charStart = range2 ? body ? range2.charEnd - body.length : range2.charStart : 0;
|
|
15835
|
+
const source = block.buildSource ?? (range2 ? { text: narration.sourceText.slice(charStart, range2.charEnd), charStart } : void 0);
|
|
14077
15836
|
if (!source) return block;
|
|
14078
15837
|
let cursor = 0;
|
|
14079
15838
|
const starts = [];
|
|
14080
15839
|
for (const step2 of steps) {
|
|
14081
|
-
const
|
|
14082
|
-
const offset =
|
|
15840
|
+
const text2 = step2.text.trim();
|
|
15841
|
+
const offset = text2 ? source.text.indexOf(text2, cursor) : -1;
|
|
14083
15842
|
if (offset < 0) return block;
|
|
14084
15843
|
const word = narration.bookmarks.find(
|
|
14085
|
-
(word2) => word2.charOffset >= source.charStart + offset && word2.charOffset < source.charStart + offset +
|
|
15844
|
+
(word2) => word2.charOffset >= source.charStart + offset && word2.charOffset < source.charStart + offset + text2.length
|
|
14086
15845
|
);
|
|
14087
15846
|
if (!word) return block;
|
|
14088
15847
|
starts.push(Math.max(0, word.time - block.startTime));
|
|
14089
|
-
cursor = offset +
|
|
15848
|
+
cursor = offset + text2.length;
|
|
14090
15849
|
}
|
|
14091
15850
|
return { ...block, buildTiming: { sourceText: buildSourceText(steps), starts } };
|
|
14092
15851
|
});
|
|
@@ -14109,10 +15868,16 @@ function listParagraphs(list, level = 0) {
|
|
|
14109
15868
|
const ordered = Boolean(list.ordered);
|
|
14110
15869
|
return list.children.flatMap((item, index) => {
|
|
14111
15870
|
const ownText = item.children.filter((node) => node.type !== "list").map((node) => extractPlainText(node).trim()).filter(Boolean).join(" ");
|
|
15871
|
+
const checked = typeof item.checked === "boolean" ? { checked: item.checked } : {};
|
|
14112
15872
|
const paragraphs = ownText ? [
|
|
14113
15873
|
{
|
|
14114
15874
|
text: ownText,
|
|
14115
|
-
list: {
|
|
15875
|
+
list: {
|
|
15876
|
+
ordered,
|
|
15877
|
+
level,
|
|
15878
|
+
...ordered ? { number: (list.start ?? 1) + index } : {},
|
|
15879
|
+
...checked
|
|
15880
|
+
}
|
|
14116
15881
|
}
|
|
14117
15882
|
] : [];
|
|
14118
15883
|
for (const child of item.children) {
|
|
@@ -14121,10 +15886,10 @@ function listParagraphs(list, level = 0) {
|
|
|
14121
15886
|
return paragraphs;
|
|
14122
15887
|
});
|
|
14123
15888
|
}
|
|
14124
|
-
function plainParagraphText({ text, list }) {
|
|
14125
|
-
if (!list) return
|
|
14126
|
-
const marker = list.ordered ? `${list.number ?? 1}.` : "\u2022";
|
|
14127
|
-
return `${" ".repeat(list.level)}${marker} ${
|
|
15889
|
+
function plainParagraphText({ text: text2, list }) {
|
|
15890
|
+
if (!list) return text2;
|
|
15891
|
+
const marker = list.checked !== void 0 ? taskMarker(list.checked) : list.ordered ? `${list.number ?? 1}.` : "\u2022";
|
|
15892
|
+
return `${" ".repeat(list.level)}${marker} ${text2}`;
|
|
14128
15893
|
}
|
|
14129
15894
|
function completeBodyParagraphs(nodes) {
|
|
14130
15895
|
const paragraphs = nodes.flatMap((node, index) => {
|
|
@@ -14245,7 +16010,15 @@ export {
|
|
|
14245
16010
|
countUpSpecFor,
|
|
14246
16011
|
formatCountValue,
|
|
14247
16012
|
formatCountUp,
|
|
14248
|
-
|
|
16013
|
+
listBlock,
|
|
16014
|
+
CONCEPT_TEMPLATES,
|
|
16015
|
+
CONCEPT_MAX_DRAWN,
|
|
16016
|
+
CONCEPT_SPECS,
|
|
16017
|
+
conceptTemplate,
|
|
16018
|
+
conceptItemCount,
|
|
16019
|
+
conceptItemsProblem,
|
|
16020
|
+
conceptDowngrade,
|
|
16021
|
+
conceptDrawsAs,
|
|
14249
16022
|
conceptBuildStepTexts,
|
|
14250
16023
|
cardsBlock,
|
|
14251
16024
|
processBlock,
|
|
@@ -14264,7 +16037,12 @@ export {
|
|
|
14264
16037
|
mediaOnly,
|
|
14265
16038
|
mapBlock,
|
|
14266
16039
|
fullBleedQuote,
|
|
14267
|
-
|
|
16040
|
+
statsBlock,
|
|
16041
|
+
pyramidBlock,
|
|
16042
|
+
funnelBlock,
|
|
16043
|
+
hubBlock,
|
|
16044
|
+
vennBlock,
|
|
16045
|
+
matrixBlock,
|
|
14268
16046
|
photoGrid,
|
|
14269
16047
|
definitionCard,
|
|
14270
16048
|
comparisonBar,
|
|
@@ -14314,20 +16092,39 @@ export {
|
|
|
14314
16092
|
materializeBlockLayersWithRuntime,
|
|
14315
16093
|
TEMPLATE_METADATA,
|
|
14316
16094
|
TEMPLATE_AUTHORING_METADATA,
|
|
16095
|
+
PRESENTATION_KEY,
|
|
16096
|
+
PRESENTATION_LAYOUTS,
|
|
16097
|
+
AUTHORED_PRESENTATION_LAYOUT,
|
|
16098
|
+
presentationStat,
|
|
16099
|
+
presentationLayoutFits,
|
|
16100
|
+
presentationLayoutProblem,
|
|
16101
|
+
presentationLayoutNeeds,
|
|
16102
|
+
presentationBeatExtras,
|
|
16103
|
+
PRESENTATION_LIMITS,
|
|
16104
|
+
parsePresentationPlan,
|
|
16105
|
+
serializePresentationPlan,
|
|
14317
16106
|
TEMPLATE_GUIDES,
|
|
14318
16107
|
guideText,
|
|
14319
16108
|
templateCatalog,
|
|
14320
16109
|
PRESENTATION_LAYOUT_GUIDE,
|
|
16110
|
+
PRESENTATION_LAYOUT_LABELS,
|
|
14321
16111
|
AUTHORING_TRICKS,
|
|
14322
16112
|
authoringTricks,
|
|
14323
16113
|
coverBlock,
|
|
14324
16114
|
expandCoverBlock,
|
|
16115
|
+
MARKDOWN_SHAPE_EXAMPLES,
|
|
16116
|
+
markdownShapeExample,
|
|
16117
|
+
markdownShapeGuide,
|
|
16118
|
+
markdownShapeSummary,
|
|
14325
16119
|
expandDocBlocks,
|
|
14326
16120
|
getAvailableTemplates,
|
|
14327
16121
|
hasTemplate,
|
|
14328
16122
|
detectPromotableBodyAnnotation,
|
|
16123
|
+
blockVideoLength,
|
|
16124
|
+
collectVideoDurationSources,
|
|
14329
16125
|
getPinnedBlockMeta,
|
|
14330
16126
|
markdownToDoc,
|
|
16127
|
+
applyVideoDurations,
|
|
14331
16128
|
flattenBlocks,
|
|
14332
16129
|
flattenRenderableBlocks,
|
|
14333
16130
|
countBlocks,
|