@@ -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) | |||
}) | |||
} | |||
} |