Skip to content

Add Katex Equation rendering plugin to Markdown Editor #19988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"jquery-mousewheel": "^3.1.13",
"jquery-ui": "^1.13.2",
"jspdf": "^2.5.1",
"katex": "^0.16.22",
"linkify-html": "^4.1.1",
"linkifyjs": "^4.1.1",
"lodash.isequal": "^4.5.0",
Expand Down
10 changes: 10 additions & 0 deletions client/src/components/Markdown/Editor/templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ Markdown:
- First item
- Second item
- Third item
- title: "Equation Inline"
description: "Mathematics using KaTeX"
cell:
name: "markdown"
content: $c = \sqrt{a^2 + b^2}$
- title: "Equation Block"
description: "Mathematics using KaTeX"
cell:
name: "markdown"
content: $$c = \sqrt{a^2 + b^2}$$

Galaxy:
- title: "Collection"
Expand Down
4 changes: 4 additions & 0 deletions client/src/components/Markdown/Sections/MarkdownDefault.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import MarkdownIt from "markdown-it";
import markdownItRegexp from "markdown-it-regexp";
import { computed } from "vue";

//@ts-ignore
import markdownItKatex from "./Plugins/markdown-it-katex";

const mdNewline = markdownItRegexp(/<br>/, () => {
return "<div style='clear:both;'/><br>";
});

const md = MarkdownIt();
md.use(mdNewline);
md.use(markdownItKatex, { throwOnError: false });

const props = defineProps<{
content: string;
Expand Down
226 changes: 226 additions & 0 deletions client/src/components/Markdown/Sections/Plugins/markdown-it-katex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
Like markdown-it-simplemath, this is a stripped down, simplified version of:
https://github.com/runarberg/markdown-it-math

It differs in that it takes (a subset of) LaTeX as input and relies on KaTeX
for rendering output.
*/

import "katex/dist/katex.min.css";

import katex from "katex";

// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim(state, pos) {
const max = state.posMax;
const nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;
const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;

let can_close = true;
let can_open = true;

// Check non-whitespace conditions for opening and closing, and
// check that closing delimeter isn't followed by a number
if (
prevChar === 0x20 /* " " */ ||
prevChar === 0x09 /* \t */ ||
(nextChar >= 0x30 /* "0" */ && nextChar <= 0x39) /* "9" */
) {
can_close = false;
}
if (nextChar === 0x20 /* " " */ || nextChar === 0x09 /* \t */) {
can_open = false;
}

return {
can_open: can_open,
can_close: can_close,
};
}

function math_inline(state, silent) {
let match;
let res;
let pos;
let esc_count;

if (state.src[state.pos] !== "$") {
return false;
}

res = isValidDelim(state, state.pos);
if (!res.can_open) {
if (!silent) {
state.pending += "$";
}
state.pos += 1;
return true;
}

// First check for and bypass all properly escaped delimieters
// This loop will assume that the first leading backtick can not
// be the first character in state.src, which is known since
// we have found an opening delimieter already.
const start = state.pos + 1;
match = start;
while ((match = state.src.indexOf("$", match)) !== -1) {
// Found potential $, look for escapes, pos will point to
// first non escape when complete
pos = match - 1;
while (state.src[pos] === "\\") {
pos -= 1;
}

// Even number of escapes, potential closing delimiter found
if ((match - pos) % 2 == 1) {
break;
}
match += 1;
}

// No closing delimter found. Consume $ and continue.
if (match === -1) {
if (!silent) {
state.pending += "$";
}
state.pos = start;
return true;
}

// Check if we have empty content, ie: $$. Do not parse.
if (match - start === 0) {
if (!silent) {
state.pending += "$$";
}
state.pos = start + 1;
return true;
}

// Check for valid closing delimiter
res = isValidDelim(state, match);
if (!res.can_close) {
if (!silent) {
state.pending += "$";
}
state.pos = start;
return true;
}

if (!silent) {
const token = state.push("math_inline", "math", 0);
token.markup = "$";
token.content = state.src.slice(start, match);
}

state.pos = match + 1;
return true;
}

