Apple style span 已移除

本周,我提交了 WebKit 的更改 r92823r93001。它们可能是我提交给 WebKit 代码库的最重要的更改集,因为这些更改集使得 WebKit 在复制粘贴时不再生成包装样式 span 以及 class="Apple-style-span"。事实上,自从我2009年夏天开始在 WebKit 的编辑组件工作以来,这正是我一直想做的两项更改。

Apple style spans 简介

Apple-style-span 是一个带有类名“Apple-style-span”的 HTML span 元素。 每当 WebKit 通过 CSS 为文本应用样式时,就会创建它。 例如,document.execCommand('HiliteColor', false, 'blue'); 可能会生成

<span style="background-color: #0000ff;">hello world</span>

如果选中了“hello world”。 最初的目的是让 WebKit 能够通过区分 WebKit 自己添加的 span 和作者创建的 span,来避免移除或修改作者创建并希望保留的元素。

我们还使用 Apple-style-span 包装复制的内容,以保留复制内容的样式。 例如,如果您在本页复制“hello world”,WebKit 会在 Mac 上的剪贴板中放入以下标记

<span style="border-collapse: separate; color: rgb(0, 0, 0); font-family: Times; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: normal; orphans: 2; text-align: -webkit-auto; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-border-horizontal-spacing: 0px; -webkit-border-vertical-spacing: 0px; -webkit-text-decorations-in-effect: none; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; font-size: medium; "><span style="color: rgb(51, 51, 51); font-family: 'Lucida Grande', Verdana, Arial; font-size: 12px; line-height: 18px; ">hello world</span></span>

Apple style spans 的问题

然而,事实证明,避免修改非 WebKit 创建的 span 充其量是无效的,因为编辑组件必须添加和移除许多其他元素,而且 WebKit 还必须处理由其他浏览器和 CMS 编辑器生成的元素。 此外,避免移除不带 class="Apple-style-span" 的 span 导致标记随着时间的推移变得越来越冗长,因为有时我们不得不取消由这些元素添加的样式,例如 (<b><span style="font-weight: normal;">unbolded text</span></b>)。这在使用 WebKit 作为编辑器的邮件客户端上尤其明显,例如 Apple 的 Mail 或 Gmail(如果用户恰好使用基于 WebKit 的浏览器)。 在某些情况下,一封由 3 行文本组成的电子邮件在 HTML 中占用 3MB 空间,这是因为 WebKit 和其他邮件客户端创建了嵌套的 span。

如果复制的内容包含块节点,包装复制内容的 Apple-style-span 会变得更糟。 考虑以下将“This is title”标记为一级标题的标记

<h1>This is title</h1>

复制“This is title”时,WebKit 在剪贴板中放入以下标记

<span style="color: #000000; font-family: Times; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: normal; orphans: 2; text-align: -webkit-auto; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-decorations-in-effect: none; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; font-size: medium; "><h1>This is title</h1></span>

注意 h1 被包装在一个 span 中! 此外,在 r86983 之前,WebKit 曾经用两个 span 包装内容,以分别保留文档的样式。 在这里,font-family: sans-serif 被设置在 body 元素上,因此存储在下面一个单独的 span 中

<span style="border-collapse: separate; color: #000000; font-family: Times; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: normal; orphans: 2; text-align: -webkit-auto; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-border-horizontal-spacing: 0px; -webkit-border-vertical-spacing: 0px; -webkit-text-decorations-in-effect: none; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; font-size: medium; ">l;<span style="font-family: sans-serif; "><h1>This is title</h1></span></span>

如果我们将上面的例子粘贴到以下标记中 br 元素所在的位置

<h1><br></h1>

WebKit 会生成这个

<h1><span style="font-weight: normal; font-size: medium; "><h1>This is title</h1></span></h1>

这里,两个嵌套 h1 之间的 span 取消了外部 h1 的样式,因为该 span 保留了内容复制自的容器样式;即紧邻 <h1>This is title</h1> 的外部。 这非常糟糕,因为 span 和 h1 都没有为页面添加任何语义或视觉信息,并且它在 HTML4.01、XHTML1.0 和 HTML5 的任何标准下都是无效的。

移除 Apple style spans 的两年项目

当我在2009年夏天开始在 Google 实习时,这个问题引起了我的注意,我决定研究解决它的方法。 然而,ApplyStyleCommand(实现了诸如 execCommand('bold')execCommand('italitc') 等内联样式应用命令)、以及分别负责复制和粘贴的 markup.cppReplaceSelectionCommand 都严重依赖于类名“Apple-style-span”。 特别是,ReplaceSelectionCommand 在复制时检测和处理由 markup.cpp 生成的包装 span 的方式与其他元素截然不同。我很快意识到移除 Apple style spans 需要以下 3 个步骤

  1. 改进 ApplyStyleCommand 使其不依赖于 Apple style spans
  2. 改进复制粘贴代码使其不使用 Apple style spans
  3. 移除 Apple style spans

由于当时我还是实习生,只剩下几周时间,我决定专注于第一步。所以我修复了 ApplyStyleCommand 中的各种 Bug 并重构了代码。

