编辑器模块特定规则¶
编辑器模块大约十年没有得到积极维护。因此,需要将此模块视为一个新的模块或处于过渡阶段,以使其行为与其他浏览器保持一致,并采用现代 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]]
,并且它返回 nsresult
或 Result<*, 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
,但至少在构建时可能是。使用引用在构建时修复了此错误。
对于无状态方法,使用 EditorUtils
或 HTMLEditUtils
¶
当您向编辑器类添加新的静态方法,或在定义编辑器类的 cpp 文件中添加新的内联方法时,请检查它是否是一个可能在编辑器模块的其他地方使用的通用方法。如果该方法只可能在 HTMLEditor
或其辅助类中使用,则该方法应放在 HTMLEditUtils
中。如果它可能在 EditorBase
或 TextEditor
或其辅助类中使用,则它应放在 EditorUtils
中。
不要使用 bool 参数¶
如果您创建了一个带有一个或多个 bool
参数的新方法,请使用 enum class
代替,因为调用方代码中的 true
或 false
不易于阅读。例如,您可能无法理解此示例的含义
if (IsEmpty(aNode, true)) {
为了避免此问题,您应该为每个布尔参数创建一个新的 enum class
。例如:
if (IsEmpty(aNode, TreatSingleBR::AsVisible)) {
基本上,enum class
的名称及其值的名称都能清晰地表达含义。但是,如果无法做到这一点,请使用 No
和 Yes
作为值,例如
if (DoSomething(aNode, OnlyIfEmpty::Yes)) {
不要使用输出参数¶
在大多数情况下,编辑器方法会遇到底层 API 的错误,因此编辑器方法通常会返回错误代码。另一方面,很多代码需要返回计算结果,例如新的光标位置、是否已处理、忽略或取消、查找的目标节点等。我们过去使用 nsresult
作为返回值类型,并使用输出参数表示其他结果,但这会导致调用方代码中出现大量自动变量,并且重复使用这些变量会使代码更难以理解。
现在我们可以使用 mozilla::Result<Foo, nsresult>
代替。