@sutrata/editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/LICENSE +674 -0
  2. package/dist/ai/OnboardingDialog.d.ts +6 -0
  3. package/dist/ai/OnboardingDialog.js +115 -0
  4. package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
  5. package/dist/ai/VoiceConfirmDialog.js +37 -0
  6. package/dist/ai/VoiceToolbar.d.ts +2 -0
  7. package/dist/ai/VoiceToolbar.js +177 -0
  8. package/dist/ai/ai-client.d.ts +71 -0
  9. package/dist/ai/ai-client.js +494 -0
  10. package/dist/ai/commands.d.ts +12 -0
  11. package/dist/ai/commands.js +140 -0
  12. package/dist/ai/generated-providers.json +3696 -0
  13. package/dist/ai/prompts.d.ts +25 -0
  14. package/dist/ai/prompts.js +59 -0
  15. package/dist/ai/voice-service.d.ts +15 -0
  16. package/dist/ai/voice-service.js +81 -0
  17. package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
  18. package/dist/autocomplete/AutocompletePlugin.js +19 -0
  19. package/dist/autocomplete/suggestions.d.ts +25 -0
  20. package/dist/autocomplete/suggestions.js +126 -0
  21. package/dist/context/DocumentContext.d.ts +68 -0
  22. package/dist/context/DocumentContext.js +605 -0
  23. package/dist/desktop-api.d.ts +40 -0
  24. package/dist/editor/EditorView.d.ts +2 -0
  25. package/dist/editor/EditorView.js +78 -0
  26. package/dist/editor/SelectionMenu.d.ts +2 -0
  27. package/dist/editor/SelectionMenu.js +102 -0
  28. package/dist/editor/attr-pencil-plugin.d.ts +17 -0
  29. package/dist/editor/attr-pencil-plugin.js +158 -0
  30. package/dist/editor/character-decoration-plugin.d.ts +26 -0
  31. package/dist/editor/character-decoration-plugin.js +77 -0
  32. package/dist/editor/character-view.d.ts +35 -0
  33. package/dist/editor/character-view.js +112 -0
  34. package/dist/editor/editor-bus.d.ts +14 -0
  35. package/dist/editor/editor-bus.js +39 -0
  36. package/dist/editor/find-highlight-plugin.d.ts +7 -0
  37. package/dist/editor/find-highlight-plugin.js +57 -0
  38. package/dist/editor/frontmatter-field-view.d.ts +10 -0
  39. package/dist/editor/frontmatter-field-view.js +48 -0
  40. package/dist/editor/frontmatter-field.d.ts +21 -0
  41. package/dist/editor/frontmatter-field.js +116 -0
  42. package/dist/editor/grapheme-cursor.d.ts +10 -0
  43. package/dist/editor/grapheme-cursor.js +104 -0
  44. package/dist/editor/ime-plugin.d.ts +18 -0
  45. package/dist/editor/ime-plugin.js +71 -0
  46. package/dist/editor/insert-helper.d.ts +1 -0
  47. package/dist/editor/insert-helper.js +42 -0
  48. package/dist/editor/keymap.d.ts +3 -0
  49. package/dist/editor/keymap.js +131 -0
  50. package/dist/editor/language-class-plugin.d.ts +7 -0
  51. package/dist/editor/language-class-plugin.js +75 -0
  52. package/dist/editor/plugins.d.ts +1 -0
  53. package/dist/editor/plugins.js +25 -0
  54. package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
  55. package/dist/editor/prosemirror-to-sutra.js +208 -0
  56. package/dist/editor/scene-heading-view.d.ts +27 -0
  57. package/dist/editor/scene-heading-view.js +136 -0
  58. package/dist/editor/schema.d.ts +3 -0
  59. package/dist/editor/schema.js +135 -0
  60. package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
  61. package/dist/editor/sutra-to-prosemirror.js +154 -0
  62. package/dist/editor/transliterate-input.d.ts +10 -0
  63. package/dist/editor/transliterate-input.js +88 -0
  64. package/dist/extensions/storage-adapter.d.ts +59 -0
  65. package/dist/extensions/storage-adapter.js +9 -0
  66. package/dist/extensions/types.d.ts +73 -0
  67. package/dist/extensions/types.js +12 -0
  68. package/dist/file/docx-exporter.d.ts +8 -0
  69. package/dist/file/docx-exporter.js +494 -0
  70. package/dist/file/docx-importer.d.ts +10 -0
  71. package/dist/file/docx-importer.js +283 -0
  72. package/dist/file/print-paginator.d.ts +29 -0
  73. package/dist/file/print-paginator.js +289 -0
  74. package/dist/file/templates.d.ts +2 -0
  75. package/dist/file/templates.js +66 -0
  76. package/dist/file/workflow-reports.d.ts +34 -0
  77. package/dist/file/workflow-reports.js +719 -0
  78. package/dist/findreplace/FindReplace.d.ts +2 -0
  79. package/dist/findreplace/FindReplace.js +164 -0
  80. package/dist/findreplace/find-engine.d.ts +29 -0
  81. package/dist/findreplace/find-engine.js +69 -0
  82. package/dist/findreplace/scroll-to-match.d.ts +29 -0
  83. package/dist/findreplace/scroll-to-match.js +74 -0
  84. package/dist/fonts/tofu-detect.d.ts +1 -0
  85. package/dist/fonts/tofu-detect.js +48 -0
  86. package/dist/i18n/LanguageContext.d.ts +10 -0
  87. package/dist/i18n/LanguageContext.js +174 -0
  88. package/dist/i18n/language-detect.d.ts +1 -0
  89. package/dist/i18n/language-detect.js +58 -0
  90. package/dist/i18n/useTranslation.d.ts +11 -0
  91. package/dist/i18n/useTranslation.js +60 -0
  92. package/dist/index.d.ts +11 -0
  93. package/dist/index.js +5 -0
  94. package/dist/locales/bn.json +143 -0
  95. package/dist/locales/en.json +143 -0
  96. package/dist/locales/gu.json +143 -0
  97. package/dist/locales/hi.json +143 -0
  98. package/dist/locales/kn.json +143 -0
  99. package/dist/locales/ml.json +143 -0
  100. package/dist/locales/mr.json +143 -0
  101. package/dist/locales/or.json +143 -0
  102. package/dist/locales/pa.json +143 -0
  103. package/dist/locales/si.json +143 -0
  104. package/dist/locales/ta.json +143 -0
  105. package/dist/locales/te.json +143 -0
  106. package/dist/navigator/SceneNavigator.d.ts +2 -0
  107. package/dist/navigator/SceneNavigator.js +612 -0
  108. package/dist/navigator/StatisticsDialog.d.ts +6 -0
  109. package/dist/navigator/StatisticsDialog.js +146 -0
  110. package/dist/navigator/scene-list.d.ts +16 -0
  111. package/dist/navigator/scene-list.js +70 -0
  112. package/dist/settings/SettingsDialog.d.ts +6 -0
  113. package/dist/settings/SettingsDialog.js +147 -0
  114. package/dist/shell/AppBar.d.ts +2 -0
  115. package/dist/shell/AppBar.js +34 -0
  116. package/dist/shell/AppShell.d.ts +2 -0
  117. package/dist/shell/AppShell.js +30 -0
  118. package/dist/shell/ConfirmDialog.d.ts +16 -0
  119. package/dist/shell/ConfirmDialog.js +23 -0
  120. package/dist/shell/ElementToolbar.d.ts +2 -0
  121. package/dist/shell/ElementToolbar.js +30 -0
  122. package/dist/shell/LanguageStatus.d.ts +2 -0
  123. package/dist/shell/LanguageStatus.js +65 -0
  124. package/dist/shell/StatusBar.d.ts +2 -0
  125. package/dist/shell/StatusBar.js +54 -0
  126. package/dist/shell/SutrataLogo.d.ts +8 -0
  127. package/dist/shell/SutrataLogo.js +7 -0
  128. package/dist/shell/Toast.d.ts +12 -0
  129. package/dist/shell/Toast.js +21 -0
  130. package/dist/shell/ToolbarButton.d.ts +11 -0
  131. package/dist/shell/ToolbarButton.js +4 -0
  132. package/dist/shell/VersionHistory.d.ts +12 -0
  133. package/dist/shell/VersionHistory.js +89 -0
  134. package/dist/shell/Watermark.d.ts +3 -0
  135. package/dist/shell/Watermark.js +10 -0
  136. package/dist/shell/fonts/fonts.css +1133 -0
  137. package/dist/shell/icons.d.ts +45 -0
  138. package/dist/shell/icons.js +51 -0
  139. package/dist/shell/toolbar-actions.d.ts +6 -0
  140. package/dist/shell/toolbar-actions.js +58 -0
  141. package/dist/source/SourceView.d.ts +1 -0
  142. package/dist/source/SourceView.js +106 -0
  143. package/dist/source/find-highlight-source.d.ts +14 -0
  144. package/dist/source/find-highlight-source.js +47 -0
  145. package/dist/source/sutra-lang.d.ts +2 -0
  146. package/dist/source/sutra-lang.js +97 -0
  147. package/dist/spellcheck/dictionaries.d.ts +9 -0
  148. package/dist/spellcheck/dictionaries.js +16 -0
  149. package/dist/spellcheck/hunspell.d.ts +23 -0
  150. package/dist/spellcheck/hunspell.js +94 -0
  151. package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
  152. package/dist/spellcheck/spellcheck-plugin.js +75 -0
  153. package/dist/styles/StyleDialog.d.ts +6 -0
  154. package/dist/styles/StyleDialog.js +63 -0
  155. package/dist/styles/builtin-styles.d.ts +43 -0
  156. package/dist/styles/builtin-styles.js +131 -0
  157. package/dist/styles/css-adapter.d.ts +32 -0
  158. package/dist/styles/css-adapter.js +71 -0
  159. package/dist/styles/docx-adapter.d.ts +7 -0
  160. package/dist/styles/docx-adapter.js +84 -0
  161. package/dist/styles/embed-fonts.d.ts +20 -0
  162. package/dist/styles/embed-fonts.js +81 -0
  163. package/dist/styles/pagination.d.ts +19 -0
  164. package/dist/styles/pagination.js +22 -0
  165. package/dist/styles/print-adapter.d.ts +6 -0
  166. package/dist/styles/print-adapter.js +288 -0
  167. package/dist/styles/registry.d.ts +6 -0
  168. package/dist/styles/registry.js +14 -0
  169. package/dist/styles/screenplay.css +3006 -0
  170. package/dist/styles/script-ranges.d.ts +6 -0
  171. package/dist/styles/script-ranges.js +17 -0
  172. package/dist/styles/types.d.ts +93 -0
  173. package/dist/styles/types.js +18 -0
  174. package/dist/styles/validate.d.ts +12 -0
  175. package/dist/styles/validate.js +105 -0
  176. package/dist/templates/templates.d.ts +10 -0
  177. package/dist/templates/templates.js +181 -0
  178. package/dist/titlepage/ExportDialog.d.ts +1 -0
  179. package/dist/titlepage/ExportDialog.js +152 -0
  180. package/dist/titlepage/TitlePageDialog.d.ts +6 -0
  181. package/dist/titlepage/TitlePageDialog.js +96 -0
  182. package/dist/titlepage/frontmatter-form.d.ts +56 -0
  183. package/dist/titlepage/frontmatter-form.js +245 -0
  184. package/dist/titlepage/languages.d.ts +10 -0
  185. package/dist/titlepage/languages.js +31 -0
  186. package/dist/types.d.ts +6 -0
  187. package/dist/types.js +1 -0
  188. package/package.json +47 -0
  189. package/scripts/copy-assets.mjs +16 -0
  190. package/scripts/generate-providers.mjs +127 -0
  191. package/src/ai/OnboardingDialog.tsx +390 -0
  192. package/src/ai/VoiceConfirmDialog.tsx +137 -0
  193. package/src/ai/VoiceToolbar.tsx +321 -0
  194. package/src/ai/ai-client.ts +596 -0
  195. package/src/ai/commands.ts +164 -0
  196. package/src/ai/generated-providers.json +3696 -0
  197. package/src/ai/prompts.ts +62 -0
  198. package/src/ai/voice-service.ts +96 -0
  199. package/src/autocomplete/AutocompletePlugin.ts +21 -0
  200. package/src/autocomplete/suggestions.ts +147 -0
  201. package/src/context/DocumentContext.tsx +648 -0
  202. package/src/desktop-api.d.ts +40 -0
  203. package/src/editor/EditorView.tsx +82 -0
  204. package/src/editor/SelectionMenu.tsx +152 -0
  205. package/src/editor/attr-pencil-plugin.ts +172 -0
  206. package/src/editor/character-decoration-plugin.ts +104 -0
  207. package/src/editor/editor-bus.ts +51 -0
  208. package/src/editor/find-highlight-plugin.ts +68 -0
  209. package/src/editor/frontmatter-field-view.ts +52 -0
  210. package/src/editor/frontmatter-field.ts +115 -0
  211. package/src/editor/grapheme-cursor.ts +99 -0
  212. package/src/editor/ime-plugin.ts +85 -0
  213. package/src/editor/insert-helper.ts +43 -0
  214. package/src/editor/keymap.ts +124 -0
  215. package/src/editor/language-class-plugin.ts +91 -0
  216. package/src/editor/plugins.ts +26 -0
  217. package/src/editor/prosemirror-to-sutra.ts +192 -0
  218. package/src/editor/scene-heading-view.ts +147 -0
  219. package/src/editor/schema.ts +156 -0
  220. package/src/editor/sutra-to-prosemirror.ts +156 -0
  221. package/src/extensions/storage-adapter.ts +65 -0
  222. package/src/extensions/types.ts +60 -0
  223. package/src/file/docx-exporter.ts +585 -0
  224. package/src/file/docx-importer.ts +313 -0
  225. package/src/file/print-paginator.ts +301 -0
  226. package/src/file/templates.ts +73 -0
  227. package/src/file/workflow-reports.ts +820 -0
  228. package/src/findreplace/FindReplace.tsx +242 -0
  229. package/src/findreplace/find-engine.ts +97 -0
  230. package/src/findreplace/scroll-to-match.ts +89 -0
  231. package/src/fonts/tofu-detect.ts +53 -0
  232. package/src/i18n/LanguageContext.tsx +207 -0
  233. package/src/i18n/language-detect.ts +73 -0
  234. package/src/i18n/useTranslation.tsx +74 -0
  235. package/src/index.ts +27 -0
  236. package/src/locales/bn.json +143 -0
  237. package/src/locales/en.json +143 -0
  238. package/src/locales/gu.json +143 -0
  239. package/src/locales/hi.json +143 -0
  240. package/src/locales/kn.json +143 -0
  241. package/src/locales/ml.json +143 -0
  242. package/src/locales/mr.json +143 -0
  243. package/src/locales/or.json +143 -0
  244. package/src/locales/pa.json +143 -0
  245. package/src/locales/si.json +143 -0
  246. package/src/locales/ta.json +143 -0
  247. package/src/locales/te.json +143 -0
  248. package/src/navigator/SceneNavigator.tsx +844 -0
  249. package/src/navigator/StatisticsDialog.tsx +249 -0
  250. package/src/navigator/scene-list.ts +84 -0
  251. package/src/settings/SettingsDialog.tsx +303 -0
  252. package/src/shell/AppBar.tsx +354 -0
  253. package/src/shell/AppShell.tsx +71 -0
  254. package/src/shell/ConfirmDialog.tsx +71 -0
  255. package/src/shell/ElementToolbar.tsx +175 -0
  256. package/src/shell/LanguageStatus.tsx +101 -0
  257. package/src/shell/StatusBar.tsx +135 -0
  258. package/src/shell/SutrataLogo.tsx +89 -0
  259. package/src/shell/Toast.tsx +46 -0
  260. package/src/shell/ToolbarButton.tsx +30 -0
  261. package/src/shell/VersionHistory.tsx +212 -0
  262. package/src/shell/Watermark.tsx +9 -0
  263. package/src/shell/fonts/fonts.css +1133 -0
  264. package/src/shell/icons.tsx +335 -0
  265. package/src/shell/toolbar-actions.ts +59 -0
  266. package/src/source/SourceView.tsx +111 -0
  267. package/src/source/find-highlight-source.ts +57 -0
  268. package/src/source/sutra-lang.ts +121 -0
  269. package/src/spellcheck/dictionaries.ts +21 -0
  270. package/src/spellcheck/hunspell.ts +110 -0
  271. package/src/spellcheck/spellcheck-plugin.ts +83 -0
  272. package/src/styles/StyleDialog.tsx +148 -0
  273. package/src/styles/builtin-styles.ts +139 -0
  274. package/src/styles/css-adapter.ts +76 -0
  275. package/src/styles/docx-adapter.ts +109 -0
  276. package/src/styles/embed-fonts.ts +81 -0
  277. package/src/styles/pagination.ts +27 -0
  278. package/src/styles/print-adapter.ts +288 -0
  279. package/src/styles/registry.ts +20 -0
  280. package/src/styles/screenplay.css +3006 -0
  281. package/src/styles/script-ranges.ts +17 -0
  282. package/src/styles/types.ts +104 -0
  283. package/src/styles/validate.ts +114 -0
  284. package/src/templates/templates.ts +194 -0
  285. package/src/titlepage/ExportDialog.tsx +494 -0
  286. package/src/titlepage/TitlePageDialog.tsx +312 -0
  287. package/src/titlepage/frontmatter-form.ts +280 -0
  288. package/src/titlepage/languages.ts +37 -0
  289. package/src/types.ts +7 -0
  290. package/tests/AppBar.test.tsx +35 -0
  291. package/tests/AppShell.test.tsx +22 -0
  292. package/tests/ElementToolbar.test.tsx +36 -0
  293. package/tests/LanguageStatus.test.tsx +129 -0
  294. package/tests/SelectionMenu.test.tsx +71 -0
  295. package/tests/ToolbarButton.test.tsx +24 -0
  296. package/tests/ai-onboarding.test.ts +102 -0
  297. package/tests/ai-status.test.tsx +72 -0
  298. package/tests/ai-structured.test.ts +94 -0
  299. package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
  300. package/tests/autocomplete/suggestions.test.ts +106 -0
  301. package/tests/document-context.test.tsx +565 -0
  302. package/tests/docx-importer.test.ts +160 -0
  303. package/tests/editor/character-decoration-plugin.test.ts +101 -0
  304. package/tests/editor/character-extension.test.ts +44 -0
  305. package/tests/editor/grapheme-cursor.test.ts +118 -0
  306. package/tests/editor/ime-plugin.test.ts +72 -0
  307. package/tests/editor/inline-note-bridge.test.ts +32 -0
  308. package/tests/editor/language-class-plugin.test.ts +265 -0
  309. package/tests/editor/title-page.test.ts +124 -0
  310. package/tests/embed-fonts.test.ts +17 -0
  311. package/tests/exporters.test.ts +365 -0
  312. package/tests/findreplace/FindReplace.test.tsx +391 -0
  313. package/tests/findreplace/find-engine.test.ts +86 -0
  314. package/tests/findreplace/scroll-to-match.test.ts +50 -0
  315. package/tests/fonts/tofu-detect.test.ts +122 -0
  316. package/tests/i18n/LanguageContext.test.tsx +146 -0
  317. package/tests/i18n/language-detect.test.ts +54 -0
  318. package/tests/i18n/locale-parity.test.ts +25 -0
  319. package/tests/i18n/useTranslation.test.tsx +45 -0
  320. package/tests/keymap.test.ts +18 -0
  321. package/tests/navigator/SceneNavigator.test.tsx +177 -0
  322. package/tests/navigator/StatisticsDialog.test.tsx +63 -0
  323. package/tests/navigator/scene-list.test.ts +71 -0
  324. package/tests/print-paginator.test.ts +82 -0
  325. package/tests/shell/VersionHistory.test.tsx +129 -0
  326. package/tests/spellcheck/hunspell.test.ts +82 -0
  327. package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
  328. package/tests/styles.test.ts +315 -0
  329. package/tests/templates/templates.test.ts +83 -0
  330. package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
  331. package/tests/titlepage/apply-title-page.test.ts +61 -0
  332. package/tests/titlepage/frontmatter-form.test.ts +115 -0
  333. package/tests/toolbar-actions.test.ts +93 -0
  334. package/tests/voice-commands.test.ts +58 -0
  335. package/tsconfig.json +17 -0
  336. package/vitest.config.ts +11 -0
  337. package/vitest.setup.ts +65 -0
