@texion/react 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Texion contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @texion/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One input for text and files, for React.
4
+
5
+ ```sh
6
+ npm install @texion/react
7
+ ```
8
+
9
+ ```tsx
10
+ import { TexionInput } from '@texion/react';
11
+ import '@texion/react/styles.css';
12
+
13
+ export default function App() {
14
+ return (
15
+ <TexionInput
16
+ preview
17
+ onSubmit={({ content, attachments }) => {
18
+ console.log(content.source, attachments);
19
+ }}
20
+ />
21
+ );
22
+ }
23
+ ```
24
+
25
+ Users can type, paste, or attach documents. Submission waits for processing and stops if a file fails or the draft changes. Previews appear above the input by default, with an option to move them below.
26
+
27
+ Use `mode="rich" live` for editable Markdown, or `highlight` for source editing. JSON and CSV have structured previews. PDF and Word attachments provide extracted text; image attachments retain their original files and metadata.
28
+
29
+ For your own interface, use `useTexion`, or combine `useSourceInput` and `useTexionAttachments`. Render saved content with `TexionViewer` and restore host-owned attachment references with `TexionMessageViewer`.
30
+
31
+ Built-in parsing runs locally. Your submit handler decides what gets stored or sent. OCR is optional, and PDF layout reconstruction is not guaranteed.
32
+
33
+ Experimental release. React 18 or 19 is required.
34
+
35
+ [Try the demo](https://texion.pages.dev) · [Documentation](https://texion.pages.dev/docs) · [Source](https://github.com/Sirily23/texion/tree/main/packages/react) · [MIT license](LICENSE)
@@ -0,0 +1,9 @@
1
+ import {
2
+ RichMarkdownEditor,
3
+ RichMarkdownEditor_default
4
+ } from "./chunk-LSFUTTAB.js";
5
+ export {
6
+ RichMarkdownEditor,
7
+ RichMarkdownEditor_default as default
8
+ };
9
+ //# sourceMappingURL=RichMarkdownEditor-OAHLD4XC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,169 @@
1
+ import {
2
+ highlightColors
3
+ } from "./chunk-T56S7VW3.js";
4
+
5
+ // src/renderers/markdown/index.tsx
6
+ import { Fragment, createElement } from "react";
7
+
8
+ // src/components/CodeBlock.tsx
9
+ import { useId, useMemo, useState } from "react";
10
+ import { inspectCodeBlock } from "@texion/core";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ function CodeBlock({ source, language = "text" }) {
13
+ const id = useId();
14
+ const [expanded, setExpanded] = useState(true);
15
+ const [copyStatus, setCopyStatus] = useState("");
16
+ const inspection = useMemo(
17
+ () => inspectCodeBlock(source, language),
18
+ [source, language]
19
+ );
20
+ const segments = [];
21
+ let offset = 0;
22
+ for (const token of inspection.tokens) {
23
+ if (token.start > offset) segments.push(source.slice(offset, token.start));
24
+ segments.push(
25
+ /* @__PURE__ */ jsx(
26
+ "span",
27
+ {
28
+ "data-highlight-kind": token.kind,
29
+ style: { color: highlightColors[token.kind] },
30
+ children: source.slice(token.start, token.end)
31
+ },
32
+ token.start
33
+ )
34
+ );
35
+ offset = token.end;
36
+ }
37
+ if (offset < source.length) segments.push(source.slice(offset));
38
+ async function copy() {
39
+ try {
40
+ await navigator.clipboard.writeText(source);
41
+ setCopyStatus("Copied");
42
+ } catch {
43
+ setCopyStatus("Could not copy. Select and copy the code manually.");
44
+ }
45
+ }
46
+ return /* @__PURE__ */ jsxs(
47
+ "div",
48
+ {
49
+ className: "texion-code-block",
50
+ role: "group",
51
+ "aria-label": `${language} code block`,
52
+ children: [
53
+ /* @__PURE__ */ jsxs("div", { className: "texion-code-toolbar", children: [
54
+ /* @__PURE__ */ jsx("span", { children: language }),
55
+ /* @__PURE__ */ jsx(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ onClick: () => void copy(),
60
+ "aria-label": `Copy ${language} code`,
61
+ children: "Copy"
62
+ }
63
+ ),
64
+ /* @__PURE__ */ jsx(
65
+ "button",
66
+ {
67
+ type: "button",
68
+ "aria-expanded": expanded,
69
+ "aria-controls": id,
70
+ onClick: () => setExpanded((value) => !value),
71
+ children: expanded ? "Collapse code" : "Expand code"
72
+ }
73
+ )
74
+ ] }),
75
+ /* @__PURE__ */ jsx("pre", { id, hidden: !expanded, children: /* @__PURE__ */ jsx("code", { "data-language": language, children: segments }) }),
76
+ inspection.warnings.map((warning, index) => /* @__PURE__ */ jsxs("p", { role: "status", children: [
77
+ language,
78
+ " block: ",
79
+ warning.message
80
+ ] }, index)),
81
+ inspection.limited && /* @__PURE__ */ jsx("p", { role: "status", children: "Code highlighting is paused for this block; the source is preserved." }),
82
+ copyStatus && /* @__PURE__ */ jsx("p", { role: "status", children: copyStatus })
83
+ ]
84
+ }
85
+ );
86
+ }
87
+
88
+ // src/renderers/markdown/index.tsx
89
+ import { safeUrl } from "@texion/core";
90
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
91
+ function render(node, index) {
92
+ const children = node.children?.map(render);
93
+ switch (node.type) {
94
+ case "text":
95
+ return node.value ?? "";
96
+ case "paragraph":
97
+ return /* @__PURE__ */ jsx2("p", { children }, index);
98
+ case "heading":
99
+ return createElement(
100
+ `h${Math.max(1, Math.min(6, node.depth ?? 1))}`,
101
+ { key: index },
102
+ children
103
+ );
104
+ case "strong":
105
+ return /* @__PURE__ */ jsx2("strong", { children }, index);
106
+ case "emphasis":
107
+ return /* @__PURE__ */ jsx2("em", { children }, index);
108
+ case "delete":
109
+ return /* @__PURE__ */ jsx2("del", { children }, index);
110
+ case "blockquote":
111
+ return /* @__PURE__ */ jsx2("blockquote", { children }, index);
112
+ case "code":
113
+ return /* @__PURE__ */ jsx2(
114
+ CodeBlock,
115
+ {
116
+ source: node.value ?? "",
117
+ ...node.language ? { language: node.language } : {}
118
+ },
119
+ index
120
+ );
121
+ case "inlineCode":
122
+ return /* @__PURE__ */ jsx2("code", { children: node.value }, index);
123
+ case "break":
124
+ return /* @__PURE__ */ jsx2("br", {}, index);
125
+ case "thematicBreak":
126
+ return /* @__PURE__ */ jsx2("hr", {}, index);
127
+ case "list":
128
+ return node.ordered ? /* @__PURE__ */ jsx2("ol", { start: node.start, children }, index) : /* @__PURE__ */ jsx2("ul", { children }, index);
129
+ case "listItem":
130
+ return /* @__PURE__ */ jsxs2("li", { children: [
131
+ node.checked !== void 0 && /* @__PURE__ */ jsx2(
132
+ "input",
133
+ {
134
+ "aria-label": node.checked ? "Completed task" : "Incomplete task",
135
+ type: "checkbox",
136
+ checked: node.checked,
137
+ disabled: true
138
+ }
139
+ ),
140
+ children
141
+ ] }, index);
142
+ case "link": {
143
+ const url = safeUrl(node.url ?? "");
144
+ return url ? /* @__PURE__ */ jsx2("a", { href: url, rel: "noopener noreferrer", children }, index) : /* @__PURE__ */ jsx2(Fragment, { children }, index);
145
+ }
146
+ case "table":
147
+ return /* @__PURE__ */ jsxs2("table", { children: [
148
+ /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { children: node.children?.[0]?.children?.map((cell, i) => /* @__PURE__ */ jsx2("th", { scope: "col", children: cell.children?.map(render) }, i)) }) }),
149
+ /* @__PURE__ */ jsx2("tbody", { children: node.children?.slice(1).map(render) })
150
+ ] }, index);
151
+ case "tableRow":
152
+ return /* @__PURE__ */ jsx2("tr", { children: node.children?.map((cell, i) => /* @__PURE__ */ jsx2("td", { children: cell.children?.map(render) }, i)) }, index);
153
+ case "html":
154
+ case "image":
155
+ return null;
156
+ default:
157
+ return /* @__PURE__ */ jsx2(Fragment, { children: children ?? node.value ?? "" }, index);
158
+ }
159
+ }
160
+ function MarkdownRenderer({ preview }) {
161
+ return /* @__PURE__ */ jsx2(Fragment2, { children: preview.nodes.map(render) });
162
+ }
163
+ var markdown_default = MarkdownRenderer;
164
+
165
+ export {
166
+ MarkdownRenderer,
167
+ markdown_default
168
+ };
169
+ //# sourceMappingURL=chunk-JZO3KR6A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/renderers/markdown/index.tsx","../src/components/CodeBlock.tsx"],"sourcesContent":["import type { PreviewRendererProps } from '../types';\nimport { Fragment, createElement } from 'react';\nimport { CodeBlock } from '../../components/CodeBlock';\nimport type { ReactNode } from 'react';\nimport { safeUrl } from '@texion/core';\nimport type { DocumentPreview, PreviewNode } from '@texion/core';\n\nfunction render(node: PreviewNode, index: number): ReactNode {\n const children = node.children?.map(render);\n switch (node.type) {\n case 'text':\n return node.value ?? '';\n case 'paragraph':\n return <p key={index}>{children}</p>;\n case 'heading':\n return createElement(\n `h${Math.max(1, Math.min(6, node.depth ?? 1))}`,\n { key: index },\n children,\n );\n case 'strong':\n return <strong key={index}>{children}</strong>;\n case 'emphasis':\n return <em key={index}>{children}</em>;\n case 'delete':\n return <del key={index}>{children}</del>;\n case 'blockquote':\n return <blockquote key={index}>{children}</blockquote>;\n case 'code':\n return (\n <CodeBlock\n key={index}\n source={node.value ?? ''}\n {...(node.language ? { language: node.language } : {})}\n />\n );\n case 'inlineCode':\n return <code key={index}>{node.value}</code>;\n case 'break':\n return <br key={index} />;\n case 'thematicBreak':\n return <hr key={index} />;\n case 'list':\n return node.ordered ? (\n <ol key={index} start={node.start}>\n {children}\n </ol>\n ) : (\n <ul key={index}>{children}</ul>\n );\n case 'listItem':\n return (\n <li key={index}>\n {node.checked !== undefined && (\n <input\n aria-label={node.checked ? 'Completed task' : 'Incomplete task'}\n type=\"checkbox\"\n checked={node.checked}\n disabled\n />\n )}\n {children}\n </li>\n );\n case 'link': {\n const url = safeUrl(node.url ?? '');\n\n return url ? (\n <a key={index} href={url} rel=\"noopener noreferrer\">\n {children}\n </a>\n ) : (\n <Fragment key={index}>{children}</Fragment>\n );\n }\n case 'table':\n return (\n <table key={index}>\n <thead>\n <tr>\n {node.children?.[0]?.children?.map((cell, i) => (\n <th key={i} scope=\"col\">\n {cell.children?.map(render)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>{node.children?.slice(1).map(render)}</tbody>\n </table>\n );\n case 'tableRow':\n return (\n <tr key={index}>\n {node.children?.map((cell, i) => (\n <td key={i}>{cell.children?.map(render)}</td>\n ))}\n </tr>\n );\n case 'html':\n case 'image':\n return null;\n default:\n return <Fragment key={index}>{children ?? node.value ?? ''}</Fragment>;\n }\n}\n\nexport function MarkdownRenderer({ preview }: PreviewRendererProps) {\n return <>{preview.nodes.map(render)}</>;\n}\n\nexport default MarkdownRenderer;\n","import { useId, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { inspectCodeBlock } from '@texion/core';\nimport { highlightColors } from '../rendering/HighlightOverlay';\n\nexport interface CodeBlockProps {\n readonly source: string;\n readonly language?: string;\n}\n\nexport function CodeBlock({ source, language = 'text' }: CodeBlockProps) {\n const id = useId();\n const [expanded, setExpanded] = useState(true);\n const [copyStatus, setCopyStatus] = useState('');\n const inspection = useMemo(\n () => inspectCodeBlock(source, language),\n [source, language],\n );\n const segments: ReactNode[] = [];\n let offset = 0;\n for (const token of inspection.tokens) {\n if (token.start > offset) segments.push(source.slice(offset, token.start));\n segments.push(\n <span\n key={token.start}\n data-highlight-kind={token.kind}\n style={{ color: highlightColors[token.kind] }}\n >\n {source.slice(token.start, token.end)}\n </span>,\n );\n offset = token.end;\n }\n if (offset < source.length) segments.push(source.slice(offset));\n async function copy() {\n try {\n await navigator.clipboard.writeText(source);\n setCopyStatus('Copied');\n } catch {\n setCopyStatus('Could not copy. Select and copy the code manually.');\n }\n }\n return (\n <div\n className=\"texion-code-block\"\n role=\"group\"\n aria-label={`${language} code block`}\n >\n <div className=\"texion-code-toolbar\">\n <span>{language}</span>\n <button\n type=\"button\"\n onClick={() => void copy()}\n aria-label={`Copy ${language} code`}\n >\n Copy\n </button>\n <button\n type=\"button\"\n aria-expanded={expanded}\n aria-controls={id}\n onClick={() => setExpanded((value) => !value)}\n >\n {expanded ? 'Collapse code' : 'Expand code'}\n </button>\n </div>\n <pre id={id} hidden={!expanded}>\n <code data-language={language}>{segments}</code>\n </pre>\n {inspection.warnings.map((warning, index) => (\n <p role=\"status\" key={index}>\n {language} block: {warning.message}\n </p>\n ))}\n {inspection.limited && (\n <p role=\"status\">\n Code highlighting is paused for this block; the source is preserved.\n </p>\n )}\n {copyStatus && <p role=\"status\">{copyStatus}</p>}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","jsx","jsxs"]}
@@ -0,0 +1,390 @@
1
+ // src/editors/rich/RichMarkdownEditor.tsx
2
+ import { useEffect as useEffect2, useMemo, useRef as useRef2, useState } from "react";
3
+ import { LexicalExtensionComposer } from "@lexical/react/LexicalExtensionComposer";
4
+ import { ContentEditable } from "@lexical/react/LexicalContentEditable";
5
+ import { ListPlugin } from "@lexical/react/LexicalListPlugin";
6
+ import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
7
+ import {
8
+ DEFAULT_TRANSFORMERS as DEFAULT_TRANSFORMERS3,
9
+ MarkdownShortcutPlugin
10
+ } from "@lexical/react/LexicalMarkdownShortcutPlugin";
11
+ import { safeUrl as safeUrl2 } from "@texion/core";
12
+
13
+ // src/editors/rich/compatibility.ts
14
+ import { inspectMarkdownSource } from "@texion/core";
15
+ var sourceOnlyNodes = /* @__PURE__ */ new Set([
16
+ "yaml",
17
+ "toml",
18
+ "table",
19
+ "footnoteDefinition",
20
+ "footnoteReference",
21
+ "definition",
22
+ "linkReference",
23
+ "imageReference",
24
+ "taskList"
25
+ ]);
26
+ function richImportIssue(source, maxCharacters = 5e4) {
27
+ const features = inspectMarkdownSource(source, maxCharacters);
28
+ if (features.limited) {
29
+ return "This content exceeds the rich editor complexity limits. Use source mode.";
30
+ }
31
+ if (features.nodeTypes.some((type) => sourceOnlyNodes.has(type))) {
32
+ return "This Markdown uses syntax that requires source mode to preserve its meaning.";
33
+ }
34
+ return void 0;
35
+ }
36
+
37
+ // src/editors/rich/extension.ts
38
+ import { defineExtension } from "lexical";
39
+ import { $convertFromMarkdownString } from "@lexical/markdown";
40
+ import { HistoryExtension } from "@lexical/history";
41
+ import { CodeNode } from "@lexical/code";
42
+ import { LinkNode as LinkNode2 } from "@lexical/link";
43
+ import { ListItemNode, ListNode } from "@lexical/list";
44
+ import { HeadingNode, QuoteNode, RichTextExtension } from "@lexical/rich-text";
45
+ import { DEFAULT_TRANSFORMERS } from "@lexical/react/LexicalMarkdownShortcutPlugin";
46
+ import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
47
+
48
+ // src/editors/rich/link-policy.ts
49
+ import { $getNodeByKey, mergeRegister } from "lexical";
50
+ import { LinkNode } from "@lexical/link";
51
+ import { safeUrl } from "@texion/core";
52
+ function normalizeLink(node) {
53
+ const url = safeUrl(node.getURL());
54
+ if (!url) {
55
+ for (const child of node.getChildren()) {
56
+ node.insertBefore(child);
57
+ }
58
+ node.remove();
59
+ return;
60
+ }
61
+ if (node.getURL() !== url) {
62
+ node.setURL(url);
63
+ }
64
+ if (node.getRel() !== "noopener noreferrer") {
65
+ node.setRel("noopener noreferrer");
66
+ }
67
+ }
68
+ function registerLinkPolicy(editor) {
69
+ const removeTransform = editor.registerNodeTransform(LinkNode, normalizeLink);
70
+ const removeListener = editor.registerMutationListener(
71
+ LinkNode,
72
+ (mutations) => {
73
+ editor.getEditorState().read(() => {
74
+ for (const [key, change] of mutations) {
75
+ if (change === "destroyed") {
76
+ continue;
77
+ }
78
+ const node = $getNodeByKey(key);
79
+ const element = editor.getElementByKey(key);
80
+ if (!(node instanceof LinkNode) || !element) {
81
+ continue;
82
+ }
83
+ const url = safeUrl(node.getURL());
84
+ if (url) {
85
+ element.setAttribute("href", url);
86
+ } else {
87
+ element.removeAttribute("href");
88
+ }
89
+ }
90
+ });
91
+ }
92
+ );
93
+ return mergeRegister(removeTransform, removeListener);
94
+ }
95
+
96
+ // src/editors/rich/extension.ts
97
+ function createRichExtension(options) {
98
+ return defineExtension({
99
+ name: "@texion/rich-markdown",
100
+ namespace: "Texion Markdown",
101
+ dependencies: [RichTextExtension, HistoryExtension],
102
+ nodes: [
103
+ HeadingNode,
104
+ QuoteNode,
105
+ ListNode,
106
+ ListItemNode,
107
+ CodeNode,
108
+ LinkNode2,
109
+ HorizontalRuleNode
110
+ ],
111
+ theme: {
112
+ text: {
113
+ bold: "texion-rich-bold",
114
+ italic: "texion-rich-italic",
115
+ code: "texion-rich-inline-code",
116
+ strikethrough: "texion-rich-strikethrough"
117
+ },
118
+ code: "texion-rich-code"
119
+ },
120
+ onError: options.onError,
121
+ $initialEditorState() {
122
+ const source = options.getSource();
123
+ const limit = options.getLimit();
124
+ const exceedsLimit = source.length > limit;
125
+ const isUnsupported = richImportIssue(source, limit);
126
+ const initialSource = exceedsLimit || isUnsupported ? "" : source;
127
+ $convertFromMarkdownString(initialSource, DEFAULT_TRANSFORMERS);
128
+ },
129
+ register: registerLinkPolicy
130
+ });
131
+ }
132
+
133
+ // src/editors/rich/RichEditorBridge.tsx
134
+ import { useEffect, useRef } from "react";
135
+ import {
136
+ $addUpdateTag,
137
+ $createTextNode,
138
+ $getRoot,
139
+ $getSelection,
140
+ $insertNodes,
141
+ $isRangeSelection,
142
+ COMMAND_PRIORITY_HIGH,
143
+ HISTORY_PUSH_TAG,
144
+ KEY_ENTER_COMMAND,
145
+ PASTE_COMMAND,
146
+ mergeRegister as mergeRegister2
147
+ } from "lexical";
148
+ import {
149
+ $convertFromMarkdownString as $convertFromMarkdownString2,
150
+ $convertToMarkdownString,
151
+ $generateNodesFromMarkdownString
152
+ } from "@lexical/markdown";
153
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
154
+ import { DEFAULT_TRANSFORMERS as DEFAULT_TRANSFORMERS2 } from "@lexical/react/LexicalMarkdownShortcutPlugin";
155
+ import { $createCodeNode } from "@lexical/code";
156
+ import { detectTextFormat } from "@texion/core";
157
+ var externalUpdateTag = "texion:external";
158
+ function RichEditorBridge({
159
+ options,
160
+ reportError,
161
+ reportImportIssue
162
+ }) {
163
+ const [editor] = useLexicalComposerContext();
164
+ const current = useRef(options);
165
+ current.current = options;
166
+ const lastValue = useRef(options.value);
167
+ useEffect(() => {
168
+ const handleUpdate = ({
169
+ editorState,
170
+ dirtyElements,
171
+ dirtyLeaves,
172
+ tags
173
+ }) => {
174
+ if (tags.has(externalUpdateTag) || !dirtyElements.size && !dirtyLeaves.size) {
175
+ return;
176
+ }
177
+ const source = editorState.read(
178
+ () => $convertToMarkdownString(DEFAULT_TRANSFORMERS2)
179
+ );
180
+ if (source === lastValue.current) {
181
+ return;
182
+ }
183
+ lastValue.current = source;
184
+ current.current.onChange(source);
185
+ };
186
+ const handlePaste = (event) => {
187
+ if (editor.isComposing()) {
188
+ return false;
189
+ }
190
+ const clipboard = "clipboardData" in event ? event.clipboardData : null;
191
+ if (!clipboard) {
192
+ return false;
193
+ }
194
+ event.preventDefault();
195
+ if (clipboard.files.length) {
196
+ if (current.current.onFiles) {
197
+ current.current.onFiles(Array.from(clipboard.files));
198
+ } else {
199
+ reportError("Use source mode or the attachment picker for files.");
200
+ }
201
+ return true;
202
+ }
203
+ const text = clipboard.getData("text/plain");
204
+ if (!text) {
205
+ return true;
206
+ }
207
+ if ($getRoot().getTextContentSize() + text.length > (current.current.maxCharacters ?? 5e4)) {
208
+ reportError(
209
+ "This paste exceeds the rich editor limit. Use source mode to edit it."
210
+ );
211
+ return true;
212
+ }
213
+ const wasEmpty = $getRoot().getTextContentSize() === 0;
214
+ const recognized = detectTextFormat(text, current.current.formats);
215
+ if (recognized !== "markdown") {
216
+ if (wasEmpty && current.current.onRequestSource) {
217
+ current.current.onImport?.(text);
218
+ current.current.onChange(text);
219
+ current.current.onRequestSource();
220
+ } else {
221
+ if (!$isRangeSelection($getSelection())) {
222
+ $getRoot().selectEnd();
223
+ }
224
+ $addUpdateTag(HISTORY_PUSH_TAG);
225
+ $insertNodes([
226
+ $createCodeNode(recognized).append($createTextNode(text))
227
+ ]);
228
+ }
229
+ return true;
230
+ }
231
+ const issue = richImportIssue(
232
+ text,
233
+ current.current.maxCharacters ?? 5e4
234
+ );
235
+ if (issue) {
236
+ if (wasEmpty && current.current.onRequestSource) {
237
+ current.current.onImport?.(text);
238
+ current.current.onChange(text);
239
+ current.current.onRequestSource();
240
+ } else {
241
+ reportError(issue + " Paste it in source mode.");
242
+ }
243
+ return true;
244
+ }
245
+ if (!$isRangeSelection($getSelection())) {
246
+ $getRoot().selectEnd();
247
+ }
248
+ $addUpdateTag(HISTORY_PUSH_TAG);
249
+ if (current.current.live === false) {
250
+ const selection = $getSelection();
251
+ if ($isRangeSelection(selection)) {
252
+ selection.insertRawText(text);
253
+ }
254
+ } else {
255
+ $insertNodes(
256
+ $generateNodesFromMarkdownString(text, DEFAULT_TRANSFORMERS2)
257
+ );
258
+ }
259
+ if (wasEmpty) {
260
+ current.current.onImport?.(text);
261
+ }
262
+ reportError("");
263
+ return true;
264
+ };
265
+ const handleEnter = (event) => {
266
+ if (!event || editor.isComposing() || event.isComposing || event.keyCode === 229 || event.shiftKey || !current.current.onSubmit) {
267
+ return false;
268
+ }
269
+ const submit = current.current.submitOnEnter ? !event.altKey && !event.ctrlKey && !event.metaKey : (event.ctrlKey || event.metaKey) && !event.altKey;
270
+ if (!submit) {
271
+ return false;
272
+ }
273
+ event.preventDefault();
274
+ current.current.onSubmit();
275
+ return true;
276
+ };
277
+ return mergeRegister2(
278
+ editor.registerUpdateListener(handleUpdate),
279
+ editor.registerCommand(PASTE_COMMAND, handlePaste, COMMAND_PRIORITY_HIGH),
280
+ editor.registerCommand(
281
+ KEY_ENTER_COMMAND,
282
+ handleEnter,
283
+ COMMAND_PRIORITY_HIGH
284
+ ),
285
+ () => current.current.onCompositionChange?.(false)
286
+ );
287
+ }, [editor, reportError]);
288
+ useEffect(() => {
289
+ if (options.value === lastValue.current) {
290
+ return;
291
+ }
292
+ const issue = richImportIssue(
293
+ options.value,
294
+ options.maxCharacters ?? 5e4
295
+ );
296
+ if (issue) {
297
+ reportImportIssue(issue);
298
+ return;
299
+ }
300
+ lastValue.current = options.value;
301
+ editor.update(
302
+ () => $convertFromMarkdownString2(options.value, DEFAULT_TRANSFORMERS2),
303
+ {
304
+ tag: externalUpdateTag
305
+ }
306
+ );
307
+ }, [editor, options.value, options.maxCharacters, reportImportIssue]);
308
+ useEffect(() => {
309
+ editor.setEditable(!options.readOnly);
310
+ }, [editor, options.readOnly]);
311
+ return null;
312
+ }
313
+
314
+ // src/editors/rich/RichMarkdownEditor.tsx
315
+ import { jsx, jsxs } from "react/jsx-runtime";
316
+ var validateUrl = (url) => safeUrl2(url) !== void 0;
317
+ function RichMarkdownEditor(props) {
318
+ const [error, setError] = useState("");
319
+ const [importIssue, setImportIssue] = useState(
320
+ () => richImportIssue(props.value, props.maxCharacters ?? 5e4)
321
+ );
322
+ useEffect2(() => {
323
+ if (importIssue) {
324
+ setImportIssue(
325
+ richImportIssue(props.value, props.maxCharacters ?? 5e4)
326
+ );
327
+ }
328
+ }, [props.value, props.maxCharacters, importIssue]);
329
+ const initialValue = useRef2(props.value);
330
+ initialValue.current = props.value;
331
+ const currentLimit = useRef2(props.maxCharacters ?? 5e4);
332
+ currentLimit.current = props.maxCharacters ?? 5e4;
333
+ const maxCharacters = props.maxCharacters ?? 5e4;
334
+ if (!Number.isSafeInteger(maxCharacters) || maxCharacters < 1) {
335
+ throw new RangeError("maxCharacters must be a positive safe integer");
336
+ }
337
+ const extension = useMemo(
338
+ () => createRichExtension({
339
+ getSource: () => initialValue.current,
340
+ getLimit: () => currentLimit.current,
341
+ onError: (error2) => setError(error2.message)
342
+ }),
343
+ []
344
+ );
345
+ if (props.value.length > maxCharacters || importIssue) {
346
+ return /* @__PURE__ */ jsxs("div", { children: [
347
+ /* @__PURE__ */ jsx("p", { role: "status", children: importIssue ?? "This content exceeds the rich editor limit. Use source mode to edit the complete source." }),
348
+ props.onRequestSource && /* @__PURE__ */ jsx("button", { type: "button", onClick: props.onRequestSource, children: "Edit source" })
349
+ ] });
350
+ }
351
+ const placeholder = props.placeholder ?? "Write something\u2026";
352
+ return /* @__PURE__ */ jsxs("div", { className: "texion-rich-editor", children: [
353
+ /* @__PURE__ */ jsxs(LexicalExtensionComposer, { extension, contentEditable: null, children: [
354
+ /* @__PURE__ */ jsx(
355
+ ContentEditable,
356
+ {
357
+ id: props.id,
358
+ role: "textbox",
359
+ "aria-label": props.label ?? "Message",
360
+ "aria-multiline": "true",
361
+ "aria-placeholder": placeholder,
362
+ placeholder: /* @__PURE__ */ jsx("span", { className: "texion-rich-placeholder", children: placeholder }),
363
+ className: "texion-rich-content",
364
+ onCompositionStart: () => props.onCompositionChange?.(true),
365
+ onCompositionEnd: () => props.onCompositionChange?.(false)
366
+ }
367
+ ),
368
+ /* @__PURE__ */ jsx(
369
+ RichEditorBridge,
370
+ {
371
+ options: props,
372
+ reportError: setError,
373
+ reportImportIssue: setImportIssue
374
+ }
375
+ ),
376
+ /* @__PURE__ */ jsx(ListPlugin, {}),
377
+ /* @__PURE__ */ jsx(LinkPlugin, { validateUrl }),
378
+ props.live !== false && /* @__PURE__ */ jsx(MarkdownShortcutPlugin, { transformers: DEFAULT_TRANSFORMERS3 })
379
+ ] }),
380
+ error && /* @__PURE__ */ jsx("p", { role: "alert", children: error }),
381
+ error && props.onRequestSource && /* @__PURE__ */ jsx("button", { type: "button", onClick: props.onRequestSource, children: "Edit source" })
382
+ ] });
383
+ }
384
+ var RichMarkdownEditor_default = RichMarkdownEditor;
385
+
386
+ export {
387
+ RichMarkdownEditor,
388
+ RichMarkdownEditor_default
389
+ };
390
+ //# sourceMappingURL=chunk-LSFUTTAB.js.map