traceback.js
Version:
代码回溯。指定文本的位置,格式化显示选定区域
255 lines (249 loc) • 8.75 kB
JavaScript
// 展示行数解析规则
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;