238 行
7.0 KiB
TypeScript
238 行
7.0 KiB
TypeScript
import type { ComponentType } from 'react'
|
|
|
|
type ArticleLanguage = { currentEdition: string; publishedEditions: string[] }
|
|
|
|
export type ArticleMeta = {
|
|
identifier: string
|
|
canonicalLanguage: string
|
|
languages: Record<string, ArticleLanguage> }
|
|
|
|
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<HelpSearchEntry, 'identifier' | 'edition' | 'title'> & {
|
|
pred: string | null
|
|
change: 'new' | 'update'
|
|
date: string }
|
|
export type HelpTitleIndex = Record<string, string[]>
|
|
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<ArticleMeta | null> => {
|
|
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<string[]> => {
|
|
const article = await getHelpArticle (identifier)
|
|
return article?.languages[language]?.publishedEditions ?? []
|
|
}
|
|
|
|
|
|
export const getHelpPublishedEditions = async (
|
|
identifier: string,
|
|
language: string,
|
|
): Promise<string[]> => await getHelpEditions (identifier, language)
|
|
|
|
|
|
export const getHelpEdition = async (
|
|
identifier: string,
|
|
language: string,
|
|
edition: string,
|
|
): Promise<ComponentType | null> => {
|
|
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<HelpEditionSource | null> => {
|
|
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<HelpEditionText | null> => {
|
|
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<HelpSearchEntry[]> => {
|
|
const { default: search } = await import ('virtual:help/search')
|
|
return search[language] ?? []
|
|
}
|
|
|
|
|
|
export const getHelpEditionContent = async (
|
|
identifier: string,
|
|
language: string,
|
|
edition: string,
|
|
): Promise<HelpEditionContent | null> => {
|
|
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<HelpChange[]> => {
|
|
const { default: changes } = await import ('virtual:help/changes')
|
|
return changes[language] ?? []
|
|
}
|
|
|
|
|
|
export const getHelpTitleIndex = async (language: string): Promise<HelpTitleIndex> => {
|
|
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<HelpDiff | null> => {
|
|
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) }
|
|
}
|