このコミットが含まれているのは:
@@ -33,7 +33,6 @@ const mdComponents = { h1: ({ children }) => <SectionTitle>{children}</SectionT
|
|||||||
|
|
||||||
export default ({ title, body }: Props) => {
|
export default ({ title, body }: Props) => {
|
||||||
const [pageNames, setPageNames] = useState<string[]> ([])
|
const [pageNames, setPageNames] = useState<string[]> ([])
|
||||||
const [realBody, setRealBody] = useState<string> ('')
|
|
||||||
|
|
||||||
const remarkPlugins = useMemo (() => [remarkWikiAutoLink (pageNames)], [pageNames])
|
const remarkPlugins = useMemo (() => [remarkWikiAutoLink (pageNames)], [pageNames])
|
||||||
|
|
||||||
@@ -53,52 +52,10 @@ export default ({ title, body }: Props) => {
|
|||||||
setPageNames ([])
|
setPageNames ([])
|
||||||
}
|
}
|
||||||
}) ()
|
}) ()
|
||||||
|
|
||||||
setRealBody ('')
|
|
||||||
}, [body])
|
}, [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 (
|
return (
|
||||||
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
|
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
|
||||||
{realBody || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
|
{body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
|
||||||
</ReactMarkdown>)
|
</ReactMarkdown>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { visitParents } from 'unist-util-visit-parents'
|
import type { Content, Parent, Root, RootContent } from 'mdast'
|
||||||
|
|
||||||
import type { Parent, Root, RootContent, Text } from 'mdast'
|
|
||||||
|
|
||||||
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
|
||||||
|
|
||||||
export default (pageNames: string[], basePath = '/wiki'): ((tree: Root) => void) => {
|
export default (pageNames: string[], basePath = '/wiki'): ((tree: Root) => void) => {
|
||||||
const names = ([...(new Set(pageNames))]
|
const names = pageNames.sort ((a, b) => b.length - a.length)
|
||||||
.filter (Boolean)
|
|
||||||
.sort ((a, b) => b.length - a.length))
|
|
||||||
|
|
||||||
if (names.length === 0)
|
if (names.length === 0)
|
||||||
{
|
{
|
||||||
@@ -20,47 +16,86 @@ export default (pageNames: string[], basePath = '/wiki'): ((tree: Root) => void)
|
|||||||
const re = new RegExp (`(${ names.map (escapeForRegExp).join ('|') })`, 'g')
|
const re = new RegExp (`(${ names.map (escapeForRegExp).join ('|') })`, 'g')
|
||||||
|
|
||||||
return (tree: Root) => {
|
return (tree: Root) => {
|
||||||
visitParents (tree, 'text', (node: Text, ancestors: Parent[]) => {
|
const edits: { parent: Parent; index: number; parts: RootContent[] }[] = []
|
||||||
if (ancestors.some (ancestor => ['link',
|
|
||||||
'linkReference',
|
const walk = (node: Content | Root, ancestors: Parent[]) => {
|
||||||
'image',
|
if (!(node) || (typeof node !== 'object'))
|
||||||
'imageReference',
|
|
||||||
'code',
|
|
||||||
'inlineCode'].includes (ancestor.type)))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
const value = node.value
|
if (!(ancestors.some (ancestor => ['link',
|
||||||
re.lastIndex = 0
|
'linkReference',
|
||||||
let match: RegExpExecArray | null
|
'image',
|
||||||
let last = 0
|
'imageReference',
|
||||||
const parts: RootContent[] = []
|
'code',
|
||||||
|
'inlineCode'].includes (ancestor?.type)))
|
||||||
while (match = re.exec (value))
|
&& (node.type === 'text'))
|
||||||
{
|
{
|
||||||
const start = match.index
|
const value = node.value ?? ''
|
||||||
const end = start + match[0].length
|
if (value)
|
||||||
if (start > last)
|
{
|
||||||
parts.push ({ type: 'text', value: value.slice (last, start) })
|
re.lastIndex = 0
|
||||||
|
let m: RegExpExecArray | null
|
||||||
|
let last = 0
|
||||||
|
const parts: RootContent[] = []
|
||||||
|
|
||||||
const name = match[0]
|
while (m = re.exec (value))
|
||||||
parts.push ({
|
{
|
||||||
type: 'link',
|
const start = m.index
|
||||||
url: `${ basePath }/${ encodeURIComponent (name) }`,
|
const end = start + m[0].length
|
||||||
title: null,
|
|
||||||
children: [{ type: 'text', value: name }],
|
if (start > last)
|
||||||
data: { hProperties: { 'data-wiki': '1' } } })
|
parts.push ({ type: 'text', value: value.slice (last, start) })
|
||||||
last = end
|
|
||||||
|
const name = m[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)
|
||||||
|
{
|
||||||
|
if (last < value.length)
|
||||||
|
parts.push ({ type: 'text', value: value.slice (last) })
|
||||||
|
const parent = ancestors[ancestors.length - 1]
|
||||||
|
if (parent && Array.isArray (parent.children))
|
||||||
|
{
|
||||||
|
const index = parent.children.indexOf (node)
|
||||||
|
if (index >= 0)
|
||||||
|
edits.push ({ parent, index, parts })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length === 0)
|
const maybeChidren = (node as any).children
|
||||||
return
|
if (Array.isArray (maybeChidren))
|
||||||
|
{
|
||||||
|
const parent = node as Parent
|
||||||
|
|
||||||
if (last < value.length)
|
for (let i = 0; i < maybeChidren.length; ++i)
|
||||||
parts.push ({ type: 'text', value: value.slice (last) })
|
{
|
||||||
|
const child: Content | undefined = maybeChidren[i]
|
||||||
|
if (!(child))
|
||||||
|
continue
|
||||||
|
walk (child, ancestors.concat (parent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parent: Parent = ancestors[ancestors.length - 1]
|
walk (tree, [])
|
||||||
const idx = parent.children.indexOf (node)
|
|
||||||
parent.children.splice (idx, 1, ...parts)
|
for (let i = edits.length - 1; i >= 0; --i)
|
||||||
})
|
{
|
||||||
|
const { parent, index, parts } = edits[i]
|
||||||
|
|
||||||
|
if (!(parent) || !(Array.isArray (parent.children)))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (0 <= index && index < parent.children.length)
|
||||||
|
parent.children.splice (index, 1, ...parts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ export default ({ user }: Props) => {
|
|||||||
|
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
const [title, setTitle] = useState ('')
|
|
||||||
const [body, setBody] = useState ('')
|
const [body, setBody] = useState ('')
|
||||||
|
const [loading, setLoading] = useState (true)
|
||||||
|
const [title, setTitle] = useState ('')
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const formData = new FormData ()
|
const formData = new FormData ()
|
||||||
@@ -51,10 +52,12 @@ export default ({ user }: Props) => {
|
|||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
setLoading (true)
|
||||||
const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }`)
|
const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }`)
|
||||||
const data = res.data as WikiPage
|
const data = res.data as WikiPage
|
||||||
setTitle (data.title)
|
setTitle (data.title)
|
||||||
setBody (data.body)
|
setBody (data.body)
|
||||||
|
setLoading (false)
|
||||||
}) ()
|
}) ()
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
@@ -66,30 +69,33 @@ export default ({ user }: Props) => {
|
|||||||
<div className="max-w-xl mx-auto p-4 space-y-4">
|
<div className="max-w-xl mx-auto p-4 space-y-4">
|
||||||
<h1 className="text-2xl font-bold mb-2">Wiki ページを編輯</h1>
|
<h1 className="text-2xl font-bold mb-2">Wiki ページを編輯</h1>
|
||||||
|
|
||||||
{/* タイトル */}
|
{loading ? 'Loading...' : (
|
||||||
{/* TODO: タグ補完 */}
|
<>
|
||||||
<div>
|
{/* タイトル */}
|
||||||
<label className="block font-semibold mb-1">タイトル</label>
|
{/* TODO: タグ補完 */}
|
||||||
<input type="text"
|
<div>
|
||||||
value={title}
|
<label className="block font-semibold mb-1">タイトル</label>
|
||||||
onChange={e => setTitle (e.target.value)}
|
<input type="text"
|
||||||
className="w-full border p-2 rounded" />
|
value={title}
|
||||||
</div>
|
onChange={e => setTitle (e.target.value)}
|
||||||
|
className="w-full border p-2 rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 本文 */}
|
{/* 本文 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-semibold mb-1">本文</label>
|
<label className="block font-semibold mb-1">本文</label>
|
||||||
<MdEditor value={body}
|
<MdEditor value={body}
|
||||||
style={{ height: '500px' }}
|
style={{ height: '500px' }}
|
||||||
renderHTML={text => mdParser.render (text)}
|
renderHTML={text => mdParser.render (text)}
|
||||||
onChange={({ text }) => setBody (text)} />
|
onChange={({ text }) => setBody (text)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 送信 */}
|
{/* 送信 */}
|
||||||
<button onClick={handleSubmit}
|
<button onClick={handleSubmit}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
|
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
|
||||||
追加
|
追加
|
||||||
</button>
|
</button>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする