Quill文档(五):Delta格式

news/2024/4/19 19:46:26/

富文本编辑器缺乏一种表达其自身内容的规范。直到最近,大多数富文本编辑器甚至不知道它们自己的编辑区域中有什么内容。这些编辑器只是传递用户的HTML,以及解析和解释这些HTML的负担。在任何给定的时间,这种解释都会与主要浏览器供应商的解释不同,导致用户的编辑体验不同。

Quill是第一个真正理解其自身内容的富文本编辑器。关键在于Deltas,这是描述富文本的规范。Deltas的设计旨在易于理解和使用。我们将探讨Deltas背后的一些思考,以阐明为什么事物是这样的。

如果您正在寻找关于Deltas是什么的参考,Delta文档是一个更简明的资源。

纯文本


让我们从只有纯文本的基础知识开始。已经有一个无处不在的格式来存储纯文本:字符串。现在,如果我们想在此基础上描述格式化文本,例如当一个范围是粗体时,我们需要添加额外的信息。

数组是唯一其他可用的有序数据类型,所以我们使用一个对象数组。这也允许我们利用JSON与广泛的工具兼容。

const content = [{ text: 'Hello' },{ text: 'World', bold: true }
];

我们可以在主对象中添加斜体、下划线和其他格式;但如果我们想要更清晰地将文本与所有这些格式分开,我们就在一个字段下组织格式,我们将这个字段命名为attributes

const content = [{ text: 'Hello' },{ text: 'World', attributes: { bold: true } }
];

紧凑


即使我们到目前为止的简单Delta格式,它也是不可预测的,因为上述“Hello World”示例可以有不同的表示,所以我们无法预测将生成哪一个:

const content = [{ text: 'Hel' },{ text: 'lo' },{ text: 'World', attributes: { bold: true } }
];

为了解决这个问题,我们增加了Deltas必须是紧凑的约束。有了这个约束,上述表示不是一个有效的Delta,因为它可以通过前面的示例更紧凑地表示,其中"Hel"和"lo"不是分开的。同样,我们不能有{ bold: false, italic: true, underline: null },因为{ italic: true }更紧凑。

规范


我们还没有为粗体分配任何含义,只是描述了文本的某种格式。我们完全可以使用不同的名字,比如加权或强,或使用不同的可能值范围,比如数值或描述性的权重范围。一个例子可以在CSS中找到,那里大多数这些模糊之处都在起作用。如果我们在页面上看到粗体文本,我们无法预测它的规则集是font-weight: bold还是font-weight: 700。这使得解析CSS以辨别其含义的任务复杂得多。

我们没有定义可能属性的集合,也没有定义它们的含义,但我们确实增加了一个额外的约束,即Deltas必须是规范的。如果两个Deltas相等,它们所表示的内容必须相等,并且不能有两个不相等的Deltas表示相同的内容。从编程角度来看,这允许您简单地深度比较两个Deltas,以确定它们所表示的内容是否相等。

所以如果我们有以下内容,我们唯一能得出的结论是a与b不同,但我们不知道a或b的含义是什么。

const content = [{text: "Mystery",attributes: {a: true,b: true}
}];

实施者需要选择合适的名称:

const content = [{text: "Mystery",attributes: {italic: true,bold: true}
}];

这种规范化适用于键和值、文本和属性。例如,Quill默认:

  • 使用六位字符的十六进制值表示颜色,而不是RGB。
  • 只有一种表示换行的方式,即\n,而不是\r\r\n
  • text: "Hello World"明确表示“Hello”和“World”之间恰好有两个空格。
    一些用户可能会定制这些选择,但Deltas中的规范约束规定选择必须是唯一的。

这种明确的可预测性使Deltas更易于处理,因为您需要处理的案例更少,也因为对于相应的Delta会是什么样子没有惊喜。长期来看,这使得使用Deltas的应用程序更易于理解和维护。

行格式


行格式影响整行的内容,因此它们对我们的紧凑和规范约束提出了一个有趣的挑战。一个看似合理的表示居中文本的方式如下:

