ANode 参考

ANode 全名抽象节点,是 San 组件框架 template 解析的返回结果。本文档对 ANode 进行说明。

template 简述
  插值语法
  普通属性语法
  双向绑定语法
  指令语法
表达式
  表达式类型
  STRING
  NUMBER
  BOOL
  ACCESSOR
  INTERP
  CALL
  TEXT
  BINARY
  UNARY
  TERTIARY
  ARRAY LITERAL
  OBJECT LITERAL
  PARENTHESIZED
ANode 的结构
模板解析结果
  文本
  属性
  双向绑定
  复杂的插值
  事件绑定
  条件指令
  循环指令

template 简述

在 San 中,template 是一个符合 HTML 语法规则的字符串。在 template 中,数据绑定与事件的声明通过以下形式:

插值语法

文本中通过 {{...}} 声明插值,插值内部支持表达式和过滤器的声明。

插值语法

  1. {{ expr [[| filter-call1] | filter-call2...] }}

示例

  1. <p>Hello {{name}}!</p>

普通属性语法

属性内部可以出现插值语法。

示例

  1. <span title="This is {{name}}">{{name}}</span>

属性声明根据不同形式,处理成不同的绑定表达式:

  • 复杂形式的值,处理成TEXT表达式。如 title="This is {{name}}"
  • 只包含单一插值,并且无 filter 时,插值内部的表达式会被抽取出来。如 title="{{name}}"

双向绑定语法

San 认为 template 应该尽量保持 HTML 的语法简洁性,所以双向绑定方式在属性的值上做文章:属性值形式为 {= expression =} 的认为是双向绑定。

示例

  1. <input type="text" value="{= name =}">

双向绑定仅支持普通变量和属性访问表达式。

指令语法

s- 为前缀的属性,将被解析成指令。常见的指令有 for、if 等。

示例

  1. <span s-if="isOnline">Hello!</span>
  2. <span s-else>Offline</span>
  3. <dl>
  4. <dt>name - email</dt>
  5. <dd s-for="p in persons" title="{{p.name}}">{{p.name}}({{dept}}) - {{p.email}}</dd>
  6. </dl>

表达式

San 的 template 支持多种形式的表达式,表达式信息在 template 解析过程中会被解析并以 Object 格式保存在 ANode 中。下面是一个简单字符串表达式的信息形式:

  1. exprInfo = {
  2. "type": 1,
  3. "value": "hello"
  4. }

本章对保存在 ANode 中的表达式信息进行相应说明。在下面的描述中,用 exprInfo 代替表达式信息对象。

表达式类型

从源码中下面枚举类型的声明,可以看出 San 支持的表达式类型。

  1. var ExprType = {
  2. STRING: 1,
  3. NUMBER: 2,
  4. BOOL: 3,
  5. ACCESSOR: 4,
  6. INTERP: 5,
  7. CALL: 6,
  8. TEXT: 7,
  9. BINARY: 8,
  10. UNARY: 9,
  11. TERTIARY: 10,
  12. ARRAY: 11,
  13. OBJECT: 12
  14. };

exprInfo 中必须包含 type 属性,值为上面类型值之一。下面不再对 type 属性赘述。

STRING

字符串字面量

  1. // value - 字符串的值
  2. exprInfo = {
  3. type: ExprType.STRING,
  4. value: '你好'
  5. }

NUMBER

数值字面量

  1. // value - 数值的值
  2. exprInfo = {
  3. type: ExprType.NUMBER,
  4. value: 123.456
  5. }

BOOL

布尔字面量

  1. // value - 数值的值
  2. exprInfo = {
  3. type: ExprType.BOOL,
  4. value: true
  5. }

ACCESSOR

数据访问表达式,比如 aa.b.ca[index],代表对一个数据项的引用

  1. // paths - 属性路径。数组,里面每一项是一个表达式对象
  2. exprInfo = {
  3. type: ExprType.ACCESSOR,
  4. paths: [
  5. {type: ExprType.STRING, value: 'user'},
  6. {type: ExprType.STRING, value: 'phones'},
  7. {
  8. type: ExprType.ACCESSOR,
  9. paths: [
  10. {type: ExprType.STRING, value: 'DefaultConfig'},
  11. {type: ExprType.STRING, value: 'PHONE-INDEX'}
  12. ]
  13. }
  14. ]
  15. }

