@sutrata/editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/dist/ai/OnboardingDialog.d.ts +6 -0
- package/dist/ai/OnboardingDialog.js +115 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
- package/dist/ai/VoiceConfirmDialog.js +37 -0
- package/dist/ai/VoiceToolbar.d.ts +2 -0
- package/dist/ai/VoiceToolbar.js +177 -0
- package/dist/ai/ai-client.d.ts +71 -0
- package/dist/ai/ai-client.js +494 -0
- package/dist/ai/commands.d.ts +12 -0
- package/dist/ai/commands.js +140 -0
- package/dist/ai/generated-providers.json +3696 -0
- package/dist/ai/prompts.d.ts +25 -0
- package/dist/ai/prompts.js +59 -0
- package/dist/ai/voice-service.d.ts +15 -0
- package/dist/ai/voice-service.js +81 -0
- package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
- package/dist/autocomplete/AutocompletePlugin.js +19 -0
- package/dist/autocomplete/suggestions.d.ts +25 -0
- package/dist/autocomplete/suggestions.js +126 -0
- package/dist/context/DocumentContext.d.ts +68 -0
- package/dist/context/DocumentContext.js +605 -0
- package/dist/desktop-api.d.ts +40 -0
- package/dist/editor/EditorView.d.ts +2 -0
- package/dist/editor/EditorView.js +78 -0
- package/dist/editor/SelectionMenu.d.ts +2 -0
- package/dist/editor/SelectionMenu.js +102 -0
- package/dist/editor/attr-pencil-plugin.d.ts +17 -0
- package/dist/editor/attr-pencil-plugin.js +158 -0
- package/dist/editor/character-decoration-plugin.d.ts +26 -0
- package/dist/editor/character-decoration-plugin.js +77 -0
- package/dist/editor/character-view.d.ts +35 -0
- package/dist/editor/character-view.js +112 -0
- package/dist/editor/editor-bus.d.ts +14 -0
- package/dist/editor/editor-bus.js +39 -0
- package/dist/editor/find-highlight-plugin.d.ts +7 -0
- package/dist/editor/find-highlight-plugin.js +57 -0
- package/dist/editor/frontmatter-field-view.d.ts +10 -0
- package/dist/editor/frontmatter-field-view.js +48 -0
- package/dist/editor/frontmatter-field.d.ts +21 -0
- package/dist/editor/frontmatter-field.js +116 -0
- package/dist/editor/grapheme-cursor.d.ts +10 -0
- package/dist/editor/grapheme-cursor.js +104 -0
- package/dist/editor/ime-plugin.d.ts +18 -0
- package/dist/editor/ime-plugin.js +71 -0
- package/dist/editor/insert-helper.d.ts +1 -0
- package/dist/editor/insert-helper.js +42 -0
- package/dist/editor/keymap.d.ts +3 -0
- package/dist/editor/keymap.js +131 -0
- package/dist/editor/language-class-plugin.d.ts +7 -0
- package/dist/editor/language-class-plugin.js +75 -0
- package/dist/editor/plugins.d.ts +1 -0
- package/dist/editor/plugins.js +25 -0
- package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
- package/dist/editor/prosemirror-to-sutra.js +208 -0
- package/dist/editor/scene-heading-view.d.ts +27 -0
- package/dist/editor/scene-heading-view.js +136 -0
- package/dist/editor/schema.d.ts +3 -0
- package/dist/editor/schema.js +135 -0
- package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
- package/dist/editor/sutra-to-prosemirror.js +154 -0
- package/dist/editor/transliterate-input.d.ts +10 -0
- package/dist/editor/transliterate-input.js +88 -0
- package/dist/extensions/storage-adapter.d.ts +59 -0
- package/dist/extensions/storage-adapter.js +9 -0
- package/dist/extensions/types.d.ts +73 -0
- package/dist/extensions/types.js +12 -0
- package/dist/file/docx-exporter.d.ts +8 -0
- package/dist/file/docx-exporter.js +494 -0
- package/dist/file/docx-importer.d.ts +10 -0
- package/dist/file/docx-importer.js +283 -0
- package/dist/file/print-paginator.d.ts +29 -0
- package/dist/file/print-paginator.js +289 -0
- package/dist/file/templates.d.ts +2 -0
- package/dist/file/templates.js +66 -0
- package/dist/file/workflow-reports.d.ts +34 -0
- package/dist/file/workflow-reports.js +719 -0
- package/dist/findreplace/FindReplace.d.ts +2 -0
- package/dist/findreplace/FindReplace.js +164 -0
- package/dist/findreplace/find-engine.d.ts +29 -0
- package/dist/findreplace/find-engine.js +69 -0
- package/dist/findreplace/scroll-to-match.d.ts +29 -0
- package/dist/findreplace/scroll-to-match.js +74 -0
- package/dist/fonts/tofu-detect.d.ts +1 -0
- package/dist/fonts/tofu-detect.js +48 -0
- package/dist/i18n/LanguageContext.d.ts +10 -0
- package/dist/i18n/LanguageContext.js +174 -0
- package/dist/i18n/language-detect.d.ts +1 -0
- package/dist/i18n/language-detect.js +58 -0
- package/dist/i18n/useTranslation.d.ts +11 -0
- package/dist/i18n/useTranslation.js +60 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/locales/bn.json +143 -0
- package/dist/locales/en.json +143 -0
- package/dist/locales/gu.json +143 -0
- package/dist/locales/hi.json +143 -0
- package/dist/locales/kn.json +143 -0
- package/dist/locales/ml.json +143 -0
- package/dist/locales/mr.json +143 -0
- package/dist/locales/or.json +143 -0
- package/dist/locales/pa.json +143 -0
- package/dist/locales/si.json +143 -0
- package/dist/locales/ta.json +143 -0
- package/dist/locales/te.json +143 -0
- package/dist/navigator/SceneNavigator.d.ts +2 -0
- package/dist/navigator/SceneNavigator.js +612 -0
- package/dist/navigator/StatisticsDialog.d.ts +6 -0
- package/dist/navigator/StatisticsDialog.js +146 -0
- package/dist/navigator/scene-list.d.ts +16 -0
- package/dist/navigator/scene-list.js +70 -0
- package/dist/settings/SettingsDialog.d.ts +6 -0
- package/dist/settings/SettingsDialog.js +147 -0
- package/dist/shell/AppBar.d.ts +2 -0
- package/dist/shell/AppBar.js +34 -0
- package/dist/shell/AppShell.d.ts +2 -0
- package/dist/shell/AppShell.js +30 -0
- package/dist/shell/ConfirmDialog.d.ts +16 -0
- package/dist/shell/ConfirmDialog.js +23 -0
- package/dist/shell/ElementToolbar.d.ts +2 -0
- package/dist/shell/ElementToolbar.js +30 -0
- package/dist/shell/LanguageStatus.d.ts +2 -0
- package/dist/shell/LanguageStatus.js +65 -0
- package/dist/shell/StatusBar.d.ts +2 -0
- package/dist/shell/StatusBar.js +54 -0
- package/dist/shell/SutrataLogo.d.ts +8 -0
- package/dist/shell/SutrataLogo.js +7 -0
- package/dist/shell/Toast.d.ts +12 -0
- package/dist/shell/Toast.js +21 -0
- package/dist/shell/ToolbarButton.d.ts +11 -0
- package/dist/shell/ToolbarButton.js +4 -0
- package/dist/shell/VersionHistory.d.ts +12 -0
- package/dist/shell/VersionHistory.js +89 -0
- package/dist/shell/Watermark.d.ts +3 -0
- package/dist/shell/Watermark.js +10 -0
- package/dist/shell/fonts/fonts.css +1133 -0
- package/dist/shell/icons.d.ts +45 -0
- package/dist/shell/icons.js +51 -0
- package/dist/shell/toolbar-actions.d.ts +6 -0
- package/dist/shell/toolbar-actions.js +58 -0
- package/dist/source/SourceView.d.ts +1 -0
- package/dist/source/SourceView.js +106 -0
- package/dist/source/find-highlight-source.d.ts +14 -0
- package/dist/source/find-highlight-source.js +47 -0
- package/dist/source/sutra-lang.d.ts +2 -0
- package/dist/source/sutra-lang.js +97 -0
- package/dist/spellcheck/dictionaries.d.ts +9 -0
- package/dist/spellcheck/dictionaries.js +16 -0
- package/dist/spellcheck/hunspell.d.ts +23 -0
- package/dist/spellcheck/hunspell.js +94 -0
- package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
- package/dist/spellcheck/spellcheck-plugin.js +75 -0
- package/dist/styles/StyleDialog.d.ts +6 -0
- package/dist/styles/StyleDialog.js +63 -0
- package/dist/styles/builtin-styles.d.ts +43 -0
- package/dist/styles/builtin-styles.js +131 -0
- package/dist/styles/css-adapter.d.ts +32 -0
- package/dist/styles/css-adapter.js +71 -0
- package/dist/styles/docx-adapter.d.ts +7 -0
- package/dist/styles/docx-adapter.js +84 -0
- package/dist/styles/embed-fonts.d.ts +20 -0
- package/dist/styles/embed-fonts.js +81 -0
- package/dist/styles/pagination.d.ts +19 -0
- package/dist/styles/pagination.js +22 -0
- package/dist/styles/print-adapter.d.ts +6 -0
- package/dist/styles/print-adapter.js +288 -0
- package/dist/styles/registry.d.ts +6 -0
- package/dist/styles/registry.js +14 -0
- package/dist/styles/screenplay.css +3006 -0
- package/dist/styles/script-ranges.d.ts +6 -0
- package/dist/styles/script-ranges.js +17 -0
- package/dist/styles/types.d.ts +93 -0
- package/dist/styles/types.js +18 -0
- package/dist/styles/validate.d.ts +12 -0
- package/dist/styles/validate.js +105 -0
- package/dist/templates/templates.d.ts +10 -0
- package/dist/templates/templates.js +181 -0
- package/dist/titlepage/ExportDialog.d.ts +1 -0
- package/dist/titlepage/ExportDialog.js +152 -0
- package/dist/titlepage/TitlePageDialog.d.ts +6 -0
- package/dist/titlepage/TitlePageDialog.js +96 -0
- package/dist/titlepage/frontmatter-form.d.ts +56 -0
- package/dist/titlepage/frontmatter-form.js +245 -0
- package/dist/titlepage/languages.d.ts +10 -0
- package/dist/titlepage/languages.js +31 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
- package/scripts/copy-assets.mjs +16 -0
- package/scripts/generate-providers.mjs +127 -0
- package/src/ai/OnboardingDialog.tsx +390 -0
- package/src/ai/VoiceConfirmDialog.tsx +137 -0
- package/src/ai/VoiceToolbar.tsx +321 -0
- package/src/ai/ai-client.ts +596 -0
- package/src/ai/commands.ts +164 -0
- package/src/ai/generated-providers.json +3696 -0
- package/src/ai/prompts.ts +62 -0
- package/src/ai/voice-service.ts +96 -0
- package/src/autocomplete/AutocompletePlugin.ts +21 -0
- package/src/autocomplete/suggestions.ts +147 -0
- package/src/context/DocumentContext.tsx +648 -0
- package/src/desktop-api.d.ts +40 -0
- package/src/editor/EditorView.tsx +82 -0
- package/src/editor/SelectionMenu.tsx +152 -0
- package/src/editor/attr-pencil-plugin.ts +172 -0
- package/src/editor/character-decoration-plugin.ts +104 -0
- package/src/editor/editor-bus.ts +51 -0
- package/src/editor/find-highlight-plugin.ts +68 -0
- package/src/editor/frontmatter-field-view.ts +52 -0
- package/src/editor/frontmatter-field.ts +115 -0
- package/src/editor/grapheme-cursor.ts +99 -0
- package/src/editor/ime-plugin.ts +85 -0
- package/src/editor/insert-helper.ts +43 -0
- package/src/editor/keymap.ts +124 -0
- package/src/editor/language-class-plugin.ts +91 -0
- package/src/editor/plugins.ts +26 -0
- package/src/editor/prosemirror-to-sutra.ts +192 -0
- package/src/editor/scene-heading-view.ts +147 -0
- package/src/editor/schema.ts +156 -0
- package/src/editor/sutra-to-prosemirror.ts +156 -0
- package/src/extensions/storage-adapter.ts +65 -0
- package/src/extensions/types.ts +60 -0
- package/src/file/docx-exporter.ts +585 -0
- package/src/file/docx-importer.ts +313 -0
- package/src/file/print-paginator.ts +301 -0
- package/src/file/templates.ts +73 -0
- package/src/file/workflow-reports.ts +820 -0
- package/src/findreplace/FindReplace.tsx +242 -0
- package/src/findreplace/find-engine.ts +97 -0
- package/src/findreplace/scroll-to-match.ts +89 -0
- package/src/fonts/tofu-detect.ts +53 -0
- package/src/i18n/LanguageContext.tsx +207 -0
- package/src/i18n/language-detect.ts +73 -0
- package/src/i18n/useTranslation.tsx +74 -0
- package/src/index.ts +27 -0
- package/src/locales/bn.json +143 -0
- package/src/locales/en.json +143 -0
- package/src/locales/gu.json +143 -0
- package/src/locales/hi.json +143 -0
- package/src/locales/kn.json +143 -0
- package/src/locales/ml.json +143 -0
- package/src/locales/mr.json +143 -0
- package/src/locales/or.json +143 -0
- package/src/locales/pa.json +143 -0
- package/src/locales/si.json +143 -0
- package/src/locales/ta.json +143 -0
- package/src/locales/te.json +143 -0
- package/src/navigator/SceneNavigator.tsx +844 -0
- package/src/navigator/StatisticsDialog.tsx +249 -0
- package/src/navigator/scene-list.ts +84 -0
- package/src/settings/SettingsDialog.tsx +303 -0
- package/src/shell/AppBar.tsx +354 -0
- package/src/shell/AppShell.tsx +71 -0
- package/src/shell/ConfirmDialog.tsx +71 -0
- package/src/shell/ElementToolbar.tsx +175 -0
- package/src/shell/LanguageStatus.tsx +101 -0
- package/src/shell/StatusBar.tsx +135 -0
- package/src/shell/SutrataLogo.tsx +89 -0
- package/src/shell/Toast.tsx +46 -0
- package/src/shell/ToolbarButton.tsx +30 -0
- package/src/shell/VersionHistory.tsx +212 -0
- package/src/shell/Watermark.tsx +9 -0
- package/src/shell/fonts/fonts.css +1133 -0
- package/src/shell/icons.tsx +335 -0
- package/src/shell/toolbar-actions.ts +59 -0
- package/src/source/SourceView.tsx +111 -0
- package/src/source/find-highlight-source.ts +57 -0
- package/src/source/sutra-lang.ts +121 -0
- package/src/spellcheck/dictionaries.ts +21 -0
- package/src/spellcheck/hunspell.ts +110 -0
- package/src/spellcheck/spellcheck-plugin.ts +83 -0
- package/src/styles/StyleDialog.tsx +148 -0
- package/src/styles/builtin-styles.ts +139 -0
- package/src/styles/css-adapter.ts +76 -0
- package/src/styles/docx-adapter.ts +109 -0
- package/src/styles/embed-fonts.ts +81 -0
- package/src/styles/pagination.ts +27 -0
- package/src/styles/print-adapter.ts +288 -0
- package/src/styles/registry.ts +20 -0
- package/src/styles/screenplay.css +3006 -0
- package/src/styles/script-ranges.ts +17 -0
- package/src/styles/types.ts +104 -0
- package/src/styles/validate.ts +114 -0
- package/src/templates/templates.ts +194 -0
- package/src/titlepage/ExportDialog.tsx +494 -0
- package/src/titlepage/TitlePageDialog.tsx +312 -0
- package/src/titlepage/frontmatter-form.ts +280 -0
- package/src/titlepage/languages.ts +37 -0
- package/src/types.ts +7 -0
- package/tests/AppBar.test.tsx +35 -0
- package/tests/AppShell.test.tsx +22 -0
- package/tests/ElementToolbar.test.tsx +36 -0
- package/tests/LanguageStatus.test.tsx +129 -0
- package/tests/SelectionMenu.test.tsx +71 -0
- package/tests/ToolbarButton.test.tsx +24 -0
- package/tests/ai-onboarding.test.ts +102 -0
- package/tests/ai-status.test.tsx +72 -0
- package/tests/ai-structured.test.ts +94 -0
- package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
- package/tests/autocomplete/suggestions.test.ts +106 -0
- package/tests/document-context.test.tsx +565 -0
- package/tests/docx-importer.test.ts +160 -0
- package/tests/editor/character-decoration-plugin.test.ts +101 -0
- package/tests/editor/character-extension.test.ts +44 -0
- package/tests/editor/grapheme-cursor.test.ts +118 -0
- package/tests/editor/ime-plugin.test.ts +72 -0
- package/tests/editor/inline-note-bridge.test.ts +32 -0
- package/tests/editor/language-class-plugin.test.ts +265 -0
- package/tests/editor/title-page.test.ts +124 -0
- package/tests/embed-fonts.test.ts +17 -0
- package/tests/exporters.test.ts +365 -0
- package/tests/findreplace/FindReplace.test.tsx +391 -0
- package/tests/findreplace/find-engine.test.ts +86 -0
- package/tests/findreplace/scroll-to-match.test.ts +50 -0
- package/tests/fonts/tofu-detect.test.ts +122 -0
- package/tests/i18n/LanguageContext.test.tsx +146 -0
- package/tests/i18n/language-detect.test.ts +54 -0
- package/tests/i18n/locale-parity.test.ts +25 -0
- package/tests/i18n/useTranslation.test.tsx +45 -0
- package/tests/keymap.test.ts +18 -0
- package/tests/navigator/SceneNavigator.test.tsx +177 -0
- package/tests/navigator/StatisticsDialog.test.tsx +63 -0
- package/tests/navigator/scene-list.test.ts +71 -0
- package/tests/print-paginator.test.ts +82 -0
- package/tests/shell/VersionHistory.test.tsx +129 -0
- package/tests/spellcheck/hunspell.test.ts +82 -0
- package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
- package/tests/styles.test.ts +315 -0
- package/tests/templates/templates.test.ts +83 -0
- package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
- package/tests/titlepage/apply-title-page.test.ts +61 -0
- package/tests/titlepage/frontmatter-form.test.ts +115 -0
- package/tests/toolbar-actions.test.ts +93 -0
- package/tests/voice-commands.test.ts +58 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +11 -0
- package/vitest.setup.ts +65 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface DictionaryManifest {
|
|
2
|
+
aff: string // path relative to public/
|
|
3
|
+
dic: string // path relative to public/
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dictionary manifest: maps BCP-47 language code → asset paths under public/dict/.
|
|
8
|
+
* The actual .dic/.aff files are not committed; see public/dict/README.md for acquisition.
|
|
9
|
+
*/
|
|
10
|
+
export const DICTIONARIES: Record<string, DictionaryManifest> = {
|
|
11
|
+
'en': { aff: '/dict/en_US.aff', dic: '/dict/en_US.dic' },
|
|
12
|
+
'hi': { aff: '/dict/hi_IN.aff', dic: '/dict/hi_IN.dic' },
|
|
13
|
+
'ta': { aff: '/dict/ta_IN.aff', dic: '/dict/ta_IN.dic' },
|
|
14
|
+
'te': { aff: '/dict/te_IN.aff', dic: '/dict/te_IN.dic' },
|
|
15
|
+
'kn': { aff: '/dict/kn_IN.aff', dic: '/dict/kn_IN.dic' },
|
|
16
|
+
'ml': { aff: '/dict/ml_IN.aff', dic: '/dict/ml_IN.dic' },
|
|
17
|
+
'bn': { aff: '/dict/bn_IN.aff', dic: '/dict/bn_IN.dic' },
|
|
18
|
+
'gu': { aff: '/dict/gu_IN.aff', dic: '/dict/gu_IN.dic' },
|
|
19
|
+
'pa': { aff: '/dict/pa_IN.aff', dic: '/dict/pa_IN.dic' },
|
|
20
|
+
'or': { aff: '/dict/or_IN.aff', dic: '/dict/or_IN.dic' },
|
|
21
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DICTIONARIES } from './dictionaries'
|
|
2
|
+
|
|
3
|
+
export interface SpellChecker {
|
|
4
|
+
check(word: string): boolean
|
|
5
|
+
suggest(word: string): string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-memory dictionary: Set-based word lookup.
|
|
10
|
+
* Production: load from .dic file; tests: use fixture words.
|
|
11
|
+
*/
|
|
12
|
+
class SimpleSpellChecker implements SpellChecker {
|
|
13
|
+
private words: Set<string>
|
|
14
|
+
private customWords: Set<string>
|
|
15
|
+
|
|
16
|
+
constructor(words: string[]) {
|
|
17
|
+
this.words = new Set(words.map(w => w.toLowerCase().trim()).filter(Boolean))
|
|
18
|
+
this.customWords = new Set()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
check(word: string): boolean {
|
|
22
|
+
const lower = word.toLowerCase()
|
|
23
|
+
return this.words.has(lower) || this.customWords.has(lower)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
suggest(_word: string): string[] {
|
|
27
|
+
// Minimal: return empty (full Hunspell suggestions require .aff parsing)
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
addCustomWord(word: string) {
|
|
32
|
+
this.customWords.add(word.toLowerCase())
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cache: lang → checker
|
|
37
|
+
const checkerCache = new Map<string, SpellChecker>()
|
|
38
|
+
|
|
39
|
+
// Custom words per-project (in-memory; production uses IndexedDB)
|
|
40
|
+
const customWords = new Map<string, Set<string>>()
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load a dictionary for the given language.
|
|
44
|
+
* In production: fetches .dic file, parses word list.
|
|
45
|
+
* In test/jsdom: fetch will fail gracefully, returning an empty checker.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadDictionary(lang: string): Promise<SpellChecker> {
|
|
48
|
+
if (checkerCache.has(lang)) {
|
|
49
|
+
return checkerCache.get(lang)!
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const manifest = DICTIONARIES[lang]
|
|
53
|
+
if (!manifest) {
|
|
54
|
+
// Fallback: empty checker for unknown language codes
|
|
55
|
+
const checker = new SimpleSpellChecker([])
|
|
56
|
+
checkerCache.set(lang, checker)
|
|
57
|
+
return checker
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Fetch .dic file (word list, one per line)
|
|
62
|
+
const response = await fetch(manifest.dic)
|
|
63
|
+
if (!response.ok) throw new Error(`Failed to load dictionary: ${manifest.dic}`)
|
|
64
|
+
const text = await response.text()
|
|
65
|
+
const words = text.split('\n').filter(Boolean)
|
|
66
|
+
const checker = new SimpleSpellChecker(words)
|
|
67
|
+
|
|
68
|
+
// Apply any cached custom words
|
|
69
|
+
const custom = customWords.get(lang)
|
|
70
|
+
if (custom) {
|
|
71
|
+
for (const w of custom) checker.addCustomWord(w)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
checkerCache.set(lang, checker)
|
|
75
|
+
return checker
|
|
76
|
+
} catch {
|
|
77
|
+
// Network unavailable (expected in dev/CI without dict files)
|
|
78
|
+
const checker = new SimpleSpellChecker([])
|
|
79
|
+
checkerCache.set(lang, checker)
|
|
80
|
+
return checker
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add a word to the custom dictionary for a language.
|
|
86
|
+
* Persists in-memory; production would write to IndexedDB.
|
|
87
|
+
*/
|
|
88
|
+
export function addToCustomDictionary(lang: string, word: string) {
|
|
89
|
+
if (!customWords.has(lang)) customWords.set(lang, new Set())
|
|
90
|
+
customWords.get(lang)!.add(word.toLowerCase())
|
|
91
|
+
|
|
92
|
+
// Update cached checker if loaded
|
|
93
|
+
const checker = checkerCache.get(lang) as SimpleSpellChecker | undefined
|
|
94
|
+
if (checker) checker.addCustomWord(word)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a spell checker from a word list (for testing).
|
|
99
|
+
*/
|
|
100
|
+
export function createCheckerFromWords(words: string[]): SpellChecker {
|
|
101
|
+
return new SimpleSpellChecker(words)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clear cached checkers (for testing).
|
|
106
|
+
*/
|
|
107
|
+
export function clearCheckerCache() {
|
|
108
|
+
checkerCache.clear()
|
|
109
|
+
customWords.clear()
|
|
110
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Plugin, EditorState } from 'prosemirror-state'
|
|
2
|
+
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
3
|
+
import { loadDictionary, SpellChecker } from './hunspell'
|
|
4
|
+
|
|
5
|
+
// Module-level state
|
|
6
|
+
let currentChecker: SpellChecker | null = null
|
|
7
|
+
let currentLang = 'en'
|
|
8
|
+
|
|
9
|
+
export function setSpellcheckLanguage(lang: string) {
|
|
10
|
+
if (lang === currentLang) return
|
|
11
|
+
currentLang = lang
|
|
12
|
+
currentChecker = null // force reload
|
|
13
|
+
loadDictionary(lang).then(checker => {
|
|
14
|
+
currentChecker = checker
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tokenize text into words (splitting on whitespace and punctuation).
|
|
20
|
+
* Returns array of {word, start, end} with character offsets.
|
|
21
|
+
* Uses Unicode letter/mark categories to support Indic scripts.
|
|
22
|
+
*/
|
|
23
|
+
export function tokenizeWords(text: string): Array<{ word: string; start: number; end: number }> {
|
|
24
|
+
const results: Array<{ word: string; start: number; end: number }> = []
|
|
25
|
+
const regex = /[\p{L}\p{M}'-]+/gu
|
|
26
|
+
let match
|
|
27
|
+
while ((match = regex.exec(text)) !== null) {
|
|
28
|
+
results.push({ word: match[0], start: match.index, end: match.index + match[0].length })
|
|
29
|
+
}
|
|
30
|
+
return results
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ProseMirror plugin that underlines misspelled words via Decoration API.
|
|
35
|
+
* Recomputes decorations on every doc change.
|
|
36
|
+
*/
|
|
37
|
+
export function createSpellcheckPlugin() {
|
|
38
|
+
return new Plugin({
|
|
39
|
+
state: {
|
|
40
|
+
init() {
|
|
41
|
+
return DecorationSet.empty
|
|
42
|
+
},
|
|
43
|
+
apply(tr, set, _oldState, newState) {
|
|
44
|
+
if (!tr.docChanged) return set
|
|
45
|
+
if (!currentChecker) return DecorationSet.empty
|
|
46
|
+
|
|
47
|
+
return computeDecorations(newState, currentChecker)
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
props: {
|
|
51
|
+
decorations(state) {
|
|
52
|
+
return this.getState(state)
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function computeDecorations(state: EditorState, checker: SpellChecker): DecorationSet {
|
|
59
|
+
const decorations: Decoration[] = []
|
|
60
|
+
|
|
61
|
+
state.doc.forEach((node, offset) => {
|
|
62
|
+
if (!node.isBlock || !node.textContent) return
|
|
63
|
+
|
|
64
|
+
const text = node.textContent
|
|
65
|
+
const tokens = tokenizeWords(text)
|
|
66
|
+
|
|
67
|
+
for (const token of tokens) {
|
|
68
|
+
if (!checker.check(token.word)) {
|
|
69
|
+
// Misspelled: add wavy underline decoration
|
|
70
|
+
const from = offset + 1 + token.start
|
|
71
|
+
const to = offset + 1 + token.end
|
|
72
|
+
decorations.push(
|
|
73
|
+
Decoration.inline(from, to, {
|
|
74
|
+
class: 'cs-spell-error',
|
|
75
|
+
title: `"${token.word}" may be misspelled`,
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return DecorationSet.create(state.doc, decorations)
|
|
83
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useDocument } from '../context/DocumentContext'
|
|
3
|
+
import { useTranslation } from '../i18n/useTranslation'
|
|
4
|
+
import { allStyles } from './registry'
|
|
5
|
+
import { isBuiltinStyleId } from './builtin-styles'
|
|
6
|
+
import { validateStyleJson } from './validate'
|
|
7
|
+
import { useStorageAdapter } from '../extensions/storage-adapter'
|
|
8
|
+
import type { ScreenplayStyleDefinition } from './types'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
onClose: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StyleDialog({ onClose }: Props) {
|
|
15
|
+
const { resolvedStyle, customStyles, setDocumentStyle, reloadCustomStyles } = useDocument()
|
|
16
|
+
const { t } = useTranslation()
|
|
17
|
+
const storageAdapter = useStorageAdapter()
|
|
18
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
dialogRef.current?.focus()
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
26
|
+
if (e.key === 'Escape') onClose()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function handleImport() {
|
|
30
|
+
setError(null)
|
|
31
|
+
const picked = await storageAdapter.openStyleJson()
|
|
32
|
+
if (!picked) return
|
|
33
|
+
|
|
34
|
+
let data: unknown
|
|
35
|
+
try {
|
|
36
|
+
data = JSON.parse(picked.text)
|
|
37
|
+
} catch {
|
|
38
|
+
setError(`${t('style.importInvalid')}: not valid JSON`)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = validateStyleJson(data)
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
setError(`${t('style.importInvalid')}: ${result.errors.join('; ')}`)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isBuiltinStyleId(result.style.id)) {
|
|
49
|
+
setError(t('style.importIdReserved'))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (customStyles.some(s => s.id === result.style.id) && !confirm(t('style.importOverwriteConfirm'))) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await storageAdapter.saveStyle(result.style)
|
|
57
|
+
await reloadCustomStyles()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleDelete(id: string) {
|
|
61
|
+
if (!confirm(t('style.deleteConfirm'))) return
|
|
62
|
+
await storageAdapter.deleteStyle(id)
|
|
63
|
+
await reloadCustomStyles()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderRow(style: ScreenplayStyleDefinition, deletable: boolean) {
|
|
67
|
+
const active = style.id === resolvedStyle.id
|
|
68
|
+
const swatchColor = style.elements.sceneHeading.color ? `#${style.elements.sceneHeading.color}` : '#999'
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
key={style.id}
|
|
72
|
+
className="cs-settings-row"
|
|
73
|
+
style={{ alignItems: 'flex-start', cursor: 'pointer', padding: '6px 0' }}
|
|
74
|
+
onClick={() => setDocumentStyle(style.id)}
|
|
75
|
+
>
|
|
76
|
+
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
|
|
77
|
+
<span style={{ display: 'inline-block', width: '10px', height: '10px', borderRadius: '50%', background: swatchColor, flexShrink: 0, marginTop: '4px' }} />
|
|
78
|
+
<div>
|
|
79
|
+
<div className="cs-settings-label" style={{ fontWeight: active ? 600 : 400 }}>
|
|
80
|
+
{style.name}
|
|
81
|
+
{active && <span style={{ color: '#1976d2', fontWeight: 600 }}> — {t('style.current')}</span>}
|
|
82
|
+
</div>
|
|
83
|
+
{style.description && (
|
|
84
|
+
<div style={{ fontSize: '11px', color: '#888', marginTop: '2px' }}>{style.description}</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
{deletable && (
|
|
89
|
+
<button
|
|
90
|
+
className="cs-panel-close"
|
|
91
|
+
aria-label={t('style.delete')}
|
|
92
|
+
onClick={e => { e.stopPropagation(); void handleDelete(style.id) }}
|
|
93
|
+
>
|
|
94
|
+
×
|
|
95
|
+
</button>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const builtins = allStyles([]).filter(s => isBuiltinStyleId(s.id))
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="cs-dialog-overlay" onClick={onClose}>
|
|
105
|
+
<div
|
|
106
|
+
ref={dialogRef}
|
|
107
|
+
className="cs-dialog cs-settings-dialog"
|
|
108
|
+
role="dialog"
|
|
109
|
+
aria-modal="true"
|
|
110
|
+
aria-label={t('style.title')}
|
|
111
|
+
tabIndex={-1}
|
|
112
|
+
onClick={e => e.stopPropagation()}
|
|
113
|
+
onKeyDown={handleKeyDown}
|
|
114
|
+
>
|
|
115
|
+
<div className="cs-dialog-header">
|
|
116
|
+
<span className="cs-dialog-title">{t('style.title')}</span>
|
|
117
|
+
<button className="cs-fr-close" aria-label={t('style.close')} onClick={onClose}>×</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="cs-settings-body">
|
|
121
|
+
{error && (
|
|
122
|
+
<div className="cs-export-warnings" style={{ marginTop: 0 }}>
|
|
123
|
+
<div className="cs-export-warn-item">{error}</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<div className="cs-settings-section-label">{t('style.builtIn')}</div>
|
|
128
|
+
{builtins.map(s => renderRow(s, false))}
|
|
129
|
+
|
|
130
|
+
{customStyles.length > 0 && (
|
|
131
|
+
<>
|
|
132
|
+
<div className="cs-settings-section-label" style={{ marginTop: '8px' }}>{t('style.custom')}</div>
|
|
133
|
+
{customStyles.map(s => renderRow(s, true))}
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
<button
|
|
138
|
+
className="cs-export-btn-cancel"
|
|
139
|
+
style={{ marginTop: '8px', alignSelf: 'flex-start' }}
|
|
140
|
+
onClick={() => { void handleImport() }}
|
|
141
|
+
>
|
|
142
|
+
{t('style.import')}
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ScreenplayStyleDefinition } from './types.js'
|
|
2
|
+
import { DEFAULT_STYLE_ID } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The app's long-standing default look, extracted from the values that were
|
|
6
|
+
* hardcoded across screenplay.css / docx-exporter.ts / workflow-reports.ts
|
|
7
|
+
* before those three were unified onto this shared definition. Where the
|
|
8
|
+
* three disagreed (they had drifted — see the multi-style implementation
|
|
9
|
+
* plan), the live editor's CSS is treated as canonical for font-size/color/
|
|
10
|
+
* margin-%, so the editor's appearance is unchanged; DOCX/PDF absorb the
|
|
11
|
+
* (small, confirmed) corrections needed to match it.
|
|
12
|
+
*/
|
|
13
|
+
export const CLASSIC_STYLE: ScreenplayStyleDefinition = {
|
|
14
|
+
id: 'classic',
|
|
15
|
+
name: 'Classic',
|
|
16
|
+
description: 'The original Sutrata look — teal scene headings, rust character cues.',
|
|
17
|
+
fontFamily: 'Noto Sans',
|
|
18
|
+
page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
|
|
19
|
+
elements: {
|
|
20
|
+
sceneHeading: { fontSizePt: 13, bold: true, color: '0A5C6A', marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 13, spaceAfterPt: 7.2 },
|
|
21
|
+
action: { fontSizePt: 12, marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 6, spaceAfterPt: 7.2 },
|
|
22
|
+
character: { fontSizePt: 13, bold: true, color: '9C4221', marginLeftPct: 17, spaceBeforePt: 6.5, spaceAfterPt: 0 },
|
|
23
|
+
parenthetical: { fontSizePt: 13, marginLeftPct: 17, marginRightPct: 20, spaceAfterPt: 0 },
|
|
24
|
+
dialogue: { fontSizePt: 12, marginLeftPct: 17, marginRightPct: 17, spaceAfterPt: 7.2 },
|
|
25
|
+
transition: { fontSizePt: 13, align: 'right', marginRightPct: 8, spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
|
|
26
|
+
centered: { fontSizePt: 13, align: 'center', spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
|
|
27
|
+
lyrics: { fontSizePt: 13, italic: true, marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
|
|
28
|
+
section: { fontSizePt: 14, bold: true, color: '6F6F6F', marginLeftPct: 10, spaceBeforePt: 21, spaceAfterPt: 7.2 },
|
|
29
|
+
note: { fontSizePt: 10, color: '7A6000', marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
|
|
30
|
+
comment: { fontSizePt: 10, color: 'AAAAAA', italic: true, marginLeftPct: 10, spaceAfterPt: 3.6 },
|
|
31
|
+
title: { fontSizePt: 24, bold: true, color: '0A5C6A', align: 'center', spaceAfterPt: 14.4 },
|
|
32
|
+
author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
|
|
33
|
+
logline: { fontSizePt: 12, italic: true, color: '555555', align: 'center', spaceAfterPt: 7.2 },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A second built-in style with a genuinely different look — monospace,
|
|
39
|
+
* monochrome (no `color` on any element, exercising the fallback-to-black
|
|
40
|
+
* path), industry-standard flush-left margins — to prove the architecture
|
|
41
|
+
* isn't secretly hardcoded to CLASSIC_STYLE's shape.
|
|
42
|
+
*/
|
|
43
|
+
export const INDUSTRY_COURIER_STYLE: ScreenplayStyleDefinition = {
|
|
44
|
+
id: 'industry-courier',
|
|
45
|
+
name: 'Industry Courier',
|
|
46
|
+
description: 'Monospace, monochrome, flush-left margins — the classic spec-script look.',
|
|
47
|
+
fontFamily: 'Courier Prime',
|
|
48
|
+
page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1.5, marginRightIn: 1 },
|
|
49
|
+
elements: {
|
|
50
|
+
sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
|
|
51
|
+
action: { fontSizePt: 12, spaceBeforePt: 12, spaceAfterPt: 12 },
|
|
52
|
+
character: { fontSizePt: 12, bold: true, marginLeftPct: 30, spaceBeforePt: 12, spaceAfterPt: 0 },
|
|
53
|
+
parenthetical: { fontSizePt: 12, marginLeftPct: 26, marginRightPct: 15, spaceAfterPt: 0 },
|
|
54
|
+
dialogue: { fontSizePt: 12, marginLeftPct: 20, marginRightPct: 15, spaceAfterPt: 12 },
|
|
55
|
+
transition: { fontSizePt: 12, align: 'right', spaceBeforePt: 12, spaceAfterPt: 12 },
|
|
56
|
+
centered: { fontSizePt: 12, align: 'center', spaceBeforePt: 12, spaceAfterPt: 12 },
|
|
57
|
+
lyrics: { fontSizePt: 12, italic: true, spaceAfterPt: 6 },
|
|
58
|
+
section: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
|
|
59
|
+
note: { fontSizePt: 10, spaceAfterPt: 6 },
|
|
60
|
+
comment: { fontSizePt: 10, italic: true, spaceAfterPt: 6 },
|
|
61
|
+
title: { fontSizePt: 24, bold: true, align: 'center', spaceAfterPt: 14.4 },
|
|
62
|
+
author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
|
|
63
|
+
logline: { fontSizePt: 12, italic: true, align: 'center', spaceAfterPt: 7.2 },
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Every Indic script Sutrata already self-hosts Noto Sans for, pointed at its Noto
|
|
68
|
+
* Serif equivalent instead (all bundled in shell/fonts/fonts.css the same way as Noto
|
|
69
|
+
* Sans — see fonts-serif-block.css generation notes in that file's history). Reused by
|
|
70
|
+
* TRADITIONAL_STYLE's `complexScriptFontOverrides`: Noto Serif was chosen over the
|
|
71
|
+
* originally-considered Vijaya specifically because it already covers every script
|
|
72
|
+
* Noto Sans does and carries an OFL license, so — unlike Vijaya — it can actually ship
|
|
73
|
+
* in the repo instead of depending on the reader's OS/Word already having it installed. */
|
|
74
|
+
const NOTO_SERIF_COMPLEX_SCRIPT_FONTS: Record<string, string> = {
|
|
75
|
+
hi: 'Noto Serif Devanagari', ta: 'Noto Serif Tamil', te: 'Noto Serif Telugu',
|
|
76
|
+
kn: 'Noto Serif Kannada', ml: 'Noto Serif Malayalam', bn: 'Noto Serif Bengali',
|
|
77
|
+
gu: 'Noto Serif Gujarati', pa: 'Noto Serif Gurmukhi', or: 'Noto Serif Oriya',
|
|
78
|
+
si: 'Noto Serif Sinhala',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Modeled on a real Tamil feature screenplay PDF (betterfountain/Fountain export):
|
|
83
|
+
* bold scene headings, underlined (not bold) character cues, fully monochrome,
|
|
84
|
+
* flush-left action/scene headings, A4 with 1in margins all around.
|
|
85
|
+
* Measurements (font sizes, indents as % of content width) were taken
|
|
86
|
+
* directly from the PDF's text positions; elements the reference document
|
|
87
|
+
* doesn't print at all (section/note/comment — Fountain strips these from
|
|
88
|
+
* output) are extrapolated to match its scale rather than measured.
|
|
89
|
+
*
|
|
90
|
+
* The reference PDF actually used Vijaya, a proprietary Microsoft font — not
|
|
91
|
+
* bundled here (unlike Noto, it can't legally ship in the repo, and depending
|
|
92
|
+
* on the reader's OS/Word already having it installed is a worse default than
|
|
93
|
+
* a font that just works). Noto Serif is the deliberate second choice: not a
|
|
94
|
+
* perfect visual match, but it already has a variant for every script Noto
|
|
95
|
+
* Sans does (so this style isn't just "Tamil-only" the way a Vijaya-based one
|
|
96
|
+
* would be) and ships under the OFL, so it's self-hosted for real instead of
|
|
97
|
+
* hoped-for. Tamil specifically renders in Noto Serif Tamil's *italic* face by
|
|
98
|
+
* default (`italicScripts`, forced even on bold scene headings/sections) since
|
|
99
|
+
* that slant reads closer to Vijaya than the upright serif does — every other
|
|
100
|
+
* script stays upright serif, `italicScripts` only lists 'ta'.
|
|
101
|
+
*/
|
|
102
|
+
export const TRADITIONAL_STYLE: ScreenplayStyleDefinition = {
|
|
103
|
+
id: 'traditional',
|
|
104
|
+
name: 'Traditional',
|
|
105
|
+
description: 'Bold serif scene headings, underlined cues, italic Tamil — modeled on a real Tamil shooting script.',
|
|
106
|
+
fontFamily: 'Noto Serif',
|
|
107
|
+
complexScriptFontOverrides: NOTO_SERIF_COMPLEX_SCRIPT_FONTS,
|
|
108
|
+
italicScripts: ['ta'],
|
|
109
|
+
// The reference PDF's cover: title / "by <author>" / logline flush top-left,
|
|
110
|
+
// date and contact down in the bottom-right corner.
|
|
111
|
+
coverLayout: 'corners',
|
|
112
|
+
page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
|
|
113
|
+
elements: {
|
|
114
|
+
sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 17, spaceAfterPt: 9 },
|
|
115
|
+
action: { fontSizePt: 10, spaceBeforePt: 8, spaceAfterPt: 9 },
|
|
116
|
+
character: { fontSizePt: 10, underline: true, marginLeftPct: 32, spaceBeforePt: 8, spaceAfterPt: 0 },
|
|
117
|
+
parenthetical: { fontSizePt: 10, marginLeftPct: 32, marginRightPct: 20, spaceAfterPt: 0 },
|
|
118
|
+
dialogue: { fontSizePt: 10, marginLeftPct: 24, marginRightPct: 17, spaceAfterPt: 9 },
|
|
119
|
+
transition: { fontSizePt: 10, align: 'right', spaceBeforePt: 9, spaceAfterPt: 9 },
|
|
120
|
+
centered: { fontSizePt: 10, align: 'center', italic: true, spaceBeforePt: 9, spaceAfterPt: 9 },
|
|
121
|
+
lyrics: { fontSizePt: 10, italic: true, marginLeftPct: 6, spaceAfterPt: 5 },
|
|
122
|
+
section: { fontSizePt: 12, bold: true, spaceBeforePt: 27, spaceAfterPt: 9 },
|
|
123
|
+
note: { fontSizePt: 10, color: '666666', spaceAfterPt: 5 },
|
|
124
|
+
comment: { fontSizePt: 13, italic: true, spaceAfterPt: 5 },
|
|
125
|
+
title: { fontSizePt: 32, bold: true, align: 'left', spaceAfterPt: 16 },
|
|
126
|
+
author: { fontSizePt: 16, align: 'left', spaceAfterPt: 8 },
|
|
127
|
+
logline: { fontSizePt: 10, align: 'left', spaceAfterPt: 8 },
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const BUILTIN_STYLES: ScreenplayStyleDefinition[] = [CLASSIC_STYLE, INDUSTRY_COURIER_STYLE, TRADITIONAL_STYLE]
|
|
132
|
+
|
|
133
|
+
export function getBuiltinStyle(id: string): ScreenplayStyleDefinition | undefined {
|
|
134
|
+
return BUILTIN_STYLES.find(s => s.id === id)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isBuiltinStyleId(id: string): boolean {
|
|
138
|
+
return id === DEFAULT_STYLE_ID || BUILTIN_STYLES.some(s => s.id === id)
|
|
139
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BODY_ELEMENT_KEYS, COMPLEX_SCRIPT_CODES } from './types.js'
|
|
2
|
+
import type { ScreenplayStyleDefinition, ElementStyle } from './types.js'
|
|
3
|
+
|
|
4
|
+
function kebab(key: string): string {
|
|
5
|
+
return key.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a style into the CSS custom properties screenplay.css reads via
|
|
10
|
+
* `var(--cs-<element>-<prop>, <classic-fallback>)`. Applied to `.cs-editor-mount`
|
|
11
|
+
* so the live editor picks up whichever style the current document (or an
|
|
12
|
+
* explicit override) resolves to — works identically for built-in and
|
|
13
|
+
* user-imported styles since both are plain `ScreenplayStyleDefinition` objects
|
|
14
|
+
* by the time they reach here.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately does NOT emit anything for `spaceAfterPt` — the live editor only
|
|
17
|
+
* ever expresses "space before" via margin-top; adjacent elements form their gap
|
|
18
|
+
* from the *following* element's margin-top, not a margin-bottom on the
|
|
19
|
+
* preceding one (see types.ts). `spaceAfterPt` is DOCX/print-only.
|
|
20
|
+
*
|
|
21
|
+
* A `null` entry means "no override" and must be cleared with `removeProperty`,
|
|
22
|
+
* never `setProperty(k, 'inherit')`: for a CUSTOM property, the literal value
|
|
23
|
+
* `inherit` is a CSS-wide keyword that makes the property itself inherit from
|
|
24
|
+
* *its own* parent (here, .cs-editor-mount's parent, which never defines it) —
|
|
25
|
+
* it does not store the string "inherit" for descendants to read. That breaks
|
|
26
|
+
* inheritance for every element below it, not just the unset one.
|
|
27
|
+
*/
|
|
28
|
+
export function styleToCssVars(style: ScreenplayStyleDefinition): Record<string, string | null> {
|
|
29
|
+
const vars: Record<string, string | null> = {
|
|
30
|
+
'--cs-body-font-family': style.fontFamily,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const key of BODY_ELEMENT_KEYS) {
|
|
34
|
+
const el: ElementStyle = style.elements[key]
|
|
35
|
+
const prefix = `--cs-${kebab(key)}`
|
|
36
|
+
vars[`${prefix}-font-size`] = `${el.fontSizePt}pt`
|
|
37
|
+
vars[`${prefix}-font-weight`] = el.bold ? 'bold' : 'normal'
|
|
38
|
+
vars[`${prefix}-font-style`] = el.italic ? 'italic' : 'normal'
|
|
39
|
+
vars[`${prefix}-text-decoration`] = el.underline ? 'underline' : 'none'
|
|
40
|
+
vars[`${prefix}-color`] = el.color ? `#${el.color}` : null
|
|
41
|
+
vars[`${prefix}-margin-left`] = `${el.marginLeftPct ?? 0}%`
|
|
42
|
+
vars[`${prefix}-margin-right`] = `${el.marginRightPct ?? 0}%`
|
|
43
|
+
vars[`${prefix}-text-align`] = el.align ?? 'left'
|
|
44
|
+
vars[`${prefix}-margin-top`] = `${el.spaceBeforePt ?? 0}pt`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Per-script font override: screenplay.css's .cs-lang-<code> rules prefer this over
|
|
48
|
+
// the generic --cs-body-font-family fallback. Emit `null` for every code the style
|
|
49
|
+
// doesn't override so a previously-applied style's choice can't linger (see
|
|
50
|
+
// applyStyleVars). italicScripts is NOT emitted as a var — see applyStyleVars.
|
|
51
|
+
for (const code of COMPLEX_SCRIPT_CODES) {
|
|
52
|
+
vars[`--cs-lang-${code}-font-family`] = style.complexScriptFontOverrides?.[code] ?? null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return vars
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Applies every var from `styleToCssVars` onto an element (the `.cs-editor-mount` ref),
|
|
59
|
+
* clearing any that resolved to `null` so a previous style's value can't linger. Also
|
|
60
|
+
* toggles a `.cs-force-italic-<code>` class per `style.italicScripts` — a class rather
|
|
61
|
+
* than a CSS var because `.cs-lang-<code>` rules already exist for lyrics/comments etc.
|
|
62
|
+
* that are legitimately italic on their own; a `font-style` var on `.cs-lang-<code>`
|
|
63
|
+
* itself would unconditionally win the cascade and stomp that even for styles that
|
|
64
|
+
* don't ask for forced italics (see types.ts's `italicScripts` doc for the full
|
|
65
|
+
* reasoning). The class only ever *adds* specificity to force italic on; it's absent
|
|
66
|
+
* entirely for scripts/styles that don't want it, so nothing is stomped either way. */
|
|
67
|
+
export function applyStyleVars(el: HTMLElement, style: ScreenplayStyleDefinition): void {
|
|
68
|
+
const vars = styleToCssVars(style)
|
|
69
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
70
|
+
if (v === null) el.style.removeProperty(k)
|
|
71
|
+
else el.style.setProperty(k, v)
|
|
72
|
+
}
|
|
73
|
+
for (const code of COMPLEX_SCRIPT_CODES) {
|
|
74
|
+
el.classList.toggle(`cs-force-italic-${code}`, style.italicScripts?.includes(code) ?? false)
|
|
75
|
+
}
|
|
76
|
+
}
|