CKEditor 4 Copy Formatting 插件手册测试全解析:从手动验证步骤到配置与源码原理
CKEditor 4 Copy Formatting 插件手册测试全解析:从手动验证步骤到配置与源码原理
Copy Formatting 是 CKEditor 4 中一个非常实用的"格式刷"类插件:它允许用户把一处文本的样式(字体、颜色、背景、下划线、列表类型、表格单元格样式等)复制下来,再应用到文档的其他位置,而无需手动逐个设置格式。本指南以仓库中的 Copy Formatting 手动测试文档为核心,讲解该插件的手动测试流程、全部配置参数、底层实现原理,并结合仓库内的源码与自动化测试用例给出可验证的技术依据。读完本文,你将能够搭建测试环境亲手验证 Copy Formatting 的每一项行为,并理解它的样式提取、过滤、上下文识别与粘贴应用机制。
一、测试文档定位:一份 Bender 手动测试用例
本文所依据的核心文档是仓库中的 tests/plugins/copyformatting/manual/copyformatting.md,它是 CKEditor 4 测试体系(Bender.js)中的一份手动测试(manual test)说明。文件全文如下:
@bender-ui: collapsed
@bender-tags: bug, copyformatting, 4.6.0
@bender-ckeditor-plugins: copyformatting, toolbar, wysiwygarea, floatingspace, elementspath, undo
Have some fun with copy formatting!
不要被这段简洁的文本迷惑,这些以 @bender- 开头的注释行是 Bender 测试框架的元数据声明,每一行都承载着明确的测试语义:
| 元数据 | 值 | 含义 |
|---|---|---|
@bender-ui: collapsed |
collapsed |
手动测试页面默认以折叠(不展开 UI 面板)的方式呈现,便于直接聚焦编辑器本身 |
@bender-tags |
bug, copyformatting, 4.6.0 |
测试标签:该插件从 4.6.0 版本引入,且包含对历史 bug 的回归验证 |
@bender-ckeditor-plugins |
copyformatting, toolbar, wysiwygarea, floatingspace, elementspath, undo |
运行该测试所需加载的插件集合,其中 wysiwygarea(经典编辑区)、floatingspace(浮动工具栏)、elementspath(元素路径)、undo(撤销/重做)都是验证复制格式行为所必需的配套插件 |
与其配对的可视化测试页面是 tests/plugins/copyformatting/manual/copyformatting.html,页面内建了两个编辑器实例:
editor1:通过CKEDITOR.replace()创建经典(classic)编辑器,配置了allowedContent: true(关闭内容过滤限制,保证所有样式都能被复制)与height: 550;editor2:通过CKEDITOR.inline()创建内联(inline)编辑器,并额外加载dialog与sourcedialog插件用于查看源码。
两个编辑器填充了相同的、精心设计的测试内容,覆盖了 Copy Formatting 最核心的三种应用上下文:
- 行内文本(Inline text):
<strong><u><span style="color: #f00;" class="important">Apollo 11</span></u></strong>、<s>was the</s>、带background-color的<span>、带href的链接<a>等,用于验证文字装饰(粗体、下划线、删除线)、颜色、背景色、class 属性乃至链接属性的整体复制; - 列表(Lists):一个
start="3"的有序列表<ol>与一个无序列表<ul>,列表项中包含嵌套行内样式与background: #ff0;的背景样式,用于验证列表类型(ol/ul)与列表项样式的复制; - 表格(Tables):两张结构不同的表格,其中一张带
<caption>、<thead>、<tbody>结构,<th>/<td>单元格内既有background-color、color、text-decoration等样式,也有<strong><u>行内标签,用于验证表格单元格、表头样式的复制与类型转换。
这份手动测试的任务就是"Have some fun with copy formatting!"——在页面上自由地把上述三类内容的格式互相复制、应用,观察样式是否被正确搬移。
二、搭建环境并运行手动测试
copyformatting.md 属于 Bender.js 手动测试(manual test),运行方式与自动化测试不同:它不会自动断言结果,而是打开一个浏览器页面,由测试人员按步骤手动操作并对照预期。
- 安装依赖:在仓库根目录执行
npm install,安装 Bender 测试框架所需的全部依赖(项目依赖清单见 package.json)。 - 启动 Bender 服务器:运行
node node_modules/bender/bender.js server run(Bender 的主配置文件为仓库根目录的 bender.js,其中声明了applications.ckeditor指向根目录的ckeditor.js,端口与主机等参数均在此配置)。 - 打开手动测试列表:浏览器访问 Bender 提供的手动测试入口,定位到
tests/plugins/copyformatting/manual/目录下的copyformatting.html页面。由于文档声明了@bender-ckeditor-plugins,Bender 会自动为页面加载 copyformatting 及其配套插件。
说明:手动测试依赖真实浏览器环境(尤其涉及鼠标事件、光标样式与
contenteditable行为),因此必须在浏览器中人工操作,而非命令行断言。这正是本目录下所有manual/*.html与manual/*.md成对存在的原因——.md描述操作步骤与预期,.html提供可交互的测试页面。
三、Copy Formatting 的核心交互流程
结合同目录下的 keystroke.md、sticky.md 等手动测试文档,Copy Formatting 的完整交互流程可以概括为"复制格式 → 移动选区 → 应用格式"三个阶段:
基本流程(非粘性模式):
- 将光标放入带有样式的文本中(如
Apo^llo 11,即Apollo中间); - 点击工具栏的 Copy Formatting 按钮(或按
Ctrl + Shift + C)复制格式; - 用鼠标点击(或键盘移动选区)到另一处未样式化的文本;
- 再次点击目标位置或按
Ctrl + Shift + V应用格式。
需要验证的关键行为(来自 keystroke.md):
- 复制样式后,工具栏按钮进入 "On"(按下)状态;
- 全部文本样式都被复制:文字装饰(text decoration)、字体(style)、字重(weight)、颜色(color)、背景色(background color);
- 全部属性(attributes)都被复制;
- 新应用的样式会覆盖目标位置原有的样式;
- 用
Ctrl + Shift + C复制的样式,可以多次按Ctrl + Shift + V重复粘贴; - 按
Escape键可随时取消复制,按钮回到 "Off" 状态; - 若从无样式元素复制格式,则应用到目标元素的效果是移除其原有格式。
粘性模式(sticky)(来自 sticky.md):
- 将光标放入带样式文本;
- 双击工具栏的 Copy Formatting 按钮,进入 sticky 模式;
- 用鼠标依次点击多处未样式化文本,格式会被持续应用;
- 再次单击按钮或按
Escape键退出粘性模式。
粘性模式下按钮状态在每次应用后保持不变,页面光标会一直呈现 Copy Formatting 专用光标,直到手动退出。
鼠标按键约束(来自 mousebutton.md):只有左键点击才会触发格式应用;使用中键或右键点击时,编辑器不应执行任何动作,而应保留浏览器默认行为(如中键滚动)。
四、插件全部配置参数详解
所有配置均以 copyFormatting_ 为前缀,在编辑器初始化时作为 config 选项传入,其默认值与完整注释定义在 plugins/copyformatting/plugin.js 底部。下面逐一说明。
4.1 copyFormatting_allowRules:允许复制哪些样式(ACF 规则)
config.copyFormatting_allowRules = 'b s u i em strong span p div td th ol ul li(*)[*]{*}';
默认值为 'b s u i em strong span p div td th ol ul li(*)[*]{*}',使用 CKEditor 4 的 Advanced Content Filter(ACF)语法描述"允许从哪些元素提取样式"。
- 设为一个 ACF 规则字符串时,例如
'span(*)[*]{*}',表示只允许复制<span>元素(任意类、任意属性、任意内联样式); - 设为
true时,彻底禁用过滤,允许复制任意元素上的任意样式。
源码中该配置在 State 构造函数里被用来构造过滤实例(见 plugin.js#L264):this.filter = new CKEDITOR.filter( editor, editor.config.copyFormatting_allowRules );。当配置为 true 时,过滤实例被直接禁用(this.filter.disabled = true)。
4.2 copyFormatting_disallowRules:禁止复制哪些样式
config.copyFormatting_disallowRules = '*data-cke-widget*,data-widget*,data-cke-realelement';
默认规则禁止从 widget 相关元素上提取样式,防止把 CKEditor 4 widget(如图片、嵌入内容)的内部标记当成普通格式复制走。同样采用 ACF 语法,例如 'span(important)' 表示禁止复制带 important 类的 <span>。源码中通过 this.filter.disallow( editor.config.copyFormatting_disallowRules ) 叠加到允许规则之上(见 plugin.js#L270)。
4.3 copyFormatting_allowedContexts:允许在哪些上下文工作
config.copyFormatting_allowedContexts = true; // 默认:全部上下文可用
// 或
config.copyFormatting_allowedContexts = [ 'text', 'list' ];
插件把编辑内容划分为三种上下文:
'text':普通文本段落;'list':列表(ul/ol/li);'table':表格(table/tr/td/th等)。
设为 true 时三种上下文全部启用;设为数组时只启用列出的上下文。判定逻辑在 _determineContext()(plugin.js#L797,实际路径 plugins/copyformatting/plugin.js#L797):先检测选区范围内是否存在 ul/ol 祖先,其次检测 table,否则判定为 text;随后 _isContextAllowed()(plugin.js#L284)根据配置决定该上下文是否放行。
这一行为有专门的手动测试 allowedcontexts.md 与 allowedcontexts.html 验证:例如配置 [ 'text', 'list' ] 的编辑器,可以正常在文本与列表间复制/应用格式,但从表格复制或向表格应用格式则"什么都不发生"。该 HTML 中通过 evt.editor.config.copyFormatting_allowedContexts = [ ... ] 动态配置了 5 个不同上下文组合的编辑器实例,覆盖 ['text']、['text','list']、['text','table']、['list','table'] 与 false 五种情况。
4.4 copyFormatting_keystrokeCopy:复制格式快捷键
config.copyFormatting_keystrokeCopy = CKEDITOR.CTRL + CKEDITOR.SHIFT + 67; // 默认 Ctrl+Shift+C
// 自定义示例:Ctrl+Shift+B
config.copyFormatting_keystrokeCopy = CKEDITOR.CTRL + CKEDITOR.SHIFT + 66;
// 关闭:
config.copyFormatting_keystrokeCopy = false;
默认值为 Ctrl + Shift + C(键码 67)。当配置为真值时,插件在初始化时调用 editor.setKeystroke( editor.config.copyFormatting_keystrokeCopy, 'copyFormatting' )(plugin.js#L133)注册快捷键。
4.5 copyFormatting_keystrokePaste:应用格式快捷键
config.copyFormatting_keystrokePaste = CKEDITOR.CTRL + CKEDITOR.SHIFT + 86; // 默认 Ctrl+Shift+V
// 自定义示例:Ctrl+Shift+M
config.copyFormatting_keystrokePaste = CKEDITOR.CTRL + CKEDITOR.SHIFT + 77;
// 关闭:
config.copyFormatting_keystrokePaste = false;
默认值为 Ctrl + Shift + V(键码 86)。注意这个默认值会与 paste 类插件的"粘贴纯文本"快捷键产生冲突,因此插件采用了临时接管策略:调用 copyFormatting 命令时,通过 _attachPasteKeystrokeHandler()(plugin.js#L1078)把该快捷键临时改绑为 applyFormatting 命令,并记住原本绑定的命令(_initialKeystrokePasteCommand);退出复制状态或完成应用后,再由 _detachPasteKeystrokeHandler()(plugin.js#L1093)恢复原绑定。prematurepaste.md 手动测试专门验证了未复制样式时按 Ctrl + Shift + V 不会误触发格式应用,而会走普通粘贴行为。
自定义快捷键的验证见 customkeystroke.md 与 customkeystroke.html:第一个编辑器配置了 Ctrl+Shift+B/Ctrl+Shift+M,应正常工作;第二个编辑器把两个快捷键都设为 false,Ctrl+Shift+C/Ctrl+Shift+V 不应再触发 Copy Formatting 功能。
4.6 copyFormatting_outerCursor:编辑器外光标是否显示"禁用"样式
config.copyFormatting_outerCursor = true; // 默认
// 关闭:
config.copyFormatting_outerCursor = false;
当 Copy Formatting 激活时,插件会给页面 documentElement 添加 cke_copyformatting_disabled 类(见 plugin.js#L405),使编辑器外的鼠标显示为"禁用"光标,提示用户当前位置无法应用格式。相关 CSS 定义在 plugins/copyformatting/styles/copyformatting.css:.cke_copyformatting_disabled 使用 cursor: url(../cursors/cursor-disabled.svg) 12 1, auto;,而编辑区内的 .cke_copyformatting_active 则使用专用光标 cursor.svg,并带有 !important 以覆盖 Table Resize 等插件的光标干扰。disabledcursor.md 手动测试验证了在 IE/Edge 中(该浏览器不支持在 contenteditable 内自定义光标)编辑器外光标不应异常显示为禁用态。
五、源码级实现原理
5.1 插件的状态对象与命令体系
插件在 init() 中为每个编辑器实例创建一个独立的状态对象 editor.copyFormatting = new plugin.state( editor )(plugin.js#L82)。State 类(plugin.js#L233)保存三个关键数据:
styles:当前已复制的CKEDITOR.style[]数组,null表示尚未复制;sticky:是否处于粘性模式;filter:由copyFormatting_allowRules/copyFormatting_disallowRules构造的样式过滤器。
同时插件注册两个命令(plugin.js#L84-L85):
copyFormatting:执行时调用_extractStylesFromElement()从当前光标所在元素向上遍历提取样式并存入copyFormatting.styles,命令状态切为TRISTATE_ON,随后根据isSticky(来自双击按钮或快捷键触发的data.sticky)决定是否进入粘性模式,并为光标容器添加激活类(plugin.js#L371-L415);applyFormatting:执行时调用_applyFormat()把copyFormatting.styles应用到当前选区;若为非粘性模式,应用后清空样式、移除光标类并把命令状态切回TRISTATE_OFF(plugin.js#L417-L450)。
工具栏按钮 CopyFormatting 以 isToggle: true 注册(plugin.js#L87),其 mouseup 事件会被拦截(evt.data.stopPropagation()),防止点击按钮本身触发编辑器内的格式应用。
5.2 样式提取:从元素向上遍历
_extractStylesFromElement()(plugin.js#L501)从当前元素开始,沿着 DOM 祖先链向上遍历,直到到达 editable 或遇到 breakOnElements(ul、ol、table)为止。对每个元素:
- 触发
extractFormatting事件; - 默认监听器调用
_convertElementToStyleDef()(plugin.js#L478),把元素名、属性(排除excludedAttributes:id、style、href、data-cke-saved-href、dir)与内联样式(parseCssText解析的style属性)合并为一个CKEDITOR.STYLE_INLINE类型的样式定义; - 用
copyFormatting.filter.check()对样式做 ACF 过滤,被过滤掉的元素直接evt.cancel()。
extractFormatting 事件可被开发者监听和改写,例如强制给提取的样式附加 class 或取消特定元素的提取(事件文档见 plugin.js#L1219-L1244)。
5.3 格式应用:先清旧样式,再上新样式
applyFormatting 事件注册了两个监听器(plugin.js#L169-L221):
- 优先级 10(先执行)——移除目标位置的旧样式:通过
_extractStylesFromRange()收集选区内的旧样式,逐一remove();对preservedElements(ul、ol、li、td、th、tr、thead、tbody、table)不删除元素本身,而是用_removeStylesFromElementInRange()剥离其属性(如<ul style="...">还原为<ul>)。每个步骤之间用range.createBookmark()/moveToBookmark()恢复选区,避免列表/表格的怪异行为。 - 优先级 999(后执行)——应用新样式:根据
_determineContext()判定出的上下文,分派到三个函数之一:_applyStylesToTextContext()(plugin.js#L831):普通文本上下文,逐个style.apply( editor );对于elementsForInlineTransform(li)会把元素转换为<span>再应用,并剔除value、type属性;_applyStylesToListContext()(plugin.js#L873):列表上下文,若复制的是ol/ul类型则把目标列表renameNode成对应类型并applyToObject,复制li样式则应用到每个列表项;_applyStylesToTableContext()(plugin.js#L918):表格上下文,table/tr样式直接应用到节点,td/th样式按目标单元格名适配后应用,thead/tbody同理。
应用前还会经过 _filterStyles()(plugin.js#L763)处理:把 h1~h6、p、div 等 inlineBoundary 块级元素统一转换为 <span> 再应用,并丢弃空 <span>。
5.4 折叠选区的"单词"定位
当光标处于折叠状态(未选中文字)时,直接应用样式会破坏相邻内容。为此 _applyFormat()(plugin.js#L973)会调用 _getSelectedWordOffset()(plugin.js#L589),用正则 /\b\w+\b/ig 定位光标所在单词的起止位置,把选区扩展到整个单词后再应用样式。该函数还处理了跨节点单词(如 lu<span style="color: #f00;">n</span>ar)、跳过注释节点、处理段落/列表/表格边界等情况。getwordbetweenelements.md、spaceattheend.md、selectionfromrighttoleft.md 等手动测试均围绕该逻辑设计(例如从右往左选择文本时"BBB"也必须被正确样式化)。
5.5 无障碍与撤销集成
- 屏幕阅读器:插件通过
_addScreenReaderContainer()(plugin.js#L1035)在页面插入一个aria-live="polite"的隐藏容器,复制/应用/取消时调用_putScreenReaderMessage()写入状态文本("Formatting copied"、"Formatting applied"、"Formatting canceled" 等,见 plugins/copyformatting/lang/en.js)。 - 撤销集成:插件的
mouseup处理(plugin.js#L105-L117)只在cmd.state === TRISTATE_ON且左键点击时才执行命令,并且点击编辑器外部会自动关闭 Copy Formatting 状态。这避免了连续点击选区产生大量无意义的历史快照。自动化测试 undointegration.js 验证了连续 20 余次改变选区后 Undo 步骤数保持不变(对应 issue #2780)。
六、围绕核心用例的完整手动测试清单
除了 copyformatting.md 本身,tests/plugins/copyformatting/manual/ 目录还提供了针对同一插件不同侧面的多份手动测试,可作为深入验证的清单:
| 测试文件 | 验证主题 |
|---|---|
| keystroke.md | 默认快捷键 Ctrl+Shift+C/Ctrl+Shift+V 的完整流程与按钮状态切换 |
| customkeystroke.md | 自定义/关闭快捷键后的行为 |
| sticky.md | 双击按钮进入粘性模式,多次应用与退出 |
| allowedcontexts.md | 不同 copyFormatting_allowedContexts 组合下的上下文限制 |
| mousebutton.md | 中键/右键点击不应触发格式应用 |
| lists.md | 混合上下文、列表类型转换、列表不被拆分 |
| tables.md | 表格单元格/表头样式应用、仅行内样式作用于文本 |
| undointegration.md | 多次改变选区后 Undo/Redo 按钮保持可用 |
| selectionfromrighttoleft.md | 从右到左选择文本时样式仍被应用 |
| disabledcursor.md | 编辑器外禁用光标行为(IE/Edge 特例) |
| prematurepaste.md | 未复制样式时按 Ctrl+Shift+V 只走普通粘贴 |
自动化测试方面,tests/plugins/copyformatting/copyformatting.js 覆盖了折叠选区应用样式、非折叠选区应用、键盘快捷键应用、文本样式应用到表格等场景;applyformatcontext.js 验证 extractFormatting/applyFormatting 事件的参数结构;keystrokes.js 则逐条断言快捷键的挂载、临时接管与还原行为。这些测试与手动测试文档共同构成了对 Copy Formatting 插件行为最完整的事实依据。
七、小结
本文以 tests/plugins/copyformatting/manual/copyformatting.md 为骨架,完整还原了 Copy Formatting 手动测试的元数据语义与测试内容设计,并延伸讲解了插件的六项配置参数、两条命令与两个事件构成的命令体系、三种上下文的分派逻辑、折叠选区的单词定位、无障碍与撤销集成等源码实现。无论你是要在自己的 CKEditor 4 项目中启用该插件,还是希望深入理解其"格式刷"的底层机制,都可以直接以本仓库的 plugins/copyformatting/plugin.js、plugins/copyformatting/styles/copyformatting.css 以及 tests/plugins/copyformatting/ 下的手动与自动化测试为蓝本进行验证与二次开发。