INTERP

插值。解析器为了方便解析和求值,将插值看成一种表达式

  1. // expr - 数据访问部分表达式信息,一个表达式对象
  2. // filters - 过滤器部分信息。数组,其中每一项是一个 CALL 表达式对象
  3. exprInfo = {
  4. type: ExprType.INTERP,
  5. expr: {
  6. type: ExprType.ACCESSOR,
  7. paths: [
  8. {type: ExprType.STRING, value: 'user'},
  9. {type: ExprType.STRING, value: 'phones'}
  10. ]
  11. },
  12. filters: [
  13. {
  14. type: ExprType.CALL,
  15. name: {
  16. type: ExprType.ACCESSOR,
  17. paths: [
  18. {type: ExprType.STRING, value: 'comma'}
  19. ]
  20. },
  21. args: [
  22. {type: ExprType.NUMBER, literal: '3'}
  23. ]
  24. }
  25. ]
  26. }

CALL

调用表达式,表示对方法或过滤器的调用。调用表达式一般出现在插值的过滤器列表,或事件绑定信息中。

  1. // name - 调用方法名。字符串
  2. // args - 调用参数列表。数组,其中每一项是一个表达式对象
  3. exprInfo = {
  4. type: ExprType.CALL,
  5. name: {
  6. type: ExprType.ACCESSOR,
  7. paths: [
  8. {type: ExprType.STRING, value: 'comma'}
  9. ]
  10. },
  11. args: [
  12. {type: ExprType.NUMBER, literal: '3'}
  13. ]
  14. }

TEXT

文本。文本是一段由静态字符串和插值表达式组成的复杂内容,通常用于 text 节点与属性绑定。

  1. // segs - 文本组成片段。数组,其中每一项是一个 STRING 或 INTERP表达式对象
  2. exprInfo = {
  3. type: ExprType.TEXT,
  4. segs: [
  5. {type: ExprType.STRING, value: 'Hello '},
  6. {
  7. type: ExprType.INTERP,
  8. expr: {
  9. type: ExprType.ACCESSOR,
  10. paths: [
  11. {type: ExprType.STRING, value: 'whoAmI'}
  12. ]
  13. },
  14. filters: []
  15. },
  16. {type: ExprType.STRING, value: '!'}
  17. ]
  18. }

BINARY

二元表达式,支持多种计算和比较,包括 + | - | * | / | && | || | == | != | === | !== | > | >= | < | <=

  1. // operator - 操作符。数值,值为操作符各个 char 的 ascii 之和。比如 == 操作符的 operator 为 61 + 61 = 122
  2. // segs - 包含两个表达式对象的数组
  3. exprInfo = {
  4. type: ExprType.BINARY,
  5. segs: [
  6. {
  7. type: ExprType.ACCESSOR,
  8. paths: [
  9. {type: ExprType.STRING, value: 'commaLength'}
  10. ]
  11. },
  12. {
  13. type: ExprType.NUMBER,
  14. literal: "1"
  15. }
  16. ],
  17. operator: 43
  18. }

UNARY

一元表达式,支持:

  • ! 逻辑否定
  • - 取负
  • + 转换成数值
  1. // operator - 操作符。数值,值为操作符 char 的 ascii。
  2. exprInfo = {
  3. type: ExprType.UNARY,
  4. expr: {
  5. type: ExprType.ACCESSOR,
  6. paths: [
  7. {type: ExprType.STRING, value: 'user'},
  8. {type: ExprType.STRING, value: 'isLogin'}
  9. ]
  10. },
  11. operator: 33
  12. }

TERTIARY

三元表达式,其实就是 conditional ? yes-expr : no-expr 的条件表达式。

  1. // segs - 包含3个表达式对象的数组,第一个是条件表达式,第二个是值为真时的表达式,第三个是值为假时的表达式
  2. exprInfo = {
  3. type: ExprType.TERTIARY,
  4. segs: [
  5. {
  6. type: ExprType.ACCESSOR,
  7. paths: [
  8. {type: ExprType.STRING, value: 'user'},
  9. {type: ExprType.STRING, value: 'isLogin'}
  10. ]
  11. },
  12. {
  13. type: ExprType.STRING,
  14. value: 'yes'
  15. },
  16. {
  17. type: ExprType.STRING,
  18. value: 'no'
  19. }
  20. ]
  21. }

