feat(code): 新增行高亮功能 (#2763)

* feat(code): 新增行高亮功能

支持行高亮,聚焦,diff,自定义行高亮

* update code

* chore: 优化变量命名

* docs(code): 更新文档

* test(code): 添加行高亮测试用例

* docs(code): 优化 highlightLine 文档

---------

Co-authored-by: 贤心 <3277200+sentsim@users.noreply.github.com>
main
morning-star 2025-09-08 23:04:32 +08:00 committed by GitHub
parent af6ba6c972
commit fd78240b53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 296 additions and 26 deletions

View File

@ -393,6 +393,49 @@ onCopy: function(code, copied){
```
</td>
</tr>
<tr>
<td>
[highlightLine](#options.highlightLine) <sup>2.12+</sup>
</td>
<td colspan="3">
<div id="options.highlightLine" lay-pid="options" class="ws-anchor">
设置行高亮,可选项:
- `hl` : 高亮
- `++` : diff++
- `--` : diff--
- `focus` : 聚焦
</div>
可通过 `hl.range` 选项来设置行高亮范围:
```
highlightLine: {
hl: {
range: '1,3-5,8', // 指定行高亮范围 '1,3,4,5,8'
comment: true, // 是否解析行中的高亮注释
}
}
```
或通过特定注释格式: `[!code <type>:<lines>]` 指定行高亮范围,规则解释:
- `<type>` : `highlightLine` 的可选项,如 `h1, focus`
- `<lines>` : 行数(含本行)
```
111 聚焦 2 行(含本行) // [!code focus:2]
222 高亮本行 // [!code hl]
333
```
highlightLine 选项的详细用法可参考:<a href="https://stackblitz.com/edit/iw6hx7mi?file=index.html" rel="nofollow" target="_blank">highlightLine 行高亮在线示例</a>
</td>
</tr>
</tbody>

View File

@ -4,15 +4,16 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Code blocks adorn - Layui</title>
<link href="../src/css/layui.css" rel="stylesheet">
<style>
body{padding: 32px;}
pre{margin: 16px 0;}
</style>
</head>
<body>
<body class="layui-padding-3">
<div class="layui-text">
<h2>代码高亮</h2>
</div>
<pre id="test" class="layui-test">
<textarea class="layui-hide">
<div class="layui-btn-container">
@ -54,6 +55,104 @@ Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
</textarea>
</pre>
<div class="layui-text">
<h2>行高亮和聚焦</h2>
</div>
<pre
class="layui-code"
lay-options="{
highlightLine: {
hl: {
range: '1,3-5,8', // 指定行高亮范围 '1,3,4,5,8'
comment: true, // 是否解析行中的高亮注释
}
}
}">
<textarea class="layui-hide">
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quick Start - Layui</title>
<link href="//unpkg.com/layui@2.11.4/dist/css/layui.css" rel="stylesheet">
</head>
<body>
<!-- HTML Content -->
<script src="//unpkg.com/layui@2.11.4/dist/layui.js"></script>
<script>
// Usage // [!code focus:6]
layui.use(function(){
var layer = layui.layer;
// Welcome
layer.msg('Hello World', {icon: 6});
});
</script>
</body>
</html>
</textarea>
</pre>
<div class="layui-text">
<h2>diff</h2>
</div>
<pre
class="layui-code"
lay-options="{
highlightLine: {
hl:{range: '38-40'},
'++':{comment: true},
'--':{comment: true}
}
}">
<textarea class="layui-hide">
<template id="test">
<p>转义输出:{{= d.desc }}</p>
<p>原文输出:{{- d.desc }}</p>
{{# 注释标签 - 仅在模板中显示,不在视图中输出 }} // [!code ++]
<p>{{! 忽略标签,可显示原始标签:
{{# let a = 0; }} // [!code --]
{{ let a = 0; }} // [!code ++]
!}}
</p>
<p>
条件语句:
{{= d.list.length ? d.title : '' }}
{{# if(d.title){}} - #AAAA{{='A'}}{{# } }} //[!code --]
{{ if(d.title){}} - #AAAA{{='A'}}{{ } }} //[!code ++]
</p>
<p>循环语句:</p>
<ul>
{{# d.list.forEach(function(item) { }} //[!code --]
{{ d.list.forEach(function(item) { }} //[!code ++]
</p>
<li>
<span>{{= item.title }}</span>
<span>{{= item.name }}</span>
</li>
{{# }); }} //[!code --]
{{ }); }} //[!code ++]
</ul>
{{# if (d.list.length === 0) { }} //[!code --]
{{ if (d.list.length === 0) { }} //[!code ++]
无数据
{{#} }} //[!code --]
{{} }} //[!code ++]
</template>
<script>
layui.use(function(){
var laytpl = layui.laytpl;
laytpl.config({
tagStyle: 'modern'
})
})
</script>
</textarea>
</pre>
<h2>普通示例</h2>
<pre><code class="layui-code" lay-options="{header: true}">
code line
code line

View File

@ -71,3 +71,12 @@ html #layuicss-skincodecss{display: none; position: absolute; width: 1989px;}
.layui-code-view.layui-code-hl > .layui-code-ln-side{background-color: transparent;}
.layui-code-theme-dark.layui-code-hl,
.layui-code-theme-dark.layui-code-hl > .layui-code-ln-side{border-color: rgb(126 122 122 / 15%);}
/*行高亮*/
.layui-code-line-highlighted{background-color:rgba(142, 150, 170, .14)}
.layui-code-line-diff-add{background-color: rgba(16, 185, 129, .14);}
.layui-code-line-diff-remove{background-color: rgba(244, 63, 94, .14);}
.layui-code-line-diff-add:before{position:absolute; content: "+"; color: #18794e;}
.layui-code-line-diff-remove:before{position:absolute; content: "-"; color: #b8272c;}
.layui-code-has-focused-lines .layui-code-line:not(.layui-code-line-has-focus) {filter: blur(.095rem); opacity: .7; -webkit-transition: filter .35s, opacity .35s; transition: filter .35s, opacity .35s;}
.layui-code-has-focused-lines:hover .layui-code-line:not(.layui-code-line-has-focus) {filter: blur(); opacity: 1;}

View File

@ -48,6 +48,30 @@ layui.define(['lay', 'i18n', 'util', 'element', 'tabs', 'form'], function(export
lang: 'text', // 指定语言类型
highlighter: false, // 是否开启语法高亮,'hljs','prism','shiki'
langMarker: false, // 代码区域是否显示语言类型标记
highlightLine: { // 行高亮
// 聚焦
focus: {
range: '', // 高亮范围,不可全局设置值 '1,3-5,8'
comment: false, // 是否解析注释,性能敏感不可全局开启 [!code type:<lines>]
classActiveLine: 'layui-code-line-has-focus', // 添加到高亮行上的类
classActivePre: 'layui-code-has-focused-lines' // 有高亮行时向根元素添加的类
},
// 高亮
hl: {
comment: false,
classActiveLine: 'layui-code-line-highlighted',
},
// diff++
'++':{
comment: false,
classActiveLine: 'layui-code-line-diff-add',
},
// diff--
'--': {
comment: false,
classActiveLine: 'layui-code-line-diff-remove',
}
}
};
// 初始索引
@ -62,6 +86,91 @@ layui.define(['lay', 'i18n', 'util', 'element', 'tabs', 'form'], function(export
return trimEnd(str).replace(/^\n|\n$/, '');
};
// '1,3-5,8' -> [1,3,4,5,8]
var parseHighlightedLines = function(rangeStr){
if (typeof rangeStr !== 'string') return [];
var lines = $.map(rangeStr.split(','), function(v){
var range = v.split('-');
var start = parseInt(range[0], 10);
var end = parseInt(range[1], 10);
return start && end
? $.map(new Array(end - start + 1), function(_, index){ return start + index })
: start ? start : undefined
})
return lines;
}
// 引用自 https://github.com/innocenzi/shiki-processor/blob/efa20624be415c866cc8e350d1ada886b6b5cd52/src/utils/create-range-processor.ts#L7
// 添加了 HTML 注释支持,用来处理预览场景
var highlightLineRegex = /(?:\/\/|\/\*{1,2}|<!--|&lt;!--) *\[!code ([\w+-]+)(?::(\d+))?] *(?:\*{1,2}\/|-->|--&gt;)?/;
var preprocessHighlightLine = function (highlightLineOptions, codeLines) {
var hasHighlightLine = false;
var needParseComment = false;
var lineClassMap = Object.create(null);
var preClassMap = Object.create(null);
var updateLineClassMap = function (lineNumber, className) {
if (!lineClassMap[lineNumber]) {
lineClassMap[lineNumber] = [CONST.ELEM_LINE];
}
lineClassMap[lineNumber].push(className);
}
// 收集高亮行 className
$.each(highlightLineOptions, function (type, opts) {
if (opts.range) {
var highlightLines = parseHighlightedLines(opts.range);
if (highlightLines.length > 0) {
hasHighlightLine = true;
if (opts.classActivePre) {
preClassMap[opts.classActivePre] = true;
}
$.each(highlightLines, function (i, lineNumber) {
updateLineClassMap(lineNumber, opts.classActiveLine);
});
}
}
if (opts.comment) {
needParseComment = true;
}
});
// 解析行高亮注释并收集 className
if (needParseComment) {
$.each(codeLines, function (i, line) {
var match = line.match(highlightLineRegex);
if (match && match[1] && lay.hasOwn(highlightLineOptions, match[1])) {
var opts = highlightLineOptions[match[1]];
hasHighlightLine = true;
if (opts.classActivePre) {
preClassMap[opts.classActivePre] = true;
}
// 高亮的行数
var lines = parseInt(match[2], 10)
if (match[2] && lines && lines > 1) {
var startLine = i + 1;
var endLine = startLine + lines - 1;
var highlightLines = parseHighlightedLines(startLine + '-' + endLine);
if (highlightLines.length > 0) {
$.each(highlightLines, function (i, lineNumber) {
updateLineClassMap(lineNumber, opts.classActiveLine);
});
}
}else{
updateLineClassMap(i + 1, opts.classActiveLine);
}
}
});
}
return {
needParseComment: needParseComment,
hasHighlightLine: hasHighlightLine,
preClass: Object.keys(preClassMap).join(' '),
lineClassMap: lineClassMap
}
}
// export api
exports('code', function(options, mode){
options = $.extend(true, {}, config, options);
@ -142,10 +251,16 @@ layui.define(['lay', 'i18n', 'util', 'element', 'tabs', 'form'], function(export
// code 行
var lines = String(html).split(/\r?\n/g);
// 预处理行高亮
var highlightLineInfo = preprocessHighlightLine(options.highlightLine, lines);
// 包裹 code 行结构
html = $.map(lines, function(line, num) {
var lineClass = (highlightLineInfo.hasHighlightLine && highlightLineInfo.lineClassMap[num + 1])
? highlightLineInfo.lineClassMap[num + 1].join(' ')
: CONST.ELEM_LINE;
return [
'<div class="'+ CONST.ELEM_LINE +'">',
'<div class="'+ lineClass + '">',
(
options.ln ? [
'<div class="'+ CONST.ELEM_LINE_NUM +'">',
@ -154,12 +269,16 @@ layui.define(['lay', 'i18n', 'util', 'element', 'tabs', 'form'], function(export
].join('') : ''
),
'<div class="layui-code-line-content">',
(line || ' '),
(highlightLineInfo.needParseComment ? line.replace(highlightLineRegex, '') : line) || ' ',
'</div>',
'</div>'
].join('');
});
if(highlightLineInfo.preClass){
othis.addClass(highlightLineInfo.preClass)
}
return {
lines: lines,
html: html