Apple style span 已移除
本周,我提交了 WebKit 的更改 r92823 和 r93001。它们可能是我提交给 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.cpp 和 ReplaceSelectionCommand 都严重依赖于类名“Apple-style-span
”。 特别是,ReplaceSelectionCommand 在复制时检测和处理由 markup.cpp 生成的包装 span 的方式与其他元素截然不同。我很快意识到移除 Apple style spans 需要以下 3 个步骤
- 改进 ApplyStyleCommand 使其不依赖于 Apple style spans
- 改进复制粘贴代码使其不使用 Apple style spans
- 移除 Apple style spans
由于当时我还是实习生,只剩下几周时间,我决定专注于第一步。所以我修复了 ApplyStyleCommand 中的各种 Bug 并重构了代码。
一年后,当我作为全职员工回到 Google 时,我继续修复和重构这个类。 结果,我设计了一种 样式应用算法,该算法现已被 Aryeh 的编辑规范 部分采纳。 它是一个三阶段算法,描述如下
- 移除冲突的样式(例如,如果我们要将文本设为斜体,则移除所有 font-style 属性值非 italic 的实例)。
- 对于每个内联运行,移除所有与正在应用的样式匹配的样式(例如,如果我们要将文本设为斜体,则移除所有 font-style 属性、em 和 i)。
- 用适当的元素或带有适当样式属性的 span 包装每个内联运行;或者向包装每个运行的现有元素添加适当的属性。
我自己对这个算法感到非常自豪,因为它最终会生成非常干净的标记(当前的 WebKit 实现中存在一个样式下推的 Bug)。
在重构 ApplyStyleCommand 取得一些进展后,我也开始清理 markup.cpp 中的 DOM 序列化代码,该代码负责生成两个包装 span。 但我必须处理一些障碍
- 存在两个冲突的 createMarkup 函数:一个用于复制,另一个用于 innerHTML, 它们通过函数调用而不是类 层次结构来共享代码。 这使得修改每个函数的接口并进行必要的重构以避免添加包装样式 span 变得困难。
- 用于复制的 createMarkup 是一个长达 250 行的函数,它序列化范围,确定要序列化的最高祖先,并添加包装 span。 这使得很难看出哪个变量或条件依赖于什么。
- 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 而需要做的事情列表
改进 ApplyStyleCommand 使其不依赖于 Apple style spans改进复制粘贴代码使其不使用 Apple style spans- 移除 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