function math_block(state, start, end, silent) {
let firstLine;
let lastLine;
let next;
let lastPos;
let found = false;
let pos = state.bMarks[start] + state.tShift[start];
let max = state.eMarks[start];

if (pos + 2 > max) {
return false;
}
if (state.src.slice(pos, pos + 2) !== "$$") {
return false;
}

pos += 2;
firstLine = state.src.slice(pos, max);

if (silent) {
return true;
}
if (firstLine.trim().slice(-2) === "$$") {
// Single line expression
firstLine = firstLine.trim().slice(0, -2);
found = true;
}

for (next = start; !found; ) {
next++;

if (next >= end) {
break;
}

pos = state.bMarks[next] + state.tShift[next];
max = state.eMarks[next];

if (pos < max && state.tShift[next] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
break;
}

if (state.src.slice(pos, max).trim().slice(-2) === "$$") {
lastPos = state.src.slice(0, max).lastIndexOf("$$");
lastLine = state.src.slice(pos, lastPos);
found = true;
}
}

state.line = next + 1;

const token = state.push("math_block", "math", 0);
token.block = true;
token.content =
(firstLine && firstLine.trim() ? firstLine + "\n" : "") +
state.getLines(start + 1, next, state.tShift[start], true) +
(lastLine && lastLine.trim() ? lastLine : "");
token.map = [start, state.line];
token.markup = "$$";
return true;
}

export default function (md, options) {
// Default options
options = options || {};

// set KaTeX as the renderer for markdown-it-simplemath
const katexInline = function (latex) {
options.displayMode = false;
try {
return katex.renderToString(latex, options);
} catch (error) {
if (options.throwOnError) {
console.log(error);
}
return latex;
}
};

const inlineRenderer = function (tokens, idx) {
return katexInline(tokens[idx].content);
};

const katexBlock = function (latex) {
options.displayMode = true;
try {
return "<p>" + katex.renderToString(latex, options) + "</p>";
} catch (error) {
if (options.throwOnError) {
console.log(error);
}
return latex;
}
};

const blockRenderer = function (tokens, idx) {
return katexBlock(tokens[idx].content) + "\n";
};

md.inline.ruler.after("escape", "math_inline", math_inline);
md.block.ruler.after("blockquote", "math_block", math_block, {
alt: ["paragraph", "reference", "blockquote", "list"],
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
}
2 changes: 1 addition & 1 deletion client/src/components/PageDisplay/pageTemplate.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
content: |
#### Your Page Title

Welcome to Galaxy Pages — a *Markdown* space where you can add text, Galaxy components like Tables, Datasets, and Visualizations to create rich, Interactive Pages.
Welcome to Galaxy Pages — a *Markdown* space where you can add text, mathematical equations, Galaxy components like Tables, Datasets, and Visualizations to create rich, Interactive Pages.
12 changes: 12 additions & 0 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4463,6 +4463,11 @@ commander@^12.0.0, commander@^12.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==

commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==

compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz"
Expand Down Expand Up @@ -8065,6 +8070,13 @@ karma-safari-launcher@^1.0.0:
resolved "https://registry.yarnpkg.com/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz#96982a2cc47d066aae71c553babb28319115a2ce"
integrity sha512-qmypLWd6F2qrDJfAETvXDfxHvKDk+nyIjpH9xIeI3/hENr0U3nuqkxaftq73PfXZ4aOuOChA6SnLW4m4AxfRjQ==

katex@^0.16.22:
version "0.16.22"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.22.tgz#d2b3d66464b1e6d69e6463b28a86ced5a02c5ccd"
integrity sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==
dependencies:
commander "^8.3.0"

kind-of@^3.0.2:
version "3.2.2"
resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz"
Expand Down
Loading