diff --git a/app/markdown/markdown.tsx b/app/markdown/markdown.tsx index 2203c96..2fa0aa2 100644 --- a/app/markdown/markdown.tsx +++ b/app/markdown/markdown.tsx @@ -2,13 +2,12 @@ import Markdown, { Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import removeComments from "remark-remove-comments"; import remarkCjkFriendly from "remark-cjk-friendly"; -import { - MultiHighlightTag, - remarkMultiHighlight, -} from "./multiHighlight"; +import { MultiHighlightTag, remarkMultiHighlight } from "./multiHighlight"; import { Heading } from "./heading"; import { AutoCodeBlock } from "./codeBlock"; import { ReplacedRange } from "@/lib/docs"; +import remarkTerm from "./remarkTerm"; +import Term from "./term"; export function StyledMarkdown(props: { content: string; @@ -21,6 +20,7 @@ export function StyledMarkdown(props: { removeComments, remarkCjkFriendly, [remarkMultiHighlight, props.replacedRange], + remarkTerm, ]} components={components} > @@ -58,4 +58,5 @@ const components: Components = { pre: ({ node, ...props }) => props.children, code: AutoCodeBlock, ins: MultiHighlightTag, + q: Term, }; diff --git a/app/markdown/remarkTerm.ts b/app/markdown/remarkTerm.ts new file mode 100644 index 0000000..69da4ac --- /dev/null +++ b/app/markdown/remarkTerm.ts @@ -0,0 +1,94 @@ +import type { Plugin } from "unified"; +import type { Nodes, PhrasingContent, Root, RootContent } from "mdast"; +import { phrasing } from "mdast-util-phrasing"; + +/** + * `[[用語]]`を`用語`に変換するプラグイン。 + * + * https://github.com/ut-code/utcode-learn/blob/main/src/remark/remark-term.ts からコピペ + * Copyright (c) 2023 ut.code(); + * + * @example + * // returns "**HTML**CSS、そしてJavaScriptです。" + * String( + * await remark() + * .use(remarkMdx) + * .use(remarkTerm) + * .process("[[**HTML**]]と[[CSS]]、そして[[JavaScript]]です。"), + * ); + */ +const remarkTerm: Plugin<[], Root> = () => (tree) => transform(tree); + +export default remarkTerm; + +function isParent(node: Nodes) { + return "children" in node; +} + +function transform(node: Nodes) { + if (!isParent(node)) return; + + for (const child of node.children) { + transform(child); + } + + node.children = wrapDelimitedPhrasingContentsAsTerm( + node.children.flatMap((child) => isolateTermDelimiters(child)) + ); +} + +function isolateTermDelimiters(node: RootContent): RootContent[] { + if (node.type !== "text") return [node]; + + return node.value + .split(/(\[\[|\]\])/) + .filter((segment) => segment !== "") + .map((segment) => ({ + type: "text", + value: segment, + })); +} + +function wrapDelimitedPhrasingContentsAsTerm( + children: RootContent[] +): RootContent[] { + const result: RootContent[] = []; + const buffer: PhrasingContent[] = []; + + for (const child of children) { + if (buffer.length === 0) { + if (child.type === "text" && child.value === "[[") { + buffer.push(child); + continue; + } + result.push(child); + continue; + } + + if (child.type === "text" && child.value === "]]") { + // 修正ポイント:MDXノードの代わりに、hNameを持った汎用ノードを作成 + result.push({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type: "term" as any, + data: { + hName: "q", + }, + children: buffer.slice(1), + }); + buffer.length = 0; + continue; + } + + if (phrasing(child)) { + buffer.push(child); + continue; + } + + result.push(...buffer, child); + buffer.length = 0; + } + + result.push(...buffer); + + return result; +} diff --git a/app/markdown/term.tsx b/app/markdown/term.tsx new file mode 100644 index 0000000..fcbb6ca --- /dev/null +++ b/app/markdown/term.tsx @@ -0,0 +1,49 @@ +import { JSX } from "react"; +import { ExtraProps } from "react-markdown"; +import { onlyText } from "react-children-utilities"; + +/** + * https://github.com/ut-code/utcode-learn/blob/main/src/components/Term/index.tsx をもとに独自実装 + * Copyright (c) 2023 ut.code(); + */ +export default function Term(props: JSX.IntrinsicElements["q"] & ExtraProps) { + // 動作確認用 + return <>[{props.children} → term:{onlyText(props.children)}]; + + // const term = props.id + // ? terms.find((term) => term.id === props.id) + // : terms.find( + // (term) => + // term.name === onlyText(props.children) || + // term.aliases.includes(onlyText(props.children)), + // ); + // if (!term) + // throw new Error( + // `${props.id ? props.id : onlyText(props.children)}という用語は定義されていません`, + // ); + + // const wrap = (content: JSX.Element) => { + + // return ( + // + //
{term.name}
+ //
{term.definition}
+ // {shouldLinkToReferencePage && ( + // + // {referencePageTitle} へ + // + // + // )} + // + // } + // > + // {content} + //
+ // ); + // }; +} diff --git a/package-lock.json b/package-lock.json index 2f75755..38786a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "prismjs": "^1.30.0", "react": "^19", "react-ace": "^14.0.1", + "react-children-utilities": "^2.10.0", "react-dom": "^19", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", @@ -18354,6 +18355,15 @@ "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-children-utilities": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/react-children-utilities/-/react-children-utilities-2.10.0.tgz", + "integrity": "sha512-9naSkrOHACjoDsHDtAQR5mGMp3ffv1DkUSQPDa8iJFP0+lXloS4fw44hMHaWeNF3qL4B3GPiJ9032Oo309oa9g==", + "license": "MIT", + "peerDependencies": { + "react": ">=15" + } + }, "node_modules/react-dom": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", diff --git a/package.json b/package.json index e5b4bc9..5d68893 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "prismjs": "^1.30.0", "react": "^19", "react-ace": "^14.0.1", + "react-children-utilities": "^2.10.0", "react-dom": "^19", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0",