@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,283 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
3
|
+
/** Paragraph styles produced by docx-exporter.ts that mark actual screenplay body content
|
|
4
|
+
* (as opposed to the free-form title-page paragraphs, which carry no style, or CineTitle /
|
|
5
|
+
* CineAuthor / CineLogline, which belong to the title page and are not reimported here). */
|
|
6
|
+
const BODY_STYLES = new Set([
|
|
7
|
+
'CineSceneHeading', 'CineSection', 'CineAction', 'CineCharacter',
|
|
8
|
+
'CineParenthetical', 'CineDialogue', 'CineTransition', 'CineCentered',
|
|
9
|
+
'CineLyrics', 'CineNote', 'CineComment',
|
|
10
|
+
]);
|
|
11
|
+
function attrVal(el, tag) {
|
|
12
|
+
const child = el.getElementsByTagNameNS(W_NS, tag)[0];
|
|
13
|
+
if (!child)
|
|
14
|
+
return null;
|
|
15
|
+
const val = child.getAttributeNS(W_NS, 'val');
|
|
16
|
+
return val;
|
|
17
|
+
}
|
|
18
|
+
function flagOn(el, tag) {
|
|
19
|
+
const child = el.getElementsByTagNameNS(W_NS, tag)[0];
|
|
20
|
+
if (!child)
|
|
21
|
+
return false;
|
|
22
|
+
const val = child.getAttributeNS(W_NS, 'val');
|
|
23
|
+
if (val === null)
|
|
24
|
+
return true;
|
|
25
|
+
return !['false', '0', 'off', 'none'].includes(val.toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
function parseParagraph(p) {
|
|
28
|
+
let style = null;
|
|
29
|
+
const runs = [];
|
|
30
|
+
let isPageBreak = false;
|
|
31
|
+
for (const child of Array.from(p.children)) {
|
|
32
|
+
if (child.localName === 'pPr') {
|
|
33
|
+
style = attrVal(child, 'pStyle');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (child.localName === 'r') {
|
|
37
|
+
const rPr = child.getElementsByTagNameNS(W_NS, 'rPr')[0];
|
|
38
|
+
const bold = !!rPr && flagOn(rPr, 'b');
|
|
39
|
+
const italic = !!rPr && flagOn(rPr, 'i');
|
|
40
|
+
const underline = !!rPr && flagOn(rPr, 'u');
|
|
41
|
+
let text = '';
|
|
42
|
+
for (const rc of Array.from(child.children)) {
|
|
43
|
+
if (rc.localName === 't')
|
|
44
|
+
text += rc.textContent ?? '';
|
|
45
|
+
// Word normalizes a literal tab character inside <w:t> (what docx.js emits) into a
|
|
46
|
+
// <w:tab/> element when it resaves the file — recognize both forms.
|
|
47
|
+
else if (rc.localName === 'tab')
|
|
48
|
+
text += '\t';
|
|
49
|
+
else if (rc.localName === 'br')
|
|
50
|
+
isPageBreak = true;
|
|
51
|
+
}
|
|
52
|
+
if (text)
|
|
53
|
+
runs.push({ text, bold, italic, underline });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { style, text: runs.map(r => r.text).join(''), runs, isPageBreak };
|
|
57
|
+
}
|
|
58
|
+
function parseTable(tbl) {
|
|
59
|
+
const tr = tbl.getElementsByTagNameNS(W_NS, 'tr')[0];
|
|
60
|
+
const cells = [];
|
|
61
|
+
if (tr) {
|
|
62
|
+
for (const tc of Array.from(tr.children)) {
|
|
63
|
+
if (tc.localName !== 'tc')
|
|
64
|
+
continue;
|
|
65
|
+
const paras = Array.from(tc.children)
|
|
66
|
+
.filter(c => c.localName === 'p')
|
|
67
|
+
.map(p => parseParagraph(p));
|
|
68
|
+
cells.push(paras);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { cells };
|
|
72
|
+
}
|
|
73
|
+
/** Rebuilds bold/italic/underline markup from formatting runs, matching the parser's inline syntax.
|
|
74
|
+
* Types that don't support inline spans in Sutra source (headings, character cues, notes, etc.)
|
|
75
|
+
* should pass all three `allow` flags false so forced style formatting (e.g. bold character names)
|
|
76
|
+
* isn't reinterpreted as user markup. */
|
|
77
|
+
function renderInline(runs, opts) {
|
|
78
|
+
let out = '';
|
|
79
|
+
for (const r of runs) {
|
|
80
|
+
const bold = opts.allowBold && r.bold;
|
|
81
|
+
const italic = opts.allowItalic && r.italic;
|
|
82
|
+
const underline = opts.allowUnderline && r.underline;
|
|
83
|
+
let text = r.text;
|
|
84
|
+
if (!text)
|
|
85
|
+
continue;
|
|
86
|
+
if (underline)
|
|
87
|
+
text = `_${text}_`;
|
|
88
|
+
if (italic)
|
|
89
|
+
text = `*${text}*`;
|
|
90
|
+
if (bold)
|
|
91
|
+
text = `**${text}**`;
|
|
92
|
+
out += text;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function plainText(para) {
|
|
97
|
+
return para.text;
|
|
98
|
+
}
|
|
99
|
+
function splitNameExtension(text) {
|
|
100
|
+
const m = text.trim().match(/^(.*)\s(\([^()]*\))$/);
|
|
101
|
+
if (m)
|
|
102
|
+
return { name: m[1].trim(), extension: m[2] };
|
|
103
|
+
return { name: text.trim(), extension: null };
|
|
104
|
+
}
|
|
105
|
+
function renderCharacterLine(text, isDual) {
|
|
106
|
+
const { name, extension } = splitNameExtension(text);
|
|
107
|
+
return `@${name}${extension ? ` ${extension}` : ''}${isDual ? ' ^' : ''}`;
|
|
108
|
+
}
|
|
109
|
+
function renderSceneHeading(para) {
|
|
110
|
+
const text = plainText(para);
|
|
111
|
+
const tabIdx = text.indexOf('\t');
|
|
112
|
+
if (tabIdx === -1)
|
|
113
|
+
return `## ${text.trim()}`;
|
|
114
|
+
const heading = text.slice(0, tabIdx).trim();
|
|
115
|
+
const id = text.slice(tabIdx + 1).trim();
|
|
116
|
+
return id ? `## ${heading} {#${id}}` : `## ${heading}`;
|
|
117
|
+
}
|
|
118
|
+
class BlockBuilder {
|
|
119
|
+
blocks = [];
|
|
120
|
+
charLines = null;
|
|
121
|
+
unstyledCount = 0;
|
|
122
|
+
flushChar() {
|
|
123
|
+
if (this.charLines) {
|
|
124
|
+
this.blocks.push(this.charLines.join('\n'));
|
|
125
|
+
this.charLines = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
push(block) {
|
|
129
|
+
this.flushChar();
|
|
130
|
+
this.blocks.push(block);
|
|
131
|
+
}
|
|
132
|
+
startCharacter(line) {
|
|
133
|
+
this.flushChar();
|
|
134
|
+
this.charLines = [line];
|
|
135
|
+
}
|
|
136
|
+
appendToCharacter(line) {
|
|
137
|
+
if (!this.charLines)
|
|
138
|
+
return false;
|
|
139
|
+
this.charLines.push(line);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
finish() {
|
|
143
|
+
this.flushChar();
|
|
144
|
+
return this.blocks.join('\n\n');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function addParagraph(para, builder) {
|
|
148
|
+
if (para.isPageBreak) {
|
|
149
|
+
builder.push('===');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const text = plainText(para).trim();
|
|
153
|
+
switch (para.style) {
|
|
154
|
+
case 'CineSceneHeading':
|
|
155
|
+
builder.push(renderSceneHeading(para));
|
|
156
|
+
return;
|
|
157
|
+
case 'CineSection':
|
|
158
|
+
builder.push(`# ${text}`);
|
|
159
|
+
return;
|
|
160
|
+
case 'CineAction': {
|
|
161
|
+
const rendered = renderInline(para.runs, { allowBold: true, allowItalic: true, allowUnderline: true });
|
|
162
|
+
if (rendered.trim())
|
|
163
|
+
builder.push(rendered);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
case 'CineCharacter':
|
|
167
|
+
builder.startCharacter(renderCharacterLine(text, false));
|
|
168
|
+
return;
|
|
169
|
+
case 'CineParenthetical':
|
|
170
|
+
if (!builder.appendToCharacter(text))
|
|
171
|
+
builder.push(text);
|
|
172
|
+
return;
|
|
173
|
+
case 'CineDialogue': {
|
|
174
|
+
const rendered = renderInline(para.runs, { allowBold: true, allowItalic: true, allowUnderline: true });
|
|
175
|
+
if (!builder.appendToCharacter(rendered))
|
|
176
|
+
builder.push(rendered);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
case 'CineTransition':
|
|
180
|
+
builder.push(`>> ${text}`);
|
|
181
|
+
return;
|
|
182
|
+
case 'CineCentered':
|
|
183
|
+
builder.push(`>> ${text} <<`);
|
|
184
|
+
return;
|
|
185
|
+
case 'CineLyrics': {
|
|
186
|
+
const rendered = renderInline(para.runs, { allowBold: true, allowItalic: false, allowUnderline: true });
|
|
187
|
+
builder.push(`~ ${rendered}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
case 'CineNote':
|
|
191
|
+
builder.push(text);
|
|
192
|
+
return;
|
|
193
|
+
case 'CineComment':
|
|
194
|
+
builder.push(text);
|
|
195
|
+
return;
|
|
196
|
+
default: {
|
|
197
|
+
if (!text)
|
|
198
|
+
return;
|
|
199
|
+
builder.unstyledCount++;
|
|
200
|
+
const rendered = renderInline(para.runs, { allowBold: true, allowItalic: true, allowUnderline: true });
|
|
201
|
+
builder.push(rendered);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function isBodyStart(item) {
|
|
206
|
+
if (item.kind === 'table')
|
|
207
|
+
return true;
|
|
208
|
+
return item.para.style !== null && BODY_STYLES.has(item.para.style);
|
|
209
|
+
}
|
|
210
|
+
/** Parses a .docx file (as produced by exportToDocx, possibly re-saved from Word with
|
|
211
|
+
* corrections and/or font changes) back into Sutra body text. The title page, if any,
|
|
212
|
+
* is not reimported — only content carrying the CineXxx paragraph styles the exporter
|
|
213
|
+
* applies to the screenplay body is recognized. */
|
|
214
|
+
export async function importDocx(buffer) {
|
|
215
|
+
const warnings = [];
|
|
216
|
+
let zip;
|
|
217
|
+
try {
|
|
218
|
+
zip = await JSZip.loadAsync(buffer);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new Error('This file is not a valid .docx (Word) document.');
|
|
222
|
+
}
|
|
223
|
+
const docXml = zip.file('word/document.xml');
|
|
224
|
+
if (!docXml)
|
|
225
|
+
throw new Error('This file is not a valid .docx (Word) document.');
|
|
226
|
+
const xmlText = await docXml.async('text');
|
|
227
|
+
const parser = new DOMParser();
|
|
228
|
+
const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
|
|
229
|
+
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
|
|
230
|
+
throw new Error('Could not read the Word document — its XML appears corrupted.');
|
|
231
|
+
}
|
|
232
|
+
const body = xmlDoc.getElementsByTagNameNS(W_NS, 'body')[0];
|
|
233
|
+
if (!body)
|
|
234
|
+
throw new Error('This .docx file has no document body.');
|
|
235
|
+
const items = [];
|
|
236
|
+
for (const child of Array.from(body.children)) {
|
|
237
|
+
if (child.localName === 'p')
|
|
238
|
+
items.push({ kind: 'para', para: parseParagraph(child) });
|
|
239
|
+
else if (child.localName === 'tbl')
|
|
240
|
+
items.push({ kind: 'table', table: parseTable(child) });
|
|
241
|
+
}
|
|
242
|
+
const startIdx = items.findIndex(isBodyStart);
|
|
243
|
+
if (startIdx === -1) {
|
|
244
|
+
warnings.push('No Sutrata screenplay content was found in this document — nothing was imported.');
|
|
245
|
+
return { bodyText: null, warnings };
|
|
246
|
+
}
|
|
247
|
+
const builder = new BlockBuilder();
|
|
248
|
+
let skippedTables = 0;
|
|
249
|
+
for (let i = startIdx; i < items.length; i++) {
|
|
250
|
+
const item = items[i];
|
|
251
|
+
if (item.kind === 'para') {
|
|
252
|
+
addParagraph(item.para, builder);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// dual-dialogue table: exactly two cells, each a character block
|
|
256
|
+
if (item.table.cells.length !== 2) {
|
|
257
|
+
skippedTables++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const [left, right] = item.table.cells;
|
|
261
|
+
for (const p of left)
|
|
262
|
+
addParagraph(p, builder);
|
|
263
|
+
const rightFirst = right[0];
|
|
264
|
+
if (rightFirst && rightFirst.style === 'CineCharacter') {
|
|
265
|
+
builder.startCharacter(renderCharacterLine(plainText(rightFirst).trim(), true));
|
|
266
|
+
for (const p of right.slice(1))
|
|
267
|
+
addParagraph(p, builder);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
for (const p of right)
|
|
271
|
+
addParagraph(p, builder);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const bodyText = builder.finish();
|
|
275
|
+
if (builder.unstyledCount > 0) {
|
|
276
|
+
warnings.push(`${builder.unstyledCount} paragraph(s) had no recognized Sutrata style and were imported as action lines.`);
|
|
277
|
+
}
|
|
278
|
+
if (skippedTables > 0) {
|
|
279
|
+
warnings.push(`${skippedTables} table(s) in the document were not recognized as dual dialogue and were skipped.`);
|
|
280
|
+
}
|
|
281
|
+
warnings.push('The title page was not imported — edit it from the Title Page panel if it also needs corrections.');
|
|
282
|
+
return { bodyText: bodyText || null, warnings };
|
|
283
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Per-page headers ("<scene> CONTINUED:" on the left, "p<n>" on the right) need to know
|
|
2
|
+
* where the page breaks actually fall, and a browser tells no one that: Chromium supports
|
|
3
|
+
* neither CSS Paged Media margin boxes (@top-left/@top-right) nor running elements, and
|
|
4
|
+
* exposes no JS hook for its own break positions. So the print document paginates itself:
|
|
5
|
+
* the script below runs inside the print window (or the hidden WebView2 window the Windows
|
|
6
|
+
* desktop export prints from) and distributes the rendered blocks into fixed-height
|
|
7
|
+
* .print-page boxes it can then put a real header on.
|
|
8
|
+
*
|
|
9
|
+
* It runs in that window, not here — `printPaginatorScript` stringifies the function into
|
|
10
|
+
* the exported HTML — so `paginatePrintDocument` must stay self-contained: no imports, no
|
|
11
|
+
* module-scope references, nothing a bundler could rename out from under it. */
|
|
12
|
+
export interface PrintPaginationOptions {
|
|
13
|
+
/** Printable height of one page in CSS px (paper height minus the style's top/bottom
|
|
14
|
+
* margins) — the browser applies those margins via @page, so a box of exactly this
|
|
15
|
+
* height fills one sheet. */
|
|
16
|
+
contentHeightPx: number;
|
|
17
|
+
/** Right-hand header cell: this prefix plus the page number (e.g. "p" -> "p12"). */
|
|
18
|
+
pageNumberPrefix: string;
|
|
19
|
+
/** Left-hand header cell on a page that opens mid-scene: the scene number, then this
|
|
20
|
+
* (e.g. "8 CONTINUED:"). */
|
|
21
|
+
continuedSuffix: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function paginatePrintDocument(opts: PrintPaginationOptions): void;
|
|
24
|
+
/** The `<script>` that runs `paginatePrintDocument` in the exported document, once web
|
|
25
|
+
* fonts have settled — measuring before that would paginate against fallback metrics
|
|
26
|
+
* (and Indic text reflows a lot between the two). Any failure leaves the un-paginated
|
|
27
|
+
* flow exactly as it was, so the export degrades to the browser's own page breaks
|
|
28
|
+
* (which styles/pagination.ts's CSS rules still govern) instead of producing nothing. */
|
|
29
|
+
export declare function printPaginatorScript(opts: PrintPaginationOptions): string;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/** Per-page headers ("<scene> CONTINUED:" on the left, "p<n>" on the right) need to know
|
|
2
|
+
* where the page breaks actually fall, and a browser tells no one that: Chromium supports
|
|
3
|
+
* neither CSS Paged Media margin boxes (@top-left/@top-right) nor running elements, and
|
|
4
|
+
* exposes no JS hook for its own break positions. So the print document paginates itself:
|
|
5
|
+
* the script below runs inside the print window (or the hidden WebView2 window the Windows
|
|
6
|
+
* desktop export prints from) and distributes the rendered blocks into fixed-height
|
|
7
|
+
* .print-page boxes it can then put a real header on.
|
|
8
|
+
*
|
|
9
|
+
* It runs in that window, not here — `printPaginatorScript` stringifies the function into
|
|
10
|
+
* the exported HTML — so `paginatePrintDocument` must stay self-contained: no imports, no
|
|
11
|
+
* module-scope references, nothing a bundler could rename out from under it. */
|
|
12
|
+
export function paginatePrintDocument(opts) {
|
|
13
|
+
const flow = document.getElementById('print-flow');
|
|
14
|
+
const pagesRoot = document.getElementById('print-pages');
|
|
15
|
+
if (!flow || !pagesRoot)
|
|
16
|
+
return;
|
|
17
|
+
const root = pagesRoot;
|
|
18
|
+
const blocks = [];
|
|
19
|
+
for (let i = 0; i < flow.children.length; i++)
|
|
20
|
+
blocks.push(flow.children[i]);
|
|
21
|
+
if (blocks.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
// Elements that may be broken across a page boundary mid-paragraph. Everything else
|
|
24
|
+
// moves to the next page whole (a cue, a heading and a note are all short enough that
|
|
25
|
+
// splitting one is always the wrong answer).
|
|
26
|
+
const SPLITTABLE = /\bprint-(action|dialogue|lyrics)\b/;
|
|
27
|
+
// ...and elements that must not be the last thing on a page — the same rule
|
|
28
|
+
// styles/pagination.ts states for DOCX/CSS, re-stated here because inside a
|
|
29
|
+
// self-paginated flow the browser's own break-after: avoid never comes into play.
|
|
30
|
+
// (?![\w-] so this doesn't also catch the print-character-block wrapper — a *finished*
|
|
31
|
+
// speech is free to end a page; it's the cue inside it that must not.)
|
|
32
|
+
const KEEP_WITH_NEXT = /\bprint-(scene-heading|character|parenthetical|section)(?![\w-])/;
|
|
33
|
+
// A cue + its dialogue: not splittable as text, but its children can be distributed.
|
|
34
|
+
const CONTAINER = /\bprint-character-block\b/;
|
|
35
|
+
const MIN_LINES = 2;
|
|
36
|
+
// Slack against sub-pixel rounding: a box that measures a hair over the page height
|
|
37
|
+
// would spill a stray sliver onto the next sheet.
|
|
38
|
+
const SLACK_PX = 4;
|
|
39
|
+
const pages = [];
|
|
40
|
+
function newPage() {
|
|
41
|
+
const el = document.createElement('section');
|
|
42
|
+
el.className = 'print-page';
|
|
43
|
+
el.style.height = opts.contentHeightPx + 'px';
|
|
44
|
+
const header = document.createElement('div');
|
|
45
|
+
header.className = 'print-page-header';
|
|
46
|
+
const continued = document.createElement('span');
|
|
47
|
+
continued.className = 'print-page-continued';
|
|
48
|
+
const number = document.createElement('span');
|
|
49
|
+
number.className = 'print-page-number';
|
|
50
|
+
header.appendChild(continued);
|
|
51
|
+
header.appendChild(number);
|
|
52
|
+
const body = document.createElement('div');
|
|
53
|
+
body.className = 'print-page-body';
|
|
54
|
+
el.appendChild(header);
|
|
55
|
+
el.appendChild(body);
|
|
56
|
+
root.appendChild(el);
|
|
57
|
+
const page = { el, body, continued, number };
|
|
58
|
+
pages.push(page);
|
|
59
|
+
// Numbered as soon as the page exists, not in the header pass at the end: an empty
|
|
60
|
+
// header measures as a bare margin, and every block would then be placed against a
|
|
61
|
+
// page height ~one line too generous.
|
|
62
|
+
number.textContent = opts.pageNumberPrefix + String(pages.length);
|
|
63
|
+
return page;
|
|
64
|
+
}
|
|
65
|
+
let page = newPage();
|
|
66
|
+
/** How much of the page box is left for body content, in the body's own coordinates —
|
|
67
|
+
* i.e. page height less everything above the body (the header and its margin). */
|
|
68
|
+
const limit = () => opts.contentHeightPx
|
|
69
|
+
- (page.body.getBoundingClientRect().top - page.el.getBoundingClientRect().top)
|
|
70
|
+
- SLACK_PX;
|
|
71
|
+
function lineCount(el) {
|
|
72
|
+
const range = document.createRange();
|
|
73
|
+
range.selectNodeContents(el);
|
|
74
|
+
return range.getClientRects().length;
|
|
75
|
+
}
|
|
76
|
+
/** Every position a line could break at, as (text node, offset) pairs. */
|
|
77
|
+
function wordBoundaries(el) {
|
|
78
|
+
const spots = [];
|
|
79
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
80
|
+
let node = walker.nextNode();
|
|
81
|
+
while (node) {
|
|
82
|
+
const text = node.data;
|
|
83
|
+
for (let i = 1; i < text.length; i++) {
|
|
84
|
+
if (/\s/.test(text[i - 1]) && !/\s/.test(text[i]))
|
|
85
|
+
spots.push({ node, offset: i });
|
|
86
|
+
}
|
|
87
|
+
node = walker.nextNode();
|
|
88
|
+
}
|
|
89
|
+
return spots;
|
|
90
|
+
}
|
|
91
|
+
/** Cut `el` so that what stays fits in `maxHeight`, returning the remainder as a new
|
|
92
|
+
* element of the same kind (or null when there is no split leaving MIN_LINES on both
|
|
93
|
+
* sides — then the whole block moves to the next page instead). */
|
|
94
|
+
function split(el, maxHeight) {
|
|
95
|
+
const range = document.createRange();
|
|
96
|
+
// Nothing to split against without a layout engine to measure line boxes with
|
|
97
|
+
// (a headless DOM, say) — blocks then move whole rather than being cut blind.
|
|
98
|
+
if (typeof range.getClientRects !== 'function')
|
|
99
|
+
return null;
|
|
100
|
+
const spots = wordBoundaries(el);
|
|
101
|
+
if (spots.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
const top = el.getBoundingClientRect().top;
|
|
104
|
+
const fits = (spot) => {
|
|
105
|
+
range.setStartBefore(el);
|
|
106
|
+
range.setEnd(spot.node, spot.offset);
|
|
107
|
+
const rects = range.getClientRects();
|
|
108
|
+
const last = rects[rects.length - 1];
|
|
109
|
+
return last ? last.bottom - top <= maxHeight : true;
|
|
110
|
+
};
|
|
111
|
+
let lo = 0;
|
|
112
|
+
let hi = spots.length - 1;
|
|
113
|
+
let best = -1;
|
|
114
|
+
while (lo <= hi) {
|
|
115
|
+
const mid = (lo + hi) >> 1;
|
|
116
|
+
if (fits(spots[mid])) {
|
|
117
|
+
best = mid;
|
|
118
|
+
lo = mid + 1;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
hi = mid - 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (best < 0)
|
|
125
|
+
return null;
|
|
126
|
+
const cut = spots[best];
|
|
127
|
+
const rest = el.cloneNode(false);
|
|
128
|
+
const tail = document.createRange();
|
|
129
|
+
tail.setStart(cut.node, cut.offset);
|
|
130
|
+
tail.setEndAfter(el.lastChild);
|
|
131
|
+
rest.appendChild(tail.extractContents());
|
|
132
|
+
page.body.appendChild(rest);
|
|
133
|
+
const ok = lineCount(el) >= MIN_LINES && lineCount(rest) >= MIN_LINES;
|
|
134
|
+
page.body.removeChild(rest);
|
|
135
|
+
if (!ok) {
|
|
136
|
+
// Put it back exactly as it was rather than leaving a half-cut paragraph behind.
|
|
137
|
+
while (rest.firstChild)
|
|
138
|
+
el.appendChild(rest.firstChild);
|
|
139
|
+
el.normalize();
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return rest;
|
|
143
|
+
}
|
|
144
|
+
/** A character block is a cue plus its parentheticals and dialogue lines. When it
|
|
145
|
+
* straddles a break, the split runs *between* those children (and, where the straddling
|
|
146
|
+
* line is dialogue, inside it) so a long speech doesn't jump to the next page whole and
|
|
147
|
+
* leave a hole behind. The cue never ends up alone: a continuation is only produced
|
|
148
|
+
* when the cue keeps at least part of the first speech with it. */
|
|
149
|
+
function splitContainer(el, maxHeight) {
|
|
150
|
+
const children = [];
|
|
151
|
+
for (let i = 0; i < el.children.length; i++)
|
|
152
|
+
children.push(el.children[i]);
|
|
153
|
+
if (children.length < 2)
|
|
154
|
+
return null;
|
|
155
|
+
const top = el.getBoundingClientRect().top;
|
|
156
|
+
let cut = -1;
|
|
157
|
+
for (let i = 0; i < children.length; i++) {
|
|
158
|
+
if (children[i].getBoundingClientRect().bottom - top > maxHeight) {
|
|
159
|
+
cut = i;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (cut <= 0)
|
|
164
|
+
return null; // the cue itself doesn't fit: the whole block moves down
|
|
165
|
+
const rest = el.cloneNode(false);
|
|
166
|
+
const crossing = children[cut];
|
|
167
|
+
let from = cut;
|
|
168
|
+
if (SPLITTABLE.test(crossing.className)) {
|
|
169
|
+
const tail = split(crossing, maxHeight - (crossing.getBoundingClientRect().top - top));
|
|
170
|
+
if (tail) {
|
|
171
|
+
rest.appendChild(tail);
|
|
172
|
+
from = cut + 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (from === cut && cut < 2)
|
|
176
|
+
return null; // would strand the cue on its own
|
|
177
|
+
for (let i = from; i < children.length; i++)
|
|
178
|
+
rest.appendChild(children[i]);
|
|
179
|
+
return rest.children.length > 0 ? rest : null;
|
|
180
|
+
}
|
|
181
|
+
/** Break `block` so what's left fits in `room`, returning the continuation (or null when
|
|
182
|
+
* it can't be broken and has to move down whole). */
|
|
183
|
+
function breakBlock(block, room) {
|
|
184
|
+
if (SPLITTABLE.test(block.className))
|
|
185
|
+
return split(block, room);
|
|
186
|
+
if (CONTAINER.test(block.className))
|
|
187
|
+
return splitContainer(block, room);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/** The run of keep-with-next blocks immediately before the break, which have to travel
|
|
191
|
+
* to the next page with the block they introduce. */
|
|
192
|
+
function trailingKeepWithNext() {
|
|
193
|
+
const moved = [];
|
|
194
|
+
let last = page.body.lastElementChild;
|
|
195
|
+
while (last && KEEP_WITH_NEXT.test(last.className)) {
|
|
196
|
+
moved.unshift(last);
|
|
197
|
+
last = last.previousElementSibling;
|
|
198
|
+
}
|
|
199
|
+
// Never empty a page for the sake of the rule — that would loop forever.
|
|
200
|
+
return moved.length < page.body.children.length ? moved : [];
|
|
201
|
+
}
|
|
202
|
+
let pending = null;
|
|
203
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
204
|
+
const block = pending ?? blocks[i];
|
|
205
|
+
pending = null;
|
|
206
|
+
// An explicit Sutra page break (===) just starts the next page.
|
|
207
|
+
if (block.className.indexOf('print-page-break') >= 0) {
|
|
208
|
+
if (page.body.children.length > 0)
|
|
209
|
+
page = newPage();
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
page.body.appendChild(block);
|
|
213
|
+
if (page.body.scrollHeight <= limit())
|
|
214
|
+
continue;
|
|
215
|
+
// Doesn't fit. Break the block itself where that's allowed...
|
|
216
|
+
const rest = breakBlock(block, limit() - block.offsetTop);
|
|
217
|
+
if (rest) {
|
|
218
|
+
page = newPage();
|
|
219
|
+
pending = rest; // re-measured next time round: a long speech can span three pages
|
|
220
|
+
i--;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// ...otherwise the whole block moves down, dragging any cue/heading introducing it.
|
|
224
|
+
page.body.removeChild(block);
|
|
225
|
+
const travelling = trailingKeepWithNext();
|
|
226
|
+
if (page.body.children.length === travelling.length) {
|
|
227
|
+
// Nothing would be left behind: this block is simply taller than a page, so let
|
|
228
|
+
// it sit here and overflow rather than bounce between empty pages forever.
|
|
229
|
+
page.body.appendChild(block);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
page = newPage();
|
|
233
|
+
for (let k = 0; k < travelling.length; k++)
|
|
234
|
+
page.body.appendChild(travelling[k]);
|
|
235
|
+
page.body.appendChild(block);
|
|
236
|
+
if (page.body.scrollHeight > limit()) {
|
|
237
|
+
const tail = breakBlock(block, limit() - block.offsetTop);
|
|
238
|
+
if (tail) {
|
|
239
|
+
page = newPage();
|
|
240
|
+
pending = tail;
|
|
241
|
+
i--;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Headers, once every block has landed: which page a scene starts on is only knowable
|
|
246
|
+
// after the fact (a heading can be pushed to the next page by the keep-with-next rule).
|
|
247
|
+
const sceneFirstPage = {};
|
|
248
|
+
for (let i = 0; i < pages.length; i++) {
|
|
249
|
+
const body = pages[i].body;
|
|
250
|
+
for (let k = 0; k < body.children.length; k++) {
|
|
251
|
+
const scene = body.children[k].getAttribute('data-scene-start');
|
|
252
|
+
if (scene !== null && sceneFirstPage[scene] === undefined)
|
|
253
|
+
sceneFirstPage[scene] = i;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (let i = 0; i < pages.length; i++) {
|
|
257
|
+
const p = pages[i];
|
|
258
|
+
const first = p.body.firstElementChild;
|
|
259
|
+
const scene = first ? first.getAttribute('data-scene') : null;
|
|
260
|
+
const startsHere = first !== null && first.getAttribute('data-scene-start') !== null;
|
|
261
|
+
if (scene !== null && !startsHere && sceneFirstPage[scene] !== undefined && sceneFirstPage[scene] < i) {
|
|
262
|
+
p.continued.textContent = scene + ' ' + opts.continuedSuffix;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
flow.parentNode?.removeChild(flow);
|
|
266
|
+
}
|
|
267
|
+
/** The `<script>` that runs `paginatePrintDocument` in the exported document, once web
|
|
268
|
+
* fonts have settled — measuring before that would paginate against fallback metrics
|
|
269
|
+
* (and Indic text reflows a lot between the two). Any failure leaves the un-paginated
|
|
270
|
+
* flow exactly as it was, so the export degrades to the browser's own page breaks
|
|
271
|
+
* (which styles/pagination.ts's CSS rules still govern) instead of producing nothing. */
|
|
272
|
+
export function printPaginatorScript(opts) {
|
|
273
|
+
return `<script>
|
|
274
|
+
(function () {
|
|
275
|
+
var run = function () {
|
|
276
|
+
try {
|
|
277
|
+
(${paginatePrintDocument.toString()})(${JSON.stringify(opts)});
|
|
278
|
+
} catch (e) {
|
|
279
|
+
console.error('Sutrata: pagination failed, falling back to browser page breaks', e);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
if (document.fonts && document.fonts.ready) {
|
|
283
|
+
document.fonts.ready.then(run, run);
|
|
284
|
+
} else {
|
|
285
|
+
run();
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
</script>`;
|
|
289
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const BASE_FEATURE = `---
|
|
2
|
+
title: Untitled Feature
|
|
3
|
+
author:
|
|
4
|
+
page: A4
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ACT ONE
|
|
8
|
+
|
|
9
|
+
## INT. LOCATION - DAY {#sc1}
|
|
10
|
+
=
|
|
11
|
+
|
|
12
|
+
`;
|
|
13
|
+
const BASE_TV_PILOT = `---
|
|
14
|
+
title: Untitled Pilot
|
|
15
|
+
author:
|
|
16
|
+
page: A4
|
|
17
|
+
series:
|
|
18
|
+
episode: 1
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## COLD OPEN
|
|
22
|
+
|
|
23
|
+
## INT. LOCATION - DAY {#sc1}
|
|
24
|
+
=
|
|
25
|
+
|
|
26
|
+
`;
|
|
27
|
+
const BASE_SHORT = `---
|
|
28
|
+
title: Untitled Short
|
|
29
|
+
author:
|
|
30
|
+
page: A4
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## INT. LOCATION - DAY {#sc1}
|
|
34
|
+
=
|
|
35
|
+
|
|
36
|
+
`;
|
|
37
|
+
const BASE_WEB_SERIES = `---
|
|
38
|
+
title: Untitled Web Series
|
|
39
|
+
author:
|
|
40
|
+
page: A4
|
|
41
|
+
series:
|
|
42
|
+
episode: 1
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## INT. LOCATION - DAY {#sc1}
|
|
46
|
+
=
|
|
47
|
+
|
|
48
|
+
`;
|
|
49
|
+
const BASE_STAGE_PLAY = `---
|
|
50
|
+
title: Untitled Play
|
|
51
|
+
author:
|
|
52
|
+
page: A4
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
# ACT ONE
|
|
56
|
+
|
|
57
|
+
## Scene 1 {#sc1}
|
|
58
|
+
|
|
59
|
+
`;
|
|
60
|
+
export const TEMPLATES = {
|
|
61
|
+
'feature': BASE_FEATURE,
|
|
62
|
+
'tv-pilot': BASE_TV_PILOT,
|
|
63
|
+
'short': BASE_SHORT,
|
|
64
|
+
'web-series': BASE_WEB_SERIES,
|
|
65
|
+
'stage-play': BASE_STAGE_PLAY,
|
|
66
|
+
};
|