@helloao/tools 0.1.0 → 0.2.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.
Files changed (57) hide show
  1. package/dist/cjs/generation/api.cjs +727 -2
  2. package/dist/cjs/generation/api.cjs.map +2 -2
  3. package/dist/cjs/generation/audio.cjs +52 -5
  4. package/dist/cjs/generation/audio.cjs.map +2 -2
  5. package/dist/cjs/generation/book-order.cjs +1 -1
  6. package/dist/cjs/generation/book-order.cjs.map +2 -2
  7. package/dist/cjs/generation/common-types.cjs +651 -0
  8. package/dist/cjs/generation/common-types.cjs.map +2 -2
  9. package/dist/cjs/generation/dataset.cjs +31 -8
  10. package/dist/cjs/generation/dataset.cjs.map +2 -2
  11. package/dist/cjs/index.cjs.map +1 -1
  12. package/dist/cjs/log.cjs.map +1 -1
  13. package/dist/cjs/parser/codex-parser.cjs.map +1 -1
  14. package/dist/cjs/parser/commentary-csv-parser.cjs.map +1 -1
  15. package/dist/cjs/parser/iterators.cjs.map +1 -1
  16. package/dist/cjs/parser/lockman-parser.cjs +334 -0
  17. package/dist/cjs/parser/lockman-parser.cjs.map +7 -0
  18. package/dist/cjs/parser/tyndale-xml-parser.cjs.map +1 -1
  19. package/dist/cjs/parser/types.cjs.map +1 -1
  20. package/dist/cjs/parser/usfm-parser.cjs.map +1 -1
  21. package/dist/cjs/parser/usx-parser.cjs +24 -7
  22. package/dist/cjs/parser/usx-parser.cjs.map +2 -2
  23. package/dist/cjs/utils.cjs +152 -0
  24. package/dist/cjs/utils.cjs.map +3 -3
  25. package/dist/esm/generation/api.js +696 -2
  26. package/dist/esm/generation/api.js.map +2 -2
  27. package/dist/esm/generation/audio.js +46 -3
  28. package/dist/esm/generation/audio.js.map +2 -2
  29. package/dist/esm/generation/book-order.js +1 -1
  30. package/dist/esm/generation/book-order.js.map +2 -2
  31. package/dist/esm/generation/common-types.js +590 -0
  32. package/dist/esm/generation/common-types.js.map +3 -3
  33. package/dist/esm/generation/dataset.js +29 -8
  34. package/dist/esm/generation/dataset.js.map +2 -2
  35. package/dist/esm/index.js.map +1 -1
  36. package/dist/esm/log.js.map +1 -1
  37. package/dist/esm/parser/codex-parser.js.map +1 -1
  38. package/dist/esm/parser/commentary-csv-parser.js.map +1 -1
  39. package/dist/esm/parser/iterators.js.map +1 -1
  40. package/dist/esm/parser/lockman-parser.js +306 -0
  41. package/dist/esm/parser/lockman-parser.js.map +7 -0
  42. package/dist/esm/parser/tyndale-xml-parser.js.map +1 -1
  43. package/dist/esm/parser/usfm-parser.js.map +1 -1
  44. package/dist/esm/parser/usx-parser.js +24 -7
  45. package/dist/esm/parser/usx-parser.js.map +2 -2
  46. package/dist/esm/utils.js +138 -0
  47. package/dist/esm/utils.js.map +2 -2
  48. package/dist/types/generation/api.d.ts +2515 -470
  49. package/dist/types/generation/audio.d.ts +5 -0
  50. package/dist/types/generation/common-types.d.ts +1222 -413
  51. package/dist/types/generation/dataset.d.ts +1 -0
  52. package/dist/types/parser/codex-parser.d.ts +18 -138
  53. package/dist/types/parser/lockman-parser.d.ts +14 -0
  54. package/dist/types/parser/types.d.ts +4 -0
  55. package/dist/types/parser/usx-parser.d.ts +2 -2
  56. package/dist/types/utils.d.ts +223 -18
  57. package/package.json +5 -3
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ import { bookOrderMap } from "../generation/book-order";
3
+ export const LOCKMAN_PARSER_VERSION = 3;
4
+ const bookNumberIdMap = /* @__PURE__ */ new Map();
5
+ for (const [id, order] of bookOrderMap) {
6
+ if (!bookNumberIdMap.has(order)) {
7
+ bookNumberIdMap.set(order, id);
8
+ }
9
+ }
10
+ export class LockmanParser {
11
+ parse(text) {
12
+ const roots = [];
13
+ let currentRoot = null;
14
+ let currentChapter = null;
15
+ let currentVerse = null;
16
+ const structureRegex = /(<BN>.*?<\/BN>)|((?:<CN>.*?<\/CN>)|(?:<SN>.*?<\/SN>))|((?:<SH>.*?<\/SH>)|(?:<SS>.*?<\/SS>))|((?:<V>|<C>|<CC>|<CP>|<PM>|<A>|<PO>|<P>|<PN>)\{\{.*?\}\}\d+(?:<T>|\^)(?:\{.*?\})?)/g;
17
+ let lastIndex = 0;
18
+ let match;
19
+ let isPoem = false;
20
+ while ((match = structureRegex.exec(text)) !== null) {
21
+ const fullMatch = match[0];
22
+ const startIndex = match.index;
23
+ if (startIndex > lastIndex) {
24
+ const content = text.substring(lastIndex, startIndex);
25
+ if (currentVerse && currentChapter) {
26
+ this.parseVerseContent(
27
+ content,
28
+ currentVerse,
29
+ currentChapter,
30
+ isPoem
31
+ );
32
+ }
33
+ }
34
+ lastIndex = startIndex + fullMatch.length;
35
+ if (match[1]) {
36
+ const title = match[1].replace(/<\/?BN>/g, "").trim();
37
+ currentRoot = {
38
+ type: "root",
39
+ title,
40
+ content: []
41
+ };
42
+ roots.push(currentRoot);
43
+ currentChapter = null;
44
+ currentVerse = null;
45
+ } else if (match[2]) {
46
+ if (!currentRoot) {
47
+ currentRoot = {
48
+ type: "root",
49
+ content: []
50
+ };
51
+ roots.push(currentRoot);
52
+ }
53
+ const inner = match[2].replace(/<\/?(CN|SN)>/g, "").trim();
54
+ const numMatch = inner.match(/\d+/);
55
+ const num = numMatch ? parseInt(numMatch[0], 10) : 0;
56
+ currentChapter = {
57
+ type: "chapter",
58
+ number: num,
59
+ content: [],
60
+ footnotes: []
61
+ };
62
+ currentRoot.content.push(currentChapter);
63
+ currentVerse = null;
64
+ } else if (match[3]) {
65
+ if (!currentRoot) {
66
+ currentRoot = { type: "root", content: [] };
67
+ roots.push(currentRoot);
68
+ }
69
+ const raw = match[3];
70
+ if (raw.startsWith("<SS>")) {
71
+ const inner = raw.replace(/<\/?SS>/g, "").trim();
72
+ const subtitle = {
73
+ type: "hebrew_subtitle",
74
+ content: []
75
+ };
76
+ if (currentChapter) {
77
+ this.parseHebrewSubtitleContent(
78
+ inner,
79
+ subtitle,
80
+ currentChapter
81
+ );
82
+ currentChapter.content.push(subtitle);
83
+ }
84
+ } else {
85
+ const inner = raw.replace(/<\/?SH>/g, "").trim();
86
+ const normalized = inner.replace(/<\\>|<\/?>|[{}]/g, "");
87
+ const heading = {
88
+ type: "heading",
89
+ content: [normalized]
90
+ };
91
+ if (currentChapter) {
92
+ currentChapter.content.push(heading);
93
+ } else {
94
+ currentRoot.content.push(heading);
95
+ }
96
+ }
97
+ } else if (match[4]) {
98
+ const tagContent = match[4];
99
+ if (currentRoot && !currentRoot.id) {
100
+ const bookId = this.getBookIdFromVerseTag(tagContent);
101
+ if (bookId) {
102
+ currentRoot.id = bookId;
103
+ }
104
+ }
105
+ const vNumMatch = tagContent.match(/(\d+)(?:<T>|\^)/);
106
+ const vNum = vNumMatch ? parseInt(vNumMatch[1], 10) : 0;
107
+ if (currentChapter) {
108
+ if (tagContent.startsWith("<PM>")) {
109
+ currentChapter.content.push({ type: "line_break" });
110
+ }
111
+ isPoem = /<(?:P|PO|PN|CC|CP)>/.test(tagContent);
112
+ currentVerse = {
113
+ type: "verse",
114
+ number: vNum,
115
+ content: []
116
+ };
117
+ currentChapter.content.push(currentVerse);
118
+ }
119
+ }
120
+ }
121
+ if (lastIndex < text.length) {
122
+ const content = text.substring(lastIndex);
123
+ if (currentVerse && currentChapter) {
124
+ this.parseVerseContent(
125
+ content,
126
+ currentVerse,
127
+ currentChapter,
128
+ isPoem
129
+ );
130
+ }
131
+ }
132
+ return roots;
133
+ }
134
+ getBookIdFromVerseTag(tagContent) {
135
+ const match = tagContent.match(/\{\{(\d+)::/);
136
+ if (!match) {
137
+ return null;
138
+ }
139
+ const bookNumber = parseInt(match[1], 10);
140
+ if (isNaN(bookNumber)) {
141
+ return null;
142
+ }
143
+ return bookNumberIdMap.get(bookNumber) ?? null;
144
+ }
145
+ parseVerseContent(text, verse, chapter, isPoem) {
146
+ let cleanText = text.replace(/\s+/g, " ").trim();
147
+ cleanText = cleanText.replace(/[\+\-][“‘"]/g, "");
148
+ cleanText = cleanText.replace(/\+\[/g, "[").replace(/\+\]/g, "]");
149
+ cleanText = cleanText.replace(/@\[.*?@\]/g, "");
150
+ const segmentRegex = /(<\$F.*?\$E>)|(<\$R.*?\$RE>)|(<[^>]*>)|(\{.*?\})/g;
151
+ let lastIdx = 0;
152
+ let m;
153
+ let wordsOfJesus = false;
154
+ while ((m = segmentRegex.exec(cleanText)) !== null) {
155
+ const snippet = cleanText.substring(lastIdx, m.index);
156
+ if (snippet) {
157
+ this.addText(verse, snippet, isPoem, wordsOfJesus);
158
+ }
159
+ if (m[1]) {
160
+ this.processFootnote(m[1], verse, chapter);
161
+ } else if (m[2]) {
162
+ } else if (m[3]) {
163
+ if (m[3] === "<PO>") {
164
+ verse.content.push({ lineBreak: true });
165
+ lastIdx = segmentRegex.lastIndex;
166
+ continue;
167
+ }
168
+ if (m[3] === "<RS>") {
169
+ wordsOfJesus = true;
170
+ } else if (m[3] === "</RS>") {
171
+ wordsOfJesus = false;
172
+ }
173
+ } else if (m[4]) {
174
+ const content = m[4].substring(1, m[4].length - 1);
175
+ if (content) {
176
+ let text2 = {
177
+ text: content,
178
+ italics: true
179
+ };
180
+ if (isPoem) {
181
+ text2.poem = 1;
182
+ }
183
+ if (wordsOfJesus) {
184
+ text2.wordsOfJesus = true;
185
+ }
186
+ verse.content.push(text2);
187
+ }
188
+ }
189
+ lastIdx = segmentRegex.lastIndex;
190
+ }
191
+ const remaining = cleanText.substring(lastIdx);
192
+ if (remaining) {
193
+ this.addText(verse, remaining, isPoem, wordsOfJesus);
194
+ }
195
+ }
196
+ addText(verse, text, isPoem, wordsOfJesus) {
197
+ if (!text) return;
198
+ if (isPoem || wordsOfJesus) {
199
+ const lastItem = verse.content[verse.content.length - 1];
200
+ if (lastItem && typeof lastItem !== "string" && !("noteId" in lastItem) && lastItem.poem === (isPoem ? 1 : void 0) && lastItem.wordsOfJesus === (wordsOfJesus ? true : void 0) && !lastItem.italics) {
201
+ lastItem.text += text;
202
+ } else {
203
+ const formatted = {
204
+ text
205
+ };
206
+ if (isPoem) {
207
+ formatted.poem = 1;
208
+ }
209
+ if (wordsOfJesus) {
210
+ formatted.wordsOfJesus = true;
211
+ }
212
+ verse.content.push(formatted);
213
+ }
214
+ } else {
215
+ const lastItem = verse.content[verse.content.length - 1];
216
+ if (typeof lastItem === "string") {
217
+ verse.content[verse.content.length - 1] = lastItem + text;
218
+ } else {
219
+ verse.content.push(text);
220
+ }
221
+ }
222
+ }
223
+ parseHebrewSubtitleContent(text, subtitle, chapter) {
224
+ let cleanText = text.replace(/\s+/g, " ").trim();
225
+ cleanText = cleanText.replace(/[{}]/g, "");
226
+ cleanText = cleanText.replace(/@\[.*?@\]/g, "");
227
+ const segmentRegex = /(<\$F.*?\$E>)|(<\$R.*?\$RE>)|(<[^>]+>)|(\{.*?\})/g;
228
+ let lastIdx = 0;
229
+ let m;
230
+ while ((m = segmentRegex.exec(cleanText)) !== null) {
231
+ const snippet = cleanText.substring(lastIdx, m.index);
232
+ if (snippet) {
233
+ this.addSubtitleText(subtitle, snippet);
234
+ }
235
+ if (m[1]) {
236
+ const ref = this.processFootnoteForSubtitle(m[1], chapter);
237
+ if (ref) {
238
+ subtitle.content.push(ref);
239
+ }
240
+ } else if (m[2]) {
241
+ } else if (m[3]) {
242
+ } else if (m[4]) {
243
+ const content = m[4].substring(1, m[4].length - 1);
244
+ if (content) {
245
+ this.addSubtitleText(subtitle, content);
246
+ }
247
+ }
248
+ lastIdx = segmentRegex.lastIndex;
249
+ }
250
+ const remaining = cleanText.substring(lastIdx);
251
+ if (remaining) {
252
+ this.addSubtitleText(subtitle, remaining);
253
+ }
254
+ }
255
+ addSubtitleText(subtitle, text) {
256
+ if (!text) return;
257
+ const lastItem = subtitle.content[subtitle.content.length - 1];
258
+ if (typeof lastItem === "string") {
259
+ subtitle.content[subtitle.content.length - 1] = lastItem + text;
260
+ } else {
261
+ subtitle.content.push(text);
262
+ }
263
+ }
264
+ processFootnote(block, verse, chapter) {
265
+ const text = this.extractFootnoteText(block);
266
+ if (!text) return;
267
+ const noteId = chapter.footnotes.length + 1;
268
+ const fn = {
269
+ noteId,
270
+ text,
271
+ caller: "+",
272
+ reference: {
273
+ chapter: chapter.number,
274
+ verse: verse.number
275
+ }
276
+ };
277
+ chapter.footnotes.push(fn);
278
+ verse.content.push({
279
+ noteId
280
+ });
281
+ }
282
+ processFootnoteForSubtitle(block, chapter) {
283
+ const text = this.extractFootnoteText(block);
284
+ if (!text) return null;
285
+ const noteId = chapter.footnotes.length + 1;
286
+ const fn = {
287
+ noteId,
288
+ text,
289
+ caller: "+",
290
+ reference: {
291
+ chapter: chapter.number,
292
+ verse: 0
293
+ }
294
+ };
295
+ chapter.footnotes.push(fn);
296
+ return {
297
+ noteId
298
+ };
299
+ }
300
+ extractFootnoteText(block) {
301
+ let inner = block.replace(/^<\$F/, "").replace(/\$E>$/, "");
302
+ inner = inner.replace(/<FN>.*?<\/FN>/g, "");
303
+ return inner.replace(/<[^>]+>/g, "").replace(/[\{\}]/g, "").trim();
304
+ }
305
+ }
306
+ //# sourceMappingURL=lockman-parser.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../parser/lockman-parser.ts"],
4
+ "sourcesContent": ["import {\r\n ParseTree,\r\n Chapter,\r\n Verse,\r\n Heading,\r\n Footnote,\r\n Text,\r\n FootnoteReference,\r\n HebrewSubtitle,\r\n} from './types';\r\nimport { bookOrderMap } from '../generation/book-order';\r\n\r\nexport const LOCKMAN_PARSER_VERSION = 3;\r\n\r\nconst bookNumberIdMap = new Map<number, string>();\r\nfor (const [id, order] of bookOrderMap) {\r\n if (!bookNumberIdMap.has(order)) {\r\n bookNumberIdMap.set(order, id);\r\n }\r\n}\r\n\r\nexport class LockmanParser {\r\n parse(text: string): ParseTree[] {\r\n const roots: ParseTree[] = [];\r\n let currentRoot: ParseTree | null = null;\r\n let currentChapter: Chapter | null = null;\r\n let currentVerse: Verse | null = null;\r\n\r\n // Splits text into tokens based on structural tags\r\n // Identify major blocks: BN, CN, SH, and Verse Starters (V, C, PM, A, PO)\r\n // Note: SH and CN content is strictly inside the tag.\r\n // Verse content follows the tag.\r\n // We use a regex that captures the tag AND its \"attributes\" like {{..}}Num<T> for verses.\r\n\r\n // Regex for structural separators\r\n // Captures:\r\n // 1. Title: <BN>...</BN>\r\n // 2. Chapter Heading: <CN>...</CN> or <SN>...</SN>\r\n // 3. Section Heading: <SH>...</SH> or <SS>...</SS>\r\n // 4. Verse Start: (<V>|<C>|<CC>|<CP>|<PM>|<A>|<PO>|<P>|<PN>){{...}}Number<T>\r\n\r\n const structureRegex =\r\n /(<BN>.*?<\\/BN>)|((?:<CN>.*?<\\/CN>)|(?:<SN>.*?<\\/SN>))|((?:<SH>.*?<\\/SH>)|(?:<SS>.*?<\\/SS>))|((?:<V>|<C>|<CC>|<CP>|<PM>|<A>|<PO>|<P>|<PN>)\\{\\{.*?\\}\\}\\d+(?:<T>|\\^)(?:\\{.*?\\})?)/g;\r\n\r\n let lastIndex = 0;\r\n let match;\r\n let isPoem = false;\r\n\r\n while ((match = structureRegex.exec(text)) !== null) {\r\n const fullMatch = match[0];\r\n const startIndex = match.index;\r\n\r\n // Content belonging to the PREVIOUS structural element\r\n if (startIndex > lastIndex) {\r\n const content = text.substring(lastIndex, startIndex);\r\n if (currentVerse && currentChapter) {\r\n this.parseVerseContent(\r\n content,\r\n currentVerse,\r\n currentChapter,\r\n isPoem\r\n );\r\n }\r\n }\r\n\r\n lastIndex = startIndex + fullMatch.length;\r\n\r\n if (match[1]) {\r\n // <BN>Title</BN>\r\n // Start of a NEW Book\r\n const title = match[1].replace(/<\\/?BN>/g, '').trim();\r\n\r\n currentRoot = {\r\n type: 'root',\r\n title: title,\r\n content: [],\r\n };\r\n roots.push(currentRoot);\r\n\r\n // Reset context for new book\r\n currentChapter = null;\r\n currentVerse = null;\r\n } else if (match[2]) {\r\n // <CN>CHAPTER 1</CN> or <SN>PSALM 1</SN>\r\n // Start new Chapter\r\n // Ensure we have a root (handle case where no <BN> provided, implicitly create one? Or just attach to last root)\r\n if (!currentRoot) {\r\n // Fallback if no <BN> was found first\r\n currentRoot = {\r\n type: 'root',\r\n content: [],\r\n };\r\n roots.push(currentRoot);\r\n }\r\n\r\n // Remove tags: <CN>, </CN>, <SN>, </SN>\r\n const inner = match[2].replace(/<\\/?(CN|SN)>/g, '').trim();\r\n const numMatch = inner.match(/\\d+/);\r\n const num = numMatch ? parseInt(numMatch[0], 10) : 0;\r\n\r\n currentChapter = {\r\n type: 'chapter',\r\n number: num,\r\n content: [],\r\n footnotes: [],\r\n };\r\n currentRoot.content.push(currentChapter);\r\n currentVerse = null;\r\n } else if (match[3]) {\r\n // <SH>Heading</SH> or <SS>Subtitle</SS>\r\n if (!currentRoot) {\r\n currentRoot = { type: 'root', content: [] };\r\n roots.push(currentRoot);\r\n }\r\n\r\n const raw = match[3];\r\n if (raw.startsWith('<SS>')) {\r\n // Hebrew subtitle in Psalms\r\n const inner = raw.replace(/<\\/?SS>/g, '').trim();\r\n const subtitle: HebrewSubtitle = {\r\n type: 'hebrew_subtitle',\r\n content: [],\r\n };\r\n\r\n if (currentChapter) {\r\n this.parseHebrewSubtitleContent(\r\n inner,\r\n subtitle,\r\n currentChapter\r\n );\r\n currentChapter.content.push(subtitle);\r\n }\r\n } else {\r\n // Section heading\r\n const inner = raw.replace(/<\\/?SH>/g, '').trim();\r\n const normalized = inner.replace(/<\\\\>|<\\/?>|[{}]/g, '');\r\n const heading: Heading = {\r\n type: 'heading',\r\n content: [normalized],\r\n };\r\n if (currentChapter) {\r\n currentChapter.content.push(heading);\r\n } else {\r\n // Start of book heading? Not typical in this structure but possible\r\n currentRoot.content.push(heading);\r\n }\r\n }\r\n } else if (match[4]) {\r\n // Verse Start: <Tag>{{Ref}}Number<T>\r\n // Extract Verse Number\r\n const tagContent = match[4];\r\n if (currentRoot && !currentRoot.id) {\r\n const bookId = this.getBookIdFromVerseTag(tagContent);\r\n if (bookId) {\r\n currentRoot.id = bookId;\r\n }\r\n }\r\n // Regex to extract digits before <T>\r\n const vNumMatch = tagContent.match(/(\\d+)(?:<T>|\\^)/);\r\n const vNum = vNumMatch ? parseInt(vNumMatch[1], 10) : 0;\r\n\r\n if (currentChapter) {\r\n // Check for Paragraph Marker\r\n if (tagContent.startsWith('<PM>')) {\r\n currentChapter.content.push({ type: 'line_break' });\r\n }\r\n\r\n // Update global isPoem flag\r\n isPoem = /<(?:P|PO|PN|CC|CP)>/.test(tagContent);\r\n\r\n currentVerse = {\r\n type: 'verse',\r\n number: vNum,\r\n content: [],\r\n };\r\n currentChapter.content.push(currentVerse);\r\n }\r\n }\r\n }\r\n\r\n // Process remaining text after the last match\r\n if (lastIndex < text.length) {\r\n const content = text.substring(lastIndex);\r\n if (currentVerse && currentChapter) {\r\n this.parseVerseContent(\r\n content,\r\n currentVerse,\r\n currentChapter,\r\n isPoem\r\n );\r\n }\r\n }\r\n\r\n return roots;\r\n }\r\n\r\n private getBookIdFromVerseTag(tagContent: string): string | null {\r\n const match = tagContent.match(/\\{\\{(\\d+)::/);\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n const bookNumber = parseInt(match[1], 10);\r\n if (isNaN(bookNumber)) {\r\n return null;\r\n }\r\n\r\n return bookNumberIdMap.get(bookNumber) ?? null;\r\n }\r\n\r\n private parseVerseContent(\r\n text: string,\r\n verse: Verse,\r\n chapter: Chapter,\r\n isPoem: boolean\r\n ) {\r\n // Remove line breaks that are just formatting in the source file\r\n // But keep spaces.\r\n // The source has newlines. We should arguably treat them as spaces.\r\n let cleanText = text.replace(/\\s+/g, ' ').trim();\r\n cleanText = cleanText.replace(/[\\+\\-][\u201C\u2018\"]/g, '');\r\n cleanText = cleanText.replace(/\\+\\[/g, '[').replace(/\\+\\]/g, ']');\r\n cleanText = cleanText.replace(/@\\[.*?@\\]/g, '');\r\n\r\n // Regex to separate text from special inline blocks:\r\n // 1. Footnotes: <$F ... $E>\r\n // 2. Cross References: <$R ... $RE> (To be removed)\r\n // 3. Formatting Tags: <RA>, <N1>, etc. (To be removed?)\r\n // 4. Italics in curly braces: {text}\r\n\r\n // Note: Curly braces are nested inside footnotes sometimes, but stripped there.\r\n // At the verse level, they denote italics.\r\n\r\n const segmentRegex =\r\n /(<\\$F.*?\\$E>)|(<\\$R.*?\\$RE>)|(<[^>]*>)|(\\{.*?\\})/g;\r\n\r\n let lastIdx = 0;\r\n let m;\r\n let wordsOfJesus = false;\r\n\r\n while ((m = segmentRegex.exec(cleanText)) !== null) {\r\n const snippet = cleanText.substring(lastIdx, m.index);\r\n if (snippet) {\r\n // Add plain text\r\n this.addText(verse, snippet, isPoem, wordsOfJesus);\r\n }\r\n\r\n if (m[1]) {\r\n // Footnote <$F ... $E>\r\n this.processFootnote(m[1], verse, chapter);\r\n } else if (m[2]) {\r\n // Cross Ref -> Ignore\r\n } else if (m[3]) {\r\n if (m[3] === '<PO>') {\r\n verse.content.push({ lineBreak: true });\r\n lastIdx = segmentRegex.lastIndex;\r\n continue;\r\n }\r\n // Handle words of Jesus markers\r\n if (m[3] === '<RS>') {\r\n wordsOfJesus = true;\r\n } else if (m[3] === '</RS>') {\r\n wordsOfJesus = false;\r\n }\r\n // Other tag -> Ignore (e.g. <RA>, <N1>, <FA>)\r\n // These are often just markers or anchors.\r\n } else if (m[4]) {\r\n // Italics {text}\r\n const content = m[4].substring(1, m[4].length - 1);\r\n // We push a Text object with italics: true\r\n // Check if we need to decode inside? Usually just text.\r\n if (content) {\r\n let text: Text = {\r\n text: content,\r\n italics: true,\r\n };\r\n\r\n if (isPoem) {\r\n text.poem = 1;\r\n }\r\n\r\n if (wordsOfJesus) {\r\n text.wordsOfJesus = true;\r\n }\r\n\r\n verse.content.push(text);\r\n }\r\n }\r\n\r\n lastIdx = segmentRegex.lastIndex;\r\n }\r\n\r\n const remaining = cleanText.substring(lastIdx);\r\n if (remaining) {\r\n this.addText(verse, remaining, isPoem, wordsOfJesus);\r\n }\r\n }\r\n\r\n private addText(\r\n verse: Verse,\r\n text: string,\r\n isPoem: boolean,\r\n wordsOfJesus: boolean\r\n ) {\r\n if (!text) return;\r\n\r\n if (isPoem || wordsOfJesus) {\r\n // When poem=1 or wordsOfJesus is true, use Text objects to carry attributes.\r\n const lastItem = verse.content[verse.content.length - 1];\r\n\r\n if (\r\n lastItem &&\r\n typeof lastItem !== 'string' &&\r\n !('noteId' in lastItem) &&\r\n (lastItem as Text).poem === (isPoem ? 1 : undefined) &&\r\n (lastItem as Text).wordsOfJesus ===\r\n (wordsOfJesus ? true : undefined) &&\r\n !(lastItem as Text).italics\r\n ) {\r\n (lastItem as Text).text += text;\r\n } else {\r\n const formatted: Text = {\r\n text: text,\r\n };\r\n if (isPoem) {\r\n formatted.poem = 1;\r\n }\r\n if (wordsOfJesus) {\r\n formatted.wordsOfJesus = true;\r\n }\r\n verse.content.push(formatted);\r\n }\r\n } else {\r\n // Merge with previous string if possible\r\n const lastItem = verse.content[verse.content.length - 1];\r\n if (typeof lastItem === 'string') {\r\n verse.content[verse.content.length - 1] = lastItem + text;\r\n } else {\r\n verse.content.push(text);\r\n }\r\n }\r\n }\r\n\r\n private parseHebrewSubtitleContent(\r\n text: string,\r\n subtitle: HebrewSubtitle,\r\n chapter: Chapter\r\n ) {\r\n let cleanText = text.replace(/\\s+/g, ' ').trim();\r\n cleanText = cleanText.replace(/[{}]/g, '');\r\n cleanText = cleanText.replace(/@\\[.*?@\\]/g, '');\r\n\r\n const segmentRegex =\r\n /(<\\$F.*?\\$E>)|(<\\$R.*?\\$RE>)|(<[^>]+>)|(\\{.*?\\})/g;\r\n\r\n let lastIdx = 0;\r\n let m;\r\n\r\n while ((m = segmentRegex.exec(cleanText)) !== null) {\r\n const snippet = cleanText.substring(lastIdx, m.index);\r\n if (snippet) {\r\n this.addSubtitleText(subtitle, snippet);\r\n }\r\n\r\n if (m[1]) {\r\n const ref = this.processFootnoteForSubtitle(m[1], chapter);\r\n if (ref) {\r\n subtitle.content.push(ref);\r\n }\r\n } else if (m[2]) {\r\n // Cross Ref -> Ignore\r\n } else if (m[3]) {\r\n // Other tag -> Ignore\r\n } else if (m[4]) {\r\n const content = m[4].substring(1, m[4].length - 1);\r\n if (content) {\r\n this.addSubtitleText(subtitle, content);\r\n }\r\n }\r\n\r\n lastIdx = segmentRegex.lastIndex;\r\n }\r\n\r\n const remaining = cleanText.substring(lastIdx);\r\n if (remaining) {\r\n this.addSubtitleText(subtitle, remaining);\r\n }\r\n }\r\n\r\n private addSubtitleText(subtitle: HebrewSubtitle, text: string) {\r\n if (!text) return;\r\n\r\n const lastItem = subtitle.content[subtitle.content.length - 1];\r\n if (typeof lastItem === 'string') {\r\n subtitle.content[subtitle.content.length - 1] = lastItem + text;\r\n } else {\r\n subtitle.content.push(text);\r\n }\r\n }\r\n\r\n private processFootnote(block: string, verse: Verse, chapter: Chapter) {\r\n // block is <$F...$E>\r\n // Inner content has tags like <FN>, <FNC>, <N1>...\r\n // We want the readable text.\r\n // Remove known technical tags.\r\n\r\n const text = this.extractFootnoteText(block);\r\n\r\n if (!text) return;\r\n\r\n const noteId = chapter.footnotes.length + 1;\r\n\r\n const fn: Footnote = {\r\n noteId: noteId,\r\n text: text,\r\n caller: '+',\r\n reference: {\r\n chapter: chapter.number,\r\n verse: verse.number,\r\n },\r\n };\r\n chapter.footnotes.push(fn);\r\n\r\n verse.content.push({\r\n noteId: noteId,\r\n } as FootnoteReference);\r\n }\r\n\r\n private processFootnoteForSubtitle(\r\n block: string,\r\n chapter: Chapter\r\n ): FootnoteReference | null {\r\n const text = this.extractFootnoteText(block);\r\n\r\n if (!text) return null;\r\n\r\n const noteId = chapter.footnotes.length + 1;\r\n\r\n const fn: Footnote = {\r\n noteId: noteId,\r\n text: text,\r\n caller: '+',\r\n reference: {\r\n chapter: chapter.number,\r\n verse: 0,\r\n },\r\n };\r\n chapter.footnotes.push(fn);\r\n\r\n return {\r\n noteId: noteId,\r\n } as FootnoteReference;\r\n }\r\n\r\n private extractFootnoteText(block: string): string {\r\n // Remove outer $F...$E\r\n let inner = block.replace(/^<\\$F/, '').replace(/\\$E>$/, '');\r\n\r\n // Remove internal technical tags\r\n // Remove <FN>...</FN> completely\r\n inner = inner.replace(/<FN>.*?<\\/FN>/g, '');\r\n\r\n // Remove other tags <N1>, <FA> but keep content\r\n // Also remove italics curly braces (keep content inside)\r\n return inner\r\n .replace(/<[^>]+>/g, '')\r\n .replace(/[\\{\\}]/g, '')\r\n .trim();\r\n }\r\n}\r\n"],
5
+ "mappings": "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",
6
+ "names": ["text"]
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../parser/tyndale-xml-parser.ts"],
4
- "sourcesContent": ["import { getLogger } from '../log.js';\nimport { parseVerseReference } from '../utils';\nimport {\n CommentaryBookNode,\n CommentaryChapterNode,\n CommentaryParseTree,\n CommentaryVerseNode,\n} from './types';\n\nenum NodeType {\n Element = 1,\n Attribute = 2,\n Text = 3,\n}\n\nexport class TyndaleXmlParser {\n private _domParser: DOMParser;\n\n constructor(domParser: DOMParser) {\n this._domParser = domParser;\n }\n\n parse(xml: string): CommentaryParseTree {\n const logger = getLogger();\n const parser = this._domParser;\n const doc = parser.parseFromString(xml, 'application/xml');\n const rootElement = doc.documentElement;\n\n let tree: CommentaryParseTree = {\n type: 'commentary/root',\n books: [],\n };\n\n let books: Map<string, CommentaryBookNode> = new Map();\n\n function getBook(id: string): CommentaryBookNode {\n let bookNode = books.get(id);\n\n if (!bookNode) {\n bookNode = {\n type: 'book',\n book: id,\n introduction: null,\n chapters: [],\n };\n\n books.set(id, bookNode);\n tree.books.push(bookNode);\n }\n return bookNode;\n }\n\n function getChapter(\n book: CommentaryBookNode,\n number: number\n ): CommentaryChapterNode {\n let chapterNode = book.chapters.find(\n (chapter) => chapter.number === number\n );\n\n if (!chapterNode) {\n chapterNode = {\n type: 'chapter',\n number: number,\n introduction: null,\n verses: [],\n };\n\n book.chapters.push(chapterNode);\n }\n\n return chapterNode;\n }\n\n function getVerse(\n chapter: CommentaryChapterNode,\n number: number\n ): CommentaryVerseNode {\n let verseNode = chapter.verses.find(\n (verse) => verse.number === number\n );\n\n if (!verseNode) {\n verseNode = {\n type: 'verse',\n number: number,\n content: [],\n };\n\n chapter.verses.push(verseNode);\n }\n\n return verseNode;\n }\n\n function formatContent(node: Node, trim: boolean = true): string {\n let text: string;\n if (node.nodeName === 'p') {\n text = (node.textContent || '') + '\\n';\n } else if (node.nodeName === '#text') {\n text = node.textContent || '';\n } else if (node.nodeName === 'br') {\n text = '\\n';\n } else if (node.nodeType === NodeType.Element) {\n text = '';\n for (let child of node.childNodes) {\n text += formatContent(child, false);\n }\n } else {\n text = node.textContent || '';\n }\n\n if (trim) {\n const lines = text.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n // Trim extra whitespace and replace multiple spaces with a single space\n lines[i] = lines[i].replace(/\\s+/g, ' ').trim();\n }\n text = lines.join('\\n').trim();\n }\n\n return text;\n }\n\n const items = rootElement.querySelectorAll('item');\n\n for (let item of items) {\n const typename = item.getAttribute('typename');\n\n if (typename === 'StudyNote') {\n const refs = item.querySelector('refs')?.textContent;\n const body = item.querySelector('body');\n\n if (!refs || !body) {\n logger.warn(\n 'Skipping study note item without refs or body:',\n item\n );\n continue;\n }\n\n const ref = parseVerseReference(refs);\n\n if (!ref) {\n logger.warn('Failed to parse verse reference:', refs);\n continue;\n }\n\n const bookNode = getBook(ref.book);\n const chapterNode = getChapter(bookNode, ref.chapter);\n const verseNode = getVerse(chapterNode, ref.verse);\n verseNode.content.push(formatContent(body));\n } else if (typename === 'BookIntro') {\n const refs = item.querySelector('refs')?.textContent;\n const body = item.querySelector('body');\n\n if (!refs || !body) {\n logger.warn(\n 'Skipping book item without refs or body:',\n item\n );\n continue;\n }\n\n const ref = parseVerseReference(refs);\n\n if (!ref) {\n logger.warn('Failed to parse verse reference:', refs);\n continue;\n }\n\n const bookNode = getBook(ref.book);\n\n bookNode.introduction = formatContent(body);\n } else if (typename === 'BookIntroSummary') {\n const refs = item.querySelector('refs')?.textContent;\n const body = item.querySelector('body');\n\n if (!refs || !body) {\n logger.warn(\n 'Skipping book item without refs or body:',\n item\n );\n continue;\n }\n\n const ref = parseVerseReference(refs);\n\n if (!ref) {\n logger.warn('Failed to parse verse reference:', refs);\n continue;\n }\n\n const bookNode = getBook(ref.book);\n\n bookNode.introductionSummary = formatContent(body);\n } else if (typename === 'Profile') {\n const refs = item.querySelector('refs')?.textContent;\n const body = item.querySelector('body');\n const title = item.querySelector('title')?.textContent;\n const name = item.getAttribute('name');\n\n if (!refs || !body || !title || !name) {\n logger.warn(\n 'Skipping profile item without refs, body, or title:',\n item\n );\n continue;\n }\n\n const ref = parseVerseReference(refs);\n\n if (!ref) {\n logger.warn('Failed to parse verse reference:', refs);\n continue;\n }\n\n if (!tree.profiles) {\n tree.profiles = [];\n }\n\n // convert camelCase to kebab-case\n const id = toKebabCase(name);\n tree.profiles.push({\n id,\n subject: title,\n content: [formatContent(body)],\n reference: ref,\n });\n }\n }\n\n return tree;\n }\n}\n\nexport function toKebabCase(camelCase: string): string {\n return camelCase.replace(/([a-z])\\s*([A-Z])/g, '$1-$2').toLowerCase();\n}\n"],
4
+ "sourcesContent": ["import { getLogger } from '../log.js';\r\nimport { parseVerseReference } from '../utils';\r\nimport {\r\n CommentaryBookNode,\r\n CommentaryChapterNode,\r\n CommentaryParseTree,\r\n CommentaryVerseNode,\r\n} from './types';\r\n\r\nenum NodeType {\r\n Element = 1,\r\n Attribute = 2,\r\n Text = 3,\r\n}\r\n\r\nexport class TyndaleXmlParser {\r\n private _domParser: DOMParser;\r\n\r\n constructor(domParser: DOMParser) {\r\n this._domParser = domParser;\r\n }\r\n\r\n parse(xml: string): CommentaryParseTree {\r\n const logger = getLogger();\r\n const parser = this._domParser;\r\n const doc = parser.parseFromString(xml, 'application/xml');\r\n const rootElement = doc.documentElement;\r\n\r\n let tree: CommentaryParseTree = {\r\n type: 'commentary/root',\r\n books: [],\r\n };\r\n\r\n let books: Map<string, CommentaryBookNode> = new Map();\r\n\r\n function getBook(id: string): CommentaryBookNode {\r\n let bookNode = books.get(id);\r\n\r\n if (!bookNode) {\r\n bookNode = {\r\n type: 'book',\r\n book: id,\r\n introduction: null,\r\n chapters: [],\r\n };\r\n\r\n books.set(id, bookNode);\r\n tree.books.push(bookNode);\r\n }\r\n return bookNode;\r\n }\r\n\r\n function getChapter(\r\n book: CommentaryBookNode,\r\n number: number\r\n ): CommentaryChapterNode {\r\n let chapterNode = book.chapters.find(\r\n (chapter) => chapter.number === number\r\n );\r\n\r\n if (!chapterNode) {\r\n chapterNode = {\r\n type: 'chapter',\r\n number: number,\r\n introduction: null,\r\n verses: [],\r\n };\r\n\r\n book.chapters.push(chapterNode);\r\n }\r\n\r\n return chapterNode;\r\n }\r\n\r\n function getVerse(\r\n chapter: CommentaryChapterNode,\r\n number: number\r\n ): CommentaryVerseNode {\r\n let verseNode = chapter.verses.find(\r\n (verse) => verse.number === number\r\n );\r\n\r\n if (!verseNode) {\r\n verseNode = {\r\n type: 'verse',\r\n number: number,\r\n content: [],\r\n };\r\n\r\n chapter.verses.push(verseNode);\r\n }\r\n\r\n return verseNode;\r\n }\r\n\r\n function formatContent(node: Node, trim: boolean = true): string {\r\n let text: string;\r\n if (node.nodeName === 'p') {\r\n text = (node.textContent || '') + '\\n';\r\n } else if (node.nodeName === '#text') {\r\n text = node.textContent || '';\r\n } else if (node.nodeName === 'br') {\r\n text = '\\n';\r\n } else if (node.nodeType === NodeType.Element) {\r\n text = '';\r\n for (let child of node.childNodes) {\r\n text += formatContent(child, false);\r\n }\r\n } else {\r\n text = node.textContent || '';\r\n }\r\n\r\n if (trim) {\r\n const lines = text.split('\\n');\r\n for (let i = 0; i < lines.length; i++) {\r\n // Trim extra whitespace and replace multiple spaces with a single space\r\n lines[i] = lines[i].replace(/\\s+/g, ' ').trim();\r\n }\r\n text = lines.join('\\n').trim();\r\n }\r\n\r\n return text;\r\n }\r\n\r\n const items = rootElement.querySelectorAll('item');\r\n\r\n for (let item of items) {\r\n const typename = item.getAttribute('typename');\r\n\r\n if (typename === 'StudyNote') {\r\n const refs = item.querySelector('refs')?.textContent;\r\n const body = item.querySelector('body');\r\n\r\n if (!refs || !body) {\r\n logger.warn(\r\n 'Skipping study note item without refs or body:',\r\n item\r\n );\r\n continue;\r\n }\r\n\r\n const ref = parseVerseReference(refs);\r\n\r\n if (!ref) {\r\n logger.warn('Failed to parse verse reference:', refs);\r\n continue;\r\n }\r\n\r\n const bookNode = getBook(ref.book);\r\n const chapterNode = getChapter(bookNode, ref.chapter);\r\n const verseNode = getVerse(chapterNode, ref.verse);\r\n verseNode.content.push(formatContent(body));\r\n } else if (typename === 'BookIntro') {\r\n const refs = item.querySelector('refs')?.textContent;\r\n const body = item.querySelector('body');\r\n\r\n if (!refs || !body) {\r\n logger.warn(\r\n 'Skipping book item without refs or body:',\r\n item\r\n );\r\n continue;\r\n }\r\n\r\n const ref = parseVerseReference(refs);\r\n\r\n if (!ref) {\r\n logger.warn('Failed to parse verse reference:', refs);\r\n continue;\r\n }\r\n\r\n const bookNode = getBook(ref.book);\r\n\r\n bookNode.introduction = formatContent(body);\r\n } else if (typename === 'BookIntroSummary') {\r\n const refs = item.querySelector('refs')?.textContent;\r\n const body = item.querySelector('body');\r\n\r\n if (!refs || !body) {\r\n logger.warn(\r\n 'Skipping book item without refs or body:',\r\n item\r\n );\r\n continue;\r\n }\r\n\r\n const ref = parseVerseReference(refs);\r\n\r\n if (!ref) {\r\n logger.warn('Failed to parse verse reference:', refs);\r\n continue;\r\n }\r\n\r\n const bookNode = getBook(ref.book);\r\n\r\n bookNode.introductionSummary = formatContent(body);\r\n } else if (typename === 'Profile') {\r\n const refs = item.querySelector('refs')?.textContent;\r\n const body = item.querySelector('body');\r\n const title = item.querySelector('title')?.textContent;\r\n const name = item.getAttribute('name');\r\n\r\n if (!refs || !body || !title || !name) {\r\n logger.warn(\r\n 'Skipping profile item without refs, body, or title:',\r\n item\r\n );\r\n continue;\r\n }\r\n\r\n const ref = parseVerseReference(refs);\r\n\r\n if (!ref) {\r\n logger.warn('Failed to parse verse reference:', refs);\r\n continue;\r\n }\r\n\r\n if (!tree.profiles) {\r\n tree.profiles = [];\r\n }\r\n\r\n // convert camelCase to kebab-case\r\n const id = toKebabCase(name);\r\n tree.profiles.push({\r\n id,\r\n subject: title,\r\n content: [formatContent(body)],\r\n reference: ref,\r\n });\r\n }\r\n }\r\n\r\n return tree;\r\n }\r\n}\r\n\r\nexport function toKebabCase(camelCase: string): string {\r\n return camelCase.replace(/([a-z])\\s*([A-Z])/g, '$1-$2').toLowerCase();\r\n}\r\n"],
5
5
  "mappings": "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",
6
6
  "names": ["NodeType"]
7
7
  }