@bendyline/squisq 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Doc-BeSeH2iX.d.ts → Doc-BsMkmzH8.d.ts} +838 -268
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-DvoeHMP9.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-4AKUQPEE.js} +536 -103
- package/dist/chunk-4AKUQPEE.js.map +1 -0
- package/dist/chunk-4ZFRZBU2.js +6582 -0
- package/dist/chunk-4ZFRZBU2.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-7MQGNR76.js} +79 -3
- package/dist/chunk-7MQGNR76.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/{chunk-MYIH7FWD.js → chunk-FBKP5CAO.js} +1 -1
- package/dist/chunk-FBKP5CAO.js.map +1 -0
- package/dist/chunk-KZNXZ5SM.js +333 -0
- package/dist/chunk-KZNXZ5SM.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-LRCV7N7F.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-LRCV7N7F.js.map} +1 -1
- package/dist/{chunk-3K5OG6KI.js → chunk-OH43RPTP.js} +403 -187
- package/dist/chunk-OH43RPTP.js.map +1 -0
- package/dist/{chunk-H3AXU4MJ.js → chunk-OKJOQXK4.js} +149 -2
- package/dist/chunk-OKJOQXK4.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/chunk-TVSGLUID.js +596 -0
- package/dist/chunk-TVSGLUID.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +4 -4
- package/dist/doc/index.js +124 -6
- package/dist/generate/index.d.ts +9 -2
- package/dist/generate/index.js +1 -1
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +11 -5
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +11 -8
- package/dist/index.js +243 -24
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/markdown/index.d.ts +212 -3
- package/dist/markdown/index.js +40 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +35 -5
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/story/index.d.ts +906 -40
- package/dist/story/index.js +124 -6
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-DQQKuRMx.d.ts} +1 -1
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/customTemplateSample.test.ts +83 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +205 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +118 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +23 -0
- package/src/__tests__/markdownSanitize.test.ts +64 -0
- package/src/__tests__/markdownToDoc.test.ts +223 -2
- package/src/__tests__/mediaAnnotations.test.ts +64 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +179 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +21 -0
- package/src/__tests__/templates.test.ts +145 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +100 -0
- package/src/doc/customTemplatesFrontmatter.ts +308 -0
- package/src/doc/customThemesFrontmatter.ts +90 -0
- package/src/doc/docToMarkdown.ts +73 -4
- package/src/doc/getLayers.ts +40 -17
- package/src/doc/index.ts +35 -1
- package/src/doc/markdownToDoc.ts +333 -12
- package/src/doc/mediaAnnotations.ts +118 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/structuredData.ts +230 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +151 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +26 -18
- package/src/doc/templates/coverBlock.ts +7 -4
- package/src/doc/templates/customTemplate.ts +45 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +51 -25
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +22 -9
- package/src/doc/templates/index.ts +215 -46
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +23 -12
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +221 -0
- package/src/doc/templates/tokens/resolveTokens.ts +0 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +13 -8
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +476 -0
- package/src/generate/slideshowGenerator.ts +7 -0
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +222 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +36 -0
- package/src/markdown/sanitize.ts +298 -0
- package/src/markdown/stringify.ts +84 -3
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +49 -1
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +74 -0
- package/src/schemas/Doc.ts +353 -29
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +23 -4
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toMdast
|
|
3
|
+
} from "./chunk-OH43RPTP.js";
|
|
4
|
+
|
|
5
|
+
// src/markdown/stringify.ts
|
|
6
|
+
import { unified } from "unified";
|
|
7
|
+
import remarkStringify from "remark-stringify";
|
|
8
|
+
import remarkGfm from "remark-gfm";
|
|
9
|
+
import remarkMath from "remark-math";
|
|
10
|
+
import remarkDirective from "remark-directive";
|
|
11
|
+
var defaultProcessor;
|
|
12
|
+
var DQ_RUN = `"(?:[^"\\\\]|\\\\.)*"`;
|
|
13
|
+
var SQ_RUN = `'(?:[^'\\\\]|\\\\.)*'`;
|
|
14
|
+
var ESCAPED_TEMPLATE_SPAN_RE = new RegExp(
|
|
15
|
+
`\\{\\\\\\[((?:${DQ_RUN}|${SQ_RUN}|\\\\.|[^\\]\\\\])+)\\]\\}`,
|
|
16
|
+
"g"
|
|
17
|
+
);
|
|
18
|
+
var HEADING_LINE_RE = /^#{1,6} .*$/gm;
|
|
19
|
+
var TRAILING_ESCAPED_PANDOC_SPAN_RE = new RegExp(
|
|
20
|
+
`\\{(?!\\\\?\\[)((?:${DQ_RUN}|${SQ_RUN}|\\\\.|[^}\\\\])*)\\}(?=\\s*(?:\\{\\[.*)?$)`
|
|
21
|
+
);
|
|
22
|
+
var UNESCAPE_PUNCT_RE = /\\([\\[\]:#.,+=/?;@%$(){}'"-])/g;
|
|
23
|
+
function unescapeMarkdownPunct(text) {
|
|
24
|
+
return text.replace(UNESCAPE_PUNCT_RE, "$1");
|
|
25
|
+
}
|
|
26
|
+
function stringifyMarkdown(doc, options) {
|
|
27
|
+
const mdastTree = toMdast(doc);
|
|
28
|
+
const useDefaults = !options || options.gfm !== false && options.math !== false && options.directive !== false && !options.bullet && !options.bulletOrdered && !options.emphasis && !options.strong && !options.rule && !options.fence && options.setext == null;
|
|
29
|
+
let processor;
|
|
30
|
+
if (useDefaults) {
|
|
31
|
+
if (!defaultProcessor) {
|
|
32
|
+
defaultProcessor = unified().use(remarkGfm).use(remarkMath).use(remarkDirective).use(remarkStringify, {
|
|
33
|
+
bullet: "-",
|
|
34
|
+
bulletOrdered: ".",
|
|
35
|
+
emphasis: "*",
|
|
36
|
+
strong: "*",
|
|
37
|
+
rule: "-",
|
|
38
|
+
fence: "`",
|
|
39
|
+
setext: false
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
processor = defaultProcessor;
|
|
43
|
+
} else {
|
|
44
|
+
processor = unified();
|
|
45
|
+
if (options?.gfm !== false) {
|
|
46
|
+
processor = processor.use(remarkGfm);
|
|
47
|
+
}
|
|
48
|
+
if (options?.math !== false) {
|
|
49
|
+
processor = processor.use(remarkMath);
|
|
50
|
+
}
|
|
51
|
+
if (options?.directive !== false) {
|
|
52
|
+
processor = processor.use(remarkDirective);
|
|
53
|
+
}
|
|
54
|
+
processor = processor.use(remarkStringify, {
|
|
55
|
+
bullet: options?.bullet ?? "-",
|
|
56
|
+
bulletOrdered: options?.bulletOrdered ?? ".",
|
|
57
|
+
emphasis: options?.emphasis ?? "*",
|
|
58
|
+
strong: options?.strong ?? "*",
|
|
59
|
+
rule: options?.rule ?? "-",
|
|
60
|
+
fence: options?.fence ?? "`",
|
|
61
|
+
setext: options?.setext ?? false
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const result = processor.stringify(mdastTree);
|
|
65
|
+
let cleaned = result.replace(
|
|
66
|
+
ESCAPED_TEMPLATE_SPAN_RE,
|
|
67
|
+
(_m, inner) => `{[${unescapeMarkdownPunct(inner)}]}`
|
|
68
|
+
);
|
|
69
|
+
cleaned = cleaned.replace(
|
|
70
|
+
HEADING_LINE_RE,
|
|
71
|
+
(line) => line.replace(
|
|
72
|
+
TRAILING_ESCAPED_PANDOC_SPAN_RE,
|
|
73
|
+
(_m, inner) => `{${unescapeMarkdownPunct(inner)}}`
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
cleaned = cleaned.replace(/\{(?!\\?\[)[^}]*\}/g, (match) => match.replace(/\\:/g, ":"));
|
|
77
|
+
if (doc.frontmatter && Object.keys(doc.frontmatter).length > 0) {
|
|
78
|
+
const yamlLines = Object.entries(doc.frontmatter).map(
|
|
79
|
+
([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`
|
|
80
|
+
);
|
|
81
|
+
return `---
|
|
82
|
+
${yamlLines.join("\n")}
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
${cleaned}`;
|
|
86
|
+
}
|
|
87
|
+
return cleaned;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/markdown/sanitize.ts
|
|
91
|
+
var SAFE_LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
92
|
+
var SAFE_MEDIA_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "blob"]);
|
|
93
|
+
var SAFE_DATA_MEDIA_RE = /^data:(?:image\/(?!svg\+xml)[a-z0-9.+-]+|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+);/i;
|
|
94
|
+
var SAFE_TAGS = /* @__PURE__ */ new Set([
|
|
95
|
+
"a",
|
|
96
|
+
"abbr",
|
|
97
|
+
"b",
|
|
98
|
+
"blockquote",
|
|
99
|
+
"br",
|
|
100
|
+
"caption",
|
|
101
|
+
"cite",
|
|
102
|
+
"code",
|
|
103
|
+
"col",
|
|
104
|
+
"colgroup",
|
|
105
|
+
"data",
|
|
106
|
+
"dd",
|
|
107
|
+
"del",
|
|
108
|
+
"details",
|
|
109
|
+
"dfn",
|
|
110
|
+
"div",
|
|
111
|
+
"dl",
|
|
112
|
+
"dt",
|
|
113
|
+
"em",
|
|
114
|
+
"figcaption",
|
|
115
|
+
"figure",
|
|
116
|
+
"h1",
|
|
117
|
+
"h2",
|
|
118
|
+
"h3",
|
|
119
|
+
"h4",
|
|
120
|
+
"h5",
|
|
121
|
+
"h6",
|
|
122
|
+
"hr",
|
|
123
|
+
"i",
|
|
124
|
+
"img",
|
|
125
|
+
"kbd",
|
|
126
|
+
"li",
|
|
127
|
+
"mark",
|
|
128
|
+
"ol",
|
|
129
|
+
"p",
|
|
130
|
+
"pre",
|
|
131
|
+
"q",
|
|
132
|
+
"s",
|
|
133
|
+
"samp",
|
|
134
|
+
"small",
|
|
135
|
+
"source",
|
|
136
|
+
"span",
|
|
137
|
+
"strong",
|
|
138
|
+
"sub",
|
|
139
|
+
"summary",
|
|
140
|
+
"sup",
|
|
141
|
+
"table",
|
|
142
|
+
"tbody",
|
|
143
|
+
"td",
|
|
144
|
+
"tfoot",
|
|
145
|
+
"th",
|
|
146
|
+
"thead",
|
|
147
|
+
"time",
|
|
148
|
+
"tr",
|
|
149
|
+
"track",
|
|
150
|
+
"u",
|
|
151
|
+
"ul",
|
|
152
|
+
"var",
|
|
153
|
+
"video",
|
|
154
|
+
"audio"
|
|
155
|
+
]);
|
|
156
|
+
var DROP_WITH_CONTENT_TAGS = /* @__PURE__ */ new Set([
|
|
157
|
+
"base",
|
|
158
|
+
"button",
|
|
159
|
+
"embed",
|
|
160
|
+
"form",
|
|
161
|
+
"iframe",
|
|
162
|
+
"input",
|
|
163
|
+
"link",
|
|
164
|
+
"meta",
|
|
165
|
+
"object",
|
|
166
|
+
"option",
|
|
167
|
+
"script",
|
|
168
|
+
"select",
|
|
169
|
+
"style",
|
|
170
|
+
"svg",
|
|
171
|
+
"template",
|
|
172
|
+
"textarea"
|
|
173
|
+
]);
|
|
174
|
+
var GLOBAL_ATTRS = /* @__PURE__ */ new Set(["class", "id", "title", "role"]);
|
|
175
|
+
var TAG_ATTRS = {
|
|
176
|
+
a: /* @__PURE__ */ new Set(["href", "target", "rel"]),
|
|
177
|
+
audio: /* @__PURE__ */ new Set(["src", "controls", "preload", "muted", "loop"]),
|
|
178
|
+
col: /* @__PURE__ */ new Set(["span", "width"]),
|
|
179
|
+
colgroup: /* @__PURE__ */ new Set(["span"]),
|
|
180
|
+
data: /* @__PURE__ */ new Set(["value"]),
|
|
181
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height", "loading", "decoding"]),
|
|
182
|
+
li: /* @__PURE__ */ new Set(["value"]),
|
|
183
|
+
ol: /* @__PURE__ */ new Set(["start", "type"]),
|
|
184
|
+
q: /* @__PURE__ */ new Set(["cite"]),
|
|
185
|
+
source: /* @__PURE__ */ new Set(["src", "type", "media"]),
|
|
186
|
+
td: /* @__PURE__ */ new Set(["colspan", "rowspan", "align"]),
|
|
187
|
+
th: /* @__PURE__ */ new Set(["colspan", "rowspan", "align", "scope"]),
|
|
188
|
+
time: /* @__PURE__ */ new Set(["datetime"]),
|
|
189
|
+
track: /* @__PURE__ */ new Set(["src", "kind", "label", "srclang", "default"]),
|
|
190
|
+
video: /* @__PURE__ */ new Set([
|
|
191
|
+
"src",
|
|
192
|
+
"poster",
|
|
193
|
+
"width",
|
|
194
|
+
"height",
|
|
195
|
+
"controls",
|
|
196
|
+
"preload",
|
|
197
|
+
"muted",
|
|
198
|
+
"loop",
|
|
199
|
+
"playsinline"
|
|
200
|
+
])
|
|
201
|
+
};
|
|
202
|
+
var SAFE_ATTR_NAME_RE = /^[a-z][a-z0-9:_.-]*$/;
|
|
203
|
+
var SAFE_OL_TYPES = /* @__PURE__ */ new Set(["1", "a", "A", "i", "I"]);
|
|
204
|
+
var SAFE_PRELOAD_VALUES = /* @__PURE__ */ new Set(["none", "metadata", "auto"]);
|
|
205
|
+
var SAFE_TRACK_KINDS = /* @__PURE__ */ new Set(["subtitles", "captions", "descriptions", "chapters", "metadata"]);
|
|
206
|
+
function sanitizeUrl(url, kind = "link") {
|
|
207
|
+
if (typeof url !== "string") return null;
|
|
208
|
+
const trimmed = url.trim();
|
|
209
|
+
if (!trimmed) return null;
|
|
210
|
+
const compact = stripUrlSchemeNoise(trimmed);
|
|
211
|
+
const colon = compact.indexOf(":");
|
|
212
|
+
const firstPathChar = firstIndexOfAny(compact, ["/", "?", "#"]);
|
|
213
|
+
const hasScheme = colon >= 0 && (firstPathChar < 0 || colon < firstPathChar);
|
|
214
|
+
if (!hasScheme) return trimmed;
|
|
215
|
+
const scheme = compact.slice(0, colon).toLowerCase();
|
|
216
|
+
if (kind === "media") {
|
|
217
|
+
if (scheme === "data") return SAFE_DATA_MEDIA_RE.test(compact) ? trimmed : null;
|
|
218
|
+
return SAFE_MEDIA_SCHEMES.has(scheme) ? trimmed : null;
|
|
219
|
+
}
|
|
220
|
+
return SAFE_LINK_SCHEMES.has(scheme) ? trimmed : null;
|
|
221
|
+
}
|
|
222
|
+
function sanitizeHtmlNodes(nodes) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const node of nodes) {
|
|
225
|
+
out.push(...sanitizeHtmlNode(node));
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
function sanitizeHtmlNode(node) {
|
|
230
|
+
switch (node.type) {
|
|
231
|
+
case "htmlText":
|
|
232
|
+
return [node];
|
|
233
|
+
case "htmlComment":
|
|
234
|
+
return [];
|
|
235
|
+
case "htmlElement":
|
|
236
|
+
return sanitizeHtmlElement(node);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function sanitizeHtmlElement(node) {
|
|
240
|
+
const tag = node.tagName.toLowerCase();
|
|
241
|
+
if (DROP_WITH_CONTENT_TAGS.has(tag)) return [];
|
|
242
|
+
const children = sanitizeHtmlNodes(node.children);
|
|
243
|
+
if (!SAFE_TAGS.has(tag)) return children;
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
type: "htmlElement",
|
|
247
|
+
tagName: tag,
|
|
248
|
+
attributes: sanitizeAttrs(tag, node.attributes),
|
|
249
|
+
children,
|
|
250
|
+
selfClosing: node.selfClosing
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
function sanitizeAttrs(tag, attrs) {
|
|
255
|
+
const out = {};
|
|
256
|
+
for (const [rawName, rawValue] of Object.entries(attrs)) {
|
|
257
|
+
const name = rawName.toLowerCase();
|
|
258
|
+
if (!isAllowedAttr(tag, name)) continue;
|
|
259
|
+
const value = String(rawValue);
|
|
260
|
+
const safeValue = sanitizeAttrValue(tag, name, value);
|
|
261
|
+
if (safeValue === null) continue;
|
|
262
|
+
out[name] = safeValue;
|
|
263
|
+
}
|
|
264
|
+
if (tag === "a" && out.target === "_blank") {
|
|
265
|
+
out.rel = ensureRelTokens(out.rel, ["noopener", "noreferrer"]);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function isAllowedAttr(tag, name) {
|
|
270
|
+
if (!SAFE_ATTR_NAME_RE.test(name)) return false;
|
|
271
|
+
if (name.startsWith("on")) return false;
|
|
272
|
+
if (name.startsWith("aria-") || name.startsWith("data-")) return true;
|
|
273
|
+
if (GLOBAL_ATTRS.has(name)) return true;
|
|
274
|
+
return TAG_ATTRS[tag]?.has(name) ?? false;
|
|
275
|
+
}
|
|
276
|
+
function sanitizeAttrValue(tag, name, value) {
|
|
277
|
+
if (name === "href") return sanitizeUrl(value, "link");
|
|
278
|
+
if (name === "src" || name === "poster") return sanitizeUrl(value, "media");
|
|
279
|
+
if ((name === "width" || name === "height" || name === "span" || name === "colspan" || name === "rowspan" || name === "value") && !isNonNegativeInteger(value)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (name === "start" && !isInteger(value)) return null;
|
|
283
|
+
if (name === "type" && tag === "ol" && !SAFE_OL_TYPES.has(value)) return null;
|
|
284
|
+
if (name === "preload" && !SAFE_PRELOAD_VALUES.has(value)) return null;
|
|
285
|
+
if (name === "kind" && tag === "track" && !SAFE_TRACK_KINDS.has(value)) return null;
|
|
286
|
+
if (name === "align" && value !== "left" && value !== "right" && value !== "center") return null;
|
|
287
|
+
if (name === "target")
|
|
288
|
+
return value === "_blank" || value === "_self" || value === "_parent" || value === "_top" ? value : null;
|
|
289
|
+
if (name === "rel") return sanitizeRel(value);
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
function sanitizeRel(value) {
|
|
293
|
+
return relTokens(value).join(" ");
|
|
294
|
+
}
|
|
295
|
+
function ensureRelTokens(value, required) {
|
|
296
|
+
const tokens = new Set(value ? relTokens(value) : []);
|
|
297
|
+
for (const token of required) tokens.add(token);
|
|
298
|
+
return Array.from(tokens).join(" ");
|
|
299
|
+
}
|
|
300
|
+
function relTokens(value) {
|
|
301
|
+
const tokens = value.split(/\s+/).map((token) => token.trim().toLowerCase()).filter(Boolean).filter((token) => /^[a-z0-9_-]+$/.test(token));
|
|
302
|
+
return Array.from(new Set(tokens));
|
|
303
|
+
}
|
|
304
|
+
function isInteger(value) {
|
|
305
|
+
return /^-?\d+$/.test(value.trim());
|
|
306
|
+
}
|
|
307
|
+
function isNonNegativeInteger(value) {
|
|
308
|
+
return /^\d+$/.test(value.trim());
|
|
309
|
+
}
|
|
310
|
+
function firstIndexOfAny(value, needles) {
|
|
311
|
+
let result = -1;
|
|
312
|
+
for (const needle of needles) {
|
|
313
|
+
const index = value.indexOf(needle);
|
|
314
|
+
if (index >= 0 && (result < 0 || index < result)) result = index;
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
function stripUrlSchemeNoise(value) {
|
|
319
|
+
let out = "";
|
|
320
|
+
for (const char of value) {
|
|
321
|
+
const code = char.charCodeAt(0);
|
|
322
|
+
if (code <= 32 || code === 127 || char.trim() === "") continue;
|
|
323
|
+
out += char;
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export {
|
|
329
|
+
stringifyMarkdown,
|
|
330
|
+
sanitizeUrl,
|
|
331
|
+
sanitizeHtmlNodes
|
|
332
|
+
};
|
|
333
|
+
//# sourceMappingURL=chunk-KZNXZ5SM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/markdown/stringify.ts","../src/markdown/sanitize.ts"],"sourcesContent":["/**\n * JSON → Markdown Serializer\n *\n * Converts a MarkdownDocument JSON structure back to a markdown string\n * using the unified/remark ecosystem with GFM, math, and directive extensions.\n */\n\nimport { unified } from 'unified';\nimport remarkStringify from 'remark-stringify';\nimport remarkGfm from 'remark-gfm';\nimport remarkMath from 'remark-math';\nimport remarkDirective from 'remark-directive';\nimport type { MarkdownDocument, StringifyOptions } from './types.js';\nimport { toMdast } from './convert.js';\n\n// Cache the default processor (all extensions, default formatting) to avoid rebuilding on every call.\nlet defaultProcessor: any;\n\n// ============================================\n// Annotation-span unescaping\n// ============================================\n\n// Quoted runs as they appear in remark-stringify output. Backslashes inside\n// the span are doubled by remark (a literal `\\` before punctuation becomes\n// `\\\\`), so `\\\\.` covers both authored escapes and remark's own.\nconst DQ_RUN = `\"(?:[^\"\\\\\\\\]|\\\\\\\\.)*\"`;\nconst SQ_RUN = `'(?:[^'\\\\\\\\]|\\\\\\\\.)*'`;\n\n/**\n * A `{[…]}` template-annotation (or inline-icon) span as it appears in\n * remark output: `{\\[inner]}`, where `inner` may contain backslash escapes\n * and quoted runs (which may themselves contain `]`).\n */\nconst ESCAPED_TEMPLATE_SPAN_RE = new RegExp(\n `\\\\{\\\\\\\\\\\\[((?:${DQ_RUN}|${SQ_RUN}|\\\\\\\\.|[^\\\\]\\\\\\\\])+)\\\\]\\\\}`,\n 'g',\n);\n\n/** A heading line (annotations are only emitted trailing on headings). */\nconst HEADING_LINE_RE = /^#{1,6} .*$/gm;\n\n/**\n * The trailing Pandoc `{…}` span on a heading line, possibly followed by an\n * already-unescaped `{[…]}` template span (canonical emit order).\n */\nconst TRAILING_ESCAPED_PANDOC_SPAN_RE = new RegExp(\n `\\\\{(?!\\\\\\\\?\\\\[)((?:${DQ_RUN}|${SQ_RUN}|\\\\\\\\.|[^}\\\\\\\\])*)\\\\}(?=\\\\s*(?:\\\\{\\\\[.*)?$)`,\n);\n\n/**\n * Backslash+punctuation pairs that are safe to unescape inside an\n * annotation span: the bare character re-parses as literal text in\n * trailing-heading position, so removing the backslash is purely\n * cosmetic and round-trips.\n *\n * Deliberately EXCLUDED (left escaped in the emitted markdown): emphasis\n * and other inline-construct openers (`*`, `_`, `` ` ``, `~`, `<`, `>`,\n * `&`, `!`, `|`, `^`). Unescaping those could make remark-parse split the\n * heading's text run into inline nodes on reparse, which would hide the\n * annotation from the extractor. remark-parse resolves the surviving\n * `\\X` escapes back to bare characters, so values still round-trip —\n * the escapes are a cosmetic cost only.\n *\n * Pairs are consumed left to right, so an authored `\\\"` that remark\n * doubled to `\\\\\"` correctly comes back as `\\\"`.\n */\nconst UNESCAPE_PUNCT_RE = /\\\\([\\\\[\\]:#.,+=/?;@%$(){}'\"-])/g;\n\nfunction unescapeMarkdownPunct(text: string): string {\n return text.replace(UNESCAPE_PUNCT_RE, '$1');\n}\n\n/**\n * Serialize a MarkdownDocument back to a markdown string.\n *\n * All extensions (GFM, math, directives) are enabled by default.\n * Use the `options` parameter to control formatting and disable extensions.\n *\n * @param doc - The MarkdownDocument to serialize\n * @param options - Serialization options (formatting, extensions)\n * @returns A markdown string\n *\n * @example\n * ```ts\n * const doc: MarkdownDocument = {\n * type: 'document',\n * children: [\n * { type: 'heading', depth: 1, children: [{ type: 'text', value: 'Hello' }] },\n * { type: 'paragraph', children: [{ type: 'text', value: 'World' }] },\n * ],\n * };\n * const md = stringifyMarkdown(doc);\n * // '# Hello\\n\\nWorld\\n'\n * ```\n */\nexport function stringifyMarkdown(doc: MarkdownDocument, options?: StringifyOptions): string {\n // Convert MarkdownDocument → mdast tree\n const mdastTree = toMdast(doc);\n\n // Use cached default processor when all extensions and default formatting are used.\n const useDefaults =\n !options ||\n (options.gfm !== false &&\n options.math !== false &&\n options.directive !== false &&\n !options.bullet &&\n !options.bulletOrdered &&\n !options.emphasis &&\n !options.strong &&\n !options.rule &&\n !options.fence &&\n options.setext == null);\n\n let processor: any;\n\n if (useDefaults) {\n if (!defaultProcessor) {\n defaultProcessor = unified()\n .use(remarkGfm)\n .use(remarkMath)\n .use(remarkDirective)\n .use(remarkStringify, {\n bullet: '-',\n bulletOrdered: '.',\n emphasis: '*',\n strong: '*',\n rule: '-',\n fence: '`',\n setext: false,\n });\n }\n processor = defaultProcessor;\n } else {\n // Build a custom processor with requested options.\n // unified's .use() chaining changes the generic signature each time,\n // making strict typing impractical — use a widened Processor type.\n processor = unified();\n\n if (options?.gfm !== false) {\n processor = processor.use(remarkGfm);\n }\n if (options?.math !== false) {\n processor = processor.use(remarkMath);\n }\n if (options?.directive !== false) {\n processor = processor.use(remarkDirective);\n }\n\n processor = processor.use(remarkStringify, {\n bullet: options?.bullet ?? '-',\n bulletOrdered: options?.bulletOrdered ?? '.',\n emphasis: options?.emphasis ?? '*',\n strong: options?.strong ?? '*',\n rule: options?.rule ?? '-',\n fence: options?.fence ?? '`',\n setext: options?.setext ?? false,\n });\n }\n\n // Stringify mdast → markdown string\n const result = processor.stringify(mdastTree) as string;\n\n // remark-stringify backslash-escapes markdown-significant characters in\n // text nodes (`[` to prevent link syntax, `:` for directives, `*`/`_` for\n // emphasis, a literal `\\` before punctuation, …). Our annotations are\n // injected as plain text, so their interiors come back escaped. Undo that\n // inside `{[…]}` template-annotation spans: removing one backslash from\n // each backslash+punctuation pair (left to right) exactly inverts\n // remark's escaping — including restoring an authored `\\\"` that remark\n // doubled to `\\\\\"`. The span regex understands quoted values, so a\n // quoted `]` doesn't terminate the span.\n let cleaned = result.replace(\n ESCAPED_TEMPLATE_SPAN_RE,\n (_m, inner: string) => `{[${unescapeMarkdownPunct(inner)}]}`,\n );\n\n // Same treatment for the trailing Pandoc `{…}` attribute block on heading\n // lines (canonical emit order is Pandoc block then template annotation,\n // so the span sits at end-of-line or just before a `{[…]}` span).\n cleaned = cleaned.replace(HEADING_LINE_RE, (line) =>\n line.replace(\n TRAILING_ESCAPED_PANDOC_SPAN_RE,\n (_m, inner: string) => `{${unescapeMarkdownPunct(inner)}}`,\n ),\n );\n\n // remark-directive escapes `:` in text (since `:name` would round-trip as\n // a text directive). Inside any other Pandoc-style `{…}` brace span, `:`\n // is purely a value separator (e.g. `connectsTo=foo:flow`), so unescape\n // it to keep round-trips lossless. The negative lookahead skips `{[…]}`\n // template annotations — those are handled above.\n cleaned = cleaned.replace(/\\{(?!\\\\?\\[)[^}]*\\}/g, (match) => match.replace(/\\\\:/g, ':'));\n\n // Prepend YAML frontmatter if present\n if (doc.frontmatter && Object.keys(doc.frontmatter).length > 0) {\n const yamlLines = Object.entries(doc.frontmatter).map(\n ([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`,\n );\n return `---\\n${yamlLines.join('\\n')}\\n---\\n\\n${cleaned}`;\n }\n\n return cleaned;\n}\n","import type { HtmlElement, HtmlNode } from './types.js';\n\nexport type HtmlPolicy = 'strip' | 'sanitize' | 'trusted';\n\nexport type UrlKind = 'link' | 'media';\n\nconst SAFE_LINK_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);\nconst SAFE_MEDIA_SCHEMES = new Set(['http', 'https', 'blob']);\n\nconst SAFE_DATA_MEDIA_RE =\n /^data:(?:image\\/(?!svg\\+xml)[a-z0-9.+-]+|audio\\/[a-z0-9.+-]+|video\\/[a-z0-9.+-]+);/i;\n\nconst SAFE_TAGS = new Set([\n 'a',\n 'abbr',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'data',\n 'dd',\n 'del',\n 'details',\n 'dfn',\n 'div',\n 'dl',\n 'dt',\n 'em',\n 'figcaption',\n 'figure',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'kbd',\n 'li',\n 'mark',\n 'ol',\n 'p',\n 'pre',\n 'q',\n 's',\n 'samp',\n 'small',\n 'source',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'audio',\n]);\n\nconst DROP_WITH_CONTENT_TAGS = new Set([\n 'base',\n 'button',\n 'embed',\n 'form',\n 'iframe',\n 'input',\n 'link',\n 'meta',\n 'object',\n 'option',\n 'script',\n 'select',\n 'style',\n 'svg',\n 'template',\n 'textarea',\n]);\n\nconst GLOBAL_ATTRS = new Set(['class', 'id', 'title', 'role']);\n\nconst TAG_ATTRS: Record<string, Set<string>> = {\n a: new Set(['href', 'target', 'rel']),\n audio: new Set(['src', 'controls', 'preload', 'muted', 'loop']),\n col: new Set(['span', 'width']),\n colgroup: new Set(['span']),\n data: new Set(['value']),\n img: new Set(['src', 'alt', 'width', 'height', 'loading', 'decoding']),\n li: new Set(['value']),\n ol: new Set(['start', 'type']),\n q: new Set(['cite']),\n source: new Set(['src', 'type', 'media']),\n td: new Set(['colspan', 'rowspan', 'align']),\n th: new Set(['colspan', 'rowspan', 'align', 'scope']),\n time: new Set(['datetime']),\n track: new Set(['src', 'kind', 'label', 'srclang', 'default']),\n video: new Set([\n 'src',\n 'poster',\n 'width',\n 'height',\n 'controls',\n 'preload',\n 'muted',\n 'loop',\n 'playsinline',\n ]),\n};\n\nconst SAFE_ATTR_NAME_RE = /^[a-z][a-z0-9:_.-]*$/;\nconst SAFE_OL_TYPES = new Set(['1', 'a', 'A', 'i', 'I']);\nconst SAFE_PRELOAD_VALUES = new Set(['none', 'metadata', 'auto']);\nconst SAFE_TRACK_KINDS = new Set(['subtitles', 'captions', 'descriptions', 'chapters', 'metadata']);\n\n/**\n * Sanitize a URL for a rendered link or media attribute.\n *\n * Relative URLs are allowed. Absolute URLs are restricted to schemes that\n * cannot execute script in a document context. Media URLs additionally allow\n * `blob:` and image/audio/video `data:` URLs because Squisq uses both for\n * browser-local media previews and self-contained exports.\n */\nexport function sanitizeUrl(url: string | null | undefined, kind: UrlKind = 'link'): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (!trimmed) return null;\n\n const compact = stripUrlSchemeNoise(trimmed);\n const colon = compact.indexOf(':');\n const firstPathChar = firstIndexOfAny(compact, ['/', '?', '#']);\n const hasScheme = colon >= 0 && (firstPathChar < 0 || colon < firstPathChar);\n\n if (!hasScheme) return trimmed;\n\n const scheme = compact.slice(0, colon).toLowerCase();\n if (kind === 'media') {\n if (scheme === 'data') return SAFE_DATA_MEDIA_RE.test(compact) ? trimmed : null;\n return SAFE_MEDIA_SCHEMES.has(scheme) ? trimmed : null;\n }\n\n return SAFE_LINK_SCHEMES.has(scheme) ? trimmed : null;\n}\n\n/**\n * Return a sanitized copy of an HtmlNode tree. The original parsed tree is left\n * untouched so markdown stringify can still preserve source fidelity.\n */\nexport function sanitizeHtmlNodes(nodes: HtmlNode[]): HtmlNode[] {\n const out: HtmlNode[] = [];\n for (const node of nodes) {\n out.push(...sanitizeHtmlNode(node));\n }\n return out;\n}\n\nfunction sanitizeHtmlNode(node: HtmlNode): HtmlNode[] {\n switch (node.type) {\n case 'htmlText':\n return [node];\n case 'htmlComment':\n return [];\n case 'htmlElement':\n return sanitizeHtmlElement(node);\n }\n}\n\nfunction sanitizeHtmlElement(node: HtmlElement): HtmlNode[] {\n const tag = node.tagName.toLowerCase();\n\n if (DROP_WITH_CONTENT_TAGS.has(tag)) return [];\n\n const children = sanitizeHtmlNodes(node.children);\n if (!SAFE_TAGS.has(tag)) return children;\n\n return [\n {\n type: 'htmlElement',\n tagName: tag,\n attributes: sanitizeAttrs(tag, node.attributes),\n children,\n selfClosing: node.selfClosing,\n },\n ];\n}\n\nfunction sanitizeAttrs(tag: string, attrs: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [rawName, rawValue] of Object.entries(attrs)) {\n const name = rawName.toLowerCase();\n if (!isAllowedAttr(tag, name)) continue;\n\n const value = String(rawValue);\n const safeValue = sanitizeAttrValue(tag, name, value);\n if (safeValue === null) continue;\n out[name] = safeValue;\n }\n if (tag === 'a' && out.target === '_blank') {\n out.rel = ensureRelTokens(out.rel, ['noopener', 'noreferrer']);\n }\n return out;\n}\n\nfunction isAllowedAttr(tag: string, name: string): boolean {\n if (!SAFE_ATTR_NAME_RE.test(name)) return false;\n if (name.startsWith('on')) return false;\n if (name.startsWith('aria-') || name.startsWith('data-')) return true;\n if (GLOBAL_ATTRS.has(name)) return true;\n return TAG_ATTRS[tag]?.has(name) ?? false;\n}\n\nfunction sanitizeAttrValue(tag: string, name: string, value: string): string | null {\n if (name === 'href') return sanitizeUrl(value, 'link');\n if (name === 'src' || name === 'poster') return sanitizeUrl(value, 'media');\n if (\n (name === 'width' ||\n name === 'height' ||\n name === 'span' ||\n name === 'colspan' ||\n name === 'rowspan' ||\n name === 'value') &&\n !isNonNegativeInteger(value)\n ) {\n return null;\n }\n if (name === 'start' && !isInteger(value)) return null;\n if (name === 'type' && tag === 'ol' && !SAFE_OL_TYPES.has(value)) return null;\n if (name === 'preload' && !SAFE_PRELOAD_VALUES.has(value)) return null;\n if (name === 'kind' && tag === 'track' && !SAFE_TRACK_KINDS.has(value)) return null;\n if (name === 'align' && value !== 'left' && value !== 'right' && value !== 'center') return null;\n if (name === 'target')\n return value === '_blank' || value === '_self' || value === '_parent' || value === '_top'\n ? value\n : null;\n if (name === 'rel') return sanitizeRel(value);\n return value;\n}\n\nfunction sanitizeRel(value: string): string {\n return relTokens(value).join(' ');\n}\n\nfunction ensureRelTokens(value: string | undefined, required: string[]): string {\n const tokens = new Set(value ? relTokens(value) : []);\n for (const token of required) tokens.add(token);\n return Array.from(tokens).join(' ');\n}\n\nfunction relTokens(value: string): string[] {\n const tokens = value\n .split(/\\s+/)\n .map((token) => token.trim().toLowerCase())\n .filter(Boolean)\n .filter((token) => /^[a-z0-9_-]+$/.test(token));\n return Array.from(new Set(tokens));\n}\n\nfunction isInteger(value: string): boolean {\n return /^-?\\d+$/.test(value.trim());\n}\n\nfunction isNonNegativeInteger(value: string): boolean {\n return /^\\d+$/.test(value.trim());\n}\n\nfunction firstIndexOfAny(value: string, needles: string[]): number {\n let result = -1;\n for (const needle of needles) {\n const index = value.indexOf(needle);\n if (index >= 0 && (result < 0 || index < result)) result = index;\n }\n return result;\n}\n\nfunction stripUrlSchemeNoise(value: string): string {\n let out = '';\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (code <= 0x20 || code === 0x7f || char.trim() === '') continue;\n out += char;\n }\n return out;\n}\n"],"mappings":";;;;;AAOA,SAAS,eAAe;AACxB,OAAO,qBAAqB;AAC5B,OAAO,eAAe;AACtB,OAAO,gBAAgB;AACvB,OAAO,qBAAqB;AAK5B,IAAI;AASJ,IAAM,SAAS;AACf,IAAM,SAAS;AAOf,IAAM,2BAA2B,IAAI;AAAA,EACnC,iBAAiB,MAAM,IAAI,MAAM;AAAA,EACjC;AACF;AAGA,IAAM,kBAAkB;AAMxB,IAAM,kCAAkC,IAAI;AAAA,EAC1C,sBAAsB,MAAM,IAAI,MAAM;AACxC;AAmBA,IAAM,oBAAoB;AAE1B,SAAS,sBAAsB,MAAsB;AACnD,SAAO,KAAK,QAAQ,mBAAmB,IAAI;AAC7C;AAyBO,SAAS,kBAAkB,KAAuB,SAAoC;AAE3F,QAAM,YAAY,QAAQ,GAAG;AAG7B,QAAM,cACJ,CAAC,WACA,QAAQ,QAAQ,SACf,QAAQ,SAAS,SACjB,QAAQ,cAAc,SACtB,CAAC,QAAQ,UACT,CAAC,QAAQ,iBACT,CAAC,QAAQ,YACT,CAAC,QAAQ,UACT,CAAC,QAAQ,QACT,CAAC,QAAQ,SACT,QAAQ,UAAU;AAEtB,MAAI;AAEJ,MAAI,aAAa;AACf,QAAI,CAAC,kBAAkB;AACrB,yBAAmB,QAAQ,EACxB,IAAI,SAAS,EACb,IAAI,UAAU,EACd,IAAI,eAAe,EACnB,IAAI,iBAAiB;AAAA,QACpB,QAAQ;AAAA,QACR,eAAe;AAAA,QACf,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,QACP,QAAQ;AAAA,MACV,CAAC;AAAA,IACL;AACA,gBAAY;AAAA,EACd,OAAO;AAIL,gBAAY,QAAQ;AAEpB,QAAI,SAAS,QAAQ,OAAO;AAC1B,kBAAY,UAAU,IAAI,SAAS;AAAA,IACrC;AACA,QAAI,SAAS,SAAS,OAAO;AAC3B,kBAAY,UAAU,IAAI,UAAU;AAAA,IACtC;AACA,QAAI,SAAS,cAAc,OAAO;AAChC,kBAAY,UAAU,IAAI,eAAe;AAAA,IAC3C;AAEA,gBAAY,UAAU,IAAI,iBAAiB;AAAA,MACzC,QAAQ,SAAS,UAAU;AAAA,MAC3B,eAAe,SAAS,iBAAiB;AAAA,MACzC,UAAU,SAAS,YAAY;AAAA,MAC/B,QAAQ,SAAS,UAAU;AAAA,MAC3B,MAAM,SAAS,QAAQ;AAAA,MACvB,OAAO,SAAS,SAAS;AAAA,MACzB,QAAQ,SAAS,UAAU;AAAA,IAC7B,CAAC;AAAA,EACH;AAGA,QAAM,SAAS,UAAU,UAAU,SAAS;AAW5C,MAAI,UAAU,OAAO;AAAA,IACnB;AAAA,IACA,CAAC,IAAI,UAAkB,KAAK,sBAAsB,KAAK,CAAC;AAAA,EAC1D;AAKA,YAAU,QAAQ;AAAA,IAAQ;AAAA,IAAiB,CAAC,SAC1C,KAAK;AAAA,MACH;AAAA,MACA,CAAC,IAAI,UAAkB,IAAI,sBAAsB,KAAK,CAAC;AAAA,IACzD;AAAA,EACF;AAOA,YAAU,QAAQ,QAAQ,uBAAuB,CAAC,UAAU,MAAM,QAAQ,QAAQ,GAAG,CAAC;AAGtF,MAAI,IAAI,eAAe,OAAO,KAAK,IAAI,WAAW,EAAE,SAAS,GAAG;AAC9D,UAAM,YAAY,OAAO,QAAQ,IAAI,WAAW,EAAE;AAAA,MAChD,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,OAAO,MAAM,WAAW,IAAI,KAAK,UAAU,CAAC,CAAC;AAAA,IACpE;AACA,WAAO;AAAA,EAAQ,UAAU,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA,EAAY,OAAO;AAAA,EACxD;AAEA,SAAO;AACT;;;ACpMA,IAAM,oBAAoB,oBAAI,IAAI,CAAC,QAAQ,SAAS,UAAU,KAAK,CAAC;AACpE,IAAM,qBAAqB,oBAAI,IAAI,CAAC,QAAQ,SAAS,MAAM,CAAC;AAE5D,IAAM,qBACJ;AAEF,IAAM,YAAY,oBAAI,IAAI;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,yBAAyB,oBAAI,IAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,eAAe,oBAAI,IAAI,CAAC,SAAS,MAAM,SAAS,MAAM,CAAC;AAE7D,IAAM,YAAyC;AAAA,EAC7C,GAAG,oBAAI,IAAI,CAAC,QAAQ,UAAU,KAAK,CAAC;AAAA,EACpC,OAAO,oBAAI,IAAI,CAAC,OAAO,YAAY,WAAW,SAAS,MAAM,CAAC;AAAA,EAC9D,KAAK,oBAAI,IAAI,CAAC,QAAQ,OAAO,CAAC;AAAA,EAC9B,UAAU,oBAAI,IAAI,CAAC,MAAM,CAAC;AAAA,EAC1B,MAAM,oBAAI,IAAI,CAAC,OAAO,CAAC;AAAA,EACvB,KAAK,oBAAI,IAAI,CAAC,OAAO,OAAO,SAAS,UAAU,WAAW,UAAU,CAAC;AAAA,EACrE,IAAI,oBAAI,IAAI,CAAC,OAAO,CAAC;AAAA,EACrB,IAAI,oBAAI,IAAI,CAAC,SAAS,MAAM,CAAC;AAAA,EAC7B,GAAG,oBAAI,IAAI,CAAC,MAAM,CAAC;AAAA,EACnB,QAAQ,oBAAI,IAAI,CAAC,OAAO,QAAQ,OAAO,CAAC;AAAA,EACxC,IAAI,oBAAI,IAAI,CAAC,WAAW,WAAW,OAAO,CAAC;AAAA,EAC3C,IAAI,oBAAI,IAAI,CAAC,WAAW,WAAW,SAAS,OAAO,CAAC;AAAA,EACpD,MAAM,oBAAI,IAAI,CAAC,UAAU,CAAC;AAAA,EAC1B,OAAO,oBAAI,IAAI,CAAC,OAAO,QAAQ,SAAS,WAAW,SAAS,CAAC;AAAA,EAC7D,OAAO,oBAAI,IAAI;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEA,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AACvD,IAAM,sBAAsB,oBAAI,IAAI,CAAC,QAAQ,YAAY,MAAM,CAAC;AAChE,IAAM,mBAAmB,oBAAI,IAAI,CAAC,aAAa,YAAY,gBAAgB,YAAY,UAAU,CAAC;AAU3F,SAAS,YAAY,KAAgC,OAAgB,QAAuB;AACjG,MAAI,OAAO,QAAQ,SAAU,QAAO;AAEpC,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,UAAU,oBAAoB,OAAO;AAC3C,QAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,QAAM,gBAAgB,gBAAgB,SAAS,CAAC,KAAK,KAAK,GAAG,CAAC;AAC9D,QAAM,YAAY,SAAS,MAAM,gBAAgB,KAAK,QAAQ;AAE9D,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,SAAS,QAAQ,MAAM,GAAG,KAAK,EAAE,YAAY;AACnD,MAAI,SAAS,SAAS;AACpB,QAAI,WAAW,OAAQ,QAAO,mBAAmB,KAAK,OAAO,IAAI,UAAU;AAC3E,WAAO,mBAAmB,IAAI,MAAM,IAAI,UAAU;AAAA,EACpD;AAEA,SAAO,kBAAkB,IAAI,MAAM,IAAI,UAAU;AACnD;AAMO,SAAS,kBAAkB,OAA+B;AAC/D,QAAM,MAAkB,CAAC;AACzB,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,GAAG,iBAAiB,IAAI,CAAC;AAAA,EACpC;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAA4B;AACpD,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,CAAC,IAAI;AAAA,IACd,KAAK;AACH,aAAO,CAAC;AAAA,IACV,KAAK;AACH,aAAO,oBAAoB,IAAI;AAAA,EACnC;AACF;AAEA,SAAS,oBAAoB,MAA+B;AAC1D,QAAM,MAAM,KAAK,QAAQ,YAAY;AAErC,MAAI,uBAAuB,IAAI,GAAG,EAAG,QAAO,CAAC;AAE7C,QAAM,WAAW,kBAAkB,KAAK,QAAQ;AAChD,MAAI,CAAC,UAAU,IAAI,GAAG,EAAG,QAAO;AAEhC,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,MACT,YAAY,cAAc,KAAK,KAAK,UAAU;AAAA,MAC9C;AAAA,MACA,aAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AAEA,SAAS,cAAc,KAAa,OAAuD;AACzF,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,SAAS,QAAQ,KAAK,OAAO,QAAQ,KAAK,GAAG;AACvD,UAAM,OAAO,QAAQ,YAAY;AACjC,QAAI,CAAC,cAAc,KAAK,IAAI,EAAG;AAE/B,UAAM,QAAQ,OAAO,QAAQ;AAC7B,UAAM,YAAY,kBAAkB,KAAK,MAAM,KAAK;AACpD,QAAI,cAAc,KAAM;AACxB,QAAI,IAAI,IAAI;AAAA,EACd;AACA,MAAI,QAAQ,OAAO,IAAI,WAAW,UAAU;AAC1C,QAAI,MAAM,gBAAgB,IAAI,KAAK,CAAC,YAAY,YAAY,CAAC;AAAA,EAC/D;AACA,SAAO;AACT;AAEA,SAAS,cAAc,KAAa,MAAuB;AACzD,MAAI,CAAC,kBAAkB,KAAK,IAAI,EAAG,QAAO;AAC1C,MAAI,KAAK,WAAW,IAAI,EAAG,QAAO;AAClC,MAAI,KAAK,WAAW,OAAO,KAAK,KAAK,WAAW,OAAO,EAAG,QAAO;AACjE,MAAI,aAAa,IAAI,IAAI,EAAG,QAAO;AACnC,SAAO,UAAU,GAAG,GAAG,IAAI,IAAI,KAAK;AACtC;AAEA,SAAS,kBAAkB,KAAa,MAAc,OAA8B;AAClF,MAAI,SAAS,OAAQ,QAAO,YAAY,OAAO,MAAM;AACrD,MAAI,SAAS,SAAS,SAAS,SAAU,QAAO,YAAY,OAAO,OAAO;AAC1E,OACG,SAAS,WACR,SAAS,YACT,SAAS,UACT,SAAS,aACT,SAAS,aACT,SAAS,YACX,CAAC,qBAAqB,KAAK,GAC3B;AACA,WAAO;AAAA,EACT;AACA,MAAI,SAAS,WAAW,CAAC,UAAU,KAAK,EAAG,QAAO;AAClD,MAAI,SAAS,UAAU,QAAQ,QAAQ,CAAC,cAAc,IAAI,KAAK,EAAG,QAAO;AACzE,MAAI,SAAS,aAAa,CAAC,oBAAoB,IAAI,KAAK,EAAG,QAAO;AAClE,MAAI,SAAS,UAAU,QAAQ,WAAW,CAAC,iBAAiB,IAAI,KAAK,EAAG,QAAO;AAC/E,MAAI,SAAS,WAAW,UAAU,UAAU,UAAU,WAAW,UAAU,SAAU,QAAO;AAC5F,MAAI,SAAS;AACX,WAAO,UAAU,YAAY,UAAU,WAAW,UAAU,aAAa,UAAU,SAC/E,QACA;AACN,MAAI,SAAS,MAAO,QAAO,YAAY,KAAK;AAC5C,SAAO;AACT;AAEA,SAAS,YAAY,OAAuB;AAC1C,SAAO,UAAU,KAAK,EAAE,KAAK,GAAG;AAClC;AAEA,SAAS,gBAAgB,OAA2B,UAA4B;AAC9E,QAAM,SAAS,IAAI,IAAI,QAAQ,UAAU,KAAK,IAAI,CAAC,CAAC;AACpD,aAAW,SAAS,SAAU,QAAO,IAAI,KAAK;AAC9C,SAAO,MAAM,KAAK,MAAM,EAAE,KAAK,GAAG;AACpC;AAEA,SAAS,UAAU,OAAyB;AAC1C,QAAM,SAAS,MACZ,MAAM,KAAK,EACX,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,YAAY,CAAC,EACzC,OAAO,OAAO,EACd,OAAO,CAAC,UAAU,gBAAgB,KAAK,KAAK,CAAC;AAChD,SAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC;AACnC;AAEA,SAAS,UAAU,OAAwB;AACzC,SAAO,UAAU,KAAK,MAAM,KAAK,CAAC;AACpC;AAEA,SAAS,qBAAqB,OAAwB;AACpD,SAAO,QAAQ,KAAK,MAAM,KAAK,CAAC;AAClC;AAEA,SAAS,gBAAgB,OAAe,SAA2B;AACjE,MAAI,SAAS;AACb,aAAW,UAAU,SAAS;AAC5B,UAAM,QAAQ,MAAM,QAAQ,MAAM;AAClC,QAAI,SAAS,MAAM,SAAS,KAAK,QAAQ,QAAS,UAAS;AAAA,EAC7D;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,OAAuB;AAClD,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,KAAK,WAAW,CAAC;AAC9B,QAAI,QAAQ,MAAQ,SAAS,OAAQ,KAAK,KAAK,MAAM,GAAI;AACzD,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
extractPlainText,
|
|
3
3
|
findNodesByType,
|
|
4
4
|
walkMarkdownTree
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OKJOQXK4.js";
|
|
6
6
|
|
|
7
7
|
// src/recommend/templates.ts
|
|
8
8
|
var EMPTY_PROFILE = Object.freeze({
|
|
@@ -164,6 +164,15 @@ function recommendedNamesForProfile(profile) {
|
|
|
164
164
|
}
|
|
165
165
|
return Array.from(names);
|
|
166
166
|
}
|
|
167
|
+
function pickAutoTemplate(profile, blockIndex = 0) {
|
|
168
|
+
if (profile.hasTable) return "dataTable";
|
|
169
|
+
if (profile.imageCount >= 2) return "photoGrid";
|
|
170
|
+
if (profile.hasImage) return blockIndex % 2 === 0 ? "leftFeature" : "rightFeature";
|
|
171
|
+
if (profile.hasBlockquote) return "quote";
|
|
172
|
+
if (profile.hasNumberHighlight) return "statHighlight";
|
|
173
|
+
if (profile.hasList) return "list";
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
167
176
|
function recommendTemplatesForBlock(profile, allNames) {
|
|
168
177
|
const desired = new Set(recommendedNamesForProfile(profile));
|
|
169
178
|
const recommended = [];
|
|
@@ -177,6 +186,7 @@ function recommendTemplatesForBlock(profile, allNames) {
|
|
|
177
186
|
|
|
178
187
|
export {
|
|
179
188
|
profileBlockContents,
|
|
189
|
+
pickAutoTemplate,
|
|
180
190
|
recommendTemplatesForBlock
|
|
181
191
|
};
|
|
182
|
-
//# sourceMappingURL=chunk-
|
|
192
|
+
//# sourceMappingURL=chunk-LRCV7N7F.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recommend/templates.ts"],"sourcesContent":["/**\n * Block template recommendations.\n *\n * Given a slice of markdown block nodes (the body underneath a heading),\n * profile the content for image / video / quote / list / table / date /\n * stat signals, then map that profile to the set of templates that make\n * sense for the block. Used by the editor's template picker to surface a\n * \"Recommended for this block\" section above the full template list.\n */\n\nimport { extractPlainText, findNodesByType, walkMarkdownTree } from '../markdown/utils.js';\nimport type {\n HtmlElement,\n HtmlNode,\n MarkdownBlockNode,\n MarkdownHtmlBlock,\n MarkdownNode,\n} from '../markdown/types.js';\n\nexport interface BlockContentProfile {\n hasImage: boolean;\n imageCount: number;\n hasVideo: boolean;\n hasBlockquote: boolean;\n hasList: boolean;\n hasTable: boolean;\n hasDate: boolean;\n hasNumberHighlight: boolean;\n wordCount: number;\n}\n\nexport interface RecommendationResult {\n recommended: string[];\n rest: string[];\n}\n\nconst EMPTY_PROFILE: BlockContentProfile = Object.freeze({\n hasImage: false,\n imageCount: 0,\n hasVideo: false,\n hasBlockquote: false,\n hasList: false,\n hasTable: false,\n hasDate: false,\n hasNumberHighlight: false,\n wordCount: 0,\n});\n\nconst DATE_PATTERNS: RegExp[] = [\n /\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d{1,2},?\\s+)?\\d{4}\\b/i,\n /\\b\\d{1,2}\\s+(January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{4}\\b/i,\n /\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\\.?\\s+\\d{1,2},?\\s+\\d{4}\\b/i,\n /\\b\\d{4}-\\d{2}-\\d{2}\\b/,\n /\\b\\d{1,2}\\/\\d{1,2}\\/\\d{2,4}\\b/,\n /\\bQ[1-4]\\s+\\d{4}\\b/,\n /\\b\\d{4}s\\b/,\n /\\b\\d{1,2}(st|nd|rd|th)\\s+century\\b/i,\n];\n\n/**\n * Matches a \"highlight number\" — a prominent figure suitable for a Stat\n * Highlight block. Qualifies on any of:\n * - currency prefix: `$2.3M`, `€500`, `¥1,234`\n * - unit suffix: `50%`, `100 years`, `5×`, `2.3M`\n * - standalone large: `1,234`, `5000`, `12,345`\n * Plain small numbers (`42`) intentionally don't qualify.\n */\nconst NUMBER_HIGHLIGHT_RE =\n /(?:[$€£¥]\\s?\\d+(?:[.,]\\d+)*(?:\\s?(?:[MBK]|million|billion|thousand))?|\\d+(?:[.,]\\d+)*\\s?(?:%|‰|x|×|[MBK]|million|billion|thousand|percent|years?|days?|hours?)|\\d{3,}(?:[.,]\\d+)*)/i;\n\nconst VIDEO_HOST_RE = /(youtube\\.com|youtu\\.be|vimeo\\.com|wistia\\.|loom\\.com)/i;\n\nfunction htmlElementsByTag(root: MarkdownNode, tagNames: Set<string>): HtmlElement[] {\n const blocks = findNodesByType<MarkdownHtmlBlock>(root, 'htmlBlock');\n // Inline HTML (htmlInline) also carries htmlChildren; include both.\n const inline = findNodesByType<MarkdownNode>(root, 'htmlInline') as Array<\n MarkdownNode & { htmlChildren?: HtmlNode[] }\n >;\n const out: HtmlElement[] = [];\n const visit = (n: HtmlNode): void => {\n if (n.type === 'htmlElement') {\n if (tagNames.has(n.tagName.toLowerCase())) out.push(n);\n for (const c of n.children) visit(c);\n }\n };\n for (const b of blocks) for (const c of b.htmlChildren) visit(c);\n for (const i of inline) {\n if (Array.isArray(i.htmlChildren)) for (const c of i.htmlChildren) visit(c);\n }\n return out;\n}\n\n/**\n * Walk a slice of block nodes and summarize what kinds of content it\n * contains. Cheap to run — no markdown parsing, just AST traversal.\n */\nexport function profileBlockContents(nodes: MarkdownBlockNode[]): BlockContentProfile {\n if (!nodes || nodes.length === 0) return { ...EMPTY_PROFILE };\n\n let imageCount = 0;\n let hasBlockquote = false;\n let hasList = false;\n let hasTable = false;\n\n const plainParts: string[] = [];\n\n for (const node of nodes) {\n imageCount += findNodesByType(node, 'image').length;\n imageCount += findNodesByType(node, 'imageReference').length;\n if (findNodesByType(node, 'blockquote').length > 0) hasBlockquote = true;\n if (findNodesByType(node, 'list').length > 0) hasList = true;\n if (findNodesByType(node, 'table').length > 0) hasTable = true;\n plainParts.push(extractPlainText(node));\n }\n\n // HTML embeds: <img>, <video>, <iframe src=…video host>\n const root: MarkdownNode = { type: 'document', children: nodes };\n const imgTags = htmlElementsByTag(root, new Set(['img']));\n imageCount += imgTags.length;\n\n const videoTags = htmlElementsByTag(root, new Set(['video', 'iframe', 'source']));\n let hasVideo = false;\n for (const el of videoTags) {\n if (el.tagName.toLowerCase() === 'video') {\n hasVideo = true;\n break;\n }\n const src = el.attributes.src || el.attributes.href || '';\n if (VIDEO_HOST_RE.test(src)) {\n hasVideo = true;\n break;\n }\n }\n\n // Link-only video embeds (markdown link to a known video host).\n if (!hasVideo) {\n const links = findNodesByType<MarkdownNode & { url?: string }>(root, 'link');\n for (const link of links) {\n if (link.url && VIDEO_HOST_RE.test(link.url)) {\n hasVideo = true;\n break;\n }\n }\n }\n\n const plainText = plainParts.join(' ').trim();\n const wordCount = plainText ? plainText.split(/\\s+/).length : 0;\n\n let hasDate = false;\n for (const re of DATE_PATTERNS) {\n if (re.test(plainText)) {\n hasDate = true;\n break;\n }\n }\n\n // Number highlight: a paragraph that's mostly a single prominent figure.\n let hasNumberHighlight = false;\n walkMarkdownTree(root, (n) => {\n if (hasNumberHighlight) return true;\n if (n.type !== 'paragraph') return;\n const text = extractPlainText(n).trim();\n if (!text) return true;\n const m = text.match(NUMBER_HIGHLIGHT_RE);\n if (!m) return true;\n // Paragraph qualifies when its word count is small AND a prominent\n // number occupies a meaningful chunk of it. Keeps \"the company grew\n // 50% last year\" from triggering, but accepts \"$2.3M raised\".\n const words = text.split(/\\s+/).length;\n if (words <= 8) hasNumberHighlight = true;\n return true;\n });\n\n return {\n hasImage: imageCount > 0,\n imageCount,\n hasVideo,\n hasBlockquote,\n hasList,\n hasTable,\n hasDate,\n hasNumberHighlight,\n wordCount,\n };\n}\n\n// ── Recommendation rules ───────────────────────────────────────────\n\nconst UNIVERSAL_DEFAULTS = ['title', 'sectionHeader', 'factCard', 'twoColumn'];\n\n/**\n * Given a content profile, return the canonical template-name list that\n * should appear in the \"Recommended\" bucket. Caller is responsible for\n * preserving deduplication / ordering when intersecting with the full\n * template list (see {@link recommendTemplatesForBlock}).\n */\nfunction recommendedNamesForProfile(profile: BlockContentProfile): string[] {\n const names = new Set<string>();\n let anyContentSignal = false;\n\n if (profile.hasImage) {\n anyContentSignal = true;\n names.add('imageWithCaption');\n names.add('leftFeature');\n names.add('rightFeature');\n if (profile.imageCount >= 2) names.add('photoGrid');\n }\n\n if (profile.hasVideo) {\n anyContentSignal = true;\n names.add('videoWithCaption');\n if (profile.hasBlockquote) names.add('videoPullQuote');\n }\n\n if (profile.hasBlockquote) {\n anyContentSignal = true;\n names.add('quote');\n names.add('pullQuote');\n names.add('fullBleedQuote');\n }\n\n if (profile.hasList) {\n anyContentSignal = true;\n names.add('list');\n }\n\n if (profile.hasTable) {\n anyContentSignal = true;\n names.add('dataTable');\n names.add('comparisonBar');\n }\n\n if (profile.hasDate) {\n anyContentSignal = true;\n names.add('dateEvent');\n }\n\n if (profile.hasNumberHighlight) {\n anyContentSignal = true;\n names.add('statHighlight');\n }\n\n if (anyContentSignal) {\n // Universal pair only — keep recommended focused.\n names.add('title');\n names.add('sectionHeader');\n } else {\n for (const n of UNIVERSAL_DEFAULTS) names.add(n);\n }\n\n return Array.from(names);\n}\n\n/**\n * Split a flat list of template names into a \"Recommended\" bucket\n * (matches the active block's content profile) and a \"rest\" bucket\n * (everything else). Both buckets preserve the input ordering of\n * `allNames`, so the visual layout stays predictable as templates are\n * added.\n */\nexport function recommendTemplatesForBlock(\n profile: BlockContentProfile,\n allNames: readonly string[],\n): RecommendationResult {\n const desired = new Set(recommendedNamesForProfile(profile));\n const recommended: string[] = [];\n const rest: string[] = [];\n for (const name of allNames) {\n if (desired.has(name)) recommended.push(name);\n else rest.push(name);\n }\n return { recommended, rest };\n}\n"],"mappings":";;;;;;;AAoCA,IAAM,gBAAqC,OAAO,OAAO;AAAA,EACvD,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,eAAe;AAAA,EACf,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,WAAW;AACb,CAAC;AAED,IAAM,gBAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUA,IAAM,sBACJ;AAEF,IAAM,gBAAgB;AAEtB,SAAS,kBAAkB,MAAoB,UAAsC;AACnF,QAAM,SAAS,gBAAmC,MAAM,WAAW;AAEnE,QAAM,SAAS,gBAA8B,MAAM,YAAY;AAG/D,QAAM,MAAqB,CAAC;AAC5B,QAAM,QAAQ,CAAC,MAAsB;AACnC,QAAI,EAAE,SAAS,eAAe;AAC5B,UAAI,SAAS,IAAI,EAAE,QAAQ,YAAY,CAAC,EAAG,KAAI,KAAK,CAAC;AACrD,iBAAW,KAAK,EAAE,SAAU,OAAM,CAAC;AAAA,IACrC;AAAA,EACF;AACA,aAAW,KAAK,OAAQ,YAAW,KAAK,EAAE,aAAc,OAAM,CAAC;AAC/D,aAAW,KAAK,QAAQ;AACtB,QAAI,MAAM,QAAQ,EAAE,YAAY,EAAG,YAAW,KAAK,EAAE,aAAc,OAAM,CAAC;AAAA,EAC5E;AACA,SAAO;AACT;AAMO,SAAS,qBAAqB,OAAiD;AACpF,MAAI,CAAC,SAAS,MAAM,WAAW,EAAG,QAAO,EAAE,GAAG,cAAc;AAE5D,MAAI,aAAa;AACjB,MAAI,gBAAgB;AACpB,MAAI,UAAU;AACd,MAAI,WAAW;AAEf,QAAM,aAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,kBAAc,gBAAgB,MAAM,OAAO,EAAE;AAC7C,kBAAc,gBAAgB,MAAM,gBAAgB,EAAE;AACtD,QAAI,gBAAgB,MAAM,YAAY,EAAE,SAAS,EAAG,iBAAgB;AACpE,QAAI,gBAAgB,MAAM,MAAM,EAAE,SAAS,EAAG,WAAU;AACxD,QAAI,gBAAgB,MAAM,OAAO,EAAE,SAAS,EAAG,YAAW;AAC1D,eAAW,KAAK,iBAAiB,IAAI,CAAC;AAAA,EACxC;AAGA,QAAM,OAAqB,EAAE,MAAM,YAAY,UAAU,MAAM;AAC/D,QAAM,UAAU,kBAAkB,MAAM,oBAAI,IAAI,CAAC,KAAK,CAAC,CAAC;AACxD,gBAAc,QAAQ;AAEtB,QAAM,YAAY,kBAAkB,MAAM,oBAAI,IAAI,CAAC,SAAS,UAAU,QAAQ,CAAC,CAAC;AAChF,MAAI,WAAW;AACf,aAAW,MAAM,WAAW;AAC1B,QAAI,GAAG,QAAQ,YAAY,MAAM,SAAS;AACxC,iBAAW;AACX;AAAA,IACF;AACA,UAAM,MAAM,GAAG,WAAW,OAAO,GAAG,WAAW,QAAQ;AACvD,QAAI,cAAc,KAAK,GAAG,GAAG;AAC3B,iBAAW;AACX;AAAA,IACF;AAAA,EACF;AAGA,MAAI,CAAC,UAAU;AACb,UAAM,QAAQ,gBAAiD,MAAM,MAAM;AAC3E,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,OAAO,cAAc,KAAK,KAAK,GAAG,GAAG;AAC5C,mBAAW;AACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,WAAW,KAAK,GAAG,EAAE,KAAK;AAC5C,QAAM,YAAY,YAAY,UAAU,MAAM,KAAK,EAAE,SAAS;AAE9D,MAAI,UAAU;AACd,aAAW,MAAM,eAAe;AAC9B,QAAI,GAAG,KAAK,SAAS,GAAG;AACtB,gBAAU;AACV;AAAA,IACF;AAAA,EACF;AAGA,MAAI,qBAAqB;AACzB,mBAAiB,MAAM,CAAC,MAAM;AAC5B,QAAI,mBAAoB,QAAO;AAC/B,QAAI,EAAE,SAAS,YAAa;AAC5B,UAAM,OAAO,iBAAiB,CAAC,EAAE,KAAK;AACtC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,IAAI,KAAK,MAAM,mBAAmB;AACxC,QAAI,CAAC,EAAG,QAAO;AAIf,UAAM,QAAQ,KAAK,MAAM,KAAK,EAAE;AAChC,QAAI,SAAS,EAAG,sBAAqB;AACrC,WAAO;AAAA,EACT,CAAC;AAED,SAAO;AAAA,IACL,UAAU,aAAa;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAIA,IAAM,qBAAqB,CAAC,SAAS,iBAAiB,YAAY,WAAW;AAQ7E,SAAS,2BAA2B,SAAwC;AAC1E,QAAM,QAAQ,oBAAI,IAAY;AAC9B,MAAI,mBAAmB;AAEvB,MAAI,QAAQ,UAAU;AACpB,uBAAmB;AACnB,UAAM,IAAI,kBAAkB;AAC5B,UAAM,IAAI,aAAa;AACvB,UAAM,IAAI,cAAc;AACxB,QAAI,QAAQ,cAAc,EAAG,OAAM,IAAI,WAAW;AAAA,EACpD;AAEA,MAAI,QAAQ,UAAU;AACpB,uBAAmB;AACnB,UAAM,IAAI,kBAAkB;AAC5B,QAAI,QAAQ,cAAe,OAAM,IAAI,gBAAgB;AAAA,EACvD;AAEA,MAAI,QAAQ,eAAe;AACzB,uBAAmB;AACnB,UAAM,IAAI,OAAO;AACjB,UAAM,IAAI,WAAW;AACrB,UAAM,IAAI,gBAAgB;AAAA,EAC5B;AAEA,MAAI,QAAQ,SAAS;AACnB,uBAAmB;AACnB,UAAM,IAAI,MAAM;AAAA,EAClB;AAEA,MAAI,QAAQ,UAAU;AACpB,uBAAmB;AACnB,UAAM,IAAI,WAAW;AACrB,UAAM,IAAI,eAAe;AAAA,EAC3B;AAEA,MAAI,QAAQ,SAAS;AACnB,uBAAmB;AACnB,UAAM,IAAI,WAAW;AAAA,EACvB;AAEA,MAAI,QAAQ,oBAAoB;AAC9B,uBAAmB;AACnB,UAAM,IAAI,eAAe;AAAA,EAC3B;AAEA,MAAI,kBAAkB;AAEpB,UAAM,IAAI,OAAO;AACjB,UAAM,IAAI,eAAe;AAAA,EAC3B,OAAO;AACL,eAAW,KAAK,mBAAoB,OAAM,IAAI,CAAC;AAAA,EACjD;AAEA,SAAO,MAAM,KAAK,KAAK;AACzB;AASO,SAAS,2BACd,SACA,UACsB;AACtB,QAAM,UAAU,IAAI,IAAI,2BAA2B,OAAO,CAAC;AAC3D,QAAM,cAAwB,CAAC;AAC/B,QAAM,OAAiB,CAAC;AACxB,aAAW,QAAQ,UAAU;AAC3B,QAAI,QAAQ,IAAI,IAAI,EAAG,aAAY,KAAK,IAAI;AAAA,QACvC,MAAK,KAAK,IAAI;AAAA,EACrB;AACA,SAAO,EAAE,aAAa,KAAK;AAC7B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/recommend/templates.ts"],"sourcesContent":["/**\n * Block template recommendations.\n *\n * Given a slice of markdown block nodes (the body underneath a heading),\n * profile the content for image / video / quote / list / table / date /\n * stat signals, then map that profile to the set of templates that make\n * sense for the block. Used by the editor's template picker to surface a\n * \"Recommended for this block\" section above the full template list.\n */\n\nimport { extractPlainText, findNodesByType, walkMarkdownTree } from '../markdown/utils.js';\nimport type {\n HtmlElement,\n HtmlNode,\n MarkdownBlockNode,\n MarkdownHtmlBlock,\n MarkdownNode,\n} from '../markdown/types.js';\n\nexport interface BlockContentProfile {\n hasImage: boolean;\n imageCount: number;\n hasVideo: boolean;\n hasBlockquote: boolean;\n hasList: boolean;\n hasTable: boolean;\n hasDate: boolean;\n hasNumberHighlight: boolean;\n wordCount: number;\n}\n\nexport interface RecommendationResult {\n recommended: string[];\n rest: string[];\n}\n\nconst EMPTY_PROFILE: BlockContentProfile = Object.freeze({\n hasImage: false,\n imageCount: 0,\n hasVideo: false,\n hasBlockquote: false,\n hasList: false,\n hasTable: false,\n hasDate: false,\n hasNumberHighlight: false,\n wordCount: 0,\n});\n\nconst DATE_PATTERNS: RegExp[] = [\n /\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d{1,2},?\\s+)?\\d{4}\\b/i,\n /\\b\\d{1,2}\\s+(January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{4}\\b/i,\n /\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\\.?\\s+\\d{1,2},?\\s+\\d{4}\\b/i,\n /\\b\\d{4}-\\d{2}-\\d{2}\\b/,\n /\\b\\d{1,2}\\/\\d{1,2}\\/\\d{2,4}\\b/,\n /\\bQ[1-4]\\s+\\d{4}\\b/,\n /\\b\\d{4}s\\b/,\n /\\b\\d{1,2}(st|nd|rd|th)\\s+century\\b/i,\n];\n\n/**\n * Matches a \"highlight number\" — a prominent figure suitable for a Stat\n * Highlight block. Qualifies on any of:\n * - currency prefix: `$2.3M`, `€500`, `¥1,234`\n * - unit suffix: `50%`, `100 years`, `5×`, `2.3M`\n * - standalone large: `1,234`, `5000`, `12,345`\n * Plain small numbers (`42`) intentionally don't qualify.\n */\nconst NUMBER_HIGHLIGHT_RE =\n /(?:[$€£¥]\\s?\\d+(?:[.,]\\d+)*(?:\\s?(?:[MBK]|million|billion|thousand))?|\\d+(?:[.,]\\d+)*\\s?(?:%|‰|x|×|[MBK]|million|billion|thousand|percent|years?|days?|hours?)|\\d{3,}(?:[.,]\\d+)*)/i;\n\nconst VIDEO_HOST_RE = /(youtube\\.com|youtu\\.be|vimeo\\.com|wistia\\.|loom\\.com)/i;\n\nfunction htmlElementsByTag(root: MarkdownNode, tagNames: Set<string>): HtmlElement[] {\n const blocks = findNodesByType<MarkdownHtmlBlock>(root, 'htmlBlock');\n // Inline HTML (htmlInline) also carries htmlChildren; include both.\n const inline = findNodesByType<MarkdownNode>(root, 'htmlInline') as Array<\n MarkdownNode & { htmlChildren?: HtmlNode[] }\n >;\n const out: HtmlElement[] = [];\n const visit = (n: HtmlNode): void => {\n if (n.type === 'htmlElement') {\n if (tagNames.has(n.tagName.toLowerCase())) out.push(n);\n for (const c of n.children) visit(c);\n }\n };\n for (const b of blocks) for (const c of b.htmlChildren) visit(c);\n for (const i of inline) {\n if (Array.isArray(i.htmlChildren)) for (const c of i.htmlChildren) visit(c);\n }\n return out;\n}\n\n/**\n * Walk a slice of block nodes and summarize what kinds of content it\n * contains. Cheap to run — no markdown parsing, just AST traversal.\n */\nexport function profileBlockContents(nodes: MarkdownBlockNode[]): BlockContentProfile {\n if (!nodes || nodes.length === 0) return { ...EMPTY_PROFILE };\n\n let imageCount = 0;\n let hasBlockquote = false;\n let hasList = false;\n let hasTable = false;\n\n const plainParts: string[] = [];\n\n for (const node of nodes) {\n imageCount += findNodesByType(node, 'image').length;\n imageCount += findNodesByType(node, 'imageReference').length;\n if (findNodesByType(node, 'blockquote').length > 0) hasBlockquote = true;\n if (findNodesByType(node, 'list').length > 0) hasList = true;\n if (findNodesByType(node, 'table').length > 0) hasTable = true;\n plainParts.push(extractPlainText(node));\n }\n\n // HTML embeds: <img>, <video>, <iframe src=…video host>\n const root: MarkdownNode = { type: 'document', children: nodes };\n const imgTags = htmlElementsByTag(root, new Set(['img']));\n imageCount += imgTags.length;\n\n const videoTags = htmlElementsByTag(root, new Set(['video', 'iframe', 'source']));\n let hasVideo = false;\n for (const el of videoTags) {\n if (el.tagName.toLowerCase() === 'video') {\n hasVideo = true;\n break;\n }\n const src = el.attributes.src || el.attributes.href || '';\n if (VIDEO_HOST_RE.test(src)) {\n hasVideo = true;\n break;\n }\n }\n\n // Link-only video embeds (markdown link to a known video host).\n if (!hasVideo) {\n const links = findNodesByType<MarkdownNode & { url?: string }>(root, 'link');\n for (const link of links) {\n if (link.url && VIDEO_HOST_RE.test(link.url)) {\n hasVideo = true;\n break;\n }\n }\n }\n\n const plainText = plainParts.join(' ').trim();\n const wordCount = plainText ? plainText.split(/\\s+/).length : 0;\n\n let hasDate = false;\n for (const re of DATE_PATTERNS) {\n if (re.test(plainText)) {\n hasDate = true;\n break;\n }\n }\n\n // Number highlight: a paragraph that's mostly a single prominent figure.\n let hasNumberHighlight = false;\n walkMarkdownTree(root, (n) => {\n if (hasNumberHighlight) return true;\n if (n.type !== 'paragraph') return;\n const text = extractPlainText(n).trim();\n if (!text) return true;\n const m = text.match(NUMBER_HIGHLIGHT_RE);\n if (!m) return true;\n // Paragraph qualifies when its word count is small AND a prominent\n // number occupies a meaningful chunk of it. Keeps \"the company grew\n // 50% last year\" from triggering, but accepts \"$2.3M raised\".\n const words = text.split(/\\s+/).length;\n if (words <= 8) hasNumberHighlight = true;\n return true;\n });\n\n return {\n hasImage: imageCount > 0,\n imageCount,\n hasVideo,\n hasBlockquote,\n hasList,\n hasTable,\n hasDate,\n hasNumberHighlight,\n wordCount,\n };\n}\n\n// ── Recommendation rules ───────────────────────────────────────────\n\nconst UNIVERSAL_DEFAULTS = ['title', 'sectionHeader', 'factCard', 'twoColumn'];\n\n/**\n * Given a content profile, return the canonical template-name list that\n * should appear in the \"Recommended\" bucket. Caller is responsible for\n * preserving deduplication / ordering when intersecting with the full\n * template list (see {@link recommendTemplatesForBlock}).\n */\nfunction recommendedNamesForProfile(profile: BlockContentProfile): string[] {\n const names = new Set<string>();\n let anyContentSignal = false;\n\n if (profile.hasImage) {\n anyContentSignal = true;\n names.add('imageWithCaption');\n names.add('leftFeature');\n names.add('rightFeature');\n if (profile.imageCount >= 2) names.add('photoGrid');\n }\n\n if (profile.hasVideo) {\n anyContentSignal = true;\n names.add('videoWithCaption');\n if (profile.hasBlockquote) names.add('videoPullQuote');\n }\n\n if (profile.hasBlockquote) {\n anyContentSignal = true;\n names.add('quote');\n names.add('pullQuote');\n names.add('fullBleedQuote');\n }\n\n if (profile.hasList) {\n anyContentSignal = true;\n names.add('list');\n }\n\n if (profile.hasTable) {\n anyContentSignal = true;\n names.add('dataTable');\n names.add('comparisonBar');\n }\n\n if (profile.hasDate) {\n anyContentSignal = true;\n names.add('dateEvent');\n }\n\n if (profile.hasNumberHighlight) {\n anyContentSignal = true;\n names.add('statHighlight');\n }\n\n if (anyContentSignal) {\n // Universal pair only — keep recommended focused.\n names.add('title');\n names.add('sectionHeader');\n } else {\n for (const n of UNIVERSAL_DEFAULTS) names.add(n);\n }\n\n return Array.from(names);\n}\n\n/**\n * Deterministic single-template choice for an unannotated heading block,\n * from strong content signals only. Returns `undefined` when no signal is\n * decisive — the caller keeps its structural default (`sectionHeader`).\n *\n * Precedence favors the most specific/structured content: a table beats\n * an image beats a blockquote beats a stat-looking line. `blockIndex`\n * alternates left/right feature composition for single-image blocks so a\n * run of image sections doesn't stack the same layout.\n *\n * Used by `markdownToDoc`'s auto template picking (see its `autoTemplates`\n * option); the multi-candidate `recommendTemplatesForBlock` below serves\n * the editor's template-picker UI instead.\n */\nexport function pickAutoTemplate(profile: BlockContentProfile, blockIndex = 0): string | undefined {\n if (profile.hasTable) return 'dataTable';\n if (profile.imageCount >= 2) return 'photoGrid';\n if (profile.hasImage) return blockIndex % 2 === 0 ? 'leftFeature' : 'rightFeature';\n if (profile.hasBlockquote) return 'quote';\n if (profile.hasNumberHighlight) return 'statHighlight';\n if (profile.hasList) return 'list';\n return undefined;\n}\n\n/**\n * Split a flat list of template names into a \"Recommended\" bucket\n * (matches the active block's content profile) and a \"rest\" bucket\n * (everything else). Both buckets preserve the input ordering of\n * `allNames`, so the visual layout stays predictable as templates are\n * added.\n */\nexport function recommendTemplatesForBlock(\n profile: BlockContentProfile,\n allNames: readonly string[],\n): RecommendationResult {\n const desired = new Set(recommendedNamesForProfile(profile));\n const recommended: string[] = [];\n const rest: string[] = [];\n for (const name of allNames) {\n if (desired.has(name)) recommended.push(name);\n else rest.push(name);\n }\n return { recommended, rest };\n}\n"],"mappings":"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","names":[]}
|