shamela 1.3.0 → 1.3.2
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 +280 -288
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import e from"sql.js";import{unzipSync as t}from"fflate";
|
|
1
|
+
import e from"sql.js";import{unzipSync as t}from"fflate";var n=(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 r=Object.freeze({debug:()=>{},error:()=>{},info:()=>{},warn:()=>{}});let i=r;const a=e=>{if(!e){i=r;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)}`);i=e},o=()=>i,s=()=>{i=r};var c=new Proxy({},{get:(e,t)=>{let n=o(),r=n[t];return typeof r==`function`?(...e)=>r.apply(n,e):r}});let l={};const u={apiKey:`SHAMELA_API_KEY`,booksEndpoint:`SHAMELA_API_BOOKS_ENDPOINT`,masterPatchEndpoint:`SHAMELA_API_MASTER_PATCH_ENDPOINT`,sqlJsWasmUrl:`SHAMELA_SQLJS_WASM_URL`},ee=typeof process<`u`&&!!process?.env,d=e=>{let t=l[e];if(t!==void 0)return t;let n=u[e];if(ee)return process.env[n]},te=e=>{let{logger:t,...n}=e;`logger`in e&&a(t),l={...l,...n}},f=e=>e===`fetchImplementation`?l.fetchImplementation:d(e),p=()=>({apiKey:d(`apiKey`),booksEndpoint:d(`booksEndpoint`),fetchImplementation:l.fetchImplementation,masterPatchEndpoint:d(`masterPatchEndpoint`),sqlJsWasmUrl:d(`sqlJsWasmUrl`)}),m=e=>{if(e===`fetchImplementation`)throw Error(`fetchImplementation must be provided via configure().`);let t=f(e);if(!t)throw Error(`${u[e]} environment variable not set`);return t},ne=()=>{l={},s()};let h=function(e){return e.Authors=`author`,e.Books=`book`,e.Categories=`category`,e.Page=`page`,e.Title=`title`,e}({});const g=(e,t)=>e.query(`PRAGMA table_info(${t})`).all(),_=(e,t)=>!!e.query(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?1`).get(t),v=(e,t)=>_(e,t)?e.query(`SELECT * FROM ${t}`).all():[],y=e=>String(e.is_deleted)===`1`,b=(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},re=(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&&y(e)||a.push(b(t,e,n))}for(let e of t){let t=String(e.id);r.has(t)||y(e)||a.push(b(void 0,e,n))}return a},ie=(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()},ae=(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):(c.warn(`${n} table definition missing in source database`),!1)},x=(e,t,n,r)=>{if(!_(t,r)){c.warn(`${r} table missing in source database`);return}if(!ae(e,t,r))return;let i=g(t,r),a=n&&_(n,r)?g(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)}ie(e,r,o,re(v(t,r),n?v(n,r):[],o))},oe=(e,t,n)=>{e.transaction(()=>{x(e,t,n,h.Page),x(e,t,n,h.Title)})()},se=(e,t)=>{e.transaction(()=>{x(e,t,null,h.Page),x(e,t,null,h.Title)})()},ce=e=>{e.run(`CREATE TABLE ${h.Page} (
|
|
2
2
|
id INTEGER,
|
|
3
3
|
content TEXT,
|
|
4
4
|
part TEXT,
|
|
@@ -12,7 +12,8 @@ import e from"sql.js";import{unzipSync as t}from"fflate";const n=Object.freeze({
|
|
|
12
12
|
page INTEGER,
|
|
13
13
|
parent INTEGER,
|
|
14
14
|
is_deleted TEXT
|
|
15
|
-
)`)},
|
|
15
|
+
)`)},S=e=>e.query(`SELECT * FROM ${h.Page}`).all(),C=e=>e.query(`SELECT * FROM ${h.Title}`).all(),w=e=>({pages:S(e),titles:C(e)}),T=e=>{try{return n(`node:fs`).existsSync(e)}catch{return!1}},le=()=>{if(n!==void 0&&n.resolve!==void 0)try{let e=n.resolve(`sql.js`),t=n(`node:path`),r=t.dirname(e),i=t.join(r,`dist`,`sql-wasm.wasm`);if(T(i))return i}catch{}if(typeof process<`u`&&process.cwd)try{let e=n(`node:path`),t=process.cwd(),r=[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 r)if(T(e))return e}catch{}if(n!==void 0&&n.resolve!==void 0&&n.resolve.paths)try{let e=n(`node:path`),t=n.resolve.paths(`sql.js`)||[];for(let n of t){let t=e.join(n,`sql.js`,`dist`,`sql-wasm.wasm`);if(T(t))return t}}catch{}try{if(import.meta.url){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(T(n))return n}}catch{}return null};var ue=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()}},E=class{constructor(e){this.db=e}run=(e,t=[])=>{this.db.run(e,t)};prepare=e=>new ue(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)=>this.all(e,t)[0]};let D=null,O=null;const de=typeof process<`u`&&!!process?.versions?.node,fe=()=>{if(!O){let e=f(`sqlJsWasmUrl`);if(e)O=e;else if(de){let e=le();if(e)O=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 O=`https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/sql-wasm.wasm`}return O},k=()=>(D||=e({locateFile:()=>fe()}),D),A=async()=>new E(new(await(k())).Database),j=async e=>new E(new(await(k())).Database(e)),pe=(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)},me=async(e,t)=>{let n={author:h.Authors,book:h.Books,category:h.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 j(e.data))}try{let t=Object.entries(r);e.transaction(()=>{for(let[n,r]of t){pe(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())}},M=(e,t,n)=>{e.run(`DROP VIEW IF EXISTS ${t}`),e.run(`CREATE VIEW ${t} AS SELECT * FROM ${n}`)},he=e=>{e.run(`CREATE TABLE ${h.Authors} (
|
|
16
17
|
id INTEGER,
|
|
17
18
|
is_deleted TEXT,
|
|
18
19
|
name TEXT,
|
|
@@ -39,9 +40,9 @@ import e from"sql.js";import{unzipSync as t}from"fflate";const n=Object.freeze({
|
|
|
39
40
|
is_deleted TEXT,
|
|
40
41
|
"order" TEXT,
|
|
41
42
|
name TEXT
|
|
42
|
-
)`),M(e,`authors`,h.Authors),M(e,`books`,h.Books),M(e,`categories`,h.Categories)},
|
|
43
|
+
)`),M(e,`authors`,h.Authors),M(e,`books`,h.Books),M(e,`categories`,h.Categories)},ge=e=>e.query(`SELECT * FROM ${h.Authors}`).all(),_e=e=>e.query(`SELECT * FROM ${h.Books}`).all(),N=e=>e.query(`SELECT * FROM ${h.Categories}`).all(),P=(e,t)=>({authors:ge(e),books:_e(e),categories:N(e),version:t}),F=(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()},I=e=>({content:e.content,id:e.id,...e.number&&{number:e.number},...e.page&&{page:Number(e.page)},...e.part&&{part:e.part}}),ve=e=>{let t=Number(e.parent);return{content:e.content,id:e.id,page:Number(e.page),...t&&{parent:t}}},L={"<img[^>]*>>":``,舄:``,"﵀":`رَحِمَهُ ٱللَّٰهُ`,"﵁":`رضي الله عنه`,"﵂":`رَضِيَ ٱللَّٰهُ عَنْهَا`,"﵃":`رَضِيَ اللَّهُ عَنْهُمْ`,"﵄":`رَضِيَ ٱللَّٰهُ عَنْهُمَا`,"﵅":`رَضِيَ اللَّهُ عَنْهُنَّ`,"﵌":`صلى الله عليه وآله وسلم`,"﵏":`رَحِمَهُمُ ٱللَّٰهُ`},R=e=>{let t=new URL(e);return t.protocol=`https`,t.toString()},z=e=>/\.(sqlite|db)$/i.test(e.name),B=e=>e.find(z),V=e=>{let t=/\.([^.]+)$/.exec(e);return t?`.${t[1].toLowerCase()}`:``},H=(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`,m(`apiKey`)),r.search=i.toString(),r},U=async(e,t={})=>{let n=typeof e==`string`?e:e.toString(),r=await(t.fetchImpl??p().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)},ye=typeof process<`u`&&!!process?.versions?.node,be=async()=>{if(!ye)throw Error(`File system operations are only supported in Node.js environments`);return import(`node:fs/promises`)},xe=async e=>{let[t,n]=await Promise.all([be(),import(`node:path`)]),r=n.dirname(e);return await t.mkdir(r,{recursive:!0}),t},W=async e=>{let n=await U(e),r=n instanceof Uint8Array?n.length:n&&typeof n.byteLength==`number`?n.byteLength:0;return c.debug(`unzipFromUrl:bytes`,r),new Promise((e,r)=>{let i=n instanceof Uint8Array?n:new Uint8Array(n);try{let n=t(i),r=Object.entries(n).map(([e,t])=>({data:t,name:e}));c.debug(`unzipFromUrl:entries`,r.map(e=>e.name)),e(r)}catch(e){r(Error(`Error processing URL: ${e.message}`))}})},G=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 xe(e.path);typeof t==`string`?await n.writeFile(e.path,t,`utf-8`):await n.writeFile(e.path,t)},Se=[`author.sqlite`,`book.sqlite`,`category.sqlite`],K=()=>{let{apiKey:e,booksEndpoint:t,masterPatchEndpoint:n}=p(),r=[[`apiKey`,e],[`booksEndpoint`,t],[`masterPatchEndpoint`,n]].filter(([,e])=>!e).map(([e])=>e);if(r.length)throw Error(`${r.join(`, `)} environment variables not set`)},Ce=e=>{let t=new Set(e.map(e=>e.match(/[^\\/]+$/)?.[0]??e).map(e=>e.toLowerCase()));return Se.every(e=>t.has(e.toLowerCase()))},q=async(e,t)=>{c.info(`Setting up book database for ${e}`);let n=t||await Y(e),r=n.minorReleaseUrl?W(n.minorReleaseUrl):Promise.resolve([]),[i,a]=await Promise.all([W(n.majorReleaseUrl),r]),o=B(i);if(!o)throw Error(`Unable to locate book database in archive`);let s=await A();try{c.info(`Creating tables`),ce(s);let e=await j(o.data);try{let t=B(a);if(t){c.info(`Applying patches from ${t.name} to ${o.name}`);let n=await j(t.data);try{oe(s,e,n)}finally{n.close()}}else c.info(`Copying table data from ${o.name}`),se(s,e)}finally{e.close()}return{cleanup:async()=>{s.close()},client:s}}catch(e){throw s.close(),e}},J=async e=>{c.info(`Setting up master database`);let t=e||await X(0);c.info(`Downloading master database ${t.version} from: ${F(t.url)}`);let n=await W(R(t.url));if(c.debug?.(`sourceTables downloaded: ${n.map(e=>e.name).toString()}`),!Ce(n.map(e=>e.name)))throw c.error(`Some source tables were not found: ${n.map(e=>e.name).toString()}`),Error(`Expected tables not found!`);let r=await A();try{return c.info(`Creating master tables`),he(r),c.info(`Copying data to master table`),await me(r,n.filter(z)),{cleanup:async()=>{r.close()},client:r,version:t.version}}catch(e){throw r.close(),e}},Y=async(e,t)=>{K();let n=H(`${m(`booksEndpoint`)}/${e}`,{major_release:(t?.majorVersion||0).toString(),minor_release:(t?.minorVersion||0).toString()});c.info(`Fetching shamela.ws book link: ${F(n)}`);try{let e=await U(n);return{majorRelease:e.major_release,majorReleaseUrl:R(e.major_release_url),...e.minor_release_url&&{minorReleaseUrl:R(e.minor_release_url)},...e.minor_release_url&&{minorRelease:e.minor_release}}}catch(e){throw Error(`Error fetching book metadata: ${e.message}`)}},we=async(e,t)=>{if(c.info(`downloadBook ${e} ${JSON.stringify(t)}`),!t.outputFile.path)throw Error(`outputFile.path must be provided to determine output format`);let n=V(t.outputFile.path).toLowerCase(),{client:r,cleanup:i}=await q(e,t?.bookMetadata);try{if(n===`.json`){let e=await w(r);await G(t.outputFile,JSON.stringify(e,null,2))}else if(n===`.db`||n===`.sqlite`){let e=r.export();await G(t.outputFile,e)}else throw Error(`Unsupported output extension: ${n}`)}finally{await i()}return t.outputFile.path},X=async(e=0)=>{K();let t=H(m(`masterPatchEndpoint`),{version:e.toString()});c.info(`Fetching shamela.ws master database patch link: ${F(t)}`);try{let e=await U(t);return{url:e.patch_url,version:e.version}}catch(e){throw Error(`Error fetching master patch: ${e.message}`)}},Te=e=>{let t=m(`masterPatchEndpoint`),{origin:n}=new URL(t);return`${n}/covers/${e}.jpg`},Ee=async e=>{if(c.info(`downloadMasterDatabase ${JSON.stringify(e)}`),!e.outputFile.path)throw Error(`outputFile.path must be provided to determine output format`);let t=V(e.outputFile.path),{client:n,cleanup:r,version:i}=await J(e.masterMetadata);try{if(t===`.json`){let t=P(n,i);await G(e.outputFile,JSON.stringify(t,null,2))}else if(t===`.db`||t===`.sqlite`)await G(e.outputFile,n.export());else throw Error(`Unsupported output extension: ${t}`)}finally{await r()}return e.outputFile.path},De=async e=>{c.info(`getBook ${e}`);let{client:t,cleanup:n}=await q(e);try{let e=await w(t);return{pages:e.pages.map(I),titles:e.titles.map(ve)}}finally{await n()}},Oe=async()=>{c.info(`getMaster`);let{client:e,cleanup:t,version:n}=await J();try{return P(e,n)}finally{await t()}},ke=/^[)\]\u00BB"”'’.,?!:\u061B\u060C\u061F\u06D4\u2026]+$/,Z=/[[({«“‘]$/,Ae=e=>{let t=[];for(let n of e){let e=t[t.length-1];e?.id&&ke.test(n.text)?e.text+=n.text:t.push(n)}return t},je=e=>{let t=e.replace(/\r\n/g,`
|
|
43
44
|
`).replace(/\r/g,`
|
|
44
45
|
`);return/\n/.test(t)||(t=t.replace(/([.?!\u061F\u061B\u06D4\u2026]["“”'’»«)\]]?)\s+(?=[\u0600-\u06FF])/,`$1
|
|
45
46
|
`)),t.split(`
|
|
46
|
-
`).map(e=>e.replace(/^\*+/,``).trim()).filter(Boolean)},
|
|
47
|
+
`).map(e=>e.replace(/^\*+/,``).trim()).filter(Boolean)},Q=e=>je(e).map(e=>({text:e})),$=(e,t)=>{let n=RegExp(`${t}\\s*=\\s*("([^"]*)"|'([^']*)'|([^s>]+))`,`i`),r=e.match(n);if(r)return r[2]??r[3]??r[4]},Me=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],o=/^<\//.test(a),s=a.match(/^<\/?\s*([a-zA-Z0-9:-]+)/),c=s?s[1].toLowerCase():``;if(o)t.push({name:c,type:`end`});else{let e={};e.id=$(a,`id`),e[`data-type`]=$(a,`data-type`),t.push({attributes:e,name:c,type:`start`})}r=n.lastIndex,i=n.exec(e)}return r<e.length&&t.push({type:`text`,value:e.slice(r)}),t},Ne=(e,t)=>{let n=e[e.length-1];return!t||!n||!n.id||!Z.test(n.text)||/\n/.test(t)?!1:(n.text+=t.replace(/^\s+/,``),!0)},Pe=e=>{if(!/<span[^>]*>/i.test(e))return Q(e);let t=Me(`<root>${e}</root>`),n=[],r=0,i=null,a=e=>{if(!e)return;if(r>0&&i){let t=r===1?e.replace(/^\s+/,``):e;i.text+=t;return}if(Ne(n,e))return;let t=e.trim();t&&n.push(...Q(t))};for(let e of t)e.type===`text`?a(e.value):e.type===`start`&&e.name===`span`?e.attributes[`data-type`]===`title`&&(r===0&&(i={id:e.attributes.id?.replace(/^toc-/,``)??``,text:``},n.push(i)),r+=1):e.type===`end`&&e.name===`span`&&r>0&&(--r,r===0&&(i=null));return Ae(n.map(e=>e.id?e:{...e,text:e.text.trim()}).map(e=>e.id?e:{...e,text:e.text})).filter(e=>e.text.length>0)},Fe=Object.entries(L).map(([e,t])=>({regex:new RegExp(e,`g`),replacement:t})),Ie=e=>{if(e===L)return Fe;let t=[];for(let n in e)t.push({regex:new RegExp(n,`g`),replacement:e[n]});return t},Le=(e,t=L)=>{let n=Ie(t),r=e;for(let e=0;e<n.length;e++){let{regex:t,replacement:i}=n[e];r=r.replace(t,i)}return r},Re=(e,t=`_________`)=>{let n=``,r=e.lastIndexOf(t);return r>=0&&(n=e.slice(r+t.length),e=e.slice(0,r)),[e,n]},ze=e=>e.replace(/\s?⦗[\u0660-\u0669]+⦘\s?/,` `),Be=e=>(e=e.replace(/<a[^>]*>(.*?)<\/a>/g,`$1`),e=e.replace(/<hadeeth[^>]*>|<\/hadeeth>|<hadeeth-\d+>/g,``),e);export{te as configure,we as downloadBook,Ee as downloadMasterDatabase,De as getBook,Y as getBookMetadata,Te as getCoverUrl,Oe as getMaster,X as getMasterMetadata,Pe as parseContentRobust,ze as removeArabicNumericPageMarkers,Be as removeTagsExceptSpan,ne as resetConfig,Le as sanitizePageContent,Re as splitPageBodyFromFooter};
|
|
47
48
|
//# 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","DEFAULT_SANITIZATION_RULES: Record<string, string>","error: any","bookResponse: GetBookMetadataResponsePayload","error: any","getBookData","response: Record<string, any>","getMasterData","out: Line[]","tokens: Token[]","match: RegExpExecArray | null","attributes: Record<string, string | undefined>","result: Line[]","currentTitle: Line | null"],"sources":["../src/utils/logger.ts","../src/config.ts","../src/db/types.ts","../src/db/book.ts","../src/db/sqlite.ts","../src/db/master.ts","../src/utils/common.ts","../src/utils/constants.ts","../src/utils/downloads.ts","../src/utils/network.ts","../src/utils/io.ts","../src/utils/validation.ts","../src/api.ts","../src/content.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","import initSqlJs, { type Database as SqlJsDatabase, type SqlJsStatic, type Statement } from 'sql.js';\n\nimport { getConfigValue } from '../config';\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 const configured = getConfigValue('sqlJsWasmUrl');\n if (configured) {\n resolvedWasmPath = configured;\n } else if (isNodeEnvironment) {\n const url = new URL('../../node_modules/sql.js/dist/sql-wasm.wasm', import.meta.url);\n resolvedWasmPath = decodeURIComponent(url.pathname);\n } else {\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","/**\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","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","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]+$/;\nconst OPENER_AT_END = /[[({«“‘]$/;\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?.id && PUNCT_ONLY.test(item.text)) {\n last.text += item.text;\n } else {\n out.push(item);\n }\n }\n return out;\n};\n\nconst splitIntoLines = (text: string) => {\n let normalized = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n if (!/\\n/.test(normalized)) {\n normalized = normalized.replace(/([.?!\\u061F\\u061B\\u06D4\\u2026][\"“”'’»«)\\]]?)\\s+(?=[\\u0600-\\u06FF])/, '$1\\n');\n }\n\n return normalized\n .split('\\n')\n .map((line) => line.replace(/^\\*+/, '').trim())\n .filter(Boolean);\n};\n\nconst processTextContent = (content: string): Line[] => {\n return splitIntoLines(content).map((line) => ({ text: line }));\n};\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\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\nconst maybeAppendToPrevTitle = (result: Line[], raw: string) => {\n const last = result[result.length - 1];\n if (!raw) {\n return false;\n }\n if (!last || !last.id) {\n return false;\n }\n if (!OPENER_AT_END.test(last.text)) {\n return false;\n }\n if (/\\n/.test(raw)) {\n return false;\n }\n last.text += raw.replace(/^\\s+/, '');\n return true;\n};\n\nexport const parseContentRobust = (content: string): Line[] => {\n if (!/<span[^>]*>/i.test(content)) {\n return processTextContent(content);\n }\n\n const tokens = tokenize(`<root>${content}</root>`);\n const result: Line[] = [];\n\n let titleDepth = 0;\n let currentTitle: Line | null = null;\n\n const pushText = (raw: string) => {\n if (!raw) {\n return;\n }\n\n if (titleDepth > 0 && currentTitle) {\n const cleaned = titleDepth === 1 ? raw.replace(/^\\s+/, '') : raw;\n currentTitle.text += cleaned;\n return;\n }\n\n if (maybeAppendToPrevTitle(result, raw)) {\n return;\n }\n\n const text = raw.trim();\n if (text) {\n result.push(...processTextContent(text));\n }\n };\n\n for (const token of tokens) {\n if (token.type === 'text') {\n pushText(token.value);\n } else if (token.type === 'start' && token.name === 'span') {\n const dataType = token.attributes['data-type'];\n if (dataType === 'title') {\n if (titleDepth === 0) {\n const id = token.attributes.id?.replace(/^toc-/, '') ?? '';\n currentTitle = { id, text: '' };\n result.push(currentTitle);\n }\n titleDepth += 1;\n }\n } else if (token.type === 'end' && token.name === 'span') {\n if (titleDepth > 0) {\n titleDepth -= 1;\n if (titleDepth === 0) {\n currentTitle = null;\n }\n }\n }\n }\n\n const cleaned = result.map((line) => (line.id ? line : { ...line, text: line.text.trim() }));\n\n return mergeDanglingPunctuation(cleaned.map((line) => (line.id ? line : { ...line, text: line.text }))).filter(\n (line) => line.text.length > 0,\n );\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 sanitization rules into RegExp objects for performance\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 * Sanitizes page content by applying regex replacement rules\n * @param text - The text to sanitize\n * @param rules - Optional custom rules (defaults to DEFAULT_SANITIZATION_RULES)\n * @returns The sanitized text\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\nexport const splitPageBodyFromFooter = (content: string, footnoteMarker = '_________') => {\n let footnote = '';\n const indexOfFootnote = content.lastIndexOf(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\nexport const removeArabicNumericPageMarkers = (text: string) => {\n return text.replace(/\\s?⦗[\\u0660-\\u0669]+⦘\\s?/, ' ');\n};\n\nexport const removeTagsExceptSpan = (content: string) => {\n // Remove <a> tags and their content, keeping only the text inside\n content = content.replace(/<a[^>]*>(.*?)<\\/a>/g, '$1');\n\n // Remove <hadeeth> tags (both self-closing, with content, and numbered)\n content = content.replace(/<hadeeth[^>]*>|<\\/hadeeth>|<hadeeth-\\d+>/g, '');\n\n return content;\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","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","DEFAULT_SANITIZATION_RULES: Record<string, string>","error: any","bookResponse: GetBookMetadataResponsePayload","error: any","getBookData","response: Record<string, any>","getMasterData","out: Line[]","tokens: Token[]","match: RegExpExecArray | null","attributes: Record<string, string | undefined>","result: Line[]","currentTitle: Line | null"],"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/constants.ts","../src/utils/downloads.ts","../src/utils/network.ts","../src/utils/io.ts","../src/utils/validation.ts","../src/api.ts","../src/content.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 */\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","/**\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","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","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]+$/;\nconst OPENER_AT_END = /[[({«“‘]$/;\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?.id && PUNCT_ONLY.test(item.text)) {\n last.text += item.text;\n } else {\n out.push(item);\n }\n }\n return out;\n};\n\nconst splitIntoLines = (text: string) => {\n let normalized = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n if (!/\\n/.test(normalized)) {\n normalized = normalized.replace(/([.?!\\u061F\\u061B\\u06D4\\u2026][\"“”'’»«)\\]]?)\\s+(?=[\\u0600-\\u06FF])/, '$1\\n');\n }\n\n return normalized\n .split('\\n')\n .map((line) => line.replace(/^\\*+/, '').trim())\n .filter(Boolean);\n};\n\nconst processTextContent = (content: string): Line[] => {\n return splitIntoLines(content).map((line) => ({ text: line }));\n};\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\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\nconst maybeAppendToPrevTitle = (result: Line[], raw: string) => {\n const last = result[result.length - 1];\n if (!raw) {\n return false;\n }\n if (!last || !last.id) {\n return false;\n }\n if (!OPENER_AT_END.test(last.text)) {\n return false;\n }\n if (/\\n/.test(raw)) {\n return false;\n }\n last.text += raw.replace(/^\\s+/, '');\n return true;\n};\n\nexport const parseContentRobust = (content: string): Line[] => {\n if (!/<span[^>]*>/i.test(content)) {\n return processTextContent(content);\n }\n\n const tokens = tokenize(`<root>${content}</root>`);\n const result: Line[] = [];\n\n let titleDepth = 0;\n let currentTitle: Line | null = null;\n\n const pushText = (raw: string) => {\n if (!raw) {\n return;\n }\n\n if (titleDepth > 0 && currentTitle) {\n const cleaned = titleDepth === 1 ? raw.replace(/^\\s+/, '') : raw;\n currentTitle.text += cleaned;\n return;\n }\n\n if (maybeAppendToPrevTitle(result, raw)) {\n return;\n }\n\n const text = raw.trim();\n if (text) {\n result.push(...processTextContent(text));\n }\n };\n\n for (const token of tokens) {\n if (token.type === 'text') {\n pushText(token.value);\n } else if (token.type === 'start' && token.name === 'span') {\n const dataType = token.attributes['data-type'];\n if (dataType === 'title') {\n if (titleDepth === 0) {\n const id = token.attributes.id?.replace(/^toc-/, '') ?? '';\n currentTitle = { id, text: '' };\n result.push(currentTitle);\n }\n titleDepth += 1;\n }\n } else if (token.type === 'end' && token.name === 'span') {\n if (titleDepth > 0) {\n titleDepth -= 1;\n if (titleDepth === 0) {\n currentTitle = null;\n }\n }\n }\n }\n\n const cleaned = result.map((line) => (line.id ? line : { ...line, text: line.text.trim() }));\n\n return mergeDanglingPunctuation(cleaned.map((line) => (line.id ? line : { ...line, text: line.text }))).filter(\n (line) => line.text.length > 0,\n );\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 sanitization rules into RegExp objects for performance\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 * Sanitizes page content by applying regex replacement rules\n * @param text - The text to sanitize\n * @param rules - Optional custom rules (defaults to DEFAULT_SANITIZATION_RULES)\n * @returns The sanitized text\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\nexport const splitPageBodyFromFooter = (content: string, footnoteMarker = '_________') => {\n let footnote = '';\n const indexOfFootnote = content.lastIndexOf(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\nexport const removeArabicNumericPageMarkers = (text: string) => {\n return text.replace(/\\s?⦗[\\u0660-\\u0669]+⦘\\s?/, ' ');\n};\n\nexport const removeTagsExceptSpan = (content: string) => {\n // Remove <a> tags and their content, keeping only the text inside\n content = content.replace(/<a[^>]*>(.*?)<\\/a>/g, '$1');\n\n // Remove <hadeeth> tags (both self-closing, with content, and numbered)\n content = content.replace(/<hadeeth[^>]*>|<\\/hadeeth>|<hadeeth-\\d+>/g, '');\n\n return content;\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@biomejs/biome": "^2.2.7",
|
|
11
11
|
"@types/node": "^24.9.1",
|
|
12
|
-
"@types/react": "^
|
|
13
|
-
"@types/react-dom": "^
|
|
12
|
+
"@types/react": "^19.2.2",
|
|
13
|
+
"@types/react-dom": "^19.2.2",
|
|
14
14
|
"@types/sql.js": "^1.4.9",
|
|
15
|
-
"next": "^16.0.
|
|
16
|
-
"react": "^
|
|
17
|
-
"react-dom": "^
|
|
15
|
+
"next": "^16.0.0",
|
|
16
|
+
"react": "^19.2.0",
|
|
17
|
+
"react-dom": "^19.2.0",
|
|
18
18
|
"semantic-release": "^25.0.1",
|
|
19
19
|
"tsdown": "^0.15.9",
|
|
20
20
|
"typescript": "^5.9.3"
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"source": "src/index.ts",
|
|
61
61
|
"type": "module",
|
|
62
62
|
"types": "dist/index.d.ts",
|
|
63
|
-
"version": "1.3.
|
|
63
|
+
"version": "1.3.2"
|
|
64
64
|
}
|