Migrate utils to ts

This commit is contained in:
2025-04-04 19:23:31 +02:00
parent 49ecaac720
commit e4fb276cf7
12 changed files with 287 additions and 111 deletions

7
app/src/types/api.ts Normal file
View File

@@ -0,0 +1,7 @@
declare global {
interface Window {
API_BASE_URL: string;
}
}
export const API_BASE_URL = window.API_BASE_URL;

36
app/src/types/file.ts Normal file
View File

@@ -0,0 +1,36 @@
export enum FileAction {
Create = 'create',
Delete = 'delete',
Rename = 'rename',
}
export enum FileExtension {
Markdown = '.md',
JPG = '.jpg',
JPEG = '.jpeg',
PNG = '.png',
GIF = '.gif',
WebP = '.webp',
SVG = '.svg',
}
export const IMAGE_EXTENSIONS = [
FileExtension.JPG,
FileExtension.JPEG,
FileExtension.PNG,
FileExtension.GIF,
FileExtension.WebP,
FileExtension.SVG,
];
export interface DefaultFile {
name: string;
path: string;
content: string;
}
export const DEFAULT_FILE: DefaultFile = {
name: 'New File.md',
path: 'New File.md',
content: '# Welcome to NovaMD\n\nStart editing here!',
};

18
app/src/types/markdown.ts Normal file
View File

@@ -0,0 +1,18 @@
export enum InlineContainerType {
Paragraph = 'paragraph',
ListItem = 'listItem',
TableCell = 'tableCell',
Blockquote = 'blockquote',
Heading = 'heading',
Emphasis = 'emphasis',
Strong = 'strong',
Delete = 'delete',
}
export const INLINE_CONTAINER_TYPES = new Set<InlineContainerType>(
Object.values(InlineContainerType)
);
export const MARKDOWN_REGEX = {
WIKILINK: /(!?)\[\[(.*?)\]\]/g,
} as const;

5
app/src/types/modal.ts Normal file
View File

@@ -0,0 +1,5 @@
export enum ModalType {
NewFile = 'newFile',
DeleteFile = 'deleteFile',
CommitMessage = 'commitMessage',
}

4
app/src/types/theme.ts Normal file
View File

@@ -0,0 +1,4 @@
export enum Theme {
Light = 'light',
Dark = 'dark',
}

View File

@@ -0,0 +1,32 @@
import { Theme } from './theme';
export interface WorkspaceSettings {
theme: Theme;
autoSave: boolean;
gitEnabled: boolean;
gitUrl: string;
gitUser: string;
gitToken: string;
gitAutoCommit: boolean;
gitCommitMsgTemplate: string;
}
export const DEFAULT_WORKSPACE_SETTINGS: WorkspaceSettings = {
theme: Theme.Light,
autoSave: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
};
export interface Workspace extends WorkspaceSettings {
name: string;
}
export const DEFAULT_WORKSPACE: Workspace = {
name: '',
...DEFAULT_WORKSPACE_SETTINGS,
};

View File

@@ -1,67 +0,0 @@
export const API_BASE_URL = window.API_BASE_URL;
export const THEMES = {
LIGHT: 'light',
DARK: 'dark',
};
export const FILE_ACTIONS = {
CREATE: 'create',
DELETE: 'delete',
RENAME: 'rename',
};
export const MODAL_TYPES = {
NEW_FILE: 'newFile',
DELETE_FILE: 'deleteFile',
COMMIT_MESSAGE: 'commitMessage',
};
export const IMAGE_EXTENSIONS = [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.webp',
'.svg',
];
// Renamed from DEFAULT_SETTINGS to be more specific
export const DEFAULT_WORKSPACE_SETTINGS = {
theme: THEMES.LIGHT,
autoSave: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
};
// Template for creating new workspaces
export const DEFAULT_WORKSPACE = {
name: '',
...DEFAULT_WORKSPACE_SETTINGS,
};
export const DEFAULT_FILE = {
name: 'New File.md',
path: 'New File.md',
content: '# Welcome to Lemma\n\nStart editing here!',
};
export const MARKDOWN_REGEX = {
WIKILINK: /(!?)\[\[(.*?)\]\]/g,
};
// List of element types that can contain inline content
export const INLINE_CONTAINER_TYPES = new Set([
'paragraph',
'listItem',
'tableCell',
'blockquote',
'heading',
'emphasis',
'strong',
'delete',
]);

View File

@@ -1,5 +0,0 @@
import { IMAGE_EXTENSIONS } from './constants';
export const isImageFile = (filePath) => {
return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext));
};

View File

