From 6ef8472706e6560b88c2240c92139f9c737872fe Mon Sep 17 00:00:00 2001 From: WindRunnerMax <651525974@qq.com> Date: Sun, 15 Dec 2024 17:23:06 +0800 Subject: [PATCH] 24/12/15 --- ...15\345\272\217\345\210\227\345\214\226.md" | 283 +++++++++++++++++- ...\247\206Chrome\346\211\251\345\261\225.md" | 2 +- README.md | 2 +- Timeline.md | 2 +- 4 files changed, 280 insertions(+), 9 deletions(-) diff --git "a/Backup/\345\210\235\346\216\242\345\257\214\346\226\207\346\234\254\344\271\213\345\272\217\345\210\227\345\214\226\344\270\216\345\217\215\345\272\217\345\210\227\345\214\226.md" "b/Backup/\345\210\235\346\216\242\345\257\214\346\226\207\346\234\254\344\271\213\345\272\217\345\210\227\345\214\226\344\270\216\345\217\215\345\272\217\345\210\227\345\214\226.md" index dcf4f392..44951bf0 100644 --- "a/Backup/\345\210\235\346\216\242\345\257\214\346\226\207\346\234\254\344\271\213\345\272\217\345\210\227\345\214\226\344\270\216\345\217\215\345\272\217\345\210\227\345\214\226.md" +++ "b/Backup/\345\210\235\346\216\242\345\257\214\346\226\207\346\234\254\344\271\213\345\272\217\345\210\227\345\214\226\344\270\216\345\217\215\345\272\217\345\210\227\345\214\226.md" @@ -442,7 +442,7 @@ export class HeadingPlugin extends BlockPlugin { ``` ### 组合结构 -组合结构在这里指的是引用块、有序列表、无序列表等结构样式,这里则以引用块为例来处理序列化与反序列化。序列化组合结构,我同样需要`Node`是引用块节点时,构造相关的`HTML`节点进行包装。 +组合结构在这里指的是引用块、有序列表、无序列表等结构样式,这里则以引用块为例来处理序列化与反序列化。序列化组合结构,同样需要`Node`是引用块节点时,构造相关的`HTML`节点进行包装。 ```js // packages/plugin/src/quote-block/index.tsx @@ -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; @@ -685,16 +688,282 @@ root -- lines -- | -- line -- leaves ··· ---------
---| -- 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(`
HelloWorld
`); +}); +``` + +反序列化部分则是判断当前正在处理的`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 = `
HelloWorld
`; + 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(`

Hello

`); +}); +``` + +反序列化则是相反的操作,判断当前正在处理的`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 = `

Hello

World

`; + 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(`
Hello
`); +}); +``` + +反序列化同样是判断是否为引用块节点,并且构造对应的`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 = `

Hello

World

`; + 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(`
`); +}); +``` + +对于反序列化的结构,判断当前正在处理的`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 = ``; + 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( + `
inside
` + ); +}); +``` + +反序列化则是判断当前正在处理的`HTML`节点是否为块级节点,如果是的话就将其转换为`Node`节点。这里的处理方式则是,深度优先遍历处理节点内容时,若是出现`block`节点,则生成`id`并放置于`deltas`中,然后在`ROOT`结构中引用该节点。 + +```js +it("deserialize", () => { + const deltas: Record = {}; + 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 = `
inside
`; + 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"), + }); +}); +``` ## 每日一题 @@ -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 -``` \ No newline at end of file +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 +``` diff --git "a/Plugin/\344\273\216\350\204\232\346\234\254\347\256\241\347\220\206\345\231\250\347\232\204\350\247\222\345\272\246\345\256\241\350\247\206Chrome\346\211\251\345\261\225.md" "b/Plugin/\344\273\216\350\204\232\346\234\254\347\256\241\347\220\206\345\231\250\347\232\204\350\247\222\345\272\246\345\256\241\350\247\206Chrome\346\211\251\345\261\225.md" index c91d6fc3..813ac574 100644 --- "a/Plugin/\344\273\216\350\204\232\346\234\254\347\256\241\347\220\206\345\231\250\347\232\204\350\247\222\345\272\246\345\256\241\350\247\206Chrome\346\211\251\345\261\225.md" +++ "b/Plugin/\344\273\216\350\204\232\346\234\254\347\256\241\347\220\206\345\231\250\347\232\204\350\247\222\345\272\246\345\256\241\350\247\206Chrome\346\211\251\345\261\225.md" @@ -1,6 +1,6 @@ # 从脚本管理器的角度审视Chrome扩展 -在之前一段时间,我需要借助Chrome扩展来完成一个需求,当时还在使用油猴脚本与浏览器扩展之间调研了一波,而此时恰好我又有一些做的还可以的油猴脚本[TKScript](https://github.com/WindrunnerMax/TKScript),相对会比较熟悉脚本管理器的能力,预估是不太能完成需求的,所以趁着这个机会,我又学习了一波浏览器扩展的能力。那么在后来需求的开发过程中,因为有些能力是类似于脚本管理器提供的基础环境,致使我越来越好奇脚本管理器是怎么实现的,而实际上脚本管理器实际上还是一个浏览器扩展,浏览器也并没有给脚本管理器开后门来实现相关能力,而让我疑惑的三个问题是: +在之前一段时间,我需要借助Chrome扩展来完成一个需求,当时还在使用油猴脚本与浏览器扩展之间调研了一波,而此时恰好我又有一些做的还可以的油猴脚本 [TKScript](https://github.com/WindrunnerMax/TKScript),相对会比较熟悉脚本管理器的能力,预估是不太能完成需求的,所以趁着这个机会,我又学习了一波浏览器扩展的能力。那么在后来需求的开发过程中,因为有些能力是类似于脚本管理器提供的基础环境,致使我越来越好奇脚本管理器是怎么实现的,而实际上脚本管理器实际上还是一个浏览器扩展,浏览器也并没有给脚本管理器开后门来实现相关能力,而让我疑惑的三个问题是: 1. 脚本管理器为什么能够先于页面的`JS`运行。 2. 脚本管理器是如何能够得到页面`window`对象。 diff --git a/README.md b/README.md index 2e497bd7..27f3b3b8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 如果觉得还不错,点个`star`吧 😁 -版本库中共有`489`篇文章,总计`91912`行,`1084938`字,`3020472`字符。 +版本库中共有`489`篇文章,总计`92184`行,`1086842`字,`3030350`字符。 这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字`EveryDay`就是希望激励我能够每天学习,下面的文章就是从`2020.02.25`开始积累的文章,都是参考众多文章归纳整理学习而写的,文章包括了`HTML`基础、`CSS`基础、`JavaScript`基础与拓展、`Browser`浏览器相关、`Vue`使用与分析、`React`使用与分析、`Plugin`插件相关、`Patterns`设计模式、`Linux`命令、`LeetCode`题解等类别,内容都是比较基础的,毕竟我也还是个小。此外基本上每个示例都是本着能够即时运行为目标的,新建一个`HTML`文件复制之后即可在浏览器运行或者直接可以在`console`中运行。 diff --git a/Timeline.md b/Timeline.md index ed42c752..df0f33fc 100644 --- a/Timeline.md +++ b/Timeline.md @@ -1,6 +1,6 @@ # Timeline -前端笔记系列共有 419 篇文章,总计 74512 行, 847468 字, 2357809 字符。 +前端笔记系列共有 419 篇文章,总计 74512 行, 847468 字, 2357810 字符。 ### 2024-12-15 第 419 题:[从脚本管理器的角度审视Chrome扩展](Plugin/从脚本管理器的角度审视Chrome扩展.md)