usfm2html 0.1.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/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # usfm2html-cli
2
+
3
+ Convert USFM to HTML using Proskomma and Sofria renderers.
4
+
5
+ Repository: `https://github.com/unfoldingWord/node-usfm2html-cli`
6
+
7
+ ## Install
8
+
9
+ ```
10
+ # npm
11
+ npm install -g usfm2html-cli
12
+
13
+ # pnpm
14
+ pnpm add -g usfm2html-cli
15
+
16
+ # yarn
17
+ yarn global add usfm2html-cli
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```
23
+ # Write to stdout
24
+ usfm2html path/to/file.usfm > output.html
25
+
26
+ # Write to a file directly
27
+ usfm2html path/to/file.usfm -o output.html
28
+
29
+ # Read from stdin
30
+ cat path/to/file.usfm | usfm2html - > output.html
31
+ ```
32
+
33
+ ## Options
34
+
35
+ ```
36
+ -o, --out <path> Write output to a file instead of stdout
37
+ -h, --help Show help
38
+ -v, --version Show version
39
+ ```
40
+
41
+ ## Notes
42
+
43
+ - Alignment markers are removed before rendering.
44
+ - Output is a full HTML document with embedded CSS.
45
+ - The renderer matches the Sofria HTML output used in the door43 preview app.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.js';
3
+
4
+ run().catch((error) => {
5
+ console.error(`usfm2html: ${error.message}`);
6
+ process.exitCode = 1;
7
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "usfm2html",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=18",
7
+ "pnpm": ">=8"
8
+ },
9
+ "bin": {
10
+ "usfm2html": "bin/usfm2html.js"
11
+ },
12
+ "exports": {
13
+ ".": "./src/index.js"
14
+ },
15
+ "scripts": {
16
+ "lint": "echo 'No lint configured'"
17
+ },
18
+ "description": "Convert USFM to HTML using Proskomma and Sofria renderers.",
19
+ "keywords": [
20
+ "usfm",
21
+ "bible",
22
+ "proskomma",
23
+ "cli",
24
+ "html"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/unfoldingWord/node-usfm2html-cli.git"
29
+ },
30
+ "homepage": "https://github.com/unfoldingWord/node-usfm2html-cli",
31
+ "bugs": {
32
+ "url": "https://github.com/unfoldingWord/node-usfm2html-cli/issues"
33
+ },
34
+ "license": "UNLICENSED",
35
+ "files": [
36
+ "bin",
37
+ "src",
38
+ "README.md"
39
+ ],
40
+ "dependencies": {
41
+ "proskomma-core": "^0.11.3",
42
+ "proskomma-json-tools": "^0.9.1",
43
+ "usfm-alignment-remover": "^0.1.6"
44
+ }
45
+ }
package/src/cli.js ADDED
@@ -0,0 +1,93 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { createRequire } from 'node:module';
5
+ import { usfmToHtml } from './index.js';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const { version } = require('../package.json');
9
+
10
+ const usage = `usfm2html <usfm file path> [options]\n\nOptions:\n -o, --out <path> Write output to a file instead of stdout\n -h, --help Show this help message\n -v, --version Show version`;
11
+
12
+ const parseArgs = (argv) => {
13
+ const args = [...argv];
14
+ const positional = [];
15
+ let outPath = null;
16
+
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ const arg = args[i];
19
+ if (arg === '-o' || arg === '--out') {
20
+ if (i + 1 >= args.length) {
21
+ throw new Error('Missing value for -o/--out.');
22
+ }
23
+ outPath = args[i + 1];
24
+ i += 1;
25
+ continue;
26
+ }
27
+
28
+ if (arg === '-h' || arg === '--help') {
29
+ return { help: true };
30
+ }
31
+
32
+ if (arg === '-v' || arg === '--version') {
33
+ return { version: true };
34
+ }
35
+
36
+ if (arg.startsWith('-')) {
37
+ throw new Error(`Unknown option: ${arg}`);
38
+ }
39
+
40
+ positional.push(arg);
41
+ }
42
+
43
+ return { positional, outPath };
44
+ };
45
+
46
+ const readInput = async (inputPath) => {
47
+ if (inputPath === '-') {
48
+ const chunks = [];
49
+ for await (const chunk of process.stdin) {
50
+ chunks.push(chunk);
51
+ }
52
+ return Buffer.concat(chunks).toString('utf8');
53
+ }
54
+
55
+ return fs.readFile(inputPath, 'utf8');
56
+ };
57
+
58
+ const writeOutput = async (outPath, html) => {
59
+ if (!outPath) {
60
+ process.stdout.write(html);
61
+ return;
62
+ }
63
+
64
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
65
+ await fs.writeFile(outPath, html, 'utf8');
66
+ };
67
+
68
+ export const run = async () => {
69
+ const { positional, outPath, help, version: showVersion } = parseArgs(process.argv.slice(2));
70
+
71
+ if (help) {
72
+ console.log(usage);
73
+ return;
74
+ }
75
+
76
+ if (showVersion) {
77
+ console.log(version);
78
+ return;
79
+ }
80
+
81
+ if (!positional || positional.length === 0) {
82
+ throw new Error('Missing USFM input path.');
83
+ }
84
+
85
+ if (positional.length > 1) {
86
+ throw new Error('Only one USFM input path can be provided.');
87
+ }
88
+
89
+ const inputPath = positional[0];
90
+ const usfm = await readInput(inputPath);
91
+ const html = usfmToHtml(usfm, { sourcePath: inputPath });
92
+ await writeOutput(outPath, html);
93
+ };
package/src/index.js ADDED
@@ -0,0 +1,548 @@
1
+ import { Proskomma } from 'proskomma-core';
2
+ import { SofriaRenderFromProskomma, render } from 'proskomma-json-tools';
3
+ import { removeAlignments } from 'usfm-alignment-remover';
4
+ import { renderers } from './renderer/sofria2html.js';
5
+
6
+ const defaultFlags = {
7
+ showWordAtts: false,
8
+ showTitles: true,
9
+ showHeadings: true,
10
+ showIntroductions: true,
11
+ showFootnotes: true,
12
+ showXrefs: true,
13
+ showParaStyles: true,
14
+ showCharacterMarkup: true,
15
+ showChapterLabels: true,
16
+ showVersesLabels: true,
17
+ };
18
+
19
+ const renderFlags = {
20
+ showWordAtts: false,
21
+ showTitles: true,
22
+ showHeadings: true,
23
+ showFootnotes: true,
24
+ showXrefs: true,
25
+ showChapterLabels: true,
26
+ showVersesLabels: true,
27
+ showCharacterMarkup: true,
28
+ showParaStyles: true,
29
+ selectedBcvNotes: [],
30
+ };
31
+
32
+ const extraCss = `
33
+ pre {
34
+ font-family: inherit;
35
+ font-size: inherit;
36
+ padding-left: 2rem;
37
+ background-color: #f8f8f8;
38
+ border-radius: 4px;
39
+ overflow-x: auto;
40
+ border: 1px solid #e9ecef;
41
+ margin: 1em 0;
42
+ }
43
+
44
+ pre code {
45
+ border-radius: unset;
46
+ background: inherit;
47
+ padding: 0;
48
+ margin: 0;
49
+ font-family: inherit;
50
+ font-size: inherit;
51
+ color: inherit;
52
+ }
53
+
54
+ code {
55
+ background-color: #f4f4f4;
56
+ padding: 2px 6px;
57
+ border-radius: 3px;
58
+ font-family: 'Courier New', monospace;
59
+ font-size: 0.9em;
60
+ color: #d63384;
61
+ }
62
+
63
+ ol {
64
+ list-style: decimal;
65
+ }
66
+
67
+ ol ol {
68
+ list-style: upper-alpha;
69
+ }
70
+
71
+ ol ol ol {
72
+ list-style: upper-roman;
73
+ }
74
+
75
+ ol ol ol ol {
76
+ list-style: lower-roman;
77
+ }
78
+
79
+ ol ol ol ol ol {
80
+ list-style: lower-alpha;
81
+ }
82
+
83
+ ul, ol {
84
+ margin: 1em 0;
85
+ padding-left: 2em;
86
+ }
87
+
88
+ li {
89
+ margin: 0.5em 0;
90
+ }
91
+
92
+ blockquote {
93
+ margin-left: 40px;
94
+ margin-right: 40px;
95
+ color: #333;
96
+ padding: 1.5em;
97
+ line-height: 1.6;
98
+ border-left: 4px solid #ccc;
99
+ background-color: #f9f9f9;
100
+ }
101
+
102
+ blockquote cite {
103
+ display: block;
104
+ text-align: right;
105
+ font-size: 0.9em;
106
+ color: #666;
107
+ margin-top: 1em;
108
+ }
109
+
110
+ blockquote p {
111
+ margin: 0.5em 0;
112
+ }
113
+
114
+ blockquote p:first-child {
115
+ margin-top: 0;
116
+ }
117
+
118
+ blockquote p:last-child {
119
+ margin-bottom: 0;
120
+ }
121
+
122
+ table {
123
+ border-collapse: collapse;
124
+ width: 100%;
125
+ margin: 1.5em 0;
126
+ border: 1px solid #dee2e6;
127
+ }
128
+
129
+ th, td {
130
+ border: 1px solid #dee2e6;
131
+ padding: 8px 12px;
132
+ text-align: left;
133
+ }
134
+
135
+ th {
136
+ background-color: #f8f9fa;
137
+ font-weight: bold;
138
+ color: #495057;
139
+ }
140
+
141
+ tbody tr:nth-child(even) {
142
+ background-color: #f9f9f9;
143
+ }
144
+
145
+ h1, h2, h3, h4, h5, h6 {
146
+ color: #495057;
147
+ margin-top: 1.5em;
148
+ margin-bottom: 0.5em;
149
+ font-weight: bold;
150
+ line-height: 1.2;
151
+ }
152
+
153
+ h1 {
154
+ font-size: 2em;
155
+ padding-bottom: 0.3em;
156
+ }
157
+
158
+ h2 {
159
+ font-size: 1.5em;
160
+ }
161
+
162
+ h3 {
163
+ font-size: 1.25em;
164
+ }
165
+
166
+ p {
167
+ margin: 1em 0;
168
+ line-height: 1.6;
169
+ }
170
+
171
+ a {
172
+ color: #007acc;
173
+ text-decoration: none;
174
+ }
175
+
176
+ a:hover {
177
+ text-decoration: underline;
178
+ }
179
+
180
+ a.internal-link {
181
+ color: #007acc;
182
+ cursor: s-resize;
183
+ }
184
+
185
+ a.external-link {
186
+ position: relative;
187
+ font-weight: bold !important;
188
+ color: #666 !important;
189
+ border-bottom: dashed 1px #666 !important;
190
+ text-decoration: none !important;
191
+ cursor: ne-resize !important;
192
+ }
193
+
194
+ a.external-link::after {
195
+ content: ' [link]';
196
+ font-size: 0.8em;
197
+ vertical-align: super;
198
+ }
199
+
200
+ .back-refs a {
201
+ position: relative;
202
+ cursor: n-resize;
203
+ }
204
+
205
+ .verse-ref {
206
+ font-weight: bold;
207
+ color: #0066cc;
208
+ text-decoration: none;
209
+ }
210
+
211
+ .verse-ref:hover {
212
+ text-decoration: underline;
213
+ }
214
+
215
+ .translation-note {
216
+ background-color: #fff3cd;
217
+ border: 1px solid #ffeaa7;
218
+ padding: 1em;
219
+ margin: 1em 0;
220
+ border-radius: 4px;
221
+ border-left: 4px solid #ffc107;
222
+ }
223
+
224
+ .note-header {
225
+ font-weight: bold;
226
+ color: #856404;
227
+ margin-bottom: 0.5em;
228
+ }
229
+
230
+ .cross-ref {
231
+ font-size: 0.9em;
232
+ color: #6c757d;
233
+ font-style: italic;
234
+ }
235
+
236
+ .original-text,
237
+ .hebrew-text {
238
+ font-family: "Ezra", "SBL Hebrew", "Times New Roman", serif;
239
+ font-size: 1.1em;
240
+ font-weight: normal;
241
+ direction: rtl;
242
+ }
243
+
244
+ .greek-text {
245
+ font-family: "CharisSIL", "SBL Greek", "Gentium Plus", "Times New Roman", serif;
246
+ font-size: 1.1em;
247
+ direction: ltr;
248
+ }
249
+
250
+ .strongs {
251
+ font-size: 0.8em;
252
+ color: #6c757d;
253
+ font-weight: normal;
254
+ }
255
+
256
+ strong, b {
257
+ font-weight: bold;
258
+ }
259
+
260
+ em, i {
261
+ font-style: italic;
262
+ }
263
+
264
+ hr {
265
+ border: none;
266
+ height: 1px;
267
+ background-color: #dee2e6;
268
+ margin: 2em 0;
269
+ }
270
+
271
+ mark, .highlight {
272
+ background-color: #fff3cd;
273
+ padding: 1px 2px;
274
+ border-radius: 2px;
275
+ }
276
+
277
+ img {
278
+ max-width: 100%;
279
+ height: auto;
280
+ display: block;
281
+ margin: 1em auto;
282
+ }
283
+
284
+ dl {
285
+ margin: 1em 0;
286
+ }
287
+
288
+ dt {
289
+ font-weight: bold;
290
+ margin-top: 1em;
291
+ color: #495057;
292
+ }
293
+
294
+ dd {
295
+ margin-left: 2em;
296
+ margin-bottom: 0.5em;
297
+ }
298
+
299
+ .footnote {
300
+ font-size: 0.9em;
301
+ color: #6c757d;
302
+ border-top: 1px solid #dee2e6;
303
+ padding-top: 0.5em;
304
+ margin-top: 2em;
305
+ }
306
+
307
+ h1 {
308
+ column-span: all;
309
+ }
310
+
311
+ .new-page {
312
+ break-after: page;
313
+ column-span: all;
314
+ }
315
+
316
+ .header-link {
317
+ text-decoration: none;
318
+ color: inherit;
319
+ }
320
+
321
+ .web-preview .paras_usfm_f {
322
+ padding-left: 0.5em;
323
+ padding-right: 0.5em;
324
+ background-color: #ccc;
325
+ margin-top: 1em;
326
+ margin-bottom: 1em;
327
+ }
328
+
329
+ .paras_usfm_fq, .paras_usfm_fqa {
330
+ font-style: italic;
331
+ }
332
+
333
+ .implied-word-text {
334
+ color: #999;
335
+ font-weight: bold;
336
+ font-size: 0.9em;
337
+ }
338
+
339
+ :root {
340
+ --chapter-gutter: 4.5em;
341
+ --verse-gutter: -2em;
342
+ --label-gap: 1em;
343
+ --chapter-nudge: 4em;
344
+ --poetry-gutter: calc(var(--chapter-gutter) + var(--verse-gutter) + var(--label-gap));
345
+ --q2-extra: 2em;
346
+ --q3-extra: 3.5em;
347
+ --q4-extra: 5em;
348
+ --chapter-raise: 1em;
349
+ --label-top: 0.4em;
350
+ --chapter-break: 1.2em;
351
+ }
352
+
353
+ p.paras_usfm_q,
354
+ p.paras_usfm_q2,
355
+ p.paras_usfm_q3,
356
+ p.paras_usfm_q4 {
357
+ position: relative !important;
358
+ text-indent: 0 !important;
359
+ }
360
+
361
+ p.paras_usfm_q {
362
+ padding-left: var(--poetry-gutter) !important;
363
+ }
364
+
365
+ p.paras_usfm_q2 {
366
+ padding-left: calc(var(--poetry-gutter) + var(--q2-extra)) !important;
367
+ }
368
+
369
+ p.paras_usfm_q3 {
370
+ padding-left: calc(var(--poetry-gutter) + var(--q3-extra)) !important;
371
+ }
372
+
373
+ p.paras_usfm_q4 {
374
+ padding-left: calc(var(--poetry-gutter) + var(--q4-extra)) !important;
375
+ }
376
+
377
+ p.paras_usfm_q > .marks_chapter_label,
378
+ p.paras_usfm_q2 > .marks_chapter_label,
379
+ p.paras_usfm_q3 > .marks_chapter_label,
380
+ p.paras_usfm_q4 > .marks_chapter_label,
381
+ p.paras_usfm_q > .marks_verses_label,
382
+ p.paras_usfm_q2 > .marks_verses_label,
383
+ p.paras_usfm_q3 > .marks_verses_label,
384
+ p.paras_usfm_q4 > .marks_verses_label {
385
+ position: absolute !important;
386
+ float: none !important;
387
+ display: block !important;
388
+ margin: 0 !important;
389
+ padding: 0 !important;
390
+ white-space: nowrap !important;
391
+ line-height: 1 !important;
392
+ z-index: 2 !important;
393
+ }
394
+
395
+ p.paras_usfm_q > .marks_chapter_label,
396
+ p.paras_usfm_q2 > .marks_chapter_label,
397
+ p.paras_usfm_q3 > .marks_chapter_label,
398
+ p.paras_usfm_q4 > .marks_chapter_label {
399
+ left: calc(-1 * var(--chapter-nudge)) !important;
400
+ top: var(--label-top) !important;
401
+ width: var(--chapter-gutter) !important;
402
+ text-align: right !important;
403
+ transform: translateY(calc(-1 * var(--chapter-raise))) !important;
404
+ transform-origin: top right;
405
+ }
406
+
407
+ p.paras_usfm_q > .marks_verses_label,
408
+ p.paras_usfm_q2 > .marks_verses_label,
409
+ p.paras_usfm_q3 > .marks_verses_label,
410
+ p.paras_usfm_q4 > .marks_verses_label {
411
+ left: calc(var(--chapter-gutter) + var(--label-gap) - var(--chapter-nudge)) !important;
412
+ top: var(--label-top) !important;
413
+ width: var(--verse-gutter) !important;
414
+ text-align: right !important;
415
+ }
416
+
417
+ p.paras_usfm_q > .marks_chapter_label a,
418
+ p.paras_usfm_q2 > .marks_chapter_label a,
419
+ p.paras_usfm_q3 > .marks_chapter_label a,
420
+ p.paras_usfm_q4 > .marks_chapter_label a,
421
+ p.paras_usfm_q > .marks_verses_label a,
422
+ p.paras_usfm_q2 > .marks_verses_label a,
423
+ p.paras_usfm_q3 > .marks_verses_label a,
424
+ p.paras_usfm_q4 > .marks_verses_label a {
425
+ display: inline !important;
426
+ float: none !important;
427
+ position: static !important;
428
+ margin: 0 !important;
429
+ padding: 0 !important;
430
+ text-decoration: none;
431
+ }
432
+
433
+ p.paras_usfm_q:has(> .marks_chapter_label) {
434
+ margin-top: var(--chapter-break) !important;
435
+ padding-top: calc(var(--chapter-raise) * 0.6) !important;
436
+ }
437
+
438
+ @supports not selector(p:has(> span)) {
439
+ p.paras_usfm_q > .marks_chapter_label::before {
440
+ content: "";
441
+ display: block;
442
+ height: var(--chapter-break);
443
+ }
444
+ }
445
+
446
+ .implied-word-start,
447
+ .implied-word-end {
448
+ display: none;
449
+ }
450
+ `;
451
+
452
+ const buildCss = () => {
453
+ const baseCss = render.sofria2web.renderStyles.styleAsCSS(render.sofria2web.renderStyles.styles);
454
+ return `${baseCss}\n${extraCss}`;
455
+ };
456
+
457
+ const stripTags = (input) => input.replace(/<[^>]*>/g, '');
458
+
459
+ const escapeAttribute = (input) =>
460
+ input
461
+ .replace(/&/g, '&amp;')
462
+ .replace(/"/g, '&quot;')
463
+ .replace(/</g, '&lt;')
464
+ .replace(/>/g, '&gt;');
465
+
466
+ const parseHtml = (html, book, titleText, showChapters = true) => {
467
+ const titleMatch = html.match(/<p [^>]*>(.*?)<\/p>/);
468
+ const rawTitle = titleText || (titleMatch ? stripTags(titleMatch[1]) : '');
469
+ const safeTitle = escapeAttribute(rawTitle);
470
+ let updated = html.replace(/<p /, `<p id="nav-${book}" `);
471
+
472
+ updated = updated.replaceAll(
473
+ /<span id="chapter-(\d+)-verse-(\d+)"([^>]*)>(\d+)<\/span>/g,
474
+ `<span id="nav-${book}-$1-$2"$3><a href="#nav-${book}-$1-$2" class="header-link">$4</a></span>`
475
+ );
476
+
477
+ updated = updated.replaceAll(
478
+ /<span id="chapter-(\d+)"([^>]+)>([\d]+)<\/span>/gi,
479
+ `<span id="nav-${book}-$1"${showChapters && safeTitle ? ` data-toc-title="${safeTitle} $1"` : ''}$2><a href="#nav-${book}-$1-1" class="header-link">$3</a></span>`
480
+ );
481
+
482
+ updated = updated.replace(/<span([^>]+style="[^">]+#CCC[^">]+")/gi, '<span$1 class="footnote"');
483
+ updated = updated.replace(/[\u00A0\u202F\u2009 ]+/g, ' ');
484
+ updated = updated.replace(/(\d{1,3})(?:\s*,\s*\d{3})+\b/g, (m) => m.replace(/\s*,\s*/g, ','));
485
+
486
+ const footnotes = updated.match(/<span class="footnote">/g);
487
+ if (footnotes) {
488
+ footnotes.forEach((footnote, index) => {
489
+ const footnoteId = `footnote-${index}`;
490
+ const anchor = `<a href="#${footnoteId}">${index + 1}.</a>`;
491
+ updated = updated.replace(footnote, `${footnote}<span id="${footnoteId}">${anchor}</span>`);
492
+ });
493
+ }
494
+
495
+ return `\n<div class="section bible-book" id="nav-${book}"${safeTitle ? ` data-toc-title="${safeTitle}"` : ''}>\n${updated}\n</div>\n`;
496
+ };
497
+
498
+ const normalizeUsfm = (usfm) => {
499
+ const withoutBom = usfm.replace(/^\uFEFF/, '');
500
+ const withoutAlignments = removeAlignments(withoutBom);
501
+ return withoutAlignments.replace(/\\s5\s*\n/g, '').replace(/\r\n/g, '\n');
502
+ };
503
+
504
+ const extractBookId = (usfm) => {
505
+ const idMatch = usfm.match(/^\\id\s+([A-Za-z0-9]{3})\b/m);
506
+ return idMatch ? idMatch[1].toLowerCase() : 'book';
507
+ };
508
+
509
+ const extractTitle = (usfm) => {
510
+ const titleMatch = usfm.match(/^\\h\s+(.+)$/m) || usfm.match(/^\\toc1\s+(.+)$/m);
511
+ return titleMatch ? titleMatch[1].trim() : 'USFM Render';
512
+ };
513
+
514
+ export const usfmToHtml = (usfm, options = {}) => {
515
+ const pk = new Proskomma();
516
+ const normalized = normalizeUsfm(usfm);
517
+ const bookId = options.bookId || extractBookId(normalized);
518
+ const title = options.title || extractTitle(normalized);
519
+
520
+ const result = pk.importDocument({ lang: 'xxx', abbr: 'XXX' }, 'usfm', normalized);
521
+ if (!result?.id) {
522
+ throw new Error('Unable to import USFM into Proskomma.');
523
+ }
524
+
525
+ const config = {
526
+ ...defaultFlags,
527
+ ...renderFlags,
528
+ selectedBcvNotes: [],
529
+ renderers,
530
+ };
531
+
532
+ const renderer = new SofriaRenderFromProskomma({
533
+ proskomma: pk,
534
+ actions: render.sofria2web.renderActions.sofria2WebActions,
535
+ });
536
+
537
+ const output = {};
538
+ renderer.renderDocument({
539
+ config,
540
+ docId: result.id,
541
+ output,
542
+ });
543
+
544
+ const body = parseHtml(output.paras, bookId, title, true);
545
+ const css = buildCss();
546
+
547
+ return `<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<title>${title}</title>\n<style>\n${css}\n</style>\n</head>\n<body class="web-preview">\n${body}\n</body>\n</html>\n`;
548
+ };
@@ -0,0 +1,78 @@
1
+ const renderers = {
2
+ text: (text) =>
3
+ text
4
+ .replace(/{/g, '<span class="implied-word"><span class="implied-word-start">{</span><span class="implied-word-text">')
5
+ .replace(/}/g, '</span><span class="implied-word-end">}</span></span>'),
6
+ chapter: '0',
7
+ chapter_label(number) {
8
+ this.chapter = number;
9
+ return `<span id="chapter-${this.chapter}" class="marks_chapter_label">${number}</span>`;
10
+ },
11
+ verses_label(number) {
12
+ return `<span id="chapter-${this.chapter}-verse-${number}" class="marks_verses_label">${number}</span>`;
13
+ },
14
+ paragraph: (subType, content, footnoteNo) => {
15
+ const paraClass = subType.split(':')[1];
16
+ let paraTag = 'p';
17
+ if (['f', 'x'].includes(paraClass)) {
18
+ paraTag = 'span';
19
+ } else if (['s', 'ms', 'imt', 'imte', 'mt'].includes(paraClass)) {
20
+ paraTag = 'h1';
21
+ } else if (['s2', 'ms2', 'imt2', 'imte2', 'mt2', 'mr', 'sr'].includes(paraClass)) {
22
+ paraTag = 'h2';
23
+ } else if (['s3', 'ms3', 'imt3', 'imte3', 'mt3', 'r', 'd'].includes(paraClass)) {
24
+ paraTag = 'h3';
25
+ } else if (['s4', 'ms4', 'imt4', 'imte4', 'mt4'].includes(paraClass)) {
26
+ paraTag = 'h4';
27
+ }
28
+
29
+ const idAttr = paraClass === 'f' ? `id="footnote-${footnoteNo}" ` : '';
30
+ return `<${paraTag} ${idAttr}class="paras_usfm_${paraClass}">${content.join('')}</${paraTag}>`;
31
+ },
32
+ wrapper: (atts, subType, content) => {
33
+ if (subType === 'cell') {
34
+ if (atts.role === 'body') {
35
+ return `<td colspan=${atts.nCols} style="text-align:${atts.alignment}">${content.join('')}</td>`;
36
+ }
37
+ return `<th colspan=${atts.nCols} style="text-align:${atts.alignment}">${content.join('')}</th>`;
38
+ }
39
+
40
+ return `<span class="wrappers_usfm_${subType.split(':')[1]}">${content.join('')}</span>`;
41
+ },
42
+ wWrapper: (atts, content) => {
43
+ const safeContent = Array.isArray(content) ? content.join('') : content;
44
+ if (Object.keys(atts).length === 0) {
45
+ return safeContent;
46
+ }
47
+
48
+ return `<span
49
+ style={{
50
+ display: "inline-block",
51
+ verticalAlign: "top",
52
+ textAlign: "center"
53
+ }}
54
+ >
55
+ <div>${safeContent}</div>${Object.entries(atts)
56
+ .map(
57
+ (a) =>
58
+ `<div
59
+ style={{
60
+ fontSize: "xx-small",
61
+ fontWeight: "bold"
62
+ }}
63
+ >
64
+ {${a[0]} = ${a[1]}}
65
+ </div>`
66
+ )
67
+ .join('')
68
+ }</span>`;
69
+ },
70
+ milestone: () => '',
71
+ startChapters: (nCols) => `<section class="chapters" style="columns: ${nCols}">`,
72
+ endChapters: () => '</section>',
73
+ mergeParas: (paras) => paras.join('\n'),
74
+ row: (content) => `<tr>${content.join('')}</tr>`,
75
+ table: (content) => `<table border>${content.join(' ')}</table>`,
76
+ };
77
+
78
+ export { renderers };