解析文本

先看一下模板:

 const template = '<div>Text</div>'

经过前面对标签的处理,当模板内容来到下面这个状态的时候:

const template = 'Text</div>'

parseText函数会尝试在这段模板内容中中找到第一个出现的字符<的位置索引。在上面的例子中,字符<的索引值为4。然后,parseText函数会截取介于索引[0,4)的内容作为文本内容。在上面这个例子中,文本内容就是字符串’Text’

假设模板中存在插值,如下面的模板所示:

const template = 'Text-{{val}}</div>'

在处理这段模板时,parseText 函数会找到第一个插值定界符{{出现的位置索引。
下面的parseText函数给出了具体实现

function parseText(context){// endIndex为文本内容的结尾索引,默认将整个模板剩余内容都作为文本内容let endIndex = context.source.length// 寻找字符<的位置索引const ltIndex = context.source.indexOf('<')// 寻找定界符 {{ 的位置索引const delimiterIndex = context.source.index0f('{{')// 取ltIndex和当前 endIndex 中较小的一个作为新的结尾索引if (ltIndex > -1 && ltIndex < endindex){endIndex= ltIndex }//取 delimiterIndex 和当前 endIndex 中较小的一个作为新的结尾索引if (delimiterIndex > -1 && delimiterIndex < endindex){endIndex= delimiterIndex }//此时 endIndex 是最终的文本内容的结尾索引,调用 slice 函数截取文本内容const content = context.source.slice(0,endIndex)// 消耗文本内容context.advanceBy(content.length)//返回文本节点return {type: 'Text',//文本内容content}
}

如上面的代码所示,由于字符<与定界符 {{ 的出现顺序是未知的,所以我们需要取两者中较小的一个作为文本截取的终点。最后,要创建一个类型为 Text 的文本节点,将其作为parseText 函数的返回值。

配合上述parseText函数解析如下模板:

const ast = parse( '<div>Text</div>' )

得到如下AST:

const ast = {type: 'Root',children: [{type: 'Element',tag: 'div',props: [],isSelfClosing: false,children: [// 文本节点{ type: 'Text', content: 'Text'}]}]
}

这样,就实现了对文本节点的解析。解析文本节点本身并不复杂,复杂点在于,需要对解析后的文本内容进行HTML实体的解码工作。为此,我们有必要先了解什么是HTML实体。

解码命名字符引用

HTML实体是一段以字符&开始的文本内容。实体用来描述HTML中的保留字符和一些难以通过普通键盘输入的字符,以及一些不可见的字符
例如,在HTML 中,字符<具有特殊含义,如果希望以普通文本的方式来显示字符<,需要通过实体来表达:

<div>A&lt;B</div>

其中字符串&lt;就是一个HTML实体,用来表示字符<。
HTML实体总是以字符&开头,以字符;结尾。

HTML实体有两类,一类叫作命名字符引用(named character reference),也叫命名实体。顾名思义,这类实体具有特定的名称,例如上文中的&lt;
除了命名字符引用之外,还有一类字符引用没有特定的名称,只能用数字表示,这类实体叫作数字字符引用。与命名字符用不同,数字字符引用以字符串&#开头,比命名字符引用的开头部分多出了字符#,例如<。实际上,<对应的字符也是<,换句话说,<&lt;是等价的。

因为Vue.js解析的文本节点所包含的HTML实体会被当作字符串不会被浏览器解析,因此Vue要对HTML实体进行处理。
具体实现的代码如下:

//第二个参数是一个布尔值,代表文本内容是否作为属性值
function decodeHtml(rawText, asAttr=false){let offset = 0const end = rawText.length//经过解码后的文本将作为返回值被返回let decodedText = ''//引用表中实体名称的最大长度let maxCRNameLength = 0// advance 函数用于消费指定长度的文本function advance(length){offset += lengthrawText = rawText.slice(length)}//消费字符串,直到处理完毕为止while(offset < end){// 用于匹配字符引用的开始部分,如果匹配成功,那么 head[0] 的值将有三种可能://1.head[0]==='&',这说明该字符引用是命名宇符引用//2.head[0] ==='&#',这说明该字符引用是用十进制表示的数字字符引用//3.head[0]==='&#x’,这说明该字符引用是用十六进制表示的数字字符引用const head = /&(?:#x?)?/i.exec(rawText)// 如果没有匹配,说明已经没有需要解码的内容了if(!head){// 计算剩余内容的长度const remaining = end - offset// 将剩余内容加到 decodedText 上decodedText += rawText.slice(0,remaining)// 消费剩余内容advance(remaining)break}// head.index为匹配的字符&在 rawText 中的位置索引// 截取字符&之前的内容加到 decodedText 上decodedText += rawText.slice(0,head.index)// 消费字符 &之前的内容advance(head.index)// 如果满足条件,则说明是命名字符引用,否则为数字字符引用if(head[0] === '&'){let name = ''let value// 字符&的下一个字符必须是 ASCII 字母或数字,这样才是合法的命名字符引用if(/[0-9a-z]/i.test(rawText[1])){// 根据引用表计算实体名称的最大长度if(!maxCRNameLength){maxCRNameLength = Object.keys(namedCharacterReference).reduce((max,name)=>Math.max(max,name.length),0)}//从最大长度开始对文本进行截取,并试图去引用表中找到对应的项for(let length = maxCRNameLegnth; !value && length>0;--length){// 截取字符 &到最大长度之间的字符作为实体名称name = rawText.substr(1, length)// 使用实体名称去索引表中查找对应项的值value = (namedCharacterReferences)[name]}// 如果找到了对应项的值,说明解码成功if(value){// 检查实体名称的最后一个匹配字符是否是分号const semi = name.endsWith(';')// 如果解码的文本作为属性值,最后一个匹配的字符不是分号,//并且最后一个匹配字符的下一个字符是等于号(=)、ASCII 宇母或数字//由于历史原因,将字符 &和实体名称 name 作为普通文本if(asAttr && !semi && /[=a-z0-9]/i.test(rawText[name.length + 1] || '')){decodedText += '&' + nameadvance(1+name.length)}else{// 其他情况下,正常使用解码后的内容拼接到 decodedText 上decodedText += valueadvance(1+name.length)}}else{// 如果没有找到对应的值,说明解码失败decodedText += '&'+nameadvance(1+name.length)}}else{//如果字符 &的下一个字符不是 ASCII字母或数字,则将字符 &作为普通文本decodedText += '&'advance(1)}}}return decodedText
}

有了 decodeHtml 函数之后,就可以在解析文本节点时通过它对文本内容进行解码:

function parseText(context){// 省略部分代码return {type: 'Text',content: decodeHtml(content)}
}

解码数字字符引用

在上一节中,使用下面的正则表达式来匹配一个文本中字符引用的开始部分。
我们可以根据该正则的匹配结果,来判断字符引用的类型。

数字字符引用的格式是:前缀+Unicode码点。解码数字字符引用的关键在于,如何提取字符引用中的 Unicode 码点。考虑到数字字符引用的前缀可以是以十进制表示(&#)也可以是以十六进制表示(&#x),所以我们使用下面的代码来完成码点的提取:

//判断是以十进制表示还是以十六进制表示
const hex = head[0] ==='&#x'
// 根据不同进制表示法,选用不同的正则
const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
//最终,body[1]的值就是 Unicode 码点
const body = pattern.exec(rawText)

有了Unicode码点之后,只需要调用String.fromCodePoint 函数即可将其解码为对应的字符:

if(body){// 根据对应的进制,将码点字符串转换为数字const cp = parseInt(body[1],hex?16:10)// 解码const char = String.fromCodePoint(cp)
}

不过,在真正进行解码前,需要对码点的值进行合法性检查。WHATWG规范中对此也有明确的定义。
关于对码点的值进行合法性检查,这里就不做详解,知道有这个概念即可

关于码点合法性检查的具体实现如下:

if(body){// 根据对应的进制,将码点字符串转换为数字const cp = parseInt(body[1],hex?16:10)// 检查码点的合法性if(cp === 0){// 如果码点值为 0x00,替换为 0xfffdcp = 0xfffd}else if(cp > 0x10ffff){// 如果码点值超过 Unicode 的最大值,替换为 0xfffdcp = 0xfffd}else if((cp >= 0xd800 && cp <= 0xdfff){// 如果码点值处于 surrogate pair 范围内,替换为 0xfffdcp = 0xfffd}else if((cp >= 0xfdd && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {// 如果码点值处于 noncharacter 范围内,则什么都不做,交给平台处理// noop}else if(// 控制字待集的范围是:[0x01,0x1f] 加上[0x7f,0x9f]// 去掉 ASICC 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF)// 0x0D(CR) 虽然也是 ASICC 空白符,但需要包含(cp >= 0x01 && cp <=0x08) ||cp === 0x0b ||(cp >= 0x0d && cp <= 0x1f) ||(cp >= 0x7f && cp <= 0x9f)) {// 在CCR_REPLACEMENTS表中查找替换码点,如果找不到,则使用原码点cp = CCR_REPLACEMENTS[cp] || cp}// 最后进行解码const char = String.fromCodePoint(cp)}

最后,我们将上述代码整合到 decodeHtml 函数中,这样就实现一个完善的 HTML文本解码函数

function decodeHtml(rawText, asAttr = false) (// 省略部分代码// 消费字符串,直到处理完毕为止while(offset<end){// 省略部分代码// 如果满足条件,则说明是命名字符引用,否则为数字宇符引用if(head[0]==='&'){//省略部分代码}else{//判断是以十进制表示还是以十六进制表示const hex = head[0] ==='&#x'// 根据不同进制表示法,选用不同的正则const pattern = hex ?/^&#x([0-9a-f]+);?/i :/^&#([0-9]+);?///最终,body[1]的值就是 Unicode 码点const body = pattern.exec(rawText)if(body){// 根据对应的进制,将码点字符串转换为数字const cp = parseInt(body[1],hex?16:10)// 检查码点的合法性if(cp === 0){// 如果码点值为 0x00,替换为 0xfffdcp = 0xfffd}else if(cp > 0x10ffff){// 如果码点值超过 Unicode 的最大值,替换为 0xfffdcp = 0xfffd}else if((cp >= 0xd800 && cp <= 0xdfff){// 如果码点值处于 surrogate pair 范围内,替换为 0xfffdcp = 0xfffd}else if((cp >= 0xfdd && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {// 如果码点值处于 noncharacter 范围内,则什么都不做,交给平台处理// noop}else if(// 控制字待集的范围是:[0x01,0x1f] 加上[0x7f,0x9f]// 去掉 ASICC 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF)// 0x0D(CR) 虽然也是 ASICC 空白符,但需要包含(cp >= 0x01 && cp <=0x08) ||cp === 0x0b ||(cp >= 0x0d && cp <= 0x1f) ||(cp >= 0x7f && cp <= 0x9f)) {// 在CCR_REPLACEMENTS表中查找替换码点,如果找不到,则使用原码点cp = CCR_REPLACEMENTS[cp] || cp}// 最后进行解码const char = String.fromCodePoint(cp)//消费整个数字字符引用的内容     advance(body[0].length)}else{//如果没有匹配,则不进行解码操作,只是把 head[0] 追加到 decodedText 上并消费decodedText += head[0]advance(head[0].length)}}}return decodedText
}

解析插值与注释

文本插值是 Vue;js 模板中用来渲染动态数据的常用方法:

([ count ))

解析器在遇到文本插值的起始定界符{{时,会进人文本“插值状态6”,并调用parseInterpolation函数来解析插值内容

解析器在解析插值时,只需要将文本插值的开始定界符与结束定界符之间的内容提取出来,作为JavaScript表达式即可,

function parseInterpolation(context){// 消费开始定界符context.advanceBy('{{'.length)// 找到结束定界符的位置索引closeIndex = context.source.indexOf('}}')if(closeIndex<0){console.error('插值缺少结束定界符')}// 截取开始定界符与结束定界符之间的内容作为插值表达式const content = context.source.slice(0, closeIndex)// 消费表达式的内容context.advanceBy(content.length)// 消费结束定界符context.advanceBy('}}'.length)//返回类型为 Interpolation 的节点,代表插值节点return{type: 'Interpolation',// 插值节点的 content 是一个类型为 Expression 的表达式节点content: {type: 'Expression',//表达式节点的内容则是经过 HTML 解码后的插值表达式content: decodeHtml(content)} }}

配合上面的parseInterpolation 函数,解析如下模板内容:

const ast = parse( `<div>foo {{ bar }} baz</div>`)

最终将得到如下AST:

const ast = {type: 'Root',children: [{type: 'Element',tag: 'div',isSelfClosing: false,props: [],children: [{type: 'Text', content: 'foo'},// 插值节点{type: 'Interpolation',content: [type: 'Expression',content: ' bar ']},{type: 'Text', content: ' baz'}]}]
}

解析注释的思路与解析插值非常相似,如下面的 parseComment 函数所示:

function parseComment(context){// 消费注释的开始部分context.advanceBy('<!--'.length)// 找到注释结束部分的位置索引closeIndex = context.source.indexOf('-->')// 裁取注释节点的内容const content = context.source.slice(0,closeIndex)// 消费内容context.advanceBy(content.length)// 消费注释的结束部分context.advanceBy('-->'.length)// 返回类型为 Comment 的节点return {type: 'Comment',content}
}

配合 parseComment 函数,解析如下模板内容:

const ast = parse( `<div><!-- comments --></div>`)

最终得到如下AST:

const ast = {type: 'Root',children: [{type: 'Element',tag: 'div',isSelfClosing: false,props: [],children: [{type: 'Comment', content: ' comments '}]}]
}

【vue设计与实现】解析器 - 解析文本与解码HTML实体相关推荐

  1. java sax解析xml_在Java中使用DOM,SAX和StAX解析器解析XML

    java sax解析xml 我碰巧通读了有关Java中XML解析和构建API的章节. 我试用了样本XML上的其他解析器. 然后,我想在我的博客上分享它,这样我就可以得到该代码的参考以及任何阅读此代码的 ...

  2. 在Java中使用DOM,SAX和StAX解析器解析XML

    我碰巧通读了有关Java中的XML解析和构建API的章节. 我试用了样本XML上的其他解析器. 然后,我想在我的博客上分享它,这样我就可以参考该代码以及任何阅读此书的参考. 在本文中,我将在不同的解析 ...

  3. jsp springmvc 视图解析器_Springmvc中多视图解析器解析问题

    最近被问到过几次关于springmvc多视图解析器解析的问题:总结一下. 1.问题: 假设我有两个jsp: WEB-INF/html/a.jsp WEB-INF/report/b.jsp 且我配置了视 ...

  4. spring MVC使用自定义的参数解析器解析参数

    目录 写在前面 编写自定义的参数解析器解析请求参数 项目结构 定义注解 实体类 controller 定义参数解析器 注册参数解析器 启动项目 发起请求查看结果 写在前面 如果还有小伙伴不知道spri ...

  5. Android学习笔记---15_采用Pull解析器解析和生成XML内容

    15_采用Pull解析器解析和生成XML内容 -------------------------------------- 使用SAX或者DOM或者pull解析XML文件 -------------- ...

  6. 用PULL解析器解析XML文件

    第一种方式(简洁,直接用pullparser.nextText()来返回下一个String类型的值): 1 package lee.service; 2 3 import java.io.InputS ...

  7. 15_采用Pull解析器解析和生成XML内容

    java还提供SAX和DOM用于解析XML Android还集成了Pull解析器--推荐 package cn.itcast.service;import java.io.InputStream; i ...

  8. php 嵌套函数公式解析,Pyparsing,使用嵌套解析器解析php函数注释块的内容

    AKA"添加根据解析器.parseAction到父解析树" 我尝试使用PyParsing(规则IMHO)解析PHP文件,其中函数定义用JavaDoc样式的注释进行了注释.原因是我想 ...

  9. Android学习笔记---26_网络通信之资讯客户端,使用pull解析器,解析,从网络中获得的自定义xml文件

    7.25_网络通信之资讯客户端 ---------------------------- 1.网络中一般是使用自己定义的格式比如:   案例:酷6网的视频客户端有一个功能:"在手机上显示最新 ...

最新文章

  1. LAMP+Postfix+Dovecot+Postfixadmin搭建邮件管理系统(四)
  2. DL之FasterR-CNN:Faster R-CNN算法的简介(论文介绍)、架构详解、案例应用等配图集合之详细攻略
  3. SAP中创建分部机构凭证号码并且按年度编号
  4. 【CentOS】如何在线安装pcre?
  5. sql server2008给数据表,字段,添加修改注释
  6. AI 是否会取代计算机程序员
  7. Windows 2000活动目录详解之基础篇
  8. 【学习0605】NVIDIA DRIVE AGX Developer Kit - How to set up
  9. Linux安装和卸载JDK8详解
  10. php小偷cookie,php小偷程序新概念之实时更新(二) | 学步园
  11. VM虚拟机BT5下对usb无线网卡的配置
  12. java怎么用switch求闰年_使用switch语句编程,根据输入的年份判断是否为闰年,根据输入的月份判断这月有多少天...
  13. php怎么求最小公倍数,最小公倍数算法
  14. 【前端优化】在线图片压缩有这4个网站就够了(免费又好用)
  15. 如何根据论文文章名称一键查询该篇论文的引用格式?
  16. 自动驾驶的分级和无人驾驶系统简介
  17. iClone走路改为原地踏步
  18. Cisco-小型网络拓扑(DNS、DHCP、网站服务器、无线路由器)
  19. UnityShader图形学中的数学之Normal融合
  20. 【开源】STC单片机免冷启动自动下载器

热门文章

  1. linux rapidio测试,Linux 下RapidIO 子系统的分析与实现.pdf
  2. 什么是蓝牙适配器?它有哪些性能特点?-道合顺大数据Infinigo
  3. win11搜索不到win7共享打印机?已解决
  4. TCGA数据下载教程:使用官方gdc-client软件下载
  5. 机器学习将成为对抗蜂窝网络欺诈的秘密武器
  6. python实现ping工具
  7. 嵌入式C语言基础知识梳理
  8. C语言编程鉴赏,吴坚鸿单片机程序风格赏析(一)
  9. mblock机器人指令_mBot机器人如何通过蓝牙实现与PC端mBlock的无线通信?
  10. Jade学习中一些需要注意的地方