const content = [{ text: "Hello", attributes: { align: "center" } },{ text: "\nWorld" }
];

但如果用户删除了换行符呢?如果我们只是简单地去掉换行符,Delta现在看起来会是这样:

const content = [{ text: "Hello", attributes: { align: "center" } },{ text: "World" }
];

这行文本是否仍然是居中的?如果答案是否,那么我们的表示不是紧凑的,因为我们不需要属性对象,可以将两个字符串合并:

const content = [{ text: "HelloWorld" }
];

但如果答案是肯定的,那么我们违反了规范约束,因为任何具有对齐属性的字符排列都将表示相同的内容。

所以我们不能简单地去掉换行符。我们还需要要么去掉行属性,要么扩展它们以填充整行的所有字符。

如果我们从以下内容中移除换行符会怎样?

const content = [{ text: "Hello", attributes: { align: "center" } },{ text: "\n" },{ text: "World", attributes: { align: "right" } }
];

我们不清楚结果行是居中对齐还是右对齐。我们可以删除两者或有一些排序规则来优先考虑其中一个,但我们的Delta正在变得更加复杂,更难处理。

这个问题要求原子性,我们在换行符本身找到了这一点。但我们有一个偏移问题,即如果我们有n行,我们只有n-1个换行符。

为了解决这个问题,Quill“添加”了一个换行符到所有文档,并始终以\n结束Deltas。

// 两行的“Hello World”
const content = [{ text: "Hello" },{ text: "\n", attributes: { align: "center" } },{ text: "World" },{ text: "\n", attributes: { align: "right" } }   // Deltas必须以换行结束
];

当删除或添加文本时,Delta需要相应地调整以确保行对齐属性仍然正确地应用。

  1. 行格式的影响范围:在第一个示例中,"Hello"是居中对齐的,但当删除换行符后,"HelloWorld"的对齐方式变得不确定。这展示了行格式属性(如居中对齐)是如何影响整行的内容。
  2. 维持紧凑和规范性:如果文本的对齐属性可以简单通过合并文本来丢弃,那么数据结构就不够紧凑。如果保留对齐属性,但结果不符合用户预期(例如,合并后的文本继续保持居中对齐),那就违反了规范性。
  3. 解决方案:Quill通过为每一行添加一个换行符并确保每个Delta都以换行符结束来解决这个问题。这样,每一行的开始和结束都明确地被标记了,使得行格式属性的应用更为清晰和一致。
  4. 换行符的作用:通过确保每行(通过换行符分隔)都明确定义,编辑器可以更容易地维护每行的格式属性。即使文本被修改,行的界限仍然清晰,编辑器可以正确地应用或调整格式属性。

这种方法让编辑器能够处理复杂的格式更改,保证即使在文本变动时,用户设置的行格式(如居中、右对齐)也能得到正确的维护。通过这种方式,编辑器提供了一种既紧凑又规范的方式来处理行格式问题,确保文本编辑的结果符合用户的期望。

嵌入内容


我们想要添加嵌入内容,如图像或视频。字符串自然适用于文本,但我们对嵌入物有很多选择。由于有不同类型的嵌入,我们的选择只需要包含这些类型信息,然后是实际内容的表示,可能有任何类型或值。

const img = {image: {url: 'https://quilljs.com/logo.png' }
};const f = {formula: 'e=mc^2'
};

与文本类似,图像可能有一些定义特征和一些临时特征。我们对文本内容使用了属性,并可以为图像使用相同的属性字段。但因此,我们可以保持我们一直在使用的通用结构,但应该将文本键重命名为更通用的名称。出于我们稍后将探讨的原因,我们将选择插入(insert)这个名称。将所有这些放在一起,我们有:

const content = [{insert: 'Hello'
}, {insert: 'World',attributes: { bold: true }
}, {insert: {image: 'https://exclamation.com/mark.png' },attributes: { width: '100' }
}];

描述更改


