feat: somewhat finalize the new codeblock

This commit is contained in:
thecodrr
2022-05-30 05:08:09 +05:00
parent 4e32c0ace1
commit 4e453dc047
30 changed files with 3825 additions and 97 deletions

View File

@@ -0,0 +1,28 @@
## Checklists (done):
Old:
```html
<ul style=\"list-style-type: none;\" class=\"checklist\"><li class=\"\"> Show connection state in extension<br data-mce-bogus=\"1\"></li><li>Show logged in status in extension &amp; an option to login if it is false</li><li>Show button to reconnect extension which will basically just open the web app in a separate tab</li><li>Implement Save clip functionality</li><li class=\"checked\">Move all UI to WebExtension popups</li><li class=\"checked\">Fix all UI bugs</li><li>Refactor code<ul style=\"list-style-type: none;\" class=\"checklist\"><li>Move all the modular code to <code spellcheck=\"false\">@notesnook/webextension-utils</code>&nbsp;</li><li>Clean up all the UI code</li></ul></li><li class=\"\">Make content script &amp; background script communication type-safe</li><li>Add a basic onboarding screen</li><li>Test on Firefox &amp; Chrome</li><li>Ready the extension for publishing<ul style=\"list-style-type: none;\" class=\"checklist\"><li>Icon</li><li>Description<br data-mce-bogus=\"1\"></li></ul></li></ul><p><br data-mce-bogus=\"1\"></p><p><br data-mce-bogus=\"1\"></p><p><br data-mce-bogus=\"1\"></p>
```
## Images:
Load images on editor load.
```html
<img
class="attachment"
alt="image.png"
data-mime="image/png"
data-hash="ea503ba76ff2d3a7"
data-filename="image.png"
data-size="38039"
width="199"
height="367"
style="display: block; margin-left: auto; margin-right: auto;"
src="/placeholder.svg"
/>
```
## Codeblocks

View File

@@ -0,0 +1,81 @@
import { Node } from "@tiptap/core";
import { Selection } from "prosemirror-state";
import { Node as ProsemirrorNode } from "prosemirror-model";
export declare type IndentationOptions = {
type: "space" | "tab";
length: number;
};
export declare type CodeBlockAttributes = {
indentType: IndentationOptions["type"];
indentLength: number;
language: string;
lines: CodeLine[];
};
export interface CodeBlockOptions {
/**
* Adds a prefix to language classes that are applied to code tags.
* Defaults to `'language-'`.
*/
languageClassPrefix: string;
/**
* Define whether the node should be exited on triple enter.
* Defaults to `true`.
*/
exitOnTripleEnter: boolean;
/**
* Define whether the node should be exited on arrow down if there is no node after it.
* Defaults to `true`.
*/
exitOnArrowDown: boolean;
/**
* Define whether the node should be exited on arrow up if there is no node before it.
* Defaults to `true`.
*/
exitOnArrowUp: boolean;
/**
* Custom HTML attributes that should be added to the rendered HTML tag.
*/
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
codeblock: {
/**
* Set a code block
*/
setCodeBlock: (attributes?: {
language: string;
}) => ReturnType;
/**
* Toggle a code block
*/
toggleCodeBlock: (attributes?: {
language: string;
}) => ReturnType;
/**
* Change code block indentation options
*/
changeCodeBlockIndentation: (options: IndentationOptions) => ReturnType;
};
}
}
export declare const backtickInputRegex: RegExp;
export declare const tildeInputRegex: RegExp;
export declare const CodeBlock: Node<CodeBlockOptions, any>;
export declare type CaretPosition = {
column: number;
line: number;
selected?: number;
total: number;
};
export declare function toCaretPosition(lines: CodeLine[], selection: Selection<any>): CaretPosition | undefined;
export declare function getLines(node: ProsemirrorNode<any>): CodeLine[];
declare type CodeLine = {
index: number;
from: number;
to: number;
length: number;
text: (length?: number) => string;
};
export declare function toCodeLines(code: string, pos: number): CodeLine[];
export {};

View File

