@tarekeldeeb/quran-madina-react 2.1.0 → 2.1.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/LICENSE.md ADDED
@@ -0,0 +1 @@
1
+ Code and assets are licensed under ["Waqf" General Public License 2.0](https://github.com/ojuba-org/waqf/tree/master/2.0), 3 Muharram 1435
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [![Socket Badge](https://badge.socket.dev/npm/package/@tarekeldeeb/quran-madina-react)](https://socket.dev/npm/package/@tarekeldeeb/quran-madina-react)
4
4
 
5
+ **[Documentation](https://tarekeldeeb.github.io/quran-madina-html/docs/)** · [Live demo](https://tarekeldeeb.github.io/quran-madina-html/demo/index.html)
6
+
5
7
  A thin React component around the `<quran-madina-html>` custom element. The actual rendering is
6
8
  still done by the [`quran-madina-html`](https://www.npmjs.com/package/quran-madina-html) library
7
9
  (HTML/CSS, no images) — this just loads it and exposes the tag as a normal React component. The
@@ -70,6 +72,7 @@ export default function App() {
70
72
  | `fontSize` | number \| string | px, default 16. Any size works. |
71
73
  | `src` | string | Library script URL. Default: unpkg latest. |
72
74
  | `cdn` | string | Base URL for CSS + JSON assets. Default: unpkg package root. |
75
+ | `bundled` | boolean \| string | Load the app's own copy of the library (see Offline below): `true` for `/quran-madina/`, or the URL it is served at. Overrides `src`/`cdn`. |
73
76
 
74
77
  > **Global config caveat:** `layout`, `fontSize`, `src` and `cdn` configure the single
75
78
  > library instance for the whole page. Only the **first** mounted `<QuranMadinaHtml>` sets them;
@@ -114,14 +117,30 @@ Notes for mobile:
114
117
 
115
118
  - Capacitor serves the app from `localhost`, so keep the default `cdn` (unpkg) **or** self-host the
116
119
  assets and point `cdn` at them — see above. Do not rely on the library's `../` default.
117
- - **Offline support:** bundle the library `dist/quran-madina-html.min.js`, `dist/*.min.css`, the
118
- `assets/db/v2/` files of the layout you use (its `.json` or shard folder and its `-qmh.woff2` font) into your web build, then set `src` to
119
- the bundled JS and `cdn` to the base path that contains `dist/` and `assets/`. Everything —
120
- including the font — resolves from `cdn`, so no network is needed (requires library >= 0.6,
121
- which stores font paths relative).
120
+ - **Offline:** see below; the copied files go into the app with the rest of the build.
122
121
  - This is the recommended mobile path: it reuses the battle-tested HTML/CSS renderer (fonts,
123
122
  per-line stretch, offsets) instead of reimplementing the layout natively in React Native.
124
123
 
124
+ ## Offline
125
+
126
+ Copy the library, its databases and fonts into your app once, from the app's folder:
127
+
128
+ ```bash
129
+ npx quran-madina-bundle # the 1441H layout, into public/quran-madina
130
+ npx quran-madina-bundle --layouts 1441H,IndoPak # about 4 MB per layout
131
+ ```
132
+
133
+ Then pass `bundled`, and nothing loads from the network:
134
+
135
+ ```jsx
136
+ <QuranMadinaHtml page={106} bundled />
137
+ ```
138
+
139
+ Options: `--out <folder>` (default `public/quran-madina`), `--version <x.y.z>` (default: this
140
+ package's version), `--from <folder>` (a built checkout instead of npm). If the folder is served
141
+ somewhere other than `/quran-madina/`, pass that URL: `bundled="/my-app/quran-madina/"`. Run the
142
+ command again after upgrading.
143
+
125
144
  ## React Native (native, no WebView)
126
145
 
127
146
  Not provided here. React Native has no DOM/HTML/CSS, so the custom element cannot run; it would
package/bin/bundle.mjs ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // Copies quran-madina-html (script, CSS, databases, fonts) into the app's public/ folder, so
3
+ // <QuranMadinaHtml bundled /> renders offline. Run from the app's folder:
4
+ //
5
+ // npx quran-madina-bundle # the 1441H layout, into public/quran-madina
6
+ // npx quran-madina-bundle --layouts 1441H,IndoPak
7
+ // npx quran-madina-bundle --out static/quran-madina --version 2.1.1
8
+ // npx quran-madina-bundle --from ../quran-madina-html # a built checkout instead of npm
9
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { DEFAULT_OUT, LAYOUT_STEMS, readFolder, readTarGz, selectBundle } from "./lib.mjs";
13
+
14
+ const fail = (message) => {
15
+ console.error(`quran-madina-bundle: ${message}`);
16
+ process.exit(1);
17
+ };
18
+
19
+ // the wrapper is released in step with the library: bundle the same version by default
20
+ const own = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
21
+
22
+ const args = process.argv.slice(2);
23
+ let layouts = ["1441H"];
24
+ let version = own.version;
25
+ let out = DEFAULT_OUT;
26
+ let from = null;
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ const value = () => {
30
+ if (arg.includes("=")) return arg.slice(arg.indexOf("=") + 1);
31
+ if (i + 1 >= args.length) fail(`${arg} needs a value`);
32
+ return args[++i];
33
+ };
34
+ if (arg.startsWith("--layouts")) layouts = value().split(",").map((s) => s.trim()).filter(Boolean);
35
+ else if (arg.startsWith("--version")) version = value();
36
+ else if (arg.startsWith("--out")) out = value().replace(/\/+$/, "");
37
+ else if (arg.startsWith("--from")) from = value();
38
+ else if (arg === "-h" || arg === "--help") {
39
+ console.log(`usage: npx quran-madina-bundle [--layouts ${Object.keys(LAYOUT_STEMS).join(",")}] ` +
40
+ `[--out ${DEFAULT_OUT}] [--version ${own.version} | --from <folder>]`);
41
+ process.exit(0);
42
+ } else fail(`unknown option ${arg}`);
43
+ }
44
+ if (!existsSync("package.json")) fail("run this from your app's folder");
45
+
46
+ let pkg;
47
+ if (from) {
48
+ if (!existsSync(from)) fail(`no folder ${from}`);
49
+ console.log(`Reading ${from}`);
50
+ pkg = readFolder(from);
51
+ } else {
52
+ const url = `https://registry.npmjs.org/quran-madina-html/-/quran-madina-html-${version}.tgz`;
53
+ console.log(`Downloading ${url}`);
54
+ const response = await fetch(url).catch((err) => fail(`download failed: ${err.message}`));
55
+ if (!response.ok) fail(`download failed: HTTP ${response.status}`);
56
+ pkg = readTarGz(Buffer.from(await response.arrayBuffer()));
57
+ }
58
+
59
+ let files;
60
+ try {
61
+ files = selectBundle(pkg, layouts);
62
+ } catch (err) {
63
+ fail(err.message);
64
+ }
65
+ rmSync(out, { recursive: true, force: true });
66
+ let bytes = 0;
67
+ for (const [path, data] of files) {
68
+ const target = join(out, path);
69
+ mkdirSync(dirname(target), { recursive: true });
70
+ writeFileSync(target, data);
71
+ bytes += data.length;
72
+ }
73
+ const base = `/${out.replace(/^public\/?/, "").replace(/^\/+/, "")}/`.replace(/\/+$/, "/");
74
+ console.log(`Wrote ${files.size} files (${(bytes / 1048576).toFixed(1)} MB) to ${out}/ for ${layouts.join(", ")}.`);
75
+ console.log("");
76
+ console.log(out.startsWith("public/") || out === "public"
77
+ ? `Then: <QuranMadinaHtml bundled ... />${base === "/quran-madina/" ? "" : ` (or bundled="${base}")`}`
78
+ : `Serve ${out}/ from your app, then: <QuranMadinaHtml bundled="/<where it is served>/" ... />`);
package/bin/lib.mjs ADDED
@@ -0,0 +1,97 @@
1
+ // Pure helpers for `quran-madina-bundle` (bin/bundle.mjs): read the quran-madina-html npm
2
+ // package and pick the files an app needs to render offline. Same rules as the Flutter
3
+ // package's bundler (flutter/lib/src/bundle.dart).
4
+ import { readdirSync, readFileSync, statSync } from "node:fs";
5
+ import { join, relative, sep } from "node:path";
6
+ import { gunzipSync } from "node:zlib";
7
+
8
+ /** The layouts the library ships, by the name `layout` takes, and their database folders. */
9
+ export const LAYOUT_STEMS = { "1441H": "Madina-1441H", "1405H": "Madina-1405H", IndoPak: "IndoPak-15" };
10
+
11
+ /** Where the bundle goes by default, under the app's public/ folder, and the URL it is served at. */
12
+ export const DEFAULT_OUT = "public/quran-madina";
13
+ export const DEFAULT_BASE = "/quran-madina/";
14
+
15
+ /** The files of a tar archive, by path. Plain files only (ustar names with prefixes). */
16
+ export function readTar(tar) {
17
+ const out = new Map();
18
+ const text = (start, length) => {
19
+ const raw = tar.subarray(start, start + length);
20
+ const end = raw.indexOf(0);
21
+ return Buffer.from(end < 0 ? raw : raw.subarray(0, end)).toString("utf8");
22
+ };
23
+ let pos = 0;
24
+ while (pos + 512 <= tar.length) {
25
+ if (tar.subarray(pos, pos + 512).every((b) => b === 0)) break; // end of archive
26
+ const name = text(pos, 100);
27
+ const sizeText = text(pos + 124, 12).trim();
28
+ const size = sizeText ? parseInt(sizeText, 8) : 0;
29
+ const type = tar[pos + 156];
30
+ const prefix = text(pos + 257, 6).startsWith("ustar") ? text(pos + 345, 155) : "";
31
+ const path = prefix ? `${prefix}/${name}` : name;
32
+ const start = pos + 512;
33
+ if (type === 0 || type === 0x30) out.set(path, Buffer.from(tar.subarray(start, start + size)));
34
+ pos = start + Math.ceil(size / 512) * 512;
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /** The files of a .tgz (an npm package), by path. */
40
+ export const readTarGz = (bytes) => readTar(gunzipSync(bytes));
41
+
42
+ const BUNDLE_ROOTS = ["dist/", "assets/img/", "assets/db/v2/"];
43
+
44
+ /**
45
+ * The files under `dir` (an unpacked package or a built checkout), laid out like an npm package:
46
+ * each path starts with `package/`. Only the folders a bundle draws from are read.
47
+ */
48
+ export function readFolder(dir) {
49
+ const out = new Map();
50
+ const walk = (folder) => {
51
+ for (const entry of readdirSync(folder)) {
52
+ const full = join(folder, entry);
53
+ const path = relative(dir, full).split(sep).join("/");
54
+ if (statSync(full).isDirectory()) {
55
+ if (BUNDLE_ROOTS.some((root) => root.startsWith(`${path}/`) || path.startsWith(root))) walk(full);
56
+ } else if (BUNDLE_ROOTS.some((root) => path.startsWith(root))) {
57
+ out.set(`package/${path}`, readFileSync(full));
58
+ }
59
+ }
60
+ };
61
+ walk(dir);
62
+ return out;
63
+ }
64
+
65
+ /** The stem for a layout name, any case; null if the library has no such layout. */
66
+ export function layoutStem(layout) {
67
+ const key = Object.keys(LAYOUT_STEMS).find((k) => k.toLowerCase() === String(layout).toLowerCase());
68
+ return key ? LAYOUT_STEMS[key] : null;
69
+ }
70
+
71
+ /**
72
+ * The files of the package (paths under `package/`) an app needs for `layouts`: the script, the
73
+ * CSS, the images, and each layout's sharded database and font. Result paths are relative to the
74
+ * bundle folder.
75
+ */
76
+ export function selectBundle(pkg, layouts) {
77
+ const files = new Map();
78
+ for (const [path, data] of pkg) if (path.startsWith("package/")) files.set(path.slice(8), data);
79
+ const out = new Map();
80
+ const take = (path) => {
81
+ if (!files.has(path)) throw new Error(`${path} is missing from the package`);
82
+ out.set(path, files.get(path));
83
+ };
84
+ take("dist/quran-madina-html.min.js");
85
+ take("dist/quran-madina-html.min.css");
86
+ for (const path of files.keys()) if (path.startsWith("assets/img/")) take(path);
87
+ for (const layout of layouts) {
88
+ const stem = layoutStem(layout);
89
+ if (!stem) throw new Error(`unknown layout "${layout}" (use ${Object.keys(LAYOUT_STEMS).join(", ")})`);
90
+ const dir = `assets/db/v2/${stem}/`;
91
+ for (const path of files.keys()) if (path.startsWith(dir)) take(path);
92
+ const manifest = files.get(`${dir}manifest.json`);
93
+ if (!manifest) throw new Error(`${dir}manifest.json is missing from the package`);
94
+ take(JSON.parse(manifest.toString("utf8")).font_url);
95
+ }
96
+ return out;
97
+ }
package/dist/index.cjs CHANGED
@@ -29,6 +29,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ BUNDLE_BASE: () => BUNDLE_BASE,
33
+ bundleUrls: () => bundleUrls,
32
34
  default: () => QuranMadinaHtml,
33
35
  loadQuranMadinaHtml: () => loadQuranMadinaHtml
34
36
  });
@@ -39,9 +41,16 @@ var import_react = __toESM(require("react"), 1);
39
41
  var TAG = "quran-madina-html";
40
42
  var DEFAULT_SRC = "https://unpkg.com/quran-madina-html";
41
43
  var DEFAULT_CDN = "https://unpkg.com/quran-madina-html/";
44
+ var BUNDLE_BASE = "/quran-madina/";
45
+ function bundleUrls(bundled) {
46
+ let base = bundled === true ? BUNDLE_BASE : String(bundled);
47
+ if (!base.endsWith("/")) base += "/";
48
+ return { src: `${base}dist/quran-madina-html.min.js`, cdn: base };
49
+ }
42
50
  var loaderPromise = null;
43
51
  function loadQuranMadinaHtml(opts = {}) {
44
- const { src = DEFAULT_SRC, cdn = DEFAULT_CDN, layout, font, name, fontSize } = opts;
52
+ const { layout, font, name, fontSize, bundled } = opts;
53
+ const { src = DEFAULT_SRC, cdn = DEFAULT_CDN } = bundled ? bundleUrls(bundled) : opts;
45
54
  if (typeof window === "undefined" || typeof document === "undefined") {
46
55
  return Promise.resolve();
47
56
  }
@@ -87,6 +96,7 @@ function QuranMadinaHtml({
87
96
  font,
88
97
  name,
89
98
  fontSize,
99
+ bundled,
90
100
  className,
91
101
  style,
92
102
  ...rest
@@ -97,11 +107,11 @@ function QuranMadinaHtml({
97
107
  );
98
108
  (0, import_react.useEffect)(() => {
99
109
  let active = true;
100
- loadQuranMadinaHtml({ src, cdn, layout, font, name, fontSize }).then(() => active && setReady(true)).catch((err) => console.error("[quran-madina-html]", err));
110
+ loadQuranMadinaHtml({ src, cdn, layout, font, name, fontSize, bundled }).then(() => active && setReady(true)).catch((err) => console.error("[quran-madina-html]", err));
101
111
  return () => {
102
112
  active = false;
103
113
  };
104
- }, [src, cdn, layout, font, name, fontSize]);
114
+ }, [src, cdn, layout, font, name, fontSize, bundled]);
105
115
  (0, import_react.useEffect)(() => {
106
116
  const el = ref.current;
107
117
  if (!el) return;
package/dist/index.d.ts CHANGED
@@ -16,6 +16,12 @@ export interface QuranMadinaHtmlLoaderOptions {
16
16
  name?: string;
17
17
  /** data-font-size in px (default 16). Any size works. */
18
18
  fontSize?: number | string;
19
+ /**
20
+ * Load the app's own copy of the library instead of unpkg, for offline use. Write it with
21
+ * `npx quran-madina-bundle` (default: public/quran-madina). `true` loads it from
22
+ * "/quran-madina/"; a string is the URL the bundle folder is served at. Overrides src and cdn.
23
+ */
24
+ bundled?: boolean | string;
19
25
  }
20
26
 
21
27
  export interface QuranMadinaHtmlProps
@@ -70,3 +76,9 @@ export function loadQuranMadinaHtml(
70
76
 
71
77
  declare const QuranMadinaHtml: React.FC<QuranMadinaHtmlProps>;
72
78
  export default QuranMadinaHtml;
79
+
80
+ /** URL the default bundle folder (public/quran-madina) is served at: "/quran-madina/". */
81
+ export declare const BUNDLE_BASE: string;
82
+
83
+ /** The script URL and asset base for a bundled copy of the library (see `bundled`). */
84
+ export declare function bundleUrls(bundled: true | string): { src: string; cdn: string };
package/dist/index.mjs CHANGED
@@ -3,9 +3,16 @@ import React, { useEffect, useRef, useState } from "react";
3
3
  var TAG = "quran-madina-html";
4
4
  var DEFAULT_SRC = "https://unpkg.com/quran-madina-html";
5
5
  var DEFAULT_CDN = "https://unpkg.com/quran-madina-html/";
6
+ var BUNDLE_BASE = "/quran-madina/";
7
+ function bundleUrls(bundled) {
8
+ let base = bundled === true ? BUNDLE_BASE : String(bundled);
9
+ if (!base.endsWith("/")) base += "/";
10
+ return { src: `${base}dist/quran-madina-html.min.js`, cdn: base };
11
+ }
6
12
  var loaderPromise = null;
7
13
  function loadQuranMadinaHtml(opts = {}) {
8
- const { src = DEFAULT_SRC, cdn = DEFAULT_CDN, layout, font, name, fontSize } = opts;
14
+ const { layout, font, name, fontSize, bundled } = opts;
15
+ const { src = DEFAULT_SRC, cdn = DEFAULT_CDN } = bundled ? bundleUrls(bundled) : opts;
9
16
  if (typeof window === "undefined" || typeof document === "undefined") {
10
17
  return Promise.resolve();
11
18
  }
@@ -51,6 +58,7 @@ function QuranMadinaHtml({
51
58
  font,
52
59
  name,
53
60
  fontSize,
61
+ bundled,
54
62
  className,
55
63
  style,
56
64
  ...rest
@@ -61,11 +69,11 @@ function QuranMadinaHtml({
61
69
  );
62
70
  useEffect(() => {
63
71
  let active = true;
64
- loadQuranMadinaHtml({ src, cdn, layout, font, name, fontSize }).then(() => active && setReady(true)).catch((err) => console.error("[quran-madina-html]", err));
72
+ loadQuranMadinaHtml({ src, cdn, layout, font, name, fontSize, bundled }).then(() => active && setReady(true)).catch((err) => console.error("[quran-madina-html]", err));
65
73
  return () => {
66
74
  active = false;
67
75
  };
68
- }, [src, cdn, layout, font, name, fontSize]);
76
+ }, [src, cdn, layout, font, name, fontSize, bundled]);
69
77
  useEffect(() => {
70
78
  const el = ref.current;
71
79
  if (!el) return;
@@ -97,6 +105,8 @@ function QuranMadinaHtml({
97
105
  return React.createElement(TAG, { ref, class: className, style, ...rest });
98
106
  }
99
107
  export {
108
+ BUNDLE_BASE,
109
+ bundleUrls,
100
110
  QuranMadinaHtml as default,
101
111
  loadQuranMadinaHtml
102
112
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarekeldeeb/quran-madina-react",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "React wrapper for the quran-madina-html custom element — render Madina-style Quran pages (no images) in React on web and mobile (via a Capacitor WebView).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -15,7 +15,9 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "README.md"
18
+ "bin",
19
+ "README.md",
20
+ "LICENSE.md"
19
21
  ],
20
22
  "sideEffects": false,
21
23
  "scripts": {
@@ -45,7 +47,7 @@
45
47
  "quran-madina-html"
46
48
  ],
47
49
  "author": "Tarek Eldeeb",
48
- "license": "ISC",
50
+ "license": "SEE LICENSE IN LICENSE.md",
49
51
  "repository": {
50
52
  "type": "git",
51
53
  "url": "git+https://github.com/tarekeldeeb/quran-madina-html.git",
@@ -54,5 +56,8 @@
54
56
  "bugs": {
55
57
  "url": "https://github.com/tarekeldeeb/quran-madina-html/issues"
56
58
  },
57
- "homepage": "https://github.com/tarekeldeeb/quran-madina-html/tree/main/react#readme"
59
+ "homepage": "https://github.com/tarekeldeeb/quran-madina-html/tree/main/react#readme",
60
+ "bin": {
61
+ "quran-madina-bundle": "bin/bundle.mjs"
62
+ }
58
63
  }