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