// fixtures/autoformat.mjs — typing "- " makes a bullet. // // Input rules: a pattern that matches the text just typed at the end of // a textblock, and a transformation applied when it does. Everything // here is the same shape a person has met in every editor since // Markdown, which is the whole argument for the specific set — these // are not chosen for expressiveness, they are chosen for being the ones // muscle memory already produces. // // UNDO IS THE FEATURE, not a nicety attached to it. An autoformat rule // that cannot be taken back is worse than no rule at all: it turns a // line that begins "- " into a list a person has to fight, and the // fighting happens at the exact moment they were mid-sentence. // undoInputRule, bound to Backspace immediately after a rule fires, // reverts the transformation and leaves the characters that were typed // — so "- " stays "- " and nothing was lost. It must be bound BEFORE // baseKeymap, whose own Backspace binding would otherwise delete a // character and never give the rule a chance; editor-entry.js's keymap // block already exists for exactly this ordering reason (see its // Shift-Enter comment). // // One interaction worth stating rather than assuming. This editor's // undo is y-prosemirror's Yjs UndoManager, not prosemirror-history — // buildCommands' comment in toolbar.mjs records what confusing the two // already cost once. undoInputRule is written against ProseMirror's own // plugin state, not against the history, so it works here — and // autoformat.test.mjs pins that much: the rule fires, Backspace reverts // it, and the typed characters come back. // // WHAT IS NOT TESTED, said plainly because an earlier version of this // comment claimed it was: the rule and its revert are two separate Yjs // edits, so Ctrl+Z after a Backspace-revert should re-apply the list // rather than continue backwards. That is believed, not asserted — // exercising it needs a live ySyncPlugin over a real Y.Doc, which none // of the DOM-free tests here set up. The spec named this interaction as // one of its five most likely failures and asked for a test; the test // does not exist, and saying so is better than a comment asserting it // does. import { inputRules, wrappingInputRule, textblockTypeInputRule } from 'prosemirror-inputrules' // The three bullet openers, matching what every editor accepts. The // leading \s* lets a rule fire on an already-indented line. const BULLET = /^\s*([-+*])\s$/ // A numbered list. // // The order attribute is DELIBERATELY NOT SET, and this is the one rule // here that departs from prosemirror-schema-list's own example. That // example passes the typed number through as `order`, so typing "7. " // gives a list starting at 7 — and it would be right to, except that // core/src/fragment.rs does not read `order` today: it renders every // ordered list as a plain
    , starting at 1. Setting the attribute // would make the number a person sees while typing differ from the // number a reader with no JavaScript sees, and from the number in the // PDF, with nothing anywhere reporting the disagreement. // // So the rule fires on any number and always starts at 1. Typing "1. " // — which is what almost everyone types — is exactly right; typing "7. " // starts a list at 1, which is visibly a limitation rather than a // silent inconsistency. Making `order` real is a fragment.rs arm, a // node_json attr, an
      and list numbering in // internal/export/pdf.go, and it belongs with that batch rather than // smuggled in behind an input rule. const ORDERED = /^(\d+)\.\s$/ const QUOTE = /^\s*>\s$/ // Headings 1 to 3, matching the block select's own range. fragment.rs // clamps to 6 and the stylesheet styles all six, so widening this is // three characters here and three options there — but a rule offering a // level the Style control cannot show would be a way to reach a state // the toolbar then misreports. const HEADING = /^(#{1,3})\s$/ // autoformatRules builds the plugin. Every rule is guarded by the node // type it produces existing in the schema — a schema without lists gets // the heading and quote rules and no error, which matters because this // module is imported by a test that builds minimal schemas. export function autoformatRules(schema) { const rules = [] const { bullet_list, ordered_list, blockquote, heading } = schema.nodes if (bullet_list) rules.push(wrappingInputRule(BULLET, bullet_list)) if (ordered_list) rules.push(wrappingInputRule(ORDERED, ordered_list)) if (blockquote) rules.push(wrappingInputRule(QUOTE, blockquote)) if (heading) { rules.push( textblockTypeInputRule(HEADING, heading, (match) => ({ level: match[1].length })), ) } return inputRules({ rules }) } // The patterns, exported so autoformat.test.mjs can assert what they do // and do not match without reaching into the plugin. The negative cases // are the ones worth testing: a rule that fires mid-sentence, on "3.14" // or on a hyphen inside a word, is the failure people actually notice. export const PATTERNS = { BULLET, ORDERED, QUOTE, HEADING }