PrismJS源码解读 - 插件篇 - prism-line-number

!!!警告:本文中的例子依赖于博主自己的博客,如果希望查看细节可移步至 我的博客!!!

引言

总有人拿着火炬在黑夜里探索未知。
感谢@FFFF为我们介绍了markdown-it的源码细节,同时展示了vuepressmarkdown-it的应用。这深深刺激和鼓舞了我。
——谨以此篇献给像@FFFF一样的先驱者。

介绍

写作本篇文章的契机是在制作博客的fence(代码块)换行插件的时候,发现了一些问题,由于bug已经改进,无法重现。

if (parent && parent.nodeName.toLowerCase() === 'pre' && !parent.hasAttribute('tabindex')) {
				parent.setAttribute('tabindex', '0');
			}

请先确认右上角换行按钮处于开启的状态(深色),接着你应该很容易注意到每个行号不仅仅只占据自己的一行,它包括了换行在内的所有行。在prism中称作软换行。但是vuepress的原生插件并没有实现类似的功能。偶然的契机让我发现了prism的这个特性,毫不犹豫的选择替换掉vuepress(I’m sorry)

vuepress的局限

上面已经介绍了vuepress插件不能实现软换行的功能,当强制换行时,总行数会大于有限的行号。这实际上是vuepress插件的实现方式决定的。
vuepress是基于markdown-itrender函数(你可以理解为一种插件,实际上插件是基于render来提供功能的),它会操作html元素被挂载前的字符串,因而限制了插件的功能,比如不能实现软换行。

prism的优点

prism同时也拥有自己的一套插件系统,它定义了一些hook,表示将代码渲染到页面上的生命周期。用户可以自己去定义生命周期,这里也存在一些内置的。
然而prism竟然在文档里没有提供内置生命周期的说明!这是我无法忍受的。我隐约听见了犹大的哪句名言:

为什么我要浪费自己陪家人的时间为你节省学英语的时间。

prism官方的

当然,要了解使用哪些钩子,您必须阅读 Prism 的源代码。

不能说很像只能说一模一样吧。

生命周期

在这里插入图片描述

prism的内置生命周期大致有

  • before-sanity-check
  • before-highlight
  • before-insert
  • after-highlight
  • complete

具体的流程关系在上图已经给出,在prism的hook的回调函数中,我们会获得一个env对象,在内置的函数中,它记录了一下信息来辅助我们开发

var env = {
				element: element, // 包裹code的element元素,有事它是code本身。
				language: language, // 语言字符串,比如在当前fence里面,它表示js
				grammar: grammar, // 暂时不会,hh
				code: code, // code的原来的contentText
  				highlightedCode: highlightedCode; // 解析后的html字符串
			};

值得注意的是highlightedCode属性只有在after-highlight生命周期后(包括)才会拥有。
具体的源码在prism-code.js的562行可以查看。
总之我们今天的主角就是通过了complete这个hook来实现相关功能,after-highlightcomplete中,解析后html已经挂载到元素上,我们可以进行更多的操控权,超越markdown-it。(无限鞭尸ing)
你可能会疑惑prism的hook是如何工作的,官方文档已经给出很详细的描述,在此不做赘述。

进入源码

粗看功能

为了节约篇幅,完整代码在此不再放出,有兴趣可以去github上查看完整代码
Prism.plugins.lineNumbers包含一系列方法,这些方法一起构成了其功能。

/**
		 * 通过指定下标的span元素(代表某一行)
		 *
		 * @param {Element} element pre元素(包裹容器)
		 * @param {number} number 行号
		 * @returns {Element|undefined} 返回span元素或undefined
		 */
		getLine: function (element, number)

        /**
	 * 调整给定元素(行号)的高度,通常在窗体变化时调用
	 *
	 * @param {HTMLElement[]} elements 由pre元素组成的数组
	 */
	function resizeElements(elements)
            /**
		 * 为指定的pre元素调整其行号元素的高度
		 *
		 * 这个方法不会添加行号,它只会调整给定的唯一的pre元素的行号大小
		 *
		 * @param {HTMLElement} element 一个pre元素(容器)
		 * @returns {void}
		 */
		resize: function (element) {
			resizeElements([element]);
		},

