mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Handle wiki style links
This commit is contained in:
@@ -28,6 +28,27 @@ func ListFiles(fs *filesystem.FileSystem) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func LookupFileByName(fs *filesystem.FileSystem) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
filenameOrPath := r.URL.Query().Get("filename")
|
||||
if filenameOrPath == "" {
|
||||
http.Error(w, "Filename or path is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
filePaths, err := fs.FindFileByName(filenameOrPath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(map[string][]string{"paths": filePaths}); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetFileContent(fs *filesystem.FileSystem) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
filePath := strings.TrimPrefix(r.URL.Path, "/api/v1/files/")
|
||||
|
||||
@@ -18,6 +18,7 @@ func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.FileSystem) {
|
||||
r.Get("/*", GetFileContent(fs))
|
||||
r.Post("/*", SaveFile(fs))
|
||||
r.Delete("/*", DeleteFile(fs))
|
||||
r.Get("/lookup", LookupFileByName(fs))
|
||||
})
|
||||
r.Route("/git", func(r chi.Router) {
|
||||
r.Post("/commit", StageCommitAndPush(fs))
|
||||
|
||||
@@ -103,6 +103,47 @@ func (fs *FileSystem) walkDirectory(dir string) ([]FileNode, error) {
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (fs *FileSystem) FindFileByName(filenameOrPath string) ([]string, error) {
|
||||
var foundPaths []string
|
||||
var searchPattern string
|
||||
|
||||
// If no extension is provided, assume .md
|
||||
if !strings.Contains(filenameOrPath, ".") {
|
||||
searchPattern = filenameOrPath + ".md"
|
||||
} else {
|
||||
searchPattern = filenameOrPath
|
||||
}
|
||||
|
||||
err := filepath.Walk(fs.RootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
relPath, err := filepath.Rel(fs.RootDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the file matches the search pattern
|
||||
if strings.HasSuffix(relPath, searchPattern) ||
|
||||
strings.EqualFold(info.Name(), searchPattern) {
|
||||
foundPaths = append(foundPaths, relPath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(foundPaths) == 0 {
|
||||
return nil, errors.New("file not found")
|
||||
}
|
||||
|
||||
return foundPaths, nil
|
||||
}
|
||||
|
||||
func (fs *FileSystem) GetFileContent(filePath string) ([]byte, error) {
|
||||
fullPath, err := fs.validatePath(filePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GeistProvider, CssBaseline, Page } from '@geist-ui/core';
|
||||
import { GeistProvider, CssBaseline, Page, useToasts } from '@geist-ui/core';
|
||||
import Header from './components/Header';
|
||||
import MainContent from './components/MainContent';
|
||||
import useFileManagement from './hooks/useFileManagement';
|
||||
@@ -10,6 +10,7 @@ function App() {
|
||||
const [themeType, setThemeType] = useState('light');
|
||||
const [userId, setUserId] = useState(1);
|
||||
const [settings, setSettings] = useState({ gitEnabled: false });
|
||||
const { setToast } = useToasts();
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserSettings = async () => {
|
||||
@@ -36,12 +37,33 @@ function App() {
|
||||
handleContentChange,
|
||||
handleSave,
|
||||
pullLatestChanges,
|
||||
lookupFileByName,
|
||||
} = useFileManagement(settings.gitEnabled);
|
||||
|
||||
const setTheme = (newTheme) => {
|
||||
setThemeType(newTheme);
|
||||
};
|
||||
|
||||
const handleLinkClick = async (filename) => {
|
||||
try {
|
||||
const filePaths = await lookupFileByName(filename);
|
||||
if (filePaths.length === 1) {
|
||||
handleFileSelect(filePaths[0]);
|
||||
} else if (filePaths.length > 1) {
|
||||
setFileOptions(filePaths.map((path) => ({ label: path, value: path })));
|
||||
setFileSelectionModalVisible(true);
|
||||
} else {
|
||||
setToast({ text: `File "${filename}" not found`, type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error looking up file:', error);
|
||||
setToast({
|
||||
text: 'Failed to lookup file. Please try again.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<GeistProvider themeType={themeType}>
|
||||
<CssBaseline />
|
||||
@@ -60,6 +82,7 @@ function App() {
|
||||
onSave={handleSave}
|
||||
settings={settings}
|
||||
pullLatestChanges={pullLatestChanges}
|
||||
onLinkClick={handleLinkClick}
|
||||
/>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
saveFileContent,
|
||||
deleteFile,
|
||||
getFileUrl,
|
||||
lookupFileByName,
|
||||
} from '../services/api';
|
||||
|
||||
const isImageFile = (filePath) => {
|
||||
@@ -37,6 +38,7 @@ const MainContent = ({
|
||||
onSave,
|
||||
settings,
|
||||
pullLatestChanges,
|
||||
onLinkClick,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('source');
|
||||
const { type: themeType } = useTheme();
|
||||
@@ -212,6 +214,8 @@ const MainContent = ({
|
||||
<MarkdownPreview
|
||||
content={content}
|
||||
baseUrl={window.API_BASE_URL}
|
||||
onLinkClick={onLinkClick}
|
||||
lookupFileByName={lookupFileByName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
@@ -6,7 +6,69 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
const MarkdownPreview = ({ content, baseUrl }) => {
|
||||
const MarkdownPreview = ({
|
||||
content,
|
||||
baseUrl,
|
||||
onLinkClick,
|
||||
lookupFileByName,
|
||||
}) => {
|
||||
const [processedContent, setProcessedContent] = useState(content);
|
||||
|
||||
useEffect(() => {
|
||||
const processContent = async (rawContent) => {
|
||||
const regex = /(!?)\[\[(.*?)\]\]/g;
|
||||
let result = rawContent;
|
||||
const matches = [...rawContent.matchAll(regex)];
|
||||
|
||||
for (const match of matches) {
|
||||
const [fullMatch, isImage, fileName] = match;
|
||||
try {
|
||||
const paths = await lookupFileByName(fileName);
|
||||
if (paths && paths.length > 0) {
|
||||
const filePath = paths[0];
|
||||
if (isImage) {
|
||||
result = result.replace(
|
||||
fullMatch,
|
||||
``
|
||||
);
|
||||
} else {
|
||||
// Use a valid URL format that React Markdown will recognize
|
||||
result = result.replace(
|
||||
fullMatch,
|
||||
`[${fileName}](${baseUrl}/internal/${encodeURIComponent(
|
||||
filePath
|
||||
)})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use a valid URL format for not found links
|
||||
result = result.replace(
|
||||
fullMatch,
|
||||
`[${fileName}](${baseUrl}/notfound/${encodeURIComponent(
|
||||
fileName
|
||||
)})`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error looking up file:', error);
|
||||
result = result.replace(
|
||||
fullMatch,
|
||||
`[${fileName}](${baseUrl}/notfound/${encodeURIComponent(fileName)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
processContent(content).then(setProcessedContent);
|
||||
}, [content, baseUrl, lookupFileByName]);
|
||||
|
||||
const handleImageError = (event) => {
|
||||
console.error('Failed to load image:', event.target.src);
|
||||
event.target.alt = 'Failed to load image';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="markdown-preview">
|
||||
<ReactMarkdown
|
||||
@@ -30,17 +92,48 @@ const MarkdownPreview = ({ content, baseUrl }) => {
|
||||
</code>
|
||||
);
|
||||
},
|
||||
img({ src, alt, ...props }) {
|
||||
// Check if the src is a relative path
|
||||
if (src && !src.startsWith('http') && !src.startsWith('data:')) {
|
||||
// Prepend the baseUrl to create an absolute path
|
||||
src = `${baseUrl}/files/${src}`;
|
||||
img: ({ src, alt, ...props }) => (
|
||||
<img src={src} alt={alt} onError={handleImageError} {...props} />
|
||||
),
|
||||
a: ({ href, children }) => {
|
||||
if (href.startsWith(`${baseUrl}/internal/`)) {
|
||||
const filePath = decodeURIComponent(
|
||||
href.replace(`${baseUrl}/internal/`, '')
|
||||
);
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onLinkClick(filePath);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
} else if (href.startsWith(`${baseUrl}/notfound/`)) {
|
||||
const fileName = decodeURIComponent(
|
||||
href.replace(`${baseUrl}/notfound/`, '')
|
||||
);
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
style={{ color: 'red', textDecoration: 'underline' }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onLinkClick(fileName);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <img src={src} alt={alt} {...props} />;
|
||||
// Regular markdown link
|
||||
return <a href={href}>{children}</a>;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
{processedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
fetchFileContent,
|
||||
saveFileContent,
|
||||
pullChanges,
|
||||
lookupFileByName,
|
||||
} from '../services/api';
|
||||
|
||||
const DEFAULT_FILE = {
|
||||
@@ -134,6 +135,7 @@ const useFileManagement = (gitEnabled = false) => {
|
||||
handleContentChange,
|
||||
handleSave,
|
||||
pullLatestChanges,
|
||||
lookupFileByName,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -131,3 +131,19 @@ export const commitAndPush = async (message) => {
|
||||
export const getFileUrl = (filePath) => {
|
||||
return `${API_BASE_URL}/files/${filePath}`;
|
||||
};
|
||||
|
||||
export const lookupFileByName = async (filename) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/files/lookup?filename=${encodeURIComponent(filename)}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('File not found');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.paths;
|
||||
} catch (error) {
|
||||
console.error('Error looking up file:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user