@sutrata/editor 0.1.1 → 0.2.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.
Files changed (268) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
  3. package/dist/ai/VoiceConfirmDialog.js +6 -6
  4. package/dist/ai/VoiceToolbar.d.ts +1 -0
  5. package/dist/ai/VoiceToolbar.js +22 -16
  6. package/dist/ai/clean-response.d.ts +7 -0
  7. package/dist/ai/clean-response.js +58 -0
  8. package/dist/ai/format-transcript.d.ts +3 -0
  9. package/dist/ai/format-transcript.js +7 -0
  10. package/dist/ai/prompts.d.ts +8 -1
  11. package/dist/ai/prompts.js +34 -7
  12. package/dist/autocomplete/suggestions.js +1 -1
  13. package/dist/context/DocumentContext.d.ts +28 -4
  14. package/dist/context/DocumentContext.js +67 -37
  15. package/dist/editor/EditorView.js +32 -4
  16. package/dist/editor/SelectionMenu.js +5 -3
  17. package/dist/editor/external-decorations-plugin.d.ts +23 -0
  18. package/dist/editor/external-decorations-plugin.js +151 -0
  19. package/dist/editor/omitted-scene-plugin.d.ts +9 -0
  20. package/dist/editor/omitted-scene-plugin.js +44 -0
  21. package/dist/editor/plugins.js +4 -0
  22. package/dist/editor/private-metadata-plugin.d.ts +10 -0
  23. package/dist/editor/private-metadata-plugin.js +60 -0
  24. package/dist/editor/prosemirror-to-sutra.js +179 -74
  25. package/dist/editor/scene-heading-view.js +2 -0
  26. package/dist/editor/schema.js +69 -12
  27. package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
  28. package/dist/editor/sutra-to-prosemirror.js +32 -34
  29. package/dist/extensions/ai-provider.d.ts +44 -0
  30. package/dist/extensions/ai-provider.js +27 -0
  31. package/dist/extensions/collab.d.ts +21 -0
  32. package/dist/extensions/collab.js +6 -0
  33. package/dist/extensions/command-registry.d.ts +49 -0
  34. package/dist/extensions/command-registry.js +28 -0
  35. package/dist/extensions/decorations.d.ts +48 -0
  36. package/dist/extensions/decorations.js +6 -0
  37. package/dist/extensions/export-registry.d.ts +59 -0
  38. package/dist/extensions/export-registry.js +10 -0
  39. package/dist/extensions/panel-registry.d.ts +30 -0
  40. package/dist/extensions/panel-registry.js +16 -0
  41. package/dist/extensions/registries.d.ts +15 -0
  42. package/dist/extensions/registries.js +12 -0
  43. package/dist/extensions/registry-store.d.ts +26 -0
  44. package/dist/extensions/registry-store.js +35 -0
  45. package/dist/extensions/session.d.ts +29 -0
  46. package/dist/extensions/session.js +20 -0
  47. package/dist/extensions/speech-provider.d.ts +35 -0
  48. package/dist/extensions/speech-provider.js +17 -0
  49. package/dist/file/builtin-exporters.d.ts +9 -0
  50. package/dist/file/builtin-exporters.js +72 -0
  51. package/dist/file/docx-exporter.js +7 -4
  52. package/dist/file/docx-importer.d.ts +9 -0
  53. package/dist/file/docx-importer.js +47 -19
  54. package/dist/file/scene-number.d.ts +5 -0
  55. package/dist/file/scene-number.js +8 -0
  56. package/dist/file/workflow-reports.js +13 -7
  57. package/dist/findreplace/FindReplace.js +5 -2
  58. package/dist/import/AIImportDialog.d.ts +11 -0
  59. package/dist/import/AIImportDialog.js +112 -0
  60. package/dist/import/ai-import.d.ts +51 -0
  61. package/dist/import/ai-import.js +200 -0
  62. package/dist/import/apply-import.d.ts +7 -0
  63. package/dist/import/apply-import.js +35 -0
  64. package/dist/import/extract.d.ts +17 -0
  65. package/dist/import/extract.js +18 -0
  66. package/dist/index.d.ts +17 -2
  67. package/dist/index.js +7 -0
  68. package/dist/locales/bn.json +8 -0
  69. package/dist/locales/en.json +8 -0
  70. package/dist/locales/gu.json +8 -0
  71. package/dist/locales/hi.json +8 -0
  72. package/dist/locales/kn.json +8 -0
  73. package/dist/locales/ml.json +8 -0
  74. package/dist/locales/mr.json +8 -0
  75. package/dist/locales/or.json +8 -0
  76. package/dist/locales/pa.json +8 -0
  77. package/dist/locales/si.json +8 -0
  78. package/dist/locales/ta.json +8 -0
  79. package/dist/locales/te.json +8 -0
  80. package/dist/navigator/SceneNavigator.js +125 -131
  81. package/dist/navigator/StatisticsDialog.js +5 -1
  82. package/dist/navigator/scene-edits.d.ts +18 -0
  83. package/dist/navigator/scene-edits.js +143 -0
  84. package/dist/navigator/scene-list.js +4 -4
  85. package/dist/navigator/scene-numbering.d.ts +37 -0
  86. package/dist/navigator/scene-numbering.js +88 -0
  87. package/dist/settings/SettingsDialog.js +8 -120
  88. package/dist/shell/AppBar.js +21 -2
  89. package/dist/shell/AppShell.js +18 -4
  90. package/dist/shell/ElementToolbar.d.ts +5 -0
  91. package/dist/shell/ElementToolbar.js +35 -25
  92. package/dist/shell/LanguageStatus.js +6 -0
  93. package/dist/shell/StatusBar.js +11 -6
  94. package/dist/shell/builtin-commands.d.ts +10 -0
  95. package/dist/shell/builtin-commands.js +43 -0
  96. package/dist/shell/icons.d.ts +6 -0
  97. package/dist/shell/icons.js +6 -0
  98. package/dist/shell/use-commands.d.ts +7 -0
  99. package/dist/shell/use-commands.js +24 -0
  100. package/dist/shell/use-panel-context.d.ts +3 -0
  101. package/dist/shell/use-panel-context.js +39 -0
  102. package/dist/source/SourceView.js +18 -1
  103. package/dist/styles/screenplay.css +113 -1
  104. package/dist/titlepage/ExportDialog.d.ts +5 -0
  105. package/dist/titlepage/ExportDialog.js +59 -96
  106. package/package.json +32 -5
  107. package/dist/ai/OnboardingDialog.d.ts +0 -6
  108. package/dist/ai/OnboardingDialog.js +0 -115
  109. package/dist/ai/ai-client.d.ts +0 -71
  110. package/dist/ai/ai-client.js +0 -494
  111. package/dist/ai/generated-providers.json +0 -3696
  112. package/dist/ai/voice-service.d.ts +0 -15
  113. package/dist/ai/voice-service.js +0 -81
  114. package/dist/editor/character-view.d.ts +0 -35
  115. package/dist/editor/character-view.js +0 -112
  116. package/dist/editor/transliterate-input.d.ts +0 -10
  117. package/dist/editor/transliterate-input.js +0 -88
  118. package/dist/extensions/types.d.ts +0 -73
  119. package/dist/extensions/types.js +0 -12
  120. package/scripts/copy-assets.mjs +0 -16
  121. package/scripts/generate-providers.mjs +0 -127
  122. package/src/ai/OnboardingDialog.tsx +0 -390
  123. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  124. package/src/ai/VoiceToolbar.tsx +0 -321
  125. package/src/ai/ai-client.ts +0 -596
  126. package/src/ai/commands.ts +0 -164
  127. package/src/ai/generated-providers.json +0 -3696
  128. package/src/ai/prompts.ts +0 -62
  129. package/src/ai/voice-service.ts +0 -96
  130. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  131. package/src/autocomplete/suggestions.ts +0 -147
  132. package/src/context/DocumentContext.tsx +0 -648
  133. package/src/desktop-api.d.ts +0 -40
  134. package/src/editor/EditorView.tsx +0 -82
  135. package/src/editor/SelectionMenu.tsx +0 -152
  136. package/src/editor/attr-pencil-plugin.ts +0 -172
  137. package/src/editor/character-decoration-plugin.ts +0 -104
  138. package/src/editor/editor-bus.ts +0 -51
  139. package/src/editor/find-highlight-plugin.ts +0 -68
  140. package/src/editor/frontmatter-field-view.ts +0 -52
  141. package/src/editor/frontmatter-field.ts +0 -115
  142. package/src/editor/grapheme-cursor.ts +0 -99
  143. package/src/editor/ime-plugin.ts +0 -85
  144. package/src/editor/insert-helper.ts +0 -43
  145. package/src/editor/keymap.ts +0 -124
  146. package/src/editor/language-class-plugin.ts +0 -91
  147. package/src/editor/plugins.ts +0 -26
  148. package/src/editor/prosemirror-to-sutra.ts +0 -192
  149. package/src/editor/scene-heading-view.ts +0 -147
  150. package/src/editor/schema.ts +0 -156
  151. package/src/editor/sutra-to-prosemirror.ts +0 -156
  152. package/src/extensions/storage-adapter.ts +0 -65
  153. package/src/extensions/types.ts +0 -60
  154. package/src/file/docx-exporter.ts +0 -585
  155. package/src/file/docx-importer.ts +0 -313
  156. package/src/file/print-paginator.ts +0 -301
  157. package/src/file/templates.ts +0 -73
  158. package/src/file/workflow-reports.ts +0 -820
  159. package/src/findreplace/FindReplace.tsx +0 -242
  160. package/src/findreplace/find-engine.ts +0 -97
  161. package/src/findreplace/scroll-to-match.ts +0 -89
  162. package/src/fonts/tofu-detect.ts +0 -53
  163. package/src/i18n/LanguageContext.tsx +0 -207
  164. package/src/i18n/language-detect.ts +0 -73
  165. package/src/i18n/useTranslation.tsx +0 -74
  166. package/src/index.ts +0 -27
  167. package/src/locales/bn.json +0 -143
  168. package/src/locales/en.json +0 -143
  169. package/src/locales/gu.json +0 -143
  170. package/src/locales/hi.json +0 -143
  171. package/src/locales/kn.json +0 -143
  172. package/src/locales/ml.json +0 -143
  173. package/src/locales/mr.json +0 -143
  174. package/src/locales/or.json +0 -143
  175. package/src/locales/pa.json +0 -143
  176. package/src/locales/si.json +0 -143
  177. package/src/locales/ta.json +0 -143
  178. package/src/locales/te.json +0 -143
  179. package/src/navigator/SceneNavigator.tsx +0 -844
  180. package/src/navigator/StatisticsDialog.tsx +0 -249
  181. package/src/navigator/scene-list.ts +0 -84
  182. package/src/settings/SettingsDialog.tsx +0 -303
  183. package/src/shell/AppBar.tsx +0 -354
  184. package/src/shell/AppShell.tsx +0 -71
  185. package/src/shell/ConfirmDialog.tsx +0 -71
  186. package/src/shell/ElementToolbar.tsx +0 -175
  187. package/src/shell/LanguageStatus.tsx +0 -101
  188. package/src/shell/StatusBar.tsx +0 -135
  189. package/src/shell/SutrataLogo.tsx +0 -89
  190. package/src/shell/Toast.tsx +0 -46
  191. package/src/shell/ToolbarButton.tsx +0 -30
  192. package/src/shell/VersionHistory.tsx +0 -212
  193. package/src/shell/Watermark.tsx +0 -9
  194. package/src/shell/fonts/fonts.css +0 -1133
  195. package/src/shell/icons.tsx +0 -335
  196. package/src/shell/toolbar-actions.ts +0 -59
  197. package/src/source/SourceView.tsx +0 -111
  198. package/src/source/find-highlight-source.ts +0 -57
  199. package/src/source/sutra-lang.ts +0 -121
  200. package/src/spellcheck/dictionaries.ts +0 -21
  201. package/src/spellcheck/hunspell.ts +0 -110
  202. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  203. package/src/styles/StyleDialog.tsx +0 -148
  204. package/src/styles/builtin-styles.ts +0 -139
  205. package/src/styles/css-adapter.ts +0 -76
  206. package/src/styles/docx-adapter.ts +0 -109
  207. package/src/styles/embed-fonts.ts +0 -81
  208. package/src/styles/pagination.ts +0 -27
  209. package/src/styles/print-adapter.ts +0 -288
  210. package/src/styles/registry.ts +0 -20
  211. package/src/styles/screenplay.css +0 -3006
  212. package/src/styles/script-ranges.ts +0 -17
  213. package/src/styles/types.ts +0 -104
  214. package/src/styles/validate.ts +0 -114
  215. package/src/templates/templates.ts +0 -194
  216. package/src/titlepage/ExportDialog.tsx +0 -494
  217. package/src/titlepage/TitlePageDialog.tsx +0 -312
  218. package/src/titlepage/frontmatter-form.ts +0 -280
  219. package/src/titlepage/languages.ts +0 -37
  220. package/src/types.ts +0 -7
  221. package/tests/AppBar.test.tsx +0 -35
  222. package/tests/AppShell.test.tsx +0 -22
  223. package/tests/ElementToolbar.test.tsx +0 -36
  224. package/tests/LanguageStatus.test.tsx +0 -129
  225. package/tests/SelectionMenu.test.tsx +0 -71
  226. package/tests/ToolbarButton.test.tsx +0 -24
  227. package/tests/ai-onboarding.test.ts +0 -102
  228. package/tests/ai-status.test.tsx +0 -72
  229. package/tests/ai-structured.test.ts +0 -94
  230. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  231. package/tests/autocomplete/suggestions.test.ts +0 -106
  232. package/tests/document-context.test.tsx +0 -565
  233. package/tests/docx-importer.test.ts +0 -160
  234. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  235. package/tests/editor/character-extension.test.ts +0 -44
  236. package/tests/editor/grapheme-cursor.test.ts +0 -118
  237. package/tests/editor/ime-plugin.test.ts +0 -72
  238. package/tests/editor/inline-note-bridge.test.ts +0 -32
  239. package/tests/editor/language-class-plugin.test.ts +0 -265
  240. package/tests/editor/title-page.test.ts +0 -124
  241. package/tests/embed-fonts.test.ts +0 -17
  242. package/tests/exporters.test.ts +0 -365
  243. package/tests/findreplace/FindReplace.test.tsx +0 -391
  244. package/tests/findreplace/find-engine.test.ts +0 -86
  245. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  246. package/tests/fonts/tofu-detect.test.ts +0 -122
  247. package/tests/i18n/LanguageContext.test.tsx +0 -146
  248. package/tests/i18n/language-detect.test.ts +0 -54
  249. package/tests/i18n/locale-parity.test.ts +0 -25
  250. package/tests/i18n/useTranslation.test.tsx +0 -45
  251. package/tests/keymap.test.ts +0 -18
  252. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  253. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  254. package/tests/navigator/scene-list.test.ts +0 -71
  255. package/tests/print-paginator.test.ts +0 -82
  256. package/tests/shell/VersionHistory.test.tsx +0 -129
  257. package/tests/spellcheck/hunspell.test.ts +0 -82
  258. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  259. package/tests/styles.test.ts +0 -315
  260. package/tests/templates/templates.test.ts +0 -83
  261. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  262. package/tests/titlepage/apply-title-page.test.ts +0 -61
  263. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  264. package/tests/toolbar-actions.test.ts +0 -93
  265. package/tests/voice-commands.test.ts +0 -58
  266. package/tsconfig.json +0 -17
  267. package/vitest.config.ts +0 -11
  268. package/vitest.setup.ts +0 -65