还存在着许多的工具方法,在此不一一赘述,getLine方法甚虽然被定义了,但是在代码中却没有被使用,所以忽略不计,resize的本质实际上是调用了resizeElements函数,只是将给定的element包装成数组给到resizeElements。可以看到resizeElements是插件的核心,本文主要介绍其原理。

	/* 源码中resize的定义 */
resize: function (element) {
			resizeElements([element]);
		},

关键函数 resizeElements

	/** resizeElements的定义
	 * Resizes the given elements.
	 *
	 * @param {HTMLElement[]} elements
	 */
	function resizeElements(elements) {
		elements = elements.filter(function (e) {
			var codeStyles = getStyles(e);
			var whiteSpace = codeStyles['white-space'];
			return whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line';
		});

		if (elements.length == 0) {
			return;
		}

		var infos = elements.map(function (element) {
			var codeElement = element.querySelector('code');
			var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
			if (!codeElement || !lineNumbersWrapper) {
				return undefined;
			}

			/** @type {HTMLElement} */
			var lineNumberSizer = element.querySelector('.line-numbers-sizer');
			var codeLines = codeElement.textContent.split(NEW_LINE_EXP);

			if (!lineNumberSizer) {
				lineNumberSizer = document.createElement('span');
				lineNumberSizer.className = 'line-numbers-sizer';

				codeElement.appendChild(lineNumberSizer);
			}

			lineNumberSizer.innerHTML = '0';
			lineNumberSizer.style.display = 'block';

			var oneLinerHeight = lineNumberSizer.getBoundingClientRect().height;
			lineNumberSizer.innerHTML = '';

			return {
				element: element,
				lines: codeLines,
				lineHeights: [],
				oneLinerHeight: oneLinerHeight,
				sizer: lineNumberSizer,
			};
		}).filter(Boolean);

		infos.forEach(function (info) {
			var lineNumberSizer = info.sizer;
			var lines = info.lines;
			var lineHeights = info.lineHeights;
			var oneLinerHeight = info.oneLinerHeight;

			lineHeights[lines.length - 1] = undefined;
			lines.forEach(function (line, index) {
				if (line && line.length > 1) {
					var e = lineNumberSizer.appendChild(document.createElement('span'));
					e.style.display = 'block';
					e.textContent = line;
				} else {
					lineHeights[index] = oneLinerHeight;
				}
			});
		});

		infos.forEach(function (info) {
			var lineNumberSizer = info.sizer;
			var lineHeights = info.lineHeights;

			var childIndex = 0;
			for (var i = 0; i < lineHeights.length; i++) {
				if (lineHeights[i] === undefined) {
					lineHeights[i] = lineNumberSizer.children[childIndex++].getBoundingClientRect().height;
				}
			}
		});

		infos.forEach(function (info) {
			var lineNumberSizer = info.sizer;
			var wrapper = info.element.querySelector('.line-numbers-rows');

			lineNumberSizer.style.display = 'none';
			lineNumberSizer.innerHTML = '';

			info.lineHeights.forEach(function (height, lineNumber) {
				wrapper.children[lineNumber].style.height = height + 'px';
			});
		});
	}

可以看到resizeElements由一个个循环组成,且这些方法的循环是线性的,下一个代码会继承上一个代码的副作用。
codeStyles是个工具函数,获取pre元素的white-space属性,筛出
white-space属性满足等于pre-wrappre-line的pre容器。
附一段mdn对white-space的描述:

pre-wrap
连续的空白符会被保留。在遇到换行符或
元素时,或者根据填充行框盒子的需要换行。
pre-line
连续的空白符会被合并。在遇到换行符或
元素时,或者根据填充行框盒子的需要换行。

可见这两个属性会导致软换行
这里的逻辑和prism在介绍这个插件时也一致

要使用软换行支持多行行号,请应用 CSS 或所需的 .white-space: pre-line;white-space: pre-wrap;

 
 

信息收集
var infos = elements.map(function (element) {
			var codeElement = element.querySelector('code');
			var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
			if (!codeElement || !lineNumbersWrapper) {
				return undefined;
			}

			/** @type {HTMLElement} */
			var lineNumberSizer = element.querySelector('.line-numbers-sizer');
			var codeLines = codeElement.textContent.split(NEW_LINE_EXP);

			if (!lineNumberSizer) {
				lineNumberSizer = document.createElement('span');
				lineNumberSizer.className = 'line-numbers-sizer';

				codeElement.appendChild(lineNumberSizer);
			}

			lineNumberSizer.innerHTML = '0';
			lineNumberSizer.style.display = 'block';

			var oneLinerHeight = lineNumberSizer.getBoundingClientRect().height;
			lineNumberSizer.innerHTML = '';

			return {
				element: element,
				lines: codeLines,
				lineHeights: [],
				oneLinerHeight: oneLinerHeight,
				sizer: lineNumberSizer,
			};
		}).filter(Boolean);

