@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.
- package/README.md +87 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
- package/dist/ai/VoiceConfirmDialog.js +6 -6
- package/dist/ai/VoiceToolbar.d.ts +1 -0
- package/dist/ai/VoiceToolbar.js +22 -16
- package/dist/ai/clean-response.d.ts +7 -0
- package/dist/ai/clean-response.js +58 -0
- package/dist/ai/format-transcript.d.ts +3 -0
- package/dist/ai/format-transcript.js +7 -0
- package/dist/ai/prompts.d.ts +8 -1
- package/dist/ai/prompts.js +34 -7
- package/dist/autocomplete/suggestions.js +1 -1
- package/dist/context/DocumentContext.d.ts +28 -4
- package/dist/context/DocumentContext.js +67 -37
- package/dist/editor/EditorView.js +32 -4
- package/dist/editor/SelectionMenu.js +5 -3
- package/dist/editor/external-decorations-plugin.d.ts +23 -0
- package/dist/editor/external-decorations-plugin.js +151 -0
- package/dist/editor/omitted-scene-plugin.d.ts +9 -0
- package/dist/editor/omitted-scene-plugin.js +44 -0
- package/dist/editor/plugins.js +4 -0
- package/dist/editor/private-metadata-plugin.d.ts +10 -0
- package/dist/editor/private-metadata-plugin.js +60 -0
- package/dist/editor/prosemirror-to-sutra.js +179 -74
- package/dist/editor/scene-heading-view.js +2 -0
- package/dist/editor/schema.js +69 -12
- package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
- package/dist/editor/sutra-to-prosemirror.js +32 -34
- package/dist/extensions/ai-provider.d.ts +44 -0
- package/dist/extensions/ai-provider.js +27 -0
- package/dist/extensions/collab.d.ts +21 -0
- package/dist/extensions/collab.js +6 -0
- package/dist/extensions/command-registry.d.ts +49 -0
- package/dist/extensions/command-registry.js +28 -0
- package/dist/extensions/decorations.d.ts +48 -0
- package/dist/extensions/decorations.js +6 -0
- package/dist/extensions/export-registry.d.ts +59 -0
- package/dist/extensions/export-registry.js +10 -0
- package/dist/extensions/panel-registry.d.ts +30 -0
- package/dist/extensions/panel-registry.js +16 -0
- package/dist/extensions/registries.d.ts +15 -0
- package/dist/extensions/registries.js +12 -0
- package/dist/extensions/registry-store.d.ts +26 -0
- package/dist/extensions/registry-store.js +35 -0
- package/dist/extensions/session.d.ts +29 -0
- package/dist/extensions/session.js +20 -0
- package/dist/extensions/speech-provider.d.ts +35 -0
- package/dist/extensions/speech-provider.js +17 -0
- package/dist/file/builtin-exporters.d.ts +9 -0
- package/dist/file/builtin-exporters.js +72 -0
- package/dist/file/docx-exporter.js +7 -4
- package/dist/file/docx-importer.d.ts +9 -0
- package/dist/file/docx-importer.js +47 -19
- package/dist/file/scene-number.d.ts +5 -0
- package/dist/file/scene-number.js +8 -0
- package/dist/file/workflow-reports.js +13 -7
- package/dist/findreplace/FindReplace.js +5 -2
- package/dist/import/AIImportDialog.d.ts +11 -0
- package/dist/import/AIImportDialog.js +112 -0
- package/dist/import/ai-import.d.ts +51 -0
- package/dist/import/ai-import.js +200 -0
- package/dist/import/apply-import.d.ts +7 -0
- package/dist/import/apply-import.js +35 -0
- package/dist/import/extract.d.ts +17 -0
- package/dist/import/extract.js +18 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +7 -0
- package/dist/locales/bn.json +8 -0
- package/dist/locales/en.json +8 -0
- package/dist/locales/gu.json +8 -0
- package/dist/locales/hi.json +8 -0
- package/dist/locales/kn.json +8 -0
- package/dist/locales/ml.json +8 -0
- package/dist/locales/mr.json +8 -0
- package/dist/locales/or.json +8 -0
- package/dist/locales/pa.json +8 -0
- package/dist/locales/si.json +8 -0
- package/dist/locales/ta.json +8 -0
- package/dist/locales/te.json +8 -0
- package/dist/navigator/SceneNavigator.js +125 -131
- package/dist/navigator/StatisticsDialog.js +5 -1
- package/dist/navigator/scene-edits.d.ts +18 -0
- package/dist/navigator/scene-edits.js +143 -0
- package/dist/navigator/scene-list.js +4 -4
- package/dist/navigator/scene-numbering.d.ts +37 -0
- package/dist/navigator/scene-numbering.js +88 -0
- package/dist/settings/SettingsDialog.js +8 -120
- package/dist/shell/AppBar.js +21 -2
- package/dist/shell/AppShell.js +18 -4
- package/dist/shell/ElementToolbar.d.ts +5 -0
- package/dist/shell/ElementToolbar.js +35 -25
- package/dist/shell/LanguageStatus.js +6 -0
- package/dist/shell/StatusBar.js +11 -6
- package/dist/shell/builtin-commands.d.ts +10 -0
- package/dist/shell/builtin-commands.js +43 -0
- package/dist/shell/icons.d.ts +6 -0
- package/dist/shell/icons.js +6 -0
- package/dist/shell/use-commands.d.ts +7 -0
- package/dist/shell/use-commands.js +24 -0
- package/dist/shell/use-panel-context.d.ts +3 -0
- package/dist/shell/use-panel-context.js +39 -0
- package/dist/source/SourceView.js +18 -1
- package/dist/styles/screenplay.css +113 -1
- package/dist/titlepage/ExportDialog.d.ts +5 -0
- package/dist/titlepage/ExportDialog.js +59 -96
- package/package.json +32 -5
- package/dist/ai/OnboardingDialog.d.ts +0 -6
- package/dist/ai/OnboardingDialog.js +0 -115
- package/dist/ai/ai-client.d.ts +0 -71
- package/dist/ai/ai-client.js +0 -494
- package/dist/ai/generated-providers.json +0 -3696
- package/dist/ai/voice-service.d.ts +0 -15
- package/dist/ai/voice-service.js +0 -81
- package/dist/editor/character-view.d.ts +0 -35
- package/dist/editor/character-view.js +0 -112
- package/dist/editor/transliterate-input.d.ts +0 -10
- package/dist/editor/transliterate-input.js +0 -88
- package/dist/extensions/types.d.ts +0 -73
- package/dist/extensions/types.js +0 -12
- 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
|
@@ -2,10 +2,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import { parse, importFountain, fountainDocToSutra } from '@sutrata/parser';
|
|
4
4
|
import { StorageAdapterProvider } from '../extensions/storage-adapter';
|
|
5
|
+
import { AIProviderProvider } from '../extensions/ai-provider';
|
|
6
|
+
import { SpeechProviderProvider } from '../extensions/speech-provider';
|
|
7
|
+
import { SessionProvider, DEFAULT_SESSION } from '../extensions/session';
|
|
8
|
+
import { PanelRegistryProvider, createPanelRegistry } from '../extensions/panel-registry';
|
|
9
|
+
import { CommandRegistryProvider, createCommandRegistry, matchesShortcut } from '../extensions/command-registry';
|
|
10
|
+
import { ExportRegistryProvider, createExportRegistry } from '../extensions/export-registry';
|
|
11
|
+
import { withBuiltins } from '../extensions/registry-store';
|
|
12
|
+
import { DecorationProvidersProvider } from '../extensions/decorations';
|
|
13
|
+
import { CollabBindingProvider } from '../extensions/collab';
|
|
5
14
|
import { importDocx as importDocxToSutra } from '../file/docx-importer';
|
|
6
|
-
import {
|
|
15
|
+
import { getEditorView, getSourceView } from '../editor/editor-bus';
|
|
7
16
|
import { setFlatFrontmatterField } from '../editor/frontmatter-field';
|
|
8
|
-
import {
|
|
17
|
+
import { builtinCommands } from '../shell/builtin-commands';
|
|
9
18
|
import { resolveStyle } from '../styles/registry';
|
|
10
19
|
const DocumentContext = createContext(null);
|
|
11
20
|
const EMPTY_DOC = '## Scene 1\n\nAction.\n';
|
|
@@ -48,7 +57,15 @@ function clearRecoveryPointer() {
|
|
|
48
57
|
// ignore
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
|
-
|
|
60
|
+
const NO_DECORATIONS = [];
|
|
61
|
+
/** A registry prop as a registry: a fixed list becomes a registry of its own. */
|
|
62
|
+
function useRegistryProp(value, create) {
|
|
63
|
+
return useMemo(() => (Array.isArray(value) || !value ? create(value ?? []) : value), [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
64
|
+
}
|
|
65
|
+
export function DocumentProvider({ children, storageAdapter, aiProvider, speechProvider, session, panels, commands, exporters, decorationProviders, collabBinding, }) {
|
|
66
|
+
const panelRegistry = useRegistryProp(panels, createPanelRegistry);
|
|
67
|
+
const commandRegistry = useRegistryProp(commands, createCommandRegistry);
|
|
68
|
+
const exportRegistry = useRegistryProp(exporters, createExportRegistry);
|
|
52
69
|
const [text, setTextState] = useState(EMPTY_DOC);
|
|
53
70
|
const [ast, setAst] = useState(() => parse(EMPTY_DOC));
|
|
54
71
|
const [mode, setMode] = useState('formatted');
|
|
@@ -64,7 +81,6 @@ export function DocumentProvider({ children, storageAdapter }) {
|
|
|
64
81
|
const [navVisible, setNavVisible] = useState(() => window.innerWidth > 640);
|
|
65
82
|
const [exportVisible, setExportVisible] = useState(false);
|
|
66
83
|
const [settingsVisible, setSettingsVisible] = useState(false);
|
|
67
|
-
const [aiOnboardingVisible, setAIOnboardingVisible] = useState(false);
|
|
68
84
|
const [voiceActive, setVoiceActive] = useState(false);
|
|
69
85
|
const [metadataVisible, setMetadataVisible] = useState(true);
|
|
70
86
|
const [findReplaceVisible, setFindReplaceVisible] = useState(false);
|
|
@@ -162,6 +178,8 @@ export function DocumentProvider({ children, storageAdapter }) {
|
|
|
162
178
|
// on every keystroke.
|
|
163
179
|
const textRef = useRef(text);
|
|
164
180
|
useEffect(() => { textRef.current = text; }, [text]);
|
|
181
|
+
const modeRef = useRef(mode);
|
|
182
|
+
modeRef.current = mode;
|
|
165
183
|
const filePathRef = useRef(filePath);
|
|
166
184
|
useEffect(() => { filePathRef.current = filePath; }, [filePath]);
|
|
167
185
|
const autosaveFailedRef = useRef(false);
|
|
@@ -441,10 +459,36 @@ export function DocumentProvider({ children, storageAdapter }) {
|
|
|
441
459
|
window.addEventListener('beforeunload', onBeforeUnload);
|
|
442
460
|
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
|
443
461
|
}, [isDirty]);
|
|
462
|
+
const canEditRef = useRef(true);
|
|
463
|
+
canEditRef.current = (session ?? DEFAULT_SESSION).permission === 'edit';
|
|
444
464
|
useEffect(() => {
|
|
445
465
|
const onKeyDown = (e) => {
|
|
446
466
|
const mod = e.ctrlKey || e.metaKey;
|
|
447
467
|
const cs = mod && e.shiftKey;
|
|
468
|
+
// Read-only sessions: only find, mode and panel toggles.
|
|
469
|
+
if (!canEditRef.current) {
|
|
470
|
+
if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyF') {
|
|
471
|
+
e.preventDefault();
|
|
472
|
+
setFindMode('find');
|
|
473
|
+
setFindReplaceVisible(v => !v);
|
|
474
|
+
}
|
|
475
|
+
else if (cs && !e.altKey && e.code === 'KeyE') {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
setMode(m => m === 'formatted' ? 'source' : 'formatted');
|
|
478
|
+
}
|
|
479
|
+
else if (cs && !e.altKey && e.code === 'KeyB') {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
setNavVisible(v => !v);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const cmd = commandRegistry.list().find(c => c.readOnly && c.shortcut && matchesShortcut(c.shortcut, e));
|
|
485
|
+
if (cmd) {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
cmd.run({ view: getEditorView(), sourceView: getSourceView(), mode: modeRef.current, text: textRef.current, setText });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
448
492
|
// File operations — use e.code for layout-independence
|
|
449
493
|
if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyS') {
|
|
450
494
|
e.preventDefault();
|
|
@@ -468,27 +512,13 @@ export function DocumentProvider({ children, storageAdapter }) {
|
|
|
468
512
|
setFindReplaceVisible(true);
|
|
469
513
|
return;
|
|
470
514
|
}
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const blockMap = {
|
|
479
|
-
'KeyH': ['scene_heading', { id: null }],
|
|
480
|
-
'KeyA': ['action'],
|
|
481
|
-
'KeyC': ['character', { extension: null, isDual: false }],
|
|
482
|
-
'KeyD': ['dialogue'],
|
|
483
|
-
'KeyP': ['parenthetical'],
|
|
484
|
-
'KeyX': ['transition'],
|
|
485
|
-
};
|
|
486
|
-
if (e.code in blockMap) {
|
|
487
|
-
e.preventDefault();
|
|
488
|
-
const [type, attrs] = blockMap[e.code];
|
|
489
|
-
runCommand(makeSetBlock(type, attrs ?? {}));
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
515
|
+
// Command shortcuts (built-in element types, embedder commands)
|
|
516
|
+
const cmd = withBuiltins(builtinCommands(k => k), commandRegistry.list())
|
|
517
|
+
.find(c => c.shortcut && matchesShortcut(c.shortcut, e));
|
|
518
|
+
if (cmd) {
|
|
519
|
+
e.preventDefault();
|
|
520
|
+
cmd.run({ view: getEditorView(), sourceView: getSourceView(), mode: modeRef.current, text: textRef.current, setText });
|
|
521
|
+
return;
|
|
492
522
|
}
|
|
493
523
|
// UI toggles Ctrl+Shift+letter
|
|
494
524
|
if (cs && !e.altKey) {
|
|
@@ -511,7 +541,7 @@ export function DocumentProvider({ children, storageAdapter }) {
|
|
|
511
541
|
};
|
|
512
542
|
window.addEventListener('keydown', onKeyDown);
|
|
513
543
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
514
|
-
}, [saveFile, openFile]);
|
|
544
|
+
}, [saveFile, openFile, commandRegistry, setText]);
|
|
515
545
|
// Native menu IPC (Desktop wrapper only) — receives commands from the main process menu
|
|
516
546
|
useEffect(() => {
|
|
517
547
|
const api = window.desktopAPI;
|
|
@@ -585,17 +615,17 @@ export function DocumentProvider({ children, storageAdapter }) {
|
|
|
585
615
|
});
|
|
586
616
|
return off;
|
|
587
617
|
}, [saveFile, showConfirm]);
|
|
588
|
-
return (_jsx(StorageAdapterProvider, { value: storageAdapter, children: _jsx(DocumentContext.Provider, { value: {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
618
|
+
return (_jsx(StorageAdapterProvider, { value: storageAdapter, children: _jsx(SessionProvider, { value: session ?? DEFAULT_SESSION, children: _jsx(AIProviderProvider, { value: aiProvider ?? null, children: _jsx(SpeechProviderProvider, { value: speechProvider ?? null, children: _jsx(PanelRegistryProvider, { value: panelRegistry, children: _jsx(CommandRegistryProvider, { value: commandRegistry, children: _jsx(ExportRegistryProvider, { value: exportRegistry, children: _jsx(DecorationProvidersProvider, { value: decorationProviders ?? NO_DECORATIONS, children: _jsx(CollabBindingProvider, { value: collabBinding ?? null, children: _jsx(DocumentContext.Provider, { value: {
|
|
619
|
+
text, ast, mode, filePath, isDirty, lastSaveTarget, ribbonVisible, navVisible, exportVisible, settingsVisible,
|
|
620
|
+
voiceActive, metadataVisible,
|
|
621
|
+
findReplaceVisible, findMode, styleVisible, customStyles, resolvedStyle, watermarkText,
|
|
622
|
+
confirmModal, toasts, versions, versionHistoryVisible,
|
|
623
|
+
setText, setMode, setFilePath, setRibbonVisible, setNavVisible, setExportVisible, setSettingsVisible,
|
|
624
|
+
setVoiceActive, setMetadataVisible,
|
|
625
|
+
setFindReplaceVisible, setFindMode, setStyleVisible, setVersionHistoryVisible, setDocumentStyle, reloadCustomStyles,
|
|
626
|
+
markClean, save, newDocument, openFile, saveFile, saveFileAs, importDocx, restoreVersion, clearVersionHistory,
|
|
627
|
+
showConfirm, closeConfirm, showToast, dismissToast,
|
|
628
|
+
}, children: children }) }) }) }) }) }) }) }) }) }));
|
|
599
629
|
}
|
|
600
630
|
export function useDocument() {
|
|
601
631
|
const ctx = useContext(DocumentContext);
|
|
@@ -12,8 +12,28 @@ import { useDocument } from '../context/DocumentContext';
|
|
|
12
12
|
import { SceneHeadingView } from './scene-heading-view';
|
|
13
13
|
import { FrontmatterFieldView } from './frontmatter-field-view';
|
|
14
14
|
import { applyStyleVars } from '../styles/css-adapter';
|
|
15
|
+
import { useCanEdit } from '../extensions/session';
|
|
16
|
+
import { useDecorationProviders } from '../extensions/decorations';
|
|
17
|
+
import { useCollabBinding } from '../extensions/collab';
|
|
18
|
+
import { createExternalDecorationsPlugin } from './external-decorations-plugin';
|
|
15
19
|
export function EditorView() {
|
|
16
|
-
const { text, setText, resolvedStyle } = useDocument();
|
|
20
|
+
const { text, ast, setText, resolvedStyle } = useDocument();
|
|
21
|
+
const astRef = useRef(ast);
|
|
22
|
+
astRef.current = ast;
|
|
23
|
+
// Embedder decorations and collab plugins join the built-in ones every time
|
|
24
|
+
// the state is (re)created. Both are read once per state: pass stable values.
|
|
25
|
+
const decorationProviders = useDecorationProviders();
|
|
26
|
+
const collab = useCollabBinding();
|
|
27
|
+
const plugins = () => [
|
|
28
|
+
...buildPlugins(),
|
|
29
|
+
...(decorationProviders.length > 0 ? [createExternalDecorationsPlugin(decorationProviders, () => astRef.current)] : []),
|
|
30
|
+
...(collab ? collab.plugins(schema) : []),
|
|
31
|
+
];
|
|
32
|
+
// Read-only sessions: not editable, and every doc-changing transaction —
|
|
33
|
+
// typed, or dispatched by a toolbar/navigator/node view — is dropped here.
|
|
34
|
+
const canEdit = useCanEdit();
|
|
35
|
+
const canEditRef = useRef(canEdit);
|
|
36
|
+
canEditRef.current = canEdit;
|
|
17
37
|
const mountRef = useRef(null);
|
|
18
38
|
const viewRef = useRef(null);
|
|
19
39
|
const textRef = useRef(text);
|
|
@@ -26,14 +46,17 @@ export function EditorView() {
|
|
|
26
46
|
if (!mountRef.current)
|
|
27
47
|
return;
|
|
28
48
|
const doc = sutraToProsemirror(parse(textRef.current));
|
|
29
|
-
const state = EditorState.create({ doc, schema, plugins:
|
|
49
|
+
const state = EditorState.create({ doc, schema, plugins: plugins() });
|
|
30
50
|
const view = new PmEditorView(mountRef.current, {
|
|
31
51
|
state,
|
|
32
52
|
nodeViews: {
|
|
33
53
|
scene_heading: (node, view, getPos) => new SceneHeadingView(node, view, getPos),
|
|
34
54
|
frontmatter_field: (node, view, getPos) => new FrontmatterFieldView(node, view, getPos),
|
|
35
55
|
},
|
|
56
|
+
editable: () => canEditRef.current,
|
|
36
57
|
dispatchTransaction(tr) {
|
|
58
|
+
if (tr.docChanged && !canEditRef.current)
|
|
59
|
+
return;
|
|
37
60
|
const newState = view.state.apply(tr);
|
|
38
61
|
view.updateState(newState);
|
|
39
62
|
if (tr.docChanged) {
|
|
@@ -48,7 +71,8 @@ export function EditorView() {
|
|
|
48
71
|
});
|
|
49
72
|
viewRef.current = view;
|
|
50
73
|
setEditorView(view);
|
|
51
|
-
|
|
74
|
+
const detachCollab = collab?.attach(view);
|
|
75
|
+
return () => { detachCollab?.(); setEditorView(null); view.destroy(); viewRef.current = null; };
|
|
52
76
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
53
77
|
// Sync external text changes into ProseMirror (e.g. switching from source mode).
|
|
54
78
|
// Skip when the editor itself produced the change — it already has the right state.
|
|
@@ -63,10 +87,14 @@ export function EditorView() {
|
|
|
63
87
|
const currentText = prosemirrorToSutra(view.state.doc);
|
|
64
88
|
if (currentText !== text) {
|
|
65
89
|
const doc = sutraToProsemirror(parse(text));
|
|
66
|
-
const state = EditorState.create({ doc, schema, plugins:
|
|
90
|
+
const state = EditorState.create({ doc, schema, plugins: plugins() });
|
|
67
91
|
view.updateState(state);
|
|
68
92
|
}
|
|
69
93
|
}, [text]);
|
|
94
|
+
// Re-evaluate `editable` when the session's permission changes.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
viewRef.current?.setProps({});
|
|
97
|
+
}, [canEdit]);
|
|
70
98
|
// Apply the document's resolved style as CSS custom properties on the mount element.
|
|
71
99
|
// Works identically for built-in and user-imported styles — both are plain
|
|
72
100
|
// ScreenplayStyleDefinition objects by the time they reach here.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { useLanguage } from '../i18n/LanguageContext';
|
|
4
4
|
import { useDocument } from '../context/DocumentContext';
|
|
5
5
|
import { insertSutra } from './insert-helper';
|
|
6
6
|
import { VoiceConfirmDialog } from '../ai/VoiceConfirmDialog';
|
|
7
7
|
import { SparklesIcon } from '../shell/icons';
|
|
8
|
+
import { useAI } from '../extensions/ai-provider';
|
|
8
9
|
const VOICE_LANG_MAP = {
|
|
9
10
|
en: 'en-US',
|
|
10
11
|
hi: 'hi-IN',
|
|
@@ -34,6 +35,7 @@ function isSelectionInEditor(sel) {
|
|
|
34
35
|
export function SelectionMenu() {
|
|
35
36
|
const { currentLanguage } = useLanguage();
|
|
36
37
|
const { mode } = useDocument();
|
|
38
|
+
const ai = useAI();
|
|
37
39
|
const [menuPosition, setMenuPosition] = useState(null);
|
|
38
40
|
const [selectedText, setSelectedText] = useState('');
|
|
39
41
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
@@ -84,7 +86,7 @@ export function SelectionMenu() {
|
|
|
84
86
|
setShowConfirm(true);
|
|
85
87
|
setMenuPosition(null);
|
|
86
88
|
};
|
|
87
|
-
if (showConfirm) {
|
|
89
|
+
if (showConfirm && ai) {
|
|
88
90
|
const langCode = VOICE_LANG_MAP[currentLanguage] || 'en-US';
|
|
89
91
|
return (_jsx(VoiceConfirmDialog, { rawTranscript: selectedText, langCode: langCode, onConfirm: (formattedText) => {
|
|
90
92
|
insertSutra(formattedText, mode);
|
|
@@ -98,5 +100,5 @@ export function SelectionMenu() {
|
|
|
98
100
|
return (_jsxs("div", { className: "cs-selection-menu", style: {
|
|
99
101
|
left: `${menuPosition.x}px`,
|
|
100
102
|
top: `${menuPosition.y}px`,
|
|
101
|
-
}, role: "toolbar", "aria-label": "Text Selection Actions", children: [_jsxs("button", { type: "button", className: "cs-selection-btn cs-selection-btn-format", onClick: handleFormat, title: "Format selected text using AI", children: [_jsx(SparklesIcon, { size: 12 }), _jsx("span", { children: "Format" })] }), _jsx("div", { className: "cs-selection-sep" }), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCut, children: "Cut" }), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCopy, children: "Copy" })] }));
|
|
103
|
+
}, role: "toolbar", "aria-label": "Text Selection Actions", children: [ai && (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "cs-selection-btn cs-selection-btn-format", onClick: handleFormat, title: "Format selected text using AI", children: [_jsx(SparklesIcon, { size: 12 }), _jsx("span", { children: "Format" })] }), _jsx("div", { className: "cs-selection-sep" })] })), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCut, children: "Cut" }), _jsx("button", { type: "button", className: "cs-selection-btn", onClick: handleCopy, children: "Copy" })] }));
|
|
102
104
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
import { DecorationSet } from 'prosemirror-view';
|
|
3
|
+
import type { Node as PmNode } from 'prosemirror-model';
|
|
4
|
+
import type { DocumentNode } from '@sutrata/parser';
|
|
5
|
+
import type { DecorationProvider } from '../extensions/decorations';
|
|
6
|
+
/**
|
|
7
|
+
* Renders DecorationProvider output (extensions/decorations.ts) in the
|
|
8
|
+
* formatted editor. Anchors are { sceneId, from?, to? } with offsets into the
|
|
9
|
+
* scene's text; this plugin maps them to document positions, recomputes on
|
|
10
|
+
* every document change and whenever a provider signals a change, and routes
|
|
11
|
+
* clicks on decorated elements to the spec's onClick (in any session mode).
|
|
12
|
+
*/
|
|
13
|
+
export declare const externalDecorationsKey: PluginKey<DecorationSet>;
|
|
14
|
+
interface SceneRange {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
}
|
|
18
|
+
/** Top-level position range of every scene with an id, heading through the block before the next heading. */
|
|
19
|
+
export declare function sceneRanges(doc: PmNode): Map<string, SceneRange>;
|
|
20
|
+
/** Scene text as anchors see it: blocks joined with '\n', line breaks as '\n'. */
|
|
21
|
+
export declare function textOf(doc: PmNode, range: SceneRange, to?: number): string;
|
|
22
|
+
export declare function createExternalDecorationsPlugin(providers: readonly DecorationProvider[], getAst: () => DocumentNode): Plugin<DecorationSet>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
3
|
+
/**
|
|
4
|
+
* Renders DecorationProvider output (extensions/decorations.ts) in the
|
|
5
|
+
* formatted editor. Anchors are { sceneId, from?, to? } with offsets into the
|
|
6
|
+
* scene's text; this plugin maps them to document positions, recomputes on
|
|
7
|
+
* every document change and whenever a provider signals a change, and routes
|
|
8
|
+
* clicks on decorated elements to the spec's onClick (in any session mode).
|
|
9
|
+
*/
|
|
10
|
+
export const externalDecorationsKey = new PluginKey('externalDecorations');
|
|
11
|
+
const REFRESH = 'refresh';
|
|
12
|
+
const CLICK_ATTR = 'data-cs-deco';
|
|
13
|
+
/** Top-level position range of every scene with an id, heading through the block before the next heading. */
|
|
14
|
+
export function sceneRanges(doc) {
|
|
15
|
+
const ranges = new Map();
|
|
16
|
+
let open = null;
|
|
17
|
+
doc.forEach((node, offset) => {
|
|
18
|
+
if (node.type.name !== 'scene_heading')
|
|
19
|
+
return;
|
|
20
|
+
if (open?.id && !ranges.has(open.id))
|
|
21
|
+
ranges.set(open.id, { from: open.from, to: offset });
|
|
22
|
+
open = { id: node.attrs['id'] ?? null, from: offset };
|
|
23
|
+
});
|
|
24
|
+
const last = open;
|
|
25
|
+
if (last?.id && !ranges.has(last.id))
|
|
26
|
+
ranges.set(last.id, { from: last.from, to: doc.content.size });
|
|
27
|
+
return ranges;
|
|
28
|
+
}
|
|
29
|
+
/** Scene text as anchors see it: blocks joined with '\n', line breaks as '\n'. */
|
|
30
|
+
export function textOf(doc, range, to = range.to) {
|
|
31
|
+
return doc.textBetween(range.from, to, '\n', '\n');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Document position for a scene-text offset. `start` bias picks the first
|
|
35
|
+
* position at which `offset` characters precede (so an offset at a block
|
|
36
|
+
* boundary lands at the start of the next block); `end` bias the last one.
|
|
37
|
+
*/
|
|
38
|
+
function posAt(doc, range, offset, bias) {
|
|
39
|
+
// Smallest position p with at least `target` characters of scene text before it.
|
|
40
|
+
const smallest = (target) => {
|
|
41
|
+
let lo = range.from;
|
|
42
|
+
let hi = range.to;
|
|
43
|
+
while (lo < hi) {
|
|
44
|
+
const mid = (lo + hi) >> 1;
|
|
45
|
+
if (textOf(doc, range, mid).length >= target)
|
|
46
|
+
hi = mid;
|
|
47
|
+
else
|
|
48
|
+
lo = mid + 1;
|
|
49
|
+
}
|
|
50
|
+
return lo;
|
|
51
|
+
};
|
|
52
|
+
if (bias === 'end' || offset >= textOf(doc, range).length)
|
|
53
|
+
return smallest(offset);
|
|
54
|
+
// Just before character `offset`: one position before the point where it has been passed.
|
|
55
|
+
return Math.max(range.from, smallest(offset + 1) - 1);
|
|
56
|
+
}
|
|
57
|
+
/** The top-level block containing `pos`, as a node-decoration range. */
|
|
58
|
+
function blockAt(doc, pos) {
|
|
59
|
+
let found = null;
|
|
60
|
+
doc.forEach((node, offset) => {
|
|
61
|
+
if (!found && pos >= offset && pos <= offset + node.nodeSize)
|
|
62
|
+
found = { from: offset, to: offset + node.nodeSize };
|
|
63
|
+
});
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
function toDecorations(doc, specs, ranges, onClick, keyPrefix) {
|
|
67
|
+
const out = [];
|
|
68
|
+
specs.forEach((spec, i) => {
|
|
69
|
+
const range = ranges.get(spec.anchor.sceneId);
|
|
70
|
+
if (!range)
|
|
71
|
+
return; // unknown id, or a scene without {#id}
|
|
72
|
+
const clamp = (n) => Math.max(0, Math.min(n, textOf(doc, range).length));
|
|
73
|
+
const key = `${keyPrefix}:${i}`;
|
|
74
|
+
if (spec.kind === 'widget') {
|
|
75
|
+
const pos = spec.anchor.from === undefined ? range.from : posAt(doc, range, clamp(spec.anchor.from), 'start');
|
|
76
|
+
out.push(Decoration.widget(pos, () => spec.render(), { side: spec.side ?? -1, key }));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const attrs = { ...(spec.attrs ?? {}) };
|
|
80
|
+
if (spec.className)
|
|
81
|
+
attrs['class'] = spec.className;
|
|
82
|
+
if (spec.onClick) {
|
|
83
|
+
attrs[CLICK_ATTR] = key;
|
|
84
|
+
onClick.set(key, spec.onClick);
|
|
85
|
+
}
|
|
86
|
+
if (spec.kind === 'node') {
|
|
87
|
+
const block = spec.anchor.from === undefined
|
|
88
|
+
? { from: range.from, to: range.from + doc.nodeAt(range.from).nodeSize }
|
|
89
|
+
: blockAt(doc, posAt(doc, range, clamp(spec.anchor.from), 'start'));
|
|
90
|
+
if (block)
|
|
91
|
+
out.push(Decoration.node(block.from, block.to, attrs));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const from = spec.anchor.from === undefined ? range.from : posAt(doc, range, clamp(spec.anchor.from), 'start');
|
|
95
|
+
const to = spec.anchor.to === undefined ? range.to : posAt(doc, range, clamp(spec.anchor.to), 'end');
|
|
96
|
+
if (to > from)
|
|
97
|
+
out.push(Decoration.inline(from, to, attrs));
|
|
98
|
+
});
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
export function createExternalDecorationsPlugin(providers, getAst) {
|
|
102
|
+
const onClick = new Map();
|
|
103
|
+
const compute = (state) => {
|
|
104
|
+
onClick.clear();
|
|
105
|
+
const ranges = sceneRanges(state.doc);
|
|
106
|
+
const ctx = {
|
|
107
|
+
ast: getAst(),
|
|
108
|
+
sceneText: id => {
|
|
109
|
+
const range = ranges.get(id);
|
|
110
|
+
return range ? textOf(state.doc, range) : null;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const decorations = providers.flatMap(p => {
|
|
114
|
+
try {
|
|
115
|
+
return toDecorations(state.doc, p.getDecorations(ctx), ranges, onClick, p.id);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(`DecorationProvider ${p.id} failed:`, e);
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return DecorationSet.create(state.doc, decorations);
|
|
123
|
+
};
|
|
124
|
+
return new Plugin({
|
|
125
|
+
key: externalDecorationsKey,
|
|
126
|
+
state: {
|
|
127
|
+
init: (_, state) => compute(state),
|
|
128
|
+
apply: (tr, set, _old, state) => (tr.docChanged || tr.getMeta(externalDecorationsKey) === REFRESH ? compute(state) : set),
|
|
129
|
+
},
|
|
130
|
+
view(view) {
|
|
131
|
+
const offs = providers.map(p => p.subscribe(() => {
|
|
132
|
+
if (!view.isDestroyed)
|
|
133
|
+
view.dispatch(view.state.tr.setMeta(externalDecorationsKey, REFRESH));
|
|
134
|
+
}));
|
|
135
|
+
return { destroy: () => offs.forEach(off => off()) };
|
|
136
|
+
},
|
|
137
|
+
props: {
|
|
138
|
+
decorations: state => externalDecorationsKey.getState(state),
|
|
139
|
+
handleDOMEvents: {
|
|
140
|
+
click(_view, event) {
|
|
141
|
+
const el = event.target?.closest?.(`[${CLICK_ATTR}]`);
|
|
142
|
+
const handler = el && onClick.get(el.getAttribute(CLICK_ATTR) ?? '');
|
|
143
|
+
if (!handler)
|
|
144
|
+
return false;
|
|
145
|
+
handler(event);
|
|
146
|
+
return true;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
import { DecorationSet } from 'prosemirror-view';
|
|
3
|
+
/**
|
|
4
|
+
* Omitted scenes (format spec §7.4: `& status: omitted`): the heading is
|
|
5
|
+
* struck through and followed by an OMITTED badge. The heading, number and
|
|
6
|
+
* any body kept in a comment stay editable.
|
|
7
|
+
*/
|
|
8
|
+
export declare const omittedSceneKey: PluginKey<DecorationSet>;
|
|
9
|
+
export declare function createOmittedScenePlugin(): Plugin<DecorationSet>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
3
|
+
/**
|
|
4
|
+
* Omitted scenes (format spec §7.4: `& status: omitted`): the heading is
|
|
5
|
+
* struck through and followed by an OMITTED badge. The heading, number and
|
|
6
|
+
* any body kept in a comment stay editable.
|
|
7
|
+
*/
|
|
8
|
+
export const omittedSceneKey = new PluginKey('omittedScene');
|
|
9
|
+
function decorate(doc) {
|
|
10
|
+
const decorations = [];
|
|
11
|
+
let heading = null;
|
|
12
|
+
doc.forEach((node, pos) => {
|
|
13
|
+
if (node.type.name === 'scene_heading') {
|
|
14
|
+
heading = { pos, node };
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (node.type.name !== 'scene_metadata') {
|
|
18
|
+
heading = null;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const h = heading;
|
|
22
|
+
if (h && node.attrs['metaKey'] === 'status' && node.textContent.trim().toLowerCase() === 'omitted') {
|
|
23
|
+
decorations.push(Decoration.node(h.pos, h.pos + h.node.nodeSize, { class: 'cs-scene-omitted' }));
|
|
24
|
+
decorations.push(Decoration.widget(h.pos + h.node.nodeSize - 1, () => {
|
|
25
|
+
const badge = document.createElement('span');
|
|
26
|
+
badge.className = 'cs-omitted-badge';
|
|
27
|
+
badge.contentEditable = 'false';
|
|
28
|
+
badge.textContent = 'OMITTED';
|
|
29
|
+
return badge;
|
|
30
|
+
}, { side: 1, key: `omitted-${h.pos}` }));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return DecorationSet.create(doc, decorations);
|
|
34
|
+
}
|
|
35
|
+
export function createOmittedScenePlugin() {
|
|
36
|
+
return new Plugin({
|
|
37
|
+
key: omittedSceneKey,
|
|
38
|
+
state: {
|
|
39
|
+
init: (_, state) => decorate(state.doc),
|
|
40
|
+
apply: (tr, set) => (tr.docChanged ? decorate(tr.doc) : set),
|
|
41
|
+
},
|
|
42
|
+
props: { decorations: state => omittedSceneKey.getState(state) },
|
|
43
|
+
});
|
|
44
|
+
}
|
package/dist/editor/plugins.js
CHANGED
|
@@ -9,9 +9,12 @@ import { createAutocompletePlugin } from '../autocomplete/AutocompletePlugin';
|
|
|
9
9
|
import { createSpellcheckPlugin } from '../spellcheck/spellcheck-plugin';
|
|
10
10
|
import { createAttrPencilPlugin } from './attr-pencil-plugin';
|
|
11
11
|
import { createFindHighlightPlugin } from './find-highlight-plugin';
|
|
12
|
+
import { createOmittedScenePlugin } from './omitted-scene-plugin';
|
|
13
|
+
import { createPrivateMetadataPlugin } from './private-metadata-plugin';
|
|
12
14
|
export function buildPlugins() {
|
|
13
15
|
return [
|
|
14
16
|
history(),
|
|
17
|
+
createPrivateMetadataPlugin(), // before the keymaps, so Backspace/Delete can't merge hidden x- blocks
|
|
15
18
|
buildKeymap(),
|
|
16
19
|
keymap(baseKeymap),
|
|
17
20
|
createLanguageClassPlugin(),
|
|
@@ -21,5 +24,6 @@ export function buildPlugins() {
|
|
|
21
24
|
createSpellcheckPlugin(),
|
|
22
25
|
createAttrPencilPlugin(),
|
|
23
26
|
createFindHighlightPlugin(),
|
|
27
|
+
createOmittedScenePlugin(),
|
|
24
28
|
];
|
|
25
29
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import type { Node as PmNode } from 'prosemirror-model';
|
|
3
|
+
/**
|
|
4
|
+
* Tool-private `x-` scene metadata (format spec §7.1) stays in the document
|
|
5
|
+
* but is hidden (`data-private`, see schema.ts). This plugin keeps the caret
|
|
6
|
+
* out of those hidden blocks and stops Backspace/Delete from merging text
|
|
7
|
+
* into or out of them; a range deletion that spans one still removes it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isPrivateBlock(node: PmNode | null | undefined): boolean;
|
|
10
|
+
export declare function createPrivateMetadataPlugin(): Plugin;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Plugin, TextSelection } from 'prosemirror-state';
|
|
2
|
+
/**
|
|
3
|
+
* Tool-private `x-` scene metadata (format spec §7.1) stays in the document
|
|
4
|
+
* but is hidden (`data-private`, see schema.ts). This plugin keeps the caret
|
|
5
|
+
* out of those hidden blocks and stops Backspace/Delete from merging text
|
|
6
|
+
* into or out of them; a range deletion that spans one still removes it.
|
|
7
|
+
*/
|
|
8
|
+
export function isPrivateBlock(node) {
|
|
9
|
+
return !!node && node.type.name === 'scene_metadata' && node.attrs['metaKey'].startsWith('x-');
|
|
10
|
+
}
|
|
11
|
+
/** Position just inside the nearest visible top-level block from `index` in `dir`, or null. */
|
|
12
|
+
function visibleBlockPos(doc, index, dir) {
|
|
13
|
+
for (let i = index; i >= 0 && i < doc.childCount; i += dir) {
|
|
14
|
+
if (isPrivateBlock(doc.child(i)))
|
|
15
|
+
continue;
|
|
16
|
+
let pos = 0;
|
|
17
|
+
for (let j = 0; j < i; j++)
|
|
18
|
+
pos += doc.child(j).nodeSize;
|
|
19
|
+
return dir === 1 ? pos + 1 : pos + doc.child(i).nodeSize - 1;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function moveOut(state, index, dir) {
|
|
24
|
+
const pos = visibleBlockPos(state.doc, index, dir) ?? visibleBlockPos(state.doc, index, dir === 1 ? -1 : 1);
|
|
25
|
+
return pos === null ? null : state.tr.setSelection(TextSelection.near(state.doc.resolve(pos), dir));
|
|
26
|
+
}
|
|
27
|
+
export function createPrivateMetadataPlugin() {
|
|
28
|
+
return new Plugin({
|
|
29
|
+
appendTransaction(trs, oldState, state) {
|
|
30
|
+
const { $head, empty } = state.selection;
|
|
31
|
+
if (!empty || $head.depth < 1 || !isPrivateBlock($head.node(1)))
|
|
32
|
+
return null;
|
|
33
|
+
if (!trs.some(tr => tr.selectionSet || tr.docChanged))
|
|
34
|
+
return null;
|
|
35
|
+
const dir = $head.pos < oldState.selection.head ? -1 : 1;
|
|
36
|
+
return moveOut(state, $head.index(0), dir);
|
|
37
|
+
},
|
|
38
|
+
props: {
|
|
39
|
+
handleKeyDown(view, event) {
|
|
40
|
+
if (event.key !== 'Backspace' && event.key !== 'Delete')
|
|
41
|
+
return false;
|
|
42
|
+
const { $head, empty } = view.state.selection;
|
|
43
|
+
if (!empty || $head.depth !== 1)
|
|
44
|
+
return false;
|
|
45
|
+
const back = event.key === 'Backspace';
|
|
46
|
+
if (back ? $head.parentOffset !== 0 : $head.parentOffset !== $head.parent.content.size)
|
|
47
|
+
return false;
|
|
48
|
+
const index = $head.index(0);
|
|
49
|
+
const neighbour = back ? index - 1 : index + 1;
|
|
50
|
+
if (neighbour < 0 || neighbour >= view.state.doc.childCount || !isPrivateBlock(view.state.doc.child(neighbour)))
|
|
51
|
+
return false;
|
|
52
|
+
// Step over the hidden block(s) instead of merging with them.
|
|
53
|
+
const tr = moveOut(view.state, neighbour, back ? -1 : 1);
|
|
54
|
+
if (tr)
|
|
55
|
+
view.dispatch(tr);
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|