@unfoldingword/door43-preview-renderers 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@unfoldingword/door43-preview-renderers",
3
+ "version": "1.5.0",
4
+ "type": "module",
5
+ "description": "JavaScript library for gathering content from various repositories and rendering HTML snippets for web previews and PDFs",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.esm.js",
8
+ "bin": {
9
+ "door43-renderers": "./src/cli.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist/index.js",
16
+ "dist/index.esm.js",
17
+ "src/cli.js"
18
+ ],
19
+ "scripts": {
20
+ "fetch-logos": "node scripts/fetch-logos.mjs",
21
+ "prebuild": "node -e \"const f='src/assets/logos.generated.js';require('fs').existsSync(f)||require('child_process').execSync('node scripts/fetch-logos.mjs',{stdio:'inherit'})\"",
22
+ "build": "rollup -c",
23
+ "dev": "rollup -c -w",
24
+ "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
25
+ "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
26
+ "test:coverage": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage",
27
+ "cli": "node src/cli.js",
28
+ "lint": "eslint src --ext .js,.jsx",
29
+ "format": "prettier --write \"src/**/*.{js,jsx,json,md}\"",
30
+ "styleguide": "styleguidist server --config styleguide.config.cjs",
31
+ "styleguide:dev": "styleguidist server --config styleguide.config.cjs",
32
+ "styleguide:build": "pnpm run build && styleguidist build --config styleguide.config.cjs",
33
+ "prepare": "pnpm run build"
34
+ },
35
+ "keywords": [
36
+ "door43",
37
+ "preview",
38
+ "renderer",
39
+ "html",
40
+ "pdf",
41
+ "bible",
42
+ "translation"
43
+ ],
44
+ "author": "unfoldingWord",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/unfoldingWord/door43-preview-renderers.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/unfoldingWord/door43-preview-renderers/issues"
52
+ },
53
+ "homepage": "https://github.com/unfoldingWord/door43-preview-renderers#readme",
54
+ "devDependencies": {
55
+ "@babel/core": "^7.23.0",
56
+ "@babel/preset-env": "^7.23.0",
57
+ "@babel/preset-react": "^7.23.0",
58
+ "@rollup/plugin-babel": "^6.0.4",
59
+ "@rollup/plugin-commonjs": "^25.0.7",
60
+ "@rollup/plugin-json": "^6.1.0",
61
+ "@rollup/plugin-node-resolve": "^15.2.3",
62
+ "babel-loader": "^9.1.3",
63
+ "css-loader": "^6.8.1",
64
+ "eslint": "^8.51.0",
65
+ "jest": "^29.7.0",
66
+ "netlify-cli": "^23.11.1",
67
+ "prettier": "^3.0.3",
68
+ "react": "^18.2.0",
69
+ "react-dom": "^18.2.0",
70
+ "react-styleguidist": "^13.1.2",
71
+ "rollup": "^4.1.4",
72
+ "style-loader": "^3.3.3",
73
+ "webpack": "^5.89.0"
74
+ },
75
+ "dependencies": {
76
+ "axios": "^1.6.0",
77
+ "js-yaml": "^4.1.1",
78
+ "jszip": "^3.10.1",
79
+ "markdown-it": "^14.1.1",
80
+ "proskomma-core": "^0.10.17",
81
+ "proskomma-json-tools": "0.8.12",
82
+ "tsv-quote-converters": "^1.1.18",
83
+ "usfm-alignment-remover": "^0.1.6",
84
+ "usfm-js": "^3.4.3"
85
+ },
86
+ "peerDependencies": {
87
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
88
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
89
+ },
90
+ "engines": {
91
+ "node": ">=16.0.0",
92
+ "pnpm": ">=8.0.0"
93
+ }
94
+ }
package/src/cli.js ADDED
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI tool for door43-preview-renderers
5
+ *
6
+ * Usage:
7
+ * cli.js getAllCatalogEntries --owner unfoldingWord --repo en_tn --ref v80 --book gen [--output file.json] [--dcs-api-url URL]
8
+ * cli.js getResourceData --owner unfoldingWord --repo en_tn --ref v80 --book gen [--output file.json] [--dcs-api-url URL]
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { getAllCatalogEntries } from './getAllCatalogEntries.js';
14
+ import { getResourceData } from './getResourceData.js';
15
+ import { renderHtmlData } from './renderHtmlData.js';
16
+ import { renderHTML } from './renderHTML.js';
17
+ import { renderPdf } from './pdf/renderPdf.js';
18
+
19
+ // Map the CLI's short page-size keys to PAGE_SIZES keys understood by renderHTML/renderPdf.
20
+ function resolveCliPageSize(shortKey) {
21
+ const map = {
22
+ A4: 'A4_PORTRAIT',
23
+ A5: 'A5_PORTRAIT',
24
+ USL: 'US_LETTER_PORTRAIT',
25
+ TRADE: 'TRADE',
26
+ CQ: 'CROWN_QUARTO',
27
+ };
28
+ return map[(shortKey || 'A4').toUpperCase()] || 'A4_PORTRAIT';
29
+ }
30
+
31
+ // Parse command line arguments
32
+ function parseArgs(args) {
33
+ const parsed = {
34
+ command: args[0],
35
+ params: {},
36
+ quiet: false,
37
+ };
38
+
39
+ for (let i = 1; i < args.length; i++) {
40
+ const arg = args[i];
41
+
42
+ // Handle flags without values
43
+ if (arg === '--quiet' || arg === '-q') {
44
+ parsed.quiet = true;
45
+ continue;
46
+ }
47
+
48
+ // Handle key-value pairs
49
+ if (arg.startsWith('--')) {
50
+ const key = arg.replace(/^--/, '').replace(/-/g, '_');
51
+ const value = args[i + 1];
52
+
53
+ if (key === 'book') {
54
+ parsed.params.bookId = value;
55
+ } else if (key === 'dcs_api_url') {
56
+ parsed.params.dcsApiUrl = value;
57
+ } else if (key === 'output') {
58
+ parsed.output = value;
59
+ } else if (key === 'page_size') {
60
+ parsed.params.pageSize = value;
61
+ } else if (key === 'columns') {
62
+ parsed.params.columns = parseInt(value, 10) || 1;
63
+ } else {
64
+ parsed.params[key] = value;
65
+ }
66
+ i++; // Skip the value in next iteration
67
+ }
68
+ }
69
+
70
+ return parsed;
71
+ }
72
+
73
+ // Display help message
74
+ function showHelp() {
75
+ process.stdout.write(`
76
+ door43-preview-renderers CLI
77
+
78
+ Usage:
79
+ cli.js <command> [options]
80
+
81
+ Commands:
82
+ getAllCatalogEntries Get all catalog entries for rendering
83
+ getResourceData Get resource data for a specific book
84
+ renderHtml Fetch resource data and render to HTML sections (JSON)
85
+ assemblePrint Fetch, render, and assemble a print-ready HTML document
86
+ generatePdf Fetch, render, assemble, and render a PDF via WeasyPrint (requires the weasyprint binary)
87
+
88
+ Options:
89
+ --owner <owner> Repository owner (required)
90
+ --repo <repo> Repository name (required)
91
+ --ref <ref> Git reference (branch/tag/commit)
92
+ --book <bookId> Book ID (e.g., gen, exo, mat, 1ti)
93
+ --dcs-api-url <url> Custom DCS API URL (optional)
94
+ --output <file> Output file path (optional, defaults to stdout)
95
+ --quiet, -q Suppress logging output (no API URLs or progress messages)
96
+ --page-size <size> Page size for assemblePrint (A4, A5, USL, TRADE, CQ; default A4)
97
+ --columns <n> Number of columns for assemblePrint (default 1)
98
+ --pdf-service-url <url> generatePdf: use a hosted weasyprint-pdf service instead of the local binary
99
+
100
+ Examples:
101
+ # Get catalog entries and output to stdout
102
+ node src/cli.js getAllCatalogEntries --owner unfoldingWord --repo en_tn --ref v80 --book gen
103
+
104
+ # Get catalog entries with quiet mode (no logging, just JSON)
105
+ node src/cli.js getAllCatalogEntries --owner unfoldingWord --repo en_tn --ref v80 --book gen --quiet
106
+
107
+ # Get catalog entries and save to file
108
+ node src/cli.js getAllCatalogEntries --owner unfoldingWord --repo en_tn --ref v80 --book gen --output result.json
109
+
110
+ # Quiet mode for piping to jq
111
+ node src/cli.js getAllCatalogEntries --owner BSOJ --repo ar_twl --ref v5 --book 1jn --quiet | jq '.catalogEntries[] | {owner, repo: .name, subject}'
112
+
113
+ # Get resource data
114
+ node src/cli.js getResourceData --owner unfoldingWord --repo en_tn --ref v80 --book gen
115
+
116
+ # Render HTML sections (JSON output with body, toc, css, etc.)
117
+ node src/cli.js renderHtml --owner unfoldingWord --repo en_tn --ref v88 --book tit --quiet
118
+
119
+ # Assemble print-ready HTML document (outputs complete HTML)
120
+ node src/cli.js assemblePrint --owner unfoldingWord --repo en_tn --ref v88 --book tit --output tit_tn.html
121
+
122
+ # Assemble with page size options
123
+ node src/cli.js assemblePrint --owner unfoldingWord --repo en_obs --ref v9 --page-size TRADE --output obs.html
124
+
125
+ # Generate a PDF directly (no browser; requires weasyprint installed)
126
+ node src/cli.js generatePdf --owner unfoldingWord --repo en_tn --ref v88 --book tit --output tit_tn.pdf
127
+
128
+ # Use custom DCS API URL
129
+ node src/cli.js getAllCatalogEntries --owner BSOJ --repo ar_twl --ref v5 --book 1jn --dcs-api-url https://git.door43.org/api/v1
130
+
131
+ Test Cases:
132
+ # Valid case
133
+ node src/cli.js getAllCatalogEntries --owner unfoldingWord --repo en_tn --ref v80 --book gen
134
+
135
+ # Invalid book ID (should fail)
136
+ node src/cli.js getAllCatalogEntries --owner fr_gl --repo fr_tn --ref v2 --book 1th
137
+
138
+ # Valid case
139
+ node src/cli.js getAllCatalogEntries --owner fr_gl --repo gr_tn --ref v2 --book 1ti
140
+
141
+ # Aligned Bible case (currently fails, needs fix)
142
+ node src/cli.js getAllCatalogEntries --owner BSOJ --repo ar_twl --ref v5 --book 1jn
143
+ `);
144
+ }
145
+
146
+ // Main function
147
+ async function main() {
148
+ const args = process.argv.slice(2);
149
+
150
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
151
+ showHelp();
152
+ process.exit(0);
153
+ }
154
+
155
+ const { command, params, output, quiet } = parseArgs(args);
156
+
157
+ // Validate required parameters
158
+ if (!params.owner || !params.repo) {
159
+ console.error('Error: --owner and --repo are required');
160
+ console.error('Use --help for usage information');
161
+ process.exit(1);
162
+ }
163
+
164
+ try {
165
+ let result;
166
+
167
+ switch (command) {
168
+ case 'getAllCatalogEntries': {
169
+ if (!quiet) {
170
+ console.error(`Fetching catalog entries for ${params.owner}/${params.repo}...`);
171
+ }
172
+ const books = params.bookId ? [params.bookId] : [];
173
+ result = await getAllCatalogEntries(
174
+ { owner: params.owner, repo: params.repo, ref: params.ref, books },
175
+ { dcs_api_url: params.dcsApiUrl || 'https://git.door43.org/api/v1', quiet }
176
+ );
177
+ break;
178
+ }
179
+
180
+ case 'getResourceData': {
181
+ if (!quiet) {
182
+ console.error(`Fetching resource data for ${params.owner}/${params.repo}...`);
183
+ }
184
+ const resourceBooks = params.bookId ? [params.bookId] : [];
185
+ result = await getResourceData(
186
+ { owner: params.owner, repo: params.repo, ref: params.ref, books: resourceBooks },
187
+ { dcs_api_url: params.dcsApiUrl || 'https://git.door43.org/api/v1', quiet }
188
+ );
189
+ break;
190
+ }
191
+
192
+ case 'renderHtml': {
193
+ if (!quiet) {
194
+ console.error(`Rendering HTML for ${params.owner}/${params.repo}...`);
195
+ }
196
+ const renderBooks = params.bookId ? [params.bookId] : [];
197
+ const resourceData = await getResourceData(
198
+ { owner: params.owner, repo: params.repo, ref: params.ref, books: renderBooks },
199
+ { dcs_api_url: params.dcsApiUrl || 'https://git.door43.org/api/v1', quiet }
200
+ );
201
+ // HtmlData package (identity + sections); the body can be large.
202
+ result = renderHtmlData(resourceData, { books: renderBooks });
203
+ break;
204
+ }
205
+
206
+ case 'assemblePrint': {
207
+ if (!quiet) {
208
+ console.error(`Assembling print document for ${params.owner}/${params.repo}...`);
209
+ }
210
+ const printBooks = params.bookId ? [params.bookId] : [];
211
+ const resourceData = await getResourceData(
212
+ { owner: params.owner, repo: params.repo, ref: params.ref, books: printBooks },
213
+ { dcs_api_url: params.dcsApiUrl || 'https://git.door43.org/api/v1', quiet }
214
+ );
215
+ const htmlData = renderHtmlData(resourceData, { books: printBooks });
216
+ const html = renderHTML(htmlData, {
217
+ media: 'print',
218
+ columns: params.columns || 1,
219
+ print: { pageSize: resolveCliPageSize(params.pageSize) },
220
+ });
221
+
222
+ // For assemblePrint, output the raw HTML, not JSON
223
+ if (output) {
224
+ const outputPath = path.resolve(output);
225
+ fs.writeFileSync(outputPath, html);
226
+ console.error(`✓ Print-ready HTML written to ${outputPath}`);
227
+ process.exit(0);
228
+ } else {
229
+ process.stdout.write(html + '\n', () => {
230
+ process.exit(0);
231
+ });
232
+ }
233
+ return; // Skip JSON formatting below
234
+ }
235
+
236
+ case 'generatePdf': {
237
+ if (!output) {
238
+ console.error('Error: generatePdf requires --output <file.pdf>');
239
+ process.exit(1);
240
+ }
241
+ if (!quiet) {
242
+ console.error(`Generating PDF for ${params.owner}/${params.repo}...`);
243
+ }
244
+ const pdfBooks = params.bookId ? [params.bookId] : [];
245
+ const resourceData = await getResourceData(
246
+ { owner: params.owner, repo: params.repo, ref: params.ref, books: pdfBooks },
247
+ { dcs_api_url: params.dcsApiUrl || 'https://git.door43.org/api/v1', quiet }
248
+ );
249
+ const htmlData = renderHtmlData(resourceData, { books: pdfBooks });
250
+ const pdfOutputPath = path.resolve(output);
251
+ await renderPdf(htmlData, {
252
+ pageSize: resolveCliPageSize(params.pageSize),
253
+ columns: params.columns || 1,
254
+ outputPath: pdfOutputPath,
255
+ // Offload to a hosted weasyprint-pdf service instead of the local binary.
256
+ pdfServiceUrl: params.pdf_service_url,
257
+ quiet,
258
+ });
259
+ console.error(`✓ PDF written to ${pdfOutputPath}`);
260
+ process.exit(0);
261
+ return;
262
+ }
263
+
264
+ default:
265
+ console.error(`Error: Unknown command "${command}"`);
266
+ console.error(
267
+ 'Valid commands: getAllCatalogEntries, getResourceData, renderHtml, assemblePrint, generatePdf'
268
+ );
269
+ process.exit(1);
270
+ }
271
+
272
+ // Format output
273
+ const jsonOutput = JSON.stringify(result, null, 2);
274
+
275
+ // Write to file or stdout
276
+ if (output) {
277
+ const outputPath = path.resolve(output);
278
+ fs.writeFileSync(outputPath, jsonOutput);
279
+ console.error(`✓ Output written to ${outputPath}`);
280
+ process.exit(0);
281
+ } else {
282
+ // Write to stdout and wait for it to flush before exiting
283
+ process.stdout.write(jsonOutput + '\n', () => {
284
+ process.exit(0);
285
+ });
286
+ }
287
+ } catch (error) {
288
+ console.error('Error:', error.message);
289
+ if (error.response) {
290
+ console.error('API Response:', error.response.status, error.response.statusText);
291
+ if (error.response.data) {
292
+ console.error('Details:', error.response.data);
293
+ }
294
+ }
295
+ process.exit(1);
296
+ }
297
+ }
298
+
299
+ // Run main function
300
+ main();