@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,75 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
3
|
+
import { loadDictionary } from './hunspell';
|
|
4
|
+
// Module-level state
|
|
5
|
+
let currentChecker = null;
|
|
6
|
+
let currentLang = 'en';
|
|
7
|
+
export function setSpellcheckLanguage(lang) {
|
|
8
|
+
if (lang === currentLang)
|
|
9
|
+
return;
|
|
10
|
+
currentLang = lang;
|
|
11
|
+
currentChecker = null; // force reload
|
|
12
|
+
loadDictionary(lang).then(checker => {
|
|
13
|
+
currentChecker = checker;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Tokenize text into words (splitting on whitespace and punctuation).
|
|
18
|
+
* Returns array of {word, start, end} with character offsets.
|
|
19
|
+
* Uses Unicode letter/mark categories to support Indic scripts.
|
|
20
|
+
*/
|
|
21
|
+
export function tokenizeWords(text) {
|
|
22
|
+
const results = [];
|
|
23
|
+
const regex = /[\p{L}\p{M}'-]+/gu;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = regex.exec(text)) !== null) {
|
|
26
|
+
results.push({ word: match[0], start: match.index, end: match.index + match[0].length });
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* ProseMirror plugin that underlines misspelled words via Decoration API.
|
|
32
|
+
* Recomputes decorations on every doc change.
|
|
33
|
+
*/
|
|
34
|
+
export function createSpellcheckPlugin() {
|
|
35
|
+
return new Plugin({
|
|
36
|
+
state: {
|
|
37
|
+
init() {
|
|
38
|
+
return DecorationSet.empty;
|
|
39
|
+
},
|
|
40
|
+
apply(tr, set, _oldState, newState) {
|
|
41
|
+
if (!tr.docChanged)
|
|
42
|
+
return set;
|
|
43
|
+
if (!currentChecker)
|
|
44
|
+
return DecorationSet.empty;
|
|
45
|
+
return computeDecorations(newState, currentChecker);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
props: {
|
|
49
|
+
decorations(state) {
|
|
50
|
+
return this.getState(state);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function computeDecorations(state, checker) {
|
|
56
|
+
const decorations = [];
|
|
57
|
+
state.doc.forEach((node, offset) => {
|
|
58
|
+
if (!node.isBlock || !node.textContent)
|
|
59
|
+
return;
|
|
60
|
+
const text = node.textContent;
|
|
61
|
+
const tokens = tokenizeWords(text);
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
if (!checker.check(token.word)) {
|
|
64
|
+
// Misspelled: add wavy underline decoration
|
|
65
|
+
const from = offset + 1 + token.start;
|
|
66
|
+
const to = offset + 1 + token.end;
|
|
67
|
+
decorations.push(Decoration.inline(from, to, {
|
|
68
|
+
class: 'cs-spell-error',
|
|
69
|
+
title: `"${token.word}" may be misspelled`,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return DecorationSet.create(state.doc, decorations);
|
|
75
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useDocument } from '../context/DocumentContext';
|
|
4
|
+
import { useTranslation } from '../i18n/useTranslation';
|
|
5
|
+
import { allStyles } from './registry';
|
|
6
|
+
import { isBuiltinStyleId } from './builtin-styles';
|
|
7
|
+
import { validateStyleJson } from './validate';
|
|
8
|
+
import { useStorageAdapter } from '../extensions/storage-adapter';
|
|
9
|
+
export function StyleDialog({ onClose }) {
|
|
10
|
+
const { resolvedStyle, customStyles, setDocumentStyle, reloadCustomStyles } = useDocument();
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const storageAdapter = useStorageAdapter();
|
|
13
|
+
const dialogRef = useRef(null);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
dialogRef.current?.focus();
|
|
17
|
+
}, []);
|
|
18
|
+
const handleKeyDown = (e) => {
|
|
19
|
+
if (e.key === 'Escape')
|
|
20
|
+
onClose();
|
|
21
|
+
};
|
|
22
|
+
async function handleImport() {
|
|
23
|
+
setError(null);
|
|
24
|
+
const picked = await storageAdapter.openStyleJson();
|
|
25
|
+
if (!picked)
|
|
26
|
+
return;
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
data = JSON.parse(picked.text);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
setError(`${t('style.importInvalid')}: not valid JSON`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const result = validateStyleJson(data);
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
setError(`${t('style.importInvalid')}: ${result.errors.join('; ')}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isBuiltinStyleId(result.style.id)) {
|
|
41
|
+
setError(t('style.importIdReserved'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (customStyles.some(s => s.id === result.style.id) && !confirm(t('style.importOverwriteConfirm'))) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await storageAdapter.saveStyle(result.style);
|
|
48
|
+
await reloadCustomStyles();
|
|
49
|
+
}
|
|
50
|
+
async function handleDelete(id) {
|
|
51
|
+
if (!confirm(t('style.deleteConfirm')))
|
|
52
|
+
return;
|
|
53
|
+
await storageAdapter.deleteStyle(id);
|
|
54
|
+
await reloadCustomStyles();
|
|
55
|
+
}
|
|
56
|
+
function renderRow(style, deletable) {
|
|
57
|
+
const active = style.id === resolvedStyle.id;
|
|
58
|
+
const swatchColor = style.elements.sceneHeading.color ? `#${style.elements.sceneHeading.color}` : '#999';
|
|
59
|
+
return (_jsxs("div", { className: "cs-settings-row", style: { alignItems: 'flex-start', cursor: 'pointer', padding: '6px 0' }, onClick: () => setDocumentStyle(style.id), children: [_jsxs("div", { style: { display: 'flex', gap: '10px', alignItems: 'flex-start' }, children: [_jsx("span", { style: { display: 'inline-block', width: '10px', height: '10px', borderRadius: '50%', background: swatchColor, flexShrink: 0, marginTop: '4px' } }), _jsxs("div", { children: [_jsxs("div", { className: "cs-settings-label", style: { fontWeight: active ? 600 : 400 }, children: [style.name, active && _jsxs("span", { style: { color: '#1976d2', fontWeight: 600 }, children: [" \u2014 ", t('style.current')] })] }), style.description && (_jsx("div", { style: { fontSize: '11px', color: '#888', marginTop: '2px' }, children: style.description }))] })] }), deletable && (_jsx("button", { className: "cs-panel-close", "aria-label": t('style.delete'), onClick: e => { e.stopPropagation(); void handleDelete(style.id); }, children: "\u00D7" }))] }, style.id));
|
|
60
|
+
}
|
|
61
|
+
const builtins = allStyles([]).filter(s => isBuiltinStyleId(s.id));
|
|
62
|
+
return (_jsx("div", { className: "cs-dialog-overlay", onClick: onClose, children: _jsxs("div", { ref: dialogRef, className: "cs-dialog cs-settings-dialog", role: "dialog", "aria-modal": "true", "aria-label": t('style.title'), tabIndex: -1, onClick: e => e.stopPropagation(), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsx("span", { className: "cs-dialog-title", children: t('style.title') }), _jsx("button", { className: "cs-fr-close", "aria-label": t('style.close'), onClick: onClose, children: "\u00D7" })] }), _jsxs("div", { className: "cs-settings-body", children: [error && (_jsx("div", { className: "cs-export-warnings", style: { marginTop: 0 }, children: _jsx("div", { className: "cs-export-warn-item", children: error }) })), _jsx("div", { className: "cs-settings-section-label", children: t('style.builtIn') }), builtins.map(s => renderRow(s, false)), customStyles.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-settings-section-label", style: { marginTop: '8px' }, children: t('style.custom') }), customStyles.map(s => renderRow(s, true))] })), _jsx("button", { className: "cs-export-btn-cancel", style: { marginTop: '8px', alignSelf: 'flex-start' }, onClick: () => { void handleImport(); }, children: t('style.import') })] })] }) }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ScreenplayStyleDefinition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The app's long-standing default look, extracted from the values that were
|
|
4
|
+
* hardcoded across screenplay.css / docx-exporter.ts / workflow-reports.ts
|
|
5
|
+
* before those three were unified onto this shared definition. Where the
|
|
6
|
+
* three disagreed (they had drifted — see the multi-style implementation
|
|
7
|
+
* plan), the live editor's CSS is treated as canonical for font-size/color/
|
|
8
|
+
* margin-%, so the editor's appearance is unchanged; DOCX/PDF absorb the
|
|
9
|
+
* (small, confirmed) corrections needed to match it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CLASSIC_STYLE: ScreenplayStyleDefinition;
|
|
12
|
+
/**
|
|
13
|
+
* A second built-in style with a genuinely different look — monospace,
|
|
14
|
+
* monochrome (no `color` on any element, exercising the fallback-to-black
|
|
15
|
+
* path), industry-standard flush-left margins — to prove the architecture
|
|
16
|
+
* isn't secretly hardcoded to CLASSIC_STYLE's shape.
|
|
17
|
+
*/
|
|
18
|
+
export declare const INDUSTRY_COURIER_STYLE: ScreenplayStyleDefinition;
|
|
19
|
+
/**
|
|
20
|
+
* Modeled on a real Tamil feature screenplay PDF (betterfountain/Fountain export):
|
|
21
|
+
* bold scene headings, underlined (not bold) character cues, fully monochrome,
|
|
22
|
+
* flush-left action/scene headings, A4 with 1in margins all around.
|
|
23
|
+
* Measurements (font sizes, indents as % of content width) were taken
|
|
24
|
+
* directly from the PDF's text positions; elements the reference document
|
|
25
|
+
* doesn't print at all (section/note/comment — Fountain strips these from
|
|
26
|
+
* output) are extrapolated to match its scale rather than measured.
|
|
27
|
+
*
|
|
28
|
+
* The reference PDF actually used Vijaya, a proprietary Microsoft font — not
|
|
29
|
+
* bundled here (unlike Noto, it can't legally ship in the repo, and depending
|
|
30
|
+
* on the reader's OS/Word already having it installed is a worse default than
|
|
31
|
+
* a font that just works). Noto Serif is the deliberate second choice: not a
|
|
32
|
+
* perfect visual match, but it already has a variant for every script Noto
|
|
33
|
+
* Sans does (so this style isn't just "Tamil-only" the way a Vijaya-based one
|
|
34
|
+
* would be) and ships under the OFL, so it's self-hosted for real instead of
|
|
35
|
+
* hoped-for. Tamil specifically renders in Noto Serif Tamil's *italic* face by
|
|
36
|
+
* default (`italicScripts`, forced even on bold scene headings/sections) since
|
|
37
|
+
* that slant reads closer to Vijaya than the upright serif does — every other
|
|
38
|
+
* script stays upright serif, `italicScripts` only lists 'ta'.
|
|
39
|
+
*/
|
|
40
|
+
export declare const TRADITIONAL_STYLE: ScreenplayStyleDefinition;
|
|
41
|
+
export declare const BUILTIN_STYLES: ScreenplayStyleDefinition[];
|
|
42
|
+
export declare function getBuiltinStyle(id: string): ScreenplayStyleDefinition | undefined;
|
|
43
|
+
export declare function isBuiltinStyleId(id: string): boolean;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { DEFAULT_STYLE_ID } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The app's long-standing default look, extracted from the values that were
|
|
4
|
+
* hardcoded across screenplay.css / docx-exporter.ts / workflow-reports.ts
|
|
5
|
+
* before those three were unified onto this shared definition. Where the
|
|
6
|
+
* three disagreed (they had drifted — see the multi-style implementation
|
|
7
|
+
* plan), the live editor's CSS is treated as canonical for font-size/color/
|
|
8
|
+
* margin-%, so the editor's appearance is unchanged; DOCX/PDF absorb the
|
|
9
|
+
* (small, confirmed) corrections needed to match it.
|
|
10
|
+
*/
|
|
11
|
+
export const CLASSIC_STYLE = {
|
|
12
|
+
id: 'classic',
|
|
13
|
+
name: 'Classic',
|
|
14
|
+
description: 'The original Sutrata look — teal scene headings, rust character cues.',
|
|
15
|
+
fontFamily: 'Noto Sans',
|
|
16
|
+
page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
|
|
17
|
+
elements: {
|
|
18
|
+
sceneHeading: { fontSizePt: 13, bold: true, color: '0A5C6A', marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 13, spaceAfterPt: 7.2 },
|
|
19
|
+
action: { fontSizePt: 12, marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 6, spaceAfterPt: 7.2 },
|
|
20
|
+
character: { fontSizePt: 13, bold: true, color: '9C4221', marginLeftPct: 17, spaceBeforePt: 6.5, spaceAfterPt: 0 },
|
|
21
|
+
parenthetical: { fontSizePt: 13, marginLeftPct: 17, marginRightPct: 20, spaceAfterPt: 0 },
|
|
22
|
+
dialogue: { fontSizePt: 12, marginLeftPct: 17, marginRightPct: 17, spaceAfterPt: 7.2 },
|
|
23
|
+
transition: { fontSizePt: 13, align: 'right', marginRightPct: 8, spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
|
|
24
|
+
centered: { fontSizePt: 13, align: 'center', spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
|
|
25
|
+
lyrics: { fontSizePt: 13, italic: true, marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
|
|
26
|
+
section: { fontSizePt: 14, bold: true, color: '6F6F6F', marginLeftPct: 10, spaceBeforePt: 21, spaceAfterPt: 7.2 },
|
|
27
|
+
note: { fontSizePt: 10, color: '7A6000', marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
|
|
28
|
+
comment: { fontSizePt: 10, color: 'AAAAAA', italic: true, marginLeftPct: 10, spaceAfterPt: 3.6 },
|
|
29
|
+
title: { fontSizePt: 24, bold: true, color: '0A5C6A', align: 'center', spaceAfterPt: 14.4 },
|
|
30
|
+
author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
|
|
31
|
+
logline: { fontSizePt: 12, italic: true, color: '555555', align: 'center', spaceAfterPt: 7.2 },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A second built-in style with a genuinely different look — monospace,
|
|
36
|
+
* monochrome (no `color` on any element, exercising the fallback-to-black
|
|
37
|
+
* path), industry-standard flush-left margins — to prove the architecture
|
|
38
|
+
* isn't secretly hardcoded to CLASSIC_STYLE's shape.
|
|
39
|
+
*/
|
|
40
|
+
export const INDUSTRY_COURIER_STYLE = {
|
|
41
|
+
id: 'industry-courier',
|
|
42
|
+
name: 'Industry Courier',
|
|
43
|
+
description: 'Monospace, monochrome, flush-left margins — the classic spec-script look.',
|
|
44
|
+
fontFamily: 'Courier Prime',
|
|
45
|
+
page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1.5, marginRightIn: 1 },
|
|
46
|
+
elements: {
|
|
47
|
+
sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
|
|
48
|
+
action: { fontSizePt: 12, spaceBeforePt: 12, spaceAfterPt: 12 },
|
|
49
|
+
character: { fontSizePt: 12, bold: true, marginLeftPct: 30, spaceBeforePt: 12, spaceAfterPt: 0 },
|
|
50
|
+
parenthetical: { fontSizePt: 12, marginLeftPct: 26, marginRightPct: 15, spaceAfterPt: 0 },
|
|
51
|
+
dialogue: { fontSizePt: 12, marginLeftPct: 20, marginRightPct: 15, spaceAfterPt: 12 },
|
|
52
|
+
transition: { fontSizePt: 12, align: 'right', spaceBeforePt: 12, spaceAfterPt: 12 },
|
|
53
|
+
centered: { fontSizePt: 12, align: 'center', spaceBeforePt: 12, spaceAfterPt: 12 },
|
|
54
|
+
lyrics: { fontSizePt: 12, italic: true, spaceAfterPt: 6 },
|
|
55
|
+
section: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
|
|
56
|
+
note: { fontSizePt: 10, spaceAfterPt: 6 },
|
|
57
|
+
comment: { fontSizePt: 10, italic: true, spaceAfterPt: 6 },
|
|
58
|
+
title: { fontSizePt: 24, bold: true, align: 'center', spaceAfterPt: 14.4 },
|
|
59
|
+
author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
|
|
60
|
+
logline: { fontSizePt: 12, italic: true, align: 'center', spaceAfterPt: 7.2 },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/** Every Indic script Sutrata already self-hosts Noto Sans for, pointed at its Noto
|
|
64
|
+
* Serif equivalent instead (all bundled in shell/fonts/fonts.css the same way as Noto
|
|
65
|
+
* Sans — see fonts-serif-block.css generation notes in that file's history). Reused by
|
|
66
|
+
* TRADITIONAL_STYLE's `complexScriptFontOverrides`: Noto Serif was chosen over the
|
|
67
|
+
* originally-considered Vijaya specifically because it already covers every script
|
|
68
|
+
* Noto Sans does and carries an OFL license, so — unlike Vijaya — it can actually ship
|
|
69
|
+
* in the repo instead of depending on the reader's OS/Word already having it installed. */
|
|
70
|
+
const NOTO_SERIF_COMPLEX_SCRIPT_FONTS = {
|
|
71
|
+
hi: 'Noto Serif Devanagari', ta: 'Noto Serif Tamil', te: 'Noto Serif Telugu',
|
|
72
|
+
kn: 'Noto Serif Kannada', ml: 'Noto Serif Malayalam', bn: 'Noto Serif Bengali',
|
|
73
|
+
gu: 'Noto Serif Gujarati', pa: 'Noto Serif Gurmukhi', or: 'Noto Serif Oriya',
|
|
74
|
+
si: 'Noto Serif Sinhala',
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Modeled on a real Tamil feature screenplay PDF (betterfountain/Fountain export):
|
|
78
|
+
* bold scene headings, underlined (not bold) character cues, fully monochrome,
|
|
79
|
+
* flush-left action/scene headings, A4 with 1in margins all around.
|
|
80
|
+
* Measurements (font sizes, indents as % of content width) were taken
|
|
81
|
+
* directly from the PDF's text positions; elements the reference document
|
|
82
|
+
* doesn't print at all (section/note/comment — Fountain strips these from
|
|
83
|
+
* output) are extrapolated to match its scale rather than measured.
|
|
84
|
+
*
|
|
85
|
+
* The reference PDF actually used Vijaya, a proprietary Microsoft font — not
|
|
86
|
+
* bundled here (unlike Noto, it can't legally ship in the repo, and depending
|
|
87
|
+
* on the reader's OS/Word already having it installed is a worse default than
|
|
88
|
+
* a font that just works). Noto Serif is the deliberate second choice: not a
|
|
89
|
+
* perfect visual match, but it already has a variant for every script Noto
|
|
90
|
+
* Sans does (so this style isn't just "Tamil-only" the way a Vijaya-based one
|
|
91
|
+
* would be) and ships under the OFL, so it's self-hosted for real instead of
|
|
92
|
+
* hoped-for. Tamil specifically renders in Noto Serif Tamil's *italic* face by
|
|
93
|
+
* default (`italicScripts`, forced even on bold scene headings/sections) since
|
|
94
|
+
* that slant reads closer to Vijaya than the upright serif does — every other
|
|
95
|
+
* script stays upright serif, `italicScripts` only lists 'ta'.
|
|
96
|
+
*/
|
|
97
|
+
export const TRADITIONAL_STYLE = {
|
|
98
|
+
id: 'traditional',
|
|
99
|
+
name: 'Traditional',
|
|
100
|
+
description: 'Bold serif scene headings, underlined cues, italic Tamil — modeled on a real Tamil shooting script.',
|
|
101
|
+
fontFamily: 'Noto Serif',
|
|
102
|
+
complexScriptFontOverrides: NOTO_SERIF_COMPLEX_SCRIPT_FONTS,
|
|
103
|
+
italicScripts: ['ta'],
|
|
104
|
+
// The reference PDF's cover: title / "by <author>" / logline flush top-left,
|
|
105
|
+
// date and contact down in the bottom-right corner.
|
|
106
|
+
coverLayout: 'corners',
|
|
107
|
+
page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
|
|
108
|
+
elements: {
|
|
109
|
+
sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 17, spaceAfterPt: 9 },
|
|
110
|
+
action: { fontSizePt: 10, spaceBeforePt: 8, spaceAfterPt: 9 },
|
|
111
|
+
character: { fontSizePt: 10, underline: true, marginLeftPct: 32, spaceBeforePt: 8, spaceAfterPt: 0 },
|
|
112
|
+
parenthetical: { fontSizePt: 10, marginLeftPct: 32, marginRightPct: 20, spaceAfterPt: 0 },
|
|
113
|
+
dialogue: { fontSizePt: 10, marginLeftPct: 24, marginRightPct: 17, spaceAfterPt: 9 },
|
|
114
|
+
transition: { fontSizePt: 10, align: 'right', spaceBeforePt: 9, spaceAfterPt: 9 },
|
|
115
|
+
centered: { fontSizePt: 10, align: 'center', italic: true, spaceBeforePt: 9, spaceAfterPt: 9 },
|
|
116
|
+
lyrics: { fontSizePt: 10, italic: true, marginLeftPct: 6, spaceAfterPt: 5 },
|
|
117
|
+
section: { fontSizePt: 12, bold: true, spaceBeforePt: 27, spaceAfterPt: 9 },
|
|
118
|
+
note: { fontSizePt: 10, color: '666666', spaceAfterPt: 5 },
|
|
119
|
+
comment: { fontSizePt: 13, italic: true, spaceAfterPt: 5 },
|
|
120
|
+
title: { fontSizePt: 32, bold: true, align: 'left', spaceAfterPt: 16 },
|
|
121
|
+
author: { fontSizePt: 16, align: 'left', spaceAfterPt: 8 },
|
|
122
|
+
logline: { fontSizePt: 10, align: 'left', spaceAfterPt: 8 },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
export const BUILTIN_STYLES = [CLASSIC_STYLE, INDUSTRY_COURIER_STYLE, TRADITIONAL_STYLE];
|
|
126
|
+
export function getBuiltinStyle(id) {
|
|
127
|
+
return BUILTIN_STYLES.find(s => s.id === id);
|
|
128
|
+
}
|
|
129
|
+
export function isBuiltinStyleId(id) {
|
|
130
|
+
return id === DEFAULT_STYLE_ID || BUILTIN_STYLES.some(s => s.id === id);
|
|
131
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ScreenplayStyleDefinition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Converts a style into the CSS custom properties screenplay.css reads via
|
|
4
|
+
* `var(--cs-<element>-<prop>, <classic-fallback>)`. Applied to `.cs-editor-mount`
|
|
5
|
+
* so the live editor picks up whichever style the current document (or an
|
|
6
|
+
* explicit override) resolves to — works identically for built-in and
|
|
7
|
+
* user-imported styles since both are plain `ScreenplayStyleDefinition` objects
|
|
8
|
+
* by the time they reach here.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately does NOT emit anything for `spaceAfterPt` — the live editor only
|
|
11
|
+
* ever expresses "space before" via margin-top; adjacent elements form their gap
|
|
12
|
+
* from the *following* element's margin-top, not a margin-bottom on the
|
|
13
|
+
* preceding one (see types.ts). `spaceAfterPt` is DOCX/print-only.
|
|
14
|
+
*
|
|
15
|
+
* A `null` entry means "no override" and must be cleared with `removeProperty`,
|
|
16
|
+
* never `setProperty(k, 'inherit')`: for a CUSTOM property, the literal value
|
|
17
|
+
* `inherit` is a CSS-wide keyword that makes the property itself inherit from
|
|
18
|
+
* *its own* parent (here, .cs-editor-mount's parent, which never defines it) —
|
|
19
|
+
* it does not store the string "inherit" for descendants to read. That breaks
|
|
20
|
+
* inheritance for every element below it, not just the unset one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function styleToCssVars(style: ScreenplayStyleDefinition): Record<string, string | null>;
|
|
23
|
+
/** Applies every var from `styleToCssVars` onto an element (the `.cs-editor-mount` ref),
|
|
24
|
+
* clearing any that resolved to `null` so a previous style's value can't linger. Also
|
|
25
|
+
* toggles a `.cs-force-italic-<code>` class per `style.italicScripts` — a class rather
|
|
26
|
+
* than a CSS var because `.cs-lang-<code>` rules already exist for lyrics/comments etc.
|
|
27
|
+
* that are legitimately italic on their own; a `font-style` var on `.cs-lang-<code>`
|
|
28
|
+
* itself would unconditionally win the cascade and stomp that even for styles that
|
|
29
|
+
* don't ask for forced italics (see types.ts's `italicScripts` doc for the full
|
|
30
|
+
* reasoning). The class only ever *adds* specificity to force italic on; it's absent
|
|
31
|
+
* entirely for scripts/styles that don't want it, so nothing is stomped either way. */
|
|
32
|
+
export declare function applyStyleVars(el: HTMLElement, style: ScreenplayStyleDefinition): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { BODY_ELEMENT_KEYS, COMPLEX_SCRIPT_CODES } from './types.js';
|
|
2
|
+
function kebab(key) {
|
|
3
|
+
return key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Converts a style into the CSS custom properties screenplay.css reads via
|
|
7
|
+
* `var(--cs-<element>-<prop>, <classic-fallback>)`. Applied to `.cs-editor-mount`
|
|
8
|
+
* so the live editor picks up whichever style the current document (or an
|
|
9
|
+
* explicit override) resolves to — works identically for built-in and
|
|
10
|
+
* user-imported styles since both are plain `ScreenplayStyleDefinition` objects
|
|
11
|
+
* by the time they reach here.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately does NOT emit anything for `spaceAfterPt` — the live editor only
|
|
14
|
+
* ever expresses "space before" via margin-top; adjacent elements form their gap
|
|
15
|
+
* from the *following* element's margin-top, not a margin-bottom on the
|
|
16
|
+
* preceding one (see types.ts). `spaceAfterPt` is DOCX/print-only.
|
|
17
|
+
*
|
|
18
|
+
* A `null` entry means "no override" and must be cleared with `removeProperty`,
|
|
19
|
+
* never `setProperty(k, 'inherit')`: for a CUSTOM property, the literal value
|
|
20
|
+
* `inherit` is a CSS-wide keyword that makes the property itself inherit from
|
|
21
|
+
* *its own* parent (here, .cs-editor-mount's parent, which never defines it) —
|
|
22
|
+
* it does not store the string "inherit" for descendants to read. That breaks
|
|
23
|
+
* inheritance for every element below it, not just the unset one.
|
|
24
|
+
*/
|
|
25
|
+
export function styleToCssVars(style) {
|
|
26
|
+
const vars = {
|
|
27
|
+
'--cs-body-font-family': style.fontFamily,
|
|
28
|
+
};
|
|
29
|
+
for (const key of BODY_ELEMENT_KEYS) {
|
|
30
|
+
const el = style.elements[key];
|
|
31
|
+
const prefix = `--cs-${kebab(key)}`;
|
|
32
|
+
vars[`${prefix}-font-size`] = `${el.fontSizePt}pt`;
|
|
33
|
+
vars[`${prefix}-font-weight`] = el.bold ? 'bold' : 'normal';
|
|
34
|
+
vars[`${prefix}-font-style`] = el.italic ? 'italic' : 'normal';
|
|
35
|
+
vars[`${prefix}-text-decoration`] = el.underline ? 'underline' : 'none';
|
|
36
|
+
vars[`${prefix}-color`] = el.color ? `#${el.color}` : null;
|
|
37
|
+
vars[`${prefix}-margin-left`] = `${el.marginLeftPct ?? 0}%`;
|
|
38
|
+
vars[`${prefix}-margin-right`] = `${el.marginRightPct ?? 0}%`;
|
|
39
|
+
vars[`${prefix}-text-align`] = el.align ?? 'left';
|
|
40
|
+
vars[`${prefix}-margin-top`] = `${el.spaceBeforePt ?? 0}pt`;
|
|
41
|
+
}
|
|
42
|
+
// Per-script font override: screenplay.css's .cs-lang-<code> rules prefer this over
|
|
43
|
+
// the generic --cs-body-font-family fallback. Emit `null` for every code the style
|
|
44
|
+
// doesn't override so a previously-applied style's choice can't linger (see
|
|
45
|
+
// applyStyleVars). italicScripts is NOT emitted as a var — see applyStyleVars.
|
|
46
|
+
for (const code of COMPLEX_SCRIPT_CODES) {
|
|
47
|
+
vars[`--cs-lang-${code}-font-family`] = style.complexScriptFontOverrides?.[code] ?? null;
|
|
48
|
+
}
|
|
49
|
+
return vars;
|
|
50
|
+
}
|
|
51
|
+
/** Applies every var from `styleToCssVars` onto an element (the `.cs-editor-mount` ref),
|
|
52
|
+
* clearing any that resolved to `null` so a previous style's value can't linger. Also
|
|
53
|
+
* toggles a `.cs-force-italic-<code>` class per `style.italicScripts` — a class rather
|
|
54
|
+
* than a CSS var because `.cs-lang-<code>` rules already exist for lyrics/comments etc.
|
|
55
|
+
* that are legitimately italic on their own; a `font-style` var on `.cs-lang-<code>`
|
|
56
|
+
* itself would unconditionally win the cascade and stomp that even for styles that
|
|
57
|
+
* don't ask for forced italics (see types.ts's `italicScripts` doc for the full
|
|
58
|
+
* reasoning). The class only ever *adds* specificity to force italic on; it's absent
|
|
59
|
+
* entirely for scripts/styles that don't want it, so nothing is stomped either way. */
|
|
60
|
+
export function applyStyleVars(el, style) {
|
|
61
|
+
const vars = styleToCssVars(style);
|
|
62
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
63
|
+
if (v === null)
|
|
64
|
+
el.style.removeProperty(k);
|
|
65
|
+
else
|
|
66
|
+
el.style.setProperty(k, v);
|
|
67
|
+
}
|
|
68
|
+
for (const code of COMPLEX_SCRIPT_CODES) {
|
|
69
|
+
el.classList.toggle(`cs-force-italic-${code}`, style.italicScripts?.includes(code) ?? false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IParagraphStyleOptions } from 'docx';
|
|
2
|
+
import type { ScreenplayStyleDefinition } from './types.js';
|
|
3
|
+
/** Builds the `paragraphStyles` array for `new Document({ styles: { paragraphStyles } })`
|
|
4
|
+
* from a style definition. `contentWidthTwips` is the page's printable width (page width
|
|
5
|
+
* minus left+right margins) — percentages are resolved against it, so indentation scales
|
|
6
|
+
* correctly with page size/margins instead of reusing one page format's fixed twips. */
|
|
7
|
+
export declare function styleToDocxParagraphStyles(style: ScreenplayStyleDefinition, contentWidthTwips: number): IParagraphStyleOptions[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AlignmentType } from 'docx';
|
|
2
|
+
import { BODY_ELEMENT_KEYS, COVER_ELEMENT_KEYS } from './types.js';
|
|
3
|
+
import { KEEP_WITH_NEXT_ELEMENTS, KEEP_TOGETHER_ELEMENTS } from './pagination.js';
|
|
4
|
+
/** Sutra element -> stable docx paragraph style id. Fixed forever: docx-importer.ts
|
|
5
|
+
* matches re-imported .docx files against these exact ids, regardless of appearance. */
|
|
6
|
+
const DOCX_STYLE_IDS = {
|
|
7
|
+
sceneHeading: 'CineSceneHeading',
|
|
8
|
+
action: 'CineAction',
|
|
9
|
+
character: 'CineCharacter',
|
|
10
|
+
parenthetical: 'CineParenthetical',
|
|
11
|
+
dialogue: 'CineDialogue',
|
|
12
|
+
transition: 'CineTransition',
|
|
13
|
+
centered: 'CineCentered',
|
|
14
|
+
lyrics: 'CineLyrics',
|
|
15
|
+
section: 'CineSection',
|
|
16
|
+
note: 'CineNote',
|
|
17
|
+
comment: 'CineComment',
|
|
18
|
+
};
|
|
19
|
+
/** Cover-page element -> docx paragraph style id, referenced by docx-exporter.ts's
|
|
20
|
+
* title page. These must be emitted alongside the body styles: a paragraph naming a
|
|
21
|
+
* style the document doesn't define silently falls back to Word's Normal style
|
|
22
|
+
* (left-aligned 11pt black), which is exactly how the title page used to render. */
|
|
23
|
+
const DOCX_COVER_STYLE_IDS = {
|
|
24
|
+
title: 'CineTitle',
|
|
25
|
+
author: 'CineAuthor',
|
|
26
|
+
logline: 'CineLogline',
|
|
27
|
+
};
|
|
28
|
+
const ALIGN_MAP = {
|
|
29
|
+
left: AlignmentType.LEFT,
|
|
30
|
+
center: AlignmentType.CENTER,
|
|
31
|
+
right: AlignmentType.RIGHT,
|
|
32
|
+
};
|
|
33
|
+
function pctToTwips(pct, contentWidthTwips) {
|
|
34
|
+
if (pct === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
return Math.round((pct / 100) * contentWidthTwips);
|
|
37
|
+
}
|
|
38
|
+
/** `pagination` carries the screenplay page-break rules for body elements (see
|
|
39
|
+
* pagination.ts); cover-page elements pass none — a cover page is one page by
|
|
40
|
+
* construction. */
|
|
41
|
+
function elementToDocxStyle(id, el, fontFamily, contentWidthTwips, pagination = {}) {
|
|
42
|
+
const left = pctToTwips(el.marginLeftPct, contentWidthTwips);
|
|
43
|
+
const right = pctToTwips(el.marginRightPct, contentWidthTwips);
|
|
44
|
+
const paragraph = {
|
|
45
|
+
alignment: el.align ? ALIGN_MAP[el.align] : undefined,
|
|
46
|
+
indent: (left !== undefined || right !== undefined) ? { left, right } : undefined,
|
|
47
|
+
spacing: {
|
|
48
|
+
before: el.spaceBeforePt !== undefined ? Math.round(el.spaceBeforePt * 20) : undefined,
|
|
49
|
+
after: el.spaceAfterPt !== undefined ? Math.round(el.spaceAfterPt * 20) : undefined,
|
|
50
|
+
},
|
|
51
|
+
keepNext: pagination.keepNext,
|
|
52
|
+
keepLines: pagination.keepLines,
|
|
53
|
+
// Stated rather than left to Word's default: action/dialogue stay breakable
|
|
54
|
+
// (no keepLines), so widow control is what stops a page break from stranding a
|
|
55
|
+
// single line of them on either side.
|
|
56
|
+
widowControl: true,
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
name: id,
|
|
61
|
+
run: {
|
|
62
|
+
font: fontFamily,
|
|
63
|
+
size: Math.round(el.fontSizePt * 2),
|
|
64
|
+
bold: el.bold || undefined,
|
|
65
|
+
italics: el.italic || undefined,
|
|
66
|
+
underline: el.underline ? {} : undefined,
|
|
67
|
+
color: el.color,
|
|
68
|
+
},
|
|
69
|
+
paragraph,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Builds the `paragraphStyles` array for `new Document({ styles: { paragraphStyles } })`
|
|
73
|
+
* from a style definition. `contentWidthTwips` is the page's printable width (page width
|
|
74
|
+
* minus left+right margins) — percentages are resolved against it, so indentation scales
|
|
75
|
+
* correctly with page size/margins instead of reusing one page format's fixed twips. */
|
|
76
|
+
export function styleToDocxParagraphStyles(style, contentWidthTwips) {
|
|
77
|
+
return [
|
|
78
|
+
...BODY_ELEMENT_KEYS.map(key => elementToDocxStyle(DOCX_STYLE_IDS[key], style.elements[key], style.fontFamily, contentWidthTwips, {
|
|
79
|
+
keepNext: KEEP_WITH_NEXT_ELEMENTS.includes(key) || undefined,
|
|
80
|
+
keepLines: KEEP_TOGETHER_ELEMENTS.includes(key) || undefined,
|
|
81
|
+
})),
|
|
82
|
+
...COVER_ELEMENT_KEYS.map(key => elementToDocxStyle(DOCX_COVER_STYLE_IDS[key], style.elements[key], style.fontFamily, contentWidthTwips)),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeds self-hosted @font-face rules (from shell/fonts/fonts.css, already loaded into
|
|
3
|
+
* the running app's own document) as data: URIs, for splicing into the PDF/print-preview
|
|
4
|
+
* HTML — a separate, standalone document that never sees the app's own stylesheets.
|
|
5
|
+
*
|
|
6
|
+
* Introspects the CSSOM (`document.styleSheets`) rather than hardcoding filenames/unicode
|
|
7
|
+
* ranges per family: whatever fonts.css declares for a family is exactly what gets embedded,
|
|
8
|
+
* so this can never drift out of sync with the actual font files bundled in public/fonts.
|
|
9
|
+
*
|
|
10
|
+
* Why embed at all instead of just linking `/fonts/...` from the print HTML: the native
|
|
11
|
+
* desktop PDF export writes the HTML to a temp file and loads it via a `file://` URL in a
|
|
12
|
+
* hidden webview, which cannot resolve a root-relative `/fonts/...` path back to the running
|
|
13
|
+
* app's own server/origin. A data: URI works regardless of the HTML's own location.
|
|
14
|
+
*/
|
|
15
|
+
/** Builds `@font-face` CSS text embedding every self-hosted rule for the given family
|
|
16
|
+
* names, as data: URIs. Families with no matching rules (e.g. web-safe fonts that were
|
|
17
|
+
* never in fonts.css) are silently skipped — the Google Fonts `<link>`/generic fallback
|
|
18
|
+
* stack in print-adapter.ts's font stack still applies. Fetch failures for an individual
|
|
19
|
+
* rule are likewise skipped rather than failing the whole export. */
|
|
20
|
+
export declare function embedFontFacesFor(familyNames: string[]): Promise<string>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeds self-hosted @font-face rules (from shell/fonts/fonts.css, already loaded into
|
|
3
|
+
* the running app's own document) as data: URIs, for splicing into the PDF/print-preview
|
|
4
|
+
* HTML — a separate, standalone document that never sees the app's own stylesheets.
|
|
5
|
+
*
|
|
6
|
+
* Introspects the CSSOM (`document.styleSheets`) rather than hardcoding filenames/unicode
|
|
7
|
+
* ranges per family: whatever fonts.css declares for a family is exactly what gets embedded,
|
|
8
|
+
* so this can never drift out of sync with the actual font files bundled in public/fonts.
|
|
9
|
+
*
|
|
10
|
+
* Why embed at all instead of just linking `/fonts/...` from the print HTML: the native
|
|
11
|
+
* desktop PDF export writes the HTML to a temp file and loads it via a `file://` URL in a
|
|
12
|
+
* hidden webview, which cannot resolve a root-relative `/fonts/...` path back to the running
|
|
13
|
+
* app's own server/origin. A data: URI works regardless of the HTML's own location.
|
|
14
|
+
*/
|
|
15
|
+
function urlToDataUri(url) {
|
|
16
|
+
return fetch(url)
|
|
17
|
+
.then(res => {
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
throw new Error(`Failed to fetch font: ${url}`);
|
|
20
|
+
return res.blob();
|
|
21
|
+
})
|
|
22
|
+
.then(blob => new Promise((resolve, reject) => {
|
|
23
|
+
const reader = new FileReader();
|
|
24
|
+
reader.onload = () => resolve(reader.result);
|
|
25
|
+
reader.onerror = () => reject(reader.error ?? new Error('Failed to read font file'));
|
|
26
|
+
reader.readAsDataURL(blob);
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function findFontFaceRules(familyName) {
|
|
30
|
+
const rules = [];
|
|
31
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
32
|
+
let cssRules;
|
|
33
|
+
try {
|
|
34
|
+
cssRules = sheet.cssRules;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue; // cross-origin stylesheets throw on access; nothing we bundle is cross-origin
|
|
38
|
+
}
|
|
39
|
+
for (const rule of Array.from(cssRules)) {
|
|
40
|
+
if (rule instanceof CSSFontFaceRule) {
|
|
41
|
+
const family = rule.style.getPropertyValue('font-family').replace(/^['"]|['"]$/g, '');
|
|
42
|
+
if (family === familyName)
|
|
43
|
+
rules.push(rule);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return rules;
|
|
48
|
+
}
|
|
49
|
+
/** Builds `@font-face` CSS text embedding every self-hosted rule for the given family
|
|
50
|
+
* names, as data: URIs. Families with no matching rules (e.g. web-safe fonts that were
|
|
51
|
+
* never in fonts.css) are silently skipped — the Google Fonts `<link>`/generic fallback
|
|
52
|
+
* stack in print-adapter.ts's font stack still applies. Fetch failures for an individual
|
|
53
|
+
* rule are likewise skipped rather than failing the whole export. */
|
|
54
|
+
export async function embedFontFacesFor(familyNames) {
|
|
55
|
+
const blocks = [];
|
|
56
|
+
for (const family of familyNames) {
|
|
57
|
+
for (const rule of findFontFaceRules(family)) {
|
|
58
|
+
const srcRaw = rule.style.getPropertyValue('src');
|
|
59
|
+
const match = /url\(["']?([^"')]+)["']?\)/.exec(srcRaw);
|
|
60
|
+
if (!match)
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const dataUri = await urlToDataUri(match[1]);
|
|
64
|
+
const weight = rule.style.getPropertyValue('font-weight') || '400';
|
|
65
|
+
const style = rule.style.getPropertyValue('font-style') || 'normal';
|
|
66
|
+
const unicodeRange = rule.style.getPropertyValue('unicode-range');
|
|
67
|
+
blocks.push('@font-face {\n' +
|
|
68
|
+
` font-family: '${family}';\n` +
|
|
69
|
+
` font-weight: ${weight};\n` +
|
|
70
|
+
` font-style: ${style};\n` +
|
|
71
|
+
` src: url(${dataUri}) format('woff2');\n` +
|
|
72
|
+
(unicodeRange ? ` unicode-range: ${unicodeRange};\n` : '') +
|
|
73
|
+
'}');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Skip; the Google Fonts link / generic fallback stack still covers this family.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return blocks.join('\n');
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BodyElementKey } from './types.js';
|
|
2
|
+
/** Screenplay pagination rules — format conventions, not per-style visual choices, so
|
|
3
|
+
* they live here rather than in ScreenplayStyleDefinition: no style (built-in or
|
|
4
|
+
* user-imported) gets to end a page on a character cue. Consumed by both export
|
|
5
|
+
* paths, which express the same rule differently: DOCX via `keepNext`/`keepLines`
|
|
6
|
+
* paragraph properties (docx-adapter.ts), PDF/print via `break-after`/`break-inside`
|
|
7
|
+
* (print-adapter.ts). The live editor has no pagination, so it ignores these. */
|
|
8
|
+
/** Elements that must not be the last thing on a page — a scene heading or character
|
|
9
|
+
* cue stranded at the bottom with its action/dialogue overleaf is the classic
|
|
10
|
+
* screenplay pagination error. A parenthetical is included for the same reason: it
|
|
11
|
+
* belongs to the dialogue line that follows it. */
|
|
12
|
+
export declare const KEEP_WITH_NEXT_ELEMENTS: readonly BodyElementKey[];
|
|
13
|
+
/** Elements short enough that splitting them across a page boundary is always wrong.
|
|
14
|
+
* Deliberately excludes action/dialogue/lyrics/note — those can legitimately run
|
|
15
|
+
* longer than a page, so they get orphan/widow control instead of an unbreakable box. */
|
|
16
|
+
export declare const KEEP_TOGETHER_ELEMENTS: readonly BodyElementKey[];
|
|
17
|
+
/** Minimum lines of a breakable element left on either side of a page boundary. Two is
|
|
18
|
+
* the standard screenplay/typography minimum (a single stranded line reads as an error). */
|
|
19
|
+
export declare const MIN_ORPHAN_WIDOW_LINES = 2;
|