现在介绍第一个循环的作用,先看整体
elements.map(func).filter(Boolean)这里filter的作用是对每个元素执行Boolean函数,如果前一个map后返回undefined则结果为假,就会被筛掉,否则保留。
再看代码内部,函数最终返回了一个对象,里面记录了每个pre的一系列信息。

{
	element: element,
    lines: codeLines,
    lineHeights: [],
    oneLinerHeight: oneLinerHeight,
    sizer: lineNumberSizer,
};

element不难理解,就是我们的pre元素,lineHeights暂时为空,后续会存储每个行号的高度信息lines是通过codeElement.textContent.split(NEW_LINE_EXP);
获得的,codeElement就是code元素,NEW_LINE_EXP是正则
var NEW_LINE_EXP = /\n(?!$)/g;
表示匹配除了最后一个换行符的所有换行符,lines就是每一行所包含的代码文本。
sizer是一个被添加进code元素里的span.line-numbers-sizer标签,它的display为block,contentText为0,是为了模拟单行span。
oneLinerHeight是sizer的高度,是模拟单行高度。
这里有一个非常巧妙的点。首先我们知道,经过prism解析的html标签并非会给每个token(文本元素)用span包裹起来,因此我们设置每一行的高度通常是通过设置文本的line-height来实现的,从而确保每一行的元素一样高。
然后此时span的display为inline,故不包含当前line-height的高度。但是了解每一行的高度对于计算我们的行号高度是很重要的,如何实现呢?下面展示一段mdn关于line-height的描述

line-height CSS 属性用于设置多行元素的空间量,如多行文本的间距。对于块级元素,它指定元素行盒(line boxes)的最小高度。对于非替代的 inline 元素,它用于计算行盒(line box)的高度

简单来说,如果我们想要获得line-height效果下的高度,就需要通过一个块级元素来获取,prism就是这样做的,它将sizer设置为块级元素从而获取一行中line-height效果下的高度。
有了每个pre容器的信息就可以进入下一步了。

计算行高
infos.forEach(function (info) {
			var lineNumberSizer = info.sizer;
			var lines = info.lines;
			var lineHeights = info.lineHeights;
			var oneLinerHeight = info.oneLinerHeight;

			lineHeights[lines.length - 1] = undefined;
			lines.forEach(function (line, index) {
				if (line && line.length > 1) {
					var e = lineNumberSizer.appendChild(document.createElement('span'));
					e.style.display = 'block';
					e.textContent = line;
				} else {
					lineHeights[index] = oneLinerHeight;
				}
			});
		});

lineHeights[lines.length - 1] = undefined;定义了行数一样多的数组。
接着遍历lines(每行的文本信息),如果存在且长度大于1,则添加进lineNumberSizer这个span中。
其他情况则记录lineHeights对应下标的值为oneLinerHeight,即一行的高度,这是因为只有单个字符或空字符时认为绝对不可能存在换行的情况,故很容易的记录了当前行号的高度。
这一步连同下一步其实就是为了获取每行的高度,接下来还要对无法判定是否换行的元素进行行高计算。

infos.forEach(function (info) {
			var lineNumberSizer = info.sizer;
			var lineHeights = info.lineHeights;

			var childIndex = 0;
			for (var i = 0; i < lineHeights.length; i++) {
				if (lineHeights[i] === undefined) {
					lineHeights[i] = lineNumberSizer.children[childIndex++].getBoundingClientRect().height;
				}
			}
		});

实际上这里就很简单了,将无法计算行高的元素插入,然后读取其高度,注意上面的代码里面有两句代码很精辟
e.style.display = 'block';
e.textContent = line;
第一个已经解释过了,是为了获取line-height影响下的高度
第二个是将当前行内容给textContent。
其实就是去模拟当前行,但是因为是块级元素,就可以获得当前行的高度了。