@@ -0,0 +1,560 @@
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
import { Node, textblockTypeInputRule, mergeAttributes } from "@tiptap/core";
import { Plugin, PluginKey, TextSelection, } from "prosemirror-state";
import { findParentNodeClosestToPos, ReactNodeViewRenderer, } from "@tiptap/react";
import { CodeblockComponent } from "./component";
import { HighlighterPlugin } from "./highlighter";
import detectIndent from "detect-indent";
export var backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
export var tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
var ZERO_WIDTH_SPACE = "\u200b";
var NEWLINE = "\n";
export var CodeBlock = Node.create({
name: "codeblock",
addOptions: function () {
return {
languageClassPrefix: "language-",
exitOnTripleEnter: true,
exitOnArrowDown: true,
exitOnArrowUp: true,
HTMLAttributes: {},
};
},
content: "text*",
marks: "",
group: "block",
code: true,
defining: true,
addAttributes: function () {
var _this = this;
return {
lines: {
default: [],
rendered: false,
},
indentType: {
default: "space",
parseHTML: function (element) {
var indentType = element.dataset.indentType;
if (indentType)
return indentType;
return detectIndent(element.innerText).type;
},
renderHTML: function (attributes) {
if (!attributes.indentType) {
return {};
}
return {
"data-indent-type": attributes.indentType,
};
},
},
indentLength: {
default: 2,
parseHTML: function (element) {
var indentLength = element.dataset.indentLength;
if (indentLength)
return indentLength;
return detectIndent(element.innerText).amount;
},
renderHTML: function (attributes) {
if (!attributes.indentLength) {
return {};
}
return {
"data-indent-length": attributes.indentLength,
};
},
},
language: {
default: null,
parseHTML: function (element) {
var _a;
var languageClassPrefix = _this.options.languageClassPrefix;
var classNames = __spreadArray(__spreadArray([], __read((element.classList || [])), false), __read((((_a = element === null || element === void 0 ? void 0 : element.firstElementChild) === null || _a === void 0 ? void 0 : _a.classList) || [])), false);
var languages = classNames
.filter(function (className) { return className.startsWith(languageClassPrefix); })
.map(function (className) { return className.replace(languageClassPrefix, ""); });
var language = languages[0];
if (!language) {
return null;
}
return language;
},
renderHTML: function (attributes) {
if (!attributes.language) {
return {};
}
return {
class: "language-".concat(attributes.language),
};
},
},
};
},
parseHTML: function () {
return [
{
tag: "pre",
preserveWhitespace: "full",
// contentElement: (node) => {
// if (node instanceof HTMLElement) {
// node.innerText = node.innerText.replaceAll("\n\u200b\n", "\n\n");
// }
// return node;
// },
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"pre",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addCommands: function () {
var _this = this;
return {
setCodeBlock: function (attributes) {
return function (_a) {
var commands = _a.commands;
return commands.setNode(_this.name, attributes);
};
},
toggleCodeBlock: function (attributes) {
return function (_a) {
var commands = _a.commands;
return commands.toggleNode(_this.name, "paragraph", attributes);
};
},
changeCodeBlockIndentation: function (options) {
return function (_a) {
var e_1, _b;
var editor = _a.editor, tr = _a.tr, commands = _a.commands;
var state = editor.state;
var selection = state.selection;
var $from = selection.$from;
if ($from.parent.type !== _this.type) {
return false;
}
var lines = $from.parent.attrs.lines;
try {
for (var lines_1 = __values(lines), lines_1_1 = lines_1.next(); !lines_1_1.done; lines_1_1 = lines_1.next()) {
var line = lines_1_1.value;
var text = line.text();
var whitespaceLength = text.length - text.trimStart().length;
if (!whitespaceLength)
continue;
var indentLength = whitespaceLength;
var indentToken = indent(options.type, indentLength);
tr.insertText(indentToken, tr.mapping.map(line.from), tr.mapping.map(line.from + whitespaceLength));
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (lines_1_1 && !lines_1_1.done && (_b = lines_1.return)) _b.call(lines_1);
}
finally { if (e_1) throw e_1.error; }
}
commands.updateAttributes(_this.type, {
indentType: options.type,
indentLength: options.length,
});
return true;
};
},
};
},
addKeyboardShortcuts: function () {
var _this = this;
return {
"Mod-Alt-c": function () { return _this.editor.commands.toggleCodeBlock(); },
"Mod-a": function (_a) {
var editor = _a.editor;
var $anchor = _this.editor.state.selection.$anchor;
if ($anchor.parent.type.name !== _this.name) {
return false;
}
var codeblock = findParentNodeClosestToPos($anchor, function (node) { return node.type.name === _this.type.name; });
if (!codeblock)
return false;
return editor.commands.setTextSelection({
from: codeblock.pos,
to: codeblock.pos + codeblock.node.nodeSize,
});
},
// remove code block when at start of document or code block is empty
Backspace: function () {
var _a = _this.editor.state.selection, empty = _a.empty, $anchor = _a.$anchor;
var isAtStart = $anchor.pos === 1;
if (!empty || $anchor.parent.type.name !== _this.name) {
return false;
}
if (isAtStart || !$anchor.parent.textContent.length) {
return _this.editor.commands.clearNodes();
}
return false;
},
// exit node on triple enter
Enter: function (_a) {
var editor = _a.editor;
var state = editor.state;
var selection = state.selection;
var $from = selection.$from, empty = selection.empty;
if (!empty || $from.parent.type !== _this.type) {
return false;
}
var indentation = parseIndentation($from.parent);
return ((_this.options.exitOnTripleEnter &&
exitOnTripleEnter(editor, $from)) ||
indentOnEnter(editor, $from, indentation));
},
// exit node on arrow up
ArrowUp: function (_a) {
var editor = _a.editor;
if (!_this.options.exitOnArrowUp) {
return false;
}
var state = editor.state;
var selection = state.selection;
var $anchor = selection.$anchor, empty = selection.empty;
if (!empty || $anchor.parent.type !== _this.type) {
return false;
}
var isAtStart = $anchor.pos === 1;
if (!isAtStart) {
return false;
}
return editor.commands.insertContentAt(0, "<p></p>");
},
// exit node on arrow down
ArrowDown: function (_a) {
var editor = _a.editor;
if (!_this.options.exitOnArrowDown) {
return false;
}
var state = editor.state;
var selection = state.selection, doc = state.doc;
var $from = selection.$from, empty = selection.empty;
if (!empty || $from.parent.type !== _this.type) {
return false;
}
var isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
if (!isAtEnd) {
return false;
}
var after = $from.after();
if (after === undefined) {
return false;
}
var nodeAfter = doc.nodeAt(after);
if (nodeAfter) {
editor.commands.setNodeSelection($from.before());
return false;
}
return editor.commands.exitCode();
},
"Shift-Tab": function (_a) {
var editor = _a.editor;
var state = editor.state;
var selection = state.selection;
var $from = selection.$from;
if ($from.parent.type !== _this.type) {
return false;
}
var indentation = parseIndentation($from.parent);
var indentToken = indent(indentation.type, indentation.length);
var lines = $from.parent.attrs.lines;
var selectedLines = getSelectedLines(lines, selection);
return editor
.chain()
.command(function (_a) {
var tr = _a.tr;
return withSelection(tr, function (tr) {
var e_2, _a;
try {
for (var selectedLines_1 = __values(selectedLines), selectedLines_1_1 = selectedLines_1.next(); !selectedLines_1_1.done; selectedLines_1_1 = selectedLines_1.next()) {
var line = selectedLines_1_1.value;
if (line.text(indentToken.length) !== indentToken)
continue;
tr.delete(tr.mapping.map(line.from), tr.mapping.map(line.from + indentation.length));
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (selectedLines_1_1 && !selectedLines_1_1.done && (_a = selectedLines_1.return)) _a.call(selectedLines_1);
}
finally { if (e_2) throw e_2.error; }
}
});
})
.run();
},
Tab: function (_a) {
var editor = _a.editor;
var state = editor.state;
var selection = state.selection;
var $from = selection.$from;
if ($from.parent.type !== _this.type) {
return false;
}
var lines = $from.parent.attrs.lines;
var selectedLines = getSelectedLines(lines, selection);
return editor
.chain()
.command(function (_a) {
var tr = _a.tr;
return withSelection(tr, function (tr) {
var e_3, _a;
var indentation = parseIndentation($from.parent);
var indentToken = indent(indentation.type, indentation.length);
if (selectedLines.length === 1)
return tr.insertText(indentToken, $from.pos);
try {
for (var selectedLines_2 = __values(selectedLines), selectedLines_2_1 = selectedLines_2.next(); !selectedLines_2_1.done; selectedLines_2_1 = selectedLines_2.next()) {
var line = selectedLines_2_1.value;
tr.insertText(indentToken, tr.mapping.map(line.from));
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (selectedLines_2_1 && !selectedLines_2_1.done && (_a = selectedLines_2.return)) _a.call(selectedLines_2);
}
finally { if (e_3) throw e_3.error; }
}
});
})
.run();
},
};
},
addInputRules: function () {
return [
textblockTypeInputRule({
find: backtickInputRegex,
type: this.type,
getAttributes: function (match) { return ({
language: match[1],
}); },
}),
textblockTypeInputRule({
find: tildeInputRegex,
type: this.type,
getAttributes: function (match) { return ({
language: match[1],
}); },
}),
];
},
addProseMirrorPlugins: function () {
var _this = this;
return [
// this plugin creates a code block for pasted content from VS Code
// we can also detect the copied code language
new Plugin({
key: new PluginKey("codeBlockVSCodeHandler"),
props: {
handlePaste: function (view, event) {
if (!event.clipboardData) {
return false;
}
// dont create a new code block within code blocks
if (_this.editor.isActive(_this.type.name)) {
return false;
}
var text = event.clipboardData.getData("text/plain");
var vscode = event.clipboardData.getData("vscode-editor-data");
var vscodeData = vscode ? JSON.parse(vscode) : undefined;
var language = vscodeData === null || vscodeData === void 0 ? void 0 : vscodeData.mode;
if (!text || !language) {
return false;
}
var tr = view.state.tr;
// create an empty code block
tr.replaceSelectionWith(_this.type.create({ language: language }));
// put cursor inside the newly created code block
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(0, tr.selection.from - 2))));
// add text to code block
// strip carriage return chars from text pasted as code
// see: https://github.com/ProseMirror/prosemirror-view/commit/a50a6bcceb4ce52ac8fcc6162488d8875613aacd
tr.insertText(text.replace(/\r\n?/g, "\n"));
// store meta information
// this is useful for other plugins that depends on the paste event
// like the paste rule plugin
tr.setMeta("paste", true);
view.dispatch(tr);
return true;
},
},
}),
HighlighterPlugin({ name: this.name, defaultLanguage: "txt" }),
];
},
addNodeView: function () {
return ReactNodeViewRenderer(CodeblockComponent);
},
});
export function toCaretPosition(lines, selection) {
var e_4, _a;
var $from = selection.$from, $to = selection.$to, $head = selection.$head;
if ($from.parent.type.name !== CodeBlock.name)
return;
try {
for (var lines_2 = __values(lines), lines_2_1 = lines_2.next(); !lines_2_1.done; lines_2_1 = lines_2.next()) {
var line = lines_2_1.value;
if ($head.pos >= line.from && $head.pos <= line.to) {
var lineLength = line.length + 1;
return {
line: line.index + 1,
column: lineLength - (line.to - $head.pos),
selected: $to.pos - $from.pos,
total: lines.length,
};
}
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (lines_2_1 && !lines_2_1.done && (_a = lines_2.return)) _a.call(lines_2);
}
finally { if (e_4) throw e_4.error; }
}
return;
}
export function getLines(node) {
var lines = node.attrs.lines;
return lines || [];
}
function exitOnTripleEnter(editor, $from) {
var isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
var endsWithDoubleNewline = $from.parent.textContent.endsWith("\n\n");
if (!isAtEnd || !endsWithDoubleNewline) {
return false;
}
return editor
.chain()
.command(function (_a) {
var tr = _a.tr;
tr.delete($from.pos - 2, $from.pos);
return true;
})
.exitCode()
.run();
}
function indentOnEnter(editor, $from, options) {
var lines = $from.parent.attrs.lines;
var currentLine = getLineAt(lines, $from.pos);
if (!currentLine)
return false;
var text = editor.state.doc.textBetween(currentLine.from, currentLine.to);
var indentLength = text.length - text.trimStart().length;
var newline = "".concat(NEWLINE).concat(indent(options.type, indentLength));
return editor.commands.insertContent(newline, {
parseOptions: { preserveWhitespace: "full" },
});
}
export function toCodeLines(code, pos) {
var positions = [];
var start = 0;
var from = pos + 1;
var index = 0;
var _loop_1 = function () {
var end = code.indexOf("\n", start);
if (end <= -1)
end = code.length;
var lineLength = end - start;
var to = from + lineLength;
var lineStart = start;
positions.push({
index: index,
length: lineLength,
from: from,
to: to,
text: function (length) {
return code.slice(lineStart, length ? lineStart + length : lineStart + lineLength);
},
});
from = to + 1;
start = end + 1;
++index;
};
while (start <= code.length) {
_loop_1();
}
return positions;
}
function getSelectedLines(lines, selection) {
var $from = selection.$from, $to = selection.$to;
return lines.filter(function (line) {
return inRange(line.from, $from.pos, $to.pos) ||
inRange(line.to, $from.pos, $to.pos) ||
inRange($from.pos, line.from, line.to);
});
}
function parseIndentation(node) {
var _a = node.attrs, indentType = _a.indentType, indentLength = _a.indentLength;
return {
type: indentType,
length: parseInt(indentLength),
};
}
function getLineAt(lines, pos) {
return lines.find(function (line) { return pos >= line.from && pos <= line.to; });
}
function inRange(x, a, b) {
return x >= a && x <= b;
}
function indent(type, length) {
var char = type === "space" ? " " : "\t";
return char.repeat(length);
}
/**
* Persist selection between transaction steps
*/
function withSelection(tr, callback) {
var _a = tr.selection, $anchor = _a.$anchor, $head = _a.$head;
callback(tr);
tr.setSelection(new TextSelection(tr.doc.resolve(tr.mapping.map($anchor.pos)), tr.doc.resolve(tr.mapping.map($head.pos))));
return true;
}

View File

@@ -0,0 +1,4 @@
/// <reference types="react" />
import { NodeViewProps } from "@tiptap/react";
import "prism-themes/themes/prism-dracula.min.css";
export declare function CodeblockComponent(props: NodeViewProps): JSX.Element;

View File

