@youversion/platform-core 1.20.2 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +21 -10
- package/AGENTS.md +47 -2
- package/CHANGELOG.md +16 -0
- package/dist/browser-DzQ1yOHv.d.cts +69 -0
- package/dist/browser-DzQ1yOHv.d.ts +69 -0
- package/dist/browser.cjs +270 -0
- package/dist/browser.d.cts +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +6 -0
- package/dist/chunk-2Z2S2WY3.js +245 -0
- package/dist/index.cjs +246 -2
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +16 -1
- package/dist/server.cjs +275 -0
- package/dist/server.d.cts +25 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +18 -0
- package/package.json +23 -5
- package/src/bible-html-transformer-server.ts +37 -0
- package/src/bible-html-transformer.server.test.ts +151 -0
- package/src/bible-html-transformer.test.ts +403 -0
- package/src/bible-html-transformer.ts +378 -0
- package/src/bible.ts +10 -0
- package/src/browser.ts +4 -0
- package/src/index.ts +5 -0
- package/src/server.ts +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
|
|
2
|
-
> @youversion/platform-core@1.
|
|
3
|
-
> tsup src/index.ts --format cjs,esm --dts
|
|
2
|
+
> @youversion/platform-core@1.21.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/core
|
|
3
|
+
> tsup src/index.ts src/browser.ts src/server.ts --format cjs,esm --dts
|
|
4
4
|
|
|
5
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
5
|
+
[34mCLI[39m Building entry: src/browser.ts, src/index.ts, src/server.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.5.0
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mCJS[39m Build start
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[
|
|
12
|
-
[
|
|
13
|
-
[32mESM[39m [1mdist/index.js
|
|
14
|
-
[32mESM[39m
|
|
11
|
+
[32mESM[39m [1mdist/browser.js [22m[32m135.00 B[39m
|
|
12
|
+
[32mESM[39m [1mdist/server.js [22m[32m443.00 B[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.js [22m[32m53.56 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/chunk-2Z2S2WY3.js [22m[32m7.74 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 65ms
|
|
16
|
+
[32mCJS[39m [1mdist/browser.cjs [22m[32m8.75 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m64.03 KB[39m
|
|
18
|
+
[32mCJS[39m [1mdist/server.cjs [22m[32m8.76 KB[39m
|
|
19
|
+
[32mCJS[39m ⚡️ Build success in 66ms
|
|
15
20
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.
|
|
21
|
+
[32mDTS[39m ⚡️ Build success in 3000ms
|
|
22
|
+
[32mDTS[39m [1mdist/server.d.cts [22m[32m863.00 B[39m
|
|
23
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m33.68 KB[39m
|
|
24
|
+
[32mDTS[39m [1mdist/browser.d.cts [22m[32m93.00 B[39m
|
|
25
|
+
[32mDTS[39m [1mdist/browser-DzQ1yOHv.d.cts [22m[32m2.67 KB[39m
|
|
26
|
+
[32mDTS[39m [1mdist/server.d.ts [22m[32m862.00 B[39m
|
|
27
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m33.68 KB[39m
|
|
28
|
+
[32mDTS[39m [1mdist/browser.d.ts [22m[32m92.00 B[39m
|
|
29
|
+
[32mDTS[39m [1mdist/browser-DzQ1yOHv.d.ts [22m[32m2.67 KB[39m
|
package/AGENTS.md
CHANGED
|
@@ -17,6 +17,11 @@ highlights.ts # HighlightsClient - user highlights
|
|
|
17
17
|
YouVersionAPI.ts # Base YouVersion API client
|
|
18
18
|
SignInWithYouVersionPKCE.ts # PKCE auth implementation
|
|
19
19
|
StorageStrategy.ts # Storage interface (SessionStorage, MemoryStorage)
|
|
20
|
+
bible-html-transformer.ts # Runtime-agnostic transformer (also contains browser convenience fn)
|
|
21
|
+
bible-html-transformer-server.ts # Server convenience wrapper (uses linkedom)
|
|
22
|
+
browser.ts # Browser entry point
|
|
23
|
+
server.ts # Server entry point
|
|
24
|
+
index.ts # Main entry point (runtime-agnostic)
|
|
20
25
|
```
|
|
21
26
|
|
|
22
27
|
## PUBLIC API
|
|
@@ -26,18 +31,58 @@ StorageStrategy.ts # Storage interface (SessionStorage, MemoryStorage)
|
|
|
26
31
|
- `HighlightsClient`: Manage user highlights
|
|
27
32
|
- `SignInWithYouVersionPKCE()`: PKCE auth flow function
|
|
28
33
|
- `SessionStorage`, `MemoryStorage`: Storage strategies
|
|
34
|
+
- `transformBibleHtml`: Runtime-agnostic Bible HTML transformer (requires DOM adapters)
|
|
35
|
+
- `TransformBibleHtmlOptions`: Options for DOM parsing and serialization
|
|
29
36
|
|
|
30
37
|
## DOs / DON'Ts
|
|
31
38
|
|
|
32
|
-
✅ Do: Keep this package **framework-agnostic** (no React, no
|
|
39
|
+
✅ Do: Keep this package **framework-agnostic** (no React, no environment-specific dependencies in main export)
|
|
40
|
+
✅ Do: Provide pure, runtime-agnostic functions as the main export
|
|
41
|
+
✅ Do: Use environment-specific entry points (`/browser`, `/server`) for convenience wrappers
|
|
33
42
|
✅ Do: Define all input/output types in `schemas/` using Zod; schemas are the single source of truth
|
|
34
43
|
✅ Do: Reuse `YouVersionAPI` base client for new service clients
|
|
35
44
|
✅ Do: Parse API responses with Zod schemas for validation
|
|
36
45
|
|
|
37
|
-
❌ Don't: Import React
|
|
46
|
+
❌ Don't: Import React or React-specific APIs
|
|
47
|
+
❌ Don't: Bundle linkedom in browser builds (use `/server` entry point for server-side code)
|
|
48
|
+
❌ Don't: Include environment-specific code in the default entry point
|
|
38
49
|
❌ Don't: Bypass Zod validation for API responses
|
|
39
50
|
❌ Don't: Implement UI, hooks, or React state here
|
|
40
51
|
|
|
52
|
+
## ENVIRONMENT-SPECIFIC EXPORTS
|
|
53
|
+
|
|
54
|
+
The Bible HTML transformer provides both a runtime-agnostic core and environment-specific convenience wrappers:
|
|
55
|
+
|
|
56
|
+
- `@youversion/platform-core` → Runtime-agnostic `transformBibleHtml` (requires DOM adapters)
|
|
57
|
+
- `@youversion/platform-core/browser` → Browser convenience wrapper (uses native DOMParser)
|
|
58
|
+
- `@youversion/platform-core/server` → Server convenience wrapper (uses linkedom)
|
|
59
|
+
|
|
60
|
+
**Examples:**
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Runtime-agnostic (works anywhere with custom adapters)
|
|
64
|
+
import { transformBibleHtml } from '@youversion/platform-core';
|
|
65
|
+
|
|
66
|
+
const result = transformBibleHtml(html, {
|
|
67
|
+
parseHtml: (h) => new DOMParser().parseFromString(h, 'text/html'),
|
|
68
|
+
serializeHtml: (doc) => doc.body.innerHTML,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Browser convenience (uses native DOMParser)
|
|
72
|
+
import { transformBibleHtml } from '@youversion/platform-core/browser';
|
|
73
|
+
|
|
74
|
+
const result = transformBibleHtml(html);
|
|
75
|
+
|
|
76
|
+
// Server convenience (uses linkedom, requires: npm install linkedom)
|
|
77
|
+
import { transformBibleHtml } from '@youversion/platform-core/server';
|
|
78
|
+
|
|
79
|
+
const result = transformBibleHtml(html);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Why separate entry points?**
|
|
83
|
+
|
|
84
|
+
This architecture keeps the main export truly runtime-agnostic while providing ergonomic convenience wrappers for common environments. The separate `/browser` and `/server` entry points ensure optimal bundle sizes - linkedom won't be bundled in browser builds.
|
|
85
|
+
|
|
41
86
|
## ADDING A NEW ENDPOINT OR CLIENT
|
|
42
87
|
|
|
43
88
|
1. **Define types** in `schemas/` using Zod:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @youversion/platform-core
|
|
2
2
|
|
|
3
|
+
## 1.21.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 87ad436: **`@youversion/platform-core`**: Add `transformBibleHtml` — a runtime-agnostic Bible HTML transformer with new `/browser` and `/server` subpath exports.
|
|
8
|
+
- `@youversion/platform-core` — runtime-agnostic core; accepts `parseHtml`/`serializeHtml` adapters so it works with any DOM implementation
|
|
9
|
+
- `@youversion/platform-core/browser` — zero-config convenience wrapper using the native `DOMParser`
|
|
10
|
+
- `@youversion/platform-core/server` — zero-config convenience wrapper using `linkedom` (optional peer dependency)
|
|
11
|
+
|
|
12
|
+
The transformer sanitizes API HTML (custom allowlist-based sanitizer, no DOMPurify dependency), wraps verse content for CSS targeting, and embeds footnote data as `data-verse-footnote` / `data-verse-footnote-content` attributes directly in the HTML.
|
|
13
|
+
|
|
14
|
+
**`@youversion/platform-react-ui`**: Migrate Bible HTML transformation from the UI package to `@youversion/platform-core/browser`.
|
|
15
|
+
- Removed `isomorphic-dompurify` dependency (lighter bundle)
|
|
16
|
+
- Footnote popover data is now read from DOM attributes at render time instead of a separate data structure
|
|
17
|
+
- Added SSR safety guard — `Verse.Html` returns raw HTML during server rendering and transforms on the client after hydration
|
|
18
|
+
|
|
3
19
|
## 1.20.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for transforming Bible HTML. Requires DOM adapter functions
|
|
3
|
+
* to parse and serialize HTML, making the transformer runtime-agnostic.
|
|
4
|
+
*/
|
|
5
|
+
type TransformBibleHtmlOptions = {
|
|
6
|
+
/** Parses an HTML string into a DOM Document */
|
|
7
|
+
parseHtml: (html: string) => Document;
|
|
8
|
+
/** Serializes a Document back to an HTML string */
|
|
9
|
+
serializeHtml: (doc: Document) => string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The result of transforming Bible HTML.
|
|
13
|
+
*
|
|
14
|
+
* The returned HTML is self-contained — footnote data is embedded as attributes:
|
|
15
|
+
* - `data-verse-footnote="KEY"` marks the footnote position
|
|
16
|
+
* - `data-verse-footnote-content="HTML"` contains the footnote's inner HTML
|
|
17
|
+
*
|
|
18
|
+
* Consumers can access verse context by walking up from a footnote anchor
|
|
19
|
+
* to `.closest('.yv-v[v]')`.
|
|
20
|
+
*/
|
|
21
|
+
type TransformedBibleHtml = {
|
|
22
|
+
/** The transformed HTML with footnotes replaced by marker elements */
|
|
23
|
+
html: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Transforms Bible HTML by cleaning up verse structure, extracting footnotes,
|
|
27
|
+
* and replacing them with self-contained anchor elements.
|
|
28
|
+
*
|
|
29
|
+
* Footnote data is embedded directly in the HTML via attributes:
|
|
30
|
+
* - `data-verse-footnote="KEY"` — the footnote key (verse number or `intro-N`)
|
|
31
|
+
* - `data-verse-footnote-content="HTML"` — the footnote's inner HTML content
|
|
32
|
+
*
|
|
33
|
+
* Verse context is available by walking up from a footnote anchor:
|
|
34
|
+
* `anchor.closest('.yv-v[v]')` returns the verse wrapper (null for intro footnotes).
|
|
35
|
+
*
|
|
36
|
+
* @param html - The raw Bible HTML from the YouVersion API
|
|
37
|
+
* @param options - DOM adapter options for parsing and serializing HTML
|
|
38
|
+
* @returns The transformed HTML
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { transformBibleHtml } from '@youversion/platform-core';
|
|
43
|
+
*
|
|
44
|
+
* const result = transformBibleHtml(rawHtml, {
|
|
45
|
+
* parseHtml: (html) => new DOMParser().parseFromString(html, 'text/html'),
|
|
46
|
+
* serializeHtml: (doc) => doc.body.innerHTML,
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* console.log(result.html); // Clean HTML with self-contained footnote anchors
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function transformBibleHtml(html: string, options: TransformBibleHtmlOptions): TransformedBibleHtml;
|
|
53
|
+
/**
|
|
54
|
+
* Transforms Bible HTML for browser environments using the native DOMParser API.
|
|
55
|
+
*
|
|
56
|
+
* @param html - The raw Bible HTML from the YouVersion API
|
|
57
|
+
* @returns The transformed HTML
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { transformBibleHtmlForBrowser } from '@youversion/platform-core/browser';
|
|
62
|
+
*
|
|
63
|
+
* const result = transformBibleHtmlForBrowser(rawHtml);
|
|
64
|
+
* console.log(result.html); // Clean HTML with self-contained footnote anchors
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function transformBibleHtmlForBrowser(html: string): TransformedBibleHtml;
|
|
68
|
+
|
|
69
|
+
export { type TransformedBibleHtml as T, type TransformBibleHtmlOptions as a, transformBibleHtmlForBrowser as b, transformBibleHtml as t };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for transforming Bible HTML. Requires DOM adapter functions
|
|
3
|
+
* to parse and serialize HTML, making the transformer runtime-agnostic.
|
|
4
|
+
*/
|
|
5
|
+
type TransformBibleHtmlOptions = {
|
|
6
|
+
/** Parses an HTML string into a DOM Document */
|
|
7
|
+
parseHtml: (html: string) => Document;
|
|
8
|
+
/** Serializes a Document back to an HTML string */
|
|
9
|
+
serializeHtml: (doc: Document) => string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The result of transforming Bible HTML.
|
|
13
|
+
*
|
|
14
|
+
* The returned HTML is self-contained — footnote data is embedded as attributes:
|
|
15
|
+
* - `data-verse-footnote="KEY"` marks the footnote position
|
|
16
|
+
* - `data-verse-footnote-content="HTML"` contains the footnote's inner HTML
|
|
17
|
+
*
|
|
18
|
+
* Consumers can access verse context by walking up from a footnote anchor
|
|
19
|
+
* to `.closest('.yv-v[v]')`.
|
|
20
|
+
*/
|
|
21
|
+
type TransformedBibleHtml = {
|
|
22
|
+
/** The transformed HTML with footnotes replaced by marker elements */
|
|
23
|
+
html: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Transforms Bible HTML by cleaning up verse structure, extracting footnotes,
|
|
27
|
+
* and replacing them with self-contained anchor elements.
|
|
28
|
+
*
|
|
29
|
+
* Footnote data is embedded directly in the HTML via attributes:
|
|
30
|
+
* - `data-verse-footnote="KEY"` — the footnote key (verse number or `intro-N`)
|
|
31
|
+
* - `data-verse-footnote-content="HTML"` — the footnote's inner HTML content
|
|
32
|
+
*
|
|
33
|
+
* Verse context is available by walking up from a footnote anchor:
|
|
34
|
+
* `anchor.closest('.yv-v[v]')` returns the verse wrapper (null for intro footnotes).
|
|
35
|
+
*
|
|
36
|
+
* @param html - The raw Bible HTML from the YouVersion API
|
|
37
|
+
* @param options - DOM adapter options for parsing and serializing HTML
|
|
38
|
+
* @returns The transformed HTML
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { transformBibleHtml } from '@youversion/platform-core';
|
|
43
|
+
*
|
|
44
|
+
* const result = transformBibleHtml(rawHtml, {
|
|
45
|
+
* parseHtml: (html) => new DOMParser().parseFromString(html, 'text/html'),
|
|
46
|
+
* serializeHtml: (doc) => doc.body.innerHTML,
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* console.log(result.html); // Clean HTML with self-contained footnote anchors
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function transformBibleHtml(html: string, options: TransformBibleHtmlOptions): TransformedBibleHtml;
|
|
53
|
+
/**
|
|
54
|
+
* Transforms Bible HTML for browser environments using the native DOMParser API.
|
|
55
|
+
*
|
|
56
|
+
* @param html - The raw Bible HTML from the YouVersion API
|
|
57
|
+
* @returns The transformed HTML
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { transformBibleHtmlForBrowser } from '@youversion/platform-core/browser';
|
|
62
|
+
*
|
|
63
|
+
* const result = transformBibleHtmlForBrowser(rawHtml);
|
|
64
|
+
* console.log(result.html); // Clean HTML with self-contained footnote anchors
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function transformBibleHtmlForBrowser(html: string): TransformedBibleHtml;
|
|
68
|
+
|
|
69
|
+
export { type TransformedBibleHtml as T, type TransformBibleHtmlOptions as a, transformBibleHtmlForBrowser as b, transformBibleHtml as t };
|
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/browser.ts
|
|
21
|
+
var browser_exports = {};
|
|
22
|
+
__export(browser_exports, {
|
|
23
|
+
transformBibleHtml: () => transformBibleHtmlForBrowser
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(browser_exports);
|
|
26
|
+
|
|
27
|
+
// src/bible-html-transformer.ts
|
|
28
|
+
var NON_BREAKING_SPACE = "\xA0";
|
|
29
|
+
var FOOTNOTE_KEY_ATTR = "data-footnote-key";
|
|
30
|
+
var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"'»›]/;
|
|
31
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
32
|
+
"DIV",
|
|
33
|
+
"P",
|
|
34
|
+
"SPAN",
|
|
35
|
+
"SUP",
|
|
36
|
+
"SUB",
|
|
37
|
+
"EM",
|
|
38
|
+
"STRONG",
|
|
39
|
+
"I",
|
|
40
|
+
"B",
|
|
41
|
+
"SMALL",
|
|
42
|
+
"BR",
|
|
43
|
+
"SECTION",
|
|
44
|
+
"TABLE",
|
|
45
|
+
"THEAD",
|
|
46
|
+
"TBODY",
|
|
47
|
+
"TR",
|
|
48
|
+
"TD",
|
|
49
|
+
"TH"
|
|
50
|
+
]);
|
|
51
|
+
var DROP_ENTIRELY_TAGS = /* @__PURE__ */ new Set([
|
|
52
|
+
"SCRIPT",
|
|
53
|
+
"STYLE",
|
|
54
|
+
"IFRAME",
|
|
55
|
+
"OBJECT",
|
|
56
|
+
"EMBED",
|
|
57
|
+
"SVG",
|
|
58
|
+
"MATH",
|
|
59
|
+
"FORM",
|
|
60
|
+
"INPUT",
|
|
61
|
+
"BUTTON",
|
|
62
|
+
"TEXTAREA",
|
|
63
|
+
"SELECT",
|
|
64
|
+
"TEMPLATE",
|
|
65
|
+
"LINK",
|
|
66
|
+
"META",
|
|
67
|
+
"BASE",
|
|
68
|
+
"NOSCRIPT"
|
|
69
|
+
]);
|
|
70
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class", "v", "colspan", "rowspan", "dir", "usfm"]);
|
|
71
|
+
function sanitizeBibleHtmlDocument(doc) {
|
|
72
|
+
const root = doc.body ?? doc.documentElement;
|
|
73
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
74
|
+
const tag = el.tagName;
|
|
75
|
+
if (DROP_ENTIRELY_TAGS.has(tag)) {
|
|
76
|
+
el.remove();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
80
|
+
el.replaceWith(...Array.from(el.childNodes));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const attr of Array.from(el.attributes)) {
|
|
84
|
+
const name = attr.name.toLowerCase();
|
|
85
|
+
if (name.startsWith("on")) {
|
|
86
|
+
el.removeAttribute(attr.name);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!ALLOWED_ATTRS.has(name) && !name.startsWith("data-")) {
|
|
90
|
+
el.removeAttribute(attr.name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function wrapVerseContent(doc) {
|
|
96
|
+
function wrapParagraphContent(doc2, paragraph, verseNum) {
|
|
97
|
+
const children = Array.from(paragraph.childNodes);
|
|
98
|
+
if (children.length === 0) return;
|
|
99
|
+
const wrapper = doc2.createElement("span");
|
|
100
|
+
wrapper.className = "yv-v";
|
|
101
|
+
wrapper.setAttribute("v", verseNum);
|
|
102
|
+
const firstChild = children[0];
|
|
103
|
+
if (firstChild) {
|
|
104
|
+
paragraph.insertBefore(wrapper, firstChild);
|
|
105
|
+
}
|
|
106
|
+
children.forEach((child) => {
|
|
107
|
+
wrapper.appendChild(child);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function wrapParagraphsUntilBoundary(doc2, verseNum, startParagraph, endParagraph) {
|
|
111
|
+
if (!startParagraph) return;
|
|
112
|
+
let currentParagraph = startParagraph.nextElementSibling;
|
|
113
|
+
while (currentParagraph && currentParagraph !== endParagraph) {
|
|
114
|
+
const isHeading = currentParagraph.classList.contains("yv-h") || currentParagraph.matches(
|
|
115
|
+
".s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r"
|
|
116
|
+
);
|
|
117
|
+
if (isHeading) {
|
|
118
|
+
currentParagraph = currentParagraph.nextElementSibling;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (currentParagraph.querySelector(".yv-v[v]")) break;
|
|
122
|
+
if (currentParagraph.classList.contains("p") || currentParagraph.tagName === "P") {
|
|
123
|
+
wrapParagraphContent(doc2, currentParagraph, verseNum);
|
|
124
|
+
}
|
|
125
|
+
currentParagraph = currentParagraph.nextElementSibling;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum) {
|
|
129
|
+
if (!currentParagraph) return;
|
|
130
|
+
if (!nextParagraph) {
|
|
131
|
+
wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (currentParagraph !== nextParagraph) {
|
|
135
|
+
wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph, nextParagraph);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function processVerseMarker(marker, index, markers) {
|
|
139
|
+
const verseNum = marker.getAttribute("v");
|
|
140
|
+
if (!verseNum) return;
|
|
141
|
+
const nextMarker = markers[index + 1];
|
|
142
|
+
const nodesToWrap = collectNodesBetweenMarkers(marker, nextMarker);
|
|
143
|
+
if (nodesToWrap.length === 0) return;
|
|
144
|
+
const currentParagraph = marker.closest(".p, p, div.p");
|
|
145
|
+
const nextParagraph = nextMarker?.closest(".p, p, div.p") || null;
|
|
146
|
+
const doc2 = marker.ownerDocument;
|
|
147
|
+
wrapNodesInVerse(marker, verseNum, nodesToWrap);
|
|
148
|
+
handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum);
|
|
149
|
+
}
|
|
150
|
+
function wrapNodesInVerse(marker, verseNum, nodes) {
|
|
151
|
+
const wrapper = marker.ownerDocument.createElement("span");
|
|
152
|
+
wrapper.className = "yv-v";
|
|
153
|
+
wrapper.setAttribute("v", verseNum);
|
|
154
|
+
const firstNode = nodes[0];
|
|
155
|
+
if (firstNode) {
|
|
156
|
+
marker.parentNode?.insertBefore(wrapper, firstNode);
|
|
157
|
+
}
|
|
158
|
+
nodes.forEach((node) => {
|
|
159
|
+
wrapper.appendChild(node);
|
|
160
|
+
});
|
|
161
|
+
marker.remove();
|
|
162
|
+
}
|
|
163
|
+
function shouldStopCollecting(node, endMarker) {
|
|
164
|
+
if (node === endMarker) return true;
|
|
165
|
+
if (endMarker && node.nodeType === 1 && node.contains(endMarker)) return true;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function shouldSkipNode(node) {
|
|
169
|
+
return node.nodeType === 1 && node.classList.contains("yv-h");
|
|
170
|
+
}
|
|
171
|
+
function collectNodesBetweenMarkers(startMarker, endMarker) {
|
|
172
|
+
const nodes = [];
|
|
173
|
+
let current = startMarker.nextSibling;
|
|
174
|
+
while (current && !shouldStopCollecting(current, endMarker)) {
|
|
175
|
+
if (shouldSkipNode(current)) {
|
|
176
|
+
current = current.nextSibling;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
nodes.push(current);
|
|
180
|
+
current = current.nextSibling;
|
|
181
|
+
}
|
|
182
|
+
return nodes;
|
|
183
|
+
}
|
|
184
|
+
const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
|
|
185
|
+
verseMarkers.forEach(processVerseMarker);
|
|
186
|
+
}
|
|
187
|
+
function assignFootnoteKeys(doc) {
|
|
188
|
+
let introIdx = 0;
|
|
189
|
+
doc.querySelectorAll(".yv-n.f").forEach((fn) => {
|
|
190
|
+
const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
|
|
191
|
+
fn.setAttribute(FOOTNOTE_KEY_ATTR, verseNum ?? `intro-${introIdx++}`);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function replaceFootnotesWithAnchors(doc, footnotes) {
|
|
195
|
+
for (const fn of footnotes) {
|
|
196
|
+
const key = fn.getAttribute(FOOTNOTE_KEY_ATTR);
|
|
197
|
+
if (!key) continue;
|
|
198
|
+
const prev = fn.previousSibling;
|
|
199
|
+
const next = fn.nextSibling;
|
|
200
|
+
const prevText = prev?.textContent ?? "";
|
|
201
|
+
const nextText = next?.textContent ?? "";
|
|
202
|
+
const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
|
|
203
|
+
const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
|
|
204
|
+
if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
|
|
205
|
+
fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
|
|
206
|
+
}
|
|
207
|
+
const anchor = doc.createElement("span");
|
|
208
|
+
anchor.setAttribute("data-verse-footnote", key);
|
|
209
|
+
anchor.setAttribute("data-verse-footnote-content", fn.innerHTML);
|
|
210
|
+
fn.replaceWith(anchor);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function addNbspToVerseLabels(doc) {
|
|
214
|
+
doc.querySelectorAll(".yv-vlbl").forEach((label) => {
|
|
215
|
+
const text = label.textContent || "";
|
|
216
|
+
if (!text.endsWith(NON_BREAKING_SPACE)) {
|
|
217
|
+
label.textContent = text + NON_BREAKING_SPACE;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function fixIrregularTables(doc) {
|
|
222
|
+
doc.querySelectorAll("table").forEach((table) => {
|
|
223
|
+
const rows = table.querySelectorAll("tr");
|
|
224
|
+
if (rows.length === 0) return;
|
|
225
|
+
let maxColumns = 0;
|
|
226
|
+
rows.forEach((row) => {
|
|
227
|
+
let count = 0;
|
|
228
|
+
row.querySelectorAll("td, th").forEach((cell) => {
|
|
229
|
+
count += parseInt(cell.getAttribute("colspan") || "1", 10);
|
|
230
|
+
});
|
|
231
|
+
maxColumns = Math.max(maxColumns, count);
|
|
232
|
+
});
|
|
233
|
+
if (maxColumns > 1) {
|
|
234
|
+
rows.forEach((row) => {
|
|
235
|
+
const cells = row.querySelectorAll("td, th");
|
|
236
|
+
if (cells.length === 1) {
|
|
237
|
+
const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
|
|
238
|
+
if (existing < maxColumns) {
|
|
239
|
+
cells[0].setAttribute("colspan", maxColumns.toString());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function transformBibleHtml(html, options) {
|
|
247
|
+
const doc = options.parseHtml(html);
|
|
248
|
+
sanitizeBibleHtmlDocument(doc);
|
|
249
|
+
wrapVerseContent(doc);
|
|
250
|
+
assignFootnoteKeys(doc);
|
|
251
|
+
const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
|
|
252
|
+
replaceFootnotesWithAnchors(doc, footnotes);
|
|
253
|
+
addNbspToVerseLabels(doc);
|
|
254
|
+
fixIrregularTables(doc);
|
|
255
|
+
const transformedHtml = options.serializeHtml(doc);
|
|
256
|
+
return { html: transformedHtml };
|
|
257
|
+
}
|
|
258
|
+
function transformBibleHtmlForBrowser(html) {
|
|
259
|
+
if (typeof globalThis.DOMParser === "undefined") {
|
|
260
|
+
throw new Error("DOMParser is required to transform Bible HTML in browser environments");
|
|
261
|
+
}
|
|
262
|
+
return transformBibleHtml(html, {
|
|
263
|
+
parseHtml: (h) => new DOMParser().parseFromString(h, "text/html"),
|
|
264
|
+
serializeHtml: (doc) => doc.body.innerHTML
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
268
|
+
0 && (module.exports = {
|
|
269
|
+
transformBibleHtml
|
|
270
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { T as TransformedBibleHtml, b as transformBibleHtml } from './browser-DzQ1yOHv.cjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { T as TransformedBibleHtml, b as transformBibleHtml } from './browser-DzQ1yOHv.js';
|