UNPKG

@yuntijs/ui

Version:

☁️ Yunti UI - an open-source UI component library for building Cloud Native web apps

607 lines (577 loc) 23.2 kB
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator"; import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray"; import _regeneratorRuntime from "@babel/runtime/regenerator"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from '@shikijs/transformers'; import { useTheme, useThemeMode } from 'antd-style'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ShikiStreamTokenizer } from 'shiki-stream'; import { Md5 } from 'ts-md5'; import { languageMap } from "./languageMap"; import { themeMap } from "./themeMap"; export var FALLBACK_LANG = 'txt'; // ============ 常量和工具函数 ============ var MD5_LENGTH_THRESHOLD = 10000; // 应用级缓存 var highlightCache = new Map(); var MAX_CACHE_SIZE = 1000; var cleanupCache = function cleanupCache() { if (highlightCache.size > MAX_CACHE_SIZE) { var entriesToRemove = Math.floor(MAX_CACHE_SIZE * 0.2); var keysToRemove = _toConsumableArray(highlightCache.keys()).slice(0, entriesToRemove); var _iterator = _createForOfIteratorHelper(keysToRemove), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var key = _step.value; highlightCache.delete(key); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } }; // 颜色替换映射类型 // 懒加载 shiki var loadShiki = function loadShiki() { return import('shiki').then(function (mod) { return mod.codeToHtml; }); }; var shikiPromise = loadShiki(); var loadShikiModule = function loadShikiModule() { if (typeof window === 'undefined') return Promise.resolve(null); return import('shiki'); }; var shikiModulePromise = loadShikiModule(); // 辅助函数:安全的HTML转义 var escapeHtml = function escapeHtml(str) { return str.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;'); }; var tokensToLineTokens = function tokensToLineTokens(tokens) { if (tokens.length === 0) return [[]]; var lines = [[]]; var currentLine = lines[0]; var startNewLine = function startNewLine() { currentLine = []; lines.push(currentLine); }; var _iterator2 = _createForOfIteratorHelper(tokens), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _token$content; var token = _step2.value; var content = (_token$content = token.content) !== null && _token$content !== void 0 ? _token$content : ''; if (content === '\n') { startNewLine(); continue; } if (!content.includes('\n')) { currentLine.push(token); continue; } var segments = content.split('\n'); var _iterator3 = _createForOfIteratorHelper(segments.entries()), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var _step3$value = _slicedToArray(_step3.value, 2), index = _step3$value[0], segment = _step3$value[1]; if (segment) { currentLine.push(_objectSpread(_objectSpread({}, token), {}, { content: segment })); } if (index < segments.length - 1) { startNewLine(); } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } if (lines.length === 0) return [[]]; return lines; }; var createPreStyle = function createPreStyle(bg, fg) { if (!bg && !fg) return undefined; return { backgroundColor: bg, color: fg }; }; // ============ 类型定义 ============ // ============ useHighlightConfig Hook ============ /** * 共享的高亮配置 hook * 处理主题、语言匹配、颜色替换等通用逻辑 */ export var useHighlightConfig = function useHighlightConfig(lang, enableTransformer, theme) { var _useThemeMode = useThemeMode(), isDarkMode = _useThemeMode.isDarkMode; var antTheme = useTheme(); var language = lang.toLowerCase(); // 匹配支持的语言 var matchedLanguage = useMemo(function () { return languageMap.includes(language) ? language : FALLBACK_LANG; }, [language]); // 匹配支持的主题 var matchedTheme = useMemo(function () { return themeMap.includes(theme) ? theme : undefined; }, [theme]); // 优化 transformer 创建 var transformers = useMemo(function () { if (!enableTransformer) return; return [transformerNotationDiff(), transformerNotationHighlight(), transformerNotationWordHighlight(), transformerNotationFocus(), transformerNotationErrorLevel()]; }, [enableTransformer]); // 优化颜色替换配置 var colorReplacements = useMemo(function () { return { 'slack-dark': { '#4ec9b0': antTheme.yellow, '#569cd6': antTheme.colorError, '#6a9955': antTheme.gray, '#9cdcfe': antTheme.colorText, '#b5cea8': antTheme.purple10, '#c586c0': antTheme.colorInfo, '#ce9178': antTheme.colorSuccess, '#dcdcaa': antTheme.colorWarning, '#e6e6e6': antTheme.colorText }, 'slack-ochin': { '#002339': antTheme.colorText, '#0991b6': antTheme.colorError, '#174781': antTheme.purple10, '#2f86d2': antTheme.colorText, '#357b42': antTheme.gray, '#7b30d0': antTheme.colorInfo, '#7eb233': antTheme.colorWarningTextActive, '#a44185': antTheme.colorSuccess, '#dc3eb7': antTheme.yellow11 } }; }, [antTheme]); // 构建 shiki 主题 var shikiTheme = useMemo(function () { if (matchedTheme) { return matchedTheme; } if (language === 'md') { return isDarkMode ? 'catppuccin-mocha' : 'catppuccin-latte'; } if (language === 'shellsession') { return isDarkMode ? 'material-theme-darker' : 'material-theme-lighter'; } if (language === 'diff') { return isDarkMode ? 'slack-dark' : 'github-light'; } return isDarkMode ? 'slack-dark' : 'slack-ochin'; }, [isDarkMode, language, matchedTheme]); return { colorReplacements: colorReplacements, matchedLanguage: matchedLanguage, shikiTheme: shikiTheme, transformers: transformers }; }; // ============ useStaticHighlight Hook ============ /** * 静态高亮 hook * 将代码转换为高亮 HTML 字符串 */ export var useStaticHighlight = function useStaticHighlight(text, lang, enableTransformer, theme) { var _useHighlightConfig = useHighlightConfig(lang, enableTransformer, theme), colorReplacements = _useHighlightConfig.colorReplacements, matchedLanguage = _useHighlightConfig.matchedLanguage, shikiTheme = _useHighlightConfig.shikiTheme, transformers = _useHighlightConfig.transformers; // 构建缓存键 var cacheKey = useMemo(function () { var hash = text.length < MD5_LENGTH_THRESHOLD ? text : Md5.hashStr(text); return [matchedLanguage, shikiTheme, hash].join('-'); }, [text, matchedLanguage, shikiTheme]); var _useState = useState(), _useState2 = _slicedToArray(_useState, 2), data = _useState2[0], setData = _useState2[1]; useEffect(function () { if (!cacheKey) { // eslint-disable-next-line unicorn/no-useless-undefined setData(undefined); return; } // 检查缓存 var cachedPromise = highlightCache.get(cacheKey); if (cachedPromise) { cachedPromise.then(function (html) { setData(html); }).catch(function () { // 静默处理错误,已在创建时处理 }); return; } // 创建新的高亮 Promise var highlightPromise = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() { var codeToHtmlFn, html, _codeToHtmlFn, _html, fallbackHtml; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: _context.prev = 0; _context.next = 3; return shikiPromise; case 3: codeToHtmlFn = _context.sent; _context.next = 6; return codeToHtmlFn(text, { colorReplacements: colorReplacements, lang: matchedLanguage, theme: shikiTheme, transformers: transformers }); case 6: html = _context.sent; return _context.abrupt("return", html); case 10: _context.prev = 10; _context.t0 = _context["catch"](0); console.warn('高级渲染失败:', _context.t0); _context.prev = 13; _context.next = 16; return shikiPromise; case 16: _codeToHtmlFn = _context.sent; _context.next = 19; return _codeToHtmlFn(text, { lang: matchedLanguage, theme: shikiTheme }); case 19: _html = _context.sent; return _context.abrupt("return", _html); case 23: _context.prev = 23; _context.t1 = _context["catch"](13); fallbackHtml = "<pre class=\"fallback\"><code>".concat(escapeHtml(text), "</code></pre>"); return _context.abrupt("return", fallbackHtml); case 27: case "end": return _context.stop(); } }, _callee, null, [[0, 10], [13, 23]]); }))(); // 缓存 Promise highlightCache.set(cacheKey, highlightPromise); cleanupCache(); // 处理结果 highlightPromise.then(function (html) { // 仅当缓存仍然有效时更新 if (highlightCache.get(cacheKey) === highlightPromise) { setData(html); } }).catch(function () { // 从缓存中移除失败的 Promise if (highlightCache.get(cacheKey) === highlightPromise) { highlightCache.delete(cacheKey); } }); }, [cacheKey, text, matchedLanguage, shikiTheme, transformers, colorReplacements]); return data || ''; }; // ============ useStreamHighlight Hook ============ /** * 流式高亮 hook * 用于实时流式渲染代码高亮 */ export var useStreamHighlight = function useStreamHighlight(text, lang, enableTransformer, theme) { var _useHighlightConfig2 = useHighlightConfig(lang, enableTransformer, theme), colorReplacements = _useHighlightConfig2.colorReplacements, matchedLanguage = _useHighlightConfig2.matchedLanguage, shikiTheme = _useHighlightConfig2.shikiTheme; var _useState3 = useState(), _useState4 = _slicedToArray(_useState3, 2), result = _useState4[0], setResult = _useState4[1]; var tokenizerRef = useRef(null); var previousTextRef = useRef(''); var latestTextRef = useRef(text); // eslint-disable-next-line unicorn/no-useless-undefined var preStyleRef = useRef(undefined); var colorReplacementsRef = useRef(colorReplacements[shikiTheme]); var linesRef = useRef([[]]); // 缓存 highlighter key,避免不必要的 tokenizer 重建 var highlighterKeyRef = useRef(''); useEffect(function () { latestTextRef.current = text; }, [text]); useEffect(function () { colorReplacementsRef.current = colorReplacements[shikiTheme]; }, [colorReplacements, shikiTheme]); var setStreamingResult = useCallback(function (rawLines) { var previousLines = linesRef.current; var newLinesLength = rawLines.length; var prevLinesLength = previousLines.length; // 快速路径:如果长度不同或为空,直接使用新的 lines if (newLinesLength !== prevLinesLength || newLinesLength === 0) { linesRef.current = rawLines; setResult({ colorReplacements: colorReplacementsRef.current, lines: rawLines, preStyle: preStyleRef.current }); return; } // 优化比较:只检查有变化的行 var hasChanges = false; var mergedLines = []; for (var i = 0; i < newLinesLength; i++) { var newLine = rawLines[i]; var prevLine = previousLines[i]; // 快速引用相等检查 if (prevLine === newLine) { mergedLines[i] = prevLine; continue; } // 长度检查 if (!prevLine || prevLine.length !== newLine.length) { mergedLines[i] = newLine; hasChanges = true; continue; } // 深度比较只对可能变化的行 var lineChanged = false; var _iterator4 = _createForOfIteratorHelper(newLine.entries()), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var _step4$value = _slicedToArray(_step4.value, 2), j = _step4$value[0], newToken = _step4$value[1]; if (prevLine[j] !== newToken) { lineChanged = true; break; } } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } if (lineChanged) { mergedLines[i] = newLine; hasChanges = true; } else { mergedLines[i] = prevLine; } } // 只有实际变化时才更新状态 if (hasChanges) { linesRef.current = mergedLines; setResult({ colorReplacements: colorReplacementsRef.current, lines: mergedLines, preStyle: preStyleRef.current }); } }, []); var updateTokens = useCallback( /*#__PURE__*/function () { var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(nextText) { var forceReset, tokenizer, previousText, chunk, canAppend, stableTokens, unstableTokens, totalLength, getMergedTokens, _stableTokens, _unstableTokens, _getMergedTokens, _args2 = arguments; return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) switch (_context2.prev = _context2.next) { case 0: forceReset = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : false; tokenizer = tokenizerRef.current; if (tokenizer) { _context2.next = 4; break; } return _context2.abrupt("return"); case 4: if (forceReset) { tokenizer.clear(); previousTextRef.current = ''; } previousText = previousTextRef.current; chunk = nextText; canAppend = !forceReset && nextText.startsWith(previousText); if (canAppend) { chunk = nextText.slice(previousText.length); } else if (!forceReset) { tokenizer.clear(); } previousTextRef.current = nextText; if (chunk) { _context2.next = 20; break; } stableTokens = tokenizer.tokensStable; unstableTokens = tokenizer.tokensUnstable; totalLength = stableTokens.length + unstableTokens.length; if (!(totalLength === 0)) { _context2.next = 17; break; } setStreamingResult([[]]); return _context2.abrupt("return"); case 17: // 优化:只在必要时创建合并数组 getMergedTokens = function getMergedTokens() { if (stableTokens.length === 0) return unstableTokens; if (unstableTokens.length === 0) return stableTokens; return [].concat(_toConsumableArray(stableTokens), _toConsumableArray(unstableTokens)); }; setStreamingResult(tokensToLineTokens(getMergedTokens())); return _context2.abrupt("return"); case 20: _context2.prev = 20; _context2.next = 23; return tokenizer.enqueue(chunk); case 23: _stableTokens = tokenizer.tokensStable; _unstableTokens = tokenizer.tokensUnstable; _getMergedTokens = function _getMergedTokens() { if (_stableTokens.length === 0) return _unstableTokens; if (_unstableTokens.length === 0) return _stableTokens; return [].concat(_toConsumableArray(_stableTokens), _toConsumableArray(_unstableTokens)); }; setStreamingResult(tokensToLineTokens(_getMergedTokens())); _context2.next = 32; break; case 29: _context2.prev = 29; _context2.t0 = _context2["catch"](20); // eslint-disable-next-line no-console console.error('Streaming highlighting failed:', _context2.t0); case 32: case "end": return _context2.stop(); } }, _callee2, null, [[20, 29]]); })); return function (_x) { return _ref2.apply(this, arguments); }; }(), [setStreamingResult]); useEffect(function () { // 如果 language/theme 组合没有变化,跳过 var currentKey = "".concat(matchedLanguage, "-").concat(shikiTheme); if (highlighterKeyRef.current === currentKey && tokenizerRef.current) { return; } var cancelled = false; _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() { var mod, highlighter, _tokenizerRef$current, tokenizer, themeInfo, currentText; return _regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return shikiModulePromise; case 2: mod = _context3.sent; if (!(!mod || cancelled)) { _context3.next = 5; break; } return _context3.abrupt("return"); case 5: _context3.prev = 5; _context3.next = 8; return mod.getSingletonHighlighter({ langs: matchedLanguage ? [matchedLanguage] : [], themes: [shikiTheme] }); case 8: highlighter = _context3.sent; if (!(!highlighter || cancelled)) { _context3.next = 11; break; } return _context3.abrupt("return"); case 11: // 只在 key 变化时创建新的 tokenizer if (highlighterKeyRef.current !== currentKey) { // 清理旧的 tokenizer (_tokenizerRef$current = tokenizerRef.current) === null || _tokenizerRef$current === void 0 || _tokenizerRef$current.clear(); tokenizer = new ShikiStreamTokenizer({ highlighter: highlighter, lang: matchedLanguage, theme: shikiTheme }); tokenizerRef.current = tokenizer; highlighterKeyRef.current = currentKey; previousTextRef.current = ''; linesRef.current = [[]]; themeInfo = highlighter.getTheme(shikiTheme); preStyleRef.current = createPreStyle(themeInfo === null || themeInfo === void 0 ? void 0 : themeInfo.bg, themeInfo === null || themeInfo === void 0 ? void 0 : themeInfo.fg); } currentText = latestTextRef.current; if (!currentText) { _context3.next = 18; break; } _context3.next = 16; return updateTokens(currentText, true); case 16: _context3.next = 19; break; case 18: setStreamingResult([[]]); case 19: _context3.next = 25; break; case 21: _context3.prev = 21; _context3.t0 = _context3["catch"](5); // eslint-disable-next-line no-console console.error('Streaming highlighter initialization failed:', _context3.t0); // 重置错误时的 key highlighterKeyRef.current = ''; case 25: case "end": return _context3.stop(); } }, _callee3, null, [[5, 21]]); }))(); return function () { cancelled = true; }; }, [matchedLanguage, setStreamingResult, shikiTheme, updateTokens]); useEffect(function () { if (!tokenizerRef.current) return; updateTokens(text); }, [text, updateTokens]); return result; }; // ============ 导出 ============ export { languageMap } from "./languageMap"; export { themeMap } from "./themeMap"; export { escapeHtml, loadShiki, MD5_LENGTH_THRESHOLD, shikiPromise };