@@ -0,0 +1,213 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";
import { loadLanguage } from "./loader";
import { refractor } from "refractor/lib/core";
import "prism-themes/themes/prism-dracula.min.css";
import { ThemeProvider } from "emotion-theming";
import { Button, Flex, Text } from "rebass";
import Languages from "./languages.json";
import { PopupPresenter } from "../../components/menu/menu";
import { Input } from "@rebass/forms";
import { Icon } from "../../toolbar/components/icon";
import { Icons } from "../../toolbar/icons";
import { toCaretPosition, getLines, } from "./code-block";
export function CodeblockComponent(props) {
var editor = props.editor, updateAttributes = props.updateAttributes, node = props.node;
var _a = node.attrs, language = _a.language, indentLength = _a.indentLength, indentType = _a.indentType;
var theme = editor.storage.theme;
var _b = __read(useState(false), 2), isOpen = _b[0], setIsOpen = _b[1];
var _c = __read(useState(), 2), caretPosition = _c[0], setCaretPosition = _c[1];
var toolbarRef = useRef(null);
var languageDefinition = Languages.find(function (l) { var _a; return l.filename === language || ((_a = l.alias) === null || _a === void 0 ? void 0 : _a.some(function (a) { return a === language; })); });
useEffect(function () {
(function () {
return __awaiter(this, void 0, void 0, function () {
var syntax;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!language || !languageDefinition) {
updateAttributes({ language: null });
return [2 /*return*/];
}
return [4 /*yield*/, loadLanguage(languageDefinition.filename)];
case 1:
syntax = _a.sent();
if (!syntax)
return [2 /*return*/];
refractor.register(syntax);
updateAttributes({
language: languageDefinition.filename,
});
return [2 /*return*/];
}
});
});
})();
}, [language, updateAttributes]);
useEffect(function () {
function onSelectionUpdate(_a) {
var transaction = _a.transaction;
var position = toCaretPosition(getLines(node), transaction.selection);
setCaretPosition(position);
}
editor.on("selectionUpdate", onSelectionUpdate);
return function () {
editor.off("selectionUpdate", onSelectionUpdate);
};
}, [node]);
return (_jsx(NodeViewWrapper, { children: _jsxs(ThemeProvider, __assign({ theme: theme }, { children: [_jsxs(Flex, __assign({ sx: {
flexDirection: "column",
borderRadius: "default",
overflow: "hidden",
} }, { children: [_jsx(Text, __assign({ as: "pre", sx: {
"div, span.token, span.line-number-widget": {
fontFamily: "monospace",
fontSize: "code",
whiteSpace: "pre !important",
tabSize: 1,
},
position: "relative",
lineHeight: "20px",
bg: "codeBg",
color: "static",
overflowX: "auto",
display: "flex",
px: 2,
pt: 2,
pb: 1,
}, spellCheck: false }, { children: _jsx(NodeViewContent, { as: "code" }) })), _jsxs(Flex, __assign({ ref: toolbarRef, sx: {
bg: "codeBg",
alignItems: "center",
justifyContent: "end",
borderTop: "1px solid var(--codeBorder)",
} }, { children: [caretPosition ? (_jsxs(Text, __assign({ variant: "subBody", sx: { mr: 2, color: "codeFg" } }, { children: ["Line ", caretPosition.line, ", Column ", caretPosition.column, " ", caretPosition.selected
? "(".concat(caretPosition.selected, " selected)")
: ""] }))) : null, _jsx(Button, __assign({ variant: "icon", sx: { p: 1, mr: 1, ":hover": { bg: "codeSelection" } }, title: "Toggle indentation mode", onClick: function () {
editor.commands.changeCodeBlockIndentation({
type: indentType === "space" ? "tab" : "space",
length: indentLength,
});
} }, { children: _jsxs(Text, __assign({ variant: "subBody", sx: { color: "codeFg" } }, { children: [indentType === "space" ? "Spaces" : "Tabs", ": ", indentLength] })) })), _jsx(Button, __assign({ variant: "icon", sx: {
p: 1,
mr: 1,
bg: isOpen ? "codeSelection" : "transparent",
":hover": { bg: "codeSelection" },
}, onClick: function () { return setIsOpen(true); }, title: "Change language" }, { children: _jsx(Text, __assign({ variant: "subBody", spellCheck: false, sx: { color: "codeFg" } }, { children: (languageDefinition === null || languageDefinition === void 0 ? void 0 : languageDefinition.title) || "Plaintext" })) }))] }))] })), _jsx(PopupPresenter, __assign({ isOpen: isOpen, onClose: function () {
setIsOpen(false);
editor.commands.focus();
}, mobile: "sheet", desktop: "menu", options: {
type: "menu",
position: {
target: toolbarRef.current || undefined,
align: "end",
isTargetAbsolute: true,
location: "top",
yOffset: 5,
},
} }, { children: _jsx(LanguageSelector, { selectedLanguage: (languageDefinition === null || languageDefinition === void 0 ? void 0 : languageDefinition.filename) || "Plaintext", onLanguageSelected: function (language) { return updateAttributes({ language: language }); } }) }))] })) }));
}
function LanguageSelector(props) {
var onLanguageSelected = props.onLanguageSelected, selectedLanguage = props.selectedLanguage;
var _a = __read(useState(Languages), 2), languages = _a[0], setLanguages = _a[1];
return (_jsxs(Flex, __assign({ sx: {
flexDirection: "column",
height: 200,
width: 300,
boxShadow: "menu",
borderRadius: "default",
overflowY: "auto",
bg: "background",
marginRight: 2,
} }, { children: [_jsx(Input, { autoFocus: true, placeholder: "Search languages", sx: {
mx: 2,
width: "auto",
position: "sticky",
top: 2,
bg: "background",
p: "7px",
}, onChange: function (e) {
if (!e.target.value)
return setLanguages(Languages);
var query = e.target.value.toLowerCase();
setLanguages(Languages.filter(function (lang) {
var _a;
return (lang.title.toLowerCase().indexOf(query) > -1 ||
((_a = lang.alias) === null || _a === void 0 ? void 0 : _a.some(function (alias) { return alias.toLowerCase().indexOf(query) > -1; })));
}));
} }), _jsx(Flex, __assign({ sx: {
flexDirection: "column",
pt: 2,
mt: 1,
} }, { children: languages.map(function (lang) { return (_jsxs(Button, __assign({ variant: "menuitem", sx: {
textAlign: "left",
py: 1,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}, onClick: function () { return onLanguageSelected(lang.filename); } }, { children: [_jsx(Text, __assign({ variant: "body" }, { children: lang.title })), selectedLanguage === lang.filename ? (_jsx(Icon, { path: Icons.check, size: "small" })) : lang.alias ? (_jsx(Text, __assign({ variant: "subBody", sx: { fontSize: "10px" } }, { children: lang.alias.slice(0, 3).join(", ") }))) : null] }))); }) }))] })));
}

View File

@@ -0,0 +1,5 @@
import { Plugin } from "prosemirror-state";
export declare function HighlighterPlugin({ name, defaultLanguage, }: {
name: string;
defaultLanguage: string | null | undefined;
}): Plugin<any, any>;

View File

