@sutrata/editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/LICENSE +674 -0
  2. package/dist/ai/OnboardingDialog.d.ts +6 -0
  3. package/dist/ai/OnboardingDialog.js +115 -0
  4. package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
  5. package/dist/ai/VoiceConfirmDialog.js +37 -0
  6. package/dist/ai/VoiceToolbar.d.ts +2 -0
  7. package/dist/ai/VoiceToolbar.js +177 -0
  8. package/dist/ai/ai-client.d.ts +71 -0
  9. package/dist/ai/ai-client.js +494 -0
  10. package/dist/ai/commands.d.ts +12 -0
  11. package/dist/ai/commands.js +140 -0
  12. package/dist/ai/generated-providers.json +3696 -0
  13. package/dist/ai/prompts.d.ts +25 -0
  14. package/dist/ai/prompts.js +59 -0
  15. package/dist/ai/voice-service.d.ts +15 -0
  16. package/dist/ai/voice-service.js +81 -0
  17. package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
  18. package/dist/autocomplete/AutocompletePlugin.js +19 -0
  19. package/dist/autocomplete/suggestions.d.ts +25 -0
  20. package/dist/autocomplete/suggestions.js +126 -0
  21. package/dist/context/DocumentContext.d.ts +68 -0
  22. package/dist/context/DocumentContext.js +605 -0
  23. package/dist/desktop-api.d.ts +40 -0
  24. package/dist/editor/EditorView.d.ts +2 -0
  25. package/dist/editor/EditorView.js +78 -0
  26. package/dist/editor/SelectionMenu.d.ts +2 -0
  27. package/dist/editor/SelectionMenu.js +102 -0
  28. package/dist/editor/attr-pencil-plugin.d.ts +17 -0
  29. package/dist/editor/attr-pencil-plugin.js +158 -0
  30. package/dist/editor/character-decoration-plugin.d.ts +26 -0
  31. package/dist/editor/character-decoration-plugin.js +77 -0
  32. package/dist/editor/character-view.d.ts +35 -0
  33. package/dist/editor/character-view.js +112 -0
  34. package/dist/editor/editor-bus.d.ts +14 -0
  35. package/dist/editor/editor-bus.js +39 -0
  36. package/dist/editor/find-highlight-plugin.d.ts +7 -0
  37. package/dist/editor/find-highlight-plugin.js +57 -0
  38. package/dist/editor/frontmatter-field-view.d.ts +10 -0
  39. package/dist/editor/frontmatter-field-view.js +48 -0
  40. package/dist/editor/frontmatter-field.d.ts +21 -0
  41. package/dist/editor/frontmatter-field.js +116 -0
  42. package/dist/editor/grapheme-cursor.d.ts +10 -0
  43. package/dist/editor/grapheme-cursor.js +104 -0
  44. package/dist/editor/ime-plugin.d.ts +18 -0
  45. package/dist/editor/ime-plugin.js +71 -0
  46. package/dist/editor/insert-helper.d.ts +1 -0
  47. package/dist/editor/insert-helper.js +42 -0
  48. package/dist/editor/keymap.d.ts +3 -0
  49. package/dist/editor/keymap.js +131 -0
  50. package/dist/editor/language-class-plugin.d.ts +7 -0
  51. package/dist/editor/language-class-plugin.js +75 -0
  52. package/dist/editor/plugins.d.ts +1 -0
  53. package/dist/editor/plugins.js +25 -0
  54. package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
  55. package/dist/editor/prosemirror-to-sutra.js +208 -0
  56. package/dist/editor/scene-heading-view.d.ts +27 -0
  57. package/dist/editor/scene-heading-view.js +136 -0
  58. package/dist/editor/schema.d.ts +3 -0
  59. package/dist/editor/schema.js +135 -0
  60. package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
  61. package/dist/editor/sutra-to-prosemirror.js +154 -0
  62. package/dist/editor/transliterate-input.d.ts +10 -0
  63. package/dist/editor/transliterate-input.js +88 -0
  64. package/dist/extensions/storage-adapter.d.ts +59 -0
  65. package/dist/extensions/storage-adapter.js +9 -0
  66. package/dist/extensions/types.d.ts +73 -0
  67. package/dist/extensions/types.js +12 -0
  68. package/dist/file/docx-exporter.d.ts +8 -0
  69. package/dist/file/docx-exporter.js +494 -0
  70. package/dist/file/docx-importer.d.ts +10 -0
  71. package/dist/file/docx-importer.js +283 -0
  72. package/dist/file/print-paginator.d.ts +29 -0
  73. package/dist/file/print-paginator.js +289 -0
  74. package/dist/file/templates.d.ts +2 -0
  75. package/dist/file/templates.js +66 -0
  76. package/dist/file/workflow-reports.d.ts +34 -0
  77. package/dist/file/workflow-reports.js +719 -0
  78. package/dist/findreplace/FindReplace.d.ts +2 -0
  79. package/dist/findreplace/FindReplace.js +164 -0
  80. package/dist/findreplace/find-engine.d.ts +29 -0
  81. package/dist/findreplace/find-engine.js +69 -0
  82. package/dist/findreplace/scroll-to-match.d.ts +29 -0
  83. package/dist/findreplace/scroll-to-match.js +74 -0
  84. package/dist/fonts/tofu-detect.d.ts +1 -0
  85. package/dist/fonts/tofu-detect.js +48 -0
  86. package/dist/i18n/LanguageContext.d.ts +10 -0
  87. package/dist/i18n/LanguageContext.js +174 -0
  88. package/dist/i18n/language-detect.d.ts +1 -0
  89. package/dist/i18n/language-detect.js +58 -0
  90. package/dist/i18n/useTranslation.d.ts +11 -0
  91. package/dist/i18n/useTranslation.js +60 -0
  92. package/dist/index.d.ts +11 -0
  93. package/dist/index.js +5 -0
  94. package/dist/locales/bn.json +143 -0
  95. package/dist/locales/en.json +143 -0
  96. package/dist/locales/gu.json +143 -0
  97. package/dist/locales/hi.json +143 -0
  98. package/dist/locales/kn.json +143 -0
  99. package/dist/locales/ml.json +143 -0
  100. package/dist/locales/mr.json +143 -0
  101. package/dist/locales/or.json +143 -0
  102. package/dist/locales/pa.json +143 -0
  103. package/dist/locales/si.json +143 -0
  104. package/dist/locales/ta.json +143 -0
  105. package/dist/locales/te.json +143 -0
  106. package/dist/navigator/SceneNavigator.d.ts +2 -0
  107. package/dist/navigator/SceneNavigator.js +612 -0
  108. package/dist/navigator/StatisticsDialog.d.ts +6 -0
  109. package/dist/navigator/StatisticsDialog.js +146 -0
  110. package/dist/navigator/scene-list.d.ts +16 -0
  111. package/dist/navigator/scene-list.js +70 -0
  112. package/dist/settings/SettingsDialog.d.ts +6 -0
  113. package/dist/settings/SettingsDialog.js +147 -0
  114. package/dist/shell/AppBar.d.ts +2 -0
  115. package/dist/shell/AppBar.js +34 -0
  116. package/dist/shell/AppShell.d.ts +2 -0
  117. package/dist/shell/AppShell.js +30 -0
  118. package/dist/shell/ConfirmDialog.d.ts +16 -0
  119. package/dist/shell/ConfirmDialog.js +23 -0
  120. package/dist/shell/ElementToolbar.d.ts +2 -0
  121. package/dist/shell/ElementToolbar.js +30 -0
  122. package/dist/shell/LanguageStatus.d.ts +2 -0
  123. package/dist/shell/LanguageStatus.js +65 -0
  124. package/dist/shell/StatusBar.d.ts +2 -0
  125. package/dist/shell/StatusBar.js +54 -0
  126. package/dist/shell/SutrataLogo.d.ts +8 -0
  127. package/dist/shell/SutrataLogo.js +7 -0
  128. package/dist/shell/Toast.d.ts +12 -0
  129. package/dist/shell/Toast.js +21 -0
  130. package/dist/shell/ToolbarButton.d.ts +11 -0
  131. package/dist/shell/ToolbarButton.js +4 -0
  132. package/dist/shell/VersionHistory.d.ts +12 -0
  133. package/dist/shell/VersionHistory.js +89 -0
  134. package/dist/shell/Watermark.d.ts +3 -0
  135. package/dist/shell/Watermark.js +10 -0
  136. package/dist/shell/fonts/fonts.css +1133 -0
  137. package/dist/shell/icons.d.ts +45 -0
  138. package/dist/shell/icons.js +51 -0
  139. package/dist/shell/toolbar-actions.d.ts +6 -0
  140. package/dist/shell/toolbar-actions.js +58 -0
  141. package/dist/source/SourceView.d.ts +1 -0
  142. package/dist/source/SourceView.js +106 -0
  143. package/dist/source/find-highlight-source.d.ts +14 -0
  144. package/dist/source/find-highlight-source.js +47 -0
  145. package/dist/source/sutra-lang.d.ts +2 -0
  146. package/dist/source/sutra-lang.js +97 -0
  147. package/dist/spellcheck/dictionaries.d.ts +9 -0
  148. package/dist/spellcheck/dictionaries.js +16 -0
  149. package/dist/spellcheck/hunspell.d.ts +23 -0
  150. package/dist/spellcheck/hunspell.js +94 -0
  151. package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
  152. package/dist/spellcheck/spellcheck-plugin.js +75 -0
  153. package/dist/styles/StyleDialog.d.ts +6 -0
  154. package/dist/styles/StyleDialog.js +63 -0
  155. package/dist/styles/builtin-styles.d.ts +43 -0
  156. package/dist/styles/builtin-styles.js +131 -0
  157. package/dist/styles/css-adapter.d.ts +32 -0
  158. package/dist/styles/css-adapter.js +71 -0
  159. package/dist/styles/docx-adapter.d.ts +7 -0
  160. package/dist/styles/docx-adapter.js +84 -0
  161. package/dist/styles/embed-fonts.d.ts +20 -0
  162. package/dist/styles/embed-fonts.js +81 -0
  163. package/dist/styles/pagination.d.ts +19 -0
  164. package/dist/styles/pagination.js +22 -0
  165. package/dist/styles/print-adapter.d.ts +6 -0
  166. package/dist/styles/print-adapter.js +288 -0
  167. package/dist/styles/registry.d.ts +6 -0
  168. package/dist/styles/registry.js +14 -0
  169. package/dist/styles/screenplay.css +3006 -0
  170. package/dist/styles/script-ranges.d.ts +6 -0
  171. package/dist/styles/script-ranges.js +17 -0
  172. package/dist/styles/types.d.ts +93 -0
  173. package/dist/styles/types.js +18 -0
  174. package/dist/styles/validate.d.ts +12 -0
  175. package/dist/styles/validate.js +105 -0
  176. package/dist/templates/templates.d.ts +10 -0
  177. package/dist/templates/templates.js +181 -0
  178. package/dist/titlepage/ExportDialog.d.ts +1 -0
  179. package/dist/titlepage/ExportDialog.js +152 -0
  180. package/dist/titlepage/TitlePageDialog.d.ts +6 -0
  181. package/dist/titlepage/TitlePageDialog.js +96 -0
  182. package/dist/titlepage/frontmatter-form.d.ts +56 -0
  183. package/dist/titlepage/frontmatter-form.js +245 -0
  184. package/dist/titlepage/languages.d.ts +10 -0
  185. package/dist/titlepage/languages.js +31 -0
  186. package/dist/types.d.ts +6 -0
  187. package/dist/types.js +1 -0
  188. package/package.json +47 -0
  189. package/scripts/copy-assets.mjs +16 -0
  190. package/scripts/generate-providers.mjs +127 -0
  191. package/src/ai/OnboardingDialog.tsx +390 -0
  192. package/src/ai/VoiceConfirmDialog.tsx +137 -0
  193. package/src/ai/VoiceToolbar.tsx +321 -0
  194. package/src/ai/ai-client.ts +596 -0
  195. package/src/ai/commands.ts +164 -0
  196. package/src/ai/generated-providers.json +3696 -0
  197. package/src/ai/prompts.ts +62 -0
  198. package/src/ai/voice-service.ts +96 -0
  199. package/src/autocomplete/AutocompletePlugin.ts +21 -0
  200. package/src/autocomplete/suggestions.ts +147 -0
  201. package/src/context/DocumentContext.tsx +648 -0
  202. package/src/desktop-api.d.ts +40 -0
  203. package/src/editor/EditorView.tsx +82 -0
  204. package/src/editor/SelectionMenu.tsx +152 -0
  205. package/src/editor/attr-pencil-plugin.ts +172 -0
  206. package/src/editor/character-decoration-plugin.ts +104 -0
  207. package/src/editor/editor-bus.ts +51 -0
  208. package/src/editor/find-highlight-plugin.ts +68 -0
  209. package/src/editor/frontmatter-field-view.ts +52 -0
  210. package/src/editor/frontmatter-field.ts +115 -0
  211. package/src/editor/grapheme-cursor.ts +99 -0
  212. package/src/editor/ime-plugin.ts +85 -0
  213. package/src/editor/insert-helper.ts +43 -0
  214. package/src/editor/keymap.ts +124 -0
  215. package/src/editor/language-class-plugin.ts +91 -0
  216. package/src/editor/plugins.ts +26 -0
  217. package/src/editor/prosemirror-to-sutra.ts +192 -0
  218. package/src/editor/scene-heading-view.ts +147 -0
  219. package/src/editor/schema.ts +156 -0
  220. package/src/editor/sutra-to-prosemirror.ts +156 -0
  221. package/src/extensions/storage-adapter.ts +65 -0
  222. package/src/extensions/types.ts +60 -0
  223. package/src/file/docx-exporter.ts +585 -0
  224. package/src/file/docx-importer.ts +313 -0
  225. package/src/file/print-paginator.ts +301 -0
  226. package/src/file/templates.ts +73 -0
  227. package/src/file/workflow-reports.ts +820 -0
  228. package/src/findreplace/FindReplace.tsx +242 -0
  229. package/src/findreplace/find-engine.ts +97 -0
  230. package/src/findreplace/scroll-to-match.ts +89 -0
  231. package/src/fonts/tofu-detect.ts +53 -0
  232. package/src/i18n/LanguageContext.tsx +207 -0
  233. package/src/i18n/language-detect.ts +73 -0
  234. package/src/i18n/useTranslation.tsx +74 -0
  235. package/src/index.ts +27 -0
  236. package/src/locales/bn.json +143 -0
  237. package/src/locales/en.json +143 -0
  238. package/src/locales/gu.json +143 -0
  239. package/src/locales/hi.json +143 -0
  240. package/src/locales/kn.json +143 -0
  241. package/src/locales/ml.json +143 -0
  242. package/src/locales/mr.json +143 -0
  243. package/src/locales/or.json +143 -0
  244. package/src/locales/pa.json +143 -0
  245. package/src/locales/si.json +143 -0
  246. package/src/locales/ta.json +143 -0
  247. package/src/locales/te.json +143 -0
  248. package/src/navigator/SceneNavigator.tsx +844 -0
  249. package/src/navigator/StatisticsDialog.tsx +249 -0
  250. package/src/navigator/scene-list.ts +84 -0
  251. package/src/settings/SettingsDialog.tsx +303 -0
  252. package/src/shell/AppBar.tsx +354 -0
  253. package/src/shell/AppShell.tsx +71 -0
  254. package/src/shell/ConfirmDialog.tsx +71 -0
  255. package/src/shell/ElementToolbar.tsx +175 -0
  256. package/src/shell/LanguageStatus.tsx +101 -0
  257. package/src/shell/StatusBar.tsx +135 -0
  258. package/src/shell/SutrataLogo.tsx +89 -0
  259. package/src/shell/Toast.tsx +46 -0
  260. package/src/shell/ToolbarButton.tsx +30 -0
  261. package/src/shell/VersionHistory.tsx +212 -0
  262. package/src/shell/Watermark.tsx +9 -0
  263. package/src/shell/fonts/fonts.css +1133 -0
  264. package/src/shell/icons.tsx +335 -0
  265. package/src/shell/toolbar-actions.ts +59 -0
  266. package/src/source/SourceView.tsx +111 -0
  267. package/src/source/find-highlight-source.ts +57 -0
  268. package/src/source/sutra-lang.ts +121 -0
  269. package/src/spellcheck/dictionaries.ts +21 -0
  270. package/src/spellcheck/hunspell.ts +110 -0
  271. package/src/spellcheck/spellcheck-plugin.ts +83 -0
  272. package/src/styles/StyleDialog.tsx +148 -0
  273. package/src/styles/builtin-styles.ts +139 -0
  274. package/src/styles/css-adapter.ts +76 -0
  275. package/src/styles/docx-adapter.ts +109 -0
  276. package/src/styles/embed-fonts.ts +81 -0
  277. package/src/styles/pagination.ts +27 -0
  278. package/src/styles/print-adapter.ts +288 -0
  279. package/src/styles/registry.ts +20 -0
  280. package/src/styles/screenplay.css +3006 -0
  281. package/src/styles/script-ranges.ts +17 -0
  282. package/src/styles/types.ts +104 -0
  283. package/src/styles/validate.ts +114 -0
  284. package/src/templates/templates.ts +194 -0
  285. package/src/titlepage/ExportDialog.tsx +494 -0
  286. package/src/titlepage/TitlePageDialog.tsx +312 -0
  287. package/src/titlepage/frontmatter-form.ts +280 -0
  288. package/src/titlepage/languages.ts +37 -0
  289. package/src/types.ts +7 -0
  290. package/tests/AppBar.test.tsx +35 -0
  291. package/tests/AppShell.test.tsx +22 -0
  292. package/tests/ElementToolbar.test.tsx +36 -0
  293. package/tests/LanguageStatus.test.tsx +129 -0
  294. package/tests/SelectionMenu.test.tsx +71 -0
  295. package/tests/ToolbarButton.test.tsx +24 -0
  296. package/tests/ai-onboarding.test.ts +102 -0
  297. package/tests/ai-status.test.tsx +72 -0
  298. package/tests/ai-structured.test.ts +94 -0
  299. package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
  300. package/tests/autocomplete/suggestions.test.ts +106 -0
  301. package/tests/document-context.test.tsx +565 -0
  302. package/tests/docx-importer.test.ts +160 -0
  303. package/tests/editor/character-decoration-plugin.test.ts +101 -0
  304. package/tests/editor/character-extension.test.ts +44 -0
  305. package/tests/editor/grapheme-cursor.test.ts +118 -0
  306. package/tests/editor/ime-plugin.test.ts +72 -0
  307. package/tests/editor/inline-note-bridge.test.ts +32 -0
  308. package/tests/editor/language-class-plugin.test.ts +265 -0
  309. package/tests/editor/title-page.test.ts +124 -0
  310. package/tests/embed-fonts.test.ts +17 -0
  311. package/tests/exporters.test.ts +365 -0
  312. package/tests/findreplace/FindReplace.test.tsx +391 -0
  313. package/tests/findreplace/find-engine.test.ts +86 -0
  314. package/tests/findreplace/scroll-to-match.test.ts +50 -0
  315. package/tests/fonts/tofu-detect.test.ts +122 -0
  316. package/tests/i18n/LanguageContext.test.tsx +146 -0
  317. package/tests/i18n/language-detect.test.ts +54 -0
  318. package/tests/i18n/locale-parity.test.ts +25 -0
  319. package/tests/i18n/useTranslation.test.tsx +45 -0
  320. package/tests/keymap.test.ts +18 -0
  321. package/tests/navigator/SceneNavigator.test.tsx +177 -0
  322. package/tests/navigator/StatisticsDialog.test.tsx +63 -0
  323. package/tests/navigator/scene-list.test.ts +71 -0
  324. package/tests/print-paginator.test.ts +82 -0
  325. package/tests/shell/VersionHistory.test.tsx +129 -0
  326. package/tests/spellcheck/hunspell.test.ts +82 -0
  327. package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
  328. package/tests/styles.test.ts +315 -0
  329. package/tests/templates/templates.test.ts +83 -0
  330. package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
  331. package/tests/titlepage/apply-title-page.test.ts +61 -0
  332. package/tests/titlepage/frontmatter-form.test.ts +115 -0
  333. package/tests/toolbar-actions.test.ts +93 -0
  334. package/tests/voice-commands.test.ts +58 -0
  335. package/tsconfig.json +17 -0
  336. package/vitest.config.ts +11 -0
  337. package/vitest.setup.ts +65 -0
