@astryxdesign/lab 0.2.0-canary.33d4e98 → 0.2.0-canary.4ffab6f

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.
@@ -57,6 +57,12 @@ export interface RichTextEditorRef {
57
57
  * `$convertToMarkdownString` run in a read context.
58
58
  */
59
59
  getMarkdown: () => string;
60
+ /**
61
+ * Serialize the current content to an HTML string via
62
+ * `$generateHtmlFromNodes`. Requires a DOM (available in the browser and in
63
+ * jsdom-based tests). Useful for copy/paste, email, or non-Lexical consumers.
64
+ */
65
+ getHTML: () => string;
60
66
  /**
61
67
  * Access the underlying `LexicalEditor` instance for advanced use cases
62
68
  * (custom commands, listeners, node transforms).
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAQL,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAgBf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAElD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAkBxD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAKnD,OAAO,KAAK,EACV,WAAW,EACX,KAAK,EACL,aAAa,EACb,WAAW,EAEZ,MAAM,SAAS,CAAC;AAoHjB,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvE,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,6EAA6E;IAC7E,cAAc,EAAE,MAAM,WAAW,CAAC;IAClC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,MAAM,CAAC;IAC1B;;;OAGG;IACH,SAAS,EAAE,MAAM,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAC/C,SAAS,EACT,UAAU,GAAG,cAAc,CAC5B;IACC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACrE,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC1C,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,cAAc,mHAuLzB,CAAC"}
1
+ {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"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"}
@@ -44,10 +44,8 @@ import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin
44
44
  import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
45
45
  import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
46
46
  import { TRANSFORMERS, $convertToMarkdownString } from '@lexical/markdown';
47
- import { ListNode, ListItemNode } from '@lexical/list';
48
- import { HeadingNode, QuoteNode } from '@lexical/rich-text';
49
- import { LinkNode, AutoLinkNode } from '@lexical/link';
50
- import { CodeNode, CodeHighlightNode } from '@lexical/code';
47
+ import { $generateHtmlFromNodes } from '@lexical/html';
48
+ import { DEFAULT_NODES } from "./editorNodes.js";
51
49
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
52
50
  /**
53
51
  * Serialized state for an empty editor: a root containing a single empty
@@ -106,9 +104,6 @@ const sizeStyles = {
106
104
  }
107
105
  };
108
106
 
109
- /** The default OSS node set registered with the editor. */
110
- const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode, CodeNode, CodeHighlightNode];
111
-
112
107
  /**
113
108
  * Fraction of `maxLength` at which the character counter begins announcing
114
109
  * remaining/over-limit characters to screen readers. Matches TextArea.
@@ -335,6 +330,11 @@ function EditorRefBridge({
335
330
  // transformers round-trip to Markdown. `@lexical/markdown` is a
336
331
  // subpackage (built dist) — safe, unlike a top-level `lexical` import.
337
332
  editor.getEditorState().read(() => $convertToMarkdownString(transformers)),
333
+ getHTML: () =>
334
+ // $generateHtmlFromNodes serializes the whole document (null selection)
335
+ // to HTML; must run in a read context and requires a DOM.
336
+ // `@lexical/html` is a subpackage (built dist) — safe.
337
+ editor.getEditorState().read(() => $generateHtmlFromNodes(editor, null)),
338
338
  getEditor: () => editor
339
339
  }), [editor, editable, transformers]);
340
340
  return null;
@@ -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, and code. Extend via the `nodes` prop / option rather than
5
+ * 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/RichTextEditor/editorNodes.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAC,KAAK,EAAE,WAAW,EAAC,MAAM,SAAS,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAS3D,CAAC"}
@@ -0,0 +1,23 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file editorNodes.ts
5
+ * @input Imports the default Lexical node classes registered by the editor.
6
+ * @output Exports DEFAULT_NODES — the base OSS node set.
7
+ * @position Shared by RichTextEditor.tsx (editor config) and
8
+ * markdownSerializers.ts (headless conversion), so both register the same
9
+ * nodes and Markdown round-trips consistently.
10
+ *
11
+ * SYNC: When the default node set changes, both consumers pick it up here.
12
+ */
13
+
14
+ import { ListNode, ListItemNode } from '@lexical/list';
15
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
16
+ import { LinkNode, AutoLinkNode } from '@lexical/link';
17
+ import { CodeNode, CodeHighlightNode } from '@lexical/code';
18
+ /**
19
+ * The default OSS node set registered with the editor: headings, quotes,
20
+ * lists, links, and code. Extend via the `nodes` prop / option rather than
21
+ * editing this list.
22
+ */
23
+ export const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode, CodeNode, CodeHighlightNode];
@@ -12,4 +12,6 @@ export type { RichTextEditorProps, RichTextEditorRef, RichTextEditorStatus, Rich
12
12
  export { RichTextView } from './RichTextView';
13
13
  export type { RichTextViewProps } from './RichTextView';
14
14
  export { sharedEditorTheme } from './editorTheme';
15
+ export { markdownToEditorStateJSON, editorStateJSONToMarkdown, } from './markdownSerializers';
16
+ export type { MarkdownSerializerOptions } from './markdownSerializers';
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAEhD,OAAO,EACL,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAC,yBAAyB,EAAC,MAAM,uBAAuB,CAAC"}
@@ -13,4 +13,5 @@
13
13
  */
14
14
  export { RichTextEditor } from "./RichTextEditor.js";
15
15
  export { RichTextView } from "./RichTextView.js";
16
- export { sharedEditorTheme } from "./editorTheme.js";
16
+ export { sharedEditorTheme } from "./editorTheme.js";
17
+ export { markdownToEditorStateJSON, editorStateJSONToMarkdown } from "./markdownSerializers.js";
@@ -0,0 +1,40 @@
1
+ import { type Transformer } from '@lexical/markdown';
2
+ import type { Klass, LexicalNode } from 'lexical';
3
+ /** Options shared by the Markdown serializer helpers. */
4
+ export interface MarkdownSerializerOptions {
5
+ /**
6
+ * Markdown transformers to use. Defaults to the standard `@lexical/markdown`
7
+ * `TRANSFORMERS`. Pass the same array you give `RichTextEditor`'s
8
+ * `transformers` prop so content round-trips consistently.
9
+ */
10
+ transformers?: ReadonlyArray<Transformer>;
11
+ /**
12
+ * Extra Lexical nodes to register beyond the default OSS set, mirroring the
13
+ * editor's `nodes` prop. Required for custom node types to serialize.
14
+ */
15
+ nodes?: ReadonlyArray<Klass<LexicalNode>>;
16
+ }
17
+ /**
18
+ * Convert a Markdown string to a serialized Lexical `EditorState` JSON string
19
+ * — the same shape `RichTextEditor`'s `defaultValue` expects. Runs headless
20
+ * (no DOM), so it is safe in Node / SSR.
21
+ *
22
+ * @example
23
+ * ```
24
+ * const value = markdownToEditorStateJSON('# Title\n\nHello');
25
+ * <RichTextEditor label="Notes" defaultValue={value} />
26
+ * ```
27
+ */
28
+ export declare function markdownToEditorStateJSON(markdown: string, options?: MarkdownSerializerOptions): string;
29
+ /**
30
+ * Convert a serialized Lexical `EditorState` JSON string (e.g. the value
31
+ * produced by `RichTextEditor`'s `onChange` or `defaultValue`) to a Markdown
32
+ * string. Runs headless (no DOM), so it is safe in Node / SSR.
33
+ *
34
+ * @example
35
+ * ```
36
+ * const md = editorStateJSONToMarkdown(savedJson);
37
+ * ```
38
+ */
39
+ export declare function editorStateJSONToMarkdown(editorStateJSON: string, options?: MarkdownSerializerOptions): string;
40
+ //# sourceMappingURL=markdownSerializers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownSerializers.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/markdownSerializers.ts"],"names":[],"mappings":"AA0BA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAC,KAAK,EAAE,WAAW,EAAC,MAAM,SAAS,CAAC;AAEhD,yDAAyD;AACzD,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,YAAY,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC1C;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;CAC3C;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAUR;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAKR"}
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownSerializers.ts
5
+ * @input Uses @lexical/headless (createHeadlessEditor), @lexical/markdown
6
+ * ($convertFromMarkdownString / $convertToMarkdownString), and the shared
7
+ * DEFAULT_NODES set.
8
+ * @output Standalone Markdown <-> serialized EditorState helpers:
9
+ * markdownToEditorStateJSON, editorStateJSONToMarkdown.
10
+ * @position Re-exported from RichTextEditor/index.ts and the @astryxdesign/lab
11
+ * barrel. Complements the ref's getMarkdown() by working WITHOUT a mounted
12
+ * editor (e.g. to produce a `defaultValue` from Markdown on the server).
13
+ *
14
+ * SYNC: When modified, update:
15
+ * - /packages/lab/src/RichTextEditor/index.ts (exports)
16
+ * - /packages/lab/src/index.ts (barrel re-export)
17
+ * - /packages/lab/src/RichTextEditor/RichTextEditor.doc.mjs (usage notes)
18
+ * - /packages/lab/src/RichTextEditor/RichTextEditor.test.tsx (tests)
19
+ *
20
+ * NOTE: `@lexical/headless`, `@lexical/markdown`, and `lexical` are OPTIONAL
21
+ * peer dependencies (this is a canary lab module). Install them to use these
22
+ * helpers. `@lexical/headless` runs Lexical without a DOM, so these work in
23
+ * Node / SSR contexts.
24
+ */
25
+
26
+ import { createHeadlessEditor } from '@lexical/headless';
27
+ import { TRANSFORMERS, $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown';
28
+ import { DEFAULT_NODES } from "./editorNodes.js";
29
+
30
+ /** Options shared by the Markdown serializer helpers. */
31
+
32
+ function createSerializerEditor(nodes) {
33
+ return createHeadlessEditor({
34
+ namespace: 'astryx-editor-serializer',
35
+ nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
36
+ onError(error) {
37
+ throw error;
38
+ }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Convert a Markdown string to a serialized Lexical `EditorState` JSON string
44
+ * — the same shape `RichTextEditor`'s `defaultValue` expects. Runs headless
45
+ * (no DOM), so it is safe in Node / SSR.
46
+ *
47
+ * @example
48
+ * ```
49
+ * const value = markdownToEditorStateJSON('# Title\n\nHello');
50
+ * <RichTextEditor label="Notes" defaultValue={value} />
51
+ * ```
52
+ */
53
+ export function markdownToEditorStateJSON(markdown, options = {}) {
54
+ const {
55
+ transformers = TRANSFORMERS,
56
+ nodes
57
+ } = options;
58
+ const editor = createSerializerEditor(nodes);
59
+ editor.update(() => {
60
+ $convertFromMarkdownString(markdown, [...transformers]);
61
+ }, {
62
+ discrete: true
63
+ });
64
+ return JSON.stringify(editor.getEditorState().toJSON());
65
+ }
66
+
67
+ /**
68
+ * Convert a serialized Lexical `EditorState` JSON string (e.g. the value
69
+ * produced by `RichTextEditor`'s `onChange` or `defaultValue`) to a Markdown
70
+ * string. Runs headless (no DOM), so it is safe in Node / SSR.
71
+ *
72
+ * @example
73
+ * ```
74
+ * const md = editorStateJSONToMarkdown(savedJson);
75
+ * ```
76
+ */
77
+ export function editorStateJSONToMarkdown(editorStateJSON, options = {}) {
78
+ const {
79
+ transformers = TRANSFORMERS,
80
+ nodes
81
+ } = options;
82
+ const editor = createSerializerEditor(nodes);
83
+ const state = editor.parseEditorState(editorStateJSON);
84
+ return state.read(() => $convertToMarkdownString([...transformers]));
85
+ }
package/dist/index.d.ts CHANGED
@@ -28,5 +28,5 @@ export { SankeyChart, type SankeyChartProps, SankeyLink, type SankeyLinkProps, S
28
28
  export * from './Stepper';
29
29
  export * from './CircularProgress';
30
30
  export { LogStream, type LogStreamProps, type LogEntry, type LogStreamLevel, } from './LogStream';
31
- export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorStatus, type RichTextEditorStatusType, type RichTextEditorSize, type Transformer, RichTextView, type RichTextViewProps, sharedEditorTheme, } from './RichTextEditor';
31
+ export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorStatus, type RichTextEditorStatusType, type RichTextEditorSize, type Transformer, RichTextView, type RichTextViewProps, sharedEditorTheme, markdownToEditorStateJSON, editorStateJSONToMarkdown, type MarkdownSerializerOptions, } from './RichTextEditor';
32
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -64,4 +64,4 @@ export { LogStream } from "./LogStream/index.js";
64
64
 
65
65
  // RichTextEditor — experimental Lexical WYSIWYG editor (RFC facebook/astryx#3899)
66
66
  // Optional peer deps: lexical + @lexical/*. Read-only rendering via RichTextView.
67
- export { RichTextEditor, RichTextView, sharedEditorTheme } from "./RichTextEditor/index.js";
67
+ export { RichTextEditor, RichTextView, sharedEditorTheme, markdownToEditorStateJSON, editorStateJSONToMarkdown } from "./RichTextEditor/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.2.0-canary.33d4e98",
3
+ "version": "0.2.0-canary.4ffab6f",
4
4
  "description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
5
5
  "author": "Meta Open Source",
6
6
  "license": "MIT",
@@ -45,12 +45,14 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@stylexjs/stylex": ">=0.10.0",
48
- "@astryxdesign/core": "0.2.0-canary.33d4e98",
48
+ "@astryxdesign/core": "0.2.0-canary.4ffab6f",
49
49
  "react": ">=19.0.0",
50
50
  "react-dom": ">=19.0.0",
51
51
  "lexical": "^0.46.0",
52
52
  "@lexical/react": "^0.46.0",
53
53
  "@lexical/markdown": "^0.46.0",
54
+ "@lexical/headless": "^0.46.0",
55
+ "@lexical/html": "^0.46.0",
54
56
  "@lexical/link": "^0.46.0",
55
57
  "@lexical/list": "^0.46.0",
56
58
  "@lexical/rich-text": "^0.46.0",
@@ -64,6 +66,8 @@
64
66
  },
65
67
  "devDependencies": {
66
68
  "@lexical/code": "^0.46.0",
69
+ "@lexical/headless": "^0.46.0",
70
+ "@lexical/html": "^0.46.0",
67
71
  "@lexical/link": "^0.46.0",
68
72
  "@lexical/list": "^0.46.0",
69
73
  "@lexical/markdown": "^0.46.0",
@@ -85,6 +89,12 @@
85
89
  "@lexical/markdown": {
86
90
  "optional": true
87
91
  },
92
+ "@lexical/headless": {
93
+ "optional": true
94
+ },
95
+ "@lexical/html": {
96
+ "optional": true
97
+ },
88
98
  "@lexical/link": {
89
99
  "optional": true
90
100
  },
@@ -102,7 +112,7 @@
102
112
  }
103
113
  },
104
114
  "scripts": {
105
- "build": "rm -rf dist && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && node ../../scripts/check-no-dev-jsx.mjs",
115
+ "build": "rm -rf dist && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
106
116
  "build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.js",
107
117
  "build:css": "node ../../scripts/build-css.mjs --package lab",
108
118
  "dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.js",
@@ -153,7 +153,12 @@ export const docs = {
153
153
  {
154
154
  guidance: true,
155
155
  description:
156
- 'Use a ref (RichTextEditorRef) to imperatively focus(), clear(), read the state via getEditorState(), serialize to Markdown via getMarkdown(), or reach the LexicalEditor via getEditor(). The handle is available after mount. getMarkdown() uses the same transformers prop the editor is configured with. focus() and clear() are no-ops when the editor is read-only or disabled, and clear() resets to a single empty paragraph.',
156
+ 'Use a ref (RichTextEditorRef) to imperatively focus(), clear(), read the state via getEditorState(), serialize to Markdown via getMarkdown() or HTML via getHTML(), or reach the LexicalEditor via getEditor(). The handle is available after mount. getMarkdown() uses the same transformers prop the editor is configured with. focus() and clear() are no-ops when the editor is read-only or disabled, and clear() resets to a single empty paragraph.',
157
+ },
158
+ {
159
+ guidance: true,
160
+ description:
161
+ 'To produce a defaultValue from Markdown without mounting an editor (e.g. on the server), use markdownToEditorStateJSON(markdown). Convert the other way with editorStateJSONToMarkdown(json). Both run headless via @lexical/headless and accept the same transformers/nodes options as the editor.',
157
162
  },
158
163
  {
159
164
  guidance: false,
@@ -22,6 +22,10 @@ import {
22
22
  } from '@lexical/markdown';
23
23
  import {RichTextEditor, type RichTextEditorRef} from './RichTextEditor';
24
24
  import {RichTextView} from './RichTextView';
25
+ import {
26
+ markdownToEditorStateJSON,
27
+ editorStateJSONToMarkdown,
28
+ } from './markdownSerializers';
25
29
 
26
30
  // Small plugin that captures the editor instance so tests can drive real
27
31
  // Lexical updates (jsdom does not implement contenteditable editing).
@@ -342,6 +346,7 @@ describe('RichTextEditor', () => {
342
346
  expect(typeof ref.current?.clear).toBe('function');
343
347
  expect(typeof ref.current?.getEditorState).toBe('function');
344
348
  expect(typeof ref.current?.getMarkdown).toBe('function');
349
+ expect(typeof ref.current?.getHTML).toBe('function');
345
350
  expect(typeof ref.current?.getEditor).toBe('function');
346
351
  });
347
352
 
@@ -426,6 +431,18 @@ describe('RichTextEditor', () => {
426
431
  await waitFor(() => expect(ref.current?.getMarkdown()).toBe('Title'));
427
432
  });
428
433
 
434
+ it('ref.getHTML() serializes content to an HTML string', () => {
435
+ const ref = createRef<RichTextEditorRef>();
436
+ render(
437
+ <RichTextEditor ref={ref} label="Notes" defaultValue={HELLO_STATE} />,
438
+ );
439
+ const html = ref.current?.getHTML();
440
+ expect(typeof html).toBe('string');
441
+ // A paragraph with the seeded text renders as a <p> containing "Hello world".
442
+ expect(html).toContain('<p');
443
+ expect(html).toContain('Hello world');
444
+ });
445
+
429
446
  it('ref.clear() resets the editor to a single empty paragraph', async () => {
430
447
  const ref = createRef<RichTextEditorRef>();
431
448
  render(
@@ -648,3 +665,42 @@ describe('RichTextView', () => {
648
665
  expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
649
666
  });
650
667
  });
668
+
669
+ describe('markdown serializers', () => {
670
+ it('markdownToEditorStateJSON produces a heading node from "# "', () => {
671
+ const json = markdownToEditorStateJSON('# Title');
672
+ const parsed = JSON.parse(json);
673
+ const first = parsed.root.children[0];
674
+ expect(first.type).toBe('heading');
675
+ expect(first.tag).toBe('h1');
676
+ expect(first.children[0].text).toBe('Title');
677
+ });
678
+
679
+ it('editorStateJSONToMarkdown round-trips a heading back to "# "', () => {
680
+ const json = markdownToEditorStateJSON('# Title');
681
+ expect(editorStateJSONToMarkdown(json)).toBe('# Title');
682
+ });
683
+
684
+ it('markdown round-trips through both helpers', () => {
685
+ const md = '# Heading\n\nSome **bold** text';
686
+ const json = markdownToEditorStateJSON(md);
687
+ expect(editorStateJSONToMarkdown(json)).toBe(md);
688
+ });
689
+
690
+ it('honors a custom (empty) transformers set — no heading syntax', () => {
691
+ // With no transformers, "# Title" is not recognized as a heading; it stays
692
+ // a plain paragraph, so serializing back yields the literal text.
693
+ const json = markdownToEditorStateJSON('# Title', {transformers: []});
694
+ const parsed = JSON.parse(json);
695
+ expect(parsed.root.children[0].type).toBe('paragraph');
696
+ expect(parsed.root.children[0].children[0].text).toBe('# Title');
697
+ });
698
+
699
+ it('produces JSON consumable as RichTextEditor defaultValue', async () => {
700
+ const value = markdownToEditorStateJSON('Hello world');
701
+ render(<RichTextEditor label="Notes" defaultValue={value} />);
702
+ await waitFor(() =>
703
+ expect(screen.getByRole('textbox').textContent).toContain('Hello world'),
704
+ );
705
+ });
706
+ });
@@ -76,10 +76,8 @@ import {
76
76
  type Transformer,
77
77
  } from '@lexical/markdown';
78
78
  export type {Transformer} from '@lexical/markdown';
79
- import {ListNode, ListItemNode} from '@lexical/list';
80
- import {HeadingNode, QuoteNode} from '@lexical/rich-text';
81
- import {LinkNode, AutoLinkNode} from '@lexical/link';
82
- import {CodeNode, CodeHighlightNode} from '@lexical/code';
79
+ import {$generateHtmlFromNodes} from '@lexical/html';
80
+ import {DEFAULT_NODES} from './editorNodes';
83
81
  import type {
84
82
  EditorState,
85
83
  Klass,
@@ -184,18 +182,6 @@ const sizeStyles = stylex.create({
184
182
  },
185
183
  });
186
184
 
187
- /** The default OSS node set registered with the editor. */
188
- const DEFAULT_NODES: ReadonlyArray<Klass<LexicalNode>> = [
189
- HeadingNode,
190
- QuoteNode,
191
- ListNode,
192
- ListItemNode,
193
- LinkNode,
194
- AutoLinkNode,
195
- CodeNode,
196
- CodeHighlightNode,
197
- ];
198
-
199
185
  /**
200
186
  * Fraction of `maxLength` at which the character counter begins announcing
201
187
  * remaining/over-limit characters to screen readers. Matches TextArea.
@@ -237,6 +223,12 @@ export interface RichTextEditorRef {
237
223
  * `$convertToMarkdownString` run in a read context.
238
224
  */
239
225
  getMarkdown: () => string;
226
+ /**
227
+ * Serialize the current content to an HTML string via
228
+ * `$generateHtmlFromNodes`. Requires a DOM (available in the browser and in
229
+ * jsdom-based tests). Useful for copy/paste, email, or non-Lexical consumers.
230
+ */
231
+ getHTML: () => string;
240
232
  /**
241
233
  * Access the underlying `LexicalEditor` instance for advanced use cases
242
234
  * (custom commands, listeners, node transforms).
@@ -633,6 +625,11 @@ function EditorRefBridge({
633
625
  editor
634
626
  .getEditorState()
635
627
  .read(() => $convertToMarkdownString(transformers)),
628
+ getHTML: () =>
629
+ // $generateHtmlFromNodes serializes the whole document (null selection)
630
+ // to HTML; must run in a read context and requires a DOM.
631
+ // `@lexical/html` is a subpackage (built dist) — safe.
632
+ editor.getEditorState().read(() => $generateHtmlFromNodes(editor, null)),
636
633
  getEditor: () => editor,
637
634
  }),
638
635
  [editor, editable, transformers],
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file editorNodes.ts
5
+ * @input Imports the default Lexical node classes registered by the editor.
6
+ * @output Exports DEFAULT_NODES — the base OSS node set.
7
+ * @position Shared by RichTextEditor.tsx (editor config) and
8
+ * markdownSerializers.ts (headless conversion), so both register the same
9
+ * nodes and Markdown round-trips consistently.
10
+ *
11
+ * SYNC: When the default node set changes, both consumers pick it up here.
12
+ */
13
+
14
+ import {ListNode, ListItemNode} from '@lexical/list';
15
+ import {HeadingNode, QuoteNode} from '@lexical/rich-text';
16
+ import {LinkNode, AutoLinkNode} from '@lexical/link';
17
+ import {CodeNode, CodeHighlightNode} from '@lexical/code';
18
+ import type {Klass, LexicalNode} from 'lexical';
19
+
20
+ /**
21
+ * The default OSS node set registered with the editor: headings, quotes,
22
+ * lists, links, and code. Extend via the `nodes` prop / option rather than
23
+ * editing this list.
24
+ */
25
+ export const DEFAULT_NODES: ReadonlyArray<Klass<LexicalNode>> = [
26
+ HeadingNode,
27
+ QuoteNode,
28
+ ListNode,
29
+ ListItemNode,
30
+ LinkNode,
31
+ AutoLinkNode,
32
+ CodeNode,
33
+ CodeHighlightNode,
34
+ ];
@@ -26,3 +26,9 @@ export {RichTextView} from './RichTextView';
26
26
  export type {RichTextViewProps} from './RichTextView';
27
27
 
28
28
  export {sharedEditorTheme} from './editorTheme';
29
+
30
+ export {
31
+ markdownToEditorStateJSON,
32
+ editorStateJSONToMarkdown,
33
+ } from './markdownSerializers';
34
+ export type {MarkdownSerializerOptions} from './markdownSerializers';
@@ -0,0 +1,105 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownSerializers.ts
5
+ * @input Uses @lexical/headless (createHeadlessEditor), @lexical/markdown
6
+ * ($convertFromMarkdownString / $convertToMarkdownString), and the shared
7
+ * DEFAULT_NODES set.
8
+ * @output Standalone Markdown <-> serialized EditorState helpers:
9
+ * markdownToEditorStateJSON, editorStateJSONToMarkdown.
10
+ * @position Re-exported from RichTextEditor/index.ts and the @astryxdesign/lab
11
+ * barrel. Complements the ref's getMarkdown() by working WITHOUT a mounted
12
+ * editor (e.g. to produce a `defaultValue` from Markdown on the server).
13
+ *
14
+ * SYNC: When modified, update:
15
+ * - /packages/lab/src/RichTextEditor/index.ts (exports)
16
+ * - /packages/lab/src/index.ts (barrel re-export)
17
+ * - /packages/lab/src/RichTextEditor/RichTextEditor.doc.mjs (usage notes)
18
+ * - /packages/lab/src/RichTextEditor/RichTextEditor.test.tsx (tests)
19
+ *
20
+ * NOTE: `@lexical/headless`, `@lexical/markdown`, and `lexical` are OPTIONAL
21
+ * peer dependencies (this is a canary lab module). Install them to use these
22
+ * helpers. `@lexical/headless` runs Lexical without a DOM, so these work in
23
+ * Node / SSR contexts.
24
+ */
25
+
26
+ import {createHeadlessEditor} from '@lexical/headless';
27
+ import {
28
+ TRANSFORMERS,
29
+ $convertFromMarkdownString,
30
+ $convertToMarkdownString,
31
+ type Transformer,
32
+ } from '@lexical/markdown';
33
+ import {DEFAULT_NODES} from './editorNodes';
34
+ import type {Klass, LexicalNode} from 'lexical';
35
+
36
+ /** Options shared by the Markdown serializer helpers. */
37
+ export interface MarkdownSerializerOptions {
38
+ /**
39
+ * Markdown transformers to use. Defaults to the standard `@lexical/markdown`
40
+ * `TRANSFORMERS`. Pass the same array you give `RichTextEditor`'s
41
+ * `transformers` prop so content round-trips consistently.
42
+ */
43
+ transformers?: ReadonlyArray<Transformer>;
44
+ /**
45
+ * Extra Lexical nodes to register beyond the default OSS set, mirroring the
46
+ * editor's `nodes` prop. Required for custom node types to serialize.
47
+ */
48
+ nodes?: ReadonlyArray<Klass<LexicalNode>>;
49
+ }
50
+
51
+ function createSerializerEditor(nodes?: ReadonlyArray<Klass<LexicalNode>>) {
52
+ return createHeadlessEditor({
53
+ namespace: 'astryx-editor-serializer',
54
+ nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
55
+ onError(error: Error) {
56
+ throw error;
57
+ },
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Convert a Markdown string to a serialized Lexical `EditorState` JSON string
63
+ * — the same shape `RichTextEditor`'s `defaultValue` expects. Runs headless
64
+ * (no DOM), so it is safe in Node / SSR.
65
+ *
66
+ * @example
67
+ * ```
68
+ * const value = markdownToEditorStateJSON('# Title\n\nHello');
69
+ * <RichTextEditor label="Notes" defaultValue={value} />
70
+ * ```
71
+ */
72
+ export function markdownToEditorStateJSON(
73
+ markdown: string,
74
+ options: MarkdownSerializerOptions = {},
75
+ ): string {
76
+ const {transformers = TRANSFORMERS, nodes} = options;
77
+ const editor = createSerializerEditor(nodes);
78
+ editor.update(
79
+ () => {
80
+ $convertFromMarkdownString(markdown, [...transformers]);
81
+ },
82
+ {discrete: true},
83
+ );
84
+ return JSON.stringify(editor.getEditorState().toJSON());
85
+ }
86
+
87
+ /**
88
+ * Convert a serialized Lexical `EditorState` JSON string (e.g. the value
89
+ * produced by `RichTextEditor`'s `onChange` or `defaultValue`) to a Markdown
90
+ * string. Runs headless (no DOM), so it is safe in Node / SSR.
91
+ *
92
+ * @example
93
+ * ```
94
+ * const md = editorStateJSONToMarkdown(savedJson);
95
+ * ```
96
+ */
97
+ export function editorStateJSONToMarkdown(
98
+ editorStateJSON: string,
99
+ options: MarkdownSerializerOptions = {},
100
+ ): string {
101
+ const {transformers = TRANSFORMERS, nodes} = options;
102
+ const editor = createSerializerEditor(nodes);
103
+ const state = editor.parseEditorState(editorStateJSON);
104
+ return state.read(() => $convertToMarkdownString([...transformers]));
105
+ }
package/src/index.ts CHANGED
@@ -246,4 +246,7 @@ export {
246
246
  RichTextView,
247
247
  type RichTextViewProps,
248
248
  sharedEditorTheme,
249
+ markdownToEditorStateJSON,
250
+ editorStateJSONToMarkdown,
251
+ type MarkdownSerializerOptions,
249
252
  } from './RichTextEditor';