v0.3.4: Bug修复+性能优化+文档大纲功能
🔴 Bug修复 (6): - Sidebar反斜杠正则修复 (Windows路径规范化) - FileWatcher文件删除后自动恢复监听 - SearchReplace replaceAll防过期匹配位置 - openFolderDialog null结果守卫 - 保存异常时空路径保护 - Markdown绝对路径图片拼接修复 🟡 性能优化: - Editor切换标签时内容比较,相同跳过replaceAll 🔵 代码质量: - 创建共享常量模块 src/shared/constants.ts,消除双副本 🎯 新增功能: - 文档大纲 (Table of Contents) — 侧边栏标题导航 🔧 换行符统一: CRLF → LF
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import React, { memo } from 'react'
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface Heading {
|
||||
level: number
|
||||
text: string
|
||||
/** Position in document (character offset from markdown source) */
|
||||
pos: number
|
||||
}
|
||||
|
||||
// --- Heading Parser ---
|
||||
|
||||
const HEADING_RE = /^(#{1,6})\s+(.+)$/gm
|
||||
|
||||
/**
|
||||
* Parse headings from raw markdown content using regex.
|
||||
*/
|
||||
export function parseHeadings(markdown: string): Heading[] {
|
||||
const headings: Heading[] = []
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
// Reset regex state
|
||||
HEADING_RE.lastIndex = 0
|
||||
|
||||
while ((match = HEADING_RE.exec(markdown)) !== null) {
|
||||
const level = match[1].length
|
||||
const text = match[2].trim()
|
||||
headings.push({ level, text, pos: match.index })
|
||||
}
|
||||
|
||||
return headings
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
interface OutlinePanelProps {
|
||||
headings: Heading[]
|
||||
onNavigate: (pos: number) => void
|
||||
activeHeadingIndex: number | null
|
||||
}
|
||||
|
||||
interface OutlineItemProps {
|
||||
heading: Heading
|
||||
isActive: boolean
|
||||
onNavigate: (pos: number) => void
|
||||
}
|
||||
|
||||
const OutlineItem = memo(function OutlineItem({
|
||||
heading,
|
||||
isActive,
|
||||
onNavigate
|
||||
}: OutlineItemProps) {
|
||||
return (
|
||||
<button
|
||||
className={`outline-item outline-level-${heading.level}${isActive ? ' active' : ''}`}
|
||||
onClick={() => onNavigate(heading.pos)}
|
||||
title={heading.text}
|
||||
aria-label={`跳转到标题:${heading.text}`}
|
||||
style={{ paddingLeft: `${8 + (heading.level - 1) * 12}px` }}
|
||||
>
|
||||
<span className="outline-level-dot" />
|
||||
<span className="outline-item-text">{heading.text}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
export const OutlinePanel = memo(function OutlinePanel({
|
||||
headings,
|
||||
onNavigate,
|
||||
activeHeadingIndex
|
||||
}: OutlinePanelProps) {
|
||||
if (headings.length === 0) {
|
||||
return (
|
||||
<div className="outline-panel" role="region" aria-label="文档大纲">
|
||||
<div className="outline-header">文档大纲</div>
|
||||
<div className="outline-empty">当前文档无标题</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="outline-panel" role="region" aria-label="文档大纲">
|
||||
<div className="outline-header">文档大纲</div>
|
||||
<div className="outline-list" role="list" aria-label="标题列表">
|
||||
{headings.map((h, i) => (
|
||||
<OutlineItem
|
||||
key={`${h.text}-${h.pos}`}
|
||||
heading={h}
|
||||
isActive={i === activeHeadingIndex}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
OutlinePanel.displayName = 'OutlinePanel'
|
||||
Reference in New Issue
Block a user