132 行
3.9 KiB
TypeScript
132 行
3.9 KiB
TypeScript
import type { Content, Parent, Root, RootContent } from 'mdast'
|
|
|
|
import type { WikiAutolinkCandidate } from '@/types'
|
|
import type { LocaleCode } from '@/lib/locales'
|
|
|
|
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
|
|
const wordCharacter = /[\p{L}\p{M}\p{N}_]/u
|
|
const excluded = ['link', 'linkReference', 'image', 'imageReference',
|
|
'code', 'inlineCode', 'html']
|
|
|
|
|
|
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)
|
|
{
|
|
const character = edgeCharacter (child, first)
|
|
if (character !== '')
|
|
return character
|
|
}
|
|
return ''
|
|
}
|
|
return '\u3000'
|
|
}
|
|
|
|
|
|
const remarkWikiAutoLink = (
|
|
candidates: WikiAutolinkCandidate[],
|
|
language: string,
|
|
locale: LocaleCode,
|
|
): ((tree: Root) => void) => {
|
|
const targets = new Map<string, WikiAutolinkCandidate> ()
|
|
const ambiguous = new Set<string> ()
|
|
for (const candidate of candidates)
|
|
{
|
|
if (candidate.languageCode !== language || candidate.localeCode !== locale
|
|
|| 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,
|
|
parent?: Parent,
|
|
preceding = '',
|
|
following = '',
|
|
) => {
|
|
if (excluded.includes (node.type))
|
|
return
|
|
if (node.type === 'text' && parent != null)
|
|
{
|
|
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: `/${locale}/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
|