ARRAY LITERAL

数组字面量,支持数组展开。

  1. // [name, 'text', ...ext, true]
  2. // items - 数组项列表。expr 为数组项表达式,spread 代表是否为展开项
  3. exprInfo = {
  4. type: ExprType.ARRAY,
  5. items: [
  6. {
  7. "expr": {
  8. "type": ExprType.ACCESSOR,
  9. "paths": [
  10. {
  11. "type": ExprType.STRING,
  12. "value": "name"
  13. }
  14. ]
  15. }
  16. },
  17. {
  18. "expr": {
  19. "type": ExprType.STRING,
  20. "value": "text"
  21. }
  22. },
  23. {
  24. "spread": true,
  25. "expr": {
  26. "type": ExprType.ACCESSOR,
  27. "paths": [
  28. {
  29. "type": ExprType.STRING,
  30. "value": "ext"
  31. }
  32. ]
  33. }
  34. },
  35. {
  36. "expr": {
  37. "type": ExprType.BOOL,
  38. "value": true
  39. }
  40. }
  41. ]
  42. }

OBJECT LITERAL

对象字面量,支持对象展开。

  1. // {name: realName, email, ...ext}
  2. // items - 对象项列表。name 为项 name 表达式, expr 为项 value 表达式,spread 代表是否为展开项
  3. exprInfo = {
  4. "type": ExprType.OBJECT,
  5. "items": [
  6. {
  7. "name": {
  8. "type": ExprType.STRING,
  9. "value": "name"
  10. },
  11. "expr": {
  12. "type": ExprType.ACCESSOR,
  13. "paths": [
  14. {
  15. "type": ExprType.STRING,
  16. "value": "realName"
  17. }
  18. ]
  19. }
  20. },
  21. {
  22. "name": {
  23. "type": ExprType.STRING,
  24. "value": "email"
  25. },
  26. "expr": {
  27. "type": ExprType.ACCESSOR,
  28. "paths": [
  29. {
  30. "type": ExprType.STRING,
  31. "value": "email"
  32. }
  33. ]
  34. }
  35. },
  36. {
  37. "spread": true,
  38. "expr": {
  39. "type": ExprType.ACCESSOR,
  40. "paths": [
  41. {
  42. "type": ExprType.STRING,
  43. "value": "ext"
  44. }
  45. ]
  46. }
  47. }
  48. ]
  49. }

PARENTHESIZED

括号表达式不会生成独立的表达式对象。被括号包含的表达式,在其对象上有一个 parenthesized 属性,值为 true

  1. // (a + b) * c
  2. // a + b 的表达式对象上包含 parenthesized 属性,值为 true
  3. exprInfo = {
  4. type: ExprType.BINARY,
  5. segs: [
  6. {
  7. type: ExprType.BINARY,
  8. parenthesized: true,
  9. segs: [
  10. {
  11. type: ExprType.ACCESSOR,
  12. paths: [
  13. {type: ExprType.STRING, value: 'a'}
  14. ]
  15. },
  16. {
  17. type: ExprType.ACCESSOR,
  18. paths: [
  19. {type: ExprType.STRING, value: 'b'}
  20. ]
  21. }
  22. ],
  23. operator: 43
  24. },
  25. {
  26. type: ExprType.ACCESSOR,
  27. paths: [
  28. {type: ExprType.STRING, value: 'c'}
  29. ]
  30. }
  31. ],
  32. operator: 42
  33. }

ANode 的结构

template 的 parse 直接返回一个 ANode 对象。ANode 是一个 JSON Object,不包含任何方法,只有属性。

{Object?} textExpr

文本的表达式对象。当前节点为文本节点时该属性存在。

{Array.} children

ANode 的结构与 HTML 一样,是一个树状结构。children 是当前节点的子节点列表。文本节点该属性无效

{Array.} props

节点的属性绑定信息。文本节点该属性无效

  1. // 遍历所有绑定属性
  2. aNode.props.forEach(function (prop) {
  3. });

{Array.} events

节点的事件绑定信息。文本节点该属性无效

  1. // 遍历所有绑定属性
  2. aNode.events.forEach(function (event) {
  3. });

{Object} directives

节点的指令绑定信息。文本节点该属性无效

  1. // 获取 if 指令信息。该信息是一个特定的指令信息对象
  2. aNode.directives['if'];

