diff --git a/apps/studio/server/demoPublish.ts b/apps/studio/server/demoPublish.ts index c25a160..1769453 100644 --- a/apps/studio/server/demoPublish.ts +++ b/apps/studio/server/demoPublish.ts @@ -19,16 +19,56 @@ import { } from "@sourcedraft/publishers"; import type { PublishRequestBody, PublishResponse } from "./publish.js"; +function isAllowedBranchChar(char: string): boolean { + const code = char.charCodeAt(0); + return ( + (code >= 97 && code <= 122) || + (code >= 48 && code <= 57) || + char === "." || + char === "_" || + char === "/" || + char === "-" + ); +} + +function isTrimChar(char: string | undefined): boolean { + return char === "-" || char === "." || char === "/"; +} + +function sanitizeBranchSegment(value: string): string { + const chars: string[] = []; + + for (const char of value.trim().toLowerCase()) { + if (isAllowedBranchChar(char)) { + if (char === "-") { + if (chars[chars.length - 1] !== "-") { + chars.push(char); + } + } else { + chars.push(char); + } + continue; + } + + if (chars.length > 0 && chars[chars.length - 1] !== "-") { + chars.push("-"); + } + } + + while (isTrimChar(chars[0])) { + chars.shift(); + } + + while (chars.length > 0 && isTrimChar(chars[chars.length - 1])) { + chars.pop(); + } + + return chars.length > 0 ? chars.join("") : "post"; +} + function demoPrBranch(slug: string, prefix: string): string { const safePrefix = prefix.endsWith("/") ? prefix : `${prefix}/`; - const segment = slug - .trim() - .toLowerCase() - .replace(/[^a-z0-9._/-]+/gu, "-") - .replace(/-+/gu, "-") - .replace(/^[-./]+|[-./]+$/gu, "") || "post"; - - return `${safePrefix}${segment}`; + return `${safePrefix}${sanitizeBranchSegment(slug)}`; } function renderArticle(article: Article, env: Omit): string { diff --git a/apps/studio/src/lib/prBranch.ts b/apps/studio/src/lib/prBranch.ts index f74144f..d5d8256 100644 --- a/apps/studio/src/lib/prBranch.ts +++ b/apps/studio/src/lib/prBranch.ts @@ -1,12 +1,48 @@ +function isAllowedBranchChar(char: string): boolean { + const code = char.charCodeAt(0); + return ( + (code >= 97 && code <= 122) || + (code >= 48 && code <= 57) || + char === "." || + char === "_" || + char === "/" || + char === "-" + ); +} + +function isTrimChar(char: string | undefined): boolean { + return char === "-" || char === "." || char === "/"; +} + function sanitizeBranchSegment(value: string): string { - const normalized = value - .trim() - .toLowerCase() - .replace(/[^a-z0-9._/-]+/gu, "-") - .replace(/-+/gu, "-") - .replace(/^[-./]+|[-./]+$/gu, ""); - - return normalized.length > 0 ? normalized : "post"; + const chars: string[] = []; + + for (const char of value.trim().toLowerCase()) { + if (isAllowedBranchChar(char)) { + if (char === "-") { + if (chars[chars.length - 1] !== "-") { + chars.push(char); + } + } else { + chars.push(char); + } + continue; + } + + if (chars.length > 0 && chars[chars.length - 1] !== "-") { + chars.push("-"); + } + } + + while (isTrimChar(chars[0])) { + chars.shift(); + } + + while (chars.length > 0 && isTrimChar(chars[chars.length - 1])) { + chars.pop(); + } + + return chars.length > 0 ? chars.join("") : "post"; } export function previewPrBranch(slug: string, prefix: string): string { diff --git a/packages/github-publisher/src/githubBranchNames.ts b/packages/github-publisher/src/githubBranchNames.ts index d5563a9..2d4030d 100644 --- a/packages/github-publisher/src/githubBranchNames.ts +++ b/packages/github-publisher/src/githubBranchNames.ts @@ -1,14 +1,50 @@ const BRANCH_NAME_MAX_LENGTH = 255; +function isAllowedBranchChar(char: string): boolean { + const code = char.charCodeAt(0); + return ( + (code >= 97 && code <= 122) || + (code >= 48 && code <= 57) || + char === "." || + char === "_" || + char === "/" || + char === "-" + ); +} + +function isTrimChar(char: string | undefined): boolean { + return char === "-" || char === "." || char === "/"; +} + export function sanitizeBranchSegment(value: string): string { - const normalized = value - .trim() - .toLowerCase() - .replace(/[^a-z0-9._/-]+/gu, "-") - .replace(/-+/gu, "-") - .replace(/^[-./]+|[-./]+$/gu, ""); - - return normalized.length > 0 ? normalized : "post"; + const chars: string[] = []; + + for (const char of value.trim().toLowerCase()) { + if (isAllowedBranchChar(char)) { + if (char === "-") { + if (chars[chars.length - 1] !== "-") { + chars.push(char); + } + } else { + chars.push(char); + } + continue; + } + + if (chars.length > 0 && chars[chars.length - 1] !== "-") { + chars.push("-"); + } + } + + while (isTrimChar(chars[0])) { + chars.shift(); + } + + while (chars.length > 0 && isTrimChar(chars[chars.length - 1])) { + chars.pop(); + } + + return chars.length > 0 ? chars.join("") : "post"; } export function branchNameFromSlug(