Skip to content
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
24 changes: 17 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"@babel/runtime": "^7.26.10",
"complex.js": "^2.2.5",
"decimal.js": "^10.4.3",
"escape-latex": "^1.2.0",
"fraction.js": "^5.2.1",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
Expand Down
61 changes: 61 additions & 0 deletions src/utils/escapeLatex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Inline ESM replacement for the 'escape-latex' package (v1.2.0)
// Original: https://github.com/dangmai/escape-latex (MIT license)

// Map the characters to escape to their escaped values. The list is derived
// from http://www.cespedes.org/blog/85/how-to-escape-latex-special-characters
const defaultEscapes = {
'{': '\\{',
'}': '\\}',
'\\': '\\textbackslash{}',
'#': '\\#',
$: '\\$',
'%': '\\%',
'&': '\\&',
'^': '\\textasciicircum{}',
_: '\\_',
'~': '\\textasciitilde{}'
}

const formatEscapes = {
'\u2013': '\\--',
'\u2014': '\\---',
' ': '~',
'\t': '\\qquad{}',
'\r\n': '\\newline{}',
'\n': '\\newline{}'
}

/**
* Escape a string to be used in LaTeX documents.
* @param {string} str the string to be escaped.
* @param {boolean} params.preserveFormatting whether formatting escapes should
* be performed (default: false).
* @return {string} the escaped string, ready to be used in LaTeX.
*/
export function escapeLatex (str, { preserveFormatting = false } = {}) {
const s = String(str)
const escapes = preserveFormatting
? { ...defaultEscapes, ...formatEscapes }
: defaultEscapes
const escapeKeys = Object.keys(escapes)
const parts = []
let i = 0

while (i < s.length) {
let matched = false
for (let k = 0; k < escapeKeys.length; k++) {
const key = escapeKeys[k]
if (s.startsWith(key, i)) {
parts.push(escapes[key])
i += key.length
matched = true
break
}
}
if (!matched) {
parts.push(s[i++])
}
}

return parts.join('')
}
2 changes: 1 addition & 1 deletion src/utils/latex.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint no-template-curly-in-string: "off" */

import escapeLatexLib from 'escape-latex'
import { escapeLatex as escapeLatexLib } from './escapeLatex.js'
import { hasOwnProperty } from './object.js'

export const latexSymbols = {
Expand Down
159 changes: 159 additions & 0 deletions test/unit-tests/utils/escapeLatex.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import assert from 'assert'
import { escapeLatex } from '../../../src/utils/escapeLatex.js'

describe('escapeLatex', function () {
it('should escape empty string correctly', function () {
assert.strictEqual(escapeLatex(''), '')
})

it('should escape casted string correctly', function () {
assert.strictEqual(escapeLatex(1), '1')
})

it('should escape # correctly', function () {
assert.strictEqual(
escapeLatex('Hashtag #yolo is all the rage these days #twitter'),
'Hashtag \\#yolo is all the rage these days \\#twitter'
)
})

it('should escape $ correctly', function () {
assert.strictEqual(
escapeLatex('$2 is greater than $1'),
'\\$2 is greater than \\$1'
)
})

it('should escape % correctly', function () {
assert.strictEqual(
escapeLatex('100% is 20% point greater than 80%'),
'100\\% is 20\\% point greater than 80\\%'
)
})

it('should escape & correctly', function () {
assert.strictEqual(
escapeLatex('Me & you & a dog named Boo'),
'Me \\& you \\& a dog named Boo'
)
})

it('should escape backslash correctly', function () {
assert.strictEqual(
escapeLatex('C:\\ is a good place to format'),
'C:\\textbackslash{} is a good place to format'
)
})

it('should escape { correctly', function () {
assert.strictEqual(
escapeLatex('This { does not have an matching bracket'),
'This \\{ does not have an matching bracket'
)
})

it('should escape } correctly', function () {
assert.strictEqual(
escapeLatex('There is no opening bracket for this }'),
'There is no opening bracket for this \\}'
)
})

it('should escape ^ correctly', function () {
assert.strictEqual(
escapeLatex('2^2^2^2 = 256'),
'2\\textasciicircum{}2\\textasciicircum{}2\\textasciicircum{}2 = 256'
)
})

it('should escape _ correctly', function () {
assert.strictEqual(
escapeLatex('_ is a shortcut to Underscore, e.g., _.each()'),
'\\_ is a shortcut to Underscore, e.g., \\_.each()'
)
})

it('should escape ~ correctly', function () {
assert.strictEqual(
escapeLatex('pi ~ 3.1416'),
'pi \\textasciitilde{} 3.1416'
)
})

it('should escape *nix newline correctly', function () {
assert.strictEqual(
escapeLatex('\n\n', { preserveFormatting: true }),
'\\newline{}\\newline{}'
)
})

it('should escape Windows newline correctly', function () {
assert.strictEqual(
escapeLatex('\r\n\r\n', { preserveFormatting: true }),
'\\newline{}\\newline{}'
)
})

it('should escape mixed newlines correctly', function () {
assert.strictEqual(
escapeLatex('\r\n\n\n\r\n', { preserveFormatting: true }),
'\\newline{}\\newline{}\\newline{}\\newline{}'
)
})

it('should escape \u2013 (en-dash) correctly', function () {
assert.strictEqual(
escapeLatex('\u2013', { preserveFormatting: true }),
'\\--'
)
})

it('should escape \u2014 (em-dash) correctly', function () {
assert.strictEqual(
escapeLatex('\u2014', { preserveFormatting: true }),
'\\---'
)
})

it('should escape spaces correctly', function () {
assert.strictEqual(
escapeLatex('Look ma, multiple spaces', { preserveFormatting: true }),
'Look~ma,~~multiple~spaces'
)
})

it('should escape tabs correctly', function () {
assert.strictEqual(
escapeLatex('\t\t', { preserveFormatting: true }),
'\\qquad{}\\qquad{}'
)
})

it('should not preserve formatting by default', function () {
assert.strictEqual(
escapeLatex('en dash \u2013 is cool'),
'en dash \u2013 is cool'
)
})

it('should not escape - (hyphen)', function () {
assert.strictEqual(
escapeLatex('hyphen - is the best'),
'hyphen - is the best'
)
})

it('stack overflow test', function () {
const numChars = 100000
const originalStr = Array(numChars).join('\\')
const escapedStr = Array(numChars).join('\\textbackslash{}')
assert.strictEqual(escapeLatex(originalStr), escapedStr)
})

it('composite test 1', function () {
assert.strictEqual(
escapeLatex('These {} should be escaped, as well as this \\ character'),
'These \\{\\} should be escaped, as well as this \\textbackslash{} character'
)
})
})