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