Moved to ReactJS

This commit is contained in:
Alois 2026-03-14 12:35:30 +01:00
commit 1d0ebeb2b7
100 changed files with 1909 additions and 6056 deletions

View file

@ -18,6 +18,7 @@
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.15",
"@tensamin/ui": "workspace:*",
"solid-js": "^1.9.11"
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}

View file

@ -21,12 +21,9 @@ import {
indentWithTab,
} from "@codemirror/commands";
import {
createEffect,
createMemo,
onCleanup,
onMount,
type Setter,
} from "solid-js";
useEffect,
useRef,
} from "react";
import { collectInlineRanges, ensureMarkdownStyles } from "./markdown";
@ -34,7 +31,7 @@ export type InputProps = {
ref?: HTMLDivElement;
placeholder?: string;
value: string;
setValue: Setter<string>;
setValue: (value: string) => void;
onSubmit?: () => void;
invertEnterBehavior?: boolean;
};
@ -79,21 +76,18 @@ const markdownDecorations = ViewPlugin.fromClass(
export default function Input(props: InputProps) {
ensureMarkdownStyles();
// eslint-disable-next-line no-unassigned-vars
let element: HTMLDivElement | undefined;
let view: EditorView | undefined;
let ignoreSync = false;
const elementRef = useRef<HTMLDivElement | null>(null);
const viewRef = useRef<EditorView | undefined>(undefined);
const ignoreSyncRef = useRef(false);
const externalValue = createMemo(() => props.value);
onMount(() => {
if (!element) return;
useEffect(() => {
if (!elementRef.current) return;
const state = EditorState.create({
doc: props.value,
extensions: createEditorExtensions(
(value) => {
ignoreSync = true;
ignoreSyncRef.current = true;
props.setValue(value);
},
() => props.placeholder,
@ -102,21 +96,28 @@ export default function Input(props: InputProps) {
),
});
view = new EditorView({
viewRef.current = new EditorView({
state,
parent: element,
parent: elementRef.current,
});
});
createEffect(() => {
const editor = view;
return () => {
viewRef.current?.destroy();
viewRef.current = undefined;
};
// Run once to initialize/destroy the editor instance.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const editor = viewRef.current;
if (!editor) return;
const next = externalValue();
const next = props.value;
const current = editor.state.doc.toString();
if (ignoreSync) {
ignoreSync = false;
if (ignoreSyncRef.current) {
ignoreSyncRef.current = false;
return;
}
@ -129,14 +130,9 @@ export default function Input(props: InputProps) {
insert: next,
},
});
});
}, [props.value]);
onCleanup(() => {
view?.destroy();
view = undefined;
});
return <div ref={element} class="tm-md-root" />;
return <div ref={elementRef} className="tm-md-root" />;
}
function createEditorExtensions(

View file

@ -1,4 +1,4 @@
import { For, Index, type JSX } from "solid-js";
import * as React from "react";
type InlineNode =
| { type: "text"; value: string }
@ -345,31 +345,53 @@ export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
return blocks;
}
export function renderInline(nodes: InlineNode[]): JSX.Element[] {
return nodes.map((node) => {
export function renderInline(nodes: InlineNode[]): React.ReactNode[] {
return nodes.map((node, index) => {
if (node.type === "text") {
return node.value;
}
if (node.type === "strong") {
return <strong class="tm-md-strong">{node.value}</strong>;
return (
<strong key={index} className="tm-md-strong">
{node.value}
</strong>
);
}
if (node.type === "em") {
return <em class="tm-md-em">{node.value}</em>;
return (
<em key={index} className="tm-md-em">
{node.value}
</em>
);
}
if (node.type === "del") {
return <del class="tm-md-del">{node.value}</del>;
return (
<del key={index} className="tm-md-del">
{node.value}
</del>
);
}
if (node.type === "code") {
return <code class="tm-md-code">{node.value}</code>;
return (
<code key={index} className="tm-md-code">
{node.value}
</code>
);
}
if (node.type === "link") {
return (
<a class="tm-md-link" href={node.href} target="_blank" rel="noreferrer">
<a
key={index}
className="tm-md-link"
href={node.href}
target="_blank"
rel="noreferrer"
>
{node.label}
</a>
);
@ -377,7 +399,8 @@ export function renderInline(nodes: InlineNode[]): JSX.Element[] {
return (
<img
class="tm-md-image"
key={index}
className="tm-md-image"
src={node.src}
alt={node.alt}
loading="lazy"
@ -387,44 +410,44 @@ export function renderInline(nodes: InlineNode[]): JSX.Element[] {
});
}
export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
return (
<For each={blocks}>
{(block) => {
<>
{blocks.map((block, blockIndex) => {
if (block.type === "heading") {
const className = `tm-md-heading tm-md-h${String(block.level)}`;
if (block.level === 1)
return (
<h1 class={className}>
<h1 key={blockIndex} className={className}>
{renderInline(parseInlineNodes(block.text))}
</h1>
);
if (block.level === 2)
return (
<h2 class={className}>
<h2 key={blockIndex} className={className}>
{renderInline(parseInlineNodes(block.text))}
</h2>
);
if (block.level === 3)
return (
<h3 class={className}>
<h3 key={blockIndex} className={className}>
{renderInline(parseInlineNodes(block.text))}
</h3>
);
if (block.level === 4)
return (
<h4 class={className}>
<h4 key={blockIndex} className={className}>
{renderInline(parseInlineNodes(block.text))}
</h4>
);
if (block.level === 5)
return (
<h5 class={className}>
<h5 key={blockIndex} className={className}>
{renderInline(parseInlineNodes(block.text))}
</h5>
);
return (
<h6 class={className}>
<h6 key={blockIndex} className={className}>
{renderInline(parseInlineNodes(block.text))}
</h6>
);
@ -432,18 +455,18 @@ export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
if (block.type === "blockquote") {
return (
<blockquote class="tm-md-blockquote">
<For each={block.text.split("\n")}>
{(line) => <p>{renderInline(parseInlineNodes(line))}</p>}
</For>
<blockquote key={blockIndex} className="tm-md-blockquote">
{block.text.split("\n").map((line, lineIndex) => (
<p key={lineIndex}>{renderInline(parseInlineNodes(line))}</p>
))}
</blockquote>
);
}
if (block.type === "code") {
return (
<pre class="tm-md-pre">
<code class="tm-md-codeblock" data-language={block.language}>
<pre key={blockIndex} className="tm-md-pre">
<code className="tm-md-codeblock" data-language={block.language}>
{block.code}
</code>
</pre>
@ -453,51 +476,43 @@ export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
if (block.type === "list") {
const Tag = block.ordered ? "ol" : "ul";
return (
<Tag class={block.ordered ? "tm-md-ol" : "tm-md-ul"}>
<For each={block.items}>
{(item) => (
<li class="tm-md-li">
<Tag key={blockIndex} className={block.ordered ? "tm-md-ol" : "tm-md-ul"}>
{block.items.map((item, itemIndex) => (
<li key={itemIndex} className="tm-md-li">
{item.checked !== null ? (
<input
type="checkbox"
checked={item.checked}
disabled
class="tm-md-checkbox"
className="tm-md-checkbox"
/>
) : null}
<span>{renderInline(parseInlineNodes(item.text))}</span>
</li>
)}
</For>
))}
</Tag>
);
}
if (block.type === "table") {
return (
<div class="tm-md-table-wrap">
<table class="tm-md-table">
<div key={blockIndex} className="tm-md-table-wrap">
<table className="tm-md-table">
<thead>
<tr>
<For each={block.headers}>
{(header) => (
<th>{renderInline(parseInlineNodes(header))}</th>
)}
</For>
{block.headers.map((header, headerIndex) => (
<th key={headerIndex}>{renderInline(parseInlineNodes(header))}</th>
))}
</tr>
</thead>
<tbody>
<For each={block.rows}>
{(row) => (
<tr>
<For each={row}>
{(cell) => (
<td>{renderInline(parseInlineNodes(cell))}</td>
)}
</For>
{block.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{renderInline(parseInlineNodes(cell))}</td>
))}
</tr>
)}
</For>
))}
</tbody>
</table>
</div>
@ -505,23 +520,21 @@ export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
}
if (block.type === "hr") {
return <hr class="tm-md-hr" />;
return <hr key={blockIndex} className="tm-md-hr" />;
}
return (
<p class="tm-md-p">
<Index each={block.text.split("\n")}>
{(line, idx) => (
<>
{idx > 0 ? <br /> : null}
{renderInline(parseInlineNodes(line()))}
</>
)}
</Index>
<p key={blockIndex} className="tm-md-p">
{block.text.split("\n").map((line, lineIndex) => (
<React.Fragment key={lineIndex}>
{lineIndex > 0 ? <br /> : null}
{renderInline(parseInlineNodes(line))}
</React.Fragment>
))}
</p>
);
}}
</For>
})}
</>
);
}

View file

@ -1,4 +1,4 @@
import { createMemo } from "solid-js";
import * as React from "react";
import {
ensureMarkdownStyles,
@ -13,7 +13,7 @@ export type TextProps = {
export default function Text(props: TextProps) {
ensureMarkdownStyles();
const blocks = createMemo(() => parseMarkdownBlocks(props.value));
const blocks = React.useMemo(() => parseMarkdownBlocks(props.value), [props.value]);
return <div class="tm-md-root">{renderBlocks(blocks())}</div>;
return <div className="tm-md-root">{renderBlocks(blocks)}</div>;
}

View file

@ -3,8 +3,7 @@
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true