mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
feat: somewhat finalize the new codeblock
This commit is contained in:
28
packages/editor/backward.md
Normal file
28
packages/editor/backward.md
Normal 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 & 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> </li><li>Clean up all the UI code</li></ul></li><li class=\"\">Make content script & background script communication type-safe</li><li>Add a basic onboarding screen</li><li>Test on Firefox & 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
|
||||
81
packages/editor/dist/extensions/codeblock/codeblock.d.ts
vendored
Normal file
81
packages/editor/dist/extensions/codeblock/codeblock.d.ts
vendored
Normal 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 {};
|
||||
560
packages/editor/dist/extensions/codeblock/codeblock.js
vendored
Normal file
560
packages/editor/dist/extensions/codeblock/codeblock.js
vendored
Normal 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;
|
||||
}
|
||||
// don’t 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;
|
||||
}
|
||||
4
packages/editor/dist/extensions/codeblock/component.d.ts
vendored
Normal file
4
packages/editor/dist/extensions/codeblock/component.d.ts
vendored
Normal 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;
|
||||
213
packages/editor/dist/extensions/codeblock/component.js
vendored
Normal file
213
packages/editor/dist/extensions/codeblock/component.js
vendored
Normal 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] }))); }) }))] })));
|
||||
}
|
||||
5
packages/editor/dist/extensions/codeblock/highlighter.d.ts
vendored
Normal file
5
packages/editor/dist/extensions/codeblock/highlighter.d.ts
vendored
Normal 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>;
|
||||
242
packages/editor/dist/extensions/codeblock/highlighter.js
vendored
Normal file
242
packages/editor/dist/extensions/codeblock/highlighter.js
vendored
Normal 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);
|
||||
}
|
||||
1
packages/editor/dist/extensions/codeblock/index.d.ts
vendored
Normal file
1
packages/editor/dist/extensions/codeblock/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./code-block";
|
||||
1
packages/editor/dist/extensions/codeblock/index.js
vendored
Normal file
1
packages/editor/dist/extensions/codeblock/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./code-block";
|
||||
13
packages/editor/dist/extensions/codeblock/languages.d.ts
vendored
Normal file
13
packages/editor/dist/extensions/codeblock/languages.d.ts
vendored
Normal 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;
|
||||
})[];
|
||||
339
packages/editor/dist/extensions/codeblock/languages.js
vendored
Normal file
339
packages/editor/dist/extensions/codeblock/languages.js
vendored
Normal 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: "Backus–Naur Form", shortname: "bnf" },
|
||||
{ name: "Brainfuck", shortname: "brainfuck", aliases: ["bf"] },
|
||||
{ name: "C/AL", shortname: "cal" },
|
||||
{ name: "Cap’n 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"] },
|
||||
];
|
||||
1
packages/editor/dist/extensions/codeblock/languages.json
vendored
Normal file
1
packages/editor/dist/extensions/codeblock/languages.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
packages/editor/dist/extensions/codeblock/loader.d.ts
vendored
Normal file
1
packages/editor/dist/extensions/codeblock/loader.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare function loadLanguage(shortName: string): Promise<import("refractor/lib/core").Syntax | undefined>;
|
||||
74
packages/editor/dist/extensions/codeblock/loader.js
vendored
Normal file
74
packages/editor/dist/extensions/codeblock/loader.js
vendored
Normal 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);
|
||||
})];
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
3
packages/editor/dist/index.js
vendored
3
packages/editor/dist/index.js
vendored
@@ -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
33
packages/editor/langen.js
Normal 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();
|
||||
826
packages/editor/package-lock.json
generated
826
packages/editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
658
packages/editor/src/extensions/code-block/code-block.ts
Normal file
658
packages/editor/src/extensions/code-block/code-block.ts
Normal 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;
|
||||
}
|
||||
|
||||
// don’t 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;
|
||||
}
|
||||
263
packages/editor/src/extensions/code-block/component.tsx
Normal file
263
packages/editor/src/extensions/code-block/component.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
298
packages/editor/src/extensions/code-block/highlighter.ts
Normal file
298
packages/editor/src/extensions/code-block/highlighter.ts
Normal 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);
|
||||
}
|
||||
1
packages/editor/src/extensions/code-block/index.ts
Normal file
1
packages/editor/src/extensions/code-block/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./code-block";
|
||||
1
packages/editor/src/extensions/code-block/languages.json
Normal file
1
packages/editor/src/extensions/code-block/languages.json
Normal file
File diff suppressed because one or more lines are too long
34
packages/editor/src/extensions/code-block/loader.ts
Normal file
34
packages/editor/src/extensions/code-block/loader.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -112,3 +112,4 @@ What's next:
|
||||
---
|
||||
|
||||
1. Keyboard shouldn't close on tool click
|
||||
2. Handle context toolbar menus on scroll
|
||||
|
||||
Reference in New Issue
Block a user