- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
在我们的在线文档系统中,除了基础的富文本编辑能力外,搜索替换的算法同样也是重要的功能实现。纯文本搜索替换算法相对来说是比较容易实现的,而在富文本的场景中,由于存在图文混排、嵌入模块如Mention等,实现这一功能则相对要复杂得不少。那么本文则以Quill实现的富文本编辑器为基础,通过设计索引查找和图层渲染的方式来实现搜索替换.
首先我们先来思考一下纯文本的搜索替换算法,对于纯文本而言我们可以直接通过一些方法来查找对应文本的索引值,然后无论是将其直接替换还是切割再联合的方式,都可以轻松地完成这个功能。而富文本本质上可以看作是携带着格式的纯文本,那么在这种情况下我们依然可以延续着对于纯文本的处理思路,只不过由于存在文本会被切割为多个片段的问题,就需要我们来兼容这个数据层面上的问题.
实际上在前边我们强调了当前的实现是以Quill为基础实现的方案,因为本质上搜索替换与数据结构的实现强相关,quill-delta是扁平的数据结构,相对来说比较方便我们来处理这个问题。而如果是嵌套的数据结构类型例如Slate的实现,我们仍然可以依照这个思路来实现,只不过在这里如果我们将其作为DEMO来实现并没有那么直观,嵌套的数据结构类型对于替换的处理上也会复杂很多.
那么在这里我们先来构造Quill的编辑器实例,在我们实现的实例中,我们只需要引用quill.js的资源,然后指定实例化的DOM节点即可初始化编辑器,我们在这里通过配置来注册了样式模块。文中的相关实现可以在https://github.com/WindRunnerMax/QuillBlocks/blob/master/examples/find-replace.html中查看.
<div class="editor-container">
<div id="$editor"></div>
</div>
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script>
const editor = new Quill($editor, {
modules: {
toolbar: [[{ header: [1, 2, false] }], ["bold", "italic", "underline"], ["code-block"]],
},
placeholder: "Enter Text...",
theme: "snow",
});
</script>
在Quill中是使用quill-delta来描述富文本内容的,quill-delta是一个扁平的数据结构,除了能描述富文本内容之外,还可以描述富文本内容的变更。在这里我们首先关注的是文档内容的描述,因此这里我们则是全部以insert的形式来描述,而之前我们也提到了富文本实际上就是纯文本携带属性,因此我们可以很轻松地使用下面的样式描述普通文本与加粗文本.
const delta = new Delta([
{ insert: "Hello " },
{ insert: "World", attributes: { bold: true } },
]);
我们的搜索替换方案本质上是在数据描述的基础上来查找目标内容,而从上边的数据结构来看,我们可以明显地看出在attributes上也可能存在需要查找的目标内容,例如我们需要实现Mention组件,是可以在attributes中存储user信息来展示@user的信息,而insert置入的内容则是空占位,那么在这里我们就实现一下Quill的Mention插件.
const Embed = Quill.import("blots/embed");
class Mention extends Embed {
static blotName = "mention";
static className = "ql-mention";
static tagName = "SPAN";
static create(value) {
const node = super.create(value);
node.innerHTML = `@${value}`;
node.setAttribute("data-value", value);
return node;
}
static value(domNode) {
return domNode.getAttribute("data-value");
}
}
Quill.register(Mention);
则此时我们的预置的数据结构内容则如下所示,我们可能注意到数据结构中Mention的insert是对象并且实际内容是并不是在attributes而是在insert中。这是因为在Quill中描述parchment的blots/embed中value是由insert存储的blotName对象来完成的,因此在示例中我们查找的目标还是在insert对象中,但是并不实际影响我们需要对其进行额外的查找实现.
editor.setContents([
{ "insert": "查找替换" },
{ "attributes": { "header": 1 }, "insert": "\n" },
{ "insert": "查找替换的富文本基本能力,除了基本的" },
{ "attributes": { "bold": true }, "insert": "文本查找" },
{ "insert": "之外,还有内嵌结构例如" },
{ "attributes": { "italic": true }, "insert": "Mention" },
{ "insert": "的" },
{ "insert": { mention: "查找能力" } },
{ "insert": "。\n" },
]);
首先我们先来设计一下内容的搜索算法,既然前边我们已经提到了富文本实际上就是带属性的纯文本,那么我们就先来设想对于纯文本的搜索实现。文本搜索算法实现本质上就是字符串文本的匹配,那么我们很容易想到的就是KMP算法,而实际上KMP算法并不是最高效的实现算法,在各种文本编辑器中实现的查找功能更多的是使用的Boyer-Moore算法.
在这里我们先不考虑高效的算法实现,我们实现的目标则是在长文本中查找目标文本的索引值,因此可以直接使用String对象的indexOf来实现查找。而我们实际上是需要获取目标文本的所有索引值,因此需要借助这个方法的第二个参数position来实现查找的起始位置。而如果我们使用正则来实现索引则容易出现输入的字符串被处理为正则保留字的问题,例如搜索(text)字符串时,则在不特殊处理的情况下会被认为搜索text文本.
const origin = "123123000123";
const target = "123";
const indices = [];
let startIndex = 0;
while ((startIndex = origin.indexOf(target, startIndex)) > -1) {
indices.push(startIndex);
startIndex += target.length;
}
console.log(indices); // [0, 3, 9]
const origin = "123123000123";
const target = "123";
const indices = [];
const regex = new RegExp(target, "g");
let match;
while ((match = regex.exec(origin)) !== null) {
indices.push(match.index);
}
console.log(indices); // [0, 3, 9]
在我们上述的测试中,可以将其放置于https://perf.link/测试一下执行性能,将其分别实现防止于Test Cases中测试,可以发现实际上indexOf的性能还会更高一些,因此在这里我们就直接使用indexOf来实现我们的搜索算法。在浏览器中测试万字文本的indexOf搜索,结果稳定在1ms以内,性能表现是完全可以接受的,当然这个搜索结果和电脑本身的性能也是强相关的.
indexOf // 141,400 ops/s
RegExp // 126,030 ops/s
那么此时我们就来处理关于Delta的索引建立,那么由于富文本的表达存在样式信息,反映在数据结构上则是存在文本的片段,因此我们的搜索首先需要直接将所有文本拼接为长字符串,然后使用indexOf进行查找。此外在前边我们已经看到数据结构中存在insert,在这里我们需要将其作为不可搜索的文本占位,以避免在搜索时将其作为目标文本的一部分.
const delta = editor.getContents();
const str = delta.ops
.map(op => (typeof op.insert === "string" ? op.insert : String.fromCharCode(0)))
.join("");
let index = str.indexOf(text, 0);
const ranges = [];
while (index >= 0) {
ranges.push({ index, length: text.length });
index = str.indexOf(text, index + text.length);
}
而对于类似于Mention模块的Void/Embed结构处理则需要我们特殊处理,在这里不像是先前对于纯文本的内容搜索,我们不能将这部分的相关描述直接放置于insert中,因此此时我们将难以将其还原到原本的数据结构中,如果不能还原则无法将其显示为搜索的结果样式。那么我们就需要对delta.ops再次进行迭代,然后case by case地将需要处理的属性进行处理.
const rects = [];
let iterated = 0;
delta.ops.forEach(op => {
if (typeof op.insert === "string") {
iterated = iterated + op.insert.length;
return void 0;
}
iterated = iterated + 1;
if (op.insert.mention) {
const value = op.insert.mention;
const mentionIndex = value.indexOf(text, 0);
if (mentionIndex === -1) return void 0;
// 绘制节点位置 ...
}
});
当我们将所有的数据收集到起来后,就需要构建虚拟图层来展示搜索的结果。首先我们需要处理最初处理的insert纯文本的搜索结果,由于我们在输入的时候通常都是使用input来输入搜索的文本目标,因此我们是不需要处理\n的情况,这里的图层处理是比较方便的。而具体的虚拟图层实现在先前的diff算法文章中已经描述得比较清晰,这里我们只实现相关的调用.
const rangeDOM = document.createElement("div");
rangeDOM.className = "ql-range virtual-layer";
$editor.appendChild(rangeDOM);
const COLOR = "rgba(0, 180, 42, 0.3)";
const renderVirtualLayer = (startRect, endRect) => {
// ...
};
const buildRangeLayer = ranges => {
rangeDOM.innerText = "";
ranges.forEach(range => {
const startRect = editor.getBounds(range.index, 0);
const endRect = editor.getBounds(range.index + range.length, 0);
rangeDOM.appendChild(renderVirtualLayer(startRect, endRect));
});
};
而在这里我们更需要关注的是Mention的处理,因为在这里我们不能够使用editor.getBounds来获取其相关的BoundingClientRect,因此这里我们需要自行计算其位置。因此我们此时需要取得mentionIndex所在的节点,然后通过createRange构建Range对象,然后基于该Range获取ClientRect,这样就取得了我们需要的startRect和endRect,但是需要注意的是,此时我们取得是绝对位置,还需要将其换算为编辑器实例的相对位置才可以.
const [leaf, offset] = editor.getLeaf(iterated);
if (
leaf &&
leaf.domNode &&
leaf.domNode.childNodes[1] &&
leaf.domNode.childNodes[1].firstChild
) {
const textNode = leaf.domNode.childNodes[1].firstChild;
const startRange = document.createRange();
startRange.setStart(textNode, mentionIndex + 1);
startRange.setEnd(textNode, mentionIndex + 1);
const startRect = startRange.getBoundingClientRect();
const endRange = document.createRange();
endRange.setStart(textNode, mentionIndex + 1 + text.length);
endRange.setEnd(textNode, mentionIndex + 1 + text.length);
const endRect = endRange.getBoundingClientRect();
rects.push({ startRect, endRect });
}
rects.forEach(it => {
const editorRect = editor.container.getBoundingClientRect();
const startRect = {
bottom: it.startRect.bottom - editorRect.top,
height: it.startRect.height,
left: it.startRect.left - editorRect.left,
right: it.startRect.right - editorRect.left,
top: it.startRect.top - editorRect.top,
width: it.startRect.width,
};
const endRect = {
bottom: it.endRect.bottom - editorRect.top,
height: it.endRect.height,
left: it.endRect.left - editorRect.left,
right: it.endRect.right - editorRect.left,
top: it.endRect.top - editorRect.top,
width: it.endRect.width,
};
const block = renderVirtualLayer(startRect, endRect);
rangeDOM.appendChild(block);
});
替换的实现在Delta的结构上会比较简单,在先前我们也提到了Delta不仅能够通过insert描述文档,还可以借助retain、delete、insert方法来描述文档的变更,那么我们需要做的就是在上述构造的ranges的基础上,构造目标的变更描述。而由于我们先前构造的Mention是不允许进行实质性的替换操作的,所以我们只需要关注原本的insert文本内容即可.
这里的实现重点是实现了批量的changes构造,首先需要定义Delta实例,紧接着的preIndex是用来记录上一次执行过后的索引位置,在我们的ranges循环中,retain是用来移动指针到当前即将处理的原文本内容,然后调用delete将其删除,之后的insert是替换的目标文本,注意此时的指针位置是在目标文本之后,因此需要将preIndex更新为当前处理的索引位置,最后将其应用到编辑器即可.
const batchReplace = () => {
const text = $input1.value;
const replace = $input2.value;
if (!text) return void 0;
const changes = new Delta();
let preIndex = 0;
ranges.forEach(range => {
changes.retain(range.index - preIndex);
changes.delete(range.length);
changes.insert(replace);
preIndex = range.index + range.length;
});
editor.updateContents(changes);
onFind();
};
https://github.com/WindrunnerMax/EveryDay
https://quilljs.com/docs/delta
https://www.npmjs.com/package/parchment
https://www.npmjs.com/package/quill-delta/v/4.2.2
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createRange
https://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
https://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
最后此篇关于初探富文本之搜索替换算法的文章就讲到这里了,如果你想了解更多关于初探富文本之搜索替换算法的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
表架构 DROP TABLE bla; CREATE TABLE bla (id INTEGER, city INTEGER, year_ INTEGER, month_ INTEGER, val I
我需要拆分字符串/或从具有以下结构的字符串中获取更容易的子字符串。 字符串将来自 window.location.pathname 或 window.location.href,看起来像 text/n
每当将对象添加到数组中时,我都会尝试更新 TextView ,并在 TextView 中显示该文本,如下所示: "object 1" "object 2" 问题是,每次将新对象添加到数组时,它都会覆盖
我目前正在寻找使用 Java 读取网站可见文本并将其存储为纯文本字符串的方法。 换句话说,我想转换成这样: Hello stupid World进入“ Hello World ” 或者类似的东西 Un
我正在尝试以文本和 HTML 格式发送电子邮件,但无法正确发送正确的 header 。特别是,我想设置 Content-Type header ,但我找不到如何为 html 和文本部分单独设置它。 这
我尝试了上面的代码,但我无法绑定(bind)文本,我怎样才能将资源内部文本 bloc
我刚刚完成了 Space Shooter 教程,由于没有 GUIText 对象,所以我创建了 UI.Text 对象并进行了相应的编码。它在统一播放器中有效,但在构建 Web 应用程序后无效。我花了一段
我有这个代码: - (IBAction)setButtonPressed:(id)sender { NSUserDefaults *sharedDefaults = [[NSUserDefau
抱歉标题含糊不清,但我想不出我想在标题中做什么。无论如何,对于图像上的文本,我使用了 JLabel 文本并将其添加到图标中。 JLabel icon = new JLabel(new Imag
关闭。这个问题是not reproducible or was caused by typos .它目前不接受答案。 这个问题是由于错别字或无法再重现的问题引起的。虽然类似的问题可能是on-topi
我在将 Twitter 嵌入到我从 HTML 5 转换的 wordpress 运行网站时遇到问题。 我遇到的问题是推文不是我的自定义字体... 这是我无法使用任何 css 定位的 HTML 代码,我正
我正在尝试找到解决由于使用以下形式的代码而导致的冗余字符串连接问题的最佳方法: logger.debug("Entering loop, arg is: " + arg) // @1 在大多数情况下,
我写了这个测试 @Test public void removeRequestTextFromRouteError() throws Exception { String input = "F
我目前正在创建一个正则表达式来拆分所有匹配以下格式的字符串:&[文本],并且需要获取文本。字符串可能类似于:something &[text] &[text] everything &[text] 等
有没有办法将标题文本从一个词变形为另一个词,同时保留两个词中使用的字母?我看过的许多 css 文本动画大多是视觉的,很少有旋转整个单词的。 我想要做的是从一个词过渡,例如“BEACH”到“CHANGE
总结matplotlib绘图如何设置坐标轴刻度大小和刻度。 上代码: ?
我在容器 (1) 中创建了容器 (2)。你能帮忙如何向容器(1)添加文本吗?下面是我的代码 return Scaffold( body: Padding( padding: c
我似乎找不到任何人或任何人这样做过。我试图限制我们使用的图像数量,并想创建一个带有渐变作为其“颜色”的文本,并在其周围设置渐变轮廓/描边 到目前为止,我还没有看到任何将两者结合在一起的东西。 我可以自
我正在为视频游戏暗黑破坏神 2 使用 discord.py 构建一个不和谐机器人。其中一项功能要求机器人从暗黑破坏神 2 屏幕截图中提取项目的名称和属性。我目前正在为此使用 pytesseract,但
我很难弄清楚如何旋转 strip.text theme 中的属性来自 ggplot2 .我使用的是 R 版本 3.4.2 和 ggplot2 版本 2.2.1。 以下是 MWE 的数据。 > dput
我是一名优秀的程序员,十分优秀!