mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +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 { 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);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user