toFrankie / blog

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

零宽空格 U+200B 引发的问题及扩展 #296

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

背景

是这样的,最近在写一个微信公众号的处理脚本,用来替换替换文章中的指定内容。

function getInsertElement(rootElement) {
  const matchFlag = 'AA'
  const pList = [...rootElement.querySelectorAll('p')]
  let matchedElement = pList.find(el => {
    const text = (el.textContent || el.innerText).replace(/\u00a0/gi, '').trim()
    return text === matchFlag
  })
  return matchedElement
}

上面的方法是脚本的一部分,用于获取文章中指定字符串所在的 DOM 元素,思路是通过 Node.textContent 来匹配的。

在调试的时候,发现有时候匹配不上,用「肉眼」看是没问题的,但硬是匹配不上。经过一番排查之后,发现了一个有趣的事情,如图:

在编辑器内有字符 AA,然后使用 encodeURIComponent($0.textContent) 的编码结果是 AA%E2%80%8B,所以上面 text === matchFlag 比较结果为 false,自然就匹配不上了。下面将其转换为 Unicode 字符:

function string2Unicode(str) {
  return str
    .split('')
    .map(value => {
      const temp = value.charCodeAt(0).toString(16).padStart(4, '0').toUpperCase()
      if (temp.length > 2) return '\\u' + temp
      return value
    })
    .join('')
}

const encodedStr = '%E2%80%8B'
const originString = decodeURIComponent(encodedStr)
const unicodeStr = string2Unicode(originString)
console.log(unicodeStr) // \u200B

转换得出 %E2%80%8B 的 Unicode 编码为 U+200B,然后查询这里发现它是「零宽空格」,是一种不可见的字符。

因此,只要使用正则表达式 /\u200b/gi,把所有零宽空格干掉就行了。

function getInsertElement(rootElement) {
  const matchFlag = 'AA'
  const pList = [...rootElement.querySelectorAll('p')]
  let matchedElement = pList.find(el => {
    const text = (el.textContent || el.innerText)
      .replace(/\u00a0/gi, '')
      .replace(/\u200b/gi, '')
      .trim()
    return text === matchFlag
  })
  return matchedElement
}

零宽空格

零宽空格(zero-width space,ZWSP)是一种不可见、不可打印的 Unicode 字符,用于可能需要换行处。

在 Unicode 中,该字元为 U+200B。在 HTML 中转义字符有:​​​。一般情况下,它是不可见的,但一些软件对这些不可见字符做了处理,视觉上可感知。举个例子:

相邻单词之间有一个零宽空格 👇

LoremIpsumDolorSitAmetConsecteturAdipiscingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliquaUtEnimAdMinimVeniamQuisNostrudExercitationUllamcoLaborisNisiUtAliquipExEaCommodoConsequatDuisAuteIrureDolorInReprehenderitInVoluptateVelitEsseCillumDoloreEuFugiatNullaPariaturExcepteurSintOccaecatCupidatatNonProidentSuntInCulpaQuiOfficiaDeseruntMollitAnimIdEstLaborum

相邻单词之间无零宽空格 👇

LoremIpsumDolorSitAmetConsecteturAdipiscingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliquaUtEnimAdMinimVeniamQuisNostrudExercitationUllamcoLaborisNisiUtAliquipExEaCommodoConsequatDuisAuteIrureDolorInReprehenderitInVoluptateVelitEsseCillumDoloreEuFugiatNullaPariaturExcepteurSintOccaecatCupidatatNonProidentSuntInCulpaQuiOfficiaDeseruntMollitAnimIdEstLaborum

它们在 VS Code 及页面中的展示效果,如图所示:

扩展

除此之外,还有零宽连字、零宽不连字也是不可见字符。

  • 零宽连字(zero-width joiner,ZWJ)是一个控制字符,放在某些需要复杂排版语言(如阿拉伯语、印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。其 Unicode 编码为 U+200D,HTML 转义字符有:‍‍

  • 零宽不连字(zero-width non-joiner,ZWNJ)是一个不打印字符,放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。其 Unicode 编码为U+200C,HTML 转义字符有:‌‌

相信你也看过网友「把幸福的一家几口强行分开」的段子,哈哈:

我们利用前面的 string2Unicode() 方法,将其转化为 Unicode 编码,如下:

其实它们是由多个字符组合而成的,前面所看到的“空字符串”其实就是 U+200D(零宽连字)。

一篇有趣的文章:Why […‘👩❤️💋👨’] returns [‘👩’, ‘’, ‘❤’, ‘️’, ‘’, ‘💋’, ‘’, ‘👨’] in JavaScript?

应用

零宽字符能做什么?

The end.