@bendyline/squisq 1.2.0 → 1.2.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.
Files changed (58) hide show
  1. package/dist/{Doc-Dwn5aHFC.d.ts → Doc-BfBhiCNr.d.ts} +2 -2
  2. package/dist/__tests__/coverBlock.test.d.ts +2 -0
  3. package/dist/__tests__/coverBlock.test.d.ts.map +1 -0
  4. package/dist/__tests__/coverBlock.test.js +197 -0
  5. package/dist/__tests__/coverBlock.test.js.map +1 -0
  6. package/dist/__tests__/markdown.test.js +21 -0
  7. package/dist/__tests__/markdown.test.js.map +1 -1
  8. package/dist/__tests__/markdownToDoc.test.js +59 -0
  9. package/dist/__tests__/markdownToDoc.test.js.map +1 -1
  10. package/dist/{chunk-TVPKD76V.js → chunk-53TZ2UTL.js} +2 -3
  11. package/dist/chunk-53TZ2UTL.js.map +1 -0
  12. package/dist/{chunk-QWVRN6I4.js → chunk-7GF6RDED.js} +3 -2
  13. package/dist/chunk-7GF6RDED.js.map +1 -0
  14. package/dist/{chunk-4CRBS35L.js → chunk-CYQHG27J.js} +7 -2
  15. package/dist/chunk-CYQHG27J.js.map +1 -0
  16. package/dist/{chunk-T6WBC3ND.js → chunk-GAQLMORN.js} +105 -49
  17. package/dist/chunk-GAQLMORN.js.map +1 -0
  18. package/dist/{chunk-NJLL5XYC.js → chunk-LAZELBON.js} +2 -2
  19. package/dist/{chunk-KEFWM2VC.js → chunk-XM4WSOHB.js} +1 -1
  20. package/dist/chunk-XM4WSOHB.js.map +1 -0
  21. package/dist/doc/markdownToDoc.d.ts +7 -0
  22. package/dist/doc/markdownToDoc.d.ts.map +1 -1
  23. package/dist/doc/markdownToDoc.js +49 -1
  24. package/dist/doc/markdownToDoc.js.map +1 -1
  25. package/dist/doc/templates/coverBlock.d.ts +2 -2
  26. package/dist/doc/templates/coverBlock.d.ts.map +1 -1
  27. package/dist/doc/templates/coverBlock.js +58 -33
  28. package/dist/doc/templates/coverBlock.js.map +1 -1
  29. package/dist/markdown/utils.d.ts.map +1 -1
  30. package/dist/markdown/utils.js +5 -1
  31. package/dist/markdown/utils.js.map +1 -1
  32. package/dist/schemas/Doc.d.ts +2 -2
  33. package/dist/schemas/Doc.d.ts.map +1 -1
  34. package/dist/schemas/themeLibrary.d.ts.map +1 -1
  35. package/dist/schemas/themeLibrary.js +1 -2
  36. package/dist/schemas/themeLibrary.js.map +1 -1
  37. package/dist/storage/ContentContainer.d.ts.map +1 -1
  38. package/dist/storage/ContentContainer.js +10 -1
  39. package/dist/storage/ContentContainer.js.map +1 -1
  40. package/dist/story/index.d.ts +12 -5
  41. package/dist/story/index.js +3 -3
  42. package/dist/{themeLibrary-DLJtGh8-.d.ts → themeLibrary-IP7EdyRq.d.ts} +1 -1
  43. package/package.json +13 -13
  44. package/src/__tests__/coverBlock.test.ts +234 -0
  45. package/src/__tests__/markdown.test.ts +22 -0
  46. package/src/__tests__/markdownToDoc.test.ts +80 -0
  47. package/src/doc/markdownToDoc.ts +74 -3
  48. package/src/doc/templates/coverBlock.ts +77 -47
  49. package/src/markdown/utils.ts +5 -1
  50. package/src/schemas/Doc.ts +2 -2
  51. package/src/schemas/themeLibrary.ts +1 -2
  52. package/src/storage/ContentContainer.ts +9 -1
  53. package/dist/chunk-4CRBS35L.js.map +0 -1
  54. package/dist/chunk-KEFWM2VC.js.map +0 -1
  55. package/dist/chunk-QWVRN6I4.js.map +0 -1
  56. package/dist/chunk-T6WBC3ND.js.map +0 -1
  57. package/dist/chunk-TVPKD76V.js.map +0 -1
  58. /package/dist/{chunk-NJLL5XYC.js.map → chunk-LAZELBON.js.map} +0 -0
