import type { ComponentType } from 'react' type ArticleLanguage = { currentEdition: string; publishedEditions: string[] } export type ArticleMeta = { identifier: string canonicalLanguage: string languages: Record } export type HelpEditionText = { title: string body: string } export type HelpSearchEntry = HelpEditionText & { identifier: string edition: string } export type HelpEditionContent = HelpEditionText & { content: ComponentType } export type HelpEditionSource = HelpEditionText & { source: string } export type HelpChange = Pick & { pred: string | null change: 'new' | 'update' date: string } export type HelpTitleIndex = Record export type HelpDiffLine = { type: 'added' | 'removed' | 'unchanged' content: string } export type HelpDiff = { from: HelpEditionSource to: HelpEditionSource lines: HelpDiffLine[] } export type HelpEditionIdentifier = { date: string; revision: number | null } // Fix the locale and primary strength; ICU versions still differ from MySQL UCA 9.0. const helpTitleCollator = new Intl.Collator ('en', { usage: 'sort', sensitivity: 'base', ignorePunctuation: false, numeric: false }) export const compareHelpTitles = (a: string, b: string): number => helpTitleCollator.compare (a, b) export const normaliseHelpSearchText = (value: string): string => value.normalize ('NFKC').toLowerCase ().replace (/[ァ-ヶヽヾ]/g, character => String.fromCodePoint (character.codePointAt (0)! - 0x60)) export const parseHelpEdition = (edition: string): HelpEditionIdentifier | null => { const matched = edition.match (/^(\d{4}-\d{2}-\d{2})(?:\.([1-9]\d*))?$/) if (matched == null) return null const date = new Date (matched[1]) const revision = matched[2] == null ? null : Number (matched[2]) if (Number.isNaN (date.getTime ()) || date.toISOString ().slice (0, 10) !== matched[1] || (revision != null && !(Number.isSafeInteger (revision)))) return null return { date: matched[1], revision } } export const compareHelpEditions = (a: string, b: string): number => { const before = parseHelpEdition (a) const after = parseHelpEdition (b) if (before == null || after == null) return a.localeCompare (b) return before.date.localeCompare (after.date) || (before.revision ?? 1) - (after.revision ?? 1) } export const helpEditionDate = (edition: string): Date | null => { const parsed = parseHelpEdition (edition) return parsed == null ? null : new Date (`${ parsed.date }T00:00:00`) } export const getHelpArticle = async (identifier: string): Promise => { const { default: articles } = await import ('virtual:help/articles') return Object.hasOwn (articles, identifier) ? articles[identifier] : null } export const getHelpEditions = async ( identifier: string, language: string, ): Promise => { const article = await getHelpArticle (identifier) return article?.languages[language]?.publishedEditions ?? [] } export const getHelpPublishedEditions = async ( identifier: string, language: string, ): Promise => await getHelpEditions (identifier, language) export const getHelpEdition = async ( identifier: string, language: string, edition: string, ): Promise => { const { components } = await import ('virtual:help/loaders') const loader = components[`${ identifier }/${ language }/${ edition }`] if (!(loader)) return null return (await loader ()).default } export const getHelpEditionSource = async ( identifier: string, language: string, edition: string, ): Promise => { const { sources } = await import ('virtual:help/loaders') const loader = sources[`${ identifier }/${ language }/${ edition }`] if (!(loader)) return null const [source, text] = await Promise.all ([ loader (), getHelpEditionText (identifier, language, edition)]) return text == null ? null : { source: source.default, ...text } } export const getHelpEditionText = async ( identifier: string, language: string, edition: string, ): Promise => { const { texts } = await import ('virtual:help/loaders') const loader = texts[`${ identifier }/${ language }/${ edition }`] return loader == null ? null : (await loader ()).default } export const getHelpSearchEntries = async ( language: string, ): Promise => { const { default: search } = await import ('virtual:help/search') return search[language] ?? [] } export const getHelpEditionContent = async ( identifier: string, language: string, edition: string, ): Promise => { const [content, text] = await Promise.all ([ getHelpEdition (identifier, language, edition), getHelpEditionText (identifier, language, edition)]) return content != null && text != null ? { content, ...text } : null } export const getHelpChanges = async (language: string): Promise => { const { default: changes } = await import ('virtual:help/changes') return changes[language] ?? [] } export const getHelpTitleIndex = async (language: string): Promise => { const { default: titles } = await import ('virtual:help/titles') return titles[language] ?? { } } const diffHelpLines = (from: string, to: string): HelpDiffLine[] => { const before = from.match (/[^\n]*\n|[^\n]+$/g) ?? [] const after = to.match (/[^\n]*\n|[^\n]+$/g) ?? [] const lengths = Array.from ({ length: before.length + 1 }, () => new Uint32Array (after.length + 1)) for (let i = before.length - 1; i >= 0; --i) { for (let j = after.length - 1; j >= 0; --j) { lengths[i][j] = before[i] === after[j] ? lengths[i + 1][j + 1] + 1 : Math.max (lengths[i + 1][j], lengths[i][j + 1]) } } const lines: HelpDiffLine[] = [] let i = 0 let j = 0 while (i < before.length || j < after.length) { if (i < before.length && j < after.length && before[i] === after[j]) { lines.push ({ type: 'unchanged', content: before[i] }) ++i ++j } else if (i < before.length && (j === after.length || lengths[i + 1][j] >= lengths[i][j + 1])) { lines.push ({ type: 'removed', content: before[i] }) ++i } else { lines.push ({ type: 'added', content: after[j] }) ++j } } return lines } export const getHelpDiff = async ( identifier: string, language: string, from: string, to: string, ): Promise => { const editions = await getHelpPublishedEditions (identifier, language) if (!(editions.includes (from)) || !(editions.includes (to))) return null const [before, after] = await Promise.all ([ getHelpEditionSource (identifier, language, from), getHelpEditionSource (identifier, language, to)]) if (before == null || after == null) return null return { from: before, to: after, lines: diffHelpLines (before.source, after.source) } }