component renderer

This commit is contained in:
polv committed 2026-02-23 06:24:10 +07:00
1 parent 098ee45b81
commit d96346bfea
2 files changed
+155 -5

No files matched your search

+25 -5
View File
@@ -2,9 +2,10 @@ import "./App.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Group, Input, Stack } from "@chakra-ui/react"; import { Box, Button, Card, Group, Input, Stack } from "@chakra-ui/react";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import Glossary from "./components/Glossary";
import { Provider } from "./components/ui/provider"; import { Provider } from "./components/ui/provider";
function App() { function App() {
@@ -50,16 +51,35 @@ function App() {
value={q} value={q}
onChange={(e) => setQ(e.currentTarget.value)} onChange={(e) => setQ(e.currentTarget.value)}
autoComplete="off" autoComplete="off"
spellCheck={false}
placeholder="Search..." placeholder="Search..."
/> />
<Button type="submit">Search</Button> <Button type="submit">Search</Button>
</Group> </Group>
</form> </form>
<ol> <Box as={"ol"} listStyleType={"number"} style={{ margin: "1em" }}>
{entries.map((it, i) => ( {entries.map(({ term, reading, glossary_json, ...it }, i) => (
<li key={i}>{JSON.stringify(it)}</li> <li key={i}>
<details>
<summary>
{term} ({reading})
</summary>
<Card.Root>
<Card.Header>{JSON.stringify(it)}</Card.Header>
<Card.Body>
{JSON.parse(glossary_json).map((g: any) => (
<Glossary
glossary={g}
onTermClicked={(t) => setQ(t + " ")}
/>
))}
</Card.Body>
</Card.Root>
</details>
</li>
))} ))}
</ol> </Box>
</Stack> </Stack>
</Provider> </Provider>
); );
+130
View File
@@ -0,0 +1,130 @@
import { createElement as h } from "react";
export default function Glossary({
glossary,
onTermClicked,
}: {
glossary: any;
onTermClicked: (t: string) => void;
}) {
// Plain string
if (typeof glossary === "string") {
return glossary;
}
// GlossaryText
if ("type" in glossary && glossary.type === "text") {
return glossary.text;
}
// GlossaryImage
if ("type" in glossary && glossary.type === "image") {
return <GlossaryImage {...glossary} />;
}
// GlossaryStructuredContent
if ("type" in glossary && glossary.type === "structured-content") {
const sc = glossary;
return (
<StructuredContent node={sc.content} onTermClicked={onTermClicked} />
);
}
// GlossaryDeinflection
if (Array.isArray(glossary) && glossary.length === 2) {
const [term, rules] = glossary;
return <small>{`${term} (${rules.join(", ")})`}</small>;
}
return null;
}
function GlossaryImage(img: any) {
return (
<img
src={img.path}
width={img.width}
height={img.height}
title={img.title}
alt={img.alt}
style={{
imageRendering: img.imageRendering || "auto",
width: `${img.width}em`,
height: `${img.height}em`,
}}
/>
);
}
function StructuredContent({
node,
onTermClicked,
}: {
node: any;
onTermClicked: (t: string) => void;
}): any {
if (typeof node === "string") {
return node;
}
if (Array.isArray(node)) {
return (
<>
{...node.map((n) => (
<StructuredContent node={n} onTermClicked={onTermClicked} />
))}
</>
);
}
// Line break
if ("tag" in node && node.tag === "br") {
return <br />;
}
// Image
if ("tag" in node && node.tag === "img") {
return <GlossaryImage {...node} />;
}
// Link
if ("tag" in node && node.tag === "a") {
const link = node as any;
const { href, content } = link;
const isWebLink = /^https?:\/\//.test(href);
return (
<a
href={href}
target={isWebLink ? "_blank" : ""}
onClick={(ev) => {
if (isWebLink) return true;
ev.preventDefault();
const querySearch = "?query=";
if (href.startsWith(querySearch)) {
onTermClicked(href.substring(querySearch.length));
}
return false;
}}
>
{content ? (
<StructuredContent node={content} onTermClicked={onTermClicked} />
) : null}
</a>
);
}
// Block/container elements (span, div, ol, ul, li, ruby, table, etc.)
if ("tag" in node) {
const block = node as any;
return h(
block.tag,
{ style: block.style, title: block.title, open: block.open },
StructuredContent({ node: block.content, onTermClicked }),
);
}
return null;
}