vzcode
Version:
Multiplayer code editor system
138 lines (124 loc) • 3.98 kB
text/typescript
import { TabState } from '../types';
import { VizFileId, VizContent } from '@vizhub/viz-types';
// The delimiter used to separate file names in the `tabs` parameter.
// We need a character that is both URL-safe (does not get escaped in URLs)
// and is not typically found in file names, especially for JavaScript and CSS files.
//
// The following characters are not URL-safe as they get converted to escaped sequences in URLs,
// leading to less readable URLs:
// - `+` becomes `%2B`
// - `|` becomes `%7C`
// - `;` becomes `%3B`
// - `:` becomes `%3A`
// - `,` becomes `%2C`
// - `!` becomes `%21`
// - `/` becomes `%2F`
//
// The following characters, while URL-safe, are commonly used in file names,
// hence using them could lead to conflicts or misinterpretation:
// - `.` commonly used as a dot separator in file names and extensions
// - ` ` (space) often found in file names but can lead to issues in URLs
// - `-` (hyphen) frequently used in file and folder names for readability
// - `_` (underscore) also commonly used in file and folder names
//
// Considering the above constraints, we need a character that is both URL-safe
// and not typically used in file names. The following character meets these criteria:
// - `~` is URL-safe (remains unescaped in URLs) and is not a standard character
// in file names for JavaScript and CSS files, making it a suitable choice for a delimiter.
export const delimiter = '~';
// Gets the file id of a file with the given name.
// Returns null if not found.
const getFileId = (
content: VizContent,
fileName: string,
): string | null => {
if (content && content.files) {
for (const fileId of Object.keys(content.files)) {
const file = content.files[fileId];
if (file.name === fileName) {
return fileId;
}
}
}
return null;
};
// Gets the file name of a file with the given id,
// guard against failure cases.
// Returns null if not found.
const getFileName = (
content: VizContent,
fileId: string,
): string | null => {
if (content && content.files) {
const file = content.files[fileId];
if (file) {
return file.name;
}
}
return null;
};
// An object representing the search parameters.
export type TabStateParams = {
file?: string;
tabs?: string;
};
// Decodes the tab list and active file from the search parameters.
export const decodeTabs = ({
tabStateParams,
content,
}: {
tabStateParams: TabStateParams;
content: VizContent;
}): {
tabList: Array<TabState>;
activeFileId: VizFileId;
} => {
const { file, tabs } = tabStateParams;
const tabList: TabState[] = [];
// Decode active file
const activeFileId = getFileId(content, file);
// Decode tab list
if (tabs) {
tabs.split(delimiter).forEach((fileName) => {
const fileId = getFileId(content, fileName);
if (fileId) {
tabList.push({ fileId });
}
});
} else if (activeFileId) {
// If there's only an active file without a tab list
tabList.push({ fileId: activeFileId });
}
return { tabList, activeFileId };
};
// Encodes the tab list and active file into the search parameters.
export const encodeTabs = ({
tabList,
activeFileId,
content,
}: {
tabList: Array<TabState>;
activeFileId: VizFileId | null;
content: VizContent;
}): TabStateParams => {
// Get the file name of the active file
const activeFileName = getFileName(content, activeFileId);
const params: TabStateParams = {};
// If that's missing, no need to proceed (should never happen).
if (activeFileName) {
// In any case we set the `file` parameter.
params.file = activeFileName;
// We only need to encode the `tabs` parameter
// if there's more than one tab.
if (tabList.length > 1) {
// Set tab list
const tabs = tabList
.map((tabstate: TabState) =>
getFileName(content, tabstate.fileId),
)
.join(delimiter);
params.tabs = tabs;
}
}
return params;
};