@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,494 @@
1
+ import { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, AlignmentType, BorderStyle, PageBreak, WidthType, TabStopType, LineRuleType, Header, PageNumber, } from 'docx';
2
+ import { resolveStyle } from '../styles/registry';
3
+ import { styleToDocxParagraphStyles } from '../styles/docx-adapter';
4
+ import { SCRIPT_UNICODE_RANGES } from '../styles/script-ranges';
5
+ import { DEFAULT_COVER_LAYOUT } from '../styles/types';
6
+ /** Flatten inline span trees into flat formatting runs. `skipNotes` drops inline
7
+ * `[[ ]]` note spans entirely (see the "Skip notes" export option). */
8
+ function flattenSpans(spans, props = {}, skipNotes = false) {
9
+ const result = [];
10
+ for (const s of spans) {
11
+ if (s.type === 'text') {
12
+ result.push({ text: s.text, ...props });
13
+ }
14
+ else if (s.type === 'bold') {
15
+ result.push(...flattenSpans(s.spans, { ...props, bold: true }, skipNotes));
16
+ }
17
+ else if (s.type === 'italic') {
18
+ result.push(...flattenSpans(s.spans, { ...props, italic: true }, skipNotes));
19
+ }
20
+ else if (s.type === 'underline') {
21
+ result.push(...flattenSpans(s.spans, { ...props, underline: true }, skipNotes));
22
+ }
23
+ else if (s.type === 'note') {
24
+ if (skipNotes)
25
+ continue;
26
+ // Matches whole-line note styling (italic, brackets kept visible).
27
+ result.push({ text: `[[ ${s.text} ]]`, ...props, italic: true });
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+ const SCRIPT_RANGES = Object.entries(SCRIPT_UNICODE_RANGES).map(([code, re]) => ({ code, re }));
33
+ const SCRIPT_FONT_BY_LANG = {
34
+ hi: 'Noto Sans Devanagari', ta: 'Noto Sans Tamil', te: 'Noto Sans Telugu',
35
+ kn: 'Noto Sans Kannada', ml: 'Noto Sans Malayalam', bn: 'Noto Sans Bengali',
36
+ gu: 'Noto Sans Gujarati', pa: 'Noto Sans Gurmukhi', or: 'Noto Sans Oriya',
37
+ si: 'Noto Sans Sinhala',
38
+ };
39
+ /** Which complex-script lang code (if any) a run's text belongs to \u2014 the shared key
40
+ * space for both `complexScriptFontOverrides` and `italicScripts`. Unicode presence
41
+ * wins over `defaultLang` (mixed-script documents), matching getComplexFont's
42
+ * original priority.
43
+ *
44
+ * The `defaultLang` fallback (for text with no script-specific characters at all,
45
+ * e.g. bare punctuation/numbers between Indic words) must never fire for text that
46
+ * contains actual Latin letters \u2014 otherwise a document with `lang: ta` would treat
47
+ * every English word (scene headings, "EXT.", character extensions, a plain title)
48
+ * as Tamil too, which for most styles is a harmless font substitution (Noto Sans/
49
+ * Serif Tamil both ship full Latin glyphs) but is actively wrong once a style forces
50
+ * italics for that script (Traditional) \u2014 English text would come out italicized
51
+ * for no reason. */
52
+ function matchScriptCode(text, defaultLang = 'en') {
53
+ for (const { code, re } of SCRIPT_RANGES) {
54
+ if (re.test(text))
55
+ return code;
56
+ }
57
+ if (/[A-Za-z]/.test(text))
58
+ return null;
59
+ return defaultLang in SCRIPT_FONT_BY_LANG ? defaultLang : null;
60
+ }
61
+ /** Resolve a complex-script font override for a matched script code. Returns null when
62
+ * the text needs no override (plain Latin) \u2014 never a specific "Latin font" name, since
63
+ * that decision belongs to the active style's fontFamily (applied at the paragraph-style
64
+ * level). `overrides` is a style's optional `complexScriptFontOverrides` (e.g.
65
+ * Traditional's Tamil -> Noto Serif Tamil) \u2014 DOCX runs can only ever name one font,
66
+ * unlike a CSS font stack, so a style whose body font *does* cover a script it cares
67
+ * about must say so explicitly here rather than relying on per-glyph fallback like the
68
+ * editor/print CSS do. */
69
+ function getComplexFont(code, overrides) {
70
+ if (!code)
71
+ return null;
72
+ return overrides?.[code] ?? SCRIPT_FONT_BY_LANG[code] ?? null;
73
+ }
74
+ /** Generate docx TextRun list with correct font mapping. `italicScripts` (a style's
75
+ * per-script "italic is this script's standard look here" list, e.g. Traditional's
76
+ * Tamil) forces italics regardless of the run's own markup or `forceItalics` \u2014 matches
77
+ * the editor's `.cs-force-italic-<code>` behavior. */
78
+ function createRuns(runProps, defaultLang, forceItalics = false, complexScriptFontOverrides, italicScripts) {
79
+ return runProps.map(r => {
80
+ const code = matchScriptCode(r.text, defaultLang);
81
+ const csFont = getComplexFont(code, complexScriptFontOverrides);
82
+ const scriptForcesItalic = code !== null && (italicScripts?.includes(code) ?? false);
83
+ const runOptions = {
84
+ text: r.text,
85
+ bold: r.bold,
86
+ italics: r.italic || forceItalics || scriptForcesItalic,
87
+ underline: r.underline ? {} : undefined,
88
+ };
89
+ // Only set font override for Indic scripts to support proper rendering,
90
+ // leaving latin/general characters to use the paragraph style's own font.
91
+ if (csFont) {
92
+ runOptions.font = {
93
+ ascii: csFont,
94
+ hAnsi: csFont,
95
+ cs: csFont,
96
+ eastAsia: csFont,
97
+ };
98
+ }
99
+ return new TextRun(runOptions);
100
+ });
101
+ }
102
+ /** Vertical placement of the 'corners' cover, as fractions of the printable page height:
103
+ * where the title block starts and where the bottom-right date/contact block starts.
104
+ * Matches print-adapter.ts's .print-cover-corners padding (20% top / block sitting ~6%
105
+ * above the bottom edge), so the DOCX and PDF covers land in the same place. */
106
+ const COVER_CORNERS_TOP_FRACTION = 0.22;
107
+ const COVER_CORNERS_BOTTOM_FRACTION = 0.78;
108
+ /** DOCX has no way to anchor a block to the bottom of a page, so the corner cover is
109
+ * positioned by stacking empty paragraphs of an exact, known height (rather than
110
+ * Normal-styled blank ones, whose height depends on the reader's default font). */
111
+ const COVER_SPACER_TWIPS = 240; // 12pt
112
+ /** Height budgeted per date/contact line — their run sizes (10pt/11pt) plus leading. */
113
+ const COVER_CONTACT_LINE_TWIPS = 280;
114
+ /** Enough exact-height spacers to cover `twips` (never negative — a block that already
115
+ * overshoots its target simply gets none). */
116
+ function spacerParagraphs(twips) {
117
+ const count = Math.max(0, Math.round(twips / COVER_SPACER_TWIPS));
118
+ return Array.from({ length: count }, () => new Paragraph({
119
+ spacing: { before: 0, after: 0, line: COVER_SPACER_TWIPS, lineRule: LineRuleType.EXACT },
120
+ }));
121
+ }
122
+ /** Approximate rendered height of a run of cover paragraphs, from their style's font
123
+ * sizes and spacing. Approximate is enough: it only decides how many spacers separate
124
+ * the two corner blocks, and the bottom block's placement is clamped so an
125
+ * underestimate can't push it onto a second page. Assumes each element occupies one
126
+ * line — a title or logline long enough to wrap just rides slightly lower. */
127
+ function estimateBlockHeightTwips(style, keys) {
128
+ return keys.reduce((sum, key) => {
129
+ const el = style.elements[key];
130
+ return sum + Math.round(el.fontSizePt * 1.15 * 20) + Math.round((el.spaceBeforePt ?? 0) * 20)
131
+ + Math.round((el.spaceAfterPt ?? 0) * 20);
132
+ }, 0);
133
+ }
134
+ /** Main exporter function. `styleOverride` picks a style id for this export only
135
+ * (without touching the document); otherwise the document's frontmatter `style:`
136
+ * field is used, falling back to the default style. `customStyles` is the list of
137
+ * user-imported styles loaded from IndexedDB (see file/storage.ts). `skipNotes` omits
138
+ * both block-level and inline `[[ ]]` notes from the exported document. */
139
+ export async function exportToDocx(ast, styleOverride, customStyles = [], skipNotes = false) {
140
+ const frontmatter = ast.frontmatter?.data;
141
+ const defaultLang = frontmatter?.['lang'] ?? 'en';
142
+ const style = resolveStyle(styleOverride ?? frontmatter?.['style'], customStyles);
143
+ const bodyFont = style.fontFamily;
144
+ const runsFor = (props, forceItalics = false) => createRuns(props, defaultLang, forceItalics, style.complexScriptFontOverrides, style.italicScripts);
145
+ const pageFormat = frontmatter?.['page']?.toLowerCase() ?? 'letter';
146
+ const pageWidthTwips = pageFormat === 'a4' ? 11906 : 12240;
147
+ const pageHeightTwips = pageFormat === 'a4' ? 16838 : 15840;
148
+ const pageMarginLeftTwips = Math.round(style.page.marginLeftIn * 1440);
149
+ const pageMarginRightTwips = Math.round(style.page.marginRightIn * 1440);
150
+ const pageMarginTopTwips = Math.round(style.page.marginTopIn * 1440);
151
+ const pageMarginBottomTwips = Math.round(style.page.marginBottomIn * 1440);
152
+ // Right edge of the printable area, measured from the page margin — where
153
+ // a scene number's right-aligned tab stop should land (matches the UI's
154
+ // right-aligned scene-number box, no leading "#").
155
+ const contentRightEdgeTwips = pageWidthTwips - pageMarginLeftTwips - pageMarginRightTwips;
156
+ const childrenNodes = [];
157
+ const hasCover = Boolean(frontmatter && (frontmatter['title'] || frontmatter['author']));
158
+ // 1. Cover Page (if frontmatter has title)
159
+ if (hasCover && frontmatter) {
160
+ const titleText = String(frontmatter['title'] || 'Untitled');
161
+ const authorText = String(frontmatter['author'] || '');
162
+ const loglineText = String(frontmatter['logline'] || '');
163
+ const dateText = String(frontmatter['date'] || '');
164
+ const contactText = String(frontmatter['contact'] || '');
165
+ const titlePara = new Paragraph({
166
+ style: 'CineTitle',
167
+ children: runsFor([{ text: titleText.toUpperCase(), bold: true }]),
168
+ });
169
+ const loglineParas = loglineText
170
+ ? loglineText.split('\n').map(line => new Paragraph({
171
+ style: 'CineLogline',
172
+ children: runsFor([{ text: line, italic: true }]),
173
+ }))
174
+ : [];
175
+ // Deliberately not the author style: these mirror the PDF cover's small gray
176
+ // date/contact lines (.print-cover-date / .print-cover-contact), not the
177
+ // author's display size.
178
+ const dateRun = (indent) => new Paragraph({
179
+ alignment: indent === undefined ? AlignmentType.CENTER : undefined,
180
+ indent: indent === undefined ? undefined : { left: indent },
181
+ children: [new TextRun({ text: dateText, font: bodyFont, size: 20, color: '777777' })],
182
+ });
183
+ const contactRuns = (indent) => contactText.split('\n').map(line => new Paragraph({
184
+ alignment: indent === undefined ? AlignmentType.CENTER : undefined,
185
+ indent: indent === undefined ? undefined : { left: indent },
186
+ children: [new TextRun({ text: line, font: bodyFont, size: 22, color: '888888' })],
187
+ }));
188
+ if ((style.coverLayout ?? DEFAULT_COVER_LAYOUT) === 'corners') {
189
+ // Title / "by <author>" / logline flush top-left, date + contact in the
190
+ // bottom-right corner — the PDF cover's .print-cover-corners layout, rebuilt
191
+ // out of the only vertical tool DOCX gives us: exact-height spacer paragraphs
192
+ // (see spacerParagraphs) counted off against the printable page height.
193
+ const contentHeightTwips = pageHeightTwips - pageMarginTopTwips - pageMarginBottomTwips;
194
+ const topBlock = [
195
+ titlePara,
196
+ ...(authorText ? [new Paragraph({
197
+ style: 'CineAuthor',
198
+ children: runsFor([{ text: `by ${authorText}` }]),
199
+ })] : []),
200
+ ...loglineParas,
201
+ ];
202
+ const bottomIndent = Math.round(contentRightEdgeTwips * 0.55);
203
+ const bottomBlock = [
204
+ ...(dateText ? [dateRun(bottomIndent)] : []),
205
+ ...(contactText ? contactRuns(bottomIndent) : []),
206
+ ];
207
+ const topOffsetTwips = Math.round(contentHeightTwips * COVER_CORNERS_TOP_FRACTION);
208
+ const topSpacers = spacerParagraphs(topOffsetTwips);
209
+ childrenNodes.push(...topSpacers, ...topBlock);
210
+ // Fill down to the bottom block's target, but never past the point where the
211
+ // block itself would no longer fit — an overshoot would spill the corner onto
212
+ // a second cover page.
213
+ const usedTwips = topSpacers.length * COVER_SPACER_TWIPS
214
+ + estimateBlockHeightTwips(style, ['title', ...(authorText ? ['author'] : []), ...loglineParas.map(() => 'logline')]);
215
+ const bottomHeightTwips = bottomBlock.length * COVER_CONTACT_LINE_TWIPS;
216
+ const bottomTopTwips = Math.min(Math.round(contentHeightTwips * COVER_CORNERS_BOTTOM_FRACTION), contentHeightTwips - bottomHeightTwips);
217
+ childrenNodes.push(...spacerParagraphs(bottomTopTwips - usedTwips), ...bottomBlock);
218
+ }
219
+ else {
220
+ for (let k = 0; k < 12; k++) {
221
+ childrenNodes.push(new Paragraph({ text: '' }));
222
+ }
223
+ childrenNodes.push(titlePara);
224
+ childrenNodes.push(new Paragraph({ text: '' }));
225
+ childrenNodes.push(new Paragraph({
226
+ alignment: AlignmentType.CENTER,
227
+ children: [new TextRun({ text: 'by', font: bodyFont, size: 24 /* 12pt */ })],
228
+ }));
229
+ childrenNodes.push(new Paragraph({ text: '' }));
230
+ childrenNodes.push(new Paragraph({
231
+ style: 'CineAuthor',
232
+ children: runsFor([{ text: authorText }]),
233
+ }));
234
+ if (loglineParas.length > 0) {
235
+ for (let k = 0; k < 6; k++)
236
+ childrenNodes.push(new Paragraph({ text: '' }));
237
+ childrenNodes.push(...loglineParas);
238
+ }
239
+ if (dateText) {
240
+ for (let k = 0; k < 10; k++)
241
+ childrenNodes.push(new Paragraph({ text: '' }));
242
+ childrenNodes.push(dateRun());
243
+ }
244
+ if (contactText) {
245
+ if (!dateText) {
246
+ for (let k = 0; k < 10; k++)
247
+ childrenNodes.push(new Paragraph({ text: '' }));
248
+ }
249
+ else {
250
+ childrenNodes.push(new Paragraph({ text: '' }));
251
+ childrenNodes.push(new Paragraph({ text: '' }));
252
+ }
253
+ childrenNodes.push(...contactRuns());
254
+ }
255
+ }
256
+ childrenNodes.push(new Paragraph({ children: [new PageBreak()] }));
257
+ }
258
+ // Helper to process standard scene content node and return array of docx components
259
+ function processNode(n) {
260
+ if (n.type === 'action') {
261
+ const runs = runsFor(flattenSpans(n.spans, {}, skipNotes));
262
+ return [
263
+ new Paragraph({
264
+ style: 'CineAction',
265
+ children: runs,
266
+ }),
267
+ ];
268
+ }
269
+ if (n.type === 'character') {
270
+ const charName = n.name.toUpperCase();
271
+ const ext = n.extension ? ` ${n.extension.toUpperCase()}` : '';
272
+ const headText = `${charName}${ext}`;
273
+ const runs = runsFor([{ text: headText, bold: true }]);
274
+ const result = [
275
+ new Paragraph({
276
+ style: 'CineCharacter',
277
+ children: runs,
278
+ }),
279
+ ];
280
+ for (const c of n.children) {
281
+ if (c.type === 'parenthetical') {
282
+ const pText = `(${c.text})`;
283
+ result.push(new Paragraph({
284
+ style: 'CineParenthetical',
285
+ children: runsFor([{ text: pText }]),
286
+ }));
287
+ }
288
+ else {
289
+ const dialogueRuns = runsFor(flattenSpans(c.spans, {}, skipNotes));
290
+ result.push(new Paragraph({
291
+ style: 'CineDialogue',
292
+ children: dialogueRuns,
293
+ }));
294
+ }
295
+ }
296
+ return result;
297
+ }
298
+ if (n.type === 'transition') {
299
+ const transText = n.text.toUpperCase();
300
+ return [
301
+ new Paragraph({
302
+ style: 'CineTransition',
303
+ children: runsFor([{ text: transText }]),
304
+ }),
305
+ ];
306
+ }
307
+ if (n.type === 'centered') {
308
+ return [
309
+ new Paragraph({
310
+ style: 'CineCentered',
311
+ children: runsFor([{ text: n.text }]),
312
+ }),
313
+ ];
314
+ }
315
+ if (n.type === 'lyrics') {
316
+ const runs = runsFor(flattenSpans(n.spans, {}, skipNotes), true);
317
+ return [
318
+ new Paragraph({
319
+ style: 'CineLyrics',
320
+ children: runs,
321
+ }),
322
+ ];
323
+ }
324
+ if (n.type === 'note') {
325
+ if (skipNotes)
326
+ return [];
327
+ const noteText = `[[ ${n.text} ]]`;
328
+ return [
329
+ new Paragraph({
330
+ style: 'CineNote',
331
+ children: runsFor([{ text: noteText, italic: true }]),
332
+ }),
333
+ ];
334
+ }
335
+ if (n.type === 'comment') {
336
+ const commentText = `<!-- ${n.text} -->`;
337
+ return [
338
+ new Paragraph({
339
+ style: 'CineComment',
340
+ children: runsFor([{ text: commentText, italic: true }]),
341
+ }),
342
+ ];
343
+ }
344
+ if (n.type === 'page-break') {
345
+ return [new Paragraph({ children: [new PageBreak()] })];
346
+ }
347
+ if (n.type === 'dual-dialogue') {
348
+ const leftParas = [];
349
+ const leftChar = n.left;
350
+ const leftRuns = runsFor([{ text: `${leftChar.name.toUpperCase()}${leftChar.extension ? ` ${leftChar.extension.toUpperCase()}` : ''}`, bold: true }]);
351
+ leftParas.push(new Paragraph({ style: 'CineCharacter', children: leftRuns }));
352
+ for (const c of leftChar.children) {
353
+ if (c.type === 'parenthetical') {
354
+ leftParas.push(new Paragraph({ style: 'CineParenthetical', children: runsFor([{ text: `(${c.text})` }]) }));
355
+ }
356
+ else {
357
+ leftParas.push(new Paragraph({
358
+ style: 'CineDialogue',
359
+ children: runsFor(flattenSpans(c.spans, {}, skipNotes)),
360
+ }));
361
+ }
362
+ }
363
+ const rightParas = [];
364
+ const rightChar = n.right;
365
+ const rightRuns = runsFor([{ text: `${rightChar.name.toUpperCase()}${rightChar.extension ? ` ${rightChar.extension.toUpperCase()}` : ''}`, bold: true }]);
366
+ rightParas.push(new Paragraph({ style: 'CineCharacter', children: rightRuns }));
367
+ for (const c of rightChar.children) {
368
+ if (c.type === 'parenthetical') {
369
+ rightParas.push(new Paragraph({ style: 'CineParenthetical', children: runsFor([{ text: `(${c.text})` }]) }));
370
+ }
371
+ else {
372
+ rightParas.push(new Paragraph({
373
+ style: 'CineDialogue',
374
+ children: runsFor(flattenSpans(c.spans, {}, skipNotes)),
375
+ }));
376
+ }
377
+ }
378
+ return [
379
+ new Table({
380
+ width: { size: 100, type: WidthType.PERCENTAGE },
381
+ borders: {
382
+ top: { style: BorderStyle.NONE, size: 0, color: 'auto' },
383
+ bottom: { style: BorderStyle.NONE, size: 0, color: 'auto' },
384
+ left: { style: BorderStyle.NONE, size: 0, color: 'auto' },
385
+ right: { style: BorderStyle.NONE, size: 0, color: 'auto' },
386
+ insideHorizontal: { style: BorderStyle.NONE, size: 0, color: 'auto' },
387
+ insideVertical: { style: BorderStyle.NONE, size: 0, color: 'auto' },
388
+ },
389
+ rows: [
390
+ new TableRow({
391
+ children: [
392
+ new TableCell({
393
+ children: leftParas,
394
+ width: { size: 50, type: WidthType.PERCENTAGE },
395
+ }),
396
+ new TableCell({
397
+ children: rightParas,
398
+ width: { size: 50, type: WidthType.PERCENTAGE },
399
+ }),
400
+ ],
401
+ }),
402
+ ],
403
+ }),
404
+ ];
405
+ }
406
+ return [];
407
+ }
408
+ // Iterate over children
409
+ for (const node of ast.children) {
410
+ if (node.type === 'section') {
411
+ const sectText = node.text;
412
+ childrenNodes.push(new Paragraph({
413
+ style: 'CineSection',
414
+ children: runsFor([{ text: sectText, bold: true }]),
415
+ }));
416
+ for (const subNode of node.children) {
417
+ childrenNodes.push(...processNode(subNode));
418
+ }
419
+ }
420
+ else if (node.type === 'scene-heading') {
421
+ const headingRuns = runsFor([{ text: node.text.toUpperCase(), bold: true }]);
422
+ if (node.id) {
423
+ headingRuns.push(new TextRun({ text: '\t' }), ...runsFor([{ text: node.id, bold: true }]));
424
+ }
425
+ childrenNodes.push(new Paragraph({
426
+ style: 'CineSceneHeading',
427
+ tabStops: node.id
428
+ ? [{ type: TabStopType.RIGHT, position: contentRightEdgeTwips }]
429
+ : undefined,
430
+ children: headingRuns,
431
+ }));
432
+ for (const subNode of node.children) {
433
+ childrenNodes.push(...processNode(subNode));
434
+ }
435
+ }
436
+ else {
437
+ childrenNodes.push(...processNode(node));
438
+ }
439
+ }
440
+ // Running header: "p<n>" in the top right, as a PAGE field so Word keeps it correct as
441
+ // the document is edited. The PDF export's other header cell — "<scene> CONTINUED:" on a
442
+ // page that opens mid-scene — has no DOCX equivalent: it depends on where the page break
443
+ // falls, and Word neither exposes that to a field nor lets a header vary per page within
444
+ // a section. (Only the cover is special-cased, via titlePage below.)
445
+ const runningHeader = new Header({
446
+ children: [
447
+ new Paragraph({
448
+ alignment: AlignmentType.RIGHT,
449
+ children: [
450
+ new TextRun({
451
+ children: ['p', PageNumber.CURRENT],
452
+ font: bodyFont,
453
+ size: Math.round(style.elements.action.fontSizePt * 2),
454
+ italics: true,
455
+ }),
456
+ ],
457
+ }),
458
+ ],
459
+ });
460
+ // Create document with screenplay styles driven by the resolved ScreenplayStyleDefinition
461
+ const doc = new Document({
462
+ styles: {
463
+ paragraphStyles: styleToDocxParagraphStyles(style, contentRightEdgeTwips),
464
+ },
465
+ sections: [
466
+ {
467
+ properties: {
468
+ // The cover takes Word's "different first page" header (an empty one) and page
469
+ // number 0, so the first page of the script itself comes out as p1.
470
+ titlePage: hasCover,
471
+ page: {
472
+ size: {
473
+ width: pageWidthTwips,
474
+ height: pageHeightTwips,
475
+ },
476
+ margin: {
477
+ top: pageMarginTopTwips,
478
+ bottom: pageMarginBottomTwips,
479
+ left: pageMarginLeftTwips,
480
+ right: pageMarginRightTwips,
481
+ },
482
+ pageNumbers: hasCover ? { start: 0 } : undefined,
483
+ },
484
+ },
485
+ headers: {
486
+ default: runningHeader,
487
+ ...(hasCover ? { first: new Header({ children: [new Paragraph({ text: '' })] }) } : {}),
488
+ },
489
+ children: childrenNodes,
490
+ },
491
+ ],
492
+ });
493
+ return Packer.toBlob(doc);
494
+ }
@@ -0,0 +1,10 @@
1
+ export interface DocxImportResult {
2
+ /** Reconstructed Sutra body text (no frontmatter), or null if nothing importable was found. */
3
+ bodyText: string | null;
4
+ warnings: string[];
5
+ }
6
+ /** Parses a .docx file (as produced by exportToDocx, possibly re-saved from Word with
7
+ * corrections and/or font changes) back into Sutra body text. The title page, if any,
8
+ * is not reimported — only content carrying the CineXxx paragraph styles the exporter
9
+ * applies to the screenplay body is recognized. */
10
+ export declare function importDocx(buffer: ArrayBuffer): Promise<DocxImportResult>;