Compare commits

...

2 Commits

Author SHA1 Message Date
Jörg Lohrer 36dd76a88f spa(task 4 polish): decodeURIComponent crash-safe, edge-case-tests
- decodeURIComponent in try/catch (malformed URI encoding crasht
  den SPA-Boot-Path nicht mehr, returned stattdessen null).
- JSDoc präzisiert: erwartet nur Pfad ohne Query/Fragment.
- Neue Tests: malformed %E0 → null, leerer dtag → null,
  round-trip Legacy → canonical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:17:38 +02:00
Jörg Lohrer 47decd9b70 spa: url-parser für legacy-hugo-urls (tdd) 2026-04-15 15:14:35 +02:00
2 changed files with 85 additions and 0 deletions

24
app/src/lib/url/legacy.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* Erkennt Legacy-Hugo-URLs der Form /YYYY/MM/DD/<dtag>.html oder .../<dtag>.html/
* und gibt den dtag-Teil zurück. Für alle anderen Pfade: null.
*
* Erwartet nur den Pfad ohne Query/Fragment wenn vorhanden vom Aufrufer
* trennen. `decodeURIComponent` wird defensiv gekapselt, damit malformed
* Percent-Encoding die SPA beim Boot nicht crasht.
*/
export function parseLegacyUrl(path: string): string | null {
const match = path.match(/^\/\d{4}\/\d{2}\/\d{2}\/([^/]+?)\.html\/?$/);
if (!match) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return null;
}
}
/**
* Erzeugt die kanonische kurze Post-URL /<dtag>/.
*/
export function canonicalPostPath(dtag: string): string {
return `/${encodeURIComponent(dtag)}/`;
}

View File

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import { parseLegacyUrl, canonicalPostPath } from '$lib/url/legacy';
describe('parseLegacyUrl', () => {
it('extrahiert dtag aus der Hugo-URL-Form mit Trailing-Slash', () => {
expect(parseLegacyUrl('/2025/03/04/dezentrale-oep-oer.html/')).toBe(
'dezentrale-oep-oer',
);
});
it('extrahiert dtag aus der Hugo-URL-Form ohne Trailing-Slash', () => {
expect(parseLegacyUrl('/2024/01/26/offenheit-das-wesentliche.html')).toBe(
'offenheit-das-wesentliche',
);
});
it('returned null für die kanonische kurze Form', () => {
expect(parseLegacyUrl('/dezentrale-oep-oer/')).toBeNull();
});
it('returned null für leeren Pfad', () => {
expect(parseLegacyUrl('/')).toBeNull();
});
it('returned null für andere Strukturen', () => {
expect(parseLegacyUrl('/tag/OER/')).toBeNull();
expect(parseLegacyUrl('/some/random/path/')).toBeNull();
});
it('decodiert percent-encoded dtags', () => {
expect(parseLegacyUrl('/2024/05/12/mit%20leerzeichen.html/')).toBe(
'mit leerzeichen',
);
});
it('gibt null zurück bei malformed percent-encoding (crash-sicher)', () => {
expect(parseLegacyUrl('/2024/01/26/%E0.html/')).toBeNull();
});
it('gibt null zurück für leeren dtag', () => {
expect(parseLegacyUrl('/2024/01/26/.html/')).toBeNull();
});
});
describe('canonicalPostPath', () => {
it('erzeugt /<dtag>/ mit encodeURIComponent', () => {
expect(canonicalPostPath('dezentrale-oep-oer')).toBe('/dezentrale-oep-oer/');
});
it('kodiert Sonderzeichen', () => {
expect(canonicalPostPath('mit leerzeichen')).toBe('/mit%20leerzeichen/');
});
});
describe('round-trip parseLegacyUrl → canonicalPostPath', () => {
it('Legacy-URL wird zur kanonischen kurzen Form', () => {
const dtag = parseLegacyUrl('/2025/03/04/dezentrale-oep-oer.html/');
expect(dtag).not.toBeNull();
expect(canonicalPostPath(dtag!)).toBe('/dezentrale-oep-oer/');
});
});