@yuntijs/ui
Version:
☁️ Yunti UI - an open-source UI component library for building Cloud Native web apps
607 lines (577 loc) • 23.2 kB
JavaScript
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('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
};
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 };