toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

记一次 HTML 富文本特殊字符转义 #332

Open toFrankie opened 5 months ago

toFrankie commented 5 months ago

配图源自 Freepik

背景

此前有个项目里面一个功能是:运营后台进行富文本配置,然后在浏览器、微信小程序各端做展示。

富文本编辑器本身是可以设置字体的,当预设的字体名称包含空格(比如 Microsoft Yahei)时,出现问题了,在小程序端渲染时字体不生效。

当时临时方案是将字体名称用连字符替代空格,因为第二天要上线。

这不是一个好的解决方案,比如针对 Microsoft YaheiPingFang SCHelvetica Neue 这类系统内置的字体,不应写成 Microsoft-Yahei,这是不合理的。

下面开始找找根本原因。

开始之前

通常来说,我们在 CSS 中设置 font-family 时,字体名称是可以包含空格的,但有空格时,应该要用引号括起来。

MDN

The name of a font family. For example, "Times" and "Helvetica" are font families. Font family names containing whitespace should be quoted. For example: "Comic Sans MS".

比如:

.app {
  font-family: "Helvetica Neue", "Segoe UI", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}

鉴于浏览器的包容性很强,其实不写引号,也能正常识别。

寻找原因

经排查发现,前面遇到的问题,其实是就是因为对 HTML 实体转义后,导致 HTML 结构不正确导致的。

后台的富文本编辑器用的是 Braft Editor,它设置字体的方式如下:

const fontFamilies = [
  {
    name: 'ST Song',
    family: '"ST Song"', // "'ST Song'" 在 BraftEditor 内不生效
  },
]
<BraftEditor fontFamilies={fontFamilies} />

是的,在 Braft Editor 里字体名称只能用「双引号」包裹,单引号不生效。

假设富文本编辑器输出的 HTML 如下(Hello World):

<p><span style="font-family:&quot;ST Song&quot;">&quot;Hello World&quot;</span></p>

在浏览器中,这段富文本内容直接通过 Element.innerHTML 去修改 DOM,是可以得到预期效果的。但在小程序里,需要对类似 &quot;(双引号)等 HTML Entity 进行转换,才能正常显示,否则它会将 &quot; 当作五个普通字符,而不是一个双引号。

之前是通过 entities 来做转义的。

import { decodeHTML } from 'entities'

const html = `<p><span style="font-family:&quot;ST Song&quot;">&quot;Hello World&quot;</span></p>`

const transformedHtml = decodeHTML(html)

console.log(transformedHtml)

得到的转义结果是:

<p><span style="font-family:"ST Song"">"Hello World"</span></p>

其实这就看出问题了,style 属性包裹了两个双引号,自然解析不到预期结果。

解决问题

我们知道,CSS 字符串类型的属性值,可以用单引号或双引号。我们先把 style 里可能出现的引号,全部转为单引号,这样的话就能正确解析了。

CSS 属性值使用到引号的(只想起了这几个):

Related Link: https://www.w3.org/TR/2011/REC-CSS2-20110607/syndata.html#values

这样的话,用表达式做匹配出 style 的属性值,然后将里面的引号替换掉,方法如下:

function transformHtmlInlineStyle(html) {
  return html.replace(/(\s+style="[^"]*")/gi, match => {
    return match.replace(/&quot;|&apos;|&#34;|&#39;|&#x22;|&#x27;/gi, "'")
  })
}

因此,上面的流程只要加多一步就行:

  import { decodeHTML } from 'entities'

  const html = `<p><span style="font-family:&quot;ST Song&quot;">&quot;Hello World&quot;</span></p>`

+ let transformedHtml = transformHtmlInlineStyle(html)

  transformedHtml = decodeHTML(html)

  console.log(transformedHtml)

得到的结果为:

<p><span style="font-family:'ST Song'">"Hello World"</span></p>

示例:CodeSandbox

The end.