@@ -1,109 +0,0 @@
1
- import { AlignmentType } from 'docx'
2
- import type { IParagraphStyleOptions } from 'docx'
3
- import { BODY_ELEMENT_KEYS, COVER_ELEMENT_KEYS } from './types.js'
4
- import { KEEP_WITH_NEXT_ELEMENTS, KEEP_TOGETHER_ELEMENTS } from './pagination.js'
5
- import type { ScreenplayStyleDefinition, ElementStyle, BodyElementKey, CoverElementKey } from './types.js'
6
-
7
- /** Sutra element -> stable docx paragraph style id. Fixed forever: docx-importer.ts
8
- * matches re-imported .docx files against these exact ids, regardless of appearance. */
9
- const DOCX_STYLE_IDS: Record<BodyElementKey, string> = {
10
- sceneHeading: 'CineSceneHeading',
11
- action: 'CineAction',
12
- character: 'CineCharacter',
13
- parenthetical: 'CineParenthetical',
14
- dialogue: 'CineDialogue',
15
- transition: 'CineTransition',
16
- centered: 'CineCentered',
17
- lyrics: 'CineLyrics',
18
- section: 'CineSection',
19
- note: 'CineNote',
20
- comment: 'CineComment',
21
- }
22
-
23
- /** Cover-page element -> docx paragraph style id, referenced by docx-exporter.ts's
24
- * title page. These must be emitted alongside the body styles: a paragraph naming a
25
- * style the document doesn't define silently falls back to Word's Normal style
26
- * (left-aligned 11pt black), which is exactly how the title page used to render. */
27
- const DOCX_COVER_STYLE_IDS: Record<CoverElementKey, string> = {
28
- title: 'CineTitle',
29
- author: 'CineAuthor',
30
- logline: 'CineLogline',
31
- }
32
-
33
- const ALIGN_MAP = {
34
- left: AlignmentType.LEFT,
35
- center: AlignmentType.CENTER,
36
- right: AlignmentType.RIGHT,
37
- } as const
38
-
39
- function pctToTwips(pct: number | undefined, contentWidthTwips: number): number | undefined {
40
- if (pct === undefined) return undefined
41
- return Math.round((pct / 100) * contentWidthTwips)
42
- }
43
-
44
- /** docx's *style-level* paragraph options type omits `widowControl`, but the paragraph-
45
- * property serializer it shares with `Paragraph` does emit it (verified: `<w:widowControl/>`
46
- * lands in the generated word/styles.xml). Widen the type rather than drop the property. */
47
- type StyleParagraphOptions = NonNullable<IParagraphStyleOptions['paragraph']> & { widowControl?: boolean }
48
-
49
- /** `pagination` carries the screenplay page-break rules for body elements (see
50
- * pagination.ts); cover-page elements pass none — a cover page is one page by
51
- * construction. */
52
- function elementToDocxStyle(
53
- id: string,
54
- el: ElementStyle,
55
- fontFamily: string,
56
- contentWidthTwips: number,
57
- pagination: { keepNext?: boolean; keepLines?: boolean } = {},
58
- ): IParagraphStyleOptions {
59
- const left = pctToTwips(el.marginLeftPct, contentWidthTwips)
60
- const right = pctToTwips(el.marginRightPct, contentWidthTwips)
61
- const paragraph: StyleParagraphOptions = {
62
- alignment: el.align ? ALIGN_MAP[el.align] : undefined,
63
- indent: (left !== undefined || right !== undefined) ? { left, right } : undefined,
64
- spacing: {
65
- before: el.spaceBeforePt !== undefined ? Math.round(el.spaceBeforePt * 20) : undefined,
66
- after: el.spaceAfterPt !== undefined ? Math.round(el.spaceAfterPt * 20) : undefined,
67
- },
68
- keepNext: pagination.keepNext,
69
- keepLines: pagination.keepLines,
70
- // Stated rather than left to Word's default: action/dialogue stay breakable
71
- // (no keepLines), so widow control is what stops a page break from stranding a
72
- // single line of them on either side.
73
- widowControl: true,
74
- }
75
- return {
76
- id,
77
- name: id,
78
- run: {
79
- font: fontFamily,
80
- size: Math.round(el.fontSizePt * 2),
81
- bold: el.bold || undefined,
82
- italics: el.italic || undefined,
83
- underline: el.underline ? {} : undefined,
84
- color: el.color,
85
- },
86
- paragraph,
87
- }
88
- }
89
-
90
- /** Builds the `paragraphStyles` array for `new Document({ styles: { paragraphStyles } })`
91
- * from a style definition. `contentWidthTwips` is the page's printable width (page width
92
- * minus left+right margins) — percentages are resolved against it, so indentation scales
93
- * correctly with page size/margins instead of reusing one page format's fixed twips. */
94
- export function styleToDocxParagraphStyles(
95
- style: ScreenplayStyleDefinition,
96
- contentWidthTwips: number,
97
- ): IParagraphStyleOptions[] {
98
- return [
99
- ...BODY_ELEMENT_KEYS.map(key =>
100
- elementToDocxStyle(DOCX_STYLE_IDS[key], style.elements[key], style.fontFamily, contentWidthTwips, {
101
- keepNext: KEEP_WITH_NEXT_ELEMENTS.includes(key) || undefined,
102
- keepLines: KEEP_TOGETHER_ELEMENTS.includes(key) || undefined,
103
- })
104
- ),
105
- ...COVER_ELEMENT_KEYS.map(key =>
106
- elementToDocxStyle(DOCX_COVER_STYLE_IDS[key], style.elements[key], style.fontFamily, contentWidthTwips)
107
- ),
108
- ]
109
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Embeds self-hosted @font-face rules (from shell/fonts/fonts.css, already loaded into
3
- * the running app's own document) as data: URIs, for splicing into the PDF/print-preview
4
- * HTML — a separate, standalone document that never sees the app's own stylesheets.
5
- *
6
- * Introspects the CSSOM (`document.styleSheets`) rather than hardcoding filenames/unicode
7
- * ranges per family: whatever fonts.css declares for a family is exactly what gets embedded,
8
- * so this can never drift out of sync with the actual font files bundled in public/fonts.
9
- *
10
- * Why embed at all instead of just linking `/fonts/...` from the print HTML: the native
11
- * desktop PDF export writes the HTML to a temp file and loads it via a `file://` URL in a
12
- * hidden webview, which cannot resolve a root-relative `/fonts/...` path back to the running
13
- * app's own server/origin. A data: URI works regardless of the HTML's own location.
14
- */
15
-
16
- function urlToDataUri(url: string): Promise<string> {
17
- return fetch(url)
18
- .then(res => {
19
- if (!res.ok) throw new Error(`Failed to fetch font: ${url}`)
20
- return res.blob()
21
- })
22
- .then(blob => new Promise<string>((resolve, reject) => {
23
- const reader = new FileReader()
24
- reader.onload = () => resolve(reader.result as string)
25
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read font file'))
26
- reader.readAsDataURL(blob)
27
- }))
28
- }
29
-
30
- function findFontFaceRules(familyName: string): CSSFontFaceRule[] {
31
- const rules: CSSFontFaceRule[] = []
32
- for (const sheet of Array.from(document.styleSheets)) {
33
- let cssRules: CSSRuleList
34
- try {
35
- cssRules = sheet.cssRules
36
- } catch {
37
- continue // cross-origin stylesheets throw on access; nothing we bundle is cross-origin
38
- }
39
- for (const rule of Array.from(cssRules)) {
40
- if (rule instanceof CSSFontFaceRule) {
41
- const family = rule.style.getPropertyValue('font-family').replace(/^['"]|['"]$/g, '')
42
- if (family === familyName) rules.push(rule)
43
- }
44
- }
45
- }
46
- return rules
47
- }
48
-
49
- /** Builds `@font-face` CSS text embedding every self-hosted rule for the given family
50
- * names, as data: URIs. Families with no matching rules (e.g. web-safe fonts that were
51
- * never in fonts.css) are silently skipped — the Google Fonts `<link>`/generic fallback
52
- * stack in print-adapter.ts's font stack still applies. Fetch failures for an individual
53
- * rule are likewise skipped rather than failing the whole export. */
54
- export async function embedFontFacesFor(familyNames: string[]): Promise<string> {
55
- const blocks: string[] = []
56
- for (const family of familyNames) {
57
- for (const rule of findFontFaceRules(family)) {
58
- const srcRaw = rule.style.getPropertyValue('src')
59
- const match = /url\(["']?([^"')]+)["']?\)/.exec(srcRaw)
60
- if (!match) continue
61
- try {
62
- const dataUri = await urlToDataUri(match[1]!)
63
- const weight = rule.style.getPropertyValue('font-weight') || '400'
64
- const style = rule.style.getPropertyValue('font-style') || 'normal'
65
- const unicodeRange = rule.style.getPropertyValue('unicode-range')
66
- blocks.push(
67
- '@font-face {\n' +
68
- ` font-family: '${family}';\n` +
69
- ` font-weight: ${weight};\n` +
70
- ` font-style: ${style};\n` +
71
- ` src: url(${dataUri}) format('woff2');\n` +
72
- (unicodeRange ? ` unicode-range: ${unicodeRange};\n` : '') +
73
- '}'
74
- )
75
- } catch {
76
- // Skip; the Google Fonts link / generic fallback stack still covers this family.
77
- }
78
- }
79
- }
80
- return blocks.join('\n')
81
- }
@@ -1,27 +0,0 @@
1
- import type { BodyElementKey } from './types.js'
2
-
3
- /** Screenplay pagination rules — format conventions, not per-style visual choices, so
4
- * they live here rather than in ScreenplayStyleDefinition: no style (built-in or
5
- * user-imported) gets to end a page on a character cue. Consumed by both export
6
- * paths, which express the same rule differently: DOCX via `keepNext`/`keepLines`
7
- * paragraph properties (docx-adapter.ts), PDF/print via `break-after`/`break-inside`
8
- * (print-adapter.ts). The live editor has no pagination, so it ignores these. */
9
-
10
- /** Elements that must not be the last thing on a page — a scene heading or character
11
- * cue stranded at the bottom with its action/dialogue overleaf is the classic
12
- * screenplay pagination error. A parenthetical is included for the same reason: it
13
- * belongs to the dialogue line that follows it. */
14
- export const KEEP_WITH_NEXT_ELEMENTS: readonly BodyElementKey[] = [
15
- 'sceneHeading', 'character', 'parenthetical', 'section',
16
- ]
17
-
18
- /** Elements short enough that splitting them across a page boundary is always wrong.
19
- * Deliberately excludes action/dialogue/lyrics/note — those can legitimately run
20
- * longer than a page, so they get orphan/widow control instead of an unbreakable box. */
21
- export const KEEP_TOGETHER_ELEMENTS: readonly BodyElementKey[] = [
22
- 'sceneHeading', 'character', 'parenthetical', 'transition', 'section',
23
- ]
24
-
25
- /** Minimum lines of a breakable element left on either side of a page boundary. Two is
26
- * the standard screenplay/typography minimum (a single stranded line reads as an error). */
27
- export const MIN_ORPHAN_WIDOW_LINES = 2
@@ -1,288 +0,0 @@
1
- import type { ScreenplayStyleDefinition, ElementStyle } from './types.js'
2
- import { MIN_ORPHAN_WIDOW_LINES } from './pagination.js'
3
-
4
- function color(el: ElementStyle): string {
5
- return el.color ? `#${el.color}` : 'black'
6
- }
7
-
8
- function fontProps(el: ElementStyle): string {
9
- const lines = [`font-size: ${el.fontSizePt}pt;`, `color: ${color(el)};`]
10
- if (el.bold) lines.push('font-weight: bold;')
11
- if (el.italic) lines.push('font-style: italic;')
12
- if (el.underline) lines.push('text-decoration: underline;')
13
- if (el.align) lines.push(`text-align: ${el.align};`)
14
- return lines.join('\n ')
15
- }
16
-
17
- /** Screenplay page-break rules for PDF/print — the CSS half of pagination.ts (DOCX gets
18
- * the same rules as keepNext/keepLines). `page-break-*` is emitted alongside the modern
19
- * `break-*` because print engines are uneven about which spelling they honor. */
20
- const KEEP_WITH_NEXT = `
21
- break-after: avoid;
22
- page-break-after: avoid;
23
- break-inside: avoid;
24
- page-break-inside: avoid;`
25
- const KEEP_TOGETHER = `
26
- break-inside: avoid;
27
- page-break-inside: avoid;`
28
- /** For elements that may legitimately run longer than a page: don't forbid the break,
29
- * just refuse to strand one line on either side of it. */
30
- const ORPHAN_WIDOW = `
31
- orphans: ${MIN_ORPHAN_WIDOW_LINES};
32
- widows: ${MIN_ORPHAN_WIDOW_LINES};`
33
-
34
- function marginProps(el: ElementStyle): string {
35
- const lines: string[] = []
36
- if (el.marginLeftPct !== undefined) lines.push(`margin-left: ${el.marginLeftPct}%;`)
37
- if (el.marginRightPct !== undefined) lines.push(`margin-right: ${el.marginRightPct}%;`)
38
- lines.push(`margin-top: ${el.spaceBeforePt ?? 0}pt;`)
39
- lines.push(`margin-bottom: ${el.spaceAfterPt ?? 0}pt;`)
40
- return lines.join('\n ')
41
- }
42
-
43
- /** Builds the screenplay-specific portion of the PDF/print-preview `<style>` block
44
- * (packages/app/src/file/workflow-reports.ts's buildScreenplayPrintHtml) from a style
45
- * definition. UI chrome (print button, page-break rule, dual-dialogue column layout)
46
- * stays hardcoded in workflow-reports.ts — only the parametrized visual properties live here. */
47
- export function styleToPrintCss(style: ScreenplayStyleDefinition, pageFormat: 'letter' | 'a4' = 'letter'): string {
48
- const e = style.elements
49
- const contentWidthIn = (pageFormat === 'a4' ? 8.27 : 8.5) - style.page.marginLeftIn - style.page.marginRightIn
50
- // The style's own font first, then its explicit per-script overrides (e.g. Traditional's
51
- // Noto Serif Tamil) so they win over the generic Indic fallback that always follows — this
52
- // HTML has no per-span language tagging, so one comprehensive stack has to do the work the
53
- // editor's .cs-lang-* rules split across classes. Overrides are style-specific, so unlike
54
- // the fixed Noto Sans fallback list they must come from the style itself, not be hardcoded —
55
- // otherwise every style would render Indic scripts in whatever the *last*-defined style's
56
- // overrides asked for.
57
- const overrideFonts = Object.values(style.complexScriptFontOverrides ?? {})
58
- // Noto Sans comes right after the style's own fonts: it is the Latin fallback (app spec
59
- // §4.4) and its bundled file carries every ISO 15919 letter and combining mark, which the
60
- // Indic families' Latin subsets do not. Its unicode-range stops at Latin, so Indic text
61
- // still falls through to the script-specific faces below.
62
- const fontStack = [...new Set([
63
- `'${style.fontFamily}'`,
64
- ...overrideFonts.map(f => `'${f}'`),
65
- "'Noto Sans'",
66
- "'Noto Sans Tamil'", "'Noto Sans Devanagari'", "'Noto Sans Telugu'", "'Noto Sans Kannada'",
67
- "'Noto Sans Malayalam'", "'Noto Sans Bengali'", "'Noto Sans Gujarati'", "'Noto Sans Gurmukhi'",
68
- "'Noto Sans Oriya'", 'sans-serif',
69
- ])].join(', ')
70
- return `
71
- @page {
72
- /* Stated, not left to the print dialog's default: print-paginator.ts fills
73
- fixed-height page boxes, so the paper it fills them for has to be the paper
74
- the browser actually prints on (and it's the same page: field the DOCX
75
- export already honors). */
76
- size: ${pageFormat === 'a4' ? 'A4' : 'letter'};
77
- margin: ${style.page.marginTopIn}in ${style.page.marginRightIn}in ${style.page.marginBottomIn}in ${style.page.marginLeftIn}in;
78
- }
79
- body {
80
- font-family: ${fontStack};
81
- margin: 0;
82
- padding: 0;
83
- background: white;
84
- color: black;
85
- font-size: 11pt;
86
- }
87
- /* Cover layouts — one of these two classes is on .print-cover-page, picked by
88
- the style's coverLayout (see types.ts). Both wrap the same markup: a
89
- .print-cover-top block (title / by-line / logline) and a .print-cover-bottom
90
- block (date / contact). */
91
- .print-cover-centered {
92
- height: 80vh;
93
- display: flex;
94
- flex-direction: column;
95
- align-items: center;
96
- justify-content: center;
97
- text-align: center;
98
- }
99
- .print-cover-corners {
100
- height: 88vh;
101
- display: flex;
102
- flex-direction: column;
103
- justify-content: space-between;
104
- text-align: left;
105
- }
106
- /* Down from the top edge, not glued to it — the reference script starts its
107
- title roughly a quarter of the way down the page. */
108
- .print-cover-corners .print-cover-top {
109
- padding-top: 20%;
110
- }
111
- .print-cover-corners .print-cover-bottom {
112
- align-self: flex-end;
113
- padding-bottom: 6%;
114
- max-width: 45%;
115
- }
116
- .print-cover-corners .print-cover-logline {
117
- margin-top: 12pt;
118
- max-width: 75%;
119
- }
120
- /* The corner block is its own visual group: the centered layout's big
121
- separating margins would push it off the page bottom. */
122
- .print-cover-corners .print-cover-date,
123
- .print-cover-corners .print-cover-contact {
124
- margin-top: 0;
125
- }
126
- .print-cover-title {
127
- ${fontProps(e.title)}
128
- text-transform: uppercase;
129
- margin-bottom: 20px;
130
- }
131
- .print-cover-by {
132
- font-size: 12pt;
133
- margin: 10px 0;
134
- }
135
- .print-cover-author {
136
- ${fontProps(e.author)}
137
- }
138
- .print-cover-logline {
139
- ${fontProps(e.logline)}
140
- margin-top: 40px;
141
- max-width: 600px;
142
- white-space: pre-line;
143
- }
144
- .print-cover-date {
145
- font-size: 10pt;
146
- margin-top: 50px;
147
- color: #777;
148
- }
149
- .print-cover-contact {
150
- font-size: 11pt;
151
- margin-top: 24px;
152
- color: #888;
153
- white-space: pre-line;
154
- }
155
-
156
- /* The preview lays out at exactly the printable width, on screen as well as on
157
- paper. print-paginator.ts measures where the page breaks fall in the live
158
- window, so if the on-screen line breaks differed from the printed ones (a
159
- 1280px-wide window wraps far later than a 6in column) every page it filled
160
- would overflow its sheet when printed. */
161
- #print-flow, #print-pages, .print-cover-page {
162
- width: ${contentWidthIn}in;
163
- margin-left: auto;
164
- margin-right: auto;
165
- }
166
-
167
- /* One printed sheet of body content, built by print-paginator.ts: an exact
168
- page-height box (set inline, from the paper size) so the browser puts exactly
169
- one on each sheet, with the running header at its top. */
170
- .print-page {
171
- break-after: page;
172
- page-break-after: always;
173
- box-sizing: border-box;
174
- overflow: visible;
175
- }
176
- .print-page:last-child {
177
- break-after: auto;
178
- page-break-after: auto;
179
- }
180
- .print-page-body {
181
- position: relative;
182
- display: flow-root; /* so a first child's margin-top counts inside the page */
183
- }
184
- .print-page-header {
185
- display: flex;
186
- justify-content: space-between;
187
- align-items: baseline;
188
- font-size: ${e.action.fontSizePt}pt;
189
- font-style: italic;
190
- margin-bottom: 24pt;
191
- }
192
- @media screen {
193
- .print-page {
194
- border-bottom: 1px dashed #ccc;
195
- margin-bottom: 24px;
196
- }
197
- }
198
-
199
- /* Screenplay items */
200
- .print-scene-heading {
201
- ${fontProps(e.sceneHeading)}
202
- text-transform: uppercase;
203
- ${marginProps(e.sceneHeading)}
204
- display: flex;
205
- justify-content: space-between;${KEEP_WITH_NEXT}
206
- }
207
- .print-action {
208
- ${fontProps(e.action)}
209
- ${marginProps(e.action)}
210
- line-height: 1.4;${ORPHAN_WIDOW}
211
- }
212
- .print-character-block {
213
- margin-top: ${e.character.spaceBeforePt ?? 0}pt;
214
- margin-bottom: ${e.character.spaceAfterPt ?? 0}pt;
215
- }
216
- .print-character {
217
- ${fontProps(e.character)}
218
- margin-left: ${e.character.marginLeftPct ?? 0}%;
219
- margin-bottom: 2pt;${KEEP_WITH_NEXT}
220
- }
221
- .print-parenthetical {
222
- ${fontProps(e.parenthetical)}
223
- margin-left: ${e.parenthetical.marginLeftPct ?? 0}%;
224
- margin-right: ${e.parenthetical.marginRightPct ?? 0}%;
225
- margin-bottom: 2pt;${KEEP_WITH_NEXT}
226
- }
227
- .print-dialogue {
228
- ${fontProps(e.dialogue)}
229
- ${marginProps(e.dialogue)}
230
- line-height: 1.3;${ORPHAN_WIDOW}
231
- }
232
- .print-transition {
233
- ${fontProps(e.transition)}
234
- ${marginProps(e.transition)}
235
- text-transform: uppercase;${KEEP_TOGETHER}
236
- }
237
- .print-centered {
238
- ${fontProps(e.centered)}
239
- ${marginProps(e.centered)}
240
- }
241
- .print-lyrics {
242
- ${fontProps(e.lyrics)}
243
- ${marginProps(e.lyrics)}${ORPHAN_WIDOW}
244
- }
245
- .print-note {
246
- background: #fffbeb;
247
- border-left: 2px solid #d97706;
248
- padding: 4px 8px;
249
- ${fontProps(e.note)}
250
- ${marginProps(e.note)}
251
- }
252
- .print-comment {
253
- ${fontProps(e.comment)}
254
- ${marginProps(e.comment)}
255
- }
256
- .print-section {
257
- ${fontProps(e.section)}
258
- ${marginProps(e.section)}${KEEP_WITH_NEXT}
259
- }
260
- .print-page-break {
261
- page-break-after: always;
262
- }
263
-
264
- /* Dual dialogue */
265
- .print-dual-dialogue {
266
- display: flex;
267
- margin-top: 6pt;
268
- margin-bottom: 6pt;
269
- }
270
- .print-dual-column {
271
- flex: 1;
272
- min-width: 0;
273
- }
274
- .print-left-column {
275
- margin-right: 20px;
276
- }
277
- .print-right-column {
278
- margin-left: 20px;
279
- }
280
- .print-dual-column .print-character {
281
- margin-left: 10%;
282
- }
283
- .print-dual-column .print-parenthetical,
284
- .print-dual-column .print-dialogue {
285
- margin-left: 10%;
286
- margin-right: 5%;
287
- }`
288
- }
@@ -1,20 +0,0 @@
1
- import type { ScreenplayStyleDefinition } from './types.js'
2
- import { DEFAULT_STYLE_ID } from './types.js'
3
- import { BUILTIN_STYLES, getBuiltinStyle } from './builtin-styles.js'
4
-
5
- /** Resolves a style id (usually from frontmatter's `style:` field) to a definition.
6
- * Falls back to the default style when the id is missing or unrecognized — e.g. a
7
- * document referencing a custom style the current machine/browser hasn't imported. */
8
- export function resolveStyle(
9
- id: string | undefined,
10
- customStyles: ScreenplayStyleDefinition[],
11
- ): ScreenplayStyleDefinition {
12
- const wanted = id ?? DEFAULT_STYLE_ID
13
- return getBuiltinStyle(wanted)
14
- ?? customStyles.find(s => s.id === wanted)
15
- ?? getBuiltinStyle(DEFAULT_STYLE_ID)!
16
- }
17
-
18
- export function allStyles(customStyles: ScreenplayStyleDefinition[]): ScreenplayStyleDefinition[] {
19
- return [...BUILTIN_STYLES, ...customStyles]
20
- }