编辑器模块特定规则

编辑器模块大约十年没有得到积极维护。因此,需要将此模块视为一个新的模块或处于过渡阶段,以使其行为与其他浏览器保持一致,并采用现代 C++ 样式。

毫无疑问,此编辑器模块正在被重写,以使其现代化并针对当前规范进行优化。此外,此模块执行非常复杂的操作,这可能会导致安全问题。因此,有一些特定的规则。

如果其他浏览器的行为合理,则将其视为标准

编辑行为没有标准化,因为正如您在编辑器类中看到的过多行一样,需要处理边缘情况的数量非常多,这使得标准化变得不可能。此外,我们的编辑器行为并不稳定。某些行为与 Internet Explorer 保持一致,而另一些行为则不是为了为 SeaMonkey 中的电子邮件撰写器和 HTML 撰写器用户提供“更好”的用户体验而设计的,其他浏览器引擎(Blink 和 WebKit)具有相同的根源,但行为却不同于 IE 和 Gecko。

因此,没有参考行为。

如今,浏览器之间的兼容性变得越来越重要,幸运的是,在很多情况下,拥有最大市场份额的 Blink(Chrome/Chromium)的行为比我们的更合理。因此,如果我们收到 Web 兼容性问题报告,从理论上讲,我们应该将行为与 Blink 保持一致。

但是,如果 Blink 的行为也很奇怪,这是最糟糕的情况。在这种情况下,我们应该尝试将行为与 WebKit 保持一致,当且仅当 WebKit 的行为与 Blink 不同且合理时,或者做一些“更好”的事情来隐藏 Web 应用中的问题,并通过创建“临时”Web 平台测试向编辑工作组提交问题。

如果编辑器类的方法仅由辅助类使用,则不要将其设为公共方法

尽管这是一个愚蠢的规则。当然,编辑器类的 API 需要对其他模块公开。但是,其他仅由编辑器模块中的辅助类使用的方法——如果其他模块调用这些方法可能会崩溃,因为编辑器类在开始处理编辑操作(编辑命令或操作)时存储并保证其同级(例如,Selection)——出于性能原因不想这样做。因此,此类方法现在声明为受保护方法,调用者类被注册为友元。

为了解决此问题,我们可以将编辑器类拆分为一个导出类和一个未公开类,并将前者设为代理并拥有后者。但是,这种方法可能会导致性能下降,并且需要大量更改代码行(至少每个方法定义和调用方处的警告消息)。在 bug 1555916 中跟踪。

处理单个编辑器命令或操作的步骤

一个编辑命令或操作在编辑器模块中称为“编辑操作”。当 XPCOM 方法或名为 *AsAction 的公共方法被调用时,处理开始。这些方法首先在堆栈中创建 AutoEditActionDataSetter,然后调用 CanHandle()CanHandleAndMaybeDispatchBeforeInputEvent()CanHandleAndFlushPendingNotifications() 之一。如果 CanHandleAndMaybeDispatchBeforeInputEvent() 导致分派 beforeinput 事件,并且该事件被 Web 应用使用,则返回 NS_ERROR_EDITOR_ACTION_CANCELED。在这种情况下,由于 beforeinput 事件定义,该方法可以执行任何操作。

此时,AutoEditActionDataSetter 存储处理编辑操作所需的 Selection 等对象,并将 EditorBase::mEditActionData 设置为其地址。然后所有编辑器方法都可以通过指针(通常包装在内联方法中)访问这些对象,并且对象的生存期得到保证。

然后,这些方法调用一个或多个编辑子操作处理程序。例如,当用户在未折叠的选择范围内输入字符时,编辑器需要先删除选定的内容,然后在该处插入字符。为了实现此行为,“插入文本”编辑操作处理程序需要调用“删除选择”子操作处理程序和“插入文本”子操作处理程序。子编辑操作处理程序命名为 *AsSubAction

编辑子操作处理程序的调用者或处理程序本身在堆栈中创建 AutoPlaceholderBatch。这将创建一个占位符事务,以便使用单个“撤消”命令使所有事务可撤消。

然后,每个编辑子操作处理程序在堆栈中创建 AutoEditSubActionNotifier,如果它是最高级别的编辑子操作处理程序,则在创建时调用 OnStartToHandleTopLevelEditSubAction(),并在销毁时调用 OnEndHandlingTopLevelEditSubAction()。后者将清理修改的范围,例如,删除不必要的空节点。

最后,编辑子操作在 AutoEditSubActionNotifier 生存期间执行某些操作。编辑子操作处理程序的辅助方法通常命名为 *WithTransaction,因为它们使用事务类完成,以使所有操作都可撤消。

不要立即更新 Selection

更改 Selection 的范围代价高昂(由于验证新范围、通知新选定或未选定的框架、通知选择侦听器等),并且在开始处理某些内容时检索当前 Selection 范围会使代码具有状态,这在调查错误时更难调试。因此,每个方法都应返回新的光标位置或更新作为 AutoRangeArray 的输入/输出参数给出的范围。Result<CaretPoint, nsresult> 是前者的良好结果类型,而后者在方法需要使 Selection 与给定范围类似时很有用,例如,当选择周围的段落更改为不同类型的块时。最后,编辑子操作处理程序方法应在销毁 AutoEditSubActionNotifier 之前更新 Selection,其后处理需要 Selection

