Files
overleaf-cep/services/web/shared/bibtex/latex-to-unicode.mts
T
Eric Mc Sween 7d3d60f2ba Merge pull request #36920 from overleaf/em-shared-frontend-backend
Move shared bibtex model classes to services/web/shared

GitOrigin-RevId: ef768d4f7bcf7b9c4b41844450b0256ecb46fdae
2026-09-17 12:08:21 +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',
}