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
|
||||
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
|
@ -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"
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
@ -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();
|
||||
|
@ -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" });
|
||||
|
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 {
|
||||
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
|
||||
|
@ -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 {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;
|
||||
};
|
||||
|
@ -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 { 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,
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -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" },
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -63,7 +63,7 @@ body {
|
||||
|
||||
#editor {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
top: 55px;
|
||||
bottom: 30px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user