From d96346bfea9b039372e97b2f8ee4e87880474549 Mon Sep 17 00:00:00 2001 From: polv Date: Mon, 23 Feb 2026 06:24:10 +0700 Subject: [PATCH] component renderer --- src-react/src/App.tsx | 30 +++++- src-react/src/components/Glossary.tsx | 130 ++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src-react/src/components/Glossary.tsx diff --git a/src-react/src/App.tsx b/src-react/src/App.tsx index 37aa416..5bde710 100644 --- a/src-react/src/App.tsx +++ b/src-react/src/App.tsx @@ -2,9 +2,10 @@ import "./App.css"; 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 Glossary from "./components/Glossary"; import { Provider } from "./components/ui/provider"; function App() { @@ -50,16 +51,35 @@ function App() { value={q} onChange={(e) => setQ(e.currentTarget.value)} autoComplete="off" + spellCheck={false} placeholder="Search..." /> -
    - {entries.map((it, i) => ( -
  1. {JSON.stringify(it)}
  2. + + {entries.map(({ term, reading, glossary_json, ...it }, i) => ( +
  3. +
    + + {term} ({reading}) + + + + {JSON.stringify(it)} + + {JSON.parse(glossary_json).map((g: any) => ( + setQ(t + " ")} + /> + ))} + + +
    +
  4. ))} -
+ ); diff --git a/src-react/src/components/Glossary.tsx b/src-react/src/components/Glossary.tsx new file mode 100644 index 0000000..5cc9eb1 --- /dev/null +++ b/src-react/src/components/Glossary.tsx @@ -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 ; + } + + // GlossaryStructuredContent + if ("type" in glossary && glossary.type === "structured-content") { + const sc = glossary; + return ( + + ); + } + + // GlossaryDeinflection + if (Array.isArray(glossary) && glossary.length === 2) { + const [term, rules] = glossary; + return {`${term} (${rules.join(", ")})`}; + } + + return null; +} + +function GlossaryImage(img: any) { + return ( + {img.alt} + ); +} + +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) => ( + + ))} + + ); + } + + // Line break + if ("tag" in node && node.tag === "br") { + return
; + } + + // Image + if ("tag" in node && node.tag === "img") { + return ; + } + + // Link + if ("tag" in node && node.tag === "a") { + const link = node as any; + const { href, content } = link; + + const isWebLink = /^https?:\/\//.test(href); + + return ( + { + if (isWebLink) return true; + ev.preventDefault(); + const querySearch = "?query="; + if (href.startsWith(querySearch)) { + onTermClicked(href.substring(querySearch.length)); + } + return false; + }} + > + {content ? ( + + ) : null} + + ); + } + + // 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; +}