アイヌ語追加 (#428) (#433)

Reviewed-on: #433
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
2026-10-20 20:44:14 +09:00
committed by みてるぞ
コミット 084a090cab
294個のファイルの変更、13978行の追加、3461行の削除
+114 -84
ファイルの表示
@@ -1,101 +1,131 @@
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, '\\$&')
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,
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, 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: `/${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