@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.
Files changed (123) hide show
  1. package/dist/config.d.ts +1 -1
  2. package/dist/field/apply-value-plugin.js +31 -6
  3. package/dist/field/capabilities/filter-transformers.d.ts +29 -0
  4. package/dist/field/capabilities/filter-transformers.js +8 -0
  5. package/dist/field/capabilities/registered-node-types.d.ts +29 -0
  6. package/dist/field/capabilities/registered-node-types.js +7 -0
  7. package/dist/field/config/built-in-extension-names.d.ts +42 -0
  8. package/dist/field/config/built-in-extension-names.js +12 -2
  9. package/dist/field/config/default-extensions.js +4 -0
  10. package/dist/field/config/default.js +8 -8
  11. package/dist/field/config/editor-config-context.d.ts +2 -2
  12. package/dist/field/config/editor-config-context.js +10 -13
  13. package/dist/field/config/resolve-editor-config.js +3 -3
  14. package/dist/field/config/root-dependencies.d.ts +26 -0
  15. package/dist/field/config/root-dependencies.js +8 -0
  16. package/dist/field/config/types.d.ts +61 -8
  17. package/dist/field/editor-context.js +44 -27
  18. package/dist/field/editor.js +15 -8
  19. package/dist/field/extensions/core-nodes/core-nodes-extension.d.ts +24 -0
  20. package/dist/field/extensions/core-nodes/core-nodes-extension.js +11 -0
  21. package/dist/field/extensions/heading/heading-extension.d.ts +20 -0
  22. package/dist/field/extensions/heading/heading-extension.js +9 -0
  23. package/dist/field/extensions/quote/quote-extension.d.ts +16 -0
  24. package/dist/field/extensions/quote/quote-extension.js +9 -0
  25. package/dist/field/hooks/use-markdown-toggle.d.ts +15 -0
  26. package/dist/field/hooks/use-markdown-toggle.js +12 -4
  27. package/dist/field/hooks/use-platform-modifier.d.ts +16 -0
  28. package/dist/field/hooks/use-platform-modifier.js +8 -0
  29. package/dist/field/markdown/transformers.d.ts +1 -0
  30. package/dist/field/markdown/transformers.js +5 -4
  31. package/dist/field/nodes/index.d.ts +11 -1
  32. package/dist/field/nodes/index.js +2 -2
  33. package/dist/field/normalize/adapted-notice.css +34 -0
  34. package/dist/field/normalize/adapted-notice.d.ts +29 -0
  35. package/dist/field/normalize/adapted-notice.js +35 -0
  36. package/dist/field/normalize/content-labels.d.ts +15 -0
  37. package/dist/field/normalize/content-labels.js +19 -0
  38. package/dist/field/normalize/declared-conversions.d.ts +131 -0
  39. package/dist/field/normalize/declared-conversions.js +93 -0
  40. package/dist/field/normalize/normalization-status.d.ts +33 -0
  41. package/dist/field/normalize/normalization-status.js +28 -0
  42. package/dist/field/normalize/normalize-value.d.ts +38 -0
  43. package/dist/field/normalize/normalize-value.js +132 -0
  44. package/dist/field/normalize/use-notice-text.d.ts +11 -0
  45. package/dist/field/normalize/use-notice-text.js +34 -0
  46. package/dist/field/plugins/toolbar-plugin/index.d.ts +23 -0
  47. package/dist/field/plugins/toolbar-plugin/index.js +104 -78
  48. package/dist/field/test-support/build-test-editor.js +49 -0
  49. package/dist/field/test-support/capture-editor.js +7 -0
  50. package/dist/field/test-support/frame-control.js +40 -0
  51. package/dist/field/test-support/jsdom-setup.js +21 -0
  52. package/dist/index.d.ts +7 -1
  53. package/dist/index.js +6 -1
  54. package/dist/scan/capability-manifest.d.ts +30 -0
  55. package/dist/scan/capability-manifest.js +43 -0
  56. package/dist/scan/scan-documents.d.ts +29 -0
  57. package/dist/scan/scan-documents.js +39 -0
  58. package/dist/scan/types.d.ts +63 -0
  59. package/dist/scan/types.js +1 -0
  60. package/dist/scan.d.ts +25 -0
  61. package/dist/scan.js +1 -0
  62. package/package.json +14 -5
  63. package/src/config.ts +1 -1
  64. package/src/field/apply-value-plugin.tsx +71 -5
  65. package/src/field/capabilities/filter-transformers.test.tsx +71 -0
  66. package/src/field/capabilities/filter-transformers.ts +42 -0
  67. package/src/field/capabilities/markdown-behaviour.test.tsx +188 -0
  68. package/src/field/capabilities/no-blanket-registration.test.tsx +86 -0
  69. package/src/field/capabilities/registered-node-types.test.tsx +29 -0
  70. package/src/field/capabilities/registered-node-types.ts +36 -0
  71. package/src/field/config/built-in-extension-names.test.node.ts +38 -14
  72. package/src/field/config/built-in-extension-names.ts +29 -1
  73. package/src/field/config/default-extensions.ts +6 -0
  74. package/src/field/config/default.ts +8 -8
  75. package/src/field/config/editor-config-context.tsx +14 -9
  76. package/src/field/config/list-removal.test.tsx +44 -0
  77. package/src/field/config/resolve-editor-config.test.node.ts +27 -6
  78. package/src/field/config/resolve-editor-config.ts +3 -3
  79. package/src/field/config/root-dependencies.ts +32 -0
  80. package/src/field/config/types.ts +61 -17
  81. package/src/field/editor-component.test.tsx +18 -3
  82. package/src/field/editor-context.regression.test.tsx +124 -0
  83. package/src/field/editor-context.tsx +90 -22
  84. package/src/field/editor.tsx +22 -12
  85. package/src/field/extensions/core-nodes/core-nodes-extension.test.tsx +13 -0
  86. package/src/field/extensions/core-nodes/core-nodes-extension.ts +32 -0
  87. package/src/field/extensions/heading/heading-extension.test.tsx +32 -0
  88. package/src/field/extensions/heading/heading-extension.ts +27 -0
  89. package/src/field/extensions/quote/quote-extension.ts +23 -0
  90. package/src/field/hooks/markdown-source-mode.test.tsx +221 -0
  91. package/src/field/hooks/use-markdown-toggle.ts +39 -4
  92. package/src/field/hooks/use-platform-modifier.test.tsx +81 -0
  93. package/src/field/hooks/use-platform-modifier.ts +41 -0
  94. package/src/field/markdown/transformers.ts +18 -4
  95. package/src/field/nodes/index.ts +11 -1
  96. package/src/field/normalize/adapted-notice.css +52 -0
  97. package/src/field/normalize/adapted-notice.tsx +63 -0
  98. package/src/field/normalize/clipboard-equivalence.test.tsx +330 -0
  99. package/src/field/normalize/content-labels.ts +44 -0
  100. package/src/field/normalize/declared-conversions.test.node.ts +67 -0
  101. package/src/field/normalize/declared-conversions.ts +160 -0
  102. package/src/field/normalize/emission-gate.test.tsx +302 -0
  103. package/src/field/normalize/normalization-status.tsx +48 -0
  104. package/src/field/normalize/normalize-load.test.tsx +371 -0
  105. package/src/field/normalize/normalize-value.test.node.ts +530 -0
  106. package/src/field/normalize/normalize-value.ts +180 -0
  107. package/src/field/normalize/notice-i18n.test.tsx +128 -0
  108. package/src/field/normalize/use-notice-text.ts +82 -0
  109. package/src/field/normalize/wiring.test.tsx +348 -0
  110. package/src/field/plugins/toolbar-plugin/block-format-capabilities.test.tsx +57 -0
  111. package/src/field/plugins/toolbar-plugin/index.tsx +131 -68
  112. package/src/field/test-support/build-test-editor.test.tsx +17 -0
  113. package/src/field/test-support/build-test-editor.ts +99 -0
  114. package/src/field/test-support/capture-editor.tsx +20 -0
  115. package/src/field/test-support/frame-control.ts +91 -0
  116. package/src/field/test-support/jsdom-setup.ts +41 -0
  117. package/src/index.ts +12 -1
  118. package/src/scan/capability-manifest.test.tsx +82 -0
  119. package/src/scan/capability-manifest.ts +81 -0
  120. package/src/scan/scan-documents.test.node.ts +125 -0
  121. package/src/scan/scan-documents.ts +78 -0
  122. package/src/scan/types.ts +66 -0
  123. 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'