oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1// text-keys.mjs's commands over ProseMirror state alone: the link
2// commands and the keymap's shape. No DOM, no view — EditorState is
3// enough to run a command and read the marks it left.
4import test from 'node:test'
5import assert from 'node:assert/strict'
6import { EditorState, TextSelection } from 'prosemirror-state'
7import { schema, docFromParagraphs } from './schema.mjs'
8import { linkAt, setLink, removeLink, textKeys } from './text-keys.mjs'
9
10function state (paragraphs, from, to = from) {
11 const doc = docFromParagraphs(paragraphs)
12 let s = EditorState.create({ schema, doc })
13 return s.apply(s.tr.setSelection(TextSelection.create(s.doc, from, to)))
14}
15
16function run (s, cmd) {
17 let next = s
18 const ok = cmd(s, tr => { next = s.apply(tr) })
19 return { ok, state: next }
20}
21
22function marksOf (s) {
23 const out = []
24 s.doc.descendants(n => { if (n.isText) out.push([n.text, n.marks.map(m => m.type.name === 'href' ? m.attrs.href : m.type.name)]) })
25 return out
26}
27
28test('linkAt reads the link under the cursor and nothing elsewhere', () => {
29 const s = state([[{ t: 'see ' }, { t: 'this', href: 'https://a' }, { t: ' now' }]], 6)
30 assert.equal(linkAt(s), 'https://a')
31 assert.equal(linkAt(state([[{ t: 'plain' }]], 2)), null)
32})
33
34test('setLink marks the selection', () => {
35 const { ok, state: s } = run(state([[{ t: 'hello world' }]], 1, 6), setLink('https://x'))
36 assert.ok(ok)
37 assert.deepEqual(marksOf(s), [['hello', ['https://x']], [' world', []]])
38})
39
40test('setLink on a collapsed cursor inside a link replaces that link', () => {
41 const { ok, state: s } = run(state([[{ t: 'see ' }, { t: 'this', href: 'https://a' }, { t: ' now' }]], 6), setLink('https://b'))
42 assert.ok(ok)
43 assert.deepEqual(marksOf(s), [['see ', []], ['this', ['https://b']], [' now', []]])
44})
45
46test('setLink with nothing selected and no link does nothing', () => {
47 const { ok } = run(state([[{ t: 'plain' }]], 2), setLink('https://x'))
48 assert.equal(ok, false)
49})
50
51test('removeLink clears the link under the cursor', () => {
52 const { ok, state: s } = run(state([[{ t: 'see ' }, { t: 'this', href: 'https://a' }, { t: ' now' }]], 6), removeLink)
53 assert.ok(ok)
54 assert.deepEqual(marksOf(s), [['see this now', []]])
55})
56
57test('the keymap binds every documented shortcut', () => {
58 const keys = textKeys({ undo: () => {}, redo: () => {}, link: () => {} })
59 for (const k of ['Mod-b', 'Mod-i', 'Mod-e', 'Mod-Shift-h', 'Mod-k', 'Mod-z', 'Mod-Shift-z', 'Mod-y']) {
60 assert.equal(typeof keys[k], 'function', k)
61 }
62})
63
64test('Mod-b toggles bold through the keymap', () => {
65 const keys = textKeys({ undo: () => {}, redo: () => {}, link: () => {} })
66 const { state: s } = run(state([[{ t: 'hello' }]], 1, 6), keys['Mod-b'])
67 assert.deepEqual(marksOf(s), [['hello', ['b']]])
68})
69
70test('setLink refuses an address outside the allowlist and writes nothing', () => {
71 for (const bad of ['javascript:alert(1)', 'data:text/html,hi', 'JavaScript:void(0)', '/relative', ' https://padded', '']) {
72 const s = state([[{ t: 'click here' }]], 1, 6)
73 const { ok, state: next } = run(s, setLink(bad))
74 assert.equal(ok, false, bad)
75 assert.deepEqual(marksOf(next), [['click here', []]], bad)
76 }
77 const { ok, state: next } = run(state([[{ t: 'click here' }]], 1, 6), setLink('mailto:a@b.c'))
78 assert.equal(ok, true)
79 assert.deepEqual(marksOf(next), [['click', ['mailto:a@b.c']], [' here', []]])
80})
81