@sutrata/editor 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/generated-providers.json +1 -1
  3. package/package.json +30 -3
  4. package/scripts/copy-assets.mjs +0 -16
  5. package/scripts/generate-providers.mjs +0 -127
  6. package/src/ai/OnboardingDialog.tsx +0 -390
  7. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  8. package/src/ai/VoiceToolbar.tsx +0 -321
  9. package/src/ai/ai-client.ts +0 -596
  10. package/src/ai/commands.ts +0 -164
  11. package/src/ai/generated-providers.json +0 -3696
  12. package/src/ai/prompts.ts +0 -62
  13. package/src/ai/voice-service.ts +0 -96
  14. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  15. package/src/autocomplete/suggestions.ts +0 -147
  16. package/src/context/DocumentContext.tsx +0 -648
  17. package/src/desktop-api.d.ts +0 -40
  18. package/src/editor/EditorView.tsx +0 -82
  19. package/src/editor/SelectionMenu.tsx +0 -152
  20. package/src/editor/attr-pencil-plugin.ts +0 -172
  21. package/src/editor/character-decoration-plugin.ts +0 -104
  22. package/src/editor/editor-bus.ts +0 -51
  23. package/src/editor/find-highlight-plugin.ts +0 -68
  24. package/src/editor/frontmatter-field-view.ts +0 -52
  25. package/src/editor/frontmatter-field.ts +0 -115
  26. package/src/editor/grapheme-cursor.ts +0 -99
  27. package/src/editor/ime-plugin.ts +0 -85
  28. package/src/editor/insert-helper.ts +0 -43
  29. package/src/editor/keymap.ts +0 -124
  30. package/src/editor/language-class-plugin.ts +0 -91
  31. package/src/editor/plugins.ts +0 -26
  32. package/src/editor/prosemirror-to-sutra.ts +0 -192
  33. package/src/editor/scene-heading-view.ts +0 -147
  34. package/src/editor/schema.ts +0 -156
  35. package/src/editor/sutra-to-prosemirror.ts +0 -156
  36. package/src/extensions/storage-adapter.ts +0 -65
  37. package/src/extensions/types.ts +0 -60
  38. package/src/file/docx-exporter.ts +0 -585
  39. package/src/file/docx-importer.ts +0 -313
  40. package/src/file/print-paginator.ts +0 -301
  41. package/src/file/templates.ts +0 -73
  42. package/src/file/workflow-reports.ts +0 -820
  43. package/src/findreplace/FindReplace.tsx +0 -242
  44. package/src/findreplace/find-engine.ts +0 -97
  45. package/src/findreplace/scroll-to-match.ts +0 -89
  46. package/src/fonts/tofu-detect.ts +0 -53
  47. package/src/i18n/LanguageContext.tsx +0 -207
  48. package/src/i18n/language-detect.ts +0 -73
  49. package/src/i18n/useTranslation.tsx +0 -74
  50. package/src/index.ts +0 -27
  51. package/src/locales/bn.json +0 -143
  52. package/src/locales/en.json +0 -143
  53. package/src/locales/gu.json +0 -143
  54. package/src/locales/hi.json +0 -143
  55. package/src/locales/kn.json +0 -143
  56. package/src/locales/ml.json +0 -143
  57. package/src/locales/mr.json +0 -143
  58. package/src/locales/or.json +0 -143
  59. package/src/locales/pa.json +0 -143
  60. package/src/locales/si.json +0 -143
  61. package/src/locales/ta.json +0 -143
  62. package/src/locales/te.json +0 -143
  63. package/src/navigator/SceneNavigator.tsx +0 -844
  64. package/src/navigator/StatisticsDialog.tsx +0 -249
  65. package/src/navigator/scene-list.ts +0 -84
  66. package/src/settings/SettingsDialog.tsx +0 -303
  67. package/src/shell/AppBar.tsx +0 -354
  68. package/src/shell/AppShell.tsx +0 -71
  69. package/src/shell/ConfirmDialog.tsx +0 -71
  70. package/src/shell/ElementToolbar.tsx +0 -175
  71. package/src/shell/LanguageStatus.tsx +0 -101
  72. package/src/shell/StatusBar.tsx +0 -135
  73. package/src/shell/SutrataLogo.tsx +0 -89
  74. package/src/shell/Toast.tsx +0 -46
  75. package/src/shell/ToolbarButton.tsx +0 -30
  76. package/src/shell/VersionHistory.tsx +0 -212
  77. package/src/shell/Watermark.tsx +0 -9
  78. package/src/shell/fonts/fonts.css +0 -1133
  79. package/src/shell/icons.tsx +0 -335
  80. package/src/shell/toolbar-actions.ts +0 -59
  81. package/src/source/SourceView.tsx +0 -111
  82. package/src/source/find-highlight-source.ts +0 -57
  83. package/src/source/sutra-lang.ts +0 -121
  84. package/src/spellcheck/dictionaries.ts +0 -21
  85. package/src/spellcheck/hunspell.ts +0 -110
  86. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  87. package/src/styles/StyleDialog.tsx +0 -148
  88. package/src/styles/builtin-styles.ts +0 -139
  89. package/src/styles/css-adapter.ts +0 -76
  90. package/src/styles/docx-adapter.ts +0 -109
  91. package/src/styles/embed-fonts.ts +0 -81
  92. package/src/styles/pagination.ts +0 -27
  93. package/src/styles/print-adapter.ts +0 -288
  94. package/src/styles/registry.ts +0 -20
  95. package/src/styles/screenplay.css +0 -3006
  96. package/src/styles/script-ranges.ts +0 -17
  97. package/src/styles/types.ts +0 -104
  98. package/src/styles/validate.ts +0 -114
  99. package/src/templates/templates.ts +0 -194
  100. package/src/titlepage/ExportDialog.tsx +0 -494
  101. package/src/titlepage/TitlePageDialog.tsx +0 -312
  102. package/src/titlepage/frontmatter-form.ts +0 -280
  103. package/src/titlepage/languages.ts +0 -37
  104. package/src/types.ts +0 -7
  105. package/tests/AppBar.test.tsx +0 -35
  106. package/tests/AppShell.test.tsx +0 -22
  107. package/tests/ElementToolbar.test.tsx +0 -36
  108. package/tests/LanguageStatus.test.tsx +0 -129
  109. package/tests/SelectionMenu.test.tsx +0 -71
  110. package/tests/ToolbarButton.test.tsx +0 -24
  111. package/tests/ai-onboarding.test.ts +0 -102
  112. package/tests/ai-status.test.tsx +0 -72
  113. package/tests/ai-structured.test.ts +0 -94
  114. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  115. package/tests/autocomplete/suggestions.test.ts +0 -106
  116. package/tests/document-context.test.tsx +0 -565
  117. package/tests/docx-importer.test.ts +0 -160
  118. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  119. package/tests/editor/character-extension.test.ts +0 -44
  120. package/tests/editor/grapheme-cursor.test.ts +0 -118
  121. package/tests/editor/ime-plugin.test.ts +0 -72
  122. package/tests/editor/inline-note-bridge.test.ts +0 -32
  123. package/tests/editor/language-class-plugin.test.ts +0 -265
  124. package/tests/editor/title-page.test.ts +0 -124
  125. package/tests/embed-fonts.test.ts +0 -17
  126. package/tests/exporters.test.ts +0 -365
  127. package/tests/findreplace/FindReplace.test.tsx +0 -391
  128. package/tests/findreplace/find-engine.test.ts +0 -86
  129. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  130. package/tests/fonts/tofu-detect.test.ts +0 -122
  131. package/tests/i18n/LanguageContext.test.tsx +0 -146
  132. package/tests/i18n/language-detect.test.ts +0 -54
  133. package/tests/i18n/locale-parity.test.ts +0 -25
  134. package/tests/i18n/useTranslation.test.tsx +0 -45
  135. package/tests/keymap.test.ts +0 -18
  136. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  137. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  138. package/tests/navigator/scene-list.test.ts +0 -71
  139. package/tests/print-paginator.test.ts +0 -82
  140. package/tests/shell/VersionHistory.test.tsx +0 -129
  141. package/tests/spellcheck/hunspell.test.ts +0 -82
  142. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  143. package/tests/styles.test.ts +0 -315
  144. package/tests/templates/templates.test.ts +0 -83
  145. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  146. package/tests/titlepage/apply-title-page.test.ts +0 -61
  147. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  148. package/tests/toolbar-actions.test.ts +0 -93
  149. package/tests/voice-commands.test.ts +0 -58
  150. package/tsconfig.json +0 -17
  151. package/vitest.config.ts +0 -11
  152. package/vitest.setup.ts +0 -65
