找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
Close
查看: 29|回复: 0

[维护日志] [2026-07-18] [修复] [Markdown转UBB列表解析] - 空行后非列表内容被错误吞入列表

[复制链接]
  • 打卡等级:本地老炮

9336

威望

6912

金钱

1万

贡献

管理员

自由的灵魂

积分
106852
主题
5602
回帖
26633
注册时间
2003-4-10
最后登录
2026-8-14
发表于 2026-7-18 02:56:32 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
1、问题缘起

用户反馈:Markdown转UBB时,有序列表标题(如 1. 标题一)与无序列表交替出现时,空行后的加粗标题被错误地吞入前面的无序列表中。

源文本
  1. 引子段落

  2. **1. 标题一**
  3. 正文正文正文正文。

  4. **2. 标题二**
  5. 正文正文正文正文:

  6. -   **无序列表项1**:正文正文正文正文
  7. -   **无序列表项2**:正文正文正文正文
  8. -   **无序列表项3**:正文正文正文正文

  9. **3. 标题三**
  10. 正文正文正文正文:

  11. -   **无序列表项1**:正文正文正文正文
  12. -   **无序列表项2**:正文正文正文正文

  13. **总结**:正文正文正文正文
复制代码


错误输出
  1. [list]
  2. [*] [b]无序列表项1[/b]:正文正文正文正文
  3. [*] [b]无序列表项2[/b]:正文正文正文正文
  4. [*] [b]无序列表项3[/b]:正文正文正文正文
  5. [b]3. 标题三[/b]
  6. 正文正文正文正文:
  7. [*] [b]无序列表项1[/b]:正文正文正文正文
  8. [*] [b]无序列表项2[/b]:正文正文正文正文
  9. [b]总结[/b]:正文正文正文正文
  10. [/list]
复制代码


问题分析
  • 空行后的 3. 标题三 不是列表项,但列表未关闭
  • 根因:列表项收集循环中遇到空行直接 j++; continue; 跳过,未检查空行后的内容类型
  • 空行后的非列表内容被当作当前列表项的延续内容收集进去





2、解决方案

(1)简要技术分析

核心原则:空行后的下一行不是列表项 → 关闭当前列表。 在列表项内容收集循环和外层主循环的空行处理中,统一检查空行后的下一行,如果不是列表项(且不是代码块占位符),则结束当前列表项收集或关闭整个列表。

(2)修改文件列表

文件路径修改类型
/mnt/agents/upload/index.php/mnt/agents/output/index.php修改


(3)关键作用代码段

文件:index.php

修改点1:列表项收集循环中的空行处理(内层)

位置:processNestedListsFixed 函数,列表项内容收集 while (j < lines.length) 循环内。

修改前:
  1. if (nextTrimmed === '') { j++; continue; }
复制代码


修改后:
  1. // 遇到空行,检查空行后的内容
  2. if (nextTrimmed === '') {
  3.     // 查看空行后的下一行(跳过连续空行)
  4.     let afterEmptyIdx = j + 1;
  5.     while (afterEmptyIdx < lines.length && lines[afterEmptyIdx].trim() === '') {
  6.         afterEmptyIdx++;
  7.     }
  8.     if (afterEmptyIdx < lines.length) {
  9.         // 如果空行后不是列表项、不是代码块占位符,则结束当前列表项收集
  10.         if (!isListItem(lines[afterEmptyIdx]) && !lines[afterEmptyIdx].trim().startsWith('###CODEBLOCK')) {
  11.             break;
  12.         }
  13.     }
  14.     j++;
  15.     continue;
  16. }
复制代码


修改点2:外层空行处理逻辑简化(外层)

位置:processNestedListsFixed 函数,外层主循环 if (trimmedLine === '') 分支内。

