feat: migrate to react-markdown for robust markdown rendering
- Replace custom markdown parser with react-markdown + remark-gfm - Fix document link navigation (./ and ../ references) - Simplify doc viewing flow (direct markdown content instead of parsed structure) - Update electron main process to only use api folder for docs - Add blueprint loading from docs/blueprint.md dynamically - Fix sidebar file selection path matching - Update preload scripts for new API structure
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ParsedDoc, DocTable, DocCodeBlock } from '@/lib/types'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import type { Components } from 'react-markdown'
|
||||
|
||||
interface DocContentProps {
|
||||
doc: ParsedDoc | null
|
||||
content: string
|
||||
onReferenceClick: (ref: string) => void
|
||||
}
|
||||
|
||||
export const DocContent = ({ doc, onReferenceClick }: DocContentProps) => {
|
||||
if (!doc) {
|
||||
export const DocContent = ({ content, onReferenceClick }: DocContentProps) => {
|
||||
if (!content) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
<p>Select a document from the sidebar</p>
|
||||
@@ -15,175 +16,83 @@ export const DocContent = ({ doc, onReferenceClick }: DocContentProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-white mb-4">{doc.title}</h1>
|
||||
|
||||
{doc.metadata.namespace && (
|
||||
<div className="mb-4">
|
||||
<span className="text-gray-400">Namespace: </span>
|
||||
<code className="bg-gray-800 px-2 py-1 rounded text-blue-400">{doc.metadata.namespace}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{doc.metadata.package && (
|
||||
<div className="mb-4">
|
||||
<span className="text-gray-400">Package: </span>
|
||||
<code className="bg-gray-800 px-2 py-1 rounded text-blue-400">{doc.metadata.package}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{doc.metadata.type && (
|
||||
<div className="mb-4">
|
||||
<span className="text-gray-400">Type: </span>
|
||||
<span className="text-yellow-400">{doc.metadata.type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{doc.metadata.inherits && (
|
||||
<div className="mb-4">
|
||||
<span className="text-gray-400">Inherits: </span>
|
||||
<code className="bg-gray-800 px-2 py-1 rounded text-purple-400">{doc.metadata.inherits}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{doc.metadata.description && (
|
||||
<p className="text-gray-300 mb-6">{doc.metadata.description}</p>
|
||||
)}
|
||||
|
||||
{doc.sections.map((section, idx) => (
|
||||
<Section
|
||||
key={idx}
|
||||
section={section}
|
||||
onReferenceClick={onReferenceClick}
|
||||
/>
|
||||
))}
|
||||
|
||||
{doc.references.length > 0 && (
|
||||
<div className="mt-8 pt-4 border-t border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">See Also</h3>
|
||||
<ul className="space-y-1">
|
||||
{doc.references.map((ref, idx) => (
|
||||
<li key={idx}>
|
||||
<button
|
||||
onClick={() => onReferenceClick(ref)}
|
||||
className="text-blue-400 hover:text-blue-300 hover:underline"
|
||||
>
|
||||
{ref}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
section: import('@/lib/types').DocSection
|
||||
onReferenceClick: (ref: string) => void
|
||||
}
|
||||
|
||||
const Section = ({ section, onReferenceClick }: SectionProps) => {
|
||||
const level = Math.min(section.level + 1, 6)
|
||||
const headingClass = clsx(
|
||||
'font-semibold text-white mb-3',
|
||||
section.level === 1 ? 'text-xl' : 'text-lg'
|
||||
)
|
||||
|
||||
const renderHeading = () => {
|
||||
switch (level) {
|
||||
case 2: return <h2 className={headingClass}>{section.title}</h2>
|
||||
case 3: return <h3 className={headingClass}>{section.title}</h3>
|
||||
case 4: return <h4 className={headingClass}>{section.title}</h4>
|
||||
case 5: return <h5 className={headingClass}>{section.title}</h5>
|
||||
case 6: return <h6 className={headingClass}>{section.title}</h6>
|
||||
default: return <h2 className={headingClass}>{section.title}</h2>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mb-6">
|
||||
{renderHeading()}
|
||||
|
||||
<div className="space-y-3">
|
||||
{section.content.map((item, idx) => {
|
||||
if (item.type === 'table') {
|
||||
return <TableContent key={idx} table={item.data as DocTable} />
|
||||
}
|
||||
if (item.type === 'code') {
|
||||
return <CodeContent key={idx} code={item.data as DocCodeBlock} />
|
||||
}
|
||||
return <TextContent key={idx} text={item.data as string} onReferenceClick={onReferenceClick} />
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const TextContent = ({ text, onReferenceClick }: { text: string; onReferenceClick: (ref: string) => void }) => {
|
||||
const renderText = () => {
|
||||
const parts = text.split(/(@see\s+[^\s]+)/g)
|
||||
return parts.map((part, idx) => {
|
||||
const match = part.match(/@see\s+([^\s]+)/)
|
||||
if (match) {
|
||||
const components: Components = {
|
||||
a: ({ href, children }) => {
|
||||
if (href && (href.startsWith('./') || href.startsWith('../'))) {
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => onReferenceClick(match[1])}
|
||||
onClick={() => onReferenceClick(href)}
|
||||
className="text-blue-400 hover:text-blue-300 hover:underline"
|
||||
>
|
||||
{part}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return <span key={idx}>{part}</span>
|
||||
})
|
||||
return (
|
||||
<a href={href} className="text-blue-400 hover:text-blue-300 hover:underline">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
if (match) {
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<code className="bg-gray-800 px-1.5 py-0.5 rounded text-blue-300 text-sm font-mono" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-gray-900 border border-gray-700 rounded-lg p-4 overflow-x-auto mb-4">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto mb-4">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-gray-800">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="border border-gray-600 px-3 py-2 text-left text-gray-200 font-semibold">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="border border-gray-600 px-3 py-2 text-gray-300">
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr className="hover:bg-gray-800/50">
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
}
|
||||
|
||||
return <p className="text-gray-300">{renderText()}</p>
|
||||
}
|
||||
|
||||
const TableContent = ({ table }: { table: DocTable }) => {
|
||||
if (!table.headers.length || !table.rows.length) return null
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-800">
|
||||
{table.headers.map((header, idx) => (
|
||||
<th key={idx} className="border border-gray-600 px-3 py-2 text-left text-gray-200 font-semibold">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.rows.map((row, rowIdx) => (
|
||||
<tr key={rowIdx} className="hover:bg-gray-800/50">
|
||||
{row.map((cell, cellIdx) => (
|
||||
<td key={cellIdx} className="border border-gray-600 px-3 py-2 text-gray-300">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="max-w-4xl mx-auto text-gray-200">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={components}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CodeContent = ({ code }: { code: DocCodeBlock }) => {
|
||||
return (
|
||||
<pre className="bg-gray-900 border border-gray-700 rounded-lg p-4 overflow-x-auto">
|
||||
<code className={`language-${code.language || 'text'} text-sm text-gray-200`}>
|
||||
{code.code}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user