@missing-elements/h5p-embed 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/lib/build.mjs +75 -11
- package/package.json +18 -10
- package/site/embed.js +50 -14
- package/types/lib/build.d.mts +78 -0
- package/types/site/embed.d.ts +50 -0
package/README.md
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Writes the H5P embed page as a static site for a player domain of your own. Any site then embeds
|
|
4
4
|
a package with an iframe and one script line. No H5P server, no backend, no third-party service.
|
|
5
5
|
|
|
6
|
+
Needs Node 22 or later. Write `@latest`: without it, npm on an older Node quietly installs the
|
|
7
|
+
last version that supported it, which writes a player from before 1.0.
|
|
8
|
+
|
|
6
9
|
```bash
|
|
7
|
-
npx @missing-elements/h5p-embed h5p-player # the folder defaults to ./h5p-player
|
|
10
|
+
npx @missing-elements/h5p-embed@latest h5p-player # the folder defaults to ./h5p-player
|
|
8
11
|
```
|
|
9
12
|
|
|
10
13
|
Deploy it to any static host, then embed:
|
|
@@ -27,7 +30,7 @@ a subdomain shares your site's cookies.
|
|
|
27
30
|
|
|
28
31
|
| Option | Effect |
|
|
29
32
|
|---|---|
|
|
30
|
-
| `--packages <origins>` | Play packages and fetch library bundles only from these origins (the site's own is always allowed). Add `https://cdn.jsdelivr.net` for `libraries=pack` on a site built with `--no-libraries`. Default: any https host |
|
|
33
|
+
| `--packages <origins>` | Play packages and fetch library bundles only from these origins (the site's own is always allowed). Each host on its own, with no wildcard: the page compares exact origins. Add `https://cdn.jsdelivr.net` for `libraries=pack` on a site built with `--no-libraries`. Default: any https host |
|
|
31
34
|
| `--ancestors <origins>` | Only these sites may frame the page (`frame-ancestors`, header only). Default: any site |
|
|
32
35
|
| `--default-libraries <sources>` | The `libraries` value for addresses that name none: `pack` or URLs. Checked against `--packages`. Default: none, so exports without libraries are refused unless the address asks |
|
|
33
36
|
| `--no-libraries` | Leave out the library pack, 13.8 MB in `libraries/`, one archive per library, of which a learner's browser fetches only those a package needs; `libraries=pack` then fetches them from jsDelivr, where allowed |
|
|
@@ -40,7 +43,7 @@ through:** the browser's policy applies to each hop, and a redirect off the list
|
|
|
40
43
|
network error.
|
|
41
44
|
|
|
42
45
|
```bash
|
|
43
|
-
npx @missing-elements/h5p-embed h5p-player \
|
|
46
|
+
npx @missing-elements/h5p-embed@latest h5p-player \
|
|
44
47
|
--packages https://cdn.example.org \
|
|
45
48
|
--ancestors "https://www.example.org https://lms.example.org"
|
|
46
49
|
```
|
|
@@ -82,7 +85,7 @@ To update, run the command again with `--force` and redeploy.
|
|
|
82
85
|
| `preload=auto` | Start fetching media at once |
|
|
83
86
|
| `activity-id=<IRI>` | The xAPI object id, instead of the package URL |
|
|
84
87
|
| `custom-css=<url>` | A stylesheet loaded into the content |
|
|
85
|
-
| `lang=<tag>` | The language of H5P's own buttons and dialogs, such as `de` or `pt-BR
|
|
88
|
+
| `lang=<tag>` | The language of H5P's own buttons and dialogs, such as `de` or `pt-BR`, for a package that does not name its own. A package that names its language in `h5p.json` gets them in that language, matching its content. Default: English |
|
|
86
89
|
| `xapi=<origin>` | Relay statements to the embedding page |
|
|
87
90
|
|
|
88
91
|
A custom script and a learner's name cannot be set on the address. Opened unframed, the page plays
|
|
@@ -135,6 +138,7 @@ export, which `libraries=pack` then plays from) or `packBase` (the folder of a c
|
|
|
135
138
|
`@missing-elements/h5p-runtime` export, for a bundled element) and `askInOwnFrame` (default
|
|
136
139
|
`true`; `false` plays a package from another origin as it is when a same-origin page frames it).
|
|
137
140
|
`librariesPack`, the URL of a whole `libraries.h5p`, still works, as a bundle that only fills gaps.
|
|
141
|
+
Both entries ship with TypeScript declarations.
|
|
138
142
|
|
|
139
143
|
```js
|
|
140
144
|
import '@missing-elements/h5p-offline-player'
|
package/lib/build.mjs
CHANGED
|
@@ -23,7 +23,11 @@ const PAGE_FILES = ['embed.js', 'embed.css', 'main.js', 'resizer.js']
|
|
|
23
23
|
/** The player's files, by the names the element looks for beside itself. */
|
|
24
24
|
const PLAYER_FILES = ['h5p-player.js', 'h5p-sw.js', 'h5p-jobs.js']
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Where an installed package's file is, or a message that says which one is missing.
|
|
28
|
+
* @param {string} specifier
|
|
29
|
+
* @param {string} hint
|
|
30
|
+
*/
|
|
27
31
|
function installed(specifier, hint) {
|
|
28
32
|
try {
|
|
29
33
|
return require.resolve(specifier)
|
|
@@ -35,7 +39,13 @@ function installed(specifier, hint) {
|
|
|
35
39
|
/**
|
|
36
40
|
* An origin as the CSP and the page compare it: `https://host[:port]`, or plain http on a loopback
|
|
37
41
|
* address for trying it locally. Anything with a path, a query or a trailing slash is refused
|
|
38
|
-
* rather than trimmed, so a typo is not silently a different policy.
|
|
42
|
+
* rather than trimmed, so a typo is not silently a different policy. The host is held to what a
|
|
43
|
+
* CSP host is, letters, digits and hyphens between dots: the URL parser takes `*`, `;` and `"` in
|
|
44
|
+
* a host, and a wildcard would allow what the page then refuses, by exact origin, on every load,
|
|
45
|
+
* while a quote would end the policy in the files that quote it (#58).
|
|
46
|
+
* @internal
|
|
47
|
+
* @param {string} value
|
|
48
|
+
* @returns {string}
|
|
39
49
|
*/
|
|
40
50
|
export function parseOrigin(value) {
|
|
41
51
|
let url
|
|
@@ -48,6 +58,9 @@ export function parseOrigin(value) {
|
|
|
48
58
|
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
|
|
49
59
|
throw new EmbedError(`Not an https origin: ${value}`)
|
|
50
60
|
}
|
|
61
|
+
if (!loopback && !/^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/.test(url.hostname)) {
|
|
62
|
+
throw new EmbedError(`Not an origin: ${value}. A host is letters, digits, hyphens and dots, and each host is listed on its own, with no wildcard`)
|
|
63
|
+
}
|
|
51
64
|
if (url.origin !== value) throw new EmbedError(`Not an origin: ${value}. Write it as ${url.origin}, with no path or trailing slash`)
|
|
52
65
|
return url.origin
|
|
53
66
|
}
|
|
@@ -59,6 +72,7 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
|
|
|
59
72
|
* A `libraries` value for addresses that name none, checked against the site it goes into: the
|
|
60
73
|
* tokens the page understands (`pack`, `none`, https URLs), and with a host list, nothing
|
|
61
74
|
* the page would then refuse on every load. Returns the value normalised to single spaces.
|
|
75
|
+
* @internal
|
|
62
76
|
*
|
|
63
77
|
* @param {string | null | undefined} value
|
|
64
78
|
* @param {object} [site]
|
|
@@ -97,7 +111,12 @@ export function parseDefaultLibraries(value, { packages = null, libraries = true
|
|
|
97
111
|
return tokens.join(' ')
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Origins given as a list, or as one string separated by commas or whitespace.
|
|
116
|
+
* @internal
|
|
117
|
+
* @param {string[] | string | null | undefined} values
|
|
118
|
+
* @returns {string[] | null}
|
|
119
|
+
*/
|
|
101
120
|
export function parseOrigins(values) {
|
|
102
121
|
if (values == null) return null
|
|
103
122
|
const list = (Array.isArray(values) ? values : [values]).flatMap((value) => String(value).split(/[\s,]+/)).filter(Boolean)
|
|
@@ -111,6 +130,7 @@ export function parseOrigins(values) {
|
|
|
111
130
|
* it — and names who may frame the page; without it any site may.
|
|
112
131
|
*/
|
|
113
132
|
/**
|
|
133
|
+
* @internal
|
|
114
134
|
* @param {object} [options]
|
|
115
135
|
* @param {string[] | null} [options.packages]
|
|
116
136
|
* @param {string[] | null} [options.ancestors]
|
|
@@ -134,7 +154,11 @@ export function contentSecurityPolicy({ packages = null, ancestors = null, meta
|
|
|
134
154
|
return directives.join('; ')
|
|
135
155
|
}
|
|
136
156
|
|
|
137
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Header rules, one set for every host format: the policy everywhere, and the caching that matters.
|
|
159
|
+
* @param {string} csp
|
|
160
|
+
* @returns {{ path: string, headers: Record<string, string> }[]}
|
|
161
|
+
*/
|
|
138
162
|
function headerRules(csp) {
|
|
139
163
|
const revalidate = 'public, max-age=3600, stale-while-revalidate=86400'
|
|
140
164
|
// Asked again on every load, a 304 when nothing changed. The runtime's files keep their names
|
|
@@ -161,13 +185,20 @@ function headerRules(csp) {
|
|
|
161
185
|
]
|
|
162
186
|
}
|
|
163
187
|
|
|
164
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Netlify's and Cloudflare Pages' `_headers`.
|
|
190
|
+
* @param {ReturnType<typeof headerRules>} rules
|
|
191
|
+
*/
|
|
165
192
|
function netlifyHeaders(rules) {
|
|
166
193
|
return rules.map(({ path, headers }) => `${path}\n${Object.entries(headers).map(([key, value]) => ` ${key}: ${value}`).join('\n')}`).join('\n') + '\n'
|
|
167
194
|
}
|
|
168
195
|
|
|
169
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Vercel's `vercel.json`, for deploying the folder as a project of its own.
|
|
198
|
+
* @param {ReturnType<typeof headerRules>} rules
|
|
199
|
+
*/
|
|
170
200
|
function vercelConfig(rules) {
|
|
201
|
+
/** @param {string} path */
|
|
171
202
|
const source = (path) => (path === '/*' ? '/(.*)' : path.replace(/\*$/, '(.*)'))
|
|
172
203
|
return JSON.stringify(
|
|
173
204
|
{ headers: rules.map(({ path, headers }) => ({ source: source(path), headers: Object.entries(headers).map(([key, value]) => ({ key, value })) })) },
|
|
@@ -184,13 +215,19 @@ function vercelConfig(rules) {
|
|
|
184
215
|
* in every format, which is safe because the policy carries only single quotes.
|
|
185
216
|
*/
|
|
186
217
|
|
|
187
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* The rule for every path, and the ones for a single path or prefix.
|
|
220
|
+
* @param {ReturnType<typeof headerRules>} rules
|
|
221
|
+
*/
|
|
188
222
|
function splitRules(rules) {
|
|
189
|
-
const all = rules.find((rule) => rule.path === '/*')
|
|
223
|
+
const all = /** @type {ReturnType<typeof headerRules>[number]} */ (rules.find((rule) => rule.path === '/*'))
|
|
190
224
|
return { all, specific: rules.filter((rule) => rule !== all) }
|
|
191
225
|
}
|
|
192
226
|
|
|
193
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Caddy's `Caddyfile`: a site block to run as it is, or to paste into one.
|
|
229
|
+
* @param {ReturnType<typeof headerRules>} rules
|
|
230
|
+
*/
|
|
194
231
|
function caddyfile(rules) {
|
|
195
232
|
const { all, specific } = splitRules(rules)
|
|
196
233
|
const lines = [
|
|
@@ -210,10 +247,13 @@ function caddyfile(rules) {
|
|
|
210
247
|
* nginx: a file to `include` inside the `server` block that serves the folder. nginx's
|
|
211
248
|
* `add_header` in a `location` replaces every `add_header` inherited from the server level
|
|
212
249
|
* rather than adding to it, so each location repeats the headers that apply everywhere.
|
|
250
|
+
* @param {ReturnType<typeof headerRules>} rules
|
|
213
251
|
*/
|
|
214
252
|
function nginxConf(rules) {
|
|
215
253
|
const { all, specific } = splitRules(rules)
|
|
254
|
+
/** @param {[string, string]} entry */
|
|
216
255
|
const header = ([key, value]) => `add_header ${key} "${value}" always;`
|
|
256
|
+
/** @param {string} path */
|
|
217
257
|
const matcher = (path) => (path.endsWith('/*') ? `^~ ${path.slice(0, -1)}` : `= ${path}`)
|
|
218
258
|
const lines = [
|
|
219
259
|
'# Written by h5p-embed. Include this file inside the server block that serves the folder:',
|
|
@@ -229,10 +269,15 @@ function nginxConf(rules) {
|
|
|
229
269
|
return lines.join('\n') + '\n'
|
|
230
270
|
}
|
|
231
271
|
|
|
232
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* Apache's `.htaccess`, for a folder under a vhost that allows it.
|
|
274
|
+
* @param {ReturnType<typeof headerRules>} rules
|
|
275
|
+
*/
|
|
233
276
|
function htaccess(rules) {
|
|
234
277
|
const { all, specific } = splitRules(rules)
|
|
278
|
+
/** @param {[string, string]} entry */
|
|
235
279
|
const header = ([key, value]) => `Header always set ${key} "${value}"`
|
|
280
|
+
/** @param {ReturnType<typeof headerRules>[number]} rule */
|
|
236
281
|
const block = ({ path, headers }) => {
|
|
237
282
|
const body = Object.entries(headers).map((entry) => `\t${header(entry)}`).join('\n')
|
|
238
283
|
// <Files> matches a name in any directory; a prefix needs the request URI, with no anchor at
|
|
@@ -250,6 +295,7 @@ function htaccess(rules) {
|
|
|
250
295
|
return lines.join('\n') + '\n'
|
|
251
296
|
}
|
|
252
297
|
|
|
298
|
+
/** @param {{ version: string, libraries: boolean }} site */
|
|
253
299
|
function notice({ version, libraries }) {
|
|
254
300
|
return `This folder is the H5P embed page, written by @missing-elements/h5p-embed.
|
|
255
301
|
|
|
@@ -277,7 +323,10 @@ https://github.com/missing-elements/h5p-offline-player/tree/main/packages/embed
|
|
|
277
323
|
`
|
|
278
324
|
}
|
|
279
325
|
|
|
280
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* Whether a directory exists and has anything in it. A path that is a file is an error.
|
|
328
|
+
* @param {string} dir
|
|
329
|
+
*/
|
|
281
330
|
async function occupied(dir) {
|
|
282
331
|
let info
|
|
283
332
|
try {
|
|
@@ -289,6 +338,19 @@ async function occupied(dir) {
|
|
|
289
338
|
return (await readdir(dir)).length > 0
|
|
290
339
|
}
|
|
291
340
|
|
|
341
|
+
/**
|
|
342
|
+
* What `buildSite` wrote, for a summary.
|
|
343
|
+
* @typedef {object} BuiltSite
|
|
344
|
+
* @property {string} out the folder, as an absolute path
|
|
345
|
+
* @property {string} version the player's version, as its `dist/VERSION` says
|
|
346
|
+
* @property {string} csp the policy the headers carry
|
|
347
|
+
* @property {boolean} libraries whether the site carries the library pack's parts
|
|
348
|
+
* @property {string[] | null} packages the origins packages may come from; `null` for any
|
|
349
|
+
* @property {string[] | null} ancestors the origins that may frame the page; `null` for any
|
|
350
|
+
* @property {string | null} defaultLibraries the `libraries` value for addresses that name none
|
|
351
|
+
* @property {number} size the folder's size in bytes
|
|
352
|
+
*/
|
|
353
|
+
|
|
292
354
|
/**
|
|
293
355
|
* Writes the site into `out`. Refuses a folder that already has files in it unless `force`, and
|
|
294
356
|
* even then only replaces the files it writes, so pointing it at the wrong folder costs nothing
|
|
@@ -301,6 +363,7 @@ async function occupied(dir) {
|
|
|
301
363
|
* @param {string[] | string | null} [options.ancestors] the only origins that may frame the page
|
|
302
364
|
* @param {string | null} [options.defaultLibraries] the `libraries` value for addresses that name none
|
|
303
365
|
* @param {boolean} [options.force]
|
|
366
|
+
* @returns {Promise<BuiltSite>}
|
|
304
367
|
*/
|
|
305
368
|
export async function buildSite({ out, libraries = true, packages = null, ancestors = null, defaultLibraries = null, force = false }) {
|
|
306
369
|
if (!out) throw new EmbedError('No output folder given.')
|
|
@@ -366,6 +429,7 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
|
|
|
366
429
|
/**
|
|
367
430
|
* The folder's size in bytes, for the summary. Paths from a recursive `readdir` rather than
|
|
368
431
|
* `Dirent.parentPath`, which Node 20 has only from 20.12.
|
|
432
|
+
* @param {string} dir
|
|
369
433
|
*/
|
|
370
434
|
async function sizeOf(dir) {
|
|
371
435
|
let total = 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@missing-elements/h5p-embed",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Self-hosted H5P embeds: writes the H5P embed page as a static site for a player domain of your own, then any site frames it with an iframe and one script line. No H5P server, no third-party service; optional allowlists for package hosts and embedding sites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,14 @@
|
|
|
20
20
|
"h5p-embed": "./bin/h5p-embed.mjs"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./types/lib/build.d.mts",
|
|
25
|
+
"default": "./lib/build.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./embed.js": {
|
|
28
|
+
"types": "./types/site/embed.d.ts",
|
|
29
|
+
"default": "./site/embed.js"
|
|
30
|
+
},
|
|
25
31
|
"./embed.css": "./site/embed.css",
|
|
26
32
|
"./resizer.js": "./site/resizer.js",
|
|
27
33
|
"./package.json": "./package.json"
|
|
@@ -29,7 +35,8 @@
|
|
|
29
35
|
"files": [
|
|
30
36
|
"bin",
|
|
31
37
|
"lib",
|
|
32
|
-
"site"
|
|
38
|
+
"site",
|
|
39
|
+
"types"
|
|
33
40
|
],
|
|
34
41
|
"keywords": [
|
|
35
42
|
"h5p",
|
|
@@ -44,21 +51,22 @@
|
|
|
44
51
|
],
|
|
45
52
|
"dependencies": {
|
|
46
53
|
"@missing-elements/h5p-libraries": "^1.0.0",
|
|
47
|
-
"@missing-elements/h5p-
|
|
48
|
-
"@missing-elements/h5p-
|
|
54
|
+
"@missing-elements/h5p-runtime": "^1.0.0",
|
|
55
|
+
"@missing-elements/h5p-offline-player": "^1.1.1"
|
|
49
56
|
},
|
|
50
57
|
"devDependencies": {
|
|
51
58
|
"@types/node": "^26.6.5",
|
|
52
|
-
"@zip.js/zip.js": "^2.
|
|
53
|
-
"playwright": "^1.64.
|
|
59
|
+
"@zip.js/zip.js": "^2.24.0",
|
|
60
|
+
"playwright": "^1.64.1",
|
|
54
61
|
"typescript": "^7.0.2",
|
|
55
|
-
"vitest": "^5.0.
|
|
62
|
+
"vitest": "^5.0.5"
|
|
56
63
|
},
|
|
57
64
|
"engines": {
|
|
58
65
|
"node": ">=22"
|
|
59
66
|
},
|
|
60
67
|
"scripts": {
|
|
61
68
|
"test": "pnpm --filter @missing-elements/h5p-offline-player build && vitest run",
|
|
62
|
-
"typecheck": "tsc -p tsconfig.json"
|
|
69
|
+
"typecheck": "pnpm types && tsc -p tsconfig.json",
|
|
70
|
+
"types": "node -e \"require('node:fs').rmSync('types', { recursive: true, force: true })\" && tsc -p tsconfig.types.json"
|
|
63
71
|
}
|
|
64
72
|
}
|
package/site/embed.js
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* package shows under its preview.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @import { H5PPlayerElement, PlayerErrorDetail, PlayerStateChangeDetail, ReadyDetail } from '@missing-elements/h5p-offline-player'
|
|
23
|
+
*/
|
|
24
|
+
|
|
21
25
|
/** Where the element's `libraries="pack"` fetches the library pack from, for a site without its own copy. */
|
|
22
26
|
const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
|
|
23
27
|
|
|
@@ -25,7 +29,7 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
|
|
|
25
29
|
* @param {object} [options]
|
|
26
30
|
* @param {string | null} [options.packBase] the folder of this site's copy of the library pack's
|
|
27
31
|
* parts (`parts/` of `@missing-elements/h5p-libraries`), which `libraries=pack` plays from
|
|
28
|
-
* @param {
|
|
32
|
+
* @param {H5PPlayerElement['pack']} [options.pack] the same, as the `pack` export a bundler
|
|
29
33
|
* resolves; it wins over `packBase`. Without either, `pack` is the element's copy on jsDelivr
|
|
30
34
|
* @param {string | null} [options.librariesPack] the URL of a whole `libraries.h5p` on this site,
|
|
31
35
|
* for a page written before the pack was split: played as any bundle, with no refresh
|
|
@@ -35,7 +39,7 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
|
|
|
35
39
|
* @param {string | null} [options.defaultLibraries] the `libraries` value for an address that has
|
|
36
40
|
* none — `pack` or URLs, as the parameter — so a snippet without `&libraries=` still plays
|
|
37
41
|
* an export that carries no libraries; `&libraries=none` turns it off for one address
|
|
38
|
-
* @param {
|
|
42
|
+
* @param {H5PPlayerElement['runtime']} [options.runtime] the `runtime` export of `@missing-elements/h5p-runtime`,
|
|
39
43
|
* for a page that bundles the element; without it the element looks for `frame-assets/` beside itself
|
|
40
44
|
* @param {boolean} [options.askInOwnFrame] whether a package from another origin plays `untrusted`,
|
|
41
45
|
* with a click to play it with its own code when it needs that, when this page is framed by a
|
|
@@ -44,19 +48,24 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
|
|
|
44
48
|
*/
|
|
45
49
|
export function startEmbed({ packBase = null, pack = null, librariesPack = null, packages = null, defaultLibraries = null, runtime = null, askInOwnFrame = true } = {}) {
|
|
46
50
|
const params = new URLSearchParams(location.search)
|
|
47
|
-
const player = document.querySelector('h5p-player')
|
|
51
|
+
const player = /** @type {H5PPlayerElement} */ (document.querySelector('h5p-player'))
|
|
48
52
|
// Before `src`: the element resolves the runtime's files, and reads where the pack is, when a
|
|
49
53
|
// package is set.
|
|
50
54
|
if (runtime) player.runtime = runtime
|
|
51
55
|
if (pack) player.pack = pack
|
|
52
56
|
else if (packBase) player.setAttribute('pack-base', packBase)
|
|
53
57
|
const ownPack = Boolean(pack || packBase)
|
|
54
|
-
const notice = document.querySelector('#notice')
|
|
55
|
-
const loader = document.querySelector('#loader')
|
|
58
|
+
const notice = /** @type {HTMLElement} */ (document.querySelector('#notice'))
|
|
59
|
+
const loader = /** @type {HTMLElement} */ (document.querySelector('#loader'))
|
|
56
60
|
const framed = window.parent !== window
|
|
57
61
|
|
|
58
62
|
/* ---------------------------------------------------------------- notices */
|
|
59
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} text
|
|
66
|
+
* @param {string} [kind]
|
|
67
|
+
* @param {{ href: string, text: string } | null} [link]
|
|
68
|
+
*/
|
|
60
69
|
const say = (text, kind = '', link = null) => {
|
|
61
70
|
notice.replaceChildren()
|
|
62
71
|
if (text) {
|
|
@@ -75,6 +84,10 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
75
84
|
requestAnimationFrame(announce)
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} text
|
|
89
|
+
* @param {string} [code]
|
|
90
|
+
*/
|
|
78
91
|
const refuse = (text, code = 'refused') => {
|
|
79
92
|
loader.hidden = true
|
|
80
93
|
say(text, 'error')
|
|
@@ -87,6 +100,8 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
87
100
|
* A message to the page that frames this one: the height in the shape h5p-resizer.js expects,
|
|
88
101
|
* and `report`, `error` and `xapi` in this page's own. To any parent unless a target is named,
|
|
89
102
|
* since nothing in the height is secret; the xAPI relay names the origin that opted in.
|
|
103
|
+
* @param {object} message
|
|
104
|
+
* @param {string} [target]
|
|
90
105
|
*/
|
|
91
106
|
const post = (message, target = '*') => {
|
|
92
107
|
if (framed) window.parent.postMessage(message, target)
|
|
@@ -139,7 +154,7 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
139
154
|
let startedAt = 0
|
|
140
155
|
|
|
141
156
|
player.addEventListener('ready', (event) => {
|
|
142
|
-
const { source, metadata, libraryBundle } = event.detail
|
|
157
|
+
const { source, metadata, libraryBundle } = /** @type {CustomEvent<ReadyDetail>} */ (event).detail
|
|
143
158
|
post({
|
|
144
159
|
context: 'h5p-offline-player',
|
|
145
160
|
action: 'report',
|
|
@@ -158,7 +173,10 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
158
173
|
|
|
159
174
|
/* ---------------------------------------------------------------- xAPI, relayed on request */
|
|
160
175
|
|
|
161
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* An origin, or nothing: the parameter has to be exactly what `event.origin` will read.
|
|
178
|
+
* @param {string | null} value
|
|
179
|
+
*/
|
|
162
180
|
const originOf = (value) => {
|
|
163
181
|
if (!value) return null
|
|
164
182
|
try {
|
|
@@ -173,18 +191,22 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
173
191
|
if (relayTo && framed) {
|
|
174
192
|
for (const type of ['xapi', 'finished']) {
|
|
175
193
|
player.addEventListener(type, (event) => {
|
|
176
|
-
post({ context: 'h5p-offline-player', action: type,
|
|
194
|
+
post({ context: 'h5p-offline-player', action: type, .../** @type {CustomEvent<object>} */ (event).detail }, relayTo)
|
|
177
195
|
})
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
/* ---------------------------------------------------------------- errors, and Safari */
|
|
182
200
|
|
|
183
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Asks whether to play an untrusted package with its own code; set when one is played so.
|
|
203
|
+
* @type {(() => void) | null}
|
|
204
|
+
*/
|
|
184
205
|
let askToTrust = null
|
|
185
206
|
|
|
186
207
|
player.addEventListener('error', (event) => {
|
|
187
|
-
|
|
208
|
+
// The element's own `error`, which the DOM's types take for a script's `ErrorEvent`.
|
|
209
|
+
const { code, message, missingLibraries } = /** @type {CustomEvent<PlayerErrorDetail>} */ (/** @type {unknown} */ (event)).detail
|
|
188
210
|
// Played untrusted, it cannot do without its own copy of a library: a click decides.
|
|
189
211
|
if (missingLibraries?.carried && player.untrusted && askToTrust) {
|
|
190
212
|
askToTrust()
|
|
@@ -211,7 +233,7 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
211
233
|
})
|
|
212
234
|
|
|
213
235
|
player.addEventListener('statechange', (event) => {
|
|
214
|
-
const { state } = event.detail
|
|
236
|
+
const { state } = /** @type {CustomEvent<PlayerStateChangeDetail>} */ (event).detail
|
|
215
237
|
if (state !== 'error') say('')
|
|
216
238
|
// Shown from the HTML on, until the content is up or the load has failed.
|
|
217
239
|
loader.hidden = state === 'ready' || state === 'error' || state === 'idle'
|
|
@@ -220,7 +242,10 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
220
242
|
|
|
221
243
|
/* ---------------------------------------------------------------- which hosts */
|
|
222
244
|
|
|
223
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* The origin of a URL as this page resolves it, or `null` for what is not one.
|
|
247
|
+
* @param {string} value
|
|
248
|
+
*/
|
|
224
249
|
const urlOrigin = (value) => {
|
|
225
250
|
try {
|
|
226
251
|
const url = new URL(value, location.href)
|
|
@@ -232,15 +257,21 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
const allowed = packages ? new Set(packages) : null
|
|
235
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* Whether this player was told it may fetch from `origin`. Always true without a list.
|
|
262
|
+
* @param {string} origin
|
|
263
|
+
*/
|
|
236
264
|
const permitted = (origin) => origin === location.origin || !allowed || allowed.has(origin)
|
|
237
265
|
|
|
238
266
|
/**
|
|
239
267
|
* The `libraries` value with `pack` resolved to this site's copy, or an error to show. With a
|
|
240
268
|
* list of hosts, every bundle's origin has to be on it, jsDelivr's included when the pack comes
|
|
241
269
|
* from there; the CSP that `h5p-embed` wrote says the same, this only says it in words.
|
|
270
|
+
* @param {string} value
|
|
271
|
+
* @returns {{ value?: string, error?: string }}
|
|
242
272
|
*/
|
|
243
273
|
const librarySources = (value) => {
|
|
274
|
+
/** @type {string[]} */
|
|
244
275
|
const sources = []
|
|
245
276
|
if (value === 'none') return { value: '' }
|
|
246
277
|
for (const token of value.split(/\s+/).filter(Boolean)) {
|
|
@@ -266,7 +297,8 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
266
297
|
* that one away, `&refresh-libraries=off` plays a package's own libraries even where the pack
|
|
267
298
|
* has newer ones, `&activity-id=` names the statements' object and `&custom-css=` restyles the
|
|
268
299
|
* content to the embedding site's taste. `&lang=` is the embedding page's language, which this
|
|
269
|
-
* page cannot read across origins: H5P's own buttons and dialogs follow it
|
|
300
|
+
* page cannot read across origins: H5P's own buttons and dialogs follow it for a package that
|
|
301
|
+
* does not name its own language, and follow the content's where it does. Not `custom-js`,
|
|
270
302
|
* `embed-code` or `user`: a script is a capability on this origin that a link should not hand
|
|
271
303
|
* out, the embed is the embed, and a learner's name has no place in a URL.
|
|
272
304
|
*/
|
|
@@ -286,6 +318,10 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
|
|
|
286
318
|
if (lang && /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(lang)) player.setAttribute('lang', lang)
|
|
287
319
|
}
|
|
288
320
|
|
|
321
|
+
/**
|
|
322
|
+
* @param {string} value
|
|
323
|
+
* @param {string | undefined} libraries
|
|
324
|
+
*/
|
|
289
325
|
const start = (value, libraries) => {
|
|
290
326
|
if (libraries) player.setAttribute('libraries', libraries)
|
|
291
327
|
if (params.get('preload') === 'auto') player.setAttribute('preload', 'auto')
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes the embed page as a static site: one folder, deployable to any static host, that a site
|
|
3
|
+
* frames from a player domain of its own. Everything comes from installed packages — the element
|
|
4
|
+
* and its workers from the player's `dist/`, the H5P runtime from its own package as
|
|
5
|
+
* `frame-assets/`, the library pack's parts from `@missing-elements/h5p-libraries` as
|
|
6
|
+
* `libraries/` — so the versions are the ones this package was published with, and nothing is
|
|
7
|
+
* fetched.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EmbedError extends Error {
|
|
10
|
+
}
|
|
11
|
+
export type BuiltSite = {
|
|
12
|
+
/**
|
|
13
|
+
* the folder, as an absolute path
|
|
14
|
+
*/
|
|
15
|
+
out: string;
|
|
16
|
+
/**
|
|
17
|
+
* the player's version, as its `dist/VERSION` says
|
|
18
|
+
*/
|
|
19
|
+
version: string;
|
|
20
|
+
/**
|
|
21
|
+
* the policy the headers carry
|
|
22
|
+
*/
|
|
23
|
+
csp: string;
|
|
24
|
+
/**
|
|
25
|
+
* whether the site carries the library pack's parts
|
|
26
|
+
*/
|
|
27
|
+
libraries: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* the origins packages may come from; `null` for any
|
|
30
|
+
*/
|
|
31
|
+
packages: string[] | null;
|
|
32
|
+
/**
|
|
33
|
+
* the origins that may frame the page; `null` for any
|
|
34
|
+
*/
|
|
35
|
+
ancestors: string[] | null;
|
|
36
|
+
/**
|
|
37
|
+
* the `libraries` value for addresses that name none
|
|
38
|
+
*/
|
|
39
|
+
defaultLibraries: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* the folder's size in bytes
|
|
42
|
+
*/
|
|
43
|
+
size: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* What `buildSite` wrote, for a summary.
|
|
47
|
+
* @typedef {object} BuiltSite
|
|
48
|
+
* @property {string} out the folder, as an absolute path
|
|
49
|
+
* @property {string} version the player's version, as its `dist/VERSION` says
|
|
50
|
+
* @property {string} csp the policy the headers carry
|
|
51
|
+
* @property {boolean} libraries whether the site carries the library pack's parts
|
|
52
|
+
* @property {string[] | null} packages the origins packages may come from; `null` for any
|
|
53
|
+
* @property {string[] | null} ancestors the origins that may frame the page; `null` for any
|
|
54
|
+
* @property {string | null} defaultLibraries the `libraries` value for addresses that name none
|
|
55
|
+
* @property {number} size the folder's size in bytes
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Writes the site into `out`. Refuses a folder that already has files in it unless `force`, and
|
|
59
|
+
* even then only replaces the files it writes, so pointing it at the wrong folder costs nothing
|
|
60
|
+
* that was not its own.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} options
|
|
63
|
+
* @param {string} options.out
|
|
64
|
+
* @param {boolean} [options.libraries] include the library pack's parts, for `libraries=pack` (default true)
|
|
65
|
+
* @param {string[] | string | null} [options.packages] the only origins packages may come from
|
|
66
|
+
* @param {string[] | string | null} [options.ancestors] the only origins that may frame the page
|
|
67
|
+
* @param {string | null} [options.defaultLibraries] the `libraries` value for addresses that name none
|
|
68
|
+
* @param {boolean} [options.force]
|
|
69
|
+
* @returns {Promise<BuiltSite>}
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildSite({ out, libraries, packages, ancestors, defaultLibraries, force }: {
|
|
72
|
+
out: string;
|
|
73
|
+
libraries?: boolean;
|
|
74
|
+
packages?: string[] | string | null;
|
|
75
|
+
ancestors?: string[] | string | null;
|
|
76
|
+
defaultLibraries?: string | null;
|
|
77
|
+
force?: boolean;
|
|
78
|
+
}): Promise<BuiltSite>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*! @missing-elements/h5p-embed. MIT. */
|
|
2
|
+
/**
|
|
3
|
+
* The embed page: the element alone, driven by the query string, for a site that puts a player
|
|
4
|
+
* domain of its own in an iframe. `startEmbed()` runs it; the page's `main.js` calls it with what
|
|
5
|
+
* `h5p-embed` was told when it wrote the site, and the demo's `/embed` with the demo's copy of the
|
|
6
|
+
* library pack.
|
|
7
|
+
*
|
|
8
|
+
* ?src=<package url>[&libraries=pack|<url> …|none][&refresh-libraries=off][&preload=auto]
|
|
9
|
+
* [&xapi=<parent origin>][&frame][©right][&export][&icon][&reporting][&fullscreen=off]
|
|
10
|
+
* [&activity-id=<IRI>][&custom-css=<stylesheet url>][&lang=<language tag>]
|
|
11
|
+
*
|
|
12
|
+
* Upward it speaks H5P's own resizer protocol — the `hello` / `resize` exchange that h5p.org's
|
|
13
|
+
* embed code and its `h5p-resizer.js` use — so a page that already resizes h5p.org iframes
|
|
14
|
+
* resizes this one without a change, and any other page gets `resizer.js` from this origin. xAPI
|
|
15
|
+
* statements are relayed to the parent only when `xapi=` names the parent's origin, and they are
|
|
16
|
+
* posted to that origin only. Once the content is up the page posts one `report` upward, and a
|
|
17
|
+
* load that fails posts its `error` (see `report` below): what an embedding page that checks a
|
|
18
|
+
* package shows under its preview.
|
|
19
|
+
*/
|
|
20
|
+
import type { H5PPlayerElement } from '@missing-elements/h5p-offline-player';
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {string | null} [options.packBase] the folder of this site's copy of the library pack's
|
|
24
|
+
* parts (`parts/` of `@missing-elements/h5p-libraries`), which `libraries=pack` plays from
|
|
25
|
+
* @param {H5PPlayerElement['pack']} [options.pack] the same, as the `pack` export a bundler
|
|
26
|
+
* resolves; it wins over `packBase`. Without either, `pack` is the element's copy on jsDelivr
|
|
27
|
+
* @param {string | null} [options.librariesPack] the URL of a whole `libraries.h5p` on this site,
|
|
28
|
+
* for a page written before the pack was split: played as any bundle, with no refresh
|
|
29
|
+
* @param {string[] | null} [options.packages] the origins packages and library bundles may come
|
|
30
|
+
* from, besides this page's own; `null` plays any, one from another origin `untrusted` when the
|
|
31
|
+
* storage is this origin's
|
|
32
|
+
* @param {string | null} [options.defaultLibraries] the `libraries` value for an address that has
|
|
33
|
+
* none — `pack` or URLs, as the parameter — so a snippet without `&libraries=` still plays
|
|
34
|
+
* an export that carries no libraries; `&libraries=none` turns it off for one address
|
|
35
|
+
* @param {H5PPlayerElement['runtime']} [options.runtime] the `runtime` export of `@missing-elements/h5p-runtime`,
|
|
36
|
+
* for a page that bundles the element; without it the element looks for `frame-assets/` beside itself
|
|
37
|
+
* @param {boolean} [options.askInOwnFrame] whether a package from another origin plays `untrusted`,
|
|
38
|
+
* with a click to play it with its own code when it needs that, when this page is framed by a
|
|
39
|
+
* page of its own origin, whose storage it shares. A site whose own preview frames the page for
|
|
40
|
+
* a package the visitor just chose passes `false`
|
|
41
|
+
*/
|
|
42
|
+
export declare function startEmbed({ packBase, pack, librariesPack, packages, defaultLibraries, runtime, askInOwnFrame }?: {
|
|
43
|
+
packBase?: string | null;
|
|
44
|
+
pack?: H5PPlayerElement['pack'];
|
|
45
|
+
librariesPack?: string | null;
|
|
46
|
+
packages?: string[] | null;
|
|
47
|
+
defaultLibraries?: string | null;
|
|
48
|
+
runtime?: H5PPlayerElement['runtime'];
|
|
49
|
+
askInOwnFrame?: boolean;
|
|
50
|
+
}): void;
|