1
0
This commit is contained in:
Zef Hemel 2022-03-03 10:35:32 +01:00
parent 9b1b950c41
commit f73acae41a
20 changed files with 571 additions and 570 deletions

1
plugins/.gitignore vendored
View File

@ -1 +0,0 @@
dist

View File

@ -1,7 +1,7 @@
DENO_BUNDLE=deno run --allow-read --allow-write --unstable bundle.ts --debug
build: *
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:
ls core/* | entr make

View File

@ -1,8 +1,5 @@
{
"commands": {
"Count Words": {
"invoke": "word_count_command"
},
"Navigate To page": {
"invoke": "linkNavigate",
"key": "Ctrl-Enter",
@ -68,9 +65,6 @@
"taskToggle": {
"path": "./task.ts:taskToggle"
},
"word_count_command": {
"path": "./word_count_command.ts:wordCount"
},
"insertToday": {
"path": "./dates.ts:insertToday"
},

View File

@ -29,9 +29,15 @@ export async function deletePage() {
}
export async function renamePage() {
// console.log("HELLO WORLD");
const pageMeta = await syscall("editor.getCurrentPage");
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) {
return;
}

View File

@ -7,7 +7,7 @@
"license": "MIT",
"browserslist": "> 0.5%, last 2 versions, not dead",
"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/",
"clean": "rm -rf dist",
"check-watch": "tsc --noEmit --watch"

View File

@ -2,7 +2,14 @@ import { PageMeta } from "../types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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,
onClick,
}: {
@ -15,7 +22,7 @@ export function NavigationBar({
<span className="icon">
<FontAwesomeIcon icon={faFileLines} />
</span>
<span className="current-page">{currentPage?.name}</span>
<span className="current-page">{prettyName(currentPage?.name)}</span>
</div>
</div>
);

View File

@ -6,3 +6,5 @@ export const TagTag = Tag.define();
export const MentionTag = Tag.define();
export const TaskTag = Tag.define();
export const TaskMarkerTag = Tag.define();
export const CommentTag = Tag.define();
export const CommentMarkerTag = Tag.define();

View File

@ -8,7 +8,6 @@ import {
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
import { indentWithTab, standardKeymap } from "@codemirror/commands";
import { history, historyKeymap } from "@codemirror/history";
import { indentOnInput, syntaxTree } from "@codemirror/language";
import { bracketMatching } from "@codemirror/matchbrackets";
import { searchKeymap } from "@codemirror/search";
import { EditorState, StateField, Transaction } from "@codemirror/state";
@ -20,24 +19,31 @@ import {
KeyBinding,
keymap,
} from "@codemirror/view";
import React, { useEffect, useReducer } from "react";
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 { CommandPalette } from "./components/command_palette";
import { NavigationBar } from "./components/navigation_bar";
import { PageNavigator } from "./components/page_navigator";
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 { markdown } from "./markdown";
import { IPageNavigator, PathPageNavigator } from "./navigator";
import customMarkDown from "./parser";
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 { smartQuoteKeymap } from "./smart_quotes";
import { Space } from "./space";
import customMarkdownStyle from "./style";
import dbSyscalls from "./syscalls/db.localstorage";
import { Plugin } from "./plugins/runtime";
import editorSyscalls from "./syscalls/editor.browser";
import indexerSyscalls from "./syscalls/indexer.native";
import spaceSyscalls from "./syscalls/space.native";
@ -48,16 +54,7 @@ import {
initialViewState,
PageMeta,
} from "./types";
import {
AppEvent,
AppEventDispatcher,
ClickEvent,
IndexEvent,
} from "./app_event";
import { safeRun } from "./util";
import { Indexer } from "./indexer";
import { IPageNavigator, PathPageNavigator } from "./navigator";
import { smartQuoteKeymap } from "./smart_quotes";
class PageState {
editorState: EditorState;
@ -203,7 +200,7 @@ export class Editor implements AppEventDispatcher {
history(),
drawSelection(),
dropCursor(),
indentOnInput(),
// indentOnInput(),
customMarkdownStyle,
bracketMatching(),
closeBrackets(),
@ -217,10 +214,12 @@ export class Editor implements AppEventDispatcher {
lineWrapper([
{ selector: "ATXHeading1", class: "line-h1" },
{ 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: "CodeBlock", class: "line-code" },
{ selector: "FencedCode", class: "line-fenced-code" },
{ selector: "Comment", class: "line-comment" },
]),
keymap.of([
...smartQuoteKeymap,
@ -535,7 +534,7 @@ export class Editor implements AppEventDispatcher {
commands={viewState.commands}
/>
)}
<NavigationBar
<TopBar
currentPage={viewState.currentPage}
onClick={() => {
dispatch({ type: "start-navigate" });

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,12 @@ import { Range } from "@codemirror/rangeset";
interface WrapElement {
selector: string;
class: string;
nesting?: boolean;
}
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
let widgets: Range<Decoration>[] = [];
let elementStack: string[] = [];
for (let { from, to } of view.visibleRanges) {
const doc = view.state.doc;
syntaxTree(view.state).iterate({
@ -25,12 +27,19 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
const bodyText = doc.sliceString(from, to);
for (let wrapElement of wrapElements) {
if (type.name == wrapElement.selector) {
if (wrapElement.nesting) {
elementStack.push(type.name);
}
const bodyText = doc.sliceString(from, to);
let idx = from;
for (let line of bodyText.split("\n")) {
let cls = wrapElement.class;
if (wrapElement.nesting) {
cls = `${cls} ${cls}-${elementStack.length}`;
}
widgets.push(
Decoration.line({
class: wrapElement.class,
class: cls,
}).range(doc.lineAt(idx).from)
);
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

View File

@ -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;
};

View File

@ -1,10 +1,15 @@
import {StateCommand, Text, EditorSelection, ChangeSpec} from "@codemirror/state"
import {syntaxTree} from "@codemirror/language"
import {SyntaxNode, Tree} from "@lezer/common"
import {markdownLanguage} from "./markdown"
import {
StateCommand,
Text,
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) {
return doc.sliceString(node.from, node.from + 50)
return doc.sliceString(node.from, node.from + 50);
}
class Context {
@ -19,65 +24,126 @@ class Context {
) {}
blank(trailing: boolean = 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 : "")
let result = this.spaceBefore;
if (this.node.name == "Blockquote") {
result += ">";
} 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) {
let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item!, doc)[2] + add)) : ""
return this.spaceBefore + number + this.type + this.spaceAfter
let number =
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) {
let nodes = []
for (let cur: SyntaxNode | null = node; cur && cur.name != "Document"; cur = cur.parent) {
if (cur.name == "ListItem" || cur.name == "Blockquote")
nodes.push(cur)
let nodes = [];
for (
let cur: SyntaxNode | null = node;
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--) {
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))
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 == "Comment" &&
(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
return context;
}
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) {
for (let prev = -1, node = after;;) {
function renumberList(
after: SyntaxNode,
doc: Text,
changes: ChangeSpec[],
offset = 0
) {
for (let prev = -1, node = after; ; ) {
if (node.name == "ListItem") {
let m = itemNumber(node, doc)
let number = +m[2]
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)})
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
prev = number;
}
let next = node.nextSibling
if (!next) break
node = next
let next = node.nextSibling;
if (!next) break;
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
/// 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: StateCommand = ({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}
export const insertNewlineContinueMarkup: StateCommand = ({
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)
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: 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 {
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) {
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}
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: ChangeSpec[] = []
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
}
if (inner.node.name == "Comment" && emptyLine && line.from) {
let prevLine = doc.lineAt(line.from - 1),
commented = /%%\s*$/.exec(prevLine.text);
// Two aligned empty quoted lines in a row
if (commented && commented.index == inner.from) {
let changes = state.changes([
{ from: prevLine.from + commented.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 == "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: SyntaxNode) {
return node.name == "QuoteMark" || node.name == "ListMark"
return node.name == "QuoteMark" || node.name == "ListMark";
}
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)) {
scan = node.from
node = node.parent!
scan = node.from;
node = node.parent!;
}
for (let prev; prev = node.childBefore(scan);) {
for (let prev; (prev = node.childBefore(scan)); ) {
if (isMark(prev)) {
scan = prev.from
scan = prev.from;
} else if (prev.name == "OrderedList" || prev.name == "BulletList") {
node = prev.lastChild!
scan = node.to
node = prev.lastChild!;
scan = node.to;
} else {
break
break;
}
}
return node
return node;
}
/// 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
/// false, so it is intended to be bound alongside other deletion
/// commands, with a higher precedence than the more generic commands.
export const deleteMarkupBackward: StateCommand = ({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}}
export const deleteMarkupBackward: StateCommand = ({ 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
}
return (dont = { range });
});
if (dont) return false;
dispatch(
state.update(changes, { scrollIntoView: true, userEvent: "delete" })
);
return true;
};

View File

@ -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);
}

View File

@ -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());
};
}

View File

@ -1,5 +1,11 @@
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 * as ct from "./customtags";
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 = {
defineNodes: ["TagLink"],
parseInline: [
@ -102,6 +137,7 @@ const WikiMarkdown = commonmark.configure([
TagLink,
TaskList,
UnmarkedUrl,
Comment,
{
props: [
styleTags({
@ -112,6 +148,8 @@ const WikiMarkdown = commonmark.configure([
Task: ct.TaskTag,
TaskMarker: ct.TaskMarkerTag,
Url: t.url,
Comment: ct.CommentTag,
// CommentMarker: ct.CommentMarkerTag,
}),
],
},

View File

@ -4,6 +4,7 @@ import * as ct from "./customtags";
export default HighlightStyle.define([
{ tag: t.heading1, class: "h1" },
{ tag: t.heading2, class: "h2" },
{ tag: t.heading3, class: "h3" },
{ tag: t.link, class: "link" },
{ tag: t.meta, class: "meta" },
{ tag: t.quote, class: "quote" },
@ -15,6 +16,8 @@ export default HighlightStyle.define([
{ tag: ct.MentionTag, class: "mention" },
{ tag: ct.TaskTag, class: "task" },
{ tag: ct.TaskMarkerTag, class: "task-marker" },
{ tag: ct.CommentTag, class: "comment" },
{ tag: ct.CommentMarkerTag, class: "comment-marker" },
{ tag: t.emphasis, class: "emphasis" },
{ tag: t.strong, class: "strong" },
{ tag: t.atom, class: "atom" },

View File

@ -13,34 +13,29 @@
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;
color: #fff;
font-weight: bold;
}
.cm-line.line-h1 {
display: block;
background-color: rgba(0, 15, 52, 0.6);
}
.h1.meta {
color: orange;
}
.h2 {
.line-h2 {
font-size: 1.2em;
color: #fff;
font-weight: bold;
}
.cm-line.line-h2 {
display: block;
background-color: rgba(0, 15, 52, 0.6);
}
.h2.meta {
color: orange;
.line-h3 {
font-size: 1.1em;
}
/* Color list item this way */
@ -66,10 +61,10 @@
}
.line-blockquote {
background-color: #eee;
background-color: rgba(220, 220, 220, 0.5);
color: #676767;
text-indent: calc(-1 * (var(--ident) + 3px));
padding-left: var(--ident);
text-indent: -2ch;
padding-left: 2ch;
}
.emphasis {
@ -88,6 +83,7 @@
.link.url {
color: #7e7d7d;
}
.url:not(.link) {
color: #0330cb;
text-decoration: underline;
@ -109,12 +105,40 @@
color: #8d8d8d;
}
.line-li {
text-indent: calc(-1 * var(--ident) - 3px);
margin-left: var(--ident);
.code {
background-color: #efefef;
}
.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 {
background-color: #ddd;
}
.line-comment {
background-color: rgba(255, 255, 0, 0.5);
}
}

View File

@ -16,13 +16,29 @@
border-radius: 8px;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
label {
color: var(--highlight-color);
}
.header {
border-bottom: 1px rgb(108, 108, 108) solid;
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 {
@ -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,
.selected-option {
padding: 8px;

View File

@ -63,7 +63,7 @@ body {
#editor {
position: absolute;
top: 60px;
top: 55px;
bottom: 30px;
left: 0;
right: 0;

View File

@ -127,7 +127,7 @@ export default (editor: Editor) => ({
"editor.dispatch": (change: Transaction) => {
editor.editorView!.dispatch(change);
},
"editor.prompt": (message: string): string | null => {
return prompt(message);
"editor.prompt": (message: string, defaultValue = ""): string | null => {
return prompt(message, defaultValue);
},
});