@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
529 lines (487 loc) • 20.7 kB
JavaScript
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import { renderToString } from 'katex';
// ============================================================================
// Utility Classes
// ============================================================================
/**
* PlaceholderManager - Manages temporary replacement and restoration of protected content
* Used to protect code blocks and LaTeX expressions during preprocessing
*/
var PlaceholderManager = /*#__PURE__*/function () {
function PlaceholderManager() {
var prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'PROTECTED';
_classCallCheck(this, PlaceholderManager);
_defineProperty(this, "placeholders", []);
_defineProperty(this, "prefix", void 0);
this.prefix = prefix;
}
_createClass(PlaceholderManager, [{
key: "add",
value: function add(content) {
var index = this.placeholders.length;
this.placeholders.push(content);
return "<<".concat(this.prefix, "_").concat(index, ">>");
}
}, {
key: "restore",
value: function restore(text) {
var _this = this;
return text.replaceAll(new RegExp("<<".concat(this.prefix, "_(\\d+)>>"), 'g'), function (_, index) {
return _this.placeholders[Number.parseInt(index)] || '';
});
}
}, {
key: "clear",
value: function clear() {
this.placeholders = [];
}
}]);
return PlaceholderManager;
}(); // ============================================================================
// Helper Functions
// ============================================================================
// Helper: replace unescaped pipes with \vert within a LaTeX math fragment
var replaceUnescapedPipes = function replaceUnescapedPipes(formula) {
return (
// Use \vert{} so the control sequence terminates before the next token
formula.replaceAll(/(?<!\\)\|/g, '\\vert{}')
);
};
/**
* Converts LaTeX bracket delimiters to dollar sign delimiters.
* Converts \[...\] to $$...$$ and \(...\) to $...$
* Preserves code blocks during the conversion.
*
* @param text The input string containing LaTeX expressions
* @returns The string with LaTeX bracket delimiters converted to dollar sign delimiters
*/
export function convertLatexDelimiters(text) {
var pattern = /(```[\S\s]*?```|`.*?`)|\\\[([\S\s]*?[^\\])\\]|\\\((.*?)\\\)/g;
return text.replaceAll(pattern, function (match, codeBlock, squareBracket, roundBracket) {
if (codeBlock !== undefined) {
return codeBlock;
} else if (squareBracket !== undefined) {
return "$$".concat(squareBracket, "$$");
} else if (roundBracket !== undefined) {
return "$".concat(roundBracket, "$");
}
return match;
});
}
/**
* Escapes mhchem commands in LaTeX expressions to ensure proper rendering.
*
* @param text The input string containing LaTeX expressions with mhchem commands
* @returns The string with escaped mhchem commands
*/
export function escapeMhchemCommands(text) {
return text.replaceAll('$\\ce{', '$\\\\ce{').replaceAll('$\\pu{', '$\\\\pu{');
}
/**
* Escapes pipe characters within LaTeX expressions to prevent them from being interpreted
* as table column separators in markdown tables.
*
* @param text The input string containing LaTeX expressions
* @returns The string with pipe characters escaped in LaTeX expressions
*/
export function escapeLatexPipes(text) {
// Replace unescaped '|' inside LaTeX math spans with '\vert' so that
// remark-gfm table parsing won't treat them as column separators.
// Leave code blocks/inline code untouched.
// Also ignore escaped dollars (\$) which are currency symbols
// Process code blocks first to protect them
var codeBlocks = [];
var content = text.replaceAll(/(```[\S\s]*?```|`[^\n`]*`)/g, function (match) {
codeBlocks.push(match);
return "<<CODE_".concat(codeBlocks.length - 1, ">>");
});
// For display math, allow multiline
content = content.replaceAll(/\$\$([\S\s]*?)\$\$/g, function (match, display) {
return "$$".concat(replaceUnescapedPipes(display), "$$");
});
// For inline math, use non-greedy match that DOES NOT cross newlines
// This prevents issues in tables where $ might appear in different cells
content = content.replaceAll(/(?<!\\)\$(?!\$)([^\n$]*?)(?<!\\)\$(?!\$)/g, function (match, inline) {
return "$".concat(replaceUnescapedPipes(inline), "$");
});
// Restore code blocks
content = content.replaceAll(/<<CODE_(\d+)>>/g, function (_, index) {
return codeBlocks[Number.parseInt(index)];
});
return content;
}
/**
* Escapes underscores within \text{...} commands in LaTeX expressions
* that are not already escaped.
* For example, \text{node_domain} becomes \text{node\_domain},
* but \text{node\_domain} remains \text{node\_domain}.
*
* @param text The input string potentially containing LaTeX expressions
* @returns The string with unescaped underscores escaped within \text{...} commands
*/
export function escapeTextUnderscores(text) {
return text.replaceAll(/\\text{([^}]*)}/g, function (match, textContent) {
// textContent is the content within the braces, e.g., "node_domain" or "already\_escaped"
// Replace underscores '_' with '\_' only if they are NOT preceded by a backslash '\'.
// The (?<!\\) is a negative lookbehind assertion that ensures the character before '_' is not a '\'.
var escapedTextContent = textContent.replaceAll(/(?<!\\)_/g, '\\_');
return "\\text{".concat(escapedTextContent, "}");
});
}
/**
* Escapes dollar signs that appear to be currency symbols to prevent them from being
* interpreted as LaTeX math delimiters.
*
* This function identifies currency patterns such as:
* - $20, $100, $1,000
* - $20-50, $100+
* - Patterns within markdown tables
*
* @param text The input string containing potential currency symbols
* @returns The string with currency dollar signs escaped
*/
export function escapeCurrencyDollars(text) {
// Protect code blocks and existing LaTeX expressions from processing
var manager = new PlaceholderManager('PROTECTED');
var content = text.replaceAll(
// Match patterns to protect (in order):
// 1. Code blocks: ```...```
// 2. Inline code: `...`
// 3. Display math: $$...$$
// 4. Inline math with LaTeX commands: $...\...$ (must contain backslash to distinguish from currency)
// 5. Simple number formulas: $1$, $10$, $100$ (pure digits in math mode)
// 6. Number lists in math mode: $1,-1,0$ or $1,2,3$ (comma-separated numbers, possibly negative)
// 7. LaTeX bracket notation: \[...\]
// 8. LaTeX parenthesis notation: \(...\)
/(```[\S\s]*?```|`[^\n`]*`|\$\$[\S\s]*?\$\$|(?<!\\)\$(?!\$)(?=[\S\s]*?\\)[\S\s]*?(?<!\\)\$(?!\$)|\$\d+\$|\$-?\d+(?:,-?\d+)+\$|\\\[[\S\s]*?\\]|\\\(.*?\\\))/g, function (match) {
return manager.add(match);
});
// Escape dollar signs that are clearly currency:
// - $ followed by a digit
// - Not preceded by another $ (to avoid breaking $$)
// - Not followed immediately by another $ (to avoid breaking $1$ LaTeX)
// - Followed by number patterns with optional commas, decimals, ranges, or plus signs
// Match patterns like: $20, $1,000, $19.99, $20-50, $300+, $1,000-2,000+
// But NOT: $1$, $2$ (these are LaTeX formulas)
// In the replacement: \\ = backslash, $$ = literal $, $1 = capture group 1
content = content.replaceAll(/(?<!\$)\$(\d{1,3}(?:,\d{3})*(?:\.\d+)?(?:-\d{1,3}(?:,\d{3})*(?:\.\d+)?)?\+?)(?!\$)/g, '\\$$$1');
// Restore protected content
content = manager.restore(content);
return content;
}
// Old simple preprocessLaTeX has been replaced by the comprehensive version below
// The new preprocessLaTeX provides the same default behavior with optional advanced featuresgit
/**
* Extracts the LaTeX formula after the last $$ delimiter if there's an odd number of $$ delimiters.
*
* @param text The input string containing LaTeX formulas
* @returns The content after the last $$ if there's an odd number of $$, otherwise an empty string
*/
var extractIncompleteFormula = function extractIncompleteFormula(text) {
// Count the number of $$ delimiters
var dollarsCount = (text.match(/\$\$/g) || []).length;
// If odd number of $$ delimiters, extract content after the last $$
if (dollarsCount % 2 === 1) {
var match = text.match(/\$\$([^]*)$/);
return match ? match[1] : '';
}
// If even number of $$ delimiters, return empty string
return '';
};
/**
* Checks if the last LaTeX formula in the text is renderable.
* Only validates the formula after the last $$ if there's an odd number of $$.
*
* @param text The input string containing LaTeX formulas
* @returns True if the last formula is renderable or if there's no incomplete formula
*/
export var isLastFormulaRenderable = function isLastFormulaRenderable(text) {
var formula = extractIncompleteFormula(text);
// If no incomplete formula, return true
if (!formula) return true;
// Try to render the last formula
try {
renderToString(formula, {
displayMode: true,
throwOnError: true
});
return true;
} catch (error) {
console.log("LaTeX formula rendering error: ".concat(error));
return false;
}
};
// ============================================================================
// Advanced Preprocessing Functions
// ============================================================================
/**
* Fixes common LaTeX syntax errors automatically
* - Balances unmatched braces
* - Balances \left and \right delimiters
*
* @param text The input string containing LaTeX expressions
* @returns The string with fixed LaTeX expressions
*/
export function fixCommonLaTeXErrors(text) {
return text.replaceAll(/(\$\$[\S\s]*?\$\$|\$[\S\s]*?\$)/g, function (match) {
var fixed = match;
// Fix unbalanced braces
var openBraces = (fixed.match(/(?<!\\){/g) || []).length;
var closeBraces = (fixed.match(/(?<!\\)}/g) || []).length;
if (openBraces > closeBraces) {
var diff = openBraces - closeBraces;
var closingBraces = '}'.repeat(diff);
// Insert before the closing delimiter
fixed = fixed.replace(/(\$\$?)$/, closingBraces + '$1');
}
// Fix unbalanced \left and \right
var leftDelims = (fixed.match(/\\left[(.<[{|]/g) || []).length;
var rightDelims = (fixed.match(/\\right[).>\]|}]/g) || []).length;
if (leftDelims > rightDelims) {
var _diff = leftDelims - rightDelims;
var rightDots = '\\right.'.repeat(_diff);
fixed = fixed.replace(/(\$\$?)$/, rightDots + '$1');
}
return fixed;
});
}
/**
* Normalizes whitespace in LaTeX expressions
* - Removes extra spaces around $ delimiters
* - Normalizes multiple spaces to single space inside formulas
*
* @param text The input string containing LaTeX expressions
* @returns The string with normalized whitespace
*/
export function normalizeLatexSpacing(text) {
var result = text;
// Remove spaces inside $ delimiters (at the edges)
result = result.replaceAll(/\$\s+/g, '$');
result = result.replaceAll(/\s+\$/g, '$');
result = result.replaceAll(/\$\$\s+/g, '$$');
result = result.replaceAll(/\s+\$\$/g, '$$');
// Normalize multiple spaces inside formulas to single space
result = result.replaceAll(/(\$\$[\S\s]*?\$\$|\$[\S\s]*?\$)/g, function (match) {
return match.replaceAll(/\s{2,}/g, ' ');
});
return result;
}
/**
* Validates all LaTeX expressions in the text
* Returns detailed information about validation results
*
* @param text The input string containing LaTeX expressions
* @returns Validation results with errors if any
*/
export function validateLatexExpressions(text) {
var errors = [];
var totalExpressions = 0;
var pattern = /\$\$([\S\s]*?)\$\$|(?<!\\)\$(?!\$)([\S\s]*?)(?<!\\)\$(?!\$)/g;
var match;
while ((match = pattern.exec(text)) !== null) {
totalExpressions++;
var formula = match[1] || match[2];
var isDisplay = match[0].startsWith('$$');
try {
renderToString(formula, {
displayMode: isDisplay,
strict: 'warn',
throwOnError: true,
trust: false
});
} catch (error) {
errors.push({
formula: formula.slice(0, 50) + (formula.length > 50 ? '...' : ''),
message: error instanceof Error ? error.message : String(error),
position: match.index,
type: isDisplay ? 'display' : 'inline'
});
}
}
return {
errors: errors,
totalExpressions: totalExpressions,
valid: errors.length === 0
};
}
/**
* Handles CJK (Chinese, Japanese, Korean) characters mixed with LaTeX
* Optionally adds spaces between CJK characters and LaTeX expressions for better rendering
*
* @param text The input string
* @param addSpaces Whether to add spaces between CJK and LaTeX (default: false)
* @returns The processed string
*/
export function handleCJKWithLatex(text) {
var addSpaces = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!addSpaces) return text;
var result = text;
// Add space between CJK character and opening $
result = result.replaceAll(/([\u3040-\u30FF\u4E00-\u9FA5])(\$)/g, '$1 $2');
// Add space between closing $ and CJK character
result = result.replaceAll(/(\$)([\u3040-\u30FF\u4E00-\u9FA5])/g, '$1 $2');
return result;
}
// ============================================================================
// Advanced Preprocessing Options
// ============================================================================
/**
* Comprehensive LaTeX preprocessing with configurable options
*
* This is the main preprocessing function that handles:
* - Currency symbol escaping (e.g., $20 → \$20)
* - LaTeX delimiter conversion (\[...\] → $$...$$)
* - Special character escaping (pipes, underscores, mhchem)
* - Optional error fixing and validation
* - Optional CJK character handling
*
* @param text The input string containing LaTeX and Markdown
* @param options Configuration options for fine-grained control
* @returns The preprocessed string
*
* @example
* ```ts
* // Default behavior (same as old preprocessLaTeX)
* preprocessLaTeX('向量$90^\\circ$,非 $0^\\circ$ 和 $180^\\circ$')
*
* // With custom options
* preprocessLaTeX(text, {
* fixErrors: true,
* validate: true,
* handleCJK: true
* })
* ```
*/
export function preprocessLaTeX(text) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var _options$addCJKSpaces = options.addCJKSpaces,
addCJKSpaces = _options$addCJKSpaces === void 0 ? false : _options$addCJKSpaces,
_options$convertBrack = options.convertBrackets,
convertBrackets = _options$convertBrack === void 0 ? true : _options$convertBrack,
_options$escapeCurren = options.escapeCurrency,
escapeCurrency = _options$escapeCurren === void 0 ? true : _options$escapeCurren,
_options$escapeMhchem = options.escapeMhchem,
escapeMhchem = _options$escapeMhchem === void 0 ? true : _options$escapeMhchem,
_options$escapePipes = options.escapePipes,
escapePipes = _options$escapePipes === void 0 ? true : _options$escapePipes,
_options$escapeUnders = options.escapeUnderscores,
escapeUnderscores = _options$escapeUnders === void 0 ? true : _options$escapeUnders,
_options$fixErrors = options.fixErrors,
fixErrors = _options$fixErrors === void 0 ? false : _options$fixErrors,
_options$handleCJK = options.handleCJK,
handleCJK = _options$handleCJK === void 0 ? false : _options$handleCJK,
_options$normalizeSpa = options.normalizeSpacing,
normalizeSpacing = _options$normalizeSpa === void 0 ? false : _options$normalizeSpa,
_options$throwOnValid = options.throwOnValidationError,
throwOnValidationError = _options$throwOnValid === void 0 ? false : _options$throwOnValid,
_options$validate = options.validate,
validate = _options$validate === void 0 ? false : _options$validate;
var content = text;
// Phase 1: Currency escaping (if enabled)
if (escapeCurrency) {
content = escapeCurrencyDollars(content);
}
// Phase 2: Bracket conversion (if enabled)
if (convertBrackets) {
content = convertLatexDelimiters(content);
}
// Phase 3: LaTeX-specific escaping
if (escapeMhchem) {
content = escapeMhchemCommands(content);
}
if (escapePipes) {
content = escapeLatexPipes(content);
}
if (escapeUnderscores) {
content = escapeTextUnderscores(content);
}
// Phase 4: Error fixing (if enabled)
if (fixErrors) {
content = fixCommonLaTeXErrors(content);
}
// Phase 5: Whitespace normalization (if enabled)
if (normalizeSpacing) {
content = normalizeLatexSpacing(content);
}
// Phase 6: CJK handling (if enabled)
if (handleCJK) {
content = handleCJKWithLatex(content, addCJKSpaces);
}
// Phase 7: Validation (if enabled)
if (validate) {
var validation = validateLatexExpressions(content);
if (!validation.valid) {
var errorMessage = "LaTeX validation failed (".concat(validation.errors.length, "/").concat(validation.totalExpressions, " expressions have errors):\n").concat(validation.errors.map(function (e) {
return " - [".concat(e.type, "] at position ").concat(e.position, ": ").concat(e.message, "\n Formula: ").concat(e.formula);
}).join('\n'));
if (throwOnValidationError) {
throw new Error(errorMessage);
} else {
console.warn(errorMessage);
}
}
}
return content;
}
/**
* Strict preprocessing mode - enables all safety features and validations
* Use this when you want maximum correctness and are willing to accept the performance cost
*
* @param text The input string
* @returns The preprocessed string with all features enabled
*
* @example
* ```ts
* const processed = preprocessLaTeXStrict(userInput)
* // Enables: error fixing, validation, CJK handling, space normalization
* ```
*/
export function preprocessLaTeXStrict(text) {
return preprocessLaTeX(text, {
addCJKSpaces: false,
// Usually don't want extra spaces
convertBrackets: true,
escapeCurrency: true,
escapeMhchem: true,
escapePipes: true,
escapeUnderscores: true,
fixErrors: true,
handleCJK: true,
normalizeSpacing: true,
throwOnValidationError: false,
// Warn but don't throw
validate: true
});
}
/**
* Minimal preprocessing mode - only essential operations
* Use this for better performance when you control the input
*
* @param text The input string
* @returns The preprocessed string with minimal processing
*
* @example
* ```ts
* const processed = preprocessLaTeXMinimal(trustedInput)
* // Only escapes currency and converts brackets
* ```
*/
export function preprocessLaTeXMinimal(text) {
return preprocessLaTeX(text, {
convertBrackets: true,
escapeCurrency: true,
escapeMhchem: false,
escapePipes: false,
escapeUnderscores: false,
fixErrors: false,
handleCJK: false,
normalizeSpacing: false,
validate: false
});
}