@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,146 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render } from '@testing-library/react'
3
- import { LanguageProvider, useLanguage } from '../../src/i18n/LanguageContext'
4
-
5
- // Test component that uses the hook
6
- function TestComponent({
7
- text,
8
- caretPos,
9
- frontmatter,
10
- }: {
11
- text: string
12
- caretPos: number
13
- frontmatter?: Record<string, any>
14
- }) {
15
- const { resolveLanguageAtCaret } = useLanguage()
16
- const lang = resolveLanguageAtCaret(text, caretPos, frontmatter)
17
- return <div data-testid="lang-result">{lang}</div>
18
- }
19
-
20
- describe('LanguageContext', () => {
21
- it('uses document frontmatter lang as default with no other context', () => {
22
- const text = '## Scene 1\n\nSome english text'
23
- const { getByTestId } = render(
24
- <LanguageProvider>
25
- <TestComponent text={text} caretPos={20} frontmatter={{ lang: 'ta' }} />
26
- </LanguageProvider>
27
- )
28
- expect(getByTestId('lang-result').textContent).toBe('ta')
29
- })
30
-
31
- it('scene metadata lang beats frontmatter lang', () => {
32
- // Scene with & lang: metadata should take precedence over frontmatter
33
- const text = '## Scene 1\n& lang: hi\n\nयह एक हिंदी पंक्ति है।'
34
- const { getByTestId } = render(
35
- <LanguageProvider>
36
- <TestComponent text={text} caretPos={30} frontmatter={{ lang: 'ta' }} />
37
- </LanguageProvider>
38
- )
39
- expect(getByTestId('lang-result').textContent).toBe('hi')
40
- })
41
-
42
- it('inline {lang=…} tag beats all precedence', () => {
43
- const text = '## Scene 1\n\nAction with {lang=ml} tag in it.'
44
- const { getByTestId } = render(
45
- <LanguageProvider>
46
- <TestComponent text={text} caretPos={25} frontmatter={{ lang: 'ta' }} />
47
- </LanguageProvider>
48
- )
49
- expect(getByTestId('lang-result').textContent).toBe('ml')
50
- })
51
-
52
- it('falls back to detectScript when no metadata', () => {
53
- const text = '## Scene 1\n\nஇது ஒரு தமிழ் வரி.'
54
- const { getByTestId } = render(
55
- <LanguageProvider>
56
- <TestComponent text={text} caretPos={20} />
57
- </LanguageProvider>
58
- )
59
- expect(getByTestId('lang-result').textContent).toBe('ta')
60
- })
61
-
62
- it('defaults to English for unrecognized text with no metadata', () => {
63
- const text = '## Scene 1\n\n12345 !!!!'
64
- const { getByTestId } = render(
65
- <LanguageProvider>
66
- <TestComponent text={text} caretPos={20} />
67
- </LanguageProvider>
68
- )
69
- expect(getByTestId('lang-result').textContent).toBe('en')
70
- })
71
-
72
- it('throws error when useLanguage used outside provider', () => {
73
- // This should error during render
74
- const BadComponent = () => {
75
- const { currentLanguage } = useLanguage()
76
- return <div>{currentLanguage}</div>
77
- }
78
-
79
- expect(() => {
80
- render(<BadComponent />)
81
- }).toThrow('useLanguage must be used within LanguageProvider')
82
- })
83
-
84
- it('inline tag beats scene metadata', () => {
85
- // Scene has metadata 'hi', but inline tag 'ml' should win
86
- const text = '## Scene 1\n& lang: hi\n\nयह {lang=ml} पंक्ति है।'
87
- const { getByTestId } = render(
88
- <LanguageProvider>
89
- <TestComponent text={text} caretPos={35} frontmatter={{ lang: 'ta' }} />
90
- </LanguageProvider>
91
- )
92
- expect(getByTestId('lang-result').textContent).toBe('ml')
93
- })
94
-
95
- it('detects Hindi from Devanagari script', () => {
96
- const text = '## Scene 1\n\nयह एक नमूना पंक्ति है।'
97
- const { getByTestId } = render(
98
- <LanguageProvider>
99
- <TestComponent text={text} caretPos={20} />
100
- </LanguageProvider>
101
- )
102
- expect(getByTestId('lang-result').textContent).toBe('hi')
103
- })
104
-
105
- it('detects Bengali from Bengali script', () => {
106
- const text = '## Scene 1\n\nএটি একটি নমুনা লাইন।'
107
- const { getByTestId } = render(
108
- <LanguageProvider>
109
- <TestComponent text={text} caretPos={20} />
110
- </LanguageProvider>
111
- )
112
- expect(getByTestId('lang-result').textContent).toBe('bn')
113
- })
114
-
115
- it('detects Malayalam from Malayalam script', () => {
116
- const text = '## Scene 1\n\nഇത് ഒരു സാമ്പിൾ ലൈൻ ആണ്.'
117
- const { getByTestId } = render(
118
- <LanguageProvider>
119
- <TestComponent text={text} caretPos={20} />
120
- </LanguageProvider>
121
- )
122
- expect(getByTestId('lang-result').textContent).toBe('ml')
123
- })
124
-
125
- it('falls back to frontmatter when scene has no metadata', () => {
126
- // Scene without metadata, only frontmatter specified
127
- const text = '## Scene 1\n\nEnglish text without any script markers'
128
- const { getByTestId } = render(
129
- <LanguageProvider>
130
- <TestComponent text={text} caretPos={20} frontmatter={{ lang: 'hi' }} />
131
- </LanguageProvider>
132
- )
133
- expect(getByTestId('lang-result').textContent).toBe('hi')
134
- })
135
-
136
- it('uses scene metadata when no inline tag present', () => {
137
- // Scene metadata present, no inline tag
138
- const text = '## Scene 1\n& lang: gu\n\nGujarati text here'
139
- const { getByTestId } = render(
140
- <LanguageProvider>
141
- <TestComponent text={text} caretPos={25} frontmatter={{ lang: 'ta' }} />
142
- </LanguageProvider>
143
- )
144
- expect(getByTestId('lang-result').textContent).toBe('gu')
145
- })
146
- })
@@ -1,54 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { detectScript } from '../../src/i18n/language-detect'
3
-
4
- describe('language-detect', () => {
5
- it('detects pure Devanagari text as Hindi', () => {
6
- const result = detectScript('यह एक नमूना पंक्ति है।')
7
- expect(result).toBe('hi')
8
- })
9
-
10
- it('detects pure Tamil text as Tamil', () => {
11
- const result = detectScript('இது ஒரு மாதிரி வரி.')
12
- expect(result).toBe('ta')
13
- })
14
-
15
- it('detects pure Telugu text as Telugu', () => {
16
- const result = detectScript('ఇది ఒక నమూనా లైన్.')
17
- expect(result).toBe('te')
18
- })
19
-
20
- it('detects mixed Devanagari + Latin as dominant Devanagari (Hindi)', () => {
21
- const result = detectScript('यह एक नमूना hello')
22
- expect(result).toBe('hi')
23
- })
24
-
25
- it('detects mixed Tamil + Latin as dominant Tamil', () => {
26
- const result = detectScript('இது hello வரி')
27
- expect(result).toBe('ta')
28
- })
29
-
30
- it('detects pure Latin / English as English', () => {
31
- const result = detectScript('This is a sample line.')
32
- expect(result).toBe('en')
33
- })
34
-
35
- it('returns English for empty string', () => {
36
- const result = detectScript('')
37
- expect(result).toBe('en')
38
- })
39
-
40
- it('detects Bengali text', () => {
41
- const result = detectScript('এটি একটি নমুনা লাইন।')
42
- expect(result).toBe('bn')
43
- })
44
-
45
- it('detects Gujarati text', () => {
46
- const result = detectScript('આ એક નમૂનો લાઇન છે.')
47
- expect(result).toBe('gu')
48
- })
49
-
50
- it('detects Malayalam text', () => {
51
- const result = detectScript('ഇത് ഒരു സാമ്പിൾ ലൈൻ ആണ്.')
52
- expect(result).toBe('ml')
53
- })
54
- })
@@ -1,25 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import en from '../../src/locales/en.json'
3
- import hi from '../../src/locales/hi.json'
4
- import ta from '../../src/locales/ta.json'
5
- import te from '../../src/locales/te.json'
6
- import ml from '../../src/locales/ml.json'
7
- import kn from '../../src/locales/kn.json'
8
- import bn from '../../src/locales/bn.json'
9
- import gu from '../../src/locales/gu.json'
10
- import mr from '../../src/locales/mr.json'
11
- import pa from '../../src/locales/pa.json'
12
- import or from '../../src/locales/or.json'
13
- import si from '../../src/locales/si.json'
14
-
15
- const LOCALES: Record<string, Record<string, string>> = { hi, ta, te, ml, kn, bn, gu, mr, pa, or, si }
16
- const EN_KEYS = Object.keys(en)
17
-
18
- describe('locale parity', () => {
19
- for (const [lang, strings] of Object.entries(LOCALES)) {
20
- it(`${lang}.json has all keys from en.json`, () => {
21
- const missingKeys = EN_KEYS.filter(key => !(key in strings))
22
- expect(missingKeys).toEqual([])
23
- })
24
- }
25
- })
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { renderHook, act } from '@testing-library/react'
3
- import { TranslationProvider, useTranslation } from '../../src/i18n/useTranslation'
4
- import React from 'react'
5
-
6
- function wrapper({ children }: { children: React.ReactNode }) {
7
- return React.createElement(TranslationProvider, null, children)
8
- }
9
-
10
- describe('useTranslation', () => {
11
- beforeEach(() => {
12
- localStorage.clear()
13
- })
14
-
15
- it('t() returns English string by default', () => {
16
- const { result } = renderHook(() => useTranslation(), { wrapper })
17
- expect(result.current.t('toolbar.undo')).toBe('Undo')
18
- })
19
-
20
- it('t() returns Hindi string after setLocale("hi")', () => {
21
- const { result } = renderHook(() => useTranslation(), { wrapper })
22
- act(() => result.current.setLocale('hi'))
23
- expect(result.current.t('toolbar.undo')).toBe('पूर्ववत करें')
24
- })
25
-
26
- it('t() falls back to English for missing key in locale', () => {
27
- const { result } = renderHook(() => useTranslation(), { wrapper })
28
- act(() => result.current.setLocale('hi'))
29
- // Key that exists in English but hypothetically missing in Hindi would fallback
30
- const val = result.current.t('toolbar.undo')
31
- expect(typeof val).toBe('string')
32
- expect(val.length).toBeGreaterThan(0)
33
- })
34
-
35
- it('t() returns key itself for completely missing key', () => {
36
- const { result } = renderHook(() => useTranslation(), { wrapper })
37
- expect(result.current.t('nonexistent.key')).toBe('nonexistent.key')
38
- })
39
-
40
- it('setLocale ignores unknown locale codes', () => {
41
- const { result } = renderHook(() => useTranslation(), { wrapper })
42
- act(() => result.current.setLocale('xx')) // unknown
43
- expect(result.current.locale).toBe('en') // unchanged
44
- })
45
- })
@@ -1,18 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { keymapBindings } from '../src/editor/keymap'
3
-
4
- describe('keymap bindings', () => {
5
- it('does not bind element-insertion shortcuts Mod-1..Mod-7', () => {
6
- for (let n = 1; n <= 7; n++) {
7
- expect(keymapBindings[`Mod-${n}`]).toBeUndefined()
8
- }
9
- })
10
-
11
- it('keeps editing shortcuts', () => {
12
- expect(keymapBindings['Mod-z']).toBeDefined()
13
- expect(keymapBindings['Mod-b']).toBeDefined()
14
- expect(keymapBindings['Mod-i']).toBeDefined()
15
- expect(keymapBindings['Mod-u']).toBeDefined()
16
- expect(keymapBindings['Enter']).toBeDefined()
17
- })
18
- })
@@ -1,177 +0,0 @@
1
- import React, { useEffect } from 'react'
2
- import { describe, it, expect, vi } from 'vitest'
3
- import { render, screen, act, fireEvent } from '@testing-library/react'
4
- import { SceneNavigator } from '../../src/navigator/SceneNavigator'
5
- import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
6
- import { LanguageProvider } from '../../src/i18n/LanguageContext'
7
- import { TranslationProvider } from '../../src/i18n/useTranslation'
8
- import { setSourceView } from '../../src/editor/editor-bus'
9
-
10
- const SAMPLE_TEXT =
11
- '## INT. HOUSE - DAY\n& synopsis: A scene.\n\n## EXT. PARK - NIGHT\n& synopsis: Another scene.\n'
12
-
13
- /**
14
- * Helper component that sets the document text after mounting.
15
- * DocumentProvider does not accept an initialText prop, so we drive
16
- * the text through the context API.
17
- */
18
- function TextSetter({ text }: { text: string }) {
19
- const { setText } = useDocument()
20
- useEffect(() => {
21
- setText(text)
22
- // eslint-disable-next-line react-hooks/exhaustive-deps
23
- }, [])
24
- return null
25
- }
26
-
27
- function Wrapper({ text = SAMPLE_TEXT }: { text?: string }) {
28
- return (
29
- <TranslationProvider>
30
- <DocumentProvider>
31
- <LanguageProvider>
32
- <TextSetter text={text} />
33
- <SceneNavigator />
34
- </LanguageProvider>
35
- </DocumentProvider>
36
- </TranslationProvider>
37
- )
38
- }
39
-
40
- describe('SceneNavigator', () => {
41
- it('renders scene headings from text', async () => {
42
- await act(async () => {
43
- render(<Wrapper />)
44
- })
45
- expect(screen.getByText('INT. HOUSE - DAY')).toBeDefined()
46
- expect(screen.getByText('EXT. PARK - NIGHT')).toBeDefined()
47
- })
48
-
49
- it('shows scene synopses below headings', async () => {
50
- await act(async () => {
51
- render(<Wrapper />)
52
- })
53
- expect(screen.getByText('A scene.')).toBeDefined()
54
- expect(screen.getByText('Another scene.')).toBeDefined()
55
- })
56
-
57
- it('shows empty state when no scenes', async () => {
58
- await act(async () => {
59
- render(<Wrapper text="Just some plain text." />)
60
- })
61
- expect(screen.getByText(/No scenes yet/)).toBeDefined()
62
- })
63
-
64
- it('shows scene count in header', async () => {
65
- let container!: HTMLElement
66
- await act(async () => {
67
- ;({ container } = render(<Wrapper />))
68
- })
69
- const countBadge = container.querySelector('.cs-nav-count')
70
- expect(countBadge).not.toBeNull()
71
- expect(countBadge!.textContent).toBe('2')
72
- })
73
-
74
- it('renders the navigator panel when mounted', async () => {
75
- let container!: HTMLElement
76
- await act(async () => {
77
- ;({ container } = render(<Wrapper />))
78
- })
79
- // AppShell gates visibility via navVisible; SceneNavigator itself always renders
80
- expect(container.querySelector('.cs-navigator')).not.toBeNull()
81
- })
82
-
83
- it('dispatches to CodeMirror when clicked in source mode', async () => {
84
- const mockCmView = {
85
- dispatch: vi.fn(),
86
- focus: vi.fn(),
87
- }
88
- act(() => {
89
- setSourceView(mockCmView as any)
90
- })
91
-
92
- await act(async () => {
93
- render(<Wrapper />)
94
- })
95
-
96
- const sceneItem = screen.getByText('INT. HOUSE - DAY')
97
- fireEvent.click(sceneItem)
98
-
99
- expect(mockCmView.dispatch).toHaveBeenCalledWith(
100
- expect.objectContaining({
101
- selection: { anchor: 0 },
102
- effects: expect.anything(),
103
- })
104
- )
105
- expect(mockCmView.focus).toHaveBeenCalled()
106
- })
107
-
108
- it('shows a single merged synopsis+duration AI action as an icon-only button beside the synopsis field, not as labeled buttons in a separate row', async () => {
109
- let container!: HTMLElement
110
- await act(async () => {
111
- ;({ container } = render(<Wrapper />))
112
- })
113
-
114
- // No visible text labels — icon-only per-scene action now.
115
- expect(screen.queryByText('Synopsis')).toBeNull()
116
- expect(screen.queryByText('Duration')).toBeNull()
117
-
118
- // Still reachable via its accessible name, and living inside the
119
- // synopsis row (sibling of the synopsis text/input), not a separate row.
120
- const metadataBtn = screen.getAllByLabelText('Generate synopsis & estimate duration')[0]!
121
- const row = metadataBtn.closest('.cs-nav-synopsis-row')
122
- expect(row).not.toBeNull()
123
- expect(row!.querySelector('.cs-nav-scene-synopsis')).not.toBeNull()
124
- })
125
-
126
- it('hides estimated duration along with synopsis when "Hide Synopsis & Duration" is toggled', async () => {
127
- const text = '## INT. HOUSE - DAY\n& synopsis: A scene.\n& est-duration: 01:15\n'
128
- await act(async () => {
129
- render(<Wrapper text={text} />)
130
- })
131
-
132
- expect(screen.getByText('A scene.')).toBeDefined()
133
- expect(screen.getByText('⏱ 01:15')).toBeDefined()
134
-
135
- const toggle = screen.getByLabelText('Hide Synopsis & Duration')
136
- await act(async () => {
137
- fireEvent.click(toggle)
138
- })
139
-
140
- expect(screen.queryByText('A scene.')).toBeNull()
141
- expect(screen.queryByText('⏱ 01:15')).toBeNull()
142
-
143
- const showToggle = screen.getByLabelText('Show Synopsis & Duration')
144
- await act(async () => {
145
- fireEvent.click(showToggle)
146
- })
147
-
148
- expect(screen.getByText('A scene.')).toBeDefined()
149
- expect(screen.getByText('⏱ 01:15')).toBeDefined()
150
- })
151
-
152
- it('dispatches to CodeMirror for frontmatter jump in source mode', async () => {
153
- const mockCmView = {
154
- dispatch: vi.fn(),
155
- focus: vi.fn(),
156
- }
157
- act(() => {
158
- setSourceView(mockCmView as any)
159
- })
160
-
161
- await act(async () => {
162
- render(<Wrapper />)
163
- })
164
-
165
- const fmBtn = screen.getByTitle('Title Page')
166
- fireEvent.click(fmBtn)
167
-
168
- expect(mockCmView.dispatch).toHaveBeenCalledWith(
169
- expect.objectContaining({
170
- selection: { anchor: 0 },
171
- effects: expect.anything(),
172
- })
173
- )
174
- expect(mockCmView.focus).toHaveBeenCalled()
175
- })
176
- })
177
-
@@ -1,63 +0,0 @@
1
- import React, { useEffect } from 'react'
2
- import { describe, it, expect } from 'vitest'
3
- import { render, screen, act } from '@testing-library/react'
4
- import { StatisticsDialog } from '../../src/navigator/StatisticsDialog'
5
- import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
6
-
7
- /**
8
- * Reproduces the reported bug: many scenes whose seconds components alone
9
- * (ignored by the old MM-only parser) add up to well over a minute. Three
10
- * scenes here sum to 01:15 + 00:50 + 00:40 = 2:45 total — the buggy
11
- * MM-only sum would report "1 min" (1+0+0), the fix must report "3 min"
12
- * (rounded from 2m45s).
13
- */
14
- const SAMPLE_TEXT =
15
- '## INT. HOUSE - DAY\n& est-duration: 01:15\n\n' +
16
- '## EXT. PARK - NIGHT\n& est-duration: 00:50\n\n' +
17
- '## INT. OFFICE - DAY\n& est-duration: 00:40\n'
18
-
19
- function TextSetter({ text }: { text: string }) {
20
- const { setText } = useDocument()
21
- useEffect(() => {
22
- setText(text)
23
- // eslint-disable-next-line react-hooks/exhaustive-deps
24
- }, [])
25
- return null
26
- }
27
-
28
- function Wrapper({ text = SAMPLE_TEXT }: { text?: string }) {
29
- return (
30
- <DocumentProvider>
31
- <TextSetter text={text} />
32
- <StatisticsDialog onClose={() => {}} />
33
- </DocumentProvider>
34
- )
35
- }
36
-
37
- describe('StatisticsDialog', () => {
38
- it('sums scene durations by total seconds, not just the MM part', async () => {
39
- await act(async () => {
40
- render(<Wrapper />)
41
- })
42
- // 75 + 50 + 40 = 165s = 2m45s -> rounds to 3 min, not 1 min.
43
- expect(screen.getByText('3 min')).toBeInTheDocument()
44
- })
45
-
46
- it('formats totals of an hour or more as "Xh Ym"', async () => {
47
- const text =
48
- '## INT. A - DAY\n& est-duration: 30:00\n\n' +
49
- '## INT. B - DAY\n& est-duration: 35:30\n'
50
- await act(async () => {
51
- render(<Wrapper text={text} />)
52
- })
53
- // 30:00 + 35:30 = 65m30s -> rounds to 66 min -> "1h 6m"
54
- expect(screen.getByText('1h 6m')).toBeInTheDocument()
55
- })
56
-
57
- it('shows TBD when no scene has an estimated duration', async () => {
58
- await act(async () => {
59
- render(<Wrapper text="## INT. HOUSE - DAY\nSome action.\n" />)
60
- })
61
- expect(screen.getByText('TBD')).toBeInTheDocument()
62
- })
63
- })
@@ -1,71 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildSceneList } from '../../src/navigator/scene-list'
3
-
4
- const SAMPLE = `---
5
- title: Test Script
6
- lang: hi
7
- ---
8
-
9
- ## INT. LIVING ROOM - DAY {#scene-001}
10
- & synopsis: The family gathers.
11
- & status: done
12
- & location: INT
13
-
14
- This is action text.
15
-
16
- ## EXT. STREET - NIGHT {#scene-002}
17
- & synopsis: Rain falls hard.
18
- & status: wip
19
-
20
- More action.
21
-
22
- ## INT. CAFE - DAY
23
- & synopsis: A quiet moment.
24
- `
25
-
26
- describe('buildSceneList', () => {
27
- it('returns 3 scenes from the sample', () => {
28
- const scenes = buildSceneList(SAMPLE)
29
- expect(scenes.length).toBe(3)
30
- })
31
-
32
- it('extracts scene headings correctly', () => {
33
- const scenes = buildSceneList(SAMPLE)
34
- expect(scenes[0].heading).toBe('INT. LIVING ROOM - DAY')
35
- expect(scenes[1].heading).toBe('EXT. STREET - NIGHT')
36
- expect(scenes[2].heading).toBe('INT. CAFE - DAY')
37
- })
38
-
39
- it('extracts scene synopses from & synopsis: metadata', () => {
40
- const scenes = buildSceneList(SAMPLE)
41
- expect(scenes[0].synopsis).toBe('The family gathers.')
42
- expect(scenes[1].synopsis).toBe('Rain falls hard.')
43
- expect(scenes[2].synopsis).toBe('A quiet moment.')
44
- })
45
-
46
- it('extracts status metadata', () => {
47
- const scenes = buildSceneList(SAMPLE)
48
- expect(scenes[0].status).toBe('done')
49
- expect(scenes[1].status).toBe('wip')
50
- expect(scenes[2].status).toBe('')
51
- })
52
-
53
- it('assigns sequential 1-based index', () => {
54
- const scenes = buildSceneList(SAMPLE)
55
- expect(scenes[0].index).toBe(1)
56
- expect(scenes[1].index).toBe(2)
57
- expect(scenes[2].index).toBe(3)
58
- })
59
-
60
- it('returns empty array for text with no scenes', () => {
61
- const scenes = buildSceneList('Just some action text.')
62
- expect(scenes.length).toBe(0)
63
- })
64
-
65
- it('records textOffset for each scene', () => {
66
- const scenes = buildSceneList(SAMPLE)
67
- // All offsets should be distinct and increasing
68
- expect(scenes[0].textOffset).toBeLessThan(scenes[1].textOffset)
69
- expect(scenes[1].textOffset).toBeLessThan(scenes[2].textOffset)
70
- })
71
- })
@@ -1,82 +0,0 @@
1
- import { paginatePrintDocument } from '../src/file/print-paginator'
2
-
3
- /** jsdom has no layout engine, so every element measures 0. Stub the one measurement the
4
- * placement logic depends on when nothing can be split — a page body's height — as a flat
5
- * 40px per block, which makes page capacity exactly predictable. */
6
- function stubHeights(perBlockPx: number) {
7
- const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollHeight')
8
- Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
9
- configurable: true,
10
- get(this: HTMLElement) {
11
- return this.classList.contains('print-page-body') ? this.children.length * perBlockPx : perBlockPx
12
- },
13
- })
14
- return () => {
15
- if (descriptor) Object.defineProperty(HTMLElement.prototype, 'scrollHeight', descriptor)
16
- }
17
- }
18
-
19
- function setup(blocks: string): void {
20
- document.body.innerHTML = `<div id="print-pages"></div><div id="print-flow">${blocks}</div>`
21
- }
22
-
23
- function pages() {
24
- return Array.from(document.querySelectorAll('.print-page')).map(p => ({
25
- continued: p.querySelector('.print-page-continued')!.textContent,
26
- number: p.querySelector('.print-page-number')!.textContent,
27
- blocks: Array.from(p.querySelector('.print-page-body')!.children).map(c => c.className),
28
- }))
29
- }
30
-
31
- const action = (scene: string, n: number) =>
32
- `<div class="print-action" data-scene="${scene}">Action ${n}</div>`
33
- const heading = (scene: string) =>
34
- `<div class="print-scene-heading" data-scene="${scene}" data-scene-start="${scene}">INT. ROOM</div>`
35
-
36
- describe('print paginator', () => {
37
- let restore = () => {}
38
- beforeEach(() => { restore = stubHeights(40) })
39
- afterEach(() => { restore() })
40
-
41
- it('numbers every page and marks only the ones that open mid-scene as CONTINUED', () => {
42
- setup(heading('8') + Array.from({ length: 11 }, (_, i) => action('8', i)).join(''))
43
- // 204px of page height, less the 4px rounding slack, fits five 40px blocks.
44
- paginatePrintDocument({ contentHeightPx: 204, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })
45
-
46
- const result = pages()
47
- expect(result.map(p => p.number)).toEqual(['p1', 'p2', 'p3'])
48
- expect(result[0]!.continued).toBe('') // the page the scene starts on
49
- expect(result[1]!.continued).toBe('8 CONTINUED:')
50
- expect(result[2]!.continued).toBe('8 CONTINUED:')
51
- expect(document.getElementById('print-flow')).toBeNull()
52
- })
53
-
54
- it('starts a fresh page at a scene heading rather than leaving it stranded at the bottom', () => {
55
- // Four blocks of scene 8, then scene 9's heading would land as the fifth (last) block.
56
- setup(heading('8') + Array.from({ length: 3 }, (_, i) => action('8', i)).join('')
57
- + heading('9') + action('9', 0))
58
- paginatePrintDocument({ contentHeightPx: 204, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })
59
-
60
- const result = pages()
61
- expect(result).toHaveLength(2)
62
- expect(result[0]!.blocks).toHaveLength(4)
63
- expect(result[1]!.blocks[0]).toContain('print-scene-heading')
64
- expect(result[1]!.continued).toBe('') // scene 9 starts here, so no continuation
65
- })
66
-
67
- it('breaks the page where a Sutra page break says to', () => {
68
- setup(heading('8') + action('8', 0) + '<div class="print-page-break"></div>' + action('8', 1))
69
- paginatePrintDocument({ contentHeightPx: 2000, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })
70
-
71
- const result = pages()
72
- expect(result).toHaveLength(2)
73
- expect(result[0]!.blocks).toHaveLength(2)
74
- expect(result[1]!.continued).toBe('8 CONTINUED:')
75
- })
76
-
77
- it('does nothing when the document has no flow to paginate', () => {
78
- document.body.innerHTML = '<div id="print-pages"></div>'
79
- expect(() => paginatePrintDocument({ contentHeightPx: 204, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })).not.toThrow()
80
- expect(document.querySelectorAll('.print-page')).toHaveLength(0)
81
- })
82
- })