@bendyline/squisq 1.1.0 → 1.1.1
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/ContentContainer-DNx460y8.d.ts +88 -0
- package/dist/{Doc-1hmT1LZl.d.ts → Doc-Cvt2lyzn.d.ts} +56 -4
- package/dist/__tests__/markdownToDoc.test.js +75 -0
- package/dist/__tests__/markdownToDoc.test.js.map +1 -1
- package/dist/{chunk-PJ7AODIM.js → chunk-3WBJAZZ5.js} +394 -52
- package/dist/chunk-3WBJAZZ5.js.map +1 -0
- package/dist/{chunk-6YJPI6VI.js → chunk-4X3JQXNM.js} +1 -1
- package/dist/{chunk-S4F2RY37.js → chunk-CORGFIFU.js} +1 -1
- package/dist/chunk-CORGFIFU.js.map +1 -0
- package/dist/chunk-DNJKBAI7.js +1 -0
- package/dist/chunk-DNJKBAI7.js.map +1 -0
- package/dist/{chunk-VP5SZTZ2.js → chunk-MIBRIRGJ.js} +2 -139
- package/dist/chunk-MIBRIRGJ.js.map +1 -0
- package/dist/{chunk-URU6QMLY.js → chunk-NJLL5XYC.js} +15 -8
- package/dist/chunk-NJLL5XYC.js.map +1 -0
- package/dist/chunk-OFU53HI7.js +165 -0
- package/dist/chunk-OFU53HI7.js.map +1 -0
- package/dist/chunk-TSG2OSE6.js +145 -0
- package/dist/chunk-TSG2OSE6.js.map +1 -0
- package/dist/{chunk-O5HCIUAD.js → chunk-TVPKD76V.js} +81 -187
- package/dist/chunk-TVPKD76V.js.map +1 -0
- package/dist/contentExtractor-BNfVJV2U.d.ts +113 -0
- package/dist/doc/audioMapping.d.ts +36 -0
- package/dist/doc/audioMapping.d.ts.map +1 -0
- package/dist/doc/audioMapping.js +279 -0
- package/dist/doc/audioMapping.js.map +1 -0
- package/dist/doc/index.d.ts +1 -0
- package/dist/doc/index.d.ts.map +1 -1
- package/dist/doc/index.js +1 -0
- package/dist/doc/index.js.map +1 -1
- package/dist/doc/markdownToDoc.d.ts.map +1 -1
- package/dist/doc/markdownToDoc.js +126 -1
- package/dist/doc/markdownToDoc.js.map +1 -1
- package/dist/doc/templates/comparisonBar.d.ts.map +1 -1
- package/dist/doc/templates/comparisonBar.js +23 -5
- package/dist/doc/templates/comparisonBar.js.map +1 -1
- package/dist/doc/templates/coverBlock.d.ts.map +1 -1
- package/dist/doc/templates/coverBlock.js +3 -0
- package/dist/doc/templates/coverBlock.js.map +1 -1
- package/dist/doc/templates/dateEvent.d.ts.map +1 -1
- package/dist/doc/templates/dateEvent.js +4 -0
- package/dist/doc/templates/dateEvent.js.map +1 -1
- package/dist/doc/templates/definitionCard.d.ts.map +1 -1
- package/dist/doc/templates/definitionCard.js +4 -1
- package/dist/doc/templates/definitionCard.js.map +1 -1
- package/dist/doc/templates/factCard.d.ts.map +1 -1
- package/dist/doc/templates/factCard.js +4 -0
- package/dist/doc/templates/factCard.js.map +1 -1
- package/dist/doc/templates/fullBleedQuote.d.ts.map +1 -1
- package/dist/doc/templates/fullBleedQuote.js +2 -1
- package/dist/doc/templates/fullBleedQuote.js.map +1 -1
- package/dist/doc/templates/imageWithCaption.d.ts.map +1 -1
- package/dist/doc/templates/imageWithCaption.js +4 -0
- package/dist/doc/templates/imageWithCaption.js.map +1 -1
- package/dist/doc/templates/listBlock.d.ts.map +1 -1
- package/dist/doc/templates/listBlock.js +3 -0
- package/dist/doc/templates/listBlock.js.map +1 -1
- package/dist/doc/templates/mapBlock.d.ts.map +1 -1
- package/dist/doc/templates/mapBlock.js +3 -0
- package/dist/doc/templates/mapBlock.js.map +1 -1
- package/dist/doc/templates/photoGrid.d.ts.map +1 -1
- package/dist/doc/templates/photoGrid.js +3 -0
- package/dist/doc/templates/photoGrid.js.map +1 -1
- package/dist/doc/templates/pullQuote.d.ts.map +1 -1
- package/dist/doc/templates/pullQuote.js +4 -0
- package/dist/doc/templates/pullQuote.js.map +1 -1
- package/dist/doc/templates/quoteBlock.d.ts.map +1 -1
- package/dist/doc/templates/quoteBlock.js +4 -0
- package/dist/doc/templates/quoteBlock.js.map +1 -1
- package/dist/doc/templates/sectionHeader.d.ts.map +1 -1
- package/dist/doc/templates/sectionHeader.js +3 -2
- package/dist/doc/templates/sectionHeader.js.map +1 -1
- package/dist/doc/templates/statHighlight.d.ts.map +1 -1
- package/dist/doc/templates/statHighlight.js +4 -1
- package/dist/doc/templates/statHighlight.js.map +1 -1
- package/dist/doc/templates/titleBlock.d.ts.map +1 -1
- package/dist/doc/templates/titleBlock.js +3 -0
- package/dist/doc/templates/titleBlock.js.map +1 -1
- package/dist/doc/templates/twoColumn.d.ts.map +1 -1
- package/dist/doc/templates/twoColumn.js +7 -1
- package/dist/doc/templates/twoColumn.js.map +1 -1
- package/dist/doc/templates/videoPullQuote.d.ts.map +1 -1
- package/dist/doc/templates/videoPullQuote.js +4 -0
- package/dist/doc/templates/videoPullQuote.js.map +1 -1
- package/dist/doc/templates/videoWithCaption.d.ts.map +1 -1
- package/dist/doc/templates/videoWithCaption.js +3 -0
- package/dist/doc/templates/videoWithCaption.js.map +1 -1
- package/dist/doc/utils/themeUtils.d.ts +9 -0
- package/dist/doc/utils/themeUtils.d.ts.map +1 -1
- package/dist/doc/utils/themeUtils.js +26 -0
- package/dist/doc/utils/themeUtils.js.map +1 -1
- package/dist/markdown/convert.d.ts.map +1 -1
- package/dist/markdown/convert.js +16 -7
- package/dist/markdown/convert.js.map +1 -1
- package/dist/markdown/types.d.ts +4 -2
- package/dist/markdown/types.d.ts.map +1 -1
- package/dist/schemas/BlockTemplates.d.ts +2 -2
- package/dist/schemas/BlockTemplates.d.ts.map +1 -1
- package/dist/schemas/BlockTemplates.js +2 -2
- package/dist/schemas/BlockTemplates.js.map +1 -1
- package/dist/schemas/Doc.d.ts +52 -0
- package/dist/schemas/Doc.d.ts.map +1 -1
- package/dist/schemas/Doc.js.map +1 -1
- package/dist/schemas/themeLibrary.d.ts +2 -2
- package/dist/schemas/themeLibrary.d.ts.map +1 -1
- package/dist/schemas/themeLibrary.js +91 -26
- package/dist/schemas/themeLibrary.js.map +1 -1
- package/dist/story/index.d.ts +50 -5
- package/dist/story/index.js +15 -5
- package/dist/{themeLibrary-DR1VRo-8.d.ts → themeLibrary-VOHfPzRB.d.ts} +3 -3
- package/dist/transform/applyTransform.d.ts +22 -0
- package/dist/transform/applyTransform.d.ts.map +1 -0
- package/dist/transform/applyTransform.js +68 -0
- package/dist/transform/applyTransform.js.map +1 -0
- package/dist/transform/blockAnalyzer.d.ts +48 -0
- package/dist/transform/blockAnalyzer.d.ts.map +1 -0
- package/dist/transform/blockAnalyzer.js +135 -0
- package/dist/transform/blockAnalyzer.js.map +1 -0
- package/dist/transform/index.d.ts +6 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +4 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/registry.d.ts +19 -0
- package/dist/transform/registry.d.ts.map +1 -0
- package/dist/transform/registry.js +41 -0
- package/dist/transform/registry.js.map +1 -0
- package/dist/transform/styles/dataDriven.d.ts +4 -0
- package/dist/transform/styles/dataDriven.d.ts.map +1 -0
- package/dist/transform/styles/dataDriven.js +15 -0
- package/dist/transform/styles/dataDriven.js.map +1 -0
- package/dist/transform/styles/documentary.d.ts +4 -0
- package/dist/transform/styles/documentary.d.ts.map +1 -0
- package/dist/transform/styles/documentary.js +15 -0
- package/dist/transform/styles/documentary.js.map +1 -0
- package/dist/transform/styles/index.d.ts +6 -0
- package/dist/transform/styles/index.d.ts.map +1 -0
- package/dist/transform/styles/index.js +6 -0
- package/dist/transform/styles/index.js.map +1 -0
- package/dist/transform/styles/magazine.d.ts +4 -0
- package/dist/transform/styles/magazine.d.ts.map +1 -0
- package/dist/transform/styles/magazine.js +15 -0
- package/dist/transform/styles/magazine.js.map +1 -0
- package/dist/transform/styles/minimal.d.ts +4 -0
- package/dist/transform/styles/minimal.d.ts.map +1 -0
- package/dist/transform/styles/minimal.js +15 -0
- package/dist/transform/styles/minimal.js.map +1 -0
- package/dist/transform/styles/narrative.d.ts +4 -0
- package/dist/transform/styles/narrative.d.ts.map +1 -0
- package/dist/transform/styles/narrative.js +15 -0
- package/dist/transform/styles/narrative.js.map +1 -0
- package/dist/transform/templateSelector.d.ts +30 -0
- package/dist/transform/templateSelector.d.ts.map +1 -0
- package/dist/transform/templateSelector.js +218 -0
- package/dist/transform/templateSelector.js.map +1 -0
- package/dist/transform/timingAllocator.d.ts +21 -0
- package/dist/transform/timingAllocator.d.ts.map +1 -0
- package/dist/transform/timingAllocator.js +111 -0
- package/dist/transform/timingAllocator.js.map +1 -0
- package/dist/transform/types.d.ts +86 -0
- package/dist/transform/types.d.ts.map +1 -0
- package/dist/transform/types.js +9 -0
- package/dist/transform/types.js.map +1 -0
- package/dist/{types-DHiv_Pnm.d.ts → types-DYAzisMG.d.ts} +4 -2
- package/package.json +6 -1
- package/src/__tests__/markdownToDoc.test.ts +96 -0
- package/src/doc/audioMapping.ts +346 -0
- package/src/doc/index.ts +1 -0
- package/src/doc/markdownToDoc.ts +136 -2
- package/src/doc/templates/comparisonBar.ts +23 -5
- package/src/doc/templates/coverBlock.ts +3 -0
- package/src/doc/templates/dateEvent.ts +4 -0
- package/src/doc/templates/definitionCard.ts +4 -1
- package/src/doc/templates/factCard.ts +4 -0
- package/src/doc/templates/fullBleedQuote.ts +2 -1
- package/src/doc/templates/imageWithCaption.ts +4 -0
- package/src/doc/templates/listBlock.ts +3 -0
- package/src/doc/templates/mapBlock.ts +3 -0
- package/src/doc/templates/photoGrid.ts +3 -0
- package/src/doc/templates/pullQuote.ts +4 -0
- package/src/doc/templates/quoteBlock.ts +4 -0
- package/src/doc/templates/sectionHeader.ts +3 -2
- package/src/doc/templates/statHighlight.ts +4 -1
- package/src/doc/templates/titleBlock.ts +3 -0
- package/src/doc/templates/twoColumn.ts +7 -1
- package/src/doc/templates/videoPullQuote.ts +4 -0
- package/src/doc/templates/videoWithCaption.ts +3 -0
- package/src/doc/utils/themeUtils.ts +29 -0
- package/src/markdown/convert.ts +17 -7
- package/src/markdown/types.ts +4 -2
- package/src/schemas/BlockTemplates.ts +2 -2
- package/src/schemas/Doc.ts +60 -0
- package/src/schemas/themeLibrary.ts +95 -26
- package/src/transform/applyTransform.ts +83 -0
- package/src/transform/blockAnalyzer.ts +181 -0
- package/src/transform/index.ts +20 -0
- package/src/transform/registry.ts +47 -0
- package/src/transform/styles/dataDriven.ts +16 -0
- package/src/transform/styles/documentary.ts +16 -0
- package/src/transform/styles/index.ts +5 -0
- package/src/transform/styles/magazine.ts +16 -0
- package/src/transform/styles/minimal.ts +16 -0
- package/src/transform/styles/narrative.ts +16 -0
- package/src/transform/templateSelector.ts +294 -0
- package/src/transform/timingAllocator.ts +126 -0
- package/src/transform/types.ts +105 -0
- package/dist/chunk-O5HCIUAD.js.map +0 -1
- package/dist/chunk-PJ7AODIM.js.map +0 -1
- package/dist/chunk-S4F2RY37.js.map +0 -1
- package/dist/chunk-URU6QMLY.js.map +0 -1
- package/dist/chunk-VP5SZTZ2.js.map +0 -1
- /package/dist/{chunk-6YJPI6VI.js.map → chunk-4X3JQXNM.js.map} +0 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Mapping
|
|
3
|
+
*
|
|
4
|
+
* Associates audio segments (MP3 files) with document blocks.
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Explicit annotation**: Blocks with `{[audio=filename.mp3]}` in their
|
|
8
|
+
* heading annotation are directly mapped to that audio file.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Auto-matching**: When no annotations exist but the container has MP3s
|
|
11
|
+
* with `.timing.json` files, matches MP3s to blocks by comparing the
|
|
12
|
+
* timing.json `sourceText` against block text content (word overlap).
|
|
13
|
+
* Falls back to filename-based matching (slugified title comparison).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Doc, Block, AudioSegment, AudioTimingData } from '../schemas/Doc.js';
|
|
17
|
+
import type { ContentContainer } from '../storage/ContentContainer.js';
|
|
18
|
+
import { extractPlainText } from '../markdown/utils.js';
|
|
19
|
+
|
|
20
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a timing.json file from raw data.
|
|
24
|
+
*/
|
|
25
|
+
function parseTimingJson(data: ArrayBuffer): AudioTimingData | null {
|
|
26
|
+
try {
|
|
27
|
+
const text = new TextDecoder().decode(data);
|
|
28
|
+
const parsed = JSON.parse(text);
|
|
29
|
+
if (parsed && typeof parsed.sourceText === 'string' && typeof parsed.duration === 'number') {
|
|
30
|
+
return {
|
|
31
|
+
sourceText: parsed.sourceText,
|
|
32
|
+
bookmarks: Array.isArray(parsed.bookmarks) ? parsed.bookmarks : [],
|
|
33
|
+
duration: parsed.duration,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize text for comparison: lowercase, strip punctuation, split into word set.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeToWords(text: string): Set<string> {
|
|
46
|
+
const words = text
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^\w\s]/g, ' ')
|
|
49
|
+
.split(/\s+/)
|
|
50
|
+
.filter((w) => w.length > 2);
|
|
51
|
+
return new Set(words);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculate Jaccard similarity between two texts (word overlap).
|
|
56
|
+
* Returns 0–1 where 1 = identical word sets.
|
|
57
|
+
*/
|
|
58
|
+
export function scoreTextSimilarity(a: string, b: string): number {
|
|
59
|
+
const wordsA = normalizeToWords(a);
|
|
60
|
+
const wordsB = normalizeToWords(b);
|
|
61
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
62
|
+
|
|
63
|
+
let intersection = 0;
|
|
64
|
+
for (const w of wordsA) {
|
|
65
|
+
if (wordsB.has(w)) intersection++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const union = new Set([...wordsA, ...wordsB]).size;
|
|
69
|
+
return union > 0 ? intersection / union : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Slugify a title for filename comparison.
|
|
74
|
+
* "The Behring Gambit" → "the-behring-gambit"
|
|
75
|
+
*/
|
|
76
|
+
function slugify(text: string): string {
|
|
77
|
+
return text
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^\w\s-]/g, '')
|
|
80
|
+
.replace(/\s+/g, '-')
|
|
81
|
+
.replace(/-+/g, '-')
|
|
82
|
+
.replace(/^-|-$/g, '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract plain text from a block's contents and title.
|
|
87
|
+
*/
|
|
88
|
+
function blockToPlainText(block: Block): string {
|
|
89
|
+
const parts: string[] = [];
|
|
90
|
+
if (block.title) parts.push(block.title);
|
|
91
|
+
if (block.contents) {
|
|
92
|
+
for (const node of block.contents) {
|
|
93
|
+
parts.push(extractPlainText(node));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return parts.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Flatten a block tree into a linear array of leaf blocks
|
|
101
|
+
* (blocks that represent actual content sections).
|
|
102
|
+
*/
|
|
103
|
+
function flattenForAudio(blocks: Block[]): Block[] {
|
|
104
|
+
const result: Block[] = [];
|
|
105
|
+
for (const block of blocks) {
|
|
106
|
+
if (block.children && block.children.length > 0) {
|
|
107
|
+
result.push(...flattenForAudio(block.children));
|
|
108
|
+
} else {
|
|
109
|
+
result.push(block);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Info about an MP3 file found in the container. */
|
|
116
|
+
interface Mp3Info {
|
|
117
|
+
/** Path in the container (e.g., "images/c23n-issaquah-highlands-intro.mp3") */
|
|
118
|
+
path: string;
|
|
119
|
+
/** Filename without path (e.g., "c23n-issaquah-highlands-intro.mp3") */
|
|
120
|
+
filename: string;
|
|
121
|
+
/** Parsed timing data (if .timing.json exists) */
|
|
122
|
+
timing: AudioTimingData | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Core Functions ───────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Discover all MP3 files in a container and load their timing data.
|
|
129
|
+
*/
|
|
130
|
+
async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
|
|
131
|
+
const files = await container.listFiles();
|
|
132
|
+
const mp3Files = files.filter((f) => f.path.endsWith('.mp3'));
|
|
133
|
+
const results: Mp3Info[] = [];
|
|
134
|
+
|
|
135
|
+
for (const file of mp3Files) {
|
|
136
|
+
const filename = file.path.split('/').pop() ?? file.path;
|
|
137
|
+
let timing: AudioTimingData | null = null;
|
|
138
|
+
|
|
139
|
+
// Try to load timing.json
|
|
140
|
+
const timingPath = `${file.path}.timing.json`;
|
|
141
|
+
const timingData = await container.readFile(timingPath);
|
|
142
|
+
if (timingData) {
|
|
143
|
+
timing = parseTimingJson(timingData);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
results.push({ path: file.path, filename, timing });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return results;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve audio from explicit `{[audio=filename.mp3]}` annotations on blocks.
|
|
154
|
+
* Returns the matched pairs or an empty array if no annotations found.
|
|
155
|
+
*/
|
|
156
|
+
function resolveFromAnnotations(
|
|
157
|
+
blocks: Block[],
|
|
158
|
+
mp3s: Mp3Info[],
|
|
159
|
+
): Array<{ block: Block; mp3: Mp3Info }> {
|
|
160
|
+
const matches: Array<{ block: Block; mp3: Mp3Info }> = [];
|
|
161
|
+
const flat = flattenForAudio(blocks);
|
|
162
|
+
|
|
163
|
+
for (const block of flat) {
|
|
164
|
+
const audioRef = block.templateOverrides?.audio;
|
|
165
|
+
if (!audioRef) continue;
|
|
166
|
+
|
|
167
|
+
// Find the MP3 by filename (exact or suffix match)
|
|
168
|
+
const mp3 = mp3s.find(
|
|
169
|
+
(m) => m.filename === audioRef || m.path === audioRef || m.path.endsWith(`/${audioRef}`),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (mp3) {
|
|
173
|
+
matches.push({ block, mp3 });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return matches;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Minimum similarity score to consider a content match. */
|
|
181
|
+
const MIN_CONTENT_SIMILARITY = 0.35;
|
|
182
|
+
|
|
183
|
+
/** Minimum similarity score for filename-based matching. */
|
|
184
|
+
const MIN_FILENAME_SIMILARITY = 0.5;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Auto-match MP3 files to blocks by content similarity.
|
|
188
|
+
* Uses timing.json sourceText when available, falls back to filename matching.
|
|
189
|
+
*/
|
|
190
|
+
function autoMatchBlocks(
|
|
191
|
+
blocks: Block[],
|
|
192
|
+
mp3s: Mp3Info[],
|
|
193
|
+
): Array<{ block: Block; mp3: Mp3Info; score: number }> {
|
|
194
|
+
const flat = flattenForAudio(blocks);
|
|
195
|
+
if (flat.length === 0 || mp3s.length === 0) return [];
|
|
196
|
+
|
|
197
|
+
// Score all (mp3, block) pairs
|
|
198
|
+
const scoredPairs: Array<{
|
|
199
|
+
block: Block;
|
|
200
|
+
blockIndex: number;
|
|
201
|
+
mp3: Mp3Info;
|
|
202
|
+
mp3Index: number;
|
|
203
|
+
score: number;
|
|
204
|
+
}> = [];
|
|
205
|
+
|
|
206
|
+
for (let mi = 0; mi < mp3s.length; mi++) {
|
|
207
|
+
const mp3 = mp3s[mi];
|
|
208
|
+
|
|
209
|
+
for (let bi = 0; bi < flat.length; bi++) {
|
|
210
|
+
const block = flat[bi];
|
|
211
|
+
const blockText = blockToPlainText(block);
|
|
212
|
+
if (!blockText.trim()) continue;
|
|
213
|
+
|
|
214
|
+
let score = 0;
|
|
215
|
+
|
|
216
|
+
// Primary: content similarity via timing.json sourceText
|
|
217
|
+
if (mp3.timing?.sourceText) {
|
|
218
|
+
score = scoreTextSimilarity(mp3.timing.sourceText, blockText);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fallback: filename-based matching
|
|
222
|
+
if (score < MIN_CONTENT_SIMILARITY && block.title) {
|
|
223
|
+
const titleSlug = slugify(block.title);
|
|
224
|
+
const fileSlug = slugify(mp3.filename.replace(/\.mp3$/, ''));
|
|
225
|
+
// Check if the title slug appears in the filename slug
|
|
226
|
+
if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
|
|
227
|
+
score = Math.max(score, MIN_FILENAME_SIMILARITY + 0.1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (score >= MIN_CONTENT_SIMILARITY) {
|
|
232
|
+
scoredPairs.push({ block, blockIndex: bi, mp3, mp3Index: mi, score });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Greedy assignment: highest score first, each mp3 and block used at most once
|
|
238
|
+
scoredPairs.sort((a, b) => b.score - a.score);
|
|
239
|
+
const usedBlocks = new Set<number>();
|
|
240
|
+
const usedMp3s = new Set<number>();
|
|
241
|
+
const matches: Array<{ block: Block; mp3: Mp3Info; score: number }> = [];
|
|
242
|
+
|
|
243
|
+
for (const pair of scoredPairs) {
|
|
244
|
+
if (usedBlocks.has(pair.blockIndex) || usedMp3s.has(pair.mp3Index)) continue;
|
|
245
|
+
usedBlocks.add(pair.blockIndex);
|
|
246
|
+
usedMp3s.add(pair.mp3Index);
|
|
247
|
+
matches.push({ block: pair.block, mp3: pair.mp3, score: pair.score });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return matches;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolve audio mapping for a Doc using a ContentContainer.
|
|
257
|
+
*
|
|
258
|
+
* Tries explicit `{[audio=filename.mp3]}` annotations first,
|
|
259
|
+
* then falls back to auto-matching by content similarity.
|
|
260
|
+
*
|
|
261
|
+
* Returns a new Doc with `audio.segments` populated and blocks
|
|
262
|
+
* assigned to the correct `audioSegment` indices.
|
|
263
|
+
*
|
|
264
|
+
* @param doc - The source Doc (not mutated).
|
|
265
|
+
* @param container - ContentContainer with MP3 and timing files.
|
|
266
|
+
* @returns New Doc with audio segments resolved, or original doc if no matches.
|
|
267
|
+
*/
|
|
268
|
+
export async function resolveAudioMapping(doc: Doc, container: ContentContainer): Promise<Doc> {
|
|
269
|
+
// Discover MP3 files and their timing data
|
|
270
|
+
const mp3s = await discoverMp3s(container);
|
|
271
|
+
if (mp3s.length === 0) return doc;
|
|
272
|
+
|
|
273
|
+
// Try annotations first
|
|
274
|
+
let matches = resolveFromAnnotations(doc.blocks, mp3s);
|
|
275
|
+
|
|
276
|
+
// Fall back to auto-matching if no annotations found
|
|
277
|
+
if (matches.length === 0) {
|
|
278
|
+
matches = autoMatchBlocks(doc.blocks, mp3s).map((m) => ({
|
|
279
|
+
block: m.block,
|
|
280
|
+
mp3: m.mp3,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (matches.length === 0) return doc;
|
|
285
|
+
|
|
286
|
+
// Build audio segments in document order
|
|
287
|
+
// Sort matches by the block's position in the flattened tree
|
|
288
|
+
const flat = flattenForAudio(doc.blocks);
|
|
289
|
+
const blockOrder = new Map(flat.map((b, i) => [b.id, i]));
|
|
290
|
+
matches.sort((a, b) => (blockOrder.get(a.block.id) ?? 0) - (blockOrder.get(b.block.id) ?? 0));
|
|
291
|
+
|
|
292
|
+
// Build segment list and assign indices
|
|
293
|
+
const segments: AudioSegment[] = [];
|
|
294
|
+
const blockToSegment = new Map<string, number>();
|
|
295
|
+
let startTime = 0;
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < matches.length; i++) {
|
|
298
|
+
const { block, mp3 } = matches[i];
|
|
299
|
+
const duration = mp3.timing?.duration ?? 30; // Fallback duration if no timing
|
|
300
|
+
|
|
301
|
+
segments.push({
|
|
302
|
+
src: mp3.path,
|
|
303
|
+
name: block.title ?? block.id,
|
|
304
|
+
duration,
|
|
305
|
+
startTime,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
blockToSegment.set(block.id, i);
|
|
309
|
+
startTime += duration;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Deep clone blocks and assign audioSegment indices
|
|
313
|
+
const newBlocks = assignSegmentIndices(doc.blocks, blockToSegment);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
...doc,
|
|
317
|
+
blocks: newBlocks,
|
|
318
|
+
duration: startTime,
|
|
319
|
+
audio: { segments },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Recursively clone blocks and assign audioSegment indices.
|
|
325
|
+
* Blocks not in the map keep their original audioSegment.
|
|
326
|
+
* Children of a matched block inherit the parent's segment.
|
|
327
|
+
*/
|
|
328
|
+
function assignSegmentIndices(
|
|
329
|
+
blocks: Block[],
|
|
330
|
+
blockToSegment: Map<string, number>,
|
|
331
|
+
inheritedSegment?: number,
|
|
332
|
+
): Block[] {
|
|
333
|
+
return blocks.map((block) => {
|
|
334
|
+
const segment = blockToSegment.get(block.id) ?? inheritedSegment ?? block.audioSegment;
|
|
335
|
+
const newBlock: Block = {
|
|
336
|
+
...block,
|
|
337
|
+
audioSegment: segment,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (block.children) {
|
|
341
|
+
newBlock.children = assignSegmentIndices(block.children, blockToSegment, segment);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return newBlock;
|
|
345
|
+
});
|
|
346
|
+
}
|
package/src/doc/index.ts
CHANGED
|
@@ -6,3 +6,4 @@ export type { MarkdownToDocOptions } from './markdownToDoc.js';
|
|
|
6
6
|
export { docToMarkdown } from './docToMarkdown.js';
|
|
7
7
|
export { getLayers } from './getLayers.js';
|
|
8
8
|
export type { RenderContext } from './getLayers.js';
|
|
9
|
+
export { resolveAudioMapping, scoreTextSimilarity } from './audioMapping.js';
|
package/src/doc/markdownToDoc.ts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type { Doc, Block } from '../schemas/Doc.js';
|
|
25
|
+
import type { Doc, Block, CaptionTrack, CaptionPhrase } from '../schemas/Doc.js';
|
|
26
26
|
import type { MarkdownDocument, MarkdownBlockNode, MarkdownHeading } from '../markdown/types.js';
|
|
27
27
|
import { extractPlainText } from '../markdown/utils.js';
|
|
28
|
+
import { estimateReadingTime } from '../timing/readingTime.js';
|
|
28
29
|
|
|
29
30
|
// ============================================
|
|
30
31
|
// Options
|
|
@@ -141,6 +142,10 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
141
142
|
const annotation = heading?.templateAnnotation;
|
|
142
143
|
const template = annotation?.template ?? (heading ? defaultTemplate : undefined);
|
|
143
144
|
|
|
145
|
+
// Extract heading text so templates (e.g. sectionHeader) that expect a
|
|
146
|
+
// `title` property receive it without having to reach into sourceHeading.
|
|
147
|
+
const title = heading ? extractPlainText(heading) : undefined;
|
|
148
|
+
|
|
144
149
|
const block: Block = {
|
|
145
150
|
id,
|
|
146
151
|
startTime: 0,
|
|
@@ -148,6 +153,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
148
153
|
audioSegment: 0,
|
|
149
154
|
template,
|
|
150
155
|
...(heading ? { sourceHeading: heading } : {}),
|
|
156
|
+
...(title ? { title } : {}),
|
|
151
157
|
};
|
|
152
158
|
|
|
153
159
|
// Propagate key-value params from annotation to templateOverrides
|
|
@@ -217,14 +223,50 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
217
223
|
rootBlocks.push(currentBlock);
|
|
218
224
|
}
|
|
219
225
|
|
|
220
|
-
// Calculate
|
|
226
|
+
// Calculate reading-time-based durations and generate captions
|
|
221
227
|
const allBlocks = flattenBlocks(rootBlocks);
|
|
228
|
+
const minDuration = 3; // seconds — minimum for blocks with little/no text
|
|
229
|
+
const phrases: CaptionPhrase[] = [];
|
|
230
|
+
|
|
231
|
+
// First pass: compute duration from body-content reading time
|
|
232
|
+
for (const block of allBlocks) {
|
|
233
|
+
const bodyText = getBlockBodyText(block);
|
|
234
|
+
if (bodyText.length > 0) {
|
|
235
|
+
const estimate = estimateReadingTime(bodyText);
|
|
236
|
+
block.duration = Math.max(minDuration, estimate.seconds);
|
|
237
|
+
} else {
|
|
238
|
+
block.duration = defaultDuration;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Second pass: assign start times sequentially and build caption phrases
|
|
222
243
|
let currentTime = 0;
|
|
223
244
|
for (const block of allBlocks) {
|
|
224
245
|
block.startTime = currentTime;
|
|
246
|
+
|
|
247
|
+
// Generate caption phrases from the block's body content
|
|
248
|
+
const bodyText = getBlockBodyText(block);
|
|
249
|
+
if (bodyText.length > 0) {
|
|
250
|
+
const sentences = splitIntoSentences(bodyText);
|
|
251
|
+
if (sentences.length > 0) {
|
|
252
|
+
const timePerSentence = block.duration / sentences.length;
|
|
253
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
254
|
+
phrases.push({
|
|
255
|
+
text: sentences[i],
|
|
256
|
+
startTime: currentTime + i * timePerSentence,
|
|
257
|
+
endTime: currentTime + (i + 1) * timePerSentence,
|
|
258
|
+
audioSegment: 0,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
225
264
|
currentTime += block.duration;
|
|
226
265
|
}
|
|
227
266
|
|
|
267
|
+
const captions: CaptionTrack | undefined =
|
|
268
|
+
phrases.length > 0 ? { phrases, generatedAt: new Date().toISOString(), version: 1 } : undefined;
|
|
269
|
+
|
|
228
270
|
return {
|
|
229
271
|
articleId,
|
|
230
272
|
duration: currentTime,
|
|
@@ -232,6 +274,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
232
274
|
audio: {
|
|
233
275
|
segments: [],
|
|
234
276
|
},
|
|
277
|
+
...(captions ? { captions } : {}),
|
|
235
278
|
...(markdownDoc.frontmatter ? { frontmatter: markdownDoc.frontmatter } : {}),
|
|
236
279
|
};
|
|
237
280
|
}
|
|
@@ -275,3 +318,94 @@ export function countBlocks(blocks: Block[]): number {
|
|
|
275
318
|
export function getBlockDepth(block: Block): number {
|
|
276
319
|
return block.sourceHeading?.depth ?? 0;
|
|
277
320
|
}
|
|
321
|
+
|
|
322
|
+
// ============================================
|
|
323
|
+
// Internal helpers
|
|
324
|
+
// ============================================
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extract the plain text from a block's body contents (excluding heading text).
|
|
328
|
+
*/
|
|
329
|
+
function getBlockBodyText(block: Block): string {
|
|
330
|
+
if (!block.contents || block.contents.length === 0) return '';
|
|
331
|
+
// Join with newlines to preserve paragraph/list-item boundaries.
|
|
332
|
+
// splitIntoPhrases uses these newlines as natural split points.
|
|
333
|
+
return block.contents
|
|
334
|
+
.map((node) => extractPlainText(node))
|
|
335
|
+
.join('\n')
|
|
336
|
+
.trim();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Maximum words per caption phrase. Long sentences get split at this point. */
|
|
340
|
+
const MAX_PHRASE_WORDS = 12;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Split text into caption-sized phrases.
|
|
344
|
+
*
|
|
345
|
+
* Splits on:
|
|
346
|
+
* 1. Newlines (paragraph/list-item boundaries from markdown)
|
|
347
|
+
* 2. Sentence endings (.!?) followed by whitespace
|
|
348
|
+
* 3. Long fragments (> MAX_PHRASE_WORDS) at clause boundaries (commas, semicolons, dashes)
|
|
349
|
+
*
|
|
350
|
+
* Merges very short fragments (< 15 chars) with the previous phrase.
|
|
351
|
+
*/
|
|
352
|
+
function splitIntoSentences(text: string): string[] {
|
|
353
|
+
// First split on newlines (each paragraph/list item becomes separate)
|
|
354
|
+
const lines = text
|
|
355
|
+
.split(/\n+/)
|
|
356
|
+
.map((s) => s.trim())
|
|
357
|
+
.filter((s) => s.length > 0);
|
|
358
|
+
|
|
359
|
+
// Then split each line on sentence boundaries
|
|
360
|
+
const sentenceRe = /(?<=[.!?])\s+/;
|
|
361
|
+
const fragments: string[] = [];
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
const sentences = line
|
|
364
|
+
.split(sentenceRe)
|
|
365
|
+
.map((s) => s.trim())
|
|
366
|
+
.filter((s) => s.length > 0);
|
|
367
|
+
fragments.push(...sentences);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (fragments.length === 0) return [];
|
|
371
|
+
|
|
372
|
+
// Split long fragments at clause boundaries
|
|
373
|
+
const split: string[] = [];
|
|
374
|
+
for (const frag of fragments) {
|
|
375
|
+
const words = frag.split(/\s+/);
|
|
376
|
+
if (words.length > MAX_PHRASE_WORDS) {
|
|
377
|
+
// Try to split at comma, semicolon, or dash near the midpoint
|
|
378
|
+
const mid = Math.floor(frag.length / 2);
|
|
379
|
+
const clauseRe = /[,;]\s+|\s+—\s+|\s+-\s+/g;
|
|
380
|
+
let bestSplit = -1;
|
|
381
|
+
let bestDist = Infinity;
|
|
382
|
+
let match;
|
|
383
|
+
while ((match = clauseRe.exec(frag)) !== null) {
|
|
384
|
+
const dist = Math.abs(match.index - mid);
|
|
385
|
+
if (dist < bestDist) {
|
|
386
|
+
bestDist = dist;
|
|
387
|
+
bestSplit = match.index + match[0].length;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (bestSplit > 0 && bestSplit < frag.length - 5) {
|
|
391
|
+
split.push(frag.slice(0, bestSplit).trim());
|
|
392
|
+
split.push(frag.slice(bestSplit).trim());
|
|
393
|
+
} else {
|
|
394
|
+
split.push(frag);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
split.push(frag);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Merge very short fragments (< 15 chars) with the previous phrase
|
|
402
|
+
const merged: string[] = [split[0]];
|
|
403
|
+
for (let i = 1; i < split.length; i++) {
|
|
404
|
+
if (split[i].length < 15 && merged.length > 0) {
|
|
405
|
+
merged[merged.length - 1] += ' ' + split[i];
|
|
406
|
+
} else {
|
|
407
|
+
merged.push(split[i]);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return merged;
|
|
411
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { ComparisonBarInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
13
|
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
14
|
-
import { resolveColorScheme } from '../utils/themeUtils.js';
|
|
14
|
+
import { resolveColorScheme, getThemeFont } from '../utils/themeUtils.js';
|
|
15
15
|
|
|
16
16
|
export function comparisonBar(input: ComparisonBarInput, context: TemplateContext): Layer[] {
|
|
17
17
|
const { leftLabel, leftValue, rightLabel, rightValue, unit, colorScheme = 'blue' } = input;
|
|
@@ -61,7 +61,11 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
61
61
|
id: 'left-label',
|
|
62
62
|
content: {
|
|
63
63
|
text: leftLabel,
|
|
64
|
-
style: {
|
|
64
|
+
style: {
|
|
65
|
+
fontSize: labelFontSize,
|
|
66
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
67
|
+
color: theme.colors.textMuted,
|
|
68
|
+
},
|
|
65
69
|
},
|
|
66
70
|
position: { x: `${barStartX}%`, y: `${topBarY - 8}%` },
|
|
67
71
|
},
|
|
@@ -72,7 +76,12 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
72
76
|
id: 'left-value',
|
|
73
77
|
content: {
|
|
74
78
|
text: leftDisplay,
|
|
75
|
-
style: {
|
|
79
|
+
style: {
|
|
80
|
+
fontSize: valueFontSize,
|
|
81
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
82
|
+
fontWeight: 'bold',
|
|
83
|
+
color: colors.text,
|
|
84
|
+
},
|
|
76
85
|
},
|
|
77
86
|
position: { x: `${barStartX + leftBarWidth + 2}%`, y: `${topBarY - 1}%` },
|
|
78
87
|
animation: { type: 'fadeIn', duration: 0.8, delay: 0.3 },
|
|
@@ -102,7 +111,11 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
102
111
|
id: 'right-label',
|
|
103
112
|
content: {
|
|
104
113
|
text: rightLabel,
|
|
105
|
-
style: {
|
|
114
|
+
style: {
|
|
115
|
+
fontSize: labelFontSize,
|
|
116
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
117
|
+
color: theme.colors.textMuted,
|
|
118
|
+
},
|
|
106
119
|
},
|
|
107
120
|
position: { x: `${barStartX}%`, y: `${bottomBarY - 8}%` },
|
|
108
121
|
},
|
|
@@ -113,7 +126,12 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
113
126
|
id: 'right-value',
|
|
114
127
|
content: {
|
|
115
128
|
text: rightDisplay,
|
|
116
|
-
style: {
|
|
129
|
+
style: {
|
|
130
|
+
fontSize: valueFontSize,
|
|
131
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
132
|
+
fontWeight: 'bold',
|
|
133
|
+
color: colors.accent,
|
|
134
|
+
},
|
|
117
135
|
},
|
|
118
136
|
position: { x: `${barStartX + rightBarWidth + 2}%`, y: `${bottomBarY - 1}%` },
|
|
119
137
|
animation: { type: 'fadeIn', duration: 0.8, delay: 0.6 },
|
|
@@ -18,6 +18,7 @@ import type { Layer } from '../../schemas/Doc.js';
|
|
|
18
18
|
import type { TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
19
|
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
20
20
|
import type { StartBlockConfig } from '../../schemas/Doc.js';
|
|
21
|
+
import { getThemeFont } from '../utils/themeUtils.js';
|
|
21
22
|
import { mapAmbientMotion } from './accentImage.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -87,6 +88,7 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
87
88
|
text: title,
|
|
88
89
|
style: {
|
|
89
90
|
fontSize: titleFontSize,
|
|
91
|
+
fontFamily: getThemeFont(context, 'title'),
|
|
90
92
|
fontWeight: 'bold',
|
|
91
93
|
color: theme.colors.text,
|
|
92
94
|
textAlign: 'center',
|
|
@@ -113,6 +115,7 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
113
115
|
text: subtitle,
|
|
114
116
|
style: {
|
|
115
117
|
fontSize: subtitleFontSize,
|
|
118
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
116
119
|
color: theme.colors.textMuted,
|
|
117
120
|
textAlign: 'center',
|
|
118
121
|
lineHeight: 1.5,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { Layer } from '../../schemas/Doc.js';
|
|
14
14
|
import type { DateEventInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
15
|
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
16
|
+
import { getThemeFont } from '../utils/themeUtils.js';
|
|
16
17
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
17
18
|
|
|
18
19
|
const MOOD_COLORS = {
|
|
@@ -60,6 +61,7 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
60
61
|
text: date,
|
|
61
62
|
style: {
|
|
62
63
|
fontSize: dateFontSize,
|
|
64
|
+
fontFamily: getThemeFont(context, 'title'),
|
|
63
65
|
fontWeight: 'bold',
|
|
64
66
|
color: colors.date,
|
|
65
67
|
shadow: !!accentImage,
|
|
@@ -81,6 +83,7 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
81
83
|
text: description,
|
|
82
84
|
style: {
|
|
83
85
|
fontSize: descFontSize,
|
|
86
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
84
87
|
color: colors.text,
|
|
85
88
|
textAlign: 'center',
|
|
86
89
|
lineHeight: 1.8,
|
|
@@ -105,6 +108,7 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
105
108
|
text: footer,
|
|
106
109
|
style: {
|
|
107
110
|
fontSize: footerFontSize,
|
|
111
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
108
112
|
color: theme.colors.textMuted,
|
|
109
113
|
shadow: !!accentImage,
|
|
110
114
|
},
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { DefinitionCardInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
13
|
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
14
|
-
import { resolveColorScheme } from '../utils/themeUtils.js';
|
|
14
|
+
import { resolveColorScheme, getThemeFont } from '../utils/themeUtils.js';
|
|
15
15
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
16
16
|
import { createBackgroundLayer } from './captionUtils.js';
|
|
17
17
|
|
|
@@ -47,6 +47,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
47
47
|
text: term,
|
|
48
48
|
style: {
|
|
49
49
|
fontSize: termFontSize,
|
|
50
|
+
fontFamily: getThemeFont(context, 'title'),
|
|
50
51
|
fontWeight: 'bold',
|
|
51
52
|
color: colors.text,
|
|
52
53
|
textAlign: 'center',
|
|
@@ -86,6 +87,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
86
87
|
text: definition,
|
|
87
88
|
style: {
|
|
88
89
|
fontSize: defFontSize,
|
|
90
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
89
91
|
color: theme.colors.text,
|
|
90
92
|
textAlign: 'center',
|
|
91
93
|
lineHeight: 1.6,
|
|
@@ -111,6 +113,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
111
113
|
text: origin,
|
|
112
114
|
style: {
|
|
113
115
|
fontSize: originFontSize,
|
|
116
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
114
117
|
color: theme.colors.textMuted,
|
|
115
118
|
textAlign: 'center',
|
|
116
119
|
shadow: !!accentImage,
|