parchment源码分析

1,545 阅读4分钟

背景:

Parchment是Quill的文档模型。是一个和DOM树对应的平行树结构,给内容编辑器Quill提供有用的功能。 一个Parchment 树是由Blots构成。Blot是一个DOM节点的对应物。Blots可以提供结构,格式化,或内容。Attributor可以提供轻量级的格式化信息。 Parchment tree是DOM tree的对应,二者关系紧密。

如何构造出一个parchment

  1. 基础抽象节点类型 :
import ContainerBlot from './blot/abstract/container';
import FormatBlot from './blot/abstract/format';
import LeafBlot from './blot/abstract/leaf';

import ScrollBlot from './blot/scroll';
import InlineBlot from './blot/inline';
import BlockBlot from './blot/block';
import EmbedBlot from './blot/embed';
import TextBlot from './blot/text';
  1. 基础Attributor: 可在parchment官网看到具体的配置
import Attributor from './attributor/attributor';
import ClassAttributor from './attributor/class';
import StyleAttributor from './attributor/style';

与Dom的关系如何建立?

新建Blot时,会调用static create() 创建DOM 节点,并设置blot.domNode= dom。 即建立关系

一个大概的流程:

1. static create () 创建DOM节点, 也就是新建了blot。

2. 可以添加样式属性等node = document.createElement(this.tagName);

3. 注册在Quill.register(此时DOM节点并没有插入到DOM树,也没有插入到parchment树中)会注册出formats/blod, formats/italic等

static register(path, target, overwrite = false) {
     this.imports[path] = target;
     Parchment.register(target);
 }

4. 当你点击加粗按钮时:会调用quill的format

4.1 然后调用this.editor.formatText (选中)
//选中文字 点击加粗调用
quill.format() --> 调用 this.editor.formatText ---> 
调用【this.scroll.formatAt,this.update(Delta)】--->
scrollBlot.formatAt -> parent.formatAt -> inline.formatAt -> inline.format(DOM修改)
4.2 this.selection.format(未选中)
//未选中
 this.selection.format -> this.cursor.format

5. insertEmbed基本流程:

quill.insertEmbed -> this.editor.insertEmbed -> [this.scroll.insertAt, this.update(delta)] -> 创建DOM,插到指定位置

具体源码:

quill.format('bold', true);
format(name, value, source = Emitter.sources.API) {
    console.log("点击格式化 加粗")
    return modify.call(this, () => {
      let range = this.getSelection(true);
      let change = new Delta();
      if (range == null) {
        return change;
      } else if (Parchment.query(name, Parchment.Scope.BLOCK)) {
        change = this.editor.formatLine(range.index, range.length, { [name]: value });
      } else if (range.length === 0) {
        this.selection.format(name, value); //光标之后的内容格式化
        return change;
      } else {// 格式化选中的部分
        change = this.editor.formatText(range.index, range.length, { [name]: value });
      }
      this.setSelection(range, Emitter.sources.SILENT);
      return change;
    }, source);
  }
  // this.editor.formatText
  formatText(index, length, formats = {}) {
    Object.keys(formats).forEach((format) => {
      this.scroll.formatAt(index, length, format, formats[format]);
    });
    return this.update(new Delta().retain(index).retain(length, clone(formats)));
  }

6.同步更新delta