@@ -46,7 +46,16 @@ export class MemoryContentContainer {
46
46
  return this.files.get(path)?.data ?? null;
47
47
  }
48
48
  async writeFile(path, data, mimeType) {
49
- const buffer = data instanceof ArrayBuffer ? data : data.slice().buffer;
49
+ // Copy to a standalone ArrayBuffer. Uint8Array/Buffer may share a larger
50
+ // backing buffer (e.g., Node's buffer pool), so we must slice to the
51
+ // exact byte range to avoid storing stale pool data.
52
+ let buffer;
53
+ if (data instanceof ArrayBuffer) {
54
+ buffer = data;
55
+ }
56
+ else {
57
+ buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
58
+ }
50
59
  this.files.set(path, {
51
60
  data: buffer,
52
61
  mimeType: mimeType ?? guessMimeType(path),
@@ -1 +1 @@
1
- {"version":3,"file":"ContentContainer.js","sourceRoot":"","sources":["../../src/storage/ContentContainer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ContentContainer.js","sourceRoot":"","sources":["../../src/storage/ContentContainer.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- import { H as PersistentLayer, L as Layer, J as PersistentLayerConfig, ac as TitleBlockInput, a2 as TemplateContext, W as SectionHeaderInput, _ as StatHighlightInput, U as QuoteBlockInput, F as FactCardInput, ag as TwoColumnInput, o as DateEventInput, v as ImageWithCaptionInput, M as MapBlockInput, Z as StartBlockConfig, t as FullBleedQuoteInput, x as ListBlockInput, Q as PhotoGridInput, q as DefinitionCardInput, k as ComparisonBarInput, T as PullQuoteInput, ak as VideoWithCaptionInput, aj as VideoPullQuoteInput, n as DataTableInput, A as AccentImage, a as AccentPosition, a7 as Theme, al as ViewportConfig, s as DocBlock, B as Block, a1 as TemplateBlock, a4 as TemplateRegistry, b as Animation, c as AnimationType, af as TransitionType, a9 as ThemeColorScheme, r as Doc } from '../Doc-Dwn5aHFC.js';
2
- export { w as LayoutHints, V as RenderStyle, a8 as ThemeColorPalette, aa as ThemeStyle, ab as ThemeTypography, ah as VIEWPORT_PRESETS, am as ViewportOrientation, an as ViewportPreset, aq as createTemplateContext, av as getLayoutHints, ay as getTwoColumnPositions, az as getViewport, aA as getViewportOrientation, aC as isTemplateBlock, aE as scaledFontSize } from '../Doc-Dwn5aHFC.js';
1
+ import { H as PersistentLayer, L as Layer, J as PersistentLayerConfig, ac as TitleBlockInput, a2 as TemplateContext, W as SectionHeaderInput, _ as StatHighlightInput, U as QuoteBlockInput, F as FactCardInput, ag as TwoColumnInput, o as DateEventInput, v as ImageWithCaptionInput, M as MapBlockInput, Z as StartBlockConfig, t as FullBleedQuoteInput, x as ListBlockInput, Q as PhotoGridInput, q as DefinitionCardInput, k as ComparisonBarInput, T as PullQuoteInput, ak as VideoWithCaptionInput, aj as VideoPullQuoteInput, n as DataTableInput, A as AccentImage, a as AccentPosition, a7 as Theme, al as ViewportConfig, s as DocBlock, B as Block, a1 as TemplateBlock, a4 as TemplateRegistry, b as Animation, c as AnimationType, af as TransitionType, a9 as ThemeColorScheme, r as Doc } from '../Doc-BfBhiCNr.js';
2
+ export { w as LayoutHints, V as RenderStyle, a8 as ThemeColorPalette, aa as ThemeStyle, ab as ThemeTypography, ah as VIEWPORT_PRESETS, am as ViewportOrientation, an as ViewportPreset, aq as createTemplateContext, av as getLayoutHints, ay as getTwoColumnPositions, az as getViewport, aA as getViewportOrientation, aC as isTemplateBlock, aE as scaledFontSize } from '../Doc-BfBhiCNr.js';
3
3
  import { p as MarkdownHeading, l as MarkdownDocument } from '../types-DYAzisMG.js';
4
4
  import { C as ContentContainer } from '../ContentContainer-DNx460y8.js';
5
- export { D as DEFAULT_THEME, g as getAvailableThemes, b as getThemeSummaries, r as resolveTheme } from '../themeLibrary-DLJtGh8-.js';
5
+ export { D as DEFAULT_THEME, g as getAvailableThemes, b as getThemeSummaries, r as resolveTheme } from '../themeLibrary-IP7EdyRq.js';
6
6
 
7
7
  /**
8
8
  * Persistent Layers Expansion
@@ -175,8 +175,8 @@ declare function mapBlock(input: MapBlockInput, context: TemplateContext): Layer
175
175
  * Input for coverBlock template - matches StartBlockConfig
176
176
  */
177
177
  interface CoverBlockInput {
178
- /** Path to hero image */
179
- heroSrc: string;
178
+ /** Path to hero image (omit for theme-driven background) */
179
+ heroSrc?: string;
180
180
  /** Alt text for the hero image */
181
181
  heroAlt?: string;
182
182
  /** Title to display over the hero */
@@ -579,6 +579,13 @@ interface MarkdownToDocOptions {
579
579
  defaultDuration?: number;
580
580
  /** Custom ID generator. Receives the heading node and its index. */
581
581
  generateId?: (heading: MarkdownHeading, index: number) => string;
582
+ /**
583
+ * Whether to auto-generate a cover startBlock from the first H1 heading.
584
+ * When true (default), a StartBlockConfig is created using the first H1's
585
+ * text as the title. If the document contains an image, the first image
586
+ * is used as the hero. Set to false to suppress automatic cover generation.
587
+ */
588
+ generateCoverBlock?: boolean;
582
589
  }
583
590
  /**
584
591
  * Convert a MarkdownDocument into a Doc with a heading-driven Block hierarchy.
@@ -50,13 +50,13 @@ import {
50
50
  twoColumn,
51
51
  videoPullQuote,
52
52
  videoWithCaption
53
- } from "../chunk-T6WBC3ND.js";
53
+ } from "../chunk-GAQLMORN.js";
54
54
  import {
55
55
  DEFAULT_THEME,
56
56
  getAvailableThemes,
57
57
  getThemeSummaries,
58
58
  resolveTheme
59
- } from "../chunk-TVPKD76V.js";
59
+ } from "../chunk-53TZ2UTL.js";
60
60
  import {
61
61
  VIEWPORT_PRESETS,
62
62
  createTemplateContext,
@@ -67,7 +67,7 @@ import {
67
67
  isTemplateBlock,
68
68
  scaledFontSize2 as scaledFontSize
69
69
  } from "../chunk-QM5PKNPW.js";
70
- import "../chunk-QWVRN6I4.js";
70
+ import "../chunk-7GF6RDED.js";
71
71
  import "../chunk-4X3JQXNM.js";
72
72
  import "../chunk-3E5F2XMR.js";
73
73
  export {
@@ -1,4 +1,4 @@
1
- import { a7 as Theme } from './Doc-Dwn5aHFC.js';
1
+ import { a7 as Theme } from './Doc-BfBhiCNr.js';
2
2
 
3
3
  /**
4
4
  * Built-in Theme Library
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Headless utilities for doc/block rendering, spatial math, Markdown, and storage",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -92,19 +92,19 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "dependencies": {
95
- "hast-util-from-html": "^2.0.0",
96
- "localforage": "^1.10.0",
97
- "ngeohash": "^0.6.3",
98
- "remark-directive": "^3.0.0",
99
- "remark-frontmatter": "^5.0.0",
100
- "remark-gfm": "^4.0.0",
101
- "remark-math": "^6.0.0",
102
- "remark-parse": "^11.0.0",
103
- "remark-stringify": "^11.0.0",
104
- "unified": "^11.0.0"
95
+ "hast-util-from-html": "2.0.3",
96
+ "localforage": "1.10.0",
97
+ "ngeohash": "0.6.3",
98
+ "remark-directive": "3.0.1",
99
+ "remark-frontmatter": "5.0.0",
100
+ "remark-gfm": "4.0.1",
101
+ "remark-math": "6.0.0",
102
+ "remark-parse": "11.0.0",
103
+ "remark-stringify": "11.0.0",
104
+ "unified": "11.0.5"
105
105
  },
106
106
  "devDependencies": {
107
- "tsup": "^8.0.0",
108
- "typescript": "^5.3.0"
107
+ "tsup": "8.5.1",
108
+ "typescript": "5.9.3"
109
109
  }
110
110
  }
@@ -0,0 +1,234 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { coverBlock, expandCoverBlock, startBlockToCoverInput } from '../doc/templates/coverBlock';
3
+ import { createTemplateContext, DEFAULT_THEME, VIEWPORT_PRESETS } from '../doc/templates/index';
4
+ import type { StartBlockConfig } from '../schemas/Doc';
5
+ import type { CoverBlockInput } from '../doc/templates/coverBlock';
6
+ import type { ImageLayer, TextLayer, ShapeLayer } from '../schemas/Doc';
7
+
8
+ // ── Helpers ──────────────────────────────────────────────────────────
9
+
10
+ const landscapeContext = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
11
+ const portraitContext = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.portrait);
12
+
13
+ function findLayer(layers: ReturnType<typeof coverBlock>, id: string) {
14
+ return layers.find((l) => l.id === id);
15
+ }
16
+
17
+ function findImage(layers: ReturnType<typeof coverBlock>, id: string) {
18
+ return findLayer(layers, id) as ImageLayer | undefined;
19
+ }
20
+
21
+ function findText(layers: ReturnType<typeof coverBlock>, id: string) {
22
+ return findLayer(layers, id) as TextLayer | undefined;
23
+ }
24
+
25
+ function findShape(layers: ReturnType<typeof coverBlock>, id: string) {
26
+ return findLayer(layers, id) as ShapeLayer | undefined;
27
+ }
28
+
29
+ // ── coverBlock with hero image ──────────────────────────────────────
30
+
31
+ describe('coverBlock with hero image', () => {
32
+ const input: CoverBlockInput = {
33
+ heroSrc: 'hero.jpg',
34
+ heroAlt: 'Hero image',
35
+ title: 'Test Title',
36
+ subtitle: 'A subtitle',
37
+ ambientMotion: 'zoomIn',
38
+ heroCredit: 'Photographer',
39
+ heroLicense: 'CC-BY-4.0',
40
+ };
41
+
42
+ it('produces hero image layer', () => {
43
+ const layers = coverBlock(input, landscapeContext);
44
+ const hero = findImage(layers, 'cover-hero');
45
+ expect(hero).toBeDefined();
46
+ expect(hero!.type).toBe('image');
47
+ expect(hero!.content.src).toBe('hero.jpg');
48
+ expect(hero!.content.alt).toBe('Hero image');
49
+ expect(hero!.content.fit).toBe('cover');
50
+ expect(hero!.content.credit).toBe('Photographer');
51
+ expect(hero!.content.license).toBe('CC-BY-4.0');
52
+ });
53
+
54
+ it('produces gradient overlay', () => {
55
+ const layers = coverBlock(input, landscapeContext);
56
+ const gradient = findShape(layers, 'cover-gradient');
57
+ expect(gradient).toBeDefined();
58
+ expect(gradient!.type).toBe('shape');
59
+ expect(gradient!.content.fill).toContain('linear-gradient');
60
+ });
61
+
62
+ it('produces title layer with shadow', () => {
63
+ const layers = coverBlock(input, landscapeContext);
64
+ const title = findText(layers, 'cover-title');
65
+ expect(title).toBeDefined();
66
+ expect(title!.type).toBe('text');
67
+ expect(title!.content.text).toBe('Test Title');
68
+ expect(title!.content.style.shadow).toBe(true);
69
+ expect(title!.content.style.fontWeight).toBe('bold');
70
+ });
71
+
72
+ it('produces subtitle layer', () => {
73
+ const layers = coverBlock(input, landscapeContext);
74
+ const subtitle = findText(layers, 'cover-subtitle');
75
+ expect(subtitle).toBeDefined();
76
+ expect(subtitle!.type).toBe('text');
77
+ expect(subtitle!.content.text).toBe('A subtitle');
78
+ });
79
+
80
+ it('does not produce theme background layers', () => {
81
+ const layers = coverBlock(input, landscapeContext);
82
+ expect(findLayer(layers, 'cover-bg')).toBeUndefined();
83
+ expect(findLayer(layers, 'cover-accent')).toBeUndefined();
84
+ });
85
+
86
+ it('title positioned lower (over hero)', () => {
87
+ const layers = coverBlock(input, landscapeContext);
88
+ const title = findLayer(layers, 'cover-title');
89
+ // With subtitle: 70%, without: 75% — both below center
90
+ expect(title!.position.y).toBe('70%');
91
+ });
92
+
93
+ it('applies Ken Burns animation to hero image', () => {
94
+ const layers = coverBlock(input, landscapeContext);
95
+ const hero = findLayer(layers, 'cover-hero');
96
+ expect(hero!.animation).toBeDefined();
97
+ });
98
+ });
99
+
100
+ // ── coverBlock without hero image (theme background) ────────────────
101
+
102
+ describe('coverBlock without hero image', () => {
103
+ const input: CoverBlockInput = {
104
+ title: 'Title Only',
105
+ subtitle: 'Subtitle text',
106
+ };
107
+
108
+ it('produces theme background instead of hero', () => {
109
+ const layers = coverBlock(input, landscapeContext);
110
+ const bg = findShape(layers, 'cover-bg');
111
+ expect(bg).toBeDefined();
112
+ expect(bg!.type).toBe('shape');
113
+ expect(bg!.content.fill).toContain('radial-gradient');
114
+ });
115
+
116
+ it('produces decorative accent line', () => {
117
+ const layers = coverBlock(input, landscapeContext);
118
+ const accent = findLayer(layers, 'cover-accent');
119
+ expect(accent).toBeDefined();
120
+ expect(accent!.type).toBe('shape');
121
+ });
122
+
123
+ it('does not produce hero image or gradient overlay', () => {
124
+ const layers = coverBlock(input, landscapeContext);
125
+ expect(findLayer(layers, 'cover-hero')).toBeUndefined();
126
+ expect(findLayer(layers, 'cover-gradient')).toBeUndefined();
127
+ });
128
+
129
+ it('title centered (not pushed to bottom)', () => {
130
+ const layers = coverBlock(input, landscapeContext);
131
+ const title = findLayer(layers, 'cover-title');
132
+ // Without hero: uses layout.primaryY which is typically around 35-40%
133
+ expect(title!.position.y).not.toBe('70%');
134
+ expect(title!.position.y).not.toBe('75%');
135
+ });
136
+
137
+ it('subtitle uses layout secondaryY', () => {
138
+ const layers = coverBlock(input, landscapeContext);
139
+ const subtitle = findLayer(layers, 'cover-subtitle');
140
+ expect(subtitle!.position.y).not.toBe('82%');
141
+ });
142
+ });
143
+
144
+ // ── coverBlock title-only (no subtitle) ─────────────────────────────
145
+
146
+ describe('coverBlock title-only', () => {
147
+ it('no subtitle layer when subtitle is omitted', () => {
148
+ const layers = coverBlock({ title: 'Solo Title' }, landscapeContext);
149
+ expect(findLayer(layers, 'cover-subtitle')).toBeUndefined();
150
+ });
151
+
152
+ it('hero title at 75% when no subtitle', () => {
153
+ const layers = coverBlock({ heroSrc: 'img.jpg', title: 'Solo' }, landscapeContext);
154
+ const title = findLayer(layers, 'cover-title');
155
+ expect(title!.position.y).toBe('75%');
156
+ });
157
+
158
+ it('no-hero title at 50% when no subtitle', () => {
159
+ const layers = coverBlock({ title: 'Solo' }, landscapeContext);
160
+ const title = findLayer(layers, 'cover-title');
161
+ expect(title!.position.y).toBe('50%');
162
+ });
163
+ });
164
+
165
+ // ── Viewport scaling ────────────────────────────────────────────────
166
+
167
+ describe('coverBlock viewport scaling', () => {
168
+ it('produces layers for portrait viewport', () => {
169
+ const layers = coverBlock({ heroSrc: 'img.jpg', title: 'Portrait' }, portraitContext);
170
+ expect(layers.length).toBeGreaterThanOrEqual(3); // hero + gradient + title
171
+ });
172
+
173
+ it('title font size differs between landscape and portrait', () => {
174
+ const landLayers = coverBlock({ title: 'Test' }, landscapeContext);
175
+ const portLayers = coverBlock({ title: 'Test' }, portraitContext);
176
+ const landTitle = findText(landLayers, 'cover-title');
177
+ const portTitle = findText(portLayers, 'cover-title');
178
+ expect(landTitle!.content.style.fontSize).not.toBe(portTitle!.content.style.fontSize);
179
+ });
180
+ });
181
+
182
+ // ── startBlockToCoverInput ──────────────────────────────────────────
183
+
184
+ describe('startBlockToCoverInput', () => {
185
+ it('maps all StartBlockConfig fields to CoverBlockInput', () => {
186
+ const config: StartBlockConfig = {
187
+ heroSrc: 'hero.jpg',
188
+ heroAlt: 'Alt',
189
+ title: 'Title',
190
+ subtitle: 'Sub',
191
+ ambientMotion: 'panLeft',
192
+ heroCredit: 'Credit',
193
+ heroLicense: 'MIT',
194
+ };
195
+ const result = startBlockToCoverInput(config);
196
+ expect(result.heroSrc).toBe('hero.jpg');
197
+ expect(result.heroAlt).toBe('Alt');
198
+ expect(result.title).toBe('Title');
199
+ expect(result.subtitle).toBe('Sub');
200
+ expect(result.ambientMotion).toBe('panLeft');
201
+ expect(result.heroCredit).toBe('Credit');
202
+ expect(result.heroLicense).toBe('MIT');
203
+ });
204
+
205
+ it('handles config without heroSrc', () => {
206
+ const config: StartBlockConfig = { title: 'Title Only' };
207
+ const result = startBlockToCoverInput(config);
208
+ expect(result.heroSrc).toBeUndefined();
209
+ expect(result.title).toBe('Title Only');
210
+ });
211
+ });
212
+
213
+ // ── expandCoverBlock ────────────────────────────────────────────────
214
+
215
+ describe('expandCoverBlock', () => {
216
+ it('returns layers from StartBlockConfig', () => {
217
+ const config: StartBlockConfig = {
218
+ heroSrc: 'cover.jpg',
219
+ title: 'Cover Title',
220
+ };
221
+ const layers = expandCoverBlock(config, landscapeContext);
222
+ expect(layers.length).toBeGreaterThanOrEqual(3);
223
+ expect(layers.some((l) => l.id === 'cover-hero')).toBe(true);
224
+ expect(layers.some((l) => l.id === 'cover-title')).toBe(true);
225
+ });
226
+
227
+ it('returns theme background layers when no heroSrc', () => {
228
+ const config: StartBlockConfig = { title: 'No Hero' };
229
+ const layers = expandCoverBlock(config, landscapeContext);
230
+ expect(layers.some((l) => l.id === 'cover-bg')).toBe(true);
231
+ expect(layers.some((l) => l.id === 'cover-title')).toBe(true);
232
+ expect(layers.some((l) => l.id === 'cover-hero')).toBe(false);
233
+ });
234
+ });
@@ -591,6 +591,28 @@ describe('Tree utilities', () => {
591
591
  expect(extractPlainText(doc)).toBe('TitleHello world');
592
592
  });
593
593
 
594
+ it('extractPlainText preserves list item boundaries', () => {
595
+ const list: MarkdownList = {
596
+ type: 'list',
597
+ ordered: false,
598
+ children: [
599
+ {
600
+ type: 'listItem',
601
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Apple' }] }],
602
+ },
603
+ {
604
+ type: 'listItem',
605
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Banana' }] }],
606
+ },
607
+ {
608
+ type: 'listItem',
609
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Cherry' }] }],
610
+ },
611
+ ],
612
+ };
613
+ expect(extractPlainText(list)).toBe('Apple\nBanana\nCherry');
614
+ });
615
+
594
616
  it('countNodes counts all nodes', () => {
595
617
  expect(countNodes(doc)).toBe(7); // document + heading + text + paragraph + text + link + text
596
618
  });
@@ -468,3 +468,83 @@ describe('caption generation', () => {
468
468
  }
469
469
  });
470
470
  });
471
+
472
+ // ============================================
473
+ // Auto cover block (startBlock) generation
474
+ // ============================================
475
+
476
+ describe('auto cover block generation', () => {
477
+ it('creates startBlock from H1 title', () => {
478
+ const md = parseMarkdown('# My Great Article\n\nSome introduction text.');
479
+ const doc = markdownToDoc(md);
480
+
481
+ expect(doc.startBlock).toBeDefined();
482
+ expect(doc.startBlock!.title).toBe('My Great Article');
483
+ });
484
+
485
+ it('uses first paragraph after H1 as subtitle', () => {
486
+ const md = parseMarkdown('# Title\n\nThis is a subtitle paragraph.\n\n## Section\n\nBody');
487
+ const doc = markdownToDoc(md);
488
+
489
+ expect(doc.startBlock).toBeDefined();
490
+ expect(doc.startBlock!.subtitle).toBe('This is a subtitle paragraph.');
491
+ });
492
+
493
+ it('picks first image as heroSrc', () => {
494
+ const md = parseMarkdown('# Title\n\n![photo](hero.jpg)\n\nSome text.');
495
+ const doc = markdownToDoc(md);
496
+
497
+ expect(doc.startBlock).toBeDefined();
498
+ expect(doc.startBlock!.heroSrc).toBe('hero.jpg');
499
+ expect(doc.startBlock!.heroAlt).toBe('photo');
500
+ });
501
+
502
+ it('picks image from deeper in document', () => {
503
+ const md = parseMarkdown(
504
+ '# Title\n\nIntro text.\n\n## Section\n\nBody text.\n\n![deep pic](deep.png)',
505
+ );
506
+ const doc = markdownToDoc(md);
507
+
508
+ expect(doc.startBlock).toBeDefined();
509
+ expect(doc.startBlock!.heroSrc).toBe('deep.png');
510
+ });
511
+
512
+ it('works without images (no heroSrc)', () => {
513
+ const md = parseMarkdown('# Title\n\nPlain text, no images.');
514
+ const doc = markdownToDoc(md);
515
+
516
+ expect(doc.startBlock).toBeDefined();
517
+ expect(doc.startBlock!.title).toBe('Title');
518
+ expect(doc.startBlock!.heroSrc).toBeUndefined();
519
+ });
520
+
521
+ it('sets ambientMotion to zoomIn by default', () => {
522
+ const md = parseMarkdown('# Title\n\nContent.');
523
+ const doc = markdownToDoc(md);
524
+
525
+ expect(doc.startBlock!.ambientMotion).toBe('zoomIn');
526
+ });
527
+
528
+ it('does not create startBlock when no H1 exists', () => {
529
+ const md = parseMarkdown('## Section A\n\nText A\n\n## Section B\n\nText B');
530
+ const doc = markdownToDoc(md);
531
+
532
+ expect(doc.startBlock).toBeUndefined();
533
+ });
534
+
535
+ it('does not create startBlock when generateCoverBlock is false', () => {
536
+ const md = parseMarkdown('# Title\n\nContent.');
537
+ const doc = markdownToDoc(md, { generateCoverBlock: false });
538
+
539
+ expect(doc.startBlock).toBeUndefined();
540
+ });
541
+
542
+ it('does not create startBlock for heading-only document with no content', () => {
543
+ const md = parseMarkdown('# Title');
544
+ const doc = markdownToDoc(md);
545
+
546
+ expect(doc.startBlock).toBeDefined();
547
+ expect(doc.startBlock!.title).toBe('Title');
548
+ expect(doc.startBlock!.subtitle).toBeUndefined();
549
+ });
550
+ });
@@ -22,8 +22,14 @@
22
22
  * ```
23
23
  */
24
24
 
25
- import type { Doc, Block, CaptionTrack, CaptionPhrase } from '../schemas/Doc.js';
26
- import type { MarkdownDocument, MarkdownBlockNode, MarkdownHeading } from '../markdown/types.js';
25
+ import type { Doc, Block, CaptionTrack, CaptionPhrase, StartBlockConfig } from '../schemas/Doc.js';
26
+ import type {
27
+ MarkdownDocument,
28
+ MarkdownBlockNode,
29
+ MarkdownHeading,
30
+ MarkdownNode,
31
+ MarkdownImage,
32
+ } from '../markdown/types.js';
27
33
  import { extractPlainText } from '../markdown/utils.js';
28
34
  import { estimateReadingTime } from '../timing/readingTime.js';
29
35
 
@@ -46,6 +52,14 @@ export interface MarkdownToDocOptions {
46
52
 
47
53
  /** Custom ID generator. Receives the heading node and its index. */
48
54
  generateId?: (heading: MarkdownHeading, index: number) => string;
55
+
56
+ /**
57
+ * Whether to auto-generate a cover startBlock from the first H1 heading.
58
+ * When true (default), a StartBlockConfig is created using the first H1's
59
+ * text as the title. If the document contains an image, the first image
60
+ * is used as the hero. Set to false to suppress automatic cover generation.
61
+ */
62
+ generateCoverBlock?: boolean;
49
63
  }
50
64
 
51
65
  // ============================================
@@ -267,7 +281,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
267
281
  const captions: CaptionTrack | undefined =
268
282
  phrases.length > 0 ? { phrases, generatedAt: new Date().toISOString(), version: 1 } : undefined;
269
283
 
270
- return {
284
+ const doc: Doc = {
271
285
  articleId,
272
286
  duration: currentTime,
273
287
  blocks: rootBlocks,
@@ -277,6 +291,16 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
277
291
  ...(captions ? { captions } : {}),
278
292
  ...(markdownDoc.frontmatter ? { frontmatter: markdownDoc.frontmatter } : {}),
279
293
  };
294
+
295
+ // Auto-generate cover startBlock from the first H1 heading
296
+ if (options?.generateCoverBlock ?? true) {
297
+ const coverConfig = buildStartBlock(markdownDoc, rootBlocks);
298
+ if (coverConfig) {
299
+ doc.startBlock = coverConfig;
300
+ }
301
+ }
302
+
303
+ return doc;
280
304
  }
281
305
 
282
306
  // ============================================
@@ -409,3 +433,50 @@ function splitIntoSentences(text: string): string[] {
409
433
  }
410
434
  return merged;
411
435
  }
436
+
437
+ /**
438
+ * Walk a MarkdownNode tree depth-first to find the first image node.
439
+ * Returns the MarkdownImage or undefined if none found.
440
+ */
441
+ function findFirstImage(node: MarkdownNode): MarkdownImage | undefined {
442
+ if (node.type === 'image') return node as MarkdownImage;
443
+ if ('children' in node && Array.isArray((node as { children?: unknown[] }).children)) {
444
+ for (const child of (node as { children: MarkdownNode[] }).children) {
445
+ const found = findFirstImage(child);
446
+ if (found) return found;
447
+ }
448
+ }
449
+ return undefined;
450
+ }
451
+
452
+ /**
453
+ * Build a StartBlockConfig from the document's first H1 heading and optional
454
+ * first image. Returns undefined if the document has no H1 heading.
455
+ */
456
+ function buildStartBlock(
457
+ markdownDoc: MarkdownDocument,
458
+ rootBlocks: Block[],
459
+ ): StartBlockConfig | undefined {
460
+ // Find the first H1 block — it provides the cover title
461
+ const firstH1 = rootBlocks.find((b) => b.sourceHeading?.depth === 1);
462
+ if (!firstH1) return undefined;
463
+
464
+ const title = firstH1.title ?? extractPlainText(firstH1.sourceHeading!);
465
+ if (!title) return undefined;
466
+
467
+ // Look for the first paragraph immediately after the H1 to use as subtitle
468
+ const subtitle =
469
+ firstH1.contents?.[0]?.type === 'paragraph' ? extractPlainText(firstH1.contents[0]) : undefined;
470
+
471
+ // Scan the whole document for the first image to use as the hero
472
+ const firstImage = findFirstImage(markdownDoc);
473
+
474
+ const config: StartBlockConfig = {
475
+ title,
476
+ ...(subtitle ? { subtitle } : {}),
477
+ ...(firstImage ? { heroSrc: firstImage.url, heroAlt: firstImage.alt ?? title } : {}),
478
+ ambientMotion: 'zoomIn',
479
+ };
480
+
481
+ return config;
482
+ }