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'}`, `- 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'}`, `- 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.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})` }