OpenProject 富文本 WYSIWYG 编辑器完整指南:CKEditor5 格式化、宏指令与资源链接实战
【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject
OpenProject 内置了一套基于 CKSource CKEditor5 的准所见即所得(WYSIWYG)富文本编辑器,底层存储格式为 GitHub 风格的 CommonMark(GFM),并扩展了提及(mentions)、图片尺寸调整等 HTML 能力。本文以 docs/user-guide/wysiwyg/README.md 为骨架,结合仓库前端编辑器配置源码与后端文本格式化解析器,系统讲解格式化、图片处理、键盘快捷键、宏指令、资源链接以及工作包/项目属性嵌入等完整能力,帮助你在一篇文章内掌握 OpenProject 富文本编辑器的全部实战用法。
编辑器概览:GFM + HTML 扩展
OpenProject 的编辑器基于 CKEditor5 构建,其内容格式是 GitHub 风格的 CommonMark(GFM)。这意味着你在编辑器中看到的内容与最终渲染结果基本一致,同时编辑器支持在 Markdown 之上叠加额外的 HTML 能力,例如:
- 提及(mentions):通过
@提及用户; - 图片尺寸调整:直接用鼠标拖拽调整图片大小。
[!NOTE] 并非所有场景都提供完整功能。在工作包(work package)或评论等受限编辑器中,宏指令、图片上传等功能会被裁剪;在工作包分屏视图(详情视图)中,可能需要点击编辑器右上角的三个竖点(更多操作菜单)才能访问附加功能。
从源码结构看,这一"完整版 vs 受限版"的设计在前端有明确实现:ckeditor-setup.service.ts 定义了ICKEditorType = 'full'|'constrained'两种编辑器类型,并分别挂载到window.OPClassicEditor与window.OPConstrainedEditor两个构建产物上(ckeditor-setup.service.ts)。创建编辑器实例时,服务会根据上下文中的type选择对应的构建:
const editorClass = type === 'constrained' ? window.OPConstrainedEditor : window.OPClassicEditor;编辑器功能定位
| 能力 | 完整编辑器(wiki 页面、会议等) | 受限编辑器(工作包、评论等) |
|---|---|---|
| 基础格式化(加粗、斜体、标题、引用、行内代码) | ✅ | ✅ |
| 图片上传 / 粘贴 / 拖拽 | ✅ | 视资源而定 |
| 宏指令(Macros) | ✅ | 默认不显示,可按需启用 |
| 代码块编辑 | 通过 CodeMirror 模态框 | 通过 CodeMirror 模态框 |
关于受限编辑器的宏指令,ckeditor-setup.service.ts 显示:受限编辑器默认没有宏下拉按钮,只有在上下文中显式传入宏列表(Array.isArray(resolvedContext.macros))时,才会通过constrainedToolbarWithMacroList()把macroList按钮动态插入到blockQuote之后(ckeditor-setup.service.ts)。
基本格式化
CKEditor5 构建支持基础文本样式:加粗、斜体、各级标题、删除线、行内代码、引用,以及行内图片处理。从外部粘贴内容(图片或富文本)同样支持,未被支持的样式会被编辑器自动剥离,保证输出格式的干净一致。
换行
按Enter会创建新段落;若只想换行而不新建段落,请按SHIFT+Enter。
链接
创建超链接有两种方式:
- 点击工具栏的链接按钮(可先选中部分文本再点击);
- 直接按
CTRL+K(Mac 为CMD+K)弹出链接输入弹窗。
Widget 与换行操作
CKEditor 使用 Widget 来展示图片、表格等块级元素(非行内元素)。绝大多数 Widget 点击即可选中;唯一例外是表格 Widget,需要点击左上角的"选择柄"才能选中整张表格。
当 Widget 处于选中状态时,你可以删除或剪切它,还可以快速在其上方或下方插入新行:
- 在 Widget下方新建一行:选中 Widget 后按
ENTER或↓(下箭头); - 在 Widget上方新建一行:按
SHIFT+ENTER或↑(上箭头)。
当 Widget 恰好位于页面开头或结尾时,这一操作尤其必要——它是唯一能在 Widget 外围插入空白行的方法。
代码块
由于 CKEditor5 目前不原生支持代码块编辑,OpenProject 采取"显示与编辑分离"的策略:
- 显示:代码块可以在 CKEditor 实例中正常展示;
- 编辑:双击代码块会打开一个模态窗口,在其中使用
CodeMirror编辑器实例进行编辑。
CodeMirror 方案的优势是自带语法高亮与代码提示(针对受支持的语言)。从源码看,这一交互由 editor-macros.service.ts 的editCodeBlock(content, languageClass)驱动,最终渲染CodeBlockMacroModalComponent(见 macro-code-block-modal/code-block-macro.modal.ts)。
表格
GFM 规范为 CommonMark 增加了表格语法扩展,OpenProject 的 CKEditor 支持该扩展。一个关键约束是:所有 GFM 表格必须包含表头行。对于在 CKEditor 中创建的、不含表头行的表格,编辑器会输出为 HTML<table>而非 GFM 表格——这与 GitHub 的行为一致。
Emojis
在所有文本编辑器中都可以插入 emoji:输入一个冒号加一个字母,例如:a,编辑器会弹出可用的 emoji 建议列表供选择。
自动格式化(Autoformatting)
CKEditor5 支持类 CommonMark 的键盘自动格式化输入:
- 输入
**will become bold**生成加粗,输入_will become italic_生成斜体; - 输入
#、##、###…… 生成不同层级的标题; - 行首输入
*或-加一个空格,生成无序列表; - 行首输入
1.或1)加一个空格,生成有序列表。
图片处理
在允许附件的资源中,可以通过以下三种方式添加图片:
- 使用工具栏的图片按钮;
- 从剪贴板直接粘贴图片;
- 将图片拖拽到编辑器上。
图片会被自动上传并保存为附件(attachment)。上传完成后,你可以用鼠标在编辑器中直接拖拽调整图片尺寸。
需要说明的是:在受限编辑器中(例如部分工作包或评论场景),图片上传能力可能不可用,这由编辑器上下文决定。
键盘快捷键
CKEditor 本身提供了大量快捷键(官方键盘支持文档列出了全部内容)。在此基础上,OpenProject 额外增加了一条全局快捷键:
| 快捷键(Windows / Linux) | 快捷键(Mac) | 功能 |
|---|---|---|
CTRL + ENTER | CMD + ENTER | 保存更改:对行内可编辑字段,保存并关闭该字段;对带完整 WYSIWYG 的页面(会议、wiki 页面),提交整个表单 |
前端源码也在编辑器包装元素上注册了自定义事件以支撑此类交互,例如op:ckeditor:autosave、op:ckeditor:getData、op:ckeditor:setData等(ckeditor-setup.service.ts),方便宿主页面以编程方式触发自动保存或读写编辑器内容。
宏指令(Macros)
OpenProject 在旧的 Textile 格式化时代就支持宏,WYSIWYG 编辑器沿用了这一能力。需要注意:编辑页面时宏不会被展开,而是显示为占位符(placeholder),只有在渲染/预览时才被解析执行。
从后端实现看,宏的解析发生在文本格式化管线中,位于 lib/open_project/text_formatting/filters/macro_filter.rb,各宏分别实现于 lib/open_project/text_formatting/filters/macros/ 目录下(如toc.rb、embedded_table.rb、create_work_package_link.rb、include_wiki_page.rb、child_pages.rb等)。前端则通过 editor-macros.service.ts 提供对应的配置模态框。
完整版 vs 受限版编辑器
部分资源(工作包、评论等)使用受限编辑器,并不展示全部宏能力(例如宏或图片上传会被隐藏)。前端 ckeditor-setup.service.ts 定义了宏的分级配置:
'none'/false:不启用任何宏(自定义字段、只读编辑器使用);'resource':启用目录(ToC)、嵌入表格、工作包按钮/快速信息(+ 可用时的 wiki 链接);'wiki':仅启用 wiki 链接宏;true:启用构建中包含的全部宏;string[]:精确指定启用的宏插件名。
resolveMacros()(ckeditor-setup.service.ts)还会根据实例是否配置了 wiki 提供器(configurationService.wikisAvailable),自动追加或过滤掉两个 wiki 页面链接宏(OpMacroWikiPageLinkAddExisting、OpMacroWikiPageLinkCreateNew)。
目录(Table of contents)
在适用的场景中,TOC 宏会在页面当前位置输出当前页面所有标题的列表,方便长文档导航。其渲染逻辑位于 lib/open_project/text_formatting/filters/macros/toc.rb。
工作包按钮(Work package button)
配置一个按钮或链接,指向当前项目中创建工作包的页面。你可以预选工作包类型,从而引导用户直达对应的工作包创建表单。该宏的前端配置模态框见 macro-wp-button-modal/wp-button-macro.modal.ts,配置逻辑由 editor-macros.service.ts 的configureWorkPackageButton()提供。
包含 wiki 页面(Include wiki page)
在当前项目或其他可见项目中引用一个 wiki 页面,并将其内容嵌入到当前文档。配置模态框对应 macro-wiki-include-page-modal/wiki-include-page-macro.modal.ts。
嵌入工作包表格与甘特图(Embed work package table and Gantt chart)
这是最灵活的宏,提供了与常规工作包表格同等的完整配置能力:
- 通过工具栏插入"嵌入工作包表格"宏后,可在弹窗中配置表格视图(列、分组、过滤器及其他属性);
- 渲染时,页面会动态获取工作包表格结果,并针对每个用户校验可见性/权限,确保不同用户看到各自有权限的数据;
- 典型用途:在其他页面嵌入视图、构建多结果报表、嵌入甘特图视图。
后端对嵌入表格的解析见 lib/open_project/text_formatting/filters/macros/embedded_table.rb。
链接到 OpenProject 资源
延续 Textile 时代的语法,WYSIWYG 编辑器中可以使用相同的快捷语法链接到 OpenProject 内的各类资源:
| 链接目标 | 用法示例 |
|---|---|
| Wiki 页面 | [[Wiki page]] |
| 带独立链接名称的 Wiki 页面 | [[Wiki page\|The text of the link]] |
| Sandbox 项目中的 Wiki 页面 | [[Sandbox:Wiki page]] |
| ID 为 12 的工作包 | #12 |
| 带主题与类型的工作包(ID 12) | ##12 |
| 带主题、类型与状态的工作包(ID 12) | ###12 |
| 按 ID 或名称引用版本 | version#3、version:"Release 1.0.0" |
| 按 ID/名称引用项目 | project#12、project:"My project name" |
| 按文件名引用附件 | attachment:filename.zip |
| 按 ID/名称引用会议 | meeting#12、meeting:"My meeting name" |
| 按 ID/名称引用文档 | document#12、document:"My document name" |
| 按 ID 或登录名引用用户 | user#4、user:"johndoe" |
| 按 ID 引用论坛消息 | message#1218 |
| 仓库修订版本 43 | r43 |
| 按哈希引用提交 | commit:f30e13e4 |
| 仓库中的源文件 | source:"some/file" |
避免解析:在这些标记前加一个叹号!,例如!#12将不会链接到 ID 为 12 的工作包。
[!NOTE] 所有这些宏必须以一个独立的新词形式书写(即前面至少有一个空格,或位于段落/句子的开头)。包含在单词内部的宏(如
somethingmeeting#4)不会被解析。
从后端实现看,这些链接处理由 lib/open_project/text_formatting/matchers/resource_links_matcher.rb 与 lib/open_project/text_formatting/matchers/link_handlers/ 目录下的处理器(如 work_packages.rb)完成——每个处理器通过allowed_prefixes声明自己能处理的资源前缀,并负责把匹配到的链接解析为真实资源并渲染成带正确链接与无障碍标签(accessible link label)的 HTML。
工作包与用户的自动补全
对于工作包和用户,输入#或@会分别弹出下拉菜单,自动补全当前用户可见的工作包或用户。
[!TIP] 链接工作包时想展示更多详情,可以输入
##或###后跟工作包 ID、主题、类型或关键字。
嵌入工作包属性与项目属性
OpenProject 提供了一套强大的属性嵌入宏,允许在富文本中动态引用工作包或项目的属性值,适合在 wiki、会议纪要、工作包描述等场景构建"活文档"。
[!NOTE] 这些宏只在前端展开。对每个用户都会校验相应权限,若用户无权查看对应资源,宏会渲染为错误提示。
在 PDF 导出 中,富文本属性(如工作包描述)的嵌入在某些情况下受限;简单文本格式化或表格单元之外的嵌入是受支持的。
按工作包 ID 嵌入值
使用workPackageValue:ID:attribute语法按 工作包 ID 嵌入其属性,可用属性见下方表格。
示例:嵌入 ID 为 1234 的工作包的主题:
workPackageValue:1234:subject按工作包主题嵌入值
使用workPackageValue:"Project name":attribute语法按工作包主题嵌入属性。
示例:嵌入主题为 "Project start" 的工作包的负责人:
workPackageValue:"Project start":assignee[!IMPORTANT] 按主题引用时,只在当前项目中查找具有该主题的工作包。需要跨项目引用时,请使用 ID 精确定位。官方建议不要使用主题作为引用依据——因为主题改变时引用不会自动更新。
相对嵌入当前工作包的值
使用workPackageValue:attribute语法嵌入当前工作包的属性:
- 编辑工作包描述或属于工作包的富文本自定义字段时,可以省略 ID;
- 编辑 wiki 页面、会议描述等非工作包上下文时,必须带上工作包 ID。
示例:嵌入当前工作包的负责人:
workPackageValue:assignee按项目 ID 嵌入值
使用projectValue:ID:attribute语法按项目 ID 嵌入项目属性。
示例:嵌入 ID 为 1234 的项目状态:
projectValue:1234:status相对嵌入当前项目的值
使用projectValue:attribute语法嵌入当前项目属性。
示例:嵌入当前项目的状态:
projectValue:status控制多值属性的显示方式
某些属性包含多个值,例如目标版本(Target versions)或多选自定义字段。应用中默认每个值一行显示,PDF 导出时则以逗号分隔。
在 CKEditor 中,你可以通过布局参数(layout)控制其显示方式:
workPackageValue:1234:targetVersions:multiline:每个值单独一行;workPackageValue:1234:targetVersions:singleline:所有值在一行内以逗号分隔。
布局参数同样适用于自定义字段(如workPackageValue:1234:"My custom field":singleline)、项目属性(projectValue:...)以及相对引用(workPackageValue:targetVersions:singleline)。
注意:布局参数仅对可包含多个值的属性生效;对subject这类单值属性无效。
[!NOTE] 已废弃的
version属性默认按单行显示工作包的目标版本。
后端解析器 attribute_macros.rb 对此有精确实现:它定义了LAYOUTS = %w[multiline singleline]作为保留关键字,并在正则中捕获可选的布局参数;同时reinterpret_as_relative_embed()(attribute_macros.rb)会把"裸属性恰好命中保留关键字"的相对形式(如workPackageValue:multiline)正确重解释为相对嵌入,而带引号的属性名则可以原样引用同名的自定义字段。相对嵌入时,relative_id()(attribute_macros.rb)会从渲染上下文中取出当前工作包或项目 ID。
嵌入属性帮助文本(Attribute help texts)
你还可以使用workPackageLabel或projectLabel嵌入属性值以及对应的帮助文本。例如:
workPackageLabel:1234:status会输出 "Status" 的翻译后的标签,并在存在帮助文本时附带显示对应的帮助文本。
支持的属性一览
以下两张表列出了workPackageValue/workPackageLabel与projectValue/projectLabel宏支持的全部属性名(其中1234代表工作包 ID)。
[!NOTE] 若实例使用非英语语言,只有在实例内所有用户语言一致(例如都是德语)时,才能在文本编辑器中使用翻译后的命令名(如
workPackageValue:1234:"translated attribute"),且仅命令所指向的属性被翻译。官方建议避免使用翻译后的属性名——它们可能因未来版本修复或文案变更而失效。
工作包可用属性
| 属性 | 用法示例 |
|---|---|
| %完成(%Complete) | workPackageValue:8415:percentageDone |
| 负责人(Accountable) | workPackageValue:1234:responsible |
| 受理人(Assignee) | workPackageValue:1234:assignee |
| 作者(Author) | workPackageValue:1234:author |
| 类别(Category) | workPackageValue:1234:category |
| 创建日期(Creation date) | workPackageValue:1234:createdAt |
| 自定义字段(Custom Fields) | workPackageValue:1234:"Name of the work package custom field" |
| 最后更新日期(Date of last update) | workPackageValue:1234:updatedAt |
| 描述(Description) | workPackageValue:1234:description |
| 预估工时(Estimated time) | workPackageValue:1234:estimatedTime |
| 结束日期(Finish date) | workPackageValue:1234:dueDate |
| 父工作包(Parent work package) | workPackageValue:1234:parent |
| 优先级(Priority) | workPackageValue:1234:priority |
| 所属项目(Project) | workPackageValue:1234:project |
| 项目阶段(Project phase) | workPackageValue:1234:projectPhase |
| 剩余工时(Remaining hours) | workPackageValue:1234:remainingTime |
| 剩余工作量(Remaining work) | workPackageValue:8415:remainingTime |
| 已耗工时(Spent time) | workPackageValue:1234:spentTime |
| 开始日期(Start date) | workPackageValue:1234:startDate |
| 状态(Status) | workPackageValue:1234:status |
| 主题/标题(Subject / Title) | workPackageValue:1234:subject |
| 目标版本(Target versions) | workPackageValue:1234:targetVersions |
| 版本(Version,已废弃) | workPackageValue:1234:version |
| 工作量(Work) | workPackageValue:8415:estimatedTime |
| 工作包类型(Work package type) | workPackageValue:1234:type |
[!NOTE]不支持富文本的递归嵌入。例如,你不能用
workPackageValue:description在自己的描述中嵌入自身。
项目可用属性
以下示例均引用当前文档所在的项目,也可以使用projectValue:"Identifier of the project":attribute引用其他项目。
| 属性 | 用法示例 |
|---|---|
| 自定义字段(Custom Fields) | projectValue:"Name of the project custom field" |
| 项目是否激活(布尔) | projectValue:active |
| 描述(Description) | projectValue:description |
| 项目标识(Identifier) | projectValue:identifier |
| 项目名称(Name) | projectValue:name |
| 状态(Status) | projectValue:status |
| 状态说明(Status description) | projectValue:statusExplanation |
| 父项目(Parent project) | projectValue:parent |
| 项目是否公开(布尔) | projectValue:public |
小结:从编辑器到渲染的完整链路
综合文档与源码,OpenProject 富文本体系的核心链路可以概括为:
- 编辑端:前端 ckeditor-setup.service.ts 按上下文创建
full或constrained两种 CKEditor 实例,并按资源类型(none/resource/wiki/true/string[])决定启用的宏集合; - 宏配置:editor-macros.service.ts 通过模态框完成代码块、工作包按钮、wiki 包含页、子页面等宏的参数配置;
- 保存与解析:后端 lib/open_project/text_formatting/ 中的过滤器与匹配器接管内容——macro_filter.rb 展开各类宏,resource_links_matcher.rb 处理资源链接,attribute_macros.rb 解析
workPackageValue/projectValue/workPackageLabel/projectLabel属性嵌入; - 渲染端:属性宏在前端按用户权限动态加载并展示(
opce-macro-attribute-value/opce-macro-attribute-label自定义元素),嵌入的工作包表格与甘特图则在每次页面加载时动态获取结果。
掌握这套编辑器的格式化语法、宏指令与属性嵌入能力,你可以在 OpenProject 的 wiki、工作包描述、会议纪要等几乎所有富文本场景中构建出动态、可复用、权限安全的内容页面。
【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考