@sutrata/editor 0.1.1 → 0.2.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 (268) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
  3. package/dist/ai/VoiceConfirmDialog.js +6 -6
  4. package/dist/ai/VoiceToolbar.d.ts +1 -0
  5. package/dist/ai/VoiceToolbar.js +22 -16
  6. package/dist/ai/clean-response.d.ts +7 -0
  7. package/dist/ai/clean-response.js +58 -0
  8. package/dist/ai/format-transcript.d.ts +3 -0
  9. package/dist/ai/format-transcript.js +7 -0
  10. package/dist/ai/prompts.d.ts +8 -1
  11. package/dist/ai/prompts.js +34 -7
  12. package/dist/autocomplete/suggestions.js +1 -1
  13. package/dist/context/DocumentContext.d.ts +28 -4
  14. package/dist/context/DocumentContext.js +67 -37
  15. package/dist/editor/EditorView.js +32 -4
  16. package/dist/editor/SelectionMenu.js +5 -3
  17. package/dist/editor/external-decorations-plugin.d.ts +23 -0
  18. package/dist/editor/external-decorations-plugin.js +151 -0
  19. package/dist/editor/omitted-scene-plugin.d.ts +9 -0
  20. package/dist/editor/omitted-scene-plugin.js +44 -0
  21. package/dist/editor/plugins.js +4 -0
  22. package/dist/editor/private-metadata-plugin.d.ts +10 -0
  23. package/dist/editor/private-metadata-plugin.js +60 -0
  24. package/dist/editor/prosemirror-to-sutra.js +179 -74
  25. package/dist/editor/scene-heading-view.js +2 -0
  26. package/dist/editor/schema.js +69 -12
  27. package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
  28. package/dist/editor/sutra-to-prosemirror.js +32 -34
  29. package/dist/extensions/ai-provider.d.ts +44 -0
  30. package/dist/extensions/ai-provider.js +27 -0
  31. package/dist/extensions/collab.d.ts +21 -0
  32. package/dist/extensions/collab.js +6 -0
  33. package/dist/extensions/command-registry.d.ts +49 -0
  34. package/dist/extensions/command-registry.js +28 -0
  35. package/dist/extensions/decorations.d.ts +48 -0
  36. package/dist/extensions/decorations.js +6 -0
  37. package/dist/extensions/export-registry.d.ts +59 -0
  38. package/dist/extensions/export-registry.js +10 -0
  39. package/dist/extensions/panel-registry.d.ts +30 -0
  40. package/dist/extensions/panel-registry.js +16 -0
  41. package/dist/extensions/registries.d.ts +15 -0
  42. package/dist/extensions/registries.js +12 -0
  43. package/dist/extensions/registry-store.d.ts +26 -0
  44. package/dist/extensions/registry-store.js +35 -0
  45. package/dist/extensions/session.d.ts +29 -0
  46. package/dist/extensions/session.js +20 -0
  47. package/dist/extensions/speech-provider.d.ts +35 -0
  48. package/dist/extensions/speech-provider.js +17 -0
  49. package/dist/file/builtin-exporters.d.ts +9 -0
  50. package/dist/file/builtin-exporters.js +72 -0
  51. package/dist/file/docx-exporter.js +7 -4
  52. package/dist/file/docx-importer.d.ts +9 -0
  53. package/dist/file/docx-importer.js +47 -19
  54. package/dist/file/scene-number.d.ts +5 -0
  55. package/dist/file/scene-number.js +8 -0
  56. package/dist/file/workflow-reports.js +13 -7
  57. package/dist/findreplace/FindReplace.js +5 -2
  58. package/dist/import/AIImportDialog.d.ts +11 -0
  59. package/dist/import/AIImportDialog.js +112 -0
  60. package/dist/import/ai-import.d.ts +51 -0
  61. package/dist/import/ai-import.js +200 -0
  62. package/dist/import/apply-import.d.ts +7 -0
  63. package/dist/import/apply-import.js +35 -0
  64. package/dist/import/extract.d.ts +17 -0
  65. package/dist/import/extract.js +18 -0
  66. package/dist/index.d.ts +17 -2
  67. package/dist/index.js +7 -0
  68. package/dist/locales/bn.json +8 -0
  69. package/dist/locales/en.json +8 -0
  70. package/dist/locales/gu.json +8 -0
  71. package/dist/locales/hi.json +8 -0
  72. package/dist/locales/kn.json +8 -0
  73. package/dist/locales/ml.json +8 -0
  74. package/dist/locales/mr.json +8 -0
  75. package/dist/locales/or.json +8 -0
  76. package/dist/locales/pa.json +8 -0
  77. package/dist/locales/si.json +8 -0
  78. package/dist/locales/ta.json +8 -0
  79. package/dist/locales/te.json +8 -0
  80. package/dist/navigator/SceneNavigator.js +125 -131
  81. package/dist/navigator/StatisticsDialog.js +5 -1
  82. package/dist/navigator/scene-edits.d.ts +18 -0
  83. package/dist/navigator/scene-edits.js +143 -0
  84. package/dist/navigator/scene-list.js +4 -4
  85. package/dist/navigator/scene-numbering.d.ts +37 -0
  86. package/dist/navigator/scene-numbering.js +88 -0
  87. package/dist/settings/SettingsDialog.js +8 -120
  88. package/dist/shell/AppBar.js +21 -2
  89. package/dist/shell/AppShell.js +18 -4
  90. package/dist/shell/ElementToolbar.d.ts +5 -0
  91. package/dist/shell/ElementToolbar.js +35 -25
  92. package/dist/shell/LanguageStatus.js +6 -0
  93. package/dist/shell/StatusBar.js +11 -6
  94. package/dist/shell/builtin-commands.d.ts +10 -0
  95. package/dist/shell/builtin-commands.js +43 -0
  96. package/dist/shell/icons.d.ts +6 -0
  97. package/dist/shell/icons.js +6 -0
  98. package/dist/shell/use-commands.d.ts +7 -0
  99. package/dist/shell/use-commands.js +24 -0
  100. package/dist/shell/use-panel-context.d.ts +3 -0
  101. package/dist/shell/use-panel-context.js +39 -0
  102. package/dist/source/SourceView.js +18 -1
  103. package/dist/styles/screenplay.css +113 -1
  104. package/dist/titlepage/ExportDialog.d.ts +5 -0
  105. package/dist/titlepage/ExportDialog.js +59 -96
  106. package/package.json +32 -5
  107. package/dist/ai/OnboardingDialog.d.ts +0 -6
  108. package/dist/ai/OnboardingDialog.js +0 -115
  109. package/dist/ai/ai-client.d.ts +0 -71
  110. package/dist/ai/ai-client.js +0 -494
  111. package/dist/ai/generated-providers.json +0 -3696
  112. package/dist/ai/voice-service.d.ts +0 -15
  113. package/dist/ai/voice-service.js +0 -81
  114. package/dist/editor/character-view.d.ts +0 -35
  115. package/dist/editor/character-view.js +0 -112
  116. package/dist/editor/transliterate-input.d.ts +0 -10
  117. package/dist/editor/transliterate-input.js +0 -88
  118. package/dist/extensions/types.d.ts +0 -73
  119. package/dist/extensions/types.js +0 -12
  120. package/scripts/copy-assets.mjs +0 -16
  121. package/scripts/generate-providers.mjs +0 -127
  122. package/src/ai/OnboardingDialog.tsx +0 -390
  123. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  124. package/src/ai/VoiceToolbar.tsx +0 -321
  125. package/src/ai/ai-client.ts +0 -596
  126. package/src/ai/commands.ts +0 -164
  127. package/src/ai/generated-providers.json +0 -3696
  128. package/src/ai/prompts.ts +0 -62
  129. package/src/ai/voice-service.ts +0 -96
  130. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  131. package/src/autocomplete/suggestions.ts +0 -147
  132. package/src/context/DocumentContext.tsx +0 -648
  133. package/src/desktop-api.d.ts +0 -40
  134. package/src/editor/EditorView.tsx +0 -82
  135. package/src/editor/SelectionMenu.tsx +0 -152
  136. package/src/editor/attr-pencil-plugin.ts +0 -172
  137. package/src/editor/character-decoration-plugin.ts +0 -104
  138. package/src/editor/editor-bus.ts +0 -51
  139. package/src/editor/find-highlight-plugin.ts +0 -68
  140. package/src/editor/frontmatter-field-view.ts +0 -52
  141. package/src/editor/frontmatter-field.ts +0 -115
  142. package/src/editor/grapheme-cursor.ts +0 -99
  143. package/src/editor/ime-plugin.ts +0 -85
  144. package/src/editor/insert-helper.ts +0 -43
  145. package/src/editor/keymap.ts +0 -124
  146. package/src/editor/language-class-plugin.ts +0 -91
  147. package/src/editor/plugins.ts +0 -26
  148. package/src/editor/prosemirror-to-sutra.ts +0 -192
  149. package/src/editor/scene-heading-view.ts +0 -147
  150. package/src/editor/schema.ts +0 -156
  151. package/src/editor/sutra-to-prosemirror.ts +0 -156
  152. package/src/extensions/storage-adapter.ts +0 -65
  153. package/src/extensions/types.ts +0 -60
  154. package/src/file/docx-exporter.ts +0 -585
  155. package/src/file/docx-importer.ts +0 -313
  156. package/src/file/print-paginator.ts +0 -301
  157. package/src/file/templates.ts +0 -73
  158. package/src/file/workflow-reports.ts +0 -820
  159. package/src/findreplace/FindReplace.tsx +0 -242
  160. package/src/findreplace/find-engine.ts +0 -97
  161. package/src/findreplace/scroll-to-match.ts +0 -89
  162. package/src/fonts/tofu-detect.ts +0 -53
  163. package/src/i18n/LanguageContext.tsx +0 -207
  164. package/src/i18n/language-detect.ts +0 -73
  165. package/src/i18n/useTranslation.tsx +0 -74
  166. package/src/index.ts +0 -27
  167. package/src/locales/bn.json +0 -143
  168. package/src/locales/en.json +0 -143
  169. package/src/locales/gu.json +0 -143
  170. package/src/locales/hi.json +0 -143
  171. package/src/locales/kn.json +0 -143
  172. package/src/locales/ml.json +0 -143
  173. package/src/locales/mr.json +0 -143
  174. package/src/locales/or.json +0 -143
  175. package/src/locales/pa.json +0 -143
  176. package/src/locales/si.json +0 -143
  177. package/src/locales/ta.json +0 -143
  178. package/src/locales/te.json +0 -143
  179. package/src/navigator/SceneNavigator.tsx +0 -844
  180. package/src/navigator/StatisticsDialog.tsx +0 -249
  181. package/src/navigator/scene-list.ts +0 -84
  182. package/src/settings/SettingsDialog.tsx +0 -303
  183. package/src/shell/AppBar.tsx +0 -354
  184. package/src/shell/AppShell.tsx +0 -71
  185. package/src/shell/ConfirmDialog.tsx +0 -71
  186. package/src/shell/ElementToolbar.tsx +0 -175
  187. package/src/shell/LanguageStatus.tsx +0 -101
  188. package/src/shell/StatusBar.tsx +0 -135
  189. package/src/shell/SutrataLogo.tsx +0 -89
  190. package/src/shell/Toast.tsx +0 -46
  191. package/src/shell/ToolbarButton.tsx +0 -30
  192. package/src/shell/VersionHistory.tsx +0 -212
  193. package/src/shell/Watermark.tsx +0 -9
  194. package/src/shell/fonts/fonts.css +0 -1133
  195. package/src/shell/icons.tsx +0 -335
  196. package/src/shell/toolbar-actions.ts +0 -59
  197. package/src/source/SourceView.tsx +0 -111
  198. package/src/source/find-highlight-source.ts +0 -57
  199. package/src/source/sutra-lang.ts +0 -121
  200. package/src/spellcheck/dictionaries.ts +0 -21
  201. package/src/spellcheck/hunspell.ts +0 -110
  202. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  203. package/src/styles/StyleDialog.tsx +0 -148
  204. package/src/styles/builtin-styles.ts +0 -139
  205. package/src/styles/css-adapter.ts +0 -76
  206. package/src/styles/docx-adapter.ts +0 -109
  207. package/src/styles/embed-fonts.ts +0 -81
  208. package/src/styles/pagination.ts +0 -27
  209. package/src/styles/print-adapter.ts +0 -288
  210. package/src/styles/registry.ts +0 -20
  211. package/src/styles/screenplay.css +0 -3006
  212. package/src/styles/script-ranges.ts +0 -17
  213. package/src/styles/types.ts +0 -104
  214. package/src/styles/validate.ts +0 -114
  215. package/src/templates/templates.ts +0 -194
  216. package/src/titlepage/ExportDialog.tsx +0 -494
  217. package/src/titlepage/TitlePageDialog.tsx +0 -312
  218. package/src/titlepage/frontmatter-form.ts +0 -280
  219. package/src/titlepage/languages.ts +0 -37
  220. package/src/types.ts +0 -7
  221. package/tests/AppBar.test.tsx +0 -35
  222. package/tests/AppShell.test.tsx +0 -22
  223. package/tests/ElementToolbar.test.tsx +0 -36
  224. package/tests/LanguageStatus.test.tsx +0 -129
  225. package/tests/SelectionMenu.test.tsx +0 -71
  226. package/tests/ToolbarButton.test.tsx +0 -24
  227. package/tests/ai-onboarding.test.ts +0 -102
  228. package/tests/ai-status.test.tsx +0 -72
  229. package/tests/ai-structured.test.ts +0 -94
  230. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  231. package/tests/autocomplete/suggestions.test.ts +0 -106
  232. package/tests/document-context.test.tsx +0 -565
  233. package/tests/docx-importer.test.ts +0 -160
  234. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  235. package/tests/editor/character-extension.test.ts +0 -44
  236. package/tests/editor/grapheme-cursor.test.ts +0 -118
  237. package/tests/editor/ime-plugin.test.ts +0 -72
  238. package/tests/editor/inline-note-bridge.test.ts +0 -32
  239. package/tests/editor/language-class-plugin.test.ts +0 -265
  240. package/tests/editor/title-page.test.ts +0 -124
  241. package/tests/embed-fonts.test.ts +0 -17
  242. package/tests/exporters.test.ts +0 -365
  243. package/tests/findreplace/FindReplace.test.tsx +0 -391
  244. package/tests/findreplace/find-engine.test.ts +0 -86
  245. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  246. package/tests/fonts/tofu-detect.test.ts +0 -122
  247. package/tests/i18n/LanguageContext.test.tsx +0 -146
  248. package/tests/i18n/language-detect.test.ts +0 -54
  249. package/tests/i18n/locale-parity.test.ts +0 -25
  250. package/tests/i18n/useTranslation.test.tsx +0 -45
  251. package/tests/keymap.test.ts +0 -18
  252. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  253. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  254. package/tests/navigator/scene-list.test.ts +0 -71
  255. package/tests/print-paginator.test.ts +0 -82
  256. package/tests/shell/VersionHistory.test.tsx +0 -129
  257. package/tests/spellcheck/hunspell.test.ts +0 -82
  258. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  259. package/tests/styles.test.ts +0 -315
  260. package/tests/templates/templates.test.ts +0 -83
  261. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  262. package/tests/titlepage/apply-title-page.test.ts +0 -61
  263. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  264. package/tests/toolbar-actions.test.ts +0 -93
  265. package/tests/voice-commands.test.ts +0 -58
  266. package/tsconfig.json +0 -17
  267. package/vitest.config.ts +0 -11
  268. package/vitest.setup.ts +0 -65
