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 () const ambiguous = new Set () 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