oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/autoformat.mjs — typing "- " makes a bullet.
2//
3// Input rules: a pattern that matches the text just typed at the end of
4// a textblock, and a transformation applied when it does. Everything
5// here is the same shape a person has met in every editor since
6// Markdown, which is the whole argument for the specific set — these
7// are not chosen for expressiveness, they are chosen for being the ones
8// muscle memory already produces.
9//
10// UNDO IS THE FEATURE, not a nicety attached to it. An autoformat rule
11// that cannot be taken back is worse than no rule at all: it turns a
12// line that begins "- " into a list a person has to fight, and the
13// fighting happens at the exact moment they were mid-sentence.
14// undoInputRule, bound to Backspace immediately after a rule fires,
15// reverts the transformation and leaves the characters that were typed
16// — so "- " stays "- " and nothing was lost. It must be bound BEFORE
17// baseKeymap, whose own Backspace binding would otherwise delete a
18// character and never give the rule a chance; editor-entry.js's keymap
19// block already exists for exactly this ordering reason (see its
20// Shift-Enter comment).
21//
22// One interaction worth stating rather than assuming. This editor's
23// undo is y-prosemirror's Yjs UndoManager, not prosemirror-history —
24// buildCommands' comment in toolbar.mjs records what confusing the two
25// already cost once. undoInputRule is written against ProseMirror's own
26// plugin state, not against the history, so it works here — and
27// autoformat.test.mjs pins that much: the rule fires, Backspace reverts
28// it, and the typed characters come back.
29//
30// WHAT IS NOT TESTED, said plainly because an earlier version of this
31// comment claimed it was: the rule and its revert are two separate Yjs
32// edits, so Ctrl+Z after a Backspace-revert should re-apply the list
33// rather than continue backwards. That is believed, not asserted —
34// exercising it needs a live ySyncPlugin over a real Y.Doc, which none
35// of the DOM-free tests here set up. The spec named this interaction as
36// one of its five most likely failures and asked for a test; the test
37// does not exist, and saying so is better than a comment asserting it
38// does.
39
40import { inputRules, wrappingInputRule, textblockTypeInputRule } from 'prosemirror-inputrules'
41
42// The three bullet openers, matching what every editor accepts. The
43// leading \s* lets a rule fire on an already-indented line.
44const BULLET = /^\s*([-+*])\s$/
45
46// A numbered list.
47//
48// The order attribute is DELIBERATELY NOT SET, and this is the one rule
49// here that departs from prosemirror-schema-list's own example. That
50// example passes the typed number through as `order`, so typing "7. "
51// gives a list starting at 7 — and it would be right to, except that
52// core/src/fragment.rs does not read `order` today: it renders every
53// ordered list as a plain <ol>, starting at 1. Setting the attribute
54// would make the number a person sees while typing differ from the
55// number a reader with no JavaScript sees, and from the number in the
56// PDF, with nothing anywhere reporting the disagreement.
57//
58// So the rule fires on any number and always starts at 1. Typing "1. "
59// — which is what almost everyone types — is exactly right; typing "7. "
60// starts a list at 1, which is visibly a limitation rather than a
61// silent inconsistency. Making `order` real is a fragment.rs arm, a
62// node_json attr, an <ol start> and list numbering in
63// internal/export/pdf.go, and it belongs with that batch rather than
64// smuggled in behind an input rule.
65const ORDERED = /^(\d+)\.\s$/
66
67const QUOTE = /^\s*>\s$/
68
69// Headings 1 to 3, matching the block select's own range. fragment.rs
70// clamps to 6 and the stylesheet styles all six, so widening this is
71// three characters here and three options there — but a rule offering a
72// level the Style control cannot show would be a way to reach a state
73// the toolbar then misreports.
74const HEADING = /^(#{1,3})\s$/
75
76// autoformatRules builds the plugin. Every rule is guarded by the node
77// type it produces existing in the schema — a schema without lists gets
78// the heading and quote rules and no error, which matters because this
79// module is imported by a test that builds minimal schemas.
80export function autoformatRules(schema) {
81 const rules = []
82 const { bullet_list, ordered_list, blockquote, heading } = schema.nodes
83
84 if (bullet_list) rules.push(wrappingInputRule(BULLET, bullet_list))
85 if (ordered_list) rules.push(wrappingInputRule(ORDERED, ordered_list))
86 if (blockquote) rules.push(wrappingInputRule(QUOTE, blockquote))
87 if (heading) {
88 rules.push(
89 textblockTypeInputRule(HEADING, heading, (match) => ({ level: match[1].length })),
90 )
91 }
92
93 return inputRules({ rules })
94}
95
96// The patterns, exported so autoformat.test.mjs can assert what they do
97// and do not match without reaching into the plugin. The negative cases
98// are the ones worth testing: a rule that fires mid-sentence, on "3.14"
99// or on a hyphen inside a word, is the failure people actually notice.
100export const PATTERNS = { BULLET, ORDERED, QUOTE, HEADING }
101