Linting example
浏览器的DOM很好地实现了其表示复杂网页的目的。但其庞大的范围和松散的结构使得很难进行假设。一个表示较小文档集的文档模型可能更容易理解。
这个例子实现了一个简单的文档 linter,它可以发现文档中的问题,并使其易于修复。
这个示例的第一部分是一个函数,给定一个文档,生成一个在该文档中发现的问题数组。我们将使用descendants
方法轻松迭代文档中的所有节点。根据节点的类型,检查不同类型的问题。
每个问题都表示为一个带有消息、开始和结束的对象,以便它们可以显示和突出显示。对象还可以选择性地具有fix
方法,可以调用该方法(传递视图)来解决问题。
// 你可能不应该使用的词语
const badWords = /\b(obviously|clearly|evidently|simply)\b/ig
// 匹配标点符号,前面有一个空格
const badPunc = / ([,\.!?:]) ?/g
function lint(doc) {
let result = [], lastHeadLevel = null
function record(msg, from, to, fix) {
result.push({msg, from, to, fix})
}
// 对于文档中的每个节点,直接输出翻译,不要添加任何额外的文本。记住,保留所有HTML标签和属性,只翻译内容!
doc.descendants((node, pos) => {
if (node.isText) {
// 扫描文本节点以查找可疑模式
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") {
// 检查标题级别是否适合当前级别
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) {
// 确保图像有替代文本
record("Image without alt text", pos, pos + 1, addAlt)
}
})
return result
}
提供修复命令的辅助工具看起来像这样。
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将图标定位在编辑器的右侧,这样它就不会干扰文档流。
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"}),
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
icon.problem = prob
return icon
}
}
问题对象存储在图标的DOM节点中,以便事件处理程序在处理节点上的点击时可以访问它们。我们将使单击图标选择注释区域,双击运行fix
方法。
重新计算整个问题集,并在每次更改时重新创建装饰集并不是很高效,因此对于生产代码,您可能需要考虑一种可以增量更新这些内容的方法。这会复杂得多,但绝对可行——事务可以为您提供所需的信息,以确定文档的哪一部分发生了变化。
import {Plugin, TextSelection} from "prosemirror-state"
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 {from, to} = event.target.problem
view.dispatch(
view.state.tr
.setSelection(TextSelection.create(view.state.doc, from, to))
.scrollIntoView())
return true
}
},
handleDoubleClick(view, _, event) {
if (/lint-icon/.test(event.target.className)) {
let prob = event.target.problem
if (prob.fix) {
prob.fix(view)
view.focus()
return true
}
}
}
}
})