From 2550d31e3b61e4b22dd7858e65f0fd1e10c98cc9 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 19 Aug 2025 01:48:01 +0900 Subject: [PATCH] #93 --- frontend/package-lock.json | 3 +- frontend/package.json | 3 +- frontend/src/components/WikiBody.tsx | 11 ++-- frontend/src/lib/remark-wiki-autolink.ts | 66 ++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 frontend/src/lib/remark-wiki-autolink.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 417dc54..b9f4db2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,8 @@ "react-markdown": "^10.1.0", "react-markdown-editor-lite": "^1.3.4", "react-router-dom": "^6.30.0", - "tailwind-merge": "^3.3.0" + "tailwind-merge": "^3.3.0", + "unist-util-visit-parents": "^6.0.1" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/frontend/package.json b/frontend/package.json index 7986d56..602e342 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,8 @@ "react-markdown": "^10.1.0", "react-markdown-editor-lite": "^1.3.4", "react-router-dom": "^6.30.0", - "tailwind-merge": "^3.3.0" + "tailwind-merge": "^3.3.0", + "unist-util-visit-parents": "^6.0.1" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/frontend/src/components/WikiBody.tsx b/frontend/src/components/WikiBody.tsx index e041398..22f716f 100644 --- a/frontend/src/components/WikiBody.tsx +++ b/frontend/src/components/WikiBody.tsx @@ -1,12 +1,13 @@ import axios from 'axios' import toCamel from 'camelcase-keys' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import ReactMarkdown from 'react-markdown' import { Link } from 'react-router-dom' import SectionTitle from '@/components/common/SectionTitle' import SubsectionTitle from '@/components/common/SubsectionTitle' import { API_BASE_URL } from '@/config' +import remarkWikiAutoLink from '@/lib/remark-wiki-autolink' import type { Components } from 'react-markdown' @@ -34,6 +35,8 @@ export default ({ title, body }: Props) => { const [pageNames, setPageNames] = useState ([]) const [realBody, setRealBody] = useState ('') + const remarkPlugins = useMemo (() => [remarkWikiAutoLink (pageNames)], [pageNames]) + useEffect (() => { if (!(body)) return @@ -42,7 +45,7 @@ export default ({ title, body }: Props) => { try { const res = await axios.get (`${ API_BASE_URL }/wiki`) - const data = toCamel (res.data as any, { deep: true }) as WikiPage[] + const data: WikiPage[] = toCamel (res.data as any, { deep: true }) setPageNames (data.map (page => page.title).sort ((a, b) => b.length - a.length)) } catch @@ -50,9 +53,7 @@ export default ({ title, body }: Props) => { setPageNames ([]) } }) () - }, []) - useEffect (() => { setRealBody ('') }, [body]) @@ -97,7 +98,7 @@ export default ({ title, body }: Props) => { }, [body, pageNames]) return ( - + {realBody || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} ) } diff --git a/frontend/src/lib/remark-wiki-autolink.ts b/frontend/src/lib/remark-wiki-autolink.ts new file mode 100644 index 0000000..9799e37 --- /dev/null +++ b/frontend/src/lib/remark-wiki-autolink.ts @@ -0,0 +1,66 @@ +import { visitParents } from 'unist-util-visit-parents' + +import type { Parent, Root, RootContent, Text } from 'mdast' + +const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&') + + +export default (pageNames: string[], basePath = '/wiki'): ((tree: Root) => void) => { + const names = ([...(new Set(pageNames))] + .filter (Boolean) + .sort ((a, b) => b.length - a.length)) + + if (names.length === 0) + { + return () => { + ; + } + } + + const re = new RegExp (`(${ names.map (escapeForRegExp).join ('|') })`, 'g') + + return (tree: Root) => { + visitParents (tree, 'text', (node: Text, ancestors: Parent[]) => { + if (ancestors.some (ancestor => ['link', + 'linkReference', + 'image', + 'imageReference', + 'code', + 'inlineCode'].includes (ancestor.type))) + return + + const value = node.value + re.lastIndex = 0 + let match: RegExpExecArray | null + let last = 0 + const parts: RootContent[] = [] + + while (match = re.exec (value)) + { + const start = match.index + const end = start + match[0].length + if (start > last) + parts.push ({ type: 'text', value: value.slice (last, start) }) + + const name = match[0] + parts.push ({ + type: 'link', + url: `${ basePath }/${ encodeURIComponent (name) }`, + title: null, + children: [{ type: 'text', value: name }], + data: { hProperties: { 'data-wiki': '1' } } }) + last = end + } + + if (parts.length === 0) + return + + if (last < value.length) + parts.push ({ type: 'text', value: value.slice (last) }) + + const parent: Parent = ancestors[ancestors.length - 1] + const idx = parent.children.indexOf (node) + parent.children.splice (idx, 1, ...parts) + }) + } +}