正如名称Delta所暗示的,我们的格式可以描述对文档的更改,以及文档本身。事实上,我们可以将文档视为我们要对空文档进行的更改,以获得我们正在描述的文档。正如您可能已经猜到的,使用Deltas来描述更改也是我们之前将文本重命名为插入(insert)的原因。我们将我们Delta数组中的每个元素称为操作(Operation)。

删除

要描述删除文本,我们需要知道在哪里以及要删除多少个字符。要删除嵌入物,除了理解嵌入物的长度之外,不需要任何特殊处理。如果它不是一,我们则需要指定当只删除嵌入物的一部分时会发生什么。目前还没有这样的规范,所以不管一张图片由多少像素组成,一个视频有多长,或一个幻灯片有多少张幻灯片;嵌入物的长度都是一。

描述删除的一个合理方式是明确存储其索引和长度。

const delta = [{delete: {index: 4,length: 1}
}, {delete: {index: 12,length: 3}
}];

我们必须根据索引对删除进行排序,并确保没有范围重叠,否则我们的规范约束将被违反。这种方法还有几个其他缺点,但在描述格式更改之后更容易理解。

插入

现在Deltas可能正在描述对非空文档的更改,{ insert: "Hello" }是不充分的,因为我们不知道“Hello”应该插入到哪里。我们可以通过添加一个索引来解决这个问题,类似于删除操作。

格式

与删除类似,我们需要指定要格式化的文本范围,以及格式更改本身。格式存在于属性对象中,所以一个简单的解决方案是提供一个额外的属性对象与现有对象合并。这个合并是浅层的,以保持简单。我们还没有发现一个足够有说服力的用例,需要深度合并并增加复杂性。

const delta = [{format: {index: 4,length: 1},attributes: {bold: true}
}];

特殊情况是我们想要移除格式。我们将为此使用null,所以{ bold: null }意味着移除粗体格式。我们本可以指定任何假值,但可能存在合理用例,属性值为0或空字符串。

注意:我们现在必须在应用层小心处理索引。如前所述,Deltas不赋予任何属性键值对、嵌入类型或值任何内在含义。Deltas不知道图像没有时长,文本没有替代文本,视频不能加粗。以下是一个合法的Delta,可能是应用其他合法Deltas的结果,由一个不小心格式范围的应用生成。

const delta = [{insert: {image: "https://imgur.com/" },attributes: {duration: 600}
}, {insert: "Hello",attributes: {alt: "Funny cat photo"}
}, {insert: {video: "https://youtube.com/" },attributes: {bold: true}
}];

陷阱

首先,我们应该明确索引必须参考在应用任何操作之前的文档中的位置。否则,后续操作可能会删除先前的插入,取消先前的格式等,这将违反紧凑性。

操作也必须严格排序以满足我们的规范约束。按索引、长度和类型排序是实现这一点的有效方法之一。

如前所述,删除范围不能重叠。反对重叠格式范围的情况不那么简短,但事实证明我们也不想要重叠的格式。

Delta可能无效的原因数量正在增加。更好的格式将根本不允许表达这种情况。

保留

如果我们暂时从紧凑性的形式主义中退后一步,我们可以描述一个更简单的格式来表达插入、删除和格式化:

  • Delta将具有至少与被修改文档一样长的Operation。
  • 每个Operation将描述在该索引处的字符发生了什么。
  • 可选的插入Operation可能会使Delta的长度超过它描述的文档。

这需要创建一个新的Operation,它将简单地意味着“保持这个字符不变”。我们称之为保留(retain)。

// 从"HelloWorld"开始,
// 将"Hello"加粗,并在其后立即插入一个空格
const change = [{ format: true, attributes: { bold: true } },  // H{ format: true, attributes: { bold: true } },  // e{ format: true, attributes: { bold: true } },  // l{ format: true, attributes: { bold: true } },  // l{ format: true, attributes: { bold: true } },  // o{ insert: ' ' },{ retain: true },  // W{ retain: true },  // o{ retain: true },  // r{ retain: true },  // l{ retain: true }   // d
]