一年后,当我作为全职员工回到 Google 时,我继续修复和重构这个类。 结果,我设计了一种 样式应用算法,该算法现已被 Aryeh 的编辑规范 部分采纳。 它是一个三阶段算法,描述如下

  1. 移除冲突的样式(例如,如果我们要将文本设为斜体,则移除所有 font-style 属性值非 italic 的实例)。
  2. 对于每个内联运行,移除所有与正在应用的样式匹配的样式(例如,如果我们要将文本设为斜体,则移除所有 font-style 属性、em 和 i)。
  3. 用适当的元素或带有适当样式属性的 span 包装每个内联运行;或者向包装每个运行的现有元素添加适当的属性。

我自己对这个算法感到非常自豪,因为它最终会生成非常干净的标记(当前的 WebKit 实现中存在一个样式下推的 Bug)。

在重构 ApplyStyleCommand 取得一些进展后,我也开始清理 markup.cpp 中的 DOM 序列化代码,该代码负责生成两个包装 span。 但我必须处理一些障碍

  1. 存在两个冲突的 createMarkup 函数:一个用于复制另一个用于 innerHTML, 它们通过函数调用而不是类 层次结构来共享代码。 这使得修改每个函数的接口并进行必要的重构以避免添加包装样式 span 变得困难。
  2. 用于复制的 createMarkup 是一个长达 250 行的函数,它序列化范围,确定要序列化的最高祖先,并添加包装 span。 这使得很难看出哪个变量或条件依赖于什么。
  3. markup.cpp 中的各种函数操纵了 CSSMutableStyleDeclaration,但它们的意图以及对粘贴代码的影响并不明显。

为了解决第 1 点和第 2 点,我决定对 markup.cpp 进行大规模重构。 由于 darin 已经为 innerHTML 版本的 createMarkup 引入了 MarkupAccumulator(Darin 总是有最好的重构想法!), 我决定引入 StylizedMarkupAccumulator,它继承自 MarkupAccumulator,用于复制版本的 createMarkup。 重构后, markup.cpp 开始看起来非常干净漂亮(请注意, 在我完成所有重构之前不久,abarth 提取了 MarkupAccumulator.cpp)。 事实上,StylizedMarkupAccumulator 为摆脱包装 span 提供了一个完美的抽象,并且各种重构清楚地表明这是可行的。

现在我必须解决第 3 点。 为了摆脱“Apple-style-span”,我必须完全理解 WebKit 如何保留样式以及编辑组件的各个部分如何操纵和解释样式信息。 同时,我意识到编辑组件的各个部分直接操纵 CSSMutableStyleDeclaration 是有问题的,因为根据我之前使用 ApplyStyleCommand 的经验,存在诸如 background-color 和 text-decoration 等棘手的属性。 即使是看似简单的 font-weight 也很难处理,因为它可能取数值,例如 700 和 400,或关键字,例如 bold 和 normal。 因此,我在编辑组件和 CSS 组件之间引入了一个新的抽象层,即 EditingStyle,以将所有样式操纵代码集中在一个地方。 我对这项正在进行的重构工作感到非常满意,因为它减少了代码重复并发现了许多隐藏的 Bug。

现在,时机到了。 我已经解决了所有阻碍我在复制时摆脱包装样式 span 的 3 个问题。 于是我于2011年5月开始了史诗般的尝试,以摆脱包装样式 span。 这不是一项轻松的工作,因为我们将复制粘贴代码作为其他一些编辑命令的一部分使用,事实上,我花了几近整整一周的时间才创建一个原型。 由于我通常每周提交 5 个或更多的补丁,花整整一周时间在一个甚至无法提交审查的补丁上是非常不寻常的。 但最终得到了回报。 我成功地提出了一个补丁,它去除了包装 span,并且没有导致任何测试回归。 现在,回顾一下我为了移除 Apple style spans 而需要做的事情列表

  1. 改进 ApplyStyleCommand 使其不依赖于 Apple style spans
  2. 改进复制粘贴代码使其不使用 Apple style spans
  3. 移除 Apple style spans

是的,当我本周三提交 用于 34564 的补丁 时, 我只剩下第 3 步了。 所以我继续完成了这个两年项目的第 3 步

  • Bug 66091 – 在 isStyleSpanOrSpanWithOnlyStyleAttribute, isUnstyledStyleSpan, isSpanWithoutAttributesOrUnstyleStyleSpan 和 replaceWithSpanOrRemoveIfWithoutAttributes 之间共享代码
  • Bug 12248 – Apple-style-span 类似乎是不必要的

就这样。 WebKit 版本 93001 不再生成 Apple style spans 了。 我的(也许也是您的)梦想实现了。

致谢

当然,这一切都离不开以下人员和整个 WebKit 社区的支持,我对此表示衷心的感谢

  • Darin Adler
  • Enrica Casucci
  • Eric Seidel
  • Julie Parent
  • Justin Garcia
  • Levi Weintraub
  • Ojan Vafai
  • Tony Chang