Files
RawMark/web/src/editor/format.test.ts
T
eezd 91da6e116e Add mobile format toolbar, image picker and move-to dialog
- Markdown format commands shared by Ctrl+B/I/K and a toolbar above the
  soft keyboard on touch devices; bold and italic stack correctly
- Fit the app to the visual viewport so the keyboard does not cover the
  caret or the toolbar
- Insert images from the photo library or a file picker
- Move notes and folders with a folder picker from the file tree menu or
  the command palette
- Open the file tree menu with a long press on touch devices
2026-10-17 17:12:43 +08:00

72 lines
3.0 KiB
TypeScript

import { EditorSelection, EditorState } from '@codemirror/state'
import { describe, expect, it } from 'vitest'
import { cycleHeading, insertLink, toggleBlock, toggleBold, toggleCode, toggleItalic, type FormatCommand } from './format'
// 用 | 标记光标、[ ] 标记选区,执行命令后以同样的形式返回结果
function run(cmd: FormatCommand, doc: string): string {
const ranges: { anchor: number; head: number }[] = []
let text = ''
let start = -1
for (const ch of doc) {
if (ch === '|') ranges.push({ anchor: text.length, head: text.length })
else if (ch === '[' && start < 0 && doc.includes(']')) start = text.length
else if (ch === ']' && start >= 0) {
ranges.push({ anchor: start, head: text.length })
start = -2
} else text += ch
}
let state = EditorState.create({
doc: text,
selection: EditorSelection.create(ranges.map((r) => EditorSelection.range(r.anchor, r.head))),
extensions: EditorState.allowMultipleSelections.of(true),
})
cmd({ state, dispatch: (spec) => (state = state.update(spec).state) })
let out = state.doc.toString()
for (const r of [...state.selection.ranges].reverse()) {
out = r.empty ? out.slice(0, r.from) + '|' + out.slice(r.from) : out.slice(0, r.from) + '[' + out.slice(r.from, r.to) + ']' + out.slice(r.to)
}
return out
}
describe('行内标记', () => {
it('加上与去掉', () => {
expect(run(toggleBold, 'a [bc] d')).toBe('a **[bc]** d')
expect(run(toggleBold, 'a **[bc]** d')).toBe('a [bc] d')
expect(run(toggleBold, 'a [**bc**] d')).toBe('a [bc] d')
expect(run(toggleCode, 'x|')).toBe('x`|`')
})
it('粗体与斜体叠加', () => {
expect(run(toggleItalic, '**[x]**')).toBe('***[x]***')
expect(run(toggleItalic, '***[x]***')).toBe('**[x]**')
expect(run(toggleBold, '***[x]***')).toBe('*[x]*')
expect(run(toggleBold, '*[x]*')).toBe('***[x]***')
})
})
describe('行首标记', () => {
it('列表、任务、引用:同类再按去掉,异类替换', () => {
expect(run(toggleBlock('bullet'), 'a|')).toBe('- a|')
expect(run(toggleBlock('bullet'), '- a|')).toBe('a|')
expect(run(toggleBlock('task'), '- a|')).toBe('- [ ] a|')
expect(run(toggleBlock('quote'), '## 标题|')).toBe('> 标题|')
expect(run(toggleBlock('bullet'), '|')).toBe('- |')
})
it('多行:有序列表按行编号,保留缩进', () => {
expect(run(toggleBlock('ordered'), '[a\n b\nc]')).toBe('[1. a\n 2. b\n3. c]')
expect(run(toggleBlock('ordered'), '[1. a\n2. b]')).toBe('[a\nb]')
// 部分行已是该类型时统一加上
expect(run(toggleBlock('bullet'), '[- a\nb]')).toBe('[- a\n- b]')
})
it('标题循环', () => {
expect(run(cycleHeading, '标题|')).toBe('# 标题|')
expect(run(cycleHeading, '# 标题|')).toBe('## 标题|')
expect(run(cycleHeading, '### 标题|')).toBe('标题|')
expect(run(cycleHeading, '- 项|')).toBe('# 项|')
})
})
it('链接', () => {
expect(run(insertLink, '看 [文档] 吧')).toBe('看 [文档](|) 吧')
expect(run(insertLink, '看|')).toBe('看[|]()')
})