Migrate to remark

This commit is contained in:
2024-10-31 22:41:50 +01:00
parent a5aa2dd45b
commit 3c855fce21
8 changed files with 414 additions and 223 deletions

View File

@@ -25,10 +25,15 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-arborist": "^3.4.0", "react-arborist": "^3.4.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"remark-math": "^6.0.0" "rehype-prism": "^2.3.3",
"rehype-react": "^8.0.0",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.67", "@types/react": "^18.2.67",
@@ -2011,12 +2016,14 @@
"version": "15.7.13", "version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.9", "version": "18.3.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz",
"integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@@ -2103,6 +2110,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -2367,6 +2380,22 @@
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/css-selector-parser": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz",
"integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2863,9 +2892,9 @@
} }
}, },
"node_modules/hast-util-to-jsx-runtime": { "node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.0", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz",
"integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.0", "@types/estree": "^1.0.0",
@@ -3001,16 +3030,6 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"node_modules/html-url-attributes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz",
"integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
@@ -3914,6 +3933,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4274,32 +4305,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-markdown": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz",
"integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.0.0",
"hast-util-to-jsx-runtime": "^2.0.0",
"html-url-attributes": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.0.0",
"unified": "^11.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
},
"peerDependencies": {
"@types/react": ">=18",
"react": ">=18"
}
},
"node_modules/react-number-format": { "node_modules/react-number-format": {
"version": "5.4.2", "version": "5.4.2",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.2.tgz", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.2.tgz",
@@ -4603,6 +4608,83 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/rehype-parse": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
"integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-from-html": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-prism": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/rehype-prism/-/rehype-prism-2.3.3.tgz",
"integrity": "sha512-J9mhio/CwcJRDyIhsp5hgXmyGeQsFN+/1eNEKnBRxfdJAx2CqH41kV0dqn/k2OgMdjk21IoGFgar0MfVtGYTSg==",
"license": "MIT",
"dependencies": {
"hastscript": "^8.0.0",
"prismjs": "^1.29.0",
"rehype-parse": "^9.0.1",
"unist-util-is": "^6.0.0",
"unist-util-select": "^5.1.0",
"unist-util-visit": "^5.0.0"
},
"peerDependencies": {
"unified": "^10 || ^11"
}
},
"node_modules/rehype-prism/node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-prism/node_modules/hastscript": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
"integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^6.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-react": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz",
"integrity": "sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-to-jsx-runtime": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-math": { "node_modules/remark-math": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
@@ -5085,6 +5167,23 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/unist-util-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz",
"integrity": "sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"css-selector-parser": "^3.0.0",
"devlop": "^1.1.0",
"nth-check": "^2.0.0",
"zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": { "node_modules/unist-util-stringify-position": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",

View File

@@ -39,10 +39,15 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-arborist": "^3.4.0", "react-arborist": "^3.4.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"remark-math": "^6.0.0" "rehype-prism": "^2.3.3",
"rehype-react": "^8.0.0",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.67", "@types/react": "^18.2.67",

View File

@@ -11,7 +11,7 @@ const ContentView = ({
content, content,
handleContentChange, handleContentChange,
handleSave, handleSave,
handleLinkClick, handleFileSelect,
}) => { }) => {
if (!selectedFile) { if (!selectedFile) {
return ( return (
@@ -47,7 +47,7 @@ const ContentView = ({
selectedFile={selectedFile} selectedFile={selectedFile}
/> />
) : ( ) : (
<MarkdownPreview content={content} handleLinkClick={handleLinkClick} /> <MarkdownPreview content={content} handleFileSelect={handleFileSelect} />
); );
}; };

View File

@@ -9,8 +9,7 @@ import { useWorkspace } from '../contexts/WorkspaceContext';
const Layout = () => { const Layout = () => {
const { currentWorkspace, loading: workspaceLoading } = useWorkspace(); const { currentWorkspace, loading: workspaceLoading } = useWorkspace();
const { selectedFile, handleFileSelect, handleLinkClick } = const { selectedFile, handleFileSelect } = useFileNavigation();
useFileNavigation();
const { files, loadFileList } = useFileList(); const { files, loadFileList } = useFileList();
if (workspaceLoading) { if (workspaceLoading) {
@@ -49,7 +48,6 @@ const Layout = () => {
<MainContent <MainContent
selectedFile={selectedFile} selectedFile={selectedFile}
handleFileSelect={handleFileSelect} handleFileSelect={handleFileSelect}
handleLinkClick={handleLinkClick}
loadFileList={loadFileList} loadFileList={loadFileList}
/> />
</Container> </Container>

View File

@@ -12,12 +12,7 @@ import { useFileOperations } from '../hooks/useFileOperations';
import { useGitOperations } from '../hooks/useGitOperations'; import { useGitOperations } from '../hooks/useGitOperations';
import { useWorkspace } from '../contexts/WorkspaceContext'; import { useWorkspace } from '../contexts/WorkspaceContext';
const MainContent = ({ const MainContent = ({ selectedFile, handleFileSelect, loadFileList }) => {
selectedFile,
handleFileSelect,
handleLinkClick,
loadFileList,
}) => {
const [activeTab, setActiveTab] = useState('source'); const [activeTab, setActiveTab] = useState('source');
const { settings } = useWorkspace(); const { settings } = useWorkspace();
const { const {
@@ -113,7 +108,7 @@ const MainContent = ({
content={content} content={content}
handleContentChange={handleContentChange} handleContentChange={handleContentChange}
handleSave={handleSaveFile} handleSave={handleSaveFile}
handleLinkClick={handleLinkClick} handleFileSelect={handleFileSelect}
/> />
</Box> </Box>
<CreateFileModal onCreateFile={handleCreateFile} /> <CreateFileModal onCreateFile={handleCreateFile} />

View File

@@ -1,159 +1,117 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import ReactMarkdown from 'react-markdown'; import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import remarkRehype from 'remark-rehype';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import rehypeReact from 'rehype-react';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import rehypePrism from 'rehype-prism';
import * as prod from 'react/jsx-runtime';
import { notifications } from '@mantine/notifications';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import { lookupFileByName } from '../services/api'; import { remarkWikiLinks } from '../utils/remarkWikiLinks';
import { useWorkspace } from '../contexts/WorkspaceContext';
const MarkdownPreview = ({ content, handleLinkClick }) => { const MarkdownPreview = ({ content, handleFileSelect }) => {
const [processedContent, setProcessedContent] = useState(content); const [processedContent, setProcessedContent] = useState(null);
const baseUrl = window.API_BASE_URL; const baseUrl = window.API_BASE_URL;
const { currentWorkspace } = useWorkspace();
useEffect(() => { const handleLinkClick = (e, href) => {
const processContent = async (rawContent) => { e.preventDefault();
const regex = /(!?)\[\[(.*?)\]\]/g;
let result = rawContent;
const matches = [...rawContent.matchAll(regex)];
for (const match of matches) { if (href.startsWith(`${baseUrl}/internal/`)) {
const [fullMatch, isImage, innerContent] = match; // For existing files, extract the path and directly select it
let fileName, displayText, heading; const [filePath, heading] = decodeURIComponent(
href.replace(`${baseUrl}/internal/`, '')
).split('#');
handleFileSelect(filePath);
// Parse the inner content // TODO: Handle heading navigation if needed
const pipeIndex = innerContent.indexOf('|'); if (heading) {
const hashIndex = innerContent.indexOf('#'); console.debug('Heading navigation not implemented:', heading);
if (pipeIndex !== -1) {
displayText = innerContent.slice(pipeIndex + 1).trim();
fileName = innerContent.slice(0, pipeIndex).trim();
} else {
displayText = innerContent;
fileName = innerContent;
}
if (hashIndex !== -1 && (pipeIndex === -1 || hashIndex < pipeIndex)) {
heading = fileName.slice(hashIndex + 1).trim();
fileName = fileName.slice(0, hashIndex).trim();
}
try {
const paths = await lookupFileByName(fileName);
if (paths && paths.length > 0) {
const filePath = paths[0];
if (isImage) {
result = result.replace(
fullMatch,
`![${displayText}](${baseUrl}/files/${filePath})`
);
} else {
// Include heading in the URL if present
const url = heading
? `${baseUrl}/internal/${encodeURIComponent(
filePath
)}#${encodeURIComponent(heading)}`
: `${baseUrl}/internal/${encodeURIComponent(filePath)}`;
result = result.replace(fullMatch, `[${displayText}](${url})`);
}
} else {
result = result.replace(
fullMatch,
`[${displayText}](${baseUrl}/notfound/${encodeURIComponent(
fileName
)})`
);
}
} catch (error) {
console.error('Error looking up file:', error);
result = result.replace(
fullMatch,
`[${displayText}](${baseUrl}/notfound/${encodeURIComponent(
fileName
)})`
);
}
} }
} else if (href.startsWith(`${baseUrl}/notfound/`)) {
return result; // For non-existent files, show a notification
}; const fileName = decodeURIComponent(
href.replace(`${baseUrl}/notfound/`, '')
processContent(content).then(setProcessedContent); );
}, [content, baseUrl]); notifications.show({
title: 'File Not Found',
const handleImageError = (event) => { message: `The file "${fileName}" does not exist.`,
console.error('Failed to load image:', event.target.src); color: 'red',
event.target.alt = 'Failed to load image'; });
}
}; };
return ( const processor = useMemo(
<div className="markdown-preview"> () =>
<ReactMarkdown unified()
remarkPlugins={[remarkMath]} .use(remarkParse)
rehypePlugins={[rehypeKatex]} .use(remarkWikiLinks, currentWorkspace?.id)
components={{ .use(remarkMath)
code({ node, inline, className, children, ...props }) { .use(remarkRehype)
const match = /language-(\w+)/.exec(className || ''); .use(rehypeKatex)
return !inline && match ? ( .use(rehypePrism)
<SyntaxHighlighter .use(rehypeReact, {
style={vscDarkPlus} production: true,
language={match[1]} jsx: prod.jsx,
PreTag="div" jsxs: prod.jsxs,
Fragment: prod.Fragment,
components: {
img: ({ src, alt, ...props }) => (
<img
src={src}
alt={alt}
onError={(event) => {
console.error('Failed to load image:', event.target.src);
event.target.alt = 'Failed to load image';
}}
{...props}
/>
),
a: ({ href, children, ...props }) => (
<a
href={href}
onClick={(e) => handleLinkClick(e, href)}
{...props} {...props}
> >
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children} {children}
</code> </a>
); ),
}, code: ({ children, className, ...props }) => {
img: ({ src, alt, ...props }) => ( const language = className
<img src={src} alt={alt} onError={handleImageError} {...props} /> ? className.replace('language-', '')
), : null;
a: ({ href, children }) => {
if (href.startsWith(`${baseUrl}/internal/`)) {
const [filePath, heading] = decodeURIComponent(
href.replace(`${baseUrl}/internal/`, '')
).split('#');
return ( return (
<a <pre className={className}>
href="#" <code {...props}>{children}</code>
onClick={(e) => { </pre>
e.preventDefault();
handleLinkClick(filePath, heading);
}}
>
{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();
handleLinkClick(fileName);
}}
>
{children}
</a>
);
}
// Regular markdown link
return <a href={href}>{children}</a>;
}, },
}} }),
> [baseUrl, handleFileSelect, currentWorkspace?.id]
{processedContent}
</ReactMarkdown>
</div>
); );
useEffect(() => {
const processContent = async () => {
if (!currentWorkspace) {
return;
}
try {
const result = await processor.process(content);
setProcessedContent(result.result);
} catch (error) {
console.error('Error processing markdown:', error);
}
};
processContent();
}, [content, processor, currentWorkspace]);
return <div className="markdown-preview">{processedContent}</div>;
}; };
export default MarkdownPreview; export default MarkdownPreview;

View File

@@ -1,6 +1,4 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { notifications } from '@mantine/notifications';
import { lookupFileByName } from '../services/api';
import { DEFAULT_FILE } from '../utils/constants'; import { DEFAULT_FILE } from '../utils/constants';
import { useWorkspace } from '../contexts/WorkspaceContext'; import { useWorkspace } from '../contexts/WorkspaceContext';
import { useLastOpenedFile } from './useLastOpenedFile'; import { useLastOpenedFile } from './useLastOpenedFile';
@@ -24,33 +22,6 @@ export const useFileNavigation = () => {
[saveLastOpenedFile] [saveLastOpenedFile]
); );
const handleLinkClick = useCallback(
async (filename) => {
if (!currentWorkspace) return;
try {
const filePaths = await lookupFileByName(currentWorkspace.id, filename);
if (filePaths.length >= 1) {
handleFileSelect(filePaths[0]);
} else {
notifications.show({
title: 'File Not Found',
message: `File "${filename}" not found`,
color: 'red',
});
}
} catch (error) {
console.error('Error looking up file:', error);
notifications.show({
title: 'Error',
message: 'Failed to lookup file.',
color: 'red',
});
}
},
[currentWorkspace, handleFileSelect]
);
// Load last opened file when workspace changes // Load last opened file when workspace changes
useEffect(() => { useEffect(() => {
const initializeFile = async () => { const initializeFile = async () => {
@@ -65,5 +36,5 @@ export const useFileNavigation = () => {
initializeFile(); initializeFile();
}, [currentWorkspace, loadLastOpenedFile, handleFileSelect]); }, [currentWorkspace, loadLastOpenedFile, handleFileSelect]);
return { handleLinkClick, selectedFile, isNewFile, handleFileSelect }; return { selectedFile, isNewFile, handleFileSelect };
}; };

View File

@@ -0,0 +1,165 @@
import { visit } from 'unist-util-visit';
import { lookupFileByName, getFileUrl } from '../services/api';
function createNotFoundLink(fileName, displayText, baseUrl) {
return {
type: 'link',
url: `${baseUrl}/notfound/${encodeURIComponent(fileName)}`,
children: [{ type: 'text', value: displayText }],
data: {
hProperties: { style: { color: 'red', textDecoration: 'underline' } },
},
};
}
function createFileLink(filePath, displayText, heading, baseUrl) {
const url = heading
? `${baseUrl}/internal/${encodeURIComponent(filePath)}#${encodeURIComponent(
heading
)}`
: `${baseUrl}/internal/${encodeURIComponent(filePath)}`;
return {
type: 'link',
url,
children: [{ type: 'text', value: displayText }],
};
}
function createImageNode(workspaceId, filePath, displayText) {
return {
type: 'image',
url: getFileUrl(workspaceId, filePath),
alt: displayText,
title: displayText,
};
}
function addMarkdownExtension(fileName) {
if (fileName.includes('.')) {
return fileName;
}
return `${fileName}.md`;
}
export function remarkWikiLinks(workspaceId) {
return async function transformer(tree) {
if (!workspaceId) {
console.warn('No workspace ID provided to remarkWikiLinks plugin');
return;
}
const baseUrl = window.API_BASE_URL;
const replacements = new Map();
// Find all wiki links
visit(tree, 'text', function (node) {
const regex = /(!?)\[\[(.*?)\]\]/g;
let match;
const matches = [];
while ((match = regex.exec(node.value)) !== null) {
const [fullMatch, isImage, innerContent] = match;
let fileName, displayText, heading;
// Parse the inner content
const pipeIndex = innerContent.indexOf('|');
const hashIndex = innerContent.indexOf('#');
if (pipeIndex !== -1) {
displayText = innerContent.slice(pipeIndex + 1).trim();
fileName = innerContent.slice(0, pipeIndex).trim();
} else {
displayText = innerContent;
fileName = innerContent;
}
if (hashIndex !== -1 && (pipeIndex === -1 || hashIndex < pipeIndex)) {
heading = fileName.slice(hashIndex + 1).trim();
fileName = fileName.slice(0, hashIndex).trim();
}
matches.push({
fullMatch,
isImage,
fileName,
displayText,
heading,
index: match.index,
});
}
if (matches.length > 0) {
replacements.set(node, matches);
}
});
// Process all matches
for (const [node, matches] of replacements) {
const children = [];
let lastIndex = 0;
for (const match of matches) {
// Add text before the match
if (match.index > lastIndex) {
children.push({
type: 'text',
value: node.value.slice(lastIndex, match.index),
});
}
try {
// Add .md extension for non-image files if they don't have an extension
const lookupFileName = match.isImage
? match.fileName
: addMarkdownExtension(match.fileName);
const paths = await lookupFileByName(workspaceId, lookupFileName);
if (paths && paths.length > 0) {
const filePath = paths[0];
if (match.isImage) {
children.push(
createImageNode(workspaceId, filePath, match.displayText)
);
} else {
children.push(
createFileLink(
filePath,
match.displayText,
match.heading,
baseUrl
)
);
}
} else {
children.push(
createNotFoundLink(match.fileName, match.displayText, baseUrl)
);
}
} catch (error) {
// Handle both 404s and other errors by creating a "not found" link
console.debug('File lookup failed:', match.fileName, error);
children.push(
createNotFoundLink(match.fileName, match.displayText, baseUrl)
);
}
lastIndex = match.index + match.fullMatch.length;
}
// Add any remaining text
if (lastIndex < node.value.length) {
children.push({
type: 'text',
value: node.value.slice(lastIndex),
});
}
// Replace the node with new children
node.type = 'paragraph';
node.children = children;
delete node.value;
}
};
}