Slate文档编辑器-Decorator装饰器渲染调度

在之前我们聊到了基于文档编辑器的数据结构设计,聊了聊基于slate实现的文档编辑器类型系统,那么当前我们来研究一下slate编辑器中的装饰器实现。装饰器在slate中是非常重要的实现,可以为我们方便地在编辑器渲染调度时处理range的渲染。

关于slate文档编辑器项目的相关文章:

Decorate

slatedecoration是比较有趣的功能,设想一个场景,当需要实现代码块的高亮时,我们可以有几种方案来实现: 第一种方案是我们可以通过直接将代码块的内容解析的方式,解析出的关键字类别直接写入数据结构中,这样就可以直接在渲染时将高亮信息渲染出来,缺点就是会增加数据结构存储数据的大小;那么第二种方式我们就可以只存储代码信息,当需要数据高亮时也就是前端渲染时我们再将其解析出Marks进行渲染,但是这样的话如果存在协同我们还需要为其标记为非协同操作以及无需服务端存储的纯客户端Op,会稍微增加一些复杂度;那么第三种方法就是使用decoration,实际上可以说这里只是slate帮我们把第二种方法的事情做好了,可以在不改变数据结构的情况下将额外的Marks内容渲染出来。

当然,我们使用装饰器的场景自然不只是代码块高亮,凡是涉及到不希望在数据结构中表达却要在渲染时表现的内容,都需要使用decoration来实现。还有比较明显的例子就是查找能力,如果在编辑器中实现查找功能,那么我们需要将查找到的内容标记出来,这个时候我们就可以使用decoration来实现,否则就需要绘制虚拟的图层来完成。而如果需要实现用户态的超链接解析功能,即直接贴入连接的时候,我们希望将其自动转为超链接的节点,也可以利用装饰器来实现。

在前段时间测试slate官网的search-highlighting example时,当我搜索adds时,搜索的效果很好,但是当我执行跨节点的搜索时,就不能非常有效地突出显示内容了,具体信息可以查看https://github.com/ianstormtaylor/slate/pull/5670。这也就是说当decoration执行跨节点处理的时候,是存在一些问题的。例如下面的例子,当我们搜索123或者12345时,我们能够正常将标记出的decoration渲染出来,然而当我们搜索123456时,此时我们构造的range会是path: [0], offset: [0-6],此时我们跨越了[0]节点进行标记,就无法正常标记内容了。

[
{ text: "12345" },
{ text: "67890" }
]

通过调用查找相关代码,我们可以看到上级的decorate结果会被传递到后续的渲染中,那么在本级同样会调度传递的decorate函数来生成新的decorations,并且这里需要判断如果父级的decorations与当前节点的range存在交集的话,那么内容会被继续传递下去。那么重点就在这里了,试想一下我们的场景,依旧以上述的例子中的内容为例,如果我们此时想要获取123456的索引,那么在text: 12345这个节点中肯定是不够的,我们必须要在上层数组中将所有文本节点的内容拼接起来,然后再查找才可以找到准确的索引位置。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/hooks/use-children.tsx#L21
const useChildren = (props: {
decorations: Range[]
// ...
}) => {
// ... for (let i = 0; i < node.children.length; i++) {
// ...
const ds = decorate([n, p])
for (const dec of decorations) {
const d = Range.intersection(dec, range)
if (d) {
ds.push(d)
}
}
// ...
}
// ...
}

那么此时我们就明确需要我们调用decorate的节点是父级元素,而父级节点传递到我们需要处理的text节点时,就需要Range.intersection来判断是否存在交集,实际上这里判断交集的策略很简单,在下面我们举了两个例子,分别是存在交集和不存在交集的情况,我们实际上只需要判断两个节点的最终状态即可。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/range.ts#L118

// start1          end1          start2          end2
// end1 start2
// end1 < start2 ===> 无交集 // start1 start2 end1 end2
// start2 end1
// start2 < end1 ===> 有交集 [start2, end1]

那么我们可以通过修改在decorate这部分代码中的Range.intersection逻辑部分来解决这个问题吗,具体来说就是当我们查找出的内容超出原本range的内容,则截取其需要装饰的部分,而其他部分舍弃掉,实际上这个逻辑在上边我们分析的时候已经发觉是没有问题的,也就是当我们查找123456的时候是能够将12345这部分完全展示出来的。根据前边的分析,本次循环我们的节点都在path: [0],这部分代码会将start: 0end: 5这部分代码截取range并渲染。

跨节点问题

