diff --git a/lib/string.ts b/lib/string.ts index 0891f25..c88498a 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -12,21 +12,25 @@ export function formatDate(date?: string | Date): string { return new Intl.DateTimeFormat("en-US", options).format(date); } -export function safeFileName(inputString: string): string { - let fileName = inputString.toLowerCase(); - fileName = fileName.replace(/ /g, "_"); - fileName = fileName.replace(/[^\w.-]/g, ""); - fileName = fileName.replaceAll(":", ""); - return fileName; +export function safeFileName(input: string): string { + return input + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[\s-]+/g, "_") + .replace(/[^A-Za-z0-9._]+/g, "") + .replace(/_+/g, "_") + // Trim underscores/dots from ends and prevent leading dots + .replace(/^[_\.]+|[_\.]+$/g, "").replace(/^\.+/, "") + .toLowerCase(); } export function toUrlSafeString(input: string): string { return input - .trim() // Remove leading and trailing whitespace - .toLowerCase() // Convert to lowercase - .replace(/[^a-z0-9\s-]/g, "") // Remove non-alphanumeric characters except spaces and hyphens - .replace(/\s+/g, "-") // Replace spaces with hyphens - .replace(/-+/g, "-"); // Remove consecutive hyphens + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^A-Za-z0-9 _-]+/g, "") + .replace(/\s+/g, " ") + .trim(); } export function extractHashTags(inputString: string) { diff --git a/routes/api/articles/create/index.ts b/routes/api/articles/create/index.ts index b97967a..9edf827 100644 --- a/routes/api/articles/create/index.ts +++ b/routes/api/articles/create/index.ts @@ -9,6 +9,7 @@ import { formatDate, isYoutubeLink, safeFileName, + toUrlSafeString, } from "@lib/string.ts"; import { createLogger } from "@lib/log/index.ts"; import { createResource } from "@lib/marka/index.ts"; @@ -86,7 +87,7 @@ async function processCreateArticle( streamResponse.enqueue("writing to disk"); - await createResource(`articles/${title}.md`, newArticle); + await createResource(`articles/${toUrlSafeString(title)}.md`, newArticle); streamResponse.enqueue("id: " + title); } diff --git a/routes/api/movies/[name].ts b/routes/api/movies/[name].ts index 81cb302..71acc67 100644 --- a/routes/api/movies/[name].ts +++ b/routes/api/movies/[name].ts @@ -2,7 +2,12 @@ import { Handlers } from "$fresh/server.ts"; import { json } from "@lib/helpers.ts"; import * as tmdb from "@lib/tmdb.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; -import { formatDate, isString, safeFileName } from "@lib/string.ts"; +import { + formatDate, + isString, + safeFileName, + toUrlSafeString, +} from "@lib/string.ts"; import { AccessDeniedError, BadRequestError } from "@lib/errors.ts"; import { createResource, fetchResource } from "@lib/marka/index.ts"; import { ReviewResource } from "@lib/marka/schema.ts"; @@ -59,9 +64,9 @@ export const handler: Handlers = { keywords, }; - const fileName = `${safeFileName(name)}.md`; + const fileName = toUrlSafeString(name); - await createResource(`movies/${fileName}`, movie); + await createResource(`movies/${fileName}.md`, movie); return json({ name: fileName }); }, diff --git a/routes/api/movies/enhance/[name].ts b/routes/api/movies/enhance/[name].ts index a1da457..0e347c2 100644 --- a/routes/api/movies/enhance/[name].ts +++ b/routes/api/movies/enhance/[name].ts @@ -1,7 +1,12 @@ import { FreshContext, Handlers } from "$fresh/server.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; import * as tmdb from "@lib/tmdb.ts"; -import { formatDate, isString, safeFileName } from "@lib/string.ts"; +import { + formatDate, + isString, + safeFileName, + toUrlSafeString, +} from "@lib/string.ts"; import { json } from "@lib/helpers.ts"; import { AccessDeniedError, @@ -78,7 +83,7 @@ const POST = async ( movie.content.image = finalPath; } - await createResource(`movies/${safeFileName(movie.name)}.md`, movie); + await createResource(`movies/${toUrlSafeString(movie.name)}.md`, movie); createRecommendationResource(movie, movieDetails.overview); diff --git a/routes/api/recipes/create/index.ts b/routes/api/recipes/create/index.ts index b372a15..3241968 100644 --- a/routes/api/recipes/create/index.ts +++ b/routes/api/recipes/create/index.ts @@ -5,7 +5,7 @@ import * as openai from "@lib/openai.ts"; import { createLogger } from "@lib/log/index.ts"; import recipeSchema from "@lib/recipeSchema.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; -import { safeFileName } from "@lib/string.ts"; +import { safeFileName, toUrlSafeString } from "@lib/string.ts"; import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts"; import z from "zod"; import { createResource } from "@lib/marka/index.ts"; @@ -58,7 +58,7 @@ async function processCreateRecipeFromUrl( recipe = res; } - const id = safeFileName(recipe?.name || ""); + const id = toUrlSafeString(recipe?.name || ""); if (!recipe) { streamResponse.enqueue("failed to parse recipe"); diff --git a/routes/api/series/[name].ts b/routes/api/series/[name].ts index f4ac21f..5e22588 100644 --- a/routes/api/series/[name].ts +++ b/routes/api/series/[name].ts @@ -6,6 +6,7 @@ import { formatDate, isString, safeFileName } from "@lib/string.ts"; import { AccessDeniedError, BadRequestError } from "@lib/errors.ts"; import { createResource, fetchResource } from "@lib/marka/index.ts"; import { ReviewResource } from "@lib/marka/schema.ts"; +import { toUrlSafeString } from "@lib/string.ts"; function pickDirector( credits: Awaited>, @@ -71,9 +72,9 @@ export const handler: Handlers = { keywords: keywords, }; - const fileName = `${safeFileName(name)}.md`; + const fileName = toUrlSafeString(name); - await createResource(`series/${fileName}`, series); + await createResource(`series/${fileName}.md`, series); return json({ name: fileName }); },