Skip to content

Instantly share code, notes, and snippets.

@matthiasg
Forked from BrianHung/MathBlock.css
Created June 21, 2021 07:24
Show Gist options
  • Save matthiasg/8605e5df72fbf741b382b5eaa3d045b9 to your computer and use it in GitHub Desktop.
Save matthiasg/8605e5df72fbf741b382b5eaa3d045b9 to your computer and use it in GitHub Desktop.
MathBlock NodeView for TipTap
.ProseMirror .MathBlock pre {
background: var(--default-back);
color: rgb(var(--default-font));
font-size: 0.8em;
display: flex;
padding: 1em;
}
.ProseMirror .MathBlock {
display: flex;
flex-wrap: wrap;
margin-block-end: 0.50em;
margin-block-start: 0.50em;
}
.MathBlock .katex-render
{
background-color: var(--default-back);
position: relative;
transition: all 120ms ease-in-out, height 1s ease-in;
cursor: pointer;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.MathBlock .katex-render {
display: flex;
flex: 0 0 100%;
box-sizing: border-box;
overflow-y: overlay;
}
.MathBlock .katex-editor {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
flex-grow: 1;
margin: 0 auto;
}
.MathBlock .katex-editor.hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
white-space: nowrap;
clip-path: inset(100%);
clip: rect(0 0 0 0);
overflow: hidden;
}
.MathBlock .katex-editor.active {
position: static;
width: auto;
height: auto;
margin: 0;
clip: auto;
overflow: visible;
}
.MathBlock .katex-render .katex-error {
margin: 0 auto;
padding: 1em;
width: 100%;
font-family: "Fira Code", monospace;
font-size: 0.80em;
}
.katex-render .katex-display {
margin: 0 auto;
padding: 1em;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
}
.katex-render .katex
{
font-size: 1em;
}
.MathBlock .katex-render * {
user-select: none;
}
.MathBlock.empty-node code::before {
content: "mathblock";
float: left;
color: var(--default-gray);
pointer-events: none;
height: 0;
}
.MathBlock .cm-tag {
color: var(--default-gren);
import { Node } from 'tiptap'
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
import './MathBlock.css'
import { lineIndent, lineUndent, newlineIndent, deleteMathBlock } from "./MathBlockKeymaps.js"
import katex from "katex"
import "katex/dist/katex.min.css"
/*
* Defines a ComponentView for MathBlock.
*/
export default class MathBlock extends Node {
get name() {
return "mathblock";
}
get view() {
return {
name: "mathblock",
props: ['node', 'view', "getPos"],
watch: {
"node.textContent": function(text) { this.updateKatex(text); }
},
computed: {
visibleClass() {
return (this.hasProseMirrorSelection() || true) ? "active" : "hidden"
},
},
mounted() {
this.updateKatex(this.node.textContent);
},
methods: {
updateKatex(text) {
katex.render(/\S/.test(text) ? text : "\\text{MathBlock}", this.$refs.render, {
throwOnError: false, displayMode: true
})
},
hasProseMirrorSelection() {
let anchor = this.view.state.selection.anchor
return this.getPos() <= anchor && anchor < this.node.nodeSize + this.getPos()
},
},
template: `
<div class="MathBlock">
<div class="katex-render" ref="render" :contenteditable="false"></div>
<pre class="katex-editor" v-bind:class="visibleClass" :data-lang="node.attrs.lang"><code ref="content"></code></pre>
</div>`
};
}
get schema() {
return {
attrs: { lang: { default: "stex" }},
content: "text*",
marks: "",
group: "block",
code: true,
defining: true,
selectable: true,
draggable: true,
parseDOM: [{ tag: "div", class: "MathBlock"}],
toDOM: node => ["div", {class: "MathBlock"},
["div", {class: "katex-render" , contenteditable: 'false'}],
["pre", {class: "katex-editor"}, ["code", 0]]
],
};
}
commands({ type, schema }) {
return () => toggleBlockType(type, schema.nodes.paragraph)
}
keys({ type }) {
return {
"Shift-Ctrl-\\": setBlockType(type),
"Tab": lineIndent,
"Shift-Tab": lineUndent,
"Enter": newlineIndent,
"Backspace": deleteMathBlock
}
}
inputRules({ type }) {
return [
textblockTypeInputRule(/^\$\$\$$/, type),
];
}
}
/**
* Helper functions for MathBlock keymaps, kindly taken from:
* bitbucket.org/atlassian/.../editor/editor-core/src/plugins/code-block/
*/
export const lineIndent = (state, dispatch, view) => {
if (!isSelectionEntirelyInsideMathBlock(state)) return false;
return indent(state, dispatch);
}
export const lineUndent = (state, dispatch, view) => {
if (!isSelectionEntirelyInsideMathBlock(state)) return false;
return undent(state, dispatch);
}
export const newlineIndent = (state, dispatch, view) => {
if (!isSelectionEntirelyInsideMathBlock(state)) return false;
return insertNewlineWithIndent(state, dispatch);
}
export const deleteMathBlock = (state, dispatch, view) => {
if (!isSelectionEntirelyInsideMathBlock(state)) return false;
if (!state.selection.$cursor || (state.selection.$cursor && state.selection.$cursor.node().textContent)) return false;
const { tr, selection } = state;
tr.deleteRange(selection.$cursor.before(selection.$cursor.depth),
selection.$cursor.end(selection.$cursor.depth) + 1);
dispatch(tr);
return true;
}
/**
* Return the current indentation level
* @param indentText - Text in the math block that represent an indentation
* @param indentSize - Size of the indentation token in a string
*/
function getIndentLevel(indentText, indentSize) {
return (indentSize && indentText.length) ? indentText.length / indentSize : 0;
}
import { TextSelection } from 'prosemirror-state';
function indent(state, dispatch) {
const { text, start } = getLinesFromSelection(state);
const { tr, selection } = state;
forEachLine(text, (line, offset) => {
const { indentText, indentToken } = getLineInfo(line);
const indentLevel = getIndentLevel(indentText, indentToken.size);
const indentToAdd = indentToken.token.repeat(
indentToken.size - (indentText.length % indentToken.size) || indentToken.size,
);
tr.insertText(indentToAdd, tr.mapping.map(start + offset, -1));
if (!selection.empty) {
tr.setSelection(TextSelection.create(
tr.doc,
tr.mapping.map(selection.from, -1),
tr.selection.to,
));
}
});
if (dispatch) {
dispatch(tr);
}
return true;
}
function undent(state, dispatch) {
const { text, start } = getLinesFromSelection(state);
const { tr } = state;
forEachLine(text, (line, offset) => {
const { indentText, indentToken } = getLineInfo(line);
if (indentText) {
const indentLevel = getIndentLevel(indentText, indentToken.size);
const undentLength = indentText.length % indentToken.size || indentToken.size;
tr.delete(
tr.mapping.map(start + offset),
tr.mapping.map(start + offset + undentLength),
);
}
});
if (dispatch) {
dispatch(tr);
}
return true;
}
function insertIndent(state, dispatch) {
const { text: textAtStartOfLine } = getStartOfCurrentLine(state);
const { indentToken } = getLineInfo(textAtStartOfLine);
const indentToAdd = indentToken.token.repeat(
indentToken.size - (textAtStartOfLine.length % indentToken.size) || indentToken.size,
);
dispatch(state.tr.insertText(indentToAdd));
return true;
}
function insertNewlineWithIndent(state, dispatch) {
const { text: textAtStartOfLine } = getStartOfCurrentLine(state);
const { indentText } = getLineInfo(textAtStartOfLine);
if (indentText && dispatch) {
dispatch(state.tr.insertText('\n' + indentText));
return true;
}
return false;
}
function isSelectionEntirelyInsideMathBlock(state) {
return state.selection.$from.sameParent(state.selection.$to) &&
state.selection.$from.parent.type === state.schema.nodes.mathblock;
}
function getStartOfCurrentLine(state) {
const { $from } = state.selection;
if ($from.nodeBefore && $from.nodeBefore.isText) {
const prevNewLineIndex = $from.nodeBefore.text.lastIndexOf('\n');
return {
text: $from.nodeBefore.text.substring(prevNewLineIndex + 1),
pos: $from.start() + prevNewLineIndex + 1,
};
}
return { text: '', pos: $from.pos };
};
function getEndOfCurrentLine(state) {
const { $to } = state.selection;
if ($to.nodeAfter && $to.nodeAfter.isText) {
const nextNewLineIndex = $to.nodeAfter.text.indexOf('\n');
return {
text: $to.nodeAfter.text.substring(
0,
nextNewLineIndex >= 0 ? nextNewLineIndex : undefined,
),
pos: nextNewLineIndex >= 0 ? $to.pos + nextNewLineIndex : $to.end(),
};
}
return { text: '', pos: $to.pos };
};
function getLinesFromSelection(state) {
const { pos: start } = getStartOfCurrentLine(state);
const { pos: end } = getEndOfCurrentLine(state);
const text = state.doc.textBetween(start, end);
return { text, start, end };
}
function forEachLine(text, callback) {
let offset = 0;
text.split('\n').forEach(line => {
callback(line, offset);
offset += line.length + 1;
});
};
const SPACE = { token: ' ', size: 2, regex: /[^ ]/ };
const TAB = { token: '\t', size: 1, regex: /[^\t]/ };
function getLineInfo(line) {
const indentToken = line.startsWith('\t') ? TAB : SPACE;
const indentLength = line.search(indentToken.regex);
const indentText = line.substring(
0,
indentLength >= 0 ? indentLength : line.length,
);
return { indentToken, indentText };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment