UNPKG

traceback.js

Version:

代码回溯。指定文本的位置,格式化显示选定区域

255 lines (249 loc) 8.75 kB
// 展示行数解析规则 const reg = /^-(\d+)\+(\d+)$/; // 默认展示 10 行 const defaultRows = { upward: 5, downward: 5, }; /** * 解析展示规则 * * + 数字 `-1`,全部展示 * + 字符串 `-6+4`,展示指定行前 6 行、后 4 行 * + 对象 `{ upward, downward }`,展示指定行前 `upward` 行、后 `downward` 行 * * @param {string|object} displayRows 展示规则 */ function parseRows(displayRows) { let row = {}; // 全部显示 if (displayRows === -1) { row = { upward: -1, downward: -1, }; } else if (typeof displayRows === 'object') { row = displayRows; } else if (typeof displayRows === 'string' && reg.test(displayRows)) { // @ts-ignore const [, upward, downward] = displayRows.match(reg); row = { upward: +upward, downward: +downward, }; } // eslint-disable-next-line prefer-object-spread return Object.assign({}, defaultRows, row); } /** * 格式化源文本 * * 建立一个 layer(层)的概念, * 渲染时根据 layer 而不是逻辑混写到渲染时 * * @param {string} rawInput 源文本 * @param {TracebackOption} opts 配置对象 * @returns {Row[]} 格式化行数组 */ function formatter(rawInput, opts) { const { separator, highlightRow, displayRows, start, } = opts; // @ts-ignore const { upward, downward } = displayRows; const formatRows = []; const rawList = serialize(rawInput, separator); if (highlightRow - start > rawList.length) { console.warn('traceback.js渲染异常:高亮行数超出源文本行数'); return []; } rawList.forEach((raw, index) => { const lineno = index + start; // 剔除展示规则外的行数 if (upward !== -1 && lineno < highlightRow - upward) return; if (downward !== -1 && lineno > highlightRow + downward) return; const row = factory(lineno, raw, index === highlightRow - start); formatRows.push(row); }); return formatRows; } /** * 解析文本 * * @param rawInput 源文本 * @param separator 分隔符 */ function serialize(rawInput, separator) { // TODO: 只是简单地分割,考虑处理大量文本时性能?比如 10w+ 行 return rawInput.trim().split(separator); } /** 创建格式化行对象 */ function factory(lineno, content, highlighted) { const o = { lineno, content, }; if (highlighted) o.highlighted = true; return o; } var stylesheet = ".traceback-js_container {width: 100%;margin: 0;padding: 8px 0;border: 1px solid #eee;border-radius: 2px;overflow-x: auto;}.traceback-js_list {min-width: 100%;border-spacing: 0;color: #4b4b4b;font-size: 14px;font-family: Consolas, Monaco, Liberation Mono, Menlo;white-space: pre;line-height: 24px;tab-size: 4;}.traceback-js_item {}.traceback-js_item.clicked_row {background-color: rgba(0, 128, 128, .1);}.traceback-js_item.highlight_row,.traceback-js_item.highlight_row .traceback-js_index::before {background-color: #008080;color: #fff;}.traceback-js_index {padding: 0;width: 36px;min-width: 36px;color: #999;text-align: right;cursor: pointer;}.traceback-js_index::before {content: attr(data-lineno);}.traceback-js_index:hover {color: #666;-webkit-user-select: none;user-select: none;}.traceback-js_content {padding: 0 16px;}"; /** 样式集合 */ const classNames = { /** 容器 */ container: 'traceback-js_container', /** 列表 */ list: 'traceback-js_list', /** 列表项 */ item: 'traceback-js_item', /** 列表高亮项 */ highlightRow: 'traceback-js_item highlight_row', /** 列表点击项 */ clickedRow: 'clicked_row', /** 列表项索引 */ index: 'traceback-js_index', /** 列表项内容 */ content: 'traceback-js_content', }; const ID_STYLE = 'traceback-jsid_style'; /** * 渲染源文本 * * 将格式化后的行渲染成 html,目前使用 `table` * * @param {Row[]} formatRows 格式化行数组 * @returns {null|HTMLElement} dom 对象;数组为空时返回 `null` */ function renderer(formatRows) { if (formatRows.length === 0) { return null; } const $container = document.createElement('div'); // 使用 table 渲染 const $table = document.createElement('table'); const $tbody = document.createElement('tbody'); // TODO: 分片渲染,利用 rAF formatRows.forEach((row) => { const $tr = document.createElement('tr'); const $index = document.createElement('td'); const $content = document.createElement('td'); let className = classNames.item; // 高亮行 class 名称修改 if (row.highlighted) { className = classNames.highlightRow; } // $index.id = `L${index + 1}`; // @ts-ignore $index.dataset.lineno = row.lineno; $content.textContent = row.content; $index.className = classNames.index; $content.className = classNames.content; $tr.className = className; $tr.appendChild($index); $tr.appendChild($content); $tbody.appendChild($tr); }); $table.className = classNames.list; $container.className = classNames.container; $table.appendChild($tbody); $container.appendChild($table); // 添加样式 if (!document.getElementById(ID_STYLE)) { const $style = document.createElement('style'); $style.id = ID_STYLE; $style.textContent = stylesheet; document.head.appendChild($style); } return $container; } // @ts-ignore // eslint-disable-next-line prefer-destructuring const VERSION = "0.3.0"; const DEFAULT_OPTS = { start: 1, separator: '\n', highlightRow: 1, displayRows: '-5+5', }; /** * 初始化 * * 根据选择符匹配的元素内容渲染。 * 用法:`TracebackJS.init('.traceback-js', {})(text);` * * @param {string} selectors DOM 选择符,同 `querySelector` * @param {object} opts 配置对象 */ function init(selectors, opts = DEFAULT_OPTS) { var _a; const $rootEl = document.querySelector(selectors); if (!$rootEl) { console.warn(`查询${selectors}失败,请确保页面上存在此元素`); return; } const rawInput = ((_a = $rootEl.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || ''; const $dom = render(rawInput, opts); if (!$dom) return; $rootEl.textContent = null; $rootEl.appendChild($dom); // TODO: 额外的功能转化为插件形式 let $rowClicked = null; $rootEl.addEventListener('click', (e) => { // @ts-ignore const { className, parentNode } = e.target; if (className === classNames.index && parentNode !== $rowClicked) { // eslint-disable-next-line no-unused-expressions $rowClicked === null || $rowClicked === void 0 ? void 0 : $rowClicked.classList.remove(classNames.clickedRow); parentNode.classList.add(classNames.clickedRow); $rowClicked = parentNode; } }); // TODO: 重用渲染 // return function reuse(opts); } /** * 渲染为 dom * * @param rawInput 源文本 * @param opts 配置对象 * @returns 可供加载到页面上的 dom */ function render(rawInput, opts) { const options = Object.assign(Object.assign(Object.assign({}, DEFAULT_OPTS), opts), { // 解析展示规则 displayRows: parseRows(opts.displayRows || DEFAULT_OPTS.displayRows) }); const formatRows = formatter(rawInput, options); const $result = renderer(formatRows); return $result; } /** * 渲染为 html 字符串 * * @param {string} rawInput 源文本 * @param {TracebackOption} opts 配置对象 * @returns {string} html 字符串 */ function renderToString(rawInput, opts = DEFAULT_OPTS) { const options = Object.assign(Object.assign(Object.assign({}, DEFAULT_OPTS), opts), { // 解析展示规则 displayRows: parseRows(opts.displayRows || DEFAULT_OPTS.displayRows) }); const formatRows = formatter(rawInput, options); const $result = renderer(formatRows); if (!$result) { return ''; } // 创建临时元素,转为 html 字符串 const $tmp = document.createElement('div'); $tmp.appendChild($result); return $tmp.innerHTML; } var index = { version: VERSION, init, render, renderToString, }; export default index;