@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.
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,93 @@
1
+ const BLOCK_TYPES = new Set([
2
+ 'paragraph',
3
+ 'heading',
4
+ 'quote',
5
+ 'code',
6
+ 'list',
7
+ 'listitem',
8
+ 'table',
9
+ 'tablerow',
10
+ 'tablecell',
11
+ 'layout-container',
12
+ 'layout-item',
13
+ 'admonition',
14
+ 'horizontalrule',
15
+ 'youtube',
16
+ 'vimeo'
17
+ ]);
18
+ function isBlockType(type) {
19
+ return BLOCK_TYPES.has(type);
20
+ }
21
+ const INLINE_TYPES = new Set([
22
+ 'text',
23
+ 'linebreak',
24
+ 'tab',
25
+ 'link',
26
+ 'autolink',
27
+ 'mark',
28
+ 'overflow',
29
+ 'code-highlight',
30
+ 'inline-image'
31
+ ]);
32
+ function isInlineType(type) {
33
+ return INLINE_TYPES.has(type);
34
+ }
35
+ const urlText = (node)=>{
36
+ const attributes = node.attributes;
37
+ const url = 'string' == typeof node.url ? node.url : attributes?.url;
38
+ return 'string' == typeof url && url.length > 0 ? url : void 0;
39
+ };
40
+ const DECLARED_CONVERSIONS = {
41
+ heading: {
42
+ kind: 'to-paragraph'
43
+ },
44
+ quote: {
45
+ kind: 'to-paragraph'
46
+ },
47
+ code: {
48
+ kind: 'to-paragraph'
49
+ },
50
+ listitem: {
51
+ kind: 'to-paragraph'
52
+ },
53
+ table: {
54
+ kind: 'lift-blocks'
55
+ },
56
+ tablerow: {
57
+ kind: 'lift-blocks'
58
+ },
59
+ tablecell: {
60
+ kind: 'lift-blocks'
61
+ },
62
+ list: {
63
+ kind: 'lift-blocks'
64
+ },
65
+ 'layout-container': {
66
+ kind: 'lift-blocks'
67
+ },
68
+ 'layout-item': {
69
+ kind: 'lift-blocks'
70
+ },
71
+ admonition: {
72
+ kind: 'lift-blocks',
73
+ preserveText: (node)=>'string' == typeof node.title && node.title.length > 0 ? node.title : void 0
74
+ },
75
+ 'code-highlight': {
76
+ kind: 'to-text'
77
+ },
78
+ link: {
79
+ kind: 'unwrap-inline',
80
+ preserveText: urlText
81
+ },
82
+ autolink: {
83
+ kind: 'unwrap-inline',
84
+ preserveText: urlText
85
+ },
86
+ horizontalrule: {
87
+ kind: 'drop'
88
+ }
89
+ };
90
+ function conversionFor(type) {
91
+ return DECLARED_CONVERSIONS[type];
92
+ }
93
+ export { BLOCK_TYPES, DECLARED_CONVERSIONS, INLINE_TYPES, conversionFor, isBlockType, isInlineType };
@@ -0,0 +1,33 @@
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
+ import type * as React from 'react';
9
+ export type NormalizationStatus = {
10
+ kind: 'ok';
11
+ } | {
12
+ kind: 'adapted';
13
+ convertedTypes: string[];
14
+ } | {
15
+ kind: 'refused';
16
+ unsupportedTypes: string[];
17
+ };
18
+ interface ContextValue {
19
+ status: NormalizationStatus;
20
+ setStatus: (status: NormalizationStatus) => void;
21
+ }
22
+ /**
23
+ * Carries how the stored value fared against this field's capabilities.
24
+ *
25
+ * Provided ABOVE the composer, like the other shared contexts, so the
26
+ * surface that renders the notice sits outside the editor while the
27
+ * plugin that discovers the status sits inside it.
28
+ */
29
+ export declare function NormalizationStatusProvider({ children, }: {
30
+ children: React.ReactNode;
31
+ }): React.JSX.Element;
32
+ export declare function useNormalizationStatus(): ContextValue;
33
+ export {};
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useMemo, useState } from "react";
4
+ const Context = /*#__PURE__*/ createContext({
5
+ status: {
6
+ kind: 'ok'
7
+ },
8
+ setStatus: ()=>{}
9
+ });
10
+ function NormalizationStatusProvider({ children }) {
11
+ const [status, setStatus] = useState({
12
+ kind: 'ok'
13
+ });
14
+ const value = useMemo(()=>({
15
+ status,
16
+ setStatus
17
+ }), [
18
+ status
19
+ ]);
20
+ return /*#__PURE__*/ jsx(Context.Provider, {
21
+ value: value,
22
+ children: children
23
+ });
24
+ }
25
+ function useNormalizationStatus() {
26
+ return useContext(Context);
27
+ }
28
+ export { NormalizationStatusProvider, useNormalizationStatus };
@@ -0,0 +1,38 @@
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
+ import type { SerializedEditorState } from 'lexical';
9
+ export type NormalizeResult = {
10
+ status: 'unchanged';
11
+ } | {
12
+ status: 'adapted';
13
+ value: SerializedEditorState;
14
+ convertedTypes: string[];
15
+ } | {
16
+ status: 'refused';
17
+ unsupportedTypes: string[];
18
+ };
19
+ /**
20
+ * Adapt a stored value to what an editor can actually accept.
21
+ *
22
+ * Lexical throws `parseEditorState: type "X" + not found` on an
23
+ * unregistered type and the text is unrecoverable, so a field that no
24
+ * longer supports a structure would otherwise open blank and erroring.
25
+ *
26
+ * Only the unsupported nodes are converted. A supported node is copied
27
+ * with its own properties intact — it was already in a valid position in
28
+ * the saved document, so its position is never changed — while its
29
+ * children are still inspected, because a supported parent may hold an
30
+ * unsupported descendant. Re-encoding supported content to fix an
31
+ * unrelated structure is exactly what this must not do: an inline image
32
+ * sitting beside a heading would lose its media relation and its
33
+ * nested-editor caption as collateral.
34
+ *
35
+ * The input is never mutated. Refusal wins over any conversion, so the
36
+ * whole tree is walked before a decision is returned.
37
+ */
38
+ export declare function normalizeValue(value: SerializedEditorState, supportedTypes: ReadonlySet<string>): NormalizeResult;
@@ -0,0 +1,132 @@
1
+ import { conversionFor, isBlockType, isInlineType } from "./declared-conversions.js";
2
+ const textNode = (text)=>({
3
+ detail: 0,
4
+ format: 0,
5
+ mode: 'normal',
6
+ style: '',
7
+ text,
8
+ type: 'text',
9
+ version: 1
10
+ });
11
+ const textFromNode = (node)=>({
12
+ detail: 'number' == typeof node.detail ? node.detail : 0,
13
+ format: 'number' == typeof node.format ? node.format : 0,
14
+ mode: 'string' == typeof node.mode ? node.mode : 'normal',
15
+ style: 'string' == typeof node.style ? node.style : '',
16
+ text: 'string' == typeof node.text ? node.text : '',
17
+ type: 'text',
18
+ version: 1
19
+ });
20
+ const paragraphNode = (children)=>({
21
+ children,
22
+ direction: null,
23
+ format: '',
24
+ indent: 0,
25
+ type: 'paragraph',
26
+ version: 1
27
+ });
28
+ function normalizeValue(value, supportedTypes) {
29
+ const converted = new Set();
30
+ const refused = new Set();
31
+ function groupIntoBlocks(nodes) {
32
+ const out = [];
33
+ let run = [];
34
+ const flush = ()=>{
35
+ if (run.length > 0) {
36
+ out.push(paragraphNode(run));
37
+ run = [];
38
+ }
39
+ };
40
+ for (const node of nodes){
41
+ if (isInlineType(node.type)) {
42
+ run.push(node);
43
+ continue;
44
+ }
45
+ if (!isBlockType(node.type)) refused.add(node.type);
46
+ flush();
47
+ out.push(node);
48
+ }
49
+ flush();
50
+ return out;
51
+ }
52
+ function visit(node) {
53
+ const children = Array.isArray(node.children) ? node.children : void 0;
54
+ if ('root' === node.type || supportedTypes.has(node.type)) return null == children ? [
55
+ node
56
+ ] : [
57
+ {
58
+ ...node,
59
+ children: children.flatMap(visit)
60
+ }
61
+ ];
62
+ const conversion = conversionFor(node.type);
63
+ if (null == conversion) {
64
+ refused.add(node.type);
65
+ return [
66
+ node
67
+ ];
68
+ }
69
+ converted.add(node.type);
70
+ const preserved = conversion.preserveText?.(node);
71
+ const inner = (children ?? []).flatMap(visit);
72
+ switch(conversion.kind){
73
+ case 'drop':
74
+ return [];
75
+ case 'to-paragraph':
76
+ {
77
+ const children = null != preserved ? [
78
+ ...inner,
79
+ textNode(` ${preserved}`)
80
+ ] : inner;
81
+ const grouped = groupIntoBlocks(children);
82
+ return grouped.length > 0 ? grouped : [
83
+ paragraphNode([])
84
+ ];
85
+ }
86
+ case 'to-text':
87
+ return [
88
+ textFromNode(node)
89
+ ];
90
+ case 'lift-blocks':
91
+ return null != preserved ? [
92
+ paragraphNode([
93
+ textNode(preserved)
94
+ ]),
95
+ ...inner
96
+ ] : inner;
97
+ case 'unwrap-inline':
98
+ return null != preserved ? [
99
+ ...inner,
100
+ textNode(` ${preserved}`)
101
+ ] : inner;
102
+ }
103
+ }
104
+ const visited = visit(value.root)[0];
105
+ if (refused.size > 0) return {
106
+ status: 'refused',
107
+ unsupportedTypes: [
108
+ ...refused
109
+ ]
110
+ };
111
+ if (0 === converted.size) return {
112
+ status: 'unchanged'
113
+ };
114
+ const children = Array.isArray(visited.children) ? visited.children : [];
115
+ const nextRoot = {
116
+ ...visited,
117
+ children: children.length > 0 ? children : [
118
+ paragraphNode([])
119
+ ]
120
+ };
121
+ return {
122
+ status: 'adapted',
123
+ value: {
124
+ ...value,
125
+ root: nextRoot
126
+ },
127
+ convertedTypes: [
128
+ ...converted
129
+ ]
130
+ };
131
+ }
132
+ export { normalizeValue };
@@ -0,0 +1,11 @@
1
+ export interface NoticeText {
2
+ t: (key: string, values?: Record<string, string>) => string;
3
+ locale: string;
4
+ }
5
+ /**
6
+ * Translate a notice, tolerating the absence of an I18nProvider.
7
+ *
8
+ * Reads `I18nContext` directly rather than calling `useTranslation`,
9
+ * which throws without a provider.
10
+ */
11
+ export declare function useNoticeText(): NoticeText;
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { useContext, useMemo } from "react";
3
+ import { I18nContext } from "@byline/i18n/react";
4
+ const FALLBACKS = {
5
+ 'richtext.adapted.notice': 'This content contains formatting this editor no longer supports. Saving edits will use the supported formatting.',
6
+ 'richtext.unsupported.notice': 'This content contains {items}, which this field no longer supports. The field is read-only so the content is not lost.',
7
+ 'richtext.unsupported.action': 'Ask an administrator to restore support for it, or to migrate the content.',
8
+ 'richtext.content.image': 'an image',
9
+ 'richtext.content.youtube': 'a YouTube embed',
10
+ 'richtext.content.vimeo': 'a Vimeo embed'
11
+ };
12
+ function useNoticeText() {
13
+ const context = useContext(I18nContext);
14
+ return useMemo(()=>{
15
+ if (null == context) return {
16
+ locale: 'en',
17
+ t: (key, values)=>interpolate(FALLBACKS[key] ?? key, values)
18
+ };
19
+ return {
20
+ locale: context.activeLocale,
21
+ t: (key, values)=>{
22
+ const translated = context.formatter.t('byline-admin', key, values);
23
+ return translated === key ? interpolate(FALLBACKS[key] ?? key, values) : translated;
24
+ }
25
+ };
26
+ }, [
27
+ context
28
+ ]);
29
+ }
30
+ function interpolate(template, values) {
31
+ if (null == values) return template;
32
+ return template.replace(/\{(\w+)\}/g, (match, name)=>values[name] ?? match);
33
+ }
34
+ export { useNoticeText };
@@ -13,4 +13,27 @@
13
13
  *
