@byline/richtext-lexical 5.2.0 → 6.0.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/dist/config.d.ts +1 -1
- package/dist/field/apply-value-plugin.js +31 -6
- package/dist/field/capabilities/filter-transformers.d.ts +29 -0
- package/dist/field/capabilities/filter-transformers.js +8 -0
- package/dist/field/capabilities/registered-node-types.d.ts +29 -0
- package/dist/field/capabilities/registered-node-types.js +7 -0
- package/dist/field/config/built-in-extension-names.d.ts +42 -0
- package/dist/field/config/built-in-extension-names.js +12 -2
- package/dist/field/config/default-extensions.js +4 -0
- package/dist/field/config/default.js +8 -8
- package/dist/field/config/editor-config-context.d.ts +2 -2
- package/dist/field/config/editor-config-context.js +10 -13
- package/dist/field/config/resolve-editor-config.js +3 -3
- package/dist/field/config/root-dependencies.d.ts +26 -0
- package/dist/field/config/root-dependencies.js +8 -0
- package/dist/field/config/types.d.ts +61 -8
- package/dist/field/editor-context.js +44 -27
- package/dist/field/editor.js +15 -8
- package/dist/field/extensions/core-nodes/core-nodes-extension.d.ts +24 -0
- package/dist/field/extensions/core-nodes/core-nodes-extension.js +11 -0
- package/dist/field/extensions/heading/heading-extension.d.ts +20 -0
- package/dist/field/extensions/heading/heading-extension.js +9 -0
- package/dist/field/extensions/quote/quote-extension.d.ts +16 -0
- package/dist/field/extensions/quote/quote-extension.js +9 -0
- package/dist/field/hooks/use-markdown-toggle.d.ts +15 -0
- package/dist/field/hooks/use-markdown-toggle.js +12 -4
- package/dist/field/hooks/use-platform-modifier.d.ts +16 -0
- package/dist/field/hooks/use-platform-modifier.js +8 -0
- package/dist/field/markdown/transformers.d.ts +1 -0
- package/dist/field/markdown/transformers.js +5 -4
- package/dist/field/nodes/index.d.ts +11 -1
- package/dist/field/nodes/index.js +2 -2
- package/dist/field/normalize/adapted-notice.css +34 -0
- package/dist/field/normalize/adapted-notice.d.ts +29 -0
- package/dist/field/normalize/adapted-notice.js +35 -0
- package/dist/field/normalize/content-labels.d.ts +15 -0
- package/dist/field/normalize/content-labels.js +19 -0
- package/dist/field/normalize/declared-conversions.d.ts +131 -0
- package/dist/field/normalize/declared-conversions.js +93 -0
- package/dist/field/normalize/normalization-status.d.ts +33 -0
- package/dist/field/normalize/normalization-status.js +28 -0
- package/dist/field/normalize/normalize-value.d.ts +38 -0
- package/dist/field/normalize/normalize-value.js +132 -0
- package/dist/field/normalize/use-notice-text.d.ts +11 -0
- package/dist/field/normalize/use-notice-text.js +34 -0
- package/dist/field/plugins/toolbar-plugin/index.d.ts +23 -0
- package/dist/field/plugins/toolbar-plugin/index.js +104 -78
- package/dist/field/test-support/build-test-editor.js +49 -0
- package/dist/field/test-support/capture-editor.js +7 -0
- package/dist/field/test-support/frame-control.js +40 -0
- package/dist/field/test-support/jsdom-setup.js +21 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -1
- package/dist/scan/capability-manifest.d.ts +30 -0
- package/dist/scan/capability-manifest.js +43 -0
- package/dist/scan/scan-documents.d.ts +29 -0
- package/dist/scan/scan-documents.js +39 -0
- package/dist/scan/types.d.ts +63 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan.d.ts +25 -0
- package/dist/scan.js +1 -0
- package/package.json +14 -5
- package/src/config.ts +1 -1
- package/src/field/apply-value-plugin.tsx +71 -5
- package/src/field/capabilities/filter-transformers.test.tsx +71 -0
- package/src/field/capabilities/filter-transformers.ts +42 -0
- package/src/field/capabilities/markdown-behaviour.test.tsx +188 -0
- package/src/field/capabilities/no-blanket-registration.test.tsx +86 -0
- package/src/field/capabilities/registered-node-types.test.tsx +29 -0
- package/src/field/capabilities/registered-node-types.ts +36 -0
- package/src/field/config/built-in-extension-names.test.node.ts +38 -14
- package/src/field/config/built-in-extension-names.ts +29 -1
- package/src/field/config/default-extensions.ts +6 -0
- package/src/field/config/default.ts +8 -8
- package/src/field/config/editor-config-context.tsx +14 -9
- package/src/field/config/list-removal.test.tsx +44 -0
- package/src/field/config/resolve-editor-config.test.node.ts +27 -6
- package/src/field/config/resolve-editor-config.ts +3 -3
- package/src/field/config/root-dependencies.ts +32 -0
- package/src/field/config/types.ts +61 -17
- package/src/field/editor-component.test.tsx +18 -3
- package/src/field/editor-context.regression.test.tsx +124 -0
- package/src/field/editor-context.tsx +90 -22
- package/src/field/editor.tsx +22 -12
- package/src/field/extensions/core-nodes/core-nodes-extension.test.tsx +13 -0
- package/src/field/extensions/core-nodes/core-nodes-extension.ts +32 -0
- package/src/field/extensions/heading/heading-extension.test.tsx +32 -0
- package/src/field/extensions/heading/heading-extension.ts +27 -0
- package/src/field/extensions/quote/quote-extension.ts +23 -0
- package/src/field/hooks/markdown-source-mode.test.tsx +221 -0
- package/src/field/hooks/use-markdown-toggle.ts +39 -4
- package/src/field/hooks/use-platform-modifier.test.tsx +81 -0
- package/src/field/hooks/use-platform-modifier.ts +41 -0
- package/src/field/markdown/transformers.ts +18 -4
- package/src/field/nodes/index.ts +11 -1
- package/src/field/normalize/adapted-notice.css +52 -0
- package/src/field/normalize/adapted-notice.tsx +63 -0
- package/src/field/normalize/clipboard-equivalence.test.tsx +330 -0
- package/src/field/normalize/content-labels.ts +44 -0
- package/src/field/normalize/declared-conversions.test.node.ts +67 -0
- package/src/field/normalize/declared-conversions.ts +160 -0
- package/src/field/normalize/emission-gate.test.tsx +302 -0
- package/src/field/normalize/normalization-status.tsx +48 -0
- package/src/field/normalize/normalize-load.test.tsx +371 -0
- package/src/field/normalize/normalize-value.test.node.ts +530 -0
- package/src/field/normalize/normalize-value.ts +180 -0
- package/src/field/normalize/notice-i18n.test.tsx +128 -0
- package/src/field/normalize/use-notice-text.ts +82 -0
- package/src/field/normalize/wiring.test.tsx +348 -0
- package/src/field/plugins/toolbar-plugin/block-format-capabilities.test.tsx +57 -0
- package/src/field/plugins/toolbar-plugin/index.tsx +131 -68
- package/src/field/test-support/build-test-editor.test.tsx +17 -0
- package/src/field/test-support/build-test-editor.ts +99 -0
- package/src/field/test-support/capture-editor.tsx +20 -0
- package/src/field/test-support/frame-control.ts +91 -0
- package/src/field/test-support/jsdom-setup.ts +41 -0
- package/src/index.ts +12 -1
- package/src/scan/capability-manifest.test.tsx +82 -0
- package/src/scan/capability-manifest.ts +81 -0
- package/src/scan/scan-documents.test.node.ts +125 -0
- package/src/scan/scan-documents.ts +78 -0
- package/src/scan/types.ts +66 -0
- package/src/scan.ts +27 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { normalizeValue } from './normalize-value'
|
|
4
|
+
|
|
5
|
+
const text = (value: string, format = 0) => ({
|
|
6
|
+
detail: 0,
|
|
7
|
+
format,
|
|
8
|
+
mode: 'normal',
|
|
9
|
+
style: '',
|
|
10
|
+
text: value,
|
|
11
|
+
type: 'text',
|
|
12
|
+
version: 1,
|
|
13
|
+
})
|
|
14
|
+
const para = (...children: unknown[]) => ({
|
|
15
|
+
children,
|
|
16
|
+
direction: null,
|
|
17
|
+
format: '',
|
|
18
|
+
indent: 0,
|
|
19
|
+
type: 'paragraph',
|
|
20
|
+
version: 1,
|
|
21
|
+
})
|
|
22
|
+
const heading = (tag: string, ...children: unknown[]) => ({
|
|
23
|
+
children,
|
|
24
|
+
direction: null,
|
|
25
|
+
format: '',
|
|
26
|
+
indent: 0,
|
|
27
|
+
type: 'heading',
|
|
28
|
+
version: 1,
|
|
29
|
+
tag,
|
|
30
|
+
})
|
|
31
|
+
// biome-ignore lint/suspicious/noExplicitAny: serialized fixtures are structural
|
|
32
|
+
const doc = (...children: unknown[]): any => ({
|
|
33
|
+
root: { children, direction: null, format: '', indent: 0, type: 'root', version: 1 },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const SUPPORTED = new Set(['root', 'paragraph', 'text', 'linebreak', 'tab'])
|
|
37
|
+
|
|
38
|
+
describe('normalizeValue', () => {
|
|
39
|
+
it('reports unchanged when every type is supported', () => {
|
|
40
|
+
expect(normalizeValue(doc(para(text('hi'))), SUPPORTED).status).toBe('unchanged')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('never mutates its input', () => {
|
|
44
|
+
const input = doc(heading('h1', text('Title')))
|
|
45
|
+
const snapshot = JSON.stringify(input)
|
|
46
|
+
normalizeValue(input, SUPPORTED)
|
|
47
|
+
expect(JSON.stringify(input)).toBe(snapshot)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('converts a heading to a paragraph and preserves inline formats exactly', () => {
|
|
51
|
+
const result = normalizeValue(doc(heading('h1', text('Bold title', 1))), SUPPORTED)
|
|
52
|
+
expect(result.status).toBe('adapted')
|
|
53
|
+
if (result.status !== 'adapted') return
|
|
54
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
55
|
+
const first = result.value.root.children[0] as any
|
|
56
|
+
expect(first.type).toBe('paragraph')
|
|
57
|
+
expect(first.tag).toBeUndefined()
|
|
58
|
+
// A JSON rewrite carries text formats as data, so bold survives
|
|
59
|
+
// exactly rather than being reconstructed through markup.
|
|
60
|
+
expect(first.children[0].format).toBe(1)
|
|
61
|
+
expect(result.convertedTypes).toEqual(['heading'])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('keeps a supported sibling byte-for-byte identical', () => {
|
|
65
|
+
const survivor = para(text('body'))
|
|
66
|
+
const result = normalizeValue(doc(heading('h1', text('T')), survivor), SUPPORTED)
|
|
67
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
68
|
+
expect(result.value.root.children[1]).toEqual(survivor)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('inspects children of a supported parent', () => {
|
|
72
|
+
const supported = new Set([...SUPPORTED, 'quote'])
|
|
73
|
+
const quote = {
|
|
74
|
+
children: [heading('h2', text('inner'))],
|
|
75
|
+
direction: null,
|
|
76
|
+
format: '',
|
|
77
|
+
indent: 0,
|
|
78
|
+
type: 'quote',
|
|
79
|
+
version: 1,
|
|
80
|
+
}
|
|
81
|
+
const result = normalizeValue(doc(quote), supported)
|
|
82
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
83
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
84
|
+
const out = result.value.root.children[0] as any
|
|
85
|
+
expect(out.type).toBe('quote')
|
|
86
|
+
expect(out.children[0].type).toBe('paragraph')
|
|
87
|
+
expect(out.children[0].children[0].text).toBe('inner')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('keeps adjacent headings as separate paragraphs', () => {
|
|
91
|
+
const result = normalizeValue(
|
|
92
|
+
doc(heading('h1', text('First')), heading('h2', text('Second'))),
|
|
93
|
+
SUPPORTED
|
|
94
|
+
)
|
|
95
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
96
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
97
|
+
const kids = result.value.root.children as any[]
|
|
98
|
+
expect(kids).toHaveLength(2)
|
|
99
|
+
expect(kids[0].children[0].text).toBe('First')
|
|
100
|
+
expect(kids[1].children[0].text).toBe('Second')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('keeps an empty heading as an empty paragraph', () => {
|
|
104
|
+
const result = normalizeValue(doc(heading('h1'), para(text('after'))), SUPPORTED)
|
|
105
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
106
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
107
|
+
const kids = result.value.root.children as any[]
|
|
108
|
+
expect(kids).toHaveLength(2)
|
|
109
|
+
expect(kids[0].type).toBe('paragraph')
|
|
110
|
+
expect(kids[0].children).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('keeps a supported link inside its paragraph rather than at root', () => {
|
|
114
|
+
const link = {
|
|
115
|
+
children: [text('link text')],
|
|
116
|
+
direction: null,
|
|
117
|
+
format: '',
|
|
118
|
+
indent: 0,
|
|
119
|
+
type: 'link',
|
|
120
|
+
version: 2,
|
|
121
|
+
attributes: { url: 'https://x.test' },
|
|
122
|
+
}
|
|
123
|
+
const result = normalizeValue(doc(heading('h1', link)), new Set([...SUPPORTED, 'link']))
|
|
124
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
125
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
126
|
+
const kids = result.value.root.children as any[]
|
|
127
|
+
expect(kids[0].type).toBe('paragraph')
|
|
128
|
+
expect(kids[0].children[0].type).toBe('link')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('flattens a nested table to one paragraph per cell', () => {
|
|
132
|
+
const cell = {
|
|
133
|
+
children: [para(text('cell text'))],
|
|
134
|
+
direction: null,
|
|
135
|
+
format: '',
|
|
136
|
+
indent: 0,
|
|
137
|
+
type: 'tablecell',
|
|
138
|
+
version: 1,
|
|
139
|
+
colSpan: 1,
|
|
140
|
+
rowSpan: 1,
|
|
141
|
+
headerState: 0,
|
|
142
|
+
}
|
|
143
|
+
const row = {
|
|
144
|
+
children: [cell],
|
|
145
|
+
direction: null,
|
|
146
|
+
format: '',
|
|
147
|
+
indent: 0,
|
|
148
|
+
type: 'tablerow',
|
|
149
|
+
version: 1,
|
|
150
|
+
}
|
|
151
|
+
const table = {
|
|
152
|
+
children: [row],
|
|
153
|
+
direction: null,
|
|
154
|
+
format: '',
|
|
155
|
+
indent: 0,
|
|
156
|
+
type: 'table',
|
|
157
|
+
version: 1,
|
|
158
|
+
}
|
|
159
|
+
const result = normalizeValue(doc(table), SUPPORTED)
|
|
160
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
161
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
162
|
+
const kids = result.value.root.children as any[]
|
|
163
|
+
expect(kids.map((child) => child.type)).toEqual(['paragraph'])
|
|
164
|
+
expect(kids[0].children[0].text).toBe('cell text')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('appends the link URL to its text when links are unsupported', () => {
|
|
168
|
+
const link = {
|
|
169
|
+
children: [text('link text')],
|
|
170
|
+
direction: null,
|
|
171
|
+
format: '',
|
|
172
|
+
indent: 0,
|
|
173
|
+
type: 'link',
|
|
174
|
+
version: 2,
|
|
175
|
+
attributes: { url: 'https://x.test/a' },
|
|
176
|
+
}
|
|
177
|
+
const result = normalizeValue(doc(para(link)), SUPPORTED)
|
|
178
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
179
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
180
|
+
const p = result.value.root.children[0] as any
|
|
181
|
+
expect(p.type).toBe('paragraph')
|
|
182
|
+
const joined = p.children.map((child: { text: string }) => child.text).join('')
|
|
183
|
+
expect(joined).toContain('link text')
|
|
184
|
+
expect(joined).toContain('https://x.test/a')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('emits the admonition title as the first line, not appended', () => {
|
|
188
|
+
const admonition = {
|
|
189
|
+
children: [para(text('body copy'))],
|
|
190
|
+
direction: null,
|
|
191
|
+
format: '',
|
|
192
|
+
indent: 0,
|
|
193
|
+
type: 'admonition',
|
|
194
|
+
version: 1,
|
|
195
|
+
title: 'Note title',
|
|
196
|
+
}
|
|
197
|
+
const result = normalizeValue(doc(admonition), SUPPORTED)
|
|
198
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
199
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
200
|
+
const kids = result.value.root.children as any[]
|
|
201
|
+
expect(kids[0].children[0].text).toBe('Note title')
|
|
202
|
+
expect(kids[1].children[0].text).toBe('body copy')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('drops a horizontal rule and keeps its neighbours', () => {
|
|
206
|
+
const result = normalizeValue(
|
|
207
|
+
doc(para(text('before')), { type: 'horizontalrule', version: 1 }, para(text('after'))),
|
|
208
|
+
SUPPORTED
|
|
209
|
+
)
|
|
210
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
211
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
212
|
+
const kids = result.value.root.children as any[]
|
|
213
|
+
expect(kids.map((child) => child.type)).toEqual(['paragraph', 'paragraph'])
|
|
214
|
+
expect(kids[0].children[0].text).toBe('before')
|
|
215
|
+
expect(kids[1].children[0].text).toBe('after')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('never leaves the root empty when conversion removes everything', () => {
|
|
219
|
+
// Lexical rejects an empty root outright: "setEditorState: the editor
|
|
220
|
+
// state is empty."
|
|
221
|
+
const result = normalizeValue(doc({ type: 'horizontalrule', version: 1 }), SUPPORTED)
|
|
222
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
223
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
224
|
+
const kids = result.value.root.children as any[]
|
|
225
|
+
expect(kids).toHaveLength(1)
|
|
226
|
+
expect(kids[0].type).toBe('paragraph')
|
|
227
|
+
expect(kids[0].children).toEqual([])
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('lifts a nested list out of its item rather than nesting a block in a paragraph', () => {
|
|
231
|
+
// A listitem may hold a list, not only inline content.
|
|
232
|
+
const inner = {
|
|
233
|
+
children: [
|
|
234
|
+
{
|
|
235
|
+
children: [text('inner')],
|
|
236
|
+
direction: null,
|
|
237
|
+
format: '',
|
|
238
|
+
indent: 1,
|
|
239
|
+
type: 'listitem',
|
|
240
|
+
version: 1,
|
|
241
|
+
value: 1,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
direction: null,
|
|
245
|
+
format: '',
|
|
246
|
+
indent: 0,
|
|
247
|
+
type: 'list',
|
|
248
|
+
version: 1,
|
|
249
|
+
listType: 'bullet',
|
|
250
|
+
start: 1,
|
|
251
|
+
tag: 'ul',
|
|
252
|
+
}
|
|
253
|
+
const outer = {
|
|
254
|
+
children: [
|
|
255
|
+
{
|
|
256
|
+
children: [text('outer')],
|
|
257
|
+
direction: null,
|
|
258
|
+
format: '',
|
|
259
|
+
indent: 0,
|
|
260
|
+
type: 'listitem',
|
|
261
|
+
version: 1,
|
|
262
|
+
value: 1,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
children: [inner],
|
|
266
|
+
direction: null,
|
|
267
|
+
format: '',
|
|
268
|
+
indent: 0,
|
|
269
|
+
type: 'listitem',
|
|
270
|
+
version: 1,
|
|
271
|
+
value: 2,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
direction: null,
|
|
275
|
+
format: '',
|
|
276
|
+
indent: 0,
|
|
277
|
+
type: 'list',
|
|
278
|
+
version: 1,
|
|
279
|
+
listType: 'bullet',
|
|
280
|
+
start: 1,
|
|
281
|
+
tag: 'ul',
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const result = normalizeValue(doc(outer), SUPPORTED)
|
|
285
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
286
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
287
|
+
const kids = result.value.root.children as any[]
|
|
288
|
+
expect(kids.every((child) => child.type === 'paragraph')).toBe(true)
|
|
289
|
+
// No paragraph contains a block child.
|
|
290
|
+
for (const kid of kids) {
|
|
291
|
+
for (const grandchild of kid.children ?? []) {
|
|
292
|
+
expect(grandchild.type).toBe('text')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const joined = JSON.stringify(result.value)
|
|
296
|
+
expect(joined).toContain('outer')
|
|
297
|
+
expect(joined).toContain('inner')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('keeps a still-supported nested list as a block beside the converted text', () => {
|
|
301
|
+
// Only the item is unsupported here, so its nested list must survive
|
|
302
|
+
// as a sibling block rather than be wrapped into a paragraph.
|
|
303
|
+
const supportedList = {
|
|
304
|
+
children: [
|
|
305
|
+
{
|
|
306
|
+
children: [text('kept')],
|
|
307
|
+
direction: null,
|
|
308
|
+
format: '',
|
|
309
|
+
indent: 1,
|
|
310
|
+
type: 'listitem',
|
|
311
|
+
version: 1,
|
|
312
|
+
value: 1,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
direction: null,
|
|
316
|
+
format: '',
|
|
317
|
+
indent: 0,
|
|
318
|
+
type: 'list',
|
|
319
|
+
version: 1,
|
|
320
|
+
listType: 'bullet',
|
|
321
|
+
start: 1,
|
|
322
|
+
tag: 'ul',
|
|
323
|
+
}
|
|
324
|
+
const item = {
|
|
325
|
+
children: [text('label'), supportedList],
|
|
326
|
+
direction: null,
|
|
327
|
+
format: '',
|
|
328
|
+
indent: 0,
|
|
329
|
+
type: 'listitem',
|
|
330
|
+
version: 1,
|
|
331
|
+
value: 1,
|
|
332
|
+
}
|
|
333
|
+
const result = normalizeValue(doc(item), new Set([...SUPPORTED, 'list']))
|
|
334
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
335
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
336
|
+
const kids = result.value.root.children as any[]
|
|
337
|
+
expect(kids.map((child) => child.type)).toEqual(['paragraph', 'list'])
|
|
338
|
+
expect(kids[0].children[0].text).toBe('label')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('converts highlighted code tokens to text, preserving inline formats', () => {
|
|
342
|
+
// CodeHighlightNode is a TextNode subclass owned by the same
|
|
343
|
+
// extension as CodeNode, so removing code-highlight unregisters both.
|
|
344
|
+
const code = {
|
|
345
|
+
children: [
|
|
346
|
+
{
|
|
347
|
+
detail: 0,
|
|
348
|
+
format: 1,
|
|
349
|
+
mode: 'normal',
|
|
350
|
+
style: '',
|
|
351
|
+
text: 'const',
|
|
352
|
+
type: 'code-highlight',
|
|
353
|
+
version: 1,
|
|
354
|
+
highlightType: 'keyword',
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
detail: 0,
|
|
358
|
+
format: 0,
|
|
359
|
+
mode: 'normal',
|
|
360
|
+
style: '',
|
|
361
|
+
text: ' x = 1',
|
|
362
|
+
type: 'code-highlight',
|
|
363
|
+
version: 1,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
direction: null,
|
|
367
|
+
format: '',
|
|
368
|
+
indent: 0,
|
|
369
|
+
type: 'code',
|
|
370
|
+
version: 1,
|
|
371
|
+
language: 'js',
|
|
372
|
+
}
|
|
373
|
+
const result = normalizeValue(doc(code), SUPPORTED)
|
|
374
|
+
expect(result.status).toBe('adapted')
|
|
375
|
+
if (result.status !== 'adapted') return
|
|
376
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
377
|
+
const paragraph = result.value.root.children[0] as any
|
|
378
|
+
expect(paragraph.type).toBe('paragraph')
|
|
379
|
+
expect(paragraph.children.every((child: { type: string }) => child.type === 'text')).toBe(true)
|
|
380
|
+
expect(paragraph.children.map((child: { text: string }) => child.text).join('')).toBe(
|
|
381
|
+
'const x = 1'
|
|
382
|
+
)
|
|
383
|
+
// The bold format on the first token survived the conversion.
|
|
384
|
+
expect(paragraph.children[0].format).toBe(1)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('refuses rather than inlining a supported custom block inside a converted parent', () => {
|
|
388
|
+
// A supported node CAN reach the block/inline partition, as the child
|
|
389
|
+
// of an unsupported parent being converted. Its structural role is
|
|
390
|
+
// not recoverable from the serialized shape — an inline link and a
|
|
391
|
+
// block quote look identical — so assuming inline would wrap a
|
|
392
|
+
// custom block in a paragraph and produce a tree Lexical rejects.
|
|
393
|
+
const customBlock = {
|
|
394
|
+
children: [para(text('callout body'))],
|
|
395
|
+
direction: null,
|
|
396
|
+
format: '',
|
|
397
|
+
indent: 0,
|
|
398
|
+
type: 'acme-callout',
|
|
399
|
+
version: 1,
|
|
400
|
+
}
|
|
401
|
+
const item = {
|
|
402
|
+
children: [text('label'), customBlock],
|
|
403
|
+
direction: null,
|
|
404
|
+
format: '',
|
|
405
|
+
indent: 0,
|
|
406
|
+
type: 'listitem',
|
|
407
|
+
version: 1,
|
|
408
|
+
value: 1,
|
|
409
|
+
}
|
|
410
|
+
const result = normalizeValue(doc(item), new Set([...SUPPORTED, 'acme-callout']))
|
|
411
|
+
expect(result.status).toBe('refused')
|
|
412
|
+
if (result.status !== 'refused') return
|
|
413
|
+
expect(result.unsupportedTypes).toContain('acme-callout')
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('does not refuse a supported custom block left in its own position', () => {
|
|
417
|
+
// Untouched by any conversion, it never reaches the partition.
|
|
418
|
+
const customBlock = {
|
|
419
|
+
children: [para(text('callout body'))],
|
|
420
|
+
direction: null,
|
|
421
|
+
format: '',
|
|
422
|
+
indent: 0,
|
|
423
|
+
type: 'acme-callout',
|
|
424
|
+
version: 1,
|
|
425
|
+
}
|
|
426
|
+
const result = normalizeValue(
|
|
427
|
+
doc(heading('h1', text('Title')), customBlock),
|
|
428
|
+
new Set([...SUPPORTED, 'acme-callout'])
|
|
429
|
+
)
|
|
430
|
+
expect(result.status).toBe('adapted')
|
|
431
|
+
if (result.status !== 'adapted') return
|
|
432
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
433
|
+
const kids = result.value.root.children as any[]
|
|
434
|
+
expect(kids.map((child) => child.type)).toEqual(['paragraph', 'acme-callout'])
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('keeps a known inline node inline inside a converted parent', () => {
|
|
438
|
+
const link = {
|
|
439
|
+
children: [text('link text')],
|
|
440
|
+
direction: null,
|
|
441
|
+
format: '',
|
|
442
|
+
indent: 0,
|
|
443
|
+
type: 'link',
|
|
444
|
+
version: 2,
|
|
445
|
+
attributes: { url: 'https://x.test' },
|
|
446
|
+
}
|
|
447
|
+
const item = {
|
|
448
|
+
children: [text('label '), link],
|
|
449
|
+
direction: null,
|
|
450
|
+
format: '',
|
|
451
|
+
indent: 0,
|
|
452
|
+
type: 'listitem',
|
|
453
|
+
version: 1,
|
|
454
|
+
value: 1,
|
|
455
|
+
}
|
|
456
|
+
const result = normalizeValue(doc(item), new Set([...SUPPORTED, 'link']))
|
|
457
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
458
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural assertion
|
|
459
|
+
const kids = result.value.root.children as any[]
|
|
460
|
+
expect(kids).toHaveLength(1)
|
|
461
|
+
expect(kids[0].type).toBe('paragraph')
|
|
462
|
+
expect(kids[0].children.map((child: { type: string }) => child.type)).toEqual(['text', 'link'])
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('refuses a document containing an inline image', () => {
|
|
466
|
+
const result = normalizeValue(
|
|
467
|
+
doc(para({ type: 'inline-image', version: 1, src: '/cat.png' })),
|
|
468
|
+
SUPPORTED
|
|
469
|
+
)
|
|
470
|
+
expect(result.status).toBe('refused')
|
|
471
|
+
if (result.status !== 'refused') return
|
|
472
|
+
expect(result.unsupportedTypes).toContain('inline-image')
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('refuses for an unknown custom node rather than unwrapping it', () => {
|
|
476
|
+
const custom = { children: [para(text('inner'))], type: 'acme-callout', version: 1 }
|
|
477
|
+
expect(normalizeValue(doc(custom), SUPPORTED).status).toBe('refused')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('does not refuse when the custom node is supported', () => {
|
|
481
|
+
const custom = { children: [para(text('inner'))], type: 'acme-callout', version: 1 }
|
|
482
|
+
expect(normalizeValue(doc(custom), new Set([...SUPPORTED, 'acme-callout'])).status).toBe(
|
|
483
|
+
'unchanged'
|
|
484
|
+
)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('refusal wins over a conversion elsewhere in the document', () => {
|
|
488
|
+
const result = normalizeValue(
|
|
489
|
+
doc(heading('h1', text('convertible')), para({ type: 'youtube', version: 1, videoID: 'a' })),
|
|
490
|
+
SUPPORTED
|
|
491
|
+
)
|
|
492
|
+
expect(result.status).toBe('refused')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('preserves a supported image nested inside an unsupported container', () => {
|
|
496
|
+
// The whole point of converting only what is unsupported: a
|
|
497
|
+
// document-wide re-encode would strip this image's relation and
|
|
498
|
+
// caption as collateral of converting the layout around it.
|
|
499
|
+
const image = {
|
|
500
|
+
type: 'inline-image',
|
|
501
|
+
version: 1,
|
|
502
|
+
src: '/cat.png',
|
|
503
|
+
altText: 'a cat',
|
|
504
|
+
relation: { targetDocumentId: 'doc-1', targetCollectionPath: 'media' },
|
|
505
|
+
}
|
|
506
|
+
const item = {
|
|
507
|
+
children: [para(image)],
|
|
508
|
+
direction: null,
|
|
509
|
+
format: '',
|
|
510
|
+
indent: 0,
|
|
511
|
+
type: 'layout-item',
|
|
512
|
+
version: 1,
|
|
513
|
+
}
|
|
514
|
+
const container = {
|
|
515
|
+
children: [item],
|
|
516
|
+
direction: null,
|
|
517
|
+
format: '',
|
|
518
|
+
indent: 0,
|
|
519
|
+
type: 'layout-container',
|
|
520
|
+
version: 1,
|
|
521
|
+
templateColumns: '1fr',
|
|
522
|
+
}
|
|
523
|
+
const result = normalizeValue(doc(container), new Set([...SUPPORTED, 'inline-image']))
|
|
524
|
+
if (result.status !== 'adapted') throw new Error('expected adapted')
|
|
525
|
+
const json = JSON.stringify(result.value)
|
|
526
|
+
expect(json).toContain('/cat.png')
|
|
527
|
+
expect(json).toContain('a cat')
|
|
528
|
+
expect(json).toContain('doc-1')
|
|
529
|
+
})
|
|
530
|
+
})
|