@byline/richtext-lexical 3.3.1 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ const DEFAULT_EDITOR_SETTINGS = {
9
9
  showTreeView: false,
10
10
  textAlignment: true,
11
11
  markdownShortcutPlugin: false,
12
+ markdownToggle: false,
12
13
  undoRedo: true,
13
14
  textStyle: true,
14
15
  inlineCode: true,
@@ -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 };
@@ -1,6 +1,7 @@
1
1
  .lexicalRichTextEditor {
2
2
  isolation: isolate;
3
3
  width: 100%;
4
+ min-width: 0;
4
5
  display: flex;
5
6
  }
6
7
 
@@ -15,6 +16,7 @@
15
16
 
16
17
  .lexicalRichTextEditor__wrap {
17
18
  width: 100%;
19
+ min-width: 0;
18
20
  position: relative;
19
21
  }
20
22
 
@@ -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(LexicalExtensionComposer, {
42
- extension: rootExtension,
43
- contentEditable: null,
44
- children: /*#__PURE__*/ jsxs("div", {
45
- className: "editor-shell",
46
- children: [
47
- beforeEditor,
48
- /*#__PURE__*/ jsx(Editor, {
49
- minHeight: props.minHeight,
50
- maxHeight: props.maxHeight
51
- }),
52
- afterEditor,
53
- children
54
- ]
55
- })
56
- }, composerKey + editable)
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
  });
@@ -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;
@@ -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: 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,154 @@
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: {
125
+ optional: true,
126
+ regExp: ADMONITION_END_REG_EXP
127
+ },
128
+ replace: (rootNode, children, startMatch, _endMatch, linesInBetween)=>{
129
+ const rawType = startMatch[1];
130
+ if (!ADMONITION_TYPES.has(rawType)) return false;
131
+ const admonitionType = rawType;
132
+ const title = startMatch[2] ?? '';
133
+ const node = $createAdmonitionNode({
134
+ admonitionType,
135
+ title
136
+ });
137
+ if (!children && null != linesInBetween) {
138
+ const body = linesInBetween.join('\n').trim();
139
+ if (body) node.__content.update(()=>{
140
+ $convertFromMarkdownString(body, ADMONITION_BODY_TRANSFORMERS);
141
+ }, {
142
+ discrete: true
143
+ });
144
+ }
145
+ rootNode.append(node);
146
+ },
147
+ type: 'multiline-element'
148
+ };
149
+ const BYLINE_TRANSFORMERS = [
150
+ TABLE,
151
+ ADMONITION,
152
+ ...TRANSFORMERS
153
+ ];
154
+ 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 52px;
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: absolute;
151
+ position: sticky;
144
152
  top: 0;
145
153
  left: 0;
146
154
  }
@@ -1,5 +1,6 @@
1
1
  :is(.wrapper-Kp9TyN, .byline-field-richtext) {
2
2
  flex: 1;
3
+ min-width: 0;
3
4
  height: 100%;
4
5
  display: flex;
5
6
  }
@@ -13,6 +14,7 @@
13
14
  flex-direction: column;
14
15
  flex: 1;
15
16
  gap: .25rem;
17
+ min-width: 0;
16
18
  display: flex;
17
19
  }
18
20
 
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.3.1",
6
+ "version": "3.4.1",
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/ui": "3.3.1",
81
- "@byline/admin": "3.3.1",
82
- "@byline/core": "3.3.1",
83
- "@byline/client": "3.3.1"
80
+ "@byline/client": "3.4.1",
81
+ "@byline/admin": "3.4.1",
82
+ "@byline/ui": "3.4.1",
83
+ "@byline/core": "3.4.1"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "react": "^19.0.0",
@@ -14,6 +14,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
14
14
  showTreeView: false,
15
15
  textAlignment: true,
16
16
  markdownShortcutPlugin: false,
17
+ markdownToggle: false,
17
18
  undoRedo: true,
18
19
  textStyle: true,
19
20
  inlineCode: true,
@@ -14,6 +14,7 @@ export type OptionName =
14
14
  | 'showTreeView'
15
15
  | 'textAlignment'
16
16
  | 'markdownShortcutPlugin'
17
+ | 'markdownToggle'
17
18
  | 'undoRedo'
18
19
  | 'textStyle'
19
20
  | 'inlineCode'