Files
santifer--career-ops/lib/latex-escape.mjs
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

56 lines
1.5 KiB
JavaScript

/**
* Shared LaTeX escaping for career-ops CV scripts.
*/
/**
* Escape user text for insertion into LaTeX macro arguments.
*
* @param {string} text
* @param {'text'|'url'} [mode='text']
* @returns {string}
*/
export function escapeLatex(text, mode = 'text') {
if (typeof text !== 'string') return '';
if (mode === 'url') return text;
const out = [];
for (const ch of text) {
switch (ch) {
case '\\': out.push('\\textbackslash{}'); break;
case '{': case '}': out.push('\\' + ch); break;
case '^': out.push('\\textasciicircum{}'); break;
case '~': out.push('\\textasciitilde{}'); break;
case '_': out.push('\\_'); break;
case '&': out.push('\\&'); break;
case '%': out.push('\\%'); break;
case '$': out.push('\\$'); break;
case '#': out.push('\\#'); break;
case '\u00B1': out.push('$\\pm$'); break;
case '\u2192': out.push('$\\rightarrow$'); break;
default: out.push(ch);
}
}
return out.join('');
}
/**
* Validate and normalize URLs for \\href{} (not LaTeX-escaped).
*
* @param {string} url
* @returns {string}
*/
export function sanitizeUrl(url) {
if (typeof url !== 'string') return '';
url = url.trim();
if (!url) return '';
const allowedSchemes = ['mailto:', 'http:', 'https:'];
const hasScheme = allowedSchemes.some(s => url.toLowerCase().startsWith(s));
if (!hasScheme) {
if (url.includes('@') && !url.includes('/')) {
url = 'mailto:' + url;
} else {
url = 'https://' + url;
}
}
return url.replace(/[{}%$#\\~^]/g, '');
}