diff --git a/package.json b/package.json index 5f56522..ac8e401 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "tsx watch src/server.ts", "build": "tsc && vite build", "start": "node dist/server.js", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "npm run build && node --test tests/*.test.mjs" }, "dependencies": { "archiver": "^7.0.1", diff --git a/src/slug.ts b/src/slug.ts index 6c9446e..05f4b0a 100644 --- a/src/slug.ts +++ b/src/slug.ts @@ -5,12 +5,13 @@ export function slugify(input: string, fallback = "chatgpt-share"): string { .normalize("NFKD") .replace(STOP_CHARS, "") .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") + .replace(/[^\p{L}\p{N}]+/gu, "-") .replace(/^-+|-+$/g, "") - .replace(/-{2,}/g, "-") - .slice(0, 80); + .replace(/-{2,}/g, "-"); - return base || fallback; + const truncated = Array.from(base).slice(0, 80).join("").replace(/-+$/g, ""); + + return truncated || fallback; } export function displayTitleFromSlug(slug: string): string { diff --git a/tests/slug.test.mjs b/tests/slug.test.mjs new file mode 100644 index 0000000..1d4566f --- /dev/null +++ b/tests/slug.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { slugify } from "../dist/slug.js"; + +test("preserves Unicode letters and numbers", () => { + assert.equal(slugify("多喝水减肥效果"), "多喝水减肥效果"); + assert.equal(slugify("减肥 Plan 2026"), "减肥-plan-2026"); +}); + +test("normalizes punctuation and Latin accents", () => { + assert.equal(slugify(" Café — healthy choices! "), "cafe-healthy-choices"); +}); + +test("uses the fallback when no letters or numbers remain", () => { + assert.equal(slugify("🎉 !!!"), "chatgpt-share"); + assert.equal(slugify("🎉", "capture"), "capture"); +}); + +test("limits slugs to 80 Unicode code points without a trailing separator", () => { + assert.equal(slugify(`${"界".repeat(79)} hello`), "界".repeat(79)); + assert.equal(Array.from(slugify("界".repeat(100))).length, 80); +});