CKEditor 4 Copy Formatting 插件上下文控制:allowedContexts 配置解析与手动测试验证指南
CKEditor 4 Copy Formatting 插件上下文控制:allowedContexts 配置解析与手动测试验证指南
本文围绕 CKEditor 4 的 Copy Formatting(复制格式)插件,深入讲解其 copyFormatting_allowedContexts 配置项如何控制格式复制/应用的作用范围(文本、列表、表格三种上下文)。文章以仓库中的专项手动测试用例为主线,结合插件源码与自动化测试,说明配置的取值、判定逻辑、源码实现与验证方法。读完本文,你将掌握该配置的完整语义、底层判定原理,并能据此搭建自己的上下文限制场景与测试环境。
一、背景:Copy Formatting 插件与"上下文"(Context)概念
Copy Formatting 是 CKEditor 4 自 4.6.0 起提供的特性(见 CHANGES.md 中 #14707 条记录),用于把文档中某处的字符格式(如加粗、颜色、字号等)快速复制并应用到其他位置。它位于 plugins/copyformatting/plugin.js,注册了 copyFormatting(复制格式)与 applyFormatting(应用格式)两个命令以及对应的工具栏按钮 CopyFormatting。
插件把目标内容划分为三种互不相同的上下文(context):
| 上下文 | 含义 | 判定元素 |
|---|---|---|
'text' |
普通文本上下文 | 非列表、非表格的普通段落/内联文本 |
'list' |
列表上下文 | ul、ol |
'table' |
表格上下文 | table |
上下文决定了两件事:能否从该区域复制格式,以及把格式应用到该区域时采用何种策略。例如,从列表项复制的样式应用到普通段落时,<li> 会被转换为带内联样式的 <span>;而应用到表格单元格时则走另一套针对 td/th 的处理逻辑。控制这些行为的关键配置正是 copyFormatting_allowedContexts。
二、核心文档解读:allowedContexts 手动测试用例
仓库中的专项测试文档 tests/plugins/copyformatting/manual/allowedcontexts.md 内容如下:
@bender-ui: collapsed
@bender-tags: bug, copyformatting, 4.6.0
@bender-ckeditor-plugins: copyformatting, toolbar, wysiwygarea, floatingspace, elementspath, undo
Check if the Copy Formatting plugin is respecting config for allowing contexts, e.g. if you're inside "List and text contexts" editor, then you should be able to copy/apply format from/to lists and normal text, however copying/applying from/to the table should do nothing.
这段文档虽短,但信息量并不小,它由三部分组成:
- Bender 元数据标签:该文件是 CKEditor 4 使用的 Bender.js 测试框架下的**手动测试(manual test)**用例:
@bender-ui: collapsed:手动测试页面默认以折叠方式呈现编辑器区域;@bender-tags: bug, copyformatting, 4.6.0:标记这是针对 Copy Formatting 插件的缺陷回归测试,自 4.6.0 版本引入;@bender-ckeditor-plugins: copyformatting, toolbar, wysiwygarea, floatingspace, elementspath, undo:声明测试页需要加载的插件集合(复制格式、工具栏、经典编辑器区域、浮动空间、元素路径与撤销)。
- 测试目标:验证 Copy Formatting 插件是否尊重(respecting)
copyFormatting_allowedContexts配置所允许的上下文。 - 一个具体场景示例:在"List and text contexts"(列表与文本上下文)编辑器中,格式应当能在列表与普通文本之间复制/应用;而从/向表格复制或应用格式时,则不应产生任何效果(should do nothing)。
这条示例直接点明了该配置的核心行为:未被列入 copyFormatting_allowedContexts 的上下文,其格式复制与应用会被整体禁用。
三、测试页面:5 个编辑器的场景矩阵设计
与上述 md 文档配套的测试页是 tests/plugins/copyformatting/manual/allowedcontexts.html。它在一个页面里创建了 5 个编辑器,每个编辑器都包含相同的测试内容(内联文本、两段有序/无序列表、两张表格),但通过 CKEDITOR.replace 的 loaded 事件为每个实例设置了不同的 copyFormatting_allowedContexts 值,从而构成一张完整的上下文组合矩阵:
| 编辑器 | 页面标题 | copyFormatting_allowedContexts 取值 |
允许的上下文 |
|---|---|---|---|
editor1 |
Text context | [ 'text' ] |
仅文本 |
editor2 |
List and text contexts | [ 'text', 'list' ] |
文本 + 列表 |
editor3 |
Table and text contexts | [ 'text', 'table' ] |
文本 + 表格 |
editor4 |
Lists & tables contexts | [ 'list', 'table' ] |
列表 + 表格 |
editor5 |
No contexts | false |
全部禁用 |
测试页中的典型配置片段如下:
// editor2:仅允许文本与列表上下文
CKEDITOR.replace( 'editor2', {
allowedContent: true,
extraPlugins: 'dialog,sourcedialog',
on: {
loaded: function( evt ) {
evt.editor.config.copyFormatting_allowedContexts = [
'text',
'list'
];
}
}
} );
注意其中两个细节:
- 配置时机:
copyFormatting_allowedContexts在编辑器的loaded事件回调中赋值。因为该配置会被插件在初始化时读取并缓存在editor.copyFormatting状态对象中,测试选择在编辑器加载完成后修改配置,同时利用allowedContent: true关闭内容过滤对样式提取的干扰(否则提取的样式还需通过 ACF 规则校验)。 false的特殊用法:editor5将配置设为布尔值false,用于构造"无任何上下文"的边界场景。官方文档规定的合法取值为true(全部允许)或字符串数组(如[ 'text', 'list' ]),false属于测试页自造的边界值——在源码判定逻辑下它等价于"所有上下文均不允许"。
测试时的检查要点(对应 md 文档的核心断言):
- 在
editor2中:把光标放入带样式的列表项,点击工具栏 "Copy Formatting" 按钮,再点击普通段落,格式应成功应用;反之亦然。而把样式从列表复制到表格单元格,或从表格单元格复制到列表/文本时,页面不应发生任何格式变化; - 其余编辑器依此类推:只有被
copyFormatting_allowedContexts明确列出的上下文之间才能互相复制/应用格式; - 点击编辑器外部区域或按
Esc键可以退出复制格式状态(相关行为还覆盖在 sticky.md、disabledcursor.md 等相邻手动测试中)。
四、源码级原理:上下文如何被判定与放行
理解了测试场景后,我们深入 plugins/copyformatting/plugin.js 看底层实现。
4.1 配置默认值与合法取值
配置项在插件源码末尾声明(plugin.js#L1158-L1180):
/**
* Defines which contexts should be enabled in the Copy Formatting plugin. Available contexts are:
* * `'text'` – Plain text context.
* * `'list'` – List context.
* * `'table'` – Table context.
* ...
* @since 4.6.0
* @cfg {Boolean/String[]} [copyFormatting_allowedContexts=true]
*/
CKEDITOR.config.copyFormatting_allowedContexts = true;
默认值为 true,即默认情况下三种上下文全部开放,不限制格式复制/应用范围。
4.2 上下文放行判定:_isContextAllowed
每个编辑器实例在初始化时会创建 editor.copyFormatting 状态对象(State 构造函数,plugin.js#L233-L273),并基于该配置构造内部的 CKEDITOR.filter。上下文是否被允许,由 State.prototype._isContextAllowed 判定(plugin.js#L284-L288):
State.prototype._isContextAllowed = function( testedContext ) {
var configValue = this.editor.config.copyFormatting_allowedContexts;
return configValue === true || indexOf( configValue, testedContext ) !== -1;
};
判定规则清晰:配置为 true 时全部放行;否则用 CKEDITOR.tools.indexOf 在配置数组中查找当前上下文名,命中才放行。这也解释了 false 与任意非数组值(如数字 100)为何会禁用所有上下文——既不是 true,indexOf 也无法在布尔/数字中匹配到字符串。
4.3 上下文探测:_determineContext
插件在复制与应用两个阶段都需要知道当前选区属于哪种上下文,这是由 _determineContext 完成的(plugin.js#L797-L821):
_determineContext: function( range ) {
function detect( query ) {
var walker = new CKEDITOR.dom.walker( range ),
currentNode;
// Walker sometimes does not include all nodes (e.g. if the range is in the middle of text node).
if ( range.startContainer.getAscendant( query, true ) || range.endContainer.getAscendant( query, true ) ) {
return true;
}
while ( ( currentNode = walker.next() ) ) {
if ( currentNode.getAscendant( query, true ) ) {
return true;
}
}
}
if ( detect( { ul: 1, ol: 1 } ) ) {
return 'list';
} else if ( detect( 'table' ) ) {
return 'table';
} else {
return 'text';
}
}
探测策略是:先用 CKEDITOR.dom.walker 遍历选区范围内的节点,再通过 getAscendant 向上回溯祖先元素,依次检测是否命中 ul/ol(判定为 'list')、table(判定为 'table'),两者都不命中则为 'text'。注意判定的优先级:列表优先于表格。这意味着当选区横跨列表与表格(混合上下文)时,插件会把整体视为列表上下文——这也与 lists.md 中"选区从段落开始、在列表结束"仍能正确应用样式的回归要求一致。
4.4 放行检查如何贯穿复制与应用流程
_isContextAllowed 在 applyFormatting 事件的两个内置监听器中都被调用(plugin.js#L169-L221):
- 第一个监听器(清理旧样式):在删除目标选区原有样式前,先
_determineContext探测上下文,若上下文不被允许则直接return,不做任何清理; - 第二个监听器(应用新样式,优先级 999):根据探测结果走不同的应用分支,且每个分支都先做放行检查:
editor.copyFormatting.on( 'applyFormatting', function( evt ) {
var plugin = CKEDITOR.plugins.copyformatting,
context = plugin._determineContext( evt.data.range );
if ( context === 'list' && editor.copyFormatting._isContextAllowed( 'list' ) ) {
plugin._applyStylesToListContext( evt.editor, evt.data.range, evt.data.styles );
} else if ( context === 'table' && editor.copyFormatting._isContextAllowed( 'table' ) ) {
plugin._applyStylesToTableContext( evt.editor, evt.data.range, evt.data.styles );
} else if ( editor.copyFormatting._isContextAllowed( 'text' ) ) {
plugin._applyStylesToTextContext( evt.editor, evt.data.range, evt.data.styles );
}
}, null, null, 999 );
这就是"在 editor2(文本 + 列表)中从/向表格复制格式时 should do nothing"的代码级解释:探测结果为 'table' 时,_isContextAllowed( 'table' ) 返回 false,三个分支全部落空,样式不会被应用。
五、三种上下文各自的应用策略差异
当上下文被允许时,插件会调用不同的应用函数,行为差异正是"上下文"概念的价值所在(plugin.js#L831-L959):
5.1 文本上下文:_applyStylesToTextContext
把样式当作内联样式逐条 style.apply( editor ) 到选区。此过程中涉及两个关键转换:
inlineBoundary(h1~h6、p、div):块级元素样式在应用前被统一改名为span,保证只产生内联格式;elementsForInlineTransform(如li):把列表项样式转成对应<span style="...">再应用,同时剔除value、type等列表专属属性;excludedElementsFromInlineTransform(table、thead、tbody、ul、ol):这类元素不会被转换成 span,直接跳过。
5.2 列表上下文:_applyStylesToListContext
针对 ul/ol/li 走专门逻辑:ol/ul 样式会通过 renameNode 直接改变列表类型(如从项目符号列表变成编号列表,反之亦然),li 样式直接 applyToObject 应用到列表项。实现中每应用一条样式都先创建书签(range.createBookmark())再恢复,以避免破坏列表结构、防止列表被拆分成两段——这正是 lists.md 中"列表不被拆分""列表类型按源列表类型切换"两个检查点的来源。
5.3 表格上下文:_applyStylesToTableContext
针对 table/tr/td/th/thead/tbody 分派处理:单元格样式(td/th)会先做元素名适配(style.element = cell.getName())再 applyToObject 应用到目标单元格,避免把单个单元格的样式错误地扩散到整张表格;table/tr 样式则只作用于命中的外层元素。同样通过书签恢复选区,确保"表格不被拆分成两段"(对应 tables.md 的检查点)。此外,preservedElements(ul、ol、li、td、th、tr、thead、tbody、table)保证了清理旧样式时只剥离样式属性而不删除元素本身。
六、联动配置:与 allowedContexts 协同的其他选项
上下文放行只是 Copy Formatting 配置体系的一环,源码中还有几个配套配置值得一并了解(均定义于 plugin.js#L1102-L1216):
| 配置项 | 默认值 | 作用 |
|---|---|---|
copyFormatting_allowedContexts |
true |
允许格式复制/应用的上下文(本文核心) |
copyFormatting_allowRules |
'b s u i em strong span p div td th ol ul li(*)[*]{*}' |
采用 ACF 语法,规定允许从哪些元素提取样式;设为 true 可完全关闭过滤 |
copyFormatting_disallowRules |
'*data-cke-widget*,data-widget*,data-cke-realelement' |
采用 ACF 语法,显式禁止从某些元素提取样式(默认禁止 widget 元素) |
copyFormatting_keystrokeCopy |
CKEDITOR.CTRL + CKEDITOR.SHIFT + 67(Ctrl+Shift+C) |
复制格式快捷键,设为 false 可关闭 |
copyFormatting_keystrokePaste |
CKEDITOR.CTRL + CKEDITOR.SHIFT + 86(Ctrl+Shift+V) |
应用格式快捷键 |
copyFormatting_outerCursor |
true |
复制格式激活时是否在编辑器外区域显示"禁用"光标 |
其中 allowRules/disallowRules 决定能否提取(复制侧),allowedContexts 决定能否应用(应用侧),两者共同构成"复制 + 应用"的双重闸门。样式提取阶段的过滤实现在 State 构造函数(plugin.js#L264-L272)与 extractFormatting 事件默认监听器(plugin.js#L150-L166)中:提取到的样式定义必须通过内部 filter 的 check 校验,否则取消提取。相关的过滤行为有独立测试覆盖,见 applyformatfiltering.js(如 copyFormatting_allowRules: 'img[src]'、true 两种定制场景)。
七、自动化测试印证:行为即规格
除了手动测试,仓库还提供了完全对应的自动化测试,可直接验证本文所述行为:
-
applyformatcontext.js:专门围绕
copyFormatting_allowedContexts编写。其中'test editor.copyFormatting._isContextAllowed'(L49-L77)逐一断言true、数组、错误类型(100)三种取值下_isContextAllowed的返回结果;后续'test context - text only'、'test context - list only'、'test context - text and list only'、'test context - text and table only'、'test context - all contexts'等用例,则用<p>、<ul><li>、<table><tr><td>三种 HTML 结构分别验证各配置组合下样式是否被正确应用或被静默忽略。测试通过_helpers/tools.js中的testApplyingFormat辅助函数(tests/plugins/copyformatting/_helpers/tools.js)完成"设置选区 → 应用格式 → 断言旧样式被清除、新样式已生效"的完整校验。 -
applyformat.js 中的
'test determining context'(L396-L439):直接验证_determineContext在光标位于段落、位于列表项、选区横跨文本与列表/表格、跨两个单元格/两行等 12 种选区形态下的判定结果,覆盖了"选区起始在文本、结束在列表项内"这类混合上下文场景。
需要说明的是,这些测试运行在 Bender.js 框架下(bender.js),手动测试可通过 Bender 的手动测试模式在真实浏览器中交互验证,自动化测试则通过 Bender 的测试运行器批量执行。
八、真实项目中的配置实践
将上述原理落到实际项目中,限制 Copy Formatting 只允许在普通文本与列表之间复制格式的典型配置为:
CKEDITOR.replace( 'editor', {
// 只允许文本与列表两种上下文
copyFormatting_allowedContexts: [ 'text', 'list' ],
// 可选:进一步限制允许提取样式的元素(ACF 语法)
copyFormatting_allowRules: 'b; s; u; strong; span; p; div; (*)[*]{*}',
// 可选:关闭从 widget 元素提取样式
copyFormatting_disallowRules: '*data-cke-widget*,data-widget*,data-cke-realelement',
// 可选:自定义快捷键(此处为 Ctrl+Shift+M)
copyFormatting_keystrokePaste: CKEDITOR.CTRL + CKEDITOR.SHIFT + 77
} );
配置要点总结:
- 需要全部开放时保持默认
true,或显式写[ 'text', 'list', 'table' ]; - 需要精确限制时使用字符串数组,未列出的上下文将既不能复制、也不能应用格式;
- 若需求是"只允许从文本复制、应用到文本"这类单向/局部限制,则还需配合
copyFormatting_allowRules/copyFormatting_disallowRules从提取侧做过滤(复制侧无独立的上下文开关,allowedContexts同时管住复制与应用两侧的放行检查); - 修改配置后,建议参考仓库中的 allowedcontexts.html 场景矩阵,用"文本 + 列表 + 表格"混合内容逐项验证复制与应用行为是否符合预期。
九、总结
copyFormatting_allowedContexts 是控制 CKEditor 4 Copy Formatting 插件作用范围的核心开关。通过 allowedcontexts.md 这一手动测试用例,我们完整还原了它的测试设计(5 个编辑器的场景矩阵)、期望行为(未允许的上下文"should do nothing")与源码实现(_determineContext 探测 + _isContextAllowed 放行 + 三种上下文专属应用策略)。配合 applyformatcontext.js 等自动化测试,任何开发者都可以在真实项目中精准复现、验证并定制这套上下文控制行为。