Skip to content

Commit 9ef8ce9

Browse files
authored
Prevent ZWS cleanup RangeError in ZWSManagementExtension (baserow#4821)
1 parent cd0bfac commit 9ef8ce9

File tree

2 files changed

+61
-6
lines changed

2 files changed

+61
-6
lines changed

web-frontend/modules/core/components/formula/extensions/ZWSManagementExtension.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,33 @@ export const ZWSManagementExtension = Extension.create({
1616
key: new PluginKey('zwsManagement'),
1717
appendTransaction(transactions, oldState, newState) {
1818
const tr = newState.tr
19-
let modified = false
2019

21-
// Phase 1: Clean up consecutive ZWS
20+
// Phase 1: Clean up consecutive ZWS.
21+
// Collect replacements first, then apply in reverse order to avoid
22+
// position drift: each insertText changes the document length, so
23+
// positions calculated during iteration become invalid after the first
24+
// modification. Applying in reverse (highest pos first) keeps earlier
25+
// positions valid for subsequent operations.
26+
const zwsReplacements = []
2227
newState.doc.descendants((node, pos) => {
2328
if (node.isText && node.text) {
24-
// Check if the text contains multiple consecutive ZWS
2529
const text = node.text
2630
if (text.includes('\u200B\u200B')) {
27-
// Replace multiple consecutive ZWS with a single one
2831
const cleanedText = text.replace(/\u200B+/g, '\u200B')
2932
if (cleanedText !== text) {
30-
tr.insertText(cleanedText, pos, pos + node.nodeSize)
31-
modified = true
33+
zwsReplacements.push({
34+
cleanedText,
35+
pos,
36+
end: pos + node.nodeSize,
37+
})
3238
}
3339
}
3440
}
3541
})
42+
let modified = Boolean(zwsReplacements.length)
43+
for (const { cleanedText, pos, end } of zwsReplacements.reverse()) {
44+
tr.insertText(cleanedText, pos, end)
45+
}
3646

3747
// Apply cleanup changes before checking for missing ZWS
3848
const docAfterCleanup = modified ? tr.doc : newState.doc
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Editor } from '@tiptap/core'
2+
import { Document } from '@tiptap/extension-document'
3+
import { Text } from '@tiptap/extension-text'
4+
import { Paragraph } from '@tiptap/extension-paragraph'
5+
import { ZWSManagementExtension } from '@baserow/modules/core/components/formula/extensions/ZWSManagementExtension'
6+
7+
/**
8+
* Creates a minimal TipTap editor with only the ZWSManagementExtension active.
9+
*/
10+
function createEditor(content) {
11+
return new Editor({
12+
extensions: [Document, Paragraph, Text, ZWSManagementExtension],
13+
content,
14+
})
15+
}
16+
17+
function triggerAppendTransaction(editor) {
18+
editor.view.dispatch(editor.state.tr.setMeta('forceAppendTransaction', true))
19+
}
20+
21+
describe('ZWSManagementExtension', () => {
22+
let editor
23+
24+
afterEach(() => {
25+
editor?.destroy()
26+
})
27+
28+
it('reduces triple and quadruple consecutive ZWS to a single ZWS', () => {
29+
editor = createEditor({
30+
type: 'doc',
31+
content: [
32+
{
33+
type: 'paragraph',
34+
content: [{ type: 'text', text: 'x\u200B\u200B\u200B\u200By' }], // 4× ZWS
35+
},
36+
{
37+
type: 'paragraph',
38+
content: [{ type: 'text', text: 'p\u200B\u200B\u200Bq' }], // 3× ZWS
39+
},
40+
],
41+
})
42+
43+
expect(() => triggerAppendTransaction(editor)).not.toThrow()
44+
})
45+
})

0 commit comments

Comments
 (0)