前端高亮插件--prismjs使用技巧--源码分析

4,183 阅读2分钟

Prism是一款非常好用的代码高亮的插件,最开始使用的时候很难找到文章去分析这个库,所以我尝试来介绍一下这个库。

特点

1. 简单
2. 支持webworker,可以异步完成渲染
3. 轻小
4. 可扩展--语言可以自己选择,插件也可以自己选择 

这是官网对其的一些描述,接下来我们来使用一下prismjs。

首先我们直接来到官网 prismjs.com/ ,然后点击download去下载

ATS3QE$)JY1IPD~F88@4A.png

可以自己选择配置,提供了很多插件和语言,使用起来也是非常方便。

<!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);
			}
		}
	}

自动引入很省事,但是发现使用起来其实还是存在很多的问题。

存在的问题

  1. 如果我要异步高亮该怎么办?
  2. 我不希望所有document里面的code要高亮,该怎么办?
  3. 如果我在某些代码高亮的过程中想要处理函数该怎么办?
  4. 开始使用的过程中,我不知道我要选择哪门语言怎么办,我想要继续扩展该怎么办?

解决上述问题

 //问题一:异步高亮怎么处理?
 //官方提供了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时引入即可

其实说到这里具体功能也说的差不多了,再说一点其他的吧,之后直接上源码

  1. prismjs不一定非要download使用,npm下载 import prsim from 'prismjs',也提供了相关插件,让其可以按需加载
  2. 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的逻辑是,因为是按优先级判断的,但是有特例,如拿优先级最高的注释来说,如果注释在字符串中,那逻辑就不匹配了