@@ -1,391 +0,0 @@
1
- import React, { useEffect } from 'react'
2
- import { describe, it, expect, afterEach } from 'vitest'
3
- import { render, screen, fireEvent, act } from '@testing-library/react'
4
- import { FindReplace } from '../../src/findreplace/FindReplace'
5
- import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
6
- import { LanguageProvider } from '../../src/i18n/LanguageContext'
7
- import { TranslationProvider } from '../../src/i18n/useTranslation'
8
- import { setEditorView } from '../../src/editor/editor-bus'
9
- import { schema } from '../../src/editor/schema'
10
-
11
- // Build a minimal fake PM view so findInPmDoc can find words in the test doc.
12
- // `getWords` is called lazily so the view reflects text changes during a test.
13
- function makeFakeView(getWords: () => string[]) {
14
- const view = {
15
- get state() {
16
- const words = getWords()
17
- const blocks = words.map(w =>
18
- schema.nodes['action']!.create({}, w ? schema.text(w) : undefined)
19
- )
20
- return { doc: schema.nodes['doc']!.create({}, blocks) }
21
- },
22
- dispatch: () => {},
23
- focus: () => {},
24
- nodeDOM: () => null,
25
- }
26
- return view as never
27
- }
28
-
29
- afterEach(() => {
30
- // Clear injected view between tests
31
- setEditorView(null)
32
- })
33
-
34
- /** Opens the find-replace panel via the context setter */
35
- function PanelOpener({ mode = 'replace' }: { mode?: 'find' | 'replace' } = {}) {
36
- const { setFindReplaceVisible, setFindMode } = useDocument()
37
- useEffect(() => {
38
- setFindMode(mode)
39
- setFindReplaceVisible(true)
40
- // eslint-disable-next-line react-hooks/exhaustive-deps
41
- }, [])
42
- return null
43
- }
44
-
45
- /** Sets the document text via context after mount */
46
- function TextSetter({ text }: { text: string }) {
47
- const { setText } = useDocument()
48
- useEffect(() => {
49
- setText(text)
50
- // eslint-disable-next-line react-hooks/exhaustive-deps
51
- }, [])
52
- return null
53
- }
54
-
55
- /** Switches the editor into source mode after mount (no real CodeMirror view is mounted). */
56
- function SourceModeSetter() {
57
- const { setMode } = useDocument()
58
- useEffect(() => {
59
- setMode('source')
60
- // eslint-disable-next-line react-hooks/exhaustive-deps
61
- }, [])
62
- return null
63
- }
64
-
65
- function Wrapper({ children, initialText, mode = 'replace' }: { children?: React.ReactNode; initialText?: string; mode?: 'find' | 'replace' }) {
66
- return (
67
- <TranslationProvider>
68
- <DocumentProvider>
69
- <LanguageProvider>
70
- {initialText && <TextSetter text={initialText} />}
71
- <PanelOpener mode={mode} />
72
- <FindReplace />
73
- {children}
74
- </LanguageProvider>
75
- </DocumentProvider>
76
- </TranslationProvider>
77
- )
78
- }
79
-
80
- describe('FindReplace', () => {
81
- it('renders when findReplaceVisible=true', async () => {
82
- await act(async () => {
83
- render(<Wrapper />)
84
- })
85
- expect(screen.getByPlaceholderText('Find…')).toBeDefined()
86
- expect(screen.getByPlaceholderText('Replace with…')).toBeDefined()
87
- })
88
-
89
- it('shows "No matches" when query has no results', async () => {
90
- setEditorView(makeFakeView(() => ['Hello world']))
91
- await act(async () => {
92
- render(<Wrapper initialText="Hello world" />)
93
- })
94
- const input = screen.getByPlaceholderText('Find…')
95
- await act(async () => {
96
- fireEvent.change(input, { target: { value: 'zzz' } })
97
- })
98
- expect(screen.getByText('No matches')).toBeDefined()
99
- })
100
-
101
- it('shows match count when query has results', async () => {
102
- setEditorView(makeFakeView(() => ['foo', 'foo', 'foo']))
103
- await act(async () => {
104
- render(<Wrapper initialText="foo foo foo" />)
105
- })
106
- const input = screen.getByPlaceholderText('Find…')
107
- await act(async () => {
108
- fireEvent.change(input, { target: { value: 'foo' } })
109
- })
110
- expect(screen.getByText('1 of 3')).toBeDefined()
111
- })
112
-
113
- it('replace-all updates document text via setText', async () => {
114
- setEditorView(makeFakeView(() => ['foo', 'foo', 'foo']))
115
- function TextProbe() {
116
- const { text } = useDocument()
117
- return <div data-testid="text-output">{text}</div>
118
- }
119
-
120
- await act(async () => {
121
- render(
122
- <TranslationProvider>
123
- <DocumentProvider>
124
- <LanguageProvider>
125
- <TextSetter text="foo foo foo" />
126
- <PanelOpener />
127
- <FindReplace />
128
- <TextProbe />
129
- </LanguageProvider>
130
- </DocumentProvider>
131
- </TranslationProvider>
132
- )
133
- })
134
-
135
- const queryInput = screen.getByPlaceholderText('Find…')
136
- const replaceInput = screen.getByPlaceholderText('Replace with…')
137
-
138
- await act(async () => {
139
- fireEvent.change(queryInput, { target: { value: 'foo' } })
140
- fireEvent.change(replaceInput, { target: { value: 'bar' } })
141
- })
142
-
143
- const replaceBtn = screen.getByText('Replace All')
144
- await act(async () => {
145
- fireEvent.click(replaceBtn)
146
- })
147
-
148
- expect(screen.getByTestId('text-output').textContent).toBe('bar bar bar')
149
- })
150
-
151
- it('closes on × button click', async () => {
152
- let container!: HTMLElement
153
- await act(async () => {
154
- ;({ container } = render(<Wrapper />))
155
- })
156
-
157
- expect(container.querySelector('.cs-find-replace')).not.toBeNull()
158
-
159
- const closeBtn = screen.getByLabelText('Close')
160
- await act(async () => {
161
- fireEvent.click(closeBtn)
162
- })
163
-
164
- expect(container.querySelector('.cs-find-replace')).toBeNull()
165
- })
166
-
167
- it('shows prev/next buttons always; count appears when matches exist', async () => {
168
- setEditorView(makeFakeView(() => ['foo', 'foo', 'foo']))
169
- await act(async () => {
170
- render(<Wrapper initialText="foo foo foo" mode="find" />)
171
- })
172
- const input = screen.getByPlaceholderText('Find…')
173
- await act(async () => {
174
- fireEvent.change(input, { target: { value: 'foo' } })
175
- })
176
- expect(screen.getByLabelText('Previous match')).toBeDefined()
177
- expect(screen.getByLabelText('Next match')).toBeDefined()
178
- expect(screen.getByText('1 of 3')).toBeDefined()
179
- })
180
-
181
- it('Enter advances to next match', async () => {
182
- setEditorView(makeFakeView(() => ['foo', 'foo', 'foo']))
183
- await act(async () => {
184
- render(<Wrapper initialText="foo foo foo" mode="find" />)
185
- })
186
- const input = screen.getByPlaceholderText('Find…')
187
- await act(async () => {
188
- fireEvent.change(input, { target: { value: 'foo' } })
189
- })
190
- await act(async () => {
191
- fireEvent.keyDown(input, { key: 'Enter' })
192
- })
193
- expect(screen.getByText('2 of 3')).toBeDefined()
194
- })
195
-
196
- it('Shift+Enter goes to previous match', async () => {
197
- setEditorView(makeFakeView(() => ['foo', 'foo', 'foo']))
198
- await act(async () => {
199
- render(<Wrapper initialText="foo foo foo" mode="find" />)
200
- })
201
- const input = screen.getByPlaceholderText('Find…')
202
- await act(async () => {
203
- fireEvent.change(input, { target: { value: 'foo' } })
204
- })
205
- await act(async () => {
206
- fireEvent.keyDown(input, { key: 'Enter' })
207
- })
208
- await act(async () => {
209
- fireEvent.keyDown(input, { key: 'Enter', shiftKey: true })
210
- })
211
- expect(screen.getByText('1 of 3')).toBeDefined()
212
- })
213
-
214
- it('next wraps from last to first match', async () => {
215
- setEditorView(makeFakeView(() => ['foo', 'foo']))
216
- await act(async () => {
217
- render(<Wrapper initialText="foo foo" mode="find" />)
218
- })
219
- const input = screen.getByPlaceholderText('Find…')
220
- await act(async () => {
221
- fireEvent.change(input, { target: { value: 'foo' } })
222
- })
223
- await act(async () => {
224
- fireEvent.keyDown(input, { key: 'Enter' })
225
- fireEvent.keyDown(input, { key: 'Enter' })
226
- })
227
- expect(screen.getByText('1 of 2')).toBeDefined()
228
- })
229
-
230
- it('Replace button replaces current match and advances', async () => {
231
- // The fake view needs to reflect the current text so match count updates after replace.
232
- // We store a mutable ref to the current word list and update it whenever text changes.
233
- const wordsRef = { current: ['foo', 'foo', 'foo'] }
234
- setEditorView(makeFakeView(() => wordsRef.current))
235
-
236
- function TextProbe() {
237
- const { text } = useDocument()
238
- // Keep wordsRef in sync with the live text so findInPmDoc sees the updated doc
239
- wordsRef.current = text.split(/\s+/).filter(Boolean)
240
- return <div data-testid="text-output">{text}</div>
241
- }
242
- await act(async () => {
243
- render(
244
- <TranslationProvider>
245
- <DocumentProvider>
246
- <LanguageProvider>
247
- <TextSetter text="foo foo foo" />
248
- <PanelOpener mode="replace" />
249
- <FindReplace />
250
- <TextProbe />
251
- </LanguageProvider>
252
- </DocumentProvider>
253
- </TranslationProvider>
254
- )
255
- })
256
- const queryInput = screen.getByPlaceholderText('Find…')
257
- const replaceInput = screen.getByPlaceholderText('Replace with…')
258
- await act(async () => {
259
- fireEvent.change(queryInput, { target: { value: 'foo' } })
260
- fireEvent.change(replaceInput, { target: { value: 'bar' } })
261
- })
262
- const replaceBtn = screen.getByText('Replace')
263
- await act(async () => {
264
- fireEvent.click(replaceBtn)
265
- })
266
- expect(screen.getByTestId('text-output').textContent).toContain('bar')
267
- expect(screen.getByText('1 of 2')).toBeDefined()
268
- })
269
-
270
- it('finds matches in source mode (no PM view mounted)', async () => {
271
- // Deliberately no setEditorView(...) here — source mode must not depend on it.
272
- await act(async () => {
273
- render(
274
- <TranslationProvider>
275
- <DocumentProvider>
276
- <LanguageProvider>
277
- <TextSetter text="foo foo foo" />
278
- <SourceModeSetter />
279
- <PanelOpener mode="find" />
280
- <FindReplace />
281
- </LanguageProvider>
282
- </DocumentProvider>
283
- </TranslationProvider>
284
- )
285
- })
286
- const input = screen.getByPlaceholderText('Find…')
287
- await act(async () => {
288
- fireEvent.change(input, { target: { value: 'foo' } })
289
- })
290
- expect(screen.getByText('1 of 3')).toBeDefined()
291
- })
292
-
293
- it('replace-all works in source mode', async () => {
294
- function TextProbe() {
295
- const { text } = useDocument()
296
- return <div data-testid="text-output">{text}</div>
297
- }
298
- await act(async () => {
299
- render(
300
- <TranslationProvider>
301
- <DocumentProvider>
302
- <LanguageProvider>
303
- <TextSetter text="foo foo foo" />
304
- <SourceModeSetter />
305
- <PanelOpener mode="replace" />
306
- <FindReplace />
307
- <TextProbe />
308
- </LanguageProvider>
309
- </DocumentProvider>
310
- </TranslationProvider>
311
- )
312
- })
313
- const queryInput = screen.getByPlaceholderText('Find…')
314
- const replaceInput = screen.getByPlaceholderText('Replace with…')
315
- await act(async () => {
316
- fireEvent.change(queryInput, { target: { value: 'foo' } })
317
- fireEvent.change(replaceInput, { target: { value: 'bar' } })
318
- })
319
- const replaceBtn = screen.getByText('Replace All')
320
- await act(async () => {
321
- fireEvent.click(replaceBtn)
322
- })
323
- expect(screen.getByTestId('text-output').textContent).toBe('bar bar bar')
324
- })
325
-
326
- it('replace-one uses raw offsets directly in source mode', async () => {
327
- function TextProbe() {
328
- const { text } = useDocument()
329
- return <div data-testid="text-output">{text}</div>
330
- }
331
- await act(async () => {
332
- render(
333
- <TranslationProvider>
334
- <DocumentProvider>
335
- <LanguageProvider>
336
- <TextSetter text="foo foo foo" />
337
- <SourceModeSetter />
338
- <PanelOpener mode="replace" />
339
- <FindReplace />
340
- <TextProbe />
341
- </LanguageProvider>
342
- </DocumentProvider>
343
- </TranslationProvider>
344
- )
345
- })
346
- const queryInput = screen.getByPlaceholderText('Find…')
347
- const replaceInput = screen.getByPlaceholderText('Replace with…')
348
- await act(async () => {
349
- fireEvent.change(queryInput, { target: { value: 'foo' } })
350
- fireEvent.change(replaceInput, { target: { value: 'bar' } })
351
- })
352
- const replaceBtn = screen.getByText('Replace')
353
- await act(async () => {
354
- fireEvent.click(replaceBtn)
355
- })
356
- expect(screen.getByTestId('text-output').textContent).toBe('bar foo foo')
357
- expect(screen.getByText('1 of 2')).toBeDefined()
358
- })
359
-
360
- it('Ctrl-F toggles panel visibility', async () => {
361
- let container!: HTMLElement
362
- await act(async () => {
363
- ;({ container } = render(
364
- <TranslationProvider>
365
- <DocumentProvider>
366
- <LanguageProvider>
367
- <FindReplace />
368
- </LanguageProvider>
369
- </DocumentProvider>
370
- </TranslationProvider>
371
- ))
372
- })
373
-
374
- // Panel starts hidden
375
- expect(container.querySelector('.cs-find-replace')).toBeNull()
376
-
377
- // Ctrl-F should open it
378
- await act(async () => {
379
- const ev = new KeyboardEvent('keydown', { key: 'f', code: 'KeyF', ctrlKey: true, cancelable: true, bubbles: true })
380
- window.dispatchEvent(ev)
381
- })
382
- expect(container.querySelector('.cs-find-replace')).not.toBeNull()
383
-
384
- // Ctrl-F again should close it
385
- await act(async () => {
386
- const ev = new KeyboardEvent('keydown', { key: 'f', code: 'KeyF', ctrlKey: true, cancelable: true, bubbles: true })
387
- window.dispatchEvent(ev)
388
- })
389
- expect(container.querySelector('.cs-find-replace')).toBeNull()
390
- })
391
- })
@@ -1,86 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { findAll, replaceAll, replaceInRange } from '../../src/findreplace/find-engine'
3
-
4
- describe('find-engine', () => {
5
- describe('findAll', () => {
6
- it('finds all occurrences of a query', () => {
7
- const matches = findAll('hello world hello', 'hello')
8
- expect(matches.length).toBe(2)
9
- expect(matches[0].start).toBe(0)
10
- expect(matches[1].start).toBe(12)
11
- })
12
-
13
- it('is case-sensitive by default', () => {
14
- const matches = findAll('Hello HELLO hello', 'hello')
15
- expect(matches.length).toBe(1)
16
- expect(matches[0].start).toBe(12)
17
- })
18
-
19
- it('case-insensitive mode finds all case variants', () => {
20
- const matches = findAll('Hello HELLO hello', 'hello', { caseSensitive: false })
21
- expect(matches.length).toBe(3)
22
- })
23
-
24
- it('handles NFC-normalized Devanagari text', () => {
25
- // "नमस्ते" — NFC is the canonical form
26
- const text = 'नमस्ते नमस्ते'
27
- const matches = findAll(text, 'नमस्ते')
28
- expect(matches.length).toBe(2)
29
- })
30
-
31
- it('returns empty for no matches', () => {
32
- expect(findAll('abc', 'xyz').length).toBe(0)
33
- })
34
-
35
- it('returns empty for empty query', () => {
36
- expect(findAll('abc', '').length).toBe(0)
37
- })
38
-
39
- it('case-insensitivity is silent no-op for Devanagari (caseless script)', () => {
40
- // Should not throw; just find exact matches
41
- const matches = findAll('नमस्ते', 'नमस्ते', { caseSensitive: false })
42
- expect(matches.length).toBe(1)
43
- })
44
-
45
- it('matches NFC-equivalent strings (NFC normalization)', () => {
46
- // Compose "é" two ways: precomposed NFC (U+00E9) vs NFD decomposed (e + combining accent)
47
- const precomposed = 'é' // NFC é
48
- const decomposed = 'é' // NFD é
49
- expect(precomposed.normalize('NFC')).toBe(decomposed.normalize('NFC'))
50
- const matches = findAll('café', decomposed)
51
- expect(matches.length).toBe(1)
52
- })
53
- })
54
-
55
- describe('replaceAll', () => {
56
- it('replaces all occurrences', () => {
57
- const result = replaceAll('foo bar foo baz', 'foo', 'qux')
58
- expect(result).toBe('qux bar qux baz')
59
- })
60
-
61
- it('returns original text when no matches', () => {
62
- expect(replaceAll('abc', 'xyz', 'qux')).toBe('abc')
63
- })
64
-
65
- it('replaces Devanagari text', () => {
66
- const result = replaceAll('राम राम राम', 'राम', 'श्याम')
67
- expect(result).toBe('श्याम श्याम श्याम')
68
- })
69
- })
70
-
71
- describe('replaceInRange', () => {
72
- it('replaces only within the specified range', () => {
73
- const text = 'abc foo abc foo abc'
74
- // Range covers 'foo abc foo' starting at index 4
75
- const result = replaceInRange(text, 'foo', 'bar', 4, 15)
76
- expect(result).toBe('abc bar abc bar abc')
77
- })
78
-
79
- it('does not modify text outside the range', () => {
80
- const text = 'foo abc foo'
81
- const result = replaceInRange(text, 'foo', 'bar', 4, 11)
82
- expect(result.startsWith('foo')).toBe(true)
83
- expect(result).toContain('bar')
84
- })
85
- })
86
- })
@@ -1,50 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { Node } from 'prosemirror-model'
3
- import { schema } from '../../src/editor/schema'
4
- import { textOffsetToPmPos } from '../../src/findreplace/scroll-to-match'
5
-
6
- function makeDoc(texts: string[]): Node {
7
- // Build a PM doc with one `action` block per text string
8
- const blocks = texts.map(t =>
9
- schema.nodes['action']!.create({}, t ? schema.text(t) : undefined)
10
- )
11
- return schema.nodes['doc']!.create({}, blocks)
12
- }
13
-
14
- describe('textOffsetToPmPos', () => {
15
- it('maps offset 0 to position inside first block', () => {
16
- // doc: action("hello") action("world")
17
- // PM positions: 0=doc open, 1=action open, 2..6=hello chars, 7=action close+open, 8..12=world
18
- // nodesBetween gives text node pos=1 for "hello"
19
- const doc = makeDoc(['hello', 'world'])
20
- expect(textOffsetToPmPos(doc, 0)).toBe(1) // start of "hello" text node
21
- })
22
-
23
- it('maps offset 3 into first block text', () => {
24
- const doc = makeDoc(['hello', 'world'])
25
- expect(textOffsetToPmPos(doc, 3)).toBe(4) // 3 chars into "hello"
26
- })
27
-
28
- it('maps offset 5 to start of second block text ("w" of "world")', () => {
29
- // offset 5 is the first char of "world" in the flat text map
30
- // "world" text node sits at pmPos 8 in: doc(0) action(1..6) action(7..12)
31
- const doc = makeDoc(['hello', 'world'])
32
- expect(textOffsetToPmPos(doc, 5)).toBe(8)
33
- })
34
-
35
- it('maps offset 6 into second block text ("o" of "world")', () => {
36
- // "hello"=5 chars consumed; "world" text node at pmPos=7
37
- // offset 6: textPos=5 after hello; "world" node: textPos=5..10, pmPos=7
38
- // result = 7 + (6 - 5) = 8... but actual is 9 because doc open token also counts
39
- // Verified from test run: offset 5→6, offset 6→9
40
- // The doc node itself contributes a +1 to all inner positions.
41
- // Re-verify: doc(0) action(1) text"hello"@1 /action(7) action(8) text"world"@9
42
- const doc = makeDoc(['hello', 'world'])
43
- expect(textOffsetToPmPos(doc, 6)).toBe(9)
44
- })
45
-
46
- it('returns null for out-of-range offset', () => {
47
- const doc = makeDoc(['hi'])
48
- expect(textOffsetToPmPos(doc, 999)).toBeNull()
49
- })
50
- })
@@ -1,122 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
- import { detectTofu } from '../../src/fonts/tofu-detect'
3
-
4
- describe('tofu-detect', () => {
5
- let originalCreateElement: typeof document.createElement
6
-
7
- beforeEach(() => {
8
- // Reset mocks before each test
9
- vi.clearAllMocks()
10
- originalCreateElement = document.createElement.bind(document)
11
- })
12
-
13
- afterEach(() => {
14
- // Restore original createElement
15
- document.createElement = originalCreateElement
16
- })
17
-
18
- it('returns false for a known string in a well-supported font', () => {
19
- // Track what character was rendered last
20
- let lastRenderedChar = ''
21
-
22
- const mockGetImageData = vi.fn((x, y, w, h) => {
23
- const data = new Uint8ClampedArray(w * h * 4)
24
- // Reference .notdef char renders with 50 pixels
25
- // Regular chars render with 150 pixels (more data)
26
- const pixelCount = lastRenderedChar === '�' ? 50 : 150
27
- for (let i = 0; i < Math.min(pixelCount * 4, data.length); i += 4) {
28
- data[i] = 100 // R
29
- data[i + 3] = 255 // A
30
- }
31
- return {
32
- data,
33
- width: w,
34
- height: h,
35
- colorSpace: 'srgb',
36
- }
37
- })
38
-
39
- const mockCtx = {
40
- font: '',
41
- fillStyle: '',
42
- fillText: vi.fn((text: string) => {
43
- lastRenderedChar = text
44
- }),
45
- clearRect: vi.fn(),
46
- getImageData: mockGetImageData,
47
- }
48
-
49
- document.createElement = vi.fn((tagName: string) => {
50
- if (tagName === 'canvas') {
51
- const canvas = originalCreateElement('canvas')
52
- canvas.getContext = vi.fn(() => mockCtx as any)
53
- return canvas
54
- }
55
- return originalCreateElement(tagName)
56
- })
57
-
58
- const result = detectTofu('Hello', 'monospace')
59
- expect(result).toBe(false)
60
- })
61
-
62
- it('returns true for unmapped PUA codepoint', () => {
63
- // Track what character was rendered last
64
- let lastRenderedChar = ''
65
-
66
- const mockGetImageData = vi.fn((x, y, w, h) => {
67
- const data = new Uint8ClampedArray(w * h * 4)
68
- // Both reference .notdef and unmapped char render with same pixel count (50)
69
- // This simulates the tofu detection scenario
70
- const pixelCount = 50
71
- for (let i = 0; i < Math.min(pixelCount * 4, data.length); i += 4) {
72
- data[i] = 100 // R
73
- data[i + 3] = 255 // A
74
- }
75
- return {
76
- data,
77
- width: w,
78
- height: h,
79
- colorSpace: 'srgb',
80
- }
81
- })
82
-
83
- const mockCtx = {
84
- font: '',
85
- fillStyle: '',
86
- fillText: vi.fn((text: string) => {
87
- lastRenderedChar = text
88
- }),
89
- clearRect: vi.fn(),
90
- getImageData: mockGetImageData,
91
- }
92
-
93
- document.createElement = vi.fn((tagName: string) => {
94
- if (tagName === 'canvas') {
95
- const canvas = originalCreateElement('canvas')
96
- canvas.getContext = vi.fn(() => mockCtx as any)
97
- return canvas
98
- }
99
- return originalCreateElement(tagName)
100
- })
101
-
102
- const result = detectTofu('󰀀', 'monospace')
103
- expect(result).toBe(true)
104
- })
105
-
106
- it('returns false for empty string', () => {
107
- const result = detectTofu('', 'monospace')
108
- expect(result).toBe(false)
109
- })
110
-
111
- it('handles SSR context gracefully (no document)', () => {
112
- const originalDoc = global.document
113
- // @ts-ignore
114
- delete global.document
115
- try {
116
- const result = detectTofu('test', 'monospace')
117
- expect(result).toBe(false)
118
- } finally {
119
- global.document = originalDoc
120
- }
121
- })
122
- })