Stuff
This commit is contained in:
parent
9b1b950c41
commit
f73acae41a
1
plugins/.gitignore
vendored
1
plugins/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
dist
|
|
@ -1,7 +1,7 @@
|
|||||||
DENO_BUNDLE=deno run --allow-read --allow-write --unstable bundle.ts --debug
|
DENO_BUNDLE=deno run --allow-read --allow-write --unstable bundle.ts --debug
|
||||||
build: *
|
build: *
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
$(DENO_BUNDLE) core/core.plugin.json dist/core.plugin.json
|
$(DENO_BUNDLE) core/core.plugin.json ../webapp/src/generated/core.plugin.json
|
||||||
|
|
||||||
entr:
|
entr:
|
||||||
ls core/* | entr make
|
ls core/* | entr make
|
@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"commands": {
|
"commands": {
|
||||||
"Count Words": {
|
|
||||||
"invoke": "word_count_command"
|
|
||||||
},
|
|
||||||
"Navigate To page": {
|
"Navigate To page": {
|
||||||
"invoke": "linkNavigate",
|
"invoke": "linkNavigate",
|
||||||
"key": "Ctrl-Enter",
|
"key": "Ctrl-Enter",
|
||||||
@ -68,9 +65,6 @@
|
|||||||
"taskToggle": {
|
"taskToggle": {
|
||||||
"path": "./task.ts:taskToggle"
|
"path": "./task.ts:taskToggle"
|
||||||
},
|
},
|
||||||
"word_count_command": {
|
|
||||||
"path": "./word_count_command.ts:wordCount"
|
|
||||||
},
|
|
||||||
"insertToday": {
|
"insertToday": {
|
||||||
"path": "./dates.ts:insertToday"
|
"path": "./dates.ts:insertToday"
|
||||||
},
|
},
|
||||||
|
@ -29,9 +29,15 @@ export async function deletePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function renamePage() {
|
export async function renamePage() {
|
||||||
|
// console.log("HELLO WORLD");
|
||||||
const pageMeta = await syscall("editor.getCurrentPage");
|
const pageMeta = await syscall("editor.getCurrentPage");
|
||||||
const oldName = pageMeta.name;
|
const oldName = pageMeta.name;
|
||||||
const newName = await syscall("editor.prompt", `Rename ${oldName} to:`);
|
console.log("Old name is", oldName);
|
||||||
|
const newName = await syscall(
|
||||||
|
"editor.prompt",
|
||||||
|
`Rename ${oldName} to:`,
|
||||||
|
oldName
|
||||||
|
);
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cp src/function_worker.js dist/ && parcel",
|
"start": "mkdir -p dist && cp src/function_worker.js dist/ && parcel",
|
||||||
"build": "parcel build && cp src/function_worker.js dist/",
|
"build": "parcel build && cp src/function_worker.js dist/",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"check-watch": "tsc --noEmit --watch"
|
"check-watch": "tsc --noEmit --watch"
|
||||||
|
@ -2,7 +2,14 @@ import { PageMeta } from "../types";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
|
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export function NavigationBar({
|
function prettyName(s: string | undefined): string {
|
||||||
|
if (!s) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return s.replaceAll("/", " / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopBar({
|
||||||
currentPage,
|
currentPage,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
@ -15,7 +22,7 @@ export function NavigationBar({
|
|||||||
<span className="icon">
|
<span className="icon">
|
||||||
<FontAwesomeIcon icon={faFileLines} />
|
<FontAwesomeIcon icon={faFileLines} />
|
||||||
</span>
|
</span>
|
||||||
<span className="current-page">{currentPage?.name}</span>
|
<span className="current-page">{prettyName(currentPage?.name)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -6,3 +6,5 @@ export const TagTag = Tag.define();
|
|||||||
export const MentionTag = Tag.define();
|
export const MentionTag = Tag.define();
|
||||||
export const TaskTag = Tag.define();
|
export const TaskTag = Tag.define();
|
||||||
export const TaskMarkerTag = Tag.define();
|
export const TaskMarkerTag = Tag.define();
|
||||||
|
export const CommentTag = Tag.define();
|
||||||
|
export const CommentMarkerTag = Tag.define();
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
|
||||||
import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
||||||
import { history, historyKeymap } from "@codemirror/history";
|
import { history, historyKeymap } from "@codemirror/history";
|
||||||
import { indentOnInput, syntaxTree } from "@codemirror/language";
|
|
||||||
import { bracketMatching } from "@codemirror/matchbrackets";
|
import { bracketMatching } from "@codemirror/matchbrackets";
|
||||||
import { searchKeymap } from "@codemirror/search";
|
import { searchKeymap } from "@codemirror/search";
|
||||||
import { EditorState, StateField, Transaction } from "@codemirror/state";
|
import { EditorState, StateField, Transaction } from "@codemirror/state";
|
||||||
@ -20,24 +19,31 @@ import {
|
|||||||
KeyBinding,
|
KeyBinding,
|
||||||
keymap,
|
keymap,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
|
|
||||||
import React, { useEffect, useReducer } from "react";
|
import React, { useEffect, useReducer } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import coreManifest from "../../plugins/dist/core.plugin.json";
|
import coreManifest from "./generated/core.plugin.json";
|
||||||
|
// @ts-ignore
|
||||||
|
window.coreManifest = coreManifest;
|
||||||
|
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
||||||
import * as commands from "./commands";
|
import * as commands from "./commands";
|
||||||
import { CommandPalette } from "./components/command_palette";
|
import { CommandPalette } from "./components/command_palette";
|
||||||
import { NavigationBar } from "./components/navigation_bar";
|
|
||||||
import { PageNavigator } from "./components/page_navigator";
|
import { PageNavigator } from "./components/page_navigator";
|
||||||
import { StatusBar } from "./components/status_bar";
|
import { StatusBar } from "./components/status_bar";
|
||||||
import { Space } from "./space";
|
import { TopBar } from "./components/top_bar";
|
||||||
|
import { Indexer } from "./indexer";
|
||||||
import { lineWrapper } from "./lineWrapper";
|
import { lineWrapper } from "./lineWrapper";
|
||||||
import { markdown } from "./markdown";
|
import { markdown } from "./markdown";
|
||||||
|
import { IPageNavigator, PathPageNavigator } from "./navigator";
|
||||||
import customMarkDown from "./parser";
|
import customMarkDown from "./parser";
|
||||||
import { BrowserSystem } from "./plugins/browser_system";
|
import { BrowserSystem } from "./plugins/browser_system";
|
||||||
import { Manifest, slashCommandRegexp } from "./plugins/types";
|
import { Plugin } from "./plugins/runtime";
|
||||||
|
import { slashCommandRegexp } from "./plugins/types";
|
||||||
import reducer from "./reducer";
|
import reducer from "./reducer";
|
||||||
|
import { smartQuoteKeymap } from "./smart_quotes";
|
||||||
|
import { Space } from "./space";
|
||||||
import customMarkdownStyle from "./style";
|
import customMarkdownStyle from "./style";
|
||||||
import dbSyscalls from "./syscalls/db.localstorage";
|
import dbSyscalls from "./syscalls/db.localstorage";
|
||||||
import { Plugin } from "./plugins/runtime";
|
|
||||||
import editorSyscalls from "./syscalls/editor.browser";
|
import editorSyscalls from "./syscalls/editor.browser";
|
||||||
import indexerSyscalls from "./syscalls/indexer.native";
|
import indexerSyscalls from "./syscalls/indexer.native";
|
||||||
import spaceSyscalls from "./syscalls/space.native";
|
import spaceSyscalls from "./syscalls/space.native";
|
||||||
@ -48,16 +54,7 @@ import {
|
|||||||
initialViewState,
|
initialViewState,
|
||||||
PageMeta,
|
PageMeta,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
|
||||||
AppEvent,
|
|
||||||
AppEventDispatcher,
|
|
||||||
ClickEvent,
|
|
||||||
IndexEvent,
|
|
||||||
} from "./app_event";
|
|
||||||
import { safeRun } from "./util";
|
import { safeRun } from "./util";
|
||||||
import { Indexer } from "./indexer";
|
|
||||||
import { IPageNavigator, PathPageNavigator } from "./navigator";
|
|
||||||
import { smartQuoteKeymap } from "./smart_quotes";
|
|
||||||
|
|
||||||
class PageState {
|
class PageState {
|
||||||
editorState: EditorState;
|
editorState: EditorState;
|
||||||
@ -203,7 +200,7 @@ export class Editor implements AppEventDispatcher {
|
|||||||
history(),
|
history(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
indentOnInput(),
|
// indentOnInput(),
|
||||||
customMarkdownStyle,
|
customMarkdownStyle,
|
||||||
bracketMatching(),
|
bracketMatching(),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
@ -217,10 +214,12 @@ export class Editor implements AppEventDispatcher {
|
|||||||
lineWrapper([
|
lineWrapper([
|
||||||
{ selector: "ATXHeading1", class: "line-h1" },
|
{ selector: "ATXHeading1", class: "line-h1" },
|
||||||
{ selector: "ATXHeading2", class: "line-h2" },
|
{ selector: "ATXHeading2", class: "line-h2" },
|
||||||
{ selector: "ListItem", class: "line-li" },
|
{ selector: "ATXHeading3", class: "line-h3" },
|
||||||
|
{ selector: "ListItem", class: "line-li", nesting: true },
|
||||||
{ selector: "Blockquote", class: "line-blockquote" },
|
{ selector: "Blockquote", class: "line-blockquote" },
|
||||||
{ selector: "CodeBlock", class: "line-code" },
|
{ selector: "CodeBlock", class: "line-code" },
|
||||||
{ selector: "FencedCode", class: "line-fenced-code" },
|
{ selector: "FencedCode", class: "line-fenced-code" },
|
||||||
|
{ selector: "Comment", class: "line-comment" },
|
||||||
]),
|
]),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
...smartQuoteKeymap,
|
...smartQuoteKeymap,
|
||||||
@ -535,7 +534,7 @@ export class Editor implements AppEventDispatcher {
|
|||||||
commands={viewState.commands}
|
commands={viewState.commands}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NavigationBar
|
<TopBar
|
||||||
currentPage={viewState.currentPage}
|
currentPage={viewState.currentPage}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: "start-navigate" });
|
dispatch({ type: "start-navigate" });
|
||||||
|
109
webapp/src/generated/core.plugin.json
Normal file
109
webapp/src/generated/core.plugin.json
Normal file
File diff suppressed because one or more lines are too long
@ -12,10 +12,12 @@ import { Range } from "@codemirror/rangeset";
|
|||||||
interface WrapElement {
|
interface WrapElement {
|
||||||
selector: string;
|
selector: string;
|
||||||
class: string;
|
class: string;
|
||||||
|
nesting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
||||||
let widgets: Range<Decoration>[] = [];
|
let widgets: Range<Decoration>[] = [];
|
||||||
|
let elementStack: string[] = [];
|
||||||
for (let { from, to } of view.visibleRanges) {
|
for (let { from, to } of view.visibleRanges) {
|
||||||
const doc = view.state.doc;
|
const doc = view.state.doc;
|
||||||
syntaxTree(view.state).iterate({
|
syntaxTree(view.state).iterate({
|
||||||
@ -25,12 +27,19 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
|||||||
const bodyText = doc.sliceString(from, to);
|
const bodyText = doc.sliceString(from, to);
|
||||||
for (let wrapElement of wrapElements) {
|
for (let wrapElement of wrapElements) {
|
||||||
if (type.name == wrapElement.selector) {
|
if (type.name == wrapElement.selector) {
|
||||||
|
if (wrapElement.nesting) {
|
||||||
|
elementStack.push(type.name);
|
||||||
|
}
|
||||||
const bodyText = doc.sliceString(from, to);
|
const bodyText = doc.sliceString(from, to);
|
||||||
let idx = from;
|
let idx = from;
|
||||||
for (let line of bodyText.split("\n")) {
|
for (let line of bodyText.split("\n")) {
|
||||||
|
let cls = wrapElement.class;
|
||||||
|
if (wrapElement.nesting) {
|
||||||
|
cls = `${cls} ${cls}-${elementStack.length}`;
|
||||||
|
}
|
||||||
widgets.push(
|
widgets.push(
|
||||||
Decoration.line({
|
Decoration.line({
|
||||||
class: wrapElement.class,
|
class: cls,
|
||||||
}).range(doc.lineAt(idx).from)
|
}).range(doc.lineAt(idx).from)
|
||||||
);
|
);
|
||||||
idx += line.length + 1;
|
idx += line.length + 1;
|
||||||
@ -38,7 +47,13 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leave(type, from: number, to: number) {},
|
leave(type, from: number, to: number) {
|
||||||
|
for (let wrapElement of wrapElements) {
|
||||||
|
if (type.name == wrapElement.selector && wrapElement.nesting) {
|
||||||
|
elementStack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Widgets have to be sorted by `from` in ascending order
|
// Widgets have to be sorted by `from` in ascending order
|
||||||
|
@ -1,234 +0,0 @@
|
|||||||
import { EditorSelection } from "@codemirror/state";
|
|
||||||
import { syntaxTree } from "@codemirror/language";
|
|
||||||
import { markdownLanguage } from "./markdown";
|
|
||||||
function nodeStart(node, doc) {
|
|
||||||
return doc.sliceString(node.from, node.from + 50);
|
|
||||||
}
|
|
||||||
class Context {
|
|
||||||
constructor(node, from, to, spaceBefore, spaceAfter, type, item) {
|
|
||||||
this.node = node;
|
|
||||||
this.from = from;
|
|
||||||
this.to = to;
|
|
||||||
this.spaceBefore = spaceBefore;
|
|
||||||
this.spaceAfter = spaceAfter;
|
|
||||||
this.type = type;
|
|
||||||
this.item = item;
|
|
||||||
}
|
|
||||||
blank(trailing = true) {
|
|
||||||
let result = this.spaceBefore;
|
|
||||||
if (this.node.name == "Blockquote")
|
|
||||||
result += ">";
|
|
||||||
else
|
|
||||||
for (let i = this.to - this.from - result.length - this.spaceAfter.length; i > 0; i--)
|
|
||||||
result += " ";
|
|
||||||
return result + (trailing ? this.spaceAfter : "");
|
|
||||||
}
|
|
||||||
marker(doc, add) {
|
|
||||||
let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item, doc)[2] + add)) : "";
|
|
||||||
return this.spaceBefore + number + this.type + this.spaceAfter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getContext(node, line, doc) {
|
|
||||||
let nodes = [];
|
|
||||||
for (let cur = node; cur && cur.name != "Document"; cur = cur.parent) {
|
|
||||||
if (cur.name == "ListItem" || cur.name == "Blockquote")
|
|
||||||
nodes.push(cur);
|
|
||||||
}
|
|
||||||
let context = [], pos = 0;
|
|
||||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
||||||
let node = nodes[i], match, start = pos;
|
|
||||||
if (node.name == "Blockquote" && (match = /^[ \t]*>( ?)/.exec(line.slice(pos)))) {
|
|
||||||
pos += match[0].length;
|
|
||||||
context.push(new Context(node, start, pos, "", match[1], ">", null));
|
|
||||||
}
|
|
||||||
else if (node.name == "ListItem" && node.parent.name == "OrderedList" &&
|
|
||||||
(match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(nodeStart(node, doc)))) {
|
|
||||||
let after = match[3], len = match[0].length;
|
|
||||||
if (after.length >= 4) {
|
|
||||||
after = after.slice(0, after.length - 4);
|
|
||||||
len -= 4;
|
|
||||||
}
|
|
||||||
pos += len;
|
|
||||||
context.push(new Context(node.parent, start, pos, match[1], after, match[2], node));
|
|
||||||
}
|
|
||||||
else if (node.name == "ListItem" && node.parent.name == "BulletList" &&
|
|
||||||
(match = /^([ \t]*)([-+*])([ \t]+)/.exec(nodeStart(node, doc)))) {
|
|
||||||
let after = match[3], len = match[0].length;
|
|
||||||
if (after.length > 4) {
|
|
||||||
after = after.slice(0, after.length - 4);
|
|
||||||
len -= 4;
|
|
||||||
}
|
|
||||||
pos += len;
|
|
||||||
context.push(new Context(node.parent, start, pos, match[1], after, match[2], node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
function itemNumber(item, doc) {
|
|
||||||
return /^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from, item.from + 10));
|
|
||||||
}
|
|
||||||
function renumberList(after, doc, changes, offset = 0) {
|
|
||||||
for (let prev = -1, node = after;;) {
|
|
||||||
if (node.name == "ListItem") {
|
|
||||||
let m = itemNumber(node, doc);
|
|
||||||
let number = +m[2];
|
|
||||||
if (prev >= 0) {
|
|
||||||
if (number != prev + 1)
|
|
||||||
return;
|
|
||||||
changes.push({ from: node.from + m[1].length, to: node.from + m[0].length, insert: String(prev + 2 + offset) });
|
|
||||||
}
|
|
||||||
prev = number;
|
|
||||||
}
|
|
||||||
let next = node.nextSibling;
|
|
||||||
if (!next)
|
|
||||||
break;
|
|
||||||
node = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// This command, when invoked in Markdown context with cursor
|
|
||||||
/// selection(s), will create a new line with the markup for
|
|
||||||
/// blockquotes and lists that were active on the old line. If the
|
|
||||||
/// cursor was directly after the end of the markup for the old line,
|
|
||||||
/// trailing whitespace and list markers are removed from that line.
|
|
||||||
///
|
|
||||||
/// The command does nothing in non-Markdown context, so it should
|
|
||||||
/// not be used as the only binding for Enter (even in a Markdown
|
|
||||||
/// document, HTML and code regions might use a different language).
|
|
||||||
export const insertNewlineContinueMarkup = ({ state, dispatch }) => {
|
|
||||||
let tree = syntaxTree(state), { doc } = state;
|
|
||||||
let dont = null, changes = state.changeByRange(range => {
|
|
||||||
if (!range.empty || !markdownLanguage.isActiveAt(state, range.from))
|
|
||||||
return dont = { range };
|
|
||||||
let pos = range.from, line = doc.lineAt(pos);
|
|
||||||
let context = getContext(tree.resolveInner(pos, -1), line.text, doc);
|
|
||||||
while (context.length && context[context.length - 1].from > pos - line.from)
|
|
||||||
context.pop();
|
|
||||||
if (!context.length)
|
|
||||||
return dont = { range };
|
|
||||||
let inner = context[context.length - 1];
|
|
||||||
if (inner.to - inner.spaceAfter.length > pos - line.from)
|
|
||||||
return dont = { range };
|
|
||||||
let emptyLine = pos >= (inner.to - inner.spaceAfter.length) && !/\S/.test(line.text.slice(inner.to));
|
|
||||||
// Empty line in list
|
|
||||||
if (inner.item && emptyLine) {
|
|
||||||
// First list item or blank line before: delete a level of markup
|
|
||||||
if (inner.node.firstChild.to >= pos ||
|
|
||||||
line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text)) {
|
|
||||||
let next = context.length > 1 ? context[context.length - 2] : null;
|
|
||||||
let delTo, insert = "";
|
|
||||||
if (next && next.item) { // Re-add marker for the list at the next level
|
|
||||||
delTo = line.from + next.from;
|
|
||||||
insert = next.marker(doc, 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delTo = line.from + (next ? next.to : 0);
|
|
||||||
}
|
|
||||||
let changes = [{ from: delTo, to: pos, insert }];
|
|
||||||
if (inner.node.name == "OrderedList")
|
|
||||||
renumberList(inner.item, doc, changes, -2);
|
|
||||||
if (next && next.node.name == "OrderedList")
|
|
||||||
renumberList(next.item, doc, changes);
|
|
||||||
return { range: EditorSelection.cursor(delTo + insert.length), changes };
|
|
||||||
}
|
|
||||||
else { // Move this line down
|
|
||||||
let insert = "";
|
|
||||||
for (let i = 0, e = context.length - 2; i <= e; i++)
|
|
||||||
insert += context[i].blank(i < e);
|
|
||||||
insert += state.lineBreak;
|
|
||||||
return { range: EditorSelection.cursor(pos + insert.length), changes: { from: line.from, insert } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inner.node.name == "Blockquote" && emptyLine && line.from) {
|
|
||||||
let prevLine = doc.lineAt(line.from - 1), quoted = />\s*$/.exec(prevLine.text);
|
|
||||||
// Two aligned empty quoted lines in a row
|
|
||||||
if (quoted && quoted.index == inner.from) {
|
|
||||||
let changes = state.changes([{ from: prevLine.from + quoted.index, to: prevLine.to },
|
|
||||||
{ from: line.from + inner.from, to: line.to }]);
|
|
||||||
return { range: range.map(changes), changes };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let changes = [];
|
|
||||||
if (inner.node.name == "OrderedList")
|
|
||||||
renumberList(inner.item, doc, changes);
|
|
||||||
let insert = state.lineBreak;
|
|
||||||
let continued = inner.item && inner.item.from < line.from;
|
|
||||||
// If not dedented
|
|
||||||
if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)[0].length >= inner.to) {
|
|
||||||
for (let i = 0, e = context.length - 1; i <= e; i++)
|
|
||||||
insert += i == e && !continued ? context[i].marker(doc, 1) : context[i].blank();
|
|
||||||
}
|
|
||||||
let from = pos;
|
|
||||||
while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1)))
|
|
||||||
from--;
|
|
||||||
changes.push({ from, to: pos, insert });
|
|
||||||
return { range: EditorSelection.cursor(from + insert.length), changes };
|
|
||||||
});
|
|
||||||
if (dont)
|
|
||||||
return false;
|
|
||||||
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" }));
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
function isMark(node) {
|
|
||||||
return node.name == "QuoteMark" || node.name == "ListMark";
|
|
||||||
}
|
|
||||||
function contextNodeForDelete(tree, pos) {
|
|
||||||
let node = tree.resolveInner(pos, -1), scan = pos;
|
|
||||||
if (isMark(node)) {
|
|
||||||
scan = node.from;
|
|
||||||
node = node.parent;
|
|
||||||
}
|
|
||||||
for (let prev; prev = node.childBefore(scan);) {
|
|
||||||
if (isMark(prev)) {
|
|
||||||
scan = prev.from;
|
|
||||||
}
|
|
||||||
else if (prev.name == "OrderedList" || prev.name == "BulletList") {
|
|
||||||
node = prev.lastChild;
|
|
||||||
scan = node.to;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
/// This command will, when invoked in a Markdown context with the
|
|
||||||
/// cursor directly after list or blockquote markup, delete one level
|
|
||||||
/// of markup. When the markup is for a list, it will be replaced by
|
|
||||||
/// spaces on the first invocation (a further invocation will delete
|
|
||||||
/// the spaces), to make it easy to continue a list.
|
|
||||||
///
|
|
||||||
/// When not after Markdown block markup, this command will return
|
|
||||||
/// false, so it is intended to be bound alongside other deletion
|
|
||||||
/// commands, with a higher precedence than the more generic commands.
|
|
||||||
export const deleteMarkupBackward = ({ state, dispatch }) => {
|
|
||||||
let tree = syntaxTree(state);
|
|
||||||
let dont = null, changes = state.changeByRange(range => {
|
|
||||||
let pos = range.from, { doc } = state;
|
|
||||||
if (range.empty && markdownLanguage.isActiveAt(state, range.from)) {
|
|
||||||
let line = doc.lineAt(pos);
|
|
||||||
let context = getContext(contextNodeForDelete(tree, pos), line.text, doc);
|
|
||||||
if (context.length) {
|
|
||||||
let inner = context[context.length - 1];
|
|
||||||
let spaceEnd = inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0);
|
|
||||||
// Delete extra trailing space after markup
|
|
||||||
if (pos - line.from > spaceEnd && !/\S/.test(line.text.slice(spaceEnd, pos - line.from)))
|
|
||||||
return { range: EditorSelection.cursor(line.from + spaceEnd),
|
|
||||||
changes: { from: line.from + spaceEnd, to: pos } };
|
|
||||||
if (pos - line.from == spaceEnd) {
|
|
||||||
let start = line.from + inner.from;
|
|
||||||
// Replace a list item marker with blank space
|
|
||||||
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to)))
|
|
||||||
return { range, changes: { from: start, to: line.from + inner.to, insert: inner.blank() } };
|
|
||||||
// Delete one level of indentation
|
|
||||||
if (start < pos)
|
|
||||||
return { range: EditorSelection.cursor(start), changes: { from: start, to: pos } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dont = { range };
|
|
||||||
});
|
|
||||||
if (dont)
|
|
||||||
return false;
|
|
||||||
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "delete" }));
|
|
||||||
return true;
|
|
||||||
};
|
|
@ -1,10 +1,15 @@
|
|||||||
import {StateCommand, Text, EditorSelection, ChangeSpec} from "@codemirror/state"
|
import {
|
||||||
import {syntaxTree} from "@codemirror/language"
|
StateCommand,
|
||||||
import {SyntaxNode, Tree} from "@lezer/common"
|
Text,
|
||||||
import {markdownLanguage} from "./markdown"
|
EditorSelection,
|
||||||
|
ChangeSpec,
|
||||||
|
} from "@codemirror/state";
|
||||||
|
import { syntaxTree } from "@codemirror/language";
|
||||||
|
import { SyntaxNode, Tree } from "@lezer/common";
|
||||||
|
import { markdownLanguage } from "./markdown";
|
||||||
|
|
||||||
function nodeStart(node: SyntaxNode, doc: Text) {
|
function nodeStart(node: SyntaxNode, doc: Text) {
|
||||||
return doc.sliceString(node.from, node.from + 50)
|
return doc.sliceString(node.from, node.from + 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Context {
|
class Context {
|
||||||
@ -19,65 +24,126 @@ class Context {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
blank(trailing: boolean = true) {
|
blank(trailing: boolean = true) {
|
||||||
let result = this.spaceBefore
|
let result = this.spaceBefore;
|
||||||
if (this.node.name == "Blockquote") result += ">"
|
if (this.node.name == "Blockquote") {
|
||||||
else for (let i = this.to - this.from - result.length - this.spaceAfter.length; i > 0; i--) result += " "
|
result += ">";
|
||||||
return result + (trailing ? this.spaceAfter : "")
|
} else if (this.node.name == "Comment") {
|
||||||
|
result += "%%";
|
||||||
|
} else
|
||||||
|
for (
|
||||||
|
let i = this.to - this.from - result.length - this.spaceAfter.length;
|
||||||
|
i > 0;
|
||||||
|
i--
|
||||||
|
)
|
||||||
|
result += " ";
|
||||||
|
return result + (trailing ? this.spaceAfter : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
marker(doc: Text, add: number) {
|
marker(doc: Text, add: number) {
|
||||||
let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item!, doc)[2] + add)) : ""
|
let number =
|
||||||
return this.spaceBefore + number + this.type + this.spaceAfter
|
this.node.name == "OrderedList"
|
||||||
|
? String(+itemNumber(this.item!, doc)[2] + add)
|
||||||
|
: "";
|
||||||
|
return this.spaceBefore + number + this.type + this.spaceAfter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContext(node: SyntaxNode, line: string, doc: Text) {
|
function getContext(node: SyntaxNode, line: string, doc: Text) {
|
||||||
let nodes = []
|
let nodes = [];
|
||||||
for (let cur: SyntaxNode | null = node; cur && cur.name != "Document"; cur = cur.parent) {
|
for (
|
||||||
if (cur.name == "ListItem" || cur.name == "Blockquote")
|
let cur: SyntaxNode | null = node;
|
||||||
nodes.push(cur)
|
cur && cur.name != "Document";
|
||||||
|
cur = cur.parent
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
cur.name == "ListItem" ||
|
||||||
|
cur.name == "Blockquote" ||
|
||||||
|
cur.name == "Comment"
|
||||||
|
)
|
||||||
|
nodes.push(cur);
|
||||||
}
|
}
|
||||||
let context = [], pos = 0
|
let context = [],
|
||||||
|
pos = 0;
|
||||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||||
let node = nodes[i], match, start = pos
|
let node = nodes[i],
|
||||||
if (node.name == "Blockquote" && (match = /^[ \t]*>( ?)/.exec(line.slice(pos)))) {
|
match,
|
||||||
pos += match[0].length
|
start = pos;
|
||||||
context.push(new Context(node, start, pos, "", match[1], ">", null))
|
if (
|
||||||
} else if (node.name == "ListItem" && node.parent!.name == "OrderedList" &&
|
node.name == "Blockquote" &&
|
||||||
(match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(nodeStart(node, doc)))) {
|
(match = /^[ \t]*>( ?)/.exec(line.slice(pos)))
|
||||||
let after = match[3], len = match[0].length
|
) {
|
||||||
if (after.length >= 4) { after = after.slice(0, after.length - 4); len -= 4 }
|
pos += match[0].length;
|
||||||
pos += len
|
context.push(new Context(node, start, pos, "", match[1], ">", null));
|
||||||
context.push(new Context(node.parent!, start, pos, match[1], after, match[2], node))
|
} else if (
|
||||||
} else if (node.name == "ListItem" && node.parent!.name == "BulletList" &&
|
node.name == "Comment" &&
|
||||||
(match = /^([ \t]*)([-+*])([ \t]+)/.exec(nodeStart(node, doc)))) {
|
(match = /^[ \t]*%%( ?)/.exec(line.slice(pos)))
|
||||||
let after = match[3], len = match[0].length
|
) {
|
||||||
if (after.length > 4) { after = after.slice(0, after.length - 4); len -= 4 }
|
pos += match[0].length;
|
||||||
pos += len
|
context.push(new Context(node, start, pos, "", match[1], "%%", null));
|
||||||
context.push(new Context(node.parent!, start, pos, match[1], after, match[2], node))
|
} else if (
|
||||||
|
node.name == "ListItem" &&
|
||||||
|
node.parent!.name == "OrderedList" &&
|
||||||
|
(match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(nodeStart(node, doc)))
|
||||||
|
) {
|
||||||
|
let after = match[3],
|
||||||
|
len = match[0].length;
|
||||||
|
if (after.length >= 4) {
|
||||||
|
after = after.slice(0, after.length - 4);
|
||||||
|
len -= 4;
|
||||||
|
}
|
||||||
|
pos += len;
|
||||||
|
context.push(
|
||||||
|
new Context(node.parent!, start, pos, match[1], after, match[2], node)
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
node.name == "ListItem" &&
|
||||||
|
node.parent!.name == "BulletList" &&
|
||||||
|
(match = /^([ \t]*)([-+*])([ \t]+)/.exec(nodeStart(node, doc)))
|
||||||
|
) {
|
||||||
|
let after = match[3],
|
||||||
|
len = match[0].length;
|
||||||
|
if (after.length > 4) {
|
||||||
|
after = after.slice(0, after.length - 4);
|
||||||
|
len -= 4;
|
||||||
|
}
|
||||||
|
pos += len;
|
||||||
|
context.push(
|
||||||
|
new Context(node.parent!, start, pos, match[1], after, match[2], node)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemNumber(item: SyntaxNode, doc: Text) {
|
function itemNumber(item: SyntaxNode, doc: Text) {
|
||||||
return /^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from, item.from + 10))!
|
return /^(\s*)(\d+)(?=[.)])/.exec(
|
||||||
|
doc.sliceString(item.from, item.from + 10)
|
||||||
|
)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renumberList(after: SyntaxNode, doc: Text, changes: ChangeSpec[], offset = 0) {
|
function renumberList(
|
||||||
for (let prev = -1, node = after;;) {
|
after: SyntaxNode,
|
||||||
|
doc: Text,
|
||||||
|
changes: ChangeSpec[],
|
||||||
|
offset = 0
|
||||||
|
) {
|
||||||
|
for (let prev = -1, node = after; ; ) {
|
||||||
if (node.name == "ListItem") {
|
if (node.name == "ListItem") {
|
||||||
let m = itemNumber(node, doc)
|
let m = itemNumber(node, doc);
|
||||||
let number = +m[2]
|
let number = +m[2];
|
||||||
if (prev >= 0) {
|
if (prev >= 0) {
|
||||||
if (number != prev + 1) return
|
if (number != prev + 1) return;
|
||||||
changes.push({from: node.from + m[1].length, to: node.from + m[0].length, insert: String(prev + 2 + offset)})
|
changes.push({
|
||||||
|
from: node.from + m[1].length,
|
||||||
|
to: node.from + m[0].length,
|
||||||
|
insert: String(prev + 2 + offset),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
prev = number
|
prev = number;
|
||||||
}
|
}
|
||||||
let next = node.nextSibling
|
let next = node.nextSibling;
|
||||||
if (!next) break
|
if (!next) break;
|
||||||
node = next
|
node = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,93 +156,149 @@ function renumberList(after: SyntaxNode, doc: Text, changes: ChangeSpec[], offse
|
|||||||
/// The command does nothing in non-Markdown context, so it should
|
/// The command does nothing in non-Markdown context, so it should
|
||||||
/// not be used as the only binding for Enter (even in a Markdown
|
/// not be used as the only binding for Enter (even in a Markdown
|
||||||
/// document, HTML and code regions might use a different language).
|
/// document, HTML and code regions might use a different language).
|
||||||
export const insertNewlineContinueMarkup: StateCommand = ({state, dispatch}) => {
|
export const insertNewlineContinueMarkup: StateCommand = ({
|
||||||
let tree = syntaxTree(state), {doc} = state
|
state,
|
||||||
let dont = null, changes = state.changeByRange(range => {
|
dispatch,
|
||||||
if (!range.empty || !markdownLanguage.isActiveAt(state, range.from)) return dont = {range}
|
}) => {
|
||||||
let pos = range.from, line = doc.lineAt(pos)
|
let tree = syntaxTree(state),
|
||||||
let context = getContext(tree.resolveInner(pos, -1), line.text, doc)
|
{ doc } = state;
|
||||||
while (context.length && context[context.length - 1].from > pos - line.from) context.pop()
|
let dont = null,
|
||||||
if (!context.length) return dont = {range}
|
changes = state.changeByRange((range) => {
|
||||||
let inner = context[context.length - 1]
|
if (!range.empty || !markdownLanguage.isActiveAt(state, range.from))
|
||||||
if (inner.to - inner.spaceAfter.length > pos - line.from) return dont = {range}
|
return (dont = { range });
|
||||||
|
let pos = range.from,
|
||||||
|
line = doc.lineAt(pos);
|
||||||
|
let context = getContext(tree.resolveInner(pos, -1), line.text, doc);
|
||||||
|
while (
|
||||||
|
context.length &&
|
||||||
|
context[context.length - 1].from > pos - line.from
|
||||||
|
)
|
||||||
|
context.pop();
|
||||||
|
if (!context.length) return (dont = { range });
|
||||||
|
let inner = context[context.length - 1];
|
||||||
|
if (inner.to - inner.spaceAfter.length > pos - line.from)
|
||||||
|
return (dont = { range });
|
||||||
|
|
||||||
let emptyLine = pos >= (inner.to - inner.spaceAfter.length) && !/\S/.test(line.text.slice(inner.to))
|
let emptyLine =
|
||||||
// Empty line in list
|
pos >= inner.to - inner.spaceAfter.length &&
|
||||||
if (inner.item && emptyLine) {
|
!/\S/.test(line.text.slice(inner.to));
|
||||||
// First list item or blank line before: delete a level of markup
|
// Empty line in list
|
||||||
if (inner.node.firstChild!.to >= pos ||
|
if (inner.item && emptyLine) {
|
||||||
line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text)) {
|
// First list item or blank line before: delete a level of markup
|
||||||
let next = context.length > 1 ? context[context.length - 2] : null
|
if (
|
||||||
let delTo, insert = ""
|
inner.node.firstChild!.to >= pos ||
|
||||||
if (next && next.item) { // Re-add marker for the list at the next level
|
(line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text))
|
||||||
delTo = line.from + next.from
|
) {
|
||||||
insert = next.marker(doc, 1)
|
let next = context.length > 1 ? context[context.length - 2] : null;
|
||||||
|
let delTo,
|
||||||
|
insert = "";
|
||||||
|
if (next && next.item) {
|
||||||
|
// Re-add marker for the list at the next level
|
||||||
|
delTo = line.from + next.from;
|
||||||
|
insert = next.marker(doc, 1);
|
||||||
|
} else {
|
||||||
|
delTo = line.from + (next ? next.to : 0);
|
||||||
|
}
|
||||||
|
let changes: ChangeSpec[] = [{ from: delTo, to: pos, insert }];
|
||||||
|
if (inner.node.name == "OrderedList")
|
||||||
|
renumberList(inner.item!, doc, changes, -2);
|
||||||
|
if (next && next.node.name == "OrderedList")
|
||||||
|
renumberList(next.item!, doc, changes);
|
||||||
|
return {
|
||||||
|
range: EditorSelection.cursor(delTo + insert.length),
|
||||||
|
changes,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
delTo = line.from + (next ? next.to : 0)
|
// Move this line down
|
||||||
|
let insert = "";
|
||||||
|
for (let i = 0, e = context.length - 2; i <= e; i++)
|
||||||
|
insert += context[i].blank(i < e);
|
||||||
|
insert += state.lineBreak;
|
||||||
|
return {
|
||||||
|
range: EditorSelection.cursor(pos + insert.length),
|
||||||
|
changes: { from: line.from, insert },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let changes: ChangeSpec[] = [{from: delTo, to: pos, insert}]
|
|
||||||
if (inner.node.name == "OrderedList") renumberList(inner.item!, doc, changes, -2)
|
|
||||||
if (next && next.node.name == "OrderedList") renumberList(next.item!, doc, changes)
|
|
||||||
return {range: EditorSelection.cursor(delTo + insert.length), changes}
|
|
||||||
} else { // Move this line down
|
|
||||||
let insert = ""
|
|
||||||
for (let i = 0, e = context.length - 2; i <= e; i++) insert += context[i].blank(i < e)
|
|
||||||
insert += state.lineBreak
|
|
||||||
return {range: EditorSelection.cursor(pos + insert.length), changes: {from: line.from, insert}}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (inner.node.name == "Blockquote" && emptyLine && line.from) {
|
if (inner.node.name == "Blockquote" && emptyLine && line.from) {
|
||||||
let prevLine = doc.lineAt(line.from - 1), quoted = />\s*$/.exec(prevLine.text)
|
let prevLine = doc.lineAt(line.from - 1),
|
||||||
// Two aligned empty quoted lines in a row
|
quoted = />\s*$/.exec(prevLine.text);
|
||||||
if (quoted && quoted.index == inner.from) {
|
// Two aligned empty quoted lines in a row
|
||||||
let changes = state.changes([{from: prevLine.from + quoted.index, to: prevLine.to},
|
if (quoted && quoted.index == inner.from) {
|
||||||
{from: line.from + inner.from, to: line.to}])
|
let changes = state.changes([
|
||||||
return {range: range.map(changes), changes}
|
{ from: prevLine.from + quoted.index, to: prevLine.to },
|
||||||
|
{ from: line.from + inner.from, to: line.to },
|
||||||
|
]);
|
||||||
|
return { range: range.map(changes), changes };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let changes: ChangeSpec[] = []
|
if (inner.node.name == "Comment" && emptyLine && line.from) {
|
||||||
if (inner.node.name == "OrderedList") renumberList(inner.item!, doc, changes)
|
let prevLine = doc.lineAt(line.from - 1),
|
||||||
let insert = state.lineBreak
|
commented = /%%\s*$/.exec(prevLine.text);
|
||||||
let continued = inner.item && inner.item.from < line.from
|
// Two aligned empty quoted lines in a row
|
||||||
// If not dedented
|
if (commented && commented.index == inner.from) {
|
||||||
if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)![0].length >= inner.to) {
|
let changes = state.changes([
|
||||||
for (let i = 0, e = context.length - 1; i <= e; i++)
|
{ from: prevLine.from + commented.index, to: prevLine.to },
|
||||||
insert += i == e && !continued ? context[i].marker(doc, 1) : context[i].blank()
|
{ from: line.from + inner.from, to: line.to },
|
||||||
}
|
]);
|
||||||
let from = pos
|
return { range: range.map(changes), changes };
|
||||||
while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1))) from--
|
}
|
||||||
changes.push({from, to: pos, insert})
|
}
|
||||||
return {range: EditorSelection.cursor(from + insert.length), changes}
|
|
||||||
})
|
let changes: ChangeSpec[] = [];
|
||||||
if (dont) return false
|
if (inner.node.name == "OrderedList")
|
||||||
dispatch(state.update(changes, {scrollIntoView: true, userEvent: "input"}))
|
renumberList(inner.item!, doc, changes);
|
||||||
return true
|
let insert = state.lineBreak;
|
||||||
}
|
let continued = inner.item && inner.item.from < line.from;
|
||||||
|
// If not dedented
|
||||||
|
if (
|
||||||
|
!continued ||
|
||||||
|
/^[\s\d.)\-+*>]*/.exec(line.text)![0].length >= inner.to
|
||||||
|
) {
|
||||||
|
for (let i = 0, e = context.length - 1; i <= e; i++)
|
||||||
|
insert +=
|
||||||
|
i == e && !continued
|
||||||
|
? context[i].marker(doc, 1)
|
||||||
|
: context[i].blank();
|
||||||
|
}
|
||||||
|
let from = pos;
|
||||||
|
while (
|
||||||
|
from > line.from &&
|
||||||
|
/\s/.test(line.text.charAt(from - line.from - 1))
|
||||||
|
)
|
||||||
|
from--;
|
||||||
|
changes.push({ from, to: pos, insert });
|
||||||
|
return { range: EditorSelection.cursor(from + insert.length), changes };
|
||||||
|
});
|
||||||
|
if (dont) return false;
|
||||||
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" }));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
function isMark(node: SyntaxNode) {
|
function isMark(node: SyntaxNode) {
|
||||||
return node.name == "QuoteMark" || node.name == "ListMark"
|
return node.name == "QuoteMark" || node.name == "ListMark";
|
||||||
}
|
}
|
||||||
|
|
||||||
function contextNodeForDelete(tree: Tree, pos: number) {
|
function contextNodeForDelete(tree: Tree, pos: number) {
|
||||||
let node = tree.resolveInner(pos, -1), scan = pos
|
let node = tree.resolveInner(pos, -1),
|
||||||
|
scan = pos;
|
||||||
if (isMark(node)) {
|
if (isMark(node)) {
|
||||||
scan = node.from
|
scan = node.from;
|
||||||
node = node.parent!
|
node = node.parent!;
|
||||||
}
|
}
|
||||||
for (let prev; prev = node.childBefore(scan);) {
|
for (let prev; (prev = node.childBefore(scan)); ) {
|
||||||
if (isMark(prev)) {
|
if (isMark(prev)) {
|
||||||
scan = prev.from
|
scan = prev.from;
|
||||||
} else if (prev.name == "OrderedList" || prev.name == "BulletList") {
|
} else if (prev.name == "OrderedList" || prev.name == "BulletList") {
|
||||||
node = prev.lastChild!
|
node = prev.lastChild!;
|
||||||
scan = node.to
|
scan = node.to;
|
||||||
} else {
|
} else {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return node
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This command will, when invoked in a Markdown context with the
|
/// This command will, when invoked in a Markdown context with the
|
||||||
@ -188,34 +310,62 @@ function contextNodeForDelete(tree: Tree, pos: number) {
|
|||||||
/// When not after Markdown block markup, this command will return
|
/// When not after Markdown block markup, this command will return
|
||||||
/// false, so it is intended to be bound alongside other deletion
|
/// false, so it is intended to be bound alongside other deletion
|
||||||
/// commands, with a higher precedence than the more generic commands.
|
/// commands, with a higher precedence than the more generic commands.
|
||||||
export const deleteMarkupBackward: StateCommand = ({state, dispatch}) => {
|
export const deleteMarkupBackward: StateCommand = ({ state, dispatch }) => {
|
||||||
let tree = syntaxTree(state)
|
let tree = syntaxTree(state);
|
||||||
let dont = null, changes = state.changeByRange(range => {
|
let dont = null,
|
||||||
let pos = range.from, {doc} = state
|
changes = state.changeByRange((range) => {
|
||||||
if (range.empty && markdownLanguage.isActiveAt(state, range.from)) {
|
let pos = range.from,
|
||||||
let line = doc.lineAt(pos)
|
{ doc } = state;
|
||||||
let context = getContext(contextNodeForDelete(tree, pos), line.text, doc)
|
if (range.empty && markdownLanguage.isActiveAt(state, range.from)) {
|
||||||
if (context.length) {
|
let line = doc.lineAt(pos);
|
||||||
let inner = context[context.length - 1]
|
let context = getContext(
|
||||||
let spaceEnd = inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0)
|
contextNodeForDelete(tree, pos),
|
||||||
// Delete extra trailing space after markup
|
line.text,
|
||||||
if (pos - line.from > spaceEnd && !/\S/.test(line.text.slice(spaceEnd, pos - line.from)))
|
doc
|
||||||
return {range: EditorSelection.cursor(line.from + spaceEnd),
|
);
|
||||||
changes: {from: line.from + spaceEnd, to: pos}}
|
if (context.length) {
|
||||||
if (pos - line.from == spaceEnd) {
|
let inner = context[context.length - 1];
|
||||||
let start = line.from + inner.from
|
let spaceEnd =
|
||||||
// Replace a list item marker with blank space
|
inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0);
|
||||||
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to)))
|
// Delete extra trailing space after markup
|
||||||
return {range, changes: {from: start, to: line.from + inner.to, insert: inner.blank()}}
|
if (
|
||||||
// Delete one level of indentation
|
pos - line.from > spaceEnd &&
|
||||||
if (start < pos)
|
!/\S/.test(line.text.slice(spaceEnd, pos - line.from))
|
||||||
return {range: EditorSelection.cursor(start), changes: {from: start, to: pos}}
|
)
|
||||||
|
return {
|
||||||
|
range: EditorSelection.cursor(line.from + spaceEnd),
|
||||||
|
changes: { from: line.from + spaceEnd, to: pos },
|
||||||
|
};
|
||||||
|
if (pos - line.from == spaceEnd) {
|
||||||
|
let start = line.from + inner.from;
|
||||||
|
// Replace a list item marker with blank space
|
||||||
|
if (
|
||||||
|
inner.item &&
|
||||||
|
inner.node.from < inner.item.from &&
|
||||||
|
/\S/.test(line.text.slice(inner.from, inner.to))
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
changes: {
|
||||||
|
from: start,
|
||||||
|
to: line.from + inner.to,
|
||||||
|
insert: inner.blank(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Delete one level of indentation
|
||||||
|
if (start < pos)
|
||||||
|
return {
|
||||||
|
range: EditorSelection.cursor(start),
|
||||||
|
changes: { from: start, to: pos },
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return (dont = { range });
|
||||||
return dont = {range}
|
});
|
||||||
})
|
if (dont) return false;
|
||||||
if (dont) return false
|
dispatch(
|
||||||
dispatch(state.update(changes, {scrollIntoView: true, userEvent: "delete"}))
|
state.update(changes, { scrollIntoView: true, userEvent: "delete" })
|
||||||
return true
|
);
|
||||||
}
|
return true;
|
||||||
|
};
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import { Prec } from "@codemirror/state";
|
|
||||||
import { keymap } from "@codemirror/view";
|
|
||||||
import { LanguageSupport } from "@codemirror/language";
|
|
||||||
import { MarkdownParser, parseCode } from "@lezer/markdown";
|
|
||||||
import { html } from "@codemirror/lang-html";
|
|
||||||
import { commonmarkLanguage, markdownLanguage, mkLang, getCodeParser } from "./markdown";
|
|
||||||
import { insertNewlineContinueMarkup, deleteMarkupBackward } from "./commands";
|
|
||||||
export { commonmarkLanguage, markdownLanguage, insertNewlineContinueMarkup, deleteMarkupBackward };
|
|
||||||
/// A small keymap with Markdown-specific bindings. Binds Enter to
|
|
||||||
/// [`insertNewlineContinueMarkup`](#lang-markdown.insertNewlineContinueMarkup)
|
|
||||||
/// and Backspace to
|
|
||||||
/// [`deleteMarkupBackward`](#lang-markdown.deleteMarkupBackward).
|
|
||||||
export const markdownKeymap = [
|
|
||||||
{ key: "Enter", run: insertNewlineContinueMarkup },
|
|
||||||
{ key: "Backspace", run: deleteMarkupBackward }
|
|
||||||
];
|
|
||||||
const htmlNoMatch = html({ matchClosingTags: false });
|
|
||||||
/// Markdown language support.
|
|
||||||
export function markdown(config = {}) {
|
|
||||||
let { codeLanguages, defaultCodeLanguage, addKeymap = true, base: { parser } = commonmarkLanguage } = config;
|
|
||||||
if (!(parser instanceof MarkdownParser))
|
|
||||||
throw new RangeError("Base parser provided to `markdown` should be a Markdown parser");
|
|
||||||
let extensions = config.extensions ? [config.extensions] : [];
|
|
||||||
let support = [htmlNoMatch.support], defaultCode;
|
|
||||||
if (defaultCodeLanguage instanceof LanguageSupport) {
|
|
||||||
support.push(defaultCodeLanguage.support);
|
|
||||||
defaultCode = defaultCodeLanguage.language;
|
|
||||||
}
|
|
||||||
else if (defaultCodeLanguage) {
|
|
||||||
defaultCode = defaultCodeLanguage;
|
|
||||||
}
|
|
||||||
let codeParser = codeLanguages || defaultCode ? getCodeParser(codeLanguages || [], defaultCode) : undefined;
|
|
||||||
extensions.push(parseCode({ codeParser, htmlParser: htmlNoMatch.language.parser }));
|
|
||||||
if (addKeymap)
|
|
||||||
support.push(Prec.high(keymap.of(markdownKeymap)));
|
|
||||||
return new LanguageSupport(mkLang(parser.configure(extensions)), support);
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import { Language, defineLanguageFacet, languageDataProp, foldNodeProp, indentNodeProp, LanguageDescription, ParseContext } from "@codemirror/language";
|
|
||||||
import { styleTags, tags as t } from "@codemirror/highlight";
|
|
||||||
import { parser as baseParser, GFM, Subscript, Superscript, Emoji } from "@lezer/markdown";
|
|
||||||
const data = defineLanguageFacet({ block: { open: "<!--", close: "-->" } });
|
|
||||||
export const commonmark = baseParser.configure({
|
|
||||||
props: [
|
|
||||||
styleTags({
|
|
||||||
"Blockquote/...": t.quote,
|
|
||||||
HorizontalRule: t.contentSeparator,
|
|
||||||
"ATXHeading1/... SetextHeading1/...": t.heading1,
|
|
||||||
"ATXHeading2/... SetextHeading2/...": t.heading2,
|
|
||||||
"ATXHeading3/...": t.heading3,
|
|
||||||
"ATXHeading4/...": t.heading4,
|
|
||||||
"ATXHeading5/...": t.heading5,
|
|
||||||
"ATXHeading6/...": t.heading6,
|
|
||||||
"Comment CommentBlock": t.comment,
|
|
||||||
Escape: t.escape,
|
|
||||||
Entity: t.character,
|
|
||||||
"Emphasis/...": t.emphasis,
|
|
||||||
"StrongEmphasis/...": t.strong,
|
|
||||||
"Link/... Image/...": t.link,
|
|
||||||
"OrderedList/... BulletList/...": t.list,
|
|
||||||
// "CodeBlock/... FencedCode/...": t.blockComment,
|
|
||||||
"InlineCode CodeText": t.monospace,
|
|
||||||
URL: t.url,
|
|
||||||
"HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark": t.processingInstruction,
|
|
||||||
"CodeInfo LinkLabel": t.labelName,
|
|
||||||
LinkTitle: t.string,
|
|
||||||
Paragraph: t.content
|
|
||||||
}),
|
|
||||||
foldNodeProp.add(type => {
|
|
||||||
if (!type.is("Block") || type.is("Document"))
|
|
||||||
return undefined;
|
|
||||||
return (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to });
|
|
||||||
}),
|
|
||||||
indentNodeProp.add({
|
|
||||||
Document: () => null
|
|
||||||
}),
|
|
||||||
languageDataProp.add({
|
|
||||||
Document: data
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
export function mkLang(parser) {
|
|
||||||
return new Language(data, parser, parser.nodeSet.types.find(t => t.name == "Document"));
|
|
||||||
}
|
|
||||||
/// Language support for strict CommonMark.
|
|
||||||
export const commonmarkLanguage = mkLang(commonmark);
|
|
||||||
const extended = commonmark.configure([GFM, Subscript, Superscript, Emoji, {
|
|
||||||
props: [
|
|
||||||
styleTags({
|
|
||||||
"TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark": t.processingInstruction,
|
|
||||||
"TableHeader/...": t.heading,
|
|
||||||
"Strikethrough/...": t.strikethrough,
|
|
||||||
TaskMarker: t.atom,
|
|
||||||
Task: t.list,
|
|
||||||
Emoji: t.character,
|
|
||||||
"Subscript Superscript": t.special(t.content),
|
|
||||||
TableCell: t.content
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}]);
|
|
||||||
/// Language support for [GFM](https://github.github.com/gfm/) plus
|
|
||||||
/// subscript, superscript, and emoji syntax.
|
|
||||||
export const markdownLanguage = mkLang(extended);
|
|
||||||
export function getCodeParser(languages, defaultLanguage) {
|
|
||||||
return (info) => {
|
|
||||||
let found = info && LanguageDescription.matchLanguageName(languages, info, true);
|
|
||||||
if (!found)
|
|
||||||
return defaultLanguage ? defaultLanguage.parser : null;
|
|
||||||
if (found.support)
|
|
||||||
return found.support.language.parser;
|
|
||||||
return ParseContext.getSkippingParser(found.load());
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,5 +1,11 @@
|
|||||||
import { styleTags, tags as t } from "@codemirror/highlight";
|
import { styleTags, tags as t } from "@codemirror/highlight";
|
||||||
import { MarkdownConfig, TaskList } from "@lezer/markdown";
|
import {
|
||||||
|
MarkdownConfig,
|
||||||
|
TaskList,
|
||||||
|
BlockContext,
|
||||||
|
LeafBlock,
|
||||||
|
LeafBlockParser,
|
||||||
|
} from "@lezer/markdown";
|
||||||
import { commonmark, mkLang } from "./markdown/markdown";
|
import { commonmark, mkLang } from "./markdown/markdown";
|
||||||
import * as ct from "./customtags";
|
import * as ct from "./customtags";
|
||||||
import { pageLinkRegex } from "./constant";
|
import { pageLinkRegex } from "./constant";
|
||||||
@ -77,6 +83,35 @@ const UnmarkedUrl: MarkdownConfig = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class CommentParser implements LeafBlockParser {
|
||||||
|
nextLine() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(cx: BlockContext, leaf: LeafBlock) {
|
||||||
|
cx.addLeafElement(
|
||||||
|
leaf,
|
||||||
|
cx.elt("Comment", leaf.start, leaf.start + leaf.content.length, [
|
||||||
|
// cx.elt("CommentMarker", leaf.start, leaf.start + 3),
|
||||||
|
...cx.parser.parseInline(leaf.content.slice(3), leaf.start + 3),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const Comment: MarkdownConfig = {
|
||||||
|
defineNodes: [{ name: "Comment", block: true }],
|
||||||
|
parseBlock: [
|
||||||
|
{
|
||||||
|
name: "Comment",
|
||||||
|
leaf(cx, leaf) {
|
||||||
|
return /^%%\s/.test(leaf.content) ? new CommentParser() : null;
|
||||||
|
},
|
||||||
|
after: "SetextHeading",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const TagLink: MarkdownConfig = {
|
const TagLink: MarkdownConfig = {
|
||||||
defineNodes: ["TagLink"],
|
defineNodes: ["TagLink"],
|
||||||
parseInline: [
|
parseInline: [
|
||||||
@ -102,6 +137,7 @@ const WikiMarkdown = commonmark.configure([
|
|||||||
TagLink,
|
TagLink,
|
||||||
TaskList,
|
TaskList,
|
||||||
UnmarkedUrl,
|
UnmarkedUrl,
|
||||||
|
Comment,
|
||||||
{
|
{
|
||||||
props: [
|
props: [
|
||||||
styleTags({
|
styleTags({
|
||||||
@ -112,6 +148,8 @@ const WikiMarkdown = commonmark.configure([
|
|||||||
Task: ct.TaskTag,
|
Task: ct.TaskTag,
|
||||||
TaskMarker: ct.TaskMarkerTag,
|
TaskMarker: ct.TaskMarkerTag,
|
||||||
Url: t.url,
|
Url: t.url,
|
||||||
|
Comment: ct.CommentTag,
|
||||||
|
// CommentMarker: ct.CommentMarkerTag,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ import * as ct from "./customtags";
|
|||||||
export default HighlightStyle.define([
|
export default HighlightStyle.define([
|
||||||
{ tag: t.heading1, class: "h1" },
|
{ tag: t.heading1, class: "h1" },
|
||||||
{ tag: t.heading2, class: "h2" },
|
{ tag: t.heading2, class: "h2" },
|
||||||
|
{ tag: t.heading3, class: "h3" },
|
||||||
{ tag: t.link, class: "link" },
|
{ tag: t.link, class: "link" },
|
||||||
{ tag: t.meta, class: "meta" },
|
{ tag: t.meta, class: "meta" },
|
||||||
{ tag: t.quote, class: "quote" },
|
{ tag: t.quote, class: "quote" },
|
||||||
@ -15,6 +16,8 @@ export default HighlightStyle.define([
|
|||||||
{ tag: ct.MentionTag, class: "mention" },
|
{ tag: ct.MentionTag, class: "mention" },
|
||||||
{ tag: ct.TaskTag, class: "task" },
|
{ tag: ct.TaskTag, class: "task" },
|
||||||
{ tag: ct.TaskMarkerTag, class: "task-marker" },
|
{ tag: ct.TaskMarkerTag, class: "task-marker" },
|
||||||
|
{ tag: ct.CommentTag, class: "comment" },
|
||||||
|
{ tag: ct.CommentMarkerTag, class: "comment-marker" },
|
||||||
{ tag: t.emphasis, class: "emphasis" },
|
{ tag: t.emphasis, class: "emphasis" },
|
||||||
{ tag: t.strong, class: "strong" },
|
{ tag: t.strong, class: "strong" },
|
||||||
{ tag: t.atom, class: "atom" },
|
{ tag: t.atom, class: "atom" },
|
||||||
|
@ -13,34 +13,29 @@
|
|||||||
background-color: #d7e1f6 !important;
|
background-color: #d7e1f6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h1 {
|
.line-h1,
|
||||||
|
.line-h2,
|
||||||
|
.line-h3 {
|
||||||
|
background-color: rgba(0, 15, 52, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 2px;
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-line.line-h1 {
|
.line-h2 {
|
||||||
display: block;
|
|
||||||
background-color: rgba(0, 15, 52, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h1.meta {
|
|
||||||
color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h2 {
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-line.line-h2 {
|
.line-h3 {
|
||||||
display: block;
|
font-size: 1.1em;
|
||||||
background-color: rgba(0, 15, 52, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h2.meta {
|
|
||||||
color: orange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color list item this way */
|
/* Color list item this way */
|
||||||
@ -66,10 +61,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.line-blockquote {
|
.line-blockquote {
|
||||||
background-color: #eee;
|
background-color: rgba(220, 220, 220, 0.5);
|
||||||
color: #676767;
|
color: #676767;
|
||||||
text-indent: calc(-1 * (var(--ident) + 3px));
|
text-indent: -2ch;
|
||||||
padding-left: var(--ident);
|
padding-left: 2ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emphasis {
|
.emphasis {
|
||||||
@ -88,6 +83,7 @@
|
|||||||
.link.url {
|
.link.url {
|
||||||
color: #7e7d7d;
|
color: #7e7d7d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url:not(.link) {
|
.url:not(.link) {
|
||||||
color: #0330cb;
|
color: #0330cb;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@ -109,12 +105,40 @@
|
|||||||
color: #8d8d8d;
|
color: #8d8d8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-li {
|
.code {
|
||||||
text-indent: calc(-1 * var(--ident) - 3px);
|
background-color: #efefef;
|
||||||
margin-left: var(--ident);
|
}
|
||||||
|
|
||||||
|
.line-li-1 {
|
||||||
|
text-indent: -2ch;
|
||||||
|
padding-left: 2ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-li-1.line-li-2 {
|
||||||
|
text-indent: -4ch;
|
||||||
|
padding-left: 4ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-li-1.line-li-2.line-li-3 {
|
||||||
|
text-indent: -6ch;
|
||||||
|
padding-left: 6ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-li-1.line-li-2.line-li-3.line-li-4 {
|
||||||
|
text-indent: -8ch;
|
||||||
|
padding-left: 8ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-li-1.line-li-2.line-li-3.line-li-4.line-li-5 {
|
||||||
|
text-indent: -10ch;
|
||||||
|
padding-left: 10ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-marker {
|
.task-marker {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-comment {
|
||||||
|
background-color: rgba(255, 255, 0, 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,29 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
|
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
|
||||||
|
|
||||||
label {
|
|
||||||
color: var(--highlight-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
border-bottom: 1px rgb(108, 108, 108) solid;
|
border-bottom: 1px rgb(108, 108, 108) solid;
|
||||||
padding: 13px 10px 10px 10px;
|
padding: 13px 10px 10px 10px;
|
||||||
|
display: flex;
|
||||||
|
label {
|
||||||
|
color: var(--highlight-color);
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-family: "Arial";
|
||||||
|
background: transparent;
|
||||||
|
color: #000;
|
||||||
|
border: 0;
|
||||||
|
padding: 3px;
|
||||||
|
outline: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
flex-grow: 100;
|
||||||
|
}
|
||||||
|
input::placeholder {
|
||||||
|
color: rgb(199, 199, 199);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-text {
|
.help-text {
|
||||||
@ -45,21 +61,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
|
||||||
font-family: "Arial";
|
|
||||||
background: transparent;
|
|
||||||
color: #000;
|
|
||||||
border: 0;
|
|
||||||
padding: 3px;
|
|
||||||
outline: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input::placeholder {
|
|
||||||
color: rgb(199, 199, 199);
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option,
|
.option,
|
||||||
.selected-option {
|
.selected-option {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -63,7 +63,7 @@ body {
|
|||||||
|
|
||||||
#editor {
|
#editor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 60px;
|
top: 55px;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -127,7 +127,7 @@ export default (editor: Editor) => ({
|
|||||||
"editor.dispatch": (change: Transaction) => {
|
"editor.dispatch": (change: Transaction) => {
|
||||||
editor.editorView!.dispatch(change);
|
editor.editorView!.dispatch(change);
|
||||||
},
|
},
|
||||||
"editor.prompt": (message: string): string | null => {
|
"editor.prompt": (message: string, defaultValue = ""): string | null => {
|
||||||
return prompt(message);
|
return prompt(message, defaultValue);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user