修改前:
  1. // 判断下一行是否是新的有序列表项(数字. 开头)
  2. const nextIsOrderedListItem = /^\d+\.\s+/.test(nextLine);
  3. // 判断下一行是否是无序列表项
  4. const nextIsUnorderedListItem = /^[-*+]\s+/.test(nextLine);
  5. // 判断下一行是否是代码块占位符(作为列表项内容继续)
  6. const nextIsCodeBlock = nextLine.startsWith('###CODEBLOCK');

  7. // 获取下一行的列表类型
  8. const nextListType = /^\d+\./.test(nextLine) ? 'list=1' : 'list';
  9. const currentListType = listStack.length > 0 ? listStack[listStack.length - 1].type : null;

  10. // 列表类型切换检测:如果下一行是不同类型列表,关闭当前列表
  11. const isTypeSwitch = listStack.length > 0 &&
  12.     (nextIsOrderedListItem || nextIsUnorderedListItem) &&
  13.     nextListType !== currentListType;

  14. // 只有在下一行不是列表项、不是代码块、且不是类型切换时,才关闭列表
  15. if (listStack.length > 0 && !nextIsOrderedListItem && !nextIsCodeBlock && !isTypeSwitch) {
  16.     closeAllLists();
  17. }
复制代码


修改后:
  1. // 判断下一行是否是列表项
  2. const nextIsListItem = isListItem(lines[nextIdx] || '');
  3. // 判断下一行是否是代码块占位符(作为列表项内容继续)
  4. const nextIsCodeBlock = nextLine.startsWith('###CODEBLOCK');

  5. // 获取下一行的列表类型
  6. const nextListType = nextIsListItem ? getListType(lines[nextIdx] || '') : null;
  7. const currentListType = listStack.length > 0 ? listStack[listStack.length - 1].type : null;

  8. // 列表类型切换检测:如果下一行是不同类型列表,关闭当前列表
  9. const isTypeSwitch = listStack.length > 0 && nextIsListItem && nextListType !== currentListType;

  10. // 空行后的下一行不是列表项(且不是代码块),关闭当前列表
  11. if (listStack.length > 0 && !nextIsListItem && !nextIsCodeBlock) {
  12.     closeAllLists();
  13. } else if (listStack.length > 0 && isTypeSwitch) {
  14.     // 列表类型切换,关闭当前列表
  15.     closeAllLists();
  16. }
复制代码





3、最终实现效果

输入(Markdown)输出(UBB)
1. 标题一 + 正文1. 标题一 + 正文(独立段落)
- 列表项1
  • 列表项1
空行 + 3. 标题三 关闭列表,然后 3. 标题三(独立段落)
空行 + - 新列表项 关闭旧列表,然后
  • 新列表项(新列表)


完整输出示例
  1. 引子段落

  2. [b]1. 标题一[/b]
  3. 正文正文正文正文。

  4. [b]2. 标题二[/b]
  5. 正文正文正文正文:

  6. [list]
  7. [*] [b]无序列表项1[/b]:正文正文正文正文
  8. [*] [b]无序列表项2[/b]:正文正文正文正文
  9. [*] [b]无序列表项3[/b]:正文正文正文正文
  10. [/list]

  11. [b]3. 标题三[/b]
  12. 正文正文正文正文:

  13. [list]
  14. [*] [b]无序列表项1[/b]:正文正文正文正文
  15. [*] [b]无序列表项2[/b]:正文正文正文正文
  16. [/list]

  17. [b]总结[/b]:正文正文正文正文
复制代码





4、重要技术沉淀

  • 空行是列表边界的重要信号:Markdown 中,空行通常表示段落/列表的边界。列表解析器遇到空行时,必须"向前看"检查后续内容,再决定是否关闭列表。
  • 统一使用 isListItem() 判断:外层空行处理原先用独立正则判断有序/无序列表项,与 isListItem() 函数逻辑不一致,导致判断结果不同。统一使用函数判断可避免逻辑分歧。
  • 列表项收集循环也需要"向前看":不仅外层主循环需要检查空行后的内容,内层列表项内容收集循环同样需要,否则非列表内容会被吞入当前。





5、进一步优化的可能性

  • 嵌套列表中的空行处理:当前修复主要针对同级列表与正文交替的场景,深层嵌套列表(如三级嵌套)中的空行边界情况可能需要进一步测试。
  • 引用块与列表的交互> 引用块与列表相邻时的边界处理尚未充分测试。
  • 表格与列表的交互:表格行与列表项相邻时的解析边界。
不懂就搜!点此搜点拨论坛。如果本坛没有,请尝试点此问AI,或跟帖提问。
发帖前注意看置顶帖
不定期借助AI对点拨论坛陈年老帖进行挖坟回复,打扰勿怪!
点拨论坛的域名你记住了吗?www.dianbo.org
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋|点拨论坛 |网站地图|网站地图

GMT+8, 2026-8-14 20:40 , Processed in 0.062907 second(s), 9 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表