Introduce createFilenameFromString function

This commit is contained in:
Koen Vlaswinkel
2024-03-11 11:31:43 +01:00
parent 2c35a97b09
commit e8efbbb9fd
5 changed files with 134 additions and 56 deletions

View File

@@ -0,0 +1,43 @@
type FilenameOptions = {
removeDots?: boolean;
};
/**
* This will create a filename from an arbitrary string by removing
* all characters which are not allowed in filenames and making them
* more filesystem-friendly be replacing undesirable characters with
* hyphens. The result will always be lowercase ASCII.
*
* @param str The string to create a filename from
* @param removeDots Whether to remove dots from the filename [default: false]
* @returns The filename
*/
export function createFilenameFromString(
str: string,
{ removeDots }: FilenameOptions = {},
) {
let fileName = str;
// Lowercase everything
fileName = fileName.toLowerCase();
// Replace all spaces, underscores, slashes, and backslashes with hyphens
fileName = fileName.replaceAll(/[\s_/\\]+/g, "-");
// Replace all characters which are not allowed by empty strings
fileName = fileName.replaceAll(/[^a-z0-9.-]/g, "");
// Remove any leading or trailing hyphens or dots
fileName = fileName.replaceAll(/^[.-]+|[.-]+$/g, "");
// Remove any duplicate hyphens
fileName = fileName.replaceAll(/-{2,}/g, "-");
// Remove any duplicate dots
fileName = fileName.replaceAll(/\.{2,}/g, ".");
if (removeDots) {
fileName = fileName.replaceAll(/\./g, "-");
}
return fileName;
}

View File

@@ -40,6 +40,7 @@ import { getLanguageDisplayName } from "../common/query-language";
import type { DatabaseOrigin } from "./local-databases/database-origin";
import { createTimeoutSignal } from "../common/fetch-stream";
import type { App } from "../common/app";
import { createFilenameFromString } from "../common/filenames";
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -421,22 +422,7 @@ async function getStorageFolder(
let lastName: string;
if (nameOverrride) {
// Lowercase everything
let name = nameOverrride.toLowerCase();
// Replace all spaces, dots, underscores, and forward slashes with hyphens
name = name.replaceAll(/[\s._/]+/g, "-");
// Replace all characters which are not allowed by empty strings
name = name.replaceAll(/[^a-z0-9-]/g, "");
// Remove any leading or trailing hyphens
name = name.replaceAll(/^-|-$/g, "");
// Remove any duplicate hyphens
name = name.replaceAll(/-{2,}/g, "-");
lastName = name;
lastName = createFilenameFromString(nameOverrride);
} else {
// we need to generate a folder name for the unzipped archive,
// this needs to be human readable since we may use this name as the initial

View File

@@ -1,3 +1,5 @@
import { createFilenameFromString } from "../common/filenames";
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const packNameRegex = new RegExp(
`^(?<scope>${packNamePartRegex.source})/(?<name>${packNamePartRegex.source})$`,
@@ -23,7 +25,11 @@ export function autoNameExtensionPack(
}
const parts = packName.split("/");
const sanitizedParts = parts.map((part) => sanitizeExtensionPackName(part));
const sanitizedParts = parts.map((part) =>
createFilenameFromString(part, {
removeDots: true,
}),
);
// If the scope is empty (e.g. if the given name is "-/b"), then we need to still set a scope
if (sanitizedParts[0].length === 0) {
@@ -37,25 +43,6 @@ export function autoNameExtensionPack(
};
}
function sanitizeExtensionPackName(name: string) {
// Lowercase everything
name = name.toLowerCase();
// Replace all spaces, dots, and underscores with hyphens
name = name.replaceAll(/[\s._]+/g, "-");
// Replace all characters which are not allowed by empty strings
name = name.replaceAll(/[^a-z0-9-]/g, "");
// Remove any leading or trailing hyphens
name = name.replaceAll(/^-|-$/g, "");
// Remove any duplicate hyphens
name = name.replaceAll(/-{2,}/g, "-");
return name;
}
export function parsePackName(packName: string): ExtensionPackName | undefined {
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {

View File

@@ -20,6 +20,7 @@ import type {
ModelExtension,
ModelExtensionFile,
} from "./model-extension-file";
import { createFilenameFromString } from "../common/filenames";
import type { QueryLanguage } from "../common/query-language";
import modelExtensionFileSchema from "./model-extension-file.schema.json";
@@ -275,26 +276,7 @@ export function createFilenameForLibrary(
prefix = "models/",
suffix = ".model",
) {
let libraryName = library;
// Lowercase everything
libraryName = libraryName.toLowerCase();
// Replace all spaces and underscores with hyphens
libraryName = libraryName.replaceAll(/[\s_]+/g, "-");
// Replace all characters which are not allowed by empty strings
libraryName = libraryName.replaceAll(/[^a-z0-9.-]/g, "");
// Remove any leading or trailing hyphens or dots
libraryName = libraryName.replaceAll(/^[.-]+|[.-]+$/g, "");
// Remove any duplicate hyphens
libraryName = libraryName.replaceAll(/-{2,}/g, "-");
// Remove any duplicate dots
libraryName = libraryName.replaceAll(/\.{2,}/g, ".");
return `${prefix}${libraryName}${suffix}.yml`;
return `${prefix}${createFilenameFromString(library)}${suffix}.yml`;
}
export function createFilenameForPackage(

View File

@@ -0,0 +1,80 @@
import { createFilenameFromString } from "../../../src/common/filenames";
describe("createFilenameFromString", () => {
const testCases: Array<{
input: string;
filename: string;
filenameWithoutDots?: string;
}> = [
{
input: "sql2o",
filename: "sql2o",
},
{
input: "spring-boot",
filename: "spring-boot",
},
{
input: "spring--boot",
filename: "spring-boot",
},
{
input: "rt",
filename: "rt",
},
{
input: "System.Runtime",
filename: "system.runtime",
filenameWithoutDots: "system-runtime",
},
{
input: "System..Runtime",
filename: "system.runtime",
filenameWithoutDots: "system-runtime",
},
{
input: "google/brotli",
filename: "google-brotli",
},
{
input: "github/vscode-codeql",
filename: "github-vscode-codeql",
},
{
input: "github/vscode---codeql--",
filename: "github-vscode-codeql",
},
{
input: "github...vscode--c..odeql",
filename: "github.vscode-c.odeql",
filenameWithoutDots: "github-vscode-c-odeql",
},
{
input: "github\\vscode-codeql",
filename: "github-vscode-codeql",
},
{
input: "uNetworking/uWebSockets.js",
filename: "unetworking-uwebsockets.js",
filenameWithoutDots: "unetworking-uwebsockets-js",
},
];
test.each(testCases)(
"returns $filename if string is $input",
({ input, filename }) => {
expect(createFilenameFromString(input)).toEqual(filename);
},
);
test.each(testCases)(
"returns $filename if string is $input and dots are not allowed",
({ input, filename, filenameWithoutDots }) => {
expect(
createFilenameFromString(input, {
removeDots: true,
}),
).toEqual(filenameWithoutDots ?? filename);
},
);
});