@sutrata/editor 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/generated-providers.json +1 -1
  3. package/package.json +36 -9
  4. package/scripts/copy-assets.mjs +0 -16
  5. package/scripts/generate-providers.mjs +0 -127
  6. package/src/ai/OnboardingDialog.tsx +0 -390
  7. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  8. package/src/ai/VoiceToolbar.tsx +0 -321
  9. package/src/ai/ai-client.ts +0 -596
  10. package/src/ai/commands.ts +0 -164
  11. package/src/ai/generated-providers.json +0 -3696
  12. package/src/ai/prompts.ts +0 -62
  13. package/src/ai/voice-service.ts +0 -96
  14. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  15. package/src/autocomplete/suggestions.ts +0 -147
  16. package/src/context/DocumentContext.tsx +0 -648
  17. package/src/desktop-api.d.ts +0 -40
  18. package/src/editor/EditorView.tsx +0 -82
  19. package/src/editor/SelectionMenu.tsx +0 -152
  20. package/src/editor/attr-pencil-plugin.ts +0 -172
  21. package/src/editor/character-decoration-plugin.ts +0 -104
  22. package/src/editor/editor-bus.ts +0 -51
  23. package/src/editor/find-highlight-plugin.ts +0 -68
  24. package/src/editor/frontmatter-field-view.ts +0 -52
  25. package/src/editor/frontmatter-field.ts +0 -115
  26. package/src/editor/grapheme-cursor.ts +0 -99
  27. package/src/editor/ime-plugin.ts +0 -85
  28. package/src/editor/insert-helper.ts +0 -43
  29. package/src/editor/keymap.ts +0 -124
  30. package/src/editor/language-class-plugin.ts +0 -91
  31. package/src/editor/plugins.ts +0 -26
  32. package/src/editor/prosemirror-to-sutra.ts +0 -192
  33. package/src/editor/scene-heading-view.ts +0 -147
  34. package/src/editor/schema.ts +0 -156
  35. package/src/editor/sutra-to-prosemirror.ts +0 -156
  36. package/src/extensions/storage-adapter.ts +0 -65
  37. package/src/extensions/types.ts +0 -60
  38. package/src/file/docx-exporter.ts +0 -585
  39. package/src/file/docx-importer.ts +0 -313
  40. package/src/file/print-paginator.ts +0 -301
  41. package/src/file/templates.ts +0 -73
  42. package/src/file/workflow-reports.ts +0 -820
  43. package/src/findreplace/FindReplace.tsx +0 -242
  44. package/src/findreplace/find-engine.ts +0 -97
  45. package/src/findreplace/scroll-to-match.ts +0 -89
  46. package/src/fonts/tofu-detect.ts +0 -53
  47. package/src/i18n/LanguageContext.tsx +0 -207
  48. package/src/i18n/language-detect.ts +0 -73
  49. package/src/i18n/useTranslation.tsx +0 -74
  50. package/src/index.ts +0 -27
  51. package/src/locales/bn.json +0 -143
  52. package/src/locales/en.json +0 -143
  53. package/src/locales/gu.json +0 -143
  54. package/src/locales/hi.json +0 -143
  55. package/src/locales/kn.json +0 -143
  56. package/src/locales/ml.json +0 -143
  57. package/src/locales/mr.json +0 -143
  58. package/src/locales/or.json +0 -143
  59. package/src/locales/pa.json +0 -143
  60. package/src/locales/si.json +0 -143
  61. package/src/locales/ta.json +0 -143
  62. package/src/locales/te.json +0 -143
  63. package/src/navigator/SceneNavigator.tsx +0 -844
  64. package/src/navigator/StatisticsDialog.tsx +0 -249
  65. package/src/navigator/scene-list.ts +0 -84
  66. package/src/settings/SettingsDialog.tsx +0 -303
  67. package/src/shell/AppBar.tsx +0 -354
  68. package/src/shell/AppShell.tsx +0 -71
  69. package/src/shell/ConfirmDialog.tsx +0 -71
  70. package/src/shell/ElementToolbar.tsx +0 -175
  71. package/src/shell/LanguageStatus.tsx +0 -101
  72. package/src/shell/StatusBar.tsx +0 -135
  73. package/src/shell/SutrataLogo.tsx +0 -89
  74. package/src/shell/Toast.tsx +0 -46
  75. package/src/shell/ToolbarButton.tsx +0 -30
  76. package/src/shell/VersionHistory.tsx +0 -212
  77. package/src/shell/Watermark.tsx +0 -9
  78. package/src/shell/fonts/fonts.css +0 -1133
  79. package/src/shell/icons.tsx +0 -335
  80. package/src/shell/toolbar-actions.ts +0 -59
  81. package/src/source/SourceView.tsx +0 -111
  82. package/src/source/find-highlight-source.ts +0 -57
  83. package/src/source/sutra-lang.ts +0 -121
  84. package/src/spellcheck/dictionaries.ts +0 -21
  85. package/src/spellcheck/hunspell.ts +0 -110
  86. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  87. package/src/styles/StyleDialog.tsx +0 -148
  88. package/src/styles/builtin-styles.ts +0 -139
  89. package/src/styles/css-adapter.ts +0 -76
  90. package/src/styles/docx-adapter.ts +0 -109
  91. package/src/styles/embed-fonts.ts +0 -81
  92. package/src/styles/pagination.ts +0 -27
  93. package/src/styles/print-adapter.ts +0 -288
  94. package/src/styles/registry.ts +0 -20
  95. package/src/styles/screenplay.css +0 -3006
  96. package/src/styles/script-ranges.ts +0 -17
  97. package/src/styles/types.ts +0 -104
  98. package/src/styles/validate.ts +0 -114
  99. package/src/templates/templates.ts +0 -194
  100. package/src/titlepage/ExportDialog.tsx +0 -494
  101. package/src/titlepage/TitlePageDialog.tsx +0 -312
  102. package/src/titlepage/frontmatter-form.ts +0 -280
  103. package/src/titlepage/languages.ts +0 -37
  104. package/src/types.ts +0 -7
  105. package/tests/AppBar.test.tsx +0 -35
  106. package/tests/AppShell.test.tsx +0 -22
  107. package/tests/ElementToolbar.test.tsx +0 -36
  108. package/tests/LanguageStatus.test.tsx +0 -129
  109. package/tests/SelectionMenu.test.tsx +0 -71
  110. package/tests/ToolbarButton.test.tsx +0 -24
  111. package/tests/ai-onboarding.test.ts +0 -102
  112. package/tests/ai-status.test.tsx +0 -72
  113. package/tests/ai-structured.test.ts +0 -94
  114. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  115. package/tests/autocomplete/suggestions.test.ts +0 -106
  116. package/tests/document-context.test.tsx +0 -565
  117. package/tests/docx-importer.test.ts +0 -160
  118. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  119. package/tests/editor/character-extension.test.ts +0 -44
  120. package/tests/editor/grapheme-cursor.test.ts +0 -118
  121. package/tests/editor/ime-plugin.test.ts +0 -72
  122. package/tests/editor/inline-note-bridge.test.ts +0 -32
  123. package/tests/editor/language-class-plugin.test.ts +0 -265
  124. package/tests/editor/title-page.test.ts +0 -124
  125. package/tests/embed-fonts.test.ts +0 -17
  126. package/tests/exporters.test.ts +0 -365
  127. package/tests/findreplace/FindReplace.test.tsx +0 -391
  128. package/tests/findreplace/find-engine.test.ts +0 -86
  129. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  130. package/tests/fonts/tofu-detect.test.ts +0 -122
  131. package/tests/i18n/LanguageContext.test.tsx +0 -146
  132. package/tests/i18n/language-detect.test.ts +0 -54
  133. package/tests/i18n/locale-parity.test.ts +0 -25
  134. package/tests/i18n/useTranslation.test.tsx +0 -45
  135. package/tests/keymap.test.ts +0 -18
  136. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  137. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  138. package/tests/navigator/scene-list.test.ts +0 -71
  139. package/tests/print-paginator.test.ts +0 -82
  140. package/tests/shell/VersionHistory.test.tsx +0 -129
  141. package/tests/spellcheck/hunspell.test.ts +0 -82
  142. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  143. package/tests/styles.test.ts +0 -315
  144. package/tests/templates/templates.test.ts +0 -83
  145. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  146. package/tests/titlepage/apply-title-page.test.ts +0 -61
  147. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  148. package/tests/toolbar-actions.test.ts +0 -93
  149. package/tests/voice-commands.test.ts +0 -58
  150. package/tsconfig.json +0 -17
  151. package/vitest.config.ts +0 -11
  152. 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
- })