由于每个字符都被描述了,显式索引和长度不再必要。这使得重叠范围和无序索引不可能表达。

因此,我们可以轻松地优化合并相邻的相同Operation,重新引入长度。如果最后一个Operation是保留,我们可以简单地删除它,因为它只是指示对文档的其余部分“什么都不做”。

const change = [{ format: 5, attributes: { bold: true } }{ insert: ' ' }
]

此外,您可能会注意到,保留在某些方面只是格式的一种特殊情况。例如,{ format: 1, attributes: {} }{ retain: 1 }之间没有实际区别。紧凑会丢弃空的属性对象,留给我们只有{ format: 1 },这将创建一个规范化冲突。因此,在我们的示例中,我们将简单地合并format和retain,并保留名称为retain。

const change = [{ retain: 5, attributes: { bold: true } },{ insert: ' ' }
]

现在我们有了一个非常接近当前标准格式的Delta。

retain操作

Quill编辑器使用一种叫做Delta的格式来表示文档的状态和变化。Delta是一系列的操作(insert、delete、retain),用来描述如何从一个文档状态转换到另一个。这里,我们专注于解释Delta中的retain操作。

retain操作在Quill的Delta框架中用来表示“保持当前文档中的一定数量的内容不变”。这是编辑过程中非常重要的一个概念,因为它允许Delta描述文档的变化,而不仅仅是插入和删除操作。简而言之,retain允许Delta跳过一定数量的文档内容,这些内容在编辑操作中保持不变。

Retain操作的作用

  • 定位编辑操作: 通过指定在执行下一个操作之前应保持不变的字符数,retain帮助定位后续的insertdelete操作。
  • 保持格式属性: retain还可以与属性一起使用,这允许对文档中的特定部分应用或修改格式,而不改变文本内容。

Retain操作的基本语法

{ "retain": number, "attributes": { /* formatting options */ } }

  • "retain": 表示保持(即跳过)的字符数量。
  • "attributes": 一个对象,包含要应用于这些字符的格式。如果没有"attributes"字段,那么这个retain操作就仅仅是跳过指定数量的字符,不改变任何格式。

示例:将"World"加粗

假设我们有文本"Hello World",现在我们想要将"World"这个词加粗。首先,我们需要使用retain操作跳过"Hello ",这是6个字符(包括空格)。然后,我们将对"World"应用加粗格式。

在Quill的Delta操作中,这可以表示为:

[{ "retain": 6 },{ "retain": 5, "attributes": { "bold": true } }
]

这里的含义是:

  1. 首先,跳过前6个字符("Hello ")。
  2. 然后,对接下来的5个字符("World")应用加粗格式。

通过这个Delta操作,"World"会被加粗,而"Hello "保持不变。这是一个非常基本的示例,但它展示了如何使用retain操作以及如何结合属性来更改文本格式。

结合其他操作

在实际使用中,retain操作通常与insertdelete操作结合使用,以描述复杂的文本变更。例如,如果在“Hello World”后面插入一个感叹号,同时保持“World”加粗,要在“Hello World”后面插入一个感叹号,并保持“World”加粗,我们需要结合使用 insert 和 retain 操作。首先,我们使用 retain 操作跳过整个“Hello World”文本,然后应用加粗格式到“World”。接着,使用 insert 操作在文本末尾插入感叹号。

在Quill的Delta操作中,这可以表示为:

[{ "retain": 6 },{ "retain": 5, "attributes": { "bold": true } },{ "insert": "!" }
]

这里的含义是:

  1. Retain 6: 跳过前6个字符("Hello "),因为我们不对这部分文本做任何改变。
  2. Retain 5 with Bold: 接着跳过接下来的5个字符("World"),并对这些字符应用加粗格式。这样,“World”就保持了加粗状态。
  3. Insert "!": 在当前位置(文本的末尾)插入一个感叹号。

