@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,115 +0,0 @@
1
- import type { EditorView } from 'prosemirror-view'
2
- import { parse } from '@sutrata/parser'
3
- import { schema } from './schema'
4
- import { sutraToProsemirror } from './sutra-to-prosemirror'
5
-
6
- /**
7
- * Sets a single flat (non-dotted) top-level frontmatter key, e.g. `style: classic`.
8
- * Mirrors StatisticsDialog.tsx's actor-name updater (dotted-key, nested-map case) but
9
- * for the simpler flat-key case: patches the live ProseMirror `title_page` node when a
10
- * view is mounted and already has one, otherwise falls back to editing the raw text
11
- * (also used when no title_page node exists yet — the editor's text-sync effect will
12
- * rebuild the doc, including a fresh title_page node, from the new text).
13
- */
14
- export function setFlatFrontmatterField(
15
- view: EditorView | null,
16
- text: string,
17
- setText: (next: string) => void,
18
- key: string,
19
- value: string,
20
- ): void {
21
- if (view) {
22
- let titlePagePos: number | null = null
23
- view.state.doc.forEach((node, offset) => {
24
- if (node.type.name === 'title_page') titlePagePos = offset
25
- })
26
-
27
- if (titlePagePos !== null) {
28
- let tr = view.state.tr
29
- const tpNode = view.state.doc.nodeAt(titlePagePos)!
30
- let existingPos: number | null = null
31
- let pos = titlePagePos + 1
32
- tpNode.forEach(field => {
33
- if (field.attrs['fmKey'] === key) existingPos = pos
34
- pos += field.nodeSize
35
- })
36
-
37
- if (existingPos !== null) {
38
- const fieldNode = view.state.doc.nodeAt(existingPos)!
39
- if (value === '') {
40
- tr = tr.delete(existingPos, existingPos + fieldNode.nodeSize)
41
- } else {
42
- tr = tr.replaceWith(existingPos + 1, existingPos + fieldNode.nodeSize - 1, schema.text(value))
43
- }
44
- } else if (value !== '') {
45
- const insertPos = titlePagePos + tpNode.nodeSize - 1
46
- const newField = schema.nodes['frontmatter_field']!.create({ fmKey: key }, schema.text(value))
47
- tr = tr.insert(insertPos, newField)
48
- }
49
- view.dispatch(tr)
50
- return
51
- }
52
- }
53
- setText(setFlatFrontmatterFieldInText(text, key, value))
54
- }
55
-
56
- /** Raw-text fallback for when no ProseMirror view is mounted (or it has no title_page
57
- * node yet). Creates a frontmatter block from scratch if the document doesn't have one. */
58
- export function setFlatFrontmatterFieldInText(text: string, key: string, value: string): string {
59
- const lines = text.split('\n')
60
- const fmStartIndex = lines.indexOf('---')
61
- if (fmStartIndex === -1) {
62
- if (value === '') return text
63
- return `---\n${key}: ${value}\n---\n\n${text}`
64
- }
65
- const fmEndIndex = lines.indexOf('---', fmStartIndex + 1)
66
- if (fmEndIndex === -1) return text
67
-
68
- const fmLines = lines.slice(fmStartIndex + 1, fmEndIndex)
69
- let keyLineIdx = -1
70
- for (let i = 0; i < fmLines.length; i++) {
71
- const match = /^([A-Za-z0-9_-]+):/.exec(fmLines[i]!)
72
- if (match && match[1] === key) { keyLineIdx = i; break }
73
- }
74
-
75
- if (keyLineIdx === -1) {
76
- if (value !== '') fmLines.push(`${key}: ${value}`)
77
- } else if (value === '') {
78
- fmLines.splice(keyLineIdx, 1)
79
- } else {
80
- fmLines[keyLineIdx] = `${key}: ${value}`
81
- }
82
-
83
- return [...lines.slice(0, fmStartIndex + 1), ...fmLines, ...lines.slice(fmEndIndex)].join('\n')
84
- }
85
-
86
- /**
87
- * Replace the whole title page with the frontmatter of `nextText` (the full Sutra text
88
- * after a title-page form save). When the formatted editor is mounted, this is one
89
- * ProseMirror transaction that swaps the `title_page` node, so it is a single undo step and
90
- * the caret and scroll position are kept; the editor's normal sync then updates the text.
91
- * Otherwise (source mode, no view) the text is set directly.
92
- */
93
- export function applyTitlePageText(
94
- view: EditorView | null,
95
- nextText: string,
96
- setText: (next: string) => void,
97
- ): void {
98
- if (!view) { setText(nextText); return }
99
- const nextDoc = sutraToProsemirror(parse(nextText))
100
- const first = nextDoc.firstChild
101
- const nextTp = first && first.type.name === 'title_page' ? first : null
102
-
103
- let tpPos: number | null = null
104
- let tpSize = 0
105
- view.state.doc.forEach((node, offset) => {
106
- if (tpPos === null && node.type.name === 'title_page') { tpPos = offset; tpSize = node.nodeSize }
107
- })
108
-
109
- let tr = view.state.tr
110
- if (tpPos !== null && nextTp) tr = tr.replaceWith(tpPos, tpPos + tpSize, nextTp)
111
- else if (tpPos !== null) tr = tr.delete(tpPos, tpPos + tpSize)
112
- else if (nextTp) tr = tr.insert(0, nextTp)
113
- else return
114
- view.dispatch(tr)
115
- }
@@ -1,99 +0,0 @@
1
- import type { Command } from 'prosemirror-state'
2
- import { EditorState, TextSelection, Transaction } from 'prosemirror-state'
3
-
4
- /**
5
- * Segment text into grapheme clusters using Intl.Segmenter.
6
- * Falls back to codepoint-by-codepoint if Segmenter is unavailable.
7
- */
8
- function getGraphemes(text: string): string[] {
9
- if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
10
- const segmenter = new (Intl as any).Segmenter(undefined, { granularity: 'grapheme' })
11
- return Array.from(segmenter.segment(text), (s: any) => s.segment)
12
- }
13
- // Fallback: iterate by Unicode codepoint (Array.from handles surrogates)
14
- return Array.from(text)
15
- }
16
-
17
- /**
18
- * Move cursor one grapheme cluster left or right.
19
- * Returns true if the command was applied, false if at a boundary.
20
- */
21
- export function moveByGrapheme(dir: 'left' | 'right'): Command {
22
- return (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
23
- const { selection, doc } = state
24
- const { $cursor } = selection as TextSelection
25
-
26
- // Only apply to collapsed (cursor) selections
27
- if (!$cursor) return false
28
-
29
- const pos = $cursor.pos
30
-
31
- if (dir === 'right') {
32
- // Get text from cursor to end of parent block
33
- const end = $cursor.end()
34
- if (pos >= end) return false
35
- const text = doc.textBetween(pos, end)
36
- const graphemes = getGraphemes(text)
37
- if (graphemes.length === 0) return false
38
- const advance = graphemes[0]?.length ?? 0
39
- if (advance === 0) return false
40
- if (dispatch) {
41
- const tr = state.tr.setSelection(TextSelection.create(doc, pos + advance))
42
- dispatch(tr)
43
- }
44
- return true
45
- } else {
46
- // dir === 'left'
47
- const start = $cursor.start()
48
- if (pos <= start) return false
49
- const text = doc.textBetween(start, pos)
50
- const graphemes = getGraphemes(text)
51
- if (graphemes.length === 0) return false
52
- const retreat = graphemes[graphemes.length - 1]?.length ?? 0
53
- if (dispatch) {
54
- const tr = state.tr.setSelection(TextSelection.create(doc, pos - retreat))
55
- dispatch(tr)
56
- }
57
- return true
58
- }
59
- }
60
- }
61
-
62
- /**
63
- * Extend selection one grapheme cluster left or right.
64
- */
65
- export function extendByGrapheme(dir: 'left' | 'right'): Command {
66
- return (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
67
- const { selection, doc } = state
68
- const { anchor, head } = selection
69
-
70
- if (dir === 'right') {
71
- const $head = doc.resolve(head)
72
- const end = $head.end()
73
- if (head >= end) return false
74
- const text = doc.textBetween(head, end)
75
- const graphemes = getGraphemes(text)
76
- if (graphemes.length === 0) return false
77
- const advance = graphemes[0]?.length ?? 0
78
- if (advance === 0) return false
79
- if (dispatch) {
80
- const tr = state.tr.setSelection(TextSelection.create(doc, anchor, head + advance))
81
- dispatch(tr)
82
- }
83
- return true
84
- } else {
85
- const $head = doc.resolve(head)
86
- const start = $head.start()
87
- if (head <= start) return false
88
- const text = doc.textBetween(start, head)
89
- const graphemes = getGraphemes(text)
90
- if (graphemes.length === 0) return false
91
- const retreat = graphemes[graphemes.length - 1]?.length ?? 0
92
- if (dispatch) {
93
- const tr = state.tr.setSelection(TextSelection.create(doc, anchor, head - retreat))
94
- dispatch(tr)
95
- }
96
- return true
97
- }
98
- }
99
- }
@@ -1,85 +0,0 @@
1
- import { Plugin } from 'prosemirror-state'
2
- import { EditorView } from 'prosemirror-view'
3
-
4
- /**
5
- * IME Composition plugin for ProseMirror.
6
- *
7
- * During IME composition (phonetic input for Indic/CJK scripts), the browser
8
- * fires compositionstart → compositionupdate (many) → compositionend.
9
- * ProseMirror's default behaviour re-renders the DOM on every transaction,
10
- * which destroys the IME candidate window mid-composition.
11
- *
12
- * This plugin:
13
- * 1. Tracks composition state via DOM events.
14
- * 2. Holds incoming transactions in a queue while composition is active.
15
- * 3. Flushes the queue on compositionend so the final committed text is
16
- * applied as a single transaction — no duplicates, no drops.
17
- */
18
- export function createImePlugin() {
19
- return new Plugin({
20
- view(editorView: EditorView) {
21
- return new ImePluginView(editorView)
22
- },
23
- })
24
- }
25
-
26
- class ImePluginView {
27
- private composing = false
28
- private pendingTr: import('prosemirror-state').Transaction | null = null
29
- private view: EditorView
30
-
31
- constructor(view: EditorView) {
32
- this.view = view
33
-
34
- this.onCompositionStart = this.onCompositionStart.bind(this)
35
- this.onCompositionEnd = this.onCompositionEnd.bind(this)
36
-
37
- view.dom.addEventListener('compositionstart', this.onCompositionStart)
38
- view.dom.addEventListener('compositionend', this.onCompositionEnd)
39
- }
40
-
41
- private onCompositionStart() {
42
- this.composing = true
43
- }
44
-
45
- private onCompositionEnd() {
46
- this.composing = false
47
- // Flush any pending transaction now that composition is done
48
- if (this.pendingTr) {
49
- const tr = this.pendingTr
50
- this.pendingTr = null
51
- this.view.dispatch(tr)
52
- }
53
- }
54
-
55
- update() {
56
- // Called by ProseMirror after each state update.
57
- // Nothing to do here — we handle composition via DOM events.
58
- }
59
-
60
- destroy() {
61
- this.view.dom.removeEventListener('compositionstart', this.onCompositionStart)
62
- this.view.dom.removeEventListener('compositionend', this.onCompositionEnd)
63
- }
64
-
65
- /** Returns whether composition is currently active. */
66
- isComposing(): boolean {
67
- return this.composing
68
- }
69
- }
70
-
71
- /** Singleton accessor — lets EditorView.tsx skip DOM updates during composition. */
72
- let _imeView: ImePluginView | null = null
73
-
74
- export function createImePluginWithRef() {
75
- return new Plugin({
76
- view(editorView: EditorView) {
77
- _imeView = new ImePluginView(editorView)
78
- return _imeView
79
- },
80
- })
81
- }
82
-
83
- export function isComposing(): boolean {
84
- return _imeView?.isComposing() ?? false
85
- }
@@ -1,43 +0,0 @@
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
-
6
- export function insertSutra(sutraText: string, mode: 'formatted' | 'source') {
7
- if (mode === 'formatted') {
8
- const view = getEditorView()
9
- if (!view) return
10
-
11
- const ast = parse(sutraText)
12
- const pmDoc = sutraToProsemirror(ast)
13
-
14
- // Extract nodes from the parsed doc, skipping title_page
15
- const nodes: any[] = []
16
- pmDoc.content.forEach(node => {
17
- if (node.type.name !== 'title_page') {
18
- nodes.push(node)
19
- }
20
- })
21
-
22
- if (nodes.length > 0) {
23
- const fragment = Fragment.from(nodes)
24
- const slice = new Slice(fragment, 0, 0)
25
- const tr = view.state.tr.replaceSelection(slice)
26
- view.dispatch(tr)
27
- view.focus()
28
- }
29
- } else {
30
- const cm = getSourceView()
31
- if (!cm) return
32
- const transaction = cm.state.update({
33
- changes: {
34
- from: cm.state.selection.main.from,
35
- to: cm.state.selection.main.to,
36
- insert: sutraText,
37
- },
38
- selection: { anchor: cm.state.selection.main.from + sutraText.length }
39
- })
40
- cm.dispatch(transaction)
41
- cm.focus()
42
- }
43
- }
@@ -1,124 +0,0 @@
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 type { Command } from 'prosemirror-state'
6
- import { schema } from './schema'
7
- import { moveByGrapheme, extendByGrapheme } from './grapheme-cursor'
8
-
9
- const smartEnter: Command = (state, dispatch) => {
10
- const { $from } = state.selection
11
- const node = $from.parent
12
-
13
- if (node.type === schema.nodes['frontmatter_field']) {
14
- // Enter moves to the next field, or exits into the body. New fields are
15
- // added via the pencil menu, never by splitting.
16
- if (dispatch) {
17
- const after = $from.after()
18
- dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(after), 1)))
19
- }
20
- return true
21
- }
22
- if (node.type === schema.nodes['scene_heading'] && $from.pos === $from.end()) {
23
- if (dispatch) {
24
- const actionType = schema.nodes['action']!
25
- const insertPos = $from.after()
26
- const tr = state.tr.insert(insertPos, actionType.create())
27
- dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos + 1))))
28
- }
29
- return true
30
- }
31
- if (node.type === schema.nodes['character']) {
32
- if (dispatch) {
33
- const dialogueType = schema.nodes['dialogue']!
34
- const insertPos = $from.after()
35
- const tr = state.tr.insert(insertPos, dialogueType.create())
36
- dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos + 1))))
37
- }
38
- return true
39
- }
40
- if (node.type === schema.nodes['dialogue'] || node.type === schema.nodes['parenthetical']) {
41
- if (dispatch) {
42
- const actionType = schema.nodes['action']!
43
- const insertPos = $from.after()
44
- const tr = state.tr.insert(insertPos, actionType.create())
45
- dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos + 1))))
46
- }
47
- return true
48
- }
49
- // Every other block type (action, transition, centered, lyrics, section,
50
- // scene_metadata, note, comment…) has no dedicated rule above: split into
51
- // another node of the SAME type. Without an explicit `typesAfter`, PM's
52
- // default splitBlock command picks the first node type declared in the
53
- // `block` group in schema.ts (scene_heading) rather than preserving the
54
- // current type — so plain baseKeymap fallthrough silently turns the next
55
- // line into a scene heading.
56
- if (dispatch) {
57
- const tr = state.tr.split($from.pos, 1, [{ type: node.type }])
58
- dispatch(tr.scrollIntoView())
59
- }
60
- return true
61
- }
62
-
63
- export const keymapBindings: Record<string, Command> = {
64
- // Editing
65
- 'Mod-z': undo,
66
- 'Mod-y': redo,
67
- 'Mod-Shift-Z': redo,
68
- 'Mod-b': (state, dispatch) => {
69
- const mark = schema.marks['bold']!
70
- if (state.selection.empty) return false
71
- return toggleMark(mark)(state, dispatch)
72
- },
73
- 'Mod-i': (state, dispatch) => {
74
- const mark = schema.marks['italic']!
75
- if (state.selection.empty) return false
76
- return toggleMark(mark)(state, dispatch)
77
- },
78
- 'Mod-u': (state, dispatch) => {
79
- const mark = schema.marks['underline']!
80
- if (state.selection.empty) return false
81
- return toggleMark(mark)(state, dispatch)
82
- },
83
- 'Enter': smartEnter,
84
- 'Shift-Enter': (state, dispatch) => {
85
- const { $from } = state.selection
86
- if ($from.parent.type !== schema.nodes['frontmatter_field']) 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']) return false
96
- if (!view) return false
97
- const pos = $from.before()
98
- const domNode = view.nodeDOM(pos)
99
- const el = domNode instanceof HTMLElement ? domNode : (domNode as Node | null)?.parentElement
100
- const input = el?.querySelector<HTMLInputElement>('input.cs-scene-id-input')
101
- if (input) { input.focus(); input.select(); return true }
102
- return false
103
- },
104
- 'ArrowDown': (state, dispatch) => {
105
- const { $from } = state.selection
106
- if ($from.parent.type !== schema.nodes['scene_heading']) return false
107
- // PM's built-in ArrowDown gets confused by the sibling scene-number input.
108
- // Explicitly jump to the start of the node after the scene heading.
109
- const afterPos = $from.after()
110
- if (afterPos > state.doc.content.size) return false
111
- if (dispatch) {
112
- dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(afterPos), 1)))
113
- }
114
- return true
115
- },
116
- 'ArrowLeft': moveByGrapheme('left'),
117
- 'ArrowRight': moveByGrapheme('right'),
118
- 'Shift-ArrowLeft': extendByGrapheme('left'),
119
- 'Shift-ArrowRight': extendByGrapheme('right'),
120
- }
121
-
122
- export function buildKeymap() {
123
- return keymap(keymapBindings)
124
- }
@@ -1,91 +0,0 @@
1
- import { Plugin } from 'prosemirror-state'
2
- import { Decoration, DecorationSet } from 'prosemirror-view'
3
- import { detectScript } from '../i18n/language-detect'
4
- import type { Node as PmNode } from 'prosemirror-model'
5
- import type { EditorState } from 'prosemirror-state'
6
-
7
- /**
8
- * ProseMirror plugin that applies language-specific CSS classes to block nodes
9
- * based on detected or specified language (from metadata or inline tags)
10
- */
11
- export function createLanguageClassPlugin() {
12
- return new Plugin({
13
- state: {
14
- init(config, state) {
15
- // Initialize decorations for the initial document
16
- return computeDecorations(state.doc)
17
- },
18
- apply(tr, set, oldState, newState) {
19
- // If document changed, recompute decorations
20
- if (tr.docChanged) {
21
- return computeDecorations(newState.doc)
22
- }
23
- // Otherwise, return the existing decoration set
24
- return set
25
- },
26
- },
27
- props: {
28
- decorations(state: EditorState) {
29
- return this.getState(state)
30
- },
31
- },
32
- })
33
- }
34
-
35
- /**
36
- * Compute language decorations for the entire document
37
- */
38
- function computeDecorations(doc: PmNode): DecorationSet {
39
- const decorations: Decoration[] = []
40
-
41
- // Iterate through all block nodes and assign language class
42
- doc.forEach((node, offset) => {
43
- if (node.isBlock && node.type.name !== 'doc') {
44
- const lang = resolveBlockLanguage(node)
45
- const className = `cs-lang-${lang}`
46
-
47
- // Create a decoration (class marker) for this block
48
- // Pass class in both attrs (for DOM rendering) and spec (for testing/reference)
49
- const deco = Decoration.node(
50
- offset,
51
- offset + node.nodeSize,
52
- {
53
- class: className,
54
- },
55
- {
56
- class: className,
57
- }
58
- )
59
- decorations.push(deco)
60
- }
61
- })
62
-
63
- return DecorationSet.create(doc, decorations)
64
- }
65
-
66
- /**
67
- * Resolve the language for a single block node
68
- * Checks: node metadata → detected script → fallback to 'en'
69
- */
70
- function resolveBlockLanguage(node: PmNode): string {
71
- // Check for metadata lang attribute (e.g., lang: hi)
72
- if (node.attrs?.lang) {
73
- return node.attrs.lang as string
74
- }
75
-
76
- // Check for inline {lang=…} tag in the node's text
77
- if (node.textContent) {
78
- const inlineMatch = /\{lang=([a-z]{2})\}/.exec(node.textContent)
79
- if (inlineMatch?.[1]) {
80
- return inlineMatch[1]
81
- }
82
- }
83
-
84
- // Detect from text content
85
- if (node.textContent) {
86
- return detectScript(node.textContent)
87
- }
88
-
89
- // Fallback to English
90
- return 'en'
91
- }
@@ -1,26 +0,0 @@
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
-
13
- export function buildPlugins() {
14
- return [
15
- history(),
16
- buildKeymap(),
17
- keymap(baseKeymap),
18
- createLanguageClassPlugin(),
19
- createCharacterDecorationPlugin(),
20
- createImePluginWithRef(),
21
- createAutocompletePlugin(),
22
- createSpellcheckPlugin(),
23
- createAttrPencilPlugin(),
24
- createFindHighlightPlugin(),
25
- ]
26
- }