@blue-patterns/richtext 0.0.0-stage → 0.7.18
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/CHANGELOG.md +275 -0
- package/LICENSE +22 -0
- package/README.md +88 -2
- package/astryx.integration.mjs +11 -0
- package/blocks/RichTextEditorShowcase.template.mjs +13 -0
- package/blocks/RichTextEditorShowcase.tsx +19 -0
- package/dist/RichTextEditor.d.ts +282 -0
- package/dist/RichTextEditor.d.ts.map +1 -0
- package/dist/RichTextEditor.js +657 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
- package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
- package/dist/RichTextEditorToolbar.d.ts +186 -0
- package/dist/RichTextEditorToolbar.d.ts.map +1 -0
- package/dist/RichTextEditorToolbar.js +688 -0
- package/dist/RichTextView.d.ts +92 -0
- package/dist/RichTextView.d.ts.map +1 -0
- package/dist/RichTextView.js +267 -0
- package/dist/codeLanguages.d.ts +14 -0
- package/dist/codeLanguages.d.ts.map +1 -0
- package/dist/codeLanguages.js +34 -0
- package/dist/codeLineTokens.d.ts +53 -0
- package/dist/codeLineTokens.d.ts.map +1 -0
- package/dist/codeLineTokens.js +188 -0
- package/dist/editorNodes.d.ts +8 -0
- package/dist/editorNodes.d.ts.map +1 -0
- package/dist/editorNodes.js +30 -0
- package/dist/editorTheme.d.ts +7 -0
- package/dist/editorTheme.d.ts.map +1 -0
- package/dist/editorTheme.js +328 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
- package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
- package/dist/internal/CodeBackspacePlugin.js +70 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
- package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
- package/dist/internal/CodePastePlugin.d.ts +5 -0
- package/dist/internal/CodePastePlugin.d.ts.map +1 -0
- package/dist/internal/CodePastePlugin.js +104 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
- package/dist/internal/CodeSyntaxPlugin.js +397 -0
- package/dist/internal/CodeTabPlugin.d.ts +2 -0
- package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
- package/dist/internal/CodeTabPlugin.js +256 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
- package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
- package/dist/internal/ImageSourceCaretPlugin.js +105 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
- package/dist/internal/ListItemShortcutsPlugin.js +314 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
- package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
- package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
- package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
- package/dist/internal/SelectAllScopePlugin.js +67 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
- package/dist/internal/TableColumnFloorPlugin.js +84 -0
- package/dist/internal/TablePastePlugin.d.ts +9 -0
- package/dist/internal/TablePastePlugin.d.ts.map +1 -0
- package/dist/internal/TablePastePlugin.js +183 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
- package/dist/internal/TableScrollRegionPlugin.js +95 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
- package/dist/internal/TaskCheckboxPlugin.js +193 -0
- package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
- package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
- package/dist/internal/ToolbarLinkPopover.js +292 -0
- package/dist/linkEditing.d.ts +6 -0
- package/dist/linkEditing.d.ts.map +1 -0
- package/dist/linkEditing.js +20 -0
- package/dist/linkForm.d.ts +28 -0
- package/dist/linkForm.d.ts.map +1 -0
- package/dist/linkForm.js +137 -0
- package/dist/linkUtils.d.ts +27 -0
- package/dist/linkUtils.d.ts.map +1 -0
- package/dist/linkUtils.js +101 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +17 -0
- package/dist/markdownCharacterReferences.d.ts +135 -0
- package/dist/markdownCharacterReferences.d.ts.map +1 -0
- package/dist/markdownCharacterReferences.js +1127 -0
- package/dist/markdownCodeFence.d.ts +4 -0
- package/dist/markdownCodeFence.d.ts.map +1 -0
- package/dist/markdownCodeFence.js +167 -0
- package/dist/markdownExtensionNode.d.ts +88 -0
- package/dist/markdownExtensionNode.d.ts.map +1 -0
- package/dist/markdownExtensionNode.js +248 -0
- package/dist/markdownExtensions.d.ts +115 -0
- package/dist/markdownExtensions.d.ts.map +1 -0
- package/dist/markdownExtensions.js +392 -0
- package/dist/markdownHardLineBreak.d.ts +25 -0
- package/dist/markdownHardLineBreak.d.ts.map +1 -0
- package/dist/markdownHardLineBreak.js +59 -0
- package/dist/markdownHeading.d.ts +20 -0
- package/dist/markdownHeading.d.ts.map +1 -0
- package/dist/markdownHeading.js +30 -0
- package/dist/markdownLink.d.ts +5 -0
- package/dist/markdownLink.d.ts.map +1 -0
- package/dist/markdownLink.js +80 -0
- package/dist/markdownListExport.d.ts +3 -0
- package/dist/markdownListExport.d.ts.map +1 -0
- package/dist/markdownListExport.js +68 -0
- package/dist/markdownListIndentation.d.ts +8 -0
- package/dist/markdownListIndentation.d.ts.map +1 -0
- package/dist/markdownListIndentation.js +101 -0
- package/dist/markdownOrderedList.d.ts +3 -0
- package/dist/markdownOrderedList.d.ts.map +1 -0
- package/dist/markdownOrderedList.js +51 -0
- package/dist/markdownQuote.d.ts +22 -0
- package/dist/markdownQuote.d.ts.map +1 -0
- package/dist/markdownQuote.js +32 -0
- package/dist/markdownSerializers.d.ts +49 -0
- package/dist/markdownSerializers.d.ts.map +1 -0
- package/dist/markdownSerializers.js +83 -0
- package/dist/markdownSource.d.ts +71 -0
- package/dist/markdownSource.d.ts.map +1 -0
- package/dist/markdownSource.js +804 -0
- package/dist/markdownSpaceRuns.d.ts +24 -0
- package/dist/markdownSpaceRuns.d.ts.map +1 -0
- package/dist/markdownSpaceRuns.js +147 -0
- package/dist/markdownTable.d.ts +35 -0
- package/dist/markdownTable.d.ts.map +1 -0
- package/dist/markdownTable.js +227 -0
- package/dist/markdownTaskList.d.ts +48 -0
- package/dist/markdownTaskList.d.ts.map +1 -0
- package/dist/markdownTaskList.js +166 -0
- package/dist/markdownThematicBreak.d.ts +3 -0
- package/dist/markdownThematicBreak.d.ts.map +1 -0
- package/dist/markdownThematicBreak.js +55 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
- package/dist/richtext.css +213 -0
- package/dist/textSemantics.d.ts +3 -0
- package/dist/textSemantics.d.ts.map +1 -0
- package/dist/textSemantics.js +87 -0
- package/package.json +158 -3
- package/src/RichTextEditor.doc.mjs +308 -0
- package/src/RichTextEditor.tsx +1109 -0
- package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
- package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
- package/src/RichTextEditorToolbar.doc.mjs +92 -0
- package/src/RichTextEditorToolbar.tsx +871 -0
- package/src/RichTextView.doc.mjs +101 -0
- package/src/RichTextView.tsx +351 -0
- package/src/codeLanguages.ts +42 -0
- package/src/codeLineTokens.ts +241 -0
- package/src/doc-mjs.d.ts +23 -0
- package/src/editorNodes.ts +44 -0
- package/src/editorTheme.ts +550 -0
- package/src/index.ts +59 -0
- package/src/internal/CodeBackspacePlugin.tsx +96 -0
- package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
- package/src/internal/CodePastePlugin.tsx +144 -0
- package/src/internal/CodeSyntaxPlugin.tsx +493 -0
- package/src/internal/CodeTabPlugin.tsx +298 -0
- package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
- package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
- package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
- package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
- package/src/internal/SelectAllScopePlugin.tsx +86 -0
- package/src/internal/TableColumnFloorPlugin.tsx +99 -0
- package/src/internal/TablePastePlugin.tsx +223 -0
- package/src/internal/TableScrollRegionPlugin.tsx +107 -0
- package/src/internal/TaskCheckboxPlugin.tsx +246 -0
- package/src/internal/ToolbarLinkPopover.tsx +392 -0
- package/src/linkEditing.ts +20 -0
- package/src/linkForm.tsx +156 -0
- package/src/linkUtils.ts +106 -0
- package/src/markdown.ts +25 -0
- package/src/markdownCharacterReferences.ts +1137 -0
- package/src/markdownCodeFence.ts +216 -0
- package/src/markdownExtensionNode.ts +339 -0
- package/src/markdownExtensions.ts +496 -0
- package/src/markdownHardLineBreak.ts +73 -0
- package/src/markdownHeading.ts +33 -0
- package/src/markdownLink.ts +86 -0
- package/src/markdownListExport.ts +85 -0
- package/src/markdownListIndentation.ts +105 -0
- package/src/markdownOrderedList.ts +60 -0
- package/src/markdownQuote.ts +35 -0
- package/src/markdownSerializers.ts +108 -0
- package/src/markdownSource.ts +1045 -0
- package/src/markdownSpaceRuns.ts +164 -0
- package/src/markdownTable.ts +309 -0
- package/src/markdownTaskList.ts +222 -0
- package/src/markdownThematicBreak.ts +66 -0
- package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
- package/src/textSemantics.ts +104 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RichTextView.tsx
|
|
3
|
+
* @input Uses React, Lexical (lexical + @lexical/react, composed through
|
|
4
|
+
* LexicalExtensionComposer), mergeProps from core utils, design tokens,
|
|
5
|
+
* TableScrollRegionPlugin
|
|
6
|
+
* @output Exports RichTextView component and RichTextViewProps
|
|
7
|
+
* @position Read-only renderer for serialized Lexical editor state, exposed to
|
|
8
|
+
* assistive technology as document content rather than a form field;
|
|
9
|
+
* exported from @blue-patterns/richtext
|
|
10
|
+
*
|
|
11
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
12
|
+
* - /packages/richtext/src/RichTextEditor.test.tsx
|
|
13
|
+
* - /packages/richtext/src/index.ts
|
|
14
|
+
* - /apps/storybook/stories/RichTextEditor.stories.tsx
|
|
15
|
+
*/
|
|
16
|
+
import { type ReactNode } from 'react';
|
|
17
|
+
import type { BaseProps } from '@blue-patterns/core';
|
|
18
|
+
import type { RichTextMarkdownExtension } from './markdownExtensions.js';
|
|
19
|
+
import type { CodeLanguage } from '@blue-patterns/core/CodeBlock';
|
|
20
|
+
import type { Klass, LexicalNode } from 'lexical';
|
|
21
|
+
export interface RichTextViewProps extends BaseProps {
|
|
22
|
+
/**
|
|
23
|
+
* Serialized editor state to render (a JSON string produced by
|
|
24
|
+
* `JSON.stringify(editorState.toJSON())`).
|
|
25
|
+
*/
|
|
26
|
+
value: string;
|
|
27
|
+
/**
|
|
28
|
+
* Additional Lexical nodes to register beyond the default OSS set. Must match
|
|
29
|
+
* the nodes used to author `value` so custom node types deserialize.
|
|
30
|
+
*/
|
|
31
|
+
nodes?: ReadonlyArray<Klass<LexicalNode>>;
|
|
32
|
+
/**
|
|
33
|
+
* Markdown plugins whose nodes this surface draws, each adopted with
|
|
34
|
+
* `createRichTextExtension` (spec:AST-064). A plugin node renders exactly as
|
|
35
|
+
* core `Markdown` renders it, and one whose plugin is not given here shows
|
|
36
|
+
* its source. Pass the extensions the content was converted with. Create
|
|
37
|
+
* them in a client module: they hold the plugins' functions, so they are not
|
|
38
|
+
* serializable props.
|
|
39
|
+
*/
|
|
40
|
+
markdownExtensions?: ReadonlyArray<RichTextMarkdownExtension>;
|
|
41
|
+
/**
|
|
42
|
+
* CodeBlock language values a fence may name, each imported on its own,
|
|
43
|
+
* such as `graphql` from `@blue-patterns/core/CodeBlock/languages/graphql`,
|
|
44
|
+
* or made with `createCodeLanguage`. Pass the same list as `Markdown`'s
|
|
45
|
+
* `codeLanguages`, so reading and editing paint and label the same
|
|
46
|
+
* languages (spec:AST-061).
|
|
47
|
+
*/
|
|
48
|
+
codeLanguages?: ReadonlyArray<CodeLanguage>;
|
|
49
|
+
/**
|
|
50
|
+
* Additional read-only plugins to render inside the composer (e.g. hover
|
|
51
|
+
* cards, decorators).
|
|
52
|
+
*/
|
|
53
|
+
plugins?: ReactNode;
|
|
54
|
+
/** The Lexical composer namespace. @default 'astryx-view' */
|
|
55
|
+
namespace?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Called when `value` cannot be parsed/rendered (e.g. malformed JSON, or
|
|
58
|
+
* state authored with node types not registered via `nodes`). A read-only
|
|
59
|
+
* view renders *persisted* content — exactly where stale or foreign-schema
|
|
60
|
+
* state shows up — so by default a parse failure renders `errorFallback`
|
|
61
|
+
* instead of throwing and taking down the host. Provide `onParseError` to log or
|
|
62
|
+
* report it.
|
|
63
|
+
*/
|
|
64
|
+
onParseError?: (error: Error) => void;
|
|
65
|
+
/**
|
|
66
|
+
* What to render when `value` fails to parse/render. Defaults to `null`
|
|
67
|
+
* (renders nothing). Pass a node to show a placeholder/empty state.
|
|
68
|
+
* @default null
|
|
69
|
+
*/
|
|
70
|
+
errorFallback?: ReactNode;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A read-only renderer for serialized Lexical content. Renders the same styled
|
|
74
|
+
* output as {@link RichTextEditor} without any editing affordances.
|
|
75
|
+
*
|
|
76
|
+
* The output is document content, not a form field: headings, lists and links
|
|
77
|
+
* keep their own roles for assistive technology, and the view takes no label.
|
|
78
|
+
* For a labelled, read-only field, use `<RichTextEditor isReadOnly />`. A
|
|
79
|
+
* table wider than the view scrolls inside a region named "Table" that takes a
|
|
80
|
+
* tab stop while it overflows, as core `Table` does.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```
|
|
84
|
+
* import {RichTextView} from '@blue-patterns/richtext';
|
|
85
|
+
* <RichTextView value={storedEditorStateJSON} />
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function RichTextView({ value, nodes, markdownExtensions, codeLanguages, plugins, namespace, onParseError, errorFallback, xstyle, className, style, ...rest }: RichTextViewProps): import("react").JSX.Element;
|
|
89
|
+
export declare namespace RichTextView {
|
|
90
|
+
var displayName: string;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=RichTextView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RichTextView.d.ts","sourceRoot":"","sources":["../src/RichTextView.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA8B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGlE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAmBnD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,+BAA+B,CAAC;AAEhE,OAAO,KAAK,EAEV,KAAK,EACL,WAAW,EAEZ,MAAM,SAAS,CAAC;AAoDjB,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,aAAa,CAAC,yBAAyB,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC;CAC3B;AAyCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,kBAAkB,EAClB,aAAa,EACb,OAAO,EACP,SAAyB,EACzB,YAAY,EACZ,aAAoB,EACpB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,iBAAiB,+BA8HnB;yBA3Ie,YAAY"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file RichTextView.tsx
|
|
7
|
+
* @input Uses React, Lexical (lexical + @lexical/react, composed through
|
|
8
|
+
* LexicalExtensionComposer), mergeProps from core utils, design tokens,
|
|
9
|
+
* TableScrollRegionPlugin
|
|
10
|
+
* @output Exports RichTextView component and RichTextViewProps
|
|
11
|
+
* @position Read-only renderer for serialized Lexical editor state, exposed to
|
|
12
|
+
* assistive technology as document content rather than a form field;
|
|
13
|
+
* exported from @blue-patterns/richtext
|
|
14
|
+
*
|
|
15
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
16
|
+
* - /packages/richtext/src/RichTextEditor.test.tsx
|
|
17
|
+
* - /packages/richtext/src/index.ts
|
|
18
|
+
* - /apps/storybook/stories/RichTextEditor.stories.tsx
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useRef, useState } from 'react';
|
|
21
|
+
import * as stylex from '@stylexjs/stylex';
|
|
22
|
+
import { sharedEditorTheme } from "./editorTheme.js";
|
|
23
|
+
import { mergeProps } from '@blue-patterns/core/utils';
|
|
24
|
+
import '@blue-patterns/core/theme/tokens.stylex';
|
|
25
|
+
import { colorVars, typeScaleVars, typographyVars } from '@blue-patterns/core/theme/tokens.stylex';
|
|
26
|
+
import { LexicalExtensionComposer } from '@lexical/react/LexicalExtensionComposer';
|
|
27
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
28
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
29
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
30
|
+
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
31
|
+
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
|
32
|
+
import { TableScrollRegionPlugin } from "./internal/TableScrollRegionPlugin.js";
|
|
33
|
+
import { TableColumnFloorPlugin } from "./internal/TableColumnFloorPlugin.js";
|
|
34
|
+
import { CodeBlockHeaderPlugin } from "./internal/CodeBlockHeaderPlugin.js";
|
|
35
|
+
import { CodeSyntaxPlugin } from "./internal/CodeSyntaxPlugin.js";
|
|
36
|
+
import { MarkdownExtensionsPlugin } from "./internal/MarkdownExtensionsPlugin.js";
|
|
37
|
+
import { TaskCheckboxPlugin } from "./internal/TaskCheckboxPlugin.js";
|
|
38
|
+
import { defineExtension } from 'lexical';
|
|
39
|
+
import { TextSemanticsExtension } from "./textSemantics.js";
|
|
40
|
+
// The same node set as the editor, so anything it writes renders here.
|
|
41
|
+
import { DEFAULT_NODES } from "./editorNodes.js";
|
|
42
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
43
|
+
const styles = {
|
|
44
|
+
root: {
|
|
45
|
+
kzqmXN: "xh8yej3",
|
|
46
|
+
kVAEAm: "x1n2onr6",
|
|
47
|
+
kMv6JI: "x9ynric",
|
|
48
|
+
kGuDYH: "xjm74w1",
|
|
49
|
+
kLWn49: "xw6l6zx",
|
|
50
|
+
kMwMTN: "x1tgivj0",
|
|
51
|
+
$$css: true
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lexical's `ContentEditable` renders `role="textbox"` and widget-only ARIA
|
|
57
|
+
* (`aria-autocomplete`, `aria-readonly`) even when the editor is not editable.
|
|
58
|
+
* A view renders document content, not a form field, so all three are cleared:
|
|
59
|
+
*
|
|
60
|
+
* - A textbox is a widget, so an unnamed one fails axe
|
|
61
|
+
* `aria-input-field-name` — and a view has no name to give, because it is
|
|
62
|
+
* content rather than a labelled input.
|
|
63
|
+
* - The widget role exposes the rendered document to assistive technology
|
|
64
|
+
* as a form field rather than as the content it is.
|
|
65
|
+
* - Widget-only ARIA is invalid without a widget role, so clearing only the
|
|
66
|
+
* role would trade `aria-input-field-name` for `aria-allowed-attr`.
|
|
67
|
+
*
|
|
68
|
+
* `role` must be `null`, not `undefined`: Lexical substitutes its `textbox`
|
|
69
|
+
* default for `undefined`. The ARIA keys reach the element through Lexical's
|
|
70
|
+
* trailing prop spread, which is why `undefined` clears them. A proposed
|
|
71
|
+
* upstream fix (facebook/lexical#9270) would make these overrides no-ops.
|
|
72
|
+
*
|
|
73
|
+
* A caller who wants read-only form-field semantics instead wants
|
|
74
|
+
* `<RichTextEditor isReadOnly />`, which is a labelled field by construction.
|
|
75
|
+
*/
|
|
76
|
+
const VIEW_CONTENT_EDITABLE_PROPS = {
|
|
77
|
+
'aria-autocomplete': undefined,
|
|
78
|
+
'aria-readonly': undefined,
|
|
79
|
+
role: null
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The view imports and exports no Markdown, so it uses no transformers. */
|
|
83
|
+
const NO_TRANSFORMERS = [];
|
|
84
|
+
/**
|
|
85
|
+
* Keeps the rendered content in sync with the `value` prop after mount.
|
|
86
|
+
*
|
|
87
|
+
* The editor's initial state is seeded once, when the composer mounts, so a
|
|
88
|
+
* plain `<RichTextView value={changingValue} />` would freeze at its first
|
|
89
|
+
* value — the content would never update when `value` changed. This plugin runs
|
|
90
|
+
* inside the composer context and re-applies `value` whenever it changes, so the
|
|
91
|
+
* read-only view stays reactive (e.g. previewing content edited elsewhere).
|
|
92
|
+
*
|
|
93
|
+
* The initial `value` is already applied via the extension's
|
|
94
|
+
* `$initialEditorState`, so we skip the first run to avoid a redundant re-parse
|
|
95
|
+
* on mount.
|
|
96
|
+
*
|
|
97
|
+
* This mirrors the canonical Lexical pattern for applying externally-sourced
|
|
98
|
+
* serialized state after mount: the Lexical Playground's ActionsPlugin does the
|
|
99
|
+
* same `editor.setEditorState(editor.parseEditorState(...))` from inside a
|
|
100
|
+
* plugin (see facebook/lexical
|
|
101
|
+
* packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx). It is
|
|
102
|
+
* necessary because the composer builds the editor once and reads the initial
|
|
103
|
+
* state exactly once at that point, so a changed prop cannot re-seed the editor
|
|
104
|
+
* on its own. A read-only view has no history, so we skip the Playground's
|
|
105
|
+
* accompanying CLEAR_HISTORY_COMMAND.
|
|
106
|
+
*
|
|
107
|
+
* `parseEditorState` / `setEditorState` are methods on the editor instance, so
|
|
108
|
+
* this plugin needs no module-level Lexical imports of its own.
|
|
109
|
+
*/
|
|
110
|
+
function SyncValuePlugin({
|
|
111
|
+
value
|
|
112
|
+
}) {
|
|
113
|
+
const [editor] = useLexicalComposerContext();
|
|
114
|
+
const isFirstRun = useRef(true);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (isFirstRun.current) {
|
|
117
|
+
isFirstRun.current = false;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
editor.setEditorState(editor.parseEditorState(value));
|
|
121
|
+
}, [editor, value]);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A read-only renderer for serialized Lexical content. Renders the same styled
|
|
127
|
+
* output as {@link RichTextEditor} without any editing affordances.
|
|
128
|
+
*
|
|
129
|
+
* The output is document content, not a form field: headings, lists and links
|
|
130
|
+
* keep their own roles for assistive technology, and the view takes no label.
|
|
131
|
+
* For a labelled, read-only field, use `<RichTextEditor isReadOnly />`. A
|
|
132
|
+
* table wider than the view scrolls inside a region named "Table" that takes a
|
|
133
|
+
* tab stop while it overflows, as core `Table` does.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```
|
|
137
|
+
* import {RichTextView} from '@blue-patterns/richtext';
|
|
138
|
+
* <RichTextView value={storedEditorStateJSON} />
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function RichTextView({
|
|
142
|
+
value,
|
|
143
|
+
nodes,
|
|
144
|
+
markdownExtensions,
|
|
145
|
+
codeLanguages,
|
|
146
|
+
plugins,
|
|
147
|
+
namespace = 'astryx-view',
|
|
148
|
+
onParseError,
|
|
149
|
+
errorFallback = null,
|
|
150
|
+
xstyle,
|
|
151
|
+
className,
|
|
152
|
+
style,
|
|
153
|
+
...rest
|
|
154
|
+
}) {
|
|
155
|
+
const themeRef = useRef(null);
|
|
156
|
+
if (themeRef.current === null) {
|
|
157
|
+
themeRef.current = sharedEditorTheme();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Built on first render (and again after an error, below) rather than per
|
|
161
|
+
// render: LexicalExtensionComposer re-creates the editor whenever the
|
|
162
|
+
// extension's identity changes, and `value` updates are applied in place by
|
|
163
|
+
// SyncValuePlugin instead.
|
|
164
|
+
const extensionRef = useRef(null);
|
|
165
|
+
const [hasError, setHasError] = useState(false);
|
|
166
|
+
|
|
167
|
+
// Reset the error state when the value changes so a corrected value recovers.
|
|
168
|
+
const lastValueRef = useRef(value);
|
|
169
|
+
if (lastValueRef.current !== value && hasError) {
|
|
170
|
+
lastValueRef.current = value;
|
|
171
|
+
setHasError(false);
|
|
172
|
+
} else {
|
|
173
|
+
lastValueRef.current = value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A parse failure is recorded during render (below, or from Lexical's
|
|
177
|
+
// `onError` while the composer builds the editor) but reported to the
|
|
178
|
+
// consumer only after commit. Updating this component's own state during
|
|
179
|
+
// render is allowed; running a consumer's callback is not — a callback that
|
|
180
|
+
// updates its own component would do so during another component's render,
|
|
181
|
+
// and StrictMode or a discarded concurrent render would repeat it or run it
|
|
182
|
+
// for output that never commits. This Effect synchronizes the consumer's
|
|
183
|
+
// `onParseError`, reporting each distinct failure exactly once.
|
|
184
|
+
const pendingErrorRef = useRef(null);
|
|
185
|
+
const reportedErrorRef = useRef(null);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const error = pendingErrorRef.current;
|
|
188
|
+
if (error !== null && error !== reportedErrorRef.current) {
|
|
189
|
+
reportedErrorRef.current = error;
|
|
190
|
+
onParseError?.(error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const handleError = error => {
|
|
194
|
+
pendingErrorRef.current = error;
|
|
195
|
+
setHasError(true);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Validate `value` parses as JSON before handing it to Lexical. Malformed
|
|
199
|
+
// JSON would otherwise throw synchronously while the composer builds the
|
|
200
|
+
// editor and escape any error boundary, taking down the host on the render
|
|
201
|
+
// path.
|
|
202
|
+
if (!hasError) {
|
|
203
|
+
try {
|
|
204
|
+
JSON.parse(value);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
handleError(err instanceof Error ? err : new Error(String(err)));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (hasError) {
|
|
210
|
+
// The composer subtree is unmounted while the fallback renders. Drop the
|
|
211
|
+
// extension so that recovering from the error builds a fresh one, seeded
|
|
212
|
+
// with the corrected `value`.
|
|
213
|
+
extensionRef.current = null;
|
|
214
|
+
return /*#__PURE__*/_jsx("div", {
|
|
215
|
+
...mergeProps(stylex.props(styles.root, xstyle), className, style),
|
|
216
|
+
...rest,
|
|
217
|
+
children: errorFallback
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (extensionRef.current === null) {
|
|
221
|
+
extensionRef.current = defineExtension({
|
|
222
|
+
name: '@blue-patterns/richtext/RichTextView',
|
|
223
|
+
namespace,
|
|
224
|
+
theme: themeRef.current,
|
|
225
|
+
editable: false,
|
|
226
|
+
nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
|
|
227
|
+
// Struck text is a deletion.
|
|
228
|
+
dependencies: [TextSemanticsExtension],
|
|
229
|
+
$initialEditorState: value,
|
|
230
|
+
// A read-only view renders persisted content; a bad node/schema should not
|
|
231
|
+
// crash the host. Surface it via onParseError + fallback instead of re-throwing.
|
|
232
|
+
onError: handleError
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return /*#__PURE__*/_jsx("div", {
|
|
236
|
+
...mergeProps(stylex.props(styles.root, xstyle), className, style),
|
|
237
|
+
...rest,
|
|
238
|
+
children: /*#__PURE__*/_jsxs(LexicalExtensionComposer, {
|
|
239
|
+
extension: extensionRef.current,
|
|
240
|
+
contentEditable: null,
|
|
241
|
+
children: [/*#__PURE__*/_jsx(SyncValuePlugin, {
|
|
242
|
+
value: value
|
|
243
|
+
}), /*#__PURE__*/_jsx(TablePlugin, {
|
|
244
|
+
hasCellMerge: false,
|
|
245
|
+
hasCellBackgroundColor: false,
|
|
246
|
+
hasTabHandler: false,
|
|
247
|
+
hasHorizontalScroll: true
|
|
248
|
+
}), /*#__PURE__*/_jsx(TableScrollRegionPlugin, {}), /*#__PURE__*/_jsx(TableColumnFloorPlugin, {}), /*#__PURE__*/_jsx(RichTextPlugin, {
|
|
249
|
+
contentEditable: /*#__PURE__*/_jsx(ContentEditable, {
|
|
250
|
+
...VIEW_CONTENT_EDITABLE_PROPS
|
|
251
|
+
}),
|
|
252
|
+
placeholder: null,
|
|
253
|
+
ErrorBoundary: LexicalErrorBoundary
|
|
254
|
+
}), /*#__PURE__*/_jsx(CodeBlockHeaderPlugin, {
|
|
255
|
+
codeLanguages: codeLanguages
|
|
256
|
+
}), /*#__PURE__*/_jsx(CodeSyntaxPlugin, {
|
|
257
|
+
codeLanguages: codeLanguages
|
|
258
|
+
}), markdownExtensions != null && markdownExtensions.length > 0 ? /*#__PURE__*/_jsx(MarkdownExtensionsPlugin, {
|
|
259
|
+
extensions: markdownExtensions,
|
|
260
|
+
transformers: NO_TRANSFORMERS
|
|
261
|
+
}) : null, /*#__PURE__*/_jsx(TaskCheckboxPlugin, {
|
|
262
|
+
isReadOnly: true
|
|
263
|
+
}), plugins]
|
|
264
|
+
})
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
RichTextView.displayName = 'RichTextView';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file codeLanguages.ts
|
|
3
|
+
* @input CodeLanguage values from @blue-patterns/core/CodeBlock
|
|
4
|
+
* @output listedLanguage, which finds the listed value a fence word names, and
|
|
5
|
+
* sameLanguages, which compares two lists entry by entry
|
|
6
|
+
* @position Shared by CodeSyntaxPlugin and CodeBlockHeaderPlugin, so RichText
|
|
7
|
+
* resolves a fence as Markdown does (component:Markdown FR30)
|
|
8
|
+
*/
|
|
9
|
+
import type { CodeLanguage } from '@blue-patterns/core/CodeBlock';
|
|
10
|
+
/** The listed value whose identifiers include `word`; a later entry wins. */
|
|
11
|
+
export declare function listedLanguage(languages: ReadonlyArray<CodeLanguage> | undefined, word: string): CodeLanguage | undefined;
|
|
12
|
+
/** Whether two lists name the same values in the same order. */
|
|
13
|
+
export declare function sameLanguages(a: ReadonlyArray<CodeLanguage> | undefined, b: ReadonlyArray<CodeLanguage> | undefined): boolean;
|
|
14
|
+
//# sourceMappingURL=codeLanguages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeLanguages.d.ts","sourceRoot":"","sources":["../src/codeLanguages.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,+BAA+B,CAAC;AAEhE,6EAA6E;AAC7E,wBAAgB,cAAc,CAC5B,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,SAAS,EAClD,IAAI,EAAE,MAAM,GACX,YAAY,GAAG,SAAS,CAU1B;AAED,gEAAgE;AAChE,wBAAgB,aAAa,CAC3B,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,SAAS,EAC1C,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,SAAS,GACzC,OAAO,CAQT"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file codeLanguages.ts
|
|
5
|
+
* @input CodeLanguage values from @blue-patterns/core/CodeBlock
|
|
6
|
+
* @output listedLanguage, which finds the listed value a fence word names, and
|
|
7
|
+
* sameLanguages, which compares two lists entry by entry
|
|
8
|
+
* @position Shared by CodeSyntaxPlugin and CodeBlockHeaderPlugin, so RichText
|
|
9
|
+
* resolves a fence as Markdown does (component:Markdown FR30)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The listed value whose identifiers include `word`; a later entry wins. */
|
|
13
|
+
export function listedLanguage(languages, word) {
|
|
14
|
+
if (languages == null) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
for (let i = languages.length - 1; i >= 0; i--) {
|
|
18
|
+
if (languages[i].identifiers.includes(word)) {
|
|
19
|
+
return languages[i];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether two lists name the same values in the same order. */
|
|
26
|
+
export function sameLanguages(a, b) {
|
|
27
|
+
if (a === b) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (a == null || b == null || a.length !== b.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return a.every((language, i) => language === b[i]);
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file codeLineTokens.ts
|
|
3
|
+
* @input Core CodeBlock's tokenizer (`tokenize`) and token line type.
|
|
4
|
+
* @output Exports CodeLineTokens, which keeps core CodeBlock's tokens for a
|
|
5
|
+
* block of code line by line and re-tokenizes only the lines an edit can
|
|
6
|
+
* change.
|
|
7
|
+
* @position Used by CodeSyntaxPlugin. Its tokens always equal what `tokenize`
|
|
8
|
+
* returns for the whole code. An edit re-tokenizes only its own line, and
|
|
9
|
+
* at most the line above it, for the built-in TypeScript, JavaScript,
|
|
10
|
+
* Python, and shell grammars while every line passes `standsAlone`;
|
|
11
|
+
* anything else re-tokenizes all of it. The checks follow those grammars'
|
|
12
|
+
* patterns in core CodeBlock's tokenizer.ts, and codeLineTokens.test.ts
|
|
13
|
+
* compares edits against a full `tokenize`, so a grammar change that
|
|
14
|
+
* breaks them fails there.
|
|
15
|
+
*/
|
|
16
|
+
import { type CodeLanguage, type TokenLine } from '@blue-patterns/core/CodeBlock';
|
|
17
|
+
/**
|
|
18
|
+
* Core CodeBlock's tokens for a block of code, kept line by line with
|
|
19
|
+
* line-relative offsets, exactly as `tokenize` returns them for the whole
|
|
20
|
+
* code.
|
|
21
|
+
*/
|
|
22
|
+
export declare class CodeLineTokens {
|
|
23
|
+
readonly language: string | CodeLanguage;
|
|
24
|
+
/** Where each line starts in the code. */
|
|
25
|
+
starts: number[];
|
|
26
|
+
/** Each line's tokens. */
|
|
27
|
+
tokens: TokenLine[];
|
|
28
|
+
private lines;
|
|
29
|
+
private readonly family;
|
|
30
|
+
/** Whether every line stands alone, so an edit re-tokenizes lines alone. */
|
|
31
|
+
private linesStandAlone;
|
|
32
|
+
constructor(text: string, language: string | CodeLanguage);
|
|
33
|
+
/** The code. */
|
|
34
|
+
get text(): string;
|
|
35
|
+
/** The line that holds `offset` in the code. */
|
|
36
|
+
lineAt(offset: number): number;
|
|
37
|
+
/**
|
|
38
|
+
* Replaces `removed` characters at `offset` in the code with `inserted`.
|
|
39
|
+
* Returns the lines it re-tokenized when the edit stays inside one line
|
|
40
|
+
* and every line still stands alone; otherwise it re-tokenizes all the
|
|
41
|
+
* code and returns null.
|
|
42
|
+
*/
|
|
43
|
+
replace(offset: number, removed: number, inserted: string): number[] | null;
|
|
44
|
+
/**
|
|
45
|
+
* A line's tokens as `tokenize` finds them in the whole code. A function
|
|
46
|
+
* name is a name followed by `(` past any whitespace, line breaks
|
|
47
|
+
* included, so when the line ends in a name, the code up to the next
|
|
48
|
+
* visible character is tokenized with it.
|
|
49
|
+
*/
|
|
50
|
+
private tokenizeLine;
|
|
51
|
+
private tokenizeAll;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=codeLineTokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeLineTokens.d.ts","sourceRoot":"","sources":["../src/codeLineTokens.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,+BAA+B,CAAC;AAkFvC;;;;GAIG;AACH,qBAAa,cAAc;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC;IACzC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,EAAE,CAAM;IACtB,0BAA0B;IAC1B,MAAM,EAAE,SAAS,EAAE,CAAM;IACzB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,4EAA4E;IAC5E,OAAO,CAAC,eAAe,CAAS;gBAEpB,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY;IAOzD,gBAAgB;IAChB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,gDAAgD;IAChD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAc9B;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IA+C3E;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,WAAW;CAgBpB"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file codeLineTokens.ts
|
|
5
|
+
* @input Core CodeBlock's tokenizer (`tokenize`) and token line type.
|
|
6
|
+
* @output Exports CodeLineTokens, which keeps core CodeBlock's tokens for a
|
|
7
|
+
* block of code line by line and re-tokenizes only the lines an edit can
|
|
8
|
+
* change.
|
|
9
|
+
* @position Used by CodeSyntaxPlugin. Its tokens always equal what `tokenize`
|
|
10
|
+
* returns for the whole code. An edit re-tokenizes only its own line, and
|
|
11
|
+
* at most the line above it, for the built-in TypeScript, JavaScript,
|
|
12
|
+
* Python, and shell grammars while every line passes `standsAlone`;
|
|
13
|
+
* anything else re-tokenizes all of it. The checks follow those grammars'
|
|
14
|
+
* patterns in core CodeBlock's tokenizer.ts, and codeLineTokens.test.ts
|
|
15
|
+
* compares edits against a full `tokenize`, so a grammar change that
|
|
16
|
+
* breaks them fails there.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { tokenize } from '@blue-patterns/core/CodeBlock';
|
|
20
|
+
const FAMILIES = new Map([['typescript', 'javascript'], ['javascript', 'javascript'], ['tsx', 'javascript'], ['jsx', 'javascript'], ['ts', 'javascript'], ['js', 'javascript'], ['python', 'python'], ['py', 'python'], ['bash', 'shell'], ['sh', 'shell'], ['zsh', 'shell'], ['shell', 'shell']]);
|
|
21
|
+
|
|
22
|
+
/** What can open a string or comment that runs on past its line. */
|
|
23
|
+
const OPENERS = {
|
|
24
|
+
javascript: /['"`]|\/\*/,
|
|
25
|
+
python: /['"]/,
|
|
26
|
+
shell: /['"`]|\$\{|\$\(|<</
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** A line whose last visible character can end a function name. */
|
|
30
|
+
const NAME_END = /[\w$]\s*$/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether the name that ends a line can change color with the code after
|
|
34
|
+
* it: its last visible character is a name character outside every string,
|
|
35
|
+
* comment, number, keyword, and constant, whose patterns come before
|
|
36
|
+
* function names.
|
|
37
|
+
*/
|
|
38
|
+
function endsInOpenName(text, tokens) {
|
|
39
|
+
const last = text.trimEnd().length - 1;
|
|
40
|
+
return NAME_END.test(text) && !tokens.some(token => token.start <= last && token.end > last && token.type !== 'function' && token.type !== 'type');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A line's first visible character, or '' when it has none. */
|
|
44
|
+
function firstVisible(line) {
|
|
45
|
+
return /\S/.exec(line)?.[0] ?? '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a line's strings and comments all close on it. Their patterns
|
|
50
|
+
* match line breaks, so an opener left open can run on to a later line.
|
|
51
|
+
* Only a string or comment the tokenizer emitted shields an opener: one that
|
|
52
|
+
* another token swallowed, such as `/*` in the operator `+/*`, still counts
|
|
53
|
+
* as open. Python tries triple quotes first, so `"""` can pair with another
|
|
54
|
+
* line even where its quotes close as single strings on this one.
|
|
55
|
+
*/
|
|
56
|
+
function standsAlone(text, tokens, family) {
|
|
57
|
+
if (family === 'python' && /'''|"""/.test(text)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const opener = OPENERS[family];
|
|
61
|
+
let position = 0;
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
if (token.end > text.length) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (token.type === 'string' || token.type === 'comment') {
|
|
67
|
+
if (opener.test(text.slice(position, token.start))) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
position = token.end;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return !opener.test(text.slice(position));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Core CodeBlock's tokens for a block of code, kept line by line with
|
|
78
|
+
* line-relative offsets, exactly as `tokenize` returns them for the whole
|
|
79
|
+
* code.
|
|
80
|
+
*/
|
|
81
|
+
export class CodeLineTokens {
|
|
82
|
+
/** Where each line starts in the code. */
|
|
83
|
+
starts = [];
|
|
84
|
+
/** Each line's tokens. */
|
|
85
|
+
tokens = [];
|
|
86
|
+
lines = [];
|
|
87
|
+
/** Whether every line stands alone, so an edit re-tokenizes lines alone. */
|
|
88
|
+
linesStandAlone = false;
|
|
89
|
+
constructor(text, language) {
|
|
90
|
+
this.language = language;
|
|
91
|
+
this.family = typeof language === 'string' ? FAMILIES.get(language) ?? null : null;
|
|
92
|
+
this.tokenizeAll(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The code. */
|
|
96
|
+
get text() {
|
|
97
|
+
return this.lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The line that holds `offset` in the code. */
|
|
101
|
+
lineAt(offset) {
|
|
102
|
+
let low = 0;
|
|
103
|
+
let high = this.starts.length - 1;
|
|
104
|
+
while (low < high) {
|
|
105
|
+
const mid = low + high + 1 >>> 1;
|
|
106
|
+
if (this.starts[mid] <= offset) {
|
|
107
|
+
low = mid;
|
|
108
|
+
} else {
|
|
109
|
+
high = mid - 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return low;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Replaces `removed` characters at `offset` in the code with `inserted`.
|
|
117
|
+
* Returns the lines it re-tokenized when the edit stays inside one line
|
|
118
|
+
* and every line still stands alone; otherwise it re-tokenizes all the
|
|
119
|
+
* code and returns null.
|
|
120
|
+
*/
|
|
121
|
+
replace(offset, removed, inserted) {
|
|
122
|
+
const line = this.lineAt(offset);
|
|
123
|
+
const before = this.lines[line];
|
|
124
|
+
const column = offset - this.starts[line];
|
|
125
|
+
if (this.family == null || !this.linesStandAlone || column + removed > before.length || inserted.includes('\n')) {
|
|
126
|
+
const text = this.text;
|
|
127
|
+
this.tokenizeAll(text.slice(0, offset) + inserted + text.slice(offset + removed));
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const after = before.slice(0, column) + inserted + before.slice(column + removed);
|
|
131
|
+
this.lines[line] = after;
|
|
132
|
+
const tokens = this.tokenizeLine(line);
|
|
133
|
+
if (!standsAlone(after, tokens, this.family)) {
|
|
134
|
+
this.tokenizeAll(this.text);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
this.tokens[line] = tokens;
|
|
138
|
+
for (let i = line + 1; i < this.starts.length; i++) {
|
|
139
|
+
this.starts[i] += inserted.length - removed;
|
|
140
|
+
}
|
|
141
|
+
const changed = [line];
|
|
142
|
+
// A name that ends an earlier line looks past blank lines for `(`, so a
|
|
143
|
+
// new first visible character here can change that name's color.
|
|
144
|
+
if (firstVisible(before) !== firstVisible(after)) {
|
|
145
|
+
let above = line - 1;
|
|
146
|
+
while (above >= 0 && firstVisible(this.lines[above]) === '') {
|
|
147
|
+
above--;
|
|
148
|
+
}
|
|
149
|
+
if (above >= 0 && endsInOpenName(this.lines[above], this.tokens[above] ?? [])) {
|
|
150
|
+
this.tokens[above] = this.tokenizeLine(above);
|
|
151
|
+
changed.unshift(above);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return changed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A line's tokens as `tokenize` finds them in the whole code. A function
|
|
159
|
+
* name is a name followed by `(` past any whitespace, line breaks
|
|
160
|
+
* included, so when the line ends in a name, the code up to the next
|
|
161
|
+
* visible character is tokenized with it.
|
|
162
|
+
*/
|
|
163
|
+
tokenizeLine(line) {
|
|
164
|
+
let text = this.lines[line];
|
|
165
|
+
if (NAME_END.test(text)) {
|
|
166
|
+
for (let next = line + 1; next < this.lines.length; next++) {
|
|
167
|
+
const visible = /\S/.exec(this.lines[next]);
|
|
168
|
+
text += '\n' + (visible == null ? this.lines[next] : this.lines[next].slice(0, visible.index + 1));
|
|
169
|
+
if (visible != null) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return tokenize(text, this.language)[0] ?? [];
|
|
175
|
+
}
|
|
176
|
+
tokenizeAll(text) {
|
|
177
|
+
this.lines = text.split('\n');
|
|
178
|
+
let start = 0;
|
|
179
|
+
this.starts = this.lines.map(line => {
|
|
180
|
+
const lineStart = start;
|
|
181
|
+
start += line.length + 1;
|
|
182
|
+
return lineStart;
|
|
183
|
+
});
|
|
184
|
+
this.tokens = tokenize(text, this.language);
|
|
185
|
+
const family = this.family;
|
|
186
|
+
this.linesStandAlone = family != null && this.lines.every((line, i) => standsAlone(line, this.tokens[i] ?? [], family));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Klass, LexicalNode } from 'lexical';
|
|
2
|
+
/**
|
|
3
|
+
* The default OSS node set registered with the editor: headings, quotes,
|
|
4
|
+
* lists, links, code, and tables. Extend via the `nodes` prop / option rather
|
|
5
|
+
* than editing this list.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_NODES: ReadonlyArray<Klass<LexicalNode>>;
|
|
8
|
+
//# sourceMappingURL=editorNodes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorNodes.d.ts","sourceRoot":"","sources":["../src/editorNodes.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAC,KAAK,EAAE,WAAW,EAAC,MAAM,SAAS,CAAC;AAGhD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAgB3D,CAAC"}
|