@@ -0,0 +1,10 @@
import { IMAGE_EXTENSIONS } from '../types/file';
/**
* Checks if the given file path has an image extension.
* @param filePath - The file path to check.
* @returns True if the file path has an image extension, false otherwise.
*/
export const isImageFile = (filePath: string): boolean => {
return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext));
};

View File

@@ -1,10 +0,0 @@
export const formatBytes = (bytes) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
};

View File

@@ -0,0 +1,24 @@
/**
* Units for file size display.
*/
type ByteUnit = 'B' | 'KB' | 'MB' | 'GB';
/**
* An array of size units in ascending order.
*/
const UNITS: readonly ByteUnit[] = ['B', 'KB', 'MB', 'GB'] as const;
/**
* Formats a number of bytes into a human-readable string.
* @param bytes - The number of bytes to format.
* @returns A string representing the formatted file size.
*/
export const formatBytes = (bytes: number): string => {
let size: number = bytes;
let unitIndex: number = 0;
while (size >= 1024 && unitIndex < UNITS.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${UNITS[unitIndex]}`;
};

View File

@@ -1,19 +1,108 @@
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import { lookupFileByName, getFileUrl } from '../services/api'; import { lookupFileByName, getFileUrl } from '../services/api';
import { INLINE_CONTAINER_TYPES, MARKDOWN_REGEX } from './constants'; import { MARKDOWN_REGEX } from '../types/markdown';
import { Node } from 'unist';
import { Parent } from 'unist';
import { Text } from 'mdast';
function createNotFoundLink(fileName, displayText, baseUrl) { /**
* Represents a wiki link match from the regex
*/
interface WikiLinkMatch {
fullMatch: string;
isImage: string;
fileName: string;
displayText: string;
heading?: string;
index: number;
}
/**
* Node replacement information for processing
*/
interface ReplacementInfo {
matches: WikiLinkMatch[];
parent: Parent;
index: number;
}
/**
* Properties for link nodes
*/
interface LinkNodeProps {
style?: {
color?: string;
textDecoration?: string;
};
}
/**
* Link node with data properties
*/
interface LinkNode extends Node {
type: 'link';
url: string;
children: Node[];
data?: {
hProperties?: LinkNodeProps;
};
}
/**
* Image node
*/
interface ImageNode extends Node {
type: 'image';
url: string;
alt?: string;
title?: string;
}
/**
* Text node
*/
interface TextNode extends Node {
type: 'text';
value: string;
}
/**
* Creates a text node with the given value
*/
function createTextNode(value: string): TextNode {
return {
type: 'text',
value,
};
}
/**
* Creates a link node for files that don't exist
*/
function createNotFoundLink(
fileName: string,
displayText: string,
baseUrl: string
): LinkNode {
return { return {
type: 'link', type: 'link',
url: `${baseUrl}/notfound/${encodeURIComponent(fileName)}`, url: `${baseUrl}/notfound/${encodeURIComponent(fileName)}`,
children: [{ type: 'text', value: displayText }], children: [createTextNode(displayText)],
data: { data: {
hProperties: { style: { color: 'red', textDecoration: 'underline' } }, hProperties: { style: { color: 'red', textDecoration: 'underline' } },
}, },
}; };
} }
function createFileLink(filePath, displayText, heading, baseUrl) { /**
* Creates a link node for existing files
*/
function createFileLink(
filePath: string,
displayText: string,
heading: string | undefined,
baseUrl: string
): LinkNode {
const url = heading const url = heading
? `${baseUrl}/internal/${encodeURIComponent(filePath)}#${encodeURIComponent( ? `${baseUrl}/internal/${encodeURIComponent(filePath)}#${encodeURIComponent(
heading heading
@@ -23,11 +112,18 @@ function createFileLink(filePath, displayText, heading, baseUrl) {
return { return {
type: 'link', type: 'link',
url, url,
children: [{ type: 'text', value: displayText }], children: [createTextNode(displayText)],
}; };
} }
function createImageNode(workspaceName, filePath, displayText) { /**
* Creates an image node
*/
function createImageNode(
workspaceName: string,
filePath: string,
displayText: string
): ImageNode {
return { return {
type: 'image', type: 'image',
url: getFileUrl(workspaceName, filePath), url: getFileUrl(workspaceName, filePath),
@@ -36,35 +132,59 @@ function createImageNode(workspaceName, filePath, displayText) {
}; };
} }
function addMarkdownExtension(fileName) { /**
* Adds markdown extension to a filename if it doesn't have one
*/
function addMarkdownExtension(fileName: string): string {
if (fileName.includes('.')) { if (fileName.includes('.')) {
return fileName; return fileName;
} }
return `${fileName}.md`; return `${fileName}.md`;
} }
export function remarkWikiLinks(workspaceName) { /**
return async function transformer(tree) { * Determines if a node type can contain inline content
*/
function canContainInline(type: string): boolean {
return [
'paragraph',
'listItem',
'tableCell',
'blockquote',
'heading',
'emphasis',
'strong',
'delete',
].includes(type);
}
/**
* Plugin for processing wiki-style links in markdown
*/
export function remarkWikiLinks(workspaceName: string) {
return async function transformer(tree: Node): Promise<void> {
if (!workspaceName) { if (!workspaceName) {
console.warn('No workspace ID provided to remarkWikiLinks plugin'); console.warn('No workspace ID provided to remarkWikiLinks plugin');
return; return;
} }
const baseUrl = window.API_BASE_URL; const baseUrl: string = window.API_BASE_URL;
const replacements = new Map(); const replacements = new Map<Text, ReplacementInfo>();
// Find all wiki links // Find all wiki links
visit(tree, 'text', function (node, index, parent) { visit(tree, 'text', function (node: Text, index: number, parent: Parent) {
const regex = MARKDOWN_REGEX.WIKILINK; const regex = MARKDOWN_REGEX.WIKILINK;
let match; let match: RegExpExecArray | null;
const matches = []; const matches: WikiLinkMatch[] = [];
while ((match = regex.exec(node.value)) !== null) { while ((match = regex.exec(node.value)) !== null) {
const [fullMatch, isImage, innerContent] = match; const [fullMatch, isImage, innerContent] = match;
let fileName, displayText, heading; let fileName: string;
let displayText: string;
let heading: string | undefined;
const pipeIndex = innerContent.indexOf('|'); const pipeIndex: number = innerContent.indexOf('|');
const hashIndex = innerContent.indexOf('#'); const hashIndex: number = innerContent.indexOf('#');
if (pipeIndex !== -1) { if (pipeIndex !== -1) {
displayText = innerContent.slice(pipeIndex + 1).trim(); displayText = innerContent.slice(pipeIndex + 1).trim();
@@ -96,8 +216,8 @@ export function remarkWikiLinks(workspaceName) {
// Process all matches // Process all matches
for (const [node, { matches, parent }] of replacements) { for (const [node, { matches, parent }] of replacements) {
const newNodes = []; const newNodes: (LinkNode | ImageNode | TextNode)[] = [];
let lastIndex = 0; let lastIndex: number = 0;
for (const match of matches) { for (const match of matches) {
// Add text before the match // Add text before the match
@@ -109,14 +229,17 @@ export function remarkWikiLinks(workspaceName) {
} }
try { try {
const lookupFileName = match.isImage const lookupFileName: string = match.isImage
? match.fileName ? match.fileName
: addMarkdownExtension(match.fileName); : addMarkdownExtension(match.fileName);
const paths = await lookupFileByName(workspaceName, lookupFileName); const paths: string[] = await lookupFileByName(
workspaceName,
lookupFileName
);
if (paths && paths.length > 0) { if (paths && paths.length > 0) {
const filePath = paths[0]; const filePath: string = paths[0];
if (match.isImage) { if (match.isImage) {
newNodes.push( newNodes.push(
createImageNode(workspaceName, filePath, match.displayText) createImageNode(workspaceName, filePath, match.displayText)
@@ -154,20 +277,19 @@ export function remarkWikiLinks(workspaceName) {
}); });
} }
// If the parent is a container that can have inline content, // Replace nodes in parent
// replace the text node directly with the new nodes if (parent && canContainInline(parent.type)) {
if (parent && INLINE_CONTAINER_TYPES.has(parent.type)) { const nodeIndex: number = parent.children.indexOf(node);
const nodeIndex = parent.children.indexOf(node);
if (nodeIndex !== -1) { if (nodeIndex !== -1) {
parent.children.splice(nodeIndex, 1, ...newNodes); parent.children.splice(nodeIndex, 1, ...newNodes);
} }
} else { } else {
// For other types of parents, wrap the nodes in a paragraph // Wrap in paragraph for other types
const paragraph = { const paragraph: Parent = {
type: 'paragraph', type: 'paragraph',
children: newNodes, children: newNodes,
}; };
const nodeIndex = parent.children.indexOf(node); const nodeIndex: number = parent.children.indexOf(node);
if (nodeIndex !== -1) { if (nodeIndex !== -1) {
parent.children.splice(nodeIndex, 1, paragraph); parent.children.splice(nodeIndex, 1, paragraph);
} }