mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Migrate utils to ts
This commit is contained in:
7
app/src/types/api.ts
Normal file
7
app/src/types/api.ts
Normal 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
36
app/src/types/file.ts
Normal 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
18
app/src/types/markdown.ts
Normal 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
5
app/src/types/modal.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum ModalType {
|
||||
NewFile = 'newFile',
|
||||
DeleteFile = 'deleteFile',
|
||||
CommitMessage = 'commitMessage',
|
||||
}
|
||||
4
app/src/types/theme.ts
Normal file
4
app/src/types/theme.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum Theme {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
32
app/src/types/workspace.ts
Normal file
32
app/src/types/workspace.ts
Normal 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,
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { IMAGE_EXTENSIONS } from './constants';
|
||||
|
||||
export const isImageFile = (filePath) => {
|
||||
return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext));
|
||||
};
|
||||
10
app/src/utils/fileHelpers.ts
Normal file
10
app/src/utils/fileHelpers.ts
Normal 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));
|
||||
};
|
||||
@@ -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]}`;
|
||||
};
|
||||
24
app/src/utils/formatBytes.ts
Normal file
24
app/src/utils/formatBytes.ts
Normal 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]}`;
|
||||
};
|
||||
@@ -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<void> {
|
||||
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<Text, ReplacementInfo>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user