@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 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`. Default: English |
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
- /** Where an installed package's file is, or a message that says which one is missing. */
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
- /** Origins given as a list, or as one string separated by commas or whitespace. */
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
- /** Header rules, one set for every host format: the policy everywhere, and the caching that matters. */
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
- /** Netlify's and Cloudflare Pages' `_headers`. */
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
- /** Vercel's `vercel.json`, for deploying the folder as a project of its own. */
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
- /** The rule for every path, and the ones for a single path or prefix. */
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
- /** Caddy's `Caddyfile`: a site block to run as it is, or to paste into one. */
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
- /** Apache's `.htaccess`, for a folder under a vhost that allows it. */
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
- /** Whether a directory exists and has anything in it. A path that is a file is an error. */
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.1",
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
- ".": "./lib/build.mjs",
24
- "./embed.js": "./site/embed.js",
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-offline-player": "^1.0.1",
48
- "@missing-elements/h5p-runtime": "^1.0.0"
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.23.0",
53
- "playwright": "^1.64.0",
59
+ "@zip.js/zip.js": "^2.24.0",
60
+ "playwright": "^1.64.1",
54
61
  "typescript": "^7.0.2",
55
- "vitest": "^5.0.3"
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 {{ libraries: object[] } | null} [options.pack] the same, as the `pack` export a bundler
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 {object | null} [options.runtime] the `runtime` export of `@missing-elements/h5p-runtime`,
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
- /** An origin, or nothing: the parameter has to be exactly what `event.origin` will read. */
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, ...event.detail }, relayTo)
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
- /** Asks whether to play an untrusted package with its own code; set when one is played so. */
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
- const { code, message, missingLibraries } = event.detail
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
- /** The origin of a URL as this page resolves it, or `null` for what is not one. */
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
- /** Whether this player was told it may fetch from `origin`. Always true without a list. */
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. Not `custom-js`,
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][&copyright][&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;