@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,820 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Document,
|
|
3
|
+
Packer,
|
|
4
|
+
Paragraph,
|
|
5
|
+
TextRun,
|
|
6
|
+
Table,
|
|
7
|
+
TableRow,
|
|
8
|
+
TableCell,
|
|
9
|
+
WidthType,
|
|
10
|
+
} from 'docx'
|
|
11
|
+
import type { DocumentNode, SceneHeadingNode, ContentNode } from '@sutrata/parser'
|
|
12
|
+
import { resolveStyle } from '../styles/registry'
|
|
13
|
+
import { styleToPrintCss } from '../styles/print-adapter'
|
|
14
|
+
import { embedFontFacesFor } from '../styles/embed-fonts'
|
|
15
|
+
import { SCRIPT_UNICODE_RANGES } from '../styles/script-ranges'
|
|
16
|
+
import { printPaginatorScript } from './print-paginator'
|
|
17
|
+
// CSS absolute units are fixed at 96px/in, print included — so a page box sized in px
|
|
18
|
+
// from the paper's inches lands exactly on one sheet.
|
|
19
|
+
const CSS_PX_PER_IN = 96
|
|
20
|
+
import { DEFAULT_COVER_LAYOUT } from '../styles/types'
|
|
21
|
+
import type { ScreenplayStyleDefinition } from '../styles/types'
|
|
22
|
+
|
|
23
|
+
// CSV Escape Helper
|
|
24
|
+
function esc(val: string): string {
|
|
25
|
+
const str = val ? String(val) : ''
|
|
26
|
+
return `"${str.replace(/"/g, '""')}"`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkflowSceneData {
|
|
30
|
+
number: string
|
|
31
|
+
heading: string
|
|
32
|
+
synopsis: string
|
|
33
|
+
actors: string
|
|
34
|
+
status: string
|
|
35
|
+
estDuration: string
|
|
36
|
+
shots: string[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CharacterStats {
|
|
40
|
+
name: string
|
|
41
|
+
actor: string
|
|
42
|
+
sceneCount: number
|
|
43
|
+
scenes: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extract all relevant workflow data from AST */
|
|
47
|
+
export function extractWorkflowData(ast: DocumentNode): {
|
|
48
|
+
scenes: WorkflowSceneData[]
|
|
49
|
+
characters: CharacterStats[]
|
|
50
|
+
} {
|
|
51
|
+
const scenes: WorkflowSceneData[] = []
|
|
52
|
+
const charMap = new Map<string, { sceneCount: number; scenes: string[] }>()
|
|
53
|
+
|
|
54
|
+
// Resolve character registry from frontmatter
|
|
55
|
+
const fmCharacters = (ast.frontmatter?.data as Record<string, unknown> | undefined)?.[
|
|
56
|
+
'characters'
|
|
57
|
+
] as Record<string, string> | undefined
|
|
58
|
+
|
|
59
|
+
let sceneCount = 0
|
|
60
|
+
for (const node of ast.children) {
|
|
61
|
+
if (node.type === 'scene-heading') {
|
|
62
|
+
sceneCount++
|
|
63
|
+
const rawHeading = node.text.toUpperCase()
|
|
64
|
+
|
|
65
|
+
// Find metadata keys
|
|
66
|
+
const numMeta = node.metadata.find(m => m.key === 'number')?.value
|
|
67
|
+
const sceneNum = numMeta || node.id || String(sceneCount)
|
|
68
|
+
const synopsis = node.metadata.find(m => m.key === 'synopsis')?.value ?? ''
|
|
69
|
+
const actors = node.metadata.find(m => m.key === 'actors')?.value ?? ''
|
|
70
|
+
const status = node.metadata.find(m => m.key === 'status')?.value ?? ''
|
|
71
|
+
const estDuration = node.metadata.find(m => m.key === 'est-duration')?.value ?? ''
|
|
72
|
+
|
|
73
|
+
// Collect all shot metadata lines
|
|
74
|
+
const shotLines = node.metadata.filter(m => m.key === 'shots').map(m => m.value)
|
|
75
|
+
|
|
76
|
+
scenes.push({
|
|
77
|
+
number: sceneNum,
|
|
78
|
+
heading: rawHeading,
|
|
79
|
+
synopsis,
|
|
80
|
+
actors,
|
|
81
|
+
status,
|
|
82
|
+
estDuration,
|
|
83
|
+
shots: shotLines,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Walk this scene's children to find characters
|
|
87
|
+
const sceneLabel = `${sceneNum}: ${rawHeading}`
|
|
88
|
+
const seenChars = new Set<string>()
|
|
89
|
+
|
|
90
|
+
const checkChar = (charName: string) => {
|
|
91
|
+
const name = charName.trim().toUpperCase()
|
|
92
|
+
if (!name) return
|
|
93
|
+
if (seenChars.has(name)) return
|
|
94
|
+
seenChars.add(name)
|
|
95
|
+
|
|
96
|
+
if (!charMap.has(name)) {
|
|
97
|
+
charMap.set(name, { sceneCount: 0, scenes: [] })
|
|
98
|
+
}
|
|
99
|
+
const stats = charMap.get(name)!
|
|
100
|
+
stats.sceneCount++
|
|
101
|
+
stats.scenes.push(sceneLabel)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const sc of node.children) {
|
|
105
|
+
if (sc.type === 'character') {
|
|
106
|
+
checkChar(sc.name)
|
|
107
|
+
} else if (sc.type === 'dual-dialogue') {
|
|
108
|
+
checkChar(sc.left.name)
|
|
109
|
+
checkChar(sc.right.name)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build character list
|
|
116
|
+
const characters: CharacterStats[] = []
|
|
117
|
+
for (const [name, stats] of charMap.entries()) {
|
|
118
|
+
const actor = fmCharacters?.[name] || fmCharacters?.[name.toLowerCase()] || ''
|
|
119
|
+
characters.push({
|
|
120
|
+
name,
|
|
121
|
+
actor,
|
|
122
|
+
sceneCount: stats.sceneCount,
|
|
123
|
+
scenes: stats.scenes,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Sort characters by occurrence count descending
|
|
128
|
+
characters.sort((a, b) => b.sceneCount - a.sceneCount)
|
|
129
|
+
|
|
130
|
+
return { scenes, characters }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------
|
|
134
|
+
// CSV Exporters
|
|
135
|
+
// ---------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export function exportOneLinerCsv(scenes: WorkflowSceneData[]): string {
|
|
138
|
+
const header = 'Scene #,Heading,Synopsis,Actors,Status,Est-Duration\n'
|
|
139
|
+
const rows = scenes
|
|
140
|
+
.map(
|
|
141
|
+
s =>
|
|
142
|
+
`${esc(s.number)},${esc(s.heading)},${esc(s.synopsis)},${esc(s.actors)},${esc(s.status)},${esc(s.estDuration)}`
|
|
143
|
+
)
|
|
144
|
+
.join('\n')
|
|
145
|
+
return header + rows
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function exportShotListCsv(scenes: WorkflowSceneData[]): string {
|
|
149
|
+
const header = 'Scene #,Heading,Actors,Shot Details\n'
|
|
150
|
+
const rows: string[] = []
|
|
151
|
+
for (const s of scenes) {
|
|
152
|
+
if (s.shots.length === 0) {
|
|
153
|
+
rows.push(`${esc(s.number)},${esc(s.heading)},${esc(s.actors)},""`)
|
|
154
|
+
} else {
|
|
155
|
+
for (const shot of s.shots) {
|
|
156
|
+
rows.push(`${esc(s.number)},${esc(s.heading)},${esc(s.actors)},${esc(shot)}`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return header + rows.join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function exportCastReportCsv(characters: CharacterStats[]): string {
|
|
164
|
+
const header = 'Character,Actor Name,Scene Occurrences,Scenes List\n'
|
|
165
|
+
const rows = characters
|
|
166
|
+
.map(
|
|
167
|
+
c =>
|
|
168
|
+
`${esc(c.name)},${esc(c.actor)},${c.sceneCount},${esc(c.scenes.join('; '))}`
|
|
169
|
+
)
|
|
170
|
+
.join('\n')
|
|
171
|
+
return header + rows
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------
|
|
175
|
+
// DOCX Table Exporters
|
|
176
|
+
// ---------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function makeCell(text: string, bold = false, widthPercent = 0): TableCell {
|
|
179
|
+
return new TableCell({
|
|
180
|
+
width: widthPercent > 0 ? { size: widthPercent, type: WidthType.PERCENTAGE } : undefined,
|
|
181
|
+
children: [
|
|
182
|
+
new Paragraph({
|
|
183
|
+
children: [
|
|
184
|
+
new TextRun({
|
|
185
|
+
text,
|
|
186
|
+
bold,
|
|
187
|
+
font: 'Courier Prime',
|
|
188
|
+
size: '10pt',
|
|
189
|
+
}),
|
|
190
|
+
],
|
|
191
|
+
}),
|
|
192
|
+
],
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function exportOneLinerDocx(scenes: WorkflowSceneData[]): Promise<Blob> {
|
|
197
|
+
const headerRow = new TableRow({
|
|
198
|
+
children: [
|
|
199
|
+
makeCell('Scene #', true, 10),
|
|
200
|
+
makeCell('Heading', true, 25),
|
|
201
|
+
makeCell('Synopsis', true, 35),
|
|
202
|
+
makeCell('Actors', true, 15),
|
|
203
|
+
makeCell('Status', true, 10),
|
|
204
|
+
makeCell('Duration', true, 5),
|
|
205
|
+
],
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const rows = [headerRow]
|
|
209
|
+
for (const s of scenes) {
|
|
210
|
+
rows.push(
|
|
211
|
+
new TableRow({
|
|
212
|
+
children: [
|
|
213
|
+
makeCell(s.number, false, 10),
|
|
214
|
+
makeCell(s.heading, false, 25),
|
|
215
|
+
makeCell(s.synopsis, false, 35),
|
|
216
|
+
makeCell(s.actors, false, 15),
|
|
217
|
+
makeCell(s.status, false, 10),
|
|
218
|
+
makeCell(s.estDuration, false, 5),
|
|
219
|
+
],
|
|
220
|
+
})
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const table = new Table({
|
|
225
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
226
|
+
rows,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const doc = new Document({
|
|
230
|
+
sections: [
|
|
231
|
+
{
|
|
232
|
+
properties: {},
|
|
233
|
+
children: [
|
|
234
|
+
new Paragraph({
|
|
235
|
+
children: [
|
|
236
|
+
new TextRun({
|
|
237
|
+
text: 'ONE-LINER SCHEDULE / SCENE BREAKDOWN',
|
|
238
|
+
bold: true,
|
|
239
|
+
font: 'Courier Prime',
|
|
240
|
+
size: '14pt',
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
spacing: { after: 288 },
|
|
244
|
+
}),
|
|
245
|
+
table,
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return Packer.toBlob(doc)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function exportShotListDocx(scenes: WorkflowSceneData[]): Promise<Blob> {
|
|
255
|
+
const headerRow = new TableRow({
|
|
256
|
+
children: [
|
|
257
|
+
makeCell('Scene #', true, 10),
|
|
258
|
+
makeCell('Heading', true, 25),
|
|
259
|
+
makeCell('Actors', true, 20),
|
|
260
|
+
makeCell('Shot Details', true, 45),
|
|
261
|
+
],
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const rows = [headerRow]
|
|
265
|
+
for (const s of scenes) {
|
|
266
|
+
const shotDetails = s.shots.join('\n')
|
|
267
|
+
rows.push(
|
|
268
|
+
new TableRow({
|
|
269
|
+
children: [
|
|
270
|
+
makeCell(s.number, false, 10),
|
|
271
|
+
makeCell(s.heading, false, 25),
|
|
272
|
+
makeCell(s.actors, false, 20),
|
|
273
|
+
makeCell(shotDetails || 'No shots specified', false, 45),
|
|
274
|
+
],
|
|
275
|
+
})
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const table = new Table({
|
|
280
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
281
|
+
rows,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const doc = new Document({
|
|
285
|
+
sections: [
|
|
286
|
+
{
|
|
287
|
+
properties: {},
|
|
288
|
+
children: [
|
|
289
|
+
new Paragraph({
|
|
290
|
+
children: [
|
|
291
|
+
new TextRun({
|
|
292
|
+
text: 'SHOT LIST REPORT',
|
|
293
|
+
bold: true,
|
|
294
|
+
font: 'Courier Prime',
|
|
295
|
+
size: '14pt',
|
|
296
|
+
}),
|
|
297
|
+
],
|
|
298
|
+
spacing: { after: 288 },
|
|
299
|
+
}),
|
|
300
|
+
table,
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
return Packer.toBlob(doc)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function exportCastReportDocx(characters: CharacterStats[]): Promise<Blob> {
|
|
310
|
+
const headerRow = new TableRow({
|
|
311
|
+
children: [
|
|
312
|
+
makeCell('Character', true, 25),
|
|
313
|
+
makeCell('Actor Name', true, 25),
|
|
314
|
+
makeCell('Occurrences', true, 15),
|
|
315
|
+
makeCell('Scenes List', true, 35),
|
|
316
|
+
],
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const rows = [headerRow]
|
|
320
|
+
for (const c of characters) {
|
|
321
|
+
rows.push(
|
|
322
|
+
new TableRow({
|
|
323
|
+
children: [
|
|
324
|
+
makeCell(c.name, false, 25),
|
|
325
|
+
makeCell(c.actor || 'TBD', false, 25),
|
|
326
|
+
makeCell(String(c.sceneCount), false, 15),
|
|
327
|
+
makeCell(c.scenes.join(', '), false, 35),
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const table = new Table({
|
|
334
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
335
|
+
rows,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const doc = new Document({
|
|
339
|
+
sections: [
|
|
340
|
+
{
|
|
341
|
+
properties: {},
|
|
342
|
+
children: [
|
|
343
|
+
new Paragraph({
|
|
344
|
+
children: [
|
|
345
|
+
new TextRun({
|
|
346
|
+
text: 'CHARACTER OCCURRENCE / CAST REPORT',
|
|
347
|
+
bold: true,
|
|
348
|
+
font: 'Courier Prime',
|
|
349
|
+
size: '14pt',
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
352
|
+
spacing: { after: 288 },
|
|
353
|
+
}),
|
|
354
|
+
table,
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
return Packer.toBlob(doc)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------
|
|
364
|
+
// Print Preview Page Generator (HTML)
|
|
365
|
+
// ---------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
export async function openPrintPreview(scenes: WorkflowSceneData[], title = 'One-Liner Schedule') {
|
|
368
|
+
// Open synchronously (inside the click) so popup blockers allow it, then embed fonts.
|
|
369
|
+
const win = window.open('', '_blank')
|
|
370
|
+
if (!win) return
|
|
371
|
+
// Noto Sans carries every ISO 15919 letter and mark, for romanized reports; the new
|
|
372
|
+
// window has no stylesheet of its own.
|
|
373
|
+
const fontFaceCss = await embedFontFacesFor(['Noto Sans'])
|
|
374
|
+
|
|
375
|
+
const rowsHtml = scenes
|
|
376
|
+
.map(
|
|
377
|
+
s => `
|
|
378
|
+
<tr>
|
|
379
|
+
<td>${s.number}</td>
|
|
380
|
+
<td><strong>${s.heading}</strong></td>
|
|
381
|
+
<td>${s.synopsis || '<span class="empty">No synopsis</span>'}</td>
|
|
382
|
+
<td>${s.actors || '-'}</td>
|
|
383
|
+
<td><span class="status-badge status-${s.status.toLowerCase().replace(/\s+/g, '-') || 'none'}">${s.status || '-'}</span></td>
|
|
384
|
+
<td>${s.estDuration || '-'}</td>
|
|
385
|
+
</tr>
|
|
386
|
+
`
|
|
387
|
+
)
|
|
388
|
+
.join('')
|
|
389
|
+
|
|
390
|
+
win.document.write(`
|
|
391
|
+
<!DOCTYPE html>
|
|
392
|
+
<html>
|
|
393
|
+
<head>
|
|
394
|
+
<title>${title}</title>
|
|
395
|
+
<style>
|
|
396
|
+
${fontFaceCss}
|
|
397
|
+
body {
|
|
398
|
+
font-family: 'Noto Sans', 'Courier Prime', sans-serif;
|
|
399
|
+
margin: 40px;
|
|
400
|
+
color: #333;
|
|
401
|
+
}
|
|
402
|
+
h1 {
|
|
403
|
+
font-family: 'Courier Prime', sans-serif;
|
|
404
|
+
text-align: center;
|
|
405
|
+
font-size: 20px;
|
|
406
|
+
margin-bottom: 30px;
|
|
407
|
+
text-transform: uppercase;
|
|
408
|
+
letter-spacing: 1px;
|
|
409
|
+
}
|
|
410
|
+
table {
|
|
411
|
+
width: 100%;
|
|
412
|
+
border-collapse: collapse;
|
|
413
|
+
font-size: 13px;
|
|
414
|
+
}
|
|
415
|
+
th, td {
|
|
416
|
+
border: 1px solid #ccc;
|
|
417
|
+
padding: 10px 12px;
|
|
418
|
+
text-align: left;
|
|
419
|
+
vertical-align: top;
|
|
420
|
+
}
|
|
421
|
+
th {
|
|
422
|
+
background-color: #f7f7f5;
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
}
|
|
425
|
+
.empty {
|
|
426
|
+
color: #999;
|
|
427
|
+
font-style: italic;
|
|
428
|
+
}
|
|
429
|
+
.status-badge {
|
|
430
|
+
display: inline-block;
|
|
431
|
+
padding: 2px 6px;
|
|
432
|
+
border-radius: 4px;
|
|
433
|
+
font-size: 11px;
|
|
434
|
+
font-weight: 600;
|
|
435
|
+
background: #f0f0f0;
|
|
436
|
+
border: 1px solid #ddd;
|
|
437
|
+
}
|
|
438
|
+
.status-done, .status-revised { background: #e6f4ea; color: #137333; border-color: #ceead6; }
|
|
439
|
+
.status-wip { background: #fef7e0; color: #b06000; border-color: #feebc8; }
|
|
440
|
+
.status-draft { background: #fce8e6; color: #c5221f; border-color: #fad2cf; }
|
|
441
|
+
|
|
442
|
+
@media print {
|
|
443
|
+
body { margin: 20px; }
|
|
444
|
+
button { display: none !important; }
|
|
445
|
+
th { background-color: #f0f0f0 !important; -webkit-print-color-adjust: exact; }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.print-btn-bar {
|
|
449
|
+
text-align: right;
|
|
450
|
+
margin-bottom: 20px;
|
|
451
|
+
}
|
|
452
|
+
.print-btn {
|
|
453
|
+
background: #1976d2;
|
|
454
|
+
color: white;
|
|
455
|
+
border: none;
|
|
456
|
+
padding: 8px 16px;
|
|
457
|
+
font-size: 13px;
|
|
458
|
+
border-radius: 4px;
|
|
459
|
+
cursor: pointer;
|
|
460
|
+
font-weight: 600;
|
|
461
|
+
}
|
|
462
|
+
.print-btn:hover { background: #1565c0; }
|
|
463
|
+
</style>
|
|
464
|
+
</head>
|
|
465
|
+
<body>
|
|
466
|
+
<div class="print-btn-bar">
|
|
467
|
+
<button class="print-btn" onclick="window.print()">Print Schedule</button>
|
|
468
|
+
</div>
|
|
469
|
+
<h1>${title}</h1>
|
|
470
|
+
<table>
|
|
471
|
+
<thead>
|
|
472
|
+
<tr>
|
|
473
|
+
<th style="width: 8%">Scene #</th>
|
|
474
|
+
<th style="width: 25%">Heading</th>
|
|
475
|
+
<th style="width: 35%">Synopsis</th>
|
|
476
|
+
<th style="width: 15%">Actors</th>
|
|
477
|
+
<th style="width: 12%">Status</th>
|
|
478
|
+
<th style="width: 5%">Duration</th>
|
|
479
|
+
</tr>
|
|
480
|
+
</thead>
|
|
481
|
+
<tbody>
|
|
482
|
+
${rowsHtml}
|
|
483
|
+
</tbody>
|
|
484
|
+
</table>
|
|
485
|
+
</body>
|
|
486
|
+
</html>
|
|
487
|
+
`)
|
|
488
|
+
win.document.close()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Print Screenplay Preview in formatted style. `styleOverride` picks a style id for
|
|
492
|
+
* this export only (without touching the document); otherwise the document's
|
|
493
|
+
* frontmatter `style:` field is used, falling back to the default style. `skipNotes`
|
|
494
|
+
* omits both block-level and inline `[[ ]]` notes from the exported document. */
|
|
495
|
+
export async function openScreenplayPrintPreview(
|
|
496
|
+
ast: DocumentNode,
|
|
497
|
+
title = 'Screenplay',
|
|
498
|
+
styleOverride?: string,
|
|
499
|
+
customStyles: ScreenplayStyleDefinition[] = [],
|
|
500
|
+
skipNotes = false,
|
|
501
|
+
) {
|
|
502
|
+
const html = await buildScreenplayPrintHtml(ast, title, styleOverride, customStyles, skipNotes)
|
|
503
|
+
|
|
504
|
+
// Desktop shell: generate a real vector-text PDF via the platform's browser
|
|
505
|
+
// engine (WebView2 PrintToPdf on Windows) instead of the OS print dialog,
|
|
506
|
+
// which avoids the rasterized/bloated output some print-to-PDF drivers produce.
|
|
507
|
+
const api = window.desktopAPI
|
|
508
|
+
if (api?.exportScreenplayToPdf && api?.showSavePdfDialog) {
|
|
509
|
+
const defaultName = `${title.replace(/[\\/:*?"<>|]/g, '_')}.pdf`
|
|
510
|
+
const path = await api.showSavePdfDialog(defaultName)
|
|
511
|
+
if (!path) return
|
|
512
|
+
await api.exportScreenplayToPdf(html, path)
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const win = window.open('', '_blank')
|
|
517
|
+
if (!win) return
|
|
518
|
+
win.document.write(html)
|
|
519
|
+
win.document.close()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function buildScreenplayPrintHtml(
|
|
523
|
+
ast: DocumentNode,
|
|
524
|
+
title: string,
|
|
525
|
+
styleOverride?: string,
|
|
526
|
+
customStyles: ScreenplayStyleDefinition[] = [],
|
|
527
|
+
skipNotes = false,
|
|
528
|
+
): Promise<string> {
|
|
529
|
+
const frontmatter = ast.frontmatter?.data as Record<string, unknown> | undefined
|
|
530
|
+
const defaultLang = (frontmatter?.['lang'] as string) ?? 'en'
|
|
531
|
+
const style = resolveStyle(styleOverride ?? (frontmatter?.['style'] as string | undefined), customStyles)
|
|
532
|
+
// Noto Sans is the Latin fallback in the print font stack (print-adapter.ts), so embed it
|
|
533
|
+
// too: the desktop's hidden print webview cannot load /fonts/ paths on its own.
|
|
534
|
+
const embeddedFontFaceCss = await embedFontFacesFor([...new Set([
|
|
535
|
+
style.fontFamily,
|
|
536
|
+
...Object.values(style.complexScriptFontOverrides ?? {}),
|
|
537
|
+
'Noto Sans',
|
|
538
|
+
])])
|
|
539
|
+
|
|
540
|
+
// Paper the export prints on — frontmatter's `page:`, the same field docx-exporter.ts
|
|
541
|
+
// reads. print-paginator.ts needs it in CSS px to size its page boxes.
|
|
542
|
+
const pageFormat = (frontmatter?.['page'] as string)?.toLowerCase() === 'a4' ? 'a4' : 'letter'
|
|
543
|
+
const pageHeightIn = pageFormat === 'a4' ? 11.69 : 11
|
|
544
|
+
|
|
545
|
+
const watermarkValue = typeof frontmatter?.['watermark'] === 'string' ? frontmatter['watermark'].trim() : ''
|
|
546
|
+
const watermarkHtml = watermarkValue ? `<div class="print-watermark">${escapeHtml(watermarkValue)}</div>` : ''
|
|
547
|
+
|
|
548
|
+
// Render cover page
|
|
549
|
+
let coverPageHtml = ''
|
|
550
|
+
if (frontmatter && (frontmatter['title'] || frontmatter['author'])) {
|
|
551
|
+
const titleText = String(frontmatter['title'] || 'Untitled')
|
|
552
|
+
const authorText = String(frontmatter['author'] || '')
|
|
553
|
+
const loglineText = String(frontmatter['logline'] || '')
|
|
554
|
+
const dateText = String(frontmatter['date'] || '')
|
|
555
|
+
const contactText = String(frontmatter['contact'] || '')
|
|
556
|
+
|
|
557
|
+
// Both cover layouts share this markup — the two blocks are what 'corners' pushes
|
|
558
|
+
// into opposite corners and 'centered' just stacks (see print-adapter.ts). The
|
|
559
|
+
// byline differs: 'corners' prints one "by <author>" line, 'centered' keeps the
|
|
560
|
+
// classic "by" / author on separate lines.
|
|
561
|
+
const corners = (style.coverLayout ?? DEFAULT_COVER_LAYOUT) === 'corners'
|
|
562
|
+
const bylineHtml = corners
|
|
563
|
+
? `<div class="print-cover-author">by ${textHtml(authorText)}</div>`
|
|
564
|
+
: `<div class="print-cover-by">by</div>
|
|
565
|
+
<div class="print-cover-author">${textHtml(authorText)}</div>`
|
|
566
|
+
coverPageHtml = `
|
|
567
|
+
<div class="print-cover-page print-cover-${corners ? 'corners' : 'centered'}">
|
|
568
|
+
<div class="print-cover-top">
|
|
569
|
+
<div class="print-cover-title">${textHtml(titleText)}</div>
|
|
570
|
+
${bylineHtml}
|
|
571
|
+
${loglineText ? `<div class="print-cover-logline">${textHtml(loglineText)}</div>` : ''}
|
|
572
|
+
</div>
|
|
573
|
+
<div class="print-cover-bottom">
|
|
574
|
+
${dateText ? `<div class="print-cover-date">${escapeHtml(dateText)}</div>` : ''}
|
|
575
|
+
${contactText ? `<div class="print-cover-contact">${escapeHtml(contactText)}</div>` : ''}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
`
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Render screenplay nodes
|
|
582
|
+
let bodyHtml = ''
|
|
583
|
+
|
|
584
|
+
function renderSpans(spans: any[]): string {
|
|
585
|
+
return spans.map(s => {
|
|
586
|
+
if (s.type === 'text') return textHtml(s.text)
|
|
587
|
+
if (s.type === 'bold') return `<strong>${renderSpans(s.spans)}</strong>`
|
|
588
|
+
if (s.type === 'italic') return `<em>${renderSpans(s.spans)}</em>`
|
|
589
|
+
if (s.type === 'underline') return `<u>${renderSpans(s.spans)}</u>`
|
|
590
|
+
if (s.type === 'note') return skipNotes ? '' : `<span class="print-note">[[ ${textHtml(s.text)} ]]</span>`
|
|
591
|
+
return ''
|
|
592
|
+
}).join('')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function escapeHtml(text: string): string {
|
|
596
|
+
return text
|
|
597
|
+
.replace(/&/g, '&')
|
|
598
|
+
.replace(/</g, '<')
|
|
599
|
+
.replace(/>/g, '>')
|
|
600
|
+
.replace(/"/g, '"')
|
|
601
|
+
.replace(/'/g, ''')
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Whether `text` contains a script this style forces italic by default
|
|
605
|
+
* (`italicScripts`, e.g. Traditional's Tamil) — mirrors docx-exporter.ts's
|
|
606
|
+
* createRuns forced-italic behavior, which the live editor and DOCX export
|
|
607
|
+
* already have but the print/PDF HTML never did (no per-span language tagging
|
|
608
|
+
* there), so Tamil rendered upright in exported PDFs even under Traditional. */
|
|
609
|
+
function forceItalic(text: string): boolean {
|
|
610
|
+
if (!style.italicScripts?.length) return false
|
|
611
|
+
return style.italicScripts.some(code => SCRIPT_UNICODE_RANGES[code]?.test(text))
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** escapeHtml, plus <i>-wrapping when forceItalic(text) — italic always,
|
|
615
|
+
* even inside otherwise-bold/underlined elements like Traditional's scene
|
|
616
|
+
* headings and character cues, matching the confirmed docx-exporter.ts behavior. */
|
|
617
|
+
function textHtml(text: string): string {
|
|
618
|
+
const escaped = escapeHtml(text)
|
|
619
|
+
return forceItalic(text) ? `<i>${escaped}</i>` : escaped
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function renderContentNode(n: ContentNode): string {
|
|
623
|
+
if (n.type === 'action') {
|
|
624
|
+
return `<div class="print-action">${renderSpans(n.spans)}</div>`
|
|
625
|
+
}
|
|
626
|
+
if (n.type === 'character') {
|
|
627
|
+
const ext = n.extension ? ` ${n.extension.toUpperCase()}` : ''
|
|
628
|
+
const childHtml = n.children.map(c => {
|
|
629
|
+
if (c.type === 'parenthetical') {
|
|
630
|
+
return `<div class="print-parenthetical">(${textHtml(c.text)})</div>`
|
|
631
|
+
} else {
|
|
632
|
+
return `<div class="print-dialogue">${renderSpans(c.spans)}</div>`
|
|
633
|
+
}
|
|
634
|
+
}).join('')
|
|
635
|
+
|
|
636
|
+
return `
|
|
637
|
+
<div class="print-character-block">
|
|
638
|
+
<div class="print-character">${textHtml(n.name.toUpperCase())}${textHtml(ext)}</div>
|
|
639
|
+
${childHtml}
|
|
640
|
+
</div>
|
|
641
|
+
`
|
|
642
|
+
}
|
|
643
|
+
if (n.type === 'transition') {
|
|
644
|
+
return `<div class="print-transition">${textHtml(n.text.toUpperCase())}</div>`
|
|
645
|
+
}
|
|
646
|
+
if (n.type === 'centered') {
|
|
647
|
+
return `<div class="print-centered">${textHtml(n.text)}</div>`
|
|
648
|
+
}
|
|
649
|
+
if (n.type === 'lyrics') {
|
|
650
|
+
return `<div class="print-lyrics">${renderSpans(n.spans)}</div>`
|
|
651
|
+
}
|
|
652
|
+
if (n.type === 'note') {
|
|
653
|
+
return skipNotes ? '' : `<div class="print-note">${textHtml(n.text)}</div>`
|
|
654
|
+
}
|
|
655
|
+
if (n.type === 'comment') {
|
|
656
|
+
return `<div class="print-comment"><!-- ${textHtml(n.text)} --></div>`
|
|
657
|
+
}
|
|
658
|
+
if (n.type === 'page-break') {
|
|
659
|
+
return `<div class="print-page-break"></div>`
|
|
660
|
+
}
|
|
661
|
+
if (n.type === 'dual-dialogue') {
|
|
662
|
+
const leftChar = n.left
|
|
663
|
+
const leftExt = leftChar.extension ? ` ${leftChar.extension.toUpperCase()}` : ''
|
|
664
|
+
const leftChildHtml = leftChar.children.map(c => {
|
|
665
|
+
if (c.type === 'parenthetical') {
|
|
666
|
+
return `<div class="print-parenthetical">(${textHtml(c.text)})</div>`
|
|
667
|
+
} else {
|
|
668
|
+
return `<div class="print-dialogue">${renderSpans(c.spans)}</div>`
|
|
669
|
+
}
|
|
670
|
+
}).join('')
|
|
671
|
+
|
|
672
|
+
const rightChar = n.right
|
|
673
|
+
const rightExt = rightChar.extension ? ` ${rightChar.extension.toUpperCase()}` : ''
|
|
674
|
+
const rightChildHtml = rightChar.children.map(c => {
|
|
675
|
+
if (c.type === 'parenthetical') {
|
|
676
|
+
return `<div class="print-parenthetical">(${textHtml(c.text)})</div>`
|
|
677
|
+
} else {
|
|
678
|
+
return `<div class="print-dialogue">${renderSpans(c.spans)}</div>`
|
|
679
|
+
}
|
|
680
|
+
}).join('')
|
|
681
|
+
|
|
682
|
+
return `
|
|
683
|
+
<div class="print-dual-dialogue">
|
|
684
|
+
<div class="print-dual-column print-left-column">
|
|
685
|
+
<div class="print-character">${textHtml(leftChar.name.toUpperCase())}${textHtml(leftExt)}</div>
|
|
686
|
+
${leftChildHtml}
|
|
687
|
+
</div>
|
|
688
|
+
<div class="print-dual-column print-right-column">
|
|
689
|
+
<div class="print-character">${textHtml(rightChar.name.toUpperCase())}${textHtml(rightExt)}</div>
|
|
690
|
+
${rightChildHtml}
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
`
|
|
694
|
+
}
|
|
695
|
+
return ''
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Stamps the scene a block belongs to onto its outermost element, which is how
|
|
699
|
+
* print-paginator.ts decides whether a page opens mid-scene (-> "8 CONTINUED:") or on
|
|
700
|
+
* a fresh one. The label is the scene's own number when it has one, else its position
|
|
701
|
+
* in the script — the same thing the heading prints on the right. */
|
|
702
|
+
function tagScene(html: string, label: string, isHeading = false): string {
|
|
703
|
+
const attrs = ` data-scene="${escapeHtml(label)}"${isHeading ? ` data-scene-start="${escapeHtml(label)}"` : ''}`
|
|
704
|
+
return html.replace('<div', `<div${attrs}`)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let sceneOrdinal = 0
|
|
708
|
+
for (const node of ast.children) {
|
|
709
|
+
if (node.type === 'section') {
|
|
710
|
+
bodyHtml += `<div class="print-section print-sect-${node.level}">${textHtml(node.text)}</div>`
|
|
711
|
+
for (const sub of node.children) {
|
|
712
|
+
bodyHtml += renderContentNode(sub)
|
|
713
|
+
}
|
|
714
|
+
} else if (node.type === 'scene-heading') {
|
|
715
|
+
sceneOrdinal++
|
|
716
|
+
const sceneLabel = node.id || String(sceneOrdinal)
|
|
717
|
+
bodyHtml += tagScene(`
|
|
718
|
+
<div class="print-scene-heading">
|
|
719
|
+
<span class="print-scene-title">${textHtml(node.text.toUpperCase())}</span>
|
|
720
|
+
${node.id ? `<span class="print-scene-num">${escapeHtml(node.id)}</span>` : ''}
|
|
721
|
+
</div>
|
|
722
|
+
`, sceneLabel, true)
|
|
723
|
+
for (const sub of node.children) {
|
|
724
|
+
bodyHtml += tagScene(renderContentNode(sub), sceneLabel)
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
bodyHtml += renderContentNode(node)
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return `
|
|
732
|
+
<!DOCTYPE html>
|
|
733
|
+
<html>
|
|
734
|
+
<head>
|
|
735
|
+
<title>${escapeHtml(title)}</title>
|
|
736
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
737
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
738
|
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Noto+Sans+Devanagari&family=Noto+Sans+Tamil&family=Noto+Sans+Telugu&family=Noto+Sans+Kannada&family=Noto+Sans+Malayalam&family=Noto+Sans+Bengali&family=Noto+Sans+Gujarati&family=Noto+Sans+Gurmukhi&family=Noto+Sans+Oriya&family=Noto+Serif:ital,wght@0,400;0,700;1,400&family=Noto+Serif+Devanagari&family=Noto+Serif+Tamil:ital@0;1&family=Noto+Serif+Telugu&family=Noto+Serif+Kannada&family=Noto+Serif+Malayalam&family=Noto+Serif+Bengali&family=Noto+Serif+Gujarati&family=Noto+Serif+Gurmukhi&family=Noto+Serif+Oriya&display=swap" rel="stylesheet">
|
|
739
|
+
<style>
|
|
740
|
+
/* Statically-instanced (non-variable) copies of this export's own fonts, embedded
|
|
741
|
+
as data: URIs — see embed-fonts.ts for why: Chromium's print/PDF-to-file font
|
|
742
|
+
subsetter has known bugs mishandling GPOS/GSUB (glyph positioning/substitution —
|
|
743
|
+
required for correct Indic conjunct/vowel-sign shaping) on *variable* fonts
|
|
744
|
+
specifically, even though the same font shapes perfectly on-screen. The Google
|
|
745
|
+
Fonts <link> above only serves variable Indic-script families, so without this,
|
|
746
|
+
a style that needs one (e.g. Traditional's Noto Serif Tamil) renders correctly in
|
|
747
|
+
the on-screen preview but comes out garbled the moment it's actually printed/
|
|
748
|
+
exported to PDF. Declared after the <link> so these take cascade precedence for
|
|
749
|
+
the same family/weight/style. */
|
|
750
|
+
${embeddedFontFaceCss}
|
|
751
|
+
</style>
|
|
752
|
+
<style>
|
|
753
|
+
/* Layout-neutral part only — the two cover layouts (.print-cover-centered /
|
|
754
|
+
.print-cover-corners) come from the style, so they live in print-adapter.ts. */
|
|
755
|
+
.print-cover-page {
|
|
756
|
+
page-break-after: always;
|
|
757
|
+
box-sizing: border-box;
|
|
758
|
+
}
|
|
759
|
+
${styleToPrintCss(style, pageFormat)}
|
|
760
|
+
|
|
761
|
+
/* Interface / buttons */
|
|
762
|
+
.print-btn-bar {
|
|
763
|
+
text-align: right;
|
|
764
|
+
margin-bottom: 20px;
|
|
765
|
+
}
|
|
766
|
+
.print-btn {
|
|
767
|
+
background: #0a5c6a;
|
|
768
|
+
color: white;
|
|
769
|
+
border: none;
|
|
770
|
+
padding: 8px 16px;
|
|
771
|
+
font-size: 13px;
|
|
772
|
+
border-radius: 4px;
|
|
773
|
+
cursor: pointer;
|
|
774
|
+
font-weight: 600;
|
|
775
|
+
}
|
|
776
|
+
.print-btn:hover {
|
|
777
|
+
background: #084954;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* Watermark, from frontmatter's watermark: field. position: fixed repeats
|
|
781
|
+
it on every printed page in Chromium's print/print-to-pdf engine (including
|
|
782
|
+
WebView2, used for the native Windows PDF export) -- the same trick the live
|
|
783
|
+
editor's .cs-watermark uses to stay visible while scrolling. */
|
|
784
|
+
.print-watermark {
|
|
785
|
+
position: fixed;
|
|
786
|
+
top: 50%;
|
|
787
|
+
left: 50%;
|
|
788
|
+
transform: translate(-50%, -50%) rotate(-45deg);
|
|
789
|
+
font-size: 60pt;
|
|
790
|
+
font-weight: 700;
|
|
791
|
+
color: rgba(0, 0, 0, 0.08);
|
|
792
|
+
white-space: nowrap;
|
|
793
|
+
pointer-events: none;
|
|
794
|
+
z-index: -1;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
@media print {
|
|
798
|
+
.print-btn-bar { display: none !important; }
|
|
799
|
+
body { padding: 0; }
|
|
800
|
+
}
|
|
801
|
+
</style>
|
|
802
|
+
</head>
|
|
803
|
+
<body>
|
|
804
|
+
<div class="print-btn-bar">
|
|
805
|
+
<button class="print-btn" onclick="window.print()">Export to PDF / Print</button>
|
|
806
|
+
</div>
|
|
807
|
+
${watermarkHtml}
|
|
808
|
+
${coverPageHtml}
|
|
809
|
+
<div id="print-pages"></div>
|
|
810
|
+
<div id="print-flow">${bodyHtml}</div>
|
|
811
|
+
${printPaginatorScript({
|
|
812
|
+
contentHeightPx: Math.round((pageHeightIn - style.page.marginTopIn - style.page.marginBottomIn) * CSS_PX_PER_IN),
|
|
813
|
+
pageNumberPrefix: 'p',
|
|
814
|
+
continuedSuffix: 'CONTINUED:',
|
|
815
|
+
})}
|
|
816
|
+
</body>
|
|
817
|
+
</html>
|
|
818
|
+
`
|
|
819
|
+
}
|
|
820
|
+
|