@sutrata/editor 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/ai/generated-providers.json +1 -1
- package/package.json +30 -3
- package/scripts/copy-assets.mjs +0 -16
- package/scripts/generate-providers.mjs +0 -127
- package/src/ai/OnboardingDialog.tsx +0 -390
- package/src/ai/VoiceConfirmDialog.tsx +0 -137
- package/src/ai/VoiceToolbar.tsx +0 -321
- package/src/ai/ai-client.ts +0 -596
- package/src/ai/commands.ts +0 -164
- package/src/ai/generated-providers.json +0 -3696
- package/src/ai/prompts.ts +0 -62
- package/src/ai/voice-service.ts +0 -96
- package/src/autocomplete/AutocompletePlugin.ts +0 -21
- package/src/autocomplete/suggestions.ts +0 -147
- package/src/context/DocumentContext.tsx +0 -648
- package/src/desktop-api.d.ts +0 -40
- package/src/editor/EditorView.tsx +0 -82
- package/src/editor/SelectionMenu.tsx +0 -152
- package/src/editor/attr-pencil-plugin.ts +0 -172
- package/src/editor/character-decoration-plugin.ts +0 -104
- package/src/editor/editor-bus.ts +0 -51
- package/src/editor/find-highlight-plugin.ts +0 -68
- package/src/editor/frontmatter-field-view.ts +0 -52
- package/src/editor/frontmatter-field.ts +0 -115
- package/src/editor/grapheme-cursor.ts +0 -99
- package/src/editor/ime-plugin.ts +0 -85
- package/src/editor/insert-helper.ts +0 -43
- package/src/editor/keymap.ts +0 -124
- package/src/editor/language-class-plugin.ts +0 -91
- package/src/editor/plugins.ts +0 -26
- package/src/editor/prosemirror-to-sutra.ts +0 -192
- package/src/editor/scene-heading-view.ts +0 -147
- package/src/editor/schema.ts +0 -156
- package/src/editor/sutra-to-prosemirror.ts +0 -156
- package/src/extensions/storage-adapter.ts +0 -65
- package/src/extensions/types.ts +0 -60
- package/src/file/docx-exporter.ts +0 -585
- package/src/file/docx-importer.ts +0 -313
- package/src/file/print-paginator.ts +0 -301
- package/src/file/templates.ts +0 -73
- package/src/file/workflow-reports.ts +0 -820
- package/src/findreplace/FindReplace.tsx +0 -242
- package/src/findreplace/find-engine.ts +0 -97
- package/src/findreplace/scroll-to-match.ts +0 -89
- package/src/fonts/tofu-detect.ts +0 -53
- package/src/i18n/LanguageContext.tsx +0 -207
- package/src/i18n/language-detect.ts +0 -73
- package/src/i18n/useTranslation.tsx +0 -74
- package/src/index.ts +0 -27
- package/src/locales/bn.json +0 -143
- package/src/locales/en.json +0 -143
- package/src/locales/gu.json +0 -143
- package/src/locales/hi.json +0 -143
- package/src/locales/kn.json +0 -143
- package/src/locales/ml.json +0 -143
- package/src/locales/mr.json +0 -143
- package/src/locales/or.json +0 -143
- package/src/locales/pa.json +0 -143
- package/src/locales/si.json +0 -143
- package/src/locales/ta.json +0 -143
- package/src/locales/te.json +0 -143
- package/src/navigator/SceneNavigator.tsx +0 -844
- package/src/navigator/StatisticsDialog.tsx +0 -249
- package/src/navigator/scene-list.ts +0 -84
- package/src/settings/SettingsDialog.tsx +0 -303
- package/src/shell/AppBar.tsx +0 -354
- package/src/shell/AppShell.tsx +0 -71
- package/src/shell/ConfirmDialog.tsx +0 -71
- package/src/shell/ElementToolbar.tsx +0 -175
- package/src/shell/LanguageStatus.tsx +0 -101
- package/src/shell/StatusBar.tsx +0 -135
- package/src/shell/SutrataLogo.tsx +0 -89
- package/src/shell/Toast.tsx +0 -46
- package/src/shell/ToolbarButton.tsx +0 -30
- package/src/shell/VersionHistory.tsx +0 -212
- package/src/shell/Watermark.tsx +0 -9
- package/src/shell/fonts/fonts.css +0 -1133
- package/src/shell/icons.tsx +0 -335
- package/src/shell/toolbar-actions.ts +0 -59
- package/src/source/SourceView.tsx +0 -111
- package/src/source/find-highlight-source.ts +0 -57
- package/src/source/sutra-lang.ts +0 -121
- package/src/spellcheck/dictionaries.ts +0 -21
- package/src/spellcheck/hunspell.ts +0 -110
- package/src/spellcheck/spellcheck-plugin.ts +0 -83
- package/src/styles/StyleDialog.tsx +0 -148
- package/src/styles/builtin-styles.ts +0 -139
- package/src/styles/css-adapter.ts +0 -76
- package/src/styles/docx-adapter.ts +0 -109
- package/src/styles/embed-fonts.ts +0 -81
- package/src/styles/pagination.ts +0 -27
- package/src/styles/print-adapter.ts +0 -288
- package/src/styles/registry.ts +0 -20
- package/src/styles/screenplay.css +0 -3006
- package/src/styles/script-ranges.ts +0 -17
- package/src/styles/types.ts +0 -104
- package/src/styles/validate.ts +0 -114
- package/src/templates/templates.ts +0 -194
- package/src/titlepage/ExportDialog.tsx +0 -494
- package/src/titlepage/TitlePageDialog.tsx +0 -312
- package/src/titlepage/frontmatter-form.ts +0 -280
- package/src/titlepage/languages.ts +0 -37
- package/src/types.ts +0 -7
- package/tests/AppBar.test.tsx +0 -35
- package/tests/AppShell.test.tsx +0 -22
- package/tests/ElementToolbar.test.tsx +0 -36
- package/tests/LanguageStatus.test.tsx +0 -129
- package/tests/SelectionMenu.test.tsx +0 -71
- package/tests/ToolbarButton.test.tsx +0 -24
- package/tests/ai-onboarding.test.ts +0 -102
- package/tests/ai-status.test.tsx +0 -72
- package/tests/ai-structured.test.ts +0 -94
- package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
- package/tests/autocomplete/suggestions.test.ts +0 -106
- package/tests/document-context.test.tsx +0 -565
- package/tests/docx-importer.test.ts +0 -160
- package/tests/editor/character-decoration-plugin.test.ts +0 -101
- package/tests/editor/character-extension.test.ts +0 -44
- package/tests/editor/grapheme-cursor.test.ts +0 -118
- package/tests/editor/ime-plugin.test.ts +0 -72
- package/tests/editor/inline-note-bridge.test.ts +0 -32
- package/tests/editor/language-class-plugin.test.ts +0 -265
- package/tests/editor/title-page.test.ts +0 -124
- package/tests/embed-fonts.test.ts +0 -17
- package/tests/exporters.test.ts +0 -365
- package/tests/findreplace/FindReplace.test.tsx +0 -391
- package/tests/findreplace/find-engine.test.ts +0 -86
- package/tests/findreplace/scroll-to-match.test.ts +0 -50
- package/tests/fonts/tofu-detect.test.ts +0 -122
- package/tests/i18n/LanguageContext.test.tsx +0 -146
- package/tests/i18n/language-detect.test.ts +0 -54
- package/tests/i18n/locale-parity.test.ts +0 -25
- package/tests/i18n/useTranslation.test.tsx +0 -45
- package/tests/keymap.test.ts +0 -18
- package/tests/navigator/SceneNavigator.test.tsx +0 -177
- package/tests/navigator/StatisticsDialog.test.tsx +0 -63
- package/tests/navigator/scene-list.test.ts +0 -71
- package/tests/print-paginator.test.ts +0 -82
- package/tests/shell/VersionHistory.test.tsx +0 -129
- package/tests/spellcheck/hunspell.test.ts +0 -82
- package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
- package/tests/styles.test.ts +0 -315
- package/tests/templates/templates.test.ts +0 -83
- package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
- package/tests/titlepage/apply-title-page.test.ts +0 -61
- package/tests/titlepage/frontmatter-form.test.ts +0 -115
- package/tests/toolbar-actions.test.ts +0 -93
- package/tests/voice-commands.test.ts +0 -58
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -11
- package/vitest.setup.ts +0 -65
|
@@ -1,585 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Document,
|
|
3
|
-
Packer,
|
|
4
|
-
Paragraph,
|
|
5
|
-
TextRun,
|
|
6
|
-
Table,
|
|
7
|
-
TableRow,
|
|
8
|
-
TableCell,
|
|
9
|
-
AlignmentType,
|
|
10
|
-
BorderStyle,
|
|
11
|
-
PageBreak,
|
|
12
|
-
WidthType,
|
|
13
|
-
TabStopType,
|
|
14
|
-
LineRuleType,
|
|
15
|
-
Header,
|
|
16
|
-
PageNumber,
|
|
17
|
-
} from 'docx'
|
|
18
|
-
import type { DocumentNode, ContentNode, InlineSpan } from '@sutrata/parser'
|
|
19
|
-
import { resolveStyle } from '../styles/registry'
|
|
20
|
-
import { styleToDocxParagraphStyles } from '../styles/docx-adapter'
|
|
21
|
-
import { SCRIPT_UNICODE_RANGES } from '../styles/script-ranges'
|
|
22
|
-
import { DEFAULT_COVER_LAYOUT } from '../styles/types'
|
|
23
|
-
import type { ScreenplayStyleDefinition, CoverElementKey } from '../styles/types'
|
|
24
|
-
|
|
25
|
-
interface TextRunProps {
|
|
26
|
-
text: string
|
|
27
|
-
bold?: boolean
|
|
28
|
-
italic?: boolean
|
|
29
|
-
underline?: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Flatten inline span trees into flat formatting runs. `skipNotes` drops inline
|
|
33
|
-
* `[[ ]]` note spans entirely (see the "Skip notes" export option). */
|
|
34
|
-
function flattenSpans(
|
|
35
|
-
spans: InlineSpan[],
|
|
36
|
-
props: { bold?: boolean; italic?: boolean; underline?: boolean } = {},
|
|
37
|
-
skipNotes = false,
|
|
38
|
-
): TextRunProps[] {
|
|
39
|
-
const result: TextRunProps[] = []
|
|
40
|
-
for (const s of spans) {
|
|
41
|
-
if (s.type === 'text') {
|
|
42
|
-
result.push({ text: s.text, ...props })
|
|
43
|
-
} else if (s.type === 'bold') {
|
|
44
|
-
result.push(...flattenSpans(s.spans, { ...props, bold: true }, skipNotes))
|
|
45
|
-
} else if (s.type === 'italic') {
|
|
46
|
-
result.push(...flattenSpans(s.spans, { ...props, italic: true }, skipNotes))
|
|
47
|
-
} else if (s.type === 'underline') {
|
|
48
|
-
result.push(...flattenSpans(s.spans, { ...props, underline: true }, skipNotes))
|
|
49
|
-
} else if (s.type === 'note') {
|
|
50
|
-
if (skipNotes) continue
|
|
51
|
-
// Matches whole-line note styling (italic, brackets kept visible).
|
|
52
|
-
result.push({ text: `[[ ${s.text} ]]`, ...props, italic: true })
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return result
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const SCRIPT_RANGES: { code: string; re: RegExp }[] = Object.entries(SCRIPT_UNICODE_RANGES).map(
|
|
59
|
-
([code, re]) => ({ code, re })
|
|
60
|
-
)
|
|
61
|
-
const SCRIPT_FONT_BY_LANG: Record<string, string> = {
|
|
62
|
-
hi: 'Noto Sans Devanagari', ta: 'Noto Sans Tamil', te: 'Noto Sans Telugu',
|
|
63
|
-
kn: 'Noto Sans Kannada', ml: 'Noto Sans Malayalam', bn: 'Noto Sans Bengali',
|
|
64
|
-
gu: 'Noto Sans Gujarati', pa: 'Noto Sans Gurmukhi', or: 'Noto Sans Oriya',
|
|
65
|
-
si: 'Noto Sans Sinhala',
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Which complex-script lang code (if any) a run's text belongs to \u2014 the shared key
|
|
69
|
-
* space for both `complexScriptFontOverrides` and `italicScripts`. Unicode presence
|
|
70
|
-
* wins over `defaultLang` (mixed-script documents), matching getComplexFont's
|
|
71
|
-
* original priority.
|
|
72
|
-
*
|
|
73
|
-
* The `defaultLang` fallback (for text with no script-specific characters at all,
|
|
74
|
-
* e.g. bare punctuation/numbers between Indic words) must never fire for text that
|
|
75
|
-
* contains actual Latin letters \u2014 otherwise a document with `lang: ta` would treat
|
|
76
|
-
* every English word (scene headings, "EXT.", character extensions, a plain title)
|
|
77
|
-
* as Tamil too, which for most styles is a harmless font substitution (Noto Sans/
|
|
78
|
-
* Serif Tamil both ship full Latin glyphs) but is actively wrong once a style forces
|
|
79
|
-
* italics for that script (Traditional) \u2014 English text would come out italicized
|
|
80
|
-
* for no reason. */
|
|
81
|
-
function matchScriptCode(text: string, defaultLang: string = 'en'): string | null {
|
|
82
|
-
for (const { code, re } of SCRIPT_RANGES) {
|
|
83
|
-
if (re.test(text)) return code
|
|
84
|
-
}
|
|
85
|
-
if (/[A-Za-z]/.test(text)) return null
|
|
86
|
-
return defaultLang in SCRIPT_FONT_BY_LANG ? defaultLang : null
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Resolve a complex-script font override for a matched script code. Returns null when
|
|
90
|
-
* the text needs no override (plain Latin) \u2014 never a specific "Latin font" name, since
|
|
91
|
-
* that decision belongs to the active style's fontFamily (applied at the paragraph-style
|
|
92
|
-
* level). `overrides` is a style's optional `complexScriptFontOverrides` (e.g.
|
|
93
|
-
* Traditional's Tamil -> Noto Serif Tamil) \u2014 DOCX runs can only ever name one font,
|
|
94
|
-
* unlike a CSS font stack, so a style whose body font *does* cover a script it cares
|
|
95
|
-
* about must say so explicitly here rather than relying on per-glyph fallback like the
|
|
96
|
-
* editor/print CSS do. */
|
|
97
|
-
function getComplexFont(code: string | null, overrides?: Record<string, string>): string | null {
|
|
98
|
-
if (!code) return null
|
|
99
|
-
return overrides?.[code] ?? SCRIPT_FONT_BY_LANG[code] ?? null
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Generate docx TextRun list with correct font mapping. `italicScripts` (a style's
|
|
103
|
-
* per-script "italic is this script's standard look here" list, e.g. Traditional's
|
|
104
|
-
* Tamil) forces italics regardless of the run's own markup or `forceItalics` \u2014 matches
|
|
105
|
-
* the editor's `.cs-force-italic-<code>` behavior. */
|
|
106
|
-
function createRuns(
|
|
107
|
-
runProps: TextRunProps[],
|
|
108
|
-
defaultLang: string,
|
|
109
|
-
forceItalics = false,
|
|
110
|
-
complexScriptFontOverrides?: Record<string, string>,
|
|
111
|
-
italicScripts?: string[],
|
|
112
|
-
): TextRun[] {
|
|
113
|
-
return runProps.map(r => {
|
|
114
|
-
const code = matchScriptCode(r.text, defaultLang)
|
|
115
|
-
const csFont = getComplexFont(code, complexScriptFontOverrides)
|
|
116
|
-
const scriptForcesItalic = code !== null && (italicScripts?.includes(code) ?? false)
|
|
117
|
-
const runOptions: any = {
|
|
118
|
-
text: r.text,
|
|
119
|
-
bold: r.bold,
|
|
120
|
-
italics: r.italic || forceItalics || scriptForcesItalic,
|
|
121
|
-
underline: r.underline ? {} : undefined,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Only set font override for Indic scripts to support proper rendering,
|
|
125
|
-
// leaving latin/general characters to use the paragraph style's own font.
|
|
126
|
-
if (csFont) {
|
|
127
|
-
runOptions.font = {
|
|
128
|
-
ascii: csFont,
|
|
129
|
-
hAnsi: csFont,
|
|
130
|
-
cs: csFont,
|
|
131
|
-
eastAsia: csFont,
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return new TextRun(runOptions)
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Vertical placement of the 'corners' cover, as fractions of the printable page height:
|
|
140
|
-
* where the title block starts and where the bottom-right date/contact block starts.
|
|
141
|
-
* Matches print-adapter.ts's .print-cover-corners padding (20% top / block sitting ~6%
|
|
142
|
-
* above the bottom edge), so the DOCX and PDF covers land in the same place. */
|
|
143
|
-
const COVER_CORNERS_TOP_FRACTION = 0.22
|
|
144
|
-
const COVER_CORNERS_BOTTOM_FRACTION = 0.78
|
|
145
|
-
|
|
146
|
-
/** DOCX has no way to anchor a block to the bottom of a page, so the corner cover is
|
|
147
|
-
* positioned by stacking empty paragraphs of an exact, known height (rather than
|
|
148
|
-
* Normal-styled blank ones, whose height depends on the reader's default font). */
|
|
149
|
-
const COVER_SPACER_TWIPS = 240 // 12pt
|
|
150
|
-
|
|
151
|
-
/** Height budgeted per date/contact line — their run sizes (10pt/11pt) plus leading. */
|
|
152
|
-
const COVER_CONTACT_LINE_TWIPS = 280
|
|
153
|
-
|
|
154
|
-
/** Enough exact-height spacers to cover `twips` (never negative — a block that already
|
|
155
|
-
* overshoots its target simply gets none). */
|
|
156
|
-
function spacerParagraphs(twips: number): Paragraph[] {
|
|
157
|
-
const count = Math.max(0, Math.round(twips / COVER_SPACER_TWIPS))
|
|
158
|
-
return Array.from({ length: count }, () => new Paragraph({
|
|
159
|
-
spacing: { before: 0, after: 0, line: COVER_SPACER_TWIPS, lineRule: LineRuleType.EXACT },
|
|
160
|
-
}))
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Approximate rendered height of a run of cover paragraphs, from their style's font
|
|
164
|
-
* sizes and spacing. Approximate is enough: it only decides how many spacers separate
|
|
165
|
-
* the two corner blocks, and the bottom block's placement is clamped so an
|
|
166
|
-
* underestimate can't push it onto a second page. Assumes each element occupies one
|
|
167
|
-
* line — a title or logline long enough to wrap just rides slightly lower. */
|
|
168
|
-
function estimateBlockHeightTwips(style: ScreenplayStyleDefinition, keys: CoverElementKey[]): number {
|
|
169
|
-
return keys.reduce((sum, key) => {
|
|
170
|
-
const el = style.elements[key]
|
|
171
|
-
return sum + Math.round(el.fontSizePt * 1.15 * 20) + Math.round((el.spaceBeforePt ?? 0) * 20)
|
|
172
|
-
+ Math.round((el.spaceAfterPt ?? 0) * 20)
|
|
173
|
-
}, 0)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Main exporter function. `styleOverride` picks a style id for this export only
|
|
177
|
-
* (without touching the document); otherwise the document's frontmatter `style:`
|
|
178
|
-
* field is used, falling back to the default style. `customStyles` is the list of
|
|
179
|
-
* user-imported styles loaded from IndexedDB (see file/storage.ts). `skipNotes` omits
|
|
180
|
-
* both block-level and inline `[[ ]]` notes from the exported document. */
|
|
181
|
-
export async function exportToDocx(
|
|
182
|
-
ast: DocumentNode,
|
|
183
|
-
styleOverride?: string,
|
|
184
|
-
customStyles: ScreenplayStyleDefinition[] = [],
|
|
185
|
-
skipNotes = false,
|
|
186
|
-
): Promise<Blob> {
|
|
187
|
-
const frontmatter = ast.frontmatter?.data as Record<string, unknown> | undefined
|
|
188
|
-
const defaultLang = (frontmatter?.['lang'] as string) ?? 'en'
|
|
189
|
-
const style = resolveStyle(styleOverride ?? (frontmatter?.['style'] as string | undefined), customStyles)
|
|
190
|
-
const bodyFont = style.fontFamily
|
|
191
|
-
const runsFor = (props: TextRunProps[], forceItalics = false) =>
|
|
192
|
-
createRuns(props, defaultLang, forceItalics, style.complexScriptFontOverrides, style.italicScripts)
|
|
193
|
-
const pageFormat = (frontmatter?.['page'] as string)?.toLowerCase() ?? 'letter'
|
|
194
|
-
const pageWidthTwips = pageFormat === 'a4' ? 11906 : 12240
|
|
195
|
-
const pageHeightTwips = pageFormat === 'a4' ? 16838 : 15840
|
|
196
|
-
const pageMarginLeftTwips = Math.round(style.page.marginLeftIn * 1440)
|
|
197
|
-
const pageMarginRightTwips = Math.round(style.page.marginRightIn * 1440)
|
|
198
|
-
const pageMarginTopTwips = Math.round(style.page.marginTopIn * 1440)
|
|
199
|
-
const pageMarginBottomTwips = Math.round(style.page.marginBottomIn * 1440)
|
|
200
|
-
// Right edge of the printable area, measured from the page margin — where
|
|
201
|
-
// a scene number's right-aligned tab stop should land (matches the UI's
|
|
202
|
-
// right-aligned scene-number box, no leading "#").
|
|
203
|
-
const contentRightEdgeTwips = pageWidthTwips - pageMarginLeftTwips - pageMarginRightTwips
|
|
204
|
-
|
|
205
|
-
const childrenNodes: (Paragraph | Table)[] = []
|
|
206
|
-
|
|
207
|
-
const hasCover = Boolean(frontmatter && (frontmatter['title'] || frontmatter['author']))
|
|
208
|
-
|
|
209
|
-
// 1. Cover Page (if frontmatter has title)
|
|
210
|
-
if (hasCover && frontmatter) {
|
|
211
|
-
const titleText = String(frontmatter['title'] || 'Untitled')
|
|
212
|
-
const authorText = String(frontmatter['author'] || '')
|
|
213
|
-
const loglineText = String(frontmatter['logline'] || '')
|
|
214
|
-
const dateText = String(frontmatter['date'] || '')
|
|
215
|
-
const contactText = String(frontmatter['contact'] || '')
|
|
216
|
-
|
|
217
|
-
const titlePara = new Paragraph({
|
|
218
|
-
style: 'CineTitle',
|
|
219
|
-
children: runsFor([{ text: titleText.toUpperCase(), bold: true }]),
|
|
220
|
-
})
|
|
221
|
-
const loglineParas = loglineText
|
|
222
|
-
? loglineText.split('\n').map(line => new Paragraph({
|
|
223
|
-
style: 'CineLogline',
|
|
224
|
-
children: runsFor([{ text: line, italic: true }]),
|
|
225
|
-
}))
|
|
226
|
-
: []
|
|
227
|
-
// Deliberately not the author style: these mirror the PDF cover's small gray
|
|
228
|
-
// date/contact lines (.print-cover-date / .print-cover-contact), not the
|
|
229
|
-
// author's display size.
|
|
230
|
-
const dateRun = (indent?: number) => new Paragraph({
|
|
231
|
-
alignment: indent === undefined ? AlignmentType.CENTER : undefined,
|
|
232
|
-
indent: indent === undefined ? undefined : { left: indent },
|
|
233
|
-
children: [new TextRun({ text: dateText, font: bodyFont, size: 20, color: '777777' })],
|
|
234
|
-
})
|
|
235
|
-
const contactRuns = (indent?: number) => contactText.split('\n').map(line => new Paragraph({
|
|
236
|
-
alignment: indent === undefined ? AlignmentType.CENTER : undefined,
|
|
237
|
-
indent: indent === undefined ? undefined : { left: indent },
|
|
238
|
-
children: [new TextRun({ text: line, font: bodyFont, size: 22, color: '888888' })],
|
|
239
|
-
}))
|
|
240
|
-
|
|
241
|
-
if ((style.coverLayout ?? DEFAULT_COVER_LAYOUT) === 'corners') {
|
|
242
|
-
// Title / "by <author>" / logline flush top-left, date + contact in the
|
|
243
|
-
// bottom-right corner — the PDF cover's .print-cover-corners layout, rebuilt
|
|
244
|
-
// out of the only vertical tool DOCX gives us: exact-height spacer paragraphs
|
|
245
|
-
// (see spacerParagraphs) counted off against the printable page height.
|
|
246
|
-
const contentHeightTwips = pageHeightTwips - pageMarginTopTwips - pageMarginBottomTwips
|
|
247
|
-
const topBlock = [
|
|
248
|
-
titlePara,
|
|
249
|
-
...(authorText ? [new Paragraph({
|
|
250
|
-
style: 'CineAuthor',
|
|
251
|
-
children: runsFor([{ text: `by ${authorText}` }]),
|
|
252
|
-
})] : []),
|
|
253
|
-
...loglineParas,
|
|
254
|
-
]
|
|
255
|
-
const bottomIndent = Math.round(contentRightEdgeTwips * 0.55)
|
|
256
|
-
const bottomBlock = [
|
|
257
|
-
...(dateText ? [dateRun(bottomIndent)] : []),
|
|
258
|
-
...(contactText ? contactRuns(bottomIndent) : []),
|
|
259
|
-
]
|
|
260
|
-
|
|
261
|
-
const topOffsetTwips = Math.round(contentHeightTwips * COVER_CORNERS_TOP_FRACTION)
|
|
262
|
-
const topSpacers = spacerParagraphs(topOffsetTwips)
|
|
263
|
-
childrenNodes.push(...topSpacers, ...topBlock)
|
|
264
|
-
|
|
265
|
-
// Fill down to the bottom block's target, but never past the point where the
|
|
266
|
-
// block itself would no longer fit — an overshoot would spill the corner onto
|
|
267
|
-
// a second cover page.
|
|
268
|
-
const usedTwips = topSpacers.length * COVER_SPACER_TWIPS
|
|
269
|
-
+ estimateBlockHeightTwips(style, ['title', ...(authorText ? ['author' as const] : []), ...loglineParas.map(() => 'logline' as const)])
|
|
270
|
-
const bottomHeightTwips = bottomBlock.length * COVER_CONTACT_LINE_TWIPS
|
|
271
|
-
const bottomTopTwips = Math.min(
|
|
272
|
-
Math.round(contentHeightTwips * COVER_CORNERS_BOTTOM_FRACTION),
|
|
273
|
-
contentHeightTwips - bottomHeightTwips,
|
|
274
|
-
)
|
|
275
|
-
childrenNodes.push(...spacerParagraphs(bottomTopTwips - usedTwips), ...bottomBlock)
|
|
276
|
-
} else {
|
|
277
|
-
for (let k = 0; k < 12; k++) {
|
|
278
|
-
childrenNodes.push(new Paragraph({ text: '' }))
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
childrenNodes.push(titlePara)
|
|
282
|
-
|
|
283
|
-
childrenNodes.push(new Paragraph({ text: '' }))
|
|
284
|
-
childrenNodes.push(
|
|
285
|
-
new Paragraph({
|
|
286
|
-
alignment: AlignmentType.CENTER,
|
|
287
|
-
children: [new TextRun({ text: 'by', font: bodyFont, size: 24 /* 12pt */ })],
|
|
288
|
-
})
|
|
289
|
-
)
|
|
290
|
-
childrenNodes.push(new Paragraph({ text: '' }))
|
|
291
|
-
|
|
292
|
-
childrenNodes.push(
|
|
293
|
-
new Paragraph({
|
|
294
|
-
style: 'CineAuthor',
|
|
295
|
-
children: runsFor([{ text: authorText }]),
|
|
296
|
-
})
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
if (loglineParas.length > 0) {
|
|
300
|
-
for (let k = 0; k < 6; k++) childrenNodes.push(new Paragraph({ text: '' }))
|
|
301
|
-
childrenNodes.push(...loglineParas)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (dateText) {
|
|
305
|
-
for (let k = 0; k < 10; k++) childrenNodes.push(new Paragraph({ text: '' }))
|
|
306
|
-
childrenNodes.push(dateRun())
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (contactText) {
|
|
310
|
-
if (!dateText) {
|
|
311
|
-
for (let k = 0; k < 10; k++) childrenNodes.push(new Paragraph({ text: '' }))
|
|
312
|
-
} else {
|
|
313
|
-
childrenNodes.push(new Paragraph({ text: '' }))
|
|
314
|
-
childrenNodes.push(new Paragraph({ text: '' }))
|
|
315
|
-
}
|
|
316
|
-
childrenNodes.push(...contactRuns())
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
childrenNodes.push(new Paragraph({ children: [new PageBreak()] }))
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Helper to process standard scene content node and return array of docx components
|
|
324
|
-
function processNode(n: ContentNode): (Paragraph | Table)[] {
|
|
325
|
-
if (n.type === 'action') {
|
|
326
|
-
const runs = runsFor(flattenSpans(n.spans, {}, skipNotes))
|
|
327
|
-
return [
|
|
328
|
-
new Paragraph({
|
|
329
|
-
style: 'CineAction',
|
|
330
|
-
children: runs,
|
|
331
|
-
}),
|
|
332
|
-
]
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (n.type === 'character') {
|
|
336
|
-
const charName = n.name.toUpperCase()
|
|
337
|
-
const ext = n.extension ? ` ${n.extension.toUpperCase()}` : ''
|
|
338
|
-
const headText = `${charName}${ext}`
|
|
339
|
-
const runs = runsFor([{ text: headText, bold: true }])
|
|
340
|
-
|
|
341
|
-
const result: (Paragraph | Table)[] = [
|
|
342
|
-
new Paragraph({
|
|
343
|
-
style: 'CineCharacter',
|
|
344
|
-
children: runs,
|
|
345
|
-
}),
|
|
346
|
-
]
|
|
347
|
-
|
|
348
|
-
for (const c of n.children) {
|
|
349
|
-
if (c.type === 'parenthetical') {
|
|
350
|
-
const pText = `(${c.text})`
|
|
351
|
-
result.push(
|
|
352
|
-
new Paragraph({
|
|
353
|
-
style: 'CineParenthetical',
|
|
354
|
-
children: runsFor([{ text: pText }]),
|
|
355
|
-
})
|
|
356
|
-
)
|
|
357
|
-
} else {
|
|
358
|
-
const dialogueRuns = runsFor(flattenSpans(c.spans, {}, skipNotes))
|
|
359
|
-
result.push(
|
|
360
|
-
new Paragraph({
|
|
361
|
-
style: 'CineDialogue',
|
|
362
|
-
children: dialogueRuns,
|
|
363
|
-
})
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return result
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (n.type === 'transition') {
|
|
371
|
-
const transText = n.text.toUpperCase()
|
|
372
|
-
return [
|
|
373
|
-
new Paragraph({
|
|
374
|
-
style: 'CineTransition',
|
|
375
|
-
children: runsFor([{ text: transText }]),
|
|
376
|
-
}),
|
|
377
|
-
]
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (n.type === 'centered') {
|
|
381
|
-
return [
|
|
382
|
-
new Paragraph({
|
|
383
|
-
style: 'CineCentered',
|
|
384
|
-
children: runsFor([{ text: n.text }]),
|
|
385
|
-
}),
|
|
386
|
-
]
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (n.type === 'lyrics') {
|
|
390
|
-
const runs = runsFor(flattenSpans(n.spans, {}, skipNotes), true)
|
|
391
|
-
return [
|
|
392
|
-
new Paragraph({
|
|
393
|
-
style: 'CineLyrics',
|
|
394
|
-
children: runs,
|
|
395
|
-
}),
|
|
396
|
-
]
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (n.type === 'note') {
|
|
400
|
-
if (skipNotes) return []
|
|
401
|
-
const noteText = `[[ ${n.text} ]]`
|
|
402
|
-
return [
|
|
403
|
-
new Paragraph({
|
|
404
|
-
style: 'CineNote',
|
|
405
|
-
children: runsFor([{ text: noteText, italic: true }]),
|
|
406
|
-
}),
|
|
407
|
-
]
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (n.type === 'comment') {
|
|
411
|
-
const commentText = `<!-- ${n.text} -->`
|
|
412
|
-
return [
|
|
413
|
-
new Paragraph({
|
|
414
|
-
style: 'CineComment',
|
|
415
|
-
children: runsFor([{ text: commentText, italic: true }]),
|
|
416
|
-
}),
|
|
417
|
-
]
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (n.type === 'page-break') {
|
|
421
|
-
return [new Paragraph({ children: [new PageBreak()] })]
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (n.type === 'dual-dialogue') {
|
|
425
|
-
const leftParas: Paragraph[] = []
|
|
426
|
-
const leftChar = n.left
|
|
427
|
-
const leftRuns = runsFor([{ text: `${leftChar.name.toUpperCase()}${leftChar.extension ? ` ${leftChar.extension.toUpperCase()}` : ''}`, bold: true }])
|
|
428
|
-
leftParas.push(new Paragraph({ style: 'CineCharacter', children: leftRuns }))
|
|
429
|
-
for (const c of leftChar.children) {
|
|
430
|
-
if (c.type === 'parenthetical') {
|
|
431
|
-
leftParas.push(new Paragraph({ style: 'CineParenthetical', children: runsFor([{ text: `(${c.text})` }]) }))
|
|
432
|
-
} else {
|
|
433
|
-
leftParas.push(
|
|
434
|
-
new Paragraph({
|
|
435
|
-
style: 'CineDialogue',
|
|
436
|
-
children: runsFor(flattenSpans(c.spans, {}, skipNotes)),
|
|
437
|
-
})
|
|
438
|
-
)
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const rightParas: Paragraph[] = []
|
|
443
|
-
const rightChar = n.right
|
|
444
|
-
const rightRuns = runsFor([{ text: `${rightChar.name.toUpperCase()}${rightChar.extension ? ` ${rightChar.extension.toUpperCase()}` : ''}`, bold: true }])
|
|
445
|
-
rightParas.push(new Paragraph({ style: 'CineCharacter', children: rightRuns }))
|
|
446
|
-
for (const c of rightChar.children) {
|
|
447
|
-
if (c.type === 'parenthetical') {
|
|
448
|
-
rightParas.push(new Paragraph({ style: 'CineParenthetical', children: runsFor([{ text: `(${c.text})` }]) }))
|
|
449
|
-
} else {
|
|
450
|
-
rightParas.push(
|
|
451
|
-
new Paragraph({
|
|
452
|
-
style: 'CineDialogue',
|
|
453
|
-
children: runsFor(flattenSpans(c.spans, {}, skipNotes)),
|
|
454
|
-
})
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return [
|
|
460
|
-
new Table({
|
|
461
|
-
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
462
|
-
borders: {
|
|
463
|
-
top: { style: BorderStyle.NONE, size: 0, color: 'auto' },
|
|
464
|
-
bottom: { style: BorderStyle.NONE, size: 0, color: 'auto' },
|
|
465
|
-
left: { style: BorderStyle.NONE, size: 0, color: 'auto' },
|
|
466
|
-
right: { style: BorderStyle.NONE, size: 0, color: 'auto' },
|
|
467
|
-
insideHorizontal: { style: BorderStyle.NONE, size: 0, color: 'auto' },
|
|
468
|
-
insideVertical: { style: BorderStyle.NONE, size: 0, color: 'auto' },
|
|
469
|
-
},
|
|
470
|
-
rows: [
|
|
471
|
-
new TableRow({
|
|
472
|
-
children: [
|
|
473
|
-
new TableCell({
|
|
474
|
-
children: leftParas,
|
|
475
|
-
width: { size: 50, type: WidthType.PERCENTAGE },
|
|
476
|
-
}),
|
|
477
|
-
new TableCell({
|
|
478
|
-
children: rightParas,
|
|
479
|
-
width: { size: 50, type: WidthType.PERCENTAGE },
|
|
480
|
-
}),
|
|
481
|
-
],
|
|
482
|
-
}),
|
|
483
|
-
],
|
|
484
|
-
}),
|
|
485
|
-
]
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return []
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Iterate over children
|
|
492
|
-
for (const node of ast.children) {
|
|
493
|
-
if (node.type === 'section') {
|
|
494
|
-
const sectText = node.text
|
|
495
|
-
childrenNodes.push(
|
|
496
|
-
new Paragraph({
|
|
497
|
-
style: 'CineSection',
|
|
498
|
-
children: runsFor([{ text: sectText, bold: true }]),
|
|
499
|
-
})
|
|
500
|
-
)
|
|
501
|
-
for (const subNode of node.children) {
|
|
502
|
-
childrenNodes.push(...processNode(subNode))
|
|
503
|
-
}
|
|
504
|
-
} else if (node.type === 'scene-heading') {
|
|
505
|
-
const headingRuns = runsFor([{ text: node.text.toUpperCase(), bold: true }])
|
|
506
|
-
if (node.id) {
|
|
507
|
-
headingRuns.push(
|
|
508
|
-
new TextRun({ text: '\t' }),
|
|
509
|
-
...runsFor([{ text: node.id, bold: true }])
|
|
510
|
-
)
|
|
511
|
-
}
|
|
512
|
-
childrenNodes.push(
|
|
513
|
-
new Paragraph({
|
|
514
|
-
style: 'CineSceneHeading',
|
|
515
|
-
tabStops: node.id
|
|
516
|
-
? [{ type: TabStopType.RIGHT, position: contentRightEdgeTwips }]
|
|
517
|
-
: undefined,
|
|
518
|
-
children: headingRuns,
|
|
519
|
-
})
|
|
520
|
-
)
|
|
521
|
-
for (const subNode of node.children) {
|
|
522
|
-
childrenNodes.push(...processNode(subNode))
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
childrenNodes.push(...processNode(node))
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Running header: "p<n>" in the top right, as a PAGE field so Word keeps it correct as
|
|
530
|
-
// the document is edited. The PDF export's other header cell — "<scene> CONTINUED:" on a
|
|
531
|
-
// page that opens mid-scene — has no DOCX equivalent: it depends on where the page break
|
|
532
|
-
// falls, and Word neither exposes that to a field nor lets a header vary per page within
|
|
533
|
-
// a section. (Only the cover is special-cased, via titlePage below.)
|
|
534
|
-
const runningHeader = new Header({
|
|
535
|
-
children: [
|
|
536
|
-
new Paragraph({
|
|
537
|
-
alignment: AlignmentType.RIGHT,
|
|
538
|
-
children: [
|
|
539
|
-
new TextRun({
|
|
540
|
-
children: ['p', PageNumber.CURRENT],
|
|
541
|
-
font: bodyFont,
|
|
542
|
-
size: Math.round(style.elements.action.fontSizePt * 2),
|
|
543
|
-
italics: true,
|
|
544
|
-
}),
|
|
545
|
-
],
|
|
546
|
-
}),
|
|
547
|
-
],
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
// Create document with screenplay styles driven by the resolved ScreenplayStyleDefinition
|
|
551
|
-
const doc = new Document({
|
|
552
|
-
styles: {
|
|
553
|
-
paragraphStyles: styleToDocxParagraphStyles(style, contentRightEdgeTwips),
|
|
554
|
-
},
|
|
555
|
-
sections: [
|
|
556
|
-
{
|
|
557
|
-
properties: {
|
|
558
|
-
// The cover takes Word's "different first page" header (an empty one) and page
|
|
559
|
-
// number 0, so the first page of the script itself comes out as p1.
|
|
560
|
-
titlePage: hasCover,
|
|
561
|
-
page: {
|
|
562
|
-
size: {
|
|
563
|
-
width: pageWidthTwips,
|
|
564
|
-
height: pageHeightTwips,
|
|
565
|
-
},
|
|
566
|
-
margin: {
|
|
567
|
-
top: pageMarginTopTwips,
|
|
568
|
-
bottom: pageMarginBottomTwips,
|
|
569
|
-
left: pageMarginLeftTwips,
|
|
570
|
-
right: pageMarginRightTwips,
|
|
571
|
-
},
|
|
572
|
-
pageNumbers: hasCover ? { start: 0 } : undefined,
|
|
573
|
-
},
|
|
574
|
-
},
|
|
575
|
-
headers: {
|
|
576
|
-
default: runningHeader,
|
|
577
|
-
...(hasCover ? { first: new Header({ children: [new Paragraph({ text: '' })] }) } : {}),
|
|
578
|
-
},
|
|
579
|
-
children: childrenNodes,
|
|
580
|
-
},
|
|
581
|
-
],
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
return Packer.toBlob(doc)
|
|
585
|
-
}
|