@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
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import { EditorState } from 'prosemirror-state'
|
|
3
|
-
import { EditorView as PmEditorView } from 'prosemirror-view'
|
|
4
|
-
import { parse } from '@sutrata/parser'
|
|
5
|
-
import { schema } from './schema'
|
|
6
|
-
import { buildPlugins } from './plugins'
|
|
7
|
-
import { sutraToProsemirror } from './sutra-to-prosemirror'
|
|
8
|
-
import { prosemirrorToSutra } from './prosemirror-to-sutra'
|
|
9
|
-
import { setEditorView, emit } from './editor-bus'
|
|
10
|
-
import { useDocument } from '../context/DocumentContext'
|
|
11
|
-
import { SceneHeadingView } from './scene-heading-view'
|
|
12
|
-
import { FrontmatterFieldView } from './frontmatter-field-view'
|
|
13
|
-
import { applyStyleVars } from '../styles/css-adapter'
|
|
14
|
-
|
|
15
|
-
export function EditorView() {
|
|
16
|
-
const { text, setText, resolvedStyle } = useDocument()
|
|
17
|
-
const mountRef = useRef<HTMLDivElement>(null)
|
|
18
|
-
const viewRef = useRef<PmEditorView | null>(null)
|
|
19
|
-
const textRef = useRef(text)
|
|
20
|
-
textRef.current = text
|
|
21
|
-
// When true, the next text change originated from the editor itself —
|
|
22
|
-
// the sync effect must skip it to avoid clobbering in-flight transactions.
|
|
23
|
-
const selfChangeRef = useRef(false)
|
|
24
|
-
|
|
25
|
-
// Mount once: create ProseMirror view
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (!mountRef.current) return
|
|
28
|
-
|
|
29
|
-
const doc = sutraToProsemirror(parse(textRef.current))
|
|
30
|
-
const state = EditorState.create({ doc, schema, plugins: buildPlugins() })
|
|
31
|
-
|
|
32
|
-
const view = new PmEditorView(mountRef.current, {
|
|
33
|
-
state,
|
|
34
|
-
nodeViews: {
|
|
35
|
-
scene_heading: (node, view, getPos) => new SceneHeadingView(node, view, getPos),
|
|
36
|
-
frontmatter_field: (node, view, getPos) => new FrontmatterFieldView(node, view, getPos),
|
|
37
|
-
},
|
|
38
|
-
dispatchTransaction(tr) {
|
|
39
|
-
const newState = view.state.apply(tr)
|
|
40
|
-
view.updateState(newState)
|
|
41
|
-
if (tr.docChanged) {
|
|
42
|
-
const newText = prosemirrorToSutra(newState.doc)
|
|
43
|
-
if (newText !== textRef.current) {
|
|
44
|
-
selfChangeRef.current = true
|
|
45
|
-
setText(newText)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
emit()
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
viewRef.current = view
|
|
52
|
-
setEditorView(view)
|
|
53
|
-
|
|
54
|
-
return () => { setEditorView(null); view.destroy(); viewRef.current = null }
|
|
55
|
-
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
56
|
-
|
|
57
|
-
// Sync external text changes into ProseMirror (e.g. switching from source mode).
|
|
58
|
-
// Skip when the editor itself produced the change — it already has the right state.
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (selfChangeRef.current) {
|
|
61
|
-
selfChangeRef.current = false
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
const view = viewRef.current
|
|
65
|
-
if (!view) return
|
|
66
|
-
const currentText = prosemirrorToSutra(view.state.doc)
|
|
67
|
-
if (currentText !== text) {
|
|
68
|
-
const doc = sutraToProsemirror(parse(text))
|
|
69
|
-
const state = EditorState.create({ doc, schema, plugins: buildPlugins() })
|
|
70
|
-
view.updateState(state)
|
|
71
|
-
}
|
|
72
|
-
}, [text])
|
|
73
|
-
|
|
74
|
-
// Apply the document's resolved style as CSS custom properties on the mount element.
|
|
75
|
-
// Works identically for built-in and user-imported styles — both are plain
|
|
76
|
-
// ScreenplayStyleDefinition objects by the time they reach here.
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (mountRef.current) applyStyleVars(mountRef.current, resolvedStyle)
|
|
79
|
-
}, [resolvedStyle])
|
|
80
|
-
|
|
81
|
-
return <div ref={mountRef} className="cs-editor-mount" />
|
|
82
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
2
|
-
import { useLanguage } from '../i18n/LanguageContext'
|
|
3
|
-
import { useDocument } from '../context/DocumentContext'
|
|
4
|
-
import { insertSutra } from './insert-helper'
|
|
5
|
-
import { VoiceConfirmDialog } from '../ai/VoiceConfirmDialog'
|
|
6
|
-
import { SparklesIcon } from '../shell/icons'
|
|
7
|
-
|
|
8
|
-
const VOICE_LANG_MAP: Record<string, string> = {
|
|
9
|
-
en: 'en-US',
|
|
10
|
-
hi: 'hi-IN',
|
|
11
|
-
ta: 'ta-IN',
|
|
12
|
-
te: 'te-IN',
|
|
13
|
-
ml: 'ml-IN',
|
|
14
|
-
kn: 'kn-IN',
|
|
15
|
-
bn: 'bn-IN',
|
|
16
|
-
gu: 'gu-IN',
|
|
17
|
-
mr: 'mr-IN',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function isSelectionInEditor(sel: Selection): boolean {
|
|
21
|
-
if (sel.rangeCount === 0) return false
|
|
22
|
-
const range = sel.getRangeAt(0)
|
|
23
|
-
let node: Node | null = range.commonAncestorContainer
|
|
24
|
-
while (node) {
|
|
25
|
-
if (node instanceof HTMLElement) {
|
|
26
|
-
if (node.classList.contains('ProseMirror') || node.classList.contains('cm-content')) {
|
|
27
|
-
return true
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
node = node.parentNode
|
|
31
|
-
}
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function SelectionMenu() {
|
|
36
|
-
const { currentLanguage } = useLanguage()
|
|
37
|
-
const { mode } = useDocument()
|
|
38
|
-
const [menuPosition, setMenuPosition] = useState<{ x: number; y: number } | null>(null)
|
|
39
|
-
const [selectedText, setSelectedText] = useState<string>('')
|
|
40
|
-
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
const handleSelectionChange = () => {
|
|
44
|
-
if (showConfirm) return
|
|
45
|
-
|
|
46
|
-
const sel = window.getSelection()
|
|
47
|
-
if (!sel || sel.isCollapsed || !isSelectionInEditor(sel)) {
|
|
48
|
-
setMenuPosition(null)
|
|
49
|
-
setSelectedText('')
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const text = sel.toString().trim()
|
|
54
|
-
if (!text) {
|
|
55
|
-
setMenuPosition(null)
|
|
56
|
-
setSelectedText('')
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const range = sel.getRangeAt(0)
|
|
61
|
-
const rect = range.getBoundingClientRect()
|
|
62
|
-
|
|
63
|
-
setMenuPosition({
|
|
64
|
-
x: rect.left + rect.width / 2,
|
|
65
|
-
y: rect.top - 8,
|
|
66
|
-
})
|
|
67
|
-
setSelectedText(sel.toString())
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
document.addEventListener('selectionchange', handleSelectionChange)
|
|
71
|
-
return () => {
|
|
72
|
-
document.removeEventListener('selectionchange', handleSelectionChange)
|
|
73
|
-
}
|
|
74
|
-
}, [showConfirm])
|
|
75
|
-
|
|
76
|
-
const handleCut = (e: React.MouseEvent) => {
|
|
77
|
-
e.preventDefault()
|
|
78
|
-
e.stopPropagation()
|
|
79
|
-
document.execCommand('cut')
|
|
80
|
-
setMenuPosition(null)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const handleCopy = (e: React.MouseEvent) => {
|
|
84
|
-
e.preventDefault()
|
|
85
|
-
e.stopPropagation()
|
|
86
|
-
document.execCommand('copy')
|
|
87
|
-
setMenuPosition(null)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const handleFormat = (e: React.MouseEvent) => {
|
|
91
|
-
e.preventDefault()
|
|
92
|
-
e.stopPropagation()
|
|
93
|
-
setShowConfirm(true)
|
|
94
|
-
setMenuPosition(null)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (showConfirm) {
|
|
98
|
-
const langCode = VOICE_LANG_MAP[currentLanguage] || 'en-US'
|
|
99
|
-
return (
|
|
100
|
-
<VoiceConfirmDialog
|
|
101
|
-
rawTranscript={selectedText}
|
|
102
|
-
langCode={langCode}
|
|
103
|
-
onConfirm={(formattedText) => {
|
|
104
|
-
insertSutra(formattedText, mode)
|
|
105
|
-
setShowConfirm(false)
|
|
106
|
-
}}
|
|
107
|
-
onCancel={() => {
|
|
108
|
-
setShowConfirm(false)
|
|
109
|
-
}}
|
|
110
|
-
/>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!menuPosition) return null
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<div
|
|
118
|
-
className="cs-selection-menu"
|
|
119
|
-
style={{
|
|
120
|
-
left: `${menuPosition.x}px`,
|
|
121
|
-
top: `${menuPosition.y}px`,
|
|
122
|
-
}}
|
|
123
|
-
role="toolbar"
|
|
124
|
-
aria-label="Text Selection Actions"
|
|
125
|
-
>
|
|
126
|
-
<button
|
|
127
|
-
type="button"
|
|
128
|
-
className="cs-selection-btn cs-selection-btn-format"
|
|
129
|
-
onClick={handleFormat}
|
|
130
|
-
title="Format selected text using AI"
|
|
131
|
-
>
|
|
132
|
-
<SparklesIcon size={12} />
|
|
133
|
-
<span>Format</span>
|
|
134
|
-
</button>
|
|
135
|
-
<div className="cs-selection-sep" />
|
|
136
|
-
<button
|
|
137
|
-
type="button"
|
|
138
|
-
className="cs-selection-btn"
|
|
139
|
-
onClick={handleCut}
|
|
140
|
-
>
|
|
141
|
-
Cut
|
|
142
|
-
</button>
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
className="cs-selection-btn"
|
|
146
|
-
onClick={handleCopy}
|
|
147
|
-
>
|
|
148
|
-
Copy
|
|
149
|
-
</button>
|
|
150
|
-
</div>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Attribute pencil plugin — uniform inline editing affordance for scene
|
|
3
|
-
* attributes and title-page frontmatter.
|
|
4
|
-
*
|
|
5
|
-
* A pencil widget (visible on hover) sits at the end of every scene heading
|
|
6
|
-
* and at the top-right of the title page card.
|
|
7
|
-
*
|
|
8
|
-
* - Scene heading: opens a dropdown of attribute keys not yet present (plus a
|
|
9
|
-
* custom-key input). Picking one inserts the node via a normal PM
|
|
10
|
-
* transaction and places the cursor in the empty value, so there is no full
|
|
11
|
-
* state rebuild and no cursor jump.
|
|
12
|
-
* - Title page: opens the sectioned title page form (TitlePageDialog), where
|
|
13
|
-
* frontmatter fields are added and edited.
|
|
14
|
-
*/
|
|
15
|
-
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
|
16
|
-
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
17
|
-
import type { EditorView } from 'prosemirror-view'
|
|
18
|
-
import type { Node as PmNode } from 'prosemirror-model'
|
|
19
|
-
import { schema } from './schema'
|
|
20
|
-
import { openTitlePageForm } from './editor-bus'
|
|
21
|
-
|
|
22
|
-
const SCENE_META_KEYS = ['synopsis', 'status', 'tags', 'location', 'time', 'lang']
|
|
23
|
-
|
|
24
|
-
let openMenu: HTMLElement | null = null
|
|
25
|
-
|
|
26
|
-
function closeMenu() {
|
|
27
|
-
if (openMenu) {
|
|
28
|
-
openMenu.remove()
|
|
29
|
-
openMenu = null
|
|
30
|
-
document.removeEventListener('mousedown', onDocMousedown)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function onDocMousedown(e: MouseEvent) {
|
|
35
|
-
if (openMenu && !openMenu.contains(e.target as Node)) closeMenu()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function showMenu(anchor: HTMLElement, options: string[], onPick: (key: string) => void) {
|
|
39
|
-
closeMenu()
|
|
40
|
-
const menu = document.createElement('div')
|
|
41
|
-
menu.className = 'cs-attr-menu'
|
|
42
|
-
|
|
43
|
-
for (const key of options) {
|
|
44
|
-
const item = document.createElement('button')
|
|
45
|
-
item.type = 'button'
|
|
46
|
-
item.className = 'cs-attr-menu-item'
|
|
47
|
-
item.textContent = `+ ${key}`
|
|
48
|
-
item.addEventListener('mousedown', e => {
|
|
49
|
-
e.preventDefault()
|
|
50
|
-
closeMenu()
|
|
51
|
-
onPick(key)
|
|
52
|
-
})
|
|
53
|
-
menu.appendChild(item)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const customRow = document.createElement('div')
|
|
57
|
-
customRow.className = 'cs-attr-menu-custom'
|
|
58
|
-
const input = document.createElement('input')
|
|
59
|
-
input.type = 'text'
|
|
60
|
-
input.placeholder = 'custom key…'
|
|
61
|
-
input.addEventListener('keydown', e => {
|
|
62
|
-
e.stopPropagation()
|
|
63
|
-
if (e.key === 'Enter') {
|
|
64
|
-
const k = input.value.trim()
|
|
65
|
-
if (k) { closeMenu(); onPick(k) }
|
|
66
|
-
} else if (e.key === 'Escape') {
|
|
67
|
-
closeMenu()
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
input.addEventListener('mousedown', e => e.stopPropagation())
|
|
71
|
-
customRow.appendChild(input)
|
|
72
|
-
menu.appendChild(customRow)
|
|
73
|
-
|
|
74
|
-
const rect = anchor.getBoundingClientRect()
|
|
75
|
-
menu.style.left = `${rect.left}px`
|
|
76
|
-
menu.style.top = `${rect.bottom + 4}px`
|
|
77
|
-
document.body.appendChild(menu)
|
|
78
|
-
openMenu = menu
|
|
79
|
-
// Defer so the click that opened the menu doesn't immediately close it
|
|
80
|
-
setTimeout(() => document.addEventListener('mousedown', onDocMousedown), 0)
|
|
81
|
-
input.focus()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Insert an attribute node and place the cursor inside its (empty) content. */
|
|
85
|
-
function insertNode(view: EditorView, pos: number, node: PmNode) {
|
|
86
|
-
const tr = view.state.tr.insert(pos, node)
|
|
87
|
-
view.dispatch(tr.setSelection(TextSelection.near(tr.doc.resolve(pos + 1))))
|
|
88
|
-
view.focus()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* For a scene heading at top-level index `index`, compute which attribute
|
|
93
|
-
* keys already exist and where a new attribute line should be inserted
|
|
94
|
-
* (after the heading, synopsis, and existing metadata lines).
|
|
95
|
-
*/
|
|
96
|
-
function sceneAttrContext(view: EditorView, headingPos: number) {
|
|
97
|
-
const doc = view.state.doc
|
|
98
|
-
const $heading = doc.resolve(headingPos)
|
|
99
|
-
const headingNode = doc.nodeAt(headingPos)!
|
|
100
|
-
let pos = headingPos + headingNode.nodeSize
|
|
101
|
-
let index = $heading.index(0) + 1
|
|
102
|
-
const presentKeys = new Set<string>()
|
|
103
|
-
|
|
104
|
-
while (index < doc.childCount) {
|
|
105
|
-
const sibling = doc.child(index)
|
|
106
|
-
if (sibling.type.name === 'scene_metadata') {
|
|
107
|
-
presentKeys.add(sibling.attrs['metaKey'] as string)
|
|
108
|
-
} else {
|
|
109
|
-
break
|
|
110
|
-
}
|
|
111
|
-
pos += sibling.nodeSize
|
|
112
|
-
index++
|
|
113
|
-
}
|
|
114
|
-
return { insertPos: pos, presentKeys }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function makePencil(className: string, title: string, onClick: (btn: HTMLElement) => void): HTMLElement {
|
|
118
|
-
const btn = document.createElement('button')
|
|
119
|
-
btn.type = 'button'
|
|
120
|
-
btn.className = className
|
|
121
|
-
btn.title = title
|
|
122
|
-
btn.textContent = '✎'
|
|
123
|
-
btn.contentEditable = 'false'
|
|
124
|
-
btn.addEventListener('mousedown', e => {
|
|
125
|
-
e.preventDefault()
|
|
126
|
-
e.stopPropagation()
|
|
127
|
-
onClick(btn)
|
|
128
|
-
})
|
|
129
|
-
return btn
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const attrPencilKey = new PluginKey('attrPencil')
|
|
133
|
-
|
|
134
|
-
export function createAttrPencilPlugin(): Plugin {
|
|
135
|
-
return new Plugin({
|
|
136
|
-
key: attrPencilKey,
|
|
137
|
-
props: {
|
|
138
|
-
decorations(state) {
|
|
139
|
-
const decos: Decoration[] = []
|
|
140
|
-
state.doc.forEach((node, offset) => {
|
|
141
|
-
if (node.type.name === 'scene_heading') {
|
|
142
|
-
// Widget inside the heading, after its text
|
|
143
|
-
decos.push(Decoration.widget(offset + node.nodeSize - 1, view =>
|
|
144
|
-
makePencil('cs-attr-pencil', 'Add scene attribute', btn => {
|
|
145
|
-
const ctx = sceneAttrContext(view, offset)
|
|
146
|
-
const options = SCENE_META_KEYS.filter(k => !ctx.presentKeys.has(k))
|
|
147
|
-
showMenu(btn, options, key => {
|
|
148
|
-
const fresh = sceneAttrContext(view, offset)
|
|
149
|
-
insertNode(view, fresh.insertPos, schema.nodes['scene_metadata']!.create({ metaKey: key }))
|
|
150
|
-
})
|
|
151
|
-
}),
|
|
152
|
-
{ side: 1 }))
|
|
153
|
-
}
|
|
154
|
-
if (node.type.name === 'title_page') {
|
|
155
|
-
// Title page fields are added and edited in the title page form; the pencil
|
|
156
|
-
// (top-right of the card, positioned by CSS) opens it.
|
|
157
|
-
decos.push(Decoration.widget(offset + 1, () =>
|
|
158
|
-
makePencil('cs-attr-pencil cs-tp-pencil', 'Edit title page', () => {
|
|
159
|
-
closeMenu()
|
|
160
|
-
openTitlePageForm()
|
|
161
|
-
}),
|
|
162
|
-
{ side: -1, key: 'tp-pencil' }))
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
return DecorationSet.create(state.doc, decos)
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
view() {
|
|
169
|
-
return { destroy: closeMenu }
|
|
170
|
-
},
|
|
171
|
-
})
|
|
172
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { Plugin } from 'prosemirror-state'
|
|
2
|
-
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
3
|
-
import type { Node as PmNode } from 'prosemirror-model'
|
|
4
|
-
import type { EditorState } from 'prosemirror-state'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* ProseMirror plugin that styles a character cue's trailing extension
|
|
8
|
-
* "(V.O.)"/"(O.S.)" and dual-dialogue "^" marker live, purely from the text
|
|
9
|
-
* pattern — there's no dedicated UI to set these; per the format spec, any
|
|
10
|
-
* content in parentheses at the end of a character line is an extension,
|
|
11
|
-
* and a trailing "^" marks dual dialogue. No node attrs or persisted marks
|
|
12
|
-
* are involved — decorations are recomputed from scratch on every doc
|
|
13
|
-
* change, the same way createLanguageClassPlugin() does for script classes.
|
|
14
|
-
*/
|
|
15
|
-
export function createCharacterDecorationPlugin() {
|
|
16
|
-
return new Plugin({
|
|
17
|
-
state: {
|
|
18
|
-
init(_config, state) {
|
|
19
|
-
return computeDecorations(state.doc)
|
|
20
|
-
},
|
|
21
|
-
apply(tr, set, _oldState, newState) {
|
|
22
|
-
if (tr.docChanged) {
|
|
23
|
-
return computeDecorations(newState.doc)
|
|
24
|
-
}
|
|
25
|
-
return set
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
props: {
|
|
29
|
-
decorations(state: EditorState) {
|
|
30
|
-
return this.getState(state)
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface DecorationRanges {
|
|
37
|
-
parenStart?: number
|
|
38
|
-
parenEnd?: number
|
|
39
|
-
dualStart?: number
|
|
40
|
-
dualEnd?: number
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Finds the trailing "(...)"-group span and/or trailing "^" span in a
|
|
45
|
-
* character cue's text, as character offsets into `text`. Either, both, or
|
|
46
|
-
* neither may be present. The "^" is checked (and stripped from
|
|
47
|
-
* consideration) first, since it comes after any extension groups.
|
|
48
|
-
*/
|
|
49
|
-
export function findCharacterDecorationRanges(text: string): DecorationRanges {
|
|
50
|
-
const ranges: DecorationRanges = {}
|
|
51
|
-
|
|
52
|
-
let working = text
|
|
53
|
-
const dualMatch = /\s(\^)\s*$/.exec(text)
|
|
54
|
-
if (dualMatch) {
|
|
55
|
-
ranges.dualStart = dualMatch.index + dualMatch[0].indexOf('^')
|
|
56
|
-
ranges.dualEnd = ranges.dualStart + 1
|
|
57
|
-
working = text.slice(0, dualMatch.index)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const parenMatch = /(\s+\([^()]*\))+\s*$/.exec(working)
|
|
61
|
-
if (parenMatch) {
|
|
62
|
-
const matched = parenMatch[0]
|
|
63
|
-
const leadingWs = matched.length - matched.trimStart().length
|
|
64
|
-
ranges.parenStart = parenMatch.index + leadingWs
|
|
65
|
-
ranges.parenEnd = parenMatch.index + matched.trimEnd().length
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return ranges
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function computeDecorations(doc: PmNode): DecorationSet {
|
|
72
|
-
const decorations: Decoration[] = []
|
|
73
|
-
|
|
74
|
-
doc.forEach((node, pos) => {
|
|
75
|
-
if (node.type.name !== 'character') return
|
|
76
|
-
const text = node.textContent
|
|
77
|
-
if (!text) return
|
|
78
|
-
|
|
79
|
-
const { parenStart, parenEnd, dualStart, dualEnd } = findCharacterDecorationRanges(text)
|
|
80
|
-
// +1: text content starts one position inside the block node
|
|
81
|
-
const contentStart = pos + 1
|
|
82
|
-
|
|
83
|
-
if (parenStart !== undefined && parenEnd !== undefined) {
|
|
84
|
-
// class passed in both attrs (DOM rendering) and spec (testing/reference) —
|
|
85
|
-
// matches language-class-plugin.ts's convention.
|
|
86
|
-
decorations.push(Decoration.inline(
|
|
87
|
-
contentStart + parenStart,
|
|
88
|
-
contentStart + parenEnd,
|
|
89
|
-
{ class: 'cs-character-extension' },
|
|
90
|
-
{ class: 'cs-character-extension' }
|
|
91
|
-
))
|
|
92
|
-
}
|
|
93
|
-
if (dualStart !== undefined && dualEnd !== undefined) {
|
|
94
|
-
decorations.push(Decoration.inline(
|
|
95
|
-
contentStart + dualStart,
|
|
96
|
-
contentStart + dualEnd,
|
|
97
|
-
{ class: 'cs-character-dual-marker' },
|
|
98
|
-
{ class: 'cs-character-dual-marker' }
|
|
99
|
-
))
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
return DecorationSet.create(doc, decorations)
|
|
104
|
-
}
|
package/src/editor/editor-bus.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { EditorView } from 'prosemirror-view'
|
|
2
|
-
import type { Command } from 'prosemirror-state'
|
|
3
|
-
import type { EditorView as CmEditorView } from '@codemirror/view'
|
|
4
|
-
|
|
5
|
-
let currentView: EditorView | null = null
|
|
6
|
-
let currentSourceView: CmEditorView | null = null
|
|
7
|
-
const listeners = new Set<() => void>()
|
|
8
|
-
|
|
9
|
-
export function setEditorView(view: EditorView | null): void {
|
|
10
|
-
currentView = view
|
|
11
|
-
emit()
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getEditorView(): EditorView | null {
|
|
15
|
-
return currentView
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function setSourceView(view: CmEditorView | null): void {
|
|
19
|
-
currentSourceView = view
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getSourceView(): CmEditorView | null {
|
|
23
|
-
return currentSourceView
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function runCommand(cmd: Command): void {
|
|
27
|
-
if (!currentView) return
|
|
28
|
-
cmd(currentView.state, currentView.dispatch, currentView)
|
|
29
|
-
currentView.focus()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function activeBlockType(): string | null {
|
|
33
|
-
if (!currentView) return null
|
|
34
|
-
return currentView.state.selection.$from.parent.type.name
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function subscribe(fn: () => void): () => void {
|
|
38
|
-
listeners.add(fn)
|
|
39
|
-
return () => listeners.delete(fn)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function emit(): void {
|
|
43
|
-
listeners.forEach(fn => fn())
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Window event that opens the title page form (TitlePageDialog, mounted by AppShell). */
|
|
47
|
-
export const OPEN_TITLE_PAGE_EVENT = 'cs:open-title-page'
|
|
48
|
-
|
|
49
|
-
export function openTitlePageForm(): void {
|
|
50
|
-
window.dispatchEvent(new CustomEvent(OPEN_TITLE_PAGE_EVENT))
|
|
51
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { Plugin, PluginKey } from 'prosemirror-state'
|
|
2
|
-
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
3
|
-
import type { PmMatch } from '../findreplace/scroll-to-match'
|
|
4
|
-
|
|
5
|
-
const findHighlightKey = new PluginKey<DecorationSet>('findHighlight')
|
|
6
|
-
|
|
7
|
-
// Module-level state pushed from FindReplace; the plugin reads this on each update.
|
|
8
|
-
let currentMatches: PmMatch[] = []
|
|
9
|
-
let currentIndex = 0
|
|
10
|
-
let pluginView: import('prosemirror-view').EditorView | null = null
|
|
11
|
-
|
|
12
|
-
/** Called by FindReplace whenever matches or active index change. */
|
|
13
|
-
export function setFindHighlights(matches: PmMatch[], activeIndex: number): void {
|
|
14
|
-
currentMatches = matches
|
|
15
|
-
currentIndex = activeIndex
|
|
16
|
-
if (pluginView) {
|
|
17
|
-
// Dispatch a no-op meta transaction to trigger decoration recompute.
|
|
18
|
-
const tr = pluginView.state.tr.setMeta(findHighlightKey, true)
|
|
19
|
-
pluginView.dispatch(tr)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Called by FindReplace when the panel closes — clears all highlights. */
|
|
24
|
-
export function clearFindHighlights(): void {
|
|
25
|
-
setFindHighlights([], 0)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function buildDecorations(matches: PmMatch[], activeIndex: number, doc: import('prosemirror-model').Node): DecorationSet {
|
|
29
|
-
if (matches.length === 0) return DecorationSet.empty
|
|
30
|
-
const decos = matches.map((m, i) =>
|
|
31
|
-
Decoration.inline(m.pmFrom, m.pmTo, {
|
|
32
|
-
class: i === activeIndex ? 'cs-find-active' : 'cs-find-match',
|
|
33
|
-
})
|
|
34
|
-
)
|
|
35
|
-
return DecorationSet.create(doc, decos)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createFindHighlightPlugin(): Plugin {
|
|
39
|
-
return new Plugin<DecorationSet>({
|
|
40
|
-
key: findHighlightKey,
|
|
41
|
-
|
|
42
|
-
view(view) {
|
|
43
|
-
pluginView = view
|
|
44
|
-
return {
|
|
45
|
-
destroy() { pluginView = null },
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
state: {
|
|
50
|
-
init(_config, state) {
|
|
51
|
-
return buildDecorations(currentMatches, currentIndex, state.doc)
|
|
52
|
-
},
|
|
53
|
-
apply(tr, _old, _oldState, newState) {
|
|
54
|
-
// Recompute on doc change (positions shift) or when we push a meta update.
|
|
55
|
-
if (tr.docChanged || tr.getMeta(findHighlightKey)) {
|
|
56
|
-
return buildDecorations(currentMatches, currentIndex, newState.doc)
|
|
57
|
-
}
|
|
58
|
-
return _old
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
props: {
|
|
63
|
-
decorations(state) {
|
|
64
|
-
return this.getState(state)
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
})
|
|
68
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { Node as PmNode } from 'prosemirror-model'
|
|
2
|
-
import type { EditorView, NodeView } from 'prosemirror-view'
|
|
3
|
-
|
|
4
|
-
export class FrontmatterFieldView implements NodeView {
|
|
5
|
-
dom: HTMLElement
|
|
6
|
-
contentDOM: HTMLElement
|
|
7
|
-
private counter: HTMLElement | null = null
|
|
8
|
-
|
|
9
|
-
constructor(node: PmNode, view: EditorView, getPos: () => number | undefined) {
|
|
10
|
-
this.dom = document.createElement('div')
|
|
11
|
-
this.dom.className = 'cs-fm-field'
|
|
12
|
-
this.dom.dataset['key'] = node.attrs['fmKey'] as string
|
|
13
|
-
|
|
14
|
-
this.contentDOM = document.createElement('span')
|
|
15
|
-
this.contentDOM.className = 'cs-fm-field-content'
|
|
16
|
-
this.dom.appendChild(this.contentDOM)
|
|
17
|
-
|
|
18
|
-
if (node.attrs['fmKey'] === 'logline') {
|
|
19
|
-
this.counter = document.createElement('span')
|
|
20
|
-
this.counter.className = 'cs-fm-logline-counter'
|
|
21
|
-
this.updateCounter(node.textContent)
|
|
22
|
-
this.dom.appendChild(this.counter)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private updateCounter(text: string) {
|
|
27
|
-
if (!this.counter) return
|
|
28
|
-
const words = text.split(/\s+/).filter(Boolean).length
|
|
29
|
-
this.counter.textContent = `${words} words`
|
|
30
|
-
if (words > 50) {
|
|
31
|
-
this.counter.style.color = '#c5221f' // red warning
|
|
32
|
-
this.counter.style.background = '#fce8e6'
|
|
33
|
-
this.counter.style.borderColor = '#fad2cf'
|
|
34
|
-
} else if (words > 30) {
|
|
35
|
-
this.counter.style.color = '#b06000' // orange warn/target
|
|
36
|
-
this.counter.style.background = '#fef7e0'
|
|
37
|
-
this.counter.style.borderColor = '#feebc8'
|
|
38
|
-
} else {
|
|
39
|
-
this.counter.style.color = '#8a887f' // clean gray
|
|
40
|
-
this.counter.style.background = '#f0efe9'
|
|
41
|
-
this.counter.style.borderColor = '#e2e0d8'
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
update(node: PmNode): boolean {
|
|
46
|
-
if (node.type.name !== 'frontmatter_field') return false
|
|
47
|
-
if (node.attrs['fmKey'] === 'logline' && this.counter) {
|
|
48
|
-
this.updateCounter(node.textContent)
|
|
49
|
-
}
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
}
|