装饰
装饰让您控制在编辑器扩展中如何绘制或者展示内容。如果您打算通过在编辑器中添加,替换或者样式化标签来更改外观,您很可能需要使用装饰。
阅读完此指南后,您将会:
- 理解如何使用装饰去改变编辑器的外观。
- 理解使用状态字段以及视图插件提供装饰之间的区别。
TIP
本页旨在提炼 CodeMirror 6 官方为 Obsidian 插件开发者们所提供文档的精华部分。想要获取更多关于状态字段的详细信息,请查阅 Decorating the Document 此文档。
先决条件
概览
不使用装饰时,文档将呈现为纯文本的形式。一点也不有趣。使用装饰后,您将改变文档的展示形式,比如高亮文本或者添加自定义 HTML 标签:
您可以使用以下类型的装饰:
- 使用 Mark decorations 修改现有元素的样式。
- 使用 Widget decorations 在文档中插入标签。
- 使用 Replace decorations 隐藏或使用其他标签替换文档的部分内容。
- 使用 Line decorations 仅为线添加样式。
要想使用装饰,您需要在编辑器扩展内创建他们,并将扩展程序 提供 给编辑器。您有两种将装饰提供给编辑器的方式,直接 使用状态字段 或者 间接 使用视图插件。
何时使用视图插件或者状态字段
视图插件以及状态字段都可以为编辑器提供装饰,但是他们之间有些区别:
- 如果您可以根据视图内部的内容决定装饰,此时可以使用视图插件。
- 如果您需要在视图外部管理装饰,此时可以使用状态字段。
- 如果您想做出可能改变视图内容的修改,比如添加分割线,此时可以使用状态字段。
如果您可以使用任何一种方法来实现您的扩展, 那么视图插件往往能带来更好的性能。比如,试想下您打算实现一个用来检查文档拼写的编辑器扩展。
一种方法是将整个文档传递给外部拼写检查器,然后返回错误列表。在此情况下,不管当前视口中有什么,您需要将每条错误映射到装饰,并使用状态字段来管理装饰。
另一种方式是仅仅只检查展示在视口中的内容。在用户滚动浏览文档时,改扩展需要不断地执行拼写检查,但您可以拼写检查包含数百万行文本的文档。
提供装饰
想象一下,您想构建一个编辑器扩展,用表情符号替换项目符号列表项。 您可以使用视图插件或状态字段来完成此操作,但有一些区别。 在本节中,您将看到如何使用这两种类型的扩展来实现它。
两种实现共享相同的核心逻辑:
- 使用 syntaxTree 查找列表项。
- 将每个列表项的前导连字符
-
替换为小部件。
小部件
小部件是您添加到编辑器中的自定义 HTML 标签。您可以在文档中的特定位置插入一个小部件,或者用一个小部件替换一段内容。
下例中定义了一个返回 <span>👉</span>
HTML 标签的小部件。您将在稍后使用到它。
import { EditorView, WidgetType } from "@codemirror/view";
export class EmojiWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement {
const div = document.createElement("span");
div.innerText = "👉";
return div;
}
}
要想使用 emoji 小部件替换您文档中的一部分内容,可以使用替换装饰。
const decoration = Decoration.replace({
widget: new EmojiWidget()
});
状态字段
提供来自状态字段的装饰:
- 使用
DecorationSet
类型定义状态字段 - 将
provide
属性添加到状态字段中。
provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
import { syntaxTree } from "@codemirror/language";
import {
Extension,
RangeSetBuilder,
StateField,
Transaction,
} from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
WidgetType,
} from "@codemirror/view";
import { EmojiWidget } from "emoji";
export const emojiListField = StateField.define<DecorationSet>({
create(state): DecorationSet {
return Decoration.none;
},
update(oldState: DecorationSet, transaction: Transaction): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
syntaxTree(transaction.state).iterate({
enter(node) {
if (node.type.name.startsWith("list")) {
// Position of the '-' or the '*'.
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
return builder.finish();
},
provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
});
视图插件
To manage your decorations using a view plugin:
使用视图插件管理您的装饰:
- 创建一个视图插件.
- 在您的插件中添加
DecorationSet
成员属性。 - 在
constructor()
方法中初始化装饰。 - 在
update()
中重新构建装饰。
以下示例仅在基础文档或视口更改时重建装饰。以下示例仅在基础文档或视口更改时重建装饰。
import { syntaxTree } from "@codemirror/language";
import { RangeSetBuilder } from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
PluginSpec,
PluginValue,
ViewPlugin,
ViewUpdate,
WidgetType,
} from "@codemirror/view";
import { EmojiWidget } from "emoji";
class EmojiListPlugin implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
destroy() {}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.type.name.startsWith("list")) {
// Position of the '-' or the '*'.
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
}
return builder.finish();
}
}
const pluginSpec: PluginSpec<EmojiListPlugin> = {
decorations: (value: EmojiListPlugin) => value.decorations,
};
export const emojiListPlugin = ViewPlugin.fromClass(
EmojiListPlugin,
pluginSpec
);
buildDecorations()
是一个辅助方法,它基于编辑器视图构建一整套装饰。
注意传入 ViewPlugin.fromClass()
的第二个参数。PluginSpec
中的 decorations
属性指定视图插件如何向编辑器提供装饰。
由于视图插件知道什么对用户可见,因此您可以使用 view.visibleRanges
来限制要访问的语法树的哪些部分。