@eten-tech-foundation/scripture-utilities 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2023-2025 ETEN Tech Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # scripture-utilities
2
+
3
+ <div align="center">
4
+
5
+ [![Build Status][github-actions-status]][github-actions-url]
6
+ [![CodeQL][gitghub-codeql-status]][gitghub-codeql-url]
7
+ [![Github Tag][npm-version-image]][npm-version-url]
8
+
9
+ </div>
10
+
11
+ Utilities for working with Scripture data.
12
+
13
+ ## Features
14
+
15
+ For data that conforms to [USX/USJ v3.1](https://docs.usfm.bible/usfm/3.1/):
16
+
17
+ - USJ to USX converter.
18
+ - USX to USJ converter.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install @eten-tech-foundation/scripture-utilities
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { usxStringToUsj, usjToUsxString } from "@eten-tech-foundation/scripture-utilities";
30
+
31
+ const emptyUsx = '<usx version="3.1" />';
32
+ const usx = `
33
+ <?xml version="1.0" encoding="utf-8"?>
34
+ <usx version="3.1">
35
+ <book code="PSA" style="id">World English Bible (WEB)</book>
36
+ <para style="mt1">The Psalms</para>
37
+ <chapter number="1" style="c" sid="PSA 1" />
38
+ <para style="q1">
39
+ <verse number="1" style="v" sid="PSA 1:1" />Blessed is the man who doesn’t walk in the counsel of the wicked,</para>
40
+ <para style="q2" vid="PSA 1:1">nor stand on the path of sinners,</para>
41
+ <para style="q2" vid="PSA 1:1">nor sit in the seat of scoffers;<verse eid="PSA 1:1" /></para>
42
+ </usx>
43
+ `;
44
+
45
+ const emptyUsj = usxStringToUsj(emptyUsx);
46
+ const usj = usxStringToUsj(usx);
47
+
48
+ const newUsx = usjToUsxString(usj);
49
+ ```
50
+
51
+ ## Building
52
+
53
+ Run `nx build utilities` to build the library.
54
+
55
+ ## Running unit tests
56
+
57
+ Run `nx test utilities` to execute the unit tests via [Jest](https://jestjs.io).
58
+
59
+ ## Develop in App
60
+
61
+ To develop these utilities in a target application you can use [yalc](https://www.npmjs.com/package/yalc) to link the editor in without having to publish to NPM every time something changes.
62
+
63
+ 1. In this monorepo, publish the editor to `yalc`, e.g.:
64
+ ```bash
65
+ nx devpub utilities
66
+ ```
67
+ 2. In the target application repo, link from `yalc`:
68
+ ```bash
69
+ yalc link @eten-tech-foundation/scripture-utilities
70
+ ```
71
+ 3. In this monorepo, make changes and re-publish the editor (see step 1).
72
+ 4. When you have finished developing in the target application repo, unlink from `yalc`:
73
+ ```bash
74
+ yalc remove @eten-tech-foundation/scripture-utilities && npm i
75
+ ```
76
+
77
+ ## License
78
+
79
+ [MIT][github-license] © [ETEN Tech Foundation](https://missionmutual.org)
80
+
81
+ <!-- define variables used above -->
82
+
83
+ [github-actions-status]: https://github.com/eten-tech-foundation/scripture-editors/actions/workflows/test-publish.yml/badge.svg
84
+ [github-actions-url]: https://github.com/eten-tech-foundation/scripture-editors/actions
85
+ [gitghub-codeql-status]: https://github.com/eten-tech-foundation/scripture-editors/actions/workflows/codeql.yml/badge.svg
86
+ [gitghub-codeql-url]: https://github.com/eten-tech-foundation/scripture-editors/actions/workflows/codeql.yml
87
+ [npm-version-image]: https://img.shields.io/npm/v/@eten-tech-foundation/scripture-utilities
88
+ [npm-version-url]: https://github.com/eten-tech-foundation/scripture-editors/releases
89
+ [github-license]: https://github.com/eten-tech-foundation/scripture-editors/blob/main/packages/utilities/LICENSE
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Converts a USJ JSONPath string into an array of indexes.
3
+ *
4
+ * @param jsonPath - The USJ JSONPath string to convert. It must start with `$` and contain `.content[index]` segments.
5
+ * @returns An array of numeric indexes extracted from the JSONPath.
6
+ * @throws Will throw an error if the JSONPath does not start with `$`.
7
+ */
8
+ export declare function indexesFromUsjJsonPath(jsonPath: string): number[];
9
+ /**
10
+ * Converts an array of indexes into a USJ JSONPath string.
11
+ *
12
+ * @param indexes - An array of numeric indexes to convert.
13
+ * @returns A USJ JSONPath string constructed from the indexes.
14
+ */
15
+ export declare function usjJsonPathFromIndexes(indexes: number[]): string;
@@ -0,0 +1,4 @@
1
+ import { Document, Element } from '@xmldom/xmldom';
2
+ import { Usj } from './usj.model';
3
+ export declare function usjToUsxString(usj: Usj): string;
4
+ export declare function usjToUsxDom(usj: Usj, usxDoc: Document): Element | undefined;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Unified Scripture JSON (USJ) - The JSON variant of USFM and USX data models.
3
+ * These types follow this schema:
4
+ * @see https://github.com/usfm-bible/tcdocs/blob/usj/grammar/usj.js
5
+ */
6
+ /** The USJ spec type */
7
+ export declare const USJ_TYPE = "USJ";
8
+ /** The USJ spec version */
9
+ export declare const USJ_VERSION = "3.1";
10
+ /** List of known properties of `MarkerObject` */
11
+ export declare const MARKER_OBJECT_PROPS: (keyof MarkerObject)[];
12
+ /** Single piece of Scripture content */
13
+ export type MarkerContent = string | MarkerObject;
14
+ /** A Scripture Marker and its contents */
15
+ export type MarkerObject = {
16
+ /**
17
+ * The kind/category of node or element this is, corresponding the USFM marker and USX node
18
+ * @example `para`, `verse`, `char`
19
+ */
20
+ type: string;
21
+ /**
22
+ * The corresponding marker in USFM or style in USX
23
+ * @example `p`, `v`, `nd`
24
+ */
25
+ marker: string;
26
+ /** This marker's contents laid out in order */
27
+ content?: MarkerContent[];
28
+ /** Indicates the Book-chapter-verse value in the paragraph based structure */
29
+ sid?: string;
30
+ /** Milestone end ID, matches start ID (not currently included in USJ spec) */
31
+ eid?: string;
32
+ /** Chapter number or verse number */
33
+ number?: string;
34
+ /** The 3-letter book code in ID element */
35
+ code?: BookCode;
36
+ /** Alternate chapter number or verse number */
37
+ altnumber?: string;
38
+ /** Published character of chapter or verse */
39
+ pubnumber?: string;
40
+ /** Caller character for footnotes and cross-refs */
41
+ caller?: string;
42
+ /** Alignment of table cells */
43
+ align?: string;
44
+ /** Category of extended study bible sections */
45
+ category?: string;
46
+ };
47
+ /** Scripture data represented in JSON format. Data compatible transformation from USX/USFM */
48
+ export type Usj = {
49
+ /** The USJ spec type */
50
+ type: typeof USJ_TYPE;
51
+ /** The USJ spec version */
52
+ version: typeof USJ_VERSION;
53
+ /** The JSON representation of scripture contents from USFM/USX */
54
+ content: MarkerContent[];
55
+ };
56
+ export declare function isValidBookCode(code: string): boolean;
57
+ /** 3-letter Scripture book code */
58
+ export type BookCode = (typeof VALID_BOOK_CODES)[number];
59
+ declare const VALID_BOOK_CODES: readonly ["GEN", "EXO", "LEV", "NUM", "DEU", "JOS", "JDG", "RUT", "1SA", "2SA", "1KI", "2KI", "1CH", "2CH", "EZR", "NEH", "EST", "JOB", "PSA", "PRO", "ECC", "SNG", "ISA", "JER", "LAM", "EZK", "DAN", "HOS", "JOL", "AMO", "OBA", "JON", "MIC", "NAM", "HAB", "ZEP", "HAG", "ZEC", "MAL", "MAT", "MRK", "LUK", "JHN", "ACT", "ROM", "1CO", "2CO", "GAL", "EPH", "PHP", "COL", "1TH", "2TH", "1TI", "2TI", "TIT", "PHM", "HEB", "JAS", "1PE", "2PE", "1JN", "2JN", "3JN", "JUD", "REV", "TOB", "JDT", "ESG", "WIS", "SIR", "BAR", "LJE", "S3Y", "SUS", "BEL", "1MA", "2MA", "3MA", "4MA", "1ES", "2ES", "MAN", "PS2", "ODA", "PSS", "EZA", "5EZ", "6EZ", "DAG", "PS3", "2BA", "LBA", "JUB", "ENO", "1MQ", "2MQ", "3MQ", "REP", "4BA", "LAO", "FRT", "BAK", "OTH", "INT", "CNC", "GLO", "TDX", "NDX", "XXA", "XXB", "XXC", "XXD", "XXE", "XXF", "XXG"];
60
+ export {};
@@ -0,0 +1,4 @@
1
+ import { Element } from '@xmldom/xmldom';
2
+ import { Usj } from './usj.model';
3
+ export declare function usxStringToUsj(usxString: string): Usj;
4
+ export declare function usxDomToUsj(inputUsxDom: Element | null): Usj;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Unified Scripture XML (USX).
3
+ * These types follow this schema:
4
+ * @see https://github.com/usfm-bible/tcdocs/blob/main/grammar/usx.rng
5
+ */
6
+ /** The USX spec type */
7
+ export declare const USX_TYPE = "usx";
8
+ /** The USX spec version */
9
+ export declare const USX_VERSION = "3.1";
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const O=require("@xmldom/xmldom"),p="USJ",N="3.1",J=["type","marker","content","sid","eid","number","code","altnumber","pubnumber","caller","align","category"];function v(e){return m.includes(e)}const m=["GEN","EXO","LEV","NUM","DEU","JOS","JDG","RUT","1SA","2SA","1KI","2KI","1CH","2CH","EZR","NEH","EST","JOB","PSA","PRO","ECC","SNG","ISA","JER","LAM","EZK","DAN","HOS","JOL","AMO","OBA","JON","MIC","NAM","HAB","ZEP","HAG","ZEC","MAL","MAT","MRK","LUK","JHN","ACT","ROM","1CO","2CO","GAL","EPH","PHP","COL","1TH","2TH","1TI","2TI","TIT","PHM","HEB","JAS","1PE","2PE","1JN","2JN","3JN","JUD","REV","TOB","JDT","ESG","WIS","SIR","BAR","LJE","S3Y","SUS","BEL","1MA","2MA","3MA","4MA","1ES","2ES","MAN","PS2","ODA","PSS","EZA","5EZ","6EZ","DAG","PS3","2BA","LBA","JUB","ENO","1MQ","2MQ","3MQ","REP","4BA","LAO","FRT","BAK","OTH","INT","CNC","GLO","TDX","NDX","XXA","XXB","XXC","XXD","XXE","XXF","XXG"],u="usx",y="3.1";function M(e){const n=new O.DOMParser().parseFromString(e,"text/xml");return R(n.documentElement)}function R(e){const[t]=e?P(e):[{content:[]}];return t.type=p,t.version=N,t}function P(e){const t={};let n=e.tagName,o,s,c="append";if(["row","cell"].includes(n)&&(n="table:"+n),e.attributes)for(const r of Array.from(e.attributes))t[r.name]=r.value;t.style&&(o=t.style,delete t.style),t.vid&&delete t.vid,t.status&&delete t.status;let i={type:n};o&&(i.marker=o),i={...i,...t},e.firstChild&&e.firstChild.nodeType===e.firstChild.TEXT_NODE&&e.firstChild.nodeValue&&E(e.firstChild.nodeValue)!==""&&(s=e.firstChild.nodeValue);const l=Array.from(e.childNodes);i.content=[],s&&i.content.push(s);for(const r of l){if(r.tagName===void 0)continue;const[f,S]=P(r);switch(S){case"append":i.content.push(f);break;case"merge":i.content=i.content.concat(f);break}r.nextSibling&&r.nextSibling.nodeType===r.nextSibling.TEXT_NODE&&r.nextSibling.nodeValue&&(E(r.nextSibling.nodeValue)!==""||r.nextSibling.nodeValue===" ")&&i.content.push(r.nextSibling.nodeValue)}return i.content.length===0&&i.type!==u&&delete i.content,"eid"in i&&["verse","chapter"].includes(n)&&(c="ignore"),[i,c]}function E(e){return e.replace(/(^[ \t\n\r\f\v]+)|([ \t\n\r\f\v]+$)/g,"")}let d,a;function X(e){const t=new O.DOMImplementation().createDocument("",u);return t.documentElement&&(t.documentElement.setAttribute("version",y),_(e,t)),t.toString()}function _(e,t){if(t.documentElement){for(const[n,o]of e.content.entries()){const s=n===e.content.length-1;b(o,t.documentElement,t,s)}return t.documentElement??void 0}}function b(e,t,n,o){let s,c,i;if(typeof e=="string")s=n.createTextNode(e);else if(c=e.type.replace("table:",""),s=n.createElement(c),B(s,e),e.content)for(const[r,f]of e.content.entries()){const S=r===e.content.length-1;b(f,s,n,S)}a&&(c==="verse"||t.tagName==="para"&&o)&&(i=T(n,a),a=void 0),c==="verse"&&typeof e!="string"&&e.sid!==void 0&&(a=e.sid),d&&(c==="chapter"||c==="para"&&o)&&(i=h(n,d),d=void 0),c==="chapter"&&typeof e!="string"&&e.sid!==void 0&&(d=e.sid);const l=t.nodeName===u&&(i==null?void 0:i.tagName)==="verse";i&&(!o||l)&&t.appendChild(i),t.appendChild(s),i&&o&&!l&&t.appendChild(i),o&&t.nodeName===u&&(a&&t.appendChild(T(n,a)),d&&t.appendChild(h(n,d)),a=void 0,d=void 0)}function B(e,t){t.type==="unmatched"?e.setAttribute("marker",t.marker):e.setAttribute("style",t.marker);for(const[n,o]of Object.entries(t))o&&!["type","marker","content"].includes(n)&&e.setAttribute(n,o)}function T(e,t){const n=e.createElement("verse");return n.setAttribute("eid",t),n}function h(e,t){const n=e.createElement("chapter");return n.setAttribute("eid",t),n}const A="$",g=".content[";function I(e){const t=e.split(g);if(t.shift()!==A)throw new Error(`indexesFromJsonPath: jsonPath didn't start with '${A}'`);return t.map(o=>parseInt(o,10))}function C(e){return e.reduce((t,n)=>`${t}${g}${n}]`,A)}exports.MARKER_OBJECT_PROPS=J;exports.USJ_TYPE=p;exports.USJ_VERSION=N;exports.indexesFromUsjJsonPath=I;exports.isValidBookCode=v;exports.usjJsonPathFromIndexes=C;exports.usjToUsxString=X;exports.usxStringToUsj=M;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/converters/usj/usj.model.ts","../src/converters/usj/usx.model.ts","../src/converters/usj/usx-to-usj.ts","../src/converters/usj/usj-to-usx.ts","../src/converters/usj/jsonpath-indexes.ts"],"sourcesContent":["/**\n * Unified Scripture JSON (USJ) - The JSON variant of USFM and USX data models.\n * These types follow this schema:\n * @see https://github.com/usfm-bible/tcdocs/blob/usj/grammar/usj.js\n */\n\n/** The USJ spec type */\nexport const USJ_TYPE = \"USJ\";\n/** The USJ spec version */\nexport const USJ_VERSION = \"3.1\";\n/** List of known properties of `MarkerObject` */\nexport const MARKER_OBJECT_PROPS: (keyof MarkerObject)[] = [\n \"type\",\n \"marker\",\n \"content\",\n \"sid\",\n \"eid\",\n \"number\",\n \"code\",\n \"altnumber\",\n \"pubnumber\",\n \"caller\",\n \"align\",\n \"category\",\n];\n\n/** Single piece of Scripture content */\nexport type MarkerContent = string | MarkerObject;\n\n/** A Scripture Marker and its contents */\nexport type MarkerObject = {\n /**\n * The kind/category of node or element this is, corresponding the USFM marker and USX node\n * @example `para`, `verse`, `char`\n */\n type: string;\n /**\n * The corresponding marker in USFM or style in USX\n * @example `p`, `v`, `nd`\n */\n marker: string;\n /** This marker's contents laid out in order */\n content?: MarkerContent[];\n /** Indicates the Book-chapter-verse value in the paragraph based structure */\n sid?: string;\n /** Milestone end ID, matches start ID (not currently included in USJ spec) */\n eid?: string;\n /** Chapter number or verse number */\n number?: string;\n /** The 3-letter book code in ID element */\n code?: BookCode;\n /** Alternate chapter number or verse number */\n altnumber?: string;\n /** Published character of chapter or verse */\n pubnumber?: string;\n /** Caller character for footnotes and cross-refs */\n caller?: string;\n /** Alignment of table cells */\n align?: string;\n /** Category of extended study bible sections */\n category?: string;\n};\n\n/** Scripture data represented in JSON format. Data compatible transformation from USX/USFM */\nexport type Usj = {\n /** The USJ spec type */\n type: typeof USJ_TYPE;\n /** The USJ spec version */\n version: typeof USJ_VERSION;\n /** The JSON representation of scripture contents from USFM/USX */\n content: MarkerContent[];\n};\n\nexport function isValidBookCode(code: string): boolean {\n return VALID_BOOK_CODES.includes(code as BookCode);\n}\n\n/** 3-letter Scripture book code */\nexport type BookCode = (typeof VALID_BOOK_CODES)[number];\n\nconst VALID_BOOK_CODES = [\n // Old Testament\n \"GEN\",\n \"EXO\",\n \"LEV\",\n \"NUM\",\n \"DEU\",\n \"JOS\",\n \"JDG\",\n \"RUT\",\n \"1SA\",\n \"2SA\",\n \"1KI\",\n \"2KI\",\n \"1CH\",\n \"2CH\",\n \"EZR\",\n \"NEH\",\n \"EST\",\n \"JOB\",\n \"PSA\",\n \"PRO\",\n \"ECC\",\n \"SNG\",\n \"ISA\",\n \"JER\",\n \"LAM\",\n \"EZK\",\n \"DAN\",\n \"HOS\",\n \"JOL\",\n \"AMO\",\n \"OBA\",\n \"JON\",\n \"MIC\",\n \"NAM\",\n \"HAB\",\n \"ZEP\",\n \"HAG\",\n \"ZEC\",\n \"MAL\",\n // New Testament\n \"MAT\",\n \"MRK\",\n \"LUK\",\n \"JHN\",\n \"ACT\",\n \"ROM\",\n \"1CO\",\n \"2CO\",\n \"GAL\",\n \"EPH\",\n \"PHP\",\n \"COL\",\n \"1TH\",\n \"2TH\",\n \"1TI\",\n \"2TI\",\n \"TIT\",\n \"PHM\",\n \"HEB\",\n \"JAS\",\n \"1PE\",\n \"2PE\",\n \"1JN\",\n \"2JN\",\n \"3JN\",\n \"JUD\",\n \"REV\",\n // Deuterocanon\n \"TOB\",\n \"JDT\",\n \"ESG\",\n \"WIS\",\n \"SIR\",\n \"BAR\",\n \"LJE\",\n \"S3Y\",\n \"SUS\",\n \"BEL\",\n \"1MA\",\n \"2MA\",\n \"3MA\",\n \"4MA\",\n \"1ES\",\n \"2ES\",\n \"MAN\",\n \"PS2\",\n \"ODA\",\n \"PSS\",\n \"EZA\",\n \"5EZ\",\n \"6EZ\",\n \"DAG\",\n \"PS3\",\n \"2BA\",\n \"LBA\",\n \"JUB\",\n \"ENO\",\n \"1MQ\",\n \"2MQ\",\n \"3MQ\",\n \"REP\",\n \"4BA\",\n \"LAO\",\n // Non scripture\n \"FRT\",\n \"BAK\",\n \"OTH\",\n \"INT\",\n \"CNC\",\n \"GLO\",\n \"TDX\",\n \"NDX\",\n \"XXA\",\n \"XXB\",\n \"XXC\",\n \"XXD\",\n \"XXE\",\n \"XXF\",\n \"XXG\",\n] as const;\n","/**\n * Unified Scripture XML (USX).\n * These types follow this schema:\n * @see https://github.com/usfm-bible/tcdocs/blob/main/grammar/usx.rng\n */\n\n/** The USX spec type */\nexport const USX_TYPE = \"usx\";\n/** The USX spec version */\nexport const USX_VERSION = \"3.1\";\n","/**\n * Convert Scripture from USX to USJ.\n * Adapted to TypeScript from this file:\n * @see https://github.com/usfm-bible/usfmtc/blob/0afa385a1f282b286cc6bff7bbc953ae788aa10c/src/usfmtc/usjproc.py\n */\n\nimport { DOMParser, Element } from \"@xmldom/xmldom\";\nimport { MarkerContent, MarkerObject, USJ_TYPE, USJ_VERSION, Usj } from \"./usj.model\";\nimport { USX_TYPE } from \"./usx.model\";\n\ntype Action = \"append\" | \"merge\" | \"ignore\";\ntype Attribs = { [name: string]: string };\n\nexport function usxStringToUsj(usxString: string): Usj {\n const parser = new DOMParser();\n const inputUsxDom = parser.parseFromString(usxString, \"text/xml\");\n return usxDomToUsj(inputUsxDom.documentElement);\n}\n\nexport function usxDomToUsj(inputUsxDom: Element | null): Usj {\n const [outputJson] = inputUsxDom\n ? convertUsxRecurse(inputUsxDom)\n : [{ content: [] as MarkerContent[] } as Usj];\n outputJson.type = USJ_TYPE;\n outputJson.version = USJ_VERSION;\n return outputJson;\n}\n\nfunction convertUsxRecurse<T extends Usj | MarkerObject = Usj>(\n inputUsxElement: Element,\n): [outputJson: T, action: Action] {\n const attribs: Attribs = {};\n let type: string = inputUsxElement.tagName;\n let marker: string | undefined;\n let text: string | undefined;\n let action: Action = \"append\";\n\n if ([\"row\", \"cell\"].includes(type)) type = \"table:\" + type;\n if (inputUsxElement.attributes) {\n for (const attrib of Array.from(inputUsxElement.attributes)) {\n attribs[attrib.name] = attrib.value;\n }\n }\n\n if (attribs.style) {\n marker = attribs.style;\n delete attribs.style;\n }\n // dropping because presence of vid in para elements is not consistent in USX\n if (attribs.vid) delete attribs.vid;\n // Not dropping `attribs.closed` for backwards compatibility.\n // dropping because it is nonstandard derived metadata that could get out of date\n if (attribs.status) delete attribs.status;\n\n let outObj: T = { type } as T;\n if (marker) (outObj as MarkerObject).marker = marker;\n outObj = { ...outObj, ...attribs };\n\n if (\n inputUsxElement.firstChild &&\n inputUsxElement.firstChild.nodeType === inputUsxElement.firstChild.TEXT_NODE &&\n inputUsxElement.firstChild.nodeValue &&\n asciiTrim(inputUsxElement.firstChild.nodeValue) !== \"\"\n ) {\n text = inputUsxElement.firstChild.nodeValue;\n }\n\n const children = Array.from(inputUsxElement.childNodes);\n outObj.content = [];\n\n if (text) {\n outObj.content.push(text);\n }\n\n for (const child of children) {\n // ChildNodes are Elements.\n if ((child as Element).tagName === undefined) {\n continue;\n }\n // ChildNodes are Elements.\n const [childDict, whatToDo] = convertUsxRecurse<MarkerObject>(child as Element);\n\n switch (whatToDo) {\n case \"append\":\n outObj.content.push(childDict);\n break;\n case \"merge\":\n outObj.content = outObj.content.concat(childDict);\n break;\n case \"ignore\":\n break;\n default:\n break;\n }\n\n // Handle tail text\n if (\n child.nextSibling &&\n child.nextSibling.nodeType === child.nextSibling.TEXT_NODE &&\n child.nextSibling.nodeValue &&\n (asciiTrim(child.nextSibling.nodeValue) !== \"\" || child.nextSibling.nodeValue === \" \")\n ) {\n outObj.content.push(child.nextSibling.nodeValue);\n }\n }\n\n // For backward compatibility, not deleting content for type: chapter, verse, optbreak, ms OR\n // marker: va, ca, b.\n if (outObj.content.length === 0 && outObj.type !== USX_TYPE) {\n delete outObj.content;\n }\n\n if (\"eid\" in outObj && [\"verse\", \"chapter\"].includes(type)) {\n action = \"ignore\";\n }\n\n return [outObj, action];\n}\n\n/**\n * Removes leading and trailing ASCII whitespace.\n *\n * Only trim ASCII whitespace characters: space, tab, line feed, carriage return, form feed,\n * vertical tab.\n * @param str - The string to remove whitespace from.\n * @returns the string with leading and trailing whitespace removed.\n */\nfunction asciiTrim(str: string): string {\n return str.replace(/(^[ \\t\\n\\r\\f\\v]+)|([ \\t\\n\\r\\f\\v]+$)/g, \"\");\n}\n","/**\n * Convert Scripture from USJ to USX.\n * Adapted to TypeScript from this file:\n * @see https://github.com/usfm-bible/usfmtc/blob/0afa385a1f282b286cc6bff7bbc953ae788aa10c/src/usfmtc/usjproc.py\n */\n\nimport { DOMImplementation, Document, Element, Text } from \"@xmldom/xmldom\";\nimport { MarkerContent, MarkerObject, Usj } from \"./usj.model\";\nimport { USX_TYPE, USX_VERSION } from \"./usx.model\";\n\nlet chapterEid: string | undefined;\nlet verseEid: string | undefined;\n\nexport function usjToUsxString(usj: Usj): string {\n const usxDoc = new DOMImplementation().createDocument(\"\", USX_TYPE);\n if (usxDoc.documentElement) {\n usxDoc.documentElement.setAttribute(\"version\", USX_VERSION);\n usjToUsxDom(usj, usxDoc);\n }\n return usxDoc.toString();\n}\n\nexport function usjToUsxDom(usj: Usj, usxDoc: Document): Element | undefined {\n if (!usxDoc.documentElement) return undefined;\n\n for (const [index, markerContent] of usj.content.entries()) {\n const isLastItem = index === usj.content.length - 1;\n convertUsjRecurse(markerContent, usxDoc.documentElement, usxDoc, isLastItem);\n }\n return usxDoc.documentElement ?? undefined;\n}\n\nfunction convertUsjRecurse(\n markerContent: MarkerContent,\n parentElement: Element,\n usxDoc: Document,\n isLastItem: boolean,\n) {\n let element: Text | Element;\n let type: string | undefined;\n let eidElement: Element | undefined;\n if (typeof markerContent === \"string\") element = usxDoc.createTextNode(markerContent);\n else {\n type = markerContent.type.replace(\"table:\", \"\");\n element = usxDoc.createElement(type);\n setAttributes(element, markerContent);\n if (markerContent.content) {\n for (const [index, item] of markerContent.content.entries()) {\n const _isLastItem = index === markerContent.content.length - 1;\n convertUsjRecurse(item, element, usxDoc, _isLastItem);\n }\n }\n }\n\n // Create chapter and verse end elements from SID attributes.\n if (verseEid && (type === \"verse\" || (parentElement.tagName === \"para\" && isLastItem))) {\n eidElement = createVerseEndElement(usxDoc, verseEid);\n verseEid = undefined;\n }\n if (type === \"verse\" && typeof markerContent !== \"string\" && markerContent.sid !== undefined)\n verseEid = markerContent.sid;\n\n if (chapterEid && (type === \"chapter\" || (type === \"para\" && isLastItem))) {\n eidElement = createChapterEndElement(usxDoc, chapterEid);\n chapterEid = undefined;\n }\n if (type === \"chapter\" && typeof markerContent !== \"string\" && markerContent.sid !== undefined)\n chapterEid = markerContent.sid;\n\n // Append to parent.\n const isVerseInImpliedPara =\n parentElement.nodeName === USX_TYPE && eidElement?.tagName === \"verse\";\n if (eidElement && (!isLastItem || isVerseInImpliedPara)) parentElement.appendChild(eidElement);\n parentElement.appendChild(element);\n if (eidElement && isLastItem && !isVerseInImpliedPara) parentElement.appendChild(eidElement);\n\n // Allow for final chapter and verse end elements at the end of an implied para.\n if (isLastItem && parentElement.nodeName === USX_TYPE) {\n if (verseEid) parentElement.appendChild(createVerseEndElement(usxDoc, verseEid));\n if (chapterEid) parentElement.appendChild(createChapterEndElement(usxDoc, chapterEid));\n verseEid = undefined;\n chapterEid = undefined;\n }\n}\n\nfunction setAttributes(element: Element, markerContent: MarkerObject) {\n if (markerContent.type === \"unmatched\") element.setAttribute(\"marker\", markerContent.marker);\n else element.setAttribute(\"style\", markerContent.marker);\n for (const [key, value] of Object.entries(markerContent)) {\n if (value && ![\"type\", \"marker\", \"content\"].includes(key)) {\n element.setAttribute(key, value as string);\n }\n }\n}\n\nfunction createVerseEndElement(usxDoc: Document, verseEid: string): Element {\n const eidElement = usxDoc.createElement(\"verse\");\n eidElement.setAttribute(\"eid\", verseEid);\n return eidElement;\n}\n\nfunction createChapterEndElement(usxDoc: Document, chapterEid: string): Element {\n const eidElement = usxDoc.createElement(\"chapter\");\n eidElement.setAttribute(\"eid\", chapterEid);\n return eidElement;\n}\n","const JSON_PATH_START = \"$\";\nconst JSON_PATH_CONTENT = \".content[\";\n\n/**\n * Converts a USJ JSONPath string into an array of indexes.\n *\n * @param jsonPath - The USJ JSONPath string to convert. It must start with `$` and contain `.content[index]` segments.\n * @returns An array of numeric indexes extracted from the JSONPath.\n * @throws Will throw an error if the JSONPath does not start with `$`.\n */\nexport function indexesFromUsjJsonPath(jsonPath: string): number[] {\n const path = jsonPath.split(JSON_PATH_CONTENT);\n if (path.shift() !== JSON_PATH_START)\n throw new Error(`indexesFromJsonPath: jsonPath didn't start with '${JSON_PATH_START}'`);\n\n const indexes = path.map((str) => parseInt(str, 10));\n return indexes;\n}\n\n/**\n * Converts an array of indexes into a USJ JSONPath string.\n *\n * @param indexes - An array of numeric indexes to convert.\n * @returns A USJ JSONPath string constructed from the indexes.\n */\nexport function usjJsonPathFromIndexes(indexes: number[]): string {\n return indexes.reduce((path, index) => `${path}${JSON_PATH_CONTENT}${index}]`, JSON_PATH_START);\n}\n"],"names":["USJ_TYPE","USJ_VERSION","MARKER_OBJECT_PROPS","isValidBookCode","code","VALID_BOOK_CODES","USX_TYPE","USX_VERSION","usxStringToUsj","usxString","inputUsxDom","DOMParser","usxDomToUsj","outputJson","convertUsxRecurse","inputUsxElement","attribs","type","marker","text","action","attrib","outObj","asciiTrim","children","child","childDict","whatToDo","str","chapterEid","verseEid","usjToUsxString","usj","usxDoc","DOMImplementation","usjToUsxDom","index","markerContent","isLastItem","convertUsjRecurse","parentElement","element","eidElement","setAttributes","item","_isLastItem","createVerseEndElement","createChapterEndElement","isVerseInImpliedPara","key","value","JSON_PATH_START","JSON_PATH_CONTENT","indexesFromUsjJsonPath","jsonPath","path","usjJsonPathFromIndexes","indexes"],"mappings":"kHAOaA,EAAW,MAEXC,EAAc,MAEdC,EAA8C,CACzD,OACA,SACA,UACA,MACA,MACA,SACA,OACA,YACA,YACA,SACA,QACA,UACF,EAiDO,SAASC,EAAgBC,EAAuB,CACrD,OAAOC,EAAiB,SAASD,CAAgB,CACnD,CAKA,MAAMC,EAAmB,CAEvB,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MAEA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MAEA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MAEA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,MACA,KACF,EClMaC,EAAW,MAEXC,EAAc,MCIpB,SAASC,EAAeC,EAAwB,CAErD,MAAMC,EADS,IAAIC,YAAA,EACQ,gBAAgBF,EAAW,UAAU,EAChE,OAAOG,EAAYF,EAAY,eAAe,CAChD,CAEO,SAASE,EAAYF,EAAkC,CAC5D,KAAM,CAACG,CAAU,EAAIH,EACjBI,EAAkBJ,CAAW,EAC7B,CAAC,CAAE,QAAS,CAAA,EAA8B,EAC9C,OAAAG,EAAW,KAAOb,EAClBa,EAAW,QAAUZ,EACdY,CACT,CAEA,SAASC,EACPC,EACiC,CACjC,MAAMC,EAAmB,CAAA,EACzB,IAAIC,EAAeF,EAAgB,QAC/BG,EACAC,EACAC,EAAiB,SAGrB,GADI,CAAC,MAAO,MAAM,EAAE,SAASH,CAAI,MAAU,SAAWA,GAClDF,EAAgB,WAClB,UAAWM,KAAU,MAAM,KAAKN,EAAgB,UAAU,EACxDC,EAAQK,EAAO,IAAI,EAAIA,EAAO,MAI9BL,EAAQ,QACVE,EAASF,EAAQ,MACjB,OAAOA,EAAQ,OAGbA,EAAQ,KAAK,OAAOA,EAAQ,IAG5BA,EAAQ,QAAQ,OAAOA,EAAQ,OAEnC,IAAIM,EAAY,CAAE,KAAAL,CAAA,EACdC,IAASI,EAAwB,OAASJ,GAC9CI,EAAS,CAAE,GAAGA,EAAQ,GAAGN,CAAA,EAGvBD,EAAgB,YAChBA,EAAgB,WAAW,WAAaA,EAAgB,WAAW,WACnEA,EAAgB,WAAW,WAC3BQ,EAAUR,EAAgB,WAAW,SAAS,IAAM,KAEpDI,EAAOJ,EAAgB,WAAW,WAGpC,MAAMS,EAAW,MAAM,KAAKT,EAAgB,UAAU,EACtDO,EAAO,QAAU,CAAA,EAEbH,GACFG,EAAO,QAAQ,KAAKH,CAAI,EAG1B,UAAWM,KAASD,EAAU,CAE5B,GAAKC,EAAkB,UAAY,OACjC,SAGF,KAAM,CAACC,EAAWC,CAAQ,EAAIb,EAAgCW,CAAgB,EAE9E,OAAQE,EAAA,CACN,IAAK,SACHL,EAAO,QAAQ,KAAKI,CAAS,EAC7B,MACF,IAAK,QACHJ,EAAO,QAAUA,EAAO,QAAQ,OAAOI,CAAS,EAChD,KAIA,CAKFD,EAAM,aACNA,EAAM,YAAY,WAAaA,EAAM,YAAY,WACjDA,EAAM,YAAY,YACjBF,EAAUE,EAAM,YAAY,SAAS,IAAM,IAAMA,EAAM,YAAY,YAAc,MAElFH,EAAO,QAAQ,KAAKG,EAAM,YAAY,SAAS,CACjD,CAKF,OAAIH,EAAO,QAAQ,SAAW,GAAKA,EAAO,OAAShB,GACjD,OAAOgB,EAAO,QAGZ,QAASA,GAAU,CAAC,QAAS,SAAS,EAAE,SAASL,CAAI,IACvDG,EAAS,UAGJ,CAACE,EAAQF,CAAM,CACxB,CAUA,SAASG,EAAUK,EAAqB,CACtC,OAAOA,EAAI,QAAQ,uCAAwC,EAAE,CAC/D,CCvHA,IAAIC,EACAC,EAEG,SAASC,EAAeC,EAAkB,CAC/C,MAAMC,EAAS,IAAIC,EAAAA,kBAAA,EAAoB,eAAe,GAAI5B,CAAQ,EAClE,OAAI2B,EAAO,kBACTA,EAAO,gBAAgB,aAAa,UAAW1B,CAAW,EAC1D4B,EAAYH,EAAKC,CAAM,GAElBA,EAAO,SAAA,CAChB,CAEO,SAASE,EAAYH,EAAUC,EAAuC,CAC3E,GAAKA,EAAO,gBAEZ,UAAW,CAACG,EAAOC,CAAa,IAAKL,EAAI,QAAQ,UAAW,CAC1D,MAAMM,EAAaF,IAAUJ,EAAI,QAAQ,OAAS,EAClDO,EAAkBF,EAAeJ,EAAO,gBAAiBA,EAAQK,CAAU,CAAA,CAE7E,OAAOL,EAAO,iBAAmB,OACnC,CAEA,SAASM,EACPF,EACAG,EACAP,EACAK,EACA,CACA,IAAIG,EACAxB,EACAyB,EACJ,GAAI,OAAOL,GAAkB,SAAUI,EAAUR,EAAO,eAAeI,CAAa,UAElFpB,EAAOoB,EAAc,KAAK,QAAQ,SAAU,EAAE,EAC9CI,EAAUR,EAAO,cAAchB,CAAI,EACnC0B,EAAcF,EAASJ,CAAa,EAChCA,EAAc,QAChB,SAAW,CAACD,EAAOQ,CAAI,IAAKP,EAAc,QAAQ,UAAW,CAC3D,MAAMQ,EAAcT,IAAUC,EAAc,QAAQ,OAAS,EAC7DE,EAAkBK,EAAMH,EAASR,EAAQY,CAAW,CAAA,CAMtDf,IAAab,IAAS,SAAYuB,EAAc,UAAY,QAAUF,KACxEI,EAAaI,EAAsBb,EAAQH,CAAQ,EACnDA,EAAW,QAETb,IAAS,SAAW,OAAOoB,GAAkB,UAAYA,EAAc,MAAQ,SACjFP,EAAWO,EAAc,KAEvBR,IAAeZ,IAAS,WAAcA,IAAS,QAAUqB,KAC3DI,EAAaK,EAAwBd,EAAQJ,CAAU,EACvDA,EAAa,QAEXZ,IAAS,WAAa,OAAOoB,GAAkB,UAAYA,EAAc,MAAQ,SACnFR,EAAaQ,EAAc,KAG7B,MAAMW,EACJR,EAAc,WAAalC,IAAYoC,GAAA,YAAAA,EAAY,WAAY,QAC7DA,IAAe,CAACJ,GAAcU,IAAuBR,EAAc,YAAYE,CAAU,EAC7FF,EAAc,YAAYC,CAAO,EAC7BC,GAAcJ,GAAc,CAACU,GAAsBR,EAAc,YAAYE,CAAU,EAGvFJ,GAAcE,EAAc,WAAalC,IACvCwB,GAAUU,EAAc,YAAYM,EAAsBb,EAAQH,CAAQ,CAAC,EAC3ED,GAAYW,EAAc,YAAYO,EAAwBd,EAAQJ,CAAU,CAAC,EACrFC,EAAW,OACXD,EAAa,OAEjB,CAEA,SAASc,EAAcF,EAAkBJ,EAA6B,CAChEA,EAAc,OAAS,cAAqB,aAAa,SAAUA,EAAc,MAAM,EACtFI,EAAQ,aAAa,QAASJ,EAAc,MAAM,EACvD,SAAW,CAACY,EAAKC,CAAK,IAAK,OAAO,QAAQb,CAAa,EACjDa,GAAS,CAAC,CAAC,OAAQ,SAAU,SAAS,EAAE,SAASD,CAAG,GACtDR,EAAQ,aAAaQ,EAAKC,CAAe,CAG/C,CAEA,SAASJ,EAAsBb,EAAkBH,EAA2B,CAC1E,MAAMY,EAAaT,EAAO,cAAc,OAAO,EAC/C,OAAAS,EAAW,aAAa,MAAOZ,CAAQ,EAChCY,CACT,CAEA,SAASK,EAAwBd,EAAkBJ,EAA6B,CAC9E,MAAMa,EAAaT,EAAO,cAAc,SAAS,EACjD,OAAAS,EAAW,aAAa,MAAOb,CAAU,EAClCa,CACT,CCzGA,MAAMS,EAAkB,IAClBC,EAAoB,YASnB,SAASC,EAAuBC,EAA4B,CACjE,MAAMC,EAAOD,EAAS,MAAMF,CAAiB,EAC7C,GAAIG,EAAK,UAAYJ,EACnB,MAAM,IAAI,MAAM,oDAAoDA,CAAe,GAAG,EAGxF,OADgBI,EAAK,IAAK3B,GAAQ,SAASA,EAAK,EAAE,CAAC,CAErD,CAQO,SAAS4B,EAAuBC,EAA2B,CAChE,OAAOA,EAAQ,OAAO,CAACF,EAAMnB,IAAU,GAAGmB,CAAI,GAAGH,CAAiB,GAAGhB,CAAK,IAAKe,CAAe,CAChG"}
@@ -0,0 +1,5 @@
1
+ export type { Usj, BookCode, MarkerContent, MarkerObject } from './converters/usj/usj.model';
2
+ export { MARKER_OBJECT_PROPS, USJ_TYPE, USJ_VERSION, isValidBookCode, } from './converters/usj/usj.model';
3
+ export { usxStringToUsj } from './converters/usj/usx-to-usj';
4
+ export { usjToUsxString } from './converters/usj/usj-to-usx';
5
+ export { indexesFromUsjJsonPath, usjJsonPathFromIndexes } from './converters/usj/jsonpath-indexes';
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ import { DOMParser as b, DOMImplementation as g } from "@xmldom/xmldom";
2
+ const v = "USJ", J = "3.1", B = [
3
+ "type",
4
+ "marker",
5
+ "content",
6
+ "sid",
7
+ "eid",
8
+ "number",
9
+ "code",
10
+ "altnumber",
11
+ "pubnumber",
12
+ "caller",
13
+ "align",
14
+ "category"
15
+ ];
16
+ function H(e) {
17
+ return P.includes(e);
18
+ }
19
+ const P = [
20
+ // Old Testament
21
+ "GEN",
22
+ "EXO",
23
+ "LEV",
24
+ "NUM",
25
+ "DEU",
26
+ "JOS",
27
+ "JDG",
28
+ "RUT",
29
+ "1SA",
30
+ "2SA",
31
+ "1KI",
32
+ "2KI",
33
+ "1CH",
34
+ "2CH",
35
+ "EZR",
36
+ "NEH",
37
+ "EST",
38
+ "JOB",
39
+ "PSA",
40
+ "PRO",
41
+ "ECC",
42
+ "SNG",
43
+ "ISA",
44
+ "JER",
45
+ "LAM",
46
+ "EZK",
47
+ "DAN",
48
+ "HOS",
49
+ "JOL",
50
+ "AMO",
51
+ "OBA",
52
+ "JON",
53
+ "MIC",
54
+ "NAM",
55
+ "HAB",
56
+ "ZEP",
57
+ "HAG",
58
+ "ZEC",
59
+ "MAL",
60
+ // New Testament
61
+ "MAT",
62
+ "MRK",
63
+ "LUK",
64
+ "JHN",
65
+ "ACT",
66
+ "ROM",
67
+ "1CO",
68
+ "2CO",
69
+ "GAL",
70
+ "EPH",
71
+ "PHP",
72
+ "COL",
73
+ "1TH",
74
+ "2TH",
75
+ "1TI",
76
+ "2TI",
77
+ "TIT",
78
+ "PHM",
79
+ "HEB",
80
+ "JAS",
81
+ "1PE",
82
+ "2PE",
83
+ "1JN",
84
+ "2JN",
85
+ "3JN",
86
+ "JUD",
87
+ "REV",
88
+ // Deuterocanon
89
+ "TOB",
90
+ "JDT",
91
+ "ESG",
92
+ "WIS",
93
+ "SIR",
94
+ "BAR",
95
+ "LJE",
96
+ "S3Y",
97
+ "SUS",
98
+ "BEL",
99
+ "1MA",
100
+ "2MA",
101
+ "3MA",
102
+ "4MA",
103
+ "1ES",
104
+ "2ES",
105
+ "MAN",
106
+ "PS2",
107
+ "ODA",
108
+ "PSS",
109
+ "EZA",
110
+ "5EZ",
111
+ "6EZ",
112
+ "DAG",
113
+ "PS3",
114
+ "2BA",
115
+ "LBA",
116
+ "JUB",
117
+ "ENO",
118
+ "1MQ",
119
+ "2MQ",
120
+ "3MQ",
121
+ "REP",
122
+ "4BA",
123
+ "LAO",
124
+ // Non scripture
125
+ "FRT",
126
+ "BAK",
127
+ "OTH",
128
+ "INT",
129
+ "CNC",
130
+ "GLO",
131
+ "TDX",
132
+ "NDX",
133
+ "XXA",
134
+ "XXB",
135
+ "XXC",
136
+ "XXD",
137
+ "XXE",
138
+ "XXF",
139
+ "XXG"
140
+ ], u = "usx", m = "3.1";
141
+ function I(e) {
142
+ const n = new b().parseFromString(e, "text/xml");
143
+ return y(n.documentElement);
144
+ }
145
+ function y(e) {
146
+ const [t] = e ? p(e) : [{ content: [] }];
147
+ return t.type = v, t.version = J, t;
148
+ }
149
+ function p(e) {
150
+ const t = {};
151
+ let n = e.tagName, o, s, c = "append";
152
+ if (["row", "cell"].includes(n) && (n = "table:" + n), e.attributes)
153
+ for (const r of Array.from(e.attributes))
154
+ t[r.name] = r.value;
155
+ t.style && (o = t.style, delete t.style), t.vid && delete t.vid, t.status && delete t.status;
156
+ let i = { type: n };
157
+ o && (i.marker = o), i = { ...i, ...t }, e.firstChild && e.firstChild.nodeType === e.firstChild.TEXT_NODE && e.firstChild.nodeValue && E(e.firstChild.nodeValue) !== "" && (s = e.firstChild.nodeValue);
158
+ const l = Array.from(e.childNodes);
159
+ i.content = [], s && i.content.push(s);
160
+ for (const r of l) {
161
+ if (r.tagName === void 0)
162
+ continue;
163
+ const [f, A] = p(r);
164
+ switch (A) {
165
+ case "append":
166
+ i.content.push(f);
167
+ break;
168
+ case "merge":
169
+ i.content = i.content.concat(f);
170
+ break;
171
+ }
172
+ r.nextSibling && r.nextSibling.nodeType === r.nextSibling.TEXT_NODE && r.nextSibling.nodeValue && (E(r.nextSibling.nodeValue) !== "" || r.nextSibling.nodeValue === " ") && i.content.push(r.nextSibling.nodeValue);
173
+ }
174
+ return i.content.length === 0 && i.type !== u && delete i.content, "eid" in i && ["verse", "chapter"].includes(n) && (c = "ignore"), [i, c];
175
+ }
176
+ function E(e) {
177
+ return e.replace(/(^[ \t\n\r\f\v]+)|([ \t\n\r\f\v]+$)/g, "");
178
+ }
179
+ let d, a;
180
+ function C(e) {
181
+ const t = new g().createDocument("", u);
182
+ return t.documentElement && (t.documentElement.setAttribute("version", m), M(e, t)), t.toString();
183
+ }
184
+ function M(e, t) {
185
+ if (t.documentElement) {
186
+ for (const [n, o] of e.content.entries()) {
187
+ const s = n === e.content.length - 1;
188
+ O(o, t.documentElement, t, s);
189
+ }
190
+ return t.documentElement ?? void 0;
191
+ }
192
+ }
193
+ function O(e, t, n, o) {
194
+ let s, c, i;
195
+ if (typeof e == "string") s = n.createTextNode(e);
196
+ else if (c = e.type.replace("table:", ""), s = n.createElement(c), X(s, e), e.content)
197
+ for (const [r, f] of e.content.entries()) {
198
+ const A = r === e.content.length - 1;
199
+ O(f, s, n, A);
200
+ }
201
+ a && (c === "verse" || t.tagName === "para" && o) && (i = h(n, a), a = void 0), c === "verse" && typeof e != "string" && e.sid !== void 0 && (a = e.sid), d && (c === "chapter" || c === "para" && o) && (i = T(n, d), d = void 0), c === "chapter" && typeof e != "string" && e.sid !== void 0 && (d = e.sid);
202
+ const l = t.nodeName === u && (i == null ? void 0 : i.tagName) === "verse";
203
+ i && (!o || l) && t.appendChild(i), t.appendChild(s), i && o && !l && t.appendChild(i), o && t.nodeName === u && (a && t.appendChild(h(n, a)), d && t.appendChild(T(n, d)), a = void 0, d = void 0);
204
+ }
205
+ function X(e, t) {
206
+ t.type === "unmatched" ? e.setAttribute("marker", t.marker) : e.setAttribute("style", t.marker);
207
+ for (const [n, o] of Object.entries(t))
208
+ o && !["type", "marker", "content"].includes(n) && e.setAttribute(n, o);
209
+ }
210
+ function h(e, t) {
211
+ const n = e.createElement("verse");
212
+ return n.setAttribute("eid", t), n;
213
+ }
214
+ function T(e, t) {
215
+ const n = e.createElement("chapter");
216
+ return n.setAttribute("eid", t), n;
217
+ }
218
+ const S = "$", N = ".content[";
219
+ function V(e) {
220
+ const t = e.split(N);
221
+ if (t.shift() !== S)
222
+ throw new Error(`indexesFromJsonPath: jsonPath didn't start with '${S}'`);
223
+ return t.map((o) => parseInt(o, 10));
224
+ }
225
+ function _(e) {
226
+ return e.reduce((t, n) => `${t}${N}${n}]`, S);
227
+ }
228
+ export {
229
+ B as MARKER_OBJECT_PROPS,
230
+ v as USJ_TYPE,
231
+ J as USJ_VERSION,
232
+ V as indexesFromUsjJsonPath,
233
+ H as isValidBookCode,
234
+ _ as usjJsonPathFromIndexes,
235
+ C as usjToUsxString,
236
+ I as usxStringToUsj
237
+ };
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/converters/usj/usj.model.ts","../src/converters/usj/usx.model.ts","../src/converters/usj/usx-to-usj.ts","../src/converters/usj/usj-to-usx.ts","../src/converters/usj/jsonpath-indexes.ts"],"sourcesContent":["/**\n * Unified Scripture JSON (USJ) - The JSON variant of USFM and USX data models.\n * These types follow this schema:\n * @see https://github.com/usfm-bible/tcdocs/blob/usj/grammar/usj.js\n */\n\n/** The USJ spec type */\nexport const USJ_TYPE = \"USJ\";\n/** The USJ spec version */\nexport const USJ_VERSION = \"3.1\";\n/** List of known properties of `MarkerObject` */\nexport const MARKER_OBJECT_PROPS: (keyof MarkerObject)[] = [\n \"type\",\n \"marker\",\n \"content\",\n \"sid\",\n \"eid\",\n \"number\",\n \"code\",\n \"altnumber\",\n \"pubnumber\",\n \"caller\",\n \"align\",\n \"category\",\n];\n\n/** Single piece of Scripture content */\nexport type MarkerContent = string | MarkerObject;\n\n/** A Scripture Marker and its contents */\nexport type MarkerObject = {\n /**\n * The kind/category of node or element this is, corresponding the USFM marker and USX node\n * @example `para`, `verse`, `char`\n */\n type: string;\n /**\n * The corresponding marker in USFM or style in USX\n * @example `p`, `v`, `nd`\n */\n marker: string;\n /** This marker's contents laid out in order */\n content?: MarkerContent[];\n /** Indicates the Book-chapter-verse value in the paragraph based structure */\n sid?: string;\n /** Milestone end ID, matches start ID (not currently included in USJ spec) */\n eid?: string;\n /** Chapter number or verse number */\n number?: string;\n /** The 3-letter book code in ID element */\n code?: BookCode;\n /** Alternate chapter number or verse number */\n altnumber?: string;\n /** Published character of chapter or verse */\n pubnumber?: string;\n /** Caller character for footnotes and cross-refs */\n caller?: string;\n /** Alignment of table cells */\n align?: string;\n /** Category of extended study bible sections */\n category?: string;\n};\n\n/** Scripture data represented in JSON format. Data compatible transformation from USX/USFM */\nexport type Usj = {\n /** The USJ spec type */\n type: typeof USJ_TYPE;\n /** The USJ spec version */\n version: typeof USJ_VERSION;\n /** The JSON representation of scripture contents from USFM/USX */\n content: MarkerContent[];\n};\n\nexport function isValidBookCode(code: string): boolean {\n return VALID_BOOK_CODES.includes(code as BookCode);\n}\n\n/** 3-letter Scripture book code */\nexport type BookCode = (typeof VALID_BOOK_CODES)[number];\n\nconst VALID_BOOK_CODES = [\n // Old Testament\n \"GEN\",\n \"EXO\",\n \"LEV\",\n \"NUM\",\n \"DEU\",\n \"JOS\",\n \"JDG\",\n \"RUT\",\n \"1SA\",\n \"2SA\",\n \"1KI\",\n \"2KI\",\n \"1CH\",\n \"2CH\",\n \"EZR\",\n \"NEH\",\n \"EST\",\n \"JOB\",\n \"PSA\",\n \"PRO\",\n \"ECC\",\n \"SNG\",\n \"ISA\",\n \"JER\",\n \"LAM\",\n \"EZK\",\n \"DAN\",\n \"HOS\",\n \"JOL\",\n \"AMO\",\n \"OBA\",\n \"JON\",\n \"MIC\",\n \"NAM\",\n \"HAB\",\n \"ZEP\",\n \"HAG\",\n \"ZEC\",\n \"MAL\",\n // New Testament\n \"MAT\",\n \"MRK\",\n \"LUK\",\n \"JHN\",\n \"ACT\",\n \"ROM\",\n \"1CO\",\n \"2CO\",\n \"GAL\",\n \"EPH\",\n \"PHP\",\n \"COL\",\n \"1TH\",\n \"2TH\",\n \"1TI\",\n \"2TI\",\n \"TIT\",\n \"PHM\",\n \"HEB\",\n \"JAS\",\n \"1PE\",\n \"2PE\",\n \"1JN\",\n \"2JN\",\n \"3JN\",\n \"JUD\",\n \"REV\",\n // Deuterocanon\n \"TOB\",\n \"JDT\",\n \"ESG\",\n \"WIS\",\n \"SIR\",\n \"BAR\",\n \"LJE\",\n \"S3Y\",\n \"SUS\",\n \"BEL\",\n \"1MA\",\n \"2MA\",\n \"3MA\",\n \"4MA\",\n \"1ES\",\n \"2ES\",\n \"MAN\",\n \"PS2\",\n \"ODA\",\n \"PSS\",\n \"EZA\",\n \"5EZ\",\n \"6EZ\",\n \"DAG\",\n \"PS3\",\n \"2BA\",\n \"LBA\",\n \"JUB\",\n \"ENO\",\n \"1MQ\",\n \"2MQ\",\n \"3MQ\",\n \"REP\",\n \"4BA\",\n \"LAO\",\n // Non scripture\n \"FRT\",\n \"BAK\",\n \"OTH\",\n \"INT\",\n \"CNC\",\n \"GLO\",\n \"TDX\",\n \"NDX\",\n \"XXA\",\n \"XXB\",\n \"XXC\",\n \"XXD\",\n \"XXE\",\n \"XXF\",\n \"XXG\",\n] as const;\n","/**\n * Unified Scripture XML (USX).\n * These types follow this schema:\n * @see https://github.com/usfm-bible/tcdocs/blob/main/grammar/usx.rng\n */\n\n/** The USX spec type */\nexport const USX_TYPE = \"usx\";\n/** The USX spec version */\nexport const USX_VERSION = \"3.1\";\n","/**\n * Convert Scripture from USX to USJ.\n * Adapted to TypeScript from this file:\n * @see https://github.com/usfm-bible/usfmtc/blob/0afa385a1f282b286cc6bff7bbc953ae788aa10c/src/usfmtc/usjproc.py\n */\n\nimport { DOMParser, Element } from \"@xmldom/xmldom\";\nimport { MarkerContent, MarkerObject, USJ_TYPE, USJ_VERSION, Usj } from \"./usj.model\";\nimport { USX_TYPE } from \"./usx.model\";\n\ntype Action = \"append\" | \"merge\" | \"ignore\";\ntype Attribs = { [name: string]: string };\n\nexport function usxStringToUsj(usxString: string): Usj {\n const parser = new DOMParser();\n const inputUsxDom = parser.parseFromString(usxString, \"text/xml\");\n return usxDomToUsj(inputUsxDom.documentElement);\n}\n\nexport function usxDomToUsj(inputUsxDom: Element | null): Usj {\n const [outputJson] = inputUsxDom\n ? convertUsxRecurse(inputUsxDom)\n : [{ content: [] as MarkerContent[] } as Usj];\n outputJson.type = USJ_TYPE;\n outputJson.version = USJ_VERSION;\n return outputJson;\n}\n\nfunction convertUsxRecurse<T extends Usj | MarkerObject = Usj>(\n inputUsxElement: Element,\n): [outputJson: T, action: Action] {\n const attribs: Attribs = {};\n let type: string = inputUsxElement.tagName;\n let marker: string | undefined;\n let text: string | undefined;\n let action: Action = \"append\";\n\n if ([\"row\", \"cell\"].includes(type)) type = \"table:\" + type;\n if (inputUsxElement.attributes) {\n for (const attrib of Array.from(inputUsxElement.attributes)) {\n attribs[attrib.name] = attrib.value;\n }\n }\n\n if (attribs.style) {\n marker = attribs.style;\n delete attribs.style;\n }\n // dropping because presence of vid in para elements is not consistent in USX\n if (attribs.vid) delete attribs.vid;\n // Not dropping `attribs.closed` for backwards compatibility.\n // dropping because it is nonstandard derived metadata that could get out of date\n if (attribs.status) delete attribs.status;\n\n let outObj: T = { type } as T;\n if (marker) (outObj as MarkerObject).marker = marker;\n outObj = { ...outObj, ...attribs };\n\n if (\n inputUsxElement.firstChild &&\n inputUsxElement.firstChild.nodeType === inputUsxElement.firstChild.TEXT_NODE &&\n inputUsxElement.firstChild.nodeValue &&\n asciiTrim(inputUsxElement.firstChild.nodeValue) !== \"\"\n ) {\n text = inputUsxElement.firstChild.nodeValue;\n }\n\n const children = Array.from(inputUsxElement.childNodes);\n outObj.content = [];\n\n if (text) {\n outObj.content.push(text);\n }\n\n for (const child of children) {\n // ChildNodes are Elements.\n if ((child as Element).tagName === undefined) {\n continue;\n }\n // ChildNodes are Elements.\n const [childDict, whatToDo] = convertUsxRecurse<MarkerObject>(child as Element);\n\n switch (whatToDo) {\n case \"append\":\n outObj.content.push(childDict);\n break;\n case \"merge\":\n outObj.content = outObj.content.concat(childDict);\n break;\n case \"ignore\":\n break;\n default:\n break;\n }\n\n // Handle tail text\n if (\n child.nextSibling &&\n child.nextSibling.nodeType === child.nextSibling.TEXT_NODE &&\n child.nextSibling.nodeValue &&\n (asciiTrim(child.nextSibling.nodeValue) !== \"\" || child.nextSibling.nodeValue === \" \")\n ) {\n outObj.content.push(child.nextSibling.nodeValue);\n }\n }\n\n // For backward compatibility, not deleting content for type: chapter, verse, optbreak, ms OR\n // marker: va, ca, b.\n if (outObj.content.length === 0 && outObj.type !== USX_TYPE) {\n delete outObj.content;\n }\n\n if (\"eid\" in outObj && [\"verse\", \"chapter\"].includes(type)) {\n action = \"ignore\";\n }\n\n return [outObj, action];\n}\n\n/**\n * Removes leading and trailing ASCII whitespace.\n *\n * Only trim ASCII whitespace characters: space, tab, line feed, carriage return, form feed,\n * vertical tab.\n * @param str - The string to remove whitespace from.\n * @returns the string with leading and trailing whitespace removed.\n */\nfunction asciiTrim(str: string): string {\n return str.replace(/(^[ \\t\\n\\r\\f\\v]+)|([ \\t\\n\\r\\f\\v]+$)/g, \"\");\n}\n","/**\n * Convert Scripture from USJ to USX.\n * Adapted to TypeScript from this file:\n * @see https://github.com/usfm-bible/usfmtc/blob/0afa385a1f282b286cc6bff7bbc953ae788aa10c/src/usfmtc/usjproc.py\n */\n\nimport { DOMImplementation, Document, Element, Text } from \"@xmldom/xmldom\";\nimport { MarkerContent, MarkerObject, Usj } from \"./usj.model\";\nimport { USX_TYPE, USX_VERSION } from \"./usx.model\";\n\nlet chapterEid: string | undefined;\nlet verseEid: string | undefined;\n\nexport function usjToUsxString(usj: Usj): string {\n const usxDoc = new DOMImplementation().createDocument(\"\", USX_TYPE);\n if (usxDoc.documentElement) {\n usxDoc.documentElement.setAttribute(\"version\", USX_VERSION);\n usjToUsxDom(usj, usxDoc);\n }\n return usxDoc.toString();\n}\n\nexport function usjToUsxDom(usj: Usj, usxDoc: Document): Element | undefined {\n if (!usxDoc.documentElement) return undefined;\n\n for (const [index, markerContent] of usj.content.entries()) {\n const isLastItem = index === usj.content.length - 1;\n convertUsjRecurse(markerContent, usxDoc.documentElement, usxDoc, isLastItem);\n }\n return usxDoc.documentElement ?? undefined;\n}\n\nfunction convertUsjRecurse(\n markerContent: MarkerContent,\n parentElement: Element,\n usxDoc: Document,\n isLastItem: boolean,\n) {\n let element: Text | Element;\n let type: string | undefined;\n let eidElement: Element | undefined;\n if (typeof markerContent === \"string\") element = usxDoc.createTextNode(markerContent);\n else {\n type = markerContent.type.replace(\"table:\", \"\");\n element = usxDoc.createElement(type);\n setAttributes(element, markerContent);\n if (markerContent.content) {\n for (const [index, item] of markerContent.content.entries()) {\n const _isLastItem = index === markerContent.content.length - 1;\n convertUsjRecurse(item, element, usxDoc, _isLastItem);\n }\n }\n }\n\n // Create chapter and verse end elements from SID attributes.\n if (verseEid && (type === \"verse\" || (parentElement.tagName === \"para\" && isLastItem))) {\n eidElement = createVerseEndElement(usxDoc, verseEid);\n verseEid = undefined;\n }\n if (type === \"verse\" && typeof markerContent !== \"string\" && markerContent.sid !== undefined)\n verseEid = markerContent.sid;\n\n if (chapterEid && (type === \"chapter\" || (type === \"para\" && isLastItem))) {\n eidElement = createChapterEndElement(usxDoc, chapterEid);\n chapterEid = undefined;\n }\n if (type === \"chapter\" && typeof markerContent !== \"string\" && markerContent.sid !== undefined)\n chapterEid = markerContent.sid;\n\n // Append to parent.\n const isVerseInImpliedPara =\n parentElement.nodeName === USX_TYPE && eidElement?.tagName === \"verse\";\n if (eidElement && (!isLastItem || isVerseInImpliedPara)) parentElement.appendChild(eidElement);\n parentElement.appendChild(element);\n if (eidElement && isLastItem && !isVerseInImpliedPara) parentElement.appendChild(eidElement);\n\n // Allow for final chapter and verse end elements at the end of an implied para.\n if (isLastItem && parentElement.nodeName === USX_TYPE) {\n if (verseEid) parentElement.appendChild(createVerseEndElement(usxDoc, verseEid));\n if (chapterEid) parentElement.appendChild(createChapterEndElement(usxDoc, chapterEid));\n verseEid = undefined;\n chapterEid = undefined;\n }\n}\n\nfunction setAttributes(element: Element, markerContent: MarkerObject) {\n if (markerContent.type === \"unmatched\") element.setAttribute(\"marker\", markerContent.marker);\n else element.setAttribute(\"style\", markerContent.marker);\n for (const [key, value] of Object.entries(markerContent)) {\n if (value && ![\"type\", \"marker\", \"content\"].includes(key)) {\n element.setAttribute(key, value as string);\n }\n }\n}\n\nfunction createVerseEndElement(usxDoc: Document, verseEid: string): Element {\n const eidElement = usxDoc.createElement(\"verse\");\n eidElement.setAttribute(\"eid\", verseEid);\n return eidElement;\n}\n\nfunction createChapterEndElement(usxDoc: Document, chapterEid: string): Element {\n const eidElement = usxDoc.createElement(\"chapter\");\n eidElement.setAttribute(\"eid\", chapterEid);\n return eidElement;\n}\n","const JSON_PATH_START = \"$\";\nconst JSON_PATH_CONTENT = \".content[\";\n\n/**\n * Converts a USJ JSONPath string into an array of indexes.\n *\n * @param jsonPath - The USJ JSONPath string to convert. It must start with `$` and contain `.content[index]` segments.\n * @returns An array of numeric indexes extracted from the JSONPath.\n * @throws Will throw an error if the JSONPath does not start with `$`.\n */\nexport function indexesFromUsjJsonPath(jsonPath: string): number[] {\n const path = jsonPath.split(JSON_PATH_CONTENT);\n if (path.shift() !== JSON_PATH_START)\n throw new Error(`indexesFromJsonPath: jsonPath didn't start with '${JSON_PATH_START}'`);\n\n const indexes = path.map((str) => parseInt(str, 10));\n return indexes;\n}\n\n/**\n * Converts an array of indexes into a USJ JSONPath string.\n *\n * @param indexes - An array of numeric indexes to convert.\n * @returns A USJ JSONPath string constructed from the indexes.\n */\nexport function usjJsonPathFromIndexes(indexes: number[]): string {\n return indexes.reduce((path, index) => `${path}${JSON_PATH_CONTENT}${index}]`, JSON_PATH_START);\n}\n"],"names":["USJ_TYPE","USJ_VERSION","MARKER_OBJECT_PROPS","isValidBookCode","code","VALID_BOOK_CODES","USX_TYPE","USX_VERSION","usxStringToUsj","usxString","inputUsxDom","DOMParser","usxDomToUsj","outputJson","convertUsxRecurse","inputUsxElement","attribs","type","marker","text","action","attrib","outObj","asciiTrim","children","child","childDict","whatToDo","str","chapterEid","verseEid","usjToUsxString","usj","usxDoc","DOMImplementation","usjToUsxDom","index","markerContent","isLastItem","convertUsjRecurse","parentElement","element","eidElement","setAttributes","item","_isLastItem","createVerseEndElement","createChapterEndElement","isVerseInImpliedPara","key","value","JSON_PATH_START","JSON_PATH_CONTENT","indexesFromUsjJsonPath","jsonPath","path","usjJsonPathFromIndexes","indexes"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@eten-tech-foundation/scripture-utilities",
3
+ "version": "0.1.1",
4
+ "description": "Utilities for working with Scripture data.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/eten-tech-foundation/scripture-editors/tree/main/packages/utilities#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/eten-tech-foundation/scripture-editors.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/eten-tech-foundation/scripture-editors/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "dist/index.cjs",
16
+ "module": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!dist/package.json",
28
+ "src",
29
+ "!src/**/*.test.ts",
30
+ "!src/**/*.data.ts"
31
+ ],
32
+ "dependencies": {
33
+ "@xmldom/xmldom": "^0.9.8"
34
+ },
35
+ "devDependencies": {
36
+ "lexical": "^0.33.0"
37
+ },
38
+ "volta": {
39
+ "extends": "../../package.json"
40
+ },
41
+ "scripts": {
42
+ "prepublish": "cd ../.. && nx build utilities"
43
+ }
44
+ }
@@ -0,0 +1,28 @@
1
+ const JSON_PATH_START = "$";
2
+ const JSON_PATH_CONTENT = ".content[";
3
+
4
+ /**
5
+ * Converts a USJ JSONPath string into an array of indexes.
6
+ *
7
+ * @param jsonPath - The USJ JSONPath string to convert. It must start with `$` and contain `.content[index]` segments.
8
+ * @returns An array of numeric indexes extracted from the JSONPath.
9
+ * @throws Will throw an error if the JSONPath does not start with `$`.
10
+ */
11
+ export function indexesFromUsjJsonPath(jsonPath: string): number[] {
12
+ const path = jsonPath.split(JSON_PATH_CONTENT);
13
+ if (path.shift() !== JSON_PATH_START)
14
+ throw new Error(`indexesFromJsonPath: jsonPath didn't start with '${JSON_PATH_START}'`);
15
+
16
+ const indexes = path.map((str) => parseInt(str, 10));
17
+ return indexes;
18
+ }
19
+
20
+ /**
21
+ * Converts an array of indexes into a USJ JSONPath string.
22
+ *
23
+ * @param indexes - An array of numeric indexes to convert.
24
+ * @returns A USJ JSONPath string constructed from the indexes.
25
+ */
26
+ export function usjJsonPathFromIndexes(indexes: number[]): string {
27
+ return indexes.reduce((path, index) => `${path}${JSON_PATH_CONTENT}${index}]`, JSON_PATH_START);
28
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Convert Scripture from USJ to USX.
3
+ * Adapted to TypeScript from this file:
4
+ * @see https://github.com/usfm-bible/usfmtc/blob/0afa385a1f282b286cc6bff7bbc953ae788aa10c/src/usfmtc/usjproc.py
5
+ */
6
+
7
+ import { DOMImplementation, Document, Element, Text } from "@xmldom/xmldom";
8
+ import { MarkerContent, MarkerObject, Usj } from "./usj.model";
9
+ import { USX_TYPE, USX_VERSION } from "./usx.model";
10
+
11
+ let chapterEid: string | undefined;
12
+ let verseEid: string | undefined;
13
+
14
+ export function usjToUsxString(usj: Usj): string {
15
+ const usxDoc = new DOMImplementation().createDocument("", USX_TYPE);
16
+ if (usxDoc.documentElement) {
17
+ usxDoc.documentElement.setAttribute("version", USX_VERSION);
18
+ usjToUsxDom(usj, usxDoc);
19
+ }
20
+ return usxDoc.toString();
21
+ }
22
+
23
+ export function usjToUsxDom(usj: Usj, usxDoc: Document): Element | undefined {
24
+ if (!usxDoc.documentElement) return undefined;
25
+
26
+ for (const [index, markerContent] of usj.content.entries()) {
27
+ const isLastItem = index === usj.content.length - 1;
28
+ convertUsjRecurse(markerContent, usxDoc.documentElement, usxDoc, isLastItem);
29
+ }
30
+ return usxDoc.documentElement ?? undefined;
31
+ }
32
+
33
+ function convertUsjRecurse(
34
+ markerContent: MarkerContent,
35
+ parentElement: Element,
36
+ usxDoc: Document,
37
+ isLastItem: boolean,
38
+ ) {
39
+ let element: Text | Element;
40
+ let type: string | undefined;
41
+ let eidElement: Element | undefined;
42
+ if (typeof markerContent === "string") element = usxDoc.createTextNode(markerContent);
43
+ else {
44
+ type = markerContent.type.replace("table:", "");
45
+ element = usxDoc.createElement(type);
46
+ setAttributes(element, markerContent);
47
+ if (markerContent.content) {
48
+ for (const [index, item] of markerContent.content.entries()) {
49
+ const _isLastItem = index === markerContent.content.length - 1;
50
+ convertUsjRecurse(item, element, usxDoc, _isLastItem);
51
+ }
52
+ }
53
+ }
54
+
55
+ // Create chapter and verse end elements from SID attributes.
56
+ if (verseEid && (type === "verse" || (parentElement.tagName === "para" && isLastItem))) {
57
+ eidElement = createVerseEndElement(usxDoc, verseEid);
58
+ verseEid = undefined;
59
+ }
60
+ if (type === "verse" && typeof markerContent !== "string" && markerContent.sid !== undefined)
61
+ verseEid = markerContent.sid;
62
+
63
+ if (chapterEid && (type === "chapter" || (type === "para" && isLastItem))) {
64
+ eidElement = createChapterEndElement(usxDoc, chapterEid);
65
+ chapterEid = undefined;
66
+ }
67
+ if (type === "chapter" && typeof markerContent !== "string" && markerContent.sid !== undefined)
68
+ chapterEid = markerContent.sid;
69
+
70
+ // Append to parent.
71
+ const isVerseInImpliedPara =
72
+ parentElement.nodeName === USX_TYPE && eidElement?.tagName === "verse";
73
+ if (eidElement && (!isLastItem || isVerseInImpliedPara)) parentElement.appendChild(eidElement);
74
+ parentElement.appendChild(element);
75
+ if (eidElement && isLastItem && !isVerseInImpliedPara) parentElement.appendChild(eidElement);
76
+
77
+ // Allow for final chapter and verse end elements at the end of an implied para.
78
+ if (isLastItem && parentElement.nodeName === USX_TYPE) {
79
+ if (verseEid) parentElement.appendChild(createVerseEndElement(usxDoc, verseEid));
80
+ if (chapterEid) parentElement.appendChild(createChapterEndElement(usxDoc, chapterEid));
81
+ verseEid = undefined;
82
+ chapterEid = undefined;
83
+ }
84
+ }
85
+
86
+ function setAttributes(element: Element, markerContent: MarkerObject) {
87
+ if (markerContent.type === "unmatched") element.setAttribute("marker", markerContent.marker);
88
+ else element.setAttribute("style", markerContent.marker);
89
+ for (const [key, value] of Object.entries(markerContent)) {
90
+ if (value && !["type", "marker", "content"].includes(key)) {
91
+ element.setAttribute(key, value as string);
92
+ }
93
+ }
94
+ }
95
+
96
+ function createVerseEndElement(usxDoc: Document, verseEid: string): Element {
97
+ const eidElement = usxDoc.createElement("verse");
98
+ eidElement.setAttribute("eid", verseEid);
99
+ return eidElement;
100
+ }
101
+
102
+ function createChapterEndElement(usxDoc: Document, chapterEid: string): Element {
103
+ const eidElement = usxDoc.createElement("chapter");
104
+ eidElement.setAttribute("eid", chapterEid);
105
+ return eidElement;
106
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Unified Scripture JSON (USJ) - The JSON variant of USFM and USX data models.
3
+ * These types follow this schema:
4
+ * @see https://github.com/usfm-bible/tcdocs/blob/usj/grammar/usj.js
5
+ */
6
+
7
+ /** The USJ spec type */
8
+ export const USJ_TYPE = "USJ";
9
+ /** The USJ spec version */
10
+ export const USJ_VERSION = "3.1";
11
+ /** List of known properties of `MarkerObject` */
12
+ export const MARKER_OBJECT_PROPS: (keyof MarkerObject)[] = [
13
+ "type",
14
+ "marker",
15
+ "content",
16
+ "sid",
17
+ "eid",
18
+ "number",
19
+ "code",
20
+ "altnumber",
21
+ "pubnumber",
22
+ "caller",
23
+ "align",
24
+ "category",
25
+ ];
26
+
27
+ /** Single piece of Scripture content */
28
+ export type MarkerContent = string | MarkerObject;
29
+
30
+ /** A Scripture Marker and its contents */
31
+ export type MarkerObject = {
32
+ /**
33
+ * The kind/category of node or element this is, corresponding the USFM marker and USX node
34
+ * @example `para`, `verse`, `char`
35
+ */
36
+ type: string;
37
+ /**
38
+ * The corresponding marker in USFM or style in USX
39
+ * @example `p`, `v`, `nd`
40
+ */
41
+ marker: string;
42
+ /** This marker's contents laid out in order */
43
+ content?: MarkerContent[];
44
+ /** Indicates the Book-chapter-verse value in the paragraph based structure */
45
+ sid?: string;
46
+ /** Milestone end ID, matches start ID (not currently included in USJ spec) */
47
+ eid?: string;
48
+ /** Chapter number or verse number */
49
+ number?: string;
50
+ /** The 3-letter book code in ID element */
51
+ code?: BookCode;
52
+ /** Alternate chapter number or verse number */
53
+ altnumber?: string;
54
+ /** Published character of chapter or verse */
55
+ pubnumber?: string;
56
+ /** Caller character for footnotes and cross-refs */
57
+ caller?: string;
58
+ /** Alignment of table cells */
59
+ align?: string;
60
+ /** Category of extended study bible sections */
61
+ category?: string;
62
+ };
63
+
64
+ /** Scripture data represented in JSON format. Data compatible transformation from USX/USFM */
65
+ export type Usj = {
66
+ /** The USJ spec type */
67
+ type: typeof USJ_TYPE;
68
+ /** The USJ spec version */
69
+ version: typeof USJ_VERSION;
70
+ /** The JSON representation of scripture contents from USFM/USX */
71
+ content: MarkerContent[];
72
+ };
73
+
74
+ export function isValidBookCode(code: string): boolean {
75
+ return VALID_BOOK_CODES.includes(code as BookCode);
76
+ }
77
+
78
+ /** 3-letter Scripture book code */
79
+ export type BookCode = (typeof VALID_BOOK_CODES)[number];
80
+
81
+ const VALID_BOOK_CODES = [
82
+ // Old Testament
83
+ "GEN",
84
+ "EXO",
85
+ "LEV",
86
+ "NUM",
87
+ "DEU",
88
+ "JOS",
89
+ "JDG",
90
+ "RUT",
91
+ "1SA",
92
+ "2SA",
93
+ "1KI",
94
+ "2KI",
95
+ "1CH",
96
+ "2CH",
97
+ "EZR",
98
+ "NEH",
99
+ "EST",
100
+ "JOB",
101
+ "PSA",
102
+ "PRO",
103
+ "ECC",
104
+ "SNG",
105
+ "ISA",
106
+ "JER",
107
+ "LAM",
108
+ "EZK",
109
+ "DAN",
110
+ "HOS",
111
+ "JOL",
112
+ "AMO",
113
+ "OBA",
114
+ "JON",
115
+ "MIC",
116
+ "NAM",
117
+ "HAB",
118
+ "ZEP",
119
+ "HAG",
120
+ "ZEC",
121
+ "MAL",
122
+ // New Testament
123
+ "MAT",
124
+ "MRK",
125
+ "LUK",
126
+ "JHN",
127
+ "ACT",
128
+ "ROM",
129
+ "1CO",
130
+ "2CO",
131
+ "GAL",
132
+ "EPH",
133
+ "PHP",
134
+ "COL",
135
+ "1TH",
136
+ "2TH",
137
+ "1TI",
138
+ "2TI",
139
+ "TIT",
140
+ "PHM",
141
+ "HEB",
142
+ "JAS",
143
+ "1PE",
144
+ "2PE",
145
+ "1JN",
146
+ "2JN",
147
+ "3JN",
148
+ "JUD",
149
+ "REV",
150
+ // Deuterocanon
151
+ "TOB",
152
+ "JDT",
153
+ "ESG",
154
+ "WIS",
155
+ "SIR",
156
+ "BAR",
157
+ "LJE",
158
+ "S3Y",
159
+ "SUS",
160
+ "BEL",
161
+ "1MA",
162
+ "2MA",
163
+ "3MA",
164
+ "4MA",
165
+ "1ES",
166
+ "2ES",
167
+ "MAN",
168
+ "PS2",
169
+ "ODA",
170
+ "PSS",
171
+ "EZA",
172
+ "5EZ",
173
+ "6EZ",
174
+ "DAG",
175
+ "PS3",
176
+ "2BA",
177
+ "LBA",
178
+ "JUB",
179
+ "ENO",
180
+ "1MQ",
181
+ "2MQ",
182
+ "3MQ",
183
+ "REP",
184
+ "4BA",
185
+ "LAO",
186
+ // Non scripture
187
+ "FRT",
188
+ "BAK",
189
+ "OTH",
190
+ "INT",
191
+ "CNC",
192
+ "GLO",
193
+ "TDX",
194
+ "NDX",
195
+ "XXA",
196
+ "XXB",
197
+ "XXC",
198
+ "XXD",
199
+ "XXE",
200
+ "XXF",
201
+ "XXG",
202
+ ] as const;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Convert Scripture from USX to USJ.
3
+ * Adapted to TypeScript from this file:
4
+ * @see https://github.com/usfm-bible/usfmtc/blob/0afa385a1f282b286cc6bff7bbc953ae788aa10c/src/usfmtc/usjproc.py
5
+ */
6
+
7
+ import { DOMParser, Element } from "@xmldom/xmldom";
8
+ import { MarkerContent, MarkerObject, USJ_TYPE, USJ_VERSION, Usj } from "./usj.model";
9
+ import { USX_TYPE } from "./usx.model";
10
+
11
+ type Action = "append" | "merge" | "ignore";
12
+ type Attribs = { [name: string]: string };
13
+
14
+ export function usxStringToUsj(usxString: string): Usj {
15
+ const parser = new DOMParser();
16
+ const inputUsxDom = parser.parseFromString(usxString, "text/xml");
17
+ return usxDomToUsj(inputUsxDom.documentElement);
18
+ }
19
+
20
+ export function usxDomToUsj(inputUsxDom: Element | null): Usj {
21
+ const [outputJson] = inputUsxDom
22
+ ? convertUsxRecurse(inputUsxDom)
23
+ : [{ content: [] as MarkerContent[] } as Usj];
24
+ outputJson.type = USJ_TYPE;
25
+ outputJson.version = USJ_VERSION;
26
+ return outputJson;
27
+ }
28
+
29
+ function convertUsxRecurse<T extends Usj | MarkerObject = Usj>(
30
+ inputUsxElement: Element,
31
+ ): [outputJson: T, action: Action] {
32
+ const attribs: Attribs = {};
33
+ let type: string = inputUsxElement.tagName;
34
+ let marker: string | undefined;
35
+ let text: string | undefined;
36
+ let action: Action = "append";
37
+
38
+ if (["row", "cell"].includes(type)) type = "table:" + type;
39
+ if (inputUsxElement.attributes) {
40
+ for (const attrib of Array.from(inputUsxElement.attributes)) {
41
+ attribs[attrib.name] = attrib.value;
42
+ }
43
+ }
44
+
45
+ if (attribs.style) {
46
+ marker = attribs.style;
47
+ delete attribs.style;
48
+ }
49
+ // dropping because presence of vid in para elements is not consistent in USX
50
+ if (attribs.vid) delete attribs.vid;
51
+ // Not dropping `attribs.closed` for backwards compatibility.
52
+ // dropping because it is nonstandard derived metadata that could get out of date
53
+ if (attribs.status) delete attribs.status;
54
+
55
+ let outObj: T = { type } as T;
56
+ if (marker) (outObj as MarkerObject).marker = marker;
57
+ outObj = { ...outObj, ...attribs };
58
+
59
+ if (
60
+ inputUsxElement.firstChild &&
61
+ inputUsxElement.firstChild.nodeType === inputUsxElement.firstChild.TEXT_NODE &&
62
+ inputUsxElement.firstChild.nodeValue &&
63
+ asciiTrim(inputUsxElement.firstChild.nodeValue) !== ""
64
+ ) {
65
+ text = inputUsxElement.firstChild.nodeValue;
66
+ }
67
+
68
+ const children = Array.from(inputUsxElement.childNodes);
69
+ outObj.content = [];
70
+
71
+ if (text) {
72
+ outObj.content.push(text);
73
+ }
74
+
75
+ for (const child of children) {
76
+ // ChildNodes are Elements.
77
+ if ((child as Element).tagName === undefined) {
78
+ continue;
79
+ }
80
+ // ChildNodes are Elements.
81
+ const [childDict, whatToDo] = convertUsxRecurse<MarkerObject>(child as Element);
82
+
83
+ switch (whatToDo) {
84
+ case "append":
85
+ outObj.content.push(childDict);
86
+ break;
87
+ case "merge":
88
+ outObj.content = outObj.content.concat(childDict);
89
+ break;
90
+ case "ignore":
91
+ break;
92
+ default:
93
+ break;
94
+ }
95
+
96
+ // Handle tail text
97
+ if (
98
+ child.nextSibling &&
99
+ child.nextSibling.nodeType === child.nextSibling.TEXT_NODE &&
100
+ child.nextSibling.nodeValue &&
101
+ (asciiTrim(child.nextSibling.nodeValue) !== "" || child.nextSibling.nodeValue === " ")
102
+ ) {
103
+ outObj.content.push(child.nextSibling.nodeValue);
104
+ }
105
+ }
106
+
107
+ // For backward compatibility, not deleting content for type: chapter, verse, optbreak, ms OR
108
+ // marker: va, ca, b.
109
+ if (outObj.content.length === 0 && outObj.type !== USX_TYPE) {
110
+ delete outObj.content;
111
+ }
112
+
113
+ if ("eid" in outObj && ["verse", "chapter"].includes(type)) {
114
+ action = "ignore";
115
+ }
116
+
117
+ return [outObj, action];
118
+ }
119
+
120
+ /**
121
+ * Removes leading and trailing ASCII whitespace.
122
+ *
123
+ * Only trim ASCII whitespace characters: space, tab, line feed, carriage return, form feed,
124
+ * vertical tab.
125
+ * @param str - The string to remove whitespace from.
126
+ * @returns the string with leading and trailing whitespace removed.
127
+ */
128
+ function asciiTrim(str: string): string {
129
+ return str.replace(/(^[ \t\n\r\f\v]+)|([ \t\n\r\f\v]+$)/g, "");
130
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Unified Scripture XML (USX).
3
+ * These types follow this schema:
4
+ * @see https://github.com/usfm-bible/tcdocs/blob/main/grammar/usx.rng
5
+ */
6
+
7
+ /** The USX spec type */
8
+ export const USX_TYPE = "usx";
9
+ /** The USX spec version */
10
+ export const USX_VERSION = "3.1";
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type { Usj, BookCode, MarkerContent, MarkerObject } from "./converters/usj/usj.model";
2
+ export {
3
+ MARKER_OBJECT_PROPS,
4
+ USJ_TYPE,
5
+ USJ_VERSION,
6
+ isValidBookCode,
7
+ } from "./converters/usj/usj.model";
8
+ export { usxStringToUsj } from "./converters/usj/usx-to-usj";
9
+ export { usjToUsxString } from "./converters/usj/usj-to-usx";
10
+ export { indexesFromUsjJsonPath, usjJsonPathFromIndexes } from "./converters/usj/jsonpath-indexes";