通过这个Delta序列,我们成功地在不改变原有文本格式的情况下,在“Hello World”后面添加了一个感叹号,并保持了“World”的加粗格式。

通过这种方式,Quill可以精确地描述文本编辑过程中的每一个步骤,无论是添加、删除文本,还是改变文本的格式。这使得Quill非常适合实现复杂的文本编辑功能,如协同编辑、历史记录回溯等。

Ops

现在我们有一个易于使用的JSON数组,用于描述富文本。这在存储和传输层非常有用,但应用程序可以从更多功能中受益。我们可以通过将Deltas实现为一个类来添加这些功能,该类可以轻松地从JSON初始化或导出,然后提供相关的功能。

在Delta诞生之时,不可能对数组进行子类化。因此,Deltas被表示为对象,具有一个名为ops的单个属性,存储像我们一直在讨论的操作数组。

const delta = {ops: [{insert: 'Hello'}, {insert: 'World',attributes: { bold: true }}, {insert: {image: 'https://exclamation.com/mark.png' },attributes: { width: '100' }}]
};

最后,这就是Delta的最终格式。


http://www.ppmy.cn/news/1405455.html

相关文章

OpenCV与AI深度学习 | OpenCV中八种不同的目标追踪算法

本文来源公众号“OpenCV与AI深度学习”,仅用于学术分享,侵权删,干货满满。 原文链接:OpenCV中八种不同的目标追踪算法 目标跟踪作为机器学习的一个重要分支,加之其在日常生活、军事行动中的广泛应用,受到…

深入理解计算机系统 家庭作业 2.70

