From e4fb276cf7f1e03249a4139fb26b9bd955ebf507 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Fri, 4 Apr 2025 19:23:31 +0200 Subject: [PATCH] Migrate utils to ts --- app/src/types/api.ts | 7 + app/src/types/file.ts | 36 ++++ app/src/types/markdown.ts | 18 ++ app/src/types/modal.ts | 5 + app/src/types/theme.ts | 4 + app/src/types/workspace.ts | 32 ++++ app/src/utils/constants.js | 67 ------- app/src/utils/fileHelpers.js | 5 - app/src/utils/fileHelpers.ts | 10 + app/src/utils/formatBytes.js | 10 - app/src/utils/formatBytes.ts | 24 +++ ...{remarkWikiLinks.js => remarkWikiLinks.ts} | 180 +++++++++++++++--- 12 files changed, 287 insertions(+), 111 deletions(-) create mode 100644 app/src/types/api.ts create mode 100644 app/src/types/file.ts create mode 100644 app/src/types/markdown.ts create mode 100644 app/src/types/modal.ts create mode 100644 app/src/types/theme.ts create mode 100644 app/src/types/workspace.ts delete mode 100644 app/src/utils/constants.js delete mode 100644 app/src/utils/fileHelpers.js create mode 100644 app/src/utils/fileHelpers.ts delete mode 100644 app/src/utils/formatBytes.js create mode 100644 app/src/utils/formatBytes.ts rename app/src/utils/{remarkWikiLinks.js => remarkWikiLinks.ts} (51%) diff --git a/app/src/types/api.ts b/app/src/types/api.ts new file mode 100644 index 0000000..77996bd --- /dev/null +++ b/app/src/types/api.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + API_BASE_URL: string; + } +} + +export const API_BASE_URL = window.API_BASE_URL; diff --git a/app/src/types/file.ts b/app/src/types/file.ts new file mode 100644 index 0000000..64741dd --- /dev/null +++ b/app/src/types/file.ts @@ -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!', +}; diff --git a/app/src/types/markdown.ts b/app/src/types/markdown.ts new file mode 100644 index 0000000..96a9a77 --- /dev/null +++ b/app/src/types/markdown.ts @@ -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( + Object.values(InlineContainerType) +); + +export const MARKDOWN_REGEX = { + WIKILINK: /(!?)\[\[(.*?)\]\]/g, +} as const; diff --git a/app/src/types/modal.ts b/app/src/types/modal.ts new file mode 100644 index 0000000..984334e --- /dev/null +++ b/app/src/types/modal.ts @@ -0,0 +1,5 @@ +export enum ModalType { + NewFile = 'newFile', + DeleteFile = 'deleteFile', + CommitMessage = 'commitMessage', +} diff --git a/app/src/types/theme.ts b/app/src/types/theme.ts new file mode 100644 index 0000000..b6f3c1e --- /dev/null +++ b/app/src/types/theme.ts @@ -0,0 +1,4 @@ +export enum Theme { + Light = 'light', + Dark = 'dark', +} diff --git a/app/src/types/workspace.ts b/app/src/types/workspace.ts new file mode 100644 index 0000000..a757e7e --- /dev/null +++ b/app/src/types/workspace.ts @@ -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, +}; diff --git a/app/src/utils/constants.js b/app/src/utils/constants.js deleted file mode 100644 index 201ab56..0000000 --- a/app/src/utils/constants.js +++ /dev/null @@ -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', -]); diff --git a/app/src/utils/fileHelpers.js b/app/src/utils/fileHelpers.js deleted file mode 100644 index 7d0c1a0..0000000 --- a/app/src/utils/fileHelpers.js +++ /dev/null @@ -1,5 +0,0 @@ -import { IMAGE_EXTENSIONS } from './constants'; - -export const isImageFile = (filePath) => { - return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext)); -}; diff --git a/app/src/utils/fileHelpers.ts b/app/src/utils/fileHelpers.ts new file mode 100644 index 0000000..7ed9c0b --- /dev/null +++ b/app/src/utils/fileHelpers.ts @@ -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)); +}; diff --git a/app/src/utils/formatBytes.js b/app/src/utils/formatBytes.js deleted file mode 100644 index dde9f21..0000000 --- a/app/src/utils/formatBytes.js +++ /dev/null @@ -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]}`; -}; diff --git a/app/src/utils/formatBytes.ts b/app/src/utils/formatBytes.ts new file mode 100644 index 0000000..595c61a --- /dev/null +++ b/app/src/utils/formatBytes.ts @@ -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]}`; +}; diff --git a/app/src/utils/remarkWikiLinks.js b/app/src/utils/remarkWikiLinks.ts similarity index 51% rename from app/src/utils/remarkWikiLinks.js rename to app/src/utils/remarkWikiLinks.ts index 15d66aa..9b7cf6f 100644 --- a/app/src/utils/remarkWikiLinks.js +++ b/app/src/utils/remarkWikiLinks.ts @@ -1,19 +1,108 @@ import { visit } from 'unist-util-visit'; 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 { type: 'link', url: `${baseUrl}/notfound/${encodeURIComponent(fileName)}`, - children: [{ type: 'text', value: displayText }], + children: [createTextNode(displayText)], data: { 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 ? `${baseUrl}/internal/${encodeURIComponent(filePath)}#${encodeURIComponent( heading @@ -23,11 +112,18 @@ function createFileLink(filePath, displayText, heading, baseUrl) { return { type: 'link', 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 { type: 'image', 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('.')) { return fileName; } 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 { if (!workspaceName) { console.warn('No workspace ID provided to remarkWikiLinks plugin'); return; } - const baseUrl = window.API_BASE_URL; - const replacements = new Map(); + const baseUrl: string = window.API_BASE_URL; + const replacements = new Map(); // 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; - let match; - const matches = []; + let match: RegExpExecArray | null; + const matches: WikiLinkMatch[] = []; while ((match = regex.exec(node.value)) !== null) { const [fullMatch, isImage, innerContent] = match; - let fileName, displayText, heading; + let fileName: string; + let displayText: string; + let heading: string | undefined; - const pipeIndex = innerContent.indexOf('|'); - const hashIndex = innerContent.indexOf('#'); + const pipeIndex: number = innerContent.indexOf('|'); + const hashIndex: number = innerContent.indexOf('#'); if (pipeIndex !== -1) { displayText = innerContent.slice(pipeIndex + 1).trim(); @@ -96,8 +216,8 @@ export function remarkWikiLinks(workspaceName) { // Process all matches for (const [node, { matches, parent }] of replacements) { - const newNodes = []; - let lastIndex = 0; + const newNodes: (LinkNode | ImageNode | TextNode)[] = []; + let lastIndex: number = 0; for (const match of matches) { // Add text before the match @@ -109,14 +229,17 @@ export function remarkWikiLinks(workspaceName) { } try { - const lookupFileName = match.isImage + const lookupFileName: string = match.isImage ? match.fileName : addMarkdownExtension(match.fileName); - const paths = await lookupFileByName(workspaceName, lookupFileName); + const paths: string[] = await lookupFileByName( + workspaceName, + lookupFileName + ); if (paths && paths.length > 0) { - const filePath = paths[0]; + const filePath: string = paths[0]; if (match.isImage) { newNodes.push( 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 the text node directly with the new nodes - if (parent && INLINE_CONTAINER_TYPES.has(parent.type)) { - const nodeIndex = parent.children.indexOf(node); + // Replace nodes in parent + if (parent && canContainInline(parent.type)) { + const nodeIndex: number = parent.children.indexOf(node); if (nodeIndex !== -1) { parent.children.splice(nodeIndex, 1, ...newNodes); } } else { - // For other types of parents, wrap the nodes in a paragraph - const paragraph = { + // Wrap in paragraph for other types + const paragraph: Parent = { type: 'paragraph', children: newNodes, }; - const nodeIndex = parent.children.indexOf(node); + const nodeIndex: number = parent.children.indexOf(node); if (nodeIndex !== -1) { parent.children.splice(nodeIndex, 1, paragraph); }