@@ -0,0 +1,57 @@
1
+ import { Plugin, PluginKey } from 'prosemirror-state';
2
+ import { Decoration, DecorationSet } from 'prosemirror-view';
3
+ const findHighlightKey = new PluginKey('findHighlight');
4
+ // Module-level state pushed from FindReplace; the plugin reads this on each update.
5
+ let currentMatches = [];
6
+ let currentIndex = 0;
7
+ let pluginView = null;
8
+ /** Called by FindReplace whenever matches or active index change. */
9
+ export function setFindHighlights(matches, activeIndex) {
10
+ currentMatches = matches;
11
+ currentIndex = activeIndex;
12
+ if (pluginView) {
13
+ // Dispatch a no-op meta transaction to trigger decoration recompute.
14
+ const tr = pluginView.state.tr.setMeta(findHighlightKey, true);
15
+ pluginView.dispatch(tr);
16
+ }
17
+ }
18
+ /** Called by FindReplace when the panel closes — clears all highlights. */
19
+ export function clearFindHighlights() {
20
+ setFindHighlights([], 0);
21
+ }
22
+ function buildDecorations(matches, activeIndex, doc) {
23
+ if (matches.length === 0)
24
+ return DecorationSet.empty;
25
+ const decos = matches.map((m, i) => Decoration.inline(m.pmFrom, m.pmTo, {
26
+ class: i === activeIndex ? 'cs-find-active' : 'cs-find-match',
27
+ }));
28
+ return DecorationSet.create(doc, decos);
29
+ }
30
+ export function createFindHighlightPlugin() {
31
+ return new Plugin({
32
+ key: findHighlightKey,
33
+ view(view) {
34
+ pluginView = view;
35
+ return {
36
+ destroy() { pluginView = null; },
37
+ };
38
+ },
39
+ state: {
40
+ init(_config, state) {
41
+ return buildDecorations(currentMatches, currentIndex, state.doc);
42
+ },
43
+ apply(tr, _old, _oldState, newState) {
44
+ // Recompute on doc change (positions shift) or when we push a meta update.
45
+ if (tr.docChanged || tr.getMeta(findHighlightKey)) {
46
+ return buildDecorations(currentMatches, currentIndex, newState.doc);
47
+ }
48
+ return _old;
49
+ },
50
+ },
51
+ props: {
52
+ decorations(state) {
53
+ return this.getState(state);
54
+ },
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,10 @@
1
+ import type { Node as PmNode } from 'prosemirror-model';
2
+ import type { EditorView, NodeView } from 'prosemirror-view';
3
+ export declare class FrontmatterFieldView implements NodeView {
4
+ dom: HTMLElement;
5
+ contentDOM: HTMLElement;
6
+ private counter;
7
+ constructor(node: PmNode, view: EditorView, getPos: () => number | undefined);
8
+ private updateCounter;
9
+ update(node: PmNode): boolean;
10
+ }
@@ -0,0 +1,48 @@
1
+ export class FrontmatterFieldView {
2
+ dom;
3
+ contentDOM;
4
+ counter = null;
5
+ constructor(node, view, getPos) {
6
+ this.dom = document.createElement('div');
7
+ this.dom.className = 'cs-fm-field';
8
+ this.dom.dataset['key'] = node.attrs['fmKey'];
9
+ this.contentDOM = document.createElement('span');
10
+ this.contentDOM.className = 'cs-fm-field-content';
11
+ this.dom.appendChild(this.contentDOM);
12
+ if (node.attrs['fmKey'] === 'logline') {
13
+ this.counter = document.createElement('span');
14
+ this.counter.className = 'cs-fm-logline-counter';
15
+ this.updateCounter(node.textContent);
16
+ this.dom.appendChild(this.counter);
17
+ }
18
+ }
19
+ updateCounter(text) {
20
+ if (!this.counter)
21
+ return;
22
+ const words = text.split(/\s+/).filter(Boolean).length;
23
+ this.counter.textContent = `${words} words`;
24
+ if (words > 50) {
25
+ this.counter.style.color = '#c5221f'; // red warning
26
+ this.counter.style.background = '#fce8e6';
27
+ this.counter.style.borderColor = '#fad2cf';
28
+ }
29
+ else if (words > 30) {
30
+ this.counter.style.color = '#b06000'; // orange warn/target
31
+ this.counter.style.background = '#fef7e0';
32
+ this.counter.style.borderColor = '#feebc8';
33
+ }
34
+ else {
35
+ this.counter.style.color = '#8a887f'; // clean gray
36
+ this.counter.style.background = '#f0efe9';
37
+ this.counter.style.borderColor = '#e2e0d8';
38
+ }
39
+ }
40
+ update(node) {
41
+ if (node.type.name !== 'frontmatter_field')
42
+ return false;
43
+ if (node.attrs['fmKey'] === 'logline' && this.counter) {
44
+ this.updateCounter(node.textContent);
45
+ }
46
+ return true;
47
+ }
48
+ }
@@ -0,0 +1,21 @@
1
+ import type { EditorView } from 'prosemirror-view';
2
+ /**
3
+ * Sets a single flat (non-dotted) top-level frontmatter key, e.g. `style: classic`.
4
+ * Mirrors StatisticsDialog.tsx's actor-name updater (dotted-key, nested-map case) but
5
+ * for the simpler flat-key case: patches the live ProseMirror `title_page` node when a
6
+ * view is mounted and already has one, otherwise falls back to editing the raw text
7
+ * (also used when no title_page node exists yet — the editor's text-sync effect will
8
+ * rebuild the doc, including a fresh title_page node, from the new text).
9
+ */
10
+ export declare function setFlatFrontmatterField(view: EditorView | null, text: string, setText: (next: string) => void, key: string, value: string): void;
11
+ /** Raw-text fallback for when no ProseMirror view is mounted (or it has no title_page
12
+ * node yet). Creates a frontmatter block from scratch if the document doesn't have one. */
13
+ export declare function setFlatFrontmatterFieldInText(text: string, key: string, value: string): string;
14
+ /**
15
+ * Replace the whole title page with the frontmatter of `nextText` (the full Sutra text
16
+ * after a title-page form save). When the formatted editor is mounted, this is one
17
+ * ProseMirror transaction that swaps the `title_page` node, so it is a single undo step and
18
+ * the caret and scroll position are kept; the editor's normal sync then updates the text.
19
+ * Otherwise (source mode, no view) the text is set directly.
20
+ */
21
+ export declare function applyTitlePageText(view: EditorView | null, nextText: string, setText: (next: string) => void): void;
@@ -0,0 +1,116 @@
1
+ import { parse } from '@sutrata/parser';
2
+ import { schema } from './schema';
3
+ import { sutraToProsemirror } from './sutra-to-prosemirror';
4
+ /**
5
+ * Sets a single flat (non-dotted) top-level frontmatter key, e.g. `style: classic`.
6
+ * Mirrors StatisticsDialog.tsx's actor-name updater (dotted-key, nested-map case) but
7
+ * for the simpler flat-key case: patches the live ProseMirror `title_page` node when a
8
+ * view is mounted and already has one, otherwise falls back to editing the raw text
9
+ * (also used when no title_page node exists yet — the editor's text-sync effect will
10
+ * rebuild the doc, including a fresh title_page node, from the new text).
11
+ */
12
+ export function setFlatFrontmatterField(view, text, setText, key, value) {
13
+ if (view) {
14
+ let titlePagePos = null;
15
+ view.state.doc.forEach((node, offset) => {
16
+ if (node.type.name === 'title_page')
17
+ titlePagePos = offset;
18
+ });
19
+ if (titlePagePos !== null) {
20
+ let tr = view.state.tr;
21
+ const tpNode = view.state.doc.nodeAt(titlePagePos);
22
+ let existingPos = null;
23
+ let pos = titlePagePos + 1;
24
+ tpNode.forEach(field => {
25
+ if (field.attrs['fmKey'] === key)
26
+ existingPos = pos;
27
+ pos += field.nodeSize;
28
+ });
29
+ if (existingPos !== null) {
30
+ const fieldNode = view.state.doc.nodeAt(existingPos);
31
+ if (value === '') {
32
+ tr = tr.delete(existingPos, existingPos + fieldNode.nodeSize);
33
+ }
34
+ else {
35
+ tr = tr.replaceWith(existingPos + 1, existingPos + fieldNode.nodeSize - 1, schema.text(value));
36
+ }
37
+ }
38
+ else if (value !== '') {
39
+ const insertPos = titlePagePos + tpNode.nodeSize - 1;
40
+ const newField = schema.nodes['frontmatter_field'].create({ fmKey: key }, schema.text(value));
41
+ tr = tr.insert(insertPos, newField);
42
+ }
43
+ view.dispatch(tr);
44
+ return;
45
+ }
46
+ }
47
+ setText(setFlatFrontmatterFieldInText(text, key, value));
48
+ }
49
+ /** Raw-text fallback for when no ProseMirror view is mounted (or it has no title_page
50
+ * node yet). Creates a frontmatter block from scratch if the document doesn't have one. */
51
+ export function setFlatFrontmatterFieldInText(text, key, value) {
52
+ const lines = text.split('\n');
53
+ const fmStartIndex = lines.indexOf('---');
54
+ if (fmStartIndex === -1) {
55
+ if (value === '')
56
+ return text;
57
+ return `---\n${key}: ${value}\n---\n\n${text}`;
58
+ }
59
+ const fmEndIndex = lines.indexOf('---', fmStartIndex + 1);
60
+ if (fmEndIndex === -1)
61
+ return text;
62
+ const fmLines = lines.slice(fmStartIndex + 1, fmEndIndex);
63
+ let keyLineIdx = -1;
64
+ for (let i = 0; i < fmLines.length; i++) {
65
+ const match = /^([A-Za-z0-9_-]+):/.exec(fmLines[i]);
66
+ if (match && match[1] === key) {
67
+ keyLineIdx = i;
68
+ break;
69
+ }
70
+ }
71
+ if (keyLineIdx === -1) {
72
+ if (value !== '')
73
+ fmLines.push(`${key}: ${value}`);
74
+ }
75
+ else if (value === '') {
76
+ fmLines.splice(keyLineIdx, 1);
77
+ }
78
+ else {
79
+ fmLines[keyLineIdx] = `${key}: ${value}`;
80
+ }
81
+ return [...lines.slice(0, fmStartIndex + 1), ...fmLines, ...lines.slice(fmEndIndex)].join('\n');
82
+ }
83
+ /**
84
+ * Replace the whole title page with the frontmatter of `nextText` (the full Sutra text
85
+ * after a title-page form save). When the formatted editor is mounted, this is one
86
+ * ProseMirror transaction that swaps the `title_page` node, so it is a single undo step and
87
+ * the caret and scroll position are kept; the editor's normal sync then updates the text.
88
+ * Otherwise (source mode, no view) the text is set directly.
89
+ */
90
+ export function applyTitlePageText(view, nextText, setText) {
91
+ if (!view) {
92
+ setText(nextText);
93
+ return;
94
+ }
95
+ const nextDoc = sutraToProsemirror(parse(nextText));
96
+ const first = nextDoc.firstChild;
97
+ const nextTp = first && first.type.name === 'title_page' ? first : null;
98
+ let tpPos = null;
99
+ let tpSize = 0;
100
+ view.state.doc.forEach((node, offset) => {
101
+ if (tpPos === null && node.type.name === 'title_page') {
102
+ tpPos = offset;
103
+ tpSize = node.nodeSize;
104
+ }
105
+ });
106
+ let tr = view.state.tr;
107
+ if (tpPos !== null && nextTp)
108
+ tr = tr.replaceWith(tpPos, tpPos + tpSize, nextTp);
109
+ else if (tpPos !== null)
110
+ tr = tr.delete(tpPos, tpPos + tpSize);
111
+ else if (nextTp)
112
+ tr = tr.insert(0, nextTp);
113
+ else
114
+ return;
115
+ view.dispatch(tr);
116
+ }
@@ -0,0 +1,10 @@
1
+ import type { Command } from 'prosemirror-state';
2
+ /**
3
+ * Move cursor one grapheme cluster left or right.
4
+ * Returns true if the command was applied, false if at a boundary.
5
+ */
6
+ export declare function moveByGrapheme(dir: 'left' | 'right'): Command;
7
+ /**
8
+ * Extend selection one grapheme cluster left or right.
9
+ */
10
+ export declare function extendByGrapheme(dir: 'left' | 'right'): Command;
@@ -0,0 +1,104 @@
1
+ import { TextSelection } from 'prosemirror-state';
2
+ /**
3
+ * Segment text into grapheme clusters using Intl.Segmenter.
4
+ * Falls back to codepoint-by-codepoint if Segmenter is unavailable.
5
+ */
6
+ function getGraphemes(text) {
7
+ if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
8
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
9
+ return Array.from(segmenter.segment(text), (s) => s.segment);
10
+ }
11
+ // Fallback: iterate by Unicode codepoint (Array.from handles surrogates)
12
+ return Array.from(text);
13
+ }
14
+ /**
15
+ * Move cursor one grapheme cluster left or right.
16
+ * Returns true if the command was applied, false if at a boundary.
17
+ */
18
+ export function moveByGrapheme(dir) {
19
+ return (state, dispatch) => {
20
+ const { selection, doc } = state;
21
+ const { $cursor } = selection;
22
+ // Only apply to collapsed (cursor) selections
23
+ if (!$cursor)
24
+ return false;
25
+ const pos = $cursor.pos;
26
+ if (dir === 'right') {
27
+ // Get text from cursor to end of parent block
28
+ const end = $cursor.end();
29
+ if (pos >= end)
30
+ return false;
31
+ const text = doc.textBetween(pos, end);
32
+ const graphemes = getGraphemes(text);
33
+ if (graphemes.length === 0)
34
+ return false;
35
+ const advance = graphemes[0]?.length ?? 0;
36
+ if (advance === 0)
37
+ return false;
38
+ if (dispatch) {
39
+ const tr = state.tr.setSelection(TextSelection.create(doc, pos + advance));
40
+ dispatch(tr);
41
+ }
42
+ return true;
43
+ }
44
+ else {
45
+ // dir === 'left'
46
+ const start = $cursor.start();
47
+ if (pos <= start)
48
+ return false;
49
+ const text = doc.textBetween(start, pos);
50
+ const graphemes = getGraphemes(text);
51
+ if (graphemes.length === 0)
52
+ return false;
53
+ const retreat = graphemes[graphemes.length - 1]?.length ?? 0;
54
+ if (dispatch) {
55
+ const tr = state.tr.setSelection(TextSelection.create(doc, pos - retreat));
56
+ dispatch(tr);
57
+ }
58
+ return true;
59
+ }
60
+ };
61
+ }
62
+ /**
63
+ * Extend selection one grapheme cluster left or right.
64
+ */
65
+ export function extendByGrapheme(dir) {
66
+ return (state, dispatch) => {
67
+ const { selection, doc } = state;
68
+ const { anchor, head } = selection;
69
+ if (dir === 'right') {
70
+ const $head = doc.resolve(head);
71
+ const end = $head.end();
72
+ if (head >= end)
73
+ return false;
74
+ const text = doc.textBetween(head, end);
75
+ const graphemes = getGraphemes(text);
76
+ if (graphemes.length === 0)
77
+ return false;
78
+ const advance = graphemes[0]?.length ?? 0;
79
+ if (advance === 0)
80
+ return false;
81
+ if (dispatch) {
82
+ const tr = state.tr.setSelection(TextSelection.create(doc, anchor, head + advance));
83
+ dispatch(tr);
84
+ }
85
+ return true;
86
+ }
87
+ else {
88
+ const $head = doc.resolve(head);
89
+ const start = $head.start();
90
+ if (head <= start)
91
+ return false;
92
+ const text = doc.textBetween(start, head);
93
+ const graphemes = getGraphemes(text);
94
+ if (graphemes.length === 0)
95
+ return false;
96
+ const retreat = graphemes[graphemes.length - 1]?.length ?? 0;
97
+ if (dispatch) {
98
+ const tr = state.tr.setSelection(TextSelection.create(doc, anchor, head - retreat));
99
+ dispatch(tr);
100
+ }
101
+ return true;
102
+ }
103
+ };
104
+ }
@@ -0,0 +1,18 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ /**
3
+ * IME Composition plugin for ProseMirror.
4
+ *
5
+ * During IME composition (phonetic input for Indic/CJK scripts), the browser
6
+ * fires compositionstart → compositionupdate (many) → compositionend.
7
+ * ProseMirror's default behaviour re-renders the DOM on every transaction,
8
+ * which destroys the IME candidate window mid-composition.
9
+ *
10
+ * This plugin:
11
+ * 1. Tracks composition state via DOM events.
12
+ * 2. Holds incoming transactions in a queue while composition is active.
13
+ * 3. Flushes the queue on compositionend so the final committed text is
14
+ * applied as a single transaction — no duplicates, no drops.
15
+ */
16
+ export declare function createImePlugin(): Plugin<any>;
17
+ export declare function createImePluginWithRef(): Plugin<any>;
18
+ export declare function isComposing(): boolean;
@@ -0,0 +1,71 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ /**
3
+ * IME Composition plugin for ProseMirror.
4
+ *
5
+ * During IME composition (phonetic input for Indic/CJK scripts), the browser
6
+ * fires compositionstart → compositionupdate (many) → compositionend.
7
+ * ProseMirror's default behaviour re-renders the DOM on every transaction,
8
+ * which destroys the IME candidate window mid-composition.
9
+ *
10
+ * This plugin:
11
+ * 1. Tracks composition state via DOM events.
12
+ * 2. Holds incoming transactions in a queue while composition is active.
13
+ * 3. Flushes the queue on compositionend so the final committed text is
14
+ * applied as a single transaction — no duplicates, no drops.
15
+ */
16
+ export function createImePlugin() {
17
+ return new Plugin({
18
+ view(editorView) {
19
+ return new ImePluginView(editorView);
20
+ },
21
+ });
22
+ }
23
+ class ImePluginView {
24
+ composing = false;
25
+ pendingTr = null;
26
+ view;
27
+ constructor(view) {
28
+ this.view = view;
29
+ this.onCompositionStart = this.onCompositionStart.bind(this);
30
+ this.onCompositionEnd = this.onCompositionEnd.bind(this);
31
+ view.dom.addEventListener('compositionstart', this.onCompositionStart);
32
+ view.dom.addEventListener('compositionend', this.onCompositionEnd);
33
+ }
34
+ onCompositionStart() {
35
+ this.composing = true;
36
+ }
37
+ onCompositionEnd() {
38
+ this.composing = false;
39
+ // Flush any pending transaction now that composition is done
40
+ if (this.pendingTr) {
41
+ const tr = this.pendingTr;
42
+ this.pendingTr = null;
43
+ this.view.dispatch(tr);
44
+ }
45
+ }
46
+ update() {
47
+ // Called by ProseMirror after each state update.
48
+ // Nothing to do here — we handle composition via DOM events.
49
+ }
50
+ destroy() {
51
+ this.view.dom.removeEventListener('compositionstart', this.onCompositionStart);
52
+ this.view.dom.removeEventListener('compositionend', this.onCompositionEnd);
53
+ }
54
+ /** Returns whether composition is currently active. */
55
+ isComposing() {
56
+ return this.composing;
57
+ }
58
+ }
59
+ /** Singleton accessor — lets EditorView.tsx skip DOM updates during composition. */
60
+ let _imeView = null;
61
+ export function createImePluginWithRef() {
62
+ return new Plugin({
63
+ view(editorView) {
64
+ _imeView = new ImePluginView(editorView);
65
+ return _imeView;
66
+ },
67
+ });
68
+ }
69
+ export function isComposing() {
70
+ return _imeView?.isComposing() ?? false;
71
+ }
@@ -0,0 +1 @@
1
+ export declare function insertSutra(sutraText: string, mode: 'formatted' | 'source'): void;
@@ -0,0 +1,42 @@
1
+ import { Fragment, Slice } from 'prosemirror-model';
2
+ import { parse } from '@sutrata/parser';
3
+ import { sutraToProsemirror } from './sutra-to-prosemirror';
4
+ import { getEditorView, getSourceView } from './editor-bus';
5
+ export function insertSutra(sutraText, mode) {
6
+ if (mode === 'formatted') {
7
+ const view = getEditorView();
8
+ if (!view)
9
+ return;
10
+ const ast = parse(sutraText);
11
+ const pmDoc = sutraToProsemirror(ast);
12
+ // Extract nodes from the parsed doc, skipping title_page
13
+ const nodes = [];
14
+ pmDoc.content.forEach(node => {
15
+ if (node.type.name !== 'title_page') {
16
+ nodes.push(node);
17
+ }
18
+ });
19
+ if (nodes.length > 0) {
20
+ const fragment = Fragment.from(nodes);
21
+ const slice = new Slice(fragment, 0, 0);
22
+ const tr = view.state.tr.replaceSelection(slice);
23
+ view.dispatch(tr);
24
+ view.focus();
25
+ }
26
+ }
27
+ else {
28
+ const cm = getSourceView();
29
+ if (!cm)
30
+ return;
31
+ const transaction = cm.state.update({
32
+ changes: {
33
+ from: cm.state.selection.main.from,
34
+ to: cm.state.selection.main.to,
35
+ insert: sutraText,
36
+ },
37
+ selection: { anchor: cm.state.selection.main.from + sutraText.length }
38
+ });
39
+ cm.dispatch(transaction);
40
+ cm.focus();
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'prosemirror-state';
2
+ export declare const keymapBindings: Record<string, Command>;
3
+ export declare function buildKeymap(): import("prosemirror-state").Plugin<any>;
@@ -0,0 +1,131 @@
1
+ import { keymap } from 'prosemirror-keymap';
2
+ import { undo, redo } from 'prosemirror-history';
3
+ import { toggleMark } from 'prosemirror-commands';
4
+ import { TextSelection } from 'prosemirror-state';
5
+ import { schema } from './schema';
6
+ import { moveByGrapheme, extendByGrapheme } from './grapheme-cursor';
7
+ const smartEnter = (state, dispatch) => {
8
+ const { $from } = state.selection;
9
+ const node = $from.parent;
10
+ if (node.type === schema.nodes['frontmatter_field']) {
11
+ // Enter moves to the next field, or exits into the body. New fields are
12
+ // added via the pencil menu, never by splitting.
13
+ if (dispatch) {
14
+ const after = $from.after();
15
+ dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(after), 1)));
16
+ }
17
+ return true;
18
+ }
19
+ if (node.type === schema.nodes['scene_heading'] && $from.pos === $from.end()) {
20
+ if (dispatch) {
21
+ const actionType = schema.nodes['action'];
22
+ const insertPos = $from.after();
23
+ const tr = state.tr.insert(insertPos, actionType.create());
24
+ dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos + 1))));
25
+ }
26
+ return true;
27
+ }
28
+ if (node.type === schema.nodes['character']) {
29
+ if (dispatch) {
30
+ const dialogueType = schema.nodes['dialogue'];
31
+ const insertPos = $from.after();
32
+ const tr = state.tr.insert(insertPos, dialogueType.create());
33
+ dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos + 1))));
34
+ }
35
+ return true;
36
+ }
37
+ if (node.type === schema.nodes['dialogue'] || node.type === schema.nodes['parenthetical']) {
38
+ if (dispatch) {
39
+ const actionType = schema.nodes['action'];
40
+ const insertPos = $from.after();
41
+ const tr = state.tr.insert(insertPos, actionType.create());
42
+ dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos + 1))));
43
+ }
44
+ return true;
45
+ }
46
+ // Every other block type (action, transition, centered, lyrics, section,
47
+ // scene_metadata, note, comment…) has no dedicated rule above: split into
48
+ // another node of the SAME type. Without an explicit `typesAfter`, PM's
49
+ // default splitBlock command picks the first node type declared in the
50
+ // `block` group in schema.ts (scene_heading) rather than preserving the
51
+ // current type — so plain baseKeymap fallthrough silently turns the next
52
+ // line into a scene heading.
53
+ if (dispatch) {
54
+ const tr = state.tr.split($from.pos, 1, [{ type: node.type }]);
55
+ dispatch(tr.scrollIntoView());
56
+ }
57
+ return true;
58
+ };
59
+ export const keymapBindings = {
60
+ // Editing
61
+ 'Mod-z': undo,
62
+ 'Mod-y': redo,
63
+ 'Mod-Shift-Z': redo,
64
+ 'Mod-b': (state, dispatch) => {
65
+ const mark = schema.marks['bold'];
66
+ if (state.selection.empty)
67
+ return false;
68
+ return toggleMark(mark)(state, dispatch);
69
+ },
70
+ 'Mod-i': (state, dispatch) => {
71
+ const mark = schema.marks['italic'];
72
+ if (state.selection.empty)
73
+ return false;
74
+ return toggleMark(mark)(state, dispatch);
75
+ },
76
+ 'Mod-u': (state, dispatch) => {
77
+ const mark = schema.marks['underline'];
78
+ if (state.selection.empty)
79
+ return false;
80
+ return toggleMark(mark)(state, dispatch);
81
+ },
82
+ 'Enter': smartEnter,
83
+ 'Shift-Enter': (state, dispatch) => {
84
+ const { $from } = state.selection;
85
+ if ($from.parent.type !== schema.nodes['frontmatter_field'])
86
+ return false;
87
+ if (dispatch) {
88
+ const br = schema.nodes['hard_break'].create();
89
+ dispatch(state.tr.replaceSelectionWith(br).scrollIntoView());
90
+ }
91
+ return true;
92
+ },
93
+ 'Tab': (state, _dispatch, view) => {
94
+ const { $from } = state.selection;
95
+ if ($from.parent.type !== schema.nodes['scene_heading'])
96
+ return false;
97
+ if (!view)
98
+ return false;
99
+ const pos = $from.before();
100
+ const domNode = view.nodeDOM(pos);
101
+ const el = domNode instanceof HTMLElement ? domNode : domNode?.parentElement;
102
+ const input = el?.querySelector('input.cs-scene-id-input');
103
+ if (input) {
104
+ input.focus();
105
+ input.select();
106
+ return true;
107
+ }
108
+ return false;
109
+ },
110
+ 'ArrowDown': (state, dispatch) => {
111
+ const { $from } = state.selection;
112
+ if ($from.parent.type !== schema.nodes['scene_heading'])
113
+ return false;
114
+ // PM's built-in ArrowDown gets confused by the sibling scene-number input.
115
+ // Explicitly jump to the start of the node after the scene heading.
116
+ const afterPos = $from.after();
117
+ if (afterPos > state.doc.content.size)
118
+ return false;
119
+ if (dispatch) {
120
+ dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(afterPos), 1)));
121
+ }
122
+ return true;
123
+ },
124
+ 'ArrowLeft': moveByGrapheme('left'),
125
+ 'ArrowRight': moveByGrapheme('right'),
126
+ 'Shift-ArrowLeft': extendByGrapheme('left'),
127
+ 'Shift-ArrowRight': extendByGrapheme('right'),
128
+ };
129
+ export function buildKeymap() {
130
+ return keymap(keymapBindings);
131
+ }
@@ -0,0 +1,7 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { DecorationSet } from 'prosemirror-view';
3
+ /**
4
+ * ProseMirror plugin that applies language-specific CSS classes to block nodes
5
+ * based on detected or specified language (from metadata or inline tags)
6
+ */
7
+ export declare function createLanguageClassPlugin(): Plugin<DecorationSet>;