diff --git a/app/package-lock.json b/app/package-lock.json
index 8fec273..2da2aa5 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -11,6 +11,7 @@
"@exampledev/new.css": "^1.1.3",
"@fastify/http-proxy": "^8.2.2",
"@lexical/history": "^0.4.1",
+ "@lexical/link": "^0.4.1",
"@lexical/rich-text": "^0.4.1",
"@tsconfig/node18-strictest-esm": "^1.0.1",
"esbuild": "^0.15.7",
@@ -138,6 +139,17 @@
"lexical": "0.4.1"
}
},
+ "node_modules/@lexical/link": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.4.1.tgz",
+ "integrity": "sha512-566lQymmuBe3Y7UDyaaTs+VDlElbu1WhnjT9lVDk0BXag7MA8tv/f60XptWnTK1pv/Dobm/CyLmyLae55OuflQ==",
+ "dependencies": {
+ "@lexical/utils": "0.4.1"
+ },
+ "peerDependencies": {
+ "lexical": "0.4.1"
+ }
+ },
"node_modules/@lexical/list": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.4.1.tgz",
@@ -1199,6 +1211,14 @@
"@lexical/selection": "0.4.1"
}
},
+ "@lexical/link": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.4.1.tgz",
+ "integrity": "sha512-566lQymmuBe3Y7UDyaaTs+VDlElbu1WhnjT9lVDk0BXag7MA8tv/f60XptWnTK1pv/Dobm/CyLmyLae55OuflQ==",
+ "requires": {
+ "@lexical/utils": "0.4.1"
+ }
+ },
"@lexical/list": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.4.1.tgz",
diff --git a/app/package.json b/app/package.json
index 702e862..6ff0bb3 100644
--- a/app/package.json
+++ b/app/package.json
@@ -14,6 +14,7 @@
"@exampledev/new.css": "^1.1.3",
"@fastify/http-proxy": "^8.2.2",
"@lexical/history": "^0.4.1",
+ "@lexical/link": "^0.4.1",
"@lexical/rich-text": "^0.4.1",
"@tsconfig/node18-strictest-esm": "^1.0.1",
"esbuild": "^0.15.7",
diff --git a/app/views/components/editor.tsx b/app/views/components/editor.tsx
index a0c8ddc..2e18d2d 100644
--- a/app/views/components/editor.tsx
+++ b/app/views/components/editor.tsx
@@ -11,11 +11,13 @@ import {
HeadingNode,
registerRichText,
} from "@lexical/rich-text";
+import { $createAutoLinkNode, AutoLinkNode } from "@lexical/link";
import { onCleanup, onMount } from "solid-js";
import type Pages from "../../protocol/pages";
import "./editor.css";
-const editor = createEditor({ nodes: [HeadingNode] });
+const urlMatcher = /https?:\/\/[^\s]+/;
+const editor = createEditor({ nodes: [HeadingNode, AutoLinkNode] });
function ref(el: HTMLElement) {
editor.setRootElement(el);
@@ -29,12 +31,26 @@ export default (props: {
}) => {
const initialEditorState = () => {
const root = $getRoot();
- for (const [i, text] of props.text.split("\n").entries()) {
- const line = i === 0 ? $createHeadingNode("h2") : $createParagraphNode();
- const indent = text.match(/^\s*/)?.[0]?.length ?? 0;
- line.setIndent(indent);
- line.append($createTextNode(text.slice(indent)));
- root.append(line);
+ const [title, ...lines] = props.text.split("\n");
+ const titleNode = $createHeadingNode("h2");
+ titleNode.append($createTextNode(title));
+ root.append(titleNode);
+ for (const line of lines) {
+ const lineNode = $createParagraphNode();
+ const indent = line.match(/^\s*/)?.[0]?.length ?? 0;
+ lineNode.setIndent(indent);
+ let text = line.slice(indent);
+ let match: RegExpMatchArray | null = null;
+ while ((match = text.match(urlMatcher))) {
+ const offset = text.slice(0, match.index!);
+ const input = match[0]!;
+ const link = $createAutoLinkNode(input);
+ link.append($createTextNode(match[0]));
+ lineNode.append($createTextNode(offset), link);
+ text = text.slice(offset.length + input.length);
+ }
+ lineNode.append($createTextNode(text));
+ root.append(lineNode);
}
};
onCleanup(registerRichText(editor, initialEditorState));
@@ -64,5 +80,17 @@ export default (props: {
)
);
onMount(() => editor.focus());
- return ;
+ return (
+ {
+ const el = e.target.parentElement;
+ if (el instanceof HTMLAnchorElement) {
+ window.open(el.href, "_blank", "noreferrer");
+ }
+ }}
+ class="editor"
+ contenteditable
+ />
+ );
};