diff --git a/CHANGELOG.md b/CHANGELOG.md index 740eb6a..0a67489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,18 @@ ## Current +- Added generated human-facing resource docs for Gitea and future ChristIT.com catalog work. + +## 0.1.2 - 2026-07-12 + +- Phase 2 generated public docs: added a docs generator that writes `RESOURCE_INDEX.md` and per-resource detail pages from source manifests and generated package catalogs. + +## 0.1.1 - 2026-07-12 + - Enriched the KJV source manifest with translation, edition, contributor, feature, attachment, license, and catalog-display metadata. - Recorded the reusable manifest convention for future Bible translations and study resources. - Adopted the convention that each named implementation phase bumps the repo version and is committed locally and remotely. -## 0.1.1 - 2026-07-12 - - Phase 1 metadata foundation: expanded the KJV manifest and manifest documentation so future translations can describe editions, source text basis, contributors, concordance links, study notes, commentaries, maps, and other attachments. ## 0.1.0 - 2026-07-12 diff --git a/README.md b/README.md index 80e0209..aa46dd7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ npm run import:kjv Generated packages land under `packages/json//`. +## Resource Index + +Generated human-facing resource docs: + +- `RESOURCE_INDEX.md` +- `docs/resources/kjv-eng-kjv2006.md` + ## Repo Rules - Every hosted text needs a source manifest. diff --git a/RESOURCE_INDEX.md b/RESOURCE_INDEX.md new file mode 100644 index 0000000..b2746b1 --- /dev/null +++ b/RESOURCE_INDEX.md @@ -0,0 +1,9 @@ +# 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 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| [King James (Authorized) Version](docs/resources/kjv-eng-kjv2006.md) | translation | English (eng) - archaic British | 1611; 1769 edition | eBible.org | Public Domain (redistributable) | Bible text, Strong's-linked words, USFM source | Strong's links, future: study_notes, future: commentary, future: cross_references, future: morphology, future: lemma_links, future: lexicon, future: dictionary, future: maps, future: timeline, future: media | 66 books, 31102 verses, 348884 Strong's links | [Details](docs/resources/kjv-eng-kjv2006.md) | diff --git a/docs/resources/kjv-eng-kjv2006.md b/docs/resources/kjv-eng-kjv2006.md new file mode 100644 index 0000000..e17dc8c --- /dev/null +++ b/docs/resources/kjv-eng-kjv2006.md @@ -0,0 +1,89 @@ +# King James (Authorized) Version + +Public-domain KJV package based on the 1769 standardized text, with Strong's numbers embedded in the source USFM. + +## Identity + +- Resource ID: `kjv-eng-kjv2006` +- Type: translation +- Abbreviation: KJV +- Short title: King James Version +- Alternate IDs: ENGKJV, eng-kjv2006 +- Language: English (eng) - archaic British +- Canon: protocanon + +## Translation + +- Base title: King James Version +- Translation year: 1611 +- Edition: 1769 standardized text with Strong's numbers added +- Edition year: 1769 +- Source text basis: Authorized Version / King James Version standardized text of 1769 +- Tradition: Protestant + +## Source + +- Provider: eBible.org +- Upstream ID: eng-kjv2006 +- Upstream page: https://ebible.org/find/show.php?id=eng-kjv2006 +- Download URL: https://ebible.org/Scriptures/eng-kjv2006_usfm.zip +- Source format: usfm-zip +- Upstream last updated: 2026-05-16 +- Available upstream formats: browser, mobile-html, epub, pdf, sword, plain-text, vpl-sql, browser-bible, usfx, usfm, xetex + +## License + +- Name: Public Domain +- Redistribution allowed: Yes +- License/source URL: https://ebible.org/find/show.php?id=eng-kjv2006 +- Attribution: This free text is provided courtesy of CrossWire Bible Society and eBible.org. +- Jurisdiction notes: eBible states this work is firmly public domain outside the United Kingdom; UK printing/import may be restricted by Crown letters patent. +- Restrictions: Printing this translation in the United Kingdom or importing printed copies into the UK may require permission from the Crown patent holders. + +## Contributors + +- King James Version translators: translation +- CrossWire Bible Society: source text provider +- eBible.org: publisher and package provider + +## Features + +- Bible text: translation_text; languages: eng; embedded +- Strong's-linked words: concordance_links; languages: hebrew, aramaic, greek; systems: strongs; embedded; package: packages/json/kjv-eng-kjv2006/strongs-links.jsonl +- USFM source: source_format; format: usfm + +## Attachments + +Included: +- Strong's links: concordance_links; word-to-strongs; anchors: verse, word, strongs_number; languages: hebrew, aramaic, greek; systems: strongs; package: packages/json/kjv-eng-kjv2006/strongs-links.jsonl + +Supported future attachment types: +- study_notes +- commentary +- cross_references +- morphology +- lemma_links +- lexicon +- dictionary +- maps +- timeline +- media + +## Generated Package + +- Package catalog: packages/json/kjv-eng-kjv2006/catalog.json +- Verses JSONL: packages/json/kjv-eng-kjv2006/verses.jsonl +- Strong's links JSONL: packages/json/kjv-eng-kjv2006/strongs-links.jsonl +- Source SHA-256: 4ea6952590d070bfa22985aded48a49581e31b568a60aa09e25f73462e700e7d +- Last checked: 2026-07-12T02:18:30Z + +## Counts + +- Books: 66 +- Verses: 31102 +- Strong's links: 348884 + +## Package Checksums + +- verses_jsonl: `verses.jsonl` (e3102f854a99c4788ed259ec61cb93fea830845c0788c8c0929bc77e9db6ca10) +- strongs_jsonl: `strongs-links.jsonl` (fa9702224776509275ea17fb245cb72150f1c262c4fbe44c7161e8e83866c779) diff --git a/package-lock.json b/package-lock.json index 1181c00..e5acf8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-bible-data", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-bible-data", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "adm-zip": "^0.5.16" } diff --git a/package.json b/package.json index d264b21..0847765 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "libre-bible-data", - "version": "0.1.1", + "version": "0.1.2", "private": true, "type": "module", "scripts": { "check": "node scripts/check-sources.js", + "docs": "node scripts/generate-docs.js", "import:kjv": "node scripts/import-usfm.js sources/kjv-eng-kjv2006.json", - "build": "npm run check && npm run import:kjv" + "build": "npm run check && npm run import:kjv && npm run docs" }, "dependencies": { "adm-zip": "^0.5.16" diff --git a/scripts/generate-docs.js b/scripts/generate-docs.js new file mode 100644 index 0000000..4e4d0c0 --- /dev/null +++ b/scripts/generate-docs.js @@ -0,0 +1,269 @@ +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'}`, + `- 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'}`, + '', + '## 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, + ] + 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})` +}