Skip to content

WIP: FEATURE: custom inline and block math delimiters #19

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
183 changes: 117 additions & 66 deletions assets/javascripts/lib/discourse-markdown/discourse-math.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -3,103 +3,98 @@
//
//

function isSafeBoundary(character_code, delimiter_code, md) {
if (character_code === delimiter_code) {
return false;
}

if (md.utils.isWhiteSpace(character_code)) {
return true;
}
function isSafeBoundary(character, delimiter) {

if (md.utils.isMdAsciiPunct(character_code)) {
return true;
let characterCode = character.charCodeAt(0);
// 0-9
if (characterCode > 47 && characterCode < 58) {
return false;
}

if (md.utils.isPunctChar(character_code)) {
return true;
// Need to distinguish $ from $$
if (delimiter.length == 1 && delimiter.charCodeAt(0) === characterCode) {
return false;
}

return false;
return true;
}

function math_input(state, silent, delimiter_code) {
let inlineMath = (startDelimiter, endDelimiter) => (state, silent) => {
let pos = state.pos,
posMax = state.posMax;

if (
silent ||
state.src.charCodeAt(pos) !== delimiter_code ||
posMax < pos + 2
!state.src.startsWith(startDelimiter, pos) ||
posMax < pos + startDelimiter.length + endDelimiter.length
) {
return false;
}

// too short
if (state.src.charCodeAt(pos + 1) === delimiter_code) {
if (state.src.startsWith(endDelimiter, pos + startDelimiter.length)) {
return false;
}

if (pos > 0) {
let prev = state.src.charCodeAt(pos - 1);
if (!isSafeBoundary(prev, delimiter_code, state.md)) {
let prev = state.src[pos - 1];
if (!isSafeBoundary(prev, startDelimiter)) {
return false;
}
}

let found;
for (let i = pos + 1; i < posMax; i++) {
let code = state.src.charCodeAt(i);
if (code === delimiter_code && state.src.charCodeAt(i - 1) !== 92 /* \ */) {
found = i;
break;
if (endDelimiter.length === 1) {
// Faster iterations, comparing numbers instead of characters
// and respecting character escaping with `\`
let endDelimCode = endDelimiter.charCodeAt(0);
for (let i = pos + 1; i < posMax; i++) {
let code = state.src.charCodeAt(i);
if (code === endDelimCode && state.src.charCodeAt(i - 1) !== 92 /* \ */) {
found = i;
break;
}
}
} else {
for (let i = pos + 1; i <= posMax - endDelimiter.length; i++) {
// we do not respect escaping here because we need to allow for
// \(...\) TeX inline delimiters
if (state.src.startsWith(endDelimiter, i)) {
found = i;
break;
}
}
}

if (!found) {
return false;
}

if (found + 1 <= posMax) {
let next = state.src.charCodeAt(found + 1);
if (next && !isSafeBoundary(next, delimiter_code, state.md)) {
if (found + endDelimiter.length <= posMax) {
let next = state.src[found + endDelimiter.length];
if (next && !isSafeBoundary(next, endDelimiter)) {
return false;
}
}

let data = state.src.slice(pos + 1, found);
let data = state.src.slice(pos + startDelimiter.length, found);
let token = state.push("html_raw", "", 0);

const escaped = state.md.utils.escapeHtml(data);
let math_class = delimiter_code === 36 ? "'math'" : "'asciimath'";
let math_class = startDelimiter === endDelimiter === '%' ? "'asciimath'" : "'math'";
token.content = `<span class=${math_class}>${escaped}</span>`;
state.pos = found + 1;
state.pos = found + endDelimiter.length;
return true;
}

function inlineMath(state, silent) {
return math_input(state, silent, 36 /* $ */);
}

function asciiMath(state, silent) {
return math_input(state, silent, 37 /* % */);
}

function isBlockMarker(state, start, max, md) {
if (state.src.charCodeAt(start) !== 36 /* $ */) {
return false;
}
function isBlockMarker(state, start, max, md, blockMarker) {

start++;

if (state.src.charCodeAt(start) !== 36 /* $ */) {
if (!state.src.startsWith(blockMarker, start)) {
return false;
}

start++;

// ensure we only have newlines after our $$
for (let i = start; i < max; i++) {

// ensure we only have spaces and newlines after block math marker
for (let i = start + blockMarker.length; i < max; i++) {
if (!md.utils.isSpace(state.src.charCodeAt(i))) {
return false;
}
Expand All @@ -108,11 +103,26 @@ function isBlockMarker(state, start, max, md) {
return true;
}

function blockMath(state, startLine, endLine, silent) {
let blockMath = (startBlockMathMarker, endBlockMathMarker) => (state, startLine, endLine, silent) => {
let start = state.bMarks[startLine] + state.tShift[startLine],
max = state.eMarks[startLine];

if (!isBlockMarker(state, start, max, state.md)) {
let startBlockMarker = startBlockMathMarker;
let endBlockMarker = endBlockMathMarker;

// Special processing for /\begin{([a-z]+)}/
if (startBlockMarker instanceof RegExp) {
let substr = state.src.substring(start, max);
let match = substr.match(startBlockMarker);
if(!match) {
return false;
}
let mathEnv = match[1];
startBlockMarker = `\\begin{${mathEnv}}`;
endBlockMarker = `\\end{${mathEnv}}`;
}

if (!isBlockMarker(state, start, max, state.md, startBlockMarker)) {
return false;
}

Expand All @@ -125,7 +135,7 @@ function blockMath(state, startLine, endLine, silent) {
for (;;) {
nextLine++;

// unclosed $$ is considered math
// Unclosed blockmarker is considered math
if (nextLine >= endLine) {
break;
}
Expand All @@ -135,7 +145,8 @@ function blockMath(state, startLine, endLine, silent) {
state,
state.bMarks[nextLine] + state.tShift[nextLine],
state.eMarks[nextLine],
state.md
state.md,
endBlockMarker
)
) {
closed = true;
Expand All @@ -145,11 +156,15 @@ function blockMath(state, startLine, endLine, silent) {

let token = state.push("html_raw", "", 0);

let endContent = closed ? state.eMarks[nextLine - 1] : state.eMarks[nextLine];
let content = state.src.slice(
state.bMarks[startLine + 1] + state.tShift[startLine + 1],
endContent
);
// Math environment blockmarkers '\begin{}' and '\end{}'
// needs to be passed to the TeX engine
let endContent = endBlockMarker.startsWith('\\end{') || !closed ?
state.eMarks[nextLine] : state.eMarks[nextLine - 1];

let startContent = startBlockMarker.startsWith('\\begin{') ?
state.bMarks[startLine] : state.bMarks[startLine + 1] + state.tShift[startLine + 1];

let content = state.src.slice(startContent, endContent);

const escaped = state.md.utils.escapeHtml(content);
token.content = `<div class='math'>\n${escaped}\n</div>\n`;
Expand All @@ -164,19 +179,55 @@ export function setup(helper) {
return;
}

let enable_asciimath;
let enableAsciiMath, enableMathEnvs;
let inlineDelimiters, blockDelimiters;
let texRenderer;
helper.registerOptions((opts, siteSettings) => {
opts.features.math = siteSettings.discourse_math_enabled;
enable_asciimath = siteSettings.discourse_math_enable_asciimath;
enableAsciiMath = siteSettings.discourse_math_enable_asciimath;
enableMathEnvs = siteSettings.discourse_math_process_tex_environments;
inlineDelimiters = siteSettings.discourse_math_inline_delimiters;
blockDelimiters = siteSettings.discourse_math_block_delimiters;
texRenderer = siteSettings.discourse_math_provider;
});

helper.registerPlugin(md => {
if (enable_asciimath) {
md.inline.ruler.after("escape", "asciimath", asciiMath);
let mathjax = texRenderer === 'mathjax';
if (enableAsciiMath && mathjax) {
md.inline.ruler.after("escape", "asciimath", inlineMath('%', '%'));
}
md.inline.ruler.after("escape", "math", inlineMath);
md.block.ruler.after("code", "math", blockMath, {
alt: ["paragraph", "reference", "blockquote", "list"]

if (enableMathEnvs && mathjax) {
md.block.ruler.after("code", "math",
blockMath(/\\begin\{([a-z]+)\}/, /\\end\{([a-z]+)\}/), {
alt: ["paragraph", "reference", "blockquote", "list"]
});
}
// Helper function for checking input
const isEmptyStr = elem => elem.trim() === '';

inlineDelimiters.split('|').forEach(d => {
let delims = d.split(',');
if (delims.length !== 2 || delims.some(isEmptyStr)) {
console.error('Invalid input in discourse_math_inline_delimiters!');
return;
}
let startDelim = delims[0].trim();
let endDelim = delims[1].trim();
md.inline.ruler.before("escape", "math", inlineMath(startDelim, endDelim));
});

blockDelimiters.split('|').forEach(d => {
let delims = d.split(',');
if (delims.length !== 2 || delims.some(isEmptyStr)) {
console.error('Invalid input in discourse_math_block_delimiters!');
return;
}
let startDelim = delims[0].trim();
let endDelim = delims[1].trim();
md.block.ruler.after("code", "math", blockMath(startDelim, endDelim), {
alt: ["paragraph", "reference", "blockquote", "list"]
});
});
});
}
3 changes: 3 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ en:
discourse_math_zoom_on_hover: 'Zoom 200% on hover (MathJax only)'
discourse_math_enable_accessibility: 'Enable accessibility features (MathJax only)'
discourse_math_enable_asciimath: 'Enable asciimath (will add special processing to % delimited input) (MathJax only)'
discourse_math_inline_delimiters: 'You can have multiple delimiters for inline math'
discourse_math_block_delimiters: 'You can have multiple delimiters for block math. Delimiters starting with "\begin{}" will be passed to the LaTeX engine and should to end with the corresponding \end{} command'
discourse_math_process_tex_environments: 'Enable processing of "\begin{*}...\end{*}" environments (MathJax only)'
11 changes: 11 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ plugins:
choices:
- mathjax
- katex
discourse_math_inline_delimiters:
default: '$,$'
client: true
type: list
discourse_math_block_delimiters:
default: '$$,$$'
client: true
type: list
discourse_math_process_tex_environments:
default: false
client: true
discourse_math_zoom_on_hover:
default: false
client: true
Expand Down
2 changes: 1 addition & 1 deletion plugin.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# name: discourse-math
# about: Official mathjax plugin for Discourse
# about: Official LaTeX plugin for Discourse
# version: 0.9
# authors: Sam Saffron (sam)
# url: https://github.com/discourse/discourse-math
Expand Down