1
1
mirror of https://github.com/varkor/quiver.git synced 2024-09-11 05:46:13 +03:00

Switch to KaTeX as the default TeX rendering library

KaTeX is significantly faster than MathJax and is thus more well-suited to being used in a real-time app.
This commit is contained in:
varkor 2019-01-12 20:33:34 +00:00
parent 308c4fa667
commit 6733a055b7
6 changed files with 205 additions and 111 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "src/MathJax"]
path = src/MathJax
url = https://github.com/mathjax/MathJax.git
[submodule "src/KaTeX"]
path = src/KaTeX
url = https://github.com/KaTeX/KaTeX.git

3
src/.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "MathJax"]
path = MathJax
url = https://github.com/mathjax/MathJax.git
[submodule "KaTeX"]
path = KaTeX
url = https://github.com/KaTeX/KaTeX.git

1
src/KaTeX Submodule

@ -0,0 +1 @@
Subproject commit e85cb97ccaf1b71908d84bd499b1a491bace5d16

View File

@ -7,7 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script type="text/javascript" src="ui.js"></script>
<script type="text/javascript" async id="MathJax" src="MathJax/MathJax.js"></script>
</head>
<body>
</body>

View File

@ -29,7 +29,7 @@ body {
outline: none;
}
.error {
.error-banner {
position: fixed;
width: 100%;
left: 0; top: 0;
@ -46,6 +46,10 @@ body {
transition: transform 0.2s;
}
.error-banner.hidden {
transform: translateY(-100%);
}
.close {
width: 28px; height: 28px;
margin-left: 20px;
@ -84,10 +88,6 @@ body {
opacity: 1;
}
.error.hidden {
transform: translateY(-100%);
}
/* The grid of cells */
.canvas {
@ -204,6 +204,11 @@ body {
visibility: hidden;
}
.label.error {
font: 22px monospace;
color: hsl(0, 100%, 40%);
}
/* Edges */
.edge {

293
src/ui.js
View File

@ -24,6 +24,10 @@ DOM.Element = class {
return this.element.id;
}
get class_list() {
return this.element.classList;
}
/// Appends an element.
/// `value` has two forms: a plain string, in which case it is added as a text node, or a
/// `DOM.Element`, in which case the corresponding element is appended.
@ -1110,9 +1114,10 @@ class UI {
/// The panel for viewing and editing cell data.
this.panel = new Panel();
/// Whether to use MathJax for rendering labels.
/// This is `false` until MathJax loads.
this.use_MathJax = false;
/// What library to use for rendering labels.
/// `null` is a basic HTML fallback: it is used until the relevant library is loaded.
/// Options include MathJax and KaTeX.
this.render_method = null;
/// A debug mode for convenience. Adds default random labels to cells.
this.debug = false;
@ -1553,9 +1558,10 @@ class UI {
this.set_background(this.canvas.element, this.view);
}
/// Active MathJax when it becomes available, updating all existing labels make use of MathJax.
activate_MathJax() {
this.use_MathJax = true;
/// Active MathJax or KaTeX when it becomes available,
/// updating all existing labels to make use of the library.
activate_render_method(method) {
this.render_method = method;
// Rerender all the existing labels now that MathJax is available.
for (const cell of this.quiver.all_cells()) {
@ -1704,25 +1710,45 @@ class UI {
/// Renders TeX with MathJax and returns the corresponding element.
render_tex(tex = "", after = x => x) {
const label = new DOM.Element("div", { class: "label" }, { display: "none", opacity: 0 })
.add(this.use_MathJax ? `\\(${tex}\\)` : tex)
.element;
// We're going to fade the label in once it's rendered, so it looks less janky.
const reveal = () => {
label.style.display = "block";
label.style.opacity = 1;
};
if (this.use_MathJax) {
MathJax.Hub.queue.Push(
["Typeset", MathJax.Hub, label],
reveal,
after,
);
} else {
reveal();
// Simulate the usual queue delay.
setTimeout(() => after(), 0);
const label = new DOM.Element("div", { class: "label" });
switch (this.render_method) {
case null:
label.add(tex);
// Simulate the usual queue delay.
setTimeout(() => after(), 0);
break;
case "MathJax":
label.add(`\\(${tex}\\)`);
// We're going to fade the label in once it's rendered, so it looks less janky.
label.element.style.display = "none";
label.element.style.opacity = 0;
MathJax.Hub.queue.Push(
["Typeset", MathJax.Hub, label.element],
() => {
label.element.style.display = "block";
label.element.style.opacity = 1;
},
after,
);
break;
case "KaTeX":
try {
katex.render(tex, label.element);
} catch (_) {
label.class_list.add("error");
label.add(tex);
}
// Simulate the usual queue delay.
setTimeout(() => after(), 0);
break;
}
return label;
}
@ -2478,38 +2504,58 @@ class Panel {
this.element.appendChild(options_list);
}
/// We buffer the MathJax rendering to reduce flickering.
/// We buffer the MathJax rendering to reduce flickering (KaTeX is fast enough not
/// to require buffering).
/// If the `.buffer` has no extra classes, then we are free to start a new MathJax
/// TeX render.
/// If the `.buffer` has a `.buffering` class, then we are rendering a label. This
/// may be out of date, in which case we add a `.pending` class (which means we're
/// going to rerender as soon as the current MathJax render has completed).
render_tex(ui, cell) {
const label = cell.element.querySelector(".label:not(.buffer)");
if (ui.use_MathJax) {
const buffer = cell.element.querySelector(".buffer");
const jax = MathJax.Hub.getAllJax(buffer);
if (!buffer.classList.contains("buffering") && jax.length > 0) {
buffer.classList.add("buffering");
MathJax.Hub.Queue(
["Text", jax[0], cell.label],
() => {
// Swap the label and the label buffer.
label.classList.add("buffer");
buffer.classList.remove("buffer", "buffering");
},
() => {
if (cell.is_edge()) {
cell.update_label_transformation();
}
},
);
} else if (!buffer.classList.contains("pending")) {
MathJax.Hub.Queue(() => this.render_tex(ui, cell));
const label = new DOM.Element(cell.element.querySelector(".label:not(.buffer)"));
label.class_list.remove("error");
const update_label_transformation = () =>{
if (cell.is_edge()) {
cell.update_label_transformation();
}
} else {
new DOM.Element(label).clear();
label.appendChild(document.createTextNode(cell.label));
};
switch (ui.render_method) {
case null:
label.clear().add(cell.label);
update_label_transformation();
break;
case "MathJax":
const buffer = cell.element.querySelector(".buffer");
const jax = MathJax.Hub.getAllJax(buffer);
if (!buffer.classList.contains("buffering") && jax.length > 0) {
buffer.classList.add("buffering");
MathJax.Hub.Queue(
["Text", jax[0], cell.label],
() => {
// Swap the label and the label buffer.
label.class_list.add("buffer");
buffer.classList.remove("buffer", "buffering");
},
update_label_transformation,
);
} else if (!buffer.classList.contains("pending")) {
MathJax.Hub.Queue(() => this.render_tex(ui, cell));
}
break;
case "KaTeX":
label.clear();
try {
katex.render(cell.label, label.element);
} catch (_) {
label.add(cell.label);
label.class_list.add("error");
}
update_label_transformation();
break;
}
};
@ -2882,9 +2928,11 @@ class Vertex extends Cell {
// Remove any existing content.
content.clear();
// Create the label.
content.add(new DOM.Element(ui.render_tex(this.label), { class: "label" }));
content.add(ui.render_tex(this.label));
// Create an empty label buffer for flicker-free rendering.
content.add(new DOM.Element(ui.render_tex(), { class: "label buffer" }));
const buffer = ui.render_tex(this.label);
buffer.class_list.add("buffer");
content.add(buffer);
}
}
@ -3020,11 +3068,11 @@ class Edge extends Cell {
}
// Create the edge label.
const label = ui.render_tex(this.label, () => this.update_label_transformation());
this.element.appendChild(label);
this.element.appendChild(label.element);
// Create an empty label buffer for flicker-free rendering.
const buffer = ui.render_tex();
buffer.classList.add("buffer");
this.element.appendChild(buffer);
buffer.class_list.add("buffer");
this.element.appendChild(buffer.element);
}
/// Draw an edge on an existing SVG and positions it with respect to a parent `element`.
@ -3538,26 +3586,8 @@ Edge.SVG_PADDING = 6;
// How much space to leave between adjacent parallel arrows.
Edge.OFFSET_DISTANCE = 8;
// Initialise MathJax.
window.MathJax = {
jax: ["input/TeX", "output/SVG"],
extensions: ["tex2jax.js", "TeX/noErrors.js"],
messageStyle: "none",
skipStartupTypeset: true,
positionToHash: false,
showMathMenu: false,
showMathMenuMSIE: false,
TeX: {
noErrors: {
multiLine: false,
style: {
color: "hsl(0, 100%, 40%)",
font: "18px monospace",
border: "none",
},
}
},
};
// Which library to use for rendering labels.
const RENDER_METHOD = "KaTeX";
// We want until the (minimal) DOM content has loaded, so we have access to `document.body`.
document.addEventListener("DOMContentLoaded", () => {
@ -3569,8 +3599,8 @@ document.addEventListener("DOMContentLoaded", () => {
const display_error = (message) => {
// If there's already an error, it's not unlikely that subsequent errors will be triggered.
// Thus, we don't display an error banner if one is already displayed.
if (document.body.querySelector(".error") === null) {
const error = new DOM.Element("div", { class: "error hidden" })
if (document.body.querySelector(".error-banner") === null) {
const error = new DOM.Element("div", { class: "error-banner hidden" })
.add(message)
.add(
new DOM.Element("button", { class: "close" })
@ -3587,37 +3617,90 @@ document.addEventListener("DOMContentLoaded", () => {
}
};
// Get the query string (i.e. the part of the URL after the "?").
const query_string = window.location.href.match(/\?(.*)$/);
if (query_string !== null) {
// If there is a query string, try to decode it as a diagram.
try {
QuiverImportExport.base64.import(ui, query_string[1]);
} catch (error) {
if (ui.quiver.is_empty()) {
display_error("The saved diagram was malformed and could not be loaded.");
} else {
// The importer will try to recover from errors, so we may have been mostly
// successful.
display_error(
"The saved diagram was malformed and may have been loaded incorrectly."
);
const load_quiver_from_query_string = () => {
// Get the query string (i.e. the part of the URL after the "?").
const query_string = window.location.href.match(/\?(.*)$/);
if (query_string !== null) {
// If there is a query string, try to decode it as a diagram.
try {
QuiverImportExport.base64.import(ui, query_string[1]);
} catch (error) {
if (ui.quiver.is_empty()) {
display_error("The saved diagram was malformed and could not be loaded.");
} else {
// The importer will try to recover from errors, so we may have been mostly
// successful.
display_error(
"The saved diagram was malformed and may have been loaded incorrectly."
);
}
// Rethrow the error so that it can be reported.
throw error;
}
// Rethrow the error so that it can be reported.
throw error;
}
};
// Immediately load the rendering library.
if (RENDER_METHOD !== null) {
// All non-`null` rendering libraries add some script.
const rendering_library = new DOM.Element("script", {
type: "text/javascript",
src: {
MathJax: "MathJax/MathJax.js",
KaTeX: "KaTeX/dist/katex.js",
}[RENDER_METHOD],
}).listen("load", () => {
ui.activate_render_method(RENDER_METHOD);
// We delay loading the quiver when using KaTeX (see comment below),
// so as soon as the library is loaded, we want to load the quiver.
if (RENDER_METHOD === "KaTeX") {
load_quiver_from_query_string();
}
}).listen("error", () => {
// Handle MathJax or KaTeX not loading (somewhat) gracefully.
display_error(`${RENDER_METHOD} failed to load.`)
});
// Specific, per-library behaviour.
switch (RENDER_METHOD) {
case "MathJax":
window.MathJax = {
jax: ["input/TeX", "output/SVG"],
extensions: ["tex2jax.js", "TeX/noErrors.js"],
messageStyle: "none",
skipStartupTypeset: true,
positionToHash: false,
showMathMenu: false,
showMathMenuMSIE: false,
TeX: {
noErrors: {
multiLine: false,
style: {
border: "none",
font: "20px monospace",
color: "hsl(0, 100%, 40%)",
},
}
},
};
break;
case "KaTeX":
document.head.appendChild(new DOM.Element("link", {
rel: "stylesheet",
href: "KaTeX/dist/katex.css",
}).element);
break;
}
// Trigger the script load.
document.head.appendChild(rendering_library.element);
}
// Handle MathJax not loading (somewhat) gracefully.
new DOM.Element(document.querySelector("#MathJax"))
.listen("load", () => {
ui.activate_MathJax();
})
.listen("error", () => {
const error = new DOM.Element("div", { class: "error hidden" })
.add("MathJax failed to load.")
.element;
document.body.appendChild(error);
setTimeout(() => error.classList.remove("hidden"), 0);
});
// KaTeX is special in that it's fast enough to be worth waiting for, but not
// immediately available. In this case, we delay loading the quiver until the
// library has loaded.
if (RENDER_METHOD !== "KaTeX") {
load_quiver_from_query_string();
}
});