@openpresentation/opf-editor 0.26.5 → 0.26.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.js +9 -2
- package/dist/json-editor.d.ts +20 -0
- package/dist/json-editor.js +186 -89
- package/dist/source-check.js +90 -0
- package/dist/source-checker.js +130 -0
- package/dist/source-completions.js +46 -10
- package/dist/source-diagnostics.js +3 -2
- package/dist/source-editing.js +53 -33
- package/dist/source-fixes-syntax.js +138 -8
- package/dist/source-folding.js +173 -0
- package/dist/source-hints.js +376 -0
- package/dist/source-keymap.js +369 -0
- package/dist/source-markdown.js +42 -1
- package/dist/source-navigation-model.js +426 -0
- package/dist/source-navigation-read.js +269 -0
- package/dist/source-navigation-ui.js +244 -0
- package/dist/source-navigation.js +217 -0
- package/dist/source-outline.js +112 -0
- package/dist/source-pane.d.ts +63 -0
- package/dist/source-pane.js +200 -44
- package/dist/source-references.js +58 -11
- package/dist/source-shortcuts.js +106 -0
- package/dist/source-symbols.js +156 -0
- package/dist/source-worker.d.ts +7 -0
- package/dist/source-worker.js +30 -0
- package/dist/styles.css +262 -0
- package/package.json +11 -7
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// The one table of key bindings of the source editor (artifacts/source-editor/SPEC.md, sections 8.6 and 11). Every binding the editor installs is
|
|
2
|
+
// declared here with its keys per platform, a title and a group; the editor installs the keys it reads from this table (`bind`, `bindDom`), and
|
|
3
|
+
// `getShortcuts()` and the shortcuts dialog list what was installed, so the list cannot drift from the keymap. The key names are CodeMirror's
|
|
4
|
+
// (`Mod-Shift-o`, `Alt-F12`; `Mod` is Cmd on macOS and Ctrl elsewhere; a space separates the strokes of a chord: `Mod-k Mod-0`).
|
|
5
|
+
//
|
|
6
|
+
// A binding object is CodeMirror's own shape: `key` for every platform, and `mac`, `win` or `linux` where it differs.
|
|
7
|
+
import {
|
|
8
|
+
addCursorAbove, addCursorBelow, copyLineDown, copyLineUp, cursorCharLeft, cursorCharRight, cursorDocEnd, cursorDocStart, cursorGroupLeft, cursorGroupRight,
|
|
9
|
+
cursorLineBoundaryBackward, cursorLineBoundaryForward, cursorLineBoundaryLeft, cursorLineBoundaryRight, cursorLineDown, cursorLineEnd, cursorLineStart,
|
|
10
|
+
cursorLineUp, cursorMatchingBracket, cursorPageDown, cursorPageUp, cursorSyntaxLeft, cursorSyntaxRight, deleteCharBackward, deleteCharForward, deleteGroupBackward,
|
|
11
|
+
deleteGroupForward, deleteLine, deleteLineBoundaryBackward, deleteLineBoundaryForward, deleteToLineEnd, indentLess, indentMore, indentSelection, insertBlankLine,
|
|
12
|
+
moveLineDown, moveLineUp, redo, redoSelection, selectAll, selectLine, selectParentSyntax, simplifySelection, splitLine, toggleBlockComment, toggleTabFocusMode,
|
|
13
|
+
insertNewlineAndIndent, transposeChars, undo, undoSelection, toggleComment,
|
|
14
|
+
} from "@codemirror/commands";
|
|
15
|
+
import { closeBracketsKeymap, closeCompletion } from "@codemirror/autocomplete";
|
|
16
|
+
import { EditorView } from "@codemirror/view";
|
|
17
|
+
import { closeSearchPanel } from "@codemirror/search";
|
|
18
|
+
import { findNext, findPrevious, gotoLine, openSearchPanel, selectNextOccurrence, selectSelectionMatches } from "@codemirror/search";
|
|
19
|
+
|
|
20
|
+
// ---- groups ---------------------------------------------------------------------------------------------------------------------------
|
|
21
|
+
export const GROUPS = Object.freeze([
|
|
22
|
+
"Suggestions", "Problems and fixes", "Hover and hints", "Go to", "Folding", "Lines", "Selection and cursors", "Find", "Format", "Undo and redo", "Editing", "Focus", "Help", "Cursor movement",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// ---- the table ------------------------------------------------------------------------------------------------------------------------
|
|
26
|
+
// id: the name the command is bound under; title and group: what the shortcuts list shows; keys: bindings; `when`: what has to be true (shown as a
|
|
27
|
+
// note); `dom`: read from the keydown event (chords and keys whose character depends on the modifiers) instead of CodeMirror's keymap.
|
|
28
|
+
const entry = (id, title, group, keys, extra = {}) => Object.freeze({ id, title, group, keys: Object.freeze(keys), ...extra });
|
|
29
|
+
const FOLD_LEVELS = [1, 2, 3, 4, 5, 6, 7];
|
|
30
|
+
|
|
31
|
+
export const TABLE = Object.freeze([
|
|
32
|
+
// Suggestions (the list is CodeMirror's autocomplete)
|
|
33
|
+
entry("suggest.trigger", "Trigger suggestions", "Suggestions", [{ key: "Ctrl-Space" }, { mac: "Mod-i" }]),
|
|
34
|
+
entry("suggest.details", "Show or hide the details of the selected suggestion", "Suggestions", [{ key: "Ctrl-Space" }], { when: "the list is open", installedWith: "suggest.trigger" }),
|
|
35
|
+
entry("suggest.accept", "Accept the selected suggestion", "Suggestions", [{ key: "Tab" }, { key: "Enter" }], { when: "the list is open" }),
|
|
36
|
+
entry("suggest.acceptReplace", "Accept and replace the whole word", "Suggestions", [{ key: "Shift-Tab" }, { key: "Shift-Enter" }], { when: "the list is open" }),
|
|
37
|
+
entry("suggest.dismiss", "Close the list", "Suggestions", [{ key: "Escape" }], { when: "the list is open" }),
|
|
38
|
+
entry("suggest.next", "Select the next suggestion", "Suggestions", [{ key: "ArrowDown" }, { mac: "Ctrl-n" }], { when: "the list is open" }),
|
|
39
|
+
entry("suggest.previous", "Select the previous suggestion", "Suggestions", [{ key: "ArrowUp" }, { mac: "Ctrl-p" }], { when: "the list is open" }),
|
|
40
|
+
entry("suggest.nextPage", "Select the suggestion a page down", "Suggestions", [{ key: "PageDown" }], { when: "the list is open" }),
|
|
41
|
+
entry("suggest.previousPage", "Select the suggestion a page up", "Suggestions", [{ key: "PageUp" }], { when: "the list is open" }),
|
|
42
|
+
entry("snippet.next", "Next tab stop of a snippet", "Suggestions", [{ key: "Tab" }], { when: "a snippet is being filled", installedWith: "suggest.accept" }),
|
|
43
|
+
entry("snippet.previous", "Previous tab stop of a snippet", "Suggestions", [{ key: "Shift-Tab" }], { when: "a snippet is being filled", installedWith: "suggest.acceptReplace" }),
|
|
44
|
+
// Problems
|
|
45
|
+
entry("problem.next", "Go to the next problem", "Problems and fixes", [{ key: "F8" }]),
|
|
46
|
+
entry("problem.previous", "Go to the previous problem", "Problems and fixes", [{ key: "Shift-F8" }]),
|
|
47
|
+
entry("problem.quickFix", "Quick fix", "Problems and fixes", [{ key: "Mod-." }]),
|
|
48
|
+
entry("problem.panel", "Move focus to the problems panel", "Problems and fixes", [{ key: "Mod-Shift-m" }], { dom: true }),
|
|
49
|
+
// Hover and hints
|
|
50
|
+
entry("hover.show", "Show the hover at the caret", "Hover and hints", [{ key: "Mod-k Mod-i" }], { dom: true }),
|
|
51
|
+
entry("hint.close", "Close the hint", "Hover and hints", [{ key: "Escape" }], { when: "a hint is shown", installedWith: "hint.shape" }),
|
|
52
|
+
entry("hint.shape", "Show the shape of the slide (parameter hints)", "Hover and hints", [{ key: "Mod-Shift-Space" }]),
|
|
53
|
+
// Go to
|
|
54
|
+
entry("nav.definition", "Go to definition", "Go to", [{ key: "F12" }, { key: "Mod-Click" }], { clickOnly: ["Mod-Click"] }),
|
|
55
|
+
entry("nav.peek", "Peek definition", "Go to", [{ key: "Alt-F12" }]),
|
|
56
|
+
entry("nav.references", "Find references", "Go to", [{ key: "Shift-F12" }]),
|
|
57
|
+
entry("nav.rename", "Rename symbol", "Go to", [{ key: "F2" }]),
|
|
58
|
+
entry("nav.close", "Close the peek, the references or the rename input", "Go to", [{ key: "Escape" }], { when: "one of them is open", installedWith: "nav.definition" }),
|
|
59
|
+
entry("nav.symbol", "Go to symbol in the document (outline)", "Go to", [{ key: "Mod-Shift-o" }], { dom: true }),
|
|
60
|
+
entry("nav.line", "Go to line", "Go to", [{ key: "Ctrl-g" }, { key: "Mod-Alt-g" }]),
|
|
61
|
+
entry("nav.bracket", "Jump to the matching bracket", "Go to", [{ key: "Mod-Shift-\\" }]),
|
|
62
|
+
// Folding
|
|
63
|
+
entry("fold.fold", "Fold", "Folding", [{ key: "Ctrl-Shift-[", mac: "Cmd-Alt-[" }]),
|
|
64
|
+
entry("fold.unfold", "Unfold", "Folding", [{ key: "Ctrl-Shift-]", mac: "Cmd-Alt-]" }]),
|
|
65
|
+
entry("fold.toggle", "Toggle fold", "Folding", [{ key: "Mod-k Mod-l" }], { dom: true }),
|
|
66
|
+
entry("fold.foldRecursively", "Fold recursively", "Folding", [{ key: "Mod-k Mod-[" }], { dom: true }),
|
|
67
|
+
entry("fold.unfoldRecursively", "Unfold recursively", "Folding", [{ key: "Mod-k Mod-]" }], { dom: true }),
|
|
68
|
+
entry("fold.all", "Fold all", "Folding", [{ key: "Mod-k Mod-0" }], { dom: true }),
|
|
69
|
+
entry("fold.unfoldAll", "Unfold all", "Folding", [{ key: "Mod-k Mod-j" }], { dom: true }),
|
|
70
|
+
...FOLD_LEVELS.map((level) => entry(`fold.level${level}`, `Fold level ${level}`, "Folding", [{ key: `Mod-k Mod-${level}` }], { dom: true })),
|
|
71
|
+
// Lines
|
|
72
|
+
entry("lines.moveUp", "Move line up", "Lines", [{ key: "Alt-ArrowUp" }]),
|
|
73
|
+
entry("lines.moveDown", "Move line down", "Lines", [{ key: "Alt-ArrowDown" }]),
|
|
74
|
+
entry("lines.copyUp", "Copy line up", "Lines", [{ key: "Shift-Alt-ArrowUp" }]),
|
|
75
|
+
entry("lines.copyDown", "Copy line down", "Lines", [{ key: "Shift-Alt-ArrowDown" }]),
|
|
76
|
+
entry("lines.delete", "Delete line", "Lines", [{ key: "Mod-Shift-k" }]),
|
|
77
|
+
entry("lines.insertBelow", "Insert line below", "Lines", [{ key: "Mod-Enter" }]),
|
|
78
|
+
entry("lines.insertAbove", "Insert line above", "Lines", [{ key: "Mod-Shift-Enter" }]),
|
|
79
|
+
entry("lines.indent", "Indent line", "Lines", [{ key: "Mod-]" }]),
|
|
80
|
+
entry("lines.outdent", "Outdent line", "Lines", [{ key: "Mod-[" }]),
|
|
81
|
+
entry("lines.comment", "Toggle line comment", "Lines", [{ key: "Mod-/" }]),
|
|
82
|
+
entry("lines.blockComment", "Toggle block comment", "Lines", [{ key: "Shift-Alt-a" }]),
|
|
83
|
+
entry("lines.join", "Join lines", "Lines", [{ mac: "Ctrl-j" }]),
|
|
84
|
+
entry("lines.deleteToEnd", "Delete to the end of the line", "Lines", [{ mac: "Ctrl-k" }]),
|
|
85
|
+
entry("lines.split", "Split the line at the caret", "Lines", [{ mac: "Ctrl-o" }]),
|
|
86
|
+
entry("lines.transpose", "Transpose the characters around the caret", "Lines", [{ mac: "Ctrl-t" }]),
|
|
87
|
+
// Selection and cursors
|
|
88
|
+
entry("select.all", "Select all", "Selection and cursors", [{ key: "Mod-a" }]),
|
|
89
|
+
entry("select.line", "Expand the line selection", "Selection and cursors", [{ key: "Mod-l" }, { mac: "Ctrl-l" }], { dom: true }),
|
|
90
|
+
entry("select.expand", "Expand selection", "Selection and cursors", [{ key: "Shift-Alt-ArrowRight", mac: "Ctrl-Shift-Cmd-ArrowRight" }]),
|
|
91
|
+
entry("select.shrink", "Shrink selection", "Selection and cursors", [{ key: "Shift-Alt-ArrowLeft", mac: "Ctrl-Shift-Cmd-ArrowLeft" }]),
|
|
92
|
+
entry("select.nextOccurrence", "Add the next occurrence to the selection", "Selection and cursors", [{ key: "Mod-d" }]),
|
|
93
|
+
entry("select.allOccurrences", "Select all occurrences of the selection", "Selection and cursors", [{ key: "Mod-Shift-l" }]),
|
|
94
|
+
entry("cursor.above", "Add a cursor above", "Selection and cursors", [{ key: "Mod-Alt-ArrowUp" }]),
|
|
95
|
+
entry("cursor.below", "Add a cursor below", "Selection and cursors", [{ key: "Mod-Alt-ArrowDown" }]),
|
|
96
|
+
entry("cursor.click", "Add a cursor", "Selection and cursors", [{ key: "Alt-Click" }], { clickOnly: ["Alt-Click"] }),
|
|
97
|
+
entry("cursor.column", "Select a column", "Selection and cursors", [{ key: "Shift-Alt-Drag" }], { clickOnly: ["Shift-Alt-Drag"] }),
|
|
98
|
+
entry("cursor.collapse", "Collapse to one cursor", "Selection and cursors", [{ key: "Escape" }], { when: "there are several cursors" }),
|
|
99
|
+
entry("select.undo", "Undo the last cursor movement", "Selection and cursors", [{ key: "Mod-u" }]),
|
|
100
|
+
entry("select.redo", "Redo the last cursor movement", "Selection and cursors", [{ key: "Alt-u", mac: "Mod-Shift-u" }]),
|
|
101
|
+
// Find
|
|
102
|
+
entry("find.open", "Find", "Find", [{ key: "Mod-f" }]),
|
|
103
|
+
entry("find.replace", "Replace", "Find", [{ mac: "Mod-Alt-f" }, { win: "Mod-h", linux: "Mod-h" }]),
|
|
104
|
+
entry("find.next", "Find next", "Find", [{ key: "F3", mac: "Mod-g" }], { when: "the find panel is open or the editor has focus" }),
|
|
105
|
+
entry("find.previous", "Find previous", "Find", [{ key: "Shift-F3", mac: "Mod-Shift-g" }]),
|
|
106
|
+
entry("find.close", "Close the find panel", "Find", [{ key: "Escape" }], { when: "the find panel is open" }),
|
|
107
|
+
// Format
|
|
108
|
+
entry("format.document", "Format the document", "Format", [{ key: "Alt-Shift-f" }, { key: "Mod-Shift-f" }], { dom: true }),
|
|
109
|
+
// Undo and redo
|
|
110
|
+
entry("edit.undo", "Undo", "Undo and redo", [{ key: "Mod-z" }]),
|
|
111
|
+
entry("edit.redo", "Redo", "Undo and redo", [{ key: "Mod-Shift-z" }, { win: "Mod-y", linux: "Mod-y" }], { dom: true }),
|
|
112
|
+
// Editing
|
|
113
|
+
entry("edit.enter", "New line (continues a list, keeps the indentation)", "Editing", [{ key: "Enter" }]),
|
|
114
|
+
entry("edit.tab", "Indent (Tab inserts whitespace)", "Editing", [{ key: "Tab" }], { when: "no list is open" }),
|
|
115
|
+
entry("edit.shiftTab", "Outdent", "Editing", [{ key: "Shift-Tab" }], { when: "no list is open" }),
|
|
116
|
+
entry("edit.fieldMenu", "Open the options of the field under the caret (dialog)", "Editing", [{ key: "Alt-Shift-o" }], { dom: true }),
|
|
117
|
+
// Focus
|
|
118
|
+
entry("focus.toggleTab", "Toggle Tab moves focus", "Focus", [{ key: "Ctrl-m", mac: "Ctrl-Shift-m" }]),
|
|
119
|
+
entry("focus.escapeTab", "Leave the editor", "Focus", [{ key: "Escape Tab" }], { documentedOnly: true }),
|
|
120
|
+
// Help
|
|
121
|
+
entry("help.shortcuts", "Show the keyboard shortcuts", "Help", [{ key: "Mod-k Mod-s" }, { key: "F1" }], { dom: true }),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const BY_ID = new Map(TABLE.map((item) => [item.id, item]));
|
|
125
|
+
export const shortcutEntry = (id) => BY_ID.get(id);
|
|
126
|
+
|
|
127
|
+
// ---- the commands CodeMirror binds, with the titles the list shows ----------------------------------------------------------------------
|
|
128
|
+
// Keyed by the command function, so the title does not depend on how a bundler renames functions. A binding whose command is not here is a bug
|
|
129
|
+
// the test finds (`unlisted`): nothing is installed without a place in the list.
|
|
130
|
+
const BASIC = new Map();
|
|
131
|
+
const basic = (command, id, title, group) => { if (command) BASIC.set(command, { id, title, group }); };
|
|
132
|
+
basic(moveLineUp, "lines.moveUp"); basic(moveLineDown, "lines.moveDown"); basic(copyLineUp, "lines.copyUp"); basic(copyLineDown, "lines.copyDown");
|
|
133
|
+
basic(deleteLine, "lines.delete"); basic(insertBlankLine, "lines.insertBelow"); basic(indentMore, "lines.indent"); basic(indentLess, "lines.outdent");
|
|
134
|
+
basic(toggleBlockComment, "lines.blockComment"); basic(deleteToLineEnd, "lines.deleteToEnd"); basic(splitLine, "lines.split"); basic(transposeChars, "lines.transpose");
|
|
135
|
+
basic(selectAll, "select.all"); basic(selectLine, "select.line"); basic(selectNextOccurrence, "select.nextOccurrence"); basic(selectSelectionMatches, "select.allOccurrences");
|
|
136
|
+
basic(addCursorAbove, "cursor.above"); basic(addCursorBelow, "cursor.below"); basic(simplifySelection, "cursor.collapse");
|
|
137
|
+
basic(undoSelection, "select.undo"); basic(redoSelection, "select.redo"); basic(undo, "edit.undo"); basic(redo, "edit.redo");
|
|
138
|
+
basic(cursorMatchingBracket, "nav.bracket"); basic(gotoLine, "nav.line"); basic(openSearchPanel, "find.open"); basic(findNext, "find.next"); basic(findPrevious, "find.previous");
|
|
139
|
+
basic(toggleTabFocusMode, "focus.toggleTab"); basic(closeSearchPanel, "find.close"); basic(closeCompletion, "suggest.dismiss"); basic(toggleComment, "lines.comment");
|
|
140
|
+
basic(indentSelection, "edit.indentSelection", "Auto-indent the selection", "Editing");
|
|
141
|
+
basic(selectParentSyntax, "select.expand"); basic(insertNewlineAndIndent, "edit.enter");
|
|
142
|
+
const MOVES = [
|
|
143
|
+
[cursorCharLeft, "left", "Move left"], [cursorCharRight, "right", "Move right"], [cursorLineUp, "up", "Move up"], [cursorLineDown, "down", "Move down"],
|
|
144
|
+
[cursorGroupLeft, "wordLeft", "Move a word left"], [cursorGroupRight, "wordRight", "Move a word right"],
|
|
145
|
+
[cursorSyntaxLeft, "syntaxLeft", "Move to the previous syntax boundary"], [cursorSyntaxRight, "syntaxRight", "Move to the next syntax boundary"],
|
|
146
|
+
[cursorLineBoundaryBackward, "lineStart", "Go to the start of the line"], [cursorLineBoundaryForward, "lineEnd", "Go to the end of the line"],
|
|
147
|
+
[cursorLineBoundaryLeft, "lineLeft", "Go to the left edge of the line"], [cursorLineBoundaryRight, "lineRight", "Go to the right edge of the line"],
|
|
148
|
+
[cursorLineStart, "lineStartLogical", "Go to the start of the line"], [cursorLineEnd, "lineEndLogical", "Go to the end of the line"],
|
|
149
|
+
[cursorDocStart, "docStart", "Go to the start of the document"], [cursorDocEnd, "docEnd", "Go to the end of the document"], [cursorPageUp, "pageUp", "Page up"], [cursorPageDown, "pageDown", "Page down"],
|
|
150
|
+
[deleteCharBackward, "deleteBack", "Delete the character before the caret"], [deleteCharForward, "deleteForward", "Delete the character after the caret"],
|
|
151
|
+
[deleteGroupBackward, "deleteWordBack", "Delete the word before the caret"], [deleteGroupForward, "deleteWordForward", "Delete the word after the caret"],
|
|
152
|
+
[deleteLineBoundaryBackward, "deleteToStart", "Delete to the start of the line"], [deleteLineBoundaryForward, "deleteToEnd", "Delete to the end of the line"],
|
|
153
|
+
];
|
|
154
|
+
for (const [command, id, title] of MOVES) basic(command, `move.${id}`, title, "Cursor movement");
|
|
155
|
+
|
|
156
|
+
// ---- strokes ---------------------------------------------------------------------------------------------------------------------------
|
|
157
|
+
/** The platform CodeMirror uses for `navigator.platform`: "mac", "win" or "linux" (anything else is "linux"). */
|
|
158
|
+
export function platformOf(navigatorPlatform = typeof navigator === "undefined" ? "" : navigator.platform) {
|
|
159
|
+
return /Mac|iPhone|iPad/.test(navigatorPlatform) ? "mac" : /Win/.test(navigatorPlatform) ? "win" : "linux";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The stroke(s) of a binding on `platform`, as CodeMirror reads them (`binding[platform] || binding.key`), or undefined. */
|
|
163
|
+
export const strokeOf = (binding, platform) => binding[platform] || binding.key;
|
|
164
|
+
|
|
165
|
+
const SYMBOLS = { Mod: "⌘", Cmd: "⌘", Meta: "⌘", Ctrl: "⌃", Alt: "⌥", Shift: "⇧" };
|
|
166
|
+
const KEY_WORDS = { ArrowUp: "Up", ArrowDown: "Down", ArrowLeft: "Left", ArrowRight: "Right", Escape: "Esc", " ": "Space" };
|
|
167
|
+
const KEY_SYMBOLS = { ArrowUp: "↑", ArrowDown: "↓", ArrowLeft: "←", ArrowRight: "→", Escape: "⎋", Enter: "↩", Tab: "⇥", Backspace: "⌫", Delete: "⌦", Space: "␣" };
|
|
168
|
+
|
|
169
|
+
/** Split one stroke (`Mod-Shift-o`, `Mod-Alt--`) into its modifiers and key. */
|
|
170
|
+
export function parseStroke(stroke, platform) {
|
|
171
|
+
const parts = stroke.split(/-(?!$)/);
|
|
172
|
+
let key = parts.pop();
|
|
173
|
+
if (key === "Space") key = " ";
|
|
174
|
+
const mods = { ctrl: false, meta: false, alt: false, shift: false };
|
|
175
|
+
for (const part of parts) {
|
|
176
|
+
if (part === "Mod") mods[platform === "mac" ? "meta" : "ctrl"] = true;
|
|
177
|
+
else if (part === "Ctrl" || part === "Control") mods.ctrl = true;
|
|
178
|
+
else if (part === "Cmd" || part === "Meta") mods.meta = true;
|
|
179
|
+
else if (part === "Alt") mods.alt = true;
|
|
180
|
+
else if (part === "Shift") mods.shift = true;
|
|
181
|
+
else throw new Error(`Unknown modifier ${part} in key ${stroke}`);
|
|
182
|
+
}
|
|
183
|
+
// An upper-case letter in a CodeMirror key name means Shift is held (`Alt-A` is Shift+Alt+A).
|
|
184
|
+
if (key.length === 1 && key !== key.toLowerCase()) mods.shift = true;
|
|
185
|
+
return { ...mods, key: key.length === 1 ? key.toLowerCase() : key };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A stroke as words for a person: `Cmd+Shift+O` on macOS, `Ctrl+Shift+O` elsewhere (`symbols`: `⌘⇧O`). */
|
|
189
|
+
export function strokeText(stroke, platform, { symbols = false } = {}) {
|
|
190
|
+
return stroke.split(" ").map((one) => {
|
|
191
|
+
const parsed = parseStroke(one, platform);
|
|
192
|
+
const key = parsed.key === " " ? "Space" : parsed.key.length === 1 ? parsed.key.toUpperCase() : KEY_WORDS[parsed.key] ?? parsed.key;
|
|
193
|
+
if (symbols && platform === "mac") {
|
|
194
|
+
const letters = [parsed.ctrl && "Ctrl", parsed.alt && "Alt", parsed.shift && "Shift", parsed.meta && "Cmd"].filter(Boolean).map((name) => SYMBOLS[name]).join("");
|
|
195
|
+
return `${letters}${KEY_SYMBOLS[parsed.key === " " ? "Space" : parsed.key] ?? key}`;
|
|
196
|
+
}
|
|
197
|
+
const words = [parsed.ctrl && "Ctrl", parsed.meta && (platform === "mac" ? "Cmd" : "Win"), parsed.alt && "Alt", parsed.shift && "Shift"].filter(Boolean);
|
|
198
|
+
return [...words, key].join("+");
|
|
199
|
+
}).join(" ");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---- reading key events (the DOM handler) ---------------------------------------------------------------------------------------------
|
|
203
|
+
const CODES = { "[": "BracketLeft", "]": "BracketRight", "\\": "Backslash", "/": "Slash", ".": "Period", ",": "Comma", "-": "Minus", "=": "Equal", " ": "Space" };
|
|
204
|
+
const codeOf = (key) => (/^[a-z]$/.test(key) ? `Key${key.toUpperCase()}` : /^\d$/.test(key) ? `Digit${key}` : CODES[key]);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Whether a keydown `event` is the stroke `want` (from `parseStroke`). The modifiers must be exactly those; the key is read as the character it types
|
|
208
|
+
* (a lowercase one even with Shift held, as some platforms report it) and, where Alt changes the character (macOS), by its physical key.
|
|
209
|
+
*/
|
|
210
|
+
export function eventMatches(event, want) {
|
|
211
|
+
if (event.ctrlKey !== want.ctrl || event.metaKey !== want.meta || event.altKey !== want.alt || event.shiftKey !== want.shift) return false;
|
|
212
|
+
if (want.key.length > 1 && want.key !== " ") return event.key === want.key;
|
|
213
|
+
if (event.key.toLowerCase() === want.key) return true;
|
|
214
|
+
return (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) && event.code === codeOf(want.key);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- the builder ------------------------------------------------------------------------------------------------------------------------
|
|
218
|
+
/**
|
|
219
|
+
* The bindings of one editor. `bind(id, run)` returns CodeMirror bindings for the keys the table gives `id` (and records that they are installed);
|
|
220
|
+
* `bindDom(id, run)` records a command read from the keydown event; `spread(list)` records a list of CodeMirror's own bindings, with the titles above.
|
|
221
|
+
* `shortcuts(platform)` is what the editor installed, for the list. `platform` is the one CodeMirror reads from `navigator.platform` unless given.
|
|
222
|
+
*/
|
|
223
|
+
export function createKeymap({ platform = platformOf() } = {}) {
|
|
224
|
+
const installed = new Map(); // id -> { entry, bindings: [binding], extra: [binding] }
|
|
225
|
+
const dom = []; // { id, run, strokes: [[parsed per chord stroke]] }
|
|
226
|
+
const unlisted = [];
|
|
227
|
+
const rowFor = (id, titleInfo) => {
|
|
228
|
+
let row = installed.get(id);
|
|
229
|
+
if (!row) {
|
|
230
|
+
const item = BY_ID.get(id);
|
|
231
|
+
row = { id, title: item?.title ?? titleInfo?.title ?? id, group: item?.group ?? titleInfo?.group ?? "Editing", when: item?.when, bindings: [], clicks: item?.clickOnly ? item.keys.filter((k) => item.clickOnly.includes(k.key)) : [] };
|
|
232
|
+
installed.set(id, row);
|
|
233
|
+
}
|
|
234
|
+
return row;
|
|
235
|
+
};
|
|
236
|
+
const bindingsOf = (item) => item.keys.filter((binding) => !(item.clickOnly ?? []).includes(binding.key));
|
|
237
|
+
return {
|
|
238
|
+
platform,
|
|
239
|
+
/** CodeMirror bindings for the keys of `id`, running `run`. `shift` gives the command of the same keys with Shift held. */
|
|
240
|
+
bind(id, run, extra = {}) {
|
|
241
|
+
const item = BY_ID.get(id);
|
|
242
|
+
if (!item) throw new Error(`No key binding named ${id}`);
|
|
243
|
+
if (item.dom) throw new Error(`${id} is read from the keydown event: use bindDom`);
|
|
244
|
+
const row = rowFor(id);
|
|
245
|
+
const own = bindingsOf(item);
|
|
246
|
+
row.bindings.push(...own);
|
|
247
|
+
// `run` may be one command or one per binding of the table entry, in its order.
|
|
248
|
+
return own.map((binding, index) => ({ ...binding, run: Array.isArray(run) ? run[index] : run, ...extra }));
|
|
249
|
+
},
|
|
250
|
+
/** Record that `id`'s keys are handled elsewhere in the editor (an alias of a command registered under another id, a menu's own keys). */
|
|
251
|
+
note(id) {
|
|
252
|
+
const item = BY_ID.get(id);
|
|
253
|
+
if (!item) throw new Error(`No key binding named ${id}`);
|
|
254
|
+
const row = rowFor(id);
|
|
255
|
+
row.bindings.push(...bindingsOf(item));
|
|
256
|
+
},
|
|
257
|
+
/** A command read from the keydown event: `run(view, event)` returns true when it handled the key. */
|
|
258
|
+
bindDom(id, run) {
|
|
259
|
+
const item = BY_ID.get(id);
|
|
260
|
+
if (!item?.dom) throw new Error(`${id} is not a keydown binding`);
|
|
261
|
+
const row = rowFor(id);
|
|
262
|
+
row.bindings.push(...item.keys);
|
|
263
|
+
const strokes = [];
|
|
264
|
+
for (const binding of item.keys) {
|
|
265
|
+
const stroke = strokeOf(binding, platform);
|
|
266
|
+
if (stroke) strokes.push(stroke.split(" ").map((one) => parseStroke(one, platform)));
|
|
267
|
+
}
|
|
268
|
+
dom.push({ id, run, strokes });
|
|
269
|
+
},
|
|
270
|
+
/** Record CodeMirror's own bindings and return them unchanged (those whose command has a title in the list). */
|
|
271
|
+
spread(list) {
|
|
272
|
+
for (const binding of list) {
|
|
273
|
+
for (const command of [binding.run, binding.shift]) {
|
|
274
|
+
if (!command) continue;
|
|
275
|
+
const info = BASIC.get(command);
|
|
276
|
+
// A Shift variant that has no title is the same command extending the selection: not a row of its own.
|
|
277
|
+
if (!info) { if (command === binding.run) unlisted.push(strokeOf(binding, platform) ?? binding.key ?? binding.mac ?? binding.linux ?? binding.win); continue; }
|
|
278
|
+
const row = rowFor(info.id, info);
|
|
279
|
+
if (command === binding.shift && command === binding.run) continue;
|
|
280
|
+
if (command === binding.shift) row.bindings.push({ ...binding, key: binding.key && `Shift-${binding.key}`, mac: binding.mac && `Shift-${binding.mac}`, win: binding.win && `Shift-${binding.win}`, linux: binding.linux && `Shift-${binding.linux}` });
|
|
281
|
+
else row.bindings.push(binding);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return list;
|
|
285
|
+
},
|
|
286
|
+
/** Bindings the editor handles itself and does not list (single characters it skips over, Backspace in a Markdown list, the menu's own keys). */
|
|
287
|
+
hidden: (list) => list,
|
|
288
|
+
dom,
|
|
289
|
+
/** Bindings recorded without a title (a bug: every installed binding has a place in the list). */
|
|
290
|
+
unlisted: () => [...unlisted],
|
|
291
|
+
/** `{ id, title, group, keys, symbols, when }` for every installed binding that has keys on `platform` (default: this editor's). */
|
|
292
|
+
shortcuts(requested) {
|
|
293
|
+
const on = requested || platform;
|
|
294
|
+
const rows = [];
|
|
295
|
+
for (const row of installed.values()) {
|
|
296
|
+
const keys = [];
|
|
297
|
+
for (const binding of row.bindings) {
|
|
298
|
+
const stroke = strokeOf(binding, on);
|
|
299
|
+
if (stroke && !keys.includes(stroke)) keys.push(stroke);
|
|
300
|
+
}
|
|
301
|
+
for (const click of row.clicks) if (!keys.includes(click.key)) keys.push(click.key);
|
|
302
|
+
if (!keys.length) continue;
|
|
303
|
+
const shown = (stroke) => (/Click|Drag$/.test(stroke) ? stroke.replace(/^Mod/, on === "mac" ? "Cmd" : "Ctrl").replaceAll("-", "+") : strokeText(stroke, on));
|
|
304
|
+
const symbol = (stroke) => (/Click|Drag$/.test(stroke) ? stroke.replace(/^Mod/, on === "mac" ? "Cmd" : "Ctrl").replaceAll("-", "+") : strokeText(stroke, on, { symbols: on === "mac" }));
|
|
305
|
+
rows.push({ id: row.id, title: row.title, group: row.group, keys: keys.map(shown), symbols: keys.map(symbol), strokes: keys, ...(row.when ? { when: row.when } : {}) });
|
|
306
|
+
}
|
|
307
|
+
const order = (row) => GROUPS.indexOf(row.group);
|
|
308
|
+
return rows.sort((a, b) => order(a) - order(b) || tableIndex(a.id) - tableIndex(b.id));
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const TABLE_INDEX = new Map(TABLE.map((item, index) => [item.id, index]));
|
|
314
|
+
const tableIndex = (id) => TABLE_INDEX.get(id) ?? 10000 + [...BASIC.values()].findIndex((info) => info.id === id);
|
|
315
|
+
|
|
316
|
+
/** `list` without the bindings of `drop` (a function of the binding) and with `add` after them. */
|
|
317
|
+
export const without = (list, drop) => list.filter((binding) => !drop(binding));
|
|
318
|
+
|
|
319
|
+
/** CodeMirror's close-brackets bindings (Backspace removes a pair), kept as they are. */
|
|
320
|
+
export const CLOSE_BRACKETS = closeBracketsKeymap;
|
|
321
|
+
|
|
322
|
+
// ---- the keydown handler --------------------------------------------------------------------------------------------------------------
|
|
323
|
+
const MODIFIER_KEYS = new Set(["Shift", "Control", "Alt", "Meta", "AltGraph", "CapsLock"]);
|
|
324
|
+
const CHORD_TIMEOUT = 4000;
|
|
325
|
+
const signature = (stroke) => `${stroke.ctrl ? "c" : ""}${stroke.meta ? "m" : ""}${stroke.alt ? "a" : ""}${stroke.shift ? "s" : ""}:${stroke.key}`;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The handler for the bindings `km.bindDom` recorded. A binding of one stroke runs when the event is that stroke exactly; a chord (`Mod-k Mod-i`) waits for
|
|
329
|
+
* its second stroke for four seconds, and a key that is not the second stroke of any chord is handled as if the first had not been pressed. `run(view, event)`
|
|
330
|
+
* returns false to leave the key alone. The first stroke of a chord is taken, so Cmd/Ctrl+K alone does nothing else; Cmd/Ctrl+Shift+K (Delete line) is another stroke.
|
|
331
|
+
*/
|
|
332
|
+
export function domKeymap(km, say) {
|
|
333
|
+
let pending = null;
|
|
334
|
+
const chords = km.dom.flatMap((command) => command.strokes.filter((strokes) => strokes.length === 2).map((strokes) => ({ command, first: strokes[0], second: strokes[1], sig: signature(strokes[0]) })));
|
|
335
|
+
return EditorView.domEventHandlers({
|
|
336
|
+
keydown(event, view) {
|
|
337
|
+
if (event.isComposing || MODIFIER_KEYS.has(event.key)) return false;
|
|
338
|
+
if (pending) {
|
|
339
|
+
const waiting = pending;
|
|
340
|
+
pending = null;
|
|
341
|
+
if (Date.now() - waiting.at < CHORD_TIMEOUT) {
|
|
342
|
+
const hit = chords.find((chord) => chord.sig === waiting.sig && eventMatches(event, chord.second));
|
|
343
|
+
if (hit) {
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
if (hit.command.run(view, event) === false) return false;
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const command of km.dom) {
|
|
351
|
+
for (const strokes of command.strokes) {
|
|
352
|
+
if (strokes.length === 1 && eventMatches(event, strokes[0])) {
|
|
353
|
+
if (command.run(view, event) === false) continue;
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const first = chords.find((chord) => eventMatches(event, chord.first));
|
|
360
|
+
if (first) {
|
|
361
|
+
pending = { sig: first.sig, at: Date.now() };
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
say?.("Chord started, waiting for the second key");
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
package/dist/source-markdown.js
CHANGED
|
@@ -258,6 +258,21 @@ function metricContext(text, position, fence) {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
// ---- the body ----------------------------------------------------------------------------------------------------------------------
|
|
261
|
+
// `.
|
|
262
|
+
const IMAGE_TARGET = /!\[(?:[^\]\\\n]|\\.)*\]\(([^)\s]*)$/;
|
|
263
|
+
const VIDEO_EXTENSIONS = /\.(?:mp4|m4v|mov|webm|ogv|mkv)(?:[?#].*)?$/i;
|
|
264
|
+
|
|
265
|
+
/** Whether the nearest line above `lineStart` that is not blank is a block options comment with `as=video` (the image line below it is a video). */
|
|
266
|
+
function videoLineAbove(text, start) {
|
|
267
|
+
for (let at = start; at > 0;) {
|
|
268
|
+
const line = lineAt(text, at - 1);
|
|
269
|
+
at = line.start;
|
|
270
|
+
if (line.text.trim() === "") continue;
|
|
271
|
+
return /^\s*<!--\s*block\s*:/.test(line.text) && /\bas=("video"|video)(?=\s|-->)/.test(line.text);
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
261
276
|
function bodyContext(text, position, where, explicit) {
|
|
262
277
|
const { scan } = where;
|
|
263
278
|
const line = lineAt(text, position);
|
|
@@ -271,6 +286,14 @@ function bodyContext(text, position, where, explicit) {
|
|
|
271
286
|
const start = position - span[2].length;
|
|
272
287
|
return { format: "md", region: "span", token: { quoted: false, closed: false, start, end, from: start, to: end }, existing: used, document: {}, path: [], explicit, opened: span[2] === "" };
|
|
273
288
|
}
|
|
289
|
+
// `.
|
|
290
|
+
const image = IMAGE_TARGET.exec(before);
|
|
291
|
+
if (image) {
|
|
292
|
+
const start = position - image[1].length;
|
|
293
|
+
let end = position;
|
|
294
|
+
while (end < line.end && !/[\s)]/.test(text[end])) end++;
|
|
295
|
+
return { format: "md", region: "image", video: videoLineAbove(text, line.start), closed: text[end] === ")", token: { quoted: false, closed: false, start, end, from: start, to: end }, document: {}, path: [] };
|
|
296
|
+
}
|
|
274
297
|
// `{{` in a line of text: the variables of the document.
|
|
275
298
|
const variable = /\{\{([A-Za-z0-9._-]*)$/.exec(before);
|
|
276
299
|
if (variable) {
|
|
@@ -347,6 +370,23 @@ export function markdownOptions(context, { catalogs, text, explicit, makers, sco
|
|
|
347
370
|
marks.forEach(([mark, word, doc], index) => options.push(kindOption(mark, "keyword", word, doc, boostAt(index), "status", { mark })));
|
|
348
371
|
return { options, validFor: /^(?:\[[x> ]?\]?)?$/ };
|
|
349
372
|
}
|
|
373
|
+
if (context.region === "image") {
|
|
374
|
+
// The assets of the document first (the videos first for a video line), then the image variables.
|
|
375
|
+
const facts = scope?.facts() ?? { assets: [], variables: [] };
|
|
376
|
+
const isVideo = (asset) => asset.mediaType.startsWith("video/") || VIDEO_EXTENSIONS.test(asset.src);
|
|
377
|
+
const assets = [...facts.assets].sort((a, b) => (context.video ? Number(isVideo(b)) - Number(isVideo(a)) : 0));
|
|
378
|
+
for (const asset of assets) {
|
|
379
|
+
const value = `asset:${asset.id}`;
|
|
380
|
+
const detail = asset.detail || "asset";
|
|
381
|
+
options.push({ label: value, type: "reference", detail, boost: boostAt(options.length), opfDoc: asset.detail, opf: { kind: "imageTarget", value }, info: () => docNode({ title: value, type: "asset", description: asset.detail || undefined, notes: [context.video && isVideo(asset) ? "A video" : ""].filter(Boolean) }) });
|
|
382
|
+
}
|
|
383
|
+
for (const variable of facts.variables) {
|
|
384
|
+
if (variable.type !== "image") continue;
|
|
385
|
+
const value = `var:${variable.id}`;
|
|
386
|
+
options.push({ label: value, type: "reference", detail: "image variable", boost: boostAt(options.length), opfDoc: `The image variable ${variable.id}`, opf: { kind: "imageTarget", value }, info: () => docNode({ title: value, type: "image variable" }) });
|
|
387
|
+
}
|
|
388
|
+
return { options, validFor: /^[A-Za-z0-9_.:+-]*$/ };
|
|
389
|
+
}
|
|
350
390
|
if (context.region === "span") {
|
|
351
391
|
// Right after `{` or a space the list opens by itself (the characters that begin a new attribute); Ctrl+Space opens it anywhere in the braces.
|
|
352
392
|
const docs = { bold: "Bold text", italic: "Italic text", underline: "Underlined text", strike: "Struck-through text", sup: "Superscript", sub: "Subscript", color: "Text color: a hex color, a scheme slot or var:name", size: "Font size in points", font: "Font family", lang: "Language of the text (a BCP-47 tag)" };
|
|
@@ -438,6 +478,7 @@ export function markdownPlan(context, completion, text, end) {
|
|
|
438
478
|
return { template: `${completion.opf.key}: `, fields: 0, chain: Boolean(completion.opf.chain) };
|
|
439
479
|
}
|
|
440
480
|
if (context.region === "metric" && kind === "literal") return { template: `${context.needsSpace ? " " : ""}${escapeTemplate(String(completion.opf.value))}`, fields: 0, chain: false };
|
|
481
|
+
if (kind === "imageTarget") return { template: `${escapeTemplate(completion.opf.value)}${context.closed ? "" : ")"}`, fields: 0, chain: false };
|
|
441
482
|
if (kind === "status") return { template: `${completion.opf.mark} `, fields: 0, chain: false };
|
|
442
483
|
if (kind === "spanAttribute") return { template: `${completion.opf.name}${completion.opf.takesValue ? "=" : ""}`, fields: 0, chain: false };
|
|
443
484
|
if (kind === "keyword") return { template: `${escapeTemplate(completion.opf.word)} `, fields: 0, chain: false };
|
|
@@ -454,7 +495,7 @@ export function markdownPlan(context, completion, text, end) {
|
|
|
454
495
|
|
|
455
496
|
// ---- hover -------------------------------------------------------------------------------------------------------------------------
|
|
456
497
|
// What a piece of the dialect's syntax stands for, as a card: the schema's words for the field it writes, and how it is written.
|
|
457
|
-
const SYNTAX_FIELDS = {
|
|
498
|
+
export const SYNTAX_FIELDS = {
|
|
458
499
|
title: { key: "title", written: "# Title" },
|
|
459
500
|
subtitle: { key: "subtitle", written: "## Subtitle" },
|
|
460
501
|
notes: { key: "notes", written: "Note: text, to the end of the slide" },
|