@byline/richtext-lexical 5.2.0 → 6.0.1
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,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { builtInExtensions } from '../field/config/built-in-extension-names'
|
|
4
|
+
import { defaultClientEditorConfig } from '../field/config/default-extensions'
|
|
5
|
+
import { buildManifest, capabilitiesFor } from './capability-manifest'
|
|
6
|
+
import { scanDocument } from './scan-documents'
|
|
7
|
+
|
|
8
|
+
function configWithout(...names: string[]) {
|
|
9
|
+
const extensions = defaultClientEditorConfig.extensions?.clone()
|
|
10
|
+
for (const name of names) extensions.remove(name)
|
|
11
|
+
return { ...defaultClientEditorConfig, extensions }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('capabilitiesFor', () => {
|
|
15
|
+
it('reports what the default configuration accepts', () => {
|
|
16
|
+
const caps = capabilitiesFor('pages', 'body', defaultClientEditorConfig)
|
|
17
|
+
for (const type of ['root', 'paragraph', 'text', 'heading', 'list', 'table', 'link']) {
|
|
18
|
+
expect(caps.supportedTypes, type).toContain(type)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('omits a removed extensions node types', () => {
|
|
23
|
+
const caps = capabilitiesFor('pages', 'title', configWithout(builtInExtensions.Heading))
|
|
24
|
+
expect(caps.supportedTypes).not.toContain('heading')
|
|
25
|
+
expect(caps.supportedTypes).toContain('paragraph')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('includes nodes contributed through dependencies, not just declared ones', () => {
|
|
29
|
+
// Table and list nodes arrive from @lexical/table and @lexical/list as
|
|
30
|
+
// dependencies of the Byline wrappers, so a manifest derived by
|
|
31
|
+
// walking declared `nodes` would miss them.
|
|
32
|
+
const caps = capabilitiesFor('pages', 'body', defaultClientEditorConfig)
|
|
33
|
+
expect(caps.supportedTypes).toContain('table')
|
|
34
|
+
expect(caps.supportedTypes).toContain('tablecell')
|
|
35
|
+
expect(caps.supportedTypes).toContain('listitem')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('includes the always-on core nodes', () => {
|
|
39
|
+
const caps = capabilitiesFor(
|
|
40
|
+
'pages',
|
|
41
|
+
'body',
|
|
42
|
+
configWithout(...Object.values(builtInExtensions))
|
|
43
|
+
)
|
|
44
|
+
expect(caps.supportedTypes).toContain('mark')
|
|
45
|
+
expect(caps.supportedTypes).toContain('overflow')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('feeds the scanner, which then agrees with the editor', () => {
|
|
49
|
+
const caps = capabilitiesFor('publications', 'title', configWithout(builtInExtensions.Heading))
|
|
50
|
+
// biome-ignore lint/suspicious/noExplicitAny: serialized fixture
|
|
51
|
+
const saved: any = {
|
|
52
|
+
root: {
|
|
53
|
+
children: [
|
|
54
|
+
{
|
|
55
|
+
children: [],
|
|
56
|
+
direction: null,
|
|
57
|
+
format: '',
|
|
58
|
+
indent: 0,
|
|
59
|
+
type: 'heading',
|
|
60
|
+
version: 1,
|
|
61
|
+
tag: 'h1',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
direction: null,
|
|
65
|
+
format: '',
|
|
66
|
+
indent: 0,
|
|
67
|
+
type: 'root',
|
|
68
|
+
version: 1,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
const finding = scanDocument(saved, caps, { documentId: 'doc-1', versionId: 'ver-1' })
|
|
72
|
+
expect(finding?.adaptedTypes).toEqual(['heading'])
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('buildManifest', () => {
|
|
77
|
+
it('stamps when it was generated', () => {
|
|
78
|
+
const manifest = buildManifest([capabilitiesFor('pages', 'body', defaultClientEditorConfig)])
|
|
79
|
+
expect(manifest.fields).toHaveLength(1)
|
|
80
|
+
expect(Number.isNaN(Date.parse(manifest.generatedAt))).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Infonomic Company Limited
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { buildEditorFromExtensions } from '@lexical/extension'
|
|
12
|
+
import { ReactPluginHostExtension } from '@lexical/react/ReactPluginHostExtension'
|
|
13
|
+
import { defineExtension } from 'lexical'
|
|
14
|
+
|
|
15
|
+
import { registeredNodeTypes } from '../field/capabilities/registered-node-types'
|
|
16
|
+
import { defaultExtensionsList } from '../field/config/default-extensions'
|
|
17
|
+
import { rootDependencies } from '../field/config/root-dependencies'
|
|
18
|
+
import type { EditorConfig } from '../field/config/types'
|
|
19
|
+
import type { CapabilityManifest, FieldCapabilities } from './types'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What one field accepts, measured by BUILDING its editor.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately not derived by walking declared `nodes` on the extension
|
|
25
|
+
* list: that would recreate the resolver — dependency-contributed nodes,
|
|
26
|
+
* conflicts, replacements and always-on core nodes — and a manifest that
|
|
27
|
+
* disagrees with the runtime is worse than no manifest. This is the same
|
|
28
|
+
* construction `EditorContext` performs, so the two cannot drift.
|
|
29
|
+
*
|
|
30
|
+
* Requires a DOM and a React-capable module graph, because several
|
|
31
|
+
* extensions host React decorators. It therefore runs in a browser or
|
|
32
|
+
* jsdom, never in a plain Node script — which is why the manifest is a
|
|
33
|
+
* file handed to the scanner rather than something the scanner derives.
|
|
34
|
+
*/
|
|
35
|
+
export function capabilitiesFor(
|
|
36
|
+
collectionPath: string,
|
|
37
|
+
fieldPath: string,
|
|
38
|
+
config: Pick<EditorConfig, 'extensions' | 'lexical'>
|
|
39
|
+
): FieldCapabilities {
|
|
40
|
+
const extensions = config.extensions ?? defaultExtensionsList()
|
|
41
|
+
const editor = buildEditorFromExtensions(
|
|
42
|
+
defineExtension({
|
|
43
|
+
name: '[capability-manifest]',
|
|
44
|
+
namespace: config.lexical?.namespace ?? 'LexicalRichText',
|
|
45
|
+
// Through the same seam the live editor uses — a manifest that
|
|
46
|
+
// disagreed about the always-on core nodes would misreport every
|
|
47
|
+
// document containing one.
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: extension arguments are heterogeneous
|
|
49
|
+
dependencies: [ReactPluginHostExtension, ...rootDependencies(extensions)] as any,
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
const supportedTypes = [...registeredNodeTypes(editor)].sort()
|
|
53
|
+
editor.dispose()
|
|
54
|
+
return { collectionPath, fieldPath, supportedTypes }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Assemble a manifest from per-field capabilities. */
|
|
58
|
+
export function buildManifest(fields: FieldCapabilities[]): CapabilityManifest {
|
|
59
|
+
return { generatedAt: new Date().toISOString(), fields }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What a live editor accepts.
|
|
64
|
+
*
|
|
65
|
+
* `capabilitiesFor` needs a resolved `EditorConfig`, which is not always
|
|
66
|
+
* reachable: `lexicalEditor()` closes over its configure callback inside
|
|
67
|
+
* a lazy component, so a site's own preset exposes no config to inspect.
|
|
68
|
+
* Measuring a mounted editor works for any field however its editor was
|
|
69
|
+
* registered, which is what a manifest walk over real collections needs.
|
|
70
|
+
*/
|
|
71
|
+
export function capabilitiesFromEditor(
|
|
72
|
+
collectionPath: string,
|
|
73
|
+
fieldPath: string,
|
|
74
|
+
editor: Parameters<typeof registeredNodeTypes>[0]
|
|
75
|
+
): FieldCapabilities {
|
|
76
|
+
return {
|
|
77
|
+
collectionPath,
|
|
78
|
+
fieldPath,
|
|
79
|
+
supportedTypes: [...registeredNodeTypes(editor)].sort(),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { scanDocument, summarise } from './scan-documents'
|
|
4
|
+
import type { FieldCapabilities } from './types'
|
|
5
|
+
|
|
6
|
+
const caps: FieldCapabilities = {
|
|
7
|
+
collectionPath: 'publications',
|
|
8
|
+
fieldPath: 'title',
|
|
9
|
+
supportedTypes: ['root', 'paragraph', 'text'],
|
|
10
|
+
}
|
|
11
|
+
const ids = { documentId: 'doc-1', versionId: 'ver-1' }
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: serialized fixtures are structural
|
|
13
|
+
const doc = (...children: unknown[]): any => ({
|
|
14
|
+
root: { children, direction: null, format: '', indent: 0, type: 'root', version: 1 },
|
|
15
|
+
})
|
|
16
|
+
const heading = () => ({
|
|
17
|
+
children: [],
|
|
18
|
+
direction: null,
|
|
19
|
+
format: '',
|
|
20
|
+
indent: 0,
|
|
21
|
+
type: 'heading',
|
|
22
|
+
version: 1,
|
|
23
|
+
tag: 'h1',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('scanDocument', () => {
|
|
27
|
+
it('reports nothing for a clean document', () => {
|
|
28
|
+
const clean = doc({
|
|
29
|
+
children: [],
|
|
30
|
+
direction: null,
|
|
31
|
+
format: '',
|
|
32
|
+
indent: 0,
|
|
33
|
+
type: 'paragraph',
|
|
34
|
+
version: 1,
|
|
35
|
+
})
|
|
36
|
+
expect(scanDocument(clean, caps, ids)).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reports nothing for an absent value', () => {
|
|
40
|
+
expect(scanDocument(undefined, caps, ids)).toBeUndefined()
|
|
41
|
+
expect(scanDocument(null, caps, ids)).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('reports an adaptable heading', () => {
|
|
45
|
+
const finding = scanDocument(doc(heading()), caps, ids)
|
|
46
|
+
expect(finding?.adaptedTypes).toEqual(['heading'])
|
|
47
|
+
expect(finding?.refusedTypes).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('reports a refusing inline image separately', () => {
|
|
51
|
+
const finding = scanDocument(doc({ type: 'inline-image', version: 1 }), caps, ids)
|
|
52
|
+
expect(finding?.refusedTypes).toContain('inline-image')
|
|
53
|
+
expect(finding?.adaptedTypes).toEqual([])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('keeps the stored instance path so sibling values are distinguishable', () => {
|
|
57
|
+
// Two captions in one block field share a declaration path; only the
|
|
58
|
+
// instance path tells them apart.
|
|
59
|
+
const first = scanDocument(doc(heading()), caps, {
|
|
60
|
+
...ids,
|
|
61
|
+
instancePath: 'content.1.photoBlock.caption',
|
|
62
|
+
})
|
|
63
|
+
const second = scanDocument(doc(heading()), caps, {
|
|
64
|
+
...ids,
|
|
65
|
+
instancePath: 'content.3.photoBlock.caption',
|
|
66
|
+
})
|
|
67
|
+
expect(first?.instancePath).toBe('content.1.photoBlock.caption')
|
|
68
|
+
expect(second?.instancePath).toBe('content.3.photoBlock.caption')
|
|
69
|
+
expect(first?.fieldPath).toBe(second?.fieldPath)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('reports findings against a localized, nested field path', () => {
|
|
73
|
+
const nested = { ...caps, fieldPath: 'content.1.photoBlock.caption' }
|
|
74
|
+
const finding = scanDocument(doc(heading()), nested, { ...ids, locale: 'fr' })
|
|
75
|
+
expect(finding?.fieldPath).toBe('content.1.photoBlock.caption')
|
|
76
|
+
expect(finding?.locale).toBe('fr')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('reports an archived version distinctly from the current one', () => {
|
|
80
|
+
const current = scanDocument(doc(heading()), caps, { documentId: 'doc-1', versionId: 'ver-2' })
|
|
81
|
+
const archived = scanDocument(doc(heading()), caps, { documentId: 'doc-1', versionId: 'ver-1' })
|
|
82
|
+
expect(current?.versionId).toBe('ver-2')
|
|
83
|
+
expect(archived?.versionId).toBe('ver-1')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('reports a field whose capabilities were never measured, rather than silence', () => {
|
|
87
|
+
const unknown: FieldCapabilities = {
|
|
88
|
+
...caps,
|
|
89
|
+
supportedTypes: null,
|
|
90
|
+
unresolvedReason: 'no editor',
|
|
91
|
+
}
|
|
92
|
+
const finding = scanDocument(doc(heading()), unknown, ids)
|
|
93
|
+
expect(finding?.unmapped).toBe(true)
|
|
94
|
+
expect(finding?.adaptedTypes).toEqual([])
|
|
95
|
+
expect(finding?.refusedTypes).toEqual([])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('reports unmapped even for content that would otherwise look clean', () => {
|
|
99
|
+
const unknown: FieldCapabilities = { ...caps, supportedTypes: null }
|
|
100
|
+
const clean = doc({
|
|
101
|
+
children: [],
|
|
102
|
+
direction: null,
|
|
103
|
+
format: '',
|
|
104
|
+
indent: 0,
|
|
105
|
+
type: 'paragraph',
|
|
106
|
+
version: 1,
|
|
107
|
+
})
|
|
108
|
+
// The point of the rule: a clean-looking document under unknown
|
|
109
|
+
// capabilities is still unexamined.
|
|
110
|
+
expect(scanDocument(clean, unknown, ids)?.unmapped).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('separates what will adapt from what will open read-only', () => {
|
|
114
|
+
const findings = [
|
|
115
|
+
scanDocument(doc(heading()), caps, ids),
|
|
116
|
+
scanDocument(doc({ type: 'youtube', version: 1 }), caps, { ...ids, versionId: 'ver-2' }),
|
|
117
|
+
].filter((finding): finding is NonNullable<typeof finding> => finding != null)
|
|
118
|
+
|
|
119
|
+
const { adapted, refused, unmapped } = summarise(findings)
|
|
120
|
+
expect(adapted).toHaveLength(1)
|
|
121
|
+
expect(refused).toHaveLength(1)
|
|
122
|
+
expect(unmapped).toHaveLength(0)
|
|
123
|
+
expect(refused[0]?.refusedTypes).toContain('youtube')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SerializedEditorState } from 'lexical'
|
|
10
|
+
|
|
11
|
+
import { normalizeValue } from '../field/normalize/normalize-value'
|
|
12
|
+
import type { FieldCapabilities, ScanFinding } from './types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Report what a field's current configuration will do with one stored
|
|
16
|
+
* value.
|
|
17
|
+
*
|
|
18
|
+
* Runs the same `normalizeValue` the editor runs, rather than a parallel
|
|
19
|
+
* rule set, so the report cannot drift from the behaviour it predicts.
|
|
20
|
+
* Pure: no database, no DOM, no editor.
|
|
21
|
+
*/
|
|
22
|
+
export function scanDocument(
|
|
23
|
+
value: SerializedEditorState | null | undefined,
|
|
24
|
+
capabilities: FieldCapabilities,
|
|
25
|
+
ids: { documentId: string; versionId: string; locale?: string; instancePath?: string }
|
|
26
|
+
): ScanFinding | undefined {
|
|
27
|
+
if (value == null) return undefined
|
|
28
|
+
|
|
29
|
+
// Capabilities that could not be measured produce a finding, never
|
|
30
|
+
// silence. Skipping here would report the document clean on the
|
|
31
|
+
// strength of never having looked at it.
|
|
32
|
+
if (capabilities.supportedTypes == null) {
|
|
33
|
+
const unmapped: ScanFinding = {
|
|
34
|
+
collectionPath: capabilities.collectionPath,
|
|
35
|
+
fieldPath: capabilities.fieldPath,
|
|
36
|
+
documentId: ids.documentId,
|
|
37
|
+
versionId: ids.versionId,
|
|
38
|
+
adaptedTypes: [],
|
|
39
|
+
refusedTypes: [],
|
|
40
|
+
unmapped: true,
|
|
41
|
+
}
|
|
42
|
+
if (ids.locale != null) unmapped.locale = ids.locale
|
|
43
|
+
if (ids.instancePath != null) unmapped.instancePath = ids.instancePath
|
|
44
|
+
return unmapped
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = normalizeValue(value, new Set(capabilities.supportedTypes))
|
|
48
|
+
if (result.status === 'unchanged') return undefined
|
|
49
|
+
|
|
50
|
+
const finding: ScanFinding = {
|
|
51
|
+
collectionPath: capabilities.collectionPath,
|
|
52
|
+
fieldPath: capabilities.fieldPath,
|
|
53
|
+
documentId: ids.documentId,
|
|
54
|
+
versionId: ids.versionId,
|
|
55
|
+
adaptedTypes: result.status === 'adapted' ? result.convertedTypes : [],
|
|
56
|
+
refusedTypes: result.status === 'refused' ? result.unsupportedTypes : [],
|
|
57
|
+
}
|
|
58
|
+
if (ids.locale != null) finding.locale = ids.locale
|
|
59
|
+
if (ids.instancePath != null) finding.instancePath = ids.instancePath
|
|
60
|
+
return finding
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Group findings for a report: what will adapt, and what will open read-only. */
|
|
64
|
+
export function summarise(findings: ScanFinding[]): {
|
|
65
|
+
adapted: ScanFinding[]
|
|
66
|
+
refused: ScanFinding[]
|
|
67
|
+
unmapped: ScanFinding[]
|
|
68
|
+
} {
|
|
69
|
+
return {
|
|
70
|
+
adapted: findings.filter(
|
|
71
|
+
(finding) => finding.unmapped !== true && finding.refusedTypes.length === 0
|
|
72
|
+
),
|
|
73
|
+
refused: findings.filter(
|
|
74
|
+
(finding) => finding.unmapped !== true && finding.refusedTypes.length > 0
|
|
75
|
+
),
|
|
76
|
+
unmapped: findings.filter((finding) => finding.unmapped === true),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** What one richtext field accepts, as its resolved editor reports it. */
|
|
10
|
+
export interface FieldCapabilities {
|
|
11
|
+
collectionPath: string
|
|
12
|
+
/**
|
|
13
|
+
* Declaration path of the field, so one nested in a block or array is
|
|
14
|
+
* addressable — `content.photoBlock.caption`, not just `caption`.
|
|
15
|
+
* Stored values carry instance paths (`content.1.photoBlock.caption`);
|
|
16
|
+
* eliding the selectors yields this.
|
|
17
|
+
*/
|
|
18
|
+
fieldPath: string
|
|
19
|
+
/**
|
|
20
|
+
* Node types the field accepts, or `null` when they could not be
|
|
21
|
+
* measured.
|
|
22
|
+
*
|
|
23
|
+
* A field whose capabilities are unknown is NOT the same as a field
|
|
24
|
+
* with none. The scanner reports it as unmapped rather than treating
|
|
25
|
+
* its documents as clean, because reporting a document safe when it
|
|
26
|
+
* was never examined is the one failure an upgrade check must not
|
|
27
|
+
* have.
|
|
28
|
+
*/
|
|
29
|
+
supportedTypes: string[] | null
|
|
30
|
+
/** Why the capabilities could not be measured, when they could not. */
|
|
31
|
+
unresolvedReason?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Field capabilities for a whole installation, produced where the client
|
|
36
|
+
* config can be loaded and consumed where the database can.
|
|
37
|
+
*/
|
|
38
|
+
export interface CapabilityManifest {
|
|
39
|
+
generatedAt: string
|
|
40
|
+
fields: FieldCapabilities[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One stored value that this field's current configuration cannot take as-is. */
|
|
44
|
+
export interface ScanFinding {
|
|
45
|
+
collectionPath: string
|
|
46
|
+
/** Declaration path — the manifest key this value was matched against. */
|
|
47
|
+
fieldPath: string
|
|
48
|
+
/**
|
|
49
|
+
* Instance path as stored, when the caller knows it —
|
|
50
|
+
* `content.1.photoBlock.caption`. Two affected captions in one version
|
|
51
|
+
* share a declaration path and are told apart only by this.
|
|
52
|
+
*/
|
|
53
|
+
instancePath?: string
|
|
54
|
+
documentId: string
|
|
55
|
+
versionId: string
|
|
56
|
+
locale?: string
|
|
57
|
+
/** Structures that will be converted when the field opens this value. */
|
|
58
|
+
adaptedTypes: string[]
|
|
59
|
+
/** Structures with no conversion — the field will open read-only. */
|
|
60
|
+
refusedTypes: string[]
|
|
61
|
+
/**
|
|
62
|
+
* Set when the field's capabilities were never measured, so this value
|
|
63
|
+
* was not examined. Neither adapted nor refused: unknown.
|
|
64
|
+
*/
|
|
65
|
+
unmapped?: true
|
|
66
|
+
}
|
package/src/scan.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pre-upgrade scanning, without the editor.
|
|
11
|
+
*
|
|
12
|
+
* This entry is deliberately **runtime-free**: it carries the pure
|
|
13
|
+
* scanning logic and its types, and imports no React, no Lexical
|
|
14
|
+
* extension and no CSS. A Node script can therefore read it, which the
|
|
15
|
+
* root barrel makes impossible — importing that pulls the editor graph
|
|
16
|
+
* and fails on the first `.css` import.
|
|
17
|
+
*
|
|
18
|
+
* The other half, measuring what a field accepts, needs a DOM and lives
|
|
19
|
+
* on the root entry as `capabilitiesFor` / `capabilitiesFromEditor`.
|
|
20
|
+
* That split is the same boundary the manifest crosses: capabilities are
|
|
21
|
+
* measured in a browser, written to a file, and consumed here.
|
|
22
|
+
*
|
|
23
|
+
* See docs/09-admin-ui/04-richtext-capabilities.md.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export { scanDocument, summarise } from './scan/scan-documents'
|
|
27
|
+
export type { CapabilityManifest, FieldCapabilities, ScanFinding } from './scan/types'
|