Linter 示例
Linting example
浏览器 DOM 很好地完成了它的使命——表示复杂的网页。但它庞大的范围和松散的结构,使得我们很难对它做出假设。一个只表示较小文档集合的文档模型,更容易进行推理。
The browser DOM serves its purpose—representing complex webpages—very well. But its huge scope and loose structure makes it difficult to make assumptions about. A document model that represents a smaller set of documents can be easier to reason about.
这个示例实现了一个简单的文档 linter,它能找出文档中的问题,并让你轻松修复它们。
This example implements a simple document linter that finds problems in your document, and makes it easy to fix them.
这个示例的第一部分是一个函数:给定一个文档,它产生在该文档中发现的问题数组。我们会使用 descendants 方法方便地遍历文档中的所有节点。根据节点的类型,检查不同类型的问题。
The first part of this example is a function that, given a document,
produces an array of problems found in that document. We'll use the
descendants method to easily iterate
over all nodes in a document. Depending on the type of node, different
types of problems are checked for.
每个问题都表示为一个带有 message、start 和 end 的对象,这样它们就能被显示和高亮。这些对象还可以可选地带有一个 fix 方法,调用它(传入视图)即可修复该问题。
Each problem is represented as an object with a message, a start, and
an end, so that they can be displayed and highlighted. The objects may
also optionally have a fix method, which can be called (passing the
view) to fix the problem.
// Words you probably shouldn't use
const badWords = /\b(obviously|clearly|evidently|simply)\b/ig
// Matches punctuation with a space before it
const badPunc = / ([,\.!?:]) ?/g
function lint(doc) {
let result = [], lastHeadLevel = null
function record(msg, from, to, fix) {
result.push({msg, from, to, fix})
}
// For each node in the document
doc.descendants((node, pos) => {
if (node.isText) {
// Scan text nodes for suspicious patterns
let m
while (m = badWords.exec(node.text))
record(`Try not to say '${m[0]}'`,
pos + m.index, pos + m.index + m[0].length)
while (m = badPunc.exec(node.text))
record("Suspicious spacing around punctuation",
pos + m.index, pos + m.index + m[0].length,
fixPunc(m[1] + " "))
} else if (node.type.name == "heading") {
// Check whether heading levels fit under the current level
let level = node.attrs.level
if (lastHeadLevel != null && level > lastHeadLevel + 1)
record(`Heading too small (${level} under ${lastHeadLevel})`,
pos + 1, pos + 1 + node.content.size,
fixHeader(lastHeadLevel + 1))
lastHeadLevel = level
} else if (node.type.name == "image" && !node.attrs.alt) {
// Ensure images have alt text
record("Image without alt text", pos, pos + 1, addAlt)
}
})
return result
}
用于提供修复命令的辅助工具看起来是这样的。
The helper utilities that are used to provide fix commands look like this.
function fixPunc(replacement) {
return function({state, dispatch}) {
dispatch(state.tr.replaceWith(this.from, this.to,
state.schema.text(replacement)))
}
}
function fixHeader(level) {
return function({state, dispatch}) {
dispatch(state.tr.setNodeMarkup(this.from - 1, null, {level}))
}
}
function addAlt({state, dispatch}) {
let alt = prompt("Alt text", "")
if (alt) {
let attrs = Object.assign({}, state.doc.nodeAt(this.from).attrs, {alt})
dispatch(state.tr.setNodeMarkup(this.from, null, attrs))
}
}
这个插件的工作方式是:维护一组高亮问题并在旁边插入一个图标的装饰。CSS 用于把图标定位在编辑器的右侧,这样它就不会干扰文档流。
The way the plugin will work is that it'll keep a set of decorations that highlight problems and inserts an icon next to them. CSS is used to position the icon on the right side of the editor, so that it doesn't interfere with the document flow.
import {Decoration, DecorationSet} from "prosemirror-view"
function lintDeco(doc) {
let decos = []
lint(doc).forEach(prob => {
decos.push(Decoration.inline(prob.from, prob.to, {class: "problem"}, {prob}),
Decoration.widget(prob.from, lintIcon(prob), {key: prob.msg}))
})
return DecorationSet.create(doc, decos)
}
function lintIcon(prob) {
return () => {
let icon = document.createElement("div")
icon.className = "lint-icon"
icon.title = prob.msg
return icon
}
}
问题对象被存储在 decoration spec 中,这样事件处理器就能从插件的 decoration set 中取回它。我们让单击图标选中被标注的区域,双击则运行 fix 方法。
The problem object is stored in the decoration spec, so that event
handlers can retrieve it from the plugin's decoration set. We'll make
a single click on an icon select the annotated region, and a double
click run the fix method.
每次变化都重新计算整组问题、重新创建整组装饰并不高效,所以对于生产代码,你可能想考虑一种能够增量更新这些内容的方法。那会复杂不少,但绝对可行——事务可以给你所需的信息,让你弄清楚文档的哪一部分发生了变化。
Recomputing the whole set of problems, and recreating the set of decorations, on every change isn't very efficient, so for production code you might want to consider an approach that can incrementally update these. That'd be quite a bit more complex, but definitely doable—the transaction can give you the information you need to figure out what part of the document changed.
import {Plugin, TextSelection} from "prosemirror-state"
function getProb(view, plugin, dom) {
let pos = view.posAtDOM(dom, 0)
let found = plugin.getState(view.state)
.find(pos, pos, spec => spec.prob?.msg == dom.title)
return found.length ? found[0].type.spec.prob : null
}
let lintPlugin = new Plugin({
state: {
init(_, {doc}) { return lintDeco(doc) },
apply(tr, old) { return tr.docChanged ? lintDeco(tr.doc) : old }
},
props: {
decorations(state) { return this.getState(state) },
handleClick(view, _, event) {
if (/lint-icon/.test(event.target.className)) {
let prob = getProb(view, this, event.target)
if (prob)
view.dispatch(view.state.tr
.setSelection(TextSelection.create(view.state.doc, prob.from, prob.to))
.scrollIntoView())
return true
}
},
handleDoubleClick(view, _, event) {
if (/lint-icon/.test(event.target.className)) {
let prob = getProb(view, this, event.target)
if (prob?.fix) {
prob.fix(view)
view.focus()
return true
}
}
}
}
})
without alt text.
You can hover over the icons on the right to see what the
problem is, click them to select the relevant text, and, obviously,
double-click them to automatically fix it (if supported).