- 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
72 lines
3.0 KiB
TypeScript
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('看[|]()')
|
|
})
|