Quill文档(五):Delta格式

富文本编辑器缺乏一种表达其自身内容的规范。直到最近,大多数富文本编辑器甚至不知道它们自己的编辑区域中有什么内容。这些编辑器只是传递用户的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的最终格式。

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这个错误可能是由于使用了已经被弃用的jQuery事件别名,比如.load、.unload或.error,这些别名在jQuery 1.8版本之后已经不再使用。请在代码中查找这些别名,并将它们替换为.on()来注册监听器。例如,将$(window).load(function(){...});替换为$(window).on("load", function(){...});\[1\] 另外,根据引用\[2\]和引用\[3\]的代码示例,这个错误可能与使用document.write或innerHTML来输出内容有关。请确保leaf.domNode是一个有效的DOM节点,并且具有getBoundingClientRect方法。如果leaf.domNode不是一个DOM节点,或者没有getBoundingClientRect方法,那么调用该方法就会导致TypeError错误。请检查代码中与leaf.domNode相关的部分,确保正确使用了这个对象\[2\]\[3\]。 #### 引用[.reference_title] - *1* [修复jquery: Uncaught TypeError: e.indexOf is not a function问题load弃用](https://blog.csdn.net/wzp20092009/article/details/127051553)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Uncaught TypeError: document.getElementsById is not a function](https://blog.csdn.net/Smtime826/article/details/82721880)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值