@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,75 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { Decoration, DecorationSet } from 'prosemirror-view';
3
+ import { detectScript } from '../i18n/language-detect';
4
+ /**
5
+ * ProseMirror plugin that applies language-specific CSS classes to block nodes
6
+ * based on detected or specified language (from metadata or inline tags)
7
+ */
8
+ export function createLanguageClassPlugin() {
9
+ return new Plugin({
10
+ state: {
11
+ init(config, state) {
12
+ // Initialize decorations for the initial document
13
+ return computeDecorations(state.doc);
14
+ },
15
+ apply(tr, set, oldState, newState) {
16
+ // If document changed, recompute decorations
17
+ if (tr.docChanged) {
18
+ return computeDecorations(newState.doc);
19
+ }
20
+ // Otherwise, return the existing decoration set
21
+ return set;
22
+ },
23
+ },
24
+ props: {
25
+ decorations(state) {
26
+ return this.getState(state);
27
+ },
28
+ },
29
+ });
30
+ }
31
+ /**
32
+ * Compute language decorations for the entire document
33
+ */
34
+ function computeDecorations(doc) {
35
+ const decorations = [];
36
+ // Iterate through all block nodes and assign language class
37
+ doc.forEach((node, offset) => {
38
+ if (node.isBlock && node.type.name !== 'doc') {
39
+ const lang = resolveBlockLanguage(node);
40
+ const className = `cs-lang-${lang}`;
41
+ // Create a decoration (class marker) for this block
42
+ // Pass class in both attrs (for DOM rendering) and spec (for testing/reference)
43
+ const deco = Decoration.node(offset, offset + node.nodeSize, {
44
+ class: className,
45
+ }, {
46
+ class: className,
47
+ });
48
+ decorations.push(deco);
49
+ }
50
+ });
51
+ return DecorationSet.create(doc, decorations);
52
+ }
53
+ /**
54
+ * Resolve the language for a single block node
55
+ * Checks: node metadata → detected script → fallback to 'en'
56
+ */
57
+ function resolveBlockLanguage(node) {
58
+ // Check for metadata lang attribute (e.g., lang: hi)
59
+ if (node.attrs?.lang) {
60
+ return node.attrs.lang;
61
+ }
62
+ // Check for inline {lang=…} tag in the node's text
63
+ if (node.textContent) {
64
+ const inlineMatch = /\{lang=([a-z]{2})\}/.exec(node.textContent);
65
+ if (inlineMatch?.[1]) {
66
+ return inlineMatch[1];
67
+ }
68
+ }
69
+ // Detect from text content
70
+ if (node.textContent) {
71
+ return detectScript(node.textContent);
72
+ }
73
+ // Fallback to English
74
+ return 'en';
75
+ }
@@ -0,0 +1 @@
1
+ export declare function buildPlugins(): (import("prosemirror-state").Plugin<any> | import("prosemirror-state").Plugin<import("prosemirror-view").DecorationSet>)[];
@@ -0,0 +1,25 @@
1
+ import { history } from 'prosemirror-history';
2
+ import { baseKeymap } from 'prosemirror-commands';
3
+ import { keymap } from 'prosemirror-keymap';
4
+ import { buildKeymap } from './keymap';
5
+ import { createLanguageClassPlugin } from './language-class-plugin';
6
+ import { createCharacterDecorationPlugin } from './character-decoration-plugin';
7
+ import { createImePluginWithRef } from './ime-plugin';
8
+ import { createAutocompletePlugin } from '../autocomplete/AutocompletePlugin';
9
+ import { createSpellcheckPlugin } from '../spellcheck/spellcheck-plugin';
10
+ import { createAttrPencilPlugin } from './attr-pencil-plugin';
11
+ import { createFindHighlightPlugin } from './find-highlight-plugin';
12
+ export function buildPlugins() {
13
+ return [
14
+ history(),
15
+ buildKeymap(),
16
+ keymap(baseKeymap),
17
+ createLanguageClassPlugin(),
18
+ createCharacterDecorationPlugin(),
19
+ createImePluginWithRef(),
20
+ createAutocompletePlugin(),
21
+ createSpellcheckPlugin(),
22
+ createAttrPencilPlugin(),
23
+ createFindHighlightPlugin(),
24
+ ];
25
+ }
@@ -0,0 +1,2 @@
1
+ import { Node as PmNode } from 'prosemirror-model';
2
+ export declare function prosemirrorToSutra(doc: PmNode): string;
@@ -0,0 +1,208 @@
1
+ import { schema } from './schema';
2
+ function nodeText(node) {
3
+ return node.textContent;
4
+ }
5
+ function inlineToMarkdown(node) {
6
+ let result = '';
7
+ node.forEach(child => {
8
+ if (child.isText) {
9
+ let text = child.text ?? '';
10
+ if (child.marks.find(m => m.type === schema.marks['bold']))
11
+ text = `**${text}**`;
12
+ else if (child.marks.find(m => m.type === schema.marks['italic']))
13
+ text = `*${text}*`;
14
+ else if (child.marks.find(m => m.type === schema.marks['underline']))
15
+ text = `_${text}_`;
16
+ else if (child.marks.find(m => m.type === schema.marks['note_mark']))
17
+ text = `[[${text}]]`;
18
+ result += text;
19
+ }
20
+ });
21
+ return result;
22
+ }
23
+ /**
24
+ * Extract the text value of a frontmatter_field node, converting hard_break
25
+ * nodes back to '\n' so multi-line values round-trip correctly.
26
+ */
27
+ function fieldText(node) {
28
+ let result = '';
29
+ node.forEach(child => {
30
+ if (child.isText)
31
+ result += child.text ?? '';
32
+ else if (child.type === schema.nodes['hard_break'])
33
+ result += '\n';
34
+ });
35
+ return result.trim();
36
+ }
37
+ /**
38
+ * title_page node → frontmatter text block. Dotted keys ("title.hi") are
39
+ * reassembled into one-level nested maps (2-space indent, matching the
40
+ * parser's minimal-YAML frontmatter format). Fields with empty key AND
41
+ * empty value are skipped; returns '' if nothing remains.
42
+ * Multi-line values are serialized as YAML literal block scalars (|).
43
+ */
44
+ function serializeTitlePage(node) {
45
+ const data = new Map();
46
+ node.forEach(field => {
47
+ if (field.type.name !== 'frontmatter_field')
48
+ return;
49
+ const key = (field.attrs['fmKey'] ?? '').trim();
50
+ const value = fieldText(field);
51
+ if (!key)
52
+ return;
53
+ const dot = key.indexOf('.');
54
+ if (dot > 0 && dot < key.length - 1) {
55
+ const parent = key.slice(0, dot);
56
+ const sub = key.slice(dot + 1);
57
+ const existing = data.get(parent);
58
+ const map = existing !== undefined && typeof existing === 'object' ? existing : {};
59
+ map[sub] = value;
60
+ data.set(parent, map);
61
+ }
62
+ else {
63
+ data.set(key, value);
64
+ }
65
+ });
66
+ if (data.size === 0)
67
+ return '';
68
+ const lines = ['---'];
69
+ for (const [key, value] of data) {
70
+ if (typeof value === 'string') {
71
+ if (value === '') {
72
+ lines.push(`${key}:`);
73
+ }
74
+ else if (value.includes('\n')) {
75
+ // YAML literal block scalar: preserves newlines verbatim
76
+ lines.push(`${key}: |`);
77
+ for (const ln of value.split('\n'))
78
+ lines.push(` ${ln}`);
79
+ }
80
+ else {
81
+ lines.push(`${key}: ${value}`);
82
+ }
83
+ }
84
+ else {
85
+ lines.push(`${key}:`);
86
+ for (const [sub, sv] of Object.entries(value))
87
+ lines.push(` ${sub}: ${sv}`);
88
+ }
89
+ }
90
+ lines.push('---');
91
+ return lines.join('\n') + '\n\n';
92
+ }
93
+ export function prosemirrorToSutra(doc) {
94
+ // Collect all top-level nodes into an array first
95
+ const pmNodes = [];
96
+ doc.forEach(node => pmNodes.push(node));
97
+ const blocks = [];
98
+ let i = 0;
99
+ let frontmatter = '';
100
+ if (pmNodes.length > 0 && pmNodes[0].type.name === 'title_page') {
101
+ frontmatter = serializeTitlePage(pmNodes[0]);
102
+ i = 1;
103
+ }
104
+ while (i < pmNodes.length) {
105
+ const node = pmNodes[i];
106
+ const type = node.type.name;
107
+ if (type === 'character') {
108
+ // Group: character cue + following dialogue/parenthetical lines (no blank line between them).
109
+ // Name, extension and dual-marker are all just typed text now — see
110
+ // character-decoration-plugin.ts — so the node's own text is the
111
+ // whole cue line, verbatim.
112
+ const lines = [`@${nodeText(node)}`];
113
+ i++;
114
+ while (i < pmNodes.length) {
115
+ const next = pmNodes[i];
116
+ const nextType = next.type.name;
117
+ if (nextType === 'dialogue') {
118
+ lines.push(inlineToMarkdown(next));
119
+ i++;
120
+ }
121
+ else if (nextType === 'parenthetical') {
122
+ lines.push(`( ${nodeText(next)} )`);
123
+ i++;
124
+ }
125
+ else {
126
+ break;
127
+ }
128
+ }
129
+ blocks.push(lines.join('\n'));
130
+ continue;
131
+ }
132
+ if (type === 'scene_heading') {
133
+ const id = node.attrs['id'] ? ` {#${node.attrs['id']}}` : '';
134
+ // Group heading + optional synopsis + metadata lines with single newlines (no blank
135
+ // line between them), so the parser recognises them as one heading block.
136
+ const headingLines = [`## ${nodeText(node)}${id}`];
137
+ i++;
138
+ while (i < pmNodes.length && pmNodes[i].type.name === 'scene_metadata') {
139
+ const m = pmNodes[i];
140
+ headingLines.push(`& ${m.attrs['metaKey']}: ${nodeText(m)}`);
141
+ i++;
142
+ }
143
+ blocks.push(headingLines.join('\n'));
144
+ continue;
145
+ }
146
+ if (type === 'scene_metadata') {
147
+ // Orphan metadata (outside a heading group) — emit as-is
148
+ blocks.push(`& ${node.attrs['metaKey']}: ${nodeText(node)}`);
149
+ i++;
150
+ continue;
151
+ }
152
+ if (type === 'section') {
153
+ const id = node.attrs['id'] ? ` {#${node.attrs['id']}}` : '';
154
+ blocks.push(`# ${nodeText(node)}${id}`);
155
+ i++;
156
+ continue;
157
+ }
158
+ if (type === 'action') {
159
+ blocks.push(inlineToMarkdown(node));
160
+ i++;
161
+ continue;
162
+ }
163
+ if (type === 'dialogue') {
164
+ // Orphan dialogue (should not occur after the grouping above, but handle gracefully)
165
+ blocks.push(inlineToMarkdown(node));
166
+ i++;
167
+ continue;
168
+ }
169
+ if (type === 'parenthetical') {
170
+ // Orphan parenthetical
171
+ blocks.push(`( ${nodeText(node)} )`);
172
+ i++;
173
+ continue;
174
+ }
175
+ if (type === 'transition') {
176
+ blocks.push(`>> ${nodeText(node)}`);
177
+ i++;
178
+ continue;
179
+ }
180
+ if (type === 'centered') {
181
+ blocks.push(`>> ${nodeText(node)} <<`);
182
+ i++;
183
+ continue;
184
+ }
185
+ if (type === 'lyrics') {
186
+ blocks.push(`~ ${inlineToMarkdown(node)}`);
187
+ i++;
188
+ continue;
189
+ }
190
+ if (type === 'note') {
191
+ blocks.push(`[[ ${nodeText(node)} ]]`);
192
+ i++;
193
+ continue;
194
+ }
195
+ if (type === 'comment') {
196
+ blocks.push(`<!-- ${nodeText(node)} -->`);
197
+ i++;
198
+ continue;
199
+ }
200
+ if (type === 'page_break') {
201
+ blocks.push('===');
202
+ i++;
203
+ continue;
204
+ }
205
+ i++; // unknown node type — skip
206
+ }
207
+ return frontmatter + blocks.join('\n\n') + '\n';
208
+ }
@@ -0,0 +1,27 @@
1
+ import type { Node as PmNode } from 'prosemirror-model';
2
+ import type { EditorView, NodeView, ViewMutationRecord } from 'prosemirror-view';
3
+ /**
4
+ * NodeView for scene_heading. Renders the heading text (ProseMirror-managed)
5
+ * plus an editable scene-number input on the right. Tab from inside the text
6
+ * area or a click on the number triggers focus of the input.
7
+ *
8
+ * The id value is stored purely as a node attribute — the PM content model
9
+ * only contains the heading text.
10
+ */
11
+ export declare class SceneHeadingView implements NodeView {
12
+ dom: HTMLElement;
13
+ contentDOM: HTMLElement;
14
+ private input;
15
+ private pmView;
16
+ private getPos;
17
+ constructor(node: PmNode, view: EditorView, getPos: () => number | undefined);
18
+ /** Focus the id input — called by Tab keymap handler */
19
+ focusInput(): void;
20
+ private commit;
21
+ private onInputKeydown;
22
+ private onInputBlur;
23
+ update(node: PmNode): boolean;
24
+ destroy(): void;
25
+ stopEvent(event: Event): boolean;
26
+ ignoreMutation(mutation: ViewMutationRecord): boolean;
27
+ }
@@ -0,0 +1,136 @@
1
+ import { TextSelection } from 'prosemirror-state';
2
+ /**
3
+ * NodeView for scene_heading. Renders the heading text (ProseMirror-managed)
4
+ * plus an editable scene-number input on the right. Tab from inside the text
5
+ * area or a click on the number triggers focus of the input.
6
+ *
7
+ * The id value is stored purely as a node attribute — the PM content model
8
+ * only contains the heading text.
9
+ */
10
+ export class SceneHeadingView {
11
+ dom;
12
+ contentDOM;
13
+ input;
14
+ pmView;
15
+ getPos;
16
+ constructor(node, view, getPos) {
17
+ this.pmView = view;
18
+ this.getPos = getPos;
19
+ // Outer wrapper — flex row, same as CSS sets for .cs-scene-heading
20
+ this.dom = document.createElement('div');
21
+ this.dom.className = 'cs-scene-heading';
22
+ if (node.attrs['id']) {
23
+ this.dom.dataset['sceneId'] = node.attrs['id'];
24
+ }
25
+ // Left: ProseMirror manages text content here
26
+ this.contentDOM = document.createElement('span');
27
+ this.contentDOM.className = 'cs-scene-heading-text';
28
+ this.dom.appendChild(this.contentDOM);
29
+ // Right: scene number input
30
+ this.input = document.createElement('input');
31
+ this.input.type = 'text';
32
+ this.input.className = 'cs-scene-id-input';
33
+ this.input.placeholder = '#';
34
+ this.input.value = node.attrs['id'] ?? '';
35
+ this.input.setAttribute('aria-label', 'Scene number');
36
+ this.input.spellcheck = false;
37
+ this.input.addEventListener('keydown', this.onInputKeydown);
38
+ this.input.addEventListener('blur', this.onInputBlur);
39
+ // Prevent PM from handling clicks inside the input
40
+ this.input.addEventListener('mousedown', (e) => e.stopPropagation());
41
+ this.dom.appendChild(this.input);
42
+ }
43
+ /** Focus the id input — called by Tab keymap handler */
44
+ focusInput() {
45
+ this.input.focus();
46
+ this.input.select();
47
+ }
48
+ commit() {
49
+ const raw = this.input.value.trim();
50
+ const id = raw === '' ? null : raw;
51
+ const pos = this.getPos();
52
+ if (pos === undefined)
53
+ return;
54
+ const node = this.pmView.state.doc.nodeAt(pos);
55
+ if (!node)
56
+ return;
57
+ if (node.attrs['id'] === id)
58
+ return;
59
+ const tr = this.pmView.state.tr.setNodeMarkup(pos, undefined, { ...node.attrs, id });
60
+ this.pmView.dispatch(tr);
61
+ }
62
+ onInputKeydown = (e) => {
63
+ if (e.key === 'Escape') {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ this.input.value = this.pmView.state.doc.nodeAt(this.getPos() ?? 0)?.attrs['id'] ?? '';
67
+ this.pmView.focus();
68
+ return;
69
+ }
70
+ if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ this.commit();
74
+ const pos = this.getPos();
75
+ if (pos !== undefined) {
76
+ const node = this.pmView.state.doc.nodeAt(pos);
77
+ if (node) {
78
+ const endOfText = pos + node.nodeSize - 1;
79
+ const sel = TextSelection.near(this.pmView.state.doc.resolve(endOfText), -1);
80
+ this.pmView.dispatch(this.pmView.state.tr.setSelection(sel));
81
+ }
82
+ }
83
+ this.pmView.focus();
84
+ return;
85
+ }
86
+ if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'ArrowDown') {
87
+ e.preventDefault();
88
+ e.stopPropagation();
89
+ this.commit();
90
+ // Move cursor to the start of the node after the scene heading
91
+ const pos = this.getPos();
92
+ if (pos !== undefined) {
93
+ const node = this.pmView.state.doc.nodeAt(pos);
94
+ if (node) {
95
+ const afterPos = pos + node.nodeSize;
96
+ const { doc } = this.pmView.state;
97
+ if (afterPos <= doc.content.size) {
98
+ const sel = TextSelection.near(doc.resolve(afterPos), 1);
99
+ this.pmView.dispatch(this.pmView.state.tr.setSelection(sel));
100
+ }
101
+ }
102
+ }
103
+ this.pmView.focus();
104
+ return;
105
+ }
106
+ };
107
+ onInputBlur = () => {
108
+ this.commit();
109
+ };
110
+ update(node) {
111
+ if (node.type.name !== 'scene_heading')
112
+ return false;
113
+ const id = node.attrs['id'] ?? '';
114
+ // Keep DOM attr in sync
115
+ if (id)
116
+ this.dom.dataset['sceneId'] = id;
117
+ else
118
+ delete this.dom.dataset['sceneId'];
119
+ // Only update input if it doesn't have focus (avoid clobbering in-progress edits)
120
+ if (document.activeElement !== this.input) {
121
+ this.input.value = id;
122
+ }
123
+ return true;
124
+ }
125
+ destroy() {
126
+ this.input.removeEventListener('keydown', this.onInputKeydown);
127
+ this.input.removeEventListener('blur', this.onInputBlur);
128
+ }
129
+ stopEvent(event) {
130
+ // Let PM handle events inside contentDOM; swallow events on the input
131
+ return event.target === this.input || (this.input.contains(event.target));
132
+ }
133
+ ignoreMutation(mutation) {
134
+ return this.input.contains(mutation.target) || mutation.target === this.input;
135
+ }
136
+ }
@@ -0,0 +1,3 @@
1
+ import { Schema } from 'prosemirror-model';
2
+ export declare const schema: Schema<string, string>;
3
+ export type SutraSchema = typeof schema;
@@ -0,0 +1,135 @@
1
+ import { Schema } from 'prosemirror-model';
2
+ const nodes = {
3
+ doc: { content: 'title_page? block+' },
4
+ // Frontmatter rendered as an editable card at the top of the document.
5
+ // Only valid as the first child of doc (not in the block group).
6
+ title_page: {
7
+ content: 'frontmatter_field+',
8
+ toDOM: () => ['div', { class: 'cs-title-page-block' }, 0],
9
+ parseDOM: [{ tag: 'div.cs-title-page-block' }],
10
+ },
11
+ frontmatter_field: {
12
+ content: '(text | hard_break)*',
13
+ marks: '',
14
+ attrs: { fmKey: { default: '' } },
15
+ toDOM: (node) => ['div', { class: 'cs-fm-field', 'data-key': node.attrs['fmKey'] }, 0],
16
+ parseDOM: [{ tag: 'div.cs-fm-field', getAttrs: (dom) => ({ fmKey: dom.dataset['key'] ?? '' }) }],
17
+ },
18
+ scene_heading: {
19
+ group: 'block',
20
+ content: 'inline*',
21
+ attrs: { id: { default: null } },
22
+ toDOM: (node) => {
23
+ const attrs = { class: 'cs-scene-heading' };
24
+ if (node.attrs['id'])
25
+ attrs['data-scene-id'] = node.attrs['id'];
26
+ return ['div', attrs, 0];
27
+ },
28
+ parseDOM: [{ tag: 'div.cs-scene-heading', getAttrs: (dom) => ({ id: dom.dataset['sceneId'] ?? null }) }],
29
+ },
30
+ action: {
31
+ group: 'block',
32
+ content: 'inline*',
33
+ toDOM: () => ['div', { class: 'cs-action' }, 0],
34
+ parseDOM: [{ tag: 'div.cs-action' }],
35
+ },
36
+ character: {
37
+ group: 'block',
38
+ content: 'inline*',
39
+ toDOM: () => ['div', { class: 'cs-character' }, 0],
40
+ parseDOM: [{ tag: 'div.cs-character' }],
41
+ },
42
+ dialogue: {
43
+ group: 'block',
44
+ content: 'inline*',
45
+ toDOM: () => ['div', { class: 'cs-dialogue' }, 0],
46
+ parseDOM: [{ tag: 'div.cs-dialogue' }],
47
+ },
48
+ parenthetical: {
49
+ group: 'block',
50
+ content: 'inline*',
51
+ toDOM: () => ['div', { class: 'cs-parenthetical' }, 0],
52
+ parseDOM: [{ tag: 'div.cs-parenthetical' }],
53
+ },
54
+ transition: {
55
+ group: 'block',
56
+ content: 'inline*',
57
+ toDOM: () => ['div', { class: 'cs-transition' }, 0],
58
+ parseDOM: [{ tag: 'div.cs-transition' }],
59
+ },
60
+ centered: {
61
+ group: 'block',
62
+ content: 'inline*',
63
+ toDOM: () => ['div', { class: 'cs-centered' }, 0],
64
+ parseDOM: [{ tag: 'div.cs-centered' }],
65
+ },
66
+ lyrics: {
67
+ group: 'block',
68
+ content: 'inline*',
69
+ toDOM: () => ['div', { class: 'cs-lyrics' }, 0],
70
+ parseDOM: [{ tag: 'div.cs-lyrics' }],
71
+ },
72
+ scene_metadata: {
73
+ group: 'block',
74
+ content: 'inline*',
75
+ attrs: { metaKey: { default: '' } },
76
+ toDOM: (node) => ['div', { class: 'cs-scene-metadata', 'data-key': node.attrs['metaKey'] }, 0],
77
+ parseDOM: [{ tag: 'div.cs-scene-metadata', getAttrs: (dom) => ({ metaKey: dom.dataset['key'] ?? '' }) }],
78
+ },
79
+ note: {
80
+ group: 'block',
81
+ content: 'inline*',
82
+ toDOM: () => ['div', { class: 'cs-note' }, 0],
83
+ parseDOM: [{ tag: 'div.cs-note' }],
84
+ },
85
+ comment: {
86
+ group: 'block',
87
+ content: 'inline*',
88
+ toDOM: () => ['div', { class: 'cs-comment' }, 0],
89
+ parseDOM: [{ tag: 'div.cs-comment' }],
90
+ },
91
+ page_break: {
92
+ group: 'block',
93
+ toDOM: () => ['div', { class: 'cs-page-break' }],
94
+ parseDOM: [{ tag: 'div.cs-page-break' }],
95
+ },
96
+ section: {
97
+ group: 'block',
98
+ content: 'inline*',
99
+ attrs: { level: { default: 1 }, id: { default: null } },
100
+ toDOM: (node) => [`h${node.attrs['level'] + 1}`, { class: 'cs-section' }, 0],
101
+ parseDOM: [
102
+ { tag: 'h2.cs-section', getAttrs: () => ({ level: 1 }) },
103
+ { tag: 'h3.cs-section', getAttrs: () => ({ level: 2 }) },
104
+ ],
105
+ },
106
+ hard_break: {
107
+ inline: true,
108
+ group: 'inline',
109
+ selectable: false,
110
+ toDOM: () => ['br'],
111
+ parseDOM: [{ tag: 'br' }],
112
+ },
113
+ text: { group: 'inline' },
114
+ };
115
+ const marks = {
116
+ bold: {
117
+ toDOM: () => ['strong', 0],
118
+ parseDOM: [{ tag: 'strong' }],
119
+ },
120
+ italic: {
121
+ toDOM: () => ['em', 0],
122
+ parseDOM: [{ tag: 'em' }],
123
+ },
124
+ underline: {
125
+ toDOM: () => ['u', 0],
126
+ parseDOM: [{ tag: 'u' }],
127
+ },
128
+ // Named note_mark, not note, since a block-level `note` node already exists
129
+ // (whole-line [[ ]] notes) and ProseMirror forbids a name being both.
130
+ note_mark: {
131
+ toDOM: () => ['span', { class: 'cs-inline-note' }, 0],
132
+ parseDOM: [{ tag: 'span.cs-inline-note' }],
133
+ },
134
+ };
135
+ export const schema = new Schema({ nodes, marks });
@@ -0,0 +1,3 @@
1
+ import { Node as PmNode } from 'prosemirror-model';
2
+ import type { DocumentNode } from '@sutrata/parser';
3
+ export declare function sutraToProsemirror(doc: DocumentNode): PmNode;