@@ -0,0 +1,242 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { findChildren } from "@tiptap/core";
import { refractor } from "refractor/lib/core";
import { getLines, toCaretPosition, toCodeLines, } from "./code-block";
function parseNodes(nodes, className) {
if (className === void 0) { className = []; }
return nodes.reduce(function (result, node) {
if (node.type === "comment" || node.type === "doctype")
return result;
var classes = __spreadArray([], __read(className), false);
if (node.type === "element" && node.properties)
classes.push.apply(classes, __spreadArray([], __read(node.properties.className), false));
else
classes.push("token", "text");
if (node.type === "element") {
result.push.apply(result, __spreadArray([], __read(parseNodes(node.children, classes)), false));
}
else {
result.push({ classes: classes, text: node.value });
}
return result;
}, []);
}
function getHighlightNodes(result) {
return result.children || [];
}
function getLineDecoration(from, line, total, isActive) {
var attributes = {
class: "line-number ".concat(isActive ? "active" : ""),
"data-line": String(line).padEnd(String(total).length, " "),
};
var spec = {
line: line,
active: isActive,
total: total,
};
// Prosemirror has a selection issue with the widget decoration
// on the first line. To work around that we use inline decoration
// for the first line.
if (line === 1) {
return Decoration.inline(from, from + 1, attributes, spec);
}
return Decoration.widget(from, function () {
var element = document.createElement("span");
element.classList.add("line-number-widget");
if (isActive)
element.classList.add("active");
element.innerHTML = attributes["data-line"];
return element;
}, __assign(__assign({}, spec), { key: "".concat(line, "-").concat(isActive ? "active" : "inactive") }));
}
function getDecorations(_a) {
var doc = _a.doc, name = _a.name, defaultLanguage = _a.defaultLanguage, currentLine = _a.currentLine;
var decorations = [];
var languages = refractor.listLanguages();
findChildren(doc, function (node) { return node.type.name === name; }).forEach(function (block) {
var e_1, _a;
var code = block.node.textContent;
var lines = block.node.attrs.lines;
try {
for (var _b = __values(lines || []), _c = _b.next(); !_c.done; _c = _b.next()) {
var line = _c.value;
var lineNumber = line.index + 1;
var isActive = lineNumber === currentLine;
var decoration = getLineDecoration(line.from, lineNumber, (lines === null || lines === void 0 ? void 0 : lines.length) || 0, isActive);
decorations.push(decoration);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
var language = block.node.attrs.language || defaultLanguage;
var nodes = languages.includes(language)
? getHighlightNodes(refractor.highlight(code, language))
: null;
if (!nodes)
return;
var from = block.pos + 1;
parseNodes(nodes).forEach(function (node) {
var to = from + node.text.length;
if (node.classes.length) {
var decoration = Decoration.inline(from, to, {
class: node.classes.join(" "),
});
decorations.push(decoration);
}
from = to;
});
});
return DecorationSet.create(doc, decorations);
}
export function HighlighterPlugin(_a) {
var name = _a.name, defaultLanguage = _a.defaultLanguage;
return new Plugin({
key: new PluginKey("highlighter"),
state: {
init: function () {
return DecorationSet.empty;
},
apply: function (transaction, decorationSet, oldState, newState) {
var oldNodeName = oldState.selection.$head.parent.type.name;
var newNodeName = newState.selection.$head.parent.type.name;
var oldNodes = findChildren(oldState.doc, function (node) { return node.type.name === name; });
var newNodes = findChildren(newState.doc, function (node) { return node.type.name === name; });
var position = toCaretPosition(getLines(newState.selection.$head.parent), newState.selection);
if (transaction.docChanged &&
// Apply decorations if:
// selection includes named node,
([oldNodeName, newNodeName].includes(name) ||
// OR transaction adds/removes named node,
newNodes.length !== oldNodes.length ||
// OR transaction has changes that completely encapsulte a node
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some(function (step) {
return (step.from !== undefined &&
step.to !== undefined &&
oldNodes.some(function (node) {
return (node.pos >= step.from &&
node.pos + node.node.nodeSize <= step.to);
}));
}))) {
return getDecorations({
doc: transaction.doc,
name: name,
defaultLanguage: defaultLanguage,
currentLine: position === null || position === void 0 ? void 0 : position.line,
});
}
decorationSet = getActiveLineDecorations(transaction.doc, decorationSet, position);
return decorationSet.map(transaction.mapping, transaction.doc);
},
},
props: {
decorations: function (state) {
return this.getState(state);
},
},
appendTransaction: function (transactions, _prevState, nextState) {
var tr = nextState.tr;
var modified = false;
if (transactions.some(function (transaction) { return transaction.docChanged; })) {
findChildren(nextState.doc, function (node) { return node.type.name === name; }).forEach(function (block) {
var node = block.node, pos = block.pos;
var lines = toCodeLines(node.textContent, pos);
tr.setNodeMarkup(pos, undefined, __assign(__assign({}, node.attrs), { lines: lines }));
modified = true;
});
}
return modified ? tr : null;
},
});
}
/**
* When `position` is undefined, all active line decorations
* are reset (e.g. when you focus out of the code block).
*/
function getActiveLineDecorations(doc, decorations, position) {
var e_2, _a;
var lineDecorations = decorations.find(undefined, undefined, function (_a) {
var line = _a.line, active = _a.active;
return (position && line === position.line) || active;
});
if (!lineDecorations.length)
return decorations;
// we have to clone because prosemirror operates in-place
var cloned = lineDecorations.slice();
// remove old line decorations which inclue the current line decoration
// and the previous current line decoration. We'll replace these with
// new decorations.
decorations = decorations.remove(lineDecorations);
var newDecorations = [];
try {
for (var cloned_1 = __values(cloned), cloned_1_1 = cloned_1.next(); !cloned_1_1.done; cloned_1_1 = cloned_1.next()) {
var decoration = cloned_1_1.value;
var from = decoration.from, _b = decoration.spec, line = _b.line, total = _b.total;
var isActive = line === (position === null || position === void 0 ? void 0 : position.line);
var newDecoration = getLineDecoration(from, line, (position === null || position === void 0 ? void 0 : position.total) || total, isActive);
newDecorations.push(newDecoration);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (cloned_1_1 && !cloned_1_1.done && (_a = cloned_1.return)) _a.call(cloned_1);
}
finally { if (e_2) throw e_2.error; }
}
return decorations.add(doc, newDecorations);
}

View File

@@ -0,0 +1 @@
export * from "./code-block";

View File

@@ -0,0 +1 @@
export * from "./code-block";

View File

@@ -0,0 +1,13 @@
import hljs from "highlight.js/lib/core";
import { Language } from "highlight.js";
export { hljs };
export declare function loadLanguage(shortName: string): Promise<Language | undefined>;
export declare const LANGUAGES: ({
name: string;
shortname: string;
aliases: string[];
} | {
name: string;
shortname: string;
aliases?: undefined;
})[];

View File

@@ -0,0 +1,339 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import hljs from "highlight.js/lib/core";
export { hljs };
// load hljs into the editor window which can be the iframe
// or the main window. This is required so language definitions
// can be loaded.
globalThis["hljs"] = hljs;
// export type LanguageDefinition = {
// name: string;
// shortname: string;
// aliases?: string[];
// };
var loadedLanguages = {};
export function loadLanguage(shortName) {
return __awaiter(this, void 0, void 0, function () {
var url, lang;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (loadedLanguages[shortName])
return [2 /*return*/, hljs.getLanguage(shortName)];
url = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/languages/".concat(shortName, ".min.js");
return [4 /*yield*/, loadScript(url)];
case 1:
_a.sent();
lang = hljs.getLanguage(shortName);
loadedLanguages[shortName] = lang;
return [2 /*return*/, lang];
}
});
});
}
function loadScript(url) {
return new Promise(function (resolve, reject) {
var script = document.createElement("script");
script.src = url;
// Append to the `head` element
document.head.appendChild(script);
script.addEventListener("load", function () {
resolve(undefined);
});
script.addEventListener("error", function (error) {
console.error(error);
reject("Could not load script at url ".concat(url, "."));
});
});
}
export var LANGUAGES = [
{ name: "Plain text", shortname: "plaintext", aliases: ["text", "txt"] },
{
name: "HTML, XML",
shortname: "xml",
aliases: [
"html",
"xhtml",
"rss",
"atom",
"xjb",
"xsd",
"xsl",
"plist",
"wsf",
"svg",
],
},
{ name: "Bash", shortname: "bash", aliases: ["sh"] },
{ name: "C", shortname: "c", aliases: ["h"] },
{
name: "C++",
shortname: "cpp",
aliases: ["cc", "c++", "h++", "hpp", "hh", "hxx", "cxx"],
},
{ name: "C#", shortname: "csharp", aliases: ["cs", "c#"] },
{ name: "CSS", shortname: "css" },
{ name: "Markdown", shortname: "markdown", aliases: ["md", "mkdown", "mkd"] },
{ name: "Diff", shortname: "diff", aliases: ["patch"] },
{
name: "Ruby",
shortname: "ruby",
aliases: ["rb", "gemspec", "podspec", "thor", "irb"],
},
{ name: "Go", shortname: "go", aliases: ["golang"] },
{ name: "TOML, also INI", shortname: "ini", aliases: ["toml"] },
{ name: "Java", shortname: "java", aliases: ["jsp"] },
{
name: "Javascript",
shortname: "javascript",
aliases: ["js", "jsx", "mjs", "cjs"],
},
{ name: "JSON", shortname: "json" },
{ name: "Kotlin", shortname: "kotlin", aliases: ["kt", "kts"] },
{ name: "Less", shortname: "less" },
{ name: "Lua", shortname: "lua" },
{ name: "Makefile", shortname: "makefile", aliases: ["mk", "mak", "make"] },
{ name: "Perl", shortname: "perl", aliases: ["pl", "pm"] },
{
name: "Objective-C",
shortname: "objectivec",
aliases: ["mm", "objc", "obj-c", "obj-c++", "objective-c++"],
},
{ name: "php", shortname: "php" },
{ name: "PHP template", shortname: "php-template" },
{ name: "Python", shortname: "python", aliases: ["py", "gyp", "ipython"] },
{ name: "python-repl", shortname: "python-repl", aliases: ["pycon"] },
{ name: "R", shortname: "r" },
{ name: "Rust", shortname: "rust", aliases: ["rs"] },
{ name: "SCSS", shortname: "scss" },
{
name: "Shell Session",
shortname: "shell",
aliases: ["console", "shellsession"],
},
{ name: "SQL", shortname: "sql" },
{ name: "Swift", shortname: "swift" },
{ name: "YAML", shortname: "yaml", aliases: ["yml"] },
{ name: "TypeScript", shortname: "typescript", aliases: ["ts", "tsx"] },
{ name: "Visual Basic .NET", shortname: "vbnet", aliases: ["vb"] },
{ name: "1C:Enterprise", shortname: "1c" },
{ name: "Augmented Backus-Naur Form", shortname: "abnf" },
{ name: "Apache Access Log", shortname: "accesslog" },
{ name: "ActionScript", shortname: "actionscript", aliases: ["as"] },
{ name: "Ada", shortname: "ada" },
{ name: "AngelScript", shortname: "angelscript", aliases: ["asc"] },
{ name: "Apache config", shortname: "apache", aliases: ["apacheconf"] },
{ name: "AppleScript", shortname: "applescript", aliases: ["osascript"] },
{ name: "ArcGIS Arcade", shortname: "arcade" },
{ name: "Arduino", shortname: "arduino", aliases: ["ino"] },
{ name: "ARM Assembly", shortname: "armasm", aliases: ["arm"] },
{ name: "AsciiDoc", shortname: "asciidoc", aliases: ["adoc"] },
{ name: "AspectJ", shortname: "aspectj" },
{ name: "AutoHotkey", shortname: "autohotkey", aliases: ["ahk"] },
{ name: "AutoIt", shortname: "autoit" },
{ name: "AVR Assembly", shortname: "avrasm" },
{ name: "Awk", shortname: "awk" },
{ name: "X++", shortname: "axapta", aliases: ["x++"] },
{ name: "BASIC", shortname: "basic" },
{ name: "BackusNaur Form", shortname: "bnf" },
{ name: "Brainfuck", shortname: "brainfuck", aliases: ["bf"] },
{ name: "C/AL", shortname: "cal" },
{ name: "Capn Proto", shortname: "capnproto", aliases: ["capnp"] },
{ name: "Ceylon", shortname: "ceylon" },
{ name: "Clean", shortname: "clean", aliases: ["icl", "dcl"] },
{ name: "Clojure", shortname: "clojure", aliases: ["clj", "edn"] },
{ name: "Clojure REPL", shortname: "clojure-repl" },
{ name: "CMake", shortname: "cmake", aliases: ["cmake.in"] },
{
name: "CoffeeScript",
shortname: "coffeescript",
aliases: ["coffee", "cson", "iced"],
},
{ name: "Coq", shortname: "coq" },
{ name: "Caché Object Script", shortname: "cos", aliases: ["cls"] },
{ name: "crmsh", shortname: "crmsh", aliases: ["crm", "pcmk"] },
{ name: "Crystal", shortname: "crystal", aliases: ["cr"] },
{ name: "CSP", shortname: "csp" },
{ name: "D", shortname: "d" },
{ name: "Dart", shortname: "dart" },
{
name: "Delphi",
shortname: "delphi",
aliases: ["dpr", "dfm", "pas", "pascal"],
},
{ name: "Django", shortname: "django", aliases: ["jinja"] },
{ name: "DNS Zone", shortname: "dns", aliases: ["bind", "zone"] },
{ name: "Dockerfile", shortname: "dockerfile", aliases: ["docker"] },
{ name: "Batch file (DOS)", shortname: "dos", aliases: ["bat", "cmd"] },
{ name: "dsconfig", shortname: "dsconfig" },
{ name: "Device Tree", shortname: "dts" },
{ name: "Dust", shortname: "dust", aliases: ["dst"] },
{ name: "Extended Backus-Naur Form", shortname: "ebnf" },
{ name: "Elixir", shortname: "elixir", aliases: ["ex", "exs"] },
{ name: "Elm", shortname: "elm" },
{ name: "ERB", shortname: "erb" },
{ name: "Erlang REPL", shortname: "erlang-repl" },
{ name: "Erlang", shortname: "erlang", aliases: ["erl"] },
{ name: "Excel formulae", shortname: "excel", aliases: ["xlsx", "xls"] },
{ name: "FIX", shortname: "fix" },
{ name: "Flix", shortname: "flix" },
{ name: "Fortran", shortname: "fortran", aliases: ["f90", "f95"] },
{ name: "F#", shortname: "fsharp", aliases: ["fs", "f#"] },
{ name: "GAMS", shortname: "gams", aliases: ["gms"] },
{ name: "GAUSS", shortname: "gauss", aliases: ["gss"] },
{ name: "G-code (ISO 6983)", shortname: "gcode", aliases: ["nc"] },
{ name: "Gherkin", shortname: "gherkin", aliases: ["feature"] },
{ name: "GLSL", shortname: "glsl" },
{ name: "GML", shortname: "gml" },
{ name: "Golo", shortname: "golo" },
{ name: "Gradle", shortname: "gradle" },
{ name: "Groovy", shortname: "groovy" },
{ name: "HAML", shortname: "haml" },
{
name: "Handlebars",
shortname: "handlebars",
aliases: ["hbs", "html.hbs", "html.handlebars", "htmlbars"],
},
{ name: "Haskell", shortname: "haskell", aliases: ["hs"] },
{ name: "Haxe", shortname: "haxe", aliases: ["hx"] },
{ name: "HSP", shortname: "hsp" },
{ name: "HTTP", shortname: "http", aliases: ["https"] },
{ name: "Hy", shortname: "hy", aliases: ["hylang"] },
{ name: "Inform 7", shortname: "inform7", aliases: ["i7"] },
{ name: "IRPF90", shortname: "irpf90" },
{ name: "ISBL", shortname: "isbl" },
{ name: "JBoss CLI", shortname: "jboss-cli", aliases: ["wildfly-cli"] },
{ name: "Julia", shortname: "julia" },
{ name: "Julia REPL", shortname: "julia-repl", aliases: ["jldoctest"] },
{ name: "Lasso", shortname: "lasso", aliases: ["ls", "lassoscript"] },
{ name: "LaTeX", shortname: "latex", aliases: ["tex"] },
{ name: "LDIF", shortname: "ldif" },
{ name: "Leaf", shortname: "leaf" },
{ name: "Lisp", shortname: "lisp" },
{ name: "LiveCode", shortname: "livecodeserver" },
{ name: "LiveScript", shortname: "livescript", aliases: ["ls"] },
{ name: "LLVM IR", shortname: "llvm" },
{ name: "LSL (Linden Scripting Language)", shortname: "lsl" },
{ name: "Mathematica", shortname: "mathematica", aliases: ["mma", "wl"] },
{ name: "Matlab", shortname: "matlab" },
{ name: "Maxima", shortname: "maxima" },
{ name: "MEL", shortname: "mel" },
{ name: "Mercury", shortname: "mercury", aliases: ["m", "moo"] },
{ name: "MIPS Assembly", shortname: "mipsasm", aliases: ["mips"] },
{ name: "Mizar", shortname: "mizar" },
{ name: "Mojolicious", shortname: "mojolicious" },
{ name: "Monkey", shortname: "monkey" },
{ name: "MoonScript", shortname: "moonscript", aliases: ["moon"] },
{ name: "N1QL", shortname: "n1ql" },
{ name: "Nested Text", shortname: "nestedtext", aliases: ["nt"] },
{ name: "Nginx config", shortname: "nginx", aliases: ["nginxconf"] },
{ name: "Nim", shortname: "nim" },
{ name: "Nix", shortname: "nix", aliases: ["nixos"] },
{ name: "Node REPL", shortname: "node-repl" },
{ name: "NSIS", shortname: "nsis" },
{ name: "OCaml", shortname: "ocaml", aliases: ["ml"] },
{ name: "OpenSCAD", shortname: "openscad", aliases: ["scad"] },
{ name: "Oxygene", shortname: "oxygene" },
{ name: "Parser3", shortname: "parser3" },
{ name: "Packet Filter config", shortname: "pf", aliases: ["pf.conf"] },
{
name: "PostgreSQL",
shortname: "pgsql",
aliases: ["postgres", "postgresql"],
},
{ name: "Pony", shortname: "pony" },
{
name: "PowerShell",
shortname: "powershell",
aliases: ["pwsh", "ps", "ps1"],
},
{ name: "Processing", shortname: "processing", aliases: ["pde"] },
{ name: "Python profiler", shortname: "profile" },
{ name: "Prolog", shortname: "prolog" },
{ name: ".properties", shortname: "properties" },
{ name: "Protocol Buffers", shortname: "protobuf" },
{ name: "Puppet", shortname: "puppet", aliases: ["pp"] },
{ name: "PureBASIC", shortname: "purebasic", aliases: ["pb", "pbi"] },
{ name: "Q", shortname: "q", aliases: ["k", "kdb"] },
{ name: "QML", shortname: "qml", aliases: ["qt"] },
{ name: "ReasonML", shortname: "reasonml", aliases: ["re"] },
{ name: "RenderMan RIB", shortname: "rib" },
{ name: "Roboconf", shortname: "roboconf", aliases: ["graph", "instances"] },
{
name: "Microtik RouterOS script",
shortname: "routeros",
aliases: ["mikrotik"],
},
{ name: "RenderMan RSL", shortname: "rsl" },
{ name: "Oracle Rules Language", shortname: "ruleslanguage" },
{ name: "SAS", shortname: "sas" },
{ name: "Scala", shortname: "scala" },
{ name: "Scheme", shortname: "scheme" },
{ name: "Scilab", shortname: "scilab", aliases: ["sci"] },
{ name: "Smali", shortname: "smali" },
{ name: "Smalltalk", shortname: "smalltalk", aliases: ["st"] },
{ name: "SML (Standard ML)", shortname: "sml", aliases: ["ml"] },
{ name: "SQF", shortname: "sqf" },
{ name: "Stan", shortname: "stan", aliases: ["stanfuncs"] },
{ name: "Stata", shortname: "stata", aliases: ["do", "ado"] },
{
name: "STEP Part 21",
shortname: "step21",
aliases: ["p21", "step", "stp"],
},
{ name: "Stylus", shortname: "stylus", aliases: ["styl"] },
{ name: "SubUnit", shortname: "subunit" },
{ name: "Tagger Script", shortname: "taggerscript" },
{ name: "Test Anything Protocol", shortname: "tap" },
{ name: "Tcl", shortname: "tcl", aliases: ["tk"] },
{ name: "Thrift", shortname: "thrift" },
{ name: "TP", shortname: "tp" },
{ name: "Twig", shortname: "twig", aliases: ["craftcms"] },
{ name: "Vala", shortname: "vala" },
{ name: "Visual Basic .NET", shortname: "vbnet", aliases: ["vb"] },
{ name: "VBScript", shortname: "vbscript", aliases: ["vbs"] },
{ name: "VBScript in HTML", shortname: "vbscript-html" },
{ name: "Verilog", shortname: "verilog", aliases: ["v", "sv", "svh"] },
{ name: "VHDL", shortname: "vhdl" },
{ name: "Vim Script", shortname: "vim" },
{ name: "WebAssembly", shortname: "wasm" },
{ name: "Wren", shortname: "wren" },
{ name: "Intel x86 Assembly", shortname: "x86asm" },
{ name: "XL", shortname: "xl", aliases: ["tao"] },
{ name: "XQuery", shortname: "xquery", aliases: ["xpath", "xq"] },
{ name: "Zephir", shortname: "zephir", aliases: ["zep"] },
];

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export declare function loadLanguage(shortName: string): Promise<import("refractor/lib/core").Syntax | undefined>;