const delta = new Delta().retain(index).retain(length, clone(formats));
return this.update(delta);`

7. 真实的修改了DOM:blot会有一些方法用来操作dom,比如添加appendChild等,insertBefore等

wrap(name: string | Parent, value?: any): Parent {
    let wrapper = typeof name === 'string' ? <Parent>Registry.create(name, value) : name;
    if (this.parent != null) {
      this.parent.insertBefore(wrapper, this.next);
    }
    wrapper.appendChild(this);
    return wrapper;
  }


源码分析:

  1. parchment.ts parchment 的入口

在parchment.ts中对外导出的有四类东西。

  • 节点Blot
    • ParentBlot 【父级节点】能对子节点进行增,删,改,移动,查
    • ContainerBlot 【容器节点】
    • LeafBlot 【叶节点】
    • EmbedBlot 嵌入式节点 【可格式化的叶节点】
    • ScrollBlot root【文档的根节点,不可格式化】
    • BlockBlot 块级 【可格式化的父级节点】
    • InlineBlot 内联 【可格式化的父级节点】
    • TextBlot 文本【叶节点】
  • 属性Attributor
    • Attributor 【一种代表格式的方法】
    • ClassAttributor 【使用classname模式来代表格式】
    • StyleAttributor 【使用内联样式来代表格式】
    • AttributorStore 【节点的attributes管理器】在BlockBlot
    • InlineBlot中使用到了
  • 注册中心
    • Registry 【static blots = new WeakMap<Node,Blot>,attributes,classes,tags,types 】
  • 类型常量Scope
    • Scope

2. blot.ts

是个接口,继承了linkedNode链表结构

3. blot create() 创建

正确创建污点有多个步骤,但是Parchment.create()可以替换这些步骤

每个Blot都有一个static create()从初始值创建DOM节点的功能。这也是在DOM节点上设置与实际Blot实例无关的初始值的好地方。 此时返回的DOM节点实际上未附加在任何地方,并且blot也未创建。因为Blot是从 DOM节点创建的,因此这个函数把他们放在了一起。不一定总是使用create功能来构造污点。例如,当用户从Quill或其他来源复制/粘贴文本时,复制的HTML结构将传递到Parchment.create()。羊皮纸将跳过调用create()并使用传递的DOM节点,跳到下一步。

看下源码:

class FormatBlot extends ContainerBlot implements Formattable {}
class LeafBlot extends ShadowBlot implements Leaf {}
class ContainerBlot extends ShadowBlot implements Parent{}
========shadow.ts源码=====
class ShadowBlot implements Blot {
  static blotName = 'abstract';
  static className: string;
  static scope: Registry.Scope;
  static tagName: string;

  // @ts-ignore
  prev: Blot;
  // @ts-ignore
  next: Blot;
  // @ts-ignore
  parent: Parent;
  // @ts-ignore
  scroll: Parent;

  // Hack for accessing inherited static methods
  get statics(): any {
    return this.constructor;
  }

  static create(value: any): Node {  
  // 如果没有tagName 则抛出错误,没有定义tagName
    if (this.tagName == null) {
      throw new Registry.ParchmentError('Blot definition missing tagName');
    }
    let node;
    //如果是数组  根据对应的值创建标签
    if (Array.isArray(this.tagName)) {
      if (typeof value === 'string') {
        value = value.toUpperCase();
        if (parseInt(value).toString() === value) {
          value = parseInt(value);
        }
      }
      if (typeof value === 'number') {
        node = document.createElement(this.tagName[value - 1]);
      } else if (this.tagName.indexOf(value) > -1) {
        node = document.createElement(value);
      } else {
        node = document.createElement(this.tagName[0]);
      }
    } else {
        // 如果是普通的,创建对应的tagName
      node = document.createElement(this.tagName);
    }
    //如果有className, 就添加className
    if (this.className) {
      node.classList.add(this.className);
    }
    // 返回一个node节点
    return node;
  }

  constructor(public domNode: Node) {
    // @ts-ignore
    this.domNode[Registry.DATA_KEY] = { blot: this };
  }


} //create的初始化就在这里

比如 Quill 的bold 加粗功能 node = document.createElement(this.tagName[0]);

Inline 继承了parchment的Inline,

class Inline extends Parchment.Inline {}
parchment的 InlineBlot 继承了FormatBlot
class InlineBlot extends FormatBlot {}
class FormatBlot extends ContainerBlot implements Formattable {}
class ContainerBlot extends ShadowBlot implements Parent {

举个栗子:

testParchment.js

import Inline from 'quill/blots/inline';
import Quill from "quill";
class ClickableSpan extends Inline {
    static tagName = "span";
    static className = "ClickableSpan";
    static create(initialValue) { //创建一个DOM
      const node = super.create(initialValue);
      // node 此时就是<span class="ClickableSpan"></span>
      node.setAttribute("spellcheck", false);
      node.classList.add("otherClass");
      return node;
    }  
}
Quill.register(ClickableSpan);
export default ClickableSpan;   

test.vue

import ClickableSpan from '../assets/js/testParchment.js'
let dom = ClickableSpan.create();   
 console.log(dom);
 // <span class="ClickableSpan otherClass" spellcheck="false"></span>