From e5f2d4ceb5fa0e1406d357d716736167c2206d5a Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 19 Aug 2025 03:42:09 +0900 Subject: [PATCH] #93 --- frontend/src/components/WikiBody.tsx | 45 +-------- frontend/src/lib/remark-wiki-autolink.ts | 115 +++++++++++++++-------- frontend/src/pages/wiki/WikiEditPage.tsx | 56 ++++++----- 3 files changed, 107 insertions(+), 109 deletions(-) diff --git a/frontend/src/components/WikiBody.tsx b/frontend/src/components/WikiBody.tsx index 22f716f..a90a9a0 100644 --- a/frontend/src/components/WikiBody.tsx +++ b/frontend/src/components/WikiBody.tsx @@ -33,7 +33,6 @@ const mdComponents = { h1: ({ children }) => {children} { const [pageNames, setPageNames] = useState ([]) - const [realBody, setRealBody] = useState ('') const remarkPlugins = useMemo (() => [remarkWikiAutoLink (pageNames)], [pageNames]) @@ -53,52 +52,10 @@ export default ({ title, body }: Props) => { setPageNames ([]) } }) () - - 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 ( - {realBody || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} + {body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} ) } diff --git a/frontend/src/lib/remark-wiki-autolink.ts b/frontend/src/lib/remark-wiki-autolink.ts index 9799e37..15a7c29 100644 --- a/frontend/src/lib/remark-wiki-autolink.ts +++ b/frontend/src/lib/remark-wiki-autolink.ts @@ -1,14 +1,10 @@ -import { visitParents } from 'unist-util-visit-parents' - -import type { Parent, Root, RootContent, Text } from 'mdast' +import type { Content, Parent, Root, RootContent } 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)) + const names = pageNames.sort ((a, b) => b.length - a.length) 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') return (tree: Root) => { - visitParents (tree, 'text', (node: Text, ancestors: Parent[]) => { - if (ancestors.some (ancestor => ['link', - 'linkReference', - 'image', - 'imageReference', - 'code', - 'inlineCode'].includes (ancestor.type))) + const edits: { parent: Parent; index: number; parts: RootContent[] }[] = [] + + const walk = (node: Content | Root, ancestors: Parent[]) => { + if (!(node) || (typeof node !== 'object')) return - const value = node.value - re.lastIndex = 0 - let match: RegExpExecArray | null - let last = 0 - const parts: RootContent[] = [] + if (!(ancestors.some (ancestor => ['link', + 'linkReference', + 'image', + 'imageReference', + 'code', + 'inlineCode'].includes (ancestor?.type))) + && (node.type === 'text')) + { + const value = node.value ?? '' + if (value) + { + re.lastIndex = 0 + let m: RegExpExecArray | null + let last = 0 + const parts: RootContent[] = [] + + while (m = re.exec (value)) + { + const start = m.index + const end = start + m[0].length - while (match = re.exec (value)) + if (start > last) + parts.push ({ type: 'text', value: value.slice (last, start) }) + + 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 }) + } + } + } + } + + const maybeChidren = (node as any).children + if (Array.isArray (maybeChidren)) { - 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 + const parent = node as Parent + + for (let i = 0; i < maybeChidren.length; ++i) + { + const child: Content | undefined = maybeChidren[i] + if (!(child)) + continue + walk (child, ancestors.concat (parent)) + } } + } - if (parts.length === 0) - return + walk (tree, []) + + for (let i = edits.length - 1; i >= 0; --i) + { + const { parent, index, parts } = edits[i] - if (last < value.length) - parts.push ({ type: 'text', value: value.slice (last) }) + if (!(parent) || !(Array.isArray (parent.children))) + continue - const parent: Parent = ancestors[ancestors.length - 1] - const idx = parent.children.indexOf (node) - parent.children.splice (idx, 1, ...parts) - }) + if (0 <= index && index < parent.children.length) + parent.children.splice (index, 1, ...parts) + } } } diff --git a/frontend/src/pages/wiki/WikiEditPage.tsx b/frontend/src/pages/wiki/WikiEditPage.tsx index 064fa1b..cd57a78 100644 --- a/frontend/src/pages/wiki/WikiEditPage.tsx +++ b/frontend/src/pages/wiki/WikiEditPage.tsx @@ -27,8 +27,9 @@ export default ({ user }: Props) => { const navigate = useNavigate () - const [title, setTitle] = useState ('') const [body, setBody] = useState ('') + const [loading, setLoading] = useState (true) + const [title, setTitle] = useState ('') const handleSubmit = async () => { const formData = new FormData () @@ -51,10 +52,12 @@ export default ({ user }: Props) => { useEffect (() => { void (async () => { + setLoading (true) const res = await axios.get (`${ API_BASE_URL }/wiki/${ id }`) const data = res.data as WikiPage setTitle (data.title) setBody (data.body) + setLoading (false) }) () }, [id]) @@ -66,30 +69,33 @@ export default ({ user }: Props) => {

Wiki ページを編輯

- {/* タイトル */} - {/* TODO: タグ補完 */} -
- - setTitle (e.target.value)} - className="w-full border p-2 rounded" /> -
- - {/* 本文 */} -
- - mdParser.render (text)} - onChange={({ text }) => setBody (text)} /> -
- - {/* 送信 */} - + {loading ? 'Loading...' : ( + <> + {/* タイトル */} + {/* TODO: タグ補完 */} +
+ + setTitle (e.target.value)} + className="w-full border p-2 rounded" /> +
+ + {/* 本文 */} +
+ + mdParser.render (text)} + onChange={({ text }) => setBody (text)} /> +
+ + {/* 送信 */} + + )}
) }