From 0c7c31b22bfe40f3c8f8e8521e6faa0b1e5dee9a Mon Sep 17 00:00:00 2001 From: Aleksandr Shevchuk Date: Mon, 9 Nov 2026 11:22:31 +0200 Subject: [PATCH] fix: build with author 0.5.1, and keep every script's letters in download names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit author 0.5.0 wrapped Question Set's intro title and results heading in
, which the set draws as text; 0.5.1 stores them plain (missing-elements/h5p-offline-player#164). A test now checks the cover shows its title as written, apostrophe and umlauts included. The download name kept ASCII letters only, so ß became a hyphen and a Cyrillic or Greek title became activity.h5p. It now keeps letters, marks and digits in any script. Closes #12 --- e2e/build.spec.ts | 28 ++++++++++++++++++++++++++-- package.json | 2 +- pnpm-lock.yaml | 20 +++++--------------- src/build-source.ts | 17 ++++++++++------- 4 files changed, 42 insertions(+), 25 deletions(-) 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. */