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.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",