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:
@@ -31,148 +31,181 @@ export function parseMarkdown(content: string): ParsedDoc {
|
||||
let tableBuffer: string[] = []
|
||||
let inTable = false
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmedLine = line.trim()
|
||||
try {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
const refMatch = trimmedLine.match(REFERENCE_REGEX)
|
||||
if (refMatch) {
|
||||
let match
|
||||
while ((match = REFERENCE_REGEX.exec(trimmedLine)) !== null) {
|
||||
references.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
const codeMatch = line.match(CODE_BLOCK_REGEX)
|
||||
if (codeMatch) {
|
||||
if (currentSection) {
|
||||
currentSection.content = [...currentContent]
|
||||
sections.push(currentSection)
|
||||
currentSection = null
|
||||
}
|
||||
inCodeBlock = true
|
||||
codeBlockLanguage = codeMatch[1] || ''
|
||||
currentContent = []
|
||||
continue
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
if (trimmedLine === '```') {
|
||||
currentContent.push({
|
||||
type: 'code',
|
||||
data: { language: codeBlockLanguage, code: codeBlockLines.join('\n') },
|
||||
} as { type: 'code'; data: DocCodeBlock })
|
||||
codeBlockLines = []
|
||||
codeBlockLanguage = ''
|
||||
inCodeBlock = false
|
||||
|
||||
if (!currentSection) {
|
||||
currentSection = { title: '', level: 2, content: [] }
|
||||
try {
|
||||
const refMatch = trimmedLine.match(REFERENCE_REGEX)
|
||||
if (refMatch) {
|
||||
let match
|
||||
const refRegex = /@see\s+([^\s]+)/g
|
||||
while ((match = refRegex.exec(trimmedLine)) !== null) {
|
||||
references.push(match[1])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
codeBlockLines.push(line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (HR_REGEX.test(trimmedLine)) {
|
||||
if (inTable && tableBuffer.length > 0) {
|
||||
const table = parseTable(tableBuffer)
|
||||
if (table) {
|
||||
currentContent.push({ type: 'table', data: table })
|
||||
const codeMatch = line.match(CODE_BLOCK_REGEX)
|
||||
if (codeMatch) {
|
||||
if (currentSection) {
|
||||
currentSection.content = [...currentContent]
|
||||
sections.push(currentSection)
|
||||
currentSection = null
|
||||
}
|
||||
inCodeBlock = true
|
||||
codeBlockLanguage = codeMatch[1] || ''
|
||||
currentContent = []
|
||||
continue
|
||||
}
|
||||
tableBuffer = []
|
||||
inTable = false
|
||||
|
||||
if (inCodeBlock) {
|
||||
if (trimmedLine === '```' || trimmedLine.startsWith('```')) {
|
||||
currentContent.push({
|
||||
type: 'code',
|
||||
data: { language: codeBlockLanguage, code: codeBlockLines.join('\n') },
|
||||
} as { type: 'code'; data: DocCodeBlock })
|
||||
codeBlockLines = []
|
||||
codeBlockLanguage = ''
|
||||
inCodeBlock = false
|
||||
|
||||
if (!currentSection) {
|
||||
currentSection = { title: '', level: 2, content: [] }
|
||||
}
|
||||
} else {
|
||||
codeBlockLines.push(line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (HR_REGEX.test(trimmedLine)) {
|
||||
if (inTable && tableBuffer.length > 0) {
|
||||
const table = parseTable(tableBuffer)
|
||||
if (table) {
|
||||
currentContent.push({ type: 'table', data: table })
|
||||
}
|
||||
tableBuffer = []
|
||||
inTable = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!result.title && line.startsWith('# ')) {
|
||||
result.title = line.slice(2).trim()
|
||||
continue
|
||||
}
|
||||
|
||||
const headingMatch = line.match(HEADING_REGEX)
|
||||
if (headingMatch) {
|
||||
if (currentSection) {
|
||||
currentSection.content = [...currentContent]
|
||||
sections.push(currentSection)
|
||||
}
|
||||
const level = headingMatch[1].length
|
||||
const title = headingMatch[2].trim()
|
||||
currentSection = { title, level, content: [] }
|
||||
currentContent = []
|
||||
inTable = false
|
||||
tableBuffer = []
|
||||
continue
|
||||
}
|
||||
|
||||
const metaMatch = trimmedLine.match(METADATA_REGEX)
|
||||
if (metaMatch) {
|
||||
const key = metaMatch[1].toLowerCase()
|
||||
const value = metaMatch[2].trim()
|
||||
if (key === 'namespace') metadata.namespace = value
|
||||
else if (key === 'description') metadata.description = value
|
||||
else if (key === 'type') metadata.type = value
|
||||
else if (key === 'inherits') metadata.inherits = value
|
||||
else if (key === 'package') metadata.package = value
|
||||
continue
|
||||
}
|
||||
|
||||
const tableMatch = line.match(TABLE_REGEX)
|
||||
if (tableMatch) {
|
||||
tableBuffer.push(trimmedLine)
|
||||
inTable = true
|
||||
continue
|
||||
} else if (inTable && tableBuffer.length > 0) {
|
||||
const table = parseTable(tableBuffer)
|
||||
if (table) {
|
||||
currentContent.push({ type: 'table', data: table })
|
||||
}
|
||||
tableBuffer = []
|
||||
inTable = false
|
||||
}
|
||||
|
||||
if (trimmedLine) {
|
||||
currentContent.push({ type: 'text', data: trimmedLine })
|
||||
}
|
||||
} catch (lineError) {
|
||||
console.warn('Error parsing line', i, ':', lineError)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!result.title && line.startsWith('# ')) {
|
||||
result.title = line.slice(2).trim()
|
||||
continue
|
||||
// 结束未闭合的代码块
|
||||
if (inCodeBlock && codeBlockLines.length > 0) {
|
||||
currentContent.push({
|
||||
type: 'code',
|
||||
data: { language: codeBlockLanguage, code: codeBlockLines.join('\n') },
|
||||
} as { type: 'code'; data: DocCodeBlock })
|
||||
}
|
||||
|
||||
const headingMatch = line.match(HEADING_REGEX)
|
||||
if (headingMatch) {
|
||||
if (currentSection) {
|
||||
currentSection.content = [...currentContent]
|
||||
sections.push(currentSection)
|
||||
}
|
||||
const level = headingMatch[1].length
|
||||
const title = headingMatch[2].trim()
|
||||
currentSection = { title, level, content: [] }
|
||||
currentContent = []
|
||||
inTable = false
|
||||
tableBuffer = []
|
||||
continue
|
||||
}
|
||||
|
||||
const metaMatch = trimmedLine.match(METADATA_REGEX)
|
||||
if (metaMatch) {
|
||||
const key = metaMatch[1].toLowerCase()
|
||||
const value = metaMatch[2].trim()
|
||||
if (key === 'namespace') metadata.namespace = value
|
||||
else if (key === 'description') metadata.description = value
|
||||
else if (key === 'type') metadata.type = value
|
||||
else if (key === 'inherits') metadata.inherits = value
|
||||
else if (key === 'package') metadata.package = value
|
||||
continue
|
||||
}
|
||||
|
||||
const tableMatch = line.match(TABLE_REGEX)
|
||||
if (tableMatch) {
|
||||
const isSeparator = /^[\s|*-]+$/.test(trimmedLine.replace(/\|/g, '').trim())
|
||||
if (!isSeparator) {
|
||||
tableBuffer.push(trimmedLine)
|
||||
inTable = true
|
||||
} else {
|
||||
inTable = true
|
||||
}
|
||||
continue
|
||||
} else if (inTable && tableBuffer.length > 0) {
|
||||
// 结束未闭合的表格
|
||||
if (inTable && tableBuffer.length > 0) {
|
||||
const table = parseTable(tableBuffer)
|
||||
if (table) {
|
||||
currentContent.push({ type: 'table', data: table })
|
||||
}
|
||||
tableBuffer = []
|
||||
inTable = false
|
||||
}
|
||||
|
||||
if (trimmedLine) {
|
||||
currentContent.push({ type: 'text', data: trimmedLine })
|
||||
if (currentSection) {
|
||||
currentSection.content = [...currentContent]
|
||||
sections.push(currentSection)
|
||||
}
|
||||
|
||||
result.metadata = metadata
|
||||
result.sections = sections
|
||||
result.references = references
|
||||
} catch (error) {
|
||||
console.error('Markdown parse error:', error)
|
||||
return {
|
||||
title: '',
|
||||
metadata: {},
|
||||
sections: [],
|
||||
references: [],
|
||||
parseError: true,
|
||||
rawContent: content
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSection) {
|
||||
currentSection.content = [...currentContent]
|
||||
sections.push(currentSection)
|
||||
}
|
||||
|
||||
result.metadata = metadata
|
||||
result.sections = sections
|
||||
result.references = references
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function parseTable(tableLines: string[]): DocTable | null {
|
||||
if (tableLines.length < 1) return null
|
||||
try {
|
||||
if (tableLines.length < 1) return null
|
||||
|
||||
const headers = tableLines[0].split('|').filter(h => h.trim()).map(h => h.trim())
|
||||
const rows: string[][] = []
|
||||
const headers = tableLines[0].split('|').filter(h => h.trim()).map(h => h.trim())
|
||||
const rows: string[][] = []
|
||||
|
||||
for (let i = 1; i < tableLines.length; i++) {
|
||||
const line = tableLines[i]
|
||||
const isSeparator = /^[\s|*-]+$/.test(line.replace(/\|/g, '').trim())
|
||||
if (isSeparator) continue
|
||||
|
||||
const cells = line.split('|').filter(c => c.trim()).map(c => c.trim())
|
||||
if (cells.length > 0) {
|
||||
rows.push(cells)
|
||||
for (let i = 1; i < tableLines.length; i++) {
|
||||
const line = tableLines[i]
|
||||
const cleaned = line.replace(/\|/g, '').trim()
|
||||
const isSeparator = /^[-\s]+$/.test(cleaned)
|
||||
if (isSeparator) continue
|
||||
|
||||
const cells = line.split('|').filter(c => c.trim()).map(c => c.trim())
|
||||
if (cells.length > 0) {
|
||||
rows.push(cells)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { headers, rows }
|
||||
return { headers, rows }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getDisplayName(filename: string): string {
|
||||
|
||||
Reference in New Issue
Block a user