Skip to content

Commit

Permalink
24/12/15
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Dec 15, 2024
1 parent c699c35 commit 6ef8472
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 9 deletions.
283 changes: 277 additions & 6 deletions Backup/初探富文本之序列化与反序列化.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export class HeadingPlugin extends BlockPlugin {
```

### 组合结构
组合结构在这里指的是引用块、有序列表、无序列表等结构样式,这里则以引用块为例来处理序列化与反序列化。序列化组合结构,我同样需要`Node`是引用块节点时,构造相关的`HTML`节点进行包装。
组合结构在这里指的是引用块、有序列表、无序列表等结构样式,这里则以引用块为例来处理序列化与反序列化。序列化组合结构,同样需要`Node`是引用块节点时,构造相关的`HTML`节点进行包装。

```js
// packages/plugin/src/quote-block/index.tsx
Expand Down Expand Up @@ -613,9 +613,12 @@ for (const op of ops) {
if (isEOLOp(op)) {
const context: SerializeContext = { op, html: lineFragment };
this.editor.plugin.call(CALLER_TYPE.SERIALIZE, context);
const lineNode = document.createElement("div");
lineNode.setAttribute(LINE_TAG, "true");
lineNode.appendChild(context.html);
let lineNode = context.html as HTMLElement;
if (!isMatchBlockTag(lineNode)) {
lineNode = document.createElement("div");
lineNode.setAttribute(LINE_TAG, "true");
lineNode.appendChild(context.html);
}
root.appendChild(lineNode);
lineFragment = document.createDocumentFragment();
continue;
Expand Down Expand Up @@ -685,16 +688,282 @@ root -- lines -- | -- line -- leaves ··· <elements> --------- <div> ---| -- n

接下来我们将会以`delta`数据结构为例,处理扁平结构的剪贴板模块设计。同样分别以行内结构、段落结构、组合结构、嵌入结构、块级结构为基础,在上述基本模式的调度下,分类型进行序列化与反序列化的插件实现。


### 行内结构
行内结构指的是加粗、斜体、下划线、删除线、行内代码块等行内的结构样式,这里以加粗为例来处理序列化与反序列化。序列化行内结构部分基本与`slate`一致,从这里开始我们采用单元测试的方式执行。

```js
// packages/core/test/clipboard/bold.test.ts
it("serialize", () => {
const plugin = getMockedPlugin({
serialize(context) {
if (context.op.attributes?.bold) {
const strong = document.createElement("strong");
strong.appendChild(context.html);
context.html = strong;
}
},
});
editor.plugin.register(plugin);
const delta = new Delta().insert("Hello", { bold: "true" }).insert("World");
const root = editor.clipboard.copyModule.serialize(delta);
const plainText = getFragmentText(root);
const htmlText = serializeHTML(root);
expect(plainText).toBe("HelloWorld");
expect(htmlText).toBe(`<div data-node="true"><strong>Hello</strong>World</div>`);
});
```
反序列化部分则是判断当前正在处理的`HTML`节点是否为加粗节点,如果是的话就将其转换为`Delta`节点。
```js
// packages/core/test/clipboard/bold.test.ts
it("deserialize", () => {
const plugin = getMockedPlugin({
deserialize(context) {
const { delta, html } = context;
if (!isHTMLElement(html)) return void 0;
if (isMatchHTMLTag(html, "strong") || isMatchHTMLTag(html, "b") || html.style.fontWeight === "bold") {
// applyMarker packages/core/src/clipboard/utils/deserialize.ts
applyMarker(delta, { bold: "true" });
}
},
});
editor.plugin.register(plugin);
const parser = new DOMParser();
const transferHTMLText = `<div><strong>Hello</strong>World</div>`;
const html = parser.parseFromString(transferHTMLText, "text/html");
const rootDelta = editor.clipboard.pasteModule.deserialize(html.body);
const delta = new Delta().insert("Hello", { bold: "true" }).insert("World");
expect(rootDelta).toEqual(delta);
});
```
### 段落结构
段落结构指的是标题、行高、文本对齐等结构样式,这里则以标题为例来处理序列化与反序列化。序列化段落结构,我们只需要`Node`是标题节点时,构造相关的`HTML`节点,将本来的节点原地包装并赋值到`context`即可,同样采用嵌套节点的方式。
```js
// packages/core/test/clipboard/heading.test.ts
it("serialize", () => {
const plugin = getMockedPlugin({
serialize(context) {
const { op, html } = context;
if (isEOLOp(op) && op.attributes?.heading) {
const element = document.createElement(op.attributes.heading);
element.appendChild(html);
context.html = element;
}
},
});
editor.plugin.register(plugin);
const delta = new MutateDelta().insert("Hello").insert("\n", { heading: "h1" });
const root = editor.clipboard.copyModule.serialize(delta);
const plainText = getFragmentText(root);
const htmlText = serializeHTML(root);
expect(plainText).toBe("Hello");
expect(htmlText).toBe(`<h1>Hello</h1>`);
});
```
反序列化则是相反的操作,判断当前正在处理的`HTML`节点是否为标题节点,如果是的话就将其转换为`Node`节点。这里同样需要原地处理数据,与行内节点不同的是,需要使用`applyLineMarker`将所有的行节点加入标题格式。
```js
// packages/core/test/clipboard/heading.test.ts
it("deserialize", () => {
const plugin = getMockedPlugin({
deserialize(context) {
const { delta, html } = context;
if (!isHTMLElement(html)) return void 0;
if (["h1", "h2"].indexOf(html.tagName.toLowerCase()) > -1) {
applyLineMarker(delta, { heading: html.tagName.toLowerCase() });
}
},
});
editor.plugin.register(plugin);
const parser = new DOMParser();
const transferHTMLText = `<div><h1>Hello</h1><h2>World</h2></div>`;
const html = parser.parseFromString(transferHTMLText, TEXT_HTML);
const rootDelta = editor.clipboard.pasteModule.deserialize(html.body);
const delta = new Delta()
.insert("Hello")
.insert("\n", { heading: "h1" })
.insert("World")
.insert("\n", { heading: "h2" });
expect(rootDelta).toEqual(MutateDelta.from(delta));
});
```
### 组合结构
组合结构在这里指的是引用块、有序列表、无序列表等结构样式,这里则以引用块为例来处理序列化与反序列化。序列化组合结构,我同样需要`Node`是引用块节点时,构造相关的`HTML`节点进行包装。在扁平结构下类似组合结构的处理方式会是渲染时进行的,因此序列化的过程与先前标题一致。
```js
// packages/core/test/clipboard/quote.test.ts
it("serialize", () => {
const plugin = getMockedPlugin({
serialize(context) {
const { op, html } = context;
if (isEOLOp(op) && op.attributes?.quote) {
const element = document.createElement("blockquote");
element.appendChild(html);
context.html = element;
}
},
});
editor.plugin.register(plugin);
const delta = new MutateDelta().insert("Hello").insert("\n", { quote: "true" });
const root = editor.clipboard.copyModule.serialize(delta);
const plainText = getFragmentText(root);
const htmlText = serializeHTML(root);
expect(plainText).toBe("Hello");
expect(htmlText).toBe(`<blockquote>Hello</blockquote>`);
});
```
反序列化同样是判断是否为引用块节点,并且构造对应的`Node`节点。这里与标题模块不同的是,标题是将格式应用到相关的行节点上,而引用块则是在原本的节点上嵌套一层结构。反序列化的结构处理方式也类似于标题处理方式,由于在`HTML`的结构上是嵌套结构,在应用时在所有行节点上加入引用格式。
```js
// packages/core/test/clipboard/quote.test.ts
it("deserialize", () => {
const plugin = getMockedPlugin({
deserialize(context) {
const { delta, html } = context;
if (!isHTMLElement(html)) return void 0;
if (isMatchHTMLTag(html, "p")) {
applyLineMarker(delta, {});
}
if (isMatchHTMLTag(html, "blockquote")) {
applyLineMarker(delta, { quote: "true" });
}
},
});
editor.plugin.register(plugin);
const parser = new DOMParser();
const transferHTMLText = `<div><blockquote><p>Hello</p><p>World</p></blockquote></div>`;
const html = parser.parseFromString(transferHTMLText, TEXT_HTML);
const rootDelta = editor.clipboard.pasteModule.deserialize(html.body);
const delta = new Delta()
.insert("Hello")
.insert("\n", { quote: "true" })
.insert("World")
.insert("\n", { quote: "true" });
expect(rootDelta).toEqual(MutateDelta.from(delta));
});
```
### 嵌入结构
嵌入结构在这里指的是图片、视频、流程图等结构样式,这里则以图片为例来处理序列化与反序列化。序列化嵌入结构,我们只需要`Node`是图片节点时,构造相关的`HTML`节点进行包装。与之前的节点不同的是,此时我们不需要嵌套`DOM`节点了,将独立节点原地替换即可。
```js
// packages/core/test/clipboard/image.test.ts
it("serialize", () => {
const plugin = getMockedPlugin({
serialize(context) {
const { op } = context;
if (op.attributes?.image && op.attributes.src) {
const element = document.createElement("img");
element.src = op.attributes.src;
context.html = element;
}
},
});
editor.plugin.register(plugin);
const delta = new Delta().insert(" ", {
image: "true",
src: "https://example.com/image.png",
});
const root = editor.clipboard.copyModule.serialize(delta);
const plainText = getFragmentText(root);
const htmlText = serializeHTML(root);
expect(plainText).toBe("");
expect(htmlText).toBe(`<div data-node="true"><img src="https://example.com/image.png"></div>`);
});
```
对于反序列化的结构,判断当前正在处理的`HTML`节点是否为图片节点,如果是的话就将其转换为`Node`节点。同样的,这里还有个常用的操作是,粘贴图片内容通常需要将原本的`src`转储到我们的服务上,例如飞书的图片就是临时链接,在生产环境中需要转储资源。
```js
// packages/core/test/clipboard/image.test.ts
it("deserialize", () => {
const plugin = getMockedPlugin({
deserialize(context) {
const { html } = context;
if (!isHTMLElement(html)) return void 0;
if (isMatchHTMLTag(html, "img")) {
const src = html.getAttribute("src") || "";
const delta = new Delta();
delta.insert(" ", { image: "true", src: src });
context.delta = delta;
}
},
});
editor.plugin.register(plugin);
const parser = new DOMParser();
const transferHTMLText = `<img src="https://example.com/image.png"></img>`;
const html = parser.parseFromString(transferHTMLText, TEXT_HTML);
const rootDelta = editor.clipboard.pasteModule.deserialize(html.body);
const delta = new Delta().insert(" ", { image: "true", src: "https://example.com/image.png" });
expect(rootDelta).toEqual(delta);
});
```
### 块级结构
### 块级结构
块级结构指的是高亮块、代码块、表格等结构样式,这里则以块结构为例来处理序列化与反序列化。这里的嵌套结构还没有实现,因此这里仅仅是实现了上述`deltas`图示的测试用例,主要的处理方式是当存在引用关系时,主动调用序列化的方式将其写入到`HTML`中。
```js
it("serialize", () => {
const block = new Delta().insert("inside");
const inside = editor.clipboard.copyModule.serialize(block);
const plugin = getMockedPlugin({
serialize(context) {
const { op } = context;
if (op.attributes?._ref) {
const element = document.createElement("div");
element.setAttribute("data-block", op.attributes._ref);
element.appendChild(inside);
context.html = element;
}
},
});
editor.plugin.register(plugin);
const delta = new Delta().insert(" ", { _ref: "id" });
const root = editor.clipboard.copyModule.serialize(delta);
const plainText = getFragmentText(root);
const htmlText = serializeHTML(root);
expect(plainText).toBe("inside\n");
expect(htmlText).toBe(
`<div data-node="true"><div data-block="id"><div data-node="true">inside</div></div></div>`
);
});
```
反序列化则是判断当前正在处理的`HTML`节点是否为块级节点,如果是的话就将其转换为`Node`节点。这里的处理方式则是,深度优先遍历处理节点内容时,若是出现`block`节点,则生成`id`并放置于`deltas`中,然后在`ROOT`结构中引用该节点。
```js
it("deserialize", () => {
const deltas: Record<string, Delta> = {};
const plugin = getMockedPlugin({
deserialize(context) {
const { html } = context;
if (!isHTMLElement(html)) return void 0;
if (isMatchHTMLTag(html, "div") && html.hasAttribute("data-block")) {
const id = html.getAttribute("data-block")!;
deltas[id] = context.delta;
context.delta = new Delta().insert(" ", { _ref: id });
}
},
});
editor.plugin.register(plugin);
const parser = new DOMParser();
const transferHTMLText = `<div data-node="true"><div data-block="id"><div data-node="true">inside</div></div></div>`;
const html = parser.parseFromString(transferHTMLText, TEXT_HTML);
const rootDelta = editor.clipboard.pasteModule.deserialize(html.body);
deltas[ROOT_BLOCK] = rootDelta;
expect(deltas).toEqual({
[ROOT_BLOCK]: new Delta().insert(" ", { _ref: "id" }),
id: new Delta().insert("inside"),
});
});
```
## 每日一题
Expand All @@ -709,4 +978,6 @@ https://quilljs.com/docs/modules/clipboard
https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
https://github.com/slab/quill/blob/ebe16ca/packages/quill/src/modules/clipboard.ts
```
https://github.com/ianstormtaylor/slate/blob/dbd0a3e/packages/slate-dom/src/utils/dom.ts
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard
```
2 changes: 1 addition & 1 deletion Plugin/从脚本管理器的角度审视Chrome扩展.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 从脚本管理器的角度审视Chrome扩展

在之前一段时间,我需要借助Chrome扩展来完成一个需求,当时还在使用油猴脚本与浏览器扩展之间调研了一波,而此时恰好我又有一些做的还可以的油猴脚本[TKScript](https://github.com/WindrunnerMax/TKScript),相对会比较熟悉脚本管理器的能力,预估是不太能完成需求的,所以趁着这个机会,我又学习了一波浏览器扩展的能力。那么在后来需求的开发过程中,因为有些能力是类似于脚本管理器提供的基础环境,致使我越来越好奇脚本管理器是怎么实现的,而实际上脚本管理器实际上还是一个浏览器扩展,浏览器也并没有给脚本管理器开后门来实现相关能力,而让我疑惑的三个问题是:
在之前一段时间,我需要借助Chrome扩展来完成一个需求,当时还在使用油猴脚本与浏览器扩展之间调研了一波,而此时恰好我又有一些做的还可以的油猴脚本 [TKScript](https://github.com/WindrunnerMax/TKScript),相对会比较熟悉脚本管理器的能力,预估是不太能完成需求的,所以趁着这个机会,我又学习了一波浏览器扩展的能力。那么在后来需求的开发过程中,因为有些能力是类似于脚本管理器提供的基础环境,致使我越来越好奇脚本管理器是怎么实现的,而实际上脚本管理器实际上还是一个浏览器扩展,浏览器也并没有给脚本管理器开后门来实现相关能力,而让我疑惑的三个问题是:

1. 脚本管理器为什么能够先于页面的`JS`运行。
2. 脚本管理器是如何能够得到页面`window`对象。
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
如果觉得还不错,点个`star`吧 😁

<!-- Summary Start -->
版本库中共有`489`篇文章,总计`91912`行,`1084938`字,`3020472`字符。
版本库中共有`489`篇文章,总计`92184`行,`1086842`字,`3030350`字符。
<!-- Summary End -->

这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字`EveryDay`就是希望激励我能够每天学习,下面的文章就是从`2020.02.25`开始积累的文章,都是参考众多文章归纳整理学习而写的,文章包括了`HTML`基础、`CSS`基础、`JavaScript`基础与拓展、`Browser`浏览器相关、`Vue`使用与分析、`React`使用与分析、`Plugin`插件相关、`Patterns`设计模式、`Linux`命令、`LeetCode`题解等类别,内容都是比较基础的,毕竟我也还是个小。此外基本上每个示例都是本着能够即时运行为目标的,新建一个`HTML`文件复制之后即可在浏览器运行或者直接可以在`console`中运行。
Expand Down
2 changes: 1 addition & 1 deletion Timeline.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Timeline

前端笔记系列共有 419 篇文章,总计 74512 行, 847468 字, 2357809 字符。
前端笔记系列共有 419 篇文章,总计 74512 行, 847468 字, 2357810 字符。

### 2024-12-15
第 419 题:[从脚本管理器的角度审视Chrome扩展](Plugin/从脚本管理器的角度审视Chrome扩展.md)
Expand Down

0 comments on commit 6ef8472

Please sign in to comment.