ファイル
btrc-hub/frontend/src/lib/help.ts
T
2026-10-12 02:37:56 +09:00

133 行
3.6 KiB
TypeScript

import { parse as parseYAML } from 'yaml'
import type { ComponentType } from 'react'
type ArticleLanguage = { currentEdition: string }
type ArticleMeta = {
identifier: string
languages: Record<string, ArticleLanguage> }
type MDXModule = { default: ComponentType }
export type HelpEditionText = {
title: string
body: string }
export type HelpSearchEntry = HelpEditionText & {
identifier: string
edition: string }
const articleFiles = import.meta.glob<string> ('../../help/*/article.yml',
{ query: '?raw',
import: 'default',
eager: true })
const editionFiles = import.meta.glob<MDXModule> ('../../help/*/editions/*/*.mdx')
const rawEditionFiles = import.meta.glob<string> ('../../help/*/editions/*/*.mdx',
{ query: '?raw',
import: 'default' })
const cameliseArticle = (
value: { identifier: string
languages: Record<string, { ['current_edition']: string }> },
): ArticleMeta => (
{ identifier: value.identifier,
languages: Object.fromEntries (
Object.entries (value.languages).map (([language, data]) => [
language, { currentEdition: data['current_edition'] }])) })
const compareEditions = (a: string, b: string): number => {
const [dateA, revisionA = '1'] = a.split ('.')
const [dateB, revisionB = '1'] = b.split ('.')
return dateA.localeCompare (dateB) || Number (revisionA) - Number (revisionB)
}
export const getHelpArticle = (identifier: string): ArticleMeta | null => {
const path = `../../help/${ identifier }/article.yml`
const source = articleFiles[path]
if (!(source))
return null
return cameliseArticle (parseYAML (source))
}
export const getHelpEditions = (
identifier: string,
language: string,
): string[] => {
const prefix = `../../help/${ identifier }/editions/${ language }/`
return (Object.keys (editionFiles)
.filter (path => path.startsWith (prefix))
.map (path => path.slice (prefix.length, -'.mdx'.length))
.sort (compareEditions))
}
export const getHelpEdition = async (
identifier: string,
language: string,
edition: string,
): Promise<ComponentType | null> => {
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
const loader = editionFiles[path]
if (!(loader))
return null
return (await loader ()).default
}
export const getHelpEditionText = async (
identifier: string,
language: string,
edition: string,
): Promise<HelpEditionText | null> => {
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
const loader = rawEditionFiles[path]
if (!(loader))
return null
const mdx = (await loader ()).replace (/^\uFEFF/, '')
const heading = mdx.match (/^# +([^\r\n]+)(?:\r?\n|$)/)
if (!(heading))
throw new Error (`Help MDX の先頭に H1 がありません: ${ path }`)
return { title: heading[1].trim (),
body: mdx.slice (heading[0].length) }
}
export const getHelpSearchEntries = async (
language: string,
): Promise<HelpSearchEntry[]> => {
const entries = await Promise.all (
Object.values (articleFiles).map (async source => {
const article = cameliseArticle (parseYAML (source))
const edition = article.languages[language]?.currentEdition
if (!(edition))
return null
const content = await getHelpEditionText (article.identifier, language, edition)
if (!(content))
throw new Error (`Help edition が見つかりません: ${ article.identifier }/${ edition }`)
return { identifier: article.identifier, edition, ...content }
}))
return entries.filter ((entry): entry is HelpSearchEntry => entry != null)
}