@byline/richtext-lexical 3.3.0 → 3.4.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.
- package/dist/field/config/default.js +1 -0
- package/dist/field/config/types.d.ts +1 -1
- package/dist/field/context/markdown-mode-context.d.ts +32 -0
- package/dist/field/context/markdown-mode-context.js +25 -0
- package/dist/field/editor-component.css +2 -0
- package/dist/field/editor-context.js +19 -16
- package/dist/field/editor.css +19 -0
- package/dist/field/editor.js +5 -2
- package/dist/field/hooks/use-markdown-toggle.d.ts +19 -0
- package/dist/field/hooks/use-markdown-toggle.js +102 -0
- package/dist/field/markdown/transformers.d.ts +30 -0
- package/dist/field/markdown/transformers.js +151 -0
- package/dist/field/plugins/toolbar-plugin/index.js +21 -1
- package/dist/field/themes/lexical-editor-theme.css +10 -2
- package/dist/richtext-field_module.css +2 -0
- package/package.json +5 -5
- package/src/field/config/default.ts +1 -0
- package/src/field/config/types.ts +1 -0
- package/src/field/context/markdown-mode-context.tsx +56 -0
- package/src/field/editor-component.css +6 -0
- package/src/field/editor-context.tsx +15 -12
- package/src/field/editor.css +27 -0
- package/src/field/editor.tsx +9 -2
- package/src/field/hooks/use-markdown-toggle.ts +142 -0
- package/src/field/markdown/transformers.ts +275 -0
- package/src/field/plugins/toolbar-plugin/index.tsx +20 -1
- package/src/field/themes/lexical-editor-theme.css +23 -4
- package/src/richtext-field.module.css +7 -0
|
@@ -7,7 +7,7 @@ import type { ExtensionsList } from './extensions-list';
|
|
|
7
7
|
* {@link EditorConfig.extensions} and is manipulated via the chainable
|
|
8
8
|
* `lexicalEditor((c) => c.extensions.add(...).remove(...))` API.
|
|
9
9
|
*/
|
|
10
|
-
export type OptionName = 'richText' | 'showTreeView' | 'textAlignment' | 'markdownShortcutPlugin' | 'undoRedo' | 'textStyle' | 'inlineCode' | 'debug';
|
|
10
|
+
export type OptionName = 'richText' | 'showTreeView' | 'textAlignment' | 'markdownShortcutPlugin' | 'markdownToggle' | 'undoRedo' | 'textStyle' | 'inlineCode' | 'debug';
|
|
11
11
|
export interface EditorSettings {
|
|
12
12
|
options: Record<OptionName, boolean>;
|
|
13
13
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
interface MarkdownModeContextValue {
|
|
10
|
+
/** React state — drives toolbar button active styling and re-render. */
|
|
11
|
+
isMarkdown: boolean;
|
|
12
|
+
setIsMarkdown: (value: boolean) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Synchronous mirror of {@link isMarkdown} for non-React readers — the
|
|
15
|
+
* editor's `OnChangePlugin` guard consults this to decide whether to
|
|
16
|
+
* suppress form persistence. React state updates are async, so the
|
|
17
|
+
* toggle handler writes the ref eagerly (before mutating the editor) to
|
|
18
|
+
* guarantee the markdown-source snapshot is never emitted to the form.
|
|
19
|
+
*/
|
|
20
|
+
markdownModeRef: React.MutableRefObject<boolean>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Holds the per-editor "view as markdown source" mode. Sits OUTSIDE the
|
|
24
|
+
* Lexical composer (alongside the shared on-change / history contexts) so
|
|
25
|
+
* both the in-composer editor surface and the toolbar button read the same
|
|
26
|
+
* mode — and so node decorators rendered as composer siblings can see it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function MarkdownModeProvider({ children, }: {
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}): React.JSX.Element;
|
|
31
|
+
export declare function useMarkdownMode(): MarkdownModeContextValue;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useMemo, useRef, useState } from "react";
|
|
4
|
+
const MarkdownModeContext = /*#__PURE__*/ createContext(null);
|
|
5
|
+
function MarkdownModeProvider({ children }) {
|
|
6
|
+
const [isMarkdown, setIsMarkdown] = useState(false);
|
|
7
|
+
const markdownModeRef = useRef(false);
|
|
8
|
+
const value = useMemo(()=>({
|
|
9
|
+
isMarkdown,
|
|
10
|
+
setIsMarkdown,
|
|
11
|
+
markdownModeRef
|
|
12
|
+
}), [
|
|
13
|
+
isMarkdown
|
|
14
|
+
]);
|
|
15
|
+
return /*#__PURE__*/ jsx(MarkdownModeContext.Provider, {
|
|
16
|
+
value: value,
|
|
17
|
+
children: children
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function useMarkdownMode() {
|
|
21
|
+
const ctx = useContext(MarkdownModeContext);
|
|
22
|
+
if (null == ctx) throw new Error('useMarkdownMode must be used within a MarkdownModeProvider');
|
|
23
|
+
return ctx;
|
|
24
|
+
}
|
|
25
|
+
export { MarkdownModeProvider, useMarkdownMode };
|
|
@@ -5,6 +5,7 @@ import { LexicalExtensionComposer } from "@lexical/react/LexicalExtensionCompose
|
|
|
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 { MarkdownModeProvider } from "./context/markdown-mode-context.js";
|
|
8
9
|
import { SharedHistoryContext } from "./context/shared-history-context.js";
|
|
9
10
|
import { SharedOnChangeContext } from "./context/shared-on-change-context.js";
|
|
10
11
|
import { Editor } from "./editor.js";
|
|
@@ -38,22 +39,24 @@ function EditorContext(props) {
|
|
|
38
39
|
children: /*#__PURE__*/ jsx(SharedOnChangeContext, {
|
|
39
40
|
onChange: onChange,
|
|
40
41
|
children: /*#__PURE__*/ jsx(SharedHistoryContext, {
|
|
41
|
-
children: /*#__PURE__*/ jsx(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
children: /*#__PURE__*/ jsx(MarkdownModeProvider, {
|
|
43
|
+
children: /*#__PURE__*/ jsx(LexicalExtensionComposer, {
|
|
44
|
+
extension: rootExtension,
|
|
45
|
+
contentEditable: null,
|
|
46
|
+
children: /*#__PURE__*/ jsxs("div", {
|
|
47
|
+
className: "editor-shell",
|
|
48
|
+
children: [
|
|
49
|
+
beforeEditor,
|
|
50
|
+
/*#__PURE__*/ jsx(Editor, {
|
|
51
|
+
minHeight: props.minHeight,
|
|
52
|
+
maxHeight: props.maxHeight
|
|
53
|
+
}),
|
|
54
|
+
afterEditor,
|
|
55
|
+
children
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
}, composerKey + editable)
|
|
59
|
+
})
|
|
57
60
|
})
|
|
58
61
|
})
|
|
59
62
|
});
|
package/dist/field/editor.css
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
resize: vertical;
|
|
39
39
|
z-index: -1;
|
|
40
40
|
flex: auto;
|
|
41
|
+
min-width: 0;
|
|
41
42
|
max-width: 100%;
|
|
42
43
|
position: relative;
|
|
43
44
|
}
|
|
@@ -1223,6 +1224,24 @@ button.toolbar-item.active i {
|
|
|
1223
1224
|
opacity: 1;
|
|
1224
1225
|
}
|
|
1225
1226
|
|
|
1227
|
+
button.toolbar-item .markdown-toggle-label {
|
|
1228
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
1229
|
+
text-align: center;
|
|
1230
|
+
color: #777;
|
|
1231
|
+
width: 18px;
|
|
1232
|
+
font-size: 15px;
|
|
1233
|
+
font-weight: 700;
|
|
1234
|
+
line-height: 18px;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
button.toolbar-item.active .markdown-toggle-label {
|
|
1238
|
+
color: #1a1a1a;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
button.toolbar-item:disabled .markdown-toggle-label {
|
|
1242
|
+
color: #bbb;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1226
1245
|
.toolbar-item.font-family .text {
|
|
1227
1246
|
max-width: 40px;
|
|
1228
1247
|
display: block;
|
package/dist/field/editor.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { TRANSFORMERS } from "@lexical/markdown";
|
|
5
4
|
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
|
6
5
|
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
7
6
|
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
|
@@ -11,11 +10,13 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
|
11
10
|
import { useExtensionDependency, useOptionalExtensionDependency } from "@lexical/react/useExtensionComponent";
|
|
12
11
|
import { useEditorConfig } from "./config/editor-config-context.js";
|
|
13
12
|
import { ContentEditable } from "./content-editable.js";
|
|
13
|
+
import { useMarkdownMode } from "./context/markdown-mode-context.js";
|
|
14
14
|
import { useSharedHistoryContext } from "./context/shared-history-context.js";
|
|
15
15
|
import { useSharedOnChange } from "./context/shared-on-change-context.js";
|
|
16
16
|
import { Debug } from "./debug.js";
|
|
17
17
|
import { BylineFloatingUIExtension, selectFloatingUIItems } from "./extensions/byline-floating-ui/byline-floating-ui-extension.js";
|
|
18
18
|
import { TableExtension } from "./extensions/table/table-extension.js";
|
|
19
|
+
import { BYLINE_TRANSFORMERS } from "./markdown/transformers.js";
|
|
19
20
|
import { TablePlugin } from "./plugins/table-plugin/index.js";
|
|
20
21
|
import { ToolbarPlugin } from "./plugins/toolbar-plugin/index.js";
|
|
21
22
|
import { TreeViewPlugin } from "./plugins/treeview-plugin/index.js";
|
|
@@ -31,6 +32,7 @@ const editor_Editor = /*#__PURE__*/ memo(function({ minHeight, maxHeight }) {
|
|
|
31
32
|
}))[0];
|
|
32
33
|
const { onChange } = useSharedOnChange();
|
|
33
34
|
const { historyState } = useSharedHistoryContext();
|
|
35
|
+
const { markdownModeRef } = useMarkdownMode();
|
|
34
36
|
const { config: { options: { debug, richText, showTreeView, markdownShortcutPlugin }, placeholderText } } = useEditorConfig();
|
|
35
37
|
const hasTableExtension = void 0 !== useOptionalExtensionDependency(TableExtension);
|
|
36
38
|
const floatingUIItems = selectFloatingUIItems(useExtensionDependency(BylineFloatingUIExtension).config.items);
|
|
@@ -91,6 +93,7 @@ const editor_Editor = /*#__PURE__*/ memo(function({ minHeight, maxHeight }) {
|
|
|
91
93
|
ignoreSelectionChange: true,
|
|
92
94
|
onChange: (editorState, editor, tags)=>{
|
|
93
95
|
if (tags.has(APPLY_VALUE_TAG)) return;
|
|
96
|
+
if (markdownModeRef.current) return;
|
|
94
97
|
if (!tags.has('focus') || tags.size > 1) {
|
|
95
98
|
if (null != onChange) onChange(editorState, editor, tags);
|
|
96
99
|
}
|
|
@@ -109,7 +112,7 @@ const editor_Editor = /*#__PURE__*/ memo(function({ minHeight, maxHeight }) {
|
|
|
109
112
|
externalHistoryState: historyState
|
|
110
113
|
}),
|
|
111
114
|
markdownShortcutPlugin && /*#__PURE__*/ jsx(MarkdownShortcutPlugin, {
|
|
112
|
-
transformers:
|
|
115
|
+
transformers: BYLINE_TRANSFORMERS
|
|
113
116
|
}),
|
|
114
117
|
null != floatingAnchorElem && !isSmallWidthViewport && floatingUIItems.map(({ id, Component })=>/*#__PURE__*/ jsx(Component, {
|
|
115
118
|
anchorElem: floatingAnchorElem
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document-level "view as markdown source" toggle for a single editor.
|
|
3
|
+
*
|
|
4
|
+
* The editor is bound to a Byline form field that accumulates
|
|
5
|
+
* `DocumentPatch[]`, so this hook is deliberate about persistence:
|
|
6
|
+
*
|
|
7
|
+
* - While in markdown mode the surface is a single `CodeNode` of raw
|
|
8
|
+
* markdown text. `markdownModeRef` suppresses the editor's
|
|
9
|
+
* `OnChangePlugin` so none of those keystrokes reach the form.
|
|
10
|
+
* - A pure round-trip (WYSIWYG → markdown → WYSIWYG with no edits)
|
|
11
|
+
* restores the *exact* captured `EditorState` — identical serialized
|
|
12
|
+
* state, so the form sees no change and records **no patch**.
|
|
13
|
+
* - Edits made in markdown produce a single conversion back to rich
|
|
14
|
+
* nodes on exit, emitting one field value change → **one patch**.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useMarkdownToggle(): {
|
|
17
|
+
isMarkdown: boolean;
|
|
18
|
+
toggleMarkdown: () => void;
|
|
19
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { $createCodeNode, $isCodeNode } from "@lexical/code";
|
|
4
|
+
import { $convertFromMarkdownString, $convertToMarkdownString } from "@lexical/markdown";
|
|
5
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
6
|
+
import { $getRoot, RootNode } from "lexical";
|
|
7
|
+
import { APPLY_VALUE_TAG } from "../constants.js";
|
|
8
|
+
import { useMarkdownMode } from "../context/markdown-mode-context.js";
|
|
9
|
+
import { BYLINE_TRANSFORMERS } from "../markdown/transformers.js";
|
|
10
|
+
const MARKDOWN_LANGUAGE = 'markdown';
|
|
11
|
+
function normalize(markdown) {
|
|
12
|
+
return markdown.replace(/\n+$/, '');
|
|
13
|
+
}
|
|
14
|
+
function useMarkdownToggle() {
|
|
15
|
+
const [editor] = useLexicalComposerContext();
|
|
16
|
+
const { isMarkdown, setIsMarkdown, markdownModeRef } = useMarkdownMode();
|
|
17
|
+
const originalEditorStateRef = useRef(null);
|
|
18
|
+
const originalMarkdownRef = useRef('');
|
|
19
|
+
const unregisterTransformRef = useRef(null);
|
|
20
|
+
const registerRootGuard = useCallback(()=>{
|
|
21
|
+
unregisterTransformRef.current?.();
|
|
22
|
+
unregisterTransformRef.current = editor.registerNodeTransform(RootNode, (rootNode)=>{
|
|
23
|
+
let codeNode = rootNode.getChildren().find($isCodeNode);
|
|
24
|
+
if (null == codeNode) codeNode = $createCodeNode(MARKDOWN_LANGUAGE);
|
|
25
|
+
if (1 !== rootNode.getChildrenSize() || null == codeNode.getParent()) {
|
|
26
|
+
rootNode.splice(0, rootNode.getChildrenSize(), [
|
|
27
|
+
codeNode
|
|
28
|
+
]);
|
|
29
|
+
codeNode.selectEnd();
|
|
30
|
+
}
|
|
31
|
+
if (codeNode.getLanguage() !== MARKDOWN_LANGUAGE) codeNode.setLanguage(MARKDOWN_LANGUAGE);
|
|
32
|
+
});
|
|
33
|
+
}, [
|
|
34
|
+
editor
|
|
35
|
+
]);
|
|
36
|
+
const clearRootGuard = useCallback(()=>{
|
|
37
|
+
unregisterTransformRef.current?.();
|
|
38
|
+
unregisterTransformRef.current = null;
|
|
39
|
+
}, []);
|
|
40
|
+
const enterMarkdown = useCallback(()=>{
|
|
41
|
+
originalEditorStateRef.current = editor.getEditorState();
|
|
42
|
+
markdownModeRef.current = true;
|
|
43
|
+
editor.update(()=>{
|
|
44
|
+
const markdown = $convertToMarkdownString(BYLINE_TRANSFORMERS, void 0, true);
|
|
45
|
+
originalMarkdownRef.current = markdown;
|
|
46
|
+
const codeNode = $createCodeNode(MARKDOWN_LANGUAGE);
|
|
47
|
+
$getRoot().clear().append(codeNode);
|
|
48
|
+
codeNode.select().insertRawText(markdown);
|
|
49
|
+
});
|
|
50
|
+
registerRootGuard();
|
|
51
|
+
setIsMarkdown(true);
|
|
52
|
+
}, [
|
|
53
|
+
editor,
|
|
54
|
+
markdownModeRef,
|
|
55
|
+
registerRootGuard,
|
|
56
|
+
setIsMarkdown
|
|
57
|
+
]);
|
|
58
|
+
const exitMarkdown = useCallback(()=>{
|
|
59
|
+
clearRootGuard();
|
|
60
|
+
let currentMarkdown = '';
|
|
61
|
+
editor.read(()=>{
|
|
62
|
+
const first = $getRoot().getFirstChild();
|
|
63
|
+
currentMarkdown = $isCodeNode(first) ? first.getTextContent() : $getRoot().getTextContent();
|
|
64
|
+
});
|
|
65
|
+
const edited = normalize(currentMarkdown) !== normalize(originalMarkdownRef.current);
|
|
66
|
+
if (edited || null == originalEditorStateRef.current) {
|
|
67
|
+
markdownModeRef.current = false;
|
|
68
|
+
editor.update(()=>{
|
|
69
|
+
$convertFromMarkdownString(currentMarkdown, BYLINE_TRANSFORMERS, void 0, true);
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
editor.setEditorState(originalEditorStateRef.current, {
|
|
73
|
+
tag: APPLY_VALUE_TAG
|
|
74
|
+
});
|
|
75
|
+
markdownModeRef.current = false;
|
|
76
|
+
}
|
|
77
|
+
originalEditorStateRef.current = null;
|
|
78
|
+
originalMarkdownRef.current = '';
|
|
79
|
+
setIsMarkdown(false);
|
|
80
|
+
}, [
|
|
81
|
+
editor,
|
|
82
|
+
clearRootGuard,
|
|
83
|
+
markdownModeRef,
|
|
84
|
+
setIsMarkdown
|
|
85
|
+
]);
|
|
86
|
+
const toggleMarkdown = useCallback(()=>{
|
|
87
|
+
if (markdownModeRef.current) exitMarkdown();
|
|
88
|
+
else enterMarkdown();
|
|
89
|
+
}, [
|
|
90
|
+
markdownModeRef,
|
|
91
|
+
enterMarkdown,
|
|
92
|
+
exitMarkdown
|
|
93
|
+
]);
|
|
94
|
+
useEffect(()=>()=>clearRootGuard(), [
|
|
95
|
+
clearRootGuard
|
|
96
|
+
]);
|
|
97
|
+
return {
|
|
98
|
+
isMarkdown,
|
|
99
|
+
toggleMarkdown
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export { useMarkdownToggle };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Byline markdown transformers.
|
|
10
|
+
*
|
|
11
|
+
* Extends the stock `@lexical/markdown` `TRANSFORMERS` with handlers for
|
|
12
|
+
* Byline's custom nodes, so the document-level markdown toggle (and the
|
|
13
|
+
* inline markdown-shortcut plugin) round-trip them instead of dropping
|
|
14
|
+
* them. The `TABLE` transformer is adapted from the Lexical playground's
|
|
15
|
+
* `MarkdownTransformers` (GFM pipe tables).
|
|
16
|
+
*
|
|
17
|
+
* Used in the browser editor only (the toggle + shortcuts). Server-side
|
|
18
|
+
* markdown export walks the serialized JSON via its own serializer and
|
|
19
|
+
* does not use this module.
|
|
20
|
+
*/
|
|
21
|
+
import { type ElementTransformer, type MultilineElementTransformer, type Transformer } from '@lexical/markdown';
|
|
22
|
+
export declare const TABLE: ElementTransformer;
|
|
23
|
+
export declare const ADMONITION: MultilineElementTransformer;
|
|
24
|
+
/**
|
|
25
|
+
* Stock transformers plus Byline's custom-node handlers. Self-referenced by
|
|
26
|
+
* the `TABLE` transformer (cells are converted with the same set), so it
|
|
27
|
+
* must be declared after `TABLE` — the references live inside callbacks that
|
|
28
|
+
* run well after module init, so there is no TDZ hazard.
|
|
29
|
+
*/
|
|
30
|
+
export declare const BYLINE_TRANSFORMERS: Array<Transformer>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, TEXT_FORMAT_TRANSFORMERS, TRANSFORMERS } from "@lexical/markdown";
|
|
2
|
+
import { $createTableCellNode, $createTableNode, $createTableRowNode, $isTableCellNode, $isTableNode, $isTableRowNode, TableCellHeaderStates, TableCellNode, TableNode, TableRowNode } from "@lexical/table";
|
|
3
|
+
import { $isParagraphNode, $isTextNode } from "lexical";
|
|
4
|
+
import { $createAdmonitionNode, $isAdmonitionNode, AdmonitionNode } from "../extensions/admonition/admonition-node.js";
|
|
5
|
+
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
|
|
6
|
+
const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-+:? ?)+\|?\s?$/;
|
|
7
|
+
function isTableRowDivider(row) {
|
|
8
|
+
return TABLE_ROW_DIVIDER_REG_EXP.test(row);
|
|
9
|
+
}
|
|
10
|
+
const TABLE = {
|
|
11
|
+
dependencies: [
|
|
12
|
+
TableNode,
|
|
13
|
+
TableRowNode,
|
|
14
|
+
TableCellNode
|
|
15
|
+
],
|
|
16
|
+
export: (node)=>{
|
|
17
|
+
if (!$isTableNode(node)) return null;
|
|
18
|
+
const output = [];
|
|
19
|
+
for (const row of node.getChildren()){
|
|
20
|
+
const rowOutput = [];
|
|
21
|
+
if (!$isTableRowNode(row)) continue;
|
|
22
|
+
let isHeaderRow = false;
|
|
23
|
+
for (const cell of row.getChildren())if ($isTableCellNode(cell)) {
|
|
24
|
+
rowOutput.push($convertToMarkdownString(BYLINE_TRANSFORMERS, cell).replace(/\n/g, '\\n').trim());
|
|
25
|
+
if (cell.hasHeaderState(TableCellHeaderStates.ROW)) isHeaderRow = true;
|
|
26
|
+
}
|
|
27
|
+
output.push(`| ${rowOutput.join(' | ')} |`);
|
|
28
|
+
if (isHeaderRow) output.push(`| ${rowOutput.map(()=>'---').join(' | ')} |`);
|
|
29
|
+
}
|
|
30
|
+
return output.join('\n');
|
|
31
|
+
},
|
|
32
|
+
regExp: TABLE_ROW_REG_EXP,
|
|
33
|
+
replace: (parentNode, _children, match)=>{
|
|
34
|
+
if (isTableRowDivider(match[0])) {
|
|
35
|
+
const table = parentNode.getPreviousSibling();
|
|
36
|
+
if (!table || !$isTableNode(table)) return;
|
|
37
|
+
const rows = table.getChildren();
|
|
38
|
+
const lastRow = rows[rows.length - 1];
|
|
39
|
+
if (!lastRow || !$isTableRowNode(lastRow)) return;
|
|
40
|
+
lastRow.getChildren().forEach((cell)=>{
|
|
41
|
+
if (!$isTableCellNode(cell)) return;
|
|
42
|
+
cell.setHeaderStyles(TableCellHeaderStates.ROW, TableCellHeaderStates.ROW);
|
|
43
|
+
});
|
|
44
|
+
parentNode.remove();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const matchCells = mapToTableCells(match[0]);
|
|
48
|
+
if (null == matchCells) return;
|
|
49
|
+
const rows = [
|
|
50
|
+
matchCells
|
|
51
|
+
];
|
|
52
|
+
let sibling = parentNode.getPreviousSibling();
|
|
53
|
+
let maxCells = matchCells.length;
|
|
54
|
+
while(sibling){
|
|
55
|
+
if (!$isParagraphNode(sibling)) break;
|
|
56
|
+
if (1 !== sibling.getChildrenSize()) break;
|
|
57
|
+
const firstChild = sibling.getFirstChild();
|
|
58
|
+
if (!$isTextNode(firstChild)) break;
|
|
59
|
+
const cells = mapToTableCells(firstChild.getTextContent());
|
|
60
|
+
if (null == cells) break;
|
|
61
|
+
maxCells = Math.max(maxCells, cells.length);
|
|
62
|
+
rows.unshift(cells);
|
|
63
|
+
const previousSibling = sibling.getPreviousSibling();
|
|
64
|
+
sibling.remove();
|
|
65
|
+
sibling = previousSibling;
|
|
66
|
+
}
|
|
67
|
+
const table = $createTableNode();
|
|
68
|
+
for (const cells of rows){
|
|
69
|
+
const tableRow = $createTableRowNode();
|
|
70
|
+
table.append(tableRow);
|
|
71
|
+
for(let i = 0; i < maxCells; i++)tableRow.append(i < cells.length ? cells[i] : $createTableCell(''));
|
|
72
|
+
}
|
|
73
|
+
const previousSibling = parentNode.getPreviousSibling();
|
|
74
|
+
if ($isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) {
|
|
75
|
+
previousSibling.append(...table.getChildren());
|
|
76
|
+
parentNode.remove();
|
|
77
|
+
} else parentNode.replace(table);
|
|
78
|
+
table.selectEnd();
|
|
79
|
+
},
|
|
80
|
+
type: 'element'
|
|
81
|
+
};
|
|
82
|
+
function getTableColumnsSize(table) {
|
|
83
|
+
const row = table.getFirstChild();
|
|
84
|
+
return $isTableRowNode(row) ? row.getChildrenSize() : 0;
|
|
85
|
+
}
|
|
86
|
+
const $createTableCell = (textContent)=>{
|
|
87
|
+
const content = textContent.replace(/\\n/g, '\n');
|
|
88
|
+
const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
|
|
89
|
+
$convertFromMarkdownString(content, BYLINE_TRANSFORMERS, cell);
|
|
90
|
+
return cell;
|
|
91
|
+
};
|
|
92
|
+
const mapToTableCells = (textContent)=>{
|
|
93
|
+
const match = textContent.match(TABLE_ROW_REG_EXP);
|
|
94
|
+
if (!match?.[1]) return null;
|
|
95
|
+
return match[1].split('|').map((text)=>$createTableCell(text));
|
|
96
|
+
};
|
|
97
|
+
const ADMONITION_TYPES = new Set([
|
|
98
|
+
'note',
|
|
99
|
+
'tip',
|
|
100
|
+
'warning',
|
|
101
|
+
'danger'
|
|
102
|
+
]);
|
|
103
|
+
const ADMONITION_START_REG_EXP = /^:::(note|tip|warning|danger)(?:\[([^\]]*)\])?\s*$/;
|
|
104
|
+
const ADMONITION_END_REG_EXP = /^:::\s*$/;
|
|
105
|
+
const ADMONITION_BODY_TRANSFORMERS = [
|
|
106
|
+
...TEXT_FORMAT_TRANSFORMERS
|
|
107
|
+
];
|
|
108
|
+
const ADMONITION = {
|
|
109
|
+
dependencies: [
|
|
110
|
+
AdmonitionNode
|
|
111
|
+
],
|
|
112
|
+
export: (node)=>{
|
|
113
|
+
if (!$isAdmonitionNode(node)) return null;
|
|
114
|
+
const type = node.getAdmonitionType();
|
|
115
|
+
const title = node.getTitle();
|
|
116
|
+
let body = '';
|
|
117
|
+
node.__content.read(()=>{
|
|
118
|
+
body = $convertToMarkdownString(ADMONITION_BODY_TRANSFORMERS).trim();
|
|
119
|
+
});
|
|
120
|
+
const heading = title ? `:::${type}[${title}]` : `:::${type}`;
|
|
121
|
+
return body ? `${heading}\n${body}\n:::` : `${heading}\n:::`;
|
|
122
|
+
},
|
|
123
|
+
regExpStart: ADMONITION_START_REG_EXP,
|
|
124
|
+
regExpEnd: ADMONITION_END_REG_EXP,
|
|
125
|
+
replace: (rootNode, children, startMatch, _endMatch, linesInBetween)=>{
|
|
126
|
+
const rawType = startMatch[1];
|
|
127
|
+
if (!ADMONITION_TYPES.has(rawType)) return false;
|
|
128
|
+
const admonitionType = rawType;
|
|
129
|
+
const title = startMatch[2] ?? '';
|
|
130
|
+
const node = $createAdmonitionNode({
|
|
131
|
+
admonitionType,
|
|
132
|
+
title
|
|
133
|
+
});
|
|
134
|
+
if (!children && null != linesInBetween) {
|
|
135
|
+
const body = linesInBetween.join('\n').trim();
|
|
136
|
+
if (body) node.__content.update(()=>{
|
|
137
|
+
$convertFromMarkdownString(body, ADMONITION_BODY_TRANSFORMERS);
|
|
138
|
+
}, {
|
|
139
|
+
discrete: true
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
rootNode.append(node);
|
|
143
|
+
},
|
|
144
|
+
type: 'multiline-element'
|
|
145
|
+
};
|
|
146
|
+
const BYLINE_TRANSFORMERS = [
|
|
147
|
+
TABLE,
|
|
148
|
+
ADMONITION,
|
|
149
|
+
...TRANSFORMERS
|
|
150
|
+
];
|
|
151
|
+
export { ADMONITION, BYLINE_TRANSFORMERS, TABLE };
|
|
@@ -14,6 +14,7 @@ import { $createParagraphNode, $getNodeByKey, $getSelection, $isRangeSelection,
|
|
|
14
14
|
import { useEditorConfig } from "../../config/editor-config-context.js";
|
|
15
15
|
import { BylineToolbarExtension, selectToolbarItems } from "../../extensions/byline-toolbar/byline-toolbar-extension.js";
|
|
16
16
|
import { $isLinkNode, LinkExtension, OPEN_LINK_MODAL_COMMAND, TOGGLE_LINK_COMMAND } from "../../extensions/link/index.js";
|
|
17
|
+
import { useMarkdownToggle } from "../../hooks/use-markdown-toggle.js";
|
|
17
18
|
import { IS_APPLE } from "../../shared/environment.js";
|
|
18
19
|
import { DropDown, DropDownItem } from "../../ui/dropdown.js";
|
|
19
20
|
import { getSelectedNode } from "../../utils/getSelectedNode.js";
|
|
@@ -283,7 +284,8 @@ function ToolbarPlugin() {
|
|
|
283
284
|
toolbarConfig.items
|
|
284
285
|
]);
|
|
285
286
|
const hasLinkExtension = void 0 !== useOptionalExtensionDependency(LinkExtension);
|
|
286
|
-
const { uuid, config: { options: { textAlignment, undoRedo, textStyle, inlineCode } } } = useEditorConfig();
|
|
287
|
+
const { uuid, config: { options: { textAlignment, undoRedo, textStyle, inlineCode, markdownToggle } } } = useEditorConfig();
|
|
288
|
+
const { isMarkdown, toggleMarkdown } = useMarkdownToggle();
|
|
287
289
|
const $updateToolbar = useCallback(()=>{
|
|
288
290
|
const selection = $getSelection();
|
|
289
291
|
if ($isRangeSelection(selection)) {
|
|
@@ -756,6 +758,24 @@ function ToolbarPlugin() {
|
|
|
756
758
|
children: trailingToolbarItems.map((item)=>/*#__PURE__*/ jsx(external_react_Fragment, {
|
|
757
759
|
children: item.node
|
|
758
760
|
}, item.id))
|
|
761
|
+
}),
|
|
762
|
+
markdownToggle && /*#__PURE__*/ jsxs(Fragment, {
|
|
763
|
+
children: [
|
|
764
|
+
/*#__PURE__*/ jsx(Divider, {}),
|
|
765
|
+
/*#__PURE__*/ jsx("button", {
|
|
766
|
+
type: "button",
|
|
767
|
+
disabled: !isEditable,
|
|
768
|
+
onClick: toggleMarkdown,
|
|
769
|
+
className: `toolbar-item spaced markdown-toggle ${isMarkdown ? 'active' : ''}`,
|
|
770
|
+
title: isMarkdown ? 'Convert from Markdown' : 'Convert to Markdown',
|
|
771
|
+
"aria-label": isMarkdown ? 'Convert from markdown' : 'Convert to markdown',
|
|
772
|
+
"aria-pressed": isMarkdown,
|
|
773
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
774
|
+
className: "markdown-toggle-label",
|
|
775
|
+
children: "M"
|
|
776
|
+
})
|
|
777
|
+
})
|
|
778
|
+
]
|
|
759
779
|
})
|
|
760
780
|
]
|
|
761
781
|
});
|
|
@@ -120,9 +120,12 @@ html[data-theme="dark"] .LexicalEditorTheme__textCode, .dark .LexicalEditorTheme
|
|
|
120
120
|
.LexicalEditorTheme__code {
|
|
121
121
|
-moz-tab-size: 2;
|
|
122
122
|
tab-size: 2;
|
|
123
|
+
white-space: pre;
|
|
124
|
+
box-sizing: border-box;
|
|
123
125
|
background-color: #f0f2f5;
|
|
126
|
+
max-width: 100%;
|
|
124
127
|
margin: 8px 0;
|
|
125
|
-
padding: 8px 8px 8px
|
|
128
|
+
padding: 8px 8px 8px 0;
|
|
126
129
|
font-family: Menlo, Consolas, Monaco, monospace;
|
|
127
130
|
font-size: 15px;
|
|
128
131
|
line-height: 1.53;
|
|
@@ -133,14 +136,19 @@ html[data-theme="dark"] .LexicalEditorTheme__textCode, .dark .LexicalEditorTheme
|
|
|
133
136
|
|
|
134
137
|
.LexicalEditorTheme__code:before {
|
|
135
138
|
content: attr(data-gutter);
|
|
139
|
+
float: left;
|
|
140
|
+
z-index: 3;
|
|
136
141
|
color: #777;
|
|
137
142
|
white-space: pre-wrap;
|
|
138
143
|
text-align: right;
|
|
139
144
|
background-color: #eee;
|
|
140
145
|
border-right: 1px solid #ccc;
|
|
141
146
|
min-width: 25px;
|
|
147
|
+
min-height: 100%;
|
|
148
|
+
margin-top: -8px;
|
|
149
|
+
margin-right: 10px;
|
|
142
150
|
padding: 8px;
|
|
143
|
-
position:
|
|
151
|
+
position: sticky;
|
|
144
152
|
top: 0;
|
|
145
153
|
left: 0;
|
|
146
154
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.4.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
"npm-run-all": "^4.1.5",
|
|
78
78
|
"prism-react-renderer": "^2.4.1",
|
|
79
79
|
"react-error-boundary": "^6.1.1",
|
|
80
|
-
"@byline/
|
|
81
|
-
"@byline/
|
|
82
|
-
"@byline/
|
|
83
|
-
"@byline/
|
|
80
|
+
"@byline/client": "3.4.0",
|
|
81
|
+
"@byline/core": "3.4.0",
|
|
82
|
+
"@byline/ui": "3.4.0",
|
|
83
|
+
"@byline/admin": "3.4.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"react": "^19.0.0",
|