然而我们在下一个text range范围内继续查找6这部分就没有那么简单了,因为前边我们实际上查找的rangepath: [0], offset: [0-6],而第二个text的基本rangepath: [1], offset: [0-5],基于上述判断条件的话我们是发现是不会存在交集的。因此如果需要在这里进行处理的话,我们就需要取得前一个range甚至在跨越多个节点的情况下我们需要向前遍历很多节点,当decorations数量比较多的情况下我们需要检查所有的节点,因为在此节点我们并不知道前一个节点是否超越了本身节点的长度,这种情况下在此处的计算量可能比较大,或许会造成性能问题。

因此我们还是从解析时构造range入手,当跨越节点时我们就需要将当前查找出来的内容分割为多个range,然后为每个range分别置入标记,还是以上边的数据为例,此时我们查找的结果就是path: [0], offset: [0, 5]path: [1], offset: [0, 1]两部分,这种情况下我们在Range.intersection时就可以正常处理交集了,此时我们的path是完全对齐的,而即使完全将内容跨越,也就是搜索内容跨越不止一个节点时,我们也可以通过这种方式来处理。

// https://github.com/ianstormtaylor/slate/pull/5670
const texts = node.children.map(it => it.text)
const str = texts.join('')
const length = search.length
let start = str.indexOf(search)
let index = 0
let iterated = 0
while (start !== -1) {
while (index < texts.length && start >= iterated + texts[index].length) {
iterated = iterated + texts[index].length
index++
}
let offset = start - iterated
let remaining = length
while (index < texts.length && remaining > 0) {
const currentText = texts[index]
const currentPath = [...path, index]
const taken = Math.min(remaining, currentText.length - offset)
ranges.push(/* 构造新的`range` */)
remaining = remaining - taken
if (remaining > 0) {
iterated = iterated + currentText.length
offset = 0
index++
}
}
start = str.indexOf(search, start + search.length)
}

此外,我们在调度装饰器的时候,需要关注在renderLeaf参数RenderLeafProps的值,因为在这里存在两种类型的文本内容,即leaf: Text;以及text: Text;基本TextInterface类型。而在我们通过renderLeaf渲染内容时,以高亮的代码块内值mark节点的渲染为例,我们实际渲染节点需要以leaf为基准而不是以text为基准,例如当在渲染markbold样式产生重叠时,这两种节点都需要以leaf为基准。

在这里的原因是,decorationsslate实现中是以text节点为基准将其拆分为多个leaves,然后再将leaves传递到renderLeaf中进行渲染。因此实际上在这里可以这么理解,text属性为原始值,而leaf属性为更细粒度的节点,调度renderLeaf的时候本身也是以leaf为粒度进行渲染的,当然在不使用装饰器的情况下,这两种属性的节点类型是等同的。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/components/text.tsx#L39
const leaves = SlateText.decorations(text, decorations)
const key = ReactEditor.findKey(editor, text)
const children = [] for (let i = 0; i < leaves.length; i++) {
const leaf = leaves[i] children.push(
<Leaf
isLast={isLast && i === leaves.length - 1}
key={`${key.id}-${i}`}
renderPlaceholder={renderPlaceholder}
leaf={leaf}
text={text}
parent={parent}
renderLeaf={renderLeaf}
/>
)
}

最后

在这里我们主要讨论了slate中的decoration装饰器的实现,以及在实际使用中可能会遇到的问题,主要是在跨节点的情况下,我们需要将range拆分为多个range,然后分别进行处理,并且还分析了源码来探究了相关问题的实现。那么在后边的文章中我们就主要聊一聊在slatePath的表达,以及在React中是如何控制其内容表达与正确维护Path路径与Element内容渲染的方案。

Blog

https://github.com/WindRunnerMax/EveryDay