@@ -0,0 +1,719 @@
1
+ import { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType, } from 'docx';
2
+ import { resolveStyle } from '../styles/registry';
3
+ import { styleToPrintCss } from '../styles/print-adapter';
4
+ import { embedFontFacesFor } from '../styles/embed-fonts';
5
+ import { SCRIPT_UNICODE_RANGES } from '../styles/script-ranges';
6
+ import { printPaginatorScript } from './print-paginator';
7
+ // CSS absolute units are fixed at 96px/in, print included — so a page box sized in px
8
+ // from the paper's inches lands exactly on one sheet.
9
+ const CSS_PX_PER_IN = 96;
10
+ import { DEFAULT_COVER_LAYOUT } from '../styles/types';
11
+ // CSV Escape Helper
12
+ function esc(val) {
13
+ const str = val ? String(val) : '';
14
+ return `"${str.replace(/"/g, '""')}"`;
15
+ }
16
+ /** Extract all relevant workflow data from AST */
17
+ export function extractWorkflowData(ast) {
18
+ const scenes = [];
19
+ const charMap = new Map();
20
+ // Resolve character registry from frontmatter
21
+ const fmCharacters = ast.frontmatter?.data?.['characters'];
22
+ let sceneCount = 0;
23
+ for (const node of ast.children) {
24
+ if (node.type === 'scene-heading') {
25
+ sceneCount++;
26
+ const rawHeading = node.text.toUpperCase();
27
+ // Find metadata keys
28
+ const numMeta = node.metadata.find(m => m.key === 'number')?.value;
29
+ const sceneNum = numMeta || node.id || String(sceneCount);
30
+ const synopsis = node.metadata.find(m => m.key === 'synopsis')?.value ?? '';
31
+ const actors = node.metadata.find(m => m.key === 'actors')?.value ?? '';
32
+ const status = node.metadata.find(m => m.key === 'status')?.value ?? '';
33
+ const estDuration = node.metadata.find(m => m.key === 'est-duration')?.value ?? '';
34
+ // Collect all shot metadata lines
35
+ const shotLines = node.metadata.filter(m => m.key === 'shots').map(m => m.value);
36
+ scenes.push({
37
+ number: sceneNum,
38
+ heading: rawHeading,
39
+ synopsis,
40
+ actors,
41
+ status,
42
+ estDuration,
43
+ shots: shotLines,
44
+ });
45
+ // Walk this scene's children to find characters
46
+ const sceneLabel = `${sceneNum}: ${rawHeading}`;
47
+ const seenChars = new Set();
48
+ const checkChar = (charName) => {
49
+ const name = charName.trim().toUpperCase();
50
+ if (!name)
51
+ return;
52
+ if (seenChars.has(name))
53
+ return;
54
+ seenChars.add(name);
55
+ if (!charMap.has(name)) {
56
+ charMap.set(name, { sceneCount: 0, scenes: [] });
57
+ }
58
+ const stats = charMap.get(name);
59
+ stats.sceneCount++;
60
+ stats.scenes.push(sceneLabel);
61
+ };
62
+ for (const sc of node.children) {
63
+ if (sc.type === 'character') {
64
+ checkChar(sc.name);
65
+ }
66
+ else if (sc.type === 'dual-dialogue') {
67
+ checkChar(sc.left.name);
68
+ checkChar(sc.right.name);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ // Build character list
74
+ const characters = [];
75
+ for (const [name, stats] of charMap.entries()) {
76
+ const actor = fmCharacters?.[name] || fmCharacters?.[name.toLowerCase()] || '';
77
+ characters.push({
78
+ name,
79
+ actor,
80
+ sceneCount: stats.sceneCount,
81
+ scenes: stats.scenes,
82
+ });
83
+ }
84
+ // Sort characters by occurrence count descending
85
+ characters.sort((a, b) => b.sceneCount - a.sceneCount);
86
+ return { scenes, characters };
87
+ }
88
+ // ---------------------------------------------------------
89
+ // CSV Exporters
90
+ // ---------------------------------------------------------
91
+ export function exportOneLinerCsv(scenes) {
92
+ const header = 'Scene #,Heading,Synopsis,Actors,Status,Est-Duration\n';
93
+ const rows = scenes
94
+ .map(s => `${esc(s.number)},${esc(s.heading)},${esc(s.synopsis)},${esc(s.actors)},${esc(s.status)},${esc(s.estDuration)}`)
95
+ .join('\n');
96
+ return header + rows;
97
+ }
98
+ export function exportShotListCsv(scenes) {
99
+ const header = 'Scene #,Heading,Actors,Shot Details\n';
100
+ const rows = [];
101
+ for (const s of scenes) {
102
+ if (s.shots.length === 0) {
103
+ rows.push(`${esc(s.number)},${esc(s.heading)},${esc(s.actors)},""`);
104
+ }
105
+ else {
106
+ for (const shot of s.shots) {
107
+ rows.push(`${esc(s.number)},${esc(s.heading)},${esc(s.actors)},${esc(shot)}`);
108
+ }
109
+ }
110
+ }
111
+ return header + rows.join('\n');
112
+ }
113
+ export function exportCastReportCsv(characters) {
114
+ const header = 'Character,Actor Name,Scene Occurrences,Scenes List\n';
115
+ const rows = characters
116
+ .map(c => `${esc(c.name)},${esc(c.actor)},${c.sceneCount},${esc(c.scenes.join('; '))}`)
117
+ .join('\n');
118
+ return header + rows;
119
+ }
120
+ // ---------------------------------------------------------
121
+ // DOCX Table Exporters
122
+ // ---------------------------------------------------------
123
+ function makeCell(text, bold = false, widthPercent = 0) {
124
+ return new TableCell({
125
+ width: widthPercent > 0 ? { size: widthPercent, type: WidthType.PERCENTAGE } : undefined,
126
+ children: [
127
+ new Paragraph({
128
+ children: [
129
+ new TextRun({
130
+ text,
131
+ bold,
132
+ font: 'Courier Prime',
133
+ size: '10pt',
134
+ }),
135
+ ],
136
+ }),
137
+ ],
138
+ });
139
+ }
140
+ export async function exportOneLinerDocx(scenes) {
141
+ const headerRow = new TableRow({
142
+ children: [
143
+ makeCell('Scene #', true, 10),
144
+ makeCell('Heading', true, 25),
145
+ makeCell('Synopsis', true, 35),
146
+ makeCell('Actors', true, 15),
147
+ makeCell('Status', true, 10),
148
+ makeCell('Duration', true, 5),
149
+ ],
150
+ });
151
+ const rows = [headerRow];
152
+ for (const s of scenes) {
153
+ rows.push(new TableRow({
154
+ children: [
155
+ makeCell(s.number, false, 10),
156
+ makeCell(s.heading, false, 25),
157
+ makeCell(s.synopsis, false, 35),
158
+ makeCell(s.actors, false, 15),
159
+ makeCell(s.status, false, 10),
160
+ makeCell(s.estDuration, false, 5),
161
+ ],
162
+ }));
163
+ }
164
+ const table = new Table({
165
+ width: { size: 100, type: WidthType.PERCENTAGE },
166
+ rows,
167
+ });
168
+ const doc = new Document({
169
+ sections: [
170
+ {
171
+ properties: {},
172
+ children: [
173
+ new Paragraph({
174
+ children: [
175
+ new TextRun({
176
+ text: 'ONE-LINER SCHEDULE / SCENE BREAKDOWN',
177
+ bold: true,
178
+ font: 'Courier Prime',
179
+ size: '14pt',
180
+ }),
181
+ ],
182
+ spacing: { after: 288 },
183
+ }),
184
+ table,
185
+ ],
186
+ },
187
+ ],
188
+ });
189
+ return Packer.toBlob(doc);
190
+ }
191
+ export async function exportShotListDocx(scenes) {
192
+ const headerRow = new TableRow({
193
+ children: [
194
+ makeCell('Scene #', true, 10),
195
+ makeCell('Heading', true, 25),
196
+ makeCell('Actors', true, 20),
197
+ makeCell('Shot Details', true, 45),
198
+ ],
199
+ });
200
+ const rows = [headerRow];
201
+ for (const s of scenes) {
202
+ const shotDetails = s.shots.join('\n');
203
+ rows.push(new TableRow({
204
+ children: [
205
+ makeCell(s.number, false, 10),
206
+ makeCell(s.heading, false, 25),
207
+ makeCell(s.actors, false, 20),
208
+ makeCell(shotDetails || 'No shots specified', false, 45),
209
+ ],
210
+ }));
211
+ }
212
+ const table = new Table({
213
+ width: { size: 100, type: WidthType.PERCENTAGE },
214
+ rows,
215
+ });
216
+ const doc = new Document({
217
+ sections: [
218
+ {
219
+ properties: {},
220
+ children: [
221
+ new Paragraph({
222
+ children: [
223
+ new TextRun({
224
+ text: 'SHOT LIST REPORT',
225
+ bold: true,
226
+ font: 'Courier Prime',
227
+ size: '14pt',
228
+ }),
229
+ ],
230
+ spacing: { after: 288 },
231
+ }),
232
+ table,
233
+ ],
234
+ },
235
+ ],
236
+ });
237
+ return Packer.toBlob(doc);
238
+ }
239
+ export async function exportCastReportDocx(characters) {
240
+ const headerRow = new TableRow({
241
+ children: [
242
+ makeCell('Character', true, 25),
243
+ makeCell('Actor Name', true, 25),
244
+ makeCell('Occurrences', true, 15),
245
+ makeCell('Scenes List', true, 35),
246
+ ],
247
+ });
248
+ const rows = [headerRow];
249
+ for (const c of characters) {
250
+ rows.push(new TableRow({
251
+ children: [
252
+ makeCell(c.name, false, 25),
253
+ makeCell(c.actor || 'TBD', false, 25),
254
+ makeCell(String(c.sceneCount), false, 15),
255
+ makeCell(c.scenes.join(', '), false, 35),
256
+ ],
257
+ }));
258
+ }
259
+ const table = new Table({
260
+ width: { size: 100, type: WidthType.PERCENTAGE },
261
+ rows,
262
+ });
263
+ const doc = new Document({
264
+ sections: [
265
+ {
266
+ properties: {},
267
+ children: [
268
+ new Paragraph({
269
+ children: [
270
+ new TextRun({
271
+ text: 'CHARACTER OCCURRENCE / CAST REPORT',
272
+ bold: true,
273
+ font: 'Courier Prime',
274
+ size: '14pt',
275
+ }),
276
+ ],
277
+ spacing: { after: 288 },
278
+ }),
279
+ table,
280
+ ],
281
+ },
282
+ ],
283
+ });
284
+ return Packer.toBlob(doc);
285
+ }
286
+ // ---------------------------------------------------------
287
+ // Print Preview Page Generator (HTML)
288
+ // ---------------------------------------------------------
289
+ export async function openPrintPreview(scenes, title = 'One-Liner Schedule') {
290
+ // Open synchronously (inside the click) so popup blockers allow it, then embed fonts.
291
+ const win = window.open('', '_blank');
292
+ if (!win)
293
+ return;
294
+ // Noto Sans carries every ISO 15919 letter and mark, for romanized reports; the new
295
+ // window has no stylesheet of its own.
296
+ const fontFaceCss = await embedFontFacesFor(['Noto Sans']);
297
+ const rowsHtml = scenes
298
+ .map(s => `
299
+ <tr>
300
+ <td>${s.number}</td>
301
+ <td><strong>${s.heading}</strong></td>
302
+ <td>${s.synopsis || '<span class="empty">No synopsis</span>'}</td>
303
+ <td>${s.actors || '-'}</td>
304
+ <td><span class="status-badge status-${s.status.toLowerCase().replace(/\s+/g, '-') || 'none'}">${s.status || '-'}</span></td>
305
+ <td>${s.estDuration || '-'}</td>
306
+ </tr>
307
+ `)
308
+ .join('');
309
+ win.document.write(`
310
+ <!DOCTYPE html>
311
+ <html>
312
+ <head>
313
+ <title>${title}</title>
314
+ <style>
315
+ ${fontFaceCss}
316
+ body {
317
+ font-family: 'Noto Sans', 'Courier Prime', sans-serif;
318
+ margin: 40px;
319
+ color: #333;
320
+ }
321
+ h1 {
322
+ font-family: 'Courier Prime', sans-serif;
323
+ text-align: center;
324
+ font-size: 20px;
325
+ margin-bottom: 30px;
326
+ text-transform: uppercase;
327
+ letter-spacing: 1px;
328
+ }
329
+ table {
330
+ width: 100%;
331
+ border-collapse: collapse;
332
+ font-size: 13px;
333
+ }
334
+ th, td {
335
+ border: 1px solid #ccc;
336
+ padding: 10px 12px;
337
+ text-align: left;
338
+ vertical-align: top;
339
+ }
340
+ th {
341
+ background-color: #f7f7f5;
342
+ font-weight: 600;
343
+ }
344
+ .empty {
345
+ color: #999;
346
+ font-style: italic;
347
+ }
348
+ .status-badge {
349
+ display: inline-block;
350
+ padding: 2px 6px;
351
+ border-radius: 4px;
352
+ font-size: 11px;
353
+ font-weight: 600;
354
+ background: #f0f0f0;
355
+ border: 1px solid #ddd;
356
+ }
357
+ .status-done, .status-revised { background: #e6f4ea; color: #137333; border-color: #ceead6; }
358
+ .status-wip { background: #fef7e0; color: #b06000; border-color: #feebc8; }
359
+ .status-draft { background: #fce8e6; color: #c5221f; border-color: #fad2cf; }
360
+
361
+ @media print {
362
+ body { margin: 20px; }
363
+ button { display: none !important; }
364
+ th { background-color: #f0f0f0 !important; -webkit-print-color-adjust: exact; }
365
+ }
366
+
367
+ .print-btn-bar {
368
+ text-align: right;
369
+ margin-bottom: 20px;
370
+ }
371
+ .print-btn {
372
+ background: #1976d2;
373
+ color: white;
374
+ border: none;
375
+ padding: 8px 16px;
376
+ font-size: 13px;
377
+ border-radius: 4px;
378
+ cursor: pointer;
379
+ font-weight: 600;
380
+ }
381
+ .print-btn:hover { background: #1565c0; }
382
+ </style>
383
+ </head>
384
+ <body>
385
+ <div class="print-btn-bar">
386
+ <button class="print-btn" onclick="window.print()">Print Schedule</button>
387
+ </div>
388
+ <h1>${title}</h1>
389
+ <table>
390
+ <thead>
391
+ <tr>
392
+ <th style="width: 8%">Scene #</th>
393
+ <th style="width: 25%">Heading</th>
394
+ <th style="width: 35%">Synopsis</th>
395
+ <th style="width: 15%">Actors</th>
396
+ <th style="width: 12%">Status</th>
397
+ <th style="width: 5%">Duration</th>
398
+ </tr>
399
+ </thead>
400
+ <tbody>
401
+ ${rowsHtml}
402
+ </tbody>
403
+ </table>
404
+ </body>
405
+ </html>
406
+ `);
407
+ win.document.close();
408
+ }
409
+ /** Print Screenplay Preview in formatted style. `styleOverride` picks a style id for
410
+ * this export only (without touching the document); otherwise the document's
411
+ * frontmatter `style:` field is used, falling back to the default style. `skipNotes`
412
+ * omits both block-level and inline `[[ ]]` notes from the exported document. */
413
+ export async function openScreenplayPrintPreview(ast, title = 'Screenplay', styleOverride, customStyles = [], skipNotes = false) {
414
+ const html = await buildScreenplayPrintHtml(ast, title, styleOverride, customStyles, skipNotes);
415
+ // Desktop shell: generate a real vector-text PDF via the platform's browser
416
+ // engine (WebView2 PrintToPdf on Windows) instead of the OS print dialog,
417
+ // which avoids the rasterized/bloated output some print-to-PDF drivers produce.
418
+ const api = window.desktopAPI;
419
+ if (api?.exportScreenplayToPdf && api?.showSavePdfDialog) {
420
+ const defaultName = `${title.replace(/[\\/:*?"<>|]/g, '_')}.pdf`;
421
+ const path = await api.showSavePdfDialog(defaultName);
422
+ if (!path)
423
+ return;
424
+ await api.exportScreenplayToPdf(html, path);
425
+ return;
426
+ }
427
+ const win = window.open('', '_blank');
428
+ if (!win)
429
+ return;
430
+ win.document.write(html);
431
+ win.document.close();
432
+ }
433
+ async function buildScreenplayPrintHtml(ast, title, styleOverride, customStyles = [], skipNotes = false) {
434
+ const frontmatter = ast.frontmatter?.data;
435
+ const defaultLang = frontmatter?.['lang'] ?? 'en';
436
+ const style = resolveStyle(styleOverride ?? frontmatter?.['style'], customStyles);
437
+ // Noto Sans is the Latin fallback in the print font stack (print-adapter.ts), so embed it
438
+ // too: the desktop's hidden print webview cannot load /fonts/ paths on its own.
439
+ const embeddedFontFaceCss = await embedFontFacesFor([...new Set([
440
+ style.fontFamily,
441
+ ...Object.values(style.complexScriptFontOverrides ?? {}),
442
+ 'Noto Sans',
443
+ ])]);
444
+ // Paper the export prints on — frontmatter's `page:`, the same field docx-exporter.ts
445
+ // reads. print-paginator.ts needs it in CSS px to size its page boxes.
446
+ const pageFormat = frontmatter?.['page']?.toLowerCase() === 'a4' ? 'a4' : 'letter';
447
+ const pageHeightIn = pageFormat === 'a4' ? 11.69 : 11;
448
+ const watermarkValue = typeof frontmatter?.['watermark'] === 'string' ? frontmatter['watermark'].trim() : '';
449
+ const watermarkHtml = watermarkValue ? `<div class="print-watermark">${escapeHtml(watermarkValue)}</div>` : '';
450
+ // Render cover page
451
+ let coverPageHtml = '';
452
+ if (frontmatter && (frontmatter['title'] || frontmatter['author'])) {
453
+ const titleText = String(frontmatter['title'] || 'Untitled');
454
+ const authorText = String(frontmatter['author'] || '');
455
+ const loglineText = String(frontmatter['logline'] || '');
456
+ const dateText = String(frontmatter['date'] || '');
457
+ const contactText = String(frontmatter['contact'] || '');
458
+ // Both cover layouts share this markup — the two blocks are what 'corners' pushes
459
+ // into opposite corners and 'centered' just stacks (see print-adapter.ts). The
460
+ // byline differs: 'corners' prints one "by <author>" line, 'centered' keeps the
461
+ // classic "by" / author on separate lines.
462
+ const corners = (style.coverLayout ?? DEFAULT_COVER_LAYOUT) === 'corners';
463
+ const bylineHtml = corners
464
+ ? `<div class="print-cover-author">by ${textHtml(authorText)}</div>`
465
+ : `<div class="print-cover-by">by</div>
466
+ <div class="print-cover-author">${textHtml(authorText)}</div>`;
467
+ coverPageHtml = `
468
+ <div class="print-cover-page print-cover-${corners ? 'corners' : 'centered'}">
469
+ <div class="print-cover-top">
470
+ <div class="print-cover-title">${textHtml(titleText)}</div>
471
+ ${bylineHtml}
472
+ ${loglineText ? `<div class="print-cover-logline">${textHtml(loglineText)}</div>` : ''}
473
+ </div>
474
+ <div class="print-cover-bottom">
475
+ ${dateText ? `<div class="print-cover-date">${escapeHtml(dateText)}</div>` : ''}
476
+ ${contactText ? `<div class="print-cover-contact">${escapeHtml(contactText)}</div>` : ''}
477
+ </div>
478
+ </div>
479
+ `;
480
+ }
481
+ // Render screenplay nodes
482
+ let bodyHtml = '';
483
+ function renderSpans(spans) {
484
+ return spans.map(s => {
485
+ if (s.type === 'text')
486
+ return textHtml(s.text);
487
+ if (s.type === 'bold')
488
+ return `<strong>${renderSpans(s.spans)}</strong>`;
489
+ if (s.type === 'italic')
490
+ return `<em>${renderSpans(s.spans)}</em>`;
491
+ if (s.type === 'underline')
492
+ return `<u>${renderSpans(s.spans)}</u>`;
493
+ if (s.type === 'note')
494
+ return skipNotes ? '' : `<span class="print-note">[[ ${textHtml(s.text)} ]]</span>`;
495
+ return '';
496
+ }).join('');
497
+ }
498
+ function escapeHtml(text) {
499
+ return text
500
+ .replace(/&/g, '&amp;')
501
+ .replace(/</g, '&lt;')
502
+ .replace(/>/g, '&gt;')
503
+ .replace(/"/g, '&quot;')
504
+ .replace(/'/g, '&#039;');
505
+ }
506
+ /** Whether `text` contains a script this style forces italic by default
507
+ * (`italicScripts`, e.g. Traditional's Tamil) — mirrors docx-exporter.ts's
508
+ * createRuns forced-italic behavior, which the live editor and DOCX export
509
+ * already have but the print/PDF HTML never did (no per-span language tagging
510
+ * there), so Tamil rendered upright in exported PDFs even under Traditional. */
511
+ function forceItalic(text) {
512
+ if (!style.italicScripts?.length)
513
+ return false;
514
+ return style.italicScripts.some(code => SCRIPT_UNICODE_RANGES[code]?.test(text));
515
+ }
516
+ /** escapeHtml, plus <i>-wrapping when forceItalic(text) — italic always,
517
+ * even inside otherwise-bold/underlined elements like Traditional's scene
518
+ * headings and character cues, matching the confirmed docx-exporter.ts behavior. */
519
+ function textHtml(text) {
520
+ const escaped = escapeHtml(text);
521
+ return forceItalic(text) ? `<i>${escaped}</i>` : escaped;
522
+ }
523
+ function renderContentNode(n) {
524
+ if (n.type === 'action') {
525
+ return `<div class="print-action">${renderSpans(n.spans)}</div>`;
526
+ }
527
+ if (n.type === 'character') {
528
+ const ext = n.extension ? ` ${n.extension.toUpperCase()}` : '';
529
+ const childHtml = n.children.map(c => {
530
+ if (c.type === 'parenthetical') {
531
+ return `<div class="print-parenthetical">(${textHtml(c.text)})</div>`;
532
+ }
533
+ else {
534
+ return `<div class="print-dialogue">${renderSpans(c.spans)}</div>`;
535
+ }
536
+ }).join('');
537
+ return `
538
+ <div class="print-character-block">
539
+ <div class="print-character">${textHtml(n.name.toUpperCase())}${textHtml(ext)}</div>
540
+ ${childHtml}
541
+ </div>
542
+ `;
543
+ }
544
+ if (n.type === 'transition') {
545
+ return `<div class="print-transition">${textHtml(n.text.toUpperCase())}</div>`;
546
+ }
547
+ if (n.type === 'centered') {
548
+ return `<div class="print-centered">${textHtml(n.text)}</div>`;
549
+ }
550
+ if (n.type === 'lyrics') {
551
+ return `<div class="print-lyrics">${renderSpans(n.spans)}</div>`;
552
+ }
553
+ if (n.type === 'note') {
554
+ return skipNotes ? '' : `<div class="print-note">${textHtml(n.text)}</div>`;
555
+ }
556
+ if (n.type === 'comment') {
557
+ return `<div class="print-comment">&lt;!-- ${textHtml(n.text)} --&gt;</div>`;
558
+ }
559
+ if (n.type === 'page-break') {
560
+ return `<div class="print-page-break"></div>`;
561
+ }
562
+ if (n.type === 'dual-dialogue') {
563
+ const leftChar = n.left;
564
+ const leftExt = leftChar.extension ? ` ${leftChar.extension.toUpperCase()}` : '';
565
+ const leftChildHtml = leftChar.children.map(c => {
566
+ if (c.type === 'parenthetical') {
567
+ return `<div class="print-parenthetical">(${textHtml(c.text)})</div>`;
568
+ }
569
+ else {
570
+ return `<div class="print-dialogue">${renderSpans(c.spans)}</div>`;
571
+ }
572
+ }).join('');
573
+ const rightChar = n.right;
574
+ const rightExt = rightChar.extension ? ` ${rightChar.extension.toUpperCase()}` : '';
575
+ const rightChildHtml = rightChar.children.map(c => {
576
+ if (c.type === 'parenthetical') {
577
+ return `<div class="print-parenthetical">(${textHtml(c.text)})</div>`;
578
+ }
579
+ else {
580
+ return `<div class="print-dialogue">${renderSpans(c.spans)}</div>`;
581
+ }
582
+ }).join('');
583
+ return `
584
+ <div class="print-dual-dialogue">
585
+ <div class="print-dual-column print-left-column">
586
+ <div class="print-character">${textHtml(leftChar.name.toUpperCase())}${textHtml(leftExt)}</div>
587
+ ${leftChildHtml}
588
+ </div>
589
+ <div class="print-dual-column print-right-column">
590
+ <div class="print-character">${textHtml(rightChar.name.toUpperCase())}${textHtml(rightExt)}</div>
591
+ ${rightChildHtml}
592
+ </div>
593
+ </div>
594
+ `;
595
+ }
596
+ return '';
597
+ }
598
+ /** Stamps the scene a block belongs to onto its outermost element, which is how
599
+ * print-paginator.ts decides whether a page opens mid-scene (-> "8 CONTINUED:") or on
600
+ * a fresh one. The label is the scene's own number when it has one, else its position
601
+ * in the script — the same thing the heading prints on the right. */
602
+ function tagScene(html, label, isHeading = false) {
603
+ const attrs = ` data-scene="${escapeHtml(label)}"${isHeading ? ` data-scene-start="${escapeHtml(label)}"` : ''}`;
604
+ return html.replace('<div', `<div${attrs}`);
605
+ }
606
+ let sceneOrdinal = 0;
607
+ for (const node of ast.children) {
608
+ if (node.type === 'section') {
609
+ bodyHtml += `<div class="print-section print-sect-${node.level}">${textHtml(node.text)}</div>`;
610
+ for (const sub of node.children) {
611
+ bodyHtml += renderContentNode(sub);
612
+ }
613
+ }
614
+ else if (node.type === 'scene-heading') {
615
+ sceneOrdinal++;
616
+ const sceneLabel = node.id || String(sceneOrdinal);
617
+ bodyHtml += tagScene(`
618
+ <div class="print-scene-heading">
619
+ <span class="print-scene-title">${textHtml(node.text.toUpperCase())}</span>
620
+ ${node.id ? `<span class="print-scene-num">${escapeHtml(node.id)}</span>` : ''}
621
+ </div>
622
+ `, sceneLabel, true);
623
+ for (const sub of node.children) {
624
+ bodyHtml += tagScene(renderContentNode(sub), sceneLabel);
625
+ }
626
+ }
627
+ else {
628
+ bodyHtml += renderContentNode(node);
629
+ }
630
+ }
631
+ return `
632
+ <!DOCTYPE html>
633
+ <html>
634
+ <head>
635
+ <title>${escapeHtml(title)}</title>
636
+ <link rel="preconnect" href="https://fonts.googleapis.com">
637
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
638
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Noto+Sans+Devanagari&family=Noto+Sans+Tamil&family=Noto+Sans+Telugu&family=Noto+Sans+Kannada&family=Noto+Sans+Malayalam&family=Noto+Sans+Bengali&family=Noto+Sans+Gujarati&family=Noto+Sans+Gurmukhi&family=Noto+Sans+Oriya&family=Noto+Serif:ital,wght@0,400;0,700;1,400&family=Noto+Serif+Devanagari&family=Noto+Serif+Tamil:ital@0;1&family=Noto+Serif+Telugu&family=Noto+Serif+Kannada&family=Noto+Serif+Malayalam&family=Noto+Serif+Bengali&family=Noto+Serif+Gujarati&family=Noto+Serif+Gurmukhi&family=Noto+Serif+Oriya&display=swap" rel="stylesheet">
639
+ <style>
640
+ /* Statically-instanced (non-variable) copies of this export's own fonts, embedded
641
+ as data: URIs — see embed-fonts.ts for why: Chromium's print/PDF-to-file font
642
+ subsetter has known bugs mishandling GPOS/GSUB (glyph positioning/substitution —
643
+ required for correct Indic conjunct/vowel-sign shaping) on *variable* fonts
644
+ specifically, even though the same font shapes perfectly on-screen. The Google
645
+ Fonts <link> above only serves variable Indic-script families, so without this,
646
+ a style that needs one (e.g. Traditional's Noto Serif Tamil) renders correctly in
647
+ the on-screen preview but comes out garbled the moment it's actually printed/
648
+ exported to PDF. Declared after the <link> so these take cascade precedence for
649
+ the same family/weight/style. */
650
+ ${embeddedFontFaceCss}
651
+ </style>
652
+ <style>
653
+ /* Layout-neutral part only — the two cover layouts (.print-cover-centered /
654
+ .print-cover-corners) come from the style, so they live in print-adapter.ts. */
655
+ .print-cover-page {
656
+ page-break-after: always;
657
+ box-sizing: border-box;
658
+ }
659
+ ${styleToPrintCss(style, pageFormat)}
660
+
661
+ /* Interface / buttons */
662
+ .print-btn-bar {
663
+ text-align: right;
664
+ margin-bottom: 20px;
665
+ }
666
+ .print-btn {
667
+ background: #0a5c6a;
668
+ color: white;
669
+ border: none;
670
+ padding: 8px 16px;
671
+ font-size: 13px;
672
+ border-radius: 4px;
673
+ cursor: pointer;
674
+ font-weight: 600;
675
+ }
676
+ .print-btn:hover {
677
+ background: #084954;
678
+ }
679
+
680
+ /* Watermark, from frontmatter's watermark: field. position: fixed repeats
681
+ it on every printed page in Chromium's print/print-to-pdf engine (including
682
+ WebView2, used for the native Windows PDF export) -- the same trick the live
683
+ editor's .cs-watermark uses to stay visible while scrolling. */
684
+ .print-watermark {
685
+ position: fixed;
686
+ top: 50%;
687
+ left: 50%;
688
+ transform: translate(-50%, -50%) rotate(-45deg);
689
+ font-size: 60pt;
690
+ font-weight: 700;
691
+ color: rgba(0, 0, 0, 0.08);
692
+ white-space: nowrap;
693
+ pointer-events: none;
694
+ z-index: -1;
695
+ }
696
+
697
+ @media print {
698
+ .print-btn-bar { display: none !important; }
699
+ body { padding: 0; }
700
+ }
701
+ </style>
702
+ </head>
703
+ <body>
704
+ <div class="print-btn-bar">
705
+ <button class="print-btn" onclick="window.print()">Export to PDF / Print</button>
706
+ </div>
707
+ ${watermarkHtml}
708
+ ${coverPageHtml}
709
+ <div id="print-pages"></div>
710
+ <div id="print-flow">${bodyHtml}</div>
711
+ ${printPaginatorScript({
712
+ contentHeightPx: Math.round((pageHeightIn - style.page.marginTopIn - style.page.marginBottomIn) * CSS_PX_PER_IN),
713
+ pageNumberPrefix: 'p',
714
+ continuedSuffix: 'CONTINUED:',
715
+ })}
716
+ </body>
717
+ </html>
718
+ `;
719
+ }