拼写检查示例
浏览器的 DOM 用来表示复杂的网页是很棒的--这也是设计它的目的。但是它巨大的页面内容和松散的结构使其很难做一些类似于「TypeScript类型推断」一样的推断(来判断用户是否书写合法的内容)。因此,一个代表了更小文档的文档模型就理所当然的应运而生了。
本示例实现了一个简单的文档 拼写检查 功能,它能够发现文档中的问题,然后方便的修复它:
拼写检查示例
这是一个句子 ,但是标点并不在正确的位置(这里我在中文中使用了英文标点以匹配示例代码)
标题等级设置低了
这是一个图片, ,它没有 alt 属性。
你可以鼠标悬浮在右侧的 icons 上,来查看错误内容,点击它以选中相关的文本,
然后,obviously,双击它以自动修复错误(如果支持的话)
这个示例的第一个部分就是一个函数,它接受一个文档参数,返回在该文档中发现的错误数组。我们将会使用 descendants
方法去方便的迭代文档中的所有节点。然后根据不同的节点类型,来应用不同的错误检查方式。
每个错误类型被表示为一个对象,它包含有一个错误提示、一个起始位置,以及一个结束位置信息,这样我们就能够展示错误提示然后高亮错误内容。对象也可选的有一个 fix
方法,可以修复错误(传 view 作为参数):
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})
}
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))
}
}
插件通过维护一个 decorations 集合来高亮错误同时插入一个紧挨着错误的 icon。CSS 用来将这个 icon 定位到编辑器的右侧,这样它就脱离了文档流而不会影响内容:
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)))
})
return DecorationSet.create(doc, decos)
}
function lintIcon(prob) {
let icon = document.createElement("div")
icon.className = "lint-icon"
icon.title = prob.msg
icon.problem = prob
return icon
}
错误对象被存储在 icon 的 DOM 节点上,这样当点击 icon 的时候,事件处理函数能够访问到相应的信息。我们将单击设计成选中错误的区域,双击设计成执行 fix
方法。
重新计算所有的错误,然后重新创建 decorations 的集合不是一种非常高效方式,因此对于生产环境的代码你可能想要考虑一种增量更新这些 decorations 的方式。想实现这个确实有点复杂,不过却是可行的--transaction 可以为你提供文档的哪部分更新了的信息:
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
}
}
}
}
})