14
14
  */
15
15
  import type * as React from 'react';
16
+ import type { LexicalEditor } from 'lexical';
17
+ export type BlockFormat = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'bullet' | 'number' | 'check' | 'quote' | 'code';
18
+ /**
19
+ * Block formats this editor can actually produce. Paragraph is always
20
+ * available.
21
+ *
22
+ * Structural availability comes from REGISTERED NODES, not from whether
23
+ * an extension appears in the configuration list: table and list nodes
24
+ * arrive through `@lexical/table` and `@lexical/list` dependencies, so a
25
+ * membership test would be wrong for exactly those cases.
26
+ *
27
+ * Registered nodes are necessary but not always sufficient.
28
+ * `hasCheckListBehaviour` is passed in because check lists need
29
+ * `CheckListExtension` registered as well as the list nodes — removing
30
+ * that extension alone leaves `ListNode` and `ListItemNode` in place, so
31
+ * a node-only rule would keep offering a control that no longer works.
32
+ *
33
+ * This is a correctness concern rather than a cosmetic one: creating an
34
+ * unregistered node throws "Attempted to create node X that was not
35
+ * configured to be used on the editor", so an offered-but-unsupported
36
+ * format is a crash.
37
+ */
38
+ export declare function availableBlockFormats(editor: LexicalEditor, hasCheckListBehaviour: boolean): BlockFormat[];
16
39
  export declare function ToolbarPlugin(): React.JSX.Element;