{string} tagName

节点的标签名。文本节点该属性无效

{Array.?} elses

当节点包含 if directive 时,其对应的 elseelif 节点

模板解析结果

模板解析的返回结果是一个标签节点的 ANode 实例,实例中 children 包含子节点、props 包含属性绑定信息、events 包含事件绑定信息、directives 包含指令信息、tagName 为节点标签名、elses 为条件节点结。

本章节通过一些示例说明模板解析的 ANode 结果。其中表达式信息的详细说明请参考 表达式 章节,ANode 结构请参考 ANode 的结构 章节。

文本

文本节点作为 p 标签的子节点存在。

  1. <p>Hello {{name}}!</p>
  1. aNode = {
  2. "directives": {},
  3. "props": [],
  4. "events": [],
  5. "children": [
  6. {
  7. "textExpr": {
  8. "type": ExprType.TEXT,
  9. "segs": [
  10. {
  11. "type": ExprType.STRING,
  12. "value": "Hello "
  13. },
  14. {
  15. "type": ExprType.INTERP,
  16. "expr": {
  17. "type": ExprType.ACCESSOR,
  18. "paths": [
  19. {
  20. "type": ExprType.STRING,
  21. "value": "name"
  22. }
  23. ]
  24. },
  25. "filters": []
  26. },
  27. {
  28. "type": ExprType.STRING,
  29. "value": "!"
  30. },
  31. ]
  32. }
  33. }
  34. ],
  35. "tagName": "p"
  36. }

属性

属性信息是一个 绑定信息对象,其中:

  • name - 属性名
  • expr - 表达式信息对象

下面例子的 title 属性绑定到一个 TEXT 类型的表达式中。

  1. <span title="This is {{name}}">{{name}}</span>
  1. aNode = {
  2. "directives": {},
  3. "props": [
  4. {
  5. "name": "title",
  6. "expr": {
  7. "type": ExprType.TEXT,
  8. "segs": [
  9. {
  10. "type": ExprType.STRING,
  11. "value": "This is "
  12. },
  13. {
  14. "type": ExprType.INTERP,
  15. "expr": {
  16. "type": ExprType.ACCESSOR,
  17. "paths": [
  18. {
  19. "type": ExprType.STRING,
  20. "value": "name"
  21. }
  22. ]
  23. },
  24. "filters": []
  25. }
  26. ]
  27. }
  28. }
  29. ],
  30. "events": [],
  31. "children": [
  32. {
  33. "textExpr": {
  34. "type": ExprType.TEXT,
  35. "segs": [
  36. {
  37. "type": ExprType.INTERP,
  38. "expr": {
  39. "type": ExprType.ACCESSOR,
  40. "paths": [
  41. {
  42. "type": ExprType.STRING,
  43. "value": "name"
  44. }
  45. ]
  46. },
  47. "filters": []
  48. }
  49. ]
  50. }
  51. }
  52. ],
  53. "tagName": "span"
  54. }

属性为单一插值并且无 filter 时,插值内部表达式被抽取。

  1. <span title="{{name}}">{{name}}</span>
  1. aNode = {
  2. "directives": {},
  3. "props": [
  4. {
  5. "name": "title",
  6. "expr": {
  7. "type": ExprType.ACCESSOR,
  8. "paths": [
  9. {
  10. "type": ExprType.STRING,
  11. "value": "name"
  12. }
  13. ]
  14. }
  15. }
  16. ],
  17. "events": [],
  18. "children": [
  19. {
  20. "textExpr": {
  21. "type": ExprType.TEXT,
  22. "segs": [
  23. {
  24. "type": ExprType.INTERP,
  25. "expr": {
  26. "type": ExprType.ACCESSOR,
  27. "paths": [
  28. {
  29. "type": ExprType.STRING,
  30. "value": "name"
  31. }
  32. ]
  33. },
  34. "filters": []
  35. }
  36. ]
  37. }
  38. }
  39. ],
  40. "tagName": "span"
  41. }

双向绑定

