UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

529 lines (487 loc) 20.7 kB
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 }); }