赋予高度
infos.forEach(function (info) {
			var lineNumberSizer = info.sizer;
			var wrapper = info.element.querySelector('.line-numbers-rows');

			lineNumberSizer.style.display = 'none';
			lineNumberSizer.innerHTML = '';

			info.lineHeights.forEach(function (height, lineNumber) {
				wrapper.children[lineNumber].style.height = height + 'px';
			});
		});

这段代码做的是一个清理的工作,lineNumberSizer下面的元素只是模拟的,为了获取每行高度并记录在lineHeights数组中,真正的行号元素是在.line-numbers-rows这个元素下面的。

调用

Prism.hooks.add('complete', function (env) {
		if (!env.code) {
			return;
		}

		var code = /** @type {Element} */ (env.element);
		var pre = /** @type {HTMLElement} */ (code.parentNode);

		// works only for <code> wrapped inside <pre> (not inline)
		if (!pre || !/pre/i.test(pre.nodeName)) {
			return;
		}

		// Abort if line numbers already exists
		if (code.querySelector('.line-numbers-rows')) {
			return;
		}

		// only add line numbers if <code> or one of its ancestors has the `line-numbers` class
		if (!Prism.util.isActive(code, PLUGIN_NAME)) {
			return;
		}

		// Remove the class 'line-numbers' from the <code>
		code.classList.remove(PLUGIN_NAME);
		// Add the class 'line-numbers' to the <pre>
		pre.classList.add(PLUGIN_NAME);

		var match = env.code.match(NEW_LINE_EXP);
		var linesNum = match ? match.length + 1 : 1;
		var lineNumbersWrapper;

		var lines = new Array(linesNum + 1).join('<span></span>');

		lineNumbersWrapper = document.createElement('span');
		lineNumbersWrapper.setAttribute('aria-hidden', 'true');
		lineNumbersWrapper.className = 'line-numbers-rows';
		lineNumbersWrapper.innerHTML = lines;

		if (pre.hasAttribute('data-start')) {
			pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
		}

		env.element.appendChild(lineNumbersWrapper);

		resizeElements([pre]);

		Prism.hooks.run('line-numbers', env);
	});

	Prism.hooks.add('line-numbers', function (env) {
		env.plugins = env.plugins || {};
		env.plugins.lineNumbers = true;
	});

介绍完了插件的所有功能,到了调用时刻了。
前面介绍了lineNumberSizer下面的元素只是模拟的,故这里会插入真正的行号元素,linesNum 记录了行号总数,lineNumbersWrapper即行号的容器,env.element.appendChild(lineNumbersWrapper);再将容器添加进code元素(前面已经给出env的属性),最后调用resizeElements对行号的高度进行计算。
这里的'line-numbers'作为hook的作用是标记env的plugins.lineNumbers为true,表示lineNumber这个插件被成功的调用了,是env的通信手段之一。

写在最后

接下来我可能会去探索高亮插件的源码,希望持续追更我的文章。
希望我也可以能手握火把,探索黑夜。

  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WPF Prism是微软新一代的界面开发框架,它具有丰富的功能和灵活的架构,广泛应用于桌面应用程序和Web应用程序的开发中。朝夕停车场项目是基于WPF Prism框架的一个实战项目,它实现了停车场的智能化管理,包括车辆进入和出场的自动识别,车位的实时监测和调度,以及财务管理和数据分析等功能。 该项目的源码包含了多个模块和组件,采用了MVVM架构和依赖注入技术,使代码组织和维护非常方便。项目中的主要模块包括: 1. Shell模块:该模块是整个应用程序的容器,它提供了主窗口和导航栏等界面组件,以及对其他模块的管理和协调。 2. Home模块:该模块实现了停车场的实时监控和调度功能,包括车位的占用和空闲状态显示,车辆进出场的记录和管理,以及停车位的预定和预约等功能。 3. Financial模块:该模块实现了停车场的财务管理和数据分析功能,包括车位租赁、停车费用计算和缴纳,以及停车场运营数据的统计和分析等功能。 4. Configuration模块:该模块实现了停车场的基础配置和参数管理功能,包括车位数量、收费标准和系统设置等功能。 5. Common模块:该模块包含了一些公共的模型和工具类,用于提供系统级别的服务和支持。 通过这个实战项目的源码学习,可以深入了解WPF Prism框架的应用及其MVVM架构和依赖注入的设计思想,也可以了解如何实现一个完整的智能化停车场管理系统。同时,该项目源码可以作为一个参考,通过在此基础上进行二次开发和定制,实现更加具体化的应用需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值