双向绑定的属性,绑定信息对象上包含 x 属性,值为 true。

  1. <input type="text" value="{= name =}">
  1. aNode = {
  2. "directives": {},
  3. "props": [
  4. {
  5. "name": "type",
  6. "expr": {
  7. "type": ExprType.STRING,
  8. "value": "text"
  9. }
  10. },
  11. {
  12. "name": "value",
  13. "expr": {
  14. "type": ExprType.ACCESSOR,
  15. "paths": [
  16. {
  17. "type": ExprType.STRING,
  18. "value": "name"
  19. }
  20. ]
  21. },
  22. "x": 1
  23. }
  24. ],
  25. "events": [],
  26. "children": [],
  27. "tagName": "input"
  28. }

复杂的插值

  1. <p title="result: {{(var1 - var2) / var3 + 'static text' | comma(commaLength + 1)}}"></p>
  1. "directives": {},
  2. "props": [
  3. {
  4. "name": "title",
  5. "expr": {
  6. "type": ExprType.TEXT,
  7. "segs": [
  8. {
  9. "type": ExprType.STRING,
  10. "value": "result: "
  11. },
  12. {
  13. "type": ExprType.INTERP,
  14. "expr": {
  15. "type": ExprType.BINARY,
  16. "segs": [
  17. {
  18. "type": ExprType.BINARY,
  19. "segs": [
  20. {
  21. "type": ExprType.BINARY,
  22. "segs": [
  23. {
  24. "type": ExprType.ACCESSOR,
  25. "paths": [
  26. {
  27. "type": ExprType.STRING,
  28. "value": "var1"
  29. }
  30. ]
  31. },
  32. {
  33. "type": ExprType.ACCESSOR,
  34. "paths": [
  35. {
  36. "type": ExprType.STRING,
  37. "value": "var2"
  38. }
  39. ]
  40. }
  41. ],
  42. "operator": 45
  43. },
  44. {
  45. "type": ExprType.ACCESSOR,
  46. "paths": [
  47. {
  48. "type": ExprType.STRING,
  49. "value": "var3"
  50. }
  51. ]
  52. }
  53. ],
  54. "operator": 47
  55. },
  56. {
  57. "type": 1,
  58. "value": "static text"
  59. }
  60. ],
  61. "operator": 43
  62. },
  63. "filters": [
  64. {
  65. "type": ExprType.CALL,
  66. "name": {
  67. type: ExprType.ACCESSOR,
  68. paths: [
  69. {type: ExprType.STRING, value: "comma"}
  70. ]
  71. },
  72. "args": [
  73. {
  74. "type": ExprType.BINARY,
  75. "segs": [
  76. {
  77. "type": ExprType.ACCESSOR,
  78. "paths": [
  79. {
  80. "type": ExprType.STRING,
  81. "value": "commaLength"
  82. }
  83. ]
  84. },
  85. {
  86. "type": 2,
  87. "value": 1
  88. }
  89. ],
  90. "operator": 43
  91. }
  92. ]
  93. }
  94. ]
  95. }
  96. ]
  97. }
  98. }
  99. ],
  100. "events": [],
  101. "children": [],
  102. "tagName": "p"
  103. }

事件绑定

事件绑定信息与属性绑定信息类似,但是事件绑定信息对象的 expr 属性一定是一个 CALL 表达式的表示。

  1. <button type="button" on-click="clicker($event)">click here</button>
  1. aNode = {
  2. "directives": {},
  3. "props": [
  4. {
  5. "name": "type",
  6. "expr": {
  7. "type": ExprType.STRING,
  8. "value": "button"
  9. }
  10. }
  11. ],
  12. "events": [
  13. {
  14. "name": "click",
  15. "expr": {
  16. "type": ExprType.CALL,
  17. "name": {
  18. type: ExprType.ACCESSOR,
  19. paths: [
  20. {type: ExprType.STRING, value: "clicker"}
  21. ]
  22. },
  23. "args": [
  24. {
  25. "type": ExprType.ACCESSOR,
  26. "paths": [
  27. {
  28. "type": ExprType.STRING,
  29. "value": "$event"
  30. }
  31. ]
  32. }
  33. ]
  34. }
  35. }
  36. ],
  37. "children": [
  38. {
  39. "textExpr": {
  40. "type": ExprType.TEXT,
  41. "segs": [
  42. {"type": ExprType.STRING, "value": "click here"}
  43. ]
  44. }
  45. }
  46. ],
  47. "tagName": "button"
  48. }

条件指令