Slate文档编辑器-Decorator装饰器渲染调度的更多相关文章

  1. 基于slate构建文档编辑器

    基于slate构建文档编辑器 slate.js是一个完全可定制的框架,用于构建富文本编辑器,在这里我们使用slate.js构建专注于文档编辑的富文本编辑器. 描述 Github | Editor DE ...

  2. [Qt及Qt Quick开发实战精解] 第1章 多文档编辑器

      这一章的例子是对<Qt Creator快速人门>基础应用篇各章节知识的综合应用, 也是一个规范的实例程序.之所以说其规范,是因为在这个程序中,我们对菜单什么时候可用/什么时候不可用.关 ...

  3. Linux_文档编辑器_简介

    1. vi 2. vim 3. ubuntu 有一个 自己的图形化的 文档编辑器,用起来比较方便: gedit 4. 5.

  4. PowerDesigner(九)-模型文档编辑器(生成项目文档)(转)

    模型文档编辑器 PowerDesigner的模型文档(Model  Report)是基于模型的,面向项目的概览文档,提供了灵活,丰富的模型文档编辑界面,实现了设计,修改和输出模型文档的全过程. 模型文 ...

  5. Python进阶之decorator装饰器

    decorator装饰器 .note-content {font-family: "Helvetica Neue",Arial,"Hiragino Sans GB&quo ...

  6. Web页面引入文档编辑器报风险

    Web页面引入文档编辑器会报风险,则需要以下操作: <system.web> <httpRuntime requestValidationMode="2.0" / ...

  7. 使用Swing实现简易而不简单的文档编辑器

    本文通过Swing来实现文档简易而不简单的文档编辑器,该文档编辑器的功能包括: 设置字体样式:粗体,斜体,下划线,可扩展 设置字体:宋体,黑体,可扩展 设置字号:12,14,18,20,30,40, ...

  8. 谈谈Python中的decorator装饰器,如何更优雅的重用代码

    众所周知,Python本身有很多优雅的语法,让你能用一行代码写出其他语言很多行代码才能做的事情,比如: 最常用的迭代(eg: for i in range(1,10)), 列表生成式(eg: [ x* ...

  9. 在线HTML文档编辑器使用入门之图片上传与图片管理的实现

    在线HTML文档编辑器使用入门之图片上传与图片管理的实现: 官方网址: http://kindeditor.net/demo.php 开发步骤: 1.开发中只需要导入选中的文件(通常在 webapp ...

  10. Python的程序结构[8] -> 装饰器/Decorator -> 装饰器浅析

    装饰器 / Decorator 目录 关于闭包 装饰器的本质 语法糖 装饰器传入参数 1 关于闭包 / About Closure 装饰器其本质是一个闭包函数,为此首先理解闭包的含义. 闭包(Clos ...

随机推荐

  1. 几行代码带你用TinyEngine低代码引擎开发侧边栏插件

    本文分享自华为云社区<实操上手TinyEngine低代码引擎插件化开发>,作者:OpenTiny. 1.背景介绍 1.1 TinyEngine 低代码引擎简介 低代码开发是近些年非常热门的 ...

  2. Redis的发布订阅Pub/Sub

    发布订阅 Redis 发布订阅(publish/subscribe)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息. Redis 客户端可以订阅任意数量的频道. 下图展示了频道 ...

  3. 【转载】碰碰彭碰彭Jingxuan —— 带中国古筝走上戛纳红毯

    视频地址: https://www.youtube.com/shorts/gl796527H1I

  4. 定制jekins-slave-jnlp镜像封装docker和kebectl命令实现pipline

    基于官方:jenkins/inbound-agent:latest DockerHub成品: docker pull svipghy/jenkins-jnlp-slave:v1 Dockerfile ...

  5. 基于Java+SpringBoot+Mysql实现的古诗词平台功能设计与实现六

    一.前言介绍: 1.1 项目摘要 随着信息技术的迅猛发展和数字化时代的到来,传统文化与现代科技的融合已成为一种趋势.古诗词作为中华民族的文化瑰宝,具有深厚的历史底蕴和独特的艺术魅力.然而,在现代社会中 ...

  6. NewStar CTF 2024 Crypto

    Week1 xor #As a freshman starting in 2024, you should know something about XOR, so this task is for ...

  7. php 异步并行后续--兼容FPM使用的组件

    上次给人推荐了这篇文章,关于PHP异步并行的文章,之后有人评论问这个组件能不能给fpm用,我测试了一下发现不行,于是又找到一个可以给fpm用的http请求组件. 安装很简单,就这样  composer ...

  8. 在table中,tbody没有充满整个table

    解决方法就是给table加上 display:table;就好了

  9. kafka之介绍

    Kafka 由于高吞吐量.可持久化.分布式.支持流数据处理等特性而被广泛应用.但当前关于Kafka原理及应用的相关资料较少,在我打算编写本文时,还没有见到中文版本的Kafka相关书籍,对于初学者甚至是 ...

  10. Linux之SaltStack

    SaltStack是基于Python开发的一套C/S架构配置管理工具(功能不仅仅是配置管理,如使用salt-cloud配置AWS EC2实例),它的底层使用ZeroMQ消息队列pub/sub方式通信, ...