不要在 OnEndHandlingTopLevelEditSubAction() 中添加新内容

当最高级别的编辑子操作被处理时,将调用 OnEndHandlingTopLevelEditSubAction,它会清理修改范围中(或周围)的一些内容。但是,这种“后处理”方法使得更难更改行为以修复 Web 兼容性问题。例如,它会删除范围内的空节点,但如果仅有意插入了一些空节点,则它没有详细信息,并且可能会意外删除必要的空节点。

相反,新内容应在修改 DOM 树时或之后立即执行,如果需要禁用后处理,则向 EditorBase::TopLevelEditSubActionData 添加新的 bool 标志,并且当它被设置时,使 OnEndHandlingTopLevelEditSubAction 停止执行某些操作。

不要使用 NS_WARN_IF 检查 NS_FAILED、 isErr() 和 Failed()

类似 NS_FAILED(rv) 的警告消息除了行号之外没有帮助,并且在我们收到 Web 兼容性报告的情况下,编辑器模块中的某个地方可能会获得意外的结果。为了节省 Web 兼容性问题调查时间,每个错误都应警告哪个方法调用失败,例如

nsresult rv = DoSomething();
if (NS_FAILED(rv)) {
  NS_WARNING("HTMLEditor::DoSomething() failed");
  return rv;
}

这些警告将让你在调试版本中了解错误的堆栈。换句话说,当您在编辑器中调查 Web 兼容性问题时,您应该首先在调试版本中执行重现步骤。然后,您将在终端中看到错误点堆栈。

当编辑器被销毁时返回 NS_ERROR_EDITOR_DESTROYED

在编辑器类方法运行期间最严重的错误是 Web 应用销毁了编辑器实例。这可以通过调用 EditorBase::Destroyed() 来检查,如果它返回 true,则方法应返回 NS_ERROR_EDITOR_DESTROYED 并停止处理任何内容。然后,所有正确处理错误结果的调用者也将停止处理。最后,公共方法应返回 EditorBase::ToGenericNSResult(rv) 而不是公开编辑器模块的内部错误。

请注意,销毁编辑器是 Web 应用的故意行为。因此,我们不应该为此错误原因抛出异常。因此,公共方法不应该返回错误。

当您使方法正确返回 NS_ERROR_EDITOR_DESTROYED 时,您应该将该方法标记为 [[nodiscard]]。换句话说,如果您在方法定义中看到 [[nodiscard]],并且它返回 nsresultResult<*, nsresult>,则方法调用者不需要自己检查 Destroyed()

尽可能使用引用而不是指针

当您创建或重新设计方法时,如果它们接受参数,则应使用引用而不是指针。此规则强制调用者执行空值检查,这避免了可能出现意外情况,例如

inline bool IsBRElement(const nsINode* aNode) {
  return aNode && aNode->IsHTMLElement(nsGkAtoms::br);
}

void DoSomethingExceptIfBRElement(const nsINode* aNode) {
  if (IsBRElement(aNode)) {
    return;
  }
  // Do something for non-BR element node.
}

在这种情况下,DoSomethingExceptIfBRElement 预期 aNode 永远不会是 nullptr,但至少在构建时可能是。使用引用在构建时修复了此错误。

对于无状态方法,使用 EditorUtilsHTMLEditUtils

当您向编辑器类添加新的静态方法,或在定义编辑器类的 cpp 文件中添加新的内联方法时,请检查它是否是一个可能在编辑器模块的其他地方使用的通用方法。如果该方法只可能在 HTMLEditor 或其辅助类中使用,则该方法应放在 HTMLEditUtils 中。如果它可能在 EditorBaseTextEditor 或其辅助类中使用,则它应放在 EditorUtils 中。

不要使用 bool 参数

如果您创建了一个带有一个或多个 bool 参数的新方法,请使用 enum class 代替,因为调用方代码中的 truefalse 不易于阅读。例如,您可能无法理解此示例的含义

if (IsEmpty(aNode, true)) {

为了避免此问题,您应该为每个布尔参数创建一个新的 enum class。例如:

if (IsEmpty(aNode, TreatSingleBR::AsVisible)) {

基本上,enum class 的名称及其值的名称都能清晰地表达含义。但是,如果无法做到这一点,请使用 NoYes 作为值,例如

if (DoSomething(aNode, OnlyIfEmpty::Yes)) {

不要使用输出参数

在大多数情况下,编辑器方法会遇到底层 API 的错误,因此编辑器方法通常会返回错误代码。另一方面,很多代码需要返回计算结果,例如新的光标位置、是否已处理、忽略或取消、查找的目标节点等。我们过去使用 nsresult 作为返回值类型,并使用输出参数表示其他结果,但这会导致调用方代码中出现大量自动变量,并且重复使用这些变量会使代码更难以理解。

现在我们可以使用 mozilla::Result<Foo, nsresult> 代替。