liujie
2026-09-17 c867f2b648ce3dfb5a679ca86bb38f228317fc3b
fix(小程序): 修复协议页无法解析后台 Word 富文本 HTML

后台协议为 Word 导出 HTML,含 <o:p> 命名空间标签与条件注释,
原解析器标签名正则不允许冒号导致抛错,页面把整段源码当纯文本显示。

- 标签名正则支持冒号,新增 Word HTML 清洗(去 head/style/条件注释/命名空间标签/html-body 外壳)
- 文本节点解码常见 HTML 实体,未闭合注释不再抛错
- 解析失败兜底改为去标签纯文本,不再倾倒 HTML 源码
2个文件已修改
88 ■■■■■ 已修改文件
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/common/html-parser.js 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/agreement-detail/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/common/html-parser.js
@@ -79,8 +79,10 @@
 *
 */
// Regular Expressions for parsing tags and attributes
var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
var endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
// fixed by 名导精讲 标签名允许冒号,兼容 Word 导出的 <o:p>、<v:shape> 等命名空间标签,
// 否则解析循环无法前进会抛出 Parse Error,导致页面把整段 HTML 当纯文本显示。
var startTag = /^<([-A-Za-z0-9_:]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
var endTag = /^<\/([-A-Za-z0-9_:]+)[^>]*>/;
var attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; // Empty Elements - HTML 5
var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr'); // Block Elements - HTML 5
@@ -121,6 +123,10 @@
          }
          html = html.substring(index + 3);
          chars = false;
        } else {
          // 未闭合注释:吞掉剩余内容,避免解析循环无法前进抛出 Parse Error
          html = '';
          chars = false;
        } // end tag
@@ -251,7 +257,72 @@
}
function removeDOCTYPE(html) {
  return html.replace(/<\?xml.*\?>\n/, '').replace(/<!doctype.*>\n/, '').replace(/<!DOCTYPE.*>\n/, '');
    return html
        .replace(/<\?xml[\s\S]*?\?>/gi, '')
        .replace(/<!DOCTYPE[^>]*>/gi, '');
}
// Word 导出 HTML 专用清洗:去掉小程序富文本无法处理的噪声内容。
function sanitizeWordHtml(html) {
    return removeDOCTYPE(html)
        // downlevel-revealed 条件注释(<![if !supportLists]> / <![endif]>)不是标准注释,会让解析循环卡死
        .replace(/<!\[if[\s\S]*?\]>/gi, '')
        .replace(/<!\[endif\]>/gi, '')
        // 标准注释与 downlevel-hidden 条件注释块(内含 <xml><w:WordDocument> 等大段配置)
        .replace(/<!--[\s\S]*?-->/g, '')
        .replace(/<head[\s\S]*?<\/head>/gi, '')
        .replace(/<(style|script|xml)[\s\S]*?<\/\1>/gi, '')
        // html/body 外壳 rich-text 不支持,可能连同子树一起被丢弃,只保留内部节点
        .replace(/<\/?html[^>]*>/gi, '')
        .replace(/<\/?body[^>]*>/gi, '')
        // 命名空间标签(<o:p>、<v:shape>、<st1:place> 等)只删标签本身,保留其中的文本
        .replace(/<\/?[A-Za-z][\w.-]*:[\w.-]*(?:\s[^>]*)?\/?>/g, '');
}
var namedEntities = {
    nbsp: ' ',
    ensp: ' ',
    emsp: ' ',
    quot: '"',
    apos: "'",
    amp: '&',
    lt: '<',
    gt: '>',
    ldquo: '\u201c',
    rdquo: '\u201d',
    lsquo: '\u2018',
    rsquo: '\u2019',
    mdash: '\u2014',
    ndash: '\u2013',
    hellip: '\u2026',
    middot: '\u00b7',
    bull: '\u2022',
    dagger: '\u2020',
    permil: '\u2030',
    lsaquo: '\u2039',
    rsaquo: '\u203a',
    times: '\u00d7',
    divide: '\u00f7',
    copy: '\u00a9',
    reg: '\u00ae',
    trade: '\u2122',
    deg: '\u00b0',
    minus: '\u2212',
    szlig: '\u00df'
};
// rich-text 的文本节点不会解码实体,Word 正文里的 &ldquo; 等需要在这里转成字符。
function decodeEntities(text) {
    if (text.indexOf('&') < 0) return text;
    return text.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, function (all, name) {
        if (name[0] === '#') {
            var code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
            return isNaN(code) || code < 0 || code > 0x10ffff ? all : String.fromCodePoint(code);
        }
        // amp 最后解,避免 &amp;lt; 被二次解码
        if (name === 'amp') return '&';
        return Object.prototype.hasOwnProperty.call(namedEntities, name) ? namedEntities[name] : all;
    });
}
function parseAttrs(attrs) {
@@ -270,7 +341,7 @@
}
function parseHtml(html) {
  html = removeDOCTYPE(html);
    html = sanitizeWordHtml(html);
  var stacks = [];
  var results = {
    node: 'root',
@@ -317,7 +388,7 @@
    chars: function chars(text) {
      var node = {
        type: 'text',
        text: text
        text: decodeEntities(text)
      };
      if (stacks.length === 0) {
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/agreement-detail/index.vue
@@ -111,7 +111,12 @@
  try {
    return htmlParser(html)
  } catch (error) {
    return [{ type: 'text', text: content || '' }]
    // 兜底只保留可读文本,避免把 HTML 源码直接展示给用户
    const plainText = (content || '')
      .replace(/<style[\s\S]*?<\/style>/gi, '')
      .replace(/<[^>]+>/g, '')
      .replace(/&nbsp;/g, ' ')
    return [{ type: 'text', text: plainText }]
  }
})