如何使用PHP递归处理富文本JSON语法树完成API侧校验与内容加工?
结构化语法树处理实现方案
结构规则对齐
从给出的语法树示例可以归纳出三类节点:
- 纯文本字符串:无结构化标记的普通文字内容
- 结构化标记对象:带有
type/value字段的自定义节点,包含样式类(加粗、斜体)、自定义元素类(图片、@提及)两类 - 嵌套数组容器:分层对应整个文档、段落区块、单行内容、节点值集合
核心处理逻辑
因为整个语法树是完全递归嵌套的结构,不需要写复杂的状态机,也不需要分步骤多次遍历,用统一递归遍历函数就能覆盖所有需求,单次遍历就能同时完成内容过滤、合法性校验、纯文本提取、通知信息收集,哪怕是几万字的长文档也不会有性能压力:
- 递归分支规则
- 遍历到数组类型的内容:逐个处理数组内的子元素,把每个子元素的有效处理结果收集起来,空值直接过滤;如果数组处理完没有有效内容,就整体丢弃,不保留空结构
- 遍历到纯字符串类型:直接把内容拼接到纯文本结果里,按照规则如果纯文本不需要存储进结构化语法树,就返回空值让它被过滤
- 遍历到带
type字段的结构化对象:按节点类型分支处理:- 加粗、斜体这类纯样式节点:直接递归处理它的
value字段集合,把处理后的有效子节点透传回原结构,只要子节点不全为空就保留这个样式节点 - 图片类自定义元素:先校验地址合规性(比如协议是否为HTTPS、域名是否在白名单内),校验通过就透传保留,纯文本结果里对应位置插入
[图片]标记 - @提及类自定义元素:校验用户名格式合法性,把合规的用户名单独存入通知列表用于后续触发提醒,纯文本结果里对应位置插入
@用户名标记,处理合规的节点透传保留
- 加粗、斜体这类纯样式节点:直接递归处理它的
- 结果收集机制
整个递归函数在运行时,在函数外层维护两个临时收集器,不需要遍历两次语法树:
- 纯文本收集器:遍历过程中把每个节点对应的文本内容逐次拼接,行与行、段落与段落之间自动加对应分隔符,最终生成和渲染效果一致的纯文本内容
- 加工后语法树收集器:收集每个节点经过校验、过滤后的有效结构,自动剔除所有不合规、空值的节点,最终生成可以直接存储、回传给前端的新版语法树
边界注意事项
- 递归深度控制:因为样式节点支持无限嵌套,需要设置递归深度阈值,避免遇到恶意构造的过深嵌套结构导致栈溢出
- 空结构自动清理:如果一个父节点处理完后,所有子元素都被过滤成空,那这个父节点本身也要被丢弃,不要在最终存储的结构里留无意义的空节点
- 未知节点拦截:不在白名单内的结构化节点直接丢弃,从入口阻断非法注入的内容
- 纯文本拼接的格式对齐:处理每一行内容的时候,要把换行符自然拼接进去,不同section之间保留段落分隔,最终生成的纯文本读起来和实际渲染的效果一致,不要把所有文字都拼成一长串
核心参考代码(Node.js 侧)
// 外层临时收集器 let plainTextResult = []; let mentionNotifyList = []; function processTreeNodes(node, depth = 0) { // 设置最大递归深度,防止栈溢出 if (depth > 100) return null; // 数组类型:逐个子元素处理 if (Array.isArray(node)) { const validChildren = []; for (const child of node) { const processedChild = processTreeNodes(child, depth + 1); if (processedChild !== null) validChildren.push(processedChild); } // 无有效内容的空数组直接丢弃 return validChildren.length ? validChildren : null; } // 纯字符串类型 if (typeof node === 'string') { plainTextResult.push(node); // 纯文本节点不存入结构化语法树 return null; } // 结构化对象类型 if (typeof node === 'object' && node.type) { // 样式类节点透传 if (['bold', 'italic'].includes(node.type)) { const processedValue = processTreeNodes(node.value, depth + 1); return processedValue ? { type: node.type, value: processedValue } : null; } // 图片节点校验 if (node.type === 'image') { const isUrlValid = /^https:\/\/允许的图片域名/.test(node.value); if (!isUrlValid) return null; plainTextResult.push('[图片]'); return { type: 'image', value: node.value }; } // @提及节点处理 if (node.type === 'mention') { const username = node.value; const isUsernameValid = /^[a-z0-9_]{3,20}$/.test(username); if (!isUsernameValid) return null; mentionNotifyList.push(username); plainTextResult.push(`@${username}`); return { type: 'mention', value: username }; } } // 未知类型节点直接拦截 return null; } // 入口调用 const finalStoredTree = processTreeNodes(originalInputTree); const finalPlainText = plainTextResult.join('').replace(/(\s*\n){3,}/g, '\n\n');
内容的提问来源于stack exchange,提问作者Austin Burk
相关产品推荐
相关产品推荐