if 指令的值是一个表达式信息对象,else 指令的值永远等于 true。else 和 elif 对应的节点会被置于同组的 if 下的 elses 属性中。

  1. <div>
  2. <span s-if="isOnline">Hello!</span>
  3. <span s-else>Offline</span>
  4. </div>
  1. aNode = {
  2. "directives": {},
  3. "props": [],
  4. "events": [],
  5. "children": [
  6. {
  7. "directives": {
  8. "if": {
  9. "value": {
  10. "type": ExprType.ACCESSOR,
  11. "paths": [
  12. {type: ExprType.STRING, value: "isOnline"}
  13. ]
  14. },
  15. "name": "if"
  16. }
  17. },
  18. "props": [],
  19. "events": [],
  20. "children": [
  21. {
  22. "textExpr": {
  23. "type": ExprType.TEXT,
  24. "segs": [
  25. {"type": ExprType.STRING, "value": "Hello!"}
  26. ]
  27. }
  28. }
  29. ],
  30. "tagName": "span",
  31. "elses": [
  32. {
  33. "directives": {
  34. "else": {
  35. "value": true,
  36. "name": "else"
  37. }
  38. },
  39. "props": [],
  40. "events": [],
  41. "children": [
  42. {
  43. "textExpr": {
  44. "type": ExprType.TEXT,
  45. "segs": [
  46. {"type": ExprType.STRING, "value": "Offline"}
  47. ]
  48. }
  49. }
  50. ],
  51. "tagName": "span"
  52. }
  53. ]
  54. },
  55. ],
  56. "tagName": "div"
  57. }

循环指令

循环指令对象的信息包括:

  • item - 表达式对象,表示循环过程中数据项对应的变量
  • index - 表达式对象,表示循环过程中数据索引对应的变量
  • value - 表达式对象,表示要循环的数据
  • name - 恒为 for
  1. <ul>
  2. <li s-for="p, index in persons">{{p.name}} - {{p.email}}</li>
  3. </ul>
  1. aNode = {
  2. "directives": [],
  3. "props": [],
  4. "events": [],
  5. "children": [
  6. {
  7. "textExpr": {
  8. "type": ExprType.TEXT,
  9. "segs": [
  10. {
  11. "type": ExprType.STRING,
  12. "value": "\n "
  13. }
  14. ],
  15. "value": "\n "
  16. }
  17. },
  18. {
  19. "directives": {
  20. "for": {
  21. "item": {
  22. type: ExprType.ACCESSOR,
  23. paths: [
  24. {"type": ExprType.STRING, "value": "p"}
  25. ]
  26. },
  27. "index": {
  28. type: ExprType.ACCESSOR,
  29. paths: [
  30. {"type": ExprType.STRING, "value": "index"}
  31. ]
  32. }
  33. "value": {
  34. type: ExprType.ACCESSOR,
  35. paths: [
  36. {"type": ExprType.STRING, "value": "persons"}
  37. ]
  38. },
  39. "name": "for"
  40. }
  41. },
  42. "props": [],
  43. "events": [],
  44. "children": [
  45. {
  46. "textExpr": {
  47. "type": ExprType.TEXT,
  48. "segs": [
  49. {
  50. "type": ExprType.INTERP,
  51. "expr": {
  52. "type": ExprType.ACCESSOR,
  53. "paths": [
  54. {"type": ExprType.STRING, "value": "p"},
  55. {"type": ExprType.STRING, "value": "name"}
  56. ]
  57. },
  58. "filters": []
  59. },
  60. {
  61. "type": ExprType.STRING,
  62. "value": " - "
  63. },
  64. {
  65. "type": ExprType.INTERP,
  66. "expr": {
  67. "type": ExprType.ACCESSOR,
  68. "paths": [
  69. {"type": ExprType.STRING, "value": "p"},
  70. {"type": ExprType.STRING, "value": "email"}
  71. ]
  72. },
  73. "filters": []
  74. }
  75. ]
  76. }
  77. }
  78. ],
  79. "tagName": "li"
  80. },
  81. {
  82. "textExpr": {
  83. "type": ExprType.TEXT,
  84. "segs": [
  85. {
  86. "type": ExprType.STRING,
  87. "value": "\n"
  88. }
  89. ],
  90. "value": "\n"
  91. }
  92. }
  93. ],
  94. "tagName": "ul"
  95. }