@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
package/dist/config.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * registration factory (which dynamic-imports the editor runtime on first
13
13
  * mount), the config types, the built-in extension **names**, and the
14
14
  * light toolbar-authoring primitives — but NOT `RichTextField` /
15
- * `EditorField` / `Nodes` or the heavy *content* extension classes
15
+ * `EditorField` / `READABLE_NODES` or the heavy *content* extension classes
16
16
  * (Table, InlineImage, Admonition, …), all of which statically pull React
17
17
  * plugins, nodes, and the Lexical core. Import from here at registration
18
18
  * sites (e.g. an admin/admin config that you want to evaluate eagerly)
@@ -1,16 +1,22 @@
1
1
  "use client";
2
- import { useEffect, useRef } from "react";
2
+ import { useLayoutEffect, useRef } from "react";
3
3
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
4
+ import { registeredNodeTypes } from "./capabilities/registered-node-types.js";
4
5
  import { APPLY_VALUE_TAG } from "./constants.js";
6
+ import { useNormalizationStatus } from "./normalize/normalization-status.js";
7
+ import { normalizeValue } from "./normalize/normalize-value.js";
5
8
  import { hashSerializedState } from "./utils/hashSerializedState.js";
6
9
  function ApplyValuePlugin({ value, incomingHash, lastEmittedHashRef, normalizedIncomingHashRef, hasNormalizedBaselineRef }) {
7
10
  const [editor] = useLexicalComposerContext();
11
+ const { setStatus } = useNormalizationStatus();
8
12
  const lastAppliedHashRef = useRef(void 0);
13
+ const isRefusedRef = useRef(false);
9
14
  const cancelWaiterRef = useRef(void 0);
10
- useEffect(()=>{
15
+ useLayoutEffect(()=>{
11
16
  if (null == value) return;
12
17
  const nextRawHash = incomingHash;
13
- if (nextRawHash === lastEmittedHashRef.current) {
18
+ const refused = isRefusedRef.current;
19
+ if (!refused && nextRawHash === lastEmittedHashRef.current) {
14
20
  if (true !== hasNormalizedBaselineRef.current) {
15
21
  hasNormalizedBaselineRef.current = true;
16
22
  if (cancelWaiterRef.current) {
@@ -20,13 +26,31 @@ function ApplyValuePlugin({ value, incomingHash, lastEmittedHashRef, normalizedI
20
26
  }
21
27
  return;
22
28
  }
23
- if (nextRawHash === lastAppliedHashRef.current) return;
29
+ if (!refused && nextRawHash === lastAppliedHashRef.current) return;
24
30
  if (cancelWaiterRef.current) {
25
31
  cancelWaiterRef.current();
26
32
  cancelWaiterRef.current = void 0;
27
33
  }
28
34
  hasNormalizedBaselineRef.current = false;
29
- const nextState = editor.parseEditorState(value);
35
+ const normalized = normalizeValue(value, registeredNodeTypes(editor));
36
+ if ('refused' === normalized.status) {
37
+ setStatus({
38
+ kind: 'refused',
39
+ unsupportedTypes: normalized.unsupportedTypes
40
+ });
41
+ lastAppliedHashRef.current = void 0;
42
+ isRefusedRef.current = true;
43
+ hasNormalizedBaselineRef.current = false;
44
+ return;
45
+ }
46
+ isRefusedRef.current = false;
47
+ setStatus('adapted' === normalized.status ? {
48
+ kind: 'adapted',
49
+ convertedTypes: normalized.convertedTypes
50
+ } : {
51
+ kind: 'ok'
52
+ });
53
+ const nextState = editor.parseEditorState('adapted' === normalized.status ? JSON.stringify(normalized.value) : value);
30
54
  editor.setEditorState(nextState, {
31
55
  tag: APPLY_VALUE_TAG
32
56
  });
@@ -51,7 +75,8 @@ function ApplyValuePlugin({ value, incomingHash, lastEmittedHashRef, normalizedI
51
75
  incomingHash,
52
76
  lastEmittedHashRef,
53
77
  normalizedIncomingHashRef,
54
- hasNormalizedBaselineRef
78
+ hasNormalizedBaselineRef,
79
+ setStatus
55
80
  ]);
56
81
  return null;
57
82
  }
@@ -0,0 +1,29 @@
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 { Transformer } from '@lexical/markdown';
9
+ import type { LexicalEditor } from 'lexical';
10
+ /**
11
+ * The transformers that can safely be used with this editor.
12
+ *
13
+ * Markdown is a second route into node creation, and it fails harder
14
+ * than paste does. `registerMarkdownShortcuts` throws when a transformer
15
+ * names a node class the editor has not registered, and it throws for
16
+ * the whole pipeline rather than skipping that one transformer — so a
17
+ * field with the table extension removed would lose Markdown entirely,
18
+ * not just tables. The same applies to every `$convertFromMarkdownString`
19
+ * call, which creates nodes directly.
20
+ *
21
+ * The membership test is `editor.hasNode`, which is exactly what
22
+ * `registerMarkdownShortcuts` uses for its own guard, so this filter
23
+ * cannot disagree with the check it exists to satisfy.
24
+ *
25
+ * Takes the list as a parameter rather than importing it: the Markdown
26
+ * module imports this one, so reaching back for `BYLINE_TRANSFORMERS`
27
+ * here would be a cycle.
28
+ */
29
+ export declare function transformersFor(editor: LexicalEditor, transformers: ReadonlyArray<Transformer>): Array<Transformer>;
@@ -0,0 +1,8 @@
1
+ function transformersFor(editor, transformers) {
2
+ return transformers.filter((transformer)=>{
3
+ const dependencies = 'dependencies' in transformer ? transformer.dependencies : void 0;
4
+ if (null == dependencies) return true;
5
+ return dependencies.every((klass)=>editor.hasNode(klass));
6
+ });
7
+ }
8
+ export { transformersFor };
@@ -0,0 +1,29 @@
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 { LexicalEditor } from 'lexical';
9
+ /**
10
+ * The set of node type strings this editor accepts.
11
+ *
12
+ * This is the ONLY place in Byline that reads `editor._nodes`. Lexical
13
+ * declares the property on the public `LexicalEditor` type but exposes
14
+ * no way to enumerate registered type strings — `hasNode` answers only
15
+ * for a class you already hold, while the normalizer works from
16
+ * serialized JSON where all it has is the type string. Keeping the
17
+ * access here gives a future Lexical change one site to fix.
18
+ */
19
+ export declare function registeredNodeTypes(editor: LexicalEditor): ReadonlySet<string>;
20
+ /**
21
+ * Whether this editor accepts `type`.
22
+ *
23
+ * Node registration is what decides whether a structure can exist in the
24
+ * field at all — it governs the legacy paste path, `parseEditorState`
25
+ * and every command that creates a node — so this is the honest answer
26
+ * to "does this field support X", not whether some extension appears in
27
+ * a configuration list.
28
+ */
29
+ export declare function supportsNodeType(editor: LexicalEditor, type: string): boolean;
@@ -0,0 +1,7 @@
1
+ function registeredNodeTypes(editor) {
2
+ return new Set(editor._nodes.keys());
3
+ }
4
+ function supportsNodeType(editor, type) {
5
+ return editor._nodes.has(type);
6
+ }
7
+ export { registeredNodeTypes, supportsNodeType };
@@ -31,6 +31,44 @@
31
31
  * against the live extension `name`s is exercised by the editor's
32
32
  * jsdom/integration coverage.
33
33
  */
34
+ export declare const bylineExtensionNames: {
35
+ readonly Admonition: '@byline/richtext-lexical/Admonition';
36
+ readonly AutoEmbed: '@byline/richtext-lexical/AutoEmbed';
37
+ readonly AutoLink: '@byline/richtext-lexical/AutoLink';
38
+ readonly CodeHighlight: '@byline/richtext-lexical/CodeHighlight';
39
+ readonly FloatingTextFormat: '@byline/richtext-lexical/FloatingTextFormat';
40
+ readonly FloatingUI: '@byline/richtext-lexical/FloatingUI';
41
+ readonly Heading: '@byline/richtext-lexical/Heading';
42
+ readonly HorizontalRule: '@byline/richtext-lexical/HorizontalRule';
43
+ readonly InlineImage: '@byline/richtext-lexical/InlineImage';
44
+ readonly Layout: '@byline/richtext-lexical/Layout';
45
+ readonly Link: '@byline/richtext-lexical/Link';
46
+ readonly Quote: '@byline/richtext-lexical/Quote';
47
+ readonly Table: '@byline/richtext-lexical/Table';
48
+ readonly Toolbar: '@byline/richtext-lexical/Toolbar';
49
+ readonly Vimeo: '@byline/richtext-lexical/Vimeo';
50
+ readonly YouTube: '@byline/richtext-lexical/YouTube';
51
+ };
52
+ /**
53
+ * Names owned by upstream Lexical packages.
54
+ *
55
+ * These deliberately break the `@byline/richtext-lexical/<Key>`
56
+ * convention because Byline does not own the names — the extensions ship
57
+ * from `@lexical/list`. They are carried here so a site can remove lists
58
+ * without importing `@lexical/list` directly and taking a
59
+ * version-pinned dependency on `lexical` of its own.
60
+ */
61
+ export declare const upstreamExtensionNames: {
62
+ readonly List: '@lexical/list/List';
63
+ readonly CheckList: '@lexical/list/CheckList';
64
+ };
65
+ /**
66
+ * Every extension a site may remove, replace or reorder.
67
+ *
68
+ * Note what is absent: `CoreNodesExtension` is injected by the editor
69
+ * root rather than configured here, because `MarkNode` and
70
+ * `OverflowNode` belong to no switchable feature.
71
+ */
34
72
  export declare const builtInExtensions: {
35
73
  readonly Admonition: '@byline/richtext-lexical/Admonition';
36
74
  readonly AutoEmbed: '@byline/richtext-lexical/AutoEmbed';
@@ -38,14 +76,18 @@ export declare const builtInExtensions: {
38
76
  readonly CodeHighlight: '@byline/richtext-lexical/CodeHighlight';
39
77
  readonly FloatingTextFormat: '@byline/richtext-lexical/FloatingTextFormat';
40
78
  readonly FloatingUI: '@byline/richtext-lexical/FloatingUI';
79
+ readonly Heading: '@byline/richtext-lexical/Heading';
41
80
  readonly HorizontalRule: '@byline/richtext-lexical/HorizontalRule';
42
81
  readonly InlineImage: '@byline/richtext-lexical/InlineImage';
43
82
  readonly Layout: '@byline/richtext-lexical/Layout';
44
83
  readonly Link: '@byline/richtext-lexical/Link';
84
+ readonly Quote: '@byline/richtext-lexical/Quote';
45
85
  readonly Table: '@byline/richtext-lexical/Table';
46
86
  readonly Toolbar: '@byline/richtext-lexical/Toolbar';
47
87
  readonly Vimeo: '@byline/richtext-lexical/Vimeo';
48
88
  readonly YouTube: '@byline/richtext-lexical/YouTube';
89
+ readonly List: '@lexical/list/List';
90
+ readonly CheckList: '@lexical/list/CheckList';
49
91
  };
50
92
  /** Union of the built-in extension `name` strings. */
51
93
  export type BuiltInExtensionName = (typeof builtInExtensions)[keyof typeof builtInExtensions];
@@ -1,17 +1,27 @@
1
- const builtInExtensions = {
1
+ const bylineExtensionNames = {
2
2
  Admonition: '@byline/richtext-lexical/Admonition',
3
3
  AutoEmbed: '@byline/richtext-lexical/AutoEmbed',
4
4
  AutoLink: '@byline/richtext-lexical/AutoLink',
5
5
  CodeHighlight: '@byline/richtext-lexical/CodeHighlight',
6
6
  FloatingTextFormat: '@byline/richtext-lexical/FloatingTextFormat',
7
7
  FloatingUI: '@byline/richtext-lexical/FloatingUI',
8
+ Heading: '@byline/richtext-lexical/Heading',
8
9
  HorizontalRule: '@byline/richtext-lexical/HorizontalRule',
9
10
  InlineImage: '@byline/richtext-lexical/InlineImage',
10
11
  Layout: '@byline/richtext-lexical/Layout',
11
12
  Link: '@byline/richtext-lexical/Link',
13
+ Quote: '@byline/richtext-lexical/Quote',
12
14
  Table: '@byline/richtext-lexical/Table',
13
15
  Toolbar: '@byline/richtext-lexical/Toolbar',
14
16
  Vimeo: '@byline/richtext-lexical/Vimeo',
15
17
  YouTube: '@byline/richtext-lexical/YouTube'
16
18
  };
17
- export { builtInExtensions };
19
+ const upstreamExtensionNames = {
20
+ List: '@lexical/list/List',
21
+ CheckList: '@lexical/list/CheckList'
22
+ };
23
+ const builtInExtensions = {
24
+ ...bylineExtensionNames,
25
+ ...upstreamExtensionNames
26
+ };
27
+ export { builtInExtensions, bylineExtensionNames, upstreamExtensionNames };
@@ -6,10 +6,12 @@ import { BylineFloatingUIExtension } from "../extensions/byline-floating-ui/byli
6
6
  import { BylineToolbarExtension } from "../extensions/byline-toolbar/byline-toolbar-extension.js";
7
7
  import { CodeHighlightExtension } from "../extensions/code-highlight/code-highlight-extension.js";
8
8
  import { FloatingTextFormatExtension } from "../extensions/floating-text-format/floating-text-format-extension.js";
9
+ import { HeadingExtension } from "../extensions/heading/heading-extension.js";
9
10
  import { HorizontalRuleExtension } from "../extensions/horizontal-rule/horizontal-rule-extension.js";
10
11
  import { InlineImageExtension } from "../extensions/inline-image/inline-image-extension.js";
11
12
  import { LayoutExtension } from "../extensions/layout/layout-extension.js";
12
13
  import { LinkExtension } from "../extensions/link/link-extension.js";
14
+ import { QuoteExtension } from "../extensions/quote/quote-extension.js";
13
15
  import { TableExtension } from "../extensions/table/table-extension.js";
14
16
  import { VimeoExtension } from "../extensions/vimeo/vimeo-extension.js";
15
17
  import { YouTubeExtension } from "../extensions/youtube/youtube-extension.js";
@@ -21,6 +23,8 @@ function defaultExtensionsArray() {
21
23
  TabIndentationExtension,
22
24
  BylineToolbarExtension,
23
25
  BylineFloatingUIExtension,
26
+ HeadingExtension,
27
+ QuoteExtension,
24
28
  ListExtension,
25
29
  CheckListExtension,
26
30
  HorizontalRuleExtension,
@@ -4,17 +4,17 @@ const defaultEditorLexicalConfig = {
4
4
  theme: theme
5
5
  };
6
6
  const DEFAULT_EDITOR_SETTINGS = {
7
- options: {
8
- richText: true,
9
- showTreeView: false,
7
+ mode: 'richText',
8
+ markdownShortcuts: false,
9
+ controls: {
10
+ blockFormat: true,
11
+ inlineCode: true,
12
+ undoRedo: true,
10
13
  textAlignment: true,
11
- markdownShortcutPlugin: false,
12
14
  markdownToggle: false,
13
- undoRedo: true,
14
- textStyle: true,
15
- inlineCode: true,
16
- debug: false
15
+ treeView: false
17
16
  },
17
+ debug: false,
18
18
  inlineImageUploadCollection: 'media',
19
19
  placeholderText: 'Enter some rich text...'
20
20
  };
@@ -1,7 +1,7 @@
1
1
  import type * as React from 'react';
2
- import type { EditorSettings, OptionName } from './types.js';
2
+ import type { EditorControls, EditorSettings } from './types.js';
3
3
  interface ContextType {
4
- setOption: (name: OptionName, value: boolean) => void;
4
+ setControl: (name: keyof EditorControls, value: boolean) => void;
5
5
  config: EditorSettings;
6
6
  uuid: string;
7
7
  }
@@ -6,31 +6,28 @@ function generateQuickGuid() {
6
6
  return Math.random().toString(36).substring(2, 12) + Math.random().toString(36).substring(2, 12);
7
7
  }
8
8
  const Context = /*#__PURE__*/ createContext({
9
- setOption: (_name, _value)=>{},
9
+ setControl: (_name, _value)=>{},
10
10
  config: DEFAULT_EDITOR_SETTINGS,
11
11
  uuid: generateQuickGuid()
12
12
  });
13
13
  const EditorConfigContext = ({ children, config: configFromProps })=>{
14
14
  const [config, setConfig] = useState(configFromProps ?? DEFAULT_EDITOR_SETTINGS);
15
15
  const [uuid] = useState(()=>generateQuickGuid());
16
- const setOption = useCallback((option, value)=>{
17
- setConfig((config)=>{
18
- const options = {
19
- ...config.options,
20
- [option]: value
21
- };
22
- return {
16
+ const setControl = useCallback((control, value)=>{
17
+ setConfig((config)=>({
23
18
  ...config,
24
- options
25
- };
26
- });
19
+ controls: {
20
+ ...config.controls,
21
+ [control]: value
22
+ }
23
+ }));
27
24
  }, []);
28
25
  const editorContext = useMemo(()=>({
29
- setOption,
26
+ setControl,
30
27
  config,
31
28
  uuid
32
29
  }), [
33
- setOption,
30
+ setControl,
34
31
  config,
35
32
  uuid
36
33
  ]);
@@ -4,9 +4,9 @@ function resolveEditorConfig(fieldConfig, registeredConfig) {
4
4
  settings: {
5
5
  ...registeredConfig.settings,
6
6
  ...fieldConfig.settings,
7
- options: {
8
- ...registeredConfig.settings.options,
9
- ...fieldConfig.settings?.options
7
+ controls: {
8
+ ...registeredConfig.settings.controls,
9
+ ...fieldConfig.settings?.controls
10
10
  }
11
11
  },
12
12
  lexical: fieldConfig.lexical ?? registeredConfig.lexical,
@@ -0,0 +1,26 @@
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 { AnyLexicalExtensionArgument } from 'lexical';
9
+ import type { ExtensionsList } from './extensions-list.js';
10
+ /**
11
+ * The complete dependency list for an editor root.
12
+ *
13
+ * `editorConfig.extensions` carries the OPTIONAL features a site
14
+ * configures. What an editor actually registers is that list plus
15
+ * mandatory infrastructure, and `CoreNodesExtension` is injected here
16
+ * rather than living in the configurable list because `MarkNode` and
17
+ * `OverflowNode` belong to no switchable feature — `ExtensionsList.remove()`
18
+ * matches by name and accepts the extension object, so an entry in the
19
+ * list could be removed by site code.
20
+ *
21
+ * Everything that builds an editor goes through this: the live editor,
22
+ * the test builders, and the capability manifest. A manifest that
23
+ * disagreed with the runtime about which nodes exist would be worse than
24
+ * no manifest, and this is the seam that keeps them identical.
25
+ */
26
+ export declare function rootDependencies(extensions: ExtensionsList): AnyLexicalExtensionArgument[];
@@ -0,0 +1,8 @@
1
+ import { CoreNodesExtension } from "../extensions/core-nodes/core-nodes-extension.js";
2
+ function rootDependencies(extensions) {
3
+ return [
4
+ CoreNodesExtension,
5
+ ...extensions.toArray()
6
+ ];
7
+ }
8
+ export { rootDependencies };
@@ -1,15 +1,65 @@
1
1
  import type { EditorConfig as LexicalEditorConfig } from 'lexical';
2
2
  import type { ExtensionsList } from './extensions-list.js';
3
3
  /**
4
- * Settings that aren't expressible as "is extension X present in the
5
- * extensions list?" — toolbar / UI mode toggles, debug switches, the
6
- * inline-image upload target, etc. Everything else now lives on
7
- * {@link EditorConfig.extensions} and is manipulated via the chainable
8
- * `lexicalEditor((c) => c.extensions.add(...).remove(...))` API.
4
+ * Interface preferences which controls the editing surface shows.
5
+ *
6
+ * Every flag here hides an affordance and NOTHING else. None of them
7
+ * restricts what the field accepts: content still arrives by paste, by
8
+ * Markdown, and from storage regardless. What a field supports is
9
+ * decided by its resolved editor — the extensions it is configured with,
10
+ * everything those pull in transitively, and mandatory core nodes.
11
+ *
12
+ * If you want to stop a structure existing in a field, remove the
13
+ * extension that owns it:
14
+ * `lexicalEditor((c) => c.extensions.remove(builtInExtensions.Heading))`.
9
15
  */
10
- export type OptionName = 'richText' | 'showTreeView' | 'textAlignment' | 'markdownShortcutPlugin' | 'markdownToggle' | 'undoRedo' | 'textStyle' | 'inlineCode' | 'debug';
16
+ export interface EditorControls {
17
+ /**
18
+ * Show the block-format dropdown. Its entries are further limited to
19
+ * the formats the editor can actually produce, so this hides a control
20
+ * that is already capability-aware.
21
+ */
22
+ blockFormat: boolean;
23
+ /**
24
+ * Show the inline-code button.
25
+ *
26
+ * This does NOT prevent inline code. Inline code is a `TextNode`
27
+ * format rather than a node type, so it has no node to unregister and
28
+ * still arrives by paste. Node-level configuration cannot constrain
29
+ * it, and neither can this flag.
30
+ */
31
+ inlineCode: boolean;
32
+ /** Show the undo and redo buttons. Keyboard undo keeps working either way. */
33
+ undoRedo: boolean;
34
+ /** Show the text-alignment controls. */
35
+ textAlignment: boolean;
36
+ /** Show the Markdown source-mode toggle. */
37
+ markdownToggle: boolean;
38
+ /** Show the Lexical tree view beneath the editor. */
39
+ treeView: boolean;
40
+ }
11
41
  export interface EditorSettings {
12
- options: Record<OptionName, boolean>;
42
+ /**
43
+ * Which editing surface to mount: `RichTextPlugin` or
44
+ * `PlainTextPlugin`. Plain-text mode also hides the toolbar.
45
+ *
46
+ * This selects the plugin and its input handling — it does NOT change
47
+ * what the field accepts. Switching to `plainText` unregisters no node
48
+ * class, so structural content already in a stored value still loads,
49
+ * and a paste is still governed by the editor's registered nodes. To
50
+ * stop a structure existing in the field, remove the extension that
51
+ * owns it.
52
+ */
53
+ mode: 'richText' | 'plainText';
54
+ /**
55
+ * Enable Markdown input shortcuts (`# `, `- `, `> ` as you type). The
56
+ * active transformer set follows the editor's registered nodes, so a
57
+ * shortcut for a structure the field does not support never fires.
58
+ */
59
+ markdownShortcuts: boolean;
60
+ controls: EditorControls;
61
+ /** Render debug output and the tree view. */
62
+ debug: boolean;
13
63
  /**
14
64
  * Upload collection passed to the inline-image picker. Forwarded to
15
65
  * `InlineImageExtension`'s config when the extensions list is built;
@@ -20,7 +70,10 @@ export interface EditorSettings {
20
70
  placeholderText: string;
21
71
  }
22
72
  export interface EditorSettingsOverride {
23
- options?: Partial<Record<OptionName, boolean>>;
73
+ mode?: EditorSettings['mode'];
74
+ markdownShortcuts?: boolean;
75
+ controls?: Partial<EditorControls>;
76
+ debug?: boolean;
24
77
  inlineImageUploadCollection?: string;
25
78
  placeholderText?: string;
26
79
  }
@@ -1,16 +1,18 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo } from "react";
4
4
  import { LexicalExtensionComposer } from "@lexical/react/LexicalExtensionComposer";
5
5
  import { defineExtension } from "lexical";
6
6
  import { defaultExtensionsList } from "./config/default-extensions.js";
7
7
  import { EditorConfigContext } from "./config/editor-config-context.js";
8
+ import { rootDependencies } from "./config/root-dependencies.js";
8
9
  import { MarkdownModeProvider } from "./context/markdown-mode-context.js";
9
10
  import { SharedHistoryContext } from "./context/shared-history-context.js";
10
11
  import { SharedOnChangeContext } from "./context/shared-on-change-context.js";
11
12
  import { Editor } from "./editor.js";
12
13
  import { InlineImageExtension } from "./extensions/inline-image/inline-image-extension.js";
13
- import { Nodes } from "./nodes/index.js";
14
+ import { AdaptedNotice, UnsupportedContentNotice } from "./normalize/adapted-notice.js";
15
+ import { NormalizationStatusProvider, useNormalizationStatus } from "./normalize/normalization-status.js";
14
16
  function EditorContext(props) {
15
17
  const { composerKey, editorConfig, onChange, readOnly, value, beforeEditor, afterEditor, children } = props;
16
18
  const editable = true !== readOnly;
@@ -20,16 +22,12 @@ function EditorContext(props) {
20
22
  if (configured.has(InlineImageExtension)) configured.configure(InlineImageExtension, {
21
23
  collection: editorConfig.settings.inlineImageUploadCollection
22
24
  });
23
- const dependencies = configured.toArray();
25
+ const dependencies = rootDependencies(configured);
24
26
  return defineExtension({
25
27
  name: '[root]',
26
28
  namespace: editorConfig.lexical.namespace,
27
- nodes: [
28
- ...Nodes
29
- ],
30
29
  theme: editorConfig.lexical.theme,
31
30
  editable,
32
- $initialEditorState: null != value ? JSON.stringify(value) : void 0,
33
31
  onError: (error)=>{
34
32
  throw error;
35
33
  },
@@ -38,29 +36,48 @@ function EditorContext(props) {
38
36
  }, []);
39
37
  return /*#__PURE__*/ jsx(EditorConfigContext, {
40
38
  config: editorConfig.settings,
41
- children: /*#__PURE__*/ jsx(SharedOnChangeContext, {
42
- onChange: onChange,
43
- children: /*#__PURE__*/ jsx(SharedHistoryContext, {
44
- children: /*#__PURE__*/ jsx(MarkdownModeProvider, {
45
- children: /*#__PURE__*/ jsx(LexicalExtensionComposer, {
46
- extension: rootExtension,
47
- contentEditable: null,
48
- children: /*#__PURE__*/ jsxs("div", {
49
- className: "editor-shell",
50
- children: [
51
- beforeEditor,
52
- /*#__PURE__*/ jsx(Editor, {
53
- minHeight: props.minHeight,
54
- maxHeight: props.maxHeight
55
- }),
56
- afterEditor,
57
- children
58
- ]
59
- })
60
- }, composerKey + editable)
39
+ children: /*#__PURE__*/ jsx(NormalizationStatusProvider, {
40
+ children: /*#__PURE__*/ jsx(SharedOnChangeContext, {
41
+ onChange: onChange,
42
+ children: /*#__PURE__*/ jsx(SharedHistoryContext, {
43
+ children: /*#__PURE__*/ jsx(MarkdownModeProvider, {
44
+ children: /*#__PURE__*/ jsx(LexicalExtensionComposer, {
45
+ extension: rootExtension,
46
+ contentEditable: null,
47
+ children: /*#__PURE__*/ jsx(EditorShell, {
48
+ beforeEditor: beforeEditor,
49
+ afterEditor: afterEditor,
50
+ minHeight: props.minHeight,
51
+ maxHeight: props.maxHeight,
52
+ children: children
53
+ })
54
+ }, composerKey + editable)
55
+ })
61
56
  })
62
57
  })
63
58
  })
64
59
  });
65
60
  }
61
+ function EditorShell({ beforeEditor, afterEditor, minHeight, maxHeight, children }) {
62
+ const { status } = useNormalizationStatus();
63
+ return /*#__PURE__*/ jsxs("div", {
64
+ className: "editor-shell",
65
+ children: [
66
+ 'refused' === status.kind ? /*#__PURE__*/ jsx(UnsupportedContentNotice, {
67
+ unsupportedTypes: status.unsupportedTypes
68
+ }) : /*#__PURE__*/ jsxs(Fragment, {
69
+ children: [
70
+ 'adapted' === status.kind && /*#__PURE__*/ jsx(AdaptedNotice, {}),
71
+ beforeEditor,
72
+ /*#__PURE__*/ jsx(Editor, {
73
+ minHeight: minHeight,
74
+ maxHeight: maxHeight
75
+ }),
76
+ afterEditor
77
+ ]
78
+ }),
79
+ children
80
+ ]
81
+ });
82
+ }
66
83
  export { EditorContext };