Skip to content

Commit d2a098e

Browse files
SD-1468 - validating int ID for structured content for early failure (#1724)
* feat: validating int ID for structured content for early failure * feat: add ID validation to update commands
1 parent ed106b5 commit d2a098e

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

packages/super-editor/src/extensions/structured-content/structured-content-commands.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ import * as structuredContentHelpers from './structuredContentHelpers/index';
1010

1111
const STRUCTURED_CONTENT_NAMES = ['structuredContent', 'structuredContentBlock'];
1212

13+
/**
14+
* Validates that an ID is a valid integer string (required for MS Word compatibility).
15+
* @param {string|number} id - The ID to validate
16+
* @returns {boolean} - True if the ID is a valid integer string
17+
*/
18+
function isValidIntegerId(id) {
19+
if (id === null || id === undefined) return true; // Allow null/undefined (will be auto-generated)
20+
const str = String(id);
21+
return /^-?\d+$/.test(str);
22+
}
23+
1324
/**
1425
* Find the first text node within a structured content node, even when wrapped.
1526
* Some plugins wrap text in inline nodes (e.g., run), so we need to search descendants.
@@ -94,6 +105,11 @@ export const StructuredContentCommands = Extension.create({
94105
insertStructuredContentInline:
95106
(options = {}) =>
96107
({ editor, dispatch, state, tr }) => {
108+
// Validate ID is an integer (required for MS Word compatibility)
109+
if (options.attrs?.id !== undefined && !isValidIntegerId(options.attrs.id)) {
110+
throw new Error('Invalid structured content id - must be an integer, got: ' + options.attrs.id);
111+
}
112+
97113
const { schema } = editor;
98114
let { from, to } = state.selection;
99115

@@ -173,6 +189,11 @@ export const StructuredContentCommands = Extension.create({
173189
insertStructuredContentBlock:
174190
(options = {}) =>
175191
({ editor, dispatch, state, tr }) => {
192+
// Validate ID is an integer (required for MS Word compatibility)
193+
if (options.attrs?.id !== undefined && !isValidIntegerId(options.attrs.id)) {
194+
throw new Error('Invalid structured content id - must be an integer, got: ' + options.attrs.id);
195+
}
196+
176197
const { schema } = editor;
177198
let { from, to } = state.selection;
178199

@@ -247,6 +268,11 @@ export const StructuredContentCommands = Extension.create({
247268
updateStructuredContentById:
248269
(id, options = {}) =>
249270
({ editor, dispatch, state, tr }) => {
271+
// Validate ID is an integer (required for MS Word compatibility)
272+
if (options.attrs?.id !== undefined && !isValidIntegerId(options.attrs.id)) {
273+
throw new Error('Invalid structured content id - must be an integer, got: ' + options.attrs.id);
274+
}
275+
250276
const structuredContentTags = getStructuredContentTagsById(id, state);
251277

252278
if (!structuredContentTags.length) {
@@ -408,6 +434,11 @@ export const StructuredContentCommands = Extension.create({
408434
updateStructuredContentByGroup:
409435
(group, options = {}) =>
410436
({ editor, dispatch, state, tr }) => {
437+
// Validate ID is an integer (required for MS Word compatibility)
438+
if (options.attrs?.id !== undefined && !isValidIntegerId(options.attrs.id)) {
439+
throw new Error('Invalid structured content id - must be an integer, got: ' + options.attrs.id);
440+
}
441+
411442
const structuredContentTags = getStructuredContentByGroup(group, state);
412443

413444
if (!structuredContentTags.length) {

packages/super-editor/src/extensions/structured-content/structured-content-commands.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ describe('updateStructuredContentById', () => {
165165
schema = null;
166166
});
167167

168+
it('throws error when updating ID with a non-integer value', () => {
169+
expect(() => {
170+
editor.commands.updateStructuredContentById(INLINE_ID, {
171+
attrs: { id: 'abc-123' },
172+
});
173+
}).toThrow('Invalid structured content id - must be an integer, got: abc-123');
174+
});
175+
168176
describe('keepTextNodeStyles option', () => {
169177
it('preserves marks from the first text node when keepTextNodeStyles is true', () => {
170178
const didUpdate = editor.commands.updateStructuredContentById(INLINE_ID, {
@@ -407,6 +415,14 @@ describe('updateStructuredContentByGroup', () => {
407415
schema = null;
408416
});
409417

418+
it('throws error when updating ID with a non-integer value', () => {
419+
expect(() => {
420+
editor.commands.updateStructuredContentByGroup(GROUP_NAME, {
421+
attrs: { id: 'abc-123' },
422+
});
423+
}).toThrow('Invalid structured content id - must be an integer, got: abc-123');
424+
});
425+
410426
describe('keepTextNodeStyles option', () => {
411427
it('preserves marks from the first text node for all nodes in group when keepTextNodeStyles is true', () => {
412428
const didUpdate = editor.commands.updateStructuredContentByGroup(GROUP_NAME, {
@@ -539,3 +555,126 @@ describe('updateStructuredContentByGroup', () => {
539555
});
540556
});
541557
});
558+
559+
describe('StructuredContent ID Validation', () => {
560+
let editor;
561+
562+
beforeEach(() => {
563+
({ editor } = initTestEditor({ mode: 'text', content: '<p></p>' }));
564+
});
565+
566+
afterEach(() => {
567+
editor?.destroy();
568+
editor = null;
569+
});
570+
571+
describe('insertStructuredContentInline', () => {
572+
it('accepts valid integer string IDs', () => {
573+
expect(() => {
574+
editor.commands.insertStructuredContentInline({
575+
attrs: { id: '123' },
576+
text: 'Test content',
577+
});
578+
}).not.toThrow();
579+
});
580+
581+
it('accepts valid negative integer string IDs', () => {
582+
expect(() => {
583+
editor.commands.insertStructuredContentInline({
584+
attrs: { id: '-456' },
585+
text: 'Test content',
586+
});
587+
}).not.toThrow();
588+
});
589+
590+
it('accepts numeric integer IDs', () => {
591+
expect(() => {
592+
editor.commands.insertStructuredContentInline({
593+
attrs: { id: 789 },
594+
text: 'Test content',
595+
});
596+
}).not.toThrow();
597+
});
598+
599+
it('auto-generates ID when not provided', () => {
600+
expect(() => {
601+
editor.commands.insertStructuredContentInline({
602+
text: 'Test content',
603+
});
604+
}).not.toThrow();
605+
});
606+
607+
it('throws error for non-integer string IDs', () => {
608+
expect(() => {
609+
editor.commands.insertStructuredContentInline({
610+
attrs: { id: 'abc-123' },
611+
text: 'Test content',
612+
});
613+
}).toThrow('Invalid structured content id - must be an integer, got: abc-123');
614+
});
615+
616+
it('throws error for float IDs', () => {
617+
expect(() => {
618+
editor.commands.insertStructuredContentInline({
619+
attrs: { id: '123.45' },
620+
text: 'Test content',
621+
});
622+
}).toThrow('Invalid structured content id - must be an integer, got: 123.45');
623+
});
624+
625+
it('throws error for UUID-style IDs', () => {
626+
expect(() => {
627+
editor.commands.insertStructuredContentInline({
628+
attrs: { id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' },
629+
text: 'Test content',
630+
});
631+
}).toThrow('Invalid structured content id - must be an integer, got: a1b2c3d4-e5f6-7890-abcd-ef1234567890');
632+
});
633+
});
634+
635+
describe('insertStructuredContentBlock', () => {
636+
it('accepts valid integer string IDs', () => {
637+
expect(() => {
638+
editor.commands.insertStructuredContentBlock({
639+
attrs: { id: '123' },
640+
html: '<p>Test content</p>',
641+
});
642+
}).not.toThrow();
643+
});
644+
645+
it('accepts valid negative integer string IDs', () => {
646+
expect(() => {
647+
editor.commands.insertStructuredContentBlock({
648+
attrs: { id: '-456' },
649+
html: '<p>Test content</p>',
650+
});
651+
}).not.toThrow();
652+
});
653+
654+
it('auto-generates ID when not provided', () => {
655+
expect(() => {
656+
editor.commands.insertStructuredContentBlock({
657+
html: '<p>Test content</p>',
658+
});
659+
}).not.toThrow();
660+
});
661+
662+
it('throws error for non-integer string IDs', () => {
663+
expect(() => {
664+
editor.commands.insertStructuredContentBlock({
665+
attrs: { id: 'my-block-id' },
666+
html: '<p>Test content</p>',
667+
});
668+
}).toThrow('Invalid structured content id - must be an integer, got: my-block-id');
669+
});
670+
671+
it('throws error for float IDs', () => {
672+
expect(() => {
673+
editor.commands.insertStructuredContentBlock({
674+
attrs: { id: '99.99' },
675+
html: '<p>Test content</p>',
676+
});
677+
}).toThrow('Invalid structured content id - must be an integer, got: 99.99');
678+
});
679+
});
680+
});

0 commit comments

Comments
 (0)