@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.
- package/README.md +87 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
- package/dist/ai/VoiceConfirmDialog.js +6 -6
- package/dist/ai/VoiceToolbar.d.ts +1 -0
- package/dist/ai/VoiceToolbar.js +22 -16
- package/dist/ai/clean-response.d.ts +7 -0
- package/dist/ai/clean-response.js +58 -0
- package/dist/ai/format-transcript.d.ts +3 -0
- package/dist/ai/format-transcript.js +7 -0
- package/dist/ai/prompts.d.ts +8 -1
- package/dist/ai/prompts.js +34 -7
- package/dist/autocomplete/suggestions.js +1 -1
- package/dist/context/DocumentContext.d.ts +28 -4
- package/dist/context/DocumentContext.js +67 -37
- package/dist/editor/EditorView.js +32 -4
- package/dist/editor/SelectionMenu.js +5 -3
- package/dist/editor/external-decorations-plugin.d.ts +23 -0
- package/dist/editor/external-decorations-plugin.js +151 -0
- package/dist/editor/omitted-scene-plugin.d.ts +9 -0
- package/dist/editor/omitted-scene-plugin.js +44 -0
- package/dist/editor/plugins.js +4 -0
- package/dist/editor/private-metadata-plugin.d.ts +10 -0
- package/dist/editor/private-metadata-plugin.js +60 -0
- package/dist/editor/prosemirror-to-sutra.js +179 -74
- package/dist/editor/scene-heading-view.js +2 -0
- package/dist/editor/schema.js +69 -12
- package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
- package/dist/editor/sutra-to-prosemirror.js +32 -34
- package/dist/extensions/ai-provider.d.ts +44 -0
- package/dist/extensions/ai-provider.js +27 -0
- package/dist/extensions/collab.d.ts +21 -0
- package/dist/extensions/collab.js +6 -0
- package/dist/extensions/command-registry.d.ts +49 -0
- package/dist/extensions/command-registry.js +28 -0
- package/dist/extensions/decorations.d.ts +48 -0
- package/dist/extensions/decorations.js +6 -0
- package/dist/extensions/export-registry.d.ts +59 -0
- package/dist/extensions/export-registry.js +10 -0
- package/dist/extensions/panel-registry.d.ts +30 -0
- package/dist/extensions/panel-registry.js +16 -0
- package/dist/extensions/registries.d.ts +15 -0
- package/dist/extensions/registries.js +12 -0
- package/dist/extensions/registry-store.d.ts +26 -0
- package/dist/extensions/registry-store.js +35 -0
- package/dist/extensions/session.d.ts +29 -0
- package/dist/extensions/session.js +20 -0
- package/dist/extensions/speech-provider.d.ts +35 -0
- package/dist/extensions/speech-provider.js +17 -0
- package/dist/file/builtin-exporters.d.ts +9 -0
- package/dist/file/builtin-exporters.js +72 -0
- package/dist/file/docx-exporter.js +7 -4
- package/dist/file/docx-importer.d.ts +9 -0
- package/dist/file/docx-importer.js +47 -19
- package/dist/file/scene-number.d.ts +5 -0
- package/dist/file/scene-number.js +8 -0
- package/dist/file/workflow-reports.js +13 -7
- package/dist/findreplace/FindReplace.js +5 -2
- package/dist/import/AIImportDialog.d.ts +11 -0
- package/dist/import/AIImportDialog.js +112 -0
- package/dist/import/ai-import.d.ts +51 -0
- package/dist/import/ai-import.js +200 -0
- package/dist/import/apply-import.d.ts +7 -0
- package/dist/import/apply-import.js +35 -0
- package/dist/import/extract.d.ts +17 -0
- package/dist/import/extract.js +18 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +7 -0
- package/dist/locales/bn.json +8 -0
- package/dist/locales/en.json +8 -0
- package/dist/locales/gu.json +8 -0
- package/dist/locales/hi.json +8 -0
- package/dist/locales/kn.json +8 -0
- package/dist/locales/ml.json +8 -0
- package/dist/locales/mr.json +8 -0
- package/dist/locales/or.json +8 -0
- package/dist/locales/pa.json +8 -0
- package/dist/locales/si.json +8 -0
- package/dist/locales/ta.json +8 -0
- package/dist/locales/te.json +8 -0
- package/dist/navigator/SceneNavigator.js +125 -131
- package/dist/navigator/StatisticsDialog.js +5 -1
- package/dist/navigator/scene-edits.d.ts +18 -0
- package/dist/navigator/scene-edits.js +143 -0
- package/dist/navigator/scene-list.js +4 -4
- package/dist/navigator/scene-numbering.d.ts +37 -0
- package/dist/navigator/scene-numbering.js +88 -0
- package/dist/settings/SettingsDialog.js +8 -120
- package/dist/shell/AppBar.js +21 -2
- package/dist/shell/AppShell.js +18 -4
- package/dist/shell/ElementToolbar.d.ts +5 -0
- package/dist/shell/ElementToolbar.js +35 -25
- package/dist/shell/LanguageStatus.js +6 -0
- package/dist/shell/StatusBar.js +11 -6
- package/dist/shell/builtin-commands.d.ts +10 -0
- package/dist/shell/builtin-commands.js +43 -0
- package/dist/shell/icons.d.ts +6 -0
- package/dist/shell/icons.js +6 -0
- package/dist/shell/use-commands.d.ts +7 -0
- package/dist/shell/use-commands.js +24 -0
- package/dist/shell/use-panel-context.d.ts +3 -0
- package/dist/shell/use-panel-context.js +39 -0
- package/dist/source/SourceView.js +18 -1
- package/dist/styles/screenplay.css +113 -1
- package/dist/titlepage/ExportDialog.d.ts +5 -0
- package/dist/titlepage/ExportDialog.js +59 -96
- package/package.json +32 -5
- package/dist/ai/OnboardingDialog.d.ts +0 -6
- package/dist/ai/OnboardingDialog.js +0 -115
- package/dist/ai/ai-client.d.ts +0 -71
- package/dist/ai/ai-client.js +0 -494
- package/dist/ai/generated-providers.json +0 -3696
- package/dist/ai/voice-service.d.ts +0 -15
- package/dist/ai/voice-service.js +0 -81
- package/dist/editor/character-view.d.ts +0 -35
- package/dist/editor/character-view.js +0 -112
- package/dist/editor/transliterate-input.d.ts +0 -10
- package/dist/editor/transliterate-input.js +0 -88
- package/dist/extensions/types.d.ts +0 -73
- package/dist/extensions/types.js +0 -12
- 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,90 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
-
import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
|
|
4
|
-
import { TranslationProvider } from '../../src/i18n/useTranslation'
|
|
5
|
-
import { TitlePageDialog } from '../../src/titlepage/TitlePageDialog'
|
|
6
|
-
|
|
7
|
-
const SRC = `---
|
|
8
|
-
title: The Last Train
|
|
9
|
-
date: June 2026
|
|
10
|
-
lang: hi
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## INT. STATION - NIGHT
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
let latestText = ''
|
|
17
|
-
|
|
18
|
-
function Harness({ onClose }: { onClose: () => void }) {
|
|
19
|
-
const { text, setText, setMode } = useDocument()
|
|
20
|
-
useEffect(() => { setMode('source'); setText(SRC) }, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
21
|
-
latestText = text
|
|
22
|
-
return text === SRC || text.includes('title:') ? <TitlePageDialog onClose={onClose} /> : null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function renderDialog() {
|
|
26
|
-
const onClose = vi.fn()
|
|
27
|
-
render(
|
|
28
|
-
<TranslationProvider>
|
|
29
|
-
<DocumentProvider>
|
|
30
|
-
<Harness onClose={onClose} />
|
|
31
|
-
</DocumentProvider>
|
|
32
|
-
</TranslationProvider>,
|
|
33
|
-
)
|
|
34
|
-
return onClose
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('TitlePageDialog', () => {
|
|
38
|
-
it('shows a tab per section and opens on Title & Story, pre-filled from frontmatter', () => {
|
|
39
|
-
renderDialog()
|
|
40
|
-
for (const name of ['Title & Story', 'Credits', 'Draft', 'Contact & Rights', 'Document Settings', 'Other Fields']) {
|
|
41
|
-
expect(screen.getByRole('tab', { name })).toBeInTheDocument()
|
|
42
|
-
}
|
|
43
|
-
expect(screen.getByRole('tab', { name: 'Title & Story' })).toHaveAttribute('aria-selected', 'true')
|
|
44
|
-
expect(screen.getByLabelText('Title')).toHaveValue('The Last Train')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('switches panels on tab click, hiding fields from other tabs', () => {
|
|
48
|
-
// The inactive panel's fields stay in the DOM (form state must survive a tab
|
|
49
|
-
// switch), but the panel is hidden via the `hidden` attribute — jest-dom's
|
|
50
|
-
// toBeVisible() honors that (and ancestor `hidden`), so it's used instead of a
|
|
51
|
-
// presence check.
|
|
52
|
-
renderDialog()
|
|
53
|
-
expect(screen.getByLabelText('Primary language')).not.toBeVisible()
|
|
54
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Document Settings' }))
|
|
55
|
-
expect(screen.getByRole('tab', { name: 'Document Settings' })).toHaveAttribute('aria-selected', 'true')
|
|
56
|
-
expect(screen.getByLabelText('Primary language')).toBeVisible()
|
|
57
|
-
expect(screen.getByLabelText('Primary language')).toHaveValue('hi')
|
|
58
|
-
expect(screen.getByLabelText('Title')).not.toBeVisible()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('keeps a free-text date as text and offers the date picker', () => {
|
|
62
|
-
renderDialog()
|
|
63
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Draft' }))
|
|
64
|
-
const date = screen.getByLabelText('Date')
|
|
65
|
-
expect(date).toHaveValue('June 2026')
|
|
66
|
-
expect(date).not.toHaveAttribute('type', 'date')
|
|
67
|
-
fireEvent.click(screen.getByRole('button', { name: 'Pick a date' }))
|
|
68
|
-
expect(screen.getByLabelText('Date')).toHaveAttribute('type', 'date')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('offers style and language dropdowns and writes the choices to the frontmatter', () => {
|
|
72
|
-
const onClose = renderDialog()
|
|
73
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Document Settings' }))
|
|
74
|
-
fireEvent.change(screen.getByLabelText('Screenplay style'), { target: { value: 'traditional' } })
|
|
75
|
-
fireEvent.change(screen.getByLabelText('Other languages'), { target: { value: 'en' } })
|
|
76
|
-
fireEvent.change(screen.getByLabelText('Page size'), { target: { value: 'A4' } })
|
|
77
|
-
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
|
78
|
-
expect(onClose).toHaveBeenCalled()
|
|
79
|
-
expect(latestText).toContain('style: traditional')
|
|
80
|
-
expect(latestText).toContain('lang-secondary: [en]')
|
|
81
|
-
expect(latestText).toContain('page: A4')
|
|
82
|
-
expect(latestText).toContain('date: June 2026')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('does not change the text when saved without edits', () => {
|
|
86
|
-
renderDialog()
|
|
87
|
-
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
|
88
|
-
expect(latestText).toBe(SRC)
|
|
89
|
-
})
|
|
90
|
-
})
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { EditorState } from 'prosemirror-state'
|
|
2
|
-
import { EditorView } from 'prosemirror-view'
|
|
3
|
-
import { history, undo } from 'prosemirror-history'
|
|
4
|
-
import { parse } from '@sutrata/parser'
|
|
5
|
-
import { schema } from '../../src/editor/schema'
|
|
6
|
-
import { sutraToProsemirror } from '../../src/editor/sutra-to-prosemirror'
|
|
7
|
-
import { prosemirrorToSutra } from '../../src/editor/prosemirror-to-sutra'
|
|
8
|
-
import { applyTitlePageText } from '../../src/editor/frontmatter-field'
|
|
9
|
-
import { readTitlePage, writeTitlePage } from '../../src/titlepage/frontmatter-form'
|
|
10
|
-
|
|
11
|
-
const SRC = `---
|
|
12
|
-
title: The Last Train
|
|
13
|
-
author: Meera
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## INT. STATION - NIGHT
|
|
17
|
-
|
|
18
|
-
Action line.
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
function mount(text: string) {
|
|
22
|
-
const state = EditorState.create({ doc: sutraToProsemirror(parse(text)), schema, plugins: [history()] })
|
|
23
|
-
return new EditorView(document.createElement('div'), { state })
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('applyTitlePageText (formatted mode)', () => {
|
|
27
|
-
it('swaps the title page in one undoable transaction and leaves the body alone', () => {
|
|
28
|
-
const view = mount(SRC)
|
|
29
|
-
const f = readTitlePage(parse(SRC).frontmatter!.data)
|
|
30
|
-
const next = writeTitlePage(SRC, f, { ...f, author: 'Vikram', style: 'traditional' })
|
|
31
|
-
const setText = vi.fn()
|
|
32
|
-
|
|
33
|
-
applyTitlePageText(view, next, setText)
|
|
34
|
-
|
|
35
|
-
expect(setText).not.toHaveBeenCalled()
|
|
36
|
-
const out = prosemirrorToSutra(view.state.doc)
|
|
37
|
-
expect(out).toContain('author: Vikram')
|
|
38
|
-
expect(out).toContain('style: traditional')
|
|
39
|
-
expect(out).toContain('Action line.')
|
|
40
|
-
|
|
41
|
-
undo(view.state, view.dispatch)
|
|
42
|
-
expect(prosemirrorToSutra(view.state.doc)).toContain('author: Meera')
|
|
43
|
-
view.destroy()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('inserts a title page when the document had none', () => {
|
|
47
|
-
const body = '## INT. ROOM - DAY\n\nAction.\n'
|
|
48
|
-
const view = mount(body)
|
|
49
|
-
const f = readTitlePage(undefined)
|
|
50
|
-
applyTitlePageText(view, writeTitlePage(body, f, { ...f, title: 'New Film' }), vi.fn())
|
|
51
|
-
expect(view.state.doc.firstChild!.type.name).toBe('title_page')
|
|
52
|
-
expect(prosemirrorToSutra(view.state.doc)).toContain('title: New Film')
|
|
53
|
-
view.destroy()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('falls back to setting the text when no editor view is mounted', () => {
|
|
57
|
-
const setText = vi.fn()
|
|
58
|
-
applyTitlePageText(null, 'X', setText)
|
|
59
|
-
expect(setText).toHaveBeenCalledWith('X')
|
|
60
|
-
})
|
|
61
|
-
})
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { parse } from '@sutrata/parser'
|
|
2
|
-
import { readTitlePage, writeTitlePage, splitList } from '../../src/titlepage/frontmatter-form'
|
|
3
|
-
|
|
4
|
-
const SRC = `---
|
|
5
|
-
format: sutra/1.0
|
|
6
|
-
title:
|
|
7
|
-
hi: आख़िरी ट्रेन
|
|
8
|
-
en: The Last Train
|
|
9
|
-
author: रंगी पराता
|
|
10
|
-
date: June 2026
|
|
11
|
-
contact: |
|
|
12
|
-
rangi@example.com
|
|
13
|
-
+91 98xxx xxxxx
|
|
14
|
-
lang: hi
|
|
15
|
-
lang-secondary: [en]
|
|
16
|
-
producer: Big Films
|
|
17
|
-
characters:
|
|
18
|
-
JOHN: Actor One
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## INT. STATION - NIGHT
|
|
22
|
-
`
|
|
23
|
-
|
|
24
|
-
const read = (text: string) => readTitlePage(parse(text).frontmatter?.data as Record<string, unknown>)
|
|
25
|
-
|
|
26
|
-
describe('readTitlePage', () => {
|
|
27
|
-
it('reads the multilingual title with the primary language first', () => {
|
|
28
|
-
const f = read(SRC)
|
|
29
|
-
expect(f.title).toBe('आख़िरी ट्रेन')
|
|
30
|
-
expect(f.altTitles).toEqual([{ lang: 'en', text: 'The Last Train' }])
|
|
31
|
-
expect(f.lang).toBe('hi')
|
|
32
|
-
expect(f.langSecondary).toEqual(['en'])
|
|
33
|
-
expect(f.contact).toBe('rangi@example.com\n+91 98xxx xxxxx')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('lists simple unknown keys as custom rows, hides format and nested maps', () => {
|
|
37
|
-
const f = read(SRC)
|
|
38
|
-
expect(f.custom).toEqual([{ key: 'producer', value: 'Big Films' }])
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('splits list values in every shape', () => {
|
|
42
|
-
expect(splitList('[en, sa]')).toEqual(['en', 'sa'])
|
|
43
|
-
expect(splitList('en')).toEqual(['en'])
|
|
44
|
-
expect(splitList(['ta'])).toEqual(['ta'])
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('writeTitlePage', () => {
|
|
49
|
-
it('leaves the text byte-identical when nothing changed', () => {
|
|
50
|
-
const f = read(SRC)
|
|
51
|
-
expect(writeTitlePage(SRC, f, { ...f })).toBe(SRC)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('rewrites only the changed key and keeps everything else in place', () => {
|
|
55
|
-
const f = read(SRC)
|
|
56
|
-
const out = writeTitlePage(SRC, f, { ...f, author: 'Meera Nair' })
|
|
57
|
-
expect(out).toBe(SRC.replace('author: रंगी पराता', 'author: Meera Nair'))
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('appends new keys in canonical order before the closing fence', () => {
|
|
61
|
-
const f = read(SRC)
|
|
62
|
-
const out = writeTitlePage(SRC, f, { ...f, style: 'traditional', page: 'A4', draft: 'Second Draft' })
|
|
63
|
-
const fm = out.split('---')[1]!
|
|
64
|
-
expect(fm.trimEnd().split('\n').slice(-3)).toEqual(['draft: Second Draft', 'page: A4', 'style: traditional'])
|
|
65
|
-
expect(parse(out).frontmatter!.data['style']).toBe('traditional')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('removes keys that were cleared', () => {
|
|
69
|
-
const f = read(SRC)
|
|
70
|
-
const out = writeTitlePage(SRC, f, { ...f, contact: '' })
|
|
71
|
-
expect(out).not.toContain('contact')
|
|
72
|
-
expect(out).not.toContain('rangi@example.com')
|
|
73
|
-
expect(out).toContain('author: रंगी पराता')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('writes the multilingual title as a nested map and a plain title as a scalar', () => {
|
|
77
|
-
const f = read(SRC)
|
|
78
|
-
const both = writeTitlePage(SRC, f, { ...f, altTitles: [...f.altTitles, { lang: 'ta', text: 'கடைசி ரயில்' }] })
|
|
79
|
-
expect(parse(both).frontmatter!.data['title']).toEqual({ hi: 'आख़िरी ट्रेन', en: 'The Last Train', ta: 'கடைசி ரயில்' })
|
|
80
|
-
const single = writeTitlePage(SRC, f, { ...f, altTitles: [] })
|
|
81
|
-
expect(parse(single).frontmatter!.data['title']).toBe('आख़िरी ट्रेन')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('writes secondary languages as a flow list and multi-line contact as a block', () => {
|
|
85
|
-
const f = read(SRC)
|
|
86
|
-
const out = writeTitlePage(SRC, f, { ...f, langSecondary: ['en', 'sa'], contact: 'a@b.c\nPhone' })
|
|
87
|
-
expect(out).toContain('lang-secondary: [en, sa]')
|
|
88
|
-
expect(out).toContain('contact: |\n a@b.c\n Phone')
|
|
89
|
-
expect(parse(out).frontmatter!.data['contact']).toBe('a@b.c\nPhone')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('edits, removes and adds custom fields, never touching nested unknown maps', () => {
|
|
93
|
-
const f = read(SRC)
|
|
94
|
-
const out = writeTitlePage(SRC, f, { ...f, custom: [{ key: 'studio', value: 'Lotus' }] })
|
|
95
|
-
expect(out).not.toContain('producer:')
|
|
96
|
-
expect(out).toContain('studio: Lotus')
|
|
97
|
-
expect(out).toContain('characters:\n JOHN: Actor One')
|
|
98
|
-
expect(out).toContain('format: sutra/1.0')
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('creates a frontmatter block when the document has none', () => {
|
|
102
|
-
const body = '## INT. ROOM - DAY\n'
|
|
103
|
-
const f = read(body)
|
|
104
|
-
const out = writeTitlePage(body, f, { ...f, title: 'New Film', date: '2026-09-19' })
|
|
105
|
-
expect(out).toBe('---\ntitle: New Film\ndate: 2026-09-19\n---\n\n## INT. ROOM - DAY\n')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('preserves CRLF line endings', () => {
|
|
109
|
-
const crlf = SRC.replace(/\n/g, '\r\n')
|
|
110
|
-
const f = read(crlf)
|
|
111
|
-
const out = writeTitlePage(crlf, f, { ...f, author: 'X' })
|
|
112
|
-
expect(out).toContain('author: X\r\n')
|
|
113
|
-
expect(out.replace(/\r\n/g, '')).not.toContain('\n')
|
|
114
|
-
})
|
|
115
|
-
})
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { EditorState, TextSelection } from 'prosemirror-state'
|
|
3
|
-
import { schema } from '../src/editor/schema'
|
|
4
|
-
import { makeSetBlock, makeSetOrToggleNote, makeToggleMark } from '../src/shell/toolbar-actions'
|
|
5
|
-
|
|
6
|
-
function stateWith(typeName: string, text: string) {
|
|
7
|
-
const node = schema.nodes[typeName]!.create(null, text ? schema.text(text) : undefined)
|
|
8
|
-
const doc = schema.nodes['doc']!.create(null, node)
|
|
9
|
-
let state = EditorState.create({ schema, doc })
|
|
10
|
-
state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1)))
|
|
11
|
-
return state
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('makeSetBlock', () => {
|
|
15
|
-
it('changes the current block type', () => {
|
|
16
|
-
const state = stateWith('action', 'hello')
|
|
17
|
-
let next = state
|
|
18
|
-
makeSetBlock('character')(state, (tr) => { next = state.apply(tr) })
|
|
19
|
-
expect(next.doc.firstChild!.type.name).toBe('character')
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('makeToggleMark', () => {
|
|
24
|
-
it('returns false on empty selection (does nothing)', () => {
|
|
25
|
-
const state = stateWith('action', 'hello')
|
|
26
|
-
const result = makeToggleMark('bold')(state, () => {})
|
|
27
|
-
expect(result).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('applies the note_mark to a selected range, so an inline note can be created from the WYSIWYG toolbar', () => {
|
|
31
|
-
const state = stateWith('action', 'Add the rain here please')
|
|
32
|
-
const withSelection = state.apply(
|
|
33
|
-
state.tr.setSelection(TextSelection.create(state.doc, 1, 1 + 'Add the rain'.length))
|
|
34
|
-
)
|
|
35
|
-
let next = withSelection
|
|
36
|
-
const applied = makeToggleMark('note_mark')(withSelection, (tr) => { next = withSelection.apply(tr) })
|
|
37
|
-
expect(applied).toBe(true)
|
|
38
|
-
|
|
39
|
-
const markType = schema.marks['note_mark']!
|
|
40
|
-
let hasMark = false
|
|
41
|
-
next.doc.firstChild!.descendants(node => {
|
|
42
|
-
if (node.isText && node.marks.some(m => m.type === markType)) hasMark = true
|
|
43
|
-
})
|
|
44
|
-
expect(hasMark).toBe(true)
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
function noteMarkedText(doc: EditorState['doc']): string {
|
|
49
|
-
const markType = schema.marks['note_mark']!
|
|
50
|
-
let text = ''
|
|
51
|
-
doc.firstChild!.descendants(node => {
|
|
52
|
-
if (node.isText && node.marks.some(m => m.type === markType)) text += node.text
|
|
53
|
-
})
|
|
54
|
-
return text
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('makeSetOrToggleNote', () => {
|
|
58
|
-
it('marks only the selected text as a note on an Action line, leaving the rest of the line intact', () => {
|
|
59
|
-
const state = stateWith('action', 'Add the rain here please')
|
|
60
|
-
const withSelection = state.apply(
|
|
61
|
-
state.tr.setSelection(TextSelection.create(state.doc, 1, 1 + 'Add the rain'.length))
|
|
62
|
-
)
|
|
63
|
-
let next = withSelection
|
|
64
|
-
const applied = makeSetOrToggleNote()(withSelection, (tr) => { next = withSelection.apply(tr) })
|
|
65
|
-
|
|
66
|
-
expect(applied).toBe(true)
|
|
67
|
-
expect(next.doc.firstChild!.type.name).toBe('action')
|
|
68
|
-
expect(noteMarkedText(next.doc)).toBe('Add the rain')
|
|
69
|
-
expect(next.doc.textContent).toBe('Add the rain here please')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('marks the whole line as a note on an Action line with no selection', () => {
|
|
73
|
-
const state = stateWith('action', 'Add the rain here please')
|
|
74
|
-
let next = state
|
|
75
|
-
const applied = makeSetOrToggleNote()(state, (tr) => { next = state.apply(tr) })
|
|
76
|
-
|
|
77
|
-
expect(applied).toBe(true)
|
|
78
|
-
expect(next.doc.firstChild!.type.name).toBe('action')
|
|
79
|
-
expect(noteMarkedText(next.doc)).toBe('Add the rain here please')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('falls back to converting the whole block to a note element on any line other than Action', () => {
|
|
83
|
-
const state = stateWith('dialogue', 'We should go home now.')
|
|
84
|
-
const withSelection = state.apply(
|
|
85
|
-
state.tr.setSelection(TextSelection.create(state.doc, 1, 1 + 'We should'.length))
|
|
86
|
-
)
|
|
87
|
-
let next = withSelection
|
|
88
|
-
const applied = makeSetOrToggleNote()(withSelection, (tr) => { next = withSelection.apply(tr) })
|
|
89
|
-
|
|
90
|
-
expect(applied).toBe(true)
|
|
91
|
-
expect(next.doc.firstChild!.type.name).toBe('note')
|
|
92
|
-
})
|
|
93
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseVoiceCommand } from '../src/ai/commands';
|
|
3
|
-
|
|
4
|
-
describe('Voice Commands Pre-processor', () => {
|
|
5
|
-
it('should parse English commands correctly', () => {
|
|
6
|
-
const result1 = parseVoiceCommand('new scene INT. HALLWAYS - DAY', 'en-US');
|
|
7
|
-
expect(result1.type).toBe('scene');
|
|
8
|
-
expect(result1.text).toBe('INT. HALLWAYS - DAY');
|
|
9
|
-
expect(result1.isLiteral).toBe(false);
|
|
10
|
-
|
|
11
|
-
const result2 = parseVoiceCommand('character RAJ', 'en-US');
|
|
12
|
-
expect(result2.type).toBe('character');
|
|
13
|
-
expect(result2.text).toBe('RAJ');
|
|
14
|
-
|
|
15
|
-
const result3 = parseVoiceCommand('dialogue Hello world', 'en-US');
|
|
16
|
-
expect(result3.type).toBe('dialogue');
|
|
17
|
-
expect(result3.text).toBe('Hello world');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should parse Hindi commands correctly', () => {
|
|
21
|
-
const result1 = parseVoiceCommand('नया दृश्य अंतःपुर - दिन', 'hi-IN');
|
|
22
|
-
expect(result1.type).toBe('scene');
|
|
23
|
-
expect(result1.text).toBe('अंतःपुर - दिन');
|
|
24
|
-
|
|
25
|
-
const result2 = parseVoiceCommand('पात्र राहुल', 'hi-IN');
|
|
26
|
-
expect(result2.type).toBe('character');
|
|
27
|
-
expect(result2.text).toBe('राहुल');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should parse Tamil commands correctly', () => {
|
|
31
|
-
const result1 = parseVoiceCommand('புதிய காட்சி தெரு - பகல்', 'ta-IN');
|
|
32
|
-
expect(result1.type).toBe('scene');
|
|
33
|
-
expect(result1.text).toBe('தெரு - பகல்');
|
|
34
|
-
|
|
35
|
-
const result2 = parseVoiceCommand('வசனம் வணக்கம் உலகமே', 'ta-IN');
|
|
36
|
-
expect(result2.type).toBe('dialogue');
|
|
37
|
-
expect(result2.text).toBe('வணக்கம் உலகமே');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should handle literal bypass keyword to escape commands', () => {
|
|
41
|
-
const result1 = parseVoiceCommand('literal new scene heading', 'en-US');
|
|
42
|
-
expect(result1.type).toBe('action');
|
|
43
|
-
expect(result1.text).toBe('new scene heading');
|
|
44
|
-
expect(result1.isLiteral).toBe(true);
|
|
45
|
-
|
|
46
|
-
const result2 = parseVoiceCommand('शब्दशः नया दृश्य', 'hi-IN');
|
|
47
|
-
expect(result2.type).toBe('action');
|
|
48
|
-
expect(result2.text).toBe('नया दृश्य');
|
|
49
|
-
expect(result2.isLiteral).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should default to action type for plain speech', () => {
|
|
53
|
-
const result = parseVoiceCommand('The sun rises slowly over the mountains.', 'en-US');
|
|
54
|
-
expect(result.type).toBe('action');
|
|
55
|
-
expect(result.text).toBe('The sun rises slowly over the mountains.');
|
|
56
|
-
expect(result.isLiteral).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"jsx": "react-jsx",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noUncheckedIndexedAccess": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"outDir": "./dist",
|
|
12
|
-
"rootDir": "./src",
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"skipLibCheck": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src"]
|
|
17
|
-
}
|
package/vitest.config.ts
DELETED
package/vitest.setup.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import '@testing-library/jest-dom/vitest'
|
|
2
|
-
import { vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
// jsdom doesn't implement URL.createObjectURL / revokeObjectURL
|
|
5
|
-
if (typeof URL.createObjectURL === 'undefined') {
|
|
6
|
-
URL.createObjectURL = vi.fn(() => 'blob:mock')
|
|
7
|
-
URL.revokeObjectURL = vi.fn()
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Mock canvas for jsdom environment - jsdom doesn't fully implement canvas API
|
|
11
|
-
if (typeof window !== 'undefined') {
|
|
12
|
-
const mockCanvas2DContext = {
|
|
13
|
-
fillStyle: 'black',
|
|
14
|
-
font: '12px monospace',
|
|
15
|
-
fillText: vi.fn(),
|
|
16
|
-
clearRect: vi.fn(),
|
|
17
|
-
getImageData: vi.fn((x, y, width, height) => {
|
|
18
|
-
// Return a mock ImageData object
|
|
19
|
-
const data = new Uint8ClampedArray(width * height * 4)
|
|
20
|
-
return {
|
|
21
|
-
data,
|
|
22
|
-
width,
|
|
23
|
-
height,
|
|
24
|
-
colorSpace: 'srgb',
|
|
25
|
-
}
|
|
26
|
-
}),
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Mock HTMLCanvasElement.prototype.getContext
|
|
30
|
-
HTMLCanvasElement.prototype.getContext = vi.fn(function (
|
|
31
|
-
contextType: string
|
|
32
|
-
) {
|
|
33
|
-
if (contextType === '2d') {
|
|
34
|
-
return { ...mockCanvas2DContext }
|
|
35
|
-
}
|
|
36
|
-
return null
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Mock localStorage for jsdom / Node 22 compatibility
|
|
41
|
-
const createStorageMock = () => {
|
|
42
|
-
let store: Record<string, string> = {}
|
|
43
|
-
return {
|
|
44
|
-
getItem: vi.fn((key: string) => store[key] ?? null),
|
|
45
|
-
setItem: vi.fn((key: string, value: string) => { store[key] = String(value) }),
|
|
46
|
-
removeItem: vi.fn((key: string) => { delete store[key] }),
|
|
47
|
-
clear: vi.fn(() => { store = {} }),
|
|
48
|
-
get length() { return Object.keys(store).length },
|
|
49
|
-
key: vi.fn((i: number) => Object.keys(store)[i] ?? null),
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const storageMock = createStorageMock()
|
|
54
|
-
if (typeof window !== 'undefined') {
|
|
55
|
-
Object.defineProperty(window, 'localStorage', {
|
|
56
|
-
value: storageMock,
|
|
57
|
-
writable: true,
|
|
58
|
-
configurable: true,
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
Object.defineProperty(globalThis, 'localStorage', {
|
|
62
|
-
value: storageMock,
|
|
63
|
-
writable: true,
|
|
64
|
-
configurable: true,
|
|
65
|
-
})
|