网页文树接口规范 0.3.0
这个能力组供在浏览器里运行的豫言应用构建当前页面。应用以 ID 获取已有节点,或按白册创建新节点,再写纯文本、受限属性并组合成树;也可以一次调用建出整棵树。句柄目前投影为字符串,语义上是当前应用实例内的资源编号;应用不得自行构造或跨实例保存。适配层必须拒绝未知标签、未知属性、可执行链接和无效句柄。
0.3.0 相对 0.2.0 只增函数与标签(旧函数签名与语义不变):选择器查询与节点读回(查询页面节点、读取页面节点标签/属性/文字/父级、查找最近页面祖先、页面节点匹配选择器)、子树导出(读取页面节点树)、替换页面节点、移除页面节点属性、设置页面节点超文本;标签白册增 em、i、kbd、sup、sub、blockquote、hr、dl、dt、dd(hr 是空元素)。它们让应用能对已经存在于页面里的服务端渲染内容(例如包阅读页)做后加工,而不必写 JavaScript;见[前端补全计划](../../豫言操作系统/文档/前端补全计划.汉语.md)。
0.2.0 相对 0.1.0:扩充标签与属性白册;新增一次建树(构建页面节点树)、插入/替换/移除/释放节点、追加文字、设置 iframe 地址、装入页面模板与读取页面网址;明确句柄的释放规则并提供自动释放。0.1.0 的函数签名全部不变。
句柄与释放规则
宿主为每个应用实例维护一张句柄表,上限 4096 项。下列函数会产生句柄:取得页面节点、新建页面节点、构建页面节点树,以及 0.3.0 的 查询页面节点(每个匹配元素一个)、读取页面节点父级、查找最近页面祖先。句柄表满时,产生句柄的函数抛事故(“页面节点句柄达到上限(4096)……”);因此应用必须遵守:
- 不再需要的句柄要用 `释放页面节点` 释放。 释放只是让应用放弃这个编号,不影响节点本身:节点若已在文档里,它仍在文档里。
- 同一个元素重复
取得页面节点得到同一个句柄(不额外占位);释放后再取得会得到新的句柄编号。 - 自动释放(子树随根管理):
移除页面节点、清空页面子节点、替换页面子节点移除的子树里,所有节点的句柄(含被移除节点本身)由宿主同步释放,之后再使用这些句柄会抛“页面节点句柄无效”。装入页面模板替换目标元素原有子节点时同理。 - 一次建树只占一个句柄:
构建页面节点树返回根节点一个句柄;树内其余节点没有句柄。若之后要单独操作树内某个节点,请在描述里给它一个id属性,再用取得页面节点。 新建页面节点与构建页面节点树造出的节点在被插入文档之前不属于文档;句柄被释放而节点从未插入,则节点随之被丢弃。
推荐用法:动态区域用 构建页面节点树 一次建出一行(一个句柄),插入页面子节点前 或 添加页面子节点 放进容器,立刻 释放页面节点;容器本身在启动时 取得页面节点 一次并持有;整个区域重绘时用 清空页面子节点 或 替换页面子节点,旧行的句柄随之自动释放。
失败语义
本组所有失败都属于违规调用或平台故障,一律抛豫言事故(可被应用捕获),事故消息为中文并保留原因:元素不存在、句柄无效或不是节点、标签或属性不在白册、链接不合规、层级违规(把祖先放进后代)、参照节点不是父节点的子节点、限额超出、句柄表满、网络与编码错误。宿主不得把失败当作成功,也不得让宿主异常越过豫言边界而使应用无法捕获。
白册
标签:a、span、small、b、strong、br、h1、h2、h3、h4、h5、p、section、div、article、nav、button、table、thead、tbody、tr、th、td、pre、code、time、ul、ol、li、label、select、option、form、input、textarea、details、summary,以及 0.3.0 增加的 em、i、kbd、sup、sub、blockquote、hr、dl、dt、dd。br、input、hr 是空元素,不能带文字或子节点。script、style、iframe、object、embed、link、meta、base、img、svg、video、dialog、template 等不在白册内。标签名区分大小写,只接受上列小写形式。
属性:与[网页界面接口规范](../网页界面/规范.汉语.md)“属性”一节是同一张表——class role type title hidden disabled readonly placeholder tabindex datetime maxlength rows colspan rowspan for value id checked selected open href target aria-* data-*;data-yy- 前缀保留给宿主;target=_blank 时宿主强制 rel=noopener noreferrer;href 只允许 /路径(不含 //)、#锚、https://、http://,http: 只对 a[href] 放行;style、src、srcdoc、name、action 与所有 on* 一律拒绝。
危险元素不给句柄:取得页面节点 对 script、style、iframe、object、embed、link、meta、base 等元素报错,避免应用改写它们的内容或属性。
函数
取得、创建、修改(0.1.0 起)
取得页面节点 : 字符串(ID) → 字符串(句柄):按 ID 取当前页面里已有的节点。新建页面节点 : 字符串(标签) → 字符串(文字) → 字符串(类名) → 字符串(句柄):创建白册内的元素,文字为纯文本(空字符串表示不设),类名规则同设置节点类名(空字符串表示不设)。文字至多 8 MiB。设置页面节点文字 : 字符串 → 字符串 → 有:整体设置节点的文字内容(textContent)。设置页面节点属性 : 字符串 → 字符串 → 字符串 → 有:按白册设置属性;布尔属性值用""、"true"、"false"。添加页面子节点 : 字符串(父) → 字符串(子) → 有:把子节点添加到父节点末尾;子节点已在文档中则是移动。清空页面子节点 : 字符串 → 有:移除节点的所有子节点,并释放被移除子树里所有节点的句柄(0.2.0 新增的释放规则)。设置页面标题 : 字符串 → 有、读取页面查询参数 : 字符串 → 字符串、读取页面资源文字 : 字符串 → 字符串:同 0.1.0。读取资源只接受以单斜杠开头的站内路径,不得含反斜杠;宿主核对解析后的 origin,禁止重定向,仅接受 HTTP 200,并逐块限制正文为 2 MiB,按严格 UTF-8 解码。
一次建树
构建页面节点树 : 字符串(描述 JSON) → 字符串(根节点句柄)。描述是 JSON 对象:
{"标签":"article","类":"event event-command","属性":{"data-yy":"打开:7","id":"行7"},
"文字":"首段文字","子":[{"标签":"p","文字":"段落"},"裸文字",{"标签":"ul","子":[{"标签":"li","文字":"一"}]}]}
标签必填,须在白册内;类、属性、文字、子可省。属性的值可以是文字、数字或布尔(数字转十进制文字,布尔转true/false)。文字是节点的第一个子节点(文本节点);子里的项可以是对象(子元素)或字符串(文本节点)。- 未知字段、类型不对、空元素带文字或子节点、
id在树内重复或与文档已有 ID 冲突,都报错并指出出错位置(如根.子[0].子[1])。 - 限额:深度至多 32(根为第 1 层)、节点(元素与文本节点)至多 2000、文字总量至多 1 MiB(UTF-8 字节)、属性名值总字符数至多 1 MiB、描述 JSON 至多 2 MiB(UTF-16 码元)。
- 树是整体原子地建出:任何一步失败都不留下任何节点与句柄。建好的树在被插入文档之前不属于文档。
组合
插入页面子节点前 : 字符串(父) → 字符串(子) → 字符串(参照) → 有:把子节点插入父节点中参照子节点之前;参照为空字符串表示追加到末尾;参照必须是父节点的子节点;子节点已在文档中是移动。替换页面子节点 : 字符串(父) → (列于 字符串)(子们) → 有:用给定的子节点列表整体替换父节点的所有子节点(至多 2000 项,不得重复);被替换掉的子树里所有句柄同时释放(仍在新列表里的节点不受影响);空列表等于清空。移除页面节点 : 字符串 → 有:把节点从其父节点移除,并释放该节点及其子树内所有节点的句柄。释放页面节点 : 字符串 → 有:释放一个句柄;句柄无效时抛事故。追加页面节点文字 : 字符串 → 字符串 → 有:在节点末尾追加纯文本,用于流式输出。末尾已是文本节点时并入它(连续追加不产生碎片节点),否则新建文本节点。单次至多 8 MiB,追加后该文本节点至多 8 MiB;空元素报错。
框架与模板
设置页面框架地址 : 字符串(iframe 的 ID) → 字符串(网址) → 有:改页面里已有 iframe 的src。iframe 必须由页面(HTML)声明sandbox属性——应用不能创建 iframe,也不能修改sandbox、allow、referrerpolicy等属性(不在白册内)。地址只允许:以/开头但不以//开头的同源路径,或 `https://` 网址,且其 origin 是页面在该 iframe 的 `data-yy-frame-origins` 属性(空格分隔的 origin 列表)里声明的(与页面同源的 https 网址亦可)。含空白、控制字符、反斜杠或超过 2048 个字符的地址拒绝;空字符串表示移除src。装入页面模板 : 字符串(资源路径) → 字符串(模板内元素 ID) → 字符串(目标 ID) → 有:用与读取页面资源文字相同的规则读取同源 HTML 模板,在模板里按 ID 选出一个元素,复制后清洗,再作为目标元素的唯一子节点装入(目标原有子节点被移除,其子树内的句柄同时释放)。清洗:剔除script、style、iframe、object、embed、link、meta、base、frame、frameset、applet、template元素,剔除所有on*、srcdoc、formaction属性,剔除javascript:与vbscript:链接,target=_blank的链接强制rel=noopener noreferrer;模板里没有该 ID 时抛事故。此函数会挂起等待网络。读取页面网址 : 有 → 字符串:当前页面的完整 URL(含查询与片段),用于构造给其他能力包使用的绝对网址。
选择器查询与节点读回(0.3.0)
这一组让应用读到页面里已有的内容(包括没有 ID 的元素)并加以改造。所有“选择器”参数是 CSS 选择器(选择器列表亦可,至多 512 个字符),语法错误抛事故;所有“节点”参数是上文的句柄,必须是元素。
查询页面节点 : 字符串(根) → 字符串(选择器) → 字符串(JSON 数组):等价于querySelectorAll。根是节点句柄,空字符串表示整个文档;根是<template>元素时在它的content里查询(用来读页面里放着的数据模板)。返回 JSON 数组文字,每项是一个元素句柄字符串,按文档顺序,同一元素得同一句柄。危险元素(script、style、iframe等)不出现在结果里;匹配超过 1024 个元素、或句柄表剩余不足时抛事故(应先收窄选择器、并及时释放句柄)。读取页面节点标签 : 字符串 → 字符串:小写标签名。读取页面节点属性 : 字符串(节点) → 字符串(名称) → (爻 合 字符串):属性的原样文字,返回(存在与否,值);名称与限制同网页界面包的读取节点属性。读取页面节点文字 : 字符串 → 字符串:textContent,按 UTF-8 至多 8 MiB。读取页面节点父级 : 字符串 → 字符串:父元素的句柄;没有父元素(含节点在<template>内容里)或父是危险元素时返回空字符串。查找最近页面祖先 : 字符串(节点) → 字符串(选择器) → 字符串:等价于closest(含自身),返回句柄,没有则空字符串。页面节点匹配选择器 : 字符串 → 字符串 → 爻:等价于matches。读取页面节点树 : 字符串(节点) → 整数(深度上限) → 字符串(JSON):把元素及其子树导出为描述文字,形状与 `构建页面节点树` 的输入相同——{"标签","类","属性","子"},文字节点是子里的字符串,节点之间的空白文字照原样保留;只导出白册内的属性(class写在类里,on*、style、src等被略去);危险元素与<template>子树被略去;深度上限 1 至 32,超过所给上限、节点超过 2000 或文字超过 1 MiB 时抛事故。导出的描述再用构建页面节点树重建时仍按白册校验:标签不在白册、id与文档中已有的重复、链接不合规都会被拒,应用需要先改写(例如去掉id)。典型用法:导出说明区的内容,在豫言里按文字偏移切分、改写后重建,再替换页面子节点。替换页面节点 : 字符串(旧) → 字符串(新) → 有:旧.replaceWith(新);新可以是新建节点或已在文档里的节点(则是移动)。旧节点及其子树的句柄不释放,旧节点可以再放到别处——例如先把它替换成一个新建的链接,再添加页面子节点(链接, 旧)完成“给原节点包一层”。旧节点没有父节点、或把祖先放进后代时抛事故。移除页面节点属性 : 字符串 → 字符串 → 有:规则同网页界面包的移除节点属性。设置页面节点超文本 : 字符串(节点) → 字符串(HTML) → 有:把 HTML 文字在惰性环境里解析,按白册清洗并复制后整体替换节点的子节点。标签与属性必须全在白册内,不合规时抛事故并指出位置,不悄悄剔除;注释被丢弃;文字至多 2 MiB、节点至多 5000、深度至多 32;被替换掉的旧子树里的句柄同时释放。用于装入豫言高亮器等可信来源产生的只含<span class>的片段;它不是通用的innerHTML。
限额
| 项 | 限额 |
| --- | --- |
| 句柄表 | 每个应用实例 4096 项 |
| 一次建树 | 深度 ≤ 32;节点 ≤ 2000;文字 ≤ 1 MiB(UTF-8);属性总量 ≤ 1 MiB;描述 ≤ 2 MiB |
| 替换页面子节点 | ≤ 2000 项 |
| 查询页面节点 | 选择器 ≤ 512 字符;匹配 ≤ 1024 个元素 |
| 读取页面节点树 | 深度 ≤ 32;节点 ≤ 2000;文字 ≤ 1 MiB |
| 设置页面节点超文本 | HTML ≤ 2 MiB;节点 ≤ 5000;深度 ≤ 32 |
| 文字与追加 | 单次与单个文本节点 ≤ 8 MiB |
| 属性值 | ≤ 65536 个 UTF-16 码元 |
| 页面资源 | ≤ 2 MiB,仅同源 GET |
跨宿主一致性
- 白册、限额、释放规则与失败分类必须与本文一致;宿主可以更严,不可以更松。
- 句柄编号是不透明字符串,应用只能原样传回;宿主不得让已释放的句柄编号悄悄指向另一个节点(参考实现的编号在实例内单调递增、绝不复用)。
构建页面节点树在树内的id唯一性检查只在一次调用范围内;建好后插入文档时若与既有 ID 重复,后者行为按浏览器(getElementById取文档顺序里的第一个),应用应保证 ID 唯一。
一致性样例
- 建树
{"标签":"div","子":[{"标签":"script"}]}:抛事故,指出位置根.子[0],句柄表不增。 - 建一棵 2 层的树并
添加页面子节点到容器,再释放页面节点(根):节点仍在文档中,句柄表回到原来的数量;循环 10000 次后句柄表仍为原数量。 - 容器里有 3 行,各行及其子节点都拿过句柄;
清空页面子节点(容器)之后,这些句柄全部失效,容器的句柄仍有效。 设置页面框架地址("网站画面", "https://evil.example/"):iframe 的data-yy-frame-origins里没有该 origin,抛事故。装入页面模板装入的模板里含<script>与onclick:装入后文档里没有脚本元素,元素上没有onclick。- (0.3.0)页面有三个
<a class="引用" href="/x">:查询页面节点("", ".引用")返回三个句柄;读取页面节点属性(柄, "href")为(阳,“/x”);读取页面节点父级(柄)是它们共同的父元素句柄;查找最近页面祖先(柄, "section")找到外层 section。 - (0.3.0)
<template id="资料"><a data-qualified="甲.乙" href="/a">甲</a></template>:查询页面节点(取得页面节点("资料"), "a[data-qualified]")得到模板内容里的那个 a(普通的文档查询看不到它)。 - (0.3.0)
<code>ab<b class="k">cd</b></code>导出为{"标签":"code","子":["ab",{"标签":"b","类":"k","子":["cd"]}]};把它原样交给构建页面节点树得到等价的新树。 - (0.3.0)
替换页面节点(旧, 新链接)之后添加页面子节点(新链接, 旧):旧节点仍有效,最终位置在新链接内、新链接在原处。 - (0.3.0)
设置页面节点超文本(柄, "<span class=\"tok-k\">甲</span>乙")成功;含<script>、onclick或style的 HTML 抛事故,节点内容不变。
边界
本包不规定:选择器查询之外的节点遍历(nextSibling、TreeWalker、Range 等:请用 读取页面节点树 在豫言里遍历)、任意 CSS 与样式对象、img 与 svg、动态创建 iframe、自定义元素、Shadow DOM、MutationObserver、文档片段。没有文树的平台不提供这个包。