@sutrata/editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/dist/ai/OnboardingDialog.d.ts +6 -0
- package/dist/ai/OnboardingDialog.js +115 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
- package/dist/ai/VoiceConfirmDialog.js +37 -0
- package/dist/ai/VoiceToolbar.d.ts +2 -0
- package/dist/ai/VoiceToolbar.js +177 -0
- package/dist/ai/ai-client.d.ts +71 -0
- package/dist/ai/ai-client.js +494 -0
- package/dist/ai/commands.d.ts +12 -0
- package/dist/ai/commands.js +140 -0
- package/dist/ai/generated-providers.json +3696 -0
- package/dist/ai/prompts.d.ts +25 -0
- package/dist/ai/prompts.js +59 -0
- package/dist/ai/voice-service.d.ts +15 -0
- package/dist/ai/voice-service.js +81 -0
- package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
- package/dist/autocomplete/AutocompletePlugin.js +19 -0
- package/dist/autocomplete/suggestions.d.ts +25 -0
- package/dist/autocomplete/suggestions.js +126 -0
- package/dist/context/DocumentContext.d.ts +68 -0
- package/dist/context/DocumentContext.js +605 -0
- package/dist/desktop-api.d.ts +40 -0
- package/dist/editor/EditorView.d.ts +2 -0
- package/dist/editor/EditorView.js +78 -0
- package/dist/editor/SelectionMenu.d.ts +2 -0
- package/dist/editor/SelectionMenu.js +102 -0
- package/dist/editor/attr-pencil-plugin.d.ts +17 -0
- package/dist/editor/attr-pencil-plugin.js +158 -0
- package/dist/editor/character-decoration-plugin.d.ts +26 -0
- package/dist/editor/character-decoration-plugin.js +77 -0
- package/dist/editor/character-view.d.ts +35 -0
- package/dist/editor/character-view.js +112 -0
- package/dist/editor/editor-bus.d.ts +14 -0
- package/dist/editor/editor-bus.js +39 -0
- package/dist/editor/find-highlight-plugin.d.ts +7 -0
- package/dist/editor/find-highlight-plugin.js +57 -0
- package/dist/editor/frontmatter-field-view.d.ts +10 -0
- package/dist/editor/frontmatter-field-view.js +48 -0
- package/dist/editor/frontmatter-field.d.ts +21 -0
- package/dist/editor/frontmatter-field.js +116 -0
- package/dist/editor/grapheme-cursor.d.ts +10 -0
- package/dist/editor/grapheme-cursor.js +104 -0
- package/dist/editor/ime-plugin.d.ts +18 -0
- package/dist/editor/ime-plugin.js +71 -0
- package/dist/editor/insert-helper.d.ts +1 -0
- package/dist/editor/insert-helper.js +42 -0
- package/dist/editor/keymap.d.ts +3 -0
- package/dist/editor/keymap.js +131 -0
- package/dist/editor/language-class-plugin.d.ts +7 -0
- package/dist/editor/language-class-plugin.js +75 -0
- package/dist/editor/plugins.d.ts +1 -0
- package/dist/editor/plugins.js +25 -0
- package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
- package/dist/editor/prosemirror-to-sutra.js +208 -0
- package/dist/editor/scene-heading-view.d.ts +27 -0
- package/dist/editor/scene-heading-view.js +136 -0
- package/dist/editor/schema.d.ts +3 -0
- package/dist/editor/schema.js +135 -0
- package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
- package/dist/editor/sutra-to-prosemirror.js +154 -0
- package/dist/editor/transliterate-input.d.ts +10 -0
- package/dist/editor/transliterate-input.js +88 -0
- package/dist/extensions/storage-adapter.d.ts +59 -0
- package/dist/extensions/storage-adapter.js +9 -0
- package/dist/extensions/types.d.ts +73 -0
- package/dist/extensions/types.js +12 -0
- package/dist/file/docx-exporter.d.ts +8 -0
- package/dist/file/docx-exporter.js +494 -0
- package/dist/file/docx-importer.d.ts +10 -0
- package/dist/file/docx-importer.js +283 -0
- package/dist/file/print-paginator.d.ts +29 -0
- package/dist/file/print-paginator.js +289 -0
- package/dist/file/templates.d.ts +2 -0
- package/dist/file/templates.js +66 -0
- package/dist/file/workflow-reports.d.ts +34 -0
- package/dist/file/workflow-reports.js +719 -0
- package/dist/findreplace/FindReplace.d.ts +2 -0
- package/dist/findreplace/FindReplace.js +164 -0
- package/dist/findreplace/find-engine.d.ts +29 -0
- package/dist/findreplace/find-engine.js +69 -0
- package/dist/findreplace/scroll-to-match.d.ts +29 -0
- package/dist/findreplace/scroll-to-match.js +74 -0
- package/dist/fonts/tofu-detect.d.ts +1 -0
- package/dist/fonts/tofu-detect.js +48 -0
- package/dist/i18n/LanguageContext.d.ts +10 -0
- package/dist/i18n/LanguageContext.js +174 -0
- package/dist/i18n/language-detect.d.ts +1 -0
- package/dist/i18n/language-detect.js +58 -0
- package/dist/i18n/useTranslation.d.ts +11 -0
- package/dist/i18n/useTranslation.js +60 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/locales/bn.json +143 -0
- package/dist/locales/en.json +143 -0
- package/dist/locales/gu.json +143 -0
- package/dist/locales/hi.json +143 -0
- package/dist/locales/kn.json +143 -0
- package/dist/locales/ml.json +143 -0
- package/dist/locales/mr.json +143 -0
- package/dist/locales/or.json +143 -0
- package/dist/locales/pa.json +143 -0
- package/dist/locales/si.json +143 -0
- package/dist/locales/ta.json +143 -0
- package/dist/locales/te.json +143 -0
- package/dist/navigator/SceneNavigator.d.ts +2 -0
- package/dist/navigator/SceneNavigator.js +612 -0
- package/dist/navigator/StatisticsDialog.d.ts +6 -0
- package/dist/navigator/StatisticsDialog.js +146 -0
- package/dist/navigator/scene-list.d.ts +16 -0
- package/dist/navigator/scene-list.js +70 -0
- package/dist/settings/SettingsDialog.d.ts +6 -0
- package/dist/settings/SettingsDialog.js +147 -0
- package/dist/shell/AppBar.d.ts +2 -0
- package/dist/shell/AppBar.js +34 -0
- package/dist/shell/AppShell.d.ts +2 -0
- package/dist/shell/AppShell.js +30 -0
- package/dist/shell/ConfirmDialog.d.ts +16 -0
- package/dist/shell/ConfirmDialog.js +23 -0
- package/dist/shell/ElementToolbar.d.ts +2 -0
- package/dist/shell/ElementToolbar.js +30 -0
- package/dist/shell/LanguageStatus.d.ts +2 -0
- package/dist/shell/LanguageStatus.js +65 -0
- package/dist/shell/StatusBar.d.ts +2 -0
- package/dist/shell/StatusBar.js +54 -0
- package/dist/shell/SutrataLogo.d.ts +8 -0
- package/dist/shell/SutrataLogo.js +7 -0
- package/dist/shell/Toast.d.ts +12 -0
- package/dist/shell/Toast.js +21 -0
- package/dist/shell/ToolbarButton.d.ts +11 -0
- package/dist/shell/ToolbarButton.js +4 -0
- package/dist/shell/VersionHistory.d.ts +12 -0
- package/dist/shell/VersionHistory.js +89 -0
- package/dist/shell/Watermark.d.ts +3 -0
- package/dist/shell/Watermark.js +10 -0
- package/dist/shell/fonts/fonts.css +1133 -0
- package/dist/shell/icons.d.ts +45 -0
- package/dist/shell/icons.js +51 -0
- package/dist/shell/toolbar-actions.d.ts +6 -0
- package/dist/shell/toolbar-actions.js +58 -0
- package/dist/source/SourceView.d.ts +1 -0
- package/dist/source/SourceView.js +106 -0
- package/dist/source/find-highlight-source.d.ts +14 -0
- package/dist/source/find-highlight-source.js +47 -0
- package/dist/source/sutra-lang.d.ts +2 -0
- package/dist/source/sutra-lang.js +97 -0
- package/dist/spellcheck/dictionaries.d.ts +9 -0
- package/dist/spellcheck/dictionaries.js +16 -0
- package/dist/spellcheck/hunspell.d.ts +23 -0
- package/dist/spellcheck/hunspell.js +94 -0
- package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
- package/dist/spellcheck/spellcheck-plugin.js +75 -0
- package/dist/styles/StyleDialog.d.ts +6 -0
- package/dist/styles/StyleDialog.js +63 -0
- package/dist/styles/builtin-styles.d.ts +43 -0
- package/dist/styles/builtin-styles.js +131 -0
- package/dist/styles/css-adapter.d.ts +32 -0
- package/dist/styles/css-adapter.js +71 -0
- package/dist/styles/docx-adapter.d.ts +7 -0
- package/dist/styles/docx-adapter.js +84 -0
- package/dist/styles/embed-fonts.d.ts +20 -0
- package/dist/styles/embed-fonts.js +81 -0
- package/dist/styles/pagination.d.ts +19 -0
- package/dist/styles/pagination.js +22 -0
- package/dist/styles/print-adapter.d.ts +6 -0
- package/dist/styles/print-adapter.js +288 -0
- package/dist/styles/registry.d.ts +6 -0
- package/dist/styles/registry.js +14 -0
- package/dist/styles/screenplay.css +3006 -0
- package/dist/styles/script-ranges.d.ts +6 -0
- package/dist/styles/script-ranges.js +17 -0
- package/dist/styles/types.d.ts +93 -0
- package/dist/styles/types.js +18 -0
- package/dist/styles/validate.d.ts +12 -0
- package/dist/styles/validate.js +105 -0
- package/dist/templates/templates.d.ts +10 -0
- package/dist/templates/templates.js +181 -0
- package/dist/titlepage/ExportDialog.d.ts +1 -0
- package/dist/titlepage/ExportDialog.js +152 -0
- package/dist/titlepage/TitlePageDialog.d.ts +6 -0
- package/dist/titlepage/TitlePageDialog.js +96 -0
- package/dist/titlepage/frontmatter-form.d.ts +56 -0
- package/dist/titlepage/frontmatter-form.js +245 -0
- package/dist/titlepage/languages.d.ts +10 -0
- package/dist/titlepage/languages.js +31 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
- package/scripts/copy-assets.mjs +16 -0
- package/scripts/generate-providers.mjs +127 -0
- package/src/ai/OnboardingDialog.tsx +390 -0
- package/src/ai/VoiceConfirmDialog.tsx +137 -0
- package/src/ai/VoiceToolbar.tsx +321 -0
- package/src/ai/ai-client.ts +596 -0
- package/src/ai/commands.ts +164 -0
- package/src/ai/generated-providers.json +3696 -0
- package/src/ai/prompts.ts +62 -0
- package/src/ai/voice-service.ts +96 -0
- package/src/autocomplete/AutocompletePlugin.ts +21 -0
- package/src/autocomplete/suggestions.ts +147 -0
- package/src/context/DocumentContext.tsx +648 -0
- package/src/desktop-api.d.ts +40 -0
- package/src/editor/EditorView.tsx +82 -0
- package/src/editor/SelectionMenu.tsx +152 -0
- package/src/editor/attr-pencil-plugin.ts +172 -0
- package/src/editor/character-decoration-plugin.ts +104 -0
- package/src/editor/editor-bus.ts +51 -0
- package/src/editor/find-highlight-plugin.ts +68 -0
- package/src/editor/frontmatter-field-view.ts +52 -0
- package/src/editor/frontmatter-field.ts +115 -0
- package/src/editor/grapheme-cursor.ts +99 -0
- package/src/editor/ime-plugin.ts +85 -0
- package/src/editor/insert-helper.ts +43 -0
- package/src/editor/keymap.ts +124 -0
- package/src/editor/language-class-plugin.ts +91 -0
- package/src/editor/plugins.ts +26 -0
- package/src/editor/prosemirror-to-sutra.ts +192 -0
- package/src/editor/scene-heading-view.ts +147 -0
- package/src/editor/schema.ts +156 -0
- package/src/editor/sutra-to-prosemirror.ts +156 -0
- package/src/extensions/storage-adapter.ts +65 -0
- package/src/extensions/types.ts +60 -0
- package/src/file/docx-exporter.ts +585 -0
- package/src/file/docx-importer.ts +313 -0
- package/src/file/print-paginator.ts +301 -0
- package/src/file/templates.ts +73 -0
- package/src/file/workflow-reports.ts +820 -0
- package/src/findreplace/FindReplace.tsx +242 -0
- package/src/findreplace/find-engine.ts +97 -0
- package/src/findreplace/scroll-to-match.ts +89 -0
- package/src/fonts/tofu-detect.ts +53 -0
- package/src/i18n/LanguageContext.tsx +207 -0
- package/src/i18n/language-detect.ts +73 -0
- package/src/i18n/useTranslation.tsx +74 -0
- package/src/index.ts +27 -0
- package/src/locales/bn.json +143 -0
- package/src/locales/en.json +143 -0
- package/src/locales/gu.json +143 -0
- package/src/locales/hi.json +143 -0
- package/src/locales/kn.json +143 -0
- package/src/locales/ml.json +143 -0
- package/src/locales/mr.json +143 -0
- package/src/locales/or.json +143 -0
- package/src/locales/pa.json +143 -0
- package/src/locales/si.json +143 -0
- package/src/locales/ta.json +143 -0
- package/src/locales/te.json +143 -0
- package/src/navigator/SceneNavigator.tsx +844 -0
- package/src/navigator/StatisticsDialog.tsx +249 -0
- package/src/navigator/scene-list.ts +84 -0
- package/src/settings/SettingsDialog.tsx +303 -0
- package/src/shell/AppBar.tsx +354 -0
- package/src/shell/AppShell.tsx +71 -0
- package/src/shell/ConfirmDialog.tsx +71 -0
- package/src/shell/ElementToolbar.tsx +175 -0
- package/src/shell/LanguageStatus.tsx +101 -0
- package/src/shell/StatusBar.tsx +135 -0
- package/src/shell/SutrataLogo.tsx +89 -0
- package/src/shell/Toast.tsx +46 -0
- package/src/shell/ToolbarButton.tsx +30 -0
- package/src/shell/VersionHistory.tsx +212 -0
- package/src/shell/Watermark.tsx +9 -0
- package/src/shell/fonts/fonts.css +1133 -0
- package/src/shell/icons.tsx +335 -0
- package/src/shell/toolbar-actions.ts +59 -0
- package/src/source/SourceView.tsx +111 -0
- package/src/source/find-highlight-source.ts +57 -0
- package/src/source/sutra-lang.ts +121 -0
- package/src/spellcheck/dictionaries.ts +21 -0
- package/src/spellcheck/hunspell.ts +110 -0
- package/src/spellcheck/spellcheck-plugin.ts +83 -0
- package/src/styles/StyleDialog.tsx +148 -0
- package/src/styles/builtin-styles.ts +139 -0
- package/src/styles/css-adapter.ts +76 -0
- package/src/styles/docx-adapter.ts +109 -0
- package/src/styles/embed-fonts.ts +81 -0
- package/src/styles/pagination.ts +27 -0
- package/src/styles/print-adapter.ts +288 -0
- package/src/styles/registry.ts +20 -0
- package/src/styles/screenplay.css +3006 -0
- package/src/styles/script-ranges.ts +17 -0
- package/src/styles/types.ts +104 -0
- package/src/styles/validate.ts +114 -0
- package/src/templates/templates.ts +194 -0
- package/src/titlepage/ExportDialog.tsx +494 -0
- package/src/titlepage/TitlePageDialog.tsx +312 -0
- package/src/titlepage/frontmatter-form.ts +280 -0
- package/src/titlepage/languages.ts +37 -0
- package/src/types.ts +7 -0
- package/tests/AppBar.test.tsx +35 -0
- package/tests/AppShell.test.tsx +22 -0
- package/tests/ElementToolbar.test.tsx +36 -0
- package/tests/LanguageStatus.test.tsx +129 -0
- package/tests/SelectionMenu.test.tsx +71 -0
- package/tests/ToolbarButton.test.tsx +24 -0
- package/tests/ai-onboarding.test.ts +102 -0
- package/tests/ai-status.test.tsx +72 -0
- package/tests/ai-structured.test.ts +94 -0
- package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
- package/tests/autocomplete/suggestions.test.ts +106 -0
- package/tests/document-context.test.tsx +565 -0
- package/tests/docx-importer.test.ts +160 -0
- package/tests/editor/character-decoration-plugin.test.ts +101 -0
- package/tests/editor/character-extension.test.ts +44 -0
- package/tests/editor/grapheme-cursor.test.ts +118 -0
- package/tests/editor/ime-plugin.test.ts +72 -0
- package/tests/editor/inline-note-bridge.test.ts +32 -0
- package/tests/editor/language-class-plugin.test.ts +265 -0
- package/tests/editor/title-page.test.ts +124 -0
- package/tests/embed-fonts.test.ts +17 -0
- package/tests/exporters.test.ts +365 -0
- package/tests/findreplace/FindReplace.test.tsx +391 -0
- package/tests/findreplace/find-engine.test.ts +86 -0
- package/tests/findreplace/scroll-to-match.test.ts +50 -0
- package/tests/fonts/tofu-detect.test.ts +122 -0
- package/tests/i18n/LanguageContext.test.tsx +146 -0
- package/tests/i18n/language-detect.test.ts +54 -0
- package/tests/i18n/locale-parity.test.ts +25 -0
- package/tests/i18n/useTranslation.test.tsx +45 -0
- package/tests/keymap.test.ts +18 -0
- package/tests/navigator/SceneNavigator.test.tsx +177 -0
- package/tests/navigator/StatisticsDialog.test.tsx +63 -0
- package/tests/navigator/scene-list.test.ts +71 -0
- package/tests/print-paginator.test.ts +82 -0
- package/tests/shell/VersionHistory.test.tsx +129 -0
- package/tests/spellcheck/hunspell.test.ts +82 -0
- package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
- package/tests/styles.test.ts +315 -0
- package/tests/templates/templates.test.ts +83 -0
- package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
- package/tests/titlepage/apply-title-page.test.ts +61 -0
- package/tests/titlepage/frontmatter-form.test.ts +115 -0
- package/tests/toolbar-actions.test.ts +93 -0
- package/tests/voice-commands.test.ts +58 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +11 -0
- package/vitest.setup.ts +65 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parse } from '@sutrata/parser'
|
|
3
|
+
import { exportToDocx } from '../src/file/docx-exporter'
|
|
4
|
+
import { importDocx } from '../src/file/docx-importer'
|
|
5
|
+
import JSZip from 'jszip'
|
|
6
|
+
|
|
7
|
+
const testSutra = `---
|
|
8
|
+
title: Test Movie
|
|
9
|
+
author: Writer Name
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Scene 1 {#1A}
|
|
13
|
+
|
|
14
|
+
Action with **bold**, *italic*, and _underline_ text.
|
|
15
|
+
|
|
16
|
+
@JOHN (V.O.)
|
|
17
|
+
Hello Mary.
|
|
18
|
+
|
|
19
|
+
@MARY
|
|
20
|
+
(smiling)
|
|
21
|
+
Hi John.
|
|
22
|
+
|
|
23
|
+
@JOHN
|
|
24
|
+
Dual dialogue left.
|
|
25
|
+
|
|
26
|
+
@MARY ^
|
|
27
|
+
Dual dialogue right.
|
|
28
|
+
|
|
29
|
+
>> CUT TO:
|
|
30
|
+
|
|
31
|
+
>> THE END <<
|
|
32
|
+
|
|
33
|
+
~ La la la
|
|
34
|
+
|
|
35
|
+
[[ Note text ]]
|
|
36
|
+
|
|
37
|
+
<!-- Comment text -->
|
|
38
|
+
|
|
39
|
+
===
|
|
40
|
+
|
|
41
|
+
# A Section
|
|
42
|
+
|
|
43
|
+
Final action line.
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
async function roundTrip(source: string) {
|
|
47
|
+
const ast = parse(source)
|
|
48
|
+
const blob = await exportToDocx(ast)
|
|
49
|
+
const buffer = await blob.arrayBuffer()
|
|
50
|
+
return importDocx(buffer)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('docx-importer', () => {
|
|
54
|
+
it('reconstructs the screenplay body from an exported docx', async () => {
|
|
55
|
+
const { bodyText, warnings } = await roundTrip(testSutra)
|
|
56
|
+
expect(bodyText).not.toBeNull()
|
|
57
|
+
|
|
58
|
+
// Reparse the reconstructed body (no frontmatter) and check it matches structurally.
|
|
59
|
+
const reparsed = parse(bodyText!)
|
|
60
|
+
const original = parse(testSutra)
|
|
61
|
+
|
|
62
|
+
expect(reparsed.children).toHaveLength(original.children.length)
|
|
63
|
+
|
|
64
|
+
const scene = reparsed.children[0]
|
|
65
|
+
expect(scene?.type).toBe('scene-heading')
|
|
66
|
+
if (scene?.type === 'scene-heading') {
|
|
67
|
+
expect(scene.text).toBe('SCENE 1')
|
|
68
|
+
expect(scene.id).toBe('1A')
|
|
69
|
+
|
|
70
|
+
const action = scene.children[0]
|
|
71
|
+
expect(action?.type).toBe('action')
|
|
72
|
+
if (action?.type === 'action') {
|
|
73
|
+
expect(action.spans).toEqual([
|
|
74
|
+
{ type: 'text', text: 'Action with ' },
|
|
75
|
+
{ type: 'bold', spans: [{ type: 'text', text: 'bold' }] },
|
|
76
|
+
{ type: 'text', text: ', ' },
|
|
77
|
+
{ type: 'italic', spans: [{ type: 'text', text: 'italic' }] },
|
|
78
|
+
{ type: 'text', text: ', and ' },
|
|
79
|
+
{ type: 'underline', spans: [{ type: 'text', text: 'underline' }] },
|
|
80
|
+
{ type: 'text', text: ' text.' },
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const john = scene.children[1]
|
|
85
|
+
expect(john?.type).toBe('character')
|
|
86
|
+
if (john?.type === 'character') {
|
|
87
|
+
expect(john.name).toBe('JOHN')
|
|
88
|
+
expect(john.extension).toBe('(V.O.)')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mary = scene.children[2]
|
|
92
|
+
expect(mary?.type).toBe('character')
|
|
93
|
+
if (mary?.type === 'character') {
|
|
94
|
+
expect(mary.children[0]).toMatchObject({ type: 'parenthetical', text: 'smiling' })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dual = scene.children[3]
|
|
98
|
+
expect(dual?.type).toBe('dual-dialogue')
|
|
99
|
+
if (dual?.type === 'dual-dialogue') {
|
|
100
|
+
expect(dual.left.name).toBe('JOHN')
|
|
101
|
+
expect(dual.right.name).toBe('MARY')
|
|
102
|
+
expect(dual.right.isDual).toBe(true)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
expect(scene.children[4]).toMatchObject({ type: 'transition', text: 'CUT TO:' })
|
|
106
|
+
expect(scene.children[5]).toMatchObject({ type: 'centered', text: 'THE END' })
|
|
107
|
+
expect(scene.children[6]).toMatchObject({ type: 'lyrics' })
|
|
108
|
+
expect(scene.children[7]).toMatchObject({ type: 'note', text: 'Note text' })
|
|
109
|
+
expect(scene.children[8]).toMatchObject({ type: 'comment', text: 'Comment text' })
|
|
110
|
+
expect(scene.children[9]).toMatchObject({ type: 'page-break' })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const section = reparsed.children[1]
|
|
114
|
+
expect(section?.type).toBe('section')
|
|
115
|
+
if (section?.type === 'section') {
|
|
116
|
+
expect(section.text).toBe('A Section')
|
|
117
|
+
}
|
|
118
|
+
expect(reparsed.children[2]).toMatchObject({ type: 'action' })
|
|
119
|
+
|
|
120
|
+
// Title page is intentionally not reimported.
|
|
121
|
+
expect(warnings.some(w => w.toLowerCase().includes('title page'))).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('returns null bodyText and a warning for a non-Sutrata docx', async () => {
|
|
125
|
+
const ast = parse('---\ntitle: Empty\n---\n')
|
|
126
|
+
const blob = await exportToDocx(ast)
|
|
127
|
+
const buffer = await blob.arrayBuffer()
|
|
128
|
+
const { bodyText, warnings } = await importDocx(buffer)
|
|
129
|
+
expect(bodyText).toBeNull()
|
|
130
|
+
expect(warnings.length).toBeGreaterThan(0)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('throws for a file that is not a docx', async () => {
|
|
134
|
+
const buffer = new TextEncoder().encode('not a docx').buffer
|
|
135
|
+
await expect(importDocx(buffer)).rejects.toThrow()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('splits the scene id from the heading when Word normalizes the tab to <w:tab/>', async () => {
|
|
139
|
+
// docx.js (the exporter) writes a literal tab character inside <w:t>. Word, when it
|
|
140
|
+
// resaves the file, normalizes that into a <w:tab/> element instead — reproduce that
|
|
141
|
+
// shape directly rather than going through exportToDocx.
|
|
142
|
+
const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
143
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
144
|
+
<w:body>
|
|
145
|
+
<w:p>
|
|
146
|
+
<w:pPr><w:pStyle w:val="CineSceneHeading"/></w:pPr>
|
|
147
|
+
<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">EXT. ROAD-RAIN EFFECT - MORNING</w:t></w:r>
|
|
148
|
+
<w:r><w:tab/></w:r>
|
|
149
|
+
<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">1A</w:t></w:r>
|
|
150
|
+
</w:p>
|
|
151
|
+
</w:body>
|
|
152
|
+
</w:document>`
|
|
153
|
+
const zip = new JSZip()
|
|
154
|
+
zip.file('word/document.xml', documentXml)
|
|
155
|
+
const buffer = await zip.generateAsync({ type: 'arraybuffer' })
|
|
156
|
+
|
|
157
|
+
const { bodyText } = await importDocx(buffer)
|
|
158
|
+
expect(bodyText).toBe('## EXT. ROAD-RAIN EFFECT - MORNING {#1A}')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { EditorState } from 'prosemirror-state'
|
|
3
|
+
import { schema } from '../../src/editor/schema'
|
|
4
|
+
import { createCharacterDecorationPlugin, findCharacterDecorationRanges } from '../../src/editor/character-decoration-plugin'
|
|
5
|
+
|
|
6
|
+
describe('findCharacterDecorationRanges', () => {
|
|
7
|
+
it('finds a single trailing extension', () => {
|
|
8
|
+
const r = findCharacterDecorationRanges('ARAVIND (V.O.)')
|
|
9
|
+
expect(r).toEqual({ parenStart: 8, parenEnd: 14 })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('finds multiple trailing paren groups as one range', () => {
|
|
13
|
+
const r = findCharacterDecorationRanges("ARAVIND (V.O.) (CONT'D)")
|
|
14
|
+
expect(r).toEqual({ parenStart: 8, parenEnd: 23 })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('finds a trailing dual-dialogue marker alone', () => {
|
|
18
|
+
const r = findCharacterDecorationRanges('ARAVIND ^')
|
|
19
|
+
expect(r).toEqual({ dualStart: 8, dualEnd: 9 })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('finds both an extension and a dual marker together', () => {
|
|
23
|
+
const r = findCharacterDecorationRanges('ARAVIND (V.O.) ^')
|
|
24
|
+
expect(r).toEqual({ parenStart: 8, parenEnd: 14, dualStart: 15, dualEnd: 16 })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('finds nothing for a plain name', () => {
|
|
28
|
+
expect(findCharacterDecorationRanges('ARAVIND')).toEqual({})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('finds nothing for an unclosed paren (still typing)', () => {
|
|
32
|
+
expect(findCharacterDecorationRanges('ARAVIND (')).toEqual({})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('character-decoration-plugin', () => {
|
|
37
|
+
function createStateWithPlugin(text: string) {
|
|
38
|
+
const plugin = createCharacterDecorationPlugin()
|
|
39
|
+
const state = EditorState.create({
|
|
40
|
+
doc: schema.node('doc', null, [schema.node('character', null, [schema.text(text)])]),
|
|
41
|
+
plugins: [plugin],
|
|
42
|
+
})
|
|
43
|
+
return { state, plugin }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it('decorates the trailing extension with cs-character-extension', () => {
|
|
47
|
+
const { state, plugin } = createStateWithPlugin('ARAVIND (V.O.)')
|
|
48
|
+
const decorations = plugin.getState(state)!.find()
|
|
49
|
+
const deco = decorations.find((d: any) => d.spec?.class === 'cs-character-extension')
|
|
50
|
+
expect(deco).toBeDefined()
|
|
51
|
+
// +1 for the block node's own opening position
|
|
52
|
+
expect(deco.from).toBe(1 + 8)
|
|
53
|
+
expect(deco.to).toBe(1 + 14)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('decorates multiple trailing paren groups as one extension span', () => {
|
|
57
|
+
const { state, plugin } = createStateWithPlugin("ARAVIND (V.O.) (CONT'D)")
|
|
58
|
+
const decorations = plugin.getState(state)!.find()
|
|
59
|
+
const deco = decorations.find((d: any) => d.spec?.class === 'cs-character-extension')
|
|
60
|
+
expect(deco.from).toBe(1 + 8)
|
|
61
|
+
expect(deco.to).toBe(1 + 23)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('decorates a trailing dual-dialogue marker with cs-character-dual-marker', () => {
|
|
65
|
+
const { state, plugin } = createStateWithPlugin('ARAVIND ^')
|
|
66
|
+
const decorations = plugin.getState(state)!.find()
|
|
67
|
+
const deco = decorations.find((d: any) => d.spec?.class === 'cs-character-dual-marker')
|
|
68
|
+
expect(deco).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('decorates both an extension and a dual marker on the same cue', () => {
|
|
72
|
+
const { state, plugin } = createStateWithPlugin('ARAVIND (V.O.) ^')
|
|
73
|
+
const decorations = plugin.getState(state)!.find()
|
|
74
|
+
expect(decorations.some((d: any) => d.spec?.class === 'cs-character-extension')).toBe(true)
|
|
75
|
+
expect(decorations.some((d: any) => d.spec?.class === 'cs-character-dual-marker')).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('produces no decorations for a plain character name', () => {
|
|
79
|
+
const { state, plugin } = createStateWithPlugin('ARAVIND')
|
|
80
|
+
expect(plugin.getState(state)!.find()).toHaveLength(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('does not decorate non-character blocks', () => {
|
|
84
|
+
const plugin = createCharacterDecorationPlugin()
|
|
85
|
+
const state = EditorState.create({
|
|
86
|
+
doc: schema.node('doc', null, [schema.node('action', null, [schema.text('Just an (aside) in action.')])]),
|
|
87
|
+
plugins: [plugin],
|
|
88
|
+
})
|
|
89
|
+
expect(plugin.getState(state)!.find()).toHaveLength(0)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('recomputes decorations after a doc-changing transaction', () => {
|
|
93
|
+
const { state: initialState, plugin } = createStateWithPlugin('ARAVIND')
|
|
94
|
+
expect(plugin.getState(initialState)!.find()).toHaveLength(0)
|
|
95
|
+
|
|
96
|
+
const tr = initialState.tr.insertText(' (V.O.)', 8)
|
|
97
|
+
const newState = initialState.apply(tr)
|
|
98
|
+
const decorations = plugin.getState(newState)!.find()
|
|
99
|
+
expect(decorations.some((d: any) => d.spec?.class === 'cs-character-extension')).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parse } from '@sutrata/parser'
|
|
3
|
+
import { sutraToProsemirror } from '../../src/editor/sutra-to-prosemirror'
|
|
4
|
+
import { prosemirrorToSutra } from '../../src/editor/prosemirror-to-sutra'
|
|
5
|
+
|
|
6
|
+
function characterText(src: string): string {
|
|
7
|
+
const pmDoc = sutraToProsemirror(parse(src))
|
|
8
|
+
let text = ''
|
|
9
|
+
pmDoc.descendants(node => {
|
|
10
|
+
if (node.type.name === 'character') text = node.textContent
|
|
11
|
+
})
|
|
12
|
+
return text
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('character extension/dual-marker — Sutra <-> ProseMirror bridge (text-based, no attrs)', () => {
|
|
16
|
+
it('puts the extension directly in the character node text, no separate attr', () => {
|
|
17
|
+
expect(characterText('## SC1\n\n@ARAVIND (V.O.)\nHello.\n')).toBe('ARAVIND (V.O.)')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('handles multiple trailing paren groups', () => {
|
|
21
|
+
expect(characterText("## SC1\n\n@ARAVIND (V.O.) (CONT'D)\nHello.\n")).toBe("ARAVIND (V.O.) (CONT'D)")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('handles a plain name with no extension', () => {
|
|
25
|
+
expect(characterText('## SC1\n\n@ARAVIND\nHello.\n')).toBe('ARAVIND')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
'@ARAVIND (V.O.)',
|
|
30
|
+
"@ARAVIND (V.O.) (CONT'D)",
|
|
31
|
+
'@ARAVIND',
|
|
32
|
+
])('round-trips %s back to the exact source line', (cue) => {
|
|
33
|
+
const src = `## SC1\n\n${cue}\nHello.\n`
|
|
34
|
+
const out = prosemirrorToSutra(sutraToProsemirror(parse(src)))
|
|
35
|
+
expect(out).toContain(cue)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('round-trips a dual-dialogue pair including the ^ marker and an extension', () => {
|
|
39
|
+
const src = "## SC1\n\n@ARAVIND (V.O.)\nHello.\n\n@MEERA ^\nHi.\n"
|
|
40
|
+
const out = prosemirrorToSutra(sutraToProsemirror(parse(src)))
|
|
41
|
+
expect(out).toContain('@ARAVIND (V.O.)')
|
|
42
|
+
expect(out).toContain('@MEERA ^')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { EditorState, TextSelection } from 'prosemirror-state'
|
|
3
|
+
import { schema } from '../../src/editor/schema'
|
|
4
|
+
import { moveByGrapheme, extendByGrapheme } from '../../src/editor/grapheme-cursor'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ProseMirror position layout for an action node containing text T (length n):
|
|
8
|
+
* pos 1 = action open (= $cursor.start())
|
|
9
|
+
* pos 2 = first UTF-16 code unit of T
|
|
10
|
+
* ...
|
|
11
|
+
* pos 1+n = action close (= $cursor.end())
|
|
12
|
+
*
|
|
13
|
+
* Each JS string character (UTF-16 code unit) takes exactly 1 PM position.
|
|
14
|
+
* Surrogate pairs (emoji, etc.) each take 2 positions in PM.
|
|
15
|
+
*
|
|
16
|
+
* Cursor at pos 1 is before the very first character (the true block-start).
|
|
17
|
+
* Cursor at pos 1+n is after the last character (the true block-end).
|
|
18
|
+
*/
|
|
19
|
+
function makeState(text: string, cursorPos: number) {
|
|
20
|
+
const doc = schema.node('doc', null, [
|
|
21
|
+
schema.node('action', null, text ? [schema.text(text)] : []),
|
|
22
|
+
])
|
|
23
|
+
const state = EditorState.create({ doc, schema })
|
|
24
|
+
return state.apply(
|
|
25
|
+
state.tr.setSelection(TextSelection.create(doc, cursorPos))
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('grapheme-cursor', () => {
|
|
30
|
+
it('moveByGrapheme right advances over a single ASCII char', () => {
|
|
31
|
+
// 'ABC': pos 1=start, pos 2='A', pos 3='B', pos 4='C', pos 5=end
|
|
32
|
+
const state = makeState('ABC', 2) // cursor after 'A'... wait: pos 2 = after action_open = before 'A'?
|
|
33
|
+
// No: pos 1 = action_open = start(). pos 2 = position after first char 'A'.
|
|
34
|
+
// Actually: textBetween(1, 4) = 'ABC' → positions 1..4 map to chars A,B,C.
|
|
35
|
+
// So pos 1 is before 'A', pos 2 is between 'A' and 'B', pos 3 is between 'B' and 'C', pos 4 is after 'C'.
|
|
36
|
+
// Cursor at pos 1: textBetween(1, 4) = 'ABC', first grapheme 'A' (len 1) → advance to pos 2.
|
|
37
|
+
// Cursor at pos 2: textBetween(2, 4) = 'BC', first grapheme 'B' → advance to pos 3. ✓
|
|
38
|
+
let dispatched: any = null
|
|
39
|
+
moveByGrapheme('right')(state, (tr) => { dispatched = tr })
|
|
40
|
+
expect(dispatched).not.toBeNull()
|
|
41
|
+
const sel = dispatched.selection as TextSelection
|
|
42
|
+
expect(sel.$cursor?.pos).toBe(3) // 'B' is next, advance by 1
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('moveByGrapheme right advances over the first grapheme in a Tamil sequence', () => {
|
|
46
|
+
// 'க்ஷ' = U+0B95 U+0BCD U+0BB7 (3 BMP chars, 3 PM positions)
|
|
47
|
+
// Node.js ICU segments 'க்ஷA' as ['க்'(len 2), 'ஷ'(len 1), 'A'(len 1)]
|
|
48
|
+
// Cursor at pos 1 (block start, before 'க'): textBetween(1, 5) = 'க்ஷA'
|
|
49
|
+
// First grapheme 'க்' has length 2 → cursor advances to pos 3.
|
|
50
|
+
const cluster = 'க்ஷ'
|
|
51
|
+
expect(cluster.length).toBe(3) // 3 UTF-16 code units
|
|
52
|
+
const state = makeState(cluster + 'A', 1) // cursor at block start, before 'க'
|
|
53
|
+
let dispatched: any = null
|
|
54
|
+
moveByGrapheme('right')(state, (tr) => { dispatched = tr })
|
|
55
|
+
expect(dispatched).not.toBeNull()
|
|
56
|
+
const sel = dispatched.selection as TextSelection
|
|
57
|
+
// Advances by 2 (length of first grapheme 'க்'), not by 1 (single codepoint)
|
|
58
|
+
expect(sel.$cursor?.pos).toBe(3)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('moveByGrapheme left retreats over a Tamil grapheme cluster', () => {
|
|
62
|
+
// 'Aக்ஷ': pos 1=start, pos 2='A', pos 3='க', pos 4='்', pos 5='ஷ', end()=5
|
|
63
|
+
// Cursor at pos 4 (after 'A','க','்'): textBetween(1, 4) = 'Aக்'
|
|
64
|
+
// graphemes of 'Aக்' = ['A', 'க்'] → last grapheme 'க்' (len 2) → retreat by 2 → pos 2
|
|
65
|
+
const cluster = 'க்ஷ'
|
|
66
|
+
const state = makeState('A' + cluster, 4) // cursor after 'க்'
|
|
67
|
+
let dispatched: any = null
|
|
68
|
+
moveByGrapheme('left')(state, (tr) => { dispatched = tr })
|
|
69
|
+
expect(dispatched).not.toBeNull()
|
|
70
|
+
const sel = dispatched.selection as TextSelection
|
|
71
|
+
// Retreats by 2 (length of 'க்'), landing before 'க'
|
|
72
|
+
expect(sel.$cursor?.pos).toBe(2)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('moveByGrapheme right advances over emoji ZWJ sequence as one grapheme', () => {
|
|
76
|
+
// '👨👩👧' is a family ZWJ emoji — 1 grapheme cluster, 8 UTF-16 code units = 8 PM positions
|
|
77
|
+
// Cursor at pos 1 (block start): textBetween(1, 9) = emoji string (length 8)
|
|
78
|
+
// Intl.Segmenter gives 1 grapheme of length 8 → advance by 8 → pos 9
|
|
79
|
+
const emoji = '👨👩👧'
|
|
80
|
+
expect(emoji.length).toBe(8) // 8 UTF-16 code units
|
|
81
|
+
const state = makeState(emoji, 1) // cursor at block start, before emoji
|
|
82
|
+
let dispatched: any = null
|
|
83
|
+
moveByGrapheme('right')(state, (tr) => { dispatched = tr })
|
|
84
|
+
expect(dispatched).not.toBeNull()
|
|
85
|
+
const sel = dispatched.selection as TextSelection
|
|
86
|
+
// Should advance by 8 (full emoji = single grapheme)
|
|
87
|
+
expect(sel.$cursor?.pos).toBe(1 + emoji.length) // 9
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('moveByGrapheme right returns false at end of block', () => {
|
|
91
|
+
// 'A': start()=1, end()=2. Cursor at pos 2 (after 'A') → pos(2) >= end(2) → false
|
|
92
|
+
const state = makeState('A', 2) // cursor after 'A', at block end
|
|
93
|
+
const result = moveByGrapheme('right')(state, () => {})
|
|
94
|
+
expect(result).toBe(false)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('moveByGrapheme left returns false at start of block', () => {
|
|
98
|
+
// start() = 1 for any inline content. Cursor at pos 1 → pos(1) <= start(1) → false
|
|
99
|
+
const state = makeState('A', 1) // cursor at block start
|
|
100
|
+
const result = moveByGrapheme('left')(state, () => {})
|
|
101
|
+
expect(result).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('extendByGrapheme right extends selection over first Tamil grapheme', () => {
|
|
105
|
+
// 'க்ஷ': cursor at pos 1 (before 'க')
|
|
106
|
+
// textBetween(head=1, end=4) = 'க்ஷ'
|
|
107
|
+
// graphemes ['க்','ஷ'] → first='க்' (len 2) → head goes to 1+2=3
|
|
108
|
+
// anchor stays at 1
|
|
109
|
+
const cluster = 'க்ஷ'
|
|
110
|
+
const state = makeState(cluster, 1) // cursor at block start
|
|
111
|
+
let dispatched: any = null
|
|
112
|
+
extendByGrapheme('right')(state, (tr) => { dispatched = tr })
|
|
113
|
+
expect(dispatched).not.toBeNull()
|
|
114
|
+
const sel = dispatched.selection as TextSelection
|
|
115
|
+
expect(sel.head).toBe(3) // extended by 2 (first grapheme 'க்')
|
|
116
|
+
expect(sel.anchor).toBe(1) // anchor unchanged
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { EditorState } from 'prosemirror-state'
|
|
3
|
+
import { schema } from '../../src/editor/schema'
|
|
4
|
+
import { createImePlugin, isComposing } from '../../src/editor/ime-plugin'
|
|
5
|
+
|
|
6
|
+
// Minimal EditorView mock — only needs dom + dispatch + state
|
|
7
|
+
function makeMockView(dom?: HTMLElement) {
|
|
8
|
+
const el = dom ?? document.createElement('div')
|
|
9
|
+
document.body.appendChild(el)
|
|
10
|
+
return {
|
|
11
|
+
dom: el,
|
|
12
|
+
dispatch: vi.fn(),
|
|
13
|
+
state: EditorState.create({ schema, plugins: [] }),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('ime-plugin', () => {
|
|
18
|
+
it('exports createImePlugin and isComposing', () => {
|
|
19
|
+
expect(typeof createImePlugin).toBe('function')
|
|
20
|
+
expect(typeof isComposing).toBe('function')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('isComposing returns false when no composition active', () => {
|
|
24
|
+
expect(isComposing()).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('plugin view attaches compositionstart/end listeners to dom', () => {
|
|
28
|
+
const dom = document.createElement('div')
|
|
29
|
+
const addSpy = vi.spyOn(dom, 'addEventListener')
|
|
30
|
+
const mockView = { ...makeMockView(dom), dom } as any
|
|
31
|
+
const plugin = createImePlugin()
|
|
32
|
+
plugin.spec.view!(mockView)
|
|
33
|
+
|
|
34
|
+
expect(addSpy).toHaveBeenCalledWith('compositionstart', expect.any(Function))
|
|
35
|
+
expect(addSpy).toHaveBeenCalledWith('compositionend', expect.any(Function))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('plugin view removes listeners on destroy', () => {
|
|
39
|
+
const dom = document.createElement('div')
|
|
40
|
+
const removeSpy = vi.spyOn(dom, 'removeEventListener')
|
|
41
|
+
const mockView = { ...makeMockView(dom), dom } as any
|
|
42
|
+
const plugin = createImePlugin()
|
|
43
|
+
const view = plugin.spec.view!(mockView)
|
|
44
|
+
|
|
45
|
+
view!.destroy!()
|
|
46
|
+
|
|
47
|
+
expect(removeSpy).toHaveBeenCalledWith('compositionstart', expect.any(Function))
|
|
48
|
+
expect(removeSpy).toHaveBeenCalledWith('compositionend', expect.any(Function))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('compositionstart sets composing = true', () => {
|
|
52
|
+
const dom = document.createElement('div')
|
|
53
|
+
const mockView = { ...makeMockView(dom), dom } as any
|
|
54
|
+
const plugin = createImePlugin()
|
|
55
|
+
const pluginView = plugin.spec.view!(mockView) as any
|
|
56
|
+
|
|
57
|
+
dom.dispatchEvent(new Event('compositionstart'))
|
|
58
|
+
expect(pluginView.isComposing()).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('compositionend sets composing = false', () => {
|
|
62
|
+
const dom = document.createElement('div')
|
|
63
|
+
const mockView = { ...makeMockView(dom), dom } as any
|
|
64
|
+
const plugin = createImePlugin()
|
|
65
|
+
const pluginView = plugin.spec.view!(mockView) as any
|
|
66
|
+
|
|
67
|
+
dom.dispatchEvent(new Event('compositionstart'))
|
|
68
|
+
expect(pluginView.isComposing()).toBe(true)
|
|
69
|
+
dom.dispatchEvent(new Event('compositionend'))
|
|
70
|
+
expect(pluginView.isComposing()).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parse } from '@sutrata/parser'
|
|
3
|
+
import { sutraToProsemirror } from '../../src/editor/sutra-to-prosemirror'
|
|
4
|
+
import { prosemirrorToSutra } from '../../src/editor/prosemirror-to-sutra'
|
|
5
|
+
import { schema } from '../../src/editor/schema'
|
|
6
|
+
|
|
7
|
+
describe('inline [[ ]] note — Sutra <-> ProseMirror bridge', () => {
|
|
8
|
+
it('carries the note_mark on the bracketed text when converting Sutra to ProseMirror', () => {
|
|
9
|
+
const src = "## SC1\n\nThere's no next train. [[दर्शक को यहीं पता चलना चाहिए]]\n"
|
|
10
|
+
const ast = parse(src)
|
|
11
|
+
const pmDoc = sutraToProsemirror(ast)
|
|
12
|
+
|
|
13
|
+
let found: { text: string | null; hasMark: boolean } | null = null
|
|
14
|
+
pmDoc.descendants(node => {
|
|
15
|
+
if (node.isText && node.text?.includes('दर्शक')) {
|
|
16
|
+
found = { text: node.text, hasMark: !!node.marks.find(m => m.type === schema.marks['note_mark']) }
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(found).not.toBeNull()
|
|
21
|
+
expect(found!.hasMark).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('round-trips the note back into [[ ]] brackets when converting ProseMirror to Sutra', () => {
|
|
25
|
+
const src = "## SC1\n\nThere's no next train. [[दर्शक को यहीं पता चलना चाहिए]]\n"
|
|
26
|
+
const ast = parse(src)
|
|
27
|
+
const pmDoc = sutraToProsemirror(ast)
|
|
28
|
+
const out = prosemirrorToSutra(pmDoc)
|
|
29
|
+
|
|
30
|
+
expect(out).toContain('[[दर्शक को यहीं पता चलना चाहिए]]')
|
|
31
|
+
})
|
|
32
|
+
})
|