shamela 1.3.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -4
- package/dist/content.d.ts +101 -2
- package/dist/content.js +9 -1
- package/dist/content.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/{types-C693UiUs.d.ts → types-D7ziiVGq.d.ts} +15 -2
- package/dist/types.d.ts +2 -2
- package/dist/utils/constants.d.ts +20 -0
- package/dist/utils/constants.js +2 -0
- package/dist/utils/constants.js.map +1 -0
- package/package.json +20 -11
- package/dist/content-B60R0uYQ.js +0 -8
- package/dist/content-B60R0uYQ.js.map +0 -1
- package/dist/content-CwjMtCQl.d.ts +0 -54
package/README.md
CHANGED
|
@@ -54,6 +54,11 @@ A universal TypeScript library for accessing and downloading Maktabah Shamela v4
|
|
|
54
54
|
- [splitPageBodyFromFooter](#splitpagebodyfromfooter)
|
|
55
55
|
- [removeArabicNumericPageMarkers](#removearabicnumericpagemarkers)
|
|
56
56
|
- [removeTagsExceptSpan](#removetagsexceptspan)
|
|
57
|
+
- [normalizeLineEndings](#normalizelineendings)
|
|
58
|
+
- [stripHtmlTags](#striphtmltags)
|
|
59
|
+
- [htmlToMarkdown](#htmltomarkdown)
|
|
60
|
+
- [normalizeHtml](#normalizehtml)
|
|
61
|
+
- [normalizeTitleSpans](#normalizetitlespans)
|
|
57
62
|
- [Supporting Utilities](#supporting-utilities)
|
|
58
63
|
- [buildUrl](#buildurl)
|
|
59
64
|
- [httpsGet](#httpsget)
|
|
@@ -94,8 +99,9 @@ import { configure, getBook } from 'shamela';
|
|
|
94
99
|
// Configure API credentials
|
|
95
100
|
configure({
|
|
96
101
|
apiKey: process.env.SHAMELA_API_KEY,
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
// Configure only the endpoints you need:
|
|
103
|
+
booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT, // Required for book APIs
|
|
104
|
+
masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT, // Required for master APIs
|
|
99
105
|
// sqlJsWasmUrl is auto-detected in standard Node.js
|
|
100
106
|
});
|
|
101
107
|
|
|
@@ -196,11 +202,33 @@ This is ideal for:
|
|
|
196
202
|
- Processing pre-downloaded book data
|
|
197
203
|
|
|
198
204
|
**Available exports from `shamela/content`:**
|
|
199
|
-
- `parseContentRobust` - Parse HTML into structured lines
|
|
200
|
-
- `
|
|
205
|
+
- `parseContentRobust` - Parse HTML into structured lines preserving title spans
|
|
206
|
+
- `mapPageCharacterContent` - Normalize Arabic text with mapping rules
|
|
201
207
|
- `splitPageBodyFromFooter` - Separate body from footnotes
|
|
202
208
|
- `removeArabicNumericPageMarkers` - Remove page markers
|
|
203
209
|
- `removeTagsExceptSpan` - Strip HTML except spans
|
|
210
|
+
- `htmlToMarkdown` - Convert Shamela HTML to Markdown (title spans → `##` headers)
|
|
211
|
+
- `normalizeHtml` - Normalize hadeeth tags to standard spans
|
|
212
|
+
- `normalizeLineEndings` - Normalize line endings to Unix-style (`\n`)
|
|
213
|
+
- `stripHtmlTags` - Strip all HTML tags from content
|
|
214
|
+
- `normalizeTitleSpans` - Handle consecutive title spans (merge, split, or hierarchy)
|
|
215
|
+
|
|
216
|
+
### Extending Content Processing Rules
|
|
217
|
+
|
|
218
|
+
You can import `DEFAULT_MAPPING_RULES` from `shamela/constants` to extend or customize the character mapping used by `mapPageCharacterContent`:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { mapPageCharacterContent } from 'shamela/content';
|
|
222
|
+
import { DEFAULT_MAPPING_RULES } from 'shamela/constants';
|
|
223
|
+
|
|
224
|
+
// Extend default rules with custom mappings
|
|
225
|
+
const customRules = {
|
|
226
|
+
...DEFAULT_MAPPING_RULES,
|
|
227
|
+
'customPattern': 'replacement',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const processed = mapPageCharacterContent(rawContent, customRules);
|
|
231
|
+
```
|
|
204
232
|
|
|
205
233
|
## API Reference
|
|
206
234
|
|
|
@@ -439,6 +467,52 @@ Strips anchor and hadeeth tags while preserving nested `<span>` elements.
|
|
|
439
467
|
removeTagsExceptSpan(content: string): string
|
|
440
468
|
```
|
|
441
469
|
|
|
470
|
+
#### normalizeLineEndings
|
|
471
|
+
|
|
472
|
+
Normalizes line endings to Unix-style (`\n`). Converts Windows (`\r\n`) and old Mac (`\r`) line endings for consistent pattern matching across platforms.
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
normalizeLineEndings(content: string): string
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
#### stripHtmlTags
|
|
479
|
+
|
|
480
|
+
Strips all HTML tags from content, keeping only the text.
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
stripHtmlTags(html: string): string
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### htmlToMarkdown
|
|
487
|
+
|
|
488
|
+
Converts Shamela HTML to Markdown format. Title spans (`<span data-type="title">`) become `##` headers, narrator links are stripped but text is preserved.
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
htmlToMarkdown(html: string): string
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### normalizeHtml
|
|
495
|
+
|
|
496
|
+
Normalizes Shamela HTML for CSS styling by converting `<hadeeth-N>` tags to `<span class="hadeeth">` and closing `</hadeeth>` to `</span>`.
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
normalizeHtml(html: string): string
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
#### normalizeTitleSpans
|
|
503
|
+
|
|
504
|
+
Normalizes consecutive Shamela-style title spans. Shamela exports sometimes contain adjacent title spans that would produce multiple headings on one line when converted to Markdown.
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
normalizeTitleSpans(html: string, options: NormalizeTitleSpanOptions): string
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Options:**
|
|
511
|
+
- `strategy: 'merge'` - Combines adjacent titles into a single span with a separator
|
|
512
|
+
- `strategy: 'splitLines'` - Places each title on its own line
|
|
513
|
+
- `strategy: 'hierarchy'` - Converts subsequent titles to subtitles (`data-type="subtitle"`)
|
|
514
|
+
- `separator` (optional) - Separator used when merging (default: ` — `)
|
|
515
|
+
|
|
442
516
|
### Supporting Utilities
|
|
443
517
|
|
|
444
518
|
#### buildUrl
|
|
@@ -663,6 +737,40 @@ bun run format # Format code
|
|
|
663
737
|
bun run lint # Lint code
|
|
664
738
|
```
|
|
665
739
|
|
|
740
|
+
## Scripts Folder
|
|
741
|
+
|
|
742
|
+
The `scripts/` directory contains standalone reverse-engineering tools for extracting and decoding data from Shamela's desktop application databases. These are **development tools**, not part of the published npm package.
|
|
743
|
+
|
|
744
|
+
### Available Scripts
|
|
745
|
+
|
|
746
|
+
| Script | Purpose |
|
|
747
|
+
|--------|---------|
|
|
748
|
+
| `shamela-decoder.ts` | Core decoder for Shamela's custom character encoding |
|
|
749
|
+
| `export-narrators.ts` | Exports 18,989 narrator biographies from S1.db |
|
|
750
|
+
| `export-roots.ts` | Exports 3.2M Arabic word→root morphological mappings from S2.db |
|
|
751
|
+
|
|
752
|
+
### Running Scripts
|
|
753
|
+
|
|
754
|
+
```bash
|
|
755
|
+
# Export narrators to JSON
|
|
756
|
+
bun run scripts/export-narrators.ts
|
|
757
|
+
|
|
758
|
+
# Export Arabic roots
|
|
759
|
+
bun run scripts/export-roots.ts
|
|
760
|
+
|
|
761
|
+
# Run script tests
|
|
762
|
+
bun test scripts/
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Script Documentation
|
|
766
|
+
|
|
767
|
+
- `scripts/README.md` – Quick start guide and reverse-engineering methodology
|
|
768
|
+
- `scripts/AGENTS.md` – Comprehensive documentation including:
|
|
769
|
+
- Database schema discoveries
|
|
770
|
+
- Character encoding algorithm (substitution cipher)
|
|
771
|
+
- Validation approaches and coverage statistics
|
|
772
|
+
- Common patterns and debugging techniques
|
|
773
|
+
|
|
666
774
|
## License
|
|
667
775
|
|
|
668
776
|
MIT License - see LICENSE file for details.
|
package/dist/content.d.ts
CHANGED
|
@@ -1,2 +1,101 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { d as NormalizeTitleSpanOptions } from "./types-D7ziiVGq.js";
|
|
2
|
+
|
|
3
|
+
//#region src/content.d.ts
|
|
4
|
+
type Line = {
|
|
5
|
+
id?: string;
|
|
6
|
+
text: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes line endings to Unix-style (`\n`).
|
|
10
|
+
*
|
|
11
|
+
* Converts Windows (`\r\n`) and old Mac (`\r`) line endings to Unix style
|
|
12
|
+
* for consistent pattern matching across platforms.
|
|
13
|
+
*
|
|
14
|
+
* @param content - Raw content with potentially mixed line endings
|
|
15
|
+
* @returns Content with all line endings normalized to `\n`
|
|
16
|
+
*/
|
|
17
|
+
declare const normalizeLineEndings: (content: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Parses Shamela HTML content into structured lines while preserving headings.
|
|
20
|
+
*
|
|
21
|
+
* @param content - The raw HTML markup representing a page
|
|
22
|
+
* @returns An array of {@link Line} objects containing text and optional IDs
|
|
23
|
+
*/
|
|
24
|
+
declare const parseContentRobust: (content: string) => Line[];
|
|
25
|
+
/**
|
|
26
|
+
* Sanitises page content by applying regex replacement rules.
|
|
27
|
+
*
|
|
28
|
+
* @param text - The text to clean
|
|
29
|
+
* @param rules - Optional custom replacements, defaults to {@link DEFAULT_MAPPING_RULES}
|
|
30
|
+
* @returns The sanitised content
|
|
31
|
+
*/
|
|
32
|
+
declare const mapPageCharacterContent: (text: string, rules?: Record<string, string>) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Splits a page body from its trailing footnotes using a marker string.
|
|
35
|
+
*
|
|
36
|
+
* @param content - Combined body and footnote text
|
|
37
|
+
* @param footnoteMarker - Marker indicating the start of footnotes
|
|
38
|
+
* @returns A tuple containing the page body followed by the footnote section
|
|
39
|
+
*/
|
|
40
|
+
declare const splitPageBodyFromFooter: (content: string, footnoteMarker?: string) => readonly [string, string];
|
|
41
|
+
/**
|
|
42
|
+
* Removes Arabic numeral page markers enclosed in turtle ⦗ ⦘ brackets.
|
|
43
|
+
* Replaces the marker along with up to two preceding whitespace characters
|
|
44
|
+
* (space or carriage return) and up to one following whitespace character
|
|
45
|
+
* with a single space.
|
|
46
|
+
*
|
|
47
|
+
* @param text - Text potentially containing page markers
|
|
48
|
+
* @returns The text with numeric markers replaced by a single space
|
|
49
|
+
*/
|
|
50
|
+
declare const removeArabicNumericPageMarkers: (text: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* Removes anchor and hadeeth tags from the content while preserving spans.
|
|
53
|
+
*
|
|
54
|
+
* @param content - HTML string containing various tags
|
|
55
|
+
* @returns The content with only span tags retained
|
|
56
|
+
*/
|
|
57
|
+
declare const removeTagsExceptSpan: (content: string) => string;
|
|
58
|
+
/**
|
|
59
|
+
* Normalizes Shamela HTML for CSS styling:
|
|
60
|
+
* - Converts <hadeeth-N> to <span class="hadeeth">
|
|
61
|
+
* - Converts </hadeeth> or standalone <hadeeth> to </span>
|
|
62
|
+
*/
|
|
63
|
+
declare const normalizeHtml: (html: string) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Strip all HTML tags from content, keeping only text.
|
|
66
|
+
*
|
|
67
|
+
* @param html - HTML content
|
|
68
|
+
* @returns Plain text content
|
|
69
|
+
*/
|
|
70
|
+
declare const stripHtmlTags: (html: string) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Convert Shamela HTML to Markdown format for easier pattern matching.
|
|
73
|
+
*
|
|
74
|
+
* Transformations:
|
|
75
|
+
* - `<span data-type="title">text</span>` → `## text`
|
|
76
|
+
* - `<a href="inr://...">text</a>` → `text` (strip narrator links)
|
|
77
|
+
* - All other HTML tags → stripped
|
|
78
|
+
*
|
|
79
|
+
* Note: Content typically already has proper line breaks before title spans,
|
|
80
|
+
* so we don't add extra newlines around the ## header.
|
|
81
|
+
* Line ending normalization is handled by segmentPages.
|
|
82
|
+
*
|
|
83
|
+
* @param html - HTML content from Shamela
|
|
84
|
+
* @returns Markdown-formatted content
|
|
85
|
+
*/
|
|
86
|
+
declare const htmlToMarkdown: (html: string) => string;
|
|
87
|
+
/**
|
|
88
|
+
* Normalizes consecutive Shamela-style title spans.
|
|
89
|
+
*
|
|
90
|
+
* Shamela exports sometimes contain adjacent title spans like:
|
|
91
|
+
* `<span data-type="title">باب الميم</span><span data-type="title">من اسمه محمد</span>`
|
|
92
|
+
*
|
|
93
|
+
* If you naively convert each title span into a markdown heading, you can end up with:
|
|
94
|
+
* `## باب الميم ## من اسمه محمد` (two headings on one line).
|
|
95
|
+
*
|
|
96
|
+
* This helper rewrites the HTML so downstream HTML→Markdown conversion can stay simple and consistent.
|
|
97
|
+
*/
|
|
98
|
+
declare const normalizeTitleSpans: (html: string, options: NormalizeTitleSpanOptions) => string;
|
|
99
|
+
//#endregion
|
|
100
|
+
export { Line, htmlToMarkdown, mapPageCharacterContent, normalizeHtml, normalizeLineEndings, normalizeTitleSpans, parseContentRobust, removeArabicNumericPageMarkers, removeTagsExceptSpan, splitPageBodyFromFooter, stripHtmlTags };
|
|
101
|
+
//# sourceMappingURL=content.d.ts.map
|
package/dist/content.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{DEFAULT_MAPPING_RULES as e,FOOTNOTE_MARKER as t}from"./utils/constants.js";const n=/^[)\]\u00BB"”'’.,?!:\u061B\u060C\u061F\u06D4\u2026]+$/,r=e=>{let t=[];for(let r of e){let e=t[t.length-1];e&&n.test(r.text)?e.text+=r.text:t.push(r)}return t},i=e=>e.replace(/\r\n/g,`
|
|
2
|
+
`).replace(/\r/g,`
|
|
3
|
+
`).split(`
|
|
4
|
+
`).map(e=>e.trim()).filter(Boolean),a=e=>i(e).map(e=>({text:e})),o=(e,t)=>{let n=RegExp(`${t}\\s*=\\s*("([^"]*)"|'([^']*)'|([^s>]+))`,`i`),r=e.match(n);if(r)return r[2]??r[3]??r[4]},s=e=>{let t=[],n=/<[^>]+>/g,r=0,i;for(i=n.exec(e);i;){i.index>r&&t.push({type:`text`,value:e.slice(r,i.index)});let a=i[0],s=/^<\//.test(a),c=a.match(/^<\/?\s*([a-zA-Z0-9:-]+)/),l=c?c[1].toLowerCase():``;if(s)t.push({name:l,type:`end`});else{let e={};e.id=o(a,`id`),e[`data-type`]=o(a,`data-type`),t.push({attributes:e,name:l,type:`start`})}r=n.lastIndex,i=n.exec(e)}return r<e.length&&t.push({type:`text`,value:e.slice(r)}),t},c=(e,t)=>{let n=e.trim();return n?t?{id:t,text:n}:{text:n}:null},l=e=>{for(let t=e.length-1;t>=0;t--){let n=e[t];if(n.isTitle&&n.id)return n.id}},u=(e,t)=>{if(!e)return;let n=e.split(`
|
|
5
|
+
`);for(let e=0;e<n.length;e++){if(e>0){let e=c(t.currentText,t.currentId);e&&t.result.push(e),t.currentText=``,t.currentId=l(t.spanStack)||void 0}n[e]&&(t.currentText+=n[e])}},d=(e,t)=>{let n=e.attributes[`data-type`]===`title`,r;n&&(r=(e.attributes.id??``).replace(/^toc-/,``)),t.spanStack.push({id:r,isTitle:n}),n&&r&&!t.currentId&&(t.currentId=r)},f=e=>e.includes(`\r`)?e.replace(/\r\n?/g,`
|
|
6
|
+
`):e,p=e=>{if(e=f(e),!/<span[^>]*>/i.test(e))return r(a(e));let t=s(`<root>${e}</root>`),n={currentId:``,currentText:``,result:[],spanStack:[]};for(let e of t)e.type===`text`?u(e.value,n):e.type===`start`&&e.name===`span`?d(e,n):e.type===`end`&&e.name===`span`&&n.spanStack.pop();let i=c(n.currentText,n.currentId);return i&&n.result.push(i),r(n.result).filter(e=>e.text.length>0)},m=Object.entries(e).map(([e,t])=>({regex:new RegExp(e,`g`),replacement:t})),h=t=>{if(t===e)return m;let n=[];for(let e in t)n.push({regex:new RegExp(e,`g`),replacement:t[e]});return n},g=(t,n=e)=>{let r=h(n),i=t;for(let e=0;e<r.length;e++){let{regex:t,replacement:n}=r[e];i=i.replace(t,n)}return i},_=(e,n=t)=>{let r=``,i=e.indexOf(n);return i>=0&&(r=e.slice(i+n.length),e=e.slice(0,i)),[e,r]},v=e=>e.replace(/(?: |\r){0,2}⦗[\u0660-\u0669]+⦘(?: |\r)?/g,` `),y=e=>(e=e.replace(/<a[^>]*>(.*?)<\/a>/gs,`$1`),e=e.replace(/<hadeeth[^>]*>|<\/hadeeth>|<hadeeth-\d+>/gs,``),e),b=e=>e.replace(/<hadeeth-\d+>/gi,`<span class="hadeeth">`).replace(/<\s*\/?\s*hadeeth\s*>/gi,`</span>`),x=e=>e.replace(/<[^>]*>/g,``),S=e=>x(e.replace(/<span[^>]*data-type=["']title["'][^>]*>(.*?)<\/span>/gi,`## $1`).replace(/<a[^>]*href=["']inr:\/\/[^"']*["'][^>]*>(.*?)<\/a>/gi,`$1`)),C=(e,t)=>{let{separator:n=` — `,strategy:r}=t;if(!e)return e;let i=/<span\b[^>]*\bdata-type=(["'])title\1[^>]*>[\s\S]*?<\/span>/gi;return e.replace(/(?:<span\b[^>]*\bdata-type=(["'])title\1[^>]*>[\s\S]*?<\/span>\s*){2,}/gi,e=>{let t=e.match(i)??[];if(t.length<2)return e;if(r===`splitLines`)return t.join(`
|
|
7
|
+
`);if(r===`merge`){let e=t.map(e=>e.replace(/^<span\b[^>]*>/i,``).replace(/<\/span>$/i,``).trim()).filter(Boolean);return`${t[0].match(/^<span\b[^>]*>/i)?.[0]??`<span data-type="title">`}${e.join(n)}</span>`}return[t[0],...t.slice(1).map(e=>e.replace(/\bdata-type=(["'])title\1/i,`data-type="subtitle"`))].join(`
|
|
8
|
+
`)})};export{S as htmlToMarkdown,g as mapPageCharacterContent,b as normalizeHtml,f as normalizeLineEndings,C as normalizeTitleSpans,p as parseContentRobust,v as removeArabicNumericPageMarkers,y as removeTagsExceptSpan,_ as splitPageBodyFromFooter,x as stripHtmlTags};
|
|
9
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","names":["out: Line[]","tokens: Token[]","match: RegExpExecArray | null","attributes: Record<string, string | undefined>","id: string | undefined"],"sources":["../src/content.ts"],"sourcesContent":["import type { NormalizeTitleSpanOptions } from './types';\nimport { DEFAULT_MAPPING_RULES, FOOTNOTE_MARKER } from './utils/constants';\n\nexport type Line = {\n id?: string;\n text: string;\n};\n\nconst PUNCT_ONLY = /^[)\\]\\u00BB\"”'’.,?!:\\u061B\\u060C\\u061F\\u06D4\\u2026]+$/;\n\n/**\n * Merges punctuation-only lines into the preceding title when appropriate.\n *\n * @param lines - The processed line candidates to normalise\n * @returns A new array where dangling punctuation fragments are appended to titles\n */\nconst mergeDanglingPunctuation = (lines: Line[]): Line[] => {\n const out: Line[] = [];\n for (const item of lines) {\n const last = out[out.length - 1];\n if (last && PUNCT_ONLY.test(item.text)) {\n last.text += item.text;\n } else {\n out.push(item);\n }\n }\n return out;\n};\n\n/**\n * Normalises raw text into discrete line entries.\n *\n * @param text - Raw book content potentially containing inconsistent breaks\n * @returns An array of trimmed line strings with empty entries removed\n */\nconst splitIntoLines = (text: string) => {\n const normalized = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n return normalized\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n};\n\n/**\n * Converts plain text content into {@link Line} objects without title metadata.\n *\n * @param content - The text content to split into line structures\n * @returns A {@link Line} array wrapping each detected sentence fragment\n */\nconst processTextContent = (content: string): Line[] => {\n return splitIntoLines(content).map((line) => ({ text: line }));\n};\n\n/**\n * Extracts an attribute value from the provided HTML tag string.\n *\n * @param tag - Raw HTML tag source\n * @param name - Attribute name to locate\n * @returns The attribute value when found; otherwise undefined\n */\nconst extractAttribute = (tag: string, name: string): string | undefined => {\n const pattern = new RegExp(`${name}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)'|([^s>]+))`, 'i');\n const match = tag.match(pattern);\n if (!match) {\n return undefined;\n }\n return match[2] ?? match[3] ?? match[4];\n};\n\ntype Token =\n | { type: 'text'; value: string }\n | { type: 'start'; name: string; attributes: Record<string, string | undefined> }\n | { type: 'end'; name: string };\n\n/**\n * Breaks the provided HTML fragment into structural tokens.\n *\n * @param html - HTML fragment containing book content markup\n * @returns A token stream describing text and span boundaries\n */\nconst tokenize = (html: string): Token[] => {\n const tokens: Token[] = [];\n const tagRegex = /<[^>]+>/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n match = tagRegex.exec(html);\n\n while (match) {\n if (match.index > lastIndex) {\n tokens.push({ type: 'text', value: html.slice(lastIndex, match.index) });\n }\n\n const raw = match[0];\n const isEnd = /^<\\//.test(raw);\n const nameMatch = raw.match(/^<\\/?\\s*([a-zA-Z0-9:-]+)/);\n const name = nameMatch ? nameMatch[1].toLowerCase() : '';\n\n if (isEnd) {\n tokens.push({ name, type: 'end' });\n } else {\n const attributes: Record<string, string | undefined> = {};\n attributes.id = extractAttribute(raw, 'id');\n attributes['data-type'] = extractAttribute(raw, 'data-type');\n tokens.push({ attributes, name, type: 'start' });\n }\n\n lastIndex = tagRegex.lastIndex;\n match = tagRegex.exec(html);\n }\n\n if (lastIndex < html.length) {\n tokens.push({ type: 'text', value: html.slice(lastIndex) });\n }\n\n return tokens;\n};\n\n/**\n * Pushes the accumulated text as a new line to the result array.\n */\nconst createLine = (text: string, id?: string): Line | null => {\n const trimmed = text.trim();\n if (!trimmed) {\n return null;\n }\n return id ? { id, text: trimmed } : { text: trimmed };\n};\n\n/**\n * Finds the active title ID from the span stack.\n */\nconst getActiveTitleId = (spanStack: Array<{ isTitle: boolean; id?: string }>): string | undefined => {\n for (let i = spanStack.length - 1; i >= 0; i--) {\n const entry = spanStack[i];\n if (entry.isTitle && entry.id) {\n return entry.id;\n }\n }\n};\n\n/**\n * Processes text content by handling line breaks and maintaining title context.\n */\nconst processTextWithLineBreaks = (\n raw: string,\n state: {\n currentText: string;\n currentId?: string;\n result: Line[];\n spanStack: Array<{ isTitle: boolean; id?: string }>;\n },\n) => {\n if (!raw) {\n return;\n }\n\n const parts = raw.split('\\n');\n\n for (let i = 0; i < parts.length; i++) {\n // Push previous line when crossing a line break\n if (i > 0) {\n const line = createLine(state.currentText, state.currentId);\n if (line) {\n state.result.push(line);\n }\n state.currentText = '';\n\n // Preserve title ID if still inside a title span\n const activeTitleId = getActiveTitleId(state.spanStack);\n state.currentId = activeTitleId || undefined;\n }\n\n // Append the text part\n if (parts[i]) {\n state.currentText += parts[i];\n }\n }\n};\n\n/**\n * Handles the start of a span tag, updating the stack and current ID.\n */\nconst handleSpanStart = (\n token: { attributes: Record<string, string | undefined> },\n state: {\n currentId?: string;\n spanStack: Array<{ isTitle: boolean; id?: string }>;\n },\n) => {\n const dataType = token.attributes['data-type'];\n const isTitle = dataType === 'title';\n\n let id: string | undefined;\n if (isTitle) {\n const rawId = token.attributes.id ?? '';\n id = rawId.replace(/^toc-/, '');\n }\n\n state.spanStack.push({ id, isTitle });\n\n // First title span on the current physical line wins\n if (isTitle && id && !state.currentId) {\n state.currentId = id;\n }\n};\n\n/**\n * Normalizes line endings to Unix-style (`\\n`).\n *\n * Converts Windows (`\\r\\n`) and old Mac (`\\r`) line endings to Unix style\n * for consistent pattern matching across platforms.\n *\n * @param content - Raw content with potentially mixed line endings\n * @returns Content with all line endings normalized to `\\n`\n */\nexport const normalizeLineEndings = (content: string) => {\n return content.includes('\\r') ? content.replace(/\\r\\n?/g, '\\n') : content;\n};\n\n/**\n * Parses Shamela HTML content into structured lines while preserving headings.\n *\n * @param content - The raw HTML markup representing a page\n * @returns An array of {@link Line} objects containing text and optional IDs\n */\nexport const parseContentRobust = (content: string): Line[] => {\n // Normalize line endings first\n content = normalizeLineEndings(content);\n\n // Fast path when there are no span tags at all\n if (!/<span[^>]*>/i.test(content)) {\n return mergeDanglingPunctuation(processTextContent(content));\n }\n\n const tokens = tokenize(`<root>${content}</root>`);\n const state = {\n currentId: '',\n currentText: '',\n result: [] as Line[],\n spanStack: [] as Array<{ isTitle: boolean; id?: string }>,\n };\n\n // Process all tokens\n for (const token of tokens) {\n if (token.type === 'text') {\n processTextWithLineBreaks(token.value, state);\n } else if (token.type === 'start' && token.name === 'span') {\n handleSpanStart(token, state);\n } else if (token.type === 'end' && token.name === 'span') {\n // Closing a span does NOT end the line; trailing text stays on the same line\n state.spanStack.pop();\n }\n }\n\n // Flush any trailing text\n const finalLine = createLine(state.currentText, state.currentId);\n if (finalLine) {\n state.result.push(finalLine);\n }\n\n // Merge punctuation-only lines and drop empties\n return mergeDanglingPunctuation(state.result).filter((line) => line.text.length > 0);\n};\n\nconst DEFAULT_COMPILED_RULES = Object.entries(DEFAULT_MAPPING_RULES).map(([pattern, replacement]) => ({\n regex: new RegExp(pattern, 'g'),\n replacement,\n}));\n\n/**\n * Compiles sanitisation rules into RegExp objects for reuse.\n *\n * @param rules - Key/value replacements used during sanitisation\n * @returns A list of compiled regular expression rules\n */\nconst getCompiledRules = (rules: Record<string, string>) => {\n if (rules === DEFAULT_MAPPING_RULES) {\n return DEFAULT_COMPILED_RULES;\n }\n\n const compiled = [];\n for (const pattern in rules) {\n compiled.push({\n regex: new RegExp(pattern, 'g'),\n replacement: rules[pattern],\n });\n }\n return compiled;\n};\n\n/**\n * Sanitises page content by applying regex replacement rules.\n *\n * @param text - The text to clean\n * @param rules - Optional custom replacements, defaults to {@link DEFAULT_MAPPING_RULES}\n * @returns The sanitised content\n */\nexport const mapPageCharacterContent = (\n text: string,\n rules: Record<string, string> = DEFAULT_MAPPING_RULES,\n): string => {\n const compiledRules = getCompiledRules(rules);\n\n let content = text;\n for (let i = 0; i < compiledRules.length; i++) {\n const { regex, replacement } = compiledRules[i];\n content = content.replace(regex, replacement);\n }\n return content;\n};\n\n/**\n * Splits a page body from its trailing footnotes using a marker string.\n *\n * @param content - Combined body and footnote text\n * @param footnoteMarker - Marker indicating the start of footnotes\n * @returns A tuple containing the page body followed by the footnote section\n */\nexport const splitPageBodyFromFooter = (content: string, footnoteMarker = FOOTNOTE_MARKER) => {\n let footnote = '';\n const indexOfFootnote = content.indexOf(footnoteMarker);\n\n if (indexOfFootnote >= 0) {\n footnote = content.slice(indexOfFootnote + footnoteMarker.length);\n content = content.slice(0, indexOfFootnote);\n }\n\n return [content, footnote] as const;\n};\n\n/**\n * Removes Arabic numeral page markers enclosed in turtle ⦗ ⦘ brackets.\n * Replaces the marker along with up to two preceding whitespace characters\n * (space or carriage return) and up to one following whitespace character\n * with a single space.\n *\n * @param text - Text potentially containing page markers\n * @returns The text with numeric markers replaced by a single space\n */\nexport const removeArabicNumericPageMarkers = (text: string) => {\n return text.replace(/(?: |\\r){0,2}⦗[\\u0660-\\u0669]+⦘(?: |\\r)?/g, ' ');\n};\n\n/**\n * Removes anchor and hadeeth tags from the content while preserving spans.\n *\n * @param content - HTML string containing various tags\n * @returns The content with only span tags retained\n */\nexport const removeTagsExceptSpan = (content: string) => {\n // Remove <a> tags and their content, keeping only the text inside\n content = content.replace(/<a[^>]*>(.*?)<\\/a>/gs, '$1');\n\n // Remove <hadeeth> tags (both self-closing, with content, and numbered)\n content = content.replace(/<hadeeth[^>]*>|<\\/hadeeth>|<hadeeth-\\d+>/gs, '');\n\n return content;\n};\n\n/**\n * Normalizes Shamela HTML for CSS styling:\n * - Converts <hadeeth-N> to <span class=\"hadeeth\">\n * - Converts </hadeeth> or standalone <hadeeth> to </span>\n */\nexport const normalizeHtml = (html: string): string => {\n return html.replace(/<hadeeth-\\d+>/gi, '<span class=\"hadeeth\">').replace(/<\\s*\\/?\\s*hadeeth\\s*>/gi, '</span>');\n};\n\n/**\n * Strip all HTML tags from content, keeping only text.\n *\n * @param html - HTML content\n * @returns Plain text content\n */\nexport const stripHtmlTags = (html: string) => {\n return html.replace(/<[^>]*>/g, '');\n};\n\n/**\n * Convert Shamela HTML to Markdown format for easier pattern matching.\n *\n * Transformations:\n * - `<span data-type=\"title\">text</span>` → `## text`\n * - `<a href=\"inr://...\">text</a>` → `text` (strip narrator links)\n * - All other HTML tags → stripped\n *\n * Note: Content typically already has proper line breaks before title spans,\n * so we don't add extra newlines around the ## header.\n * Line ending normalization is handled by segmentPages.\n *\n * @param html - HTML content from Shamela\n * @returns Markdown-formatted content\n */\nexport const htmlToMarkdown = (html: string) => {\n const converted = html\n // Convert title spans to markdown headers (no extra newlines - content already has them)\n .replace(/<span[^>]*data-type=[\"']title[\"'][^>]*>(.*?)<\\/span>/gi, '## $1')\n // Strip narrator links but keep text\n .replace(/<a[^>]*href=[\"']inr:\\/\\/[^\"']*[\"'][^>]*>(.*?)<\\/a>/gi, '$1');\n\n return stripHtmlTags(converted);\n};\n\n/**\n * Normalizes consecutive Shamela-style title spans.\n *\n * Shamela exports sometimes contain adjacent title spans like:\n * `<span data-type=\"title\">باب الميم</span><span data-type=\"title\">من اسمه محمد</span>`\n *\n * If you naively convert each title span into a markdown heading, you can end up with:\n * `## باب الميم ## من اسمه محمد` (two headings on one line).\n *\n * This helper rewrites the HTML so downstream HTML→Markdown conversion can stay simple and consistent.\n */\nexport const normalizeTitleSpans = (html: string, options: NormalizeTitleSpanOptions): string => {\n const { separator = ' — ', strategy } = options;\n if (!html) {\n return html;\n }\n\n const titleSpanRegex = /<span\\b[^>]*\\bdata-type=([\"'])title\\1[^>]*>[\\s\\S]*?<\\/span>/gi;\n // Two or more title spans with optional whitespace between them\n const titleRunRegex = /(?:<span\\b[^>]*\\bdata-type=([\"'])title\\1[^>]*>[\\s\\S]*?<\\/span>\\s*){2,}/gi;\n\n return html.replace(titleRunRegex, (run) => {\n const spans = run.match(titleSpanRegex) ?? [];\n if (spans.length < 2) {\n return run;\n }\n\n if (strategy === 'splitLines') {\n return spans.join('\\n');\n }\n\n if (strategy === 'merge') {\n const texts = spans\n .map((s) =>\n s\n .replace(/^<span\\b[^>]*>/i, '')\n .replace(/<\\/span>$/i, '')\n .trim(),\n )\n .filter(Boolean);\n\n // Preserve the first span's opening tag (attributes) but replace its inner text.\n const firstOpenTagMatch = spans[0]!.match(/^<span\\b[^>]*>/i);\n const openTag = firstOpenTagMatch?.[0] ?? '<span data-type=\"title\">';\n return `${openTag}${texts.join(separator)}</span>`;\n }\n\n // hierarchy\n const first = spans[0];\n const rest = spans.slice(1).map((s) => s.replace(/\\bdata-type=([\"'])title\\1/i, 'data-type=\"subtitle\"'));\n return [first, ...rest].join('\\n');\n });\n};\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { _ as TitleSpanStrategy, a as DownloadBookOptions, c as GetBookMetadataResponsePayload, d as NormalizeTitleSpanOptions, f as OutputOptions, g as Title, h as ShamelaConfigKey, i as Category, l as GetMasterMetadataResponsePayload, m as ShamelaConfig, n as Book, o as DownloadMasterOptions, p as Page, r as BookData, s as GetBookMetadataOptions, t as Author, u as MasterData } from "./types-D7ziiVGq.js";
|
|
2
|
+
import { Line, htmlToMarkdown, mapPageCharacterContent, normalizeHtml, normalizeLineEndings, normalizeTitleSpans, parseContentRobust, removeArabicNumericPageMarkers, removeTagsExceptSpan, splitPageBodyFromFooter, stripHtmlTags } from "./content.js";
|
|
3
|
+
import { DEFAULT_MAPPING_RULES, FOOTNOTE_MARKER } from "./utils/constants.js";
|
|
3
4
|
|
|
4
5
|
//#region src/api.d.ts
|
|
5
6
|
|
|
@@ -179,5 +180,5 @@ declare const configure: (config: ConfigureOptions) => void;
|
|
|
179
180
|
*/
|
|
180
181
|
declare const resetConfig: () => void;
|
|
181
182
|
//#endregion
|
|
182
|
-
export { Author, Book, BookData, Category, type ConfigureOptions, DownloadBookOptions, DownloadMasterOptions, GetBookMetadataOptions, GetBookMetadataResponsePayload, GetMasterMetadataResponsePayload, Line, type Logger, MasterData, OutputOptions, Page, ShamelaConfig, ShamelaConfigKey, Title, configure, downloadBook, downloadMasterDatabase, getBook, getBookMetadata, getCoverUrl, getMaster, getMasterMetadata, normalizeHtml, parseContentRobust, removeArabicNumericPageMarkers, removeTagsExceptSpan, resetConfig,
|
|
183
|
+
export { Author, Book, BookData, Category, type ConfigureOptions, DEFAULT_MAPPING_RULES, DownloadBookOptions, DownloadMasterOptions, FOOTNOTE_MARKER, GetBookMetadataOptions, GetBookMetadataResponsePayload, GetMasterMetadataResponsePayload, Line, type Logger, MasterData, NormalizeTitleSpanOptions, OutputOptions, Page, ShamelaConfig, ShamelaConfigKey, Title, TitleSpanStrategy, configure, downloadBook, downloadMasterDatabase, getBook, getBookMetadata, getCoverUrl, getMaster, getMasterMetadata, htmlToMarkdown, mapPageCharacterContent, normalizeHtml, normalizeLineEndings, normalizeTitleSpans, parseContentRobust, removeArabicNumericPageMarkers, removeTagsExceptSpan, resetConfig, splitPageBodyFromFooter, stripHtmlTags };
|
|
183
184
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{DEFAULT_MAPPING_RULES as e,DEFAULT_MASTER_METADATA_VERSION as t,FOOTNOTE_MARKER as n}from"./utils/constants.js";import{htmlToMarkdown as r,mapPageCharacterContent as i,normalizeHtml as a,normalizeLineEndings as o,normalizeTitleSpans as s,parseContentRobust as ee,removeArabicNumericPageMarkers as te,removeTagsExceptSpan as ne,splitPageBodyFromFooter as re,stripHtmlTags as ie}from"./content.js";import"./types.js";import ae from"sql.js";import{unzipSync as oe}from"fflate";var c=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function.")});const l=Object.freeze({debug:()=>{},error:()=>{},info:()=>{},warn:()=>{}});let u=l;const se=e=>{if(!e){u=l;return}let t=[`debug`,`error`,`info`,`warn`].find(t=>typeof e[t]!=`function`);if(t)throw Error(`Logger must implement debug, error, info, and warn methods. Missing: ${String(t)}`);u=e},ce=()=>u,le=()=>{u=l};var d=new Proxy({},{get:(e,t)=>{let n=ce(),r=n[t];return typeof r==`function`?(...e)=>r.apply(n,e):r}});let f={};const p={apiKey:`SHAMELA_API_KEY`,booksEndpoint:`SHAMELA_API_BOOKS_ENDPOINT`,masterPatchEndpoint:`SHAMELA_API_MASTER_PATCH_ENDPOINT`,sqlJsWasmUrl:`SHAMELA_SQLJS_WASM_URL`},ue=typeof process<`u`&&!!process?.env,m=e=>{let t=f[e];if(t!==void 0)return t;let n=p[e];if(ue)return process.env[n]},h=e=>{let{logger:t,...n}=e;`logger`in e&&se(t),f={...f,...n}},g=e=>e===`fetchImplementation`?f.fetchImplementation:m(e),de=()=>({apiKey:m(`apiKey`),booksEndpoint:m(`booksEndpoint`),fetchImplementation:f.fetchImplementation,masterPatchEndpoint:m(`masterPatchEndpoint`),sqlJsWasmUrl:m(`sqlJsWasmUrl`)}),_=e=>{if(e===`fetchImplementation`)throw Error(`fetchImplementation must be provided via configure().`);let t=g(e);if(!t)throw Error(`${p[e]} environment variable not set`);return t},fe=()=>{f={},le()};let v=function(e){return e.Authors=`author`,e.Books=`book`,e.Categories=`category`,e.Page=`page`,e.Title=`title`,e}({});const y=(e,t)=>e.query(`PRAGMA table_info(${t})`).all(),b=(e,t)=>!!e.query(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?1`).get(t),x=(e,t)=>b(e,t)?e.query(`SELECT * FROM ${t}`).all():[],S=e=>String(e.is_deleted)===`1`,C=(e,t,n)=>{let r={};for(let i of n){if(i===`id`){r.id=(t??e)?.id??null;continue}if(t&&i in t){let e=t[i];if(e!==`#`&&e!=null){r[i]=e;continue}}if(e&&i in e){r[i]=e[i];continue}r[i]=null}return r},pe=(e,t,n)=>{let r=new Set,i=new Map;for(let t of e)r.add(String(t.id));for(let e of t)i.set(String(e.id),e);let a=[];for(let t of e){let e=i.get(String(t.id));e&&S(e)||a.push(C(t,e,n))}for(let e of t){let t=String(e.id);r.has(t)||S(e)||a.push(C(void 0,e,n))}return a},me=(e,t,n,r)=>{if(r.length===0)return;let i=n.map(()=>`?`).join(`,`),a=e.prepare(`INSERT INTO ${t} (${n.join(`,`)}) VALUES (${i})`);r.forEach(e=>{let t=n.map(t=>t in e?e[t]:null);a.run(...t)}),a.finalize()},he=(e,t,n)=>{let r=t.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?1`).get(n);return r?.sql?(e.run(`DROP TABLE IF EXISTS ${n}`),e.run(r.sql),!0):(d.warn(`${n} table definition missing in source database`),!1)},w=(e,t,n,r)=>{if(!b(t,r)){d.warn(`${r} table missing in source database`);return}if(!he(e,t,r))return;let i=y(t,r),a=n&&b(n,r)?y(n,r):[],o=i.map(e=>e.name);for(let t of a)if(!o.includes(t.name)){let n=t.type&&t.type.length>0?t.type:`TEXT`;e.run(`ALTER TABLE ${r} ADD COLUMN ${t.name} ${n}`),o.push(t.name)}me(e,r,o,pe(x(t,r),n?x(n,r):[],o))},ge=(e,t,n)=>{e.transaction(()=>{w(e,t,n,v.Page),w(e,t,n,v.Title)})()},_e=(e,t)=>{e.transaction(()=>{w(e,t,null,v.Page),w(e,t,null,v.Title)})()},ve=e=>{e.run(`CREATE TABLE ${v.Page} (
|
|
2
2
|
id INTEGER,
|
|
3
3
|
content TEXT,
|
|
4
4
|
part TEXT,
|
|
@@ -6,21 +6,21 @@ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./content-B60R0uYQ.js";i
|
|
|
6
6
|
number TEXT,
|
|
7
7
|
services TEXT,
|
|
8
8
|
is_deleted TEXT
|
|
9
|
-
)`),e.run(`CREATE TABLE ${
|
|
9
|
+
)`),e.run(`CREATE TABLE ${v.Title} (
|
|
10
10
|
id INTEGER,
|
|
11
11
|
content TEXT,
|
|
12
12
|
page INTEGER,
|
|
13
13
|
parent INTEGER,
|
|
14
14
|
is_deleted TEXT
|
|
15
|
-
)`)},
|
|
16
|
-
`);throw Error(e)}}else
|
|
15
|
+
)`)},ye=e=>e.query(`SELECT * FROM ${v.Page}`).all(),be=e=>e.query(`SELECT * FROM ${v.Title}`).all(),T=e=>({pages:ye(e),titles:be(e)}),E=e=>{try{return c(`node:fs`).existsSync(e)}catch{return!1}},xe=()=>{try{let e=c(`node:path`),t=process.cwd(),n=[e.join(t,`node_modules`,`sql.js`,`dist`,`sql-wasm.wasm`),e.join(t,`..`,`node_modules`,`sql.js`,`dist`,`sql-wasm.wasm`),e.join(t,`../..`,`node_modules`,`sql.js`,`dist`,`sql-wasm.wasm`),e.join(t,`.next`,`server`,`node_modules`,`sql.js`,`dist`,`sql-wasm.wasm`)];for(let e of n)if(E(e))return e}catch{}},D=()=>{try{let e=c.resolve(`sql.js`),t=c(`node:path`),n=t.dirname(e),r=t.join(n,`dist`,`sql-wasm.wasm`);if(E(r))return r}catch{}},O=()=>{try{let e=c(`node:path`),t=c.resolve.paths(`sql.js`)||[];for(let n of t){let t=e.join(n,`sql.js`,`dist`,`sql-wasm.wasm`);if(E(t))return t}}catch{}},k=()=>{try{let e=new URL(`../../node_modules/sql.js/dist/sql-wasm.wasm`,import.meta.url),t=decodeURIComponent(e.pathname),n=process.platform===`win32`&&t.startsWith(`/`)?t.slice(1):t;if(E(n))return n}catch{}},A=()=>{let e=``;return c?.resolve!==void 0&&(e=D()),!e&&`cwd`in process&&(e=xe()),!e&&c?.resolve?.paths!==void 0&&(e=O()),!e&&import.meta.url&&(e=k()),e};var j=class{constructor(e){this.statement=e}run=(...e)=>{e.length>0&&this.statement.bind(e),this.statement.step(),this.statement.reset()};finalize=()=>{this.statement.free()}},M=class{constructor(e){this.db=e}run=(e,t=[])=>{this.db.run(e,t)};prepare=e=>new j(this.db.prepare(e));query=e=>({all:(...t)=>this.all(e,t),get:(...t)=>this.get(e,t)});transaction=e=>()=>{this.db.run(`BEGIN TRANSACTION`);try{e(),this.db.run(`COMMIT`)}catch(e){throw this.db.run(`ROLLBACK`),e}};close=()=>{this.db.close()};export=()=>this.db.export();all=(e,t)=>{let n=this.db.prepare(e);try{t.length>0&&n.bind(t);let e=[];for(;n.step();)e.push(n.getAsObject());return e}finally{n.free()}};get=(e,t)=>{let[n]=this.all(e,t);return n}};let N=null,P=null;const Se=typeof process<`u`&&!!process?.versions?.node,Ce=()=>{if(!P){let e=g(`sqlJsWasmUrl`);if(e)P=e;else if(Se){let e=A();if(e)P=e;else{let e=[`Unable to automatically locate sql-wasm.wasm file.`,`This can happen in bundled environments (Next.js, webpack, etc.).`,``,`Quick fix - add this to your code before using shamela:`,``,` import { configure, createNodeConfig } from "shamela";`,` configure(createNodeConfig({`,` apiKey: process.env.SHAMELA_API_KEY,`,` booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT,`,` masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT,`,` }));`,``,`Or manually specify the path:`,``,` import { configure } from "shamela";`,` import { join } from "node:path";`,` configure({`,` sqlJsWasmUrl: join(process.cwd(), "node_modules", "sql.js", "dist", "sql-wasm.wasm")`,` });`].join(`
|
|
16
|
+
`);throw Error(e)}}else P=`https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/sql-wasm.wasm`}return P},F=()=>(N||=ae({locateFile:()=>Ce()}),N),I=async()=>new M(new(await(F())).Database),L=async e=>new M(new(await(F())).Database(e)),we=(e,t,n)=>{let r=t.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?1`).get(n);if(!r?.sql)throw Error(`Missing table definition for ${n} in source database`);e.run(`DROP TABLE IF EXISTS ${n}`),e.run(r.sql)},Te=async(e,t)=>{let n={author:v.Authors,book:v.Books,category:v.Categories},r={};for(let e of t){let t=n[(e.name.split(`/`).pop()?.split(`\\`).pop()??e.name).replace(/\.(sqlite|db)$/i,``).toLowerCase()];t&&(r[t]=await L(e.data))}try{let t=Object.entries(r);e.transaction(()=>{for(let[n,r]of t){we(e,r,n);let t=r.query(`PRAGMA table_info(${n})`).all().map(e=>e.name);if(t.length===0)continue;let i=r.query(`SELECT * FROM ${n}`).all();if(i.length===0)continue;let a=t.map(()=>`?`).join(`,`),o=t.map(e=>e===`order`?`"order"`:e),s=e.prepare(`INSERT INTO ${n} (${o.join(`,`)}) VALUES (${a})`);try{for(let e of i){let n=t.map(t=>t in e?e[t]:null);s.run(...n)}}finally{s.finalize()}}})()}finally{Object.values(r).forEach(e=>e?.close())}},R=(e,t,n)=>{e.run(`DROP VIEW IF EXISTS ${t}`),e.run(`CREATE VIEW ${t} AS SELECT * FROM ${n}`)},Ee=e=>{e.run(`CREATE TABLE ${v.Authors} (
|
|
17
17
|
id INTEGER,
|
|
18
18
|
is_deleted TEXT,
|
|
19
19
|
name TEXT,
|
|
20
20
|
biography TEXT,
|
|
21
21
|
death_text TEXT,
|
|
22
22
|
death_number TEXT
|
|
23
|
-
)`),e.run(`CREATE TABLE ${
|
|
23
|
+
)`),e.run(`CREATE TABLE ${v.Books} (
|
|
24
24
|
id INTEGER,
|
|
25
25
|
name TEXT,
|
|
26
26
|
is_deleted TEXT,
|
|
@@ -35,10 +35,10 @@ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./content-B60R0uYQ.js";i
|
|
|
35
35
|
hint TEXT,
|
|
36
36
|
pdf_links TEXT,
|
|
37
37
|
metadata TEXT
|
|
38
|
-
)`),e.run(`CREATE TABLE ${
|
|
38
|
+
)`),e.run(`CREATE TABLE ${v.Categories} (
|
|
39
39
|
id INTEGER,
|
|
40
40
|
is_deleted TEXT,
|
|
41
41
|
"order" TEXT,
|
|
42
42
|
name TEXT
|
|
43
|
-
)`),R(e,`authors`,
|
|
43
|
+
)`),R(e,`authors`,v.Authors),R(e,`books`,v.Books),R(e,`categories`,v.Categories)},De=e=>e.query(`SELECT * FROM ${v.Authors}`).all(),Oe=e=>e.query(`SELECT * FROM ${v.Books}`).all(),ke=e=>e.query(`SELECT * FROM ${v.Categories}`).all(),z=(e,t)=>({authors:De(e),books:Oe(e),categories:ke(e),version:t}),B=(e,t=[`api_key`,`token`,`password`,`secret`,`auth`])=>{let n=typeof e==`string`?new URL(e):new URL(e.toString());return t.forEach(e=>{let t=n.searchParams.get(e);if(t&&t.length>6){let r=`${t.slice(0,3)}***${t.slice(-3)}`;n.searchParams.set(e,r)}else t&&n.searchParams.set(e,`***`)}),n.toString()},Ae=e=>({content:e.content,id:e.id,...e.number&&{number:e.number},...e.page&&{page:Number(e.page)},...e.part&&{part:e.part}}),je=e=>{let t=Number(e.parent);return{content:e.content,id:e.id,page:Number(e.page),...t&&{parent:t}}},V=e=>{let t=new URL(e);return t.protocol=`https`,t.toString()},H=e=>/\.(sqlite|db)$/i.test(e.name),U=e=>e.find(H),W=e=>{let t=/\.([^.]+)$/.exec(e);return t?`.${t[1].toLowerCase()}`:``},G=(e,t,n=!0)=>{let r=new URL(e),i=new URLSearchParams;return Object.entries(t).forEach(([e,t])=>{i.append(e,t.toString())}),n&&i.append(`api_key`,_(`apiKey`)),r.search=i.toString(),r},K=async(e,t={})=>{let n=typeof e==`string`?e:e.toString(),r=await(t.fetchImpl??de().fetchImplementation??fetch)(n);if(!r.ok)throw Error(`Error making request: ${r.status} ${r.statusText}`);if((r.headers.get(`content-type`)??``).includes(`application/json`))return await r.json();let i=await r.arrayBuffer();return new Uint8Array(i)},Me=typeof process<`u`&&!!process?.versions?.node,Ne=async()=>{if(!Me)throw Error(`File system operations are only supported in Node.js environments`);return import(`node:fs/promises`)},Pe=async e=>{let[t,n]=await Promise.all([Ne(),import(`node:path`)]),r=n.dirname(e);return await t.mkdir(r,{recursive:!0}),t},q=async e=>{let t=await K(e),n=t instanceof Uint8Array?t.length:t&&typeof t.byteLength==`number`?t.byteLength:0;return d.debug(`unzipFromUrl:bytes`,n),new Promise((e,n)=>{let r=t instanceof Uint8Array?t:new Uint8Array(t);try{let t=oe(r),n=Object.entries(t).map(([e,t])=>({data:t,name:e}));d.debug(`unzipFromUrl:entries`,n.map(e=>e.name)),e(n)}catch(e){n(Error(`Error processing URL: ${e.message}`))}})},J=async(e,t)=>{if(e.writer){await e.writer(t);return}if(!e.path)throw Error(`Output options must include either a writer or a path`);let n=await Pe(e.path);typeof t==`string`?await n.writeFile(e.path,t,`utf-8`):await n.writeFile(e.path,t)},Y=[`author.sqlite`,`book.sqlite`,`category.sqlite`],Fe=e=>{let t=new Set(e.map(e=>e.match(/[^\\/]+$/)?.[0]??e).map(e=>e.toLowerCase()));return Y.every(e=>t.has(e.toLowerCase()))},X=async(e,t)=>{d.info(`Setting up book database for ${e}`);let n=t||await Q(e),r=n.minorReleaseUrl?q(n.minorReleaseUrl):Promise.resolve([]),[i,a]=await Promise.all([q(n.majorReleaseUrl),r]),o=U(i);if(!o)throw Error(`Unable to locate book database in archive`);let s=await I();try{d.info(`Creating tables`),ve(s);let e=await L(o.data);try{let t=U(a);if(t){d.info(`Applying patches from ${t.name} to ${o.name}`);let n=await L(t.data);try{ge(s,e,n)}finally{n.close()}}else d.info(`Copying table data from ${o.name}`),_e(s,e)}finally{e.close()}return{cleanup:async()=>{s.close()},client:s}}catch(e){throw s.close(),e}},Z=async e=>{d.info(`Setting up master database`);let n=e||await $(t);d.info(`Downloading master database ${n.version} from: ${B(n.url)}`);let r=await q(V(n.url));if(d.debug?.(`sourceTables downloaded: ${r.map(e=>e.name).toString()}`),!Fe(r.map(e=>e.name)))throw d.error(`Some source tables were not found: ${r.map(e=>e.name).toString()}`),Error(`Expected tables not found!`);let i=await I();try{return d.info(`Creating master tables`),Ee(i),d.info(`Copying data to master table`),await Te(i,r.filter(H)),{cleanup:async()=>{i.close()},client:i,version:n.version}}catch(e){throw i.close(),e}},Q=async(e,t)=>{let n=G(`${_(`booksEndpoint`)}/${e}`,{major_release:(t?.majorVersion||0).toString(),minor_release:(t?.minorVersion||0).toString()});d.info(`Fetching shamela.ws book link: ${B(n)}`);try{let e=await K(n);return{majorRelease:e.major_release,majorReleaseUrl:V(e.major_release_url),...e.minor_release_url&&{minorReleaseUrl:V(e.minor_release_url)},...e.minor_release_url&&{minorRelease:e.minor_release}}}catch(e){throw Error(`Error fetching book metadata: ${e.message}`)}},Ie=async(e,t)=>{if(d.info(`downloadBook ${e} ${JSON.stringify(t)}`),!t.outputFile.path)throw Error(`outputFile.path must be provided to determine output format`);let n=W(t.outputFile.path).toLowerCase(),{client:r,cleanup:i}=await X(e,t?.bookMetadata);try{if(n===`.json`){let e=await T(r);await J(t.outputFile,JSON.stringify(e,null,2))}else if(n===`.db`||n===`.sqlite`){let e=r.export();await J(t.outputFile,e)}else throw Error(`Unsupported output extension: ${n}`)}finally{await i()}return t.outputFile.path},$=async(e=0)=>{let t=G(_(`masterPatchEndpoint`),{version:e.toString()});d.info(`Fetching shamela.ws master database patch link: ${B(t)}`);try{let e=await K(t);return{url:e.patch_url,version:e.version}}catch(e){throw Error(`Error fetching master patch: ${e.message}`)}},Le=e=>{let t=_(`masterPatchEndpoint`),{origin:n}=new URL(t);return`${n}/covers/${e}.jpg`},Re=async e=>{if(d.info(`downloadMasterDatabase ${JSON.stringify(e)}`),!e.outputFile.path)throw Error(`outputFile.path must be provided to determine output format`);let t=W(e.outputFile.path),{client:n,cleanup:r,version:i}=await Z(e.masterMetadata);try{if(t===`.json`){let t=z(n,i);await J(e.outputFile,JSON.stringify(t,null,2))}else if(t===`.db`||t===`.sqlite`)await J(e.outputFile,n.export());else throw Error(`Unsupported output extension: ${t}`)}finally{await r()}return e.outputFile.path},ze=async e=>{d.info(`getBook ${e}`);let{client:t,cleanup:n}=await X(e);try{let e=await T(t);return{pages:e.pages.map(Ae),titles:e.titles.map(je)}}finally{await n()}},Be=async()=>{d.info(`getMaster`);let{client:e,cleanup:t,version:n}=await Z();try{return z(e,n)}finally{await t()}};export{e as DEFAULT_MAPPING_RULES,n as FOOTNOTE_MARKER,h as configure,Ie as downloadBook,Re as downloadMasterDatabase,ze as getBook,Q as getBookMetadata,Le as getCoverUrl,Be as getMaster,$ as getMasterMetadata,r as htmlToMarkdown,i as mapPageCharacterContent,a as normalizeHtml,o as normalizeLineEndings,s as normalizeTitleSpans,ee as parseContentRobust,te as removeArabicNumericPageMarkers,ne as removeTagsExceptSpan,fe as resetConfig,re as splitPageBodyFromFooter,ie as stripHtmlTags};
|
|
44
44
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["SILENT_LOGGER: Logger","currentLogger: Logger","loggerProxy: Logger","runtimeConfig: Partial<ShamelaConfig>","ENV_MAP: Record<Exclude<ShamelaConfigKey, 'fetchImplementation'>, string>","merged: Row","merged: Row[]","ensureTableSchema","statement: Statement","db: SqlJsDatabase","rows: QueryRow[]","sqlPromise: Promise<SqlJsStatic> | null","resolvedWasmPath: string | null","isNodeEnvironment","TABLE_MAP: Record<string, Tables>","tableDbs: Partial<Record<Tables, SqliteDatabase>>","createTables","getData","error: any","bookResponse: GetBookMetadataResponsePayload","error: any","getBookData","response: Record<string, any>","getMasterData"],"sources":["../src/utils/logger.ts","../src/config.ts","../src/db/types.ts","../src/db/book.ts","../src/utils/wasm.ts","../src/db/sqlite.ts","../src/db/master.ts","../src/utils/common.ts","../src/utils/downloads.ts","../src/utils/network.ts","../src/utils/io.ts","../src/utils/validation.ts","../src/api.ts"],"sourcesContent":["/**\n * Signature accepted by logger methods.\n */\nexport type LogFunction = (...args: unknown[]) => void;\n\n/**\n * Contract expected from logger implementations consumed by the library.\n */\nexport interface Logger {\n debug: LogFunction;\n error: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n}\n\n/**\n * No-op logger used when consumers do not provide their own implementation.\n */\nexport const SILENT_LOGGER: Logger = Object.freeze({\n debug: () => {},\n error: () => {},\n info: () => {},\n warn: () => {},\n});\n\nlet currentLogger: Logger = SILENT_LOGGER;\n\n/**\n * Configures the active logger or falls back to {@link SILENT_LOGGER} when undefined.\n *\n * @param newLogger - The logger instance to use for subsequent log calls\n * @throws {Error} When the provided logger does not implement the required methods\n */\nexport const configureLogger = (newLogger?: Logger) => {\n if (!newLogger) {\n currentLogger = SILENT_LOGGER;\n return;\n }\n\n const requiredMethods: Array<keyof Logger> = ['debug', 'error', 'info', 'warn'];\n const missingMethod = requiredMethods.find((method) => typeof newLogger[method] !== 'function');\n\n if (missingMethod) {\n throw new Error(\n `Logger must implement debug, error, info, and warn methods. Missing: ${String(missingMethod)}`,\n );\n }\n\n currentLogger = newLogger;\n};\n\n/**\n * Retrieves the currently configured logger.\n */\nexport const getLogger = () => currentLogger;\n\n/**\n * Restores the logger configuration back to {@link SILENT_LOGGER}.\n */\nexport const resetLogger = () => {\n currentLogger = SILENT_LOGGER;\n};\n\n/**\n * Proxy that delegates logging calls to the active logger at invocation time.\n */\nconst loggerProxy: Logger = new Proxy({} as Logger, {\n get: (_target, property: keyof Logger) => {\n const activeLogger = getLogger();\n const value = activeLogger[property];\n\n if (typeof value === 'function') {\n return (...args: unknown[]) => (value as LogFunction).apply(activeLogger, args);\n }\n\n return value;\n },\n}) as Logger;\n\nexport default loggerProxy;\n","import type { ShamelaConfig, ShamelaConfigKey } from './types';\nimport type { Logger } from './utils/logger';\nimport { configureLogger, resetLogger } from './utils/logger';\n\n/**\n * Mutable runtime configuration overrides supplied at runtime via {@link configure}.\n */\nlet runtimeConfig: Partial<ShamelaConfig> = {};\n\n/**\n * Mapping between configuration keys and their corresponding environment variable names.\n */\nconst ENV_MAP: Record<Exclude<ShamelaConfigKey, 'fetchImplementation'>, string> = {\n apiKey: 'SHAMELA_API_KEY',\n booksEndpoint: 'SHAMELA_API_BOOKS_ENDPOINT',\n masterPatchEndpoint: 'SHAMELA_API_MASTER_PATCH_ENDPOINT',\n sqlJsWasmUrl: 'SHAMELA_SQLJS_WASM_URL',\n};\n\n/**\n * Detects whether the Node.js {@link process} global is available for reading environment variables.\n */\nconst isProcessAvailable = typeof process !== 'undefined' && Boolean(process?.env);\n\n/**\n * Reads a configuration value either from runtime overrides or environment variables.\n *\n * @param key - The configuration key to resolve\n * @returns The resolved configuration value if present\n */\nconst readEnv = <Key extends Exclude<ShamelaConfigKey, 'fetchImplementation'>>(key: Key) => {\n const runtimeValue = runtimeConfig[key];\n\n if (runtimeValue !== undefined) {\n return runtimeValue as ShamelaConfig[Key];\n }\n\n const envKey = ENV_MAP[key];\n\n if (isProcessAvailable) {\n return process.env[envKey] as ShamelaConfig[Key];\n }\n\n return undefined as ShamelaConfig[Key];\n};\n\n/**\n * Runtime configuration options accepted by {@link configure}.\n */\nexport type ConfigureOptions = Partial<ShamelaConfig> & { logger?: Logger };\n\n/**\n * Updates the runtime configuration for the library.\n *\n * This function merges the provided options with existing overrides and optionally\n * configures a custom logger implementation.\n *\n * @param config - Runtime configuration overrides and optional logger instance\n */\nexport const configure = (config: ConfigureOptions) => {\n const { logger, ...options } = config;\n\n if ('logger' in config) {\n configureLogger(logger);\n }\n\n runtimeConfig = { ...runtimeConfig, ...options };\n};\n\n/**\n * Retrieves a single configuration value.\n *\n * @param key - The configuration key to read\n * @returns The configuration value when available\n */\nexport const getConfigValue = <Key extends ShamelaConfigKey>(key: Key) => {\n if (key === 'fetchImplementation') {\n return runtimeConfig.fetchImplementation as ShamelaConfig[Key];\n }\n\n return readEnv(key as Exclude<Key, 'fetchImplementation'>);\n};\n\n/**\n * Resolves the current configuration by combining runtime overrides and environment variables.\n *\n * @returns The resolved {@link ShamelaConfig}\n */\nexport const getConfig = (): ShamelaConfig => {\n return {\n apiKey: readEnv('apiKey'),\n booksEndpoint: readEnv('booksEndpoint'),\n fetchImplementation: runtimeConfig.fetchImplementation,\n masterPatchEndpoint: readEnv('masterPatchEndpoint'),\n sqlJsWasmUrl: readEnv('sqlJsWasmUrl'),\n };\n};\n\n/**\n * Retrieves a configuration value and throws if it is missing.\n *\n * @param key - The configuration key to require\n * @throws {Error} If the configuration value is not defined\n * @returns The resolved configuration value\n */\nexport const requireConfigValue = <Key extends Exclude<ShamelaConfigKey, 'fetchImplementation'>>(key: Key) => {\n if ((key as ShamelaConfigKey) === 'fetchImplementation') {\n throw new Error('fetchImplementation must be provided via configure().');\n }\n\n const value = getConfigValue(key);\n if (!value) {\n throw new Error(`${ENV_MAP[key]} environment variable not set`);\n }\n\n return value as NonNullable<ShamelaConfig[Key]>;\n};\n\n/**\n * Clears runtime configuration overrides and restores the default logger.\n */\nexport const resetConfig = () => {\n runtimeConfig = {};\n resetLogger();\n};\n","/**\n * Enumeration of database table names.\n */\nexport enum Tables {\n /** Author table */\n Authors = 'author',\n /** Book table */\n Books = 'book',\n /** Category table */\n Categories = 'category',\n /** Page table */\n Page = 'page',\n /** Title table */\n Title = 'title',\n}\n\n/**\n * A record that can be deleted by patches.\n */\nexport type Deletable = {\n /** Indicates if it was deleted in the patch if it is set to '1 */\n is_deleted?: string;\n};\n\nexport type Unique = {\n /** Unique identifier */\n id: number;\n};\n\n/**\n * Database row structure for the author table.\n */\nexport type AuthorRow = Deletable &\n Unique & {\n /** Author biography */\n biography: string;\n\n /** Death year */\n death_number: string;\n\n /** The death year as a text */\n death_text: string;\n\n /** Author name */\n name: string;\n };\n\n/**\n * Database row structure for the book table.\n */\nexport type BookRow = Deletable &\n Unique & {\n /** Serialized author ID(s) \"2747, 3147\" or \"513\" */\n author: string;\n\n /** Bibliography information */\n bibliography: string;\n\n /** Category ID */\n category: string;\n\n /** Publication date (or 99999 for unavailable) */\n date: string;\n\n /** Hint or description */\n hint: string;\n\n /** Major version */\n major_release: string;\n\n /** Serialized metadata */\n metadata: string;\n\n /** Minor version */\n minor_release: string;\n\n /** Book name */\n name: string;\n\n /** Serialized PDF links */\n pdf_links: string;\n\n /** Printed flag */\n printed: string;\n\n /** Book type */\n type: string;\n };\n\n/**\n * Database row structure for the category table.\n */\nexport type CategoryRow = Deletable &\n Unique & {\n /** Category name */\n name: string;\n\n /** Category order in the list to show. */\n order: string;\n };\n\n/**\n * Database row structure for the page table.\n */\nexport type PageRow = Deletable &\n Unique & {\n /** Page content */\n content: string;\n\n /** Page number */\n number: string | null;\n\n /** Page reference */\n page: string | null;\n\n /** Part number */\n part: string | null;\n\n /** Additional metadata */\n services: string | null;\n };\n\n/**\n * Database row structure for the title table.\n */\nexport type TitleRow = Deletable &\n Unique & {\n /** Title content */\n content: string;\n\n /** Page number */\n page: string;\n\n /** Parent title ID */\n parent: string | null;\n };\n","import logger from '@/utils/logger';\nimport type { SqliteDatabase } from './sqlite';\nimport { type Deletable, type PageRow, Tables, type TitleRow } from './types';\n\ntype Row = Record<string, any> & Deletable;\n\nconst PATCH_NOOP_VALUE = '#';\n\n/**\n * Retrieves column information for a specified table.\n * @param db - The database instance\n * @param table - The table name to get info for\n * @returns Array of column information with name and type\n */\nconst getTableInfo = (db: SqliteDatabase, table: Tables) => {\n return db.query(`PRAGMA table_info(${table})`).all() as { name: string; type: string }[];\n};\n\n/**\n * Checks if a table exists in the database.\n * @param db - The database instance\n * @param table - The table name to check\n * @returns True if the table exists, false otherwise\n */\nconst hasTable = (db: SqliteDatabase, table: Tables): boolean => {\n const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?1`).get(table) as\n | { name: string }\n | undefined;\n return Boolean(result);\n};\n\n/**\n * Reads all rows from a specified table.\n * @param db - The database instance\n * @param table - The table name to read from\n * @returns Array of rows, or empty array if table doesn't exist\n */\nconst readRows = (db: SqliteDatabase, table: Tables): Row[] => {\n if (!hasTable(db, table)) {\n return [];\n }\n\n return db.query(`SELECT * FROM ${table}`).all() as Row[];\n};\n\n/**\n * Checks if a row is marked as deleted.\n * @param row - The row to check\n * @returns True if the row has is_deleted field set to '1', false otherwise\n */\nconst isDeleted = (row: Row): boolean => {\n return String(row.is_deleted) === '1';\n};\n\n/**\n * Merges values from a base row and patch row, with patch values taking precedence.\n * @param baseRow - The original row data (can be undefined)\n * @param patchRow - The patch row data with updates (can be undefined)\n * @param columns - Array of column names to merge\n * @returns Merged row with combined values\n */\nconst mergeRowValues = (baseRow: Row | undefined, patchRow: Row | undefined, columns: string[]): Row => {\n const merged: Row = {};\n\n for (const column of columns) {\n if (column === 'id') {\n merged.id = (patchRow ?? baseRow)?.id ?? null;\n continue;\n }\n\n if (patchRow && column in patchRow) {\n const value = patchRow[column];\n\n if (value !== PATCH_NOOP_VALUE && value !== null && value !== undefined) {\n merged[column] = value;\n continue;\n }\n }\n\n if (baseRow && column in baseRow) {\n merged[column] = baseRow[column];\n continue;\n }\n\n merged[column] = null;\n }\n\n return merged;\n};\n\n/**\n * Merges arrays of base rows and patch rows, handling deletions and updates.\n * @param baseRows - Original rows from the base database\n * @param patchRows - Patch rows containing updates, additions, and deletions\n * @param columns - Array of column names to merge\n * @returns Array of merged rows with patches applied\n */\nconst mergeRows = (baseRows: Row[], patchRows: Row[], columns: string[]): Row[] => {\n const baseIds = new Set<string>();\n const patchById = new Map<string, Row>();\n\n for (const row of baseRows) {\n baseIds.add(String(row.id));\n }\n\n for (const row of patchRows) {\n patchById.set(String(row.id), row);\n }\n\n const merged: Row[] = [];\n\n for (const baseRow of baseRows) {\n const patchRow = patchById.get(String(baseRow.id));\n\n if (patchRow && isDeleted(patchRow)) {\n continue;\n }\n\n merged.push(mergeRowValues(baseRow, patchRow, columns));\n }\n\n for (const row of patchRows) {\n const id = String(row.id);\n\n if (baseIds.has(id) || isDeleted(row)) {\n continue;\n }\n\n merged.push(mergeRowValues(undefined, row, columns));\n }\n\n return merged;\n};\n\n/**\n * Inserts multiple rows into a specified table using a prepared statement.\n * @param db - The database instance\n * @param table - The table name to insert into\n * @param columns - Array of column names\n * @param rows - Array of row data to insert\n */\nconst insertRows = (db: SqliteDatabase, table: Tables, columns: string[], rows: Row[]) => {\n if (rows.length === 0) {\n return;\n }\n\n const placeholders = columns.map(() => '?').join(',');\n const statement = db.prepare(`INSERT INTO ${table} (${columns.join(',')}) VALUES (${placeholders})`);\n\n rows.forEach((row) => {\n const values = columns.map((column) => (column in row ? row[column] : null));\n // Spread the values array instead of passing it directly\n statement.run(...values);\n });\n\n statement.finalize();\n};\n\n/**\n * Ensures the target database has the same table schema as the source database.\n * @param target - The target database to create/update the table in\n * @param source - The source database to copy the schema from\n * @param table - The table name to ensure schema for\n * @returns True if schema was successfully ensured, false otherwise\n */\nconst ensureTableSchema = (target: SqliteDatabase, source: SqliteDatabase, table: Tables) => {\n const row = source.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?1`).get(table) as\n | { sql: string }\n | undefined;\n\n if (!row?.sql) {\n logger.warn(`${table} table definition missing in source database`);\n return false;\n }\n\n target.run(`DROP TABLE IF EXISTS ${table}`);\n target.run(row.sql);\n return true;\n};\n\n/**\n * Copies and patches a table from source to target database, applying patch updates if provided.\n * @param target - The target database to copy/patch the table to\n * @param source - The source database containing the base table data\n * @param patch - Optional patch database containing updates (can be null)\n * @param table - The table name to copy and patch\n */\nconst copyAndPatchTable = (\n target: SqliteDatabase,\n source: SqliteDatabase,\n patch: SqliteDatabase | null,\n table: Tables,\n) => {\n if (!hasTable(source, table)) {\n logger.warn(`${table} table missing in source database`);\n return;\n }\n\n if (!ensureTableSchema(target, source, table)) {\n return;\n }\n\n const baseInfo = getTableInfo(source, table);\n const patchInfo = patch && hasTable(patch, table) ? getTableInfo(patch, table) : [];\n\n const columns = baseInfo.map((info) => info.name);\n\n for (const info of patchInfo) {\n if (!columns.includes(info.name)) {\n const columnType = info.type && info.type.length > 0 ? info.type : 'TEXT';\n target.run(`ALTER TABLE ${table} ADD COLUMN ${info.name} ${columnType}`);\n columns.push(info.name);\n }\n }\n\n const baseRows = readRows(source, table);\n const patchRows = patch ? readRows(patch, table) : [];\n\n const mergedRows = mergeRows(baseRows, patchRows, columns);\n\n insertRows(target, table, columns, mergedRows);\n};\n\n/**\n * Applies patches from a patch database to the main database.\n * @param db - The target database to apply patches to\n * @param aslDB - Path to the source ASL database file\n * @param patchDB - Path to the patch database file\n */\nexport const applyPatches = (db: SqliteDatabase, source: SqliteDatabase, patch: SqliteDatabase) => {\n db.transaction(() => {\n copyAndPatchTable(db, source, patch, Tables.Page);\n copyAndPatchTable(db, source, patch, Tables.Title);\n })();\n};\n\n/**\n * Copies table data from a source database without applying any patches.\n * @param db - The target database to copy data to\n * @param aslDB - Path to the source ASL database file\n */\nexport const copyTableData = (db: SqliteDatabase, source: SqliteDatabase) => {\n db.transaction(() => {\n copyAndPatchTable(db, source, null, Tables.Page);\n copyAndPatchTable(db, source, null, Tables.Title);\n })();\n};\n\n/**\n * Creates the required tables (Page and Title) in the database with their schema.\n * @param db - The database instance to create tables in\n */\nexport const createTables = (db: SqliteDatabase) => {\n db.run(\n `CREATE TABLE ${Tables.Page} (\n id INTEGER,\n content TEXT,\n part TEXT,\n page TEXT,\n number TEXT,\n services TEXT,\n is_deleted TEXT\n )`,\n );\n db.run(\n `CREATE TABLE ${Tables.Title} (\n id INTEGER,\n content TEXT,\n page INTEGER,\n parent INTEGER,\n is_deleted TEXT\n )`,\n );\n};\n\n/**\n * Retrieves all pages from the Page table.\n * @param db - The database instance\n * @returns Array of all pages\n */\nexport const getAllPages = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Page}`).all() as PageRow[];\n};\n\n/**\n * Retrieves all titles from the Title table.\n * @param db - The database instance\n * @returns Array of all titles\n */\nexport const getAllTitles = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Title}`).all() as TitleRow[];\n};\n\n/**\n * Retrieves all book data including pages and titles.\n * @param db - The database instance\n * @returns Object containing arrays of pages and titles\n */\nexport const getData = (db: SqliteDatabase) => {\n return { pages: getAllPages(db), titles: getAllTitles(db) };\n};\n","/**\n * Utility for resolving the sql.js WASM file path in different runtime environments.\n * Handles bundled environments (Next.js, webpack, Turbopack), monorepos, and standard Node.js.\n */\n\n/**\n * Checks if a file exists at the given path (Node.js only).\n *\n * @param path - Absolute path to validate\n * @returns True when the file system entry is present\n */\nconst fileExists = (path: string): boolean => {\n try {\n const fs = require('node:fs');\n return fs.existsSync(path);\n } catch {\n return false;\n }\n};\n\n/**\n * Attempts to find the sql.js WASM file in node_modules using multiple strategies.\n * This handles cases where the library is bundled by tools like webpack/Turbopack.\n *\n * @returns The resolved filesystem path to the WASM file, or null if not found\n */\nexport const findNodeWasmPath = (): string | null => {\n // Strategy 1: Try to resolve using require.resolve (works in most Node.js scenarios)\n if (typeof require !== 'undefined' && typeof require.resolve !== 'undefined') {\n try {\n const sqlJsPath = require.resolve('sql.js');\n const pathModule = require('node:path');\n const sqlJsDir = pathModule.dirname(sqlJsPath);\n const wasmPath = pathModule.join(sqlJsDir, 'dist', 'sql-wasm.wasm');\n\n if (fileExists(wasmPath)) {\n return wasmPath;\n }\n } catch (e) {\n // Continue to next strategy\n }\n }\n\n // Strategy 2: Try common node_modules patterns from process.cwd()\n if (typeof process !== 'undefined' && process.cwd) {\n try {\n const pathModule = require('node:path');\n const cwd = process.cwd();\n\n const candidates = [\n // Standard location\n pathModule.join(cwd, 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n // Monorepo or workspace root\n pathModule.join(cwd, '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n pathModule.join(cwd, '../..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n // Next.js specific locations\n pathModule.join(cwd, '.next', 'server', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n ];\n\n for (const candidate of candidates) {\n if (fileExists(candidate)) {\n return candidate;\n }\n }\n } catch (e) {\n // Continue to next strategy\n }\n }\n\n // Strategy 3: Try using require.resolve.paths to find all possible locations\n if (typeof require !== 'undefined' && typeof require.resolve !== 'undefined' && require.resolve.paths) {\n try {\n const pathModule = require('node:path');\n const searchPaths = require.resolve.paths('sql.js') || [];\n\n for (const searchPath of searchPaths) {\n const wasmPath = pathModule.join(searchPath, 'sql.js', 'dist', 'sql-wasm.wasm');\n if (fileExists(wasmPath)) {\n return wasmPath;\n }\n }\n } catch (e) {\n // Continue to next strategy\n }\n }\n\n // Strategy 4: Try import.meta.url (works in unbundled ESM scenarios)\n try {\n if (typeof import.meta !== 'undefined' && import.meta.url) {\n const url = new URL('../../node_modules/sql.js/dist/sql-wasm.wasm', import.meta.url);\n const path = decodeURIComponent(url.pathname);\n\n // On Windows, file URLs start with /C:/ but we need C:/\n const normalizedPath = process.platform === 'win32' && path.startsWith('/') ? path.slice(1) : path;\n\n if (fileExists(normalizedPath)) {\n return normalizedPath;\n }\n }\n } catch {\n // All strategies exhausted\n }\n\n return null;\n};\n","import initSqlJs, { type Database as SqlJsDatabase, type SqlJsStatic, type Statement } from 'sql.js';\n\nimport { getConfigValue } from '@/config';\nimport { findNodeWasmPath } from '@/utils/wasm';\n\n/**\n * Represents a row returned from a SQLite query as a generic key-value object.\n */\nexport type QueryRow = Record<string, any>;\n\n/**\n * Minimal contract for prepared statements used throughout the project.\n */\nexport interface PreparedStatement {\n run: (...params: any[]) => void;\n finalize: () => void;\n}\n\n/**\n * Interface describing reusable query helpers that return all rows or a single row.\n */\nexport interface Query {\n all: (...params: any[]) => QueryRow[];\n get: (...params: any[]) => QueryRow | undefined;\n}\n\n/**\n * Abstraction over the subset of SQLite database operations required by the library.\n */\nexport interface SqliteDatabase {\n run: (sql: string, params?: any[]) => void;\n prepare: (sql: string) => PreparedStatement;\n query: (sql: string) => Query;\n transaction: (fn: () => void) => () => void;\n close: () => void;\n export: () => Uint8Array;\n}\n\n/**\n * Adapter implementing {@link PreparedStatement} by delegating to a sql.js {@link Statement}.\n */\nclass SqlJsPreparedStatement implements PreparedStatement {\n constructor(private readonly statement: Statement) {}\n\n run = (...params: any[]) => {\n if (params.length > 0) {\n this.statement.bind(params);\n }\n\n this.statement.step();\n this.statement.reset();\n };\n\n finalize = () => {\n this.statement.free();\n };\n}\n\n/**\n * Wrapper providing the {@link SqliteDatabase} interface on top of a sql.js database instance.\n */\nclass SqlJsDatabaseWrapper implements SqliteDatabase {\n constructor(private readonly db: SqlJsDatabase) {}\n\n run = (sql: string, params: any[] = []) => {\n this.db.run(sql, params);\n };\n\n prepare = (sql: string): PreparedStatement => {\n return new SqlJsPreparedStatement(this.db.prepare(sql));\n };\n\n query = (sql: string): Query => {\n return {\n all: (...params: any[]) => this.all(sql, params),\n get: (...params: any[]) => this.get(sql, params),\n };\n };\n\n transaction = (fn: () => void) => {\n return () => {\n this.db.run('BEGIN TRANSACTION');\n try {\n fn();\n this.db.run('COMMIT');\n } catch (error) {\n this.db.run('ROLLBACK');\n throw error;\n }\n };\n };\n\n close = () => {\n this.db.close();\n };\n\n export = () => {\n return this.db.export();\n };\n\n private all = (sql: string, params: any[]): QueryRow[] => {\n const statement = this.db.prepare(sql);\n try {\n if (params.length > 0) {\n statement.bind(params);\n }\n\n const rows: QueryRow[] = [];\n while (statement.step()) {\n rows.push(statement.getAsObject());\n }\n return rows;\n } finally {\n statement.free();\n }\n };\n\n private get = (sql: string, params: any[]): QueryRow | undefined => {\n const rows = this.all(sql, params);\n return rows[0];\n };\n}\n\nlet sqlPromise: Promise<SqlJsStatic> | null = null;\nlet resolvedWasmPath: string | null = null;\n\nconst isNodeEnvironment = typeof process !== 'undefined' && Boolean(process?.versions?.node);\nconst DEFAULT_BROWSER_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/sql-wasm.wasm';\n\n/**\n * Resolves the appropriate location of the sql.js WebAssembly binary.\n *\n * @returns The resolved path or remote URL for the sql.js wasm asset\n */\nconst getWasmPath = () => {\n if (!resolvedWasmPath) {\n // First priority: user configuration\n const configured = getConfigValue('sqlJsWasmUrl');\n if (configured) {\n resolvedWasmPath = configured;\n } else if (isNodeEnvironment) {\n // Second priority: auto-detect in Node.js\n const nodePath = findNodeWasmPath();\n if (nodePath) {\n resolvedWasmPath = nodePath;\n } else {\n // Fallback: provide helpful error with suggestions\n const errorMsg = [\n 'Unable to automatically locate sql-wasm.wasm file.',\n 'This can happen in bundled environments (Next.js, webpack, etc.).',\n '',\n 'Quick fix - add this to your code before using shamela:',\n '',\n ' import { configure, createNodeConfig } from \"shamela\";',\n ' configure(createNodeConfig({',\n ' apiKey: process.env.SHAMELA_API_KEY,',\n ' booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT,',\n ' masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT,',\n ' }));',\n '',\n 'Or manually specify the path:',\n '',\n ' import { configure } from \"shamela\";',\n ' import { join } from \"node:path\";',\n ' configure({',\n ' sqlJsWasmUrl: join(process.cwd(), \"node_modules\", \"sql.js\", \"dist\", \"sql-wasm.wasm\")',\n ' });',\n ].join('\\n');\n\n throw new Error(errorMsg);\n }\n } else {\n // Browser environment: use CDN\n resolvedWasmPath = DEFAULT_BROWSER_WASM_URL;\n }\n }\n\n return resolvedWasmPath;\n};\n\n/**\n * Lazily initialises the sql.js runtime, reusing the same promise for subsequent calls.\n *\n * @returns A promise resolving to the sql.js module\n */\nconst loadSql = () => {\n if (!sqlPromise) {\n sqlPromise = initSqlJs({\n locateFile: () => getWasmPath(),\n });\n }\n\n return sqlPromise;\n};\n\n/**\n * Creates a new in-memory SQLite database instance backed by sql.js.\n *\n * @returns A promise resolving to a {@link SqliteDatabase} wrapper\n */\nexport const createDatabase = async () => {\n const SQL = await loadSql();\n return new SqlJsDatabaseWrapper(new SQL.Database());\n};\n\n/**\n * Opens an existing SQLite database from the provided binary contents.\n *\n * @param data - The Uint8Array containing the SQLite database bytes\n * @returns A promise resolving to a {@link SqliteDatabase} wrapper\n */\nexport const openDatabase = async (data: Uint8Array) => {\n const SQL = await loadSql();\n return new SqlJsDatabaseWrapper(new SQL.Database(data));\n};\n","import type { Author, Book, Category, MasterData } from '../types';\nimport type { SqliteDatabase } from './sqlite';\nimport { openDatabase } from './sqlite';\nimport { Tables } from './types';\n\n/**\n * Ensures the target database has the same table schema as the source database for a specific table.\n * @param db - The database instance\n * @param alias - The alias name of the attached database\n * @param table - The table name to ensure schema for\n * @throws {Error} When table definition is missing in the source database\n */\nconst ensureTableSchema = (db: SqliteDatabase, source: SqliteDatabase, table: Tables) => {\n const row = source.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?1`).get(table) as\n | { sql: string }\n | undefined;\n\n if (!row?.sql) {\n throw new Error(`Missing table definition for ${table} in source database`);\n }\n\n db.run(`DROP TABLE IF EXISTS ${table}`);\n db.run(row.sql);\n};\n\n/**\n * Copies data from foreign master table files into the main master database.\n *\n * This function processes the source table files (author.sqlite, book.sqlite, category.sqlite)\n * by attaching them to the current database connection, then copying their data into\n * the main master database tables. It handles data transformation including filtering\n * out deleted records and converting placeholder values.\n *\n * @param db - The database client instance for the master database\n * @param sourceTables - Array of file paths to the source SQLite table files\n *\n * @throws {Error} When source files cannot be attached or data copying operations fail\n */\nexport const copyForeignMasterTableData = async (\n db: SqliteDatabase,\n sourceTables: Array<{ name: string; data: Uint8Array }>,\n) => {\n const TABLE_MAP: Record<string, Tables> = {\n author: Tables.Authors,\n book: Tables.Books,\n category: Tables.Categories,\n };\n\n const tableDbs: Partial<Record<Tables, SqliteDatabase>> = {};\n\n for (const table of sourceTables) {\n const baseName = table.name.split('/').pop()?.split('\\\\').pop() ?? table.name;\n const normalized = baseName.replace(/\\.(sqlite|db)$/i, '').toLowerCase();\n const tableName = TABLE_MAP[normalized];\n if (!tableName) {\n continue;\n }\n\n tableDbs[tableName] = await openDatabase(table.data);\n }\n\n try {\n const entries = Object.entries(tableDbs) as Array<[Tables, SqliteDatabase]>;\n\n db.transaction(() => {\n for (const [table, sourceDb] of entries) {\n ensureTableSchema(db, sourceDb, table);\n\n const columnInfo = sourceDb.query(`PRAGMA table_info(${table})`).all() as Array<{\n name: string;\n type: string;\n }>;\n const columnNames = columnInfo.map((info) => info.name);\n if (columnNames.length === 0) {\n continue;\n }\n\n const rows = sourceDb.query(`SELECT * FROM ${table}`).all();\n if (rows.length === 0) {\n continue;\n }\n\n const placeholders = columnNames.map(() => '?').join(',');\n const sqlColumns = columnNames.map((name) => (name === 'order' ? '\"order\"' : name));\n const statement = db.prepare(`INSERT INTO ${table} (${sqlColumns.join(',')}) VALUES (${placeholders})`);\n\n try {\n for (const row of rows) {\n const values = columnNames.map((column) => (column in row ? row[column] : null));\n statement.run(...values);\n }\n } finally {\n statement.finalize();\n }\n }\n })();\n } finally {\n Object.values(tableDbs).forEach((database) => database?.close());\n }\n};\n\n/**\n * Creates a backward-compatible database view for legacy table names.\n * @param db - The database instance\n * @param viewName - The name of the view to create\n * @param sourceTable - The source table to base the view on\n */\nconst createCompatibilityView = (db: SqliteDatabase, viewName: string, sourceTable: Tables) => {\n db.run(`DROP VIEW IF EXISTS ${viewName}`);\n db.run(`CREATE VIEW ${viewName} AS SELECT * FROM ${sourceTable}`);\n};\n\n/**\n * Creates the necessary database tables for the master database.\n *\n * This function sets up the schema for the master database by creating\n * tables for authors, books, and categories with their respective columns\n * and data types. This is typically the first step in setting up a new\n * master database. Also creates backward-compatible views for legacy table names.\n *\n * @param db - The database client instance where tables should be created\n *\n * @throws {Error} When table creation fails due to database constraints or permissions\n */\nexport const createTables = (db: SqliteDatabase) => {\n db.run(\n `CREATE TABLE ${Tables.Authors} (\n id INTEGER,\n is_deleted TEXT,\n name TEXT,\n biography TEXT,\n death_text TEXT,\n death_number TEXT\n )`,\n );\n db.run(\n `CREATE TABLE ${Tables.Books} (\n id INTEGER,\n name TEXT,\n is_deleted TEXT,\n category TEXT,\n type TEXT,\n date TEXT,\n author TEXT,\n printed TEXT,\n minor_release TEXT,\n major_release TEXT,\n bibliography TEXT,\n hint TEXT,\n pdf_links TEXT,\n metadata TEXT\n )`,\n );\n db.run(\n `CREATE TABLE ${Tables.Categories} (\n id INTEGER,\n is_deleted TEXT,\n \"order\" TEXT,\n name TEXT\n )`,\n );\n\n // Provide backward-compatible pluralised views since callers historically\n // queried \"authors\", \"books\", and \"categories\" tables.\n createCompatibilityView(db, 'authors', Tables.Authors);\n createCompatibilityView(db, 'books', Tables.Books);\n createCompatibilityView(db, 'categories', Tables.Categories);\n};\n\n/**\n * Retrieves all authors from the Authors table.\n * @param db - The database instance\n * @returns Array of all authors\n */\nexport const getAllAuthors = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Authors}`).all() as Author[];\n};\n\n/**\n * Retrieves all books from the Books table.\n * @param db - The database instance\n * @returns Array of all books\n */\nexport const getAllBooks = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Books}`).all() as Book[];\n};\n\n/**\n * Retrieves all categories from the Categories table.\n * @param db - The database instance\n * @returns Array of all categories\n */\nexport const getAllCategories = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Categories}`).all() as Category[];\n};\n\n/**\n * Retrieves all master data including authors, books, and categories.\n * @param db - The database instance\n * @returns Object containing arrays of authors, books, and categories\n */\nexport const getData = (db: SqliteDatabase, version: number) => {\n return {\n authors: getAllAuthors(db),\n books: getAllBooks(db),\n categories: getAllCategories(db),\n version,\n } satisfies MasterData;\n};\n","import type { PageRow, TitleRow } from '@/db/types';\n\n/**\n * Redacts sensitive query parameters from a URL for safe logging\n * @param url - The URL to redact\n * @param sensitiveParams - Array of parameter names to redact (defaults to common sensitive params)\n * @returns The URL string with sensitive parameters redacted\n */\nexport const redactUrl = (\n url: URL | string,\n sensitiveParams: string[] = ['api_key', 'token', 'password', 'secret', 'auth'],\n): string => {\n const urlObj = typeof url === 'string' ? new URL(url) : new URL(url.toString());\n\n sensitiveParams.forEach((param) => {\n const value = urlObj.searchParams.get(param);\n if (value && value.length > 6) {\n const redacted = `${value.slice(0, 3)}***${value.slice(-3)}`;\n urlObj.searchParams.set(param, redacted);\n } else if (value) {\n urlObj.searchParams.set(param, '***');\n }\n });\n\n return urlObj.toString();\n};\n\n/**\n * Normalises a raw page row from SQLite into a serialisable {@link Page}.\n *\n * @param page - The database row representing a page\n * @returns The mapped page with numeric fields converted where appropriate\n */\nexport const mapPageRowToPage = (page: PageRow) => {\n return {\n content: page.content,\n id: page.id,\n ...(page.number && { number: page.number }),\n ...(page.page && { page: Number(page.page) }),\n ...(page.part && { part: page.part }),\n };\n};\n\n/**\n * Normalises a raw title row from SQLite into a serialisable {@link Title}.\n *\n * @param title - The database row representing a title\n * @returns The mapped title with numeric identifiers converted\n */\nexport const mapTitleRowToTitle = (title: TitleRow) => {\n const parent = Number(title.parent);\n\n return {\n content: title.content,\n id: title.id,\n page: Number(title.page),\n ...(parent && { parent }),\n };\n};\n","import type { UnzippedEntry } from '@/utils/io';\n\n/**\n * Enforces HTTPS protocol for a given URL string.\n *\n * @param originalUrl - The URL that may use an insecure scheme\n * @returns The normalized URL string using the HTTPS protocol\n */\nexport const fixHttpsProtocol = (originalUrl: string): string => {\n const url = new URL(originalUrl);\n url.protocol = 'https';\n\n return url.toString();\n};\n\n/**\n * Determines whether an archive entry contains a SQLite database file.\n *\n * @param entry - The entry extracted from an archive\n * @returns True when the entry name ends with a recognized SQLite extension\n */\nexport const isSqliteEntry = (entry: UnzippedEntry): boolean => /\\.(sqlite|db)$/i.test(entry.name);\n\n/**\n * Finds the first SQLite database entry from a list of archive entries.\n *\n * @param entries - The extracted entries to inspect\n * @returns The first matching entry or undefined when not present\n */\nexport const findSqliteEntry = (entries: UnzippedEntry[]): UnzippedEntry | undefined => {\n return entries.find(isSqliteEntry);\n};\n\n/**\n * Extracts the lowercase file extension from a path or filename.\n *\n * @param filePath - The path to inspect\n * @returns The lowercase extension (including the dot) or an empty string\n */\nexport const getExtension = (filePath: string): string => {\n const match = /\\.([^.]+)$/.exec(filePath);\n return match ? `.${match[1].toLowerCase()}` : '';\n};\n","import { getConfig, requireConfigValue } from '@/config';\n\n/**\n * Builds a URL with query parameters and optional authentication.\n * @param {string} endpoint - The base endpoint URL\n * @param {Record<string, any>} queryParams - Object containing query parameters to append\n * @param {boolean} [useAuth=true] - Whether to include the API key from environment variables\n * @returns {URL} The constructed URL object with query parameters\n */\nexport const buildUrl = (endpoint: string, queryParams: Record<string, any>, useAuth: boolean = true): URL => {\n const url = new URL(endpoint);\n const params = new URLSearchParams();\n\n Object.entries(queryParams).forEach(([key, value]) => {\n params.append(key, value.toString());\n });\n\n if (useAuth) {\n params.append('api_key', requireConfigValue('apiKey'));\n }\n\n url.search = params.toString();\n\n return url;\n};\n\n/**\n * Makes an HTTPS GET request and returns the response data using the configured fetch implementation.\n * @template T - The expected return type (Buffer or Record<string, any>)\n * @param {string | URL} url - The URL to make the request to\n * @param options - Optional overrides including a custom fetch implementation\n * @returns {Promise<T>} A promise that resolves to the response data, parsed as JSON if content-type is application/json, otherwise as Buffer\n * @throws {Error} When the request fails or JSON parsing fails\n */\nexport const httpsGet = async <T extends Uint8Array | Record<string, any>>(\n url: string | URL,\n options: { fetchImpl?: typeof fetch } = {},\n): Promise<T> => {\n const target = typeof url === 'string' ? url : url.toString();\n const activeFetch = options.fetchImpl ?? getConfig().fetchImplementation ?? fetch;\n const response = await activeFetch(target);\n\n if (!response.ok) {\n throw new Error(`Error making request: ${response.status} ${response.statusText}`);\n }\n\n const contentType = response.headers.get('content-type') ?? '';\n\n if (contentType.includes('application/json')) {\n return (await response.json()) as T;\n }\n\n const buffer = await response.arrayBuffer();\n return new Uint8Array(buffer) as T;\n};\n","import { unzipSync } from 'fflate';\n\nimport type { OutputOptions } from '@/types';\nimport logger from './logger';\nimport { httpsGet } from './network';\n\n/**\n * Representation of an extracted archive entry containing raw bytes and filename metadata.\n */\nexport type UnzippedEntry = { name: string; data: Uint8Array };\n\nconst isNodeEnvironment = typeof process !== 'undefined' && Boolean(process?.versions?.node);\n\n/**\n * Dynamically imports the Node.js fs/promises module, ensuring the runtime supports file operations.\n *\n * @throws {Error} When executed in a non-Node.js environment\n * @returns The fs/promises module when available\n */\nconst ensureNodeFs = async () => {\n if (!isNodeEnvironment) {\n throw new Error('File system operations are only supported in Node.js environments');\n }\n\n return import('node:fs/promises');\n};\n\n/**\n * Ensures the directory for a file path exists, creating parent folders as needed.\n *\n * @param filePath - The target file path whose directory should be created\n * @returns The fs/promises module instance\n */\nconst ensureDirectory = async (filePath: string) => {\n const [fs, path] = await Promise.all([ensureNodeFs(), import('node:path')]);\n const directory = path.dirname(filePath);\n await fs.mkdir(directory, { recursive: true });\n return fs;\n};\n\n/**\n * Downloads a ZIP archive from the given URL and returns its extracted entries.\n *\n * @param url - The remote URL referencing a ZIP archive\n * @returns A promise resolving to the extracted archive entries\n */\nexport const unzipFromUrl = async (url: string): Promise<UnzippedEntry[]> => {\n const binary = await httpsGet<Uint8Array>(url);\n const byteLength =\n binary instanceof Uint8Array\n ? binary.length\n : binary && typeof (binary as ArrayBufferLike).byteLength === 'number'\n ? (binary as ArrayBufferLike).byteLength\n : 0;\n logger.debug('unzipFromUrl:bytes', byteLength);\n\n return new Promise((resolve, reject) => {\n const dataToUnzip = binary instanceof Uint8Array ? binary : new Uint8Array(binary as ArrayBufferLike);\n\n try {\n const result = unzipSync(dataToUnzip);\n const entries = Object.entries(result).map(([name, data]) => ({ data, name }));\n logger.debug(\n 'unzipFromUrl:entries',\n entries.map((entry) => entry.name),\n );\n resolve(entries);\n } catch (error: any) {\n reject(new Error(`Error processing URL: ${error.message}`));\n }\n });\n};\n\n/**\n * Creates a unique temporary directory with the provided prefix.\n *\n * @param prefix - Optional prefix for the generated directory name\n * @returns The created temporary directory path\n */\nexport const createTempDir = async (prefix = 'shamela') => {\n const [fs, os, path] = await Promise.all([ensureNodeFs(), import('node:os'), import('node:path')]);\n const base = path.join(os.tmpdir(), prefix);\n return fs.mkdtemp(base);\n};\n\n/**\n * Writes output data either using a provided writer function or to a file path.\n *\n * @param output - The configured output destination or writer\n * @param payload - The payload to persist (string or binary)\n * @throws {Error} When neither a writer nor file path is provided\n */\nexport const writeOutput = async (output: OutputOptions, payload: string | Uint8Array) => {\n if (output.writer) {\n await output.writer(payload);\n return;\n }\n\n if (!output.path) {\n throw new Error('Output options must include either a writer or a path');\n }\n\n const fs = await ensureDirectory(output.path);\n\n if (typeof payload === 'string') {\n await fs.writeFile(output.path, payload, 'utf-8');\n } else {\n await fs.writeFile(output.path, payload);\n }\n};\n","import { getConfig } from '@/config';\n\nconst SOURCE_TABLES = ['author.sqlite', 'book.sqlite', 'category.sqlite'];\n\n/**\n * Validates that required environment variables are set.\n * @throws {Error} When any required environment variable is missing\n */\nexport const validateEnvVariables = () => {\n const { apiKey, booksEndpoint, masterPatchEndpoint } = getConfig();\n const envVariablesNotFound = [\n ['apiKey', apiKey],\n ['booksEndpoint', booksEndpoint],\n ['masterPatchEndpoint', masterPatchEndpoint],\n ]\n .filter(([, value]) => !value)\n .map(([key]) => key);\n\n if (envVariablesNotFound.length) {\n throw new Error(`${envVariablesNotFound.join(', ')} environment variables not set`);\n }\n};\n\n/**\n * Validates that all required master source tables are present in the provided paths.\n * @param {string[]} sourceTablePaths - Array of file paths to validate\n * @returns {boolean} True if all required source tables (author.sqlite, book.sqlite, category.sqlite) are present\n */\nexport const validateMasterSourceTables = (sourceTablePaths: string[]) => {\n const sourceTableNames = new Set(\n sourceTablePaths\n .map((tablePath) => tablePath.match(/[^\\\\/]+$/)?.[0] ?? tablePath)\n .map((name) => name.toLowerCase()),\n );\n return SOURCE_TABLES.every((table) => sourceTableNames.has(table.toLowerCase()));\n};\n","import { requireConfigValue } from './config';\nimport { applyPatches, copyTableData, createTables as createBookTables, getData as getBookData } from './db/book';\nimport { copyForeignMasterTableData, createTables as createMasterTables, getData as getMasterData } from './db/master';\nimport { createDatabase, openDatabase, type SqliteDatabase } from './db/sqlite';\nimport type {\n BookData,\n DownloadBookOptions,\n DownloadMasterOptions,\n GetBookMetadataOptions,\n GetBookMetadataResponsePayload,\n GetMasterMetadataResponsePayload,\n MasterData,\n} from './types';\nimport { mapPageRowToPage, mapTitleRowToTitle, redactUrl } from './utils/common';\nimport { DEFAULT_MASTER_METADATA_VERSION } from './utils/constants';\nimport { findSqliteEntry, fixHttpsProtocol, getExtension, isSqliteEntry } from './utils/downloads';\nimport type { UnzippedEntry } from './utils/io';\nimport { unzipFromUrl, writeOutput } from './utils/io';\nimport logger from './utils/logger';\nimport { buildUrl, httpsGet } from './utils/network';\nimport { validateEnvVariables, validateMasterSourceTables } from './utils/validation';\n\n/**\n * Response payload received when requesting book update metadata from the Shamela API.\n */\ntype BookUpdatesResponse = {\n major_release: number;\n major_release_url: string;\n minor_release?: number;\n minor_release_url?: string;\n};\n\n/**\n * Sets up a book database with tables and data, returning the database client.\n *\n * This helper function handles the common logic of downloading book files,\n * creating database tables, and applying patches or copying data.\n *\n * @param id - The unique identifier of the book\n * @param bookMetadata - Optional pre-fetched book metadata\n * @returns A promise that resolves to an object containing the database client and cleanup function\n */\nconst setupBookDatabase = async (\n id: number,\n bookMetadata?: GetBookMetadataResponsePayload,\n): Promise<{ client: SqliteDatabase; cleanup: () => Promise<void> }> => {\n logger.info(`Setting up book database for ${id}`);\n\n const bookResponse: GetBookMetadataResponsePayload = bookMetadata || (await getBookMetadata(id));\n const patchEntriesPromise = bookResponse.minorReleaseUrl\n ? unzipFromUrl(bookResponse.minorReleaseUrl)\n : Promise.resolve<UnzippedEntry[]>([]);\n\n const [bookEntries, patchEntries] = await Promise.all([\n unzipFromUrl(bookResponse.majorReleaseUrl),\n patchEntriesPromise,\n ]);\n\n const bookEntry = findSqliteEntry(bookEntries);\n\n if (!bookEntry) {\n throw new Error('Unable to locate book database in archive');\n }\n\n const client = await createDatabase();\n\n try {\n logger.info(`Creating tables`);\n createBookTables(client);\n\n const sourceDatabase = await openDatabase(bookEntry.data);\n\n try {\n const patchEntry = findSqliteEntry(patchEntries);\n\n if (patchEntry) {\n logger.info(`Applying patches from ${patchEntry.name} to ${bookEntry.name}`);\n const patchDatabase = await openDatabase(patchEntry.data);\n\n try {\n applyPatches(client, sourceDatabase, patchDatabase);\n } finally {\n patchDatabase.close();\n }\n } else {\n logger.info(`Copying table data from ${bookEntry.name}`);\n copyTableData(client, sourceDatabase);\n }\n } finally {\n sourceDatabase.close();\n }\n\n const cleanup = async () => {\n client.close();\n };\n\n return { cleanup, client };\n } catch (error) {\n client.close();\n throw error;\n }\n};\n\n/**\n * Downloads, validates, and prepares the master SQLite database for use.\n *\n * This helper is responsible for retrieving the master archive, ensuring all\n * required tables are present, copying their contents into a fresh in-memory\n * database, and returning both the database instance and cleanup hook.\n *\n * @param masterMetadata - Optional pre-fetched metadata describing the master archive\n * @returns A promise resolving to the database client, cleanup function, and version number\n */\nconst setupMasterDatabase = async (\n masterMetadata?: GetMasterMetadataResponsePayload,\n): Promise<{ client: SqliteDatabase; cleanup: () => Promise<void>; version: number }> => {\n logger.info('Setting up master database');\n\n const masterResponse = masterMetadata || (await getMasterMetadata(DEFAULT_MASTER_METADATA_VERSION));\n\n logger.info(`Downloading master database ${masterResponse.version} from: ${redactUrl(masterResponse.url)}`);\n const sourceTables = await unzipFromUrl(fixHttpsProtocol(masterResponse.url));\n\n logger.debug?.(`sourceTables downloaded: ${sourceTables.map((table) => table.name).toString()}`);\n\n if (!validateMasterSourceTables(sourceTables.map((table) => table.name))) {\n logger.error(`Some source tables were not found: ${sourceTables.map((table) => table.name).toString()}`);\n throw new Error('Expected tables not found!');\n }\n\n const client = await createDatabase();\n\n try {\n logger.info('Creating master tables');\n createMasterTables(client);\n\n logger.info('Copying data to master table');\n await copyForeignMasterTableData(client, sourceTables.filter(isSqliteEntry));\n\n const cleanup = async () => {\n client.close();\n };\n\n return { cleanup, client, version: masterResponse.version };\n } catch (error) {\n client.close();\n throw error;\n }\n};\n\n/**\n * Retrieves metadata for a specific book from the Shamela API.\n *\n * This function fetches book release information including major and minor release\n * URLs and version numbers from the Shamela web service.\n *\n * @param id - The unique identifier of the book to fetch metadata for\n * @param options - Optional parameters for specifying major and minor versions\n * @returns A promise that resolves to book metadata including release URLs and versions\n *\n * @throws {Error} When environment variables are not set or API request fails\n *\n * @example\n * ```typescript\n * const metadata = await getBookMetadata(123, { majorVersion: 1, minorVersion: 2 });\n * console.log(metadata.majorReleaseUrl); // Download URL for the book\n * ```\n */\nexport const getBookMetadata = async (\n id: number,\n options?: GetBookMetadataOptions,\n): Promise<GetBookMetadataResponsePayload> => {\n validateEnvVariables();\n\n const booksEndpoint = requireConfigValue('booksEndpoint');\n const url = buildUrl(`${booksEndpoint}/${id}`, {\n major_release: (options?.majorVersion || 0).toString(),\n minor_release: (options?.minorVersion || 0).toString(),\n });\n\n logger.info(`Fetching shamela.ws book link: ${redactUrl(url)}`);\n\n try {\n const response = (await httpsGet(url)) as BookUpdatesResponse;\n return {\n majorRelease: response.major_release,\n majorReleaseUrl: fixHttpsProtocol(response.major_release_url),\n ...(response.minor_release_url && { minorReleaseUrl: fixHttpsProtocol(response.minor_release_url) }),\n ...(response.minor_release_url && { minorRelease: response.minor_release }),\n };\n } catch (error: any) {\n throw new Error(`Error fetching book metadata: ${error.message}`);\n }\n};\n\n/**\n * Downloads and processes a book from the Shamela database.\n *\n * This function downloads the book's database files, applies patches if available,\n * creates the necessary database tables, and exports the data to the specified format.\n * The output can be either a JSON file or a SQLite database file.\n *\n * @param id - The unique identifier of the book to download\n * @param options - Configuration options including output file path and optional book metadata\n * @returns A promise that resolves to the path of the created output file\n *\n * @throws {Error} When download fails, database operations fail, or file operations fail\n *\n * @example\n * ```typescript\n * // Download as JSON\n * const jsonPath = await downloadBook(123, {\n * outputFile: { path: './book.json' }\n * });\n *\n * // Download as SQLite database\n * const dbPath = await downloadBook(123, {\n * outputFile: { path: './book.db' }\n * });\n * ```\n */\nexport const downloadBook = async (id: number, options: DownloadBookOptions): Promise<string> => {\n logger.info(`downloadBook ${id} ${JSON.stringify(options)}`);\n\n if (!options.outputFile.path) {\n throw new Error('outputFile.path must be provided to determine output format');\n }\n\n const extension = getExtension(options.outputFile.path).toLowerCase();\n\n const { client, cleanup } = await setupBookDatabase(id, options?.bookMetadata);\n\n try {\n if (extension === '.json') {\n const result = await getBookData(client);\n await writeOutput(options.outputFile, JSON.stringify(result, null, 2));\n } else if (extension === '.db' || extension === '.sqlite') {\n const payload = client.export();\n await writeOutput(options.outputFile, payload);\n } else {\n throw new Error(`Unsupported output extension: ${extension}`);\n }\n } finally {\n await cleanup();\n }\n\n return options.outputFile.path;\n};\n\n/**\n * Retrieves metadata for the master database from the Shamela API.\n *\n * The master database contains information about all books, authors, and categories\n * in the Shamela library. This function fetches the download URL and version\n * information for the master database patches.\n *\n * @param version - The version number to check for updates (defaults to 0)\n * @returns A promise that resolves to master database metadata including download URL and version\n *\n * @throws {Error} When environment variables are not set or API request fails\n *\n * @example\n * ```typescript\n * const masterMetadata = await getMasterMetadata(5);\n * console.log(masterMetadata.url); // URL to download master database patch\n * console.log(masterMetadata.version); // Latest version number\n * ```\n */\nexport const getMasterMetadata = async (version: number = 0): Promise<GetMasterMetadataResponsePayload> => {\n validateEnvVariables();\n\n const masterEndpoint = requireConfigValue('masterPatchEndpoint');\n const url = buildUrl(masterEndpoint, { version: version.toString() });\n\n logger.info(`Fetching shamela.ws master database patch link: ${redactUrl(url)}`);\n\n try {\n const response: Record<string, any> = await httpsGet(url);\n return { url: response.patch_url, version: response.version };\n } catch (error: any) {\n throw new Error(`Error fetching master patch: ${error.message}`);\n }\n};\n\n/**\n * Generates the URL for a book's cover image.\n *\n * This function constructs the URL to access the cover image for a specific book\n * using the book's ID and the API endpoint host.\n *\n * @param bookId - The unique identifier of the book\n * @returns The complete URL to the book's cover image\n *\n * @example\n * ```typescript\n * const coverUrl = getCoverUrl(123);\n * console.log(coverUrl); // \"https://api.shamela.ws/covers/123.jpg\"\n * ```\n */\nexport const getCoverUrl = (bookId: number) => {\n const masterEndpoint = requireConfigValue('masterPatchEndpoint');\n const { origin } = new URL(masterEndpoint);\n return `${origin}/covers/${bookId}.jpg`;\n};\n\n/**\n * Downloads and processes the master database from the Shamela service.\n *\n * The master database contains comprehensive information about all books, authors,\n * and categories available in the Shamela library. This function downloads the\n * database files, creates the necessary tables, and exports the data in the\n * specified format (JSON or SQLite).\n *\n * @param options - Configuration options including output file path and optional master metadata\n * @returns A promise that resolves to the path of the created output file\n *\n * @throws {Error} When download fails, expected tables are missing, database operations fail, or file operations fail\n *\n * @example\n * ```typescript\n * // Download master database as JSON\n * const jsonPath = await downloadMasterDatabase({\n * outputFile: { path: './master.json' }\n * });\n *\n * // Download master database as SQLite\n * const dbPath = await downloadMasterDatabase({\n * outputFile: { path: './master.db' }\n * });\n * ```\n */\nexport const downloadMasterDatabase = async (options: DownloadMasterOptions): Promise<string> => {\n logger.info(`downloadMasterDatabase ${JSON.stringify(options)}`);\n\n if (!options.outputFile.path) {\n throw new Error('outputFile.path must be provided to determine output format');\n }\n\n const extension = getExtension(options.outputFile.path);\n const { client, cleanup, version } = await setupMasterDatabase(options.masterMetadata);\n\n try {\n if (extension === '.json') {\n const result = getMasterData(client, version);\n await writeOutput(options.outputFile, JSON.stringify(result, null, 2));\n } else if (extension === '.db' || extension === '.sqlite') {\n await writeOutput(options.outputFile, client.export());\n } else {\n throw new Error(`Unsupported output extension: ${extension}`);\n }\n } finally {\n await cleanup();\n }\n\n return options.outputFile.path;\n};\n\n/**\n * Retrieves complete book data including pages and titles.\n *\n * This is a convenience function that downloads a book's data and returns it\n * as a structured JavaScript object. The function handles the temporary file\n * creation and cleanup automatically.\n *\n * @param id - The unique identifier of the book to retrieve\n * @returns A promise that resolves to the complete book data including pages and titles\n *\n * @throws {Error} When download fails, file operations fail, or JSON parsing fails\n *\n * @example\n * ```typescript\n * const bookData = await getBook(123);\n * console.log(bookData.pages.length); // Number of pages in the book\n * console.log(bookData.titles?.length); // Number of title entries\n * ```\n */\nexport const getBook = async (id: number): Promise<BookData> => {\n logger.info(`getBook ${id}`);\n\n const { client, cleanup } = await setupBookDatabase(id);\n\n try {\n const data = await getBookData(client);\n\n const result: BookData = {\n pages: data.pages.map(mapPageRowToPage),\n titles: data.titles.map(mapTitleRowToTitle),\n };\n\n return result;\n } finally {\n await cleanup();\n }\n};\n\n/**\n * Retrieves complete master data including authors, books, and categories.\n *\n * This convenience function downloads the master database archive, builds an in-memory\n * SQLite database, and returns structured data for immediate consumption alongside\n * the version number of the snapshot.\n *\n * @returns A promise that resolves to the complete master dataset and its version\n */\nexport const getMaster = async (): Promise<MasterData> => {\n logger.info('getMaster');\n\n const { client, cleanup, version } = await setupMasterDatabase();\n\n try {\n return getMasterData(client, version);\n } finally {\n await cleanup();\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["SILENT_LOGGER: Logger","currentLogger: Logger","loggerProxy: Logger","runtimeConfig: Partial<ShamelaConfig>","ENV_MAP: Record<Exclude<ShamelaConfigKey, 'fetchImplementation'>, string>","merged: Row","merged: Row[]","ensureTableSchema","createTables","getData","candidates: string[]","wasmPath: string","wasmPath: string | undefined","statement: Statement","db: SqlJsDatabase","rows: QueryRow[]","sqlPromise: Promise<SqlJsStatic> | null","resolvedWasmPath: string | null","isNodeEnvironment","TABLE_MAP: Record<string, Tables>","tableDbs: Partial<Record<Tables, SqliteDatabase>>","error: any","bookResponse: GetBookMetadataResponsePayload","error: any","getBookData","response: Record<string, any>","getMasterData"],"sources":["../src/utils/logger.ts","../src/config.ts","../src/db/types.ts","../src/db/book.ts","../src/utils/wasm.ts","../src/db/sqlite.ts","../src/db/master.ts","../src/utils/common.ts","../src/utils/downloads.ts","../src/utils/network.ts","../src/utils/io.ts","../src/utils/validation.ts","../src/api.ts"],"sourcesContent":["/**\n * Signature accepted by logger methods.\n */\nexport type LogFunction = (...args: unknown[]) => void;\n\n/**\n * Contract expected from logger implementations consumed by the library.\n */\nexport interface Logger {\n debug: LogFunction;\n error: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n}\n\n/**\n * No-op logger used when consumers do not provide their own implementation.\n */\nexport const SILENT_LOGGER: Logger = Object.freeze({\n debug: () => {},\n error: () => {},\n info: () => {},\n warn: () => {},\n});\n\nlet currentLogger: Logger = SILENT_LOGGER;\n\n/**\n * Configures the active logger or falls back to {@link SILENT_LOGGER} when undefined.\n *\n * @param newLogger - The logger instance to use for subsequent log calls\n * @throws {Error} When the provided logger does not implement the required methods\n */\nexport const configureLogger = (newLogger?: Logger) => {\n if (!newLogger) {\n currentLogger = SILENT_LOGGER;\n return;\n }\n\n const requiredMethods: Array<keyof Logger> = ['debug', 'error', 'info', 'warn'];\n const missingMethod = requiredMethods.find((method) => typeof newLogger[method] !== 'function');\n\n if (missingMethod) {\n throw new Error(\n `Logger must implement debug, error, info, and warn methods. Missing: ${String(missingMethod)}`,\n );\n }\n\n currentLogger = newLogger;\n};\n\n/**\n * Retrieves the currently configured logger.\n */\nexport const getLogger = () => currentLogger;\n\n/**\n * Restores the logger configuration back to {@link SILENT_LOGGER}.\n */\nexport const resetLogger = () => {\n currentLogger = SILENT_LOGGER;\n};\n\n/**\n * Proxy that delegates logging calls to the active logger at invocation time.\n */\nconst loggerProxy: Logger = new Proxy({} as Logger, {\n get: (_target, property: keyof Logger) => {\n const activeLogger = getLogger();\n const value = activeLogger[property];\n\n if (typeof value === 'function') {\n return (...args: unknown[]) => (value as LogFunction).apply(activeLogger, args);\n }\n\n return value;\n },\n}) as Logger;\n\nexport default loggerProxy;\n","import type { ShamelaConfig, ShamelaConfigKey } from './types';\nimport type { Logger } from './utils/logger';\nimport { configureLogger, resetLogger } from './utils/logger';\n\n/**\n * Mutable runtime configuration overrides supplied at runtime via {@link configure}.\n */\nlet runtimeConfig: Partial<ShamelaConfig> = {};\n\n/**\n * Mapping between configuration keys and their corresponding environment variable names.\n */\nconst ENV_MAP: Record<Exclude<ShamelaConfigKey, 'fetchImplementation'>, string> = {\n apiKey: 'SHAMELA_API_KEY',\n booksEndpoint: 'SHAMELA_API_BOOKS_ENDPOINT',\n masterPatchEndpoint: 'SHAMELA_API_MASTER_PATCH_ENDPOINT',\n sqlJsWasmUrl: 'SHAMELA_SQLJS_WASM_URL',\n};\n\n/**\n * Detects whether the Node.js {@link process} global is available for reading environment variables.\n */\nconst isProcessAvailable = typeof process !== 'undefined' && Boolean(process?.env);\n\n/**\n * Reads a configuration value either from runtime overrides or environment variables.\n *\n * @param key - The configuration key to resolve\n * @returns The resolved configuration value if present\n */\nconst readEnv = <Key extends Exclude<ShamelaConfigKey, 'fetchImplementation'>>(key: Key) => {\n const runtimeValue = runtimeConfig[key];\n\n if (runtimeValue !== undefined) {\n return runtimeValue as ShamelaConfig[Key];\n }\n\n const envKey = ENV_MAP[key];\n\n if (isProcessAvailable) {\n return process.env[envKey] as ShamelaConfig[Key];\n }\n\n return undefined as ShamelaConfig[Key];\n};\n\n/**\n * Runtime configuration options accepted by {@link configure}.\n */\nexport type ConfigureOptions = Partial<ShamelaConfig> & { logger?: Logger };\n\n/**\n * Updates the runtime configuration for the library.\n *\n * This function merges the provided options with existing overrides and optionally\n * configures a custom logger implementation.\n *\n * @param config - Runtime configuration overrides and optional logger instance\n */\nexport const configure = (config: ConfigureOptions) => {\n const { logger, ...options } = config;\n\n if ('logger' in config) {\n configureLogger(logger);\n }\n\n runtimeConfig = { ...runtimeConfig, ...options };\n};\n\n/**\n * Retrieves a single configuration value.\n *\n * @param key - The configuration key to read\n * @returns The configuration value when available\n */\nexport const getConfigValue = <Key extends ShamelaConfigKey>(key: Key) => {\n if (key === 'fetchImplementation') {\n return runtimeConfig.fetchImplementation as ShamelaConfig[Key];\n }\n\n return readEnv(key as Exclude<Key, 'fetchImplementation'>);\n};\n\n/**\n * Resolves the current configuration by combining runtime overrides and environment variables.\n *\n * @returns The resolved {@link ShamelaConfig}\n */\nexport const getConfig = (): ShamelaConfig => {\n return {\n apiKey: readEnv('apiKey'),\n booksEndpoint: readEnv('booksEndpoint'),\n fetchImplementation: runtimeConfig.fetchImplementation,\n masterPatchEndpoint: readEnv('masterPatchEndpoint'),\n sqlJsWasmUrl: readEnv('sqlJsWasmUrl'),\n };\n};\n\n/**\n * Retrieves a configuration value and throws if it is missing.\n *\n * @param key - The configuration key to require\n * @throws {Error} If the configuration value is not defined\n * @returns The resolved configuration value\n */\nexport const requireConfigValue = <Key extends Exclude<ShamelaConfigKey, 'fetchImplementation'>>(key: Key) => {\n if ((key as ShamelaConfigKey) === 'fetchImplementation') {\n throw new Error('fetchImplementation must be provided via configure().');\n }\n\n const value = getConfigValue(key);\n if (!value) {\n throw new Error(`${ENV_MAP[key]} environment variable not set`);\n }\n\n return value as NonNullable<ShamelaConfig[Key]>;\n};\n\n/**\n * Clears runtime configuration overrides and restores the default logger.\n */\nexport const resetConfig = () => {\n runtimeConfig = {};\n resetLogger();\n};\n","/**\n * Enumeration of database table names.\n */\nexport enum Tables {\n /** Author table */\n Authors = 'author',\n /** Book table */\n Books = 'book',\n /** Category table */\n Categories = 'category',\n /** Page table */\n Page = 'page',\n /** Title table */\n Title = 'title',\n}\n\n/**\n * A record that can be deleted by patches.\n */\nexport type Deletable = {\n /** Indicates if it was deleted in the patch if it is set to '1 */\n is_deleted?: string;\n};\n\nexport type Unique = {\n /** Unique identifier */\n id: number;\n};\n\n/**\n * Database row structure for the author table.\n */\nexport type AuthorRow = Deletable &\n Unique & {\n /** Author biography */\n biography: string;\n\n /** Death year */\n death_number: string;\n\n /** The death year as a text */\n death_text: string;\n\n /** Author name */\n name: string;\n };\n\n/**\n * Database row structure for the book table.\n */\nexport type BookRow = Deletable &\n Unique & {\n /** Serialized author ID(s) \"2747, 3147\" or \"513\" */\n author: string;\n\n /** Bibliography information */\n bibliography: string;\n\n /** Category ID */\n category: string;\n\n /** Publication date (or 99999 for unavailable) */\n date: string;\n\n /** Hint or description */\n hint: string;\n\n /** Major version */\n major_release: string;\n\n /** Serialized metadata */\n metadata: string;\n\n /** Minor version */\n minor_release: string;\n\n /** Book name */\n name: string;\n\n /** Serialized PDF links */\n pdf_links: string;\n\n /** Printed flag */\n printed: string;\n\n /** Book type */\n type: string;\n };\n\n/**\n * Database row structure for the category table.\n */\nexport type CategoryRow = Deletable &\n Unique & {\n /** Category name */\n name: string;\n\n /** Category order in the list to show. */\n order: string;\n };\n\n/**\n * Database row structure for the page table.\n */\nexport type PageRow = Deletable &\n Unique & {\n /** Page content */\n content: string;\n\n /** Page number */\n number: string | null;\n\n /** Page reference */\n page: string | null;\n\n /** Part number */\n part: string | null;\n\n /** Additional metadata */\n services: string | null;\n };\n\n/**\n * Database row structure for the title table.\n */\nexport type TitleRow = Deletable &\n Unique & {\n /** Title content */\n content: string;\n\n /** Page number */\n page: string;\n\n /** Parent title ID */\n parent: string | null;\n };\n","import logger from '@/utils/logger';\nimport type { SqliteDatabase } from './sqlite';\nimport { type Deletable, type PageRow, Tables, type TitleRow } from './types';\n\ntype Row = Record<string, any> & Deletable;\n\nconst PATCH_NOOP_VALUE = '#';\n\n/**\n * Retrieves column information for a specified table.\n * @param db - The database instance\n * @param table - The table name to get info for\n * @returns Array of column information with name and type\n */\nconst getTableInfo = (db: SqliteDatabase, table: Tables) => {\n return db.query(`PRAGMA table_info(${table})`).all() as { name: string; type: string }[];\n};\n\n/**\n * Checks if a table exists in the database.\n * @param db - The database instance\n * @param table - The table name to check\n * @returns True if the table exists, false otherwise\n */\nconst hasTable = (db: SqliteDatabase, table: Tables): boolean => {\n const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?1`).get(table) as\n | { name: string }\n | undefined;\n return Boolean(result);\n};\n\n/**\n * Reads all rows from a specified table.\n * @param db - The database instance\n * @param table - The table name to read from\n * @returns Array of rows, or empty array if table doesn't exist\n */\nconst readRows = (db: SqliteDatabase, table: Tables): Row[] => {\n if (!hasTable(db, table)) {\n return [];\n }\n\n return db.query(`SELECT * FROM ${table}`).all() as Row[];\n};\n\n/**\n * Checks if a row is marked as deleted.\n * @param row - The row to check\n * @returns True if the row has is_deleted field set to '1', false otherwise\n */\nconst isDeleted = (row: Row): boolean => {\n return String(row.is_deleted) === '1';\n};\n\n/**\n * Merges values from a base row and patch row, with patch values taking precedence.\n * @param baseRow - The original row data (can be undefined)\n * @param patchRow - The patch row data with updates (can be undefined)\n * @param columns - Array of column names to merge\n * @returns Merged row with combined values\n */\nconst mergeRowValues = (baseRow: Row | undefined, patchRow: Row | undefined, columns: string[]): Row => {\n const merged: Row = {};\n\n for (const column of columns) {\n if (column === 'id') {\n merged.id = (patchRow ?? baseRow)?.id ?? null;\n continue;\n }\n\n if (patchRow && column in patchRow) {\n const value = patchRow[column];\n\n if (value !== PATCH_NOOP_VALUE && value !== null && value !== undefined) {\n merged[column] = value;\n continue;\n }\n }\n\n if (baseRow && column in baseRow) {\n merged[column] = baseRow[column];\n continue;\n }\n\n merged[column] = null;\n }\n\n return merged;\n};\n\n/**\n * Merges arrays of base rows and patch rows, handling deletions and updates.\n * @param baseRows - Original rows from the base database\n * @param patchRows - Patch rows containing updates, additions, and deletions\n * @param columns - Array of column names to merge\n * @returns Array of merged rows with patches applied\n */\nconst mergeRows = (baseRows: Row[], patchRows: Row[], columns: string[]): Row[] => {\n const baseIds = new Set<string>();\n const patchById = new Map<string, Row>();\n\n for (const row of baseRows) {\n baseIds.add(String(row.id));\n }\n\n for (const row of patchRows) {\n patchById.set(String(row.id), row);\n }\n\n const merged: Row[] = [];\n\n for (const baseRow of baseRows) {\n const patchRow = patchById.get(String(baseRow.id));\n\n if (patchRow && isDeleted(patchRow)) {\n continue;\n }\n\n merged.push(mergeRowValues(baseRow, patchRow, columns));\n }\n\n for (const row of patchRows) {\n const id = String(row.id);\n\n if (baseIds.has(id) || isDeleted(row)) {\n continue;\n }\n\n merged.push(mergeRowValues(undefined, row, columns));\n }\n\n return merged;\n};\n\n/**\n * Inserts multiple rows into a specified table using a prepared statement.\n * @param db - The database instance\n * @param table - The table name to insert into\n * @param columns - Array of column names\n * @param rows - Array of row data to insert\n */\nconst insertRows = (db: SqliteDatabase, table: Tables, columns: string[], rows: Row[]) => {\n if (rows.length === 0) {\n return;\n }\n\n const placeholders = columns.map(() => '?').join(',');\n const statement = db.prepare(`INSERT INTO ${table} (${columns.join(',')}) VALUES (${placeholders})`);\n\n rows.forEach((row) => {\n const values = columns.map((column) => (column in row ? row[column] : null));\n // Spread the values array instead of passing it directly\n statement.run(...values);\n });\n\n statement.finalize();\n};\n\n/**\n * Ensures the target database has the same table schema as the source database.\n * @param target - The target database to create/update the table in\n * @param source - The source database to copy the schema from\n * @param table - The table name to ensure schema for\n * @returns True if schema was successfully ensured, false otherwise\n */\nconst ensureTableSchema = (target: SqliteDatabase, source: SqliteDatabase, table: Tables) => {\n const row = source.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?1`).get(table) as\n | { sql: string }\n | undefined;\n\n if (!row?.sql) {\n logger.warn(`${table} table definition missing in source database`);\n return false;\n }\n\n target.run(`DROP TABLE IF EXISTS ${table}`);\n target.run(row.sql);\n return true;\n};\n\n/**\n * Copies and patches a table from source to target database, applying patch updates if provided.\n * @param target - The target database to copy/patch the table to\n * @param source - The source database containing the base table data\n * @param patch - Optional patch database containing updates (can be null)\n * @param table - The table name to copy and patch\n */\nconst copyAndPatchTable = (\n target: SqliteDatabase,\n source: SqliteDatabase,\n patch: SqliteDatabase | null,\n table: Tables,\n) => {\n if (!hasTable(source, table)) {\n logger.warn(`${table} table missing in source database`);\n return;\n }\n\n if (!ensureTableSchema(target, source, table)) {\n return;\n }\n\n const baseInfo = getTableInfo(source, table);\n const patchInfo = patch && hasTable(patch, table) ? getTableInfo(patch, table) : [];\n\n const columns = baseInfo.map((info) => info.name);\n\n for (const info of patchInfo) {\n if (!columns.includes(info.name)) {\n const columnType = info.type && info.type.length > 0 ? info.type : 'TEXT';\n target.run(`ALTER TABLE ${table} ADD COLUMN ${info.name} ${columnType}`);\n columns.push(info.name);\n }\n }\n\n const baseRows = readRows(source, table);\n const patchRows = patch ? readRows(patch, table) : [];\n\n const mergedRows = mergeRows(baseRows, patchRows, columns);\n\n insertRows(target, table, columns, mergedRows);\n};\n\n/**\n * Applies patches from a patch database to the main database.\n * @param db - The target database to apply patches to\n * @param aslDB - Path to the source ASL database file\n * @param patchDB - Path to the patch database file\n */\nexport const applyPatches = (db: SqliteDatabase, source: SqliteDatabase, patch: SqliteDatabase) => {\n db.transaction(() => {\n copyAndPatchTable(db, source, patch, Tables.Page);\n copyAndPatchTable(db, source, patch, Tables.Title);\n })();\n};\n\n/**\n * Copies table data from a source database without applying any patches.\n * @param db - The target database to copy data to\n * @param aslDB - Path to the source ASL database file\n */\nexport const copyTableData = (db: SqliteDatabase, source: SqliteDatabase) => {\n db.transaction(() => {\n copyAndPatchTable(db, source, null, Tables.Page);\n copyAndPatchTable(db, source, null, Tables.Title);\n })();\n};\n\n/**\n * Creates the required tables (Page and Title) in the database with their schema.\n * @param db - The database instance to create tables in\n */\nexport const createTables = (db: SqliteDatabase) => {\n db.run(\n `CREATE TABLE ${Tables.Page} (\n id INTEGER,\n content TEXT,\n part TEXT,\n page TEXT,\n number TEXT,\n services TEXT,\n is_deleted TEXT\n )`,\n );\n db.run(\n `CREATE TABLE ${Tables.Title} (\n id INTEGER,\n content TEXT,\n page INTEGER,\n parent INTEGER,\n is_deleted TEXT\n )`,\n );\n};\n\n/**\n * Retrieves all pages from the Page table.\n * @param db - The database instance\n * @returns Array of all pages\n */\nexport const getAllPages = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Page}`).all() as PageRow[];\n};\n\n/**\n * Retrieves all titles from the Title table.\n * @param db - The database instance\n * @returns Array of all titles\n */\nexport const getAllTitles = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Title}`).all() as TitleRow[];\n};\n\n/**\n * Retrieves all book data including pages and titles.\n * @param db - The database instance\n * @returns Object containing arrays of pages and titles\n */\nexport const getData = (db: SqliteDatabase) => {\n return { pages: getAllPages(db), titles: getAllTitles(db) };\n};\n","/**\n * Utility for resolving the sql.js WASM file path in different runtime environments.\n * Handles bundled environments (Next.js, webpack, Turbopack), monorepos, and standard Node.js.\n */\n\n/**\n * Checks if a file exists at the given path (Node.js only).\n *\n * @param path - Absolute path to validate\n * @returns True when the file system entry is present\n */\nconst fileExists = (path: string) => {\n try {\n const fs = require('node:fs');\n return fs.existsSync(path);\n } catch {\n return false;\n }\n};\n\n/**\n * Try common node_modules patterns from process.cwd()\n */\nconst getNodeModuleWasmPath = () => {\n try {\n const pathModule = require('node:path');\n const cwd = process.cwd();\n\n const candidates: string[] = [\n // Standard location\n pathModule.join(cwd, 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n // Monorepo or workspace root\n pathModule.join(cwd, '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n pathModule.join(cwd, '../..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n // Next.js specific locations\n pathModule.join(cwd, '.next', 'server', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),\n ];\n\n for (const candidate of candidates) {\n if (fileExists(candidate)) {\n return candidate;\n }\n }\n } catch {\n // Continue to next strategy\n }\n};\n\n/**\n * Try to resolve using require.resolve (works in most Node.js scenarios)\n */\nconst getWasmFilePath = () => {\n try {\n const sqlJsPath = require.resolve('sql.js');\n const pathModule = require('node:path');\n const sqlJsDir = pathModule.dirname(sqlJsPath);\n const wasmPath: string = pathModule.join(sqlJsDir, 'dist', 'sql-wasm.wasm');\n\n if (fileExists(wasmPath)) {\n return wasmPath;\n }\n } catch {\n // Continue to next strategy\n }\n};\n\n/**\n * Try using require.resolve.paths to find all possible locations\n */\nconst getResolvedPath = () => {\n try {\n const pathModule = require('node:path');\n const searchPaths = require.resolve.paths('sql.js') || [];\n\n for (const searchPath of searchPaths) {\n const wasmPath: string = pathModule.join(searchPath, 'sql.js', 'dist', 'sql-wasm.wasm');\n if (fileExists(wasmPath)) {\n return wasmPath;\n }\n }\n } catch {\n // Continue to next strategy\n }\n};\n\nconst getMetaUrlPath = () => {\n try {\n const url = new URL('../../node_modules/sql.js/dist/sql-wasm.wasm', import.meta.url);\n const path = decodeURIComponent(url.pathname);\n\n // On Windows, file URLs start with /C:/ but we need C:/\n const normalizedPath = process.platform === 'win32' && path.startsWith('/') ? path.slice(1) : path;\n\n if (fileExists(normalizedPath)) {\n return normalizedPath;\n }\n } catch {\n // All strategies exhausted\n }\n};\n\n/**\n * Attempts to find the sql.js WASM file in node_modules using multiple strategies.\n * This handles cases where the library is bundled by tools like webpack/Turbopack.\n *\n * @returns The resolved filesystem path to the WASM file, or null if not found\n */\nexport const findNodeWasmPath = () => {\n let wasmPath: string | undefined = '';\n\n // Strategy 1: Try to resolve using require.resolve (works in most Node.js scenarios)\n if (typeof require?.resolve !== 'undefined') {\n wasmPath = getWasmFilePath();\n }\n\n if (!wasmPath && 'cwd' in process) {\n wasmPath = getNodeModuleWasmPath();\n }\n\n if (!wasmPath && typeof require?.resolve?.paths !== 'undefined') {\n wasmPath = getResolvedPath();\n }\n\n if (!wasmPath && typeof import.meta !== 'undefined' && import.meta.url) {\n wasmPath = getMetaUrlPath();\n }\n\n return wasmPath;\n};\n","import initSqlJs, { type Database as SqlJsDatabase, type SqlJsStatic, type Statement } from 'sql.js';\n\nimport { getConfigValue } from '@/config';\nimport { findNodeWasmPath } from '@/utils/wasm';\n\n/**\n * Represents a row returned from a SQLite query as a generic key-value object.\n */\nexport type QueryRow = Record<string, any>;\n\n/**\n * Minimal contract for prepared statements used throughout the project.\n */\nexport interface PreparedStatement {\n run: (...params: any[]) => void;\n finalize: () => void;\n}\n\n/**\n * Interface describing reusable query helpers that return all rows or a single row.\n */\nexport interface Query {\n all: (...params: any[]) => QueryRow[];\n get: (...params: any[]) => QueryRow | undefined;\n}\n\n/**\n * Abstraction over the subset of SQLite database operations required by the library.\n */\nexport interface SqliteDatabase {\n run: (sql: string, params?: any[]) => void;\n prepare: (sql: string) => PreparedStatement;\n query: (sql: string) => Query;\n transaction: (fn: () => void) => () => void;\n close: () => void;\n export: () => Uint8Array;\n}\n\n/**\n * Adapter implementing {@link PreparedStatement} by delegating to a sql.js {@link Statement}.\n */\nclass SqlJsPreparedStatement implements PreparedStatement {\n constructor(private readonly statement: Statement) {}\n\n run = (...params: any[]) => {\n if (params.length > 0) {\n this.statement.bind(params);\n }\n\n this.statement.step();\n this.statement.reset();\n };\n\n finalize = () => {\n this.statement.free();\n };\n}\n\n/**\n * Wrapper providing the {@link SqliteDatabase} interface on top of a sql.js database instance.\n */\nclass SqlJsDatabaseWrapper implements SqliteDatabase {\n constructor(private readonly db: SqlJsDatabase) {}\n\n run = (sql: string, params: any[] = []) => {\n this.db.run(sql, params);\n };\n\n prepare = (sql: string): PreparedStatement => {\n return new SqlJsPreparedStatement(this.db.prepare(sql));\n };\n\n query = (sql: string): Query => {\n return {\n all: (...params: any[]) => this.all(sql, params),\n get: (...params: any[]) => this.get(sql, params),\n };\n };\n\n transaction = (fn: () => void) => {\n return () => {\n this.db.run('BEGIN TRANSACTION');\n try {\n fn();\n this.db.run('COMMIT');\n } catch (error) {\n this.db.run('ROLLBACK');\n throw error;\n }\n };\n };\n\n close = () => {\n this.db.close();\n };\n\n export = () => {\n return this.db.export();\n };\n\n private all = (sql: string, params: any[]): QueryRow[] => {\n const statement = this.db.prepare(sql);\n try {\n if (params.length > 0) {\n statement.bind(params);\n }\n\n const rows: QueryRow[] = [];\n while (statement.step()) {\n rows.push(statement.getAsObject());\n }\n return rows;\n } finally {\n statement.free();\n }\n };\n\n private get = (sql: string, params: any[]): QueryRow | undefined => {\n const [row] = this.all(sql, params);\n return row;\n };\n}\n\nlet sqlPromise: Promise<SqlJsStatic> | null = null;\nlet resolvedWasmPath: string | null = null;\n\nconst isNodeEnvironment = typeof process !== 'undefined' && Boolean(process?.versions?.node);\nconst DEFAULT_BROWSER_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/sql-wasm.wasm';\n\n/**\n * Resolves the appropriate location of the sql.js WebAssembly binary.\n *\n * @returns The resolved path or remote URL for the sql.js wasm asset\n */\nconst getWasmPath = () => {\n if (!resolvedWasmPath) {\n // First priority: user configuration\n const configured = getConfigValue('sqlJsWasmUrl');\n if (configured) {\n resolvedWasmPath = configured;\n } else if (isNodeEnvironment) {\n // Second priority: auto-detect in Node.js\n const nodePath = findNodeWasmPath();\n if (nodePath) {\n resolvedWasmPath = nodePath;\n } else {\n // Fallback: provide helpful error with suggestions\n const errorMsg = [\n 'Unable to automatically locate sql-wasm.wasm file.',\n 'This can happen in bundled environments (Next.js, webpack, etc.).',\n '',\n 'Quick fix - add this to your code before using shamela:',\n '',\n ' import { configure, createNodeConfig } from \"shamela\";',\n ' configure(createNodeConfig({',\n ' apiKey: process.env.SHAMELA_API_KEY,',\n ' booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT,',\n ' masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT,',\n ' }));',\n '',\n 'Or manually specify the path:',\n '',\n ' import { configure } from \"shamela\";',\n ' import { join } from \"node:path\";',\n ' configure({',\n ' sqlJsWasmUrl: join(process.cwd(), \"node_modules\", \"sql.js\", \"dist\", \"sql-wasm.wasm\")',\n ' });',\n ].join('\\n');\n\n throw new Error(errorMsg);\n }\n } else {\n // Browser environment: use CDN\n resolvedWasmPath = DEFAULT_BROWSER_WASM_URL;\n }\n }\n\n return resolvedWasmPath;\n};\n\n/**\n * Lazily initialises the sql.js runtime, reusing the same promise for subsequent calls.\n *\n * @returns A promise resolving to the sql.js module\n */\nconst loadSql = () => {\n if (!sqlPromise) {\n sqlPromise = initSqlJs({\n locateFile: () => getWasmPath(),\n });\n }\n\n return sqlPromise;\n};\n\n/**\n * Creates a new in-memory SQLite database instance backed by sql.js.\n *\n * @returns A promise resolving to a {@link SqliteDatabase} wrapper\n */\nexport const createDatabase = async () => {\n const SQL = await loadSql();\n return new SqlJsDatabaseWrapper(new SQL.Database());\n};\n\n/**\n * Opens an existing SQLite database from the provided binary contents.\n *\n * @param data - The Uint8Array containing the SQLite database bytes\n * @returns A promise resolving to a {@link SqliteDatabase} wrapper\n */\nexport const openDatabase = async (data: Uint8Array) => {\n const SQL = await loadSql();\n return new SqlJsDatabaseWrapper(new SQL.Database(data));\n};\n","import type { Author, Book, Category, MasterData } from '../types';\nimport type { SqliteDatabase } from './sqlite';\nimport { openDatabase } from './sqlite';\nimport { Tables } from './types';\n\n/**\n * Ensures the target database has the same table schema as the source database for a specific table.\n * @param db - The database instance\n * @param alias - The alias name of the attached database\n * @param table - The table name to ensure schema for\n * @throws {Error} When table definition is missing in the source database\n */\nconst ensureTableSchema = (db: SqliteDatabase, source: SqliteDatabase, table: Tables) => {\n const row = source.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?1`).get(table) as\n | { sql: string }\n | undefined;\n\n if (!row?.sql) {\n throw new Error(`Missing table definition for ${table} in source database`);\n }\n\n db.run(`DROP TABLE IF EXISTS ${table}`);\n db.run(row.sql);\n};\n\n/**\n * Copies data from foreign master table files into the main master database.\n *\n * This function processes the source table files (author.sqlite, book.sqlite, category.sqlite)\n * by attaching them to the current database connection, then copying their data into\n * the main master database tables. It handles data transformation including filtering\n * out deleted records and converting placeholder values.\n *\n * @param db - The database client instance for the master database\n * @param sourceTables - Array of file paths to the source SQLite table files\n *\n * @throws {Error} When source files cannot be attached or data copying operations fail\n */\nexport const copyForeignMasterTableData = async (\n db: SqliteDatabase,\n sourceTables: Array<{ name: string; data: Uint8Array }>,\n) => {\n const TABLE_MAP: Record<string, Tables> = {\n author: Tables.Authors,\n book: Tables.Books,\n category: Tables.Categories,\n };\n\n const tableDbs: Partial<Record<Tables, SqliteDatabase>> = {};\n\n for (const table of sourceTables) {\n const baseName = table.name.split('/').pop()?.split('\\\\').pop() ?? table.name;\n const normalized = baseName.replace(/\\.(sqlite|db)$/i, '').toLowerCase();\n const tableName = TABLE_MAP[normalized];\n if (!tableName) {\n continue;\n }\n\n tableDbs[tableName] = await openDatabase(table.data);\n }\n\n try {\n const entries = Object.entries(tableDbs) as Array<[Tables, SqliteDatabase]>;\n\n db.transaction(() => {\n for (const [table, sourceDb] of entries) {\n ensureTableSchema(db, sourceDb, table);\n\n const columnInfo = sourceDb.query(`PRAGMA table_info(${table})`).all() as Array<{\n name: string;\n type: string;\n }>;\n const columnNames = columnInfo.map((info) => info.name);\n if (columnNames.length === 0) {\n continue;\n }\n\n const rows = sourceDb.query(`SELECT * FROM ${table}`).all();\n if (rows.length === 0) {\n continue;\n }\n\n const placeholders = columnNames.map(() => '?').join(',');\n const sqlColumns = columnNames.map((name) => (name === 'order' ? '\"order\"' : name));\n const statement = db.prepare(`INSERT INTO ${table} (${sqlColumns.join(',')}) VALUES (${placeholders})`);\n\n try {\n for (const row of rows) {\n const values = columnNames.map((column) => (column in row ? row[column] : null));\n statement.run(...values);\n }\n } finally {\n statement.finalize();\n }\n }\n })();\n } finally {\n Object.values(tableDbs).forEach((database) => database?.close());\n }\n};\n\n/**\n * Creates a backward-compatible database view for legacy table names.\n * @param db - The database instance\n * @param viewName - The name of the view to create\n * @param sourceTable - The source table to base the view on\n */\nconst createCompatibilityView = (db: SqliteDatabase, viewName: string, sourceTable: Tables) => {\n db.run(`DROP VIEW IF EXISTS ${viewName}`);\n db.run(`CREATE VIEW ${viewName} AS SELECT * FROM ${sourceTable}`);\n};\n\n/**\n * Creates the necessary database tables for the master database.\n *\n * This function sets up the schema for the master database by creating\n * tables for authors, books, and categories with their respective columns\n * and data types. This is typically the first step in setting up a new\n * master database. Also creates backward-compatible views for legacy table names.\n *\n * @param db - The database client instance where tables should be created\n *\n * @throws {Error} When table creation fails due to database constraints or permissions\n */\nexport const createTables = (db: SqliteDatabase) => {\n db.run(\n `CREATE TABLE ${Tables.Authors} (\n id INTEGER,\n is_deleted TEXT,\n name TEXT,\n biography TEXT,\n death_text TEXT,\n death_number TEXT\n )`,\n );\n db.run(\n `CREATE TABLE ${Tables.Books} (\n id INTEGER,\n name TEXT,\n is_deleted TEXT,\n category TEXT,\n type TEXT,\n date TEXT,\n author TEXT,\n printed TEXT,\n minor_release TEXT,\n major_release TEXT,\n bibliography TEXT,\n hint TEXT,\n pdf_links TEXT,\n metadata TEXT\n )`,\n );\n db.run(\n `CREATE TABLE ${Tables.Categories} (\n id INTEGER,\n is_deleted TEXT,\n \"order\" TEXT,\n name TEXT\n )`,\n );\n\n // Provide backward-compatible pluralised views since callers historically\n // queried \"authors\", \"books\", and \"categories\" tables.\n createCompatibilityView(db, 'authors', Tables.Authors);\n createCompatibilityView(db, 'books', Tables.Books);\n createCompatibilityView(db, 'categories', Tables.Categories);\n};\n\n/**\n * Retrieves all authors from the Authors table.\n * @param db - The database instance\n * @returns Array of all authors\n */\nexport const getAllAuthors = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Authors}`).all() as Author[];\n};\n\n/**\n * Retrieves all books from the Books table.\n * @param db - The database instance\n * @returns Array of all books\n */\nexport const getAllBooks = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Books}`).all() as Book[];\n};\n\n/**\n * Retrieves all categories from the Categories table.\n * @param db - The database instance\n * @returns Array of all categories\n */\nexport const getAllCategories = (db: SqliteDatabase) => {\n return db.query(`SELECT * FROM ${Tables.Categories}`).all() as Category[];\n};\n\n/**\n * Retrieves all master data including authors, books, and categories.\n * @param db - The database instance\n * @returns Object containing arrays of authors, books, and categories\n */\nexport const getData = (db: SqliteDatabase, version: number) => {\n return {\n authors: getAllAuthors(db),\n books: getAllBooks(db),\n categories: getAllCategories(db),\n version,\n } satisfies MasterData;\n};\n","import type { PageRow, TitleRow } from '@/db/types';\n\n/**\n * Redacts sensitive query parameters from a URL for safe logging\n * @param url - The URL to redact\n * @param sensitiveParams - Array of parameter names to redact (defaults to common sensitive params)\n * @returns The URL string with sensitive parameters redacted\n */\nexport const redactUrl = (\n url: URL | string,\n sensitiveParams: string[] = ['api_key', 'token', 'password', 'secret', 'auth'],\n): string => {\n const urlObj = typeof url === 'string' ? new URL(url) : new URL(url.toString());\n\n sensitiveParams.forEach((param) => {\n const value = urlObj.searchParams.get(param);\n if (value && value.length > 6) {\n const redacted = `${value.slice(0, 3)}***${value.slice(-3)}`;\n urlObj.searchParams.set(param, redacted);\n } else if (value) {\n urlObj.searchParams.set(param, '***');\n }\n });\n\n return urlObj.toString();\n};\n\n/**\n * Normalises a raw page row from SQLite into a serialisable {@link Page}.\n *\n * @param page - The database row representing a page\n * @returns The mapped page with numeric fields converted where appropriate\n */\nexport const mapPageRowToPage = (page: PageRow) => {\n return {\n content: page.content,\n id: page.id,\n ...(page.number && { number: page.number }),\n ...(page.page && { page: Number(page.page) }),\n ...(page.part && { part: page.part }),\n };\n};\n\n/**\n * Normalises a raw title row from SQLite into a serialisable {@link Title}.\n *\n * @param title - The database row representing a title\n * @returns The mapped title with numeric identifiers converted\n */\nexport const mapTitleRowToTitle = (title: TitleRow) => {\n const parent = Number(title.parent);\n\n return {\n content: title.content,\n id: title.id,\n page: Number(title.page),\n ...(parent && { parent }),\n };\n};\n","import type { UnzippedEntry } from '@/utils/io';\n\n/**\n * Enforces HTTPS protocol for a given URL string.\n *\n * @param originalUrl - The URL that may use an insecure scheme\n * @returns The normalized URL string using the HTTPS protocol\n */\nexport const fixHttpsProtocol = (originalUrl: string): string => {\n const url = new URL(originalUrl);\n url.protocol = 'https';\n\n return url.toString();\n};\n\n/**\n * Determines whether an archive entry contains a SQLite database file.\n *\n * @param entry - The entry extracted from an archive\n * @returns True when the entry name ends with a recognized SQLite extension\n */\nexport const isSqliteEntry = (entry: UnzippedEntry): boolean => /\\.(sqlite|db)$/i.test(entry.name);\n\n/**\n * Finds the first SQLite database entry from a list of archive entries.\n *\n * @param entries - The extracted entries to inspect\n * @returns The first matching entry or undefined when not present\n */\nexport const findSqliteEntry = (entries: UnzippedEntry[]): UnzippedEntry | undefined => {\n return entries.find(isSqliteEntry);\n};\n\n/**\n * Extracts the lowercase file extension from a path or filename.\n *\n * @param filePath - The path to inspect\n * @returns The lowercase extension (including the dot) or an empty string\n */\nexport const getExtension = (filePath: string): string => {\n const match = /\\.([^.]+)$/.exec(filePath);\n return match ? `.${match[1].toLowerCase()}` : '';\n};\n","import { getConfig, requireConfigValue } from '@/config';\n\n/**\n * Builds a URL with query parameters and optional authentication.\n * @param {string} endpoint - The base endpoint URL\n * @param {Record<string, any>} queryParams - Object containing query parameters to append\n * @param {boolean} [useAuth=true] - Whether to include the API key from environment variables\n * @returns {URL} The constructed URL object with query parameters\n */\nexport const buildUrl = (endpoint: string, queryParams: Record<string, any>, useAuth: boolean = true): URL => {\n const url = new URL(endpoint);\n const params = new URLSearchParams();\n\n Object.entries(queryParams).forEach(([key, value]) => {\n params.append(key, value.toString());\n });\n\n if (useAuth) {\n params.append('api_key', requireConfigValue('apiKey'));\n }\n\n url.search = params.toString();\n\n return url;\n};\n\n/**\n * Makes an HTTPS GET request and returns the response data using the configured fetch implementation.\n * @template T - The expected return type (Buffer or Record<string, any>)\n * @param {string | URL} url - The URL to make the request to\n * @param options - Optional overrides including a custom fetch implementation\n * @returns {Promise<T>} A promise that resolves to the response data, parsed as JSON if content-type is application/json, otherwise as Buffer\n * @throws {Error} When the request fails or JSON parsing fails\n */\nexport const httpsGet = async <T extends Uint8Array | Record<string, any>>(\n url: string | URL,\n options: { fetchImpl?: typeof fetch } = {},\n): Promise<T> => {\n const target = typeof url === 'string' ? url : url.toString();\n const activeFetch = options.fetchImpl ?? getConfig().fetchImplementation ?? fetch;\n const response = await activeFetch(target);\n\n if (!response.ok) {\n throw new Error(`Error making request: ${response.status} ${response.statusText}`);\n }\n\n const contentType = response.headers.get('content-type') ?? '';\n\n if (contentType.includes('application/json')) {\n return (await response.json()) as T;\n }\n\n const buffer = await response.arrayBuffer();\n return new Uint8Array(buffer) as T;\n};\n","import { unzipSync } from 'fflate';\n\nimport type { OutputOptions } from '@/types';\nimport logger from './logger';\nimport { httpsGet } from './network';\n\n/**\n * Representation of an extracted archive entry containing raw bytes and filename metadata.\n */\nexport type UnzippedEntry = { name: string; data: Uint8Array };\n\nconst isNodeEnvironment = typeof process !== 'undefined' && Boolean(process?.versions?.node);\n\n/**\n * Dynamically imports the Node.js fs/promises module, ensuring the runtime supports file operations.\n *\n * @throws {Error} When executed in a non-Node.js environment\n * @returns The fs/promises module when available\n */\nconst ensureNodeFs = async () => {\n if (!isNodeEnvironment) {\n throw new Error('File system operations are only supported in Node.js environments');\n }\n\n return import('node:fs/promises');\n};\n\n/**\n * Ensures the directory for a file path exists, creating parent folders as needed.\n *\n * @param filePath - The target file path whose directory should be created\n * @returns The fs/promises module instance\n */\nconst ensureDirectory = async (filePath: string) => {\n const [fs, path] = await Promise.all([ensureNodeFs(), import('node:path')]);\n const directory = path.dirname(filePath);\n await fs.mkdir(directory, { recursive: true });\n return fs;\n};\n\n/**\n * Downloads a ZIP archive from the given URL and returns its extracted entries.\n *\n * @param url - The remote URL referencing a ZIP archive\n * @returns A promise resolving to the extracted archive entries\n */\nexport const unzipFromUrl = async (url: string): Promise<UnzippedEntry[]> => {\n const binary = await httpsGet<Uint8Array>(url);\n const byteLength =\n binary instanceof Uint8Array\n ? binary.length\n : binary && typeof (binary as ArrayBufferLike).byteLength === 'number'\n ? (binary as ArrayBufferLike).byteLength\n : 0;\n logger.debug('unzipFromUrl:bytes', byteLength);\n\n return new Promise((resolve, reject) => {\n const dataToUnzip = binary instanceof Uint8Array ? binary : new Uint8Array(binary as ArrayBufferLike);\n\n try {\n const result = unzipSync(dataToUnzip);\n const entries = Object.entries(result).map(([name, data]) => ({ data, name }));\n logger.debug(\n 'unzipFromUrl:entries',\n entries.map((entry) => entry.name),\n );\n resolve(entries);\n } catch (error: any) {\n reject(new Error(`Error processing URL: ${error.message}`));\n }\n });\n};\n\n/**\n * Creates a unique temporary directory with the provided prefix.\n *\n * @param prefix - Optional prefix for the generated directory name\n * @returns The created temporary directory path\n */\nexport const createTempDir = async (prefix = 'shamela') => {\n const [fs, os, path] = await Promise.all([ensureNodeFs(), import('node:os'), import('node:path')]);\n const base = path.join(os.tmpdir(), prefix);\n return fs.mkdtemp(base);\n};\n\n/**\n * Writes output data either using a provided writer function or to a file path.\n *\n * @param output - The configured output destination or writer\n * @param payload - The payload to persist (string or binary)\n * @throws {Error} When neither a writer nor file path is provided\n */\nexport const writeOutput = async (output: OutputOptions, payload: string | Uint8Array) => {\n if (output.writer) {\n await output.writer(payload);\n return;\n }\n\n if (!output.path) {\n throw new Error('Output options must include either a writer or a path');\n }\n\n const fs = await ensureDirectory(output.path);\n\n if (typeof payload === 'string') {\n await fs.writeFile(output.path, payload, 'utf-8');\n } else {\n await fs.writeFile(output.path, payload);\n }\n};\n","import { getConfig } from '@/config';\n\nconst SOURCE_TABLES = ['author.sqlite', 'book.sqlite', 'category.sqlite'];\n\n/**\n * Validates that the API key is configured.\n *\n * Note: The `booksEndpoint` and `masterPatchEndpoint` are validated lazily\n * when the corresponding API functions are called. This allows clients to\n * configure only the endpoint they need (e.g., only `booksEndpoint` if they\n * only use book APIs).\n *\n * @throws {Error} When the API key is not configured\n */\nexport const validateEnvVariables = () => {\n const { apiKey } = getConfig();\n\n if (!apiKey) {\n throw new Error('apiKey environment variable not set');\n }\n};\n\n/**\n * Validates that all required master source tables are present in the provided paths.\n * @param {string[]} sourceTablePaths - Array of file paths to validate\n * @returns {boolean} True if all required source tables (author.sqlite, book.sqlite, category.sqlite) are present\n */\nexport const validateMasterSourceTables = (sourceTablePaths: string[]) => {\n const sourceTableNames = new Set(\n sourceTablePaths\n .map((tablePath) => tablePath.match(/[^\\\\/]+$/)?.[0] ?? tablePath)\n .map((name) => name.toLowerCase()),\n );\n return SOURCE_TABLES.every((table) => sourceTableNames.has(table.toLowerCase()));\n};\n","import { requireConfigValue } from './config';\nimport { applyPatches, copyTableData, createTables as createBookTables, getData as getBookData } from './db/book';\nimport { copyForeignMasterTableData, createTables as createMasterTables, getData as getMasterData } from './db/master';\nimport { createDatabase, openDatabase, type SqliteDatabase } from './db/sqlite';\nimport type {\n BookData,\n DownloadBookOptions,\n DownloadMasterOptions,\n GetBookMetadataOptions,\n GetBookMetadataResponsePayload,\n GetMasterMetadataResponsePayload,\n MasterData,\n} from './types';\nimport { mapPageRowToPage, mapTitleRowToTitle, redactUrl } from './utils/common';\nimport { DEFAULT_MASTER_METADATA_VERSION } from './utils/constants';\nimport { findSqliteEntry, fixHttpsProtocol, getExtension, isSqliteEntry } from './utils/downloads';\nimport type { UnzippedEntry } from './utils/io';\nimport { unzipFromUrl, writeOutput } from './utils/io';\nimport logger from './utils/logger';\nimport { buildUrl, httpsGet } from './utils/network';\nimport { validateMasterSourceTables } from './utils/validation';\n\n/**\n * Response payload received when requesting book update metadata from the Shamela API.\n */\ntype BookUpdatesResponse = {\n major_release: number;\n major_release_url: string;\n minor_release?: number;\n minor_release_url?: string;\n};\n\n/**\n * Sets up a book database with tables and data, returning the database client.\n *\n * This helper function handles the common logic of downloading book files,\n * creating database tables, and applying patches or copying data.\n *\n * @param id - The unique identifier of the book\n * @param bookMetadata - Optional pre-fetched book metadata\n * @returns A promise that resolves to an object containing the database client and cleanup function\n */\nconst setupBookDatabase = async (\n id: number,\n bookMetadata?: GetBookMetadataResponsePayload,\n): Promise<{ client: SqliteDatabase; cleanup: () => Promise<void> }> => {\n logger.info(`Setting up book database for ${id}`);\n\n const bookResponse: GetBookMetadataResponsePayload = bookMetadata || (await getBookMetadata(id));\n const patchEntriesPromise = bookResponse.minorReleaseUrl\n ? unzipFromUrl(bookResponse.minorReleaseUrl)\n : Promise.resolve<UnzippedEntry[]>([]);\n\n const [bookEntries, patchEntries] = await Promise.all([\n unzipFromUrl(bookResponse.majorReleaseUrl),\n patchEntriesPromise,\n ]);\n\n const bookEntry = findSqliteEntry(bookEntries);\n\n if (!bookEntry) {\n throw new Error('Unable to locate book database in archive');\n }\n\n const client = await createDatabase();\n\n try {\n logger.info(`Creating tables`);\n createBookTables(client);\n\n const sourceDatabase = await openDatabase(bookEntry.data);\n\n try {\n const patchEntry = findSqliteEntry(patchEntries);\n\n if (patchEntry) {\n logger.info(`Applying patches from ${patchEntry.name} to ${bookEntry.name}`);\n const patchDatabase = await openDatabase(patchEntry.data);\n\n try {\n applyPatches(client, sourceDatabase, patchDatabase);\n } finally {\n patchDatabase.close();\n }\n } else {\n logger.info(`Copying table data from ${bookEntry.name}`);\n copyTableData(client, sourceDatabase);\n }\n } finally {\n sourceDatabase.close();\n }\n\n const cleanup = async () => {\n client.close();\n };\n\n return { cleanup, client };\n } catch (error) {\n client.close();\n throw error;\n }\n};\n\n/**\n * Downloads, validates, and prepares the master SQLite database for use.\n *\n * This helper is responsible for retrieving the master archive, ensuring all\n * required tables are present, copying their contents into a fresh in-memory\n * database, and returning both the database instance and cleanup hook.\n *\n * @param masterMetadata - Optional pre-fetched metadata describing the master archive\n * @returns A promise resolving to the database client, cleanup function, and version number\n */\nconst setupMasterDatabase = async (\n masterMetadata?: GetMasterMetadataResponsePayload,\n): Promise<{ client: SqliteDatabase; cleanup: () => Promise<void>; version: number }> => {\n logger.info('Setting up master database');\n\n const masterResponse = masterMetadata || (await getMasterMetadata(DEFAULT_MASTER_METADATA_VERSION));\n\n logger.info(`Downloading master database ${masterResponse.version} from: ${redactUrl(masterResponse.url)}`);\n const sourceTables = await unzipFromUrl(fixHttpsProtocol(masterResponse.url));\n\n logger.debug?.(`sourceTables downloaded: ${sourceTables.map((table) => table.name).toString()}`);\n\n if (!validateMasterSourceTables(sourceTables.map((table) => table.name))) {\n logger.error(`Some source tables were not found: ${sourceTables.map((table) => table.name).toString()}`);\n throw new Error('Expected tables not found!');\n }\n\n const client = await createDatabase();\n\n try {\n logger.info('Creating master tables');\n createMasterTables(client);\n\n logger.info('Copying data to master table');\n await copyForeignMasterTableData(client, sourceTables.filter(isSqliteEntry));\n\n const cleanup = async () => {\n client.close();\n };\n\n return { cleanup, client, version: masterResponse.version };\n } catch (error) {\n client.close();\n throw error;\n }\n};\n\n/**\n * Retrieves metadata for a specific book from the Shamela API.\n *\n * This function fetches book release information including major and minor release\n * URLs and version numbers from the Shamela web service.\n *\n * @param id - The unique identifier of the book to fetch metadata for\n * @param options - Optional parameters for specifying major and minor versions\n * @returns A promise that resolves to book metadata including release URLs and versions\n *\n * @throws {Error} When environment variables are not set or API request fails\n *\n * @example\n * ```typescript\n * const metadata = await getBookMetadata(123, { majorVersion: 1, minorVersion: 2 });\n * console.log(metadata.majorReleaseUrl); // Download URL for the book\n * ```\n */\nexport const getBookMetadata = async (\n id: number,\n options?: GetBookMetadataOptions,\n): Promise<GetBookMetadataResponsePayload> => {\n const booksEndpoint = requireConfigValue('booksEndpoint');\n const url = buildUrl(`${booksEndpoint}/${id}`, {\n major_release: (options?.majorVersion || 0).toString(),\n minor_release: (options?.minorVersion || 0).toString(),\n });\n\n logger.info(`Fetching shamela.ws book link: ${redactUrl(url)}`);\n\n try {\n const response = (await httpsGet(url)) as BookUpdatesResponse;\n return {\n majorRelease: response.major_release,\n majorReleaseUrl: fixHttpsProtocol(response.major_release_url),\n ...(response.minor_release_url && { minorReleaseUrl: fixHttpsProtocol(response.minor_release_url) }),\n ...(response.minor_release_url && { minorRelease: response.minor_release }),\n };\n } catch (error: any) {\n throw new Error(`Error fetching book metadata: ${error.message}`);\n }\n};\n\n/**\n * Downloads and processes a book from the Shamela database.\n *\n * This function downloads the book's database files, applies patches if available,\n * creates the necessary database tables, and exports the data to the specified format.\n * The output can be either a JSON file or a SQLite database file.\n *\n * @param id - The unique identifier of the book to download\n * @param options - Configuration options including output file path and optional book metadata\n * @returns A promise that resolves to the path of the created output file\n *\n * @throws {Error} When download fails, database operations fail, or file operations fail\n *\n * @example\n * ```typescript\n * // Download as JSON\n * const jsonPath = await downloadBook(123, {\n * outputFile: { path: './book.json' }\n * });\n *\n * // Download as SQLite database\n * const dbPath = await downloadBook(123, {\n * outputFile: { path: './book.db' }\n * });\n * ```\n */\nexport const downloadBook = async (id: number, options: DownloadBookOptions): Promise<string> => {\n logger.info(`downloadBook ${id} ${JSON.stringify(options)}`);\n\n if (!options.outputFile.path) {\n throw new Error('outputFile.path must be provided to determine output format');\n }\n\n const extension = getExtension(options.outputFile.path).toLowerCase();\n\n const { client, cleanup } = await setupBookDatabase(id, options?.bookMetadata);\n\n try {\n if (extension === '.json') {\n const result = await getBookData(client);\n await writeOutput(options.outputFile, JSON.stringify(result, null, 2));\n } else if (extension === '.db' || extension === '.sqlite') {\n const payload = client.export();\n await writeOutput(options.outputFile, payload);\n } else {\n throw new Error(`Unsupported output extension: ${extension}`);\n }\n } finally {\n await cleanup();\n }\n\n return options.outputFile.path;\n};\n\n/**\n * Retrieves metadata for the master database from the Shamela API.\n *\n * The master database contains information about all books, authors, and categories\n * in the Shamela library. This function fetches the download URL and version\n * information for the master database patches.\n *\n * @param version - The version number to check for updates (defaults to 0)\n * @returns A promise that resolves to master database metadata including download URL and version\n *\n * @throws {Error} When environment variables are not set or API request fails\n *\n * @example\n * ```typescript\n * const masterMetadata = await getMasterMetadata(5);\n * console.log(masterMetadata.url); // URL to download master database patch\n * console.log(masterMetadata.version); // Latest version number\n * ```\n */\nexport const getMasterMetadata = async (version: number = 0): Promise<GetMasterMetadataResponsePayload> => {\n const masterEndpoint = requireConfigValue('masterPatchEndpoint');\n const url = buildUrl(masterEndpoint, { version: version.toString() });\n\n logger.info(`Fetching shamela.ws master database patch link: ${redactUrl(url)}`);\n\n try {\n const response: Record<string, any> = await httpsGet(url);\n return { url: response.patch_url, version: response.version };\n } catch (error: any) {\n throw new Error(`Error fetching master patch: ${error.message}`);\n }\n};\n\n/**\n * Generates the URL for a book's cover image.\n *\n * This function constructs the URL to access the cover image for a specific book\n * using the book's ID and the API endpoint host.\n *\n * @param bookId - The unique identifier of the book\n * @returns The complete URL to the book's cover image\n *\n * @example\n * ```typescript\n * const coverUrl = getCoverUrl(123);\n * console.log(coverUrl); // \"https://api.shamela.ws/covers/123.jpg\"\n * ```\n */\nexport const getCoverUrl = (bookId: number) => {\n const masterEndpoint = requireConfigValue('masterPatchEndpoint');\n const { origin } = new URL(masterEndpoint);\n return `${origin}/covers/${bookId}.jpg`;\n};\n\n/**\n * Downloads and processes the master database from the Shamela service.\n *\n * The master database contains comprehensive information about all books, authors,\n * and categories available in the Shamela library. This function downloads the\n * database files, creates the necessary tables, and exports the data in the\n * specified format (JSON or SQLite).\n *\n * @param options - Configuration options including output file path and optional master metadata\n * @returns A promise that resolves to the path of the created output file\n *\n * @throws {Error} When download fails, expected tables are missing, database operations fail, or file operations fail\n *\n * @example\n * ```typescript\n * // Download master database as JSON\n * const jsonPath = await downloadMasterDatabase({\n * outputFile: { path: './master.json' }\n * });\n *\n * // Download master database as SQLite\n * const dbPath = await downloadMasterDatabase({\n * outputFile: { path: './master.db' }\n * });\n * ```\n */\nexport const downloadMasterDatabase = async (options: DownloadMasterOptions): Promise<string> => {\n logger.info(`downloadMasterDatabase ${JSON.stringify(options)}`);\n\n if (!options.outputFile.path) {\n throw new Error('outputFile.path must be provided to determine output format');\n }\n\n const extension = getExtension(options.outputFile.path);\n const { client, cleanup, version } = await setupMasterDatabase(options.masterMetadata);\n\n try {\n if (extension === '.json') {\n const result = getMasterData(client, version);\n await writeOutput(options.outputFile, JSON.stringify(result, null, 2));\n } else if (extension === '.db' || extension === '.sqlite') {\n await writeOutput(options.outputFile, client.export());\n } else {\n throw new Error(`Unsupported output extension: ${extension}`);\n }\n } finally {\n await cleanup();\n }\n\n return options.outputFile.path;\n};\n\n/**\n * Retrieves complete book data including pages and titles.\n *\n * This is a convenience function that downloads a book's data and returns it\n * as a structured JavaScript object. The function handles the temporary file\n * creation and cleanup automatically.\n *\n * @param id - The unique identifier of the book to retrieve\n * @returns A promise that resolves to the complete book data including pages and titles\n *\n * @throws {Error} When download fails, file operations fail, or JSON parsing fails\n *\n * @example\n * ```typescript\n * const bookData = await getBook(123);\n * console.log(bookData.pages.length); // Number of pages in the book\n * console.log(bookData.titles?.length); // Number of title entries\n * ```\n */\nexport const getBook = async (id: number): Promise<BookData> => {\n logger.info(`getBook ${id}`);\n\n const { client, cleanup } = await setupBookDatabase(id);\n\n try {\n const data = await getBookData(client);\n\n const result: BookData = {\n pages: data.pages.map(mapPageRowToPage),\n titles: data.titles.map(mapTitleRowToTitle),\n };\n\n return result;\n } finally {\n await cleanup();\n }\n};\n\n/**\n * Retrieves complete master data including authors, books, and categories.\n *\n * This convenience function downloads the master database archive, builds an in-memory\n * SQLite database, and returns structured data for immediate consumption alongside\n * the version number of the snapshot.\n *\n * @returns A promise that resolves to the complete master dataset and its version\n */\nexport const getMaster = async (): Promise<MasterData> => {\n logger.info('getMaster');\n\n const { client, cleanup, version } = await setupMasterDatabase();\n\n try {\n return getMasterData(client, version);\n } finally {\n await cleanup();\n }\n};\n"],"mappings":"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"}
|
|
@@ -221,6 +221,19 @@ type ShamelaConfig = {
|
|
|
221
221
|
* Valid configuration keys.
|
|
222
222
|
*/
|
|
223
223
|
type ShamelaConfigKey = keyof ShamelaConfig;
|
|
224
|
+
type TitleSpanStrategy = 'splitLines' | 'merge' | 'hierarchy';
|
|
225
|
+
type NormalizeTitleSpanOptions = {
|
|
226
|
+
/**
|
|
227
|
+
* How to handle adjacent `<span data-type="title">...</span>` runs.
|
|
228
|
+
*
|
|
229
|
+
* - `splitLines`: Keep each title span, but insert `\\n` between them so downstream conversion produces one header per line.
|
|
230
|
+
* - `merge`: Merge adjacent title spans into a single title span, joining text with `separator`.
|
|
231
|
+
* - `hierarchy`: Keep first title span as-is, convert subsequent adjacent title spans to `data-type="subtitle"` and insert `\\n` between them.
|
|
232
|
+
*/
|
|
233
|
+
strategy: TitleSpanStrategy;
|
|
234
|
+
/** Used only for `merge` strategy. Default: `' — '`. */
|
|
235
|
+
separator?: string;
|
|
236
|
+
};
|
|
224
237
|
//#endregion
|
|
225
|
-
export { DownloadBookOptions as a, GetBookMetadataResponsePayload as c,
|
|
226
|
-
//# sourceMappingURL=types-
|
|
238
|
+
export { TitleSpanStrategy as _, DownloadBookOptions as a, GetBookMetadataResponsePayload as c, NormalizeTitleSpanOptions as d, OutputOptions as f, Title as g, ShamelaConfigKey as h, Category as i, GetMasterMetadataResponsePayload as l, ShamelaConfig as m, Book as n, DownloadMasterOptions as o, Page as p, BookData as r, GetBookMetadataOptions as s, Author as t, MasterData as u };
|
|
239
|
+
//# sourceMappingURL=types-D7ziiVGq.d.ts.map
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as DownloadBookOptions, c as GetBookMetadataResponsePayload, d as
|
|
2
|
-
export { Author, Book, BookData, Category, DownloadBookOptions, DownloadMasterOptions, GetBookMetadataOptions, GetBookMetadataResponsePayload, GetMasterMetadataResponsePayload, MasterData, OutputOptions, Page, ShamelaConfig, ShamelaConfigKey, Title };
|
|
1
|
+
import { _ as TitleSpanStrategy, a as DownloadBookOptions, c as GetBookMetadataResponsePayload, d as NormalizeTitleSpanOptions, f as OutputOptions, g as Title, h as ShamelaConfigKey, i as Category, l as GetMasterMetadataResponsePayload, m as ShamelaConfig, n as Book, o as DownloadMasterOptions, p as Page, r as BookData, s as GetBookMetadataOptions, t as Author, u as MasterData } from "./types-D7ziiVGq.js";
|
|
2
|
+
export { Author, Book, BookData, Category, DownloadBookOptions, DownloadMasterOptions, GetBookMetadataOptions, GetBookMetadataResponsePayload, GetMasterMetadataResponsePayload, MasterData, NormalizeTitleSpanOptions, OutputOptions, Page, ShamelaConfig, ShamelaConfigKey, Title, TitleSpanStrategy };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/utils/constants.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The default version number for master metadata.
|
|
4
|
+
* @constant {number}
|
|
5
|
+
*/
|
|
6
|
+
declare const DEFAULT_MASTER_METADATA_VERSION = 0;
|
|
7
|
+
declare const FOOTNOTE_MARKER = "_________";
|
|
8
|
+
/**
|
|
9
|
+
* Placeholder value used to represent unknown or missing data.
|
|
10
|
+
* @constant {string}
|
|
11
|
+
*/
|
|
12
|
+
declare const UNKNOWN_VALUE_PLACEHOLDER = "99999";
|
|
13
|
+
declare const FOREWORD_MARKER = "\u8204";
|
|
14
|
+
/**
|
|
15
|
+
* Default rules to map characters from page content.
|
|
16
|
+
*/
|
|
17
|
+
declare const DEFAULT_MAPPING_RULES: Record<string, string>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { DEFAULT_MAPPING_RULES, DEFAULT_MASTER_METADATA_VERSION, FOOTNOTE_MARKER, FOREWORD_MARKER, UNKNOWN_VALUE_PLACEHOLDER };
|
|
20
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=0,t=`_________`,n=`99999`,r=`舄`,i={"<img[^>]*>>":``,舄:``,"﵀":`رَحِمَهُ ٱللَّٰهُ`,"﵁":`رضي الله عنه`,"﵂":`رَضِيَ ٱللَّٰهُ عَنْهَا`,"﵃":`رَضِيَ اللَّهُ عَنْهُمْ`,"﵄":`رَضِيَ ٱللَّٰهُ عَنْهُمَا`,"﵅":`رَضِيَ اللَّهُ عَنْهُنَّ`,"﵇":`عَلَيْهِ ٱلسَّلَٰمُ`,"﵈":`عَلَيْهِمُ السَّلامُ`,"﵊":`عليه الصلاة والسلام`,"﵌":`صلى الله عليه وآله وسلم`,"﵍":`عَلَيْهِ ٱلسَّلَٰمُ`,"﵎":`تبارك وتعالى`,"﵏":`رَحِمَهُمُ ٱللَّٰهُ`,"﷽":`بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ`,"﷿":`عَزَّ وَجَلَّ`};export{i as DEFAULT_MAPPING_RULES,e as DEFAULT_MASTER_METADATA_VERSION,t as FOOTNOTE_MARKER,r as FOREWORD_MARKER,n as UNKNOWN_VALUE_PLACEHOLDER};
|
|
2
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":["DEFAULT_MAPPING_RULES: Record<string, string>"],"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * The default version number for master metadata.\n * @constant {number}\n */\nexport const DEFAULT_MASTER_METADATA_VERSION = 0;\n\nexport const FOOTNOTE_MARKER = '_________';\n\n/**\n * Placeholder value used to represent unknown or missing data.\n * @constant {string}\n */\nexport const UNKNOWN_VALUE_PLACEHOLDER = '99999';\n\nexport const FOREWORD_MARKER = '舄';\n\n/**\n * Default rules to map characters from page content.\n */\nexport const DEFAULT_MAPPING_RULES: Record<string, string> = {\n '<img[^>]*>>': '',\n [FOREWORD_MARKER]: '',\n '﵀': 'رَحِمَهُ ٱللَّٰهُ',\n '﵁': 'رضي الله عنه',\n '﵂': 'رَضِيَ ٱللَّٰهُ عَنْهَا',\n '﵃': 'رَضِيَ اللَّهُ عَنْهُمْ',\n '﵄': 'رَضِيَ ٱللَّٰهُ عَنْهُمَا',\n '﵅': 'رَضِيَ اللَّهُ عَنْهُنَّ',\n '﵇': 'عَلَيْهِ ٱلسَّلَٰمُ',\n '﵈': 'عَلَيْهِمُ السَّلامُ',\n '﵊': 'عليه الصلاة والسلام',\n '﵌': 'صلى الله عليه وآله وسلم',\n '﵍': 'عَلَيْهِ ٱلسَّلَٰمُ',\n '﵎': 'تبارك وتعالى',\n '﵏': 'رَحِمَهُمُ ٱللَّٰهُ',\n '﷽': 'بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ',\n '﷿': 'عَزَّ وَجَلَّ',\n};\n"],"mappings":"AAIA,MAAa,EAAkC,EAElC,EAAkB,YAMlB,EAA4B,QAE5B,EAAkB,IAKlBA,EAAgD,CACzD,cAAe,GACd,EAAkB,GACnB,IAAK,oBACL,IAAK,eACL,IAAK,0BACL,IAAK,0BACL,IAAK,4BACL,IAAK,2BACL,IAAK,sBACL,IAAK,uBACL,IAAK,sBACL,IAAK,0BACL,IAAK,sBACL,IAAK,eACL,IAAK,sBACL,IAAK,wCACL,IAAK,gBACR"}
|
package/package.json
CHANGED
|
@@ -7,22 +7,25 @@
|
|
|
7
7
|
},
|
|
8
8
|
"description": "Library to interact with the Maktabah Shamela v4 APIs",
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@biomejs/biome": "^2.3.
|
|
11
|
-
"@
|
|
12
|
-
"@
|
|
10
|
+
"@biomejs/biome": "^2.3.10",
|
|
11
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
12
|
+
"@semantic-release/git": "^10.0.1",
|
|
13
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
14
|
+
"@types/bun": "^1.3.5",
|
|
15
|
+
"@types/node": "^25.0.3",
|
|
13
16
|
"@types/react": "^19.2.7",
|
|
14
17
|
"@types/react-dom": "^19.2.3",
|
|
15
18
|
"@types/sql.js": "^1.4.9",
|
|
16
|
-
"next": "^16.0
|
|
17
|
-
"react": "^19.2.
|
|
18
|
-
"react-dom": "^19.2.
|
|
19
|
+
"next": "^16.1.0",
|
|
20
|
+
"react": "^19.2.3",
|
|
21
|
+
"react-dom": "^19.2.3",
|
|
19
22
|
"semantic-release": "^25.0.2",
|
|
20
|
-
"tsdown": "^0.
|
|
23
|
+
"tsdown": "^0.18.1",
|
|
21
24
|
"typescript": "^5.9.3"
|
|
22
25
|
},
|
|
23
26
|
"engines": {
|
|
24
|
-
"bun": ">=1.3.
|
|
25
|
-
"node": ">=
|
|
27
|
+
"bun": ">=1.3.5",
|
|
28
|
+
"node": ">=24.0.0"
|
|
26
29
|
},
|
|
27
30
|
"exports": {
|
|
28
31
|
".": {
|
|
@@ -30,6 +33,11 @@
|
|
|
30
33
|
"import": "./dist/index.js",
|
|
31
34
|
"types": "./dist/index.d.ts"
|
|
32
35
|
},
|
|
36
|
+
"./constants": {
|
|
37
|
+
"default": "./dist/constants.js",
|
|
38
|
+
"import": "./dist/constants.js",
|
|
39
|
+
"types": "./dist/constants.d.ts"
|
|
40
|
+
},
|
|
33
41
|
"./content": {
|
|
34
42
|
"default": "./dist/content.js",
|
|
35
43
|
"import": "./dist/content.js",
|
|
@@ -47,7 +55,8 @@
|
|
|
47
55
|
"shamela",
|
|
48
56
|
"Arabic",
|
|
49
57
|
"Islamic",
|
|
50
|
-
"Muslim"
|
|
58
|
+
"Muslim",
|
|
59
|
+
"Islam"
|
|
51
60
|
],
|
|
52
61
|
"license": "MIT",
|
|
53
62
|
"main": "dist/index.js",
|
|
@@ -70,5 +79,5 @@
|
|
|
70
79
|
"source": "src/index.ts",
|
|
71
80
|
"type": "module",
|
|
72
81
|
"types": "dist/index.d.ts",
|
|
73
|
-
"version": "1.
|
|
82
|
+
"version": "1.4.0"
|
|
74
83
|
}
|
package/dist/content-B60R0uYQ.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
const e=0,t={"<img[^>]*>>":``,舄:``,"﵀":`رَحِمَهُ ٱللَّٰهُ`,"﵁":`رضي الله عنه`,"﵂":`رَضِيَ ٱللَّٰهُ عَنْهَا`,"﵃":`رَضِيَ اللَّهُ عَنْهُمْ`,"﵄":`رَضِيَ ٱللَّٰهُ عَنْهُمَا`,"﵅":`رَضِيَ اللَّهُ عَنْهُنَّ`,"﵇":`عَلَيْهِ ٱلسَّلَٰمُ`,"﵈":`عَلَيْهِمُ السَّلامُ`,"﵊":`عليه الصلاة والسلام`,"﵌":`صلى الله عليه وآله وسلم`,"﵍":`عَلَيْهِ ٱلسَّلَٰمُ`,"﵎":`تبارك وتعالى`,"﵏":`رَحِمَهُمُ ٱللَّٰهُ`,"﷽":``,"﷿":`عَزَّ وَجَلَّ`},n=/^[)\]\u00BB"”'’.,?!:\u061B\u060C\u061F\u06D4\u2026]+$/,r=e=>{let t=[];for(let r of e){let e=t[t.length-1];e&&n.test(r.text)?e.text+=r.text:t.push(r)}return t},i=e=>e.replace(/\r\n/g,`
|
|
2
|
-
`).replace(/\r/g,`
|
|
3
|
-
`).split(`
|
|
4
|
-
`).map(e=>e.trim()).filter(Boolean),a=e=>i(e).map(e=>({text:e})),o=(e,t)=>{let n=RegExp(`${t}\\s*=\\s*("([^"]*)"|'([^']*)'|([^s>]+))`,`i`),r=e.match(n);if(r)return r[2]??r[3]??r[4]},s=e=>{let t=[],n=/<[^>]+>/g,r=0,i;for(i=n.exec(e);i;){i.index>r&&t.push({type:`text`,value:e.slice(r,i.index)});let a=i[0],s=/^<\//.test(a),c=a.match(/^<\/?\s*([a-zA-Z0-9:-]+)/),l=c?c[1].toLowerCase():``;if(s)t.push({name:l,type:`end`});else{let e={};e.id=o(a,`id`),e[`data-type`]=o(a,`data-type`),t.push({attributes:e,name:l,type:`start`})}r=n.lastIndex,i=n.exec(e)}return r<e.length&&t.push({type:`text`,value:e.slice(r)}),t},c=(e,t)=>{let n=e.trim();return n?t?{id:t,text:n}:{text:n}:null},l=e=>{for(let t=e.length-1;t>=0;t--){let n=e[t];if(n.isTitle&&n.id)return n.id}},u=(e,t)=>{if(!e)return;let n=e.split(`
|
|
5
|
-
`);for(let e=0;e<n.length;e++){if(e>0){let e=c(t.currentText,t.currentId);e&&t.result.push(e),t.currentText=``,t.currentId=l(t.spanStack)||void 0}n[e]&&(t.currentText+=n[e])}},d=(e,t)=>{let n=e.attributes[`data-type`]===`title`,r;n&&(r=(e.attributes.id??``).replace(/^toc-/,``)),t.spanStack.push({id:r,isTitle:n}),n&&r&&!t.currentId&&(t.currentId=r)},f=e=>{if(e=e.replace(/\r\n/g,`
|
|
6
|
-
`).replace(/\r/g,`
|
|
7
|
-
`),!/<span[^>]*>/i.test(e))return r(a(e));let t=s(`<root>${e}</root>`),n={currentId:void 0,currentText:``,result:[],spanStack:[]};for(let e of t)e.type===`text`?u(e.value,n):e.type===`start`&&e.name===`span`?d(e,n):e.type===`end`&&e.name===`span`&&n.spanStack.pop();let i=c(n.currentText,n.currentId);return i&&n.result.push(i),r(n.result).filter(e=>e.text.length>0)},p=Object.entries(t).map(([e,t])=>({regex:new RegExp(e,`g`),replacement:t})),m=e=>{if(e===t)return p;let n=[];for(let t in e)n.push({regex:new RegExp(t,`g`),replacement:e[t]});return n},h=(e,n=t)=>{let r=m(n),i=e;for(let e=0;e<r.length;e++){let{regex:t,replacement:n}=r[e];i=i.replace(t,n)}return i},g=(e,t=`_________`)=>{let n=``,r=e.indexOf(t);return r>=0&&(n=e.slice(r+t.length),e=e.slice(0,r)),[e,n]},_=e=>e.replace(/(?: |\r){0,2}⦗[\u0660-\u0669]+⦘(?: |\r)?/g,` `),v=e=>(e=e.replace(/<a[^>]*>(.*?)<\/a>/gs,`$1`),e=e.replace(/<hadeeth[^>]*>|<\/hadeeth>|<hadeeth-\d+>/gs,``),e),y=e=>e.replace(/<hadeeth-\d+>/gi,`<span class="hadeeth">`).replace(/<\s*\/?\s*hadeeth\s*>/gi,`</span>`);export{h as a,v as i,f as n,g as o,_ as r,e as s,y as t};
|
|
8
|
-
//# sourceMappingURL=content-B60R0uYQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"content-B60R0uYQ.js","names":["DEFAULT_SANITIZATION_RULES: Record<string, string>","out: Line[]","tokens: Token[]","match: RegExpExecArray | null","attributes: Record<string, string | undefined>","id: string | undefined"],"sources":["../src/utils/constants.ts","../src/content.ts"],"sourcesContent":["/**\n * The default version number for master metadata.\n * @constant {number}\n */\nexport const DEFAULT_MASTER_METADATA_VERSION = 0;\n\n/**\n * Placeholder value used to represent unknown or missing data.\n * @constant {string}\n */\nexport const UNKNOWN_VALUE_PLACEHOLDER = '99999';\n\n/**\n * Default rules to sanitize page content.\n */\nexport const DEFAULT_SANITIZATION_RULES: Record<string, string> = {\n '<img[^>]*>>': '',\n 舄: '',\n '﵀': 'رَحِمَهُ ٱللَّٰهُ',\n '﵁': 'رضي الله عنه',\n '﵂': 'رَضِيَ ٱللَّٰهُ عَنْهَا',\n '﵃': 'رَضِيَ اللَّهُ عَنْهُمْ',\n '﵄': 'رَضِيَ ٱللَّٰهُ عَنْهُمَا',\n '﵅': 'رَضِيَ اللَّهُ عَنْهُنَّ',\n '﵇': 'عَلَيْهِ ٱلسَّلَٰمُ',\n '﵈': 'عَلَيْهِمُ السَّلامُ',\n '﵊': 'عليه الصلاة والسلام',\n '﵌': 'صلى الله عليه وآله وسلم',\n '﵍': 'عَلَيْهِ ٱلسَّلَٰمُ',\n '﵎': 'تبارك وتعالى',\n '﵏': 'رَحِمَهُمُ ٱللَّٰهُ',\n '﷽': '',\n '﷿': 'عَزَّ وَجَلَّ',\n};\n","import { DEFAULT_SANITIZATION_RULES } from './utils/constants';\n\nexport type Line = {\n id?: string;\n text: string;\n};\n\nconst PUNCT_ONLY = /^[)\\]\\u00BB\"”'’.,?!:\\u061B\\u060C\\u061F\\u06D4\\u2026]+$/;\n\n/**\n * Merges punctuation-only lines into the preceding title when appropriate.\n *\n * @param lines - The processed line candidates to normalise\n * @returns A new array where dangling punctuation fragments are appended to titles\n */\nconst mergeDanglingPunctuation = (lines: Line[]): Line[] => {\n const out: Line[] = [];\n for (const item of lines) {\n const last = out[out.length - 1];\n if (last && PUNCT_ONLY.test(item.text)) {\n last.text += item.text;\n } else {\n out.push(item);\n }\n }\n return out;\n};\n\n/**\n * Normalises raw text into discrete line entries.\n *\n * @param text - Raw book content potentially containing inconsistent breaks\n * @returns An array of trimmed line strings with empty entries removed\n */\nconst splitIntoLines = (text: string) => {\n const normalized = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n return normalized\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n};\n\n/**\n * Converts plain text content into {@link Line} objects without title metadata.\n *\n * @param content - The text content to split into line structures\n * @returns A {@link Line} array wrapping each detected sentence fragment\n */\nconst processTextContent = (content: string): Line[] => {\n return splitIntoLines(content).map((line) => ({ text: line }));\n};\n\n/**\n * Extracts an attribute value from the provided HTML tag string.\n *\n * @param tag - Raw HTML tag source\n * @param name - Attribute name to locate\n * @returns The attribute value when found; otherwise undefined\n */\nconst extractAttribute = (tag: string, name: string): string | undefined => {\n const pattern = new RegExp(`${name}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)'|([^s>]+))`, 'i');\n const match = tag.match(pattern);\n if (!match) {\n return undefined;\n }\n return match[2] ?? match[3] ?? match[4];\n};\n\ntype Token =\n | { type: 'text'; value: string }\n | { type: 'start'; name: string; attributes: Record<string, string | undefined> }\n | { type: 'end'; name: string };\n\n/**\n * Breaks the provided HTML fragment into structural tokens.\n *\n * @param html - HTML fragment containing book content markup\n * @returns A token stream describing text and span boundaries\n */\nconst tokenize = (html: string): Token[] => {\n const tokens: Token[] = [];\n const tagRegex = /<[^>]+>/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n match = tagRegex.exec(html);\n\n while (match) {\n if (match.index > lastIndex) {\n tokens.push({ type: 'text', value: html.slice(lastIndex, match.index) });\n }\n\n const raw = match[0];\n const isEnd = /^<\\//.test(raw);\n const nameMatch = raw.match(/^<\\/?\\s*([a-zA-Z0-9:-]+)/);\n const name = nameMatch ? nameMatch[1].toLowerCase() : '';\n\n if (isEnd) {\n tokens.push({ name, type: 'end' });\n } else {\n const attributes: Record<string, string | undefined> = {};\n attributes.id = extractAttribute(raw, 'id');\n attributes['data-type'] = extractAttribute(raw, 'data-type');\n tokens.push({ attributes, name, type: 'start' });\n }\n\n lastIndex = tagRegex.lastIndex;\n match = tagRegex.exec(html);\n }\n\n if (lastIndex < html.length) {\n tokens.push({ type: 'text', value: html.slice(lastIndex) });\n }\n\n return tokens;\n};\n\n/**\n * Pushes the accumulated text as a new line to the result array.\n */\nconst createLine = (text: string, id?: string): Line | null => {\n const trimmed = text.trim();\n if (!trimmed) {\n return null;\n }\n return id ? { id, text: trimmed } : { text: trimmed };\n};\n\n/**\n * Finds the active title ID from the span stack.\n */\nconst getActiveTitleId = (spanStack: Array<{ isTitle: boolean; id?: string }>): string | undefined => {\n for (let i = spanStack.length - 1; i >= 0; i--) {\n const entry = spanStack[i];\n if (entry.isTitle && entry.id) {\n return entry.id;\n }\n }\n};\n\n/**\n * Processes text content by handling line breaks and maintaining title context.\n */\nconst processTextWithLineBreaks = (\n raw: string,\n state: {\n currentText: string;\n currentId?: string;\n result: Line[];\n spanStack: Array<{ isTitle: boolean; id?: string }>;\n },\n) => {\n if (!raw) {\n return;\n }\n\n const parts = raw.split('\\n');\n\n for (let i = 0; i < parts.length; i++) {\n // Push previous line when crossing a line break\n if (i > 0) {\n const line = createLine(state.currentText, state.currentId);\n if (line) {\n state.result.push(line);\n }\n state.currentText = '';\n\n // Preserve title ID if still inside a title span\n const activeTitleId = getActiveTitleId(state.spanStack);\n state.currentId = activeTitleId || undefined;\n }\n\n // Append the text part\n if (parts[i]) {\n state.currentText += parts[i];\n }\n }\n};\n\n/**\n * Handles the start of a span tag, updating the stack and current ID.\n */\nconst handleSpanStart = (\n token: { attributes: Record<string, string | undefined> },\n state: {\n currentId?: string;\n spanStack: Array<{ isTitle: boolean; id?: string }>;\n },\n) => {\n const dataType = token.attributes['data-type'];\n const isTitle = dataType === 'title';\n\n let id: string | undefined;\n if (isTitle) {\n const rawId = token.attributes.id ?? '';\n id = rawId.replace(/^toc-/, '');\n }\n\n state.spanStack.push({ id, isTitle });\n\n // First title span on the current physical line wins\n if (isTitle && id && !state.currentId) {\n state.currentId = id;\n }\n};\n\n/**\n * Parses Shamela HTML content into structured lines while preserving headings.\n *\n * @param content - The raw HTML markup representing a page\n * @returns An array of {@link Line} objects containing text and optional IDs\n */\nexport const parseContentRobust = (content: string): Line[] => {\n // Normalize line endings first\n content = content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // Fast path when there are no span tags at all\n if (!/<span[^>]*>/i.test(content)) {\n return mergeDanglingPunctuation(processTextContent(content));\n }\n\n const tokens = tokenize(`<root>${content}</root>`);\n const state = {\n currentId: undefined as string | undefined,\n currentText: '',\n result: [] as Line[],\n spanStack: [] as Array<{ isTitle: boolean; id?: string }>,\n };\n\n // Process all tokens\n for (const token of tokens) {\n if (token.type === 'text') {\n processTextWithLineBreaks(token.value, state);\n } else if (token.type === 'start' && token.name === 'span') {\n handleSpanStart(token, state);\n } else if (token.type === 'end' && token.name === 'span') {\n // Closing a span does NOT end the line; trailing text stays on the same line\n state.spanStack.pop();\n }\n }\n\n // Flush any trailing text\n const finalLine = createLine(state.currentText, state.currentId);\n if (finalLine) {\n state.result.push(finalLine);\n }\n\n // Merge punctuation-only lines and drop empties\n return mergeDanglingPunctuation(state.result).filter((line) => line.text.length > 0);\n};\n\nconst DEFAULT_COMPILED_RULES = Object.entries(DEFAULT_SANITIZATION_RULES).map(([pattern, replacement]) => ({\n regex: new RegExp(pattern, 'g'),\n replacement,\n}));\n\n/**\n * Compiles sanitisation rules into RegExp objects for reuse.\n *\n * @param rules - Key/value replacements used during sanitisation\n * @returns A list of compiled regular expression rules\n */\nconst getCompiledRules = (rules: Record<string, string>) => {\n if (rules === DEFAULT_SANITIZATION_RULES) {\n return DEFAULT_COMPILED_RULES;\n }\n\n const compiled = [];\n for (const pattern in rules) {\n compiled.push({\n regex: new RegExp(pattern, 'g'),\n replacement: rules[pattern],\n });\n }\n return compiled;\n};\n\n/**\n * Sanitises page content by applying regex replacement rules.\n *\n * @param text - The text to clean\n * @param rules - Optional custom replacements, defaults to {@link DEFAULT_SANITIZATION_RULES}\n * @returns The sanitised content\n */\nexport const sanitizePageContent = (\n text: string,\n rules: Record<string, string> = DEFAULT_SANITIZATION_RULES,\n): string => {\n const compiledRules = getCompiledRules(rules);\n\n let content = text;\n for (let i = 0; i < compiledRules.length; i++) {\n const { regex, replacement } = compiledRules[i];\n content = content.replace(regex, replacement);\n }\n return content;\n};\n\n/**\n * Splits a page body from its trailing footnotes using a marker string.\n *\n * @param content - Combined body and footnote text\n * @param footnoteMarker - Marker indicating the start of footnotes\n * @returns A tuple containing the page body followed by the footnote section\n */\nexport const splitPageBodyFromFooter = (content: string, footnoteMarker = '_________') => {\n let footnote = '';\n const indexOfFootnote = content.indexOf(footnoteMarker);\n\n if (indexOfFootnote >= 0) {\n footnote = content.slice(indexOfFootnote + footnoteMarker.length);\n content = content.slice(0, indexOfFootnote);\n }\n\n return [content, footnote] as const;\n};\n\n/**\n * Removes Arabic numeral page markers enclosed in turtle ⦗ ⦘ brackets.\n * Replaces the marker along with up to two preceding whitespace characters\n * (space or carriage return) and up to one following whitespace character\n * with a single space.\n *\n * @param text - Text potentially containing page markers\n * @returns The text with numeric markers replaced by a single space\n */\nexport const removeArabicNumericPageMarkers = (text: string) => {\n return text.replace(/(?: |\\r){0,2}⦗[\\u0660-\\u0669]+⦘(?: |\\r)?/g, ' ');\n};\n\n/**\n * Removes anchor and hadeeth tags from the content while preserving spans.\n *\n * @param content - HTML string containing various tags\n * @returns The content with only span tags retained\n */\nexport const removeTagsExceptSpan = (content: string) => {\n // Remove <a> tags and their content, keeping only the text inside\n content = content.replace(/<a[^>]*>(.*?)<\\/a>/gs, '$1');\n\n // Remove <hadeeth> tags (both self-closing, with content, and numbered)\n content = content.replace(/<hadeeth[^>]*>|<\\/hadeeth>|<hadeeth-\\d+>/gs, '');\n\n return content;\n};\n\n/**\n * Normalizes Shamela HTML for CSS styling:\n * - Converts <hadeeth-N> to <span class=\"hadeeth\">\n * - Converts </hadeeth> or standalone <hadeeth> to </span>\n */\nexport const normalizeHtml = (html: string): string => {\n return html.replace(/<hadeeth-\\d+>/gi, '<span class=\"hadeeth\">').replace(/<\\s*\\/?\\s*hadeeth\\s*>/gi, '</span>');\n};\n"],"mappings":"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"}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
//#region src/content.d.ts
|
|
2
|
-
type Line = {
|
|
3
|
-
id?: string;
|
|
4
|
-
text: string;
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Parses Shamela HTML content into structured lines while preserving headings.
|
|
8
|
-
*
|
|
9
|
-
* @param content - The raw HTML markup representing a page
|
|
10
|
-
* @returns An array of {@link Line} objects containing text and optional IDs
|
|
11
|
-
*/
|
|
12
|
-
declare const parseContentRobust: (content: string) => Line[];
|
|
13
|
-
/**
|
|
14
|
-
* Sanitises page content by applying regex replacement rules.
|
|
15
|
-
*
|
|
16
|
-
* @param text - The text to clean
|
|
17
|
-
* @param rules - Optional custom replacements, defaults to {@link DEFAULT_SANITIZATION_RULES}
|
|
18
|
-
* @returns The sanitised content
|
|
19
|
-
*/
|
|
20
|
-
declare const sanitizePageContent: (text: string, rules?: Record<string, string>) => string;
|
|
21
|
-
/**
|
|
22
|
-
* Splits a page body from its trailing footnotes using a marker string.
|
|
23
|
-
*
|
|
24
|
-
* @param content - Combined body and footnote text
|
|
25
|
-
* @param footnoteMarker - Marker indicating the start of footnotes
|
|
26
|
-
* @returns A tuple containing the page body followed by the footnote section
|
|
27
|
-
*/
|
|
28
|
-
declare const splitPageBodyFromFooter: (content: string, footnoteMarker?: string) => readonly [string, string];
|
|
29
|
-
/**
|
|
30
|
-
* Removes Arabic numeral page markers enclosed in turtle ⦗ ⦘ brackets.
|
|
31
|
-
* Replaces the marker along with up to two preceding whitespace characters
|
|
32
|
-
* (space or carriage return) and up to one following whitespace character
|
|
33
|
-
* with a single space.
|
|
34
|
-
*
|
|
35
|
-
* @param text - Text potentially containing page markers
|
|
36
|
-
* @returns The text with numeric markers replaced by a single space
|
|
37
|
-
*/
|
|
38
|
-
declare const removeArabicNumericPageMarkers: (text: string) => string;
|
|
39
|
-
/**
|
|
40
|
-
* Removes anchor and hadeeth tags from the content while preserving spans.
|
|
41
|
-
*
|
|
42
|
-
* @param content - HTML string containing various tags
|
|
43
|
-
* @returns The content with only span tags retained
|
|
44
|
-
*/
|
|
45
|
-
declare const removeTagsExceptSpan: (content: string) => string;
|
|
46
|
-
/**
|
|
47
|
-
* Normalizes Shamela HTML for CSS styling:
|
|
48
|
-
* - Converts <hadeeth-N> to <span class="hadeeth">
|
|
49
|
-
* - Converts </hadeeth> or standalone <hadeeth> to </span>
|
|
50
|
-
*/
|
|
51
|
-
declare const normalizeHtml: (html: string) => string;
|
|
52
|
-
//#endregion
|
|
53
|
-
export { removeTagsExceptSpan as a, removeArabicNumericPageMarkers as i, normalizeHtml as n, sanitizePageContent as o, parseContentRobust as r, splitPageBodyFromFooter as s, Line as t };
|
|
54
|
-
//# sourceMappingURL=content-CwjMtCQl.d.ts.map
|