fix: resolve all TS errors, ESLint warnings, and npm warnings bump to v0.3.12

- Pin all @milkdown/* packages to exact 7.21.1, add phantom-dep plugins as direct deps

- Fix TextSelection.create type error in useMilkdown.ts

- Fix Backspace auto-pair dead-code bug (unreachable due to PAIRS check order)

- Extract parseHeadings/Heading to outlineUtils.ts (react-refresh fix)

- Extract StatusBar item components to StatusBarItems.tsx (react-refresh fix)

- Configure npm allowScripts for electron and esbuild postinstall

- Update QUALITY_REVIEW_REPORT.json with all fixes applied

- Bump version to 0.3.12
This commit is contained in:
2026-07-06 10:51:39 +08:00
parent f707b5c7ee
commit 0940c2f5c6
13 changed files with 253 additions and 172 deletions
+11 -7
View File
@@ -14,7 +14,7 @@ import { listener, listenerCtx } from '@milkdown/plugin-listener'
import { indent } from '@milkdown/plugin-indent'
import { trailing } from '@milkdown/plugin-trailing'
import { clipboard } from '@milkdown/plugin-clipboard'
import { Plugin, PluginKey } from '@milkdown/prose/state'
import { Plugin, PluginKey, TextSelection } from '@milkdown/prose/state'
import { Decoration, DecorationSet } from '@milkdown/prose/view'
import type { EditorState } from '@milkdown/prose/state'
@@ -37,15 +37,13 @@ function createAutoPairPlugin(): Plugin {
return false
},
handleKeyDown(view, event) {
// 处理配对符号的自动闭合
const char = event.key
if (!PAIRS[char]) return false
const { state } = view
const { from, to } = state.selection
// Backspace: 删除配对符号(光标在两个配对符号中间时)
// 必须在 PAIRS 检查之前处理,因为 'Backspace' 不在 PAIRS 映射中
if (char === 'Backspace') {
const { state } = view
const { from, to } = state.selection
if (from !== to || from < 2) return false
const before = state.doc.textBetween(from - 1, from)
const after = state.doc.textBetween(from, from + 1)
@@ -57,6 +55,12 @@ function createAutoPairPlugin(): Plugin {
return false
}
// 处理配对符号的自动闭合
if (!PAIRS[char]) return false
const { state } = view
const { from, to } = state.selection
if (from !== to) {
// 有选区时:包裹
const close = PAIRS[char]
@@ -70,7 +74,7 @@ function createAutoPairPlugin(): Plugin {
const tr = state.tr.insertText(char + close, from)
// 光标置于中间
tr.setSelection(
state.selection.constructor.create(tr.doc, from + 1)
TextSelection.create(tr.doc, from + 1)
)
view.dispatch(tr)
return true
@@ -1,36 +1,5 @@
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
}
import type { Heading } from './outlineUtils'
// --- Component ---
@@ -1,2 +1,3 @@
export { OutlinePanel, parseHeadings } from './OutlinePanel'
export type { Heading } from './OutlinePanel'
export { OutlinePanel } from './OutlinePanel'
export { parseHeadings } from './outlineUtils'
export type { Heading } from './outlineUtils'
@@ -0,0 +1,27 @@
export interface Heading {
level: number
text: string
/** Position in document (character offset from markdown source) */
pos: number
}
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
}
@@ -0,0 +1,64 @@
import { useTabStore } from '../../stores/tabStore'
import { useEditorStore } from '../../stores/editorStore'
import { getFileName } from '../../lib/fileUtils'
import { useDocStats } from '../../hooks/useDocStats'
import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner'
/** 文件信息 — 文件名或"就绪" */
export function FileInfoItem() {
const activeTab = useTabStore(s => s.getActiveTab())
return (
<span id="status-text">
{activeTab ? (activeTab.filePath ? getFileName(activeTab.filePath) : '未命名') : '就绪'}
</span>
)
}
/** 加载指示器 — 打开文件/加载目录/渲染中 */
export function LoadingItem() {
const loadingStates = useEditorStore(s => s.loadingStates)
const hasLoading = Object.values(loadingStates).some(Boolean)
if (!hasLoading) return null
const label = loadingStates['file-open']
? '正在打开文件...'
: loadingStates['dir-load']
? '正在加载目录...'
: loadingStates['markdown-render']
? '正在渲染...'
: ''
return (
<span className="status-loading" aria-busy="true">
<LoadingSpinner size="small" />
<span>{label}</span>
</span>
)
}
/** 文档统计 — 行数 / 词数 / 字符数 */
export function DocStatsItem() {
const activeTab = useTabStore(s => s.getActiveTab())
const stats = useDocStats(activeTab?.content)
if (!activeTab) return null
return (
<>
<span className="status-stat" title="行数" aria-label={`${stats.lines}`}>{stats.lines} </span>
<span className="status-divider">|</span>
<span className="status-stat" title="单词数" aria-label={`${stats.words} 个单词`}>{stats.words} </span>
<span className="status-divider">|</span>
<span className="status-stat" title="字符数(含空格)" aria-label={`${stats.chars} 个字符`}>{stats.chars} </span>
</>
)
}
/** 静态项 — 编码 */
export function EncodingItem() {
return <span id="status-encoding">UTF-8</span>
}
/** 静态项 — 语言 */
export function LangItem() {
return <span id="status-lang">Markdown</span>
}
+7 -64
View File
@@ -1,69 +1,12 @@
import { useEffect } from 'react'
import { useStatusBarStore } from '../stores/statusBarStore'
import { useTabStore } from '../stores/tabStore'
import { useEditorStore } from '../stores/editorStore'
import { useDocStats } from './useDocStats'
import { getFileName } from '../lib/fileUtils'
import { LoadingSpinner } from '../components/LoadingSpinner/LoadingSpinner'
/** 文件信息 — 文件名或"就绪" */
function FileInfoItem() {
const activeTab = useTabStore(s => s.getActiveTab())
return (
<span id="status-text">
{activeTab ? (activeTab.filePath ? getFileName(activeTab.filePath) : '未命名') : '就绪'}
</span>
)
}
/** 加载指示器 — 打开文件/加载目录/渲染中 */
function LoadingItem() {
const loadingStates = useEditorStore(s => s.loadingStates)
const hasLoading = Object.values(loadingStates).some(Boolean)
if (!hasLoading) return null
const label = loadingStates['file-open']
? '正在打开文件...'
: loadingStates['dir-load']
? '正在加载目录...'
: loadingStates['markdown-render']
? '正在渲染...'
: ''
return (
<span className="status-loading" aria-busy="true">
<LoadingSpinner size="small" />
<span>{label}</span>
</span>
)
}
/** 文档统计 — 行数 / 词数 / 字符数 */
function DocStatsItem() {
const activeTab = useTabStore(s => s.getActiveTab())
const stats = useDocStats(activeTab?.content)
if (!activeTab) return null
return (
<>
<span className="status-stat" title="行数" aria-label={`${stats.lines}`}>{stats.lines} </span>
<span className="status-divider">|</span>
<span className="status-stat" title="单词数" aria-label={`${stats.words} 个单词`}>{stats.words} </span>
<span className="status-divider">|</span>
<span className="status-stat" title="字符数(含空格)" aria-label={`${stats.chars} 个字符`}>{stats.chars} </span>
</>
)
}
/** 静态项 — 编码 */
function EncodingItem() {
return <span id="status-encoding">UTF-8</span>
}
/** 静态项 — 语言 */
function LangItem() {
return <span id="status-lang">Markdown</span>
}
import {
FileInfoItem,
LoadingItem,
DocStatsItem,
EncodingItem,
LangItem
} from '../components/StatusBar/StatusBarItems'
/**
* useDefaultStatusBarItems — 注册所有默认状态栏项(不含 auto-save)。
+1 -1
View File
@@ -1,5 +1,5 @@
// 共享常量 — 主进程和渲染进程共用
export const APP_VERSION = 'v0.3.11'
export const APP_VERSION = 'v0.3.12'
export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
export const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const
export const SKIP_DIRS = new Set([