@@ -1,365 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { parse, romanizeSutra } from '@sutrata/parser'
3
- import { exportToDocx } from '../src/file/docx-exporter'
4
- import {
5
- extractWorkflowData,
6
- exportOneLinerCsv,
7
- exportShotListCsv,
8
- exportCastReportCsv,
9
- exportOneLinerDocx,
10
- exportShotListDocx,
11
- exportCastReportDocx,
12
- openScreenplayPrintPreview,
13
- } from '../src/file/workflow-reports'
14
- import JSZip from 'jszip'
15
-
16
- const testSutra = `---
17
- title: Test Movie
18
- author: Writer Name
19
- logline: A movie about testing.
20
- date: 2026-06-13
21
- lang: en
22
- characters:
23
- JOHN: Actor One
24
- MARY: Actor Two
25
- ---
26
-
27
- ## Scene 1
28
- & number: 1
29
- & synopsis: John and Mary talk in a room.
30
- & status: Done
31
- & est-duration: 2m
32
- & shots: Wide shot of the room
33
- & shots: Close up on John
34
-
35
- @JOHN
36
- Hello Mary.
37
-
38
- @MARY
39
- (smiling)
40
- Hi John.
41
-
42
- @JOHN
43
- (dialogue side-by-side)
44
- Dual dialogue left.
45
-
46
- @MARY ^
47
- (dialogue side-by-side)
48
- Dual dialogue right.
49
-
50
- Action line here.
51
-
52
- [[ Note text ]]
53
-
54
- <!-- Comment text -->
55
-
56
- ===
57
-
58
- *Lyrics here*
59
-
60
- `
61
-
62
- describe('Exporters', () => {
63
- const ast = parse(testSutra)
64
-
65
- it('exportToDocx generates a Blob', async () => {
66
- const blob = await exportToDocx(ast)
67
- expect(blob).toBeInstanceOf(Blob)
68
- expect(blob.size).toBeGreaterThan(0)
69
- })
70
-
71
- it('uses Noto Serif Tamil, forced italic, for Tamil runs under the traditional style, but Noto Sans Tamil (not italic) elsewhere', async () => {
72
- const tamilDoc = parse('## Scene 1\n\nஇது ஒரு தமிழ் வாக்கியம்.\n')
73
-
74
- const traditionalBlob = await exportToDocx(tamilDoc, 'traditional')
75
- const traditionalXml = await (await JSZip.loadAsync(await traditionalBlob.arrayBuffer())).file('word/document.xml')!.async('text')
76
- expect(traditionalXml).toContain('w:ascii="Noto Serif Tamil"')
77
- expect(traditionalXml).not.toContain('Noto Sans Tamil')
78
- // forced italic even though this is a plain action line with no *emphasis* markup
79
- expect(traditionalXml).toMatch(/<w:rPr>\s*<w:rFonts w:ascii="Noto Serif Tamil"[^/]*\/>\s*<w:i\/>/)
80
-
81
- const classicBlob = await exportToDocx(tamilDoc, 'classic')
82
- const classicXml = await (await JSZip.loadAsync(await classicBlob.arrayBuffer())).file('word/document.xml')!.async('text')
83
- expect(classicXml).toContain('w:ascii="Noto Sans Tamil"')
84
- expect(classicXml).not.toMatch(/<w:rPr>\s*<w:rFonts w:ascii="Noto Sans Tamil"[^/]*\/>\s*<w:i\/>/)
85
- })
86
-
87
- it('never treats plain English text as the document default language\'s script (regression: lang: ta used to force every English run to Tamil\'s font/italics)', async () => {
88
- const mixedDoc = parse('---\ntitle: Eagle\nlang: ta\n---\n\n## EXT. COURT - DAY\n\nஇது ஒரு தமிழ் வாக்கியம்.\n')
89
- const blob = await exportToDocx(mixedDoc, 'traditional')
90
- const xml = await (await JSZip.loadAsync(await blob.arrayBuffer())).file('word/document.xml')!.async('text')
91
-
92
- // "EAGLE" (title) and "EXT. COURT - DAY" (scene heading) are pure English: they must
93
- // get no run-level font override at all (inheriting the paragraph style's own font,
94
- // i.e. Noto Serif) rather than being forced into Tamil's font/italics.
95
- const englishRunPattern = /<w:rPr>\s*<w:b\/>\s*<w:bCs\/>\s*<w:i w:val="false"\/>\s*<w:iCs w:val="false"\/>\s*<\/w:rPr>\s*<w:t xml:space="preserve">EXT\. COURT - DAY<\/w:t>/
96
- expect(xml).toMatch(englishRunPattern)
97
-
98
- // The actual Tamil line still gets Noto Serif Tamil + forced italic.
99
- expect(xml).toMatch(/<w:rPr>\s*<w:rFonts w:ascii="Noto Serif Tamil"[^/]*\/>\s*<w:i\/>/)
100
- })
101
-
102
- it('does not force plain Latin runs to Noto Sans when the style uses a different body font', async () => {
103
- // Regression: createRuns used to compare the Indic-fallback font against the
104
- // active style's fontFamily and override the run whenever they differed —
105
- // which incorrectly forced every Latin run to the fallback's own "Noto Sans"
106
- // default instead of leaving it to inherit the paragraph style's real font
107
- // (e.g. Courier Prime for 'industry-courier').
108
- const blob = await exportToDocx(ast, 'industry-courier')
109
- const buffer = await blob.arrayBuffer()
110
- const zip = await JSZip.loadAsync(buffer)
111
- const xml = await zip.file('word/document.xml')!.async('text')
112
- expect(xml).not.toContain('Noto Sans')
113
- expect(xml).toContain('CineAction')
114
- })
115
-
116
- it('extractWorkflowData correctly extracts scenes and characters', () => {
117
- const data = extractWorkflowData(ast)
118
-
119
- expect(data.scenes).toHaveLength(1)
120
- const s = data.scenes[0]!
121
- expect(s.number).toBe('1')
122
- expect(s.heading).toBe('SCENE 1')
123
- expect(s.synopsis).toBe('John and Mary talk in a room.')
124
- expect(s.status).toBe('Done')
125
- expect(s.estDuration).toBe('2m')
126
- expect(s.shots).toEqual(['Wide shot of the room', 'Close up on John'])
127
-
128
- // Character occurrences
129
- expect(data.characters).toHaveLength(2)
130
- const johnStats = data.characters.find(c => c.name === 'JOHN')!
131
- expect(johnStats).toBeDefined()
132
- expect(johnStats.actor).toBe('Actor One')
133
- expect(johnStats.sceneCount).toBe(1)
134
- expect(johnStats.scenes[0]).toContain('SCENE 1')
135
- })
136
-
137
- it('exportOneLinerCsv generates correct headers and rows', () => {
138
- const { scenes } = extractWorkflowData(ast)
139
- const csv = exportOneLinerCsv(scenes)
140
- expect(csv).toContain('Scene #,Heading,Synopsis,Actors,Status,Est-Duration')
141
- expect(csv).toContain('"1","SCENE 1","John and Mary talk in a room.","","Done","2m"')
142
- })
143
-
144
- it('exportShotListCsv generates correct headers and rows', () => {
145
- const { scenes } = extractWorkflowData(ast)
146
- const csv = exportShotListCsv(scenes)
147
- expect(csv).toContain('Scene #,Heading,Actors,Shot Details')
148
- expect(csv).toContain('"1","SCENE 1","","Wide shot of the room"')
149
- expect(csv).toContain('"1","SCENE 1","","Close up on John"')
150
- })
151
-
152
- it('exportCastReportCsv generates correct headers and rows', () => {
153
- const { characters } = extractWorkflowData(ast)
154
- const csv = exportCastReportCsv(characters)
155
- expect(csv).toContain('Character,Actor Name,Scene Occurrences,Scenes List')
156
- expect(csv).toContain('"JOHN","Actor One",1,"1: SCENE 1"')
157
- })
158
-
159
- it('exportOneLinerDocx generates a Blob', async () => {
160
- const { scenes } = extractWorkflowData(ast)
161
- const blob = await exportOneLinerDocx(scenes)
162
- expect(blob).toBeInstanceOf(Blob)
163
- expect(blob.size).toBeGreaterThan(0)
164
- })
165
-
166
- it('exportShotListDocx generates a Blob', async () => {
167
- const { scenes } = extractWorkflowData(ast)
168
- const blob = await exportShotListDocx(scenes)
169
- expect(blob).toBeInstanceOf(Blob)
170
- expect(blob.size).toBeGreaterThan(0)
171
- })
172
-
173
- it('exportCastReportDocx generates a Blob', async () => {
174
- const { characters } = extractWorkflowData(ast)
175
- const blob = await exportCastReportDocx(characters)
176
- expect(blob).toBeInstanceOf(Blob)
177
- expect(blob.size).toBeGreaterThan(0)
178
- })
179
-
180
- it('includes a print-watermark div only when frontmatter.watermark is set', async () => {
181
- const originalOpen = window.open
182
- let written = ''
183
- window.open = () => ({ document: { write: (html: string) => { written = html }, close: () => {} } } as any)
184
-
185
- try {
186
- await openScreenplayPrintPreview(parse(testSutra))
187
- expect(written).not.toContain('class="print-watermark"')
188
-
189
- const watermarked = parse(testSutra.replace('lang: en', 'lang: en\nwatermark: CONFIDENTIAL DRAFT'))
190
- await openScreenplayPrintPreview(watermarked)
191
- expect(written).toContain('class="print-watermark"')
192
- expect(written).toContain('CONFIDENTIAL DRAFT')
193
- } finally {
194
- window.open = originalOpen
195
- }
196
- })
197
-
198
- it('wraps Tamil text in <i> in the print/PDF HTML under the traditional style, but not under classic (regression: print export had no per-span language tagging, so italicScripts had no effect there even though it already worked in the editor/DOCX)', async () => {
199
- const originalOpen = window.open
200
- let written = ''
201
- window.open = () => ({ document: { write: (html: string) => { written = html }, close: () => {} } } as any)
202
-
203
- try {
204
- const tamilDoc = parse('## Scene 1\n\nஇது ஒரு தமிழ் வாக்கியம்.\n')
205
-
206
- await openScreenplayPrintPreview(tamilDoc, 'Screenplay', 'traditional')
207
- expect(written).toMatch(/<i>[^<]*இது ஒரு தமிழ் வாக்கியம்[^<]*<\/i>/)
208
-
209
- await openScreenplayPrintPreview(tamilDoc, 'Screenplay', 'classic')
210
- expect(written).not.toContain('<i>')
211
- } finally {
212
- window.open = originalOpen
213
- }
214
- })
215
-
216
- it('lays the corner cover out top-left/bottom-right for traditional, and keeps the centered one for classic', async () => {
217
- const originalOpen = window.open
218
- let written = ''
219
- window.open = () => ({ document: { write: (html: string) => { written = html }, close: () => {} } } as any)
220
-
221
- try {
222
- await openScreenplayPrintPreview(ast, 'Screenplay', 'traditional')
223
- expect(written).toContain('class="print-cover-page print-cover-corners"')
224
- // One "by <author>" line in the top block, not the centered layout's split pair.
225
- expect(written).toContain('<div class="print-cover-author">by Writer Name</div>')
226
- expect(written).not.toContain('<div class="print-cover-by">')
227
-
228
- await openScreenplayPrintPreview(ast, 'Screenplay', 'classic')
229
- expect(written).toContain('class="print-cover-page print-cover-centered"')
230
- expect(written).toContain('<div class="print-cover-by">by</div>')
231
- } finally {
232
- window.open = originalOpen
233
- }
234
- })
235
-
236
- it('puts the docx corner cover\'s date/contact in an indented bottom block (traditional) but centers them (classic)', async () => {
237
- const paragraphs = async (styleId: string) => {
238
- const zip = await JSZip.loadAsync(await (await exportToDocx(ast, styleId)).arrayBuffer())
239
- const doc = await zip.file('word/document.xml')!.async('string')
240
- const cover = doc.slice(0, doc.indexOf('w:br w:type="page"'))
241
- return (cover.match(/<w:p>.*?<\/w:p>/gs) ?? []).map(p => ({
242
- text: [...p.matchAll(/<w:t[^>]*>(.*?)<\/w:t>/g)].map(m => m[1]).join(''),
243
- indent: p.match(/w:ind w:left="(\d+)"/)?.[1],
244
- centered: p.includes('w:jc w:val="center"'),
245
- })).filter(p => p.text)
246
- }
247
-
248
- const corners = await paragraphs('traditional')
249
- expect(corners.map(p => p.text)).toContain('by Writer Name')
250
- const cornerDate = corners.find(p => p.text === '2026-06-13')!
251
- expect(Number(cornerDate.indent)).toBeGreaterThan(0)
252
- expect(cornerDate.centered).toBe(false)
253
-
254
- const centered = await paragraphs('classic')
255
- expect(centered.map(p => p.text)).toContain('by') // on its own line
256
- expect(centered.map(p => p.text)).not.toContain('by Writer Name')
257
- expect(centered.find(p => p.text === '2026-06-13')!.centered).toBe(true)
258
- })
259
-
260
- it('hands the print document a paginator and the scene tags its headers need', async () => {
261
- const originalOpen = window.open
262
- let written = ''
263
- window.open = () => ({ document: { write: (html: string) => { written = html }, close: () => {} } } as any)
264
-
265
- try {
266
- await openScreenplayPrintPreview(parse('## INT. ROOM - DAY {#8}\n\nAction line.\n'))
267
- expect(written).toContain('<div id="print-pages">')
268
- expect(written).toContain('<div id="print-flow">')
269
- // The heading marks where scene 8 starts; its action blocks only say they belong to it.
270
- expect(written).toContain('data-scene="8" data-scene-start="8"')
271
- expect(written).toMatch(/<div data-scene="8" class="print-action"/)
272
- expect(written).toContain('paginatePrintDocument')
273
- expect(written).toContain('"continuedSuffix":"CONTINUED:"')
274
- } finally {
275
- window.open = originalOpen
276
- }
277
- })
278
-
279
- it('labels scenes by position when they carry no number of their own', async () => {
280
- const originalOpen = window.open
281
- let written = ''
282
- window.open = () => ({ document: { write: (html: string) => { written = html }, close: () => {} } } as any)
283
-
284
- try {
285
- await openScreenplayPrintPreview(parse('## INT. ROOM - DAY\n\nOne.\n\n## EXT. ROAD - DAY\n\nTwo.\n'))
286
- expect(written).toContain('data-scene="1" data-scene-start="1"')
287
- expect(written).toContain('data-scene="2" data-scene-start="2"')
288
- } finally {
289
- window.open = originalOpen
290
- }
291
- })
292
-
293
- it('gives the docx a "p<n>" page-number header and leaves the cover page bare', async () => {
294
- const zip = await JSZip.loadAsync(await (await exportToDocx(ast)).arrayBuffer())
295
- const headers = await Promise.all(
296
- Object.keys(zip.files).filter(f => /header\d*\.xml$/.test(f)).map(f => zip.file(f)!.async('string'))
297
- )
298
- const running = headers.find(h => h.includes('PAGE'))!
299
- expect(running).toContain('<w:t xml:space="preserve">p</w:t>')
300
- expect(running).toContain('<w:instrText xml:space="preserve">PAGE</w:instrText>')
301
- expect(running).toContain('w:jc w:val="right"')
302
- // Cover page: its own (empty) header, and numbered 0 so the script itself starts at p1.
303
- const doc = await zip.file('word/document.xml')!.async('string')
304
- expect(doc).toContain('<w:titlePg/>')
305
- expect(doc).toContain('<w:pgNumType w:start="0"/>')
306
- expect(headers.some(h => !h.includes('PAGE'))).toBe(true)
307
- })
308
-
309
- it('openScreenplayPrintPreview runs without error', () => {
310
- const originalOpen = window.open
311
- let mockDoc = {
312
- write: () => {},
313
- close: () => {}
314
- }
315
- window.open = () => ({ document: mockDoc } as any)
316
-
317
- try {
318
- expect(() => openScreenplayPrintPreview(ast)).not.toThrow()
319
- } finally {
320
- window.open = originalOpen
321
- }
322
- })
323
- })
324
-
325
- describe('romanized export', () => {
326
- const hindi = `---
327
- title: आख़िरी ट्रेन
328
- lang: hi
329
- lang-secondary: [en]
330
- ---
331
-
332
- ## INT. रेलवे स्टेशन - रात {#sc1}
333
-
334
- @विक्रम
335
- हम ट्रेन छूट गए, है ना?
336
- `
337
-
338
- it('feeds a romanized copy through the DOCX exporter', async () => {
339
- const doc = parse(romanizeSutra(hindi, { scope: 'all', variant: 'strict' }))
340
- const blob = await exportToDocx(doc)
341
- const xml = await (await JSZip.loadAsync(await blob.arrayBuffer())).file('word/document.xml')!.async('text')
342
- expect(xml).toContain('hama ṭrēna chūṭa gaē, hai nā?')
343
- expect(xml).toContain('VIKRAM') // name uses primary-language convention (cue uppercased by the exporter)
344
- expect(xml).toContain('ROMANIZED COPY')
345
- expect(xml).not.toContain('PAGE REFERENCE')
346
- expect(xml).not.toMatch(/[\u0900-\u097F]/) // no Devanagari left in the body
347
- })
348
-
349
- it('romanizes workflow reports from a fully romanized copy', () => {
350
- const src = hindi.replace('{#sc1}\n', '{#sc1}\n& synopsis: विक्रम ट्रेन छूटने का सच जानता है।\n')
351
- const data = extractWorkflowData(parse(romanizeSutra(src, { scope: 'all', variant: 'readable', notice: false })))
352
- const csv = exportOneLinerCsv(data.scenes)
353
- expect(csv).toContain('INT. RELAVE SṬEŚAN - RĀT') // report uppercases headings; readable drops only word-final vowels
354
- expect(csv).toContain('Vikram ṭren chūṭane kā sac jānatā hai.')
355
- expect(exportCastReportCsv(data.characters)).toContain('"VIKRAM"') // cast report uppercases names
356
- })
357
-
358
- it('dialogue scope leaves headings in native script', async () => {
359
- const doc = parse(romanizeSutra(hindi, { scope: 'dialogue', variant: 'readable' }))
360
- const blob = await exportToDocx(doc)
361
- const xml = await (await JSZip.loadAsync(await blob.arrayBuffer())).file('word/document.xml')!.async('text')
362
- expect(xml).toContain('रेलवे स्टेशन')
363
- expect(xml).toContain('ham ṭren chūṭ gae, hai nā?')
364
- })
365
- })