diff --git a/e2e/build.spec.ts b/e2e/build.spec.ts
index 16f4ba6..4248339 100644
--- a/e2e/build.spec.ts
+++ b/e2e/build.spec.ts
@@ -105,15 +105,39 @@ test('a link carrying the source builds it at once, deflated or not', async ({ p
await expectPlays(page)
await expect(page.locator('#source')).toHaveValue(JSON.stringify(RIVERS))
- const plain = Buffer.from(JSON.stringify({ ...RIVERS, title: 'Seas' })).toString('base64url')
+ // Plain base64url, with umlauts and ß: the bytes are UTF-8, and the file name keeps the letters.
+ const plain = Buffer.from(JSON.stringify({ ...RIVERS, language: 'de', title: 'Größte Flüsse' })).toString('base64url')
await page.goto(`/build#source=${plain}`)
- await expect(page.locator('#status')).toContainText('Built “Seas”')
+ await expect(page.locator('#status')).toContainText('Built “Größte Flüsse”')
await expectPlays(page)
+ await expect(page.locator('#download')).toHaveAttribute('download', 'größte-flüsse.h5p')
+
+ // A title in another script keeps its letters too, rather than becoming activity.h5p.
+ await page.goto(`/build#source=${Buffer.from(JSON.stringify({ ...RIVERS, language: 'uk', title: 'Річки України' })).toString('base64url')}`)
+ await expectPlays(page)
+ await expect(page.locator('#download')).toHaveAttribute('download', 'річки-україни.h5p')
expect(elsewhere, 'nothing was fetched from another site').toEqual([])
expect(problems, problems.join('\n')).toEqual([])
})
+test('a Question Set shows its cover title as written, tags and entities nowhere', async ({ page, baseURL }) => {
+ const { problems } = watch(page, baseURL)
+ // h5p-author 0.5.0 wrapped this plain field in
, which the set draws as text (h5p-offline-player#164).
+ const title = "Germany's Surrender: die größten Ereignisse"
+ const quiz = {
+ type: 'H5P.QuestionSet', title: 'Surrender', language: 'en', license: 'CC BY', authors: [{ name: 'Ann Author', role: 'Author' }],
+ params: {
+ introPage: { showIntroPage: true, title, introduction: 'Five questions.' },
+ questions: [{ library: 'H5P.TrueFalse', params: { question: 'The war in Europe ended in 1945.', correct: 'true' } }]
+ }
+ }
+ await page.goto(`/build#source=${Buffer.from(JSON.stringify(quiz)).toString('base64url')}`)
+ await expectPlays(page)
+ await expect(page.frameLocator('h5p-player iframe').first().locator('h2').first()).toHaveText(title)
+ expect(problems, problems.join('\n')).toEqual([])
+})
+
test('a source that names a file waits for it, then builds with it', async ({ page, baseURL }) => {
const { problems } = watch(page, baseURL)
const delta = {
diff --git a/package.json b/package.json
index 27276d0..be3eec5 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
"vite": "^8.3.5"
},
"dependencies": {
- "@missing-elements/h5p-author": "^0.5.0",
+ "@missing-elements/h5p-author": "^0.5.1",
"@missing-elements/h5p-embed": "^1.7.0",
"@missing-elements/h5p-libraries": "^1.1.0",
"@missing-elements/h5p-offline-player": "^1.5.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e055e19..51c6f19 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@missing-elements/h5p-author':
- specifier: ^0.5.0
- version: 0.5.0(@missing-elements/h5p-runtime@1.0.1)
+ specifier: ^0.5.1
+ version: 0.5.1(@missing-elements/h5p-runtime@1.0.1)
'@missing-elements/h5p-embed':
specifier: ^1.7.0
version: 1.7.0
@@ -39,8 +39,8 @@ importers:
packages:
- '@missing-elements/h5p-author@0.5.0':
- resolution: {integrity: sha512-5QiB0VBk8Hx2gvy5xL+7eOGCTyBB+t6CPWNR075xXjrj4ExBQ94OfbJv+ojkPeyKFb+wkQ/8Obh72cezzFqCHw==}
+ '@missing-elements/h5p-author@0.5.1':
+ resolution: {integrity: sha512-QLm2xOF/arfwFPvWw6pwIKYz6oeFQQGRVuLHl7nRAjHECr7mz3IePQx4VeaKXrcK3bLJWSL7aNSUkFPK+GJgLg==}
engines: {node: '>=22'}
hasBin: true
@@ -120,42 +120,36 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.2.16':
resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.2.16':
resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.16':
resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.16':
resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.2.16':
resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [musl]
'@rolldown/binding-openharmony-arm64@1.2.16':
resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==}
@@ -358,28 +352,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
lightningcss-linux-arm64-musl@1.33.0:
resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
lightningcss-linux-x64-gnu@1.33.0:
resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
lightningcss-linux-x64-musl@1.33.0:
resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
lightningcss-win32-arm64-msvc@1.33.0:
resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
@@ -489,7 +479,7 @@ packages:
snapshots:
- '@missing-elements/h5p-author@0.5.0(@missing-elements/h5p-runtime@1.0.1)':
+ '@missing-elements/h5p-author@0.5.1(@missing-elements/h5p-runtime@1.0.1)':
dependencies:
'@missing-elements/h5p-libraries': 1.1.0
'@missing-elements/h5p-normalize': 1.3.0
diff --git a/src/build-source.ts b/src/build-source.ts
index 6f31176..7dbe51c 100644
--- a/src/build-source.ts
+++ b/src/build-source.ts
@@ -112,17 +112,20 @@ export function forTheChat(problems: Problem[]): string {
].join('\n')
}
-/** A file name for the package, from its title. */
+/**
+ * A file name for the package, from its title: its letters, marks and digits in any script, lower
+ * case, joined by hyphens, at most 60 of them. "Die größten Ereignisse" is
+ * `die-größten-ereignisse.h5p`, and a Cyrillic or Greek title keeps its letters, where a slug of
+ * ASCII alone turned ß into a hyphen and such a title into `activity.h5p`.
+ */
export function packageName(title: unknown): string {
const words = String(title ?? '')
- .normalize('NFKD')
- .replace(/[̀-ͯ]/g, '')
+ .normalize('NFC')
.toLowerCase()
- .replace(/[^a-z0-9]+/g, '-')
+ .replace(/[^\p{L}\p{M}\p{N}]+/gu, '-')
.replace(/^-+|-+$/g, '')
- .slice(0, 60)
- .replace(/-+$/, '')
- return `${words || 'activity'}.h5p`
+ const short = Array.from(words).slice(0, 60).join('').replace(/-+$/, '')
+ return `${short || 'activity'}.h5p`
}
/** A size in kilobytes below a megabyte, in megabytes above. */