diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c4e7e5e..1f9e44e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,10 +25,15 @@ "react": "^18.3.1", "react-arborist": "^3.4.0", "react-dom": "^18.3.1", - "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "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": { "@types/react": "^18.2.67", @@ -2011,12 +2016,14 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2103,6 +2110,12 @@ "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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2367,6 +2380,22 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2863,9 +2892,9 @@ } }, "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", - "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", + "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -3001,16 +3030,6 @@ "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": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", @@ -3914,6 +3933,18 @@ "dev": true, "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4274,32 +4305,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "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": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.2.tgz", @@ -4603,6 +4608,83 @@ "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", @@ -5085,6 +5167,23 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a153cb9..86459fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,10 +39,15 @@ "react": "^18.3.1", "react-arborist": "^3.4.0", "react-dom": "^18.3.1", - "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "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": { "@types/react": "^18.2.67", diff --git a/frontend/src/components/ContentView.jsx b/frontend/src/components/ContentView.jsx index 48351f3..24e686f 100644 --- a/frontend/src/components/ContentView.jsx +++ b/frontend/src/components/ContentView.jsx @@ -11,7 +11,7 @@ const ContentView = ({ content, handleContentChange, handleSave, - handleLinkClick, + handleFileSelect, }) => { if (!selectedFile) { return ( @@ -47,7 +47,7 @@ const ContentView = ({ selectedFile={selectedFile} /> ) : ( - + ); }; diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 6f2c5ed..74b64aa 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -9,8 +9,7 @@ import { useWorkspace } from '../contexts/WorkspaceContext'; const Layout = () => { const { currentWorkspace, loading: workspaceLoading } = useWorkspace(); - const { selectedFile, handleFileSelect, handleLinkClick } = - useFileNavigation(); + const { selectedFile, handleFileSelect } = useFileNavigation(); const { files, loadFileList } = useFileList(); if (workspaceLoading) { @@ -49,7 +48,6 @@ const Layout = () => { diff --git a/frontend/src/components/MainContent.jsx b/frontend/src/components/MainContent.jsx index 637490f..731b250 100644 --- a/frontend/src/components/MainContent.jsx +++ b/frontend/src/components/MainContent.jsx @@ -12,12 +12,7 @@ import { useFileOperations } from '../hooks/useFileOperations'; import { useGitOperations } from '../hooks/useGitOperations'; import { useWorkspace } from '../contexts/WorkspaceContext'; -const MainContent = ({ - selectedFile, - handleFileSelect, - handleLinkClick, - loadFileList, -}) => { +const MainContent = ({ selectedFile, handleFileSelect, loadFileList }) => { const [activeTab, setActiveTab] = useState('source'); const { settings } = useWorkspace(); const { @@ -113,7 +108,7 @@ const MainContent = ({ content={content} handleContentChange={handleContentChange} handleSave={handleSaveFile} - handleLinkClick={handleLinkClick} + handleFileSelect={handleFileSelect} /> diff --git a/frontend/src/components/MarkdownPreview.jsx b/frontend/src/components/MarkdownPreview.jsx index ea12549..cf0e0a3 100644 --- a/frontend/src/components/MarkdownPreview.jsx +++ b/frontend/src/components/MarkdownPreview.jsx @@ -1,159 +1,117 @@ -import React, { useState, useEffect } from 'react'; -import ReactMarkdown from 'react-markdown'; +import React, { useState, useEffect, useMemo } from 'react'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; import remarkMath from 'remark-math'; +import remarkRehype from 'remark-rehype'; import rehypeKatex from 'rehype-katex'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import rehypeReact from 'rehype-react'; +import rehypePrism from 'rehype-prism'; +import * as prod from 'react/jsx-runtime'; +import { notifications } from '@mantine/notifications'; 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 [processedContent, setProcessedContent] = useState(content); +const MarkdownPreview = ({ content, handleFileSelect }) => { + const [processedContent, setProcessedContent] = useState(null); const baseUrl = window.API_BASE_URL; + const { currentWorkspace } = useWorkspace(); - useEffect(() => { - const processContent = async (rawContent) => { - const regex = /(!?)\[\[(.*?)\]\]/g; - let result = rawContent; - const matches = [...rawContent.matchAll(regex)]; + const handleLinkClick = (e, href) => { + e.preventDefault(); - for (const match of matches) { - const [fullMatch, isImage, innerContent] = match; - let fileName, displayText, heading; + if (href.startsWith(`${baseUrl}/internal/`)) { + // For existing files, extract the path and directly select it + const [filePath, heading] = decodeURIComponent( + href.replace(`${baseUrl}/internal/`, '') + ).split('#'); + handleFileSelect(filePath); - // 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(); - } - - 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 - )})` - ); - } + // TODO: Handle heading navigation if needed + if (heading) { + console.debug('Heading navigation not implemented:', heading); } - - return result; - }; - - processContent(content).then(setProcessedContent); - }, [content, baseUrl]); - - const handleImageError = (event) => { - console.error('Failed to load image:', event.target.src); - event.target.alt = 'Failed to load image'; + } else if (href.startsWith(`${baseUrl}/notfound/`)) { + // For non-existent files, show a notification + const fileName = decodeURIComponent( + href.replace(`${baseUrl}/notfound/`, '') + ); + notifications.show({ + title: 'File Not Found', + message: `The file "${fileName}" does not exist.`, + color: 'red', + }); + } }; - return ( -
- + unified() + .use(remarkParse) + .use(remarkWikiLinks, currentWorkspace?.id) + .use(remarkMath) + .use(remarkRehype) + .use(rehypeKatex) + .use(rehypePrism) + .use(rehypeReact, { + production: true, + jsx: prod.jsx, + jsxs: prod.jsxs, + Fragment: prod.Fragment, + components: { + img: ({ src, alt, ...props }) => ( + {alt} { + console.error('Failed to load image:', event.target.src); + event.target.alt = 'Failed to load image'; + }} + {...props} + /> + ), + a: ({ href, children, ...props }) => ( + handleLinkClick(e, href)} {...props} > - {String(children).replace(/\n$/, '')} - - ) : ( - {children} - - ); - }, - img: ({ src, alt, ...props }) => ( - {alt} - ), - a: ({ href, children }) => { - if (href.startsWith(`${baseUrl}/internal/`)) { - const [filePath, heading] = decodeURIComponent( - href.replace(`${baseUrl}/internal/`, '') - ).split('#'); + + ), + code: ({ children, className, ...props }) => { + const language = className + ? className.replace('language-', '') + : null; return ( - { - e.preventDefault(); - handleLinkClick(filePath, heading); - }} - > - {children} - +
+                  {children}
+                
); - } else if (href.startsWith(`${baseUrl}/notfound/`)) { - const fileName = decodeURIComponent( - href.replace(`${baseUrl}/notfound/`, '') - ); - return ( - { - e.preventDefault(); - handleLinkClick(fileName); - }} - > - {children} - - ); - } - // Regular markdown link - return {children}; + }, }, - }} - > - {processedContent} -
-
+ }), + [baseUrl, handleFileSelect, currentWorkspace?.id] ); + + 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
{processedContent}
; }; export default MarkdownPreview; diff --git a/frontend/src/hooks/useFileNavigation.js b/frontend/src/hooks/useFileNavigation.js index 0803e64..e98eb95 100644 --- a/frontend/src/hooks/useFileNavigation.js +++ b/frontend/src/hooks/useFileNavigation.js @@ -1,6 +1,4 @@ import { useState, useCallback, useEffect } from 'react'; -import { notifications } from '@mantine/notifications'; -import { lookupFileByName } from '../services/api'; import { DEFAULT_FILE } from '../utils/constants'; import { useWorkspace } from '../contexts/WorkspaceContext'; import { useLastOpenedFile } from './useLastOpenedFile'; @@ -24,33 +22,6 @@ export const useFileNavigation = () => { [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 useEffect(() => { const initializeFile = async () => { @@ -65,5 +36,5 @@ export const useFileNavigation = () => { initializeFile(); }, [currentWorkspace, loadLastOpenedFile, handleFileSelect]); - return { handleLinkClick, selectedFile, isNewFile, handleFileSelect }; + return { selectedFile, isNewFile, handleFileSelect }; }; diff --git a/frontend/src/utils/remarkWikiLinks.js b/frontend/src/utils/remarkWikiLinks.js new file mode 100644 index 0000000..1cfcd23 --- /dev/null +++ b/frontend/src/utils/remarkWikiLinks.js @@ -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; + } + }; +}