Prism是一款非常好用的代码高亮的插件,最开始使用的时候很难找到文章去分析这个库,所以我尝试来介绍一下这个库。
特点
1. 简单
2. 支持webworker,可以异步完成渲染
3. 轻小
4. 可扩展--语言可以自己选择,插件也可以自己选择
这是官网对其的一些描述,接下来我们来使用一下prismjs。
首先我们直接来到官网 prismjs.com/ ,然后点击download去下载
可以自己选择配置,提供了很多插件和语言,使用起来也是非常方便。
<!DOCTYPE html>
<html>
<head>
...
<link href="themes/prism.css" rel="stylesheet" />
</head>
<body>
<code class="language-css">
div{
width:100px;
}
</code>
<script src="prism.js"></script>
</body>
</html>
1. 选择好自己的配置,下载css和js,引入进来,创建code标签---添加class属性,value为lang-语言名称,然后就生效了。
2. 细节会在后面分析,默认会进行全局访问,调用Prism.highlightAll(false)自动寻找code标签与class为lang-xxx的元素
具体使用
//下载的prism,默认会绑定在window环境中
//1.首先默认会调用highlightAll方法,但是我们在使用过程中,肯定是希望手动调用的
window.Prism = window.Prism || {};
window.Prism.manual = true;
//这样就可以开始手动调用,切忌上述代码一定要出现在prism.js前面,否则都执行完了。
//但是这样子肯定是一点都没有用的,需要在引入script src=“prism”之后在调用
Prism.highlightAll(false)
不用担心prism.js放在<code></code>前引入会导致后面的code标签不生效,默认情况下即
window.Prism.manual为false,prism会检测document是否加载完成--加载完成在进行调用
具体源码如下:
if (!_.manual) {
var readyState = document.readyState;
if (readyState === 'loading' || readyState === 'interactive' && script && script.defer) {
document.addEventListener('DOMContentLoaded', highlightAutomaticallyCallback);
} else {
if (window.requestAnimationFrame) {
window.requestAnimationFrame(highlightAutomaticallyCallback);
} else {
window.setTimeout(highlightAutomaticallyCallback, 16);
}
}
}
自动引入很省事,但是发现使用起来其实还是存在很多的问题。
存在的问题
- 如果我要异步高亮该怎么办?
- 我不希望所有document里面的code要高亮,该怎么办?
- 如果我在某些代码高亮的过程中想要处理函数该怎么办?
- 开始使用的过程中,我不知道我要选择哪门语言怎么办,我想要继续扩展该怎么办?
解决上述问题
//问题一:异步高亮怎么处理?
//官方提供了webworker的使用方式
<script>
window.Prism=window.Prism||{}
Prism.manual=true
window.Prism.filename='worker.js'
Prism.highlightAll(true)
</script>
//webworker代码如下: 自己根据使用场景去配置
this.onmessage=function(env){
console.log(env)
}
postMessage(JSON.stringify("const a=1000000;"))
//问题二:我不希望document里面的code要高亮,怎么办?
//可能有些人会说,code里面你不写class=lang-xxx不就行了吗,但是有时候我们写了我们也不希望它渲染
//我们不调用highlightAll方法去调用highlightAllUnder
highlightAllUnder(container, async, callback);
//container即为想要渲染的父元素,async是否使用异步处理,callback即highlight完成的回调
//选择highlightAllUnder的好处是,我们像调用哪里就调用哪里,多调用几次就好了
//问题三:prism其实也为我们提供了很多钩子函数,切忌(所有的高阶玩法和使用要手动调用)
我列举一些钩子函数
before-highlightall
before-all-elements-highlight
before-insert
after-highlight
complete
before-sanity-check
before-highlight
后续会在源码中看到,这里不会介绍怎么使用,源码中会看到
//怎么使用呢?
Prism.hooks.add('before-highlightall', (env)=>{
console.log('before-highlightall',env)
});
就是这么简单引入
问题四:
<script src="https://prismjs@v1.x/plugins/autoloader/prism-autoloader.min.js"></script>
引入autoloader插件即可完成处理,在download时引入即可
其实说到这里具体功能也说的差不多了,再说一点其他的吧,之后直接上源码
- prismjs不一定非要download使用,npm下载 import prsim from 'prismjs',也提供了相关插件,让其可以按需加载
- node也可以使用
const Prism = require('prismjs');
// The code snippet you want to highlight, as a string
const code = `var data = 1;`;
// Returns a highlighted HTML string
const html = Prism.highlight(code, Prism.languages.javascript, 'javascript');
源码解读
怎么看源码呢?直接github拉下来代码,对就是这么简单,so easy,然后自己看就完了,本篇文章结束。开玩笑的,下面开始代码分析----源码直接github拉吧,主要是我也懒得发我的注释版本了。。。。 代码核心就是在commponent/prism-core.js中,把这个看完了,你就懂了。
//首先,我们先看最开始放上去的代码作为入口使用
if (!_.manual) {
var readyState = document.readyState;
if (readyState === 'loading' || readyState === 'interactive' && script && script.defer) {
document.addEventListener('DOMContentLoaded', highlightAutomaticallyCallback);
} else {
if (window.requestAnimationFrame) {
window.requestAnimationFrame(highlightAutomaticallyCallback);
} else {
window.setTimeout(highlightAutomaticallyCallback, 16);
}
}
}
//这段确实没啥可说的,无非就是load完事之后加载,看highlightAutomaticallyCallback
highlightAll((async, callback)
highlightAutomaticallyCallback 就是调用highlightAll() 这里不放源码,浪费空间
highlightAll: function (async, callback) {
_.highlightAllUnder(document, async, callback);
}
//这里都是废代码,看了这我们只能知道为啥默认渲染所有html
highlightAllUnder(container, async, callback)
highlightAllUnder: function (container, async, callback) {
//env 很关键,所有信息都放在env中
var env = {
callback: callback,
container: container,
selector: 'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'
};
//触发钩子函数,函数的第一个参数为env
_.hooks.run('before-highlightall', env);
env.elements = Array.prototype.slice.apply(env.container.querySelectorAll(env.selector));
_.hooks.run('before-all-elements-highlight', env);
for (var i = 0, element; (element = env.elements[i++]);) {
_.highlightElement(element, async === true, env.callback);
}
}
highlightElement(element, async, callback)
highlightElement: function (element, async, callback) {
//通过正则拿到对应的language,grammmar--选择对应语言的语法
var language = _.util.getLanguage(element);
var grammar = _.languages[language];
//设置元素上的语言(如果不存在)
_.util.setLanguage(element, language);
var parent = element.parentElement;
if (parent && parent.nodeName.toLowerCase() === 'pre') {
//为父元素设置语言
_.util.setLanguage(parent, language);
}
//拿到文本
var code = element.textContent;
var env = {
element: element,
language: language,
grammar: grammar,
code: code
};
//把原来的文本,替换成高亮元素
function insertHighlightedCode(highlightedCode) {
env.highlightedCode = highlightedCode;
_.hooks.run('before-insert', env);
env.element.innerHTML = env.highlightedCode;
_.hooks.run('after-highlight', env);
_.hooks.run('complete', env);
callback && callback.call(env.element);
}
_.hooks.run('before-sanity-check', env);
parent = env.element.parentElement;
if (parent && parent.nodeName.toLowerCase() === 'pre' && !parent.hasAttribute('tabindex')) {
//按tab切换到对应父元素标签
parent.setAttribute('tabindex', '0');
}
//没有文本直接执行回调和钩子函数
if (!env.code) {
_.hooks.run('complete', env);
callback && callback.call(env.element);
return;
}
_.hooks.run('before-highlight', env);
//没有语法---直接替换元素即可
if (!env.grammar) {
insertHighlightedCode(_.util.encode(env.code));
return;
}
//如果异步,走这里
if (async && _self.Worker) {
var worker = new Worker(_.filename);
worker.onmessage = function (evt) {
insertHighlightedCode(evt.data);
};
worker.postMessage(JSON.stringify({
language: env.language,
code: env.code,
immediateClose: true
}));
} else {
insertHighlightedCode(_.highlight(env.code, env.grammar, env.language));
}
}
highlight
highlight: function (text, grammar, language) {
var env = {
code: text,
grammar: grammar,
language: language
};
_.hooks.run('before-tokenize', env);
env.tokens = _.tokenize(env.code, env.grammar);
_.hooks.run('after-tokenize', env);
return Token.stringify(_.util.encode(env.tokens), env.language);
},
tokenize
//tokenize--创建链表 调用matchGrammar
tokenize: function (text, grammar) {
var rest = grammar.rest;
if (rest) {
for (var token in rest) {
grammar[token] = rest[token];
}
delete grammar.rest;
}
//创建链表
var tokenList = new LinkedList();
//从头到后添加链表
addAfter(tokenList, tokenList.head, text);
matchGrammar(text, tokenList, grammar, tokenList.head, 0);
return toArray(tokenList);
}
链表操作
function LinkedList() {
var head = { value: null, prev: null, next: null };
var tail = { value: null, prev: head, next: null };
head.next = tail;
this.head = head;
this.tail = tail;
this.length = 0;
}
//向后添加链表
function addAfter(list, node, value) {
// assumes that node != list.tail && values.length >= 0
var next = node.next;
var newNode = { value: value, prev: node, next: next };
node.next = newNode;
next.prev = newNode;
list.length++;
return newNode;
}
matchGrammar
function matchGrammar(text, tokenList, grammar, startNode, startPos, rematch) {
for (var token in grammar) {
if (!grammar.hasOwnProperty(token) || !grammar[token]) {
continue;
}
var patterns = grammar[token];
patterns = Array.isArray(patterns) ? patterns : [patterns];
for (var j = 0; j < patterns.length; ++j) {
if (rematch && rematch.cause == token + ',' + j) {
return;
}
var patternObj = patterns[j];
var inside = patternObj.inside;
var lookbehind = !!patternObj.lookbehind;
var greedy = !!patternObj.greedy;
var alias = patternObj.alias;
if (greedy && !patternObj.pattern.global) {
// Without the global flag, lastIndex won't work
var flags = patternObj.pattern.toString().match(/[imsuy]*$/)[0];
patternObj.pattern = RegExp(patternObj.pattern.source, flags + 'g');
}
/** @type {RegExp} */
var pattern = patternObj.pattern || patternObj;
for ( // iterate the token list and keep track of the current token/string position
var currentNode = startNode.next, pos = startPos;
currentNode !== tokenList.tail;
pos += currentNode.value.length, currentNode = currentNode.next
) {
if (rematch && pos >= rematch.reach) {
break;
}
var str = currentNode.value;
if (tokenList.length > text.length) {
// Something went terribly wrong, ABORT, ABORT!
return;
}
if (str instanceof Token) {
continue;
}
var removeCount = 1; // this is the to parameter of removeBetween
var match;
if (greedy) {
match = matchPattern(pattern, pos, text, lookbehind);
if (!match || match.index >= text.length) {
break;
}
var from = match.index;
var to = match.index + match[0].length;
var p = pos;
// find the node that contains the match
p += currentNode.value.length;
while (from >= p) {
currentNode = currentNode.next;
p += currentNode.value.length;
}
// adjust pos (and p)
p -= currentNode.value.length;
pos = p;
// the current node is a Token, then the match starts inside another Token, which is invalid
if (currentNode.value instanceof Token) {
continue;
}
// find the last node which is affected by this match
for (
var k = currentNode;
k !== tokenList.tail && (p < to || typeof k.value === 'string');
k = k.next
) {
removeCount++;
p += k.value.length;
}
removeCount--;
// replace with the new match
str = text.slice(pos, p);
match.index -= pos;
} else {
match = matchPattern(pattern, 0, str, lookbehind);
if (!match) {
continue;
}
}
// eslint-disable-next-line no-redeclare
var from = match.index;
var matchStr = match[0];
var before = str.slice(0, from);
var after = str.slice(from + matchStr.length);
var reach = pos + str.length;
if (rematch && reach > rematch.reach) {
rematch.reach = reach;
}
var removeFrom = currentNode.prev;
if (before) {
removeFrom = addAfter(tokenList, removeFrom, before);
pos += before.length;
}
removeRange(tokenList, removeFrom, removeCount);
var wrapped = new Token(token, inside ? _.tokenize(matchStr, inside) : matchStr, alias, matchStr);
currentNode = addAfter(tokenList, removeFrom, wrapped);
if (after) {
addAfter(tokenList, currentNode, after);
}
if (removeCount > 1) {
// at least one Token object was removed, so we have to do some rematching
// this can only happen if the current pattern is greedy
/** @type {RematchOptions} */
var nestedRematch = {
cause: token + ',' + j,
reach: reach
};
matchGrammar(text, tokenList, grammar, currentNode.prev, pos, nestedRematch);
// the reach might have been extended because of the rematching
if (rematch && nestedRematch.reach > rematch.reach) {
rematch.reach = nestedRematch.reach;
}
}
}
}
}
}
matchGrammar 是真正的核心代码,本质逻辑就是通过正则的exec方法完成判断。
至于代码为什么逻辑很乱,是因为inside和greedy,inside是子逻辑,正则里面还需要子正则去判断。
greedy的逻辑是,因为是按优先级判断的,但是有特例,如拿优先级最高的注释来说,如果注释在字符串中,那逻辑就不匹配了