このコミットが含まれているのは:
@@ -11,8 +11,8 @@ import { localeFromPath, localeLanguage, unlocalisedPath } from '@/lib/locales'
|
||||
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
||||
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
||||
import { fetchWikiPage,
|
||||
fetchWikiPageByTitle,
|
||||
import { fetchWikiAutolinkCandidates,
|
||||
fetchWikiPageByReference,
|
||||
fetchWikiPages } from '@/lib/wiki'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
@@ -114,22 +114,16 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url, locale) => {
|
||||
|
||||
const wikiPage = await qc.fetchQuery ({
|
||||
queryKey: wikiKeys.show (title, { version }, locale),
|
||||
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
|
||||
queryFn: () => fetchWikiPageByReference (title, { version }, locale) })
|
||||
|
||||
if (wikiPage)
|
||||
{
|
||||
const effectiveId = String (wikiPage.id ?? '')
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: wikiKeys.show (effectiveId, { }, locale),
|
||||
queryFn: () => fetchWikiPage (effectiveId, { }, locale ) })
|
||||
|
||||
if (wikiPage.body)
|
||||
{
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: wikiKeys.index ({ }, locale),
|
||||
queryFn: () => fetchWikiPages ({ }, locale) })
|
||||
}
|
||||
}
|
||||
if (wikiPage?.body)
|
||||
{
|
||||
const language = wikiPage.languageCode
|
||||
?? (wikiPage.resourceType === 'tag_description' ? localeLanguage (locale) : 'und')
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: wikiKeys.autolinkCandidates (language, locale),
|
||||
queryFn: () => fetchWikiAutolinkCandidates (language, locale) })
|
||||
}
|
||||
|
||||
const effectiveTitle = wikiPage?.title ?? title
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ export const materialsKeys = {
|
||||
|
||||
export const wikiKeys = {
|
||||
root: ['wiki'] as const,
|
||||
autolinkCandidates: (language: string, locale: LocaleCode) =>
|
||||
['wiki', 'autolink', locale, language] as const,
|
||||
index: (
|
||||
p: { title?: string },
|
||||
locale: LocaleCode = 'ja',
|
||||
|
||||
@@ -1,101 +1,128 @@
|
||||
import type { Content, Parent, Root, RootContent } from 'mdast'
|
||||
|
||||
import type { WikiAutolinkCandidate } from '@/types'
|
||||
|
||||
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
|
||||
export default (pageNames: string[], basePath = '/wiki'): ((tree: Root) => void) => {
|
||||
const names = [...pageNames].sort ((a, b) => b.length - a.length)
|
||||
const wordCharacter = /[\p{L}\p{M}\p{N}_]/u
|
||||
const excluded = ['link', 'linkReference', 'image', 'imageReference',
|
||||
'code', 'inlineCode', 'html']
|
||||
|
||||
if (names.length === 0)
|
||||
|
||||
const edgeCharacter = (node: Content, first: boolean): string => {
|
||||
if (node.type === 'text' || node.type === 'inlineCode')
|
||||
{
|
||||
const characters = Array.from (node.value)
|
||||
return (first ? characters[0] : characters.at (-1)) ?? ''
|
||||
}
|
||||
if ('children' in node)
|
||||
{
|
||||
const children = first ? node.children : [...node.children].reverse ()
|
||||
for (const child of children)
|
||||
{
|
||||
return () => {
|
||||
;
|
||||
}
|
||||
const character = edgeCharacter (child, first)
|
||||
if (character !== '')
|
||||
return character
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return '\u3000'
|
||||
}
|
||||
|
||||
const re = new RegExp (`(${ names.map (escapeForRegExp).join ('|') })`, 'g')
|
||||
|
||||
const remarkWikiAutoLink = (
|
||||
candidates: WikiAutolinkCandidate[],
|
||||
language: string,
|
||||
): ((tree: Root) => void) => {
|
||||
const targets = new Map<string, WikiAutolinkCandidate> ()
|
||||
const ambiguous = new Set<string> ()
|
||||
for (const candidate of candidates)
|
||||
{
|
||||
if (candidate.languageCode !== language || candidate.name.length === 0)
|
||||
continue
|
||||
const previous = targets.get (candidate.name)
|
||||
if (previous != null
|
||||
&& (previous.resourceType !== candidate.resourceType
|
||||
|| previous.resourceId !== candidate.resourceId
|
||||
|| previous.tagId !== candidate.tagId))
|
||||
{
|
||||
ambiguous.add (candidate.name)
|
||||
continue
|
||||
}
|
||||
targets.set (candidate.name, candidate)
|
||||
}
|
||||
|
||||
const names = [...targets.keys ()].sort ((a, b) => b.length - a.length)
|
||||
if (names.length === 0)
|
||||
return () => { }
|
||||
|
||||
const expression = new RegExp (names.map (escapeForRegExp).join ('|'), 'gu')
|
||||
const ainu = language === 'ain'
|
||||
|
||||
return (tree: Root) => {
|
||||
const edits: { parent: Parent; index: number; parts: RootContent[] }[] = []
|
||||
|
||||
const walk = (node: Content | Root, ancestors: Parent[]) => {
|
||||
if (!(node) || (typeof node !== 'object'))
|
||||
const walk = (
|
||||
node: Content | Root,
|
||||
parent?: Parent,
|
||||
preceding = '',
|
||||
following = '',
|
||||
) => {
|
||||
if (excluded.includes (node.type))
|
||||
return
|
||||
|
||||
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)) !== null)
|
||||
{
|
||||
const start = m.index
|
||||
const end = start + m[0].length
|
||||
|
||||
if (start > last)
|
||||
parts.push ({ type: 'text', value: value.slice (last, start) })
|
||||
|
||||
const name = m[1]
|
||||
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 = 'children' in node ? node.children : undefined
|
||||
if (Array.isArray (maybeChidren))
|
||||
{
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk (tree, [])
|
||||
|
||||
for (let i = edits.length - 1; i >= 0; --i)
|
||||
if (node.type === 'text' && parent != null)
|
||||
{
|
||||
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)
|
||||
const value = node.value
|
||||
let last = 0
|
||||
const parts: RootContent[] = []
|
||||
for (const match of value.matchAll (expression))
|
||||
{
|
||||
const start = match.index
|
||||
const end = start + match[0].length
|
||||
const candidate = targets.get (match[0])
|
||||
const before = Array.from (value.slice (0, start)).at (-1) ?? preceding
|
||||
const after = Array.from (value.slice (end))[0] ?? following
|
||||
if (candidate == null || ambiguous.has (match[0])
|
||||
|| (ainu && (wordCharacter.test (before) || wordCharacter.test (after))))
|
||||
continue
|
||||
if (start > last)
|
||||
parts.push ({ type: 'text', value: value.slice (last, start) })
|
||||
parts.push ({
|
||||
type: 'link',
|
||||
url: `/${candidate.localeCode}/wiki/${encodeURIComponent (candidate.resourceId)}`,
|
||||
title: null,
|
||||
children: [{ type: 'text', value: match[0] }],
|
||||
data: { hProperties: { 'data-wiki': '1' } } })
|
||||
last = end
|
||||
}
|
||||
if (parts.length > 0)
|
||||
{
|
||||
if (last < value.length)
|
||||
parts.push ({ type: 'text', value: value.slice (last) })
|
||||
edits.push ({ parent, index: parent.children.indexOf (node), parts })
|
||||
}
|
||||
return
|
||||
}
|
||||
if ('children' in node)
|
||||
{
|
||||
const inline = ['paragraph', 'heading', 'emphasis', 'strong',
|
||||
'delete', 'tableCell'].includes (node.type)
|
||||
for (let index = 0; index < node.children.length; ++index)
|
||||
{
|
||||
const before = index > 0
|
||||
? edgeCharacter (node.children[index - 1], false) : preceding
|
||||
const after = index + 1 < node.children.length
|
||||
? edgeCharacter (node.children[index + 1], true) : following
|
||||
walk (node.children[index], node as Parent,
|
||||
inline ? before : '', inline ? after : '')
|
||||
}
|
||||
}
|
||||
}
|
||||
walk (tree)
|
||||
for (const { parent, index, parts } of edits.reverse ())
|
||||
parent.children.splice (index, 1, ...parts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default remarkWikiAutoLink
|
||||
|
||||
+30
-2
@@ -3,7 +3,14 @@ import { isAxiosError } from 'axios'
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
import type { WikiPage, WikiPageChange } from '@/types'
|
||||
import type { WikiAutolinkCandidate, WikiPage, WikiPageChange } from '@/types'
|
||||
|
||||
|
||||
export const fetchWikiAutolinkCandidates = (
|
||||
language: string,
|
||||
locale: LocaleCode,
|
||||
): Promise<WikiAutolinkCandidate[]> =>
|
||||
apiGet ('/wiki/autolink_candidates', { locale, params: { language } })
|
||||
|
||||
|
||||
export const fetchWikiChanges = (
|
||||
@@ -24,7 +31,8 @@ export const fetchWikiPage = async (
|
||||
{ version }: { version?: string },
|
||||
locale: LocaleCode = 'ja',
|
||||
): Promise<WikiPage> =>
|
||||
await apiGet (`/wiki/${ id }`, { locale, params: version ? { version } : { } })
|
||||
await apiGet (`/wiki/${ encodeURIComponent (id) }`, {
|
||||
locale, params: version ? { version } : { } })
|
||||
|
||||
|
||||
export const fetchWikiPageByTitle = async (
|
||||
@@ -43,3 +51,23 @@ export const fetchWikiPageByTitle = async (
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const fetchWikiPageByReference = async (
|
||||
reference: string,
|
||||
params: { version?: string },
|
||||
locale: LocaleCode,
|
||||
): Promise<WikiPage | null> => {
|
||||
try
|
||||
{
|
||||
return /^\d+$/.test (reference)
|
||||
? await fetchWikiPage (reference, params, locale)
|
||||
: await fetchWikiPageByTitle (reference, params, locale)
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
if (isAxiosError (error) && error.response?.status === 404)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする