Files
libre-bible-data/scripts/generate-docs.js
T
2026-07-12 11:11:47 -05:00

283 lines
10 KiB
JavaScript

import { mkdir, readdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { readJson } from './lib.js'
const root = process.cwd()
const sourcesDir = path.join(root, 'sources')
const docsResourcesDir = path.join(root, 'docs', 'resources')
const sourceFiles = (await readdir(sourcesDir))
.filter((file) => file.endsWith('.json'))
.sort((a, b) => a.localeCompare(b))
const resources = []
for (const file of sourceFiles) {
const manifest = await readJson(path.join(sourcesDir, file))
const packageCatalogPath = path.join(root, 'packages', 'json', manifest.id, 'catalog.json')
let packageCatalog = null
try {
packageCatalog = await readJson(packageCatalogPath)
} catch {
packageCatalog = null
}
resources.push({ manifest, packageCatalog })
}
await mkdir(docsResourcesDir, { recursive: true })
const index = [
'# LibreBible Resource Index',
'',
'This file is generated from source manifests and package catalogs. Edit `sources/*.json`, package generators, or catalog scripts instead of hand-editing this index.',
'',
'## Available Resources',
'',
'| Resource | Type | Language | Date / Edition | Source | License | Features | Attachments | Counts | Detail |',
'| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
]
for (const resource of resources) {
const { manifest, packageCatalog } = resource
const detailPath = `docs/resources/${manifest.id}.md`
index.push(
[
link(manifest.title, detailPath),
manifest.resource_type ?? 'resource',
languageLabel(manifest),
editionLabel(manifest),
sourceLabel(manifest),
licenseLabel(manifest),
featureLabels(manifest).join(', ') || 'None listed',
attachmentLabels(manifest).join(', ') || 'None listed',
countsLabel(packageCatalog),
link('Details', detailPath),
].join(' | ').replace(/^/, '| ').replace(/$/, ' |'),
)
await writeFile(
path.join(docsResourcesDir, `${manifest.id}.md`),
`${resourceDetailMarkdown(manifest, packageCatalog)}\n`,
'utf8',
)
}
await writeFile(path.join(root, 'RESOURCE_INDEX.md'), `${index.join('\n')}\n`, 'utf8')
console.log(`Generated RESOURCE_INDEX.md and ${resources.length} resource detail page(s).`)
function resourceDetailMarkdown(manifest, packageCatalog) {
const lines = [
`# ${manifest.title}`,
'',
manifest.catalog_display?.summary ?? manifest.translation?.public_description ?? '',
'',
'## Identity',
'',
`- Resource ID: \`${manifest.id}\``,
`- Type: ${manifest.resource_type ?? 'resource'}`,
`- Abbreviation: ${manifest.abbreviation ?? 'n/a'}`,
`- Short title: ${manifest.short_title ?? manifest.title}`,
`- Alternate IDs: ${formatList(manifest.alternate_ids)}`,
`- Language: ${languageLabel(manifest)}`,
`- Canon: ${manifest.canon?.scope ?? 'n/a'}`,
'',
'## Translation',
'',
`- Base title: ${manifest.translation?.base_title ?? 'n/a'}`,
`- Translation year: ${manifest.translation?.translation_year ?? 'n/a'}`,
`- Edition: ${manifest.translation?.edition ?? 'n/a'}`,
`- Edition year: ${manifest.translation?.edition_year ?? 'n/a'}`,
`- Source text basis: ${manifest.translation?.source_text_basis ?? 'n/a'}`,
`- Tradition: ${manifest.translation?.tradition ?? 'n/a'}`,
'',
'## Source',
'',
`- Provider: ${manifest.source?.provider ?? 'n/a'}`,
`- Upstream ID: ${manifest.source?.upstream_id ?? 'n/a'}`,
`- Upstream page: ${manifest.source?.url ?? 'n/a'}`,
`- Download URL: ${manifest.source?.download_url ?? 'n/a'}`,
`- Source format: ${manifest.source?.format ?? 'n/a'}`,
`- Upstream last updated: ${manifest.source?.upstream_last_updated ?? 'n/a'}`,
`- Available upstream formats: ${formatList(manifest.source?.available_formats)}`,
'',
'## License',
'',
`- Name: ${manifest.license?.name ?? 'n/a'}`,
`- Redistribution allowed: ${manifest.license?.redistribution === true ? 'Yes' : 'No or unknown'}`,
`- License/source URL: ${manifest.license?.source_license_url ?? 'n/a'}`,
`- Attribution: ${manifest.license?.attribution ?? 'n/a'}`,
`- Jurisdiction notes: ${manifest.license?.jurisdiction_notes ?? 'n/a'}`,
`- Restrictions: ${manifest.license?.restricted_notes ?? 'n/a'}`,
'',
'## Contributors',
'',
...contributorsList(manifest),
'',
'## Features',
'',
...featuresList(manifest),
'',
'## Attachments',
'',
...attachmentsList(manifest),
'',
'## Generated Package',
'',
`- Package catalog: ${manifest.packages?.json_catalog ?? 'n/a'}`,
`- Verses JSONL: ${manifest.packages?.verses_jsonl ?? 'n/a'}`,
`- Strong\'s links JSONL: ${manifest.packages?.strongs_jsonl ?? 'n/a'}`,
`- Footnotes JSONL: ${manifest.packages?.footnotes_jsonl ?? 'n/a'}`,
`- Study notes JSONL: ${manifest.packages?.study_notes_jsonl ?? 'n/a'}`,
`- Entries JSONL: ${manifest.packages?.entries_jsonl ?? 'n/a'}`,
`- Source SHA-256: ${packageCatalog?.source_sha256 ?? manifest.checks?.expected_sha256 ?? 'n/a'}`,
`- Last checked: ${manifest.checks?.last_checked_at ?? 'n/a'}`,
'',
'## Counts',
'',
`- Books: ${packageCatalog?.counts?.books ?? 'n/a'}`,
`- Verses: ${packageCatalog?.counts?.verses ?? 'n/a'}`,
`- Strong\'s links: ${packageCatalog?.counts?.strongs_links ?? 'n/a'}`,
`- Footnotes: ${packageCatalog?.counts?.footnotes ?? 'n/a'}`,
`- Study notes: ${packageCatalog?.counts?.study_notes ?? 'n/a'}`,
`- Entries: ${packageCatalog?.counts?.entries ?? 'n/a'}`,
`- Commentary entries: ${packageCatalog?.counts?.commentary_entries ?? 'n/a'}`,
`- Dictionary entries: ${packageCatalog?.counts?.dictionary_entries ?? 'n/a'}`,
'',
'## Package Checksums',
'',
...packageFilesList(packageCatalog),
]
return lines.filter((line, index, all) => !(line === '' && all[index - 1] === '')).join('\n')
}
function languageLabel(manifest) {
if (typeof manifest.language === 'string') return manifest.language
const parts = [
manifest.language?.name,
manifest.language?.code ? `(${manifest.language.code})` : null,
manifest.language?.dialect ? `- ${manifest.language.dialect}` : null,
]
return parts.filter(Boolean).join(' ') || 'n/a'
}
function editionLabel(manifest) {
const translationYear = manifest.translation?.translation_year
const editionYear = manifest.translation?.edition_year
const edition = manifest.translation?.edition
if (translationYear && editionYear) return `${translationYear}; ${editionYear} edition`
if (editionYear) return `${editionYear} edition`
return edition ?? 'n/a'
}
function sourceLabel(manifest) {
return manifest.source?.provider ?? 'n/a'
}
function licenseLabel(manifest) {
const name = manifest.license?.name ?? 'n/a'
const redistribution = manifest.license?.redistribution === true ? 'redistributable' : 'restricted/unknown'
return `${name} (${redistribution})`
}
function featureLabels(manifest) {
return (manifest.features ?? []).map((feature) => feature.label ?? feature.id ?? feature.type).filter(Boolean)
}
function attachmentLabels(manifest) {
return [
...(manifest.attachments?.included ?? []).map((attachment) => attachment.label ?? attachment.id),
...(manifest.attachments?.supported_future_types ?? []).map((type) => `future: ${type}`),
].filter(Boolean)
}
function countsLabel(packageCatalog) {
if (!packageCatalog?.counts) return 'n/a'
const counts = packageCatalog.counts
const parts = [
counts.books != null ? `${counts.books} books` : null,
counts.verses != null ? `${counts.verses} verses` : null,
counts.strongs_links != null ? `${counts.strongs_links} Strong's links` : null,
counts.footnotes != null ? `${counts.footnotes} footnotes` : null,
counts.study_notes != null ? `${counts.study_notes} study notes` : null,
counts.commentary_entries != null ? `${counts.commentary_entries} commentary entries` : null,
counts.dictionary_entries != null ? `${counts.dictionary_entries} dictionary entries` : null,
counts.entries != null && counts.commentary_entries == null && counts.dictionary_entries == null ? `${counts.entries} entries` : null,
]
return parts.filter(Boolean).join(', ')
}
function contributorsList(manifest) {
const contributors = manifest.contributors ?? []
if (contributors.length === 0) return ['- None listed.']
return contributors.map((contributor) => `- ${contributor.name}: ${contributor.role}`)
}
function featuresList(manifest) {
const features = manifest.features ?? []
if (features.length === 0) return ['- None listed.']
return features.map((feature) => {
const details = [
feature.type,
feature.languages?.length ? `languages: ${feature.languages.join(', ')}` : null,
feature.systems?.length ? `systems: ${feature.systems.join(', ')}` : null,
feature.format ? `format: ${feature.format}` : null,
feature.embedded === true ? 'embedded' : null,
feature.package ? `package: ${feature.package}` : null,
].filter(Boolean)
return `- ${feature.label ?? feature.id}: ${details.join('; ') || 'listed'}`
})
}
function attachmentsList(manifest) {
const included = manifest.attachments?.included ?? []
const future = manifest.attachments?.supported_future_types ?? []
const lines = []
if (included.length > 0) {
lines.push('Included:')
for (const attachment of included) {
lines.push(
`- ${attachment.label ?? attachment.id}: ${[
attachment.resource_type,
attachment.relationship,
attachment.anchor_types?.length ? `anchors: ${attachment.anchor_types.join(', ')}` : null,
attachment.languages?.length ? `languages: ${attachment.languages.join(', ')}` : null,
attachment.systems?.length ? `systems: ${attachment.systems.join(', ')}` : null,
attachment.package ? `package: ${attachment.package}` : null,
].filter(Boolean).join('; ')}`,
)
}
} else {
lines.push('- No included attachments listed.')
}
if (future.length > 0) {
lines.push('')
lines.push('Supported future attachment types:')
for (const type of future) {
lines.push(`- ${type}`)
}
}
return lines
}
function packageFilesList(packageCatalog) {
const files = packageCatalog?.files
if (!files) return ['- No package file checksums listed.']
return Object.entries(files).map(([key, value]) => `- ${key}: \`${value.path}\` (${value.sha256})`)
}
function formatList(values) {
return values?.length ? values.join(', ') : 'n/a'
}
function link(label, href) {
return `[${label}](${href})`
}