Files
overleaf-cep/services/web/frontend/js/features/bibtex/latex-to-unicode.mts
T
Maria Florencia Besteiro Gonzalez 4132731016 Merge pull request #35680 from overleaf/mfb-migrate-bibtex-parser-to-core-feature
Migrate BibTeX parser to core frontend feature

GitOrigin-RevId: 274f64d31c1027c5eea9ec569e54dece57307220
2026-09-17 11:36:26 +00:00

877 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export function latexToUnicode(value: string) {
let s = value
// First, replace the LaTeX commands we know about that take no argument
//
// A LaTeX command is one of:
//
// - a backslash followed by letters (\textbf) and optional spaces
// - a backslash followed by a single non-letter (\^) and optional spaces
s = s.replaceAll(/\\([a-zA-Z]+|[^a-zA-Z])\s*/g, noArgsReplacer)
// Next, replace the LaTeX commands we know about that take one argument
//
// This first regex identifies arguments that are not inside braces. These
// can be:
//
// - a single character (\'e)
// - another LaTeX command (\^\alpha) - we don't support args for that command
s = s.replaceAll(
/\\([a-zA-Z]+|[^a-zA-Z])\s*([^{\\]|\\[^a-zA-Z]|[a-zA-Z]+)/g,
singleArgReplacer
)
// This second regex identifies arguments inside braces. We support two levels
// of braces. For example:
//
// - \'{e}
// - \mkbibquote{\'{e}cole}
//
// Because we support two levels of nested braces, we also need to run this
// twice.
for (let i = 0; i < 2; i++) {
s = s.replaceAll(
/\\([a-zA-Z]+|[^a-zA-Z])\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/g,
singleArgReplacer
)
}
return s.normalize('NFC')
}
function noArgsReplacer(match: string, command: string) {
return NO_ARGS_REPLACEMENTS[command] ?? match
}
function singleArgReplacer(match: string, command: string, arg: string) {
if (command === 'mkbibquote') {
return `"${arg}"`
}
const replacement = SINGLE_ARG_REPLACEMENTS[command]?.[arg]
if (replacement != null) {
return replacement
}
if (arg.length === 1) {
const combiningChar = COMBINING_CHAR_REPLACEMENTS[command]
if (combiningChar != null) {
return arg + combiningChar
}
}
return match
}
/**
* List of replacements from bib2json (https://github.com/mayanklahiri/bib2json)
*
* Copyright 2012-13 Mayank Lahiri
* mlahiri@gmail.com
* Released under the BSD License.
*
* Modifications 2016 Sharelatex
* Modifications 2017-2020 Overleaf
*/
const NO_ARGS_REPLACEMENTS: Record<string, string> = {
aa: 'å',
AA: 'Å',
AE: 'Æ',
ae: 'æ',
aleph: 'ℵ',
allequal: '≌',
Alpha: 'Α',
alpha: 'α',
amalg: '⨿',
angle: '∠',
Angle: '⦜',
approxeq: '≊',
approxnotequal: '≆',
approx: '≈',
aquarius: '♒',
aries: '♈',
arrowwaveleft: '↜',
arrowwaveright: '↝',
ast: '*',
asymp: '≍',
backepsilon: '϶',
backprime: '‵',
backsimeq: '⋍',
backsim: '∽',
barwedge: '⌅',
because: '∵',
Beta: 'Β',
beta: 'β',
beth: 'ℶ',
between: '≬',
bigcap: '⋂',
bigcirc: '◯',
bigcup: '⋃',
bigtriangledown: '▽',
bigtriangleup: '△',
blacklozenge: '⧫',
blacksquare: '▪',
blacktriangledown: '▾',
blacktriangleleft: '◂',
blacktriangleright: '▸',
blacktriangle: '▴',
bowtie: '⋈',
boxdot: '⊡',
boxminus: '⊟',
boxplus: '⊞',
boxtimes: '⊠',
bullet: '∙',
Bumpeq: '≎',
bumpeq: '≏',
cancer: '♋',
capricornus: '♑',
cap: '∩',
Cap: '⋒',
cdots: '⋯',
cdot: '·',
Chi: 'Χ',
chi: 'χ',
circeq: '≗',
circlearrowleft: '↺',
circlearrowright: '↻',
circledast: '⊛',
circledcirc: '⊚',
circleddash: '⊝',
circledS: 'Ⓢ',
circ: '∘',
clockoint: '⨏',
clwintegral: '∱',
Colon: '∷',
complement: '∁',
cong: '≅',
coprod: '∐',
cup: '∪',
Cup: '⋓',
curlyeqprec: '⋞',
curlyeqsucc: '⋟',
curlyvee: '⋎',
curlywedge: '⋏',
curvearrowleft: '↶',
curvearrowright: '↷',
daleth: 'ℸ',
dashv: '⊣',
dblarrowupdown: '⇅',
ddddot: '⃜',
dddot: '⃛',
Delta: 'Δ',
delta: 'δ',
DH: 'Ð',
dh: 'ð',
diagup: '╱',
diamond: '⋄',
Digamma: 'Ϝ',
digamma: 'ϝ',
divideontimes: '⋇',
div: '÷',
DJ: 'Đ',
dj: 'đ',
doteqdot: '≑',
doteq: '≐',
dotplus: '∔',
DownArrowBar: '⤓',
downarrow: '↓',
Downarrow: '⇓',
DownArrowUpArrow: '⇵',
downdownarrows: '⇊',
downharpoonleft: '⇃',
downharpoonright: '⇂',
DownLeftRightVector: '⥐',
DownLeftTeeVector: '⥞',
DownLeftVectorBar: '⥖',
DownRightTeeVector: '⥟',
DownRightVectorBar: '⥗',
downslopeellipsis: '⋱',
eighthnote: '♪',
Elolarr: '⥀',
Elorarr: '⥁',
ElOr: '⩖',
Elroang: '⦆',
Elxsqcup: '⨆',
Elxuplus: '⨄',
ElzAnd: '⩓',
Elzbar: '̶',
Elzbtdl: 'ɬ',
ElzCint: '⨍',
Elzcirfb: '◒',
Elzcirfl: '◐',
Elzcirfr: '◑',
Elzclomeg: 'ɷ',
Elzddfnc: '⦙',
Elzdefas: '⧋',
Elzdlcorn: '⎣',
Elzdshfnc: '┆',
Elzdyogh: 'ʤ',
Elzesh: 'ʃ',
Elzfhr: 'ɾ',
Elzglst: 'ʔ',
Elzhlmrk: 'ˑ',
ElzInf: '⨇',
Elzinglst: 'ʖ',
Elzinvv: 'ʌ',
Elzinvw: 'ʍ',
ElzLap: '⧊',
Elzlmrk: 'ː',
Elzlow: '˕',
Elzlpargt: '⦠',
Elzltlmr: 'ɱ',
Elzltln: 'ɲ',
Elzminhat: '⩟',
Elzopeno: 'ɔ',
ElzOr: '⩔',
Elzpalh: '̡',
Elzpbgam: 'ɤ',
Elzpgamma: 'ɣ',
Elzpscrv: 'ʋ',
Elzpupsil: 'ʊ',
Elzrais: '˔',
Elzrarrx: '⥇',
Elzreapos: '‛',
Elzreglst: 'ʕ',
Elzrh: '̢',
ElzRlarr: '⥂',
ElzrLarr: '⥄',
Elzrl: 'ɼ',
Elzrtld: 'ɖ',
Elzrtll: 'ɭ',
Elzrtln: 'ɳ',
Elzrtlr: 'ɽ',
Elzrtls: 'ʂ',
Elzrtlt: 'ʈ',
Elzrtlz: 'ʐ',
Elzrttrnr: 'ɻ',
Elzrvbull: '◘',
Elzsbbrg: '̪',
Elzsblhr: '˓',
Elzsbrhr: '˒',
Elzschwa: 'ə',
Elzsqfl: '◧',
Elzsqfnw: '┙',
Elzsqfr: '◨',
Elzsqfse: '◪',
Elzsqspne: '⋥',
ElzSup: '⨈',
Elztdcol: '⫶',
Elztesh: 'ʧ',
Elztfnc: '⦀',
ElzThr: '⨅',
ElzTimes: '⨯',
Elztrna: 'ɐ',
Elztrnh: 'ɥ',
Elztrnmlr: 'ɰ',
Elztrnm: 'ɯ',
Elztrnrl: 'ɺ',
Elztrnr: 'ɹ',
Elztrnsa: 'ɒ',
Elztrnt: 'ʇ',
Elztrny: 'ʎ',
Elzverti: 'ˌ',
Elzverts: 'ˈ',
Elzvrecto: '▯',
Elzxh: 'ħ',
Elzxl: '̵',
Elzxrat: '℞',
Elzyogh: 'ʒ',
Epsilon: 'Ε',
epsilon: 'ε',
eqcirc: '≖',
eqslantgtr: '⪖',
eqslantless: '⪕',
Equal: '⩵',
equiv: '≡',
estimates: '≙',
Eta: 'Η',
eta: 'η',
eth: 'ƪ',
exists: '∃',
fallingdotseq: '≒',
flat: '♭',
forall: '∀',
forcesextra: '⊨',
frown: '⌢',
Gamma: 'Γ',
gamma: 'γ',
gemini: '♊',
geqq: '≧',
geqslant: '⩾',
geq: '≥',
gg: '≫',
gimel: 'ℷ',
gnapprox: '⪊',
gneqq: '≩',
gneq: '⪈',
gnsim: '⋧',
greaterequivlnt: '≳',
gtrapprox: '⪆',
gtrdot: '⋗',
gtreqless: '⋛',
gtreqqless: '⪌',
gtrless: '≷',
guillemotleft: '«',
guillemotright: '»',
guilsinglleft: '‹',
guilsinglright: '›',
hermitconjmatrix: '⊹',
homothetic: '∻',
hookleftarrow: '↩',
hookrightarrow: '↪',
hslash: 'ℏ',
image: '⊷',
infty: '∞',
intercal: '⊺',
int: '∫',
in: '∈',
Iota: 'Ι',
iota: 'ι',
i: 'ı',
jupiter: '♃',
Kappa: 'Κ',
kappa: 'κ',
Koppa: 'Ϟ',
Lambda: 'Λ',
lambda: 'λ',
langle: '〈',
lazysinv: '∾',
lbrace: '{',
lceil: '⌈',
ldots: '…',
leftarrowtail: '↢',
leftarrow: '←',
Leftarrow: '⇐',
LeftDownTeeVector: '⥡',
LeftDownVectorBar: '⥙',
leftharpoondown: '↽',
leftharpoonup: '↼',
leftleftarrows: '⇇',
leftrightarrows: '⇆',
leftrightarrow: '↔',
Leftrightarrow: '⇔',
leftrightharpoons: '⇋',
leftrightsquigarrow: '↭',
LeftRightVector: '⥎',
LeftTeeVector: '⥚',
leftthreetimes: '⋋',
LeftTriangleBar: '⧏',
LeftUpDownVector: '⥑',
LeftUpTeeVector: '⥠',
LeftUpVectorBar: '⥘',
LeftVectorBar: '⥒',
leo: '♌',
leqq: '≦',
leqslant: '⩽',
leq: '≤',
lessapprox: '⪅',
lessdot: '⋖',
lesseqgtr: '⋚',
lesseqqgtr: '⪋',
lessequivlnt: '≲',
lessgtr: '≶',
lfloor: '⌊',
libra: '♎',
llcorner: '⌞',
Lleftarrow: '⇚',
ll: '≪',
lmoustache: '⎰',
lnapprox: '⪉',
lneqq: '≨',
lneq: '⪇',
lnot: '¬',
lnsim: '⋦',
longleftarrow: '⟵',
Longleftarrow: '⟸',
longleftrightarrow: '⟷',
Longleftrightarrow: '⟺',
longmapsto: '⟼',
longrightarrow: '⟶',
Longrightarrow: '⟹',
looparrowleft: '↫',
looparrowright: '↬',
lozenge: '◊',
lrcorner: '⌟',
Lsh: '↰',
ltimes: '⋉',
L: 'Ł',
l: 'ł',
male: '♂',
mapsto: '↦',
measuredangle: '∡',
mercury: '☿',
mho: '℧',
mid: '∣',
mp: '∓',
multimap: '⊸',
mu: 'μ',
nabla: '∇',
natural: '♮',
nearrow: '↗',
neptune: '♆',
NestedGreaterGreater: '⪢',
NestedLessLess: '⪡',
nexists: '∄',
NG: 'Ŋ',
ng: 'ŋ',
ni: '∋',
nleftarrow: '↚',
nLeftarrow: '⇍',
nleftrightarrow: '↮',
nLeftrightarrow: '⇎',
nmid: '∤',
nolinebreak: '⁠',
notgreaterless: '≹',
notlessgreater: '≸',
nparallel: '∦',
nrightarrow: '↛',
nRightarrow: '⇏',
ntrianglelefteq: '⋬',
ntriangleleft: '⋪',
ntrianglerighteq: '⋭',
ntriangleright: '⋫',
nu: 'ν',
nvdash: '⊬',
nvDash: '⊭',
nVdash: '⊮',
nVDash: '⊯',
nwarrow: '↖',
odot: '⊙',
OE: 'Œ',
oe: 'œ',
oint: '∮',
Omega: 'Ω',
omega: 'ω',
ominus: '⊖',
openbracketleft: '〚',
openbracketright: '〛',
oplus: '⊕',
original: '⊶',
oslash: '⊘',
otimes: '⊗',
O: 'Ø',
o: 'ø',
parallel: '∥',
partial: '∂',
perp: '⊥',
perspcorrespond: '⩞',
Phi: 'Φ',
phi: 'ϕ',
pisces: '♓',
pitchfork: '⋔',
Pi: 'Π',
pi: 'π',
pluto: '♇',
pm: '±',
precapprox: '⪷',
preccurlyeq: '≼',
precedesnotsimilar: '⋨',
preceq: '⪯',
precnapprox: '⪹',
precneqq: '⪵',
prec: '≺',
prod: '∏',
propto: '∝',
Psi: 'Ψ',
psi: 'ψ',
quarternote: '♩',
rangle: '〉',
rbrace: '}',
rceil: '⌉',
recorder: '⌕',
ReverseUpEquilibrium: '⥯',
rfloor: '⌋',
Rho: 'Ρ',
rho: 'ρ',
rightanglearc: '⊾',
rightangle: '∟',
rightarrowtail: '↣',
rightarrow: '→',
Rightarrow: '⇒',
RightDownTeeVector: '⥝',
RightDownVectorBar: '⥕',
rightharpoondown: '⇁',
rightharpoonup: '⇀',
rightleftarrows: '⇄',
rightleftharpoons: '⇌',
rightmoon: '☾',
rightrightarrows: '⇉',
rightsquigarrow: '⇝',
RightTeeVector: '⥛',
rightthreetimes: '⋌',
RightTriangleBar: '⧐',
RightUpDownVector: '⥏',
RightUpTeeVector: '⥜',
RightUpVectorBar: '⥔',
RightVectorBar: '⥓',
risingdotseq: '≓',
rmoustache: '⎱',
RoundImplies: '⥰',
Rrightarrow: '⇛',
Rsh: '↱',
rtimes: '⋊',
RuleDelayed: '⧴',
sagittarius: '♐',
Sampi: 'Ϡ',
saturn: '♄',
scorpio: '♏',
searrow: '↘',
setminus: '∖',
sharp: '♯',
Sigma: 'Σ',
sigma: 'σ',
simeq: '≃',
sim: '∼',
smile: '⌣',
space: ' ',
sphericalangle: '∢',
sqcap: '⊓',
sqcup: '⊔',
sqrint: '⨖',
sqsubseteq: '⊑',
sqsubset: '⊏',
sqsupseteq: '⊒',
sqsupset: '⊐',
square: '□',
ss: 'ß',
starequal: '≛',
star: '⋆',
Stigma: 'Ϛ',
subseteqq: '⫅',
subseteq: '⊆',
subsetneqq: '⫋',
subsetneq: '⊊',
subset: '⊂',
Subset: '⋐',
succapprox: '≿',
succcurlyeq: '≽',
succeq: '⪰',
succnapprox: '⪺',
succneqq: '⪶',
succnsim: '⋩',
succ: '≻',
sum: '∑',
supseteqq: '⫆',
supseteq: '⊇',
supsetneqq: '⫌',
supsetneq: '⊋',
supset: '⊃',
Supset: '⋑',
surd: '√',
surfintegral: '∯',
swarrow: '↙',
taurus: '♉',
Tau: 'Τ',
tau: 'τ',
textasciiacute: '´',
textasciibreve: '˘',
textasciicaron: 'ˇ',
textasciidieresis: '¨',
textasciigrave: '`',
textasciimacron: '¯',
textasciitilde: '~',
textbackslash: '\\',
textbrokenbar: '¦',
textbullet: '•',
textcent: '¢',
textcopyright: '©',
textcurrency: '¤',
textdaggerdbl: '‡',
textdagger: '†',
textdegree: '°',
textdollar: '$',
textdoublepipe: 'ǂ',
textemdash: '—',
textendash: '–',
texteuro: '€',
textexclamdown: '¡',
texthvlig: 'ƕ',
textnrleg: 'ƞ',
textonehalf: '½',
textonequarter: '¼',
textordfeminine: 'ª',
textordmasculine: 'º',
textparagraph: '¶',
textperiodcentered: '˙',
textpertenthousand: '‱',
textperthousand: '‰',
textphi: 'ɸ',
textquestiondown: '¿',
textquotedblleft: '“',
textquotedblright: '”',
textquotesingle: "'",
textregistered: '®',
textsection: '§',
textsterling: '£',
texttheta: 'θ',
textTheta: 'ϴ',
textthreequarters: '¾',
texttildelow: '˜',
texttimes: '×',
texttrademark: '™',
textturnk: 'ʞ',
textvartheta: 'ϑ',
textvisiblespace: '␣',
textyen: '¥',
therefore: '∴',
Theta: 'Θ',
TH: 'Þ',
th: 'þ',
tildetrpl: '≋',
top: '⊤',
triangledown: '▿',
trianglelefteq: '⊴',
triangleleft: '◃',
triangleq: '≜',
trianglerighteq: '⊵',
triangleright: '▹',
truestate: '⊧',
twoheadleftarrow: '↞',
twoheadrightarrow: '↠',
ulcorner: '⌜',
UpArrowBar: '⤒',
uparrow: '↑',
Uparrow: '⇑',
updownarrow: '↕',
Updownarrow: '⇕',
UpEquilibrium: '⥮',
upharpoonleft: '↿',
upharpoonright: '↾',
uplus: '⊎',
upsilon: 'υ',
Upsilon: 'ϒ',
upslopeellipsis: '⋰',
upuparrows: '⇈',
uranus: '♅',
urcorner: '⌝',
varepsilon: 'ɛ',
varkappa: 'ϰ',
varnothing: '∅',
varphi: 'φ',
varpi: 'ϖ',
varrho: 'ϱ',
varsigma: 'ς',
vartriangleleft: '⊲',
vartriangleright: '⊳',
vartriangle: '▵',
vdash: '⊢',
Vdash: '⊩',
VDash: '⊫',
vdots: '⋮',
veebar: '⊻',
vee: '∨',
venus: '♀',
vert: '|',
Vert: '‖',
verymuchgreater: '⋙',
verymuchless: '⋘',
virgo: '♍',
volintegral: '∰',
Vvdash: '⊪',
wedge: '∧',
wp: '℘',
wr: '≀',
Xi: 'Ξ',
xi: 'ξ',
Zeta: 'Ζ',
zeta: 'ζ',
}
const SINGLE_ARG_REPLACEMENTS: Record<string, Record<string, string>> = {
mathbb: {
A: '𝔸',
B: '𝔹',
C: 'ℂ',
D: '𝔻',
E: '𝔼',
F: '𝔽',
G: '𝔾',
H: 'ℍ',
I: '𝕀',
J: '𝕁',
K: '𝕂',
L: '𝕃',
M: '𝕄',
N: 'ℕ',
O: '𝕆',
P: 'ℙ',
Q: 'ℚ',
R: 'ℝ',
S: '𝕊',
T: '𝕋',
U: '𝕌',
V: '𝕍',
W: '𝕎',
X: '𝕏',
Y: '𝕐',
Z: 'ℤ',
a: '𝕒',
b: '𝕓',
c: '𝕔',
d: '𝕕',
e: '𝕖',
f: '𝕗',
g: '𝕘',
h: '𝕙',
i: '𝕚',
j: '𝕛',
k: '𝕜',
l: '𝕝',
m: '𝕞',
n: '𝕟',
o: '𝕠',
p: '𝕡',
q: '𝕢',
r: '𝕣',
s: '𝕤',
t: '𝕥',
u: '𝕦',
v: '𝕧',
w: '𝕨',
x: '𝕩',
y: '𝕪',
z: '𝕫',
},
mathfrak: {
A: '𝔄',
B: '𝔅',
C: 'ℭ',
D: '𝔇',
E: '𝔈',
F: '𝔉',
G: '𝔊',
H: 'ℌ',
I: 'ℑ',
J: '𝔍',
K: '𝔎',
L: '𝔏',
M: '𝔐',
N: '𝔑',
O: '𝔒',
P: '𝔓',
Q: '𝔔',
R: 'ℜ',
S: '𝔖',
T: '𝔗',
U: '𝔘',
V: '𝔙',
W: '𝔚',
X: '𝔛',
Y: '𝔜',
Z: 'ℨ',
a: '𝔞',
b: '𝔟',
c: '𝔠',
d: '𝔡',
e: '𝔢',
f: '𝔣',
g: '𝔤',
h: '𝔥',
i: '𝔦',
j: '𝔧',
k: '𝔨',
l: '𝔩',
m: '𝔪',
n: '𝔫',
o: '𝔬',
p: '𝔭',
q: '𝔮',
r: '𝔯',
s: '𝔰',
t: '𝔱',
u: '𝔲',
v: '𝔳',
w: '𝔴',
x: '𝔵',
y: '𝔶',
z: '𝔷',
},
mathscr: {
A: '𝒜',
B: 'ℬ',
C: '𝒞',
D: '𝒟',
E: 'ℰ',
F: 'ℱ',
G: '𝒢',
H: 'ℋ',
I: 'ℐ',
J: '𝒥',
K: '𝒦',
L: 'ℒ',
M: 'ℳ',
N: '𝒩',
O: '𝒪',
P: '𝒫',
Q: '𝒬',
R: 'ℛ',
S: '𝒮',
T: '𝒯',
U: '𝒰',
V: '𝒱',
W: '𝒲',
X: '𝒳',
Y: '𝒴',
Z: '𝒵',
a: '𝒶',
b: '𝒷',
c: '𝒸',
d: '𝒹',
e: 'ℯ',
f: '𝒻',
g: 'ℊ',
h: '𝒽',
i: '𝒾',
j: '𝒿',
k: '𝓀',
l: 'ℓ',
m: '𝓂',
n: '𝓃',
o: 'ℴ',
p: '𝓅',
q: '𝓆',
r: '𝓇',
s: '𝓈',
t: '𝓉',
u: '𝓊',
v: '𝓋',
w: '𝓌',
x: '𝓍',
y: '𝓎',
z: '𝓏',
},
}
const COMBINING_CHAR_REPLACEMENTS: Record<string, string> = {
"'": '\u0301',
'"': '\u0308',
'.': '\u0307',
'=': '\u0304',
'^': '\u0302',
'`': '\u0300',
'~': '\u0303',
H: '\u030B',
acute: '\u0301',
b: '\u0331',
bar: '\u0305',
breve: '\u0306',
c: '\u0327',
check: '\u030C',
d: '\u0323',
dot: '\u0307',
ddot: '\u0308',
doubleunderline: '\u0333',
grave: '\u0300',
hat: '\u0302',
mathring: '\u030A',
overline: '\u0305',
r: '\u030A',
not: '\u0338',
slash: '\u0338',
strikethrough: '\u0335',
tilde: '\u0303',
u: '\u0306',
underline: '\u0332',
v: '\u030C',
vec: '\u20d7',
}