import { describe, it, expect } from 'vitest' import { countWords, countChars, extractOutline, baseName, isMarkdownPath, pathsEqual, isPathInside, parentDirOf } from '../src/utils/text' describe('字数统计', () => { it('中文按字、英文按词、标点不计', () => { expect(countWords('你好,世界!')).toBe(4) expect(countWords('hello world, again')).toBe(3) expect(countWords('中文 mixed 文本 test')).toBe(2 + 1 + 2 + 1) }) it('代码块可排除', () => { const md = '正文一二\n\n```\ncode code code\n```\n' expect(countWords(md, true)).toBe(4 + 3) expect(countWords(md, false)).toBe(4) }) it('字符数按 Unicode 码点计', () => { expect(countChars('a你好b')).toBe(4) }) }) describe('大纲提取', () => { it('提取层级与行号,跳过代码块内的 # 行', () => { const md = '# 一级\n\n```\n# 不是标题\n```\n\n## 二级\n正文\n### 三级\n' const outline = extractOutline(md) expect(outline.map((o) => o.text)).toEqual(['一级', '二级', '三级']) expect(outline.map((o) => o.level)).toEqual([1, 2, 3]) expect(outline[1].line).toBe(6) }) }) describe('路径工具', () => { it('Windows 路径取文件名', () => { expect(baseName('C:\\docs\\笔记\\a.md')).toBe('a.md') expect(baseName('/home/x/b.md')).toBe('b.md') }) it('Markdown 路径判定', () => { expect(isMarkdownPath('a.MD')).toBe(true) expect(isMarkdownPath('a.txt')).toBe(false) }) }) describe('路径助手(拖放/改名校验)', () => { it('分隔符混用也能判等与取父目录', async () => { const { pathsEqual, parentDirOf, joinPathFs } = await import('../src/utils/text') expect(pathsEqual('C:\\ws\\sub', 'C:/ws/sub')).toBe(true) expect(pathsEqual('C:/ws/a.md', 'C:/ws/b.md')).toBe(false) expect(parentDirOf('C:/ws/sub/a.md')).toBe('C:/ws/sub') expect(parentDirOf('C:\\ws\\a.md')).toBe('C:\\ws') expect(joinPathFs('C:\\ws', 'a.md')).toBe('C:\\ws\\a.md') expect(joinPathFs('C:/ws/', 'a.md')).toBe('C:/ws/a.md') }) it('isPathInside 只认真正的子路径', async () => { const { isPathInside } = await import('../src/utils/text') expect(isPathInside('C:/ws/sub/deep', 'C:/ws/sub')).toBe(true) expect(isPathInside('C:/ws/sub', 'C:/ws/sub')).toBe(false) expect(isPathInside('C:/ws/sub2', 'C:/ws/sub')).toBe(false) expect(isPathInside('C:\\ws\\sub\\deep', 'C:/ws/sub')).toBe(true) }) }) describe('路径比较与大纲围栏(docs/29)', () => { it('Windows 路径大小写/斜杠归一后相等,POSIX 路径仍分大小写', () => { expect(pathsEqual('C:\\WS\\A.md', 'c:/ws/a.md')).toBe(true) expect(pathsEqual('/home/A.md', '/home/a.md')).toBe(false) expect(isPathInside('c:/ws/sub/a.md', 'C:\\WS')).toBe(true) expect(isPathInside('C:/ws2/a.md', 'C:/ws')).toBe(false) }) it('parentDirOf 盘符根文件返回盘符根', () => { expect(parentDirOf('C:/a.md')).toBe('C:/') expect(parentDirOf('C:\\dir\\a.md')).toBe('C:\\dir') expect(parentDirOf('/a.md')).toBe('/') }) it('四反引号围栏内的三反引号与 # 行不干扰大纲', () => { const md = ['# 标题一', '', '````md', '```', '# 假标题', '```', '````', '', '# 标题二'].join('\n') const outline = extractOutline(md) expect(outline.map((o) => o.text)).toEqual(['标题一', '标题二']) }) })