View File

@@ -0,0 +1,74 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var loadedLanguages = {};
export function loadLanguage(shortName) {
return __awaiter(this, void 0, void 0, function () {
var url, result;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (loadedLanguages[shortName])
return [2 /*return*/, loadedLanguages[shortName]];
url = "https://esm.sh/refractor@4.7.0/lang/".concat(shortName, ".js?bundle=true");
return [4 /*yield*/, loadScript(shortName, url)];
case 1:
result = _a.sent();
loadedLanguages[shortName] = result;
return [2 /*return*/, result];
}
});
});
}
function loadScript(id, url) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve, reject) {
var callbackName = "on".concat(id, "Loaded");
var script = document.createElement("script");
script.type = "module";
script.innerHTML = "\n import LanguageDefinition from \"".concat(url, "\";\n if (window[\"").concat(callbackName, "\"]) {\n window[\"").concat(callbackName, "\"](LanguageDefinition)\n }\n");
window[callbackName] = function (lang) {
script.remove();
window[callbackName] = null;
resolve(lang);
};
// Append to the `head` element
document.head.appendChild(script);
})];
});
});
}

View File

@@ -21,28 +21,54 @@ export var TaskItemNode = TaskItem.extend({
return [
"li",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
class: "checklist--item",
}),
0,
];
},
// addAttributes() {
// return {
// hash: getDataAttribute("hash"),
// filename: getDataAttribute("filename"),
// type: getDataAttribute("type"),
// size: getDataAttribute("size"),
// };
// },
// parseHTML() {
parseHTML: function () {
return [
{
tag: "li",
getAttrs: function (node) {
var _a;
if (node instanceof Node && node instanceof HTMLElement) {
return ((node.classList.contains("checklist--item") ||
((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.classList.contains("checklist"))) &&
null);
}
return false;
},
priority: 51,
},
];
},
// renderHTML({ node, HTMLAttributes }) {
// return [
// {
// tag: "span[data-hash]",
// },
// ];
// },
// renderHTML({ HTMLAttributes }) {
// return ["span", mergeAttributes(HTMLAttributes)];
// 'li',
// mergeAttributes(
// this.options.HTMLAttributes,
// HTMLAttributes,
// { 'data-type': this.name },
// ),
// [
// 'label',
// [
// 'input',
// {
// type: 'checkbox',
// checked: node.attrs.checked
// ? 'checked'
// : null,
// },
// ],
// ['span'],
// ],
// [
// 'div',
// 0,
// ],
// ]
// },
addNodeView: function () {
return ReactNodeViewRenderer(TaskItemComponent);

View File

@@ -1,3 +1,4 @@
import { mergeAttributes } from "@tiptap/core";
import { TaskList } from "@tiptap/extension-task-list";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { TaskListComponent } from "./component";
@@ -27,33 +28,30 @@ export var TaskListNode = TaskList.extend({
},
};
},
// renderHTML({ node, HTMLAttributes }) {
// return [
// "li",
// mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
// "data-type": this.name,
// }),
// 0,
// ];
// },
// addAttributes() {
// return {
// hash: getDataAttribute("hash"),
// filename: getDataAttribute("filename"),
// type: getDataAttribute("type"),
// size: getDataAttribute("size"),
// };
// },
// parseHTML() {
// return [
// {
// tag: "span[data-hash]",
// },
// ];
// },
// renderHTML({ HTMLAttributes }) {
// return ["span", mergeAttributes(HTMLAttributes)];
// },
parseHTML: function () {
return [
{
tag: "ul",
getAttrs: function (node) {
if (node instanceof Node && node instanceof HTMLElement) {
return node.classList.contains("checklist") && null;
}
return false;
},
priority: 51,
},
];
},
renderHTML: function (_a) {
var HTMLAttributes = _a.HTMLAttributes;
return [
"ul",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: "checklist",
}),
0,
];
},
addNodeView: function () {
return ReactNodeViewRenderer(TaskListComponent);
},

View File

