最近,我对如何从内部排列代码突出显示感兴趣。 最初,那里似乎一切都变得非常复杂-语法树,递归,仅此而已。 但是,经过仔细检查,结果发现这里没有困难。 所有工作都可以在一个周期内来回窥视,而且,几乎不会在结果脚本中使用正则表达式。
演示页:
Javascript代码突出显示器主要思想
我们声明
状态变量,该变量将存储有关我们所处代码部分的信息。 例如,如果
状态等于1,则意味着我们位于带单引号的字符串中。 该脚本将等待右引号,并忽略其他所有内容。 带有高亮注释,正则表达式和其他元素的同一事物,每个都有其自己的
状态值。 因此,不同的开头和结尾字符不会发生冲突; 换句话说,像这样的代码:
let a = '"\'"';
将正确地突出显示,即此类情况造成的最大困难。
开始使用
我们确定状态变量的可能值,以及代码的该部分或该部分的绘制颜色,以及Javascript关键字列表(也将突出显示):
const状态= {... const states = { NONE : 0, SINGLE_QUOTE : 1,
接下来,我们创建一个函数,该函数将一行代码插入并返回带有突出显示代码的HTML代码。 为了突出显示,字符将使用
colors变量中指定的
颜色包装在SPAN中。
该功能只有一个循环,可以分析每个字符并在必要时添加打开/关闭SPAN。
function highlight(code) { let output = ''; let state = states.NONE; for (let i = 0; i < code.length; i++) { let char = code[i], prev = code[i-1], next = code[i+1];
首先,突出显示注释:单行和多行。 如果当前字符和下一个字符是斜杠,并且它们不在行内(
状态为0,即
states.NONE ),则这是注释的开始。 更改
状态并使用所需的颜色打开SPAN:
if (state == states.NONE && char == '/' && next == '/') { state = states.SL_COMMENT; output += '<span style="color: ' + colors.SL_COMMENT + '">' + char; continue; }
需要
继续 ,以便以下检查不起作用并且不会发生冲突。
接下来,我们等待行的结尾:如果当前字符是换行符,并且
状态为单行注释,请关闭SPAN并将
状态更改为零:
if (state == states.SL_COMMENT && char == '\n') { state = states.NONE; output += char + '</span>'; continue; }
同样,我们正在寻找多行注释,算法完全相同,只是要寻找的字符不同:
if (state == states.NONE && char == '/' && next == '*') { state = states.ML_COMMENT; output += '<span style="color: ' + colors.ML_COMMENT + '">' + char; continue; } if (state == states.ML_COMMENT && char == '/' && prev == '*') { state = states.NONE; output += char + '</span>'; continue; }
字符串的突出显示以类似的方式发生,只是必须考虑到可以用反斜杠将右引号转义,因此,它已经不再是反斜杠了。
if (state == states.NONE && char == '\'') { state = states.SINGLE_QUOTE; output += '<span style="color: ' + colors.SINGLE_QUOTE + '">' + char; continue; } if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
该代码类似于上面的代码,只是现在如果引号前面有反斜杠,我们就不注册行尾。
双引号字符串的定义以完全相同的方式发生,因此详细分析它们几乎没有意义。 为了完成图片,我将它们放在扰流板下方。
如果(状态==状态。无&&字符==''''){... if (state == states.NONE && char == '"') { state = states.DOUBLE_QUOTE; output += '<span style="color: ' + colors.DOUBLE_QUOTE + '">' + char; continue; } if (state == states.DOUBLE_QUOTE && char == '"' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; } if (state == states.NONE && char == '`') { state = states.ML_QUOTE; output += '<span style="color: ' + colors.ML_QUOTE + '">' + char; continue; } if (state == states.ML_QUOTE && char == '`' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
容易与分隔符号混淆的正则表达式文字值得单独考虑。 我们将在本文的结尾处回到这个问题,但是现在我们正使用regexp和字符串进行相同的操作。
if (state == states.NONE && char == '/') { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; } if (state == states.REGEX_LITERAL && char == '/' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
当文字的开头和结尾可以由1-2个字符确定时,这结束了简单的情况。 让我们开始突出显示数字:如您所知,它们总是以数字开头,但是在组合中可以包含字母(
0xFF ,
123n )。
if (state == states.NONE && /[0-9]/.test(char) && !/[0-9a-z$_]/i.test(prev)) { state = states.NUMBER_LITERAL; output += '<span style="color: ' + colors.NUMBER_LITERAL + '">' + char; continue; } if (state == states.NUMBER_LITERAL && !/[0-9a-fnx]/i.test(char)) { state = states.NONE; output += '</span>' }
在这里,我们正在寻找数字的开头:前一个字符不应为数字或字母,否则变量名称中的数字将突出显示。 一旦当前字符不是数字或数字文字中可以包含的字母,请关闭SPAN并将
状态设置为零。
突出显示所有可能的文字类型,保留对关键字的搜索。 为此,您需要一个嵌套循环,该循环可以向前看并确定当前字符是否是关键字的开头。
if (state == states.NONE && !/[a-z0-9$_]/i.test(prev)) { let word = '', j = 0; while (code[i + j] && /[az]/i.test(code[i + j])) { word += code[i + j]; j++; } if (keywords.includes(word)) { state = states.KEYWORD; output += '<span style="color: ' + colors.KEYWORD + '">'; } }
在这里我们看到,前一个字符不能在变量名中,否则,因为关键字将在单词
outlet中突出显示。 然后,嵌套循环收集可能的最长单词,直到遇到非字母字符为止。 如果收到的单词在
关键字数组中,则打开SPAN并开始突出显示该单词。 一旦遇到非字母字符,这意味着单词的结尾-因此,请关闭SPAN:
if (state == states.KEYWORD && !/[az]/i.test(char)) { state = states.NONE; output += '</span>'; }
最简单的事情仍然是-突出显示运算符,在这里您可以简单地与运算符中可能出现的字符集进行比较:
if (state == states.NONE && '+-/*=&|%!<>?:'.indexOf(char) != -1) { output += '<span style="color: ' + colors.OPERATOR + '">' + char + '</span>'; continue; }
在循环结束时,如果没有触发任何
继续条件
继续原因,我们只需将当前字符添加到结果变量中即可。 当文字或关键字的开头或结尾出现时,我们用颜色打开/关闭SPAN。 在所有其他情况下-例如,当行已经打开时,我们一次只抛出一个字符。 还值得屏蔽打开的尖括号,否则它们可能会破坏布局。
output += char.replace('<', '&' + 'lt;');
错误修复
一切似乎都太简单了,没有白费:经过更彻底的测试,有时背光无法正常工作。
除法被认为是正则表达式,为了将一个与另一个区分开来,有必要改变确定正则表达式的方式。 我们声明变量
isRegex = true ,之后我们将尝试“证明”这不是regexp,而是除号。 除法运算之前不能有关键字或方括号-因此,我们创建了一个嵌套循环并查看斜杠所面对的内容。
和以前一样 if (state == states.NONE && char == '/') { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; }
if (state == states.NONE && char == '/') { let word = '', j = 0, isRegex = true; while (i + j >= 0) { j--;
尽管此方法解决了问题,但仍然并非没有缺陷。 您可以对其进行调整,以使该算法也无法正确突出显示,例如:
if(a)/ regex /或:
1 / / regex / / 2 。 为什么将数字分成正则表达式的人为什么需要代码突出显示? 该设计在语法上是正确的,尽管在现实生活中不会发生。
正则表达式着色在许多作品中都存在问题,例如,
prism.js中 。 显然,为了正确突出显示正则表达式,您必须像浏览器一样完全理解语法。
我必须处理的第二个错误与反斜杠有关。 由于形式
'test \\'的字符串前面没有反斜杠,因此无法识别该引号。 返回捕获行尾的条件:
if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\')
该条件的最后一部分需要更改:如果反斜杠已转义(即在其前面还有另一个反斜杠),则请注册该行的末尾。
const closingCharNotEscaped = prev != '\\' || prev == '\\' && code[i-2] == '\\';
在搜索带有双引号和反引号的字符串以及搜索regexp时,必须进行相同的替换。
就是这样,您可以通过本文开头的链接测试突出显示。