106 行
3.1 KiB
TypeScript
106 行
3.1 KiB
TypeScript
import axios from 'axios'
|
|
import toCamel from 'camelcase-keys'
|
|
import { useEffect, useState } from 'react'
|
|
import ReactMarkdown from 'react-markdown'
|
|
import { Link } from 'react-router-dom'
|
|
import remarkGFM from 'remark-gfm'
|
|
|
|
import SectionTitle from '@/components/common/SectionTitle'
|
|
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
|
import { API_BASE_URL } from '@/config'
|
|
|
|
import type { FC } from 'react'
|
|
import type { Components } from 'react-markdown'
|
|
|
|
import type { WikiPage } from '@/types'
|
|
|
|
type Props = { title: string
|
|
body?: string }
|
|
|
|
const mdComponents = { h1: ({ children }) => <SectionTitle>{children}</SectionTitle>,
|
|
h2: ({ children }) => <SubsectionTitle>{children}</SubsectionTitle>,
|
|
ol: ({ children }) => <ol className="list-decimal pl-6">{children}</ol>,
|
|
ul: ({ children }) => <ul className="list-disc pl-6">{children}</ul>,
|
|
a: (({ href, children }) => (
|
|
['/', '.'].some (e => href?.startsWith (e))
|
|
? <Link to={href!}>{children}</Link>
|
|
: (
|
|
<a href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer">
|
|
{children}
|
|
</a>))) } as const satisfies Components
|
|
|
|
|
|
export default (({ title, body }: Props) => {
|
|
const [pageNames, setPageNames] = useState<string[]> ([])
|
|
const [realBody, setRealBody] = useState<string> ('')
|
|
|
|
useEffect (() => {
|
|
if (!(body))
|
|
return
|
|
|
|
void (async () => {
|
|
try
|
|
{
|
|
const res = await axios.get (`${ API_BASE_URL }/wiki`)
|
|
const data = toCamel (res.data as any, { deep: true }) as WikiPage[]
|
|
setPageNames (data.map (page => page.title).sort ((a, b) => b.length - a.length))
|
|
}
|
|
catch
|
|
{
|
|
setPageNames ([])
|
|
}
|
|
}) ()
|
|
}, [])
|
|
|
|
useEffect (() => {
|
|
setRealBody ('')
|
|
}, [body])
|
|
|
|
useEffect (() => {
|
|
if (!(body))
|
|
return
|
|
|
|
const matchIndices = (target: string, keyword: string) => {
|
|
const indices: number[] = []
|
|
let pos = 0
|
|
let idx
|
|
while ((idx = target.indexOf (keyword, pos)) >= 0)
|
|
{
|
|
indices.push (idx)
|
|
pos = idx + keyword.length
|
|
}
|
|
|
|
return indices
|
|
}
|
|
|
|
const linkIndices = (text: string, names: string[]): [string, [number, number]][] => {
|
|
const result: [string, [number, number]][] = []
|
|
|
|
names.forEach (name => {
|
|
matchIndices (text, name).forEach (idx => {
|
|
const start = idx
|
|
const end = idx + name.length
|
|
const overlaps = result.some (([, [st, ed]]) => start < ed && end > st)
|
|
if (!(overlaps))
|
|
result.push ([name, [start, end]])
|
|
})
|
|
})
|
|
|
|
return result.sort (([, [a]], [, [b]]) => b - a)
|
|
}
|
|
|
|
setRealBody (
|
|
linkIndices (body, pageNames).reduce ((acc, [name, [start, end]]) => (
|
|
acc.slice (0, start)
|
|
+ `[${ name }](/wiki/${ encodeURIComponent (name) })`
|
|
+ acc.slice (end)), body))
|
|
}, [body, pageNames])
|
|
|
|
return (
|
|
<ReactMarkdown components={mdComponents} remarkPlugins={[remarkGFM]}>
|
|
{realBody || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
|
|
</ReactMarkdown>)
|
|
}) satisfies FC<Props>
|