@bendyline/squisq 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
- package/dist/{Doc-BsMkmzH8.d.ts → Doc-BBVGq1_9.d.ts} +94 -15
- package/dist/{ImageEditDoc-DvoeHMP9.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +1 -1
- package/dist/{chunk-4ZFRZBU2.js → chunk-5TYS5V3G.js} +866 -192
- package/dist/chunk-5TYS5V3G.js.map +1 -0
- package/dist/{chunk-7MQGNR76.js → chunk-A4O7GIWE.js} +2 -27
- package/dist/chunk-A4O7GIWE.js.map +1 -0
- package/dist/{chunk-OH43RPTP.js → chunk-BEQODJWV.js} +20 -3
- package/dist/chunk-BEQODJWV.js.map +1 -0
- package/dist/chunk-ET53IIEP.js +1 -0
- package/dist/chunk-ET53IIEP.js.map +1 -0
- package/dist/{chunk-KZNXZ5SM.js → chunk-GPLTCSXO.js} +18 -7
- package/dist/chunk-GPLTCSXO.js.map +1 -0
- package/dist/{chunk-OKJOQXK4.js → chunk-HWVFJAAH.js} +38 -2
- package/dist/chunk-HWVFJAAH.js.map +1 -0
- package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
- package/dist/chunk-IMSCRLLV.js.map +1 -0
- package/dist/{chunk-4AKUQPEE.js → chunk-KKNUBQ6Y.js} +1 -1
- package/dist/chunk-KKNUBQ6Y.js.map +1 -0
- package/dist/{chunk-TVSGLUID.js → chunk-LDQ2HJIX.js} +3 -3
- package/dist/{chunk-RUDYOTA6.js → chunk-LTN6I7QW.js} +56 -1
- package/dist/chunk-LTN6I7QW.js.map +1 -0
- package/dist/{chunk-2OIBZYKP.js → chunk-P3K7NLHF.js} +2 -2
- package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
- package/dist/chunk-T5UK6YOB.js.map +1 -0
- package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
- package/dist/chunk-TQWLI6S2.js.map +1 -0
- package/dist/{chunk-LRCV7N7F.js → chunk-UY7KGQ5R.js} +2 -2
- package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
- package/dist/chunk-VWUFZ6ZG.js.map +1 -0
- package/dist/doc/index.d.ts +4 -4
- package/dist/doc/index.js +24 -9
- package/dist/generate/index.d.ts +2 -80
- package/dist/generate/index.js +2 -7
- package/dist/imageEdit/index.d.ts +4 -4
- package/dist/imageEdit/index.js +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/index.js +41 -26
- package/dist/jsonForm/index.d.ts +42 -1
- package/dist/jsonForm/index.js +9 -1
- package/dist/markdown/index.d.ts +18 -2
- package/dist/markdown/index.js +5 -3
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/index.js +3 -5
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +1 -1
- package/dist/story/index.d.ts +151 -13
- package/dist/story/index.js +24 -9
- package/dist/{themeLibrary-DQQKuRMx.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
- package/dist/timing/index.js +3 -5
- package/dist/transform/index.d.ts +2 -2
- package/dist/transform/index.js +3 -3
- package/dist/versions/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/contentExtractor.test.ts +60 -0
- package/src/__tests__/coverBlock.test.ts +79 -3
- package/src/__tests__/customTemplateSample.test.ts +63 -2
- package/src/__tests__/customTemplatesFrontmatter.test.ts +31 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +27 -0
- package/src/__tests__/inputDescriptors.test.ts +162 -0
- package/src/__tests__/jsonForm.tokens.test.ts +84 -0
- package/src/__tests__/markdown.test.ts +64 -0
- package/src/__tests__/markdownSanitize.test.ts +15 -0
- package/src/__tests__/markdownToDoc.test.ts +173 -2
- package/src/__tests__/mediaAnnotations.test.ts +27 -0
- package/src/__tests__/shapeGeometry.test.ts +37 -0
- package/src/__tests__/structuredData.test.ts +73 -3
- package/src/__tests__/templateMetadata.test.ts +11 -1
- package/src/__tests__/templates.test.ts +112 -0
- package/src/__tests__/validateDoc.test.ts +160 -0
- package/src/doc/annotationBlocks.ts +117 -0
- package/src/doc/customTemplatesFrontmatter.ts +7 -1
- package/src/doc/customThemesFrontmatter.ts +6 -1
- package/src/doc/docToMarkdown.ts +78 -29
- package/src/doc/getLayers.ts +11 -2
- package/src/doc/index.ts +12 -0
- package/src/doc/markdownToDoc.ts +80 -3
- package/src/doc/mediaAnnotations.ts +15 -33
- package/src/doc/standaloneAnnotation.ts +58 -0
- package/src/doc/structuredData.ts +92 -21
- package/src/doc/templates/__tests__/customTemplate.test.ts +21 -2
- package/src/doc/templates/comparisonBar.ts +8 -1
- package/src/doc/templates/coverBlock.ts +78 -15
- package/src/doc/templates/customTemplate.ts +10 -4
- package/src/doc/templates/diagramBlock.ts +2 -2
- package/src/doc/templates/drawingBlock.ts +2 -2
- package/src/doc/templates/factCard.ts +4 -2
- package/src/doc/templates/imageWithCaption.ts +10 -8
- package/src/doc/templates/index.ts +12 -2
- package/src/doc/templates/inputDescriptors.ts +432 -0
- package/src/doc/templates/photoGrid.ts +16 -39
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +267 -1
- package/src/doc/templates/tokens/resolveTokens.ts +0 -0
- package/src/doc/templates/videoWithCaption.ts +15 -14
- package/src/doc/utils/nearest.ts +59 -0
- package/src/doc/utils/shapeGeometry.ts +90 -2
- package/src/doc/validate.ts +153 -54
- package/src/generate/contentExtractor.ts +148 -35
- package/src/generate/index.ts +0 -3
- package/src/jsonForm/index.ts +2 -0
- package/src/jsonForm/tokens.ts +78 -0
- package/src/markdown/annotationCoercion.ts +1 -1
- package/src/markdown/attrTokens.ts +19 -0
- package/src/markdown/convert.ts +11 -1
- package/src/markdown/index.ts +2 -1
- package/src/markdown/sanitize.ts +22 -2
- package/src/markdown/stringify.ts +12 -3
- package/src/markdown/utils.ts +62 -1
- package/src/schemas/CustomTemplates.ts +56 -4
- package/src/schemas/Doc.ts +39 -41
- package/src/schemas/themeCompile.ts +1 -1
- package/src/storage/ContentContainer.ts +6 -2
- package/src/transform/types.ts +1 -1
- package/dist/chunk-2TMAWB4Y.js.map +0 -1
- package/dist/chunk-3E5F2XMR.js.map +0 -1
- package/dist/chunk-4AKUQPEE.js.map +0 -1
- package/dist/chunk-4X3JQXNM.js +0 -35
- package/dist/chunk-4X3JQXNM.js.map +0 -1
- package/dist/chunk-4ZFRZBU2.js.map +0 -1
- package/dist/chunk-7MQGNR76.js.map +0 -1
- package/dist/chunk-FBKP5CAO.js +0 -145
- package/dist/chunk-FBKP5CAO.js.map +0 -1
- package/dist/chunk-FR2RBTKO.js.map +0 -1
- package/dist/chunk-K32VJONL.js.map +0 -1
- package/dist/chunk-KZNXZ5SM.js.map +0 -1
- package/dist/chunk-OH43RPTP.js.map +0 -1
- package/dist/chunk-OKJOQXK4.js.map +0 -1
- package/dist/chunk-RUDYOTA6.js.map +0 -1
- package/src/__tests__/slideshowGenerator.test.ts +0 -128
- package/src/generate/slideshowGenerator.ts +0 -280
- /package/dist/{chunk-TVSGLUID.js.map → chunk-LDQ2HJIX.js.map} +0 -0
- /package/dist/{chunk-2OIBZYKP.js.map → chunk-P3K7NLHF.js.map} +0 -0
- /package/dist/{chunk-LRCV7N7F.js.map → chunk-UY7KGQ5R.js.map} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { validateMarkdownSource } from '../doc/validate.js';
|
|
3
|
+
import { writeCustomTemplatesToFrontmatter } from '../doc/customTemplatesFrontmatter';
|
|
4
|
+
import type { CustomTemplateDefinition } from '../schemas/CustomTemplates.js';
|
|
3
5
|
|
|
4
6
|
function codes(md: string, options?: Parameters<typeof validateMarkdownSource>[1]): string[] {
|
|
5
7
|
return validateMarkdownSource(md, options).diagnostics.map((d) => d.code);
|
|
@@ -11,6 +13,7 @@ describe('validateMarkdownSource — templates', () => {
|
|
|
11
13
|
expect(result.diagnostics).toEqual([]);
|
|
12
14
|
expect(result.errorCount).toBe(0);
|
|
13
15
|
expect(result.warningCount).toBe(0);
|
|
16
|
+
expect(result.infoCount).toBe(0);
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
it('flags unknown templates with a did-you-mean suggestion', () => {
|
|
@@ -71,6 +74,43 @@ describe('validateMarkdownSource — attributes and structure', () => {
|
|
|
71
74
|
});
|
|
72
75
|
});
|
|
73
76
|
|
|
77
|
+
describe('validateMarkdownSource — conflicting annotation keys', () => {
|
|
78
|
+
it('flags a block-meta key set differently in both {[…]} and {…}', () => {
|
|
79
|
+
const result = validateMarkdownSource('## X {[sectionHeader duration=8]} {#x duration=10}\n');
|
|
80
|
+
const d = result.diagnostics.find((x) => x.code === 'conflicting-annotation-key');
|
|
81
|
+
expect(d).toBeDefined();
|
|
82
|
+
expect(d!.severity).toBe('info');
|
|
83
|
+
expect(d!.message).toBe(
|
|
84
|
+
'"duration" is set in both {[…]} (8) and {…} (10) — the {…} value wins.',
|
|
85
|
+
);
|
|
86
|
+
expect(d!.blockId).toBe('x');
|
|
87
|
+
expect(d!.line).toBe(1);
|
|
88
|
+
expect(result.infoCount).toBe(1);
|
|
89
|
+
expect(result.errorCount).toBe(0);
|
|
90
|
+
expect(result.warningCount).toBe(0);
|
|
91
|
+
// Precedence itself is unchanged: the Pandoc value is in effect.
|
|
92
|
+
expect(result.doc.blocks[0].duration).toBe(10);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('stays silent when both forms agree', () => {
|
|
96
|
+
expect(codes('## X {[sectionHeader duration=8]} {#x duration=8}\n')).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('ignores non-block-meta keys (they land in different channels)', () => {
|
|
100
|
+
// `style` goes to templateOverrides ({[…]}) and metadata ({…}) — no conflict.
|
|
101
|
+
expect(codes('## X {[factCard style=minimal]} {#x style=bold}\n')).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('reports each conflicting key separately', () => {
|
|
105
|
+
const result = validateMarkdownSource(
|
|
106
|
+
'## X {[sectionHeader duration=8 startTime=1]} {#x duration=10 startTime=2}\n',
|
|
107
|
+
);
|
|
108
|
+
const found = result.diagnostics.filter((d) => d.code === 'conflicting-annotation-key');
|
|
109
|
+
expect(found).toHaveLength(2);
|
|
110
|
+
expect(result.infoCount).toBe(2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
74
114
|
describe('validateMarkdownSource — assets', () => {
|
|
75
115
|
it('flags relative image references missing from the asset set', () => {
|
|
76
116
|
const result = validateMarkdownSource('## X\n\n\n', {
|
|
@@ -98,3 +138,123 @@ describe('validateMarkdownSource — assets', () => {
|
|
|
98
138
|
expect(codes('## X\n\n\n')).toEqual([]);
|
|
99
139
|
});
|
|
100
140
|
});
|
|
141
|
+
|
|
142
|
+
describe('validateMarkdownSource — template inputs', () => {
|
|
143
|
+
it('flags an unknown input with a did-you-mean suggestion', () => {
|
|
144
|
+
const result = validateMarkdownSource('## Where {[map centre="47.6,-122.3"]}');
|
|
145
|
+
const d = result.diagnostics.find((x) => x.code === 'unknown-input');
|
|
146
|
+
expect(d).toBeDefined();
|
|
147
|
+
expect(d!.severity).toBe('warning');
|
|
148
|
+
expect(d!.message).toContain('centre');
|
|
149
|
+
expect(d!.message).toContain('Did you mean "center"?');
|
|
150
|
+
expect(d!.line).toBe(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('flags a value outside a closed enum', () => {
|
|
154
|
+
const result = validateMarkdownSource('## Where {[map center="1,2" mapStyle="nope"]}');
|
|
155
|
+
const d = result.diagnostics.find((x) => x.code === 'invalid-input-value');
|
|
156
|
+
expect(d).toBeDefined();
|
|
157
|
+
expect(d!.message).toContain('mapStyle');
|
|
158
|
+
expect(d!.message).toContain('terrain');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('flags a missing required input', () => {
|
|
162
|
+
const codesFor = codes('## Compare {[twoColumn]}');
|
|
163
|
+
expect(codesFor.filter((c) => c === 'missing-input')).toEqual([
|
|
164
|
+
'missing-input',
|
|
165
|
+
'missing-input',
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not fire missing-input when the value is derivable from the body', () => {
|
|
170
|
+
const md = '## Caption {[imageWithCaption]}\n\n\n';
|
|
171
|
+
// imageSrc is required, but derivable from the body image → no warning.
|
|
172
|
+
expect(codes(md)).not.toContain('missing-input');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('does fire missing-input when the value is not derivable and absent', () => {
|
|
176
|
+
// No body image → imageSrc is neither present nor derivable.
|
|
177
|
+
expect(codes('## Caption {[imageWithCaption]}')).toContain('missing-input');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('does not lint blocks that resolve to a doc-declared custom template', () => {
|
|
181
|
+
// A custom template that shadows the built-in "map" name carries its own
|
|
182
|
+
// arbitrary inputs — the built-in descriptors must not be applied to it.
|
|
183
|
+
const custom: CustomTemplateDefinition = {
|
|
184
|
+
name: 'map',
|
|
185
|
+
label: 'My Map',
|
|
186
|
+
description: 'custom',
|
|
187
|
+
viewport: { width: 1920, height: 1080 },
|
|
188
|
+
layers: [
|
|
189
|
+
{
|
|
190
|
+
id: 't',
|
|
191
|
+
type: 'text',
|
|
192
|
+
position: { x: '5%', y: '10%' },
|
|
193
|
+
content: { text: '{title}', style: { fontSize: 40, color: '#000' } },
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
const encoded = writeCustomTemplatesToFrontmatter([custom])!;
|
|
198
|
+
const md = `---\nsquisq-custom-templates: ${encoded}\n---\n\n## Hi {[map foo="bar"]}\n`;
|
|
199
|
+
const codesFor = validateMarkdownSource(md).diagnostics.map((d) => d.code);
|
|
200
|
+
expect(codesFor).not.toContain('unknown-input');
|
|
201
|
+
expect(codesFor).not.toContain('missing-input');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does not lint templates without descriptors', () => {
|
|
205
|
+
// sectionHeader has no input descriptors → arbitrary keys are not flagged.
|
|
206
|
+
expect(codes('## Break {[sectionHeader whatever=5]}')).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('leaves a clean, fully-specified block with no findings', () => {
|
|
210
|
+
const md = '## Where {[map center="47.6,-122.3" zoom=9 mapStyle="terrain"]}';
|
|
211
|
+
expect(codes(md)).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('validateMarkdownSource — standalone annotations (S2)', () => {
|
|
216
|
+
it('does not flag a valid standalone template annotation as unparsed', () => {
|
|
217
|
+
const result = validateMarkdownSource('# H\n\n{[quote]}\n\nquoted\n');
|
|
218
|
+
expect(result.diagnostics.find((d) => d.code === 'unparsed-annotation')).toBeUndefined();
|
|
219
|
+
expect(result.diagnostics.find((d) => d.code === 'unknown-template')).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('reports unknown-template for a standalone annotation with a bad name', () => {
|
|
223
|
+
const result = validateMarkdownSource('# H\n\n{[quotte]}\n\nquoted\n');
|
|
224
|
+
const d = result.diagnostics.find((x) => x.code === 'unknown-template');
|
|
225
|
+
expect(d).toBeDefined();
|
|
226
|
+
expect(d!.message).toContain('quotte');
|
|
227
|
+
// Not double-reported as an unparsed annotation.
|
|
228
|
+
expect(result.diagnostics.find((x) => x.code === 'unparsed-annotation')).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('still flags {[…]} nested inside a list (not a standalone block)', () => {
|
|
232
|
+
const result = validateMarkdownSource('## X\n\n- {[imageWithCaption src=x.jpg]}\n');
|
|
233
|
+
const d = result.diagnostics.find((x) => x.code === 'unparsed-annotation');
|
|
234
|
+
expect(d).toBeDefined();
|
|
235
|
+
expect(d!.message).toContain('standalone paragraphs');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('validateMarkdownSource — possible-data-fence (S3)', () => {
|
|
240
|
+
const info = (md: string) =>
|
|
241
|
+
validateMarkdownSource(md).diagnostics.filter((d) => d.code === 'possible-data-fence');
|
|
242
|
+
|
|
243
|
+
it('nudges an unmarked json fence inside a template-annotated block', () => {
|
|
244
|
+
const md = '## Numbers {[dataTable]}\n\n```json\n{ "headers": ["A"], "rows": [] }\n```\n';
|
|
245
|
+
const d = info(md);
|
|
246
|
+
expect(d).toHaveLength(1);
|
|
247
|
+
expect(d[0].severity).toBe('info');
|
|
248
|
+
expect(d[0].message).toContain('```json data');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('does not fire for a code sample in a non-templated prose section', () => {
|
|
252
|
+
const md = '## Notes\n\n```json\n{ "a": 1 }\n```\n';
|
|
253
|
+
expect(info(md)).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('does not fire when the fence already has the data marker', () => {
|
|
257
|
+
const md = '## Numbers {[dataTable]}\n\n```json data\n{ "headers": ["A"], "rows": [] }\n```\n';
|
|
258
|
+
expect(info(md)).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone template-annotation block extraction.
|
|
3
|
+
*
|
|
4
|
+
* A paragraph whose entire trimmed text is exactly `{[templateName key=value …]}`
|
|
5
|
+
* — where `templateName` is not a media name (media annotations are extracted
|
|
6
|
+
* first, into typed clips) — becomes a *heading-less template block*. The body
|
|
7
|
+
* nodes that follow it, up to the next heading or the next standalone
|
|
8
|
+
* annotation, become that block's `contents`.
|
|
9
|
+
*
|
|
10
|
+
* This is the block analog of {@link import('./mediaAnnotations.js').extractMediaFromContents}:
|
|
11
|
+
* both walk a block's body `contents` and lift standalone `{[…]}` paragraphs
|
|
12
|
+
* out, sharing the {@link parseStandaloneAnnotation} parser. Runs AFTER media
|
|
13
|
+
* extraction (so media names keep their meaning) and BEFORE structured-data
|
|
14
|
+
* parsing (so `json data` / `yaml data` fences attach to the new blocks).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Block } from '../schemas/Doc.js';
|
|
18
|
+
import type { MarkdownBlockNode } from '../markdown/types.js';
|
|
19
|
+
import { coerceAnnotationValues, type CoercedBlockMeta } from '../markdown/annotationCoercion.js';
|
|
20
|
+
import { resolveTemplateName } from './templates/index.js';
|
|
21
|
+
import { isMediaAnnotationName } from './mediaAnnotations.js';
|
|
22
|
+
import { parseStandaloneAnnotation, type ParsedAnnotation } from './standaloneAnnotation.js';
|
|
23
|
+
|
|
24
|
+
/** Configuration for {@link extractTemplateBlocksFromContents}. */
|
|
25
|
+
export interface StandaloneBlockOptions {
|
|
26
|
+
/** Produce a unique block id for a parsed standalone annotation. */
|
|
27
|
+
makeId: (parsed: ParsedAnnotation) => string;
|
|
28
|
+
/** Initial `duration` for a produced block (the timing pass overrides it). */
|
|
29
|
+
defaultDuration: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The split produced by {@link extractTemplateBlocksFromContents}. */
|
|
33
|
+
export interface TemplateBlockExtraction {
|
|
34
|
+
/** Contents before the first standalone template annotation (stay on the
|
|
35
|
+
* containing block). */
|
|
36
|
+
leading: MarkdownBlockNode[];
|
|
37
|
+
/** Heading-less blocks, each carrying the body nodes that followed its
|
|
38
|
+
* annotation up to the next annotation / end of contents. */
|
|
39
|
+
blocks: Block[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply coerced block-meta values onto a produced block. Mirrors
|
|
44
|
+
* `markdownToDoc`'s `applyBlockMeta` so a standalone `{[quote duration=8]}`
|
|
45
|
+
* honors the same block-meta keys a heading `{[…]}` annotation would.
|
|
46
|
+
*/
|
|
47
|
+
function applyBlockMeta(block: Block, m: CoercedBlockMeta): void {
|
|
48
|
+
if (m.x != null) block.x = m.x;
|
|
49
|
+
if (m.y != null) block.y = m.y;
|
|
50
|
+
if (m.startTime != null) block.startTime = m.startTime;
|
|
51
|
+
if (m.duration != null) block.duration = m.duration;
|
|
52
|
+
if (m.connectsTo) block.connectsTo = m.connectsTo;
|
|
53
|
+
if (m.transition) block.transition = m.transition;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a heading-less block from a parsed standalone template annotation. */
|
|
57
|
+
function makeStandaloneBlock(parsed: ParsedAnnotation, options: StandaloneBlockOptions): Block {
|
|
58
|
+
// `parsed.template` is guaranteed defined by the caller's guard.
|
|
59
|
+
const rawTemplate = parsed.template as string;
|
|
60
|
+
const params = parsed.params;
|
|
61
|
+
const hasParams = Object.keys(params).length > 0;
|
|
62
|
+
|
|
63
|
+
const block: Block = {
|
|
64
|
+
id: options.makeId(parsed),
|
|
65
|
+
startTime: 0,
|
|
66
|
+
duration: options.defaultDuration,
|
|
67
|
+
audioSegment: 0,
|
|
68
|
+
template: resolveTemplateName(rawTemplate),
|
|
69
|
+
standaloneAnnotation: true,
|
|
70
|
+
// Keep the raw (un-resolved) name so validation can report the exact
|
|
71
|
+
// requested name — symmetric with `sourceHeading.templateAnnotation`.
|
|
72
|
+
sourceAnnotation: { template: rawTemplate, ...(hasParams ? { params } : {}) },
|
|
73
|
+
// Only carry a display title when the author supplied one.
|
|
74
|
+
...(params.title ? { title: params.title } : {}),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (hasParams) {
|
|
78
|
+
block.templateOverrides = params;
|
|
79
|
+
applyBlockMeta(block, coerceAnnotationValues(params).blockMeta);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return block;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Walk `contents`, lifting standalone template-annotation paragraphs into
|
|
87
|
+
* heading-less blocks. Nodes after an annotation (until the next annotation
|
|
88
|
+
* or the end of `contents`) attach to that block. Media annotations are left
|
|
89
|
+
* in place — they are extracted separately, before this runs. `contents` is
|
|
90
|
+
* not mutated; the split is returned.
|
|
91
|
+
*/
|
|
92
|
+
export function extractTemplateBlocksFromContents(
|
|
93
|
+
contents: MarkdownBlockNode[] | undefined,
|
|
94
|
+
options: StandaloneBlockOptions,
|
|
95
|
+
): TemplateBlockExtraction {
|
|
96
|
+
const leading: MarkdownBlockNode[] = [];
|
|
97
|
+
const blocks: Block[] = [];
|
|
98
|
+
let current: Block | null = null;
|
|
99
|
+
|
|
100
|
+
for (const node of contents ?? []) {
|
|
101
|
+
const parsed = parseStandaloneAnnotation(node);
|
|
102
|
+
const isTemplateAnnotation =
|
|
103
|
+
parsed != null && parsed.template != null && !isMediaAnnotationName(parsed.template);
|
|
104
|
+
|
|
105
|
+
if (isTemplateAnnotation) {
|
|
106
|
+
const block = makeStandaloneBlock(parsed as ParsedAnnotation, options);
|
|
107
|
+
blocks.push(block);
|
|
108
|
+
current = block;
|
|
109
|
+
} else if (current) {
|
|
110
|
+
(current.contents ??= []).push(node);
|
|
111
|
+
} else {
|
|
112
|
+
leading.push(node);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { leading, blocks };
|
|
117
|
+
}
|
|
@@ -225,6 +225,7 @@ export function readCustomTemplatesFromFrontmatter(
|
|
|
225
225
|
*/
|
|
226
226
|
export function writeCustomTemplatesToFrontmatter(
|
|
227
227
|
templates: readonly CustomTemplateDefinition[] | undefined,
|
|
228
|
+
options?: { pretty?: boolean },
|
|
228
229
|
): string | undefined {
|
|
229
230
|
if (!templates || templates.length === 0) return undefined;
|
|
230
231
|
const map: Record<string, Record<string, unknown>> = {};
|
|
@@ -235,7 +236,12 @@ export function writeCustomTemplatesToFrontmatter(
|
|
|
235
236
|
entry.ly = renameKeys(def.layers, LONG_TO_SHORT);
|
|
236
237
|
map[def.name] = entry;
|
|
237
238
|
}
|
|
238
|
-
|
|
239
|
+
// Pretty output is multi-line, so it rides out as a YAML literal block
|
|
240
|
+
// scalar (see `formatBlockScalar` / stringify's frontmatter path) and
|
|
241
|
+
// parses back via `JSON.parse`. Default stays compact (single line) so
|
|
242
|
+
// existing docs and snapshots are byte-unchanged. The compact key codec
|
|
243
|
+
// (LONG_TO_SHORT) is applied in both modes.
|
|
244
|
+
return JSON.stringify(map, null, options?.pretty ? 2 : undefined);
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
function readViewport(raw: unknown): { width: number; height: number } {
|
|
@@ -82,9 +82,14 @@ export function readCustomThemesFromFrontmatter(
|
|
|
82
82
|
*/
|
|
83
83
|
export function writeCustomThemesToFrontmatter(
|
|
84
84
|
themes: readonly Theme[] | undefined,
|
|
85
|
+
options?: { pretty?: boolean },
|
|
85
86
|
): string | undefined {
|
|
86
87
|
if (!themes || themes.length === 0) return undefined;
|
|
87
88
|
const map: Record<string, Theme> = {};
|
|
88
89
|
for (const theme of themes) map[theme.id] = theme;
|
|
89
|
-
|
|
90
|
+
// Pretty output is multi-line, so it serializes as a YAML literal block
|
|
91
|
+
// scalar (see the markdown stringify frontmatter path) and reads back via
|
|
92
|
+
// `JSON.parse`. Default stays compact (single line) so existing docs and
|
|
93
|
+
// snapshots are byte-unchanged.
|
|
94
|
+
return JSON.stringify(map, null, options?.pretty ? 2 : undefined);
|
|
90
95
|
}
|
package/src/doc/docToMarkdown.ts
CHANGED
|
@@ -23,7 +23,13 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import type { Doc, Block } from '../schemas/Doc.js';
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
MarkdownDocument,
|
|
28
|
+
MarkdownBlockNode,
|
|
29
|
+
MarkdownHeading,
|
|
30
|
+
MarkdownParagraph,
|
|
31
|
+
} from '../markdown/types.js';
|
|
32
|
+
import { serializeAnnotation } from '../markdown/attrTokens.js';
|
|
27
33
|
import {
|
|
28
34
|
FRONTMATTER_CUSTOM_TEMPLATES_KEY,
|
|
29
35
|
writeCustomTemplatesToFrontmatter,
|
|
@@ -33,6 +39,8 @@ import {
|
|
|
33
39
|
writeCustomThemesToFrontmatter,
|
|
34
40
|
} from './customThemesFrontmatter.js';
|
|
35
41
|
|
|
42
|
+
const TRANSITION_PARAM_KEYS = ['transition', 'transitionDuration', 'transitionDirection'] as const;
|
|
43
|
+
|
|
36
44
|
/**
|
|
37
45
|
* Convert a Doc with heading-driven blocks back to a MarkdownDocument.
|
|
38
46
|
*
|
|
@@ -55,6 +63,12 @@ export function docToMarkdown(doc: Doc): MarkdownDocument {
|
|
|
55
63
|
if (block.sourceHeading) {
|
|
56
64
|
const heading = ensureAnnotation(block, block.sourceHeading);
|
|
57
65
|
children.push(heading);
|
|
66
|
+
} else if (block.standaloneAnnotation) {
|
|
67
|
+
// Heading-less standalone block: re-emit its `{[…]}` annotation as a
|
|
68
|
+
// paragraph before its contents. The serializer's quoting is paired
|
|
69
|
+
// with the tokenizer, and stringifyMarkdown un-escapes `{[…]}` spans,
|
|
70
|
+
// so the annotation round-trips.
|
|
71
|
+
children.push(synthesizeAnnotationParagraph(block));
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
// Emit body content
|
|
@@ -107,61 +121,96 @@ export function docToMarkdown(doc: Doc): MarkdownDocument {
|
|
|
107
121
|
};
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Build the synthesized annotation paragraph for a heading-less standalone
|
|
126
|
+
* block: `{[templateName key=value …]}` from its `sourceAnnotation.template`
|
|
127
|
+
* and current `templateOverrides` (edits round-trip through the latter).
|
|
128
|
+
*/
|
|
129
|
+
function synthesizeAnnotationParagraph(block: Block): MarkdownParagraph {
|
|
130
|
+
const text = serializeAnnotation(block.sourceAnnotation?.template, block.templateOverrides);
|
|
131
|
+
return { type: 'paragraph', children: [{ type: 'text', value: text }] };
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
/**
|
|
111
135
|
* Ensure the heading's `templateAnnotation` reflects the block's
|
|
112
136
|
* template and templateOverrides. Returns a (possibly cloned) heading.
|
|
113
137
|
*/
|
|
114
138
|
function ensureAnnotation(block: Block, heading: MarkdownHeading): MarkdownHeading {
|
|
115
|
-
const
|
|
139
|
+
const { attributes, transitionParams } = ensureTransitionMetadata(block, heading);
|
|
116
140
|
|
|
117
141
|
// If the block has a non-default template or overrides, inject an annotation
|
|
118
142
|
const hasExplicitTemplate =
|
|
119
143
|
block.template && block.template !== 'sectionHeader' && block.autoTemplate !== true;
|
|
120
144
|
const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
|
|
121
145
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
let templateAnnotation = heading.templateAnnotation;
|
|
147
|
+
if (!templateAnnotation && (hasExplicitTemplate || hasOverrides || transitionParams)) {
|
|
148
|
+
templateAnnotation = {
|
|
149
|
+
...(hasExplicitTemplate ? { template: block.template ?? 'sectionHeader' } : {}),
|
|
150
|
+
...(hasOverrides ? { params: block.templateOverrides } : {}),
|
|
151
|
+
};
|
|
126
152
|
}
|
|
127
153
|
|
|
128
|
-
if (
|
|
129
|
-
|
|
154
|
+
if (transitionParams) {
|
|
155
|
+
templateAnnotation = {
|
|
156
|
+
...(templateAnnotation ?? {}),
|
|
157
|
+
params: {
|
|
158
|
+
...(templateAnnotation?.params ?? {}),
|
|
159
|
+
...transitionParams,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
130
162
|
}
|
|
131
163
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
templateAnnotation: {
|
|
138
|
-
template: block.template ?? 'sectionHeader',
|
|
139
|
-
...(hasOverrides ? { params: block.templateOverrides } : {}),
|
|
140
|
-
},
|
|
141
|
-
};
|
|
164
|
+
if (attributes === heading.attributes && templateAnnotation === heading.templateAnnotation) {
|
|
165
|
+
return heading;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { ...heading, children: [...heading.children], attributes, templateAnnotation };
|
|
142
169
|
}
|
|
143
170
|
|
|
144
|
-
function
|
|
171
|
+
function ensureTransitionMetadata(
|
|
145
172
|
block: Block,
|
|
146
173
|
heading: MarkdownHeading,
|
|
147
|
-
):
|
|
174
|
+
): {
|
|
175
|
+
attributes: MarkdownHeading['attributes'];
|
|
176
|
+
transitionParams: Record<string, string> | undefined;
|
|
177
|
+
} {
|
|
148
178
|
const transition = block.transition;
|
|
149
|
-
if (!transition)
|
|
179
|
+
if (!transition) {
|
|
180
|
+
return { attributes: heading.attributes, transitionParams: undefined };
|
|
181
|
+
}
|
|
150
182
|
|
|
151
|
-
|
|
152
|
-
|
|
183
|
+
// The transition is written to the squisq-native `{[…]}` params for
|
|
184
|
+
// author-facing markdown, and to `attributes.blockMeta` for the in-memory
|
|
185
|
+
// Doc → MarkdownDocument → Doc path (which does not re-parse string params).
|
|
186
|
+
const attrs = removeTransitionParams(heading.attributes) ?? {};
|
|
187
|
+
const attributes = {
|
|
153
188
|
...attrs,
|
|
154
|
-
|
|
155
|
-
...(attrs.
|
|
189
|
+
blockMeta: {
|
|
190
|
+
...(attrs.blockMeta ?? {}),
|
|
191
|
+
transition,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
attributes,
|
|
196
|
+
transitionParams: {
|
|
156
197
|
transition: transition.type,
|
|
157
198
|
...(transition.duration !== undefined
|
|
158
199
|
? { transitionDuration: String(transition.duration) }
|
|
159
200
|
: {}),
|
|
160
201
|
...(transition.direction ? { transitionDirection: transition.direction } : {}),
|
|
161
202
|
},
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function removeTransitionParams(
|
|
207
|
+
attrs: MarkdownHeading['attributes'],
|
|
208
|
+
): MarkdownHeading['attributes'] {
|
|
209
|
+
if (!attrs?.params) return attrs;
|
|
210
|
+
const params = { ...attrs.params };
|
|
211
|
+
for (const key of TRANSITION_PARAM_KEYS) delete params[key];
|
|
212
|
+
return {
|
|
213
|
+
...attrs,
|
|
214
|
+
...(Object.keys(params).length > 0 ? { params } : { params: undefined }),
|
|
166
215
|
};
|
|
167
216
|
}
|
package/src/doc/getLayers.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
|
|
|
35
35
|
import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
|
|
36
36
|
import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
|
|
37
37
|
import { templateRegistry, resolveTemplateName } from './templates/index.js';
|
|
38
|
+
import { coerceTemplateParams } from './templates/inputDescriptors.js';
|
|
38
39
|
import { expandPersistentLayers, wrapWithPersistentLayers } from './templates/persistentLayers.js';
|
|
39
40
|
import { applyRenderStyleToLayers } from './utils/applyRenderStyle.js';
|
|
40
41
|
import { fallbackBlockLayers } from './templates/fallbackBlock.js';
|
|
@@ -121,11 +122,19 @@ export function getLayers(block: DocBlock, context: RenderContext = {}): Layer[]
|
|
|
121
122
|
}
|
|
122
123
|
// Effective template input: the block's own fields, then structured
|
|
123
124
|
// body data (```json data fences, GFM tables), then `{[…]}` string
|
|
124
|
-
// overrides — the same merge order buildPreviewDoc uses.
|
|
125
|
+
// overrides — the same merge order buildPreviewDoc uses. Inline overrides
|
|
126
|
+
// are coerced to their typed shape (`center="47.6,-122.3"` → `{lat,lng}`,
|
|
127
|
+
// `zoom=9` → `9`) so pure-inline annotations render without any change to
|
|
128
|
+
// the template functions. `block.templateOverrides` itself stays raw
|
|
129
|
+
// strings for lossless round-tripping — only this ephemeral input is coerced.
|
|
125
130
|
const { templateData, templateOverrides } = block as Block;
|
|
126
131
|
const input =
|
|
127
132
|
templateData || templateOverrides
|
|
128
|
-
? ({
|
|
133
|
+
? ({
|
|
134
|
+
...block,
|
|
135
|
+
...templateData,
|
|
136
|
+
...coerceTemplateParams(block.template, templateOverrides ?? {}).input,
|
|
137
|
+
} as TemplateBlock)
|
|
129
138
|
: block;
|
|
130
139
|
let layers: Layer[];
|
|
131
140
|
try {
|
package/src/doc/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export { resolveAudioMapping, scoreTextSimilarity } from './audioMapping.js';
|
|
|
33
33
|
export {
|
|
34
34
|
isDataFence,
|
|
35
35
|
parseDataFence,
|
|
36
|
+
replaceDataFence,
|
|
36
37
|
parseYamlSubset,
|
|
37
38
|
findFirstTable,
|
|
38
39
|
extractTableData,
|
|
@@ -41,3 +42,14 @@ export type { DataFenceParseResult, ExtractedTableData } from './structuredData.
|
|
|
41
42
|
export { validateMarkdownSource, validateMarkdownDoc } from './validate.js';
|
|
42
43
|
export type { ValidateOptions, MarkdownValidationResult } from './validate.js';
|
|
43
44
|
export { fallbackBlockLayers } from './templates/fallbackBlock.js';
|
|
45
|
+
export {
|
|
46
|
+
BASE_INPUT_DESCRIPTORS,
|
|
47
|
+
TEMPLATE_INPUT_DESCRIPTORS,
|
|
48
|
+
coerceTemplateParams,
|
|
49
|
+
lintTemplateParams,
|
|
50
|
+
} from './templates/inputDescriptors.js';
|
|
51
|
+
export type {
|
|
52
|
+
InputCoercion,
|
|
53
|
+
TemplateInputDescriptor,
|
|
54
|
+
TemplateParamFinding,
|
|
55
|
+
} from './templates/inputDescriptors.js';
|