/* 网上很多都是错的,有些甚至题目都没读对 该题的意思是,x可以用多少位的补码表示. x -2时,最少用n2位补码就可以表示了 即 10 这道题求的是n 函数的核心思路: 用左右移位后的结果&一个n位的掩码就得到 x的最少位.移位没造出数字丢失时,x的最少位可以再通过移位还原成…

pygame--坦克大战(一)

项目搭建 本游戏主要分为两个对象,分别是我方坦克和敌方坦克。用户可以通过控制我方的坦克来摧毁敌方的坦克保护自己的“家”,把所有的敌方坦克消灭完达到胜利。敌方的坦克在初始的时候是默认5个的(这可以自己设置),当然,如果我方坦克被敌方坦克的子弹打中,游戏结束。从…

Redission--分布式锁

Redission的锁的好处 Redission分布式锁的底层是setnx和lua脚本(保证原子性) 1.是可重入锁。 2.Redisson 锁支持自动续期功能,这可以帮助我们合理控制分布式锁的有效时长,当业务逻辑执行时间超出了锁的过期时间,锁会自动续期,避免…

【大数据运维】Hbase shell 常见操作

文章目录 一. DDL1. 表的DDL1.1. 创建表1.2. 删除表 2. 列族的DDL2.1. 增加一个列簇2.2. 删除列族2.3. 修改列族版本(ing) 二. DML1. 插入与更新数据2. 删除数据3. 清空表 三. DQL1. scan:查一批数据1.1. 查询全部1.2. 过滤rowkey1.3. 过滤列…

CS架构---Socket基础

目录 一、Socket简介1.1 通信模型1.2 类型1.3 创建和使用:1.4 地址族 二、客户/服务器模式2.1 服务器端(Server Side)2.2 客户端(Client Side)2.3 通信方式2.4 角色分工2.5 优点 三、Socket实战四、常见应用场景 一、S…

C语言程序10题

第101题 (10.0分) 难度:易 第2章 /*------------------------------------------------------- 【程序填空】 --------------------------------------------------------- 功能:计算平均成绩并统计90分以上人数。 --…

【微服务篇】深入理解分布式消息队列系统

分布式消息队列是一种在多个服务器、应用或服务之间进行消息传递的技术。它使得各个独立的组件可以通过异步消息进行通信,提高了系统的可扩展性、解耦性和可靠性。 典型应用场景 1. 异步处理 在许多系统中,某些任务的处理可能需要较长时间&#xff0c…

Stable Diffusion 推荐硬件配置和本地化布署

Stable Diffusion简介 Stable Diffusion是由Stability AI开发的一种强大的文本到图像(Text-to-Image)生成模型,它能够根据用户提供的文本描述,生成与之相关的高质量、高分辨率图像。下面我从原理、特点、应用三个方面对Stable Diffusion作简要介绍: 1、原理:Stable Diffusion…

什么是智慧驿站?智慧驿站有哪些功能?创新型智慧公厕解说

近年来,随着智能科技的迅速发展,人们对于城市生活的期望也逐渐提升。作为城市基础设施的一部分,智慧驿站应运而生。它不仅是一座智慧公厕,更是集合了多种功能,给我们带来全新的城市生活体验。本文以智慧驿站智慧公厕源…

前端调试工具之Chrome Elements、Network、Sources、TimeLine调试

常用的调试工具有Chrome浏览器的调试工具,火狐浏览器的Firebug插件调试工具,IE的开发人员工具等。它们的功能与使用方法大致相似。Chrome浏览器简洁快速,功能强大这里主要介绍Chrome浏览器的调试工具。 打开 Google Chrome 浏览器&#xff0c…

第N6周:使用Word2vec实现文本分类

import torch import torch.nn as nn import torchvision from torchvision import transforms,datasets import os,PIL,pathlib,warnings #忽略警告信息 warnings.filterwarnings("ignore") # win10系统 device torch.device("cuda"if torch.cuda.is_ava…

20240328金融读报:国内金融安全网与银行适老化实例

1、国内金融安全网(原则:事前防范金融风险过度积累,事中、事后快速高效处置风险):1)强化金融机构的公司治理和风险管理(如重组与否)2)二加强金融监管(各种存贷…

视频剪辑软件哪个好?2024会声会影怎么样呢?

随着科技的不断发展,视频制作已经不再是专业人士的专属领域,越来越多的人开始使用各种视频制作软件来记录生活、创作内容。其中,会声会影是被广泛使用的一款视频制作软件,其旗舰版更是备受关注。 视频剪辑软件哪个好?…

SnapGene 5 for Mac 分子生物学软件

SnapGene 5 for Mac是一款专为Mac操作系统设计的分子生物学软件,以其强大的功能和用户友好的界面,为科研人员提供了高效、便捷的基因克隆和分子实验设计体验。 软件下载:SnapGene 5 for Mac v5.3.1中文激活版 这款软件支持DNA构建和克隆设计&…

C++的字节对齐

什么是字节对齐 参考什么是字节对齐,为什么要对齐? 现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规…

每日一题 --- 有效的括号[力扣][Go]

有效的括号 题目:20. 有效的括号 给定一个只包括 (,),{,},[,] 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序…

【Python基础知识点】Python的浅拷贝和深拷贝

概述 本文主要通过两个简单的代码小例子理解深拷贝和浅拷贝 主体内容 copy 模块提供了浅拷贝和深拷贝的功能。它的主要函数有: copy(x): 返回对象 x 的浅拷贝。 deepcopy(x): 返回对象 x 的深拷贝。 浅拷贝使用 copy(x) 函数,它只复制了最外层的对象,但内层的对象仍然是引用…

EfficientVMamba实战:使用EfficientVMamba实现图像分类任务(一)

文章目录 摘要安装包安装timm 数据增强Cutout和MixupEMA项目结构编译安装Vim环境环境安装过程安装库文件 计算mean和std生成数据集 摘要 论文:https://arxiv.org/pdf/2401.09417v1.pdf 作者研究了轻量级模型设计的新方法,通过引入视觉状态空间模型&…

YOLOv5改进系列:升级版ResNet的新主干网络DenseNet

一、论文理论 论文地址:Densely Connected Convolutional Networks 1.理论思想 DenseNet最大化前后层信息交流,通过建立前面所有层与后面层的密集连接,实现了特征在通道维度上的复用,不但减缓了梯度消失的现象,也使其…