@@ -51,6 +51,7 @@ import { TaskItemNode } from "./extensions/task-item";
import { Dropcursor } from "./extensions/drop-cursor";
import { SearchReplace } from "./extensions/search-replace";
import { EmbedNode } from "./extensions/embed";
import { CodeBlock } from "./extensions/code-block";
EditorView.prototype.updateState = function updateState(state) {
if (!this.docView)
return; // This prevents the matchesNode error on hot reloads
@@ -65,6 +66,7 @@ var useTiptap = function (options, deps) {
TextStyle,
StarterKit.configure({
dropcursor: false,
codeBlock: false,
}),
Dropcursor.configure({
class: "drop-cursor",
@@ -91,6 +93,7 @@ var useTiptap = function (options, deps) {
Highlight.configure({
multicolor: true,
}),
CodeBlock,
Color,
TextAlign.configure({
types: ["heading", "paragraph"],

33
packages/editor/langen.js Normal file
View File

@@ -0,0 +1,33 @@
require("isomorphic-fetch");
async function main() {
const response = await fetch(
`https://github.com/PrismJS/prism/raw/master/components.json`
);
if (!response.ok) return;
const json = await response.json();
let output = [];
for (const key in json.languages) {
if (key === "meta") continue;
const language = json.languages[key];
// if (key === "markup") {
// language.alias.forEach((alias) => {
// output.push({
// filename: key,
// title: language.aliasTitles[alias],
// });
// });
// }
output.push({
filename: key,
title: language.title,
alias: language.alias
? Array.isArray(language.alias)
? language.alias
: [language.alias]
: undefined,
});
}
console.log(JSON.stringify(output));
}
main();

File diff suppressed because it is too large Load Diff

View File

@@ -30,9 +30,16 @@
"@tiptap/extension-underline": "^2.0.0-beta.23",
"@tiptap/react": "^2.0.0-beta.98",
"@tiptap/starter-kit": "^2.0.0-beta.150",
"@types/prismjs": "^1.26.0",
"@types/rebass": "^4.0.10",
"@types/rebass__forms": "^4.0.6",
"detect-indent": "^7.0.0",
"emotion-theming": "^10.0.19",
"esm-loader": "^0.1.0",
"highlight.js": "^11.5.1",
"lowlight": "^2.6.1",
"prism-themes": "^1.9.0",
"prismjs": "^1.28.0",
"prosemirror-tables": "^1.1.1",
"re-resizable": "^6.9.5",
"react-color": "^2.19.3",
@@ -41,11 +48,13 @@
"react-toggle": "^4.1.2",
"reactjs-popup": "^2.0.5",
"rebass": "^4.0.7",
"refractor": "^4.7.0",
"tinycolor2": "^1.4.2",
"tippy.js": "^6.3.7",
"zustand": "^3.7.2"
},
"devDependencies": {
"@types/esm": "^3.2.0",
"@types/node": "^16.11.11",
"@types/react": "^17.0.37",
"@types/react-color": "^3.0.6",
@@ -53,9 +62,12 @@
"@types/react-modal": "^3.13.1",
"@types/react-toggle": "^4.0.3",
"@types/tinycolor2": "^1.4.3",
"esm": "^3.2.25",
"isomorphic-fetch": "^3.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"ts-node": "^10.8.0",
"typescript": "^4.5.2",
"typescript-plugin-css-modules": "^3.4.0",
"web-vitals": "^1.1.2"

View File

@@ -0,0 +1,658 @@
import { Editor } from "@tiptap/core";
import { Node, textblockTypeInputRule, mergeAttributes } from "@tiptap/core";
import {
Plugin,
PluginKey,
TextSelection,
Transaction,
Selection,
} from "prosemirror-state";
import { ResolvedPos, Node as ProsemirrorNode } from "prosemirror-model";
import {
findParentNodeClosestToPos,
ReactNodeViewRenderer,
} from "@tiptap/react";
import { CodeblockComponent } from "./component";
import { HighlighterPlugin } from "./highlighter";
import detectIndent from "detect-indent";
export type IndentationOptions = {
type: "space" | "tab";
length: number;
};
export type CodeBlockAttributes = {
indentType: IndentationOptions["type"];
indentLength: number;
language: string;
lines: CodeLine[];
};
export interface CodeBlockOptions {
/**
* Adds a prefix to language classes that are applied to code tags.
* Defaults to `'language-'`.
*/
languageClassPrefix: string;
/**
* Define whether the node should be exited on triple enter.
* Defaults to `true`.
*/
exitOnTripleEnter: boolean;
/**
* Define whether the node should be exited on arrow down if there is no node after it.
* Defaults to `true`.
*/
exitOnArrowDown: boolean;
/**
* Define whether the node should be exited on arrow up if there is no node before it.
* Defaults to `true`.
*/
exitOnArrowUp: boolean;
/**
* Custom HTML attributes that should be added to the rendered HTML tag.
*/
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
codeblock: {
/**
* Set a code block
*/
setCodeBlock: (attributes?: { language: string }) => ReturnType;
/**
* Toggle a code block
*/
toggleCodeBlock: (attributes?: { language: string }) => ReturnType;
/**
* Change code block indentation options
*/
changeCodeBlockIndentation: (options: IndentationOptions) => ReturnType;
};
}
}
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
const ZERO_WIDTH_SPACE = "\u200b";
const NEWLINE = "\n";
export const CodeBlock = Node.create<CodeBlockOptions>({
name: "codeblock",
addOptions() {
return {
languageClassPrefix: "language-",
exitOnTripleEnter: true,
exitOnArrowDown: true,
exitOnArrowUp: true,
HTMLAttributes: {},
};
},
content: "text*",
marks: "",
group: "block",
code: true,
defining: true,
addAttributes() {
return {
lines: {
default: [],
rendered: false,
},
indentType: {
default: "space",
parseHTML: (element) => {
const indentType = element.dataset.indentType;
if (indentType) return indentType;
return detectIndent(element.innerText).type;
},
renderHTML: (attributes) => {
if (!attributes.indentType) {
return {};
}
return {
"data-indent-type": attributes.indentType,
};
},
},
indentLength: {
default: 2,
parseHTML: (element) => {
const indentLength = element.dataset.indentLength;
if (indentLength) return indentLength;
return detectIndent(element.innerText).amount;
},
renderHTML: (attributes) => {
if (!attributes.indentLength) {
return {};
}
return {
"data-indent-length": attributes.indentLength,
};
},
},
language: {
default: null,
parseHTML: (element) => {
const { languageClassPrefix } = this.options;
const classNames = [
...(element.classList || []),
...(element?.firstElementChild?.classList || []),
];
const languages = classNames
.filter((className) => className.startsWith(languageClassPrefix))
.map((className) => className.replace(languageClassPrefix, ""));
const language = languages[0];
if (!language) {
return null;
}
return language;
},
renderHTML: (attributes) => {
if (!attributes.language) {
return {};
}
return {
class: `language-${attributes.language}`,
};
},
},
};
},
parseHTML() {
return [
{
tag: "pre",
preserveWhitespace: "full",
// contentElement: (node) => {
// if (node instanceof HTMLElement) {
// node.innerText = node.innerText.replaceAll("\n\u200b\n", "\n\n");
// }
// return node;
// },
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"pre",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addCommands() {
return {
setCodeBlock:
(attributes) =>
({ commands }) => {
return commands.setNode(this.name, attributes);
},
toggleCodeBlock:
(attributes) =>
({ commands }) => {
return commands.toggleNode(this.name, "paragraph", attributes);
},
changeCodeBlockIndentation:
(options) =>
({ editor, tr, commands }) => {
const { state } = editor;
const { selection } = state;
const { $from } = selection;
if ($from.parent.type !== this.type) {
return false;
}
const { lines } = $from.parent.attrs as CodeBlockAttributes;
for (const line of lines) {
const text = line.text();
const whitespaceLength = text.length - text.trimStart().length;
if (!whitespaceLength) continue;
const indentLength = whitespaceLength;
const indentToken = indent(options.type, indentLength);
tr.insertText(
indentToken,
tr.mapping.map(line.from),
tr.mapping.map(line.from + whitespaceLength)
);
}
commands.updateAttributes(this.type, {
indentType: options.type,
indentLength: options.length,
});
return true;
},
};
},
addKeyboardShortcuts() {
return {
"Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(),
"Mod-a": ({ editor }) => {
const { $anchor } = this.editor.state.selection;
if ($anchor.parent.type.name !== this.name) {
return false;
}
const codeblock = findParentNodeClosestToPos(
$anchor,
(node) => node.type.name === this.type.name
);
if (!codeblock) return false;
return editor.commands.setTextSelection({
from: codeblock.pos,
to: codeblock.pos + codeblock.node.nodeSize,
});
},
// remove code block when at start of document or code block is empty
Backspace: () => {
const { empty, $anchor } = this.editor.state.selection;
const isAtStart = $anchor.pos === 1;
if (!empty || $anchor.parent.type.name !== this.name) {
return false;
}
if (isAtStart || !$anchor.parent.textContent.length) {
return this.editor.commands.clearNodes();
}
return false;
},
// exit node on triple enter
Enter: ({ editor }) => {
const { state } = editor;
const { selection } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const indentation = parseIndentation($from.parent);
return (
(this.options.exitOnTripleEnter &&
exitOnTripleEnter(editor, $from)) ||
indentOnEnter(editor, $from, indentation)
);
},
// exit node on arrow up
ArrowUp: ({ editor }) => {
if (!this.options.exitOnArrowUp) {
return false;
}
const { state } = editor;
const { selection } = state;
const { $anchor, empty } = selection;
if (!empty || $anchor.parent.type !== this.type) {
return false;
}
const isAtStart = $anchor.pos === 1;
if (!isAtStart) {
return false;
}
return editor.commands.insertContentAt(0, "<p></p>");
},
// exit node on arrow down
ArrowDown: ({ editor }) => {
if (!this.options.exitOnArrowDown) {
return false;
}
const { state } = editor;
const { selection, doc } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
if (!isAtEnd) {
return false;
}
const after = $from.after();
if (after === undefined) {
return false;
}
const nodeAfter = doc.nodeAt(after);
if (nodeAfter) {
editor.commands.setNodeSelection($from.before());
return false;
}
return editor.commands.exitCode();
},
"Shift-Tab": ({ editor }) => {
const { state } = editor;
const { selection } = state;
const { $from } = selection;
if ($from.parent.type !== this.type) {
return false;
}
const indentation = parseIndentation($from.parent);
const indentToken = indent(indentation.type, indentation.length);
const { lines } = $from.parent.attrs as CodeBlockAttributes;
const selectedLines = getSelectedLines(lines, selection);
return editor
.chain()
.command(({ tr }) =>
withSelection(tr, (tr) => {
for (const line of selectedLines) {
if (line.text(indentToken.length) !== indentToken) continue;
tr.delete(
tr.mapping.map(line.from),
tr.mapping.map(line.from + indentation.length)
);
}
})
)
.run();
},
Tab: ({ editor }) => {
const { state } = editor;
const { selection } = state;
const { $from } = selection;
if ($from.parent.type !== this.type) {
return false;
}
const { lines } = $from.parent.attrs as CodeBlockAttributes;
const selectedLines = getSelectedLines(lines, selection);
return editor
.chain()
.command(({ tr }) =>
withSelection(tr, (tr) => {
const indentation = parseIndentation($from.parent);
const indentToken = indent(indentation.type, indentation.length);
if (selectedLines.length === 1)
return tr.insertText(indentToken, $from.pos);
for (const line of selectedLines) {
tr.insertText(indentToken, tr.mapping.map(line.from));
}
})
)
.run();
},
};
},
addInputRules() {
return [
textblockTypeInputRule({
find: backtickInputRegex,
type: this.type,
getAttributes: (match) => ({
language: match[1],
}),
}),
textblockTypeInputRule({
find: tildeInputRegex,
type: this.type,
getAttributes: (match) => ({
language: match[1],
}),
}),
];
},
addProseMirrorPlugins() {
return [
// this plugin creates a code block for pasted content from VS Code
// we can also detect the copied code language
new Plugin({
key: new PluginKey("codeBlockVSCodeHandler"),
props: {
handlePaste: (view, event) => {
if (!event.clipboardData) {
return false;
}
// dont create a new code block within code blocks
if (this.editor.isActive(this.type.name)) {
return false;
}
const text = event.clipboardData.getData("text/plain");
const vscode = event.clipboardData.getData("vscode-editor-data");
const vscodeData = vscode ? JSON.parse(vscode) : undefined;
const language = vscodeData?.mode;
if (!text || !language) {
return false;
}
const { tr } = view.state;
// create an empty code block
tr.replaceSelectionWith(this.type.create({ language }));
// put cursor inside the newly created code block
tr.setSelection(
TextSelection.near(
tr.doc.resolve(Math.max(0, tr.selection.from - 2))
)
);
// add text to code block
// strip carriage return chars from text pasted as code
// see: https://github.com/ProseMirror/prosemirror-view/commit/a50a6bcceb4ce52ac8fcc6162488d8875613aacd
tr.insertText(text.replace(/\r\n?/g, "\n"));
// store meta information
// this is useful for other plugins that depends on the paste event
// like the paste rule plugin
tr.setMeta("paste", true);
view.dispatch(tr);
return true;
},
},
}),
HighlighterPlugin({ name: this.name, defaultLanguage: "txt" }),
];
},
addNodeView() {
return ReactNodeViewRenderer(CodeblockComponent);
},
});
export type CaretPosition = {
column: number;
line: number;
selected?: number;
total: number;
};
export function toCaretPosition(
lines: CodeLine[],
selection: Selection<any>
): CaretPosition | undefined {
const { $from, $to, $head } = selection;
if ($from.parent.type.name !== CodeBlock.name) return;
for (const line of lines) {
if ($head.pos >= line.from && $head.pos <= line.to) {
const lineLength = line.length + 1;
return {
line: line.index + 1,
column: lineLength - (line.to - $head.pos),
selected: $to.pos - $from.pos,
total: lines.length,
};
}
}
return;
}
export function getLines(node: ProsemirrorNode<any>) {
const { lines } = node.attrs as CodeBlockAttributes;
return lines || [];
}
function exitOnTripleEnter(editor: Editor, $from: ResolvedPos<any>) {
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
const endsWithDoubleNewline = $from.parent.textContent.endsWith("\n\n");
if (!isAtEnd || !endsWithDoubleNewline) {
return false;
}
return editor
.chain()
.command(({ tr }) => {
tr.delete($from.pos - 2, $from.pos);
return true;
})
.exitCode()
.run();
}
function indentOnEnter(
editor: Editor,
$from: ResolvedPos<any>,
options: IndentationOptions
) {
const { lines } = $from.parent.attrs as CodeBlockAttributes;
const currentLine = getLineAt(lines, $from.pos);
if (!currentLine) return false;
const text = editor.state.doc.textBetween(currentLine.from, currentLine.to);
const indentLength = text.length - text.trimStart().length;
const newline = `${NEWLINE}${indent(options.type, indentLength)}`;
return editor.commands.insertContent(newline, {
parseOptions: { preserveWhitespace: "full" },
});
}
type CodeLine = {
index: number;
from: number;
to: number;
length: number;
text: (length?: number) => string;
};
export function toCodeLines(code: string, pos: number): CodeLine[] {
const positions: CodeLine[] = [];
let start = 0;
let from = pos + 1;
let index = 0;
while (start <= code.length) {
let end = code.indexOf("\n", start);
if (end <= -1) end = code.length;
const lineLength = end - start;
const to = from + lineLength;
const lineStart = start;
positions.push({
index,
length: lineLength,
from,
to,
text: (length) => {
return code.slice(
lineStart,
length ? lineStart + length : lineStart + lineLength
);
},
});
from = to + 1;
start = end + 1;
++index;
}
return positions;
}
function getSelectedLines(lines: CodeLine[], selection: Selection<any>) {
const { $from, $to } = selection;
return lines.filter(
(line) =>
inRange(line.from, $from.pos, $to.pos) ||
inRange(line.to, $from.pos, $to.pos) ||
inRange($from.pos, line.from, line.to)
);
}
function parseIndentation(node: ProsemirrorNode<any>): IndentationOptions {
const { indentType, indentLength } = node.attrs;
return {
type: indentType,
length: parseInt(indentLength),
};
}
function getLineAt(lines: CodeLine[], pos: number) {
return lines.find((line) => pos >= line.from && pos <= line.to);
}
function inRange(x: number, a: number, b: number) {
return x >= a && x <= b;
}
function indent(type: IndentationOptions["type"], length: number) {
const char = type === "space" ? " " : "\t";
return char.repeat(length);
}
/**
* Persist selection between transaction steps
*/
function withSelection(
tr: Transaction<any>,
callback: (tr: Transaction<any>) => void
): boolean {
const { $anchor, $head } = tr.selection;
callback(tr);
tr.setSelection(
new TextSelection(
tr.doc.resolve(tr.mapping.map($anchor.pos)),
tr.doc.resolve(tr.mapping.map($head.pos))
)
);
return true;
}

View File

@@ -0,0 +1,263 @@
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";
import { loadLanguage } from "./loader";
import { refractor } from "refractor/lib/core";
import "prism-themes/themes/prism-dracula.min.css";
import { Theme } from "@notesnook/theme";
import { ThemeProvider } from "emotion-theming";
import { Button, Flex, Text } from "rebass";
import Languages from "./languages.json";
import { PopupPresenter } from "../../components/menu/menu";
import { Input } from "@rebass/forms";
import { Icon } from "../../toolbar/components/icon";
import { Icons } from "../../toolbar/icons";
import {
CodeBlockAttributes,
toCaretPosition,
CaretPosition,
getLines,
} from "./code-block";
import { Transaction } from "prosemirror-state";
export function CodeblockComponent(props: NodeViewProps) {
const { editor, updateAttributes, node } = props;
const { language, indentLength, indentType } =
node.attrs as CodeBlockAttributes;
const theme = editor.storage.theme as Theme;
const [isOpen, setIsOpen] = useState(false);
const [caretPosition, setCaretPosition] = useState<CaretPosition>();
const toolbarRef = useRef<HTMLDivElement>(null);
const languageDefinition = Languages.find(
(l) => l.filename === language || l.alias?.some((a) => a === language)
);
useEffect(() => {
(async function () {
if (!language || !languageDefinition) {
updateAttributes({ language: null });
return;
}
const syntax = await loadLanguage(languageDefinition.filename);
if (!syntax) return;
refractor.register(syntax);
updateAttributes({
language: languageDefinition.filename,
});
})();
}, [language, updateAttributes]);
useEffect(() => {
function onSelectionUpdate({
transaction,
}: {
transaction: Transaction<any>;
}) {
const position = toCaretPosition(getLines(node), transaction.selection);
setCaretPosition(position);
}
editor.on("selectionUpdate", onSelectionUpdate);
return () => {
editor.off("selectionUpdate", onSelectionUpdate);
};
}, [node]);
return (
<NodeViewWrapper>
<ThemeProvider theme={theme}>
<Flex
sx={{
flexDirection: "column",
borderRadius: "default",
overflow: "hidden",
}}
>
<Text
as="pre"
sx={{
"div, span.token, span.line-number-widget": {
fontFamily: "monospace",
fontSize: "code",
whiteSpace: "pre !important",
tabSize: 1,
},
position: "relative",
lineHeight: "20px",
bg: "codeBg",
color: "static",
overflowX: "auto",
display: "flex",
px: 2,
pt: 2,
pb: 1,
}}
spellCheck={false}
>
<NodeViewContent as="code" />
</Text>
<Flex
ref={toolbarRef}
sx={{
bg: "codeBg",
alignItems: "center",
justifyContent: "end",
borderTop: "1px solid var(--codeBorder)",
}}
>
{caretPosition ? (
<Text variant={"subBody"} sx={{ mr: 2, color: "codeFg" }}>
Line {caretPosition.line}, Column {caretPosition.column}{" "}
{caretPosition.selected
? `(${caretPosition.selected} selected)`
: ""}
</Text>
) : null}
<Button
variant={"icon"}
sx={{ p: 1, mr: 1, ":hover": { bg: "codeSelection" } }}
title="Toggle indentation mode"
onClick={() => {
editor.commands.changeCodeBlockIndentation({
type: indentType === "space" ? "tab" : "space",
length: indentLength,
});
}}
>
<Text variant={"subBody"} sx={{ color: "codeFg" }}>
{indentType === "space" ? "Spaces" : "Tabs"}: {indentLength}
</Text>
</Button>
<Button
variant={"icon"}
sx={{
p: 1,
mr: 1,
bg: isOpen ? "codeSelection" : "transparent",
":hover": { bg: "codeSelection" },
}}
onClick={() => setIsOpen(true)}
title="Change language"
>
<Text
variant={"subBody"}
spellCheck={false}
sx={{ color: "codeFg" }}
>
{languageDefinition?.title || "Plaintext"}
</Text>
</Button>
</Flex>
</Flex>
<PopupPresenter
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
editor.commands.focus();
}}
mobile="sheet"
desktop="menu"
options={{
type: "menu",
position: {
target: toolbarRef.current || undefined,
align: "end",
isTargetAbsolute: true,
location: "top",
yOffset: 5,
},
}}
>
<LanguageSelector
selectedLanguage={languageDefinition?.filename || "Plaintext"}
onLanguageSelected={(language) => updateAttributes({ language })}
/>
</PopupPresenter>
</ThemeProvider>
</NodeViewWrapper>
);
}
type LanguageSelectorProps = {
onLanguageSelected: (language: string) => void;
selectedLanguage: string;
};
function LanguageSelector(props: LanguageSelectorProps) {
const { onLanguageSelected, selectedLanguage } = props;
const [languages, setLanguages] = useState(Languages);
return (
<Flex
sx={{
flexDirection: "column",
height: 200,
width: 300,
boxShadow: "menu",
borderRadius: "default",
overflowY: "auto",
bg: "background",
marginRight: 2,
}}
>
<Input
autoFocus
placeholder="Search languages"
sx={{
mx: 2,
width: "auto",
position: "sticky",
top: 2,
bg: "background",
p: "7px",
}}
onChange={(e) => {
if (!e.target.value) return setLanguages(Languages);
const query = e.target.value.toLowerCase();
setLanguages(
Languages.filter((lang) => {
return (
lang.title.toLowerCase().indexOf(query) > -1 ||
lang.alias?.some(
(alias) => alias.toLowerCase().indexOf(query) > -1
)
);
})
);
}}
/>
<Flex
sx={{
flexDirection: "column",
pt: 2,
mt: 1,
}}
>
{languages.map((lang) => (
<Button
variant={"menuitem"}
sx={{
textAlign: "left",
py: 1,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
onClick={() => onLanguageSelected(lang.filename)}
>
<Text variant={"body"}>{lang.title}</Text>
{selectedLanguage === lang.filename ? (
<Icon path={Icons.check} size="small" />
) : lang.alias ? (
<Text variant={"subBody"} sx={{ fontSize: "10px" }}>
{lang.alias.slice(0, 3).join(", ")}
</Text>
) : null}
</Button>
))}
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,298 @@
import { Plugin, PluginKey, Transaction, EditorState } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { Node as ProsemirrorNode } from "prosemirror-model";
import { findChildren } from "@tiptap/core";
import { Root, refractor } from "refractor/lib/core";
import { RootContent } from "hast";
import {
AddMarkStep,
RemoveMarkStep,
ReplaceAroundStep,
ReplaceStep,
} from "prosemirror-transform";
import {
CaretPosition,
CodeBlockAttributes,
getLines,
toCaretPosition,
toCodeLines,
} from "./code-block";
type MergedStep =
| AddMarkStep
| RemoveMarkStep
| ReplaceAroundStep
| ReplaceStep;
function parseNodes(
nodes: RootContent[],
className: string[] = []
): { text: string; classes: string[] }[] {
return nodes.reduce((result, node) => {
if (node.type === "comment" || node.type === "doctype") return result;
const classes: string[] = [...className];
if (node.type === "element" && node.properties)
classes.push(...(node.properties.className as string[]));
else classes.push("token", "text");
if (node.type === "element") {
result.push(...parseNodes(node.children, classes));
} else {
result.push({ classes, text: node.value });
}
return result;
}, [] as { text: string; classes: string[] }[]);
}
function getHighlightNodes(result: Root) {
return result.children || [];
}
function getLineDecoration(
from: number,
line: number,
total: number,
isActive: boolean
) {
const attributes = {
class: `line-number ${isActive ? "active" : ""}`,
"data-line": String(line).padEnd(String(total).length, " "),
};
const spec = {
line: line,
active: isActive,
total,
};
// Prosemirror has a selection issue with the widget decoration
// on the first line. To work around that we use inline decoration
// for the first line.
if (line === 1) {
return Decoration.inline(from, from + 1, attributes, spec);
}
return Decoration.widget(
from,
() => {
const element = document.createElement("span");
element.classList.add("line-number-widget");
if (isActive) element.classList.add("active");
element.innerHTML = attributes["data-line"];
return element;
},
{
...spec,
key: `${line}-${isActive ? "active" : "inactive"}`,
}
);
}
function getDecorations({
doc,
name,
defaultLanguage,
currentLine,
}: {
currentLine?: number;
doc: ProsemirrorNode;
name: string;
defaultLanguage: string | null | undefined;
}) {
const decorations: Decoration[] = [];
const languages = refractor.listLanguages();
findChildren(doc, (node) => node.type.name === name).forEach((block) => {
const code = block.node.textContent;
const { lines } = block.node.attrs as CodeBlockAttributes;
for (const line of lines || []) {
const lineNumber = line.index + 1;
const isActive = lineNumber === currentLine;
const decoration = getLineDecoration(
line.from,
lineNumber,
lines?.length || 0,
isActive
);
decorations.push(decoration);
}
const language = block.node.attrs.language || defaultLanguage;
const nodes = languages.includes(language)
? getHighlightNodes(refractor.highlight(code, language))
: null;
if (!nodes) return;
let from = block.pos + 1;
parseNodes(nodes).forEach((node) => {
const to = from + node.text.length;
if (node.classes.length) {
const decoration = Decoration.inline(from, to, {
class: node.classes.join(" "),
});
decorations.push(decoration);
}
from = to;
});
});
return DecorationSet.create(doc, decorations);
}
export function HighlighterPlugin({
name,
defaultLanguage,
}: {
name: string;
defaultLanguage: string | null | undefined;
}) {
return new Plugin({
key: new PluginKey("highlighter"),
state: {
init: () => {
return DecorationSet.empty;
},
apply: (
transaction,
decorationSet: DecorationSet,
oldState,
newState
) => {
const oldNodeName = oldState.selection.$head.parent.type.name;
const newNodeName = newState.selection.$head.parent.type.name;
const oldNodes = findChildren(
oldState.doc,
(node) => node.type.name === name
);
const newNodes = findChildren(
newState.doc,
(node) => node.type.name === name
);
const position = toCaretPosition(
getLines(newState.selection.$head.parent),
newState.selection
);
if (
transaction.docChanged &&
// Apply decorations if:
// selection includes named node,
([oldNodeName, newNodeName].includes(name) ||
// OR transaction adds/removes named node,
newNodes.length !== oldNodes.length ||
// OR transaction has changes that completely encapsulte a node
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
(transaction.steps as MergedStep[]).some((step) => {
return (
step.from !== undefined &&
step.to !== undefined &&
oldNodes.some((node) => {
return (
node.pos >= step.from &&
node.pos + node.node.nodeSize <= step.to
);
})
);
}))
) {
return getDecorations({
doc: transaction.doc,
name,
defaultLanguage,
currentLine: position?.line,
});
}
decorationSet = getActiveLineDecorations(
transaction.doc,
decorationSet,
position
);
return decorationSet.map(transaction.mapping, transaction.doc);
},
},
props: {
decorations(state) {
return this.getState(state);
},
},
appendTransaction: (transactions, _prevState, nextState) => {
const tr = nextState.tr;
let modified = false;
if (transactions.some((transaction) => transaction.docChanged)) {
findChildren(nextState.doc, (node) => node.type.name === name).forEach(
(block) => {
const { node, pos } = block;
const lines = toCodeLines(node.textContent, pos);
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
lines,
});
modified = true;
}
);
}
return modified ? tr : null;
},
});
}
/**
* When `position` is undefined, all active line decorations
* are reset (e.g. when you focus out of the code block).
*/
function getActiveLineDecorations(
doc: ProsemirrorNode<any>,
decorations: DecorationSet,
position?: CaretPosition
) {
const lineDecorations = decorations.find(
undefined,
undefined,
({ line, active }) => {
return (position && line === position.line) || active;
}
);
if (!lineDecorations.length) return decorations;
// we have to clone because prosemirror operates in-place
const cloned = lineDecorations.slice();
// remove old line decorations which inclue the current line decoration
// and the previous current line decoration. We'll replace these with
// new decorations.
decorations = decorations.remove(lineDecorations);
const newDecorations: Decoration[] = [];
for (const decoration of cloned) {
const {
from,
spec: { line, total },
} = decoration;
const isActive = line === position?.line;
const newDecoration = getLineDecoration(
from,
line,
position?.total || total,
isActive
);
newDecorations.push(newDecoration);
}
return decorations.add(doc, newDecorations);
}

View File

@@ -0,0 +1 @@
export * from "./code-block";

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
import { Syntax } from "refractor";
const loadedLanguages: Record<string, Syntax | undefined> = {};
export async function loadLanguage(shortName: string) {
if (loadedLanguages[shortName]) return loadedLanguages[shortName];
const url = `https://esm.sh/refractor@4.7.0/lang/${shortName}.js?bundle=true`;
const result = await loadScript(shortName, url);
loadedLanguages[shortName] = result;
return result;
}
async function loadScript(id: string, url: string) {
return new Promise<Syntax>((resolve, reject) => {
const callbackName = `on${id}Loaded`;
const script = document.createElement("script");
script.type = "module";
script.innerHTML = `
import LanguageDefinition from "${url}";
if (window["${callbackName}"]) {
window["${callbackName}"](LanguageDefinition)
}
`;
(window as any)[callbackName] = (lang: Syntax) => {
script.remove();
(window as any)[callbackName] = null;
resolve(lang);
};
// Append to the `head` element
document.head.appendChild(script);
});
}

View File

@@ -29,31 +29,57 @@ export const TaskItemNode = TaskItem.extend({
return [
"li",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
class: "checklist--item",
}),
0,
];
},
// addAttributes() {
// return {
// hash: getDataAttribute("hash"),
// filename: getDataAttribute("filename"),
// type: getDataAttribute("type"),
// size: getDataAttribute("size"),
// };
// },
parseHTML() {
return [
{
tag: `li`,
getAttrs: (node) => {
if (node instanceof Node && node instanceof HTMLElement) {
return (
(node.classList.contains("checklist--item") ||
node.parentElement?.classList.contains("checklist")) &&
null
);
}
return false;
},
priority: 51,
},
];
},
// parseHTML() {
// renderHTML({ node, HTMLAttributes }) {
// return [
// {
// tag: "span[data-hash]",
// },
// ];
// },
// renderHTML({ HTMLAttributes }) {
// return ["span", mergeAttributes(HTMLAttributes)];
// 'li',
// mergeAttributes(
// this.options.HTMLAttributes,
// HTMLAttributes,
// { 'data-type': this.name },
// ),
// [
// 'label',
// [
// 'input',
// {
// type: 'checkbox',
// checked: node.attrs.checked
// ? 'checked'
// : null,
// },
// ],
// ['span'],
// ],
// [
// 'div',
// 0,
// ],
// ]
// },
addNodeView() {

View File

@@ -32,36 +32,30 @@ export const TaskListNode = TaskList.extend({
};
},
// renderHTML({ node, HTMLAttributes }) {
// return [
// "li",
// mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
// "data-type": this.name,
// }),
// 0,
// ];
// },
parseHTML() {
return [
{
tag: `ul`,
getAttrs: (node) => {
if (node instanceof Node && node instanceof HTMLElement) {
return node.classList.contains("checklist") && null;
}
return false;
},
priority: 51,
},
];
},
// addAttributes() {
// return {
// hash: getDataAttribute("hash"),
// filename: getDataAttribute("filename"),
// type: getDataAttribute("type"),
// size: getDataAttribute("size"),
// };
// },
// parseHTML() {
// return [
// {
// tag: "span[data-hash]",
// },
// ];
// },
// renderHTML({ HTMLAttributes }) {
// return ["span", mergeAttributes(HTMLAttributes)];
// },
renderHTML({ HTMLAttributes }) {
return [
"ul",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: "checklist",
}),
0,
];
},
addNodeView() {
return ReactNodeViewRenderer(TaskListComponent);

View File

@@ -31,6 +31,7 @@ import { TaskItemNode } from "./extensions/task-item";
import { Dropcursor } from "./extensions/drop-cursor";
import { SearchReplace } from "./extensions/search-replace";
import { EmbedNode } from "./extensions/embed";
import { CodeBlock } from "./extensions/code-block";
EditorView.prototype.updateState = function updateState(state) {
if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads
@@ -50,6 +51,7 @@ const useTiptap = (
TextStyle,
StarterKit.configure({
dropcursor: false,
codeBlock: false,
}),
Dropcursor.configure({
class: "drop-cursor",
@@ -76,6 +78,7 @@ const useTiptap = (
Highlight.configure({
multicolor: true,
}),
CodeBlock,
Color,
TextAlign.configure({
types: ["heading", "paragraph"],

View File

@@ -112,3 +112,4 @@ What's next:
---
1. Keyboard shouldn't close on tool click
2. Handle context toolbar menus on scroll