| @@ -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", | |||
| @@ -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", | |||
| @@ -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<string[]> ([]) | |||
| const [realBody, setRealBody] = useState<string> ('') | |||
| 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 ( | |||
| <ReactMarkdown components={mdComponents}> | |||
| <ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}> | |||
| {realBody || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} | |||
| </ReactMarkdown>) | |||
| } | |||
| @@ -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) | |||
| }) | |||
| } | |||
| } | |||