first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:30:42 +03:00
commit 4d92991817
1982 changed files with 284835 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
Copyright (c) 2015-2021, Jason Chen
Copyright (c) 2022-2024, Slab, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,271 @@
# Parchment [![Build Status](https://github.com/quilljs/parchment/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/quilljs/parchment/actions?query=branch%3Amain)
Parchment is [Quill](https://quilljs.com)'s document model. It is a parallel tree structure to the DOM tree, and provides functionality useful for content editors, like Quill. A Parchment tree is made up of [Blots](#blots), which mirror a DOM node counterpart. Blots can provide structure, formatting, and/or content. [Attributors](#attributors) can also provide lightweight formatting information.
**Note:** You should never instantiate a Blot yourself with `new`. This may prevent necessary lifecycle functionality of a Blot. Use the [Registry](#registry)'s `create()` method instead.
`npm install parchment`
See [Cloning Medium with Parchment](https://quilljs.com/guides/cloning-medium-with-parchment/) for a guide on how Quill uses Parchment its document model.
## Blots
Blots are the basic building blocks of a Parchment document. Several basic implementations such as [Block](#block-blot), [Inline](#inline-blot), and [Embed](#embed-blot) are provided. In general you will want to extend one of these, instead of building from scratch. After implementation, blots need to be [registered](#registry) before usage.
At the very minimum a Blot must be named with a static `blotName` and associated with either a `tagName` or `className`. If a Blot is defined with both a tag and class, the class takes precedence, but the tag may be used as a fallback. Blots must also have a [scope](#registry), which determine if it is inline or block.
```typescript
class Blot {
static blotName: string;
static className: string;
static tagName: string | string[];
static scope: Scope;
domNode: Node;
prev: Blot | null;
next: Blot | null;
parent: Blot;
// Creates corresponding DOM node
static create(value?: any): Node;
constructor(domNode: Node, value?: any);
// For leaves, length of blot's value()
// For parents, sum of children's values
length(): Number;
// Manipulate at given index and length, if applicable.
// Will often pass call onto appropriate child.
deleteAt(index: number, length: number);
formatAt(index: number, length: number, format: string, value: any);
insertAt(index: number, text: string);
insertAt(index: number, embed: string, value: any);
// Returns offset between this blot and an ancestor's
offset(ancestor: Blot = this.parent): number;
// Called after update cycle completes. Cannot change the value or length
// of the document, and any DOM operation must reduce complexity of the DOM
// tree. A shared context object is passed through all blots.
optimize(context: { [key: string]: any }): void;
// Called when blot changes, with the mutation records of its change.
// Internal records of the blot values can be updated, and modifications of
// the blot itself is permitted. Can be trigger from user change or API call.
// A shared context object is passed through all blots.
update(mutations: MutationRecord[], context: { [key: string]: any });
/** Leaf Blots only **/
// Returns the value represented by domNode if it is this Blot's type
// No checking that domNode can represent this Blot type is required so
// applications needing it should check externally before calling.
static value(domNode): any;
// Given location represented by node and offset from DOM Selection Range,
// return index to that location.
index(node: Node, offset: number): number;
// Given index to location within blot, return node and offset representing
// that location, consumable by DOM Selection Range
position(index: number, inclusive: boolean): [Node, number];
// Return value represented by this blot
// Should not change without interaction from API or
// user change detectable by update()
value(): any;
/** Parent blots only **/
// Whitelist array of Blots that can be direct children.
static allowedChildren: Registry.BlotConstructor[];
// Default child blot to be inserted if this blot becomes empty.
static defaultChild: Registry.BlotConstructor;
children: LinkedList<Blot>;
// Called during construction, should fill its own children LinkedList.
build();
// Useful search functions for descendant(s), should not modify
descendant(type: BlotClass, index: number, inclusive): Blot;
descendants(type: BlotClass, index: number, length: number): Blot[];
/** Formattable blots only **/
// Returns format values represented by domNode if it is this Blot's type
// No checking that domNode is this Blot's type is required.
static formats(domNode: Node);
// Apply format to blot. Should not pass onto child or other blot.
format(format: name, value: any);
// Return formats represented by blot, including from Attributors.
formats(): Object;
}
```
### Example
Implementation for a Blot representing a link, which is a parent, inline scoped, and formattable.
```typescript
import { InlineBlot, register } from 'parchment';
class LinkBlot extends InlineBlot {
static blotName = 'link';
static tagName = 'A';
static create(url) {
let node = super.create();
node.setAttribute('href', url);
node.setAttribute('target', '_blank');
node.setAttribute('title', node.textContent);
return node;
}
static formats(domNode) {
return domNode.getAttribute('href') || true;
}
format(name, value) {
if (name === 'link' && value) {
this.domNode.setAttribute('href', value);
} else {
super.format(name, value);
}
}
formats() {
let formats = super.formats();
formats['link'] = LinkBlot.formats(this.domNode);
return formats;
}
}
register(LinkBlot);
```
Quill also provides many great example implementations in its [source code](https://github.com/quilljs/quill/tree/develop/packages/quill/src/formats).
### Block Blot
Basic implementation of a block scoped formattable parent Blot. Formatting a block blot by default will replace the appropriate subsection of the blot.
### Inline Blot
Basic implementation of an inline scoped formattable parent Blot. Formatting an inline blot by default either wraps itself with another blot or passes the call to the appropriate child.
### Embed Blot
Basic implementation of a non-text leaf blot, that is formattable. Its corresponding DOM node will often be a [Void Element](https://www.w3.org/TR/html5/syntax.html#void-elements), but can be a [Normal Element](https://www.w3.org/TR/html5/syntax.html#normal-elements). In these cases Parchment will not manipulate or generally be aware of the element's children, and it will be important to correctly implement the blot's `index()` and `position()` functions to correctly work with cursors/selections.
### Scroll
The root parent blot of a Parchment document. It is not formattable.
## Attributors
Attributors are the alternative, more lightweight, way to represent formats. Their DOM counterpart is an [Attribute](https://www.w3.org/TR/html5/syntax.html#attributes-0). Like a DOM attribute's relationship to a node, Attributors are meant to belong to Blots. Calling `formats()` on an [Inline](#inline-blot) or [Block](#block-blot) blot will return both the format of the corresponding DOM node represents (if any) and the formats the DOM node's attributes represent (if any).
Attributors have the following interface:
```typescript
class Attributor {
attrName: string;
keyName: string;
scope: Scope;
whitelist: string[];
constructor(attrName: string, keyName: string, options: Object = {});
add(node: HTMLElement, value: string): boolean;
canAdd(node: HTMLElement, value: string): boolean;
remove(node: HTMLElement);
value(node: HTMLElement);
}
```
Note custom attributors are instances, rather than class definitions like Blots. Similar to Blots, instead of creating from scratch, you will probably want to use existing Attributor implementations, such as the base [Attributor](#attributor), [Class Attributor](#class-attributor) or [Style Attributor](#style-attributor).
The implementation for Attributors is surprisingly simple, and its [source code](https://github.com/quilljs/parchment/tree/main/src/attributor) may be another source of understanding.
### Attributor
Uses a plain attribute to represent formats.
```js
import { Attributor, register } from 'parchment';
let Width = new Attributor('width', 'width');
register(Width);
let imageNode = document.createElement('img');
Width.add(imageNode, '10px');
console.log(imageNode.outerHTML); // Will print <img width="10px">
Width.value(imageNode); // Will return 10px
Width.remove(imageNode);
console.log(imageNode.outerHTML); // Will print <img>
```
### Class Attributor
Uses a class name pattern to represent formats.
```js
import { ClassAttributor, register } from 'parchment';
let Align = new ClassAttributor('align', 'blot-align');
register(Align);
let node = document.createElement('div');
Align.add(node, 'right');
console.log(node.outerHTML); // Will print <div class="blot-align-right"></div>
```
### Style Attributor
Uses inline styles to represent formats.
```js
import { StyleAttributor, register } from 'parchment';
let Align = new StyleAttributor('align', 'text-align', {
whitelist: ['right', 'center', 'justify'], // Having no value implies left align
});
register(Align);
let node = document.createElement('div');
Align.add(node, 'right');
console.log(node.outerHTML); // Will print <div style="text-align: right;"></div>
```
## Registry
All methods are accessible from Parchment ex. `Parchment.create('bold')`.
```typescript
// Creates a blot given a name or DOM node.
// When given just a scope, creates blot the same name as scope
create(domNode: Node, value?: any): Blot;
create(blotName: string, value?: any): Blot;
create(scope: Scope): Blot;
// Given DOM node, find corresponding Blot.
// Bubbling is useful when searching for a Embed Blot with its corresponding
// DOM node's descendant nodes.
find(domNode: Node, bubble: boolean = false): Blot;
// Search for a Blot or Attributor
// When given just a scope, finds blot with same name as scope
query(tagName: string, scope: Scope = Scope.ANY): BlotClass;
query(blotName: string, scope: Scope = Scope.ANY): BlotClass;
query(domNode: Node, scope: Scope = Scope.ANY): BlotClass;
query(scope: Scope): BlotClass;
query(attributorName: string, scope: Scope = Scope.ANY): Attributor;
// Register Blot class definition or Attributor instance
register(BlotClass | Attributor);
```

View File

@@ -0,0 +1,453 @@
export declare class Attributor {
readonly attrName: string;
readonly keyName: string;
static keys(node: HTMLElement): string[];
scope: Scope;
whitelist: string[] | undefined;
constructor(attrName: string, keyName: string, options?: AttributorOptions);
add(node: HTMLElement, value: any): boolean;
canAdd(_node: HTMLElement, value: any): boolean;
remove(node: HTMLElement): void;
value(node: HTMLElement): any;
}
export declare interface AttributorOptions {
scope?: Scope;
whitelist?: string[];
}
export declare class AttributorStore {
private attributes;
private domNode;
constructor(domNode: HTMLElement);
attribute(attribute: Attributor, value: any): void;
build(): void;
copy(target: Formattable): void;
move(target: Formattable): void;
values(): {
[key: string]: any;
};
}
export declare class BlockBlot extends ParentBlot implements Formattable {
static blotName: string;
static scope: Scope;
static tagName: string | string[];
static allowedChildren: BlotConstructor[];
static create(value?: unknown): HTMLElement;
static formats(domNode: HTMLElement, scroll: Root): any;
protected attributes: AttributorStore;
constructor(scroll: Root, domNode: Node);
format(name: string, value: any): void;
formats(): {
[index: string]: any;
};
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
replaceWith(name: string | Blot, value?: any): Blot;
update(mutations: MutationRecord[], context: {
[key: string]: any;
}): void;
}
/**
* Blots are the basic building blocks of a Parchment document.
*
* Several basic implementations such as Block, Inline, and Embed are provided.
* In general you will want to extend one of these, instead of building from scratch.
* After implementation, blots need to be registered before usage.
*
* At the very minimum a Blot must be named with a static blotName and associated with either a tagName or className.
* If a Blot is defined with both a tag and class, the class takes precedence, but the tag may be used as a fallback.
* Blots must also have a scope, which determine if it is inline or block.
*/
export declare interface Blot extends LinkedNode {
scroll: Root;
parent: Parent;
prev: Blot | null;
next: Blot | null;
domNode: Node;
statics: BlotConstructor;
attach(): void;
clone(): Blot;
detach(): void;
isolate(index: number, length: number): Blot;
/**
* For leaves, length of blot's value()
* For parents, sum of children's values
*/
length(): number;
/**
* Returns offset between this blot and an ancestor's
*/
offset(root?: Blot): number;
remove(): void;
replaceWith(name: string, value: any): Blot;
replaceWith(replacement: Blot): Blot;
split(index: number, force?: boolean): Blot | null;
wrap(name: string, value?: any): Parent;
wrap(wrapper: Parent): Parent;
deleteAt(index: number, length: number): void;
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
/**
* Called after update cycle completes. Cannot change the value or length
* of the document, and any DOM operation must reduce complexity of the DOM
* tree. A shared context object is passed through all blots.
*/
optimize(context: {
[key: string]: any;
}): void;
optimize(mutations: MutationRecord[], context: {
[key: string]: any;
}): void;
/**
* Called when blot changes, with the mutation records of its change.
* Internal records of the blot values can be updated, and modifications of
* the blot itself is permitted. Can be trigger from user change or API call.
* A shared context object is passed through all blots.
*/
update(mutations: MutationRecord[], context: {
[key: string]: any;
}): void;
}
export declare interface BlotConstructor {
new (...args: any[]): Blot;
/**
* Creates corresponding DOM node
*/
create(value?: any): Node;
blotName: string;
tagName: string | string[];
scope: Scope;
className?: string;
requiredContainer?: BlotConstructor;
allowedChildren?: BlotConstructor[];
defaultChild?: BlotConstructor;
}
export declare class ClassAttributor extends Attributor {
static keys(node: HTMLElement): string[];
add(node: HTMLElement, value: any): boolean;
remove(node: HTMLElement): void;
value(node: HTMLElement): any;
}
export declare class ContainerBlot extends ParentBlot {
static blotName: string;
static scope: Scope;
static tagName: string | string[];
prev: BlockBlot | ContainerBlot | null;
next: BlockBlot | ContainerBlot | null;
checkMerge(): boolean;
deleteAt(index: number, length: number): void;
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
optimize(context: {
[key: string]: any;
}): void;
}
export declare class EmbedBlot extends LeafBlot implements Formattable {
static formats(_domNode: HTMLElement, _scroll: Root): any;
format(name: string, value: any): void;
formatAt(index: number, length: number, name: string, value: any): void;
formats(): {
[index: string]: any;
};
}
export declare interface Formattable extends Blot {
/**
* Apply format to blot. Should not pass onto child or other blot.
*/
format(name: string, value: any): void;
/**
* Return formats represented by blot, including from Attributors.
*/
formats(): {
[index: string]: any;
};
}
export declare class InlineBlot extends ParentBlot implements Formattable {
static allowedChildren: BlotConstructor[];
static blotName: string;
static scope: Scope;
static tagName: string | string[];
static create(value?: unknown): HTMLElement;
static formats(domNode: HTMLElement, scroll: Root): any;
protected attributes: AttributorStore;
constructor(scroll: Root, domNode: Node);
format(name: string, value: any): void;
formats(): {
[index: string]: any;
};
formatAt(index: number, length: number, name: string, value: any): void;
optimize(context: {
[key: string]: any;
}): void;
replaceWith(name: string | Blot, value?: any): Blot;
update(mutations: MutationRecord[], context: {
[key: string]: any;
}): void;
wrap(name: string | Parent, value?: any): Parent;
}
export declare interface Leaf extends Blot {
index(node: Node, offset: number): number;
position(index: number, inclusive: boolean): [Node, number];
value(): any;
}
export declare class LeafBlot extends ShadowBlot implements Leaf {
static scope: Scope;
/**
* Returns the value represented by domNode if it is this Blot's type
* No checking that domNode can represent this Blot type is required so
* applications needing it should check externally before calling.
*/
static value(_domNode: Node): any;
/**
* Given location represented by node and offset from DOM Selection Range,
* return index to that location.
*/
index(node: Node, offset: number): number;
/**
* Given index to location within blot, return node and offset representing
* that location, consumable by DOM Selection Range
*/
position(index: number, _inclusive?: boolean): [Node, number];
/**
* Return value represented by this blot
* Should not change without interaction from API or
* user change detectable by update()
*/
value(): any;
}
export declare class LinkedList<T extends LinkedNode> {
head: T | null;
tail: T | null;
length: number;
constructor();
append(...nodes: T[]): void;
at(index: number): T | null;
contains(node: T): boolean;
indexOf(node: T): number;
insertBefore(node: T | null, refNode: T | null): void;
offset(target: T): number;
remove(node: T): void;
iterator(curNode?: T | null): () => T | null;
find(index: number, inclusive?: boolean): [T | null, number];
forEach(callback: (cur: T) => void): void;
forEachAt(index: number, length: number, callback: (cur: T, offset: number, length: number) => void): void;
map(callback: (cur: T) => any): any[];
reduce<M>(callback: (memo: M, cur: T) => M, memo: M): M;
}
export declare interface LinkedNode {
prev: LinkedNode | null;
next: LinkedNode | null;
length(): number;
}
export declare interface Parent extends Blot {
children: LinkedList<Blot>;
domNode: HTMLElement;
appendChild(child: Blot): void;
descendant<T>(type: new () => T, index: number): [T, number];
descendant<T>(matcher: (blot: Blot) => boolean, index: number): [T, number];
descendants<T>(type: new () => T, index: number, length: number): T[];
descendants<T>(matcher: (blot: Blot) => boolean, index: number, length: number): T[];
insertBefore(child: Blot, refNode?: Blot | null): void;
moveChildren(parent: Parent, refNode?: Blot | null): void;
path(index: number, inclusive?: boolean): [Blot, number][];
removeChild(child: Blot): void;
unwrap(): void;
}
export declare class ParentBlot extends ShadowBlot implements Parent {
/**
* Whitelist array of Blots that can be direct children.
*/
static allowedChildren?: BlotConstructor[];
/**
* Default child blot to be inserted if this blot becomes empty.
*/
static defaultChild?: BlotConstructor;
static uiClass: string;
children: LinkedList<Blot>;
domNode: HTMLElement;
uiNode: HTMLElement | null;
constructor(scroll: Root, domNode: Node);
appendChild(other: Blot): void;
attach(): void;
attachUI(node: HTMLElement): void;
/**
* Called during construction, should fill its own children LinkedList.
*/
build(): void;
deleteAt(index: number, length: number): void;
descendant<T extends Blot>(criteria: new (...args: any[]) => T, index: number): [T | null, number];
descendant(criteria: (blot: Blot) => boolean, index: number): [Blot | null, number];
descendants<T extends Blot>(criteria: new (...args: any[]) => T, index?: number, length?: number): T[];
descendants(criteria: (blot: Blot) => boolean, index?: number, length?: number): Blot[];
detach(): void;
enforceAllowedChildren(): void;
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
insertBefore(childBlot: Blot, refBlot?: Blot | null): void;
length(): number;
moveChildren(targetParent: Parent, refNode?: Blot | null): void;
optimize(context?: {
[key: string]: any;
}): void;
path(index: number, inclusive?: boolean): [Blot, number][];
removeChild(child: Blot): void;
replaceWith(name: string | Blot, value?: any): Blot;
split(index: number, force?: boolean): Blot | null;
splitAfter(child: Blot): Parent;
unwrap(): void;
update(mutations: MutationRecord[], _context: {
[key: string]: any;
}): void;
}
export declare class Registry implements RegistryInterface {
static blots: WeakMap<Node, Blot>;
static find(node?: Node | null, bubble?: boolean): Blot | null;
private attributes;
private classes;
private tags;
private types;
create(scroll: Root, input: Node | string | Scope, value?: any): Blot;
find(node: Node | null, bubble?: boolean): Blot | null;
query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null;
register(...definitions: RegistryDefinition[]): RegistryDefinition[];
}
export declare type RegistryDefinition = Attributor | BlotConstructor;
export declare interface RegistryInterface {
create(scroll: Root, input: Node | string | Scope, value?: any): Blot;
query(query: string | Node | Scope, scope: Scope): RegistryDefinition | null;
register(...definitions: any[]): any;
}
export declare interface Root extends Parent {
create(input: Node | string | Scope, value?: any): Blot;
find(node: Node | null, bubble?: boolean): Blot | null;
query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null;
}
export declare enum Scope {
TYPE = 3,// 0011 Lower two bits
LEVEL = 12,// 1100 Higher two bits
ATTRIBUTE = 13,// 1101
BLOT = 14,// 1110
INLINE = 7,// 0111
BLOCK = 11,// 1011
BLOCK_BLOT = 10,// 1010
INLINE_BLOT = 6,// 0110
BLOCK_ATTRIBUTE = 9,// 1001
INLINE_ATTRIBUTE = 5,// 0101
ANY = 15
}
export declare class ScrollBlot extends ParentBlot implements Root {
registry: Registry;
static blotName: string;
static defaultChild: typeof BlockBlot;
static allowedChildren: BlotConstructor[];
static scope: Scope;
static tagName: string;
observer: MutationObserver;
constructor(registry: Registry, node: HTMLDivElement);
create(input: Node | string | Scope, value?: any): Blot;
find(node: Node | null, bubble?: boolean): Blot | null;
query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null;
register(...definitions: RegistryDefinition[]): RegistryDefinition[];
build(): void;
detach(): void;
deleteAt(index: number, length: number): void;
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
optimize(context?: {
[key: string]: any;
}): void;
optimize(mutations: MutationRecord[], context: {
[key: string]: any;
}): void;
update(mutations?: MutationRecord[], context?: {
[key: string]: any;
}): void;
}
export declare class ShadowBlot implements Blot {
scroll: Root;
domNode: Node;
static blotName: string;
static className: string;
static requiredContainer: BlotConstructor;
static scope: Scope;
static tagName: string | string[];
static create(rawValue?: unknown): Node;
prev: Blot | null;
next: Blot | null;
parent: Parent;
get statics(): any;
constructor(scroll: Root, domNode: Node);
attach(): void;
clone(): Blot;
detach(): void;
deleteAt(index: number, length: number): void;
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
isolate(index: number, length: number): Blot;
length(): number;
offset(root?: Blot): number;
optimize(_context?: {
[key: string]: any;
}): void;
remove(): void;
replaceWith(name: string | Blot, value?: any): Blot;
split(index: number, _force?: boolean): Blot | null;
update(_mutations: MutationRecord[], _context: {
[key: string]: any;
}): void;
wrap(name: string | Parent, value?: any): Parent;
}
export declare class StyleAttributor extends Attributor {
static keys(node: HTMLElement): string[];
add(node: HTMLElement, value: any): boolean;
remove(node: HTMLElement): void;
value(node: HTMLElement): any;
}
export declare class TextBlot extends LeafBlot implements Leaf {
static readonly blotName = "text";
static scope: Scope;
static create(value: string): Text;
static value(domNode: Text): string;
domNode: Text;
protected text: string;
constructor(scroll: Root, node: Node);
deleteAt(index: number, length: number): void;
index(node: Node, offset: number): number;
insertAt(index: number, value: string, def?: any): void;
length(): number;
optimize(context: {
[key: string]: any;
}): void;
position(index: number, _inclusive?: boolean): [Node, number];
split(index: number, force?: boolean): Blot | null;
update(mutations: MutationRecord[], _context: {
[key: string]: any;
}): void;
value(): string;
}
export { }

View File

@@ -0,0 +1,847 @@
var Scope = /* @__PURE__ */ ((Scope2) => (Scope2[Scope2.TYPE = 3] = "TYPE", Scope2[Scope2.LEVEL = 12] = "LEVEL", Scope2[Scope2.ATTRIBUTE = 13] = "ATTRIBUTE", Scope2[Scope2.BLOT = 14] = "BLOT", Scope2[Scope2.INLINE = 7] = "INLINE", Scope2[Scope2.BLOCK = 11] = "BLOCK", Scope2[Scope2.BLOCK_BLOT = 10] = "BLOCK_BLOT", Scope2[Scope2.INLINE_BLOT = 6] = "INLINE_BLOT", Scope2[Scope2.BLOCK_ATTRIBUTE = 9] = "BLOCK_ATTRIBUTE", Scope2[Scope2.INLINE_ATTRIBUTE = 5] = "INLINE_ATTRIBUTE", Scope2[Scope2.ANY = 15] = "ANY", Scope2))(Scope || {});
class Attributor {
constructor(attrName, keyName, options = {}) {
this.attrName = attrName, this.keyName = keyName;
const attributeBit = Scope.TYPE & Scope.ATTRIBUTE;
this.scope = options.scope != null ? (
// Ignore type bits, force attribute bit
options.scope & Scope.LEVEL | attributeBit
) : Scope.ATTRIBUTE, options.whitelist != null && (this.whitelist = options.whitelist);
}
static keys(node) {
return Array.from(node.attributes).map((item) => item.name);
}
add(node, value) {
return this.canAdd(node, value) ? (node.setAttribute(this.keyName, value), !0) : !1;
}
canAdd(_node, value) {
return this.whitelist == null ? !0 : typeof value == "string" ? this.whitelist.indexOf(value.replace(/["']/g, "")) > -1 : this.whitelist.indexOf(value) > -1;
}
remove(node) {
node.removeAttribute(this.keyName);
}
value(node) {
const value = node.getAttribute(this.keyName);
return this.canAdd(node, value) && value ? value : "";
}
}
class ParchmentError extends Error {
constructor(message) {
message = "[Parchment] " + message, super(message), this.message = message, this.name = this.constructor.name;
}
}
const _Registry = class _Registry {
constructor() {
this.attributes = {}, this.classes = {}, this.tags = {}, this.types = {};
}
static find(node, bubble = !1) {
if (node == null)
return null;
if (this.blots.has(node))
return this.blots.get(node) || null;
if (bubble) {
let parentNode = null;
try {
parentNode = node.parentNode;
} catch {
return null;
}
return this.find(parentNode, bubble);
}
return null;
}
create(scroll, input, value) {
const match2 = this.query(input);
if (match2 == null)
throw new ParchmentError(`Unable to create ${input} blot`);
const blotClass = match2, node = (
// @ts-expect-error Fix me later
input instanceof Node || input.nodeType === Node.TEXT_NODE ? input : blotClass.create(value)
), blot = new blotClass(scroll, node, value);
return _Registry.blots.set(blot.domNode, blot), blot;
}
find(node, bubble = !1) {
return _Registry.find(node, bubble);
}
query(query, scope = Scope.ANY) {
let match2;
return typeof query == "string" ? match2 = this.types[query] || this.attributes[query] : query instanceof Text || query.nodeType === Node.TEXT_NODE ? match2 = this.types.text : typeof query == "number" ? query & Scope.LEVEL & Scope.BLOCK ? match2 = this.types.block : query & Scope.LEVEL & Scope.INLINE && (match2 = this.types.inline) : query instanceof Element && ((query.getAttribute("class") || "").split(/\s+/).some((name) => (match2 = this.classes[name], !!match2)), match2 = match2 || this.tags[query.tagName]), match2 == null ? null : "scope" in match2 && scope & Scope.LEVEL & match2.scope && scope & Scope.TYPE & match2.scope ? match2 : null;
}
register(...definitions) {
return definitions.map((definition) => {
const isBlot = "blotName" in definition, isAttr = "attrName" in definition;
if (!isBlot && !isAttr)
throw new ParchmentError("Invalid definition");
if (isBlot && definition.blotName === "abstract")
throw new ParchmentError("Cannot register abstract class");
const key = isBlot ? definition.blotName : isAttr ? definition.attrName : void 0;
return this.types[key] = definition, isAttr ? typeof definition.keyName == "string" && (this.attributes[definition.keyName] = definition) : isBlot && (definition.className && (this.classes[definition.className] = definition), definition.tagName && (Array.isArray(definition.tagName) ? definition.tagName = definition.tagName.map((tagName) => tagName.toUpperCase()) : definition.tagName = definition.tagName.toUpperCase(), (Array.isArray(definition.tagName) ? definition.tagName : [definition.tagName]).forEach((tag) => {
(this.tags[tag] == null || definition.className == null) && (this.tags[tag] = definition);
}))), definition;
});
}
};
_Registry.blots = /* @__PURE__ */ new WeakMap();
let Registry = _Registry;
function match(node, prefix) {
return (node.getAttribute("class") || "").split(/\s+/).filter((name) => name.indexOf(`${prefix}-`) === 0);
}
class ClassAttributor extends Attributor {
static keys(node) {
return (node.getAttribute("class") || "").split(/\s+/).map((name) => name.split("-").slice(0, -1).join("-"));
}
add(node, value) {
return this.canAdd(node, value) ? (this.remove(node), node.classList.add(`${this.keyName}-${value}`), !0) : !1;
}
remove(node) {
match(node, this.keyName).forEach((name) => {
node.classList.remove(name);
}), node.classList.length === 0 && node.removeAttribute("class");
}
value(node) {
const value = (match(node, this.keyName)[0] || "").slice(this.keyName.length + 1);
return this.canAdd(node, value) ? value : "";
}
}
const ClassAttributor$1 = ClassAttributor;
function camelize(name) {
const parts = name.split("-"), rest = parts.slice(1).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
return parts[0] + rest;
}
class StyleAttributor extends Attributor {
static keys(node) {
return (node.getAttribute("style") || "").split(";").map((value) => value.split(":")[0].trim());
}
add(node, value) {
return this.canAdd(node, value) ? (node.style[camelize(this.keyName)] = value, !0) : !1;
}
remove(node) {
node.style[camelize(this.keyName)] = "", node.getAttribute("style") || node.removeAttribute("style");
}
value(node) {
const value = node.style[camelize(this.keyName)];
return this.canAdd(node, value) ? value : "";
}
}
const StyleAttributor$1 = StyleAttributor;
class AttributorStore {
constructor(domNode) {
this.attributes = {}, this.domNode = domNode, this.build();
}
attribute(attribute, value) {
value ? attribute.add(this.domNode, value) && (attribute.value(this.domNode) != null ? this.attributes[attribute.attrName] = attribute : delete this.attributes[attribute.attrName]) : (attribute.remove(this.domNode), delete this.attributes[attribute.attrName]);
}
build() {
this.attributes = {};
const blot = Registry.find(this.domNode);
if (blot == null)
return;
const attributes = Attributor.keys(this.domNode), classes = ClassAttributor$1.keys(this.domNode), styles = StyleAttributor$1.keys(this.domNode);
attributes.concat(classes).concat(styles).forEach((name) => {
const attr = blot.scroll.query(name, Scope.ATTRIBUTE);
attr instanceof Attributor && (this.attributes[attr.attrName] = attr);
});
}
copy(target) {
Object.keys(this.attributes).forEach((key) => {
const value = this.attributes[key].value(this.domNode);
target.format(key, value);
});
}
move(target) {
this.copy(target), Object.keys(this.attributes).forEach((key) => {
this.attributes[key].remove(this.domNode);
}), this.attributes = {};
}
values() {
return Object.keys(this.attributes).reduce(
(attributes, name) => (attributes[name] = this.attributes[name].value(this.domNode), attributes),
{}
);
}
}
const AttributorStore$1 = AttributorStore, _ShadowBlot = class _ShadowBlot {
constructor(scroll, domNode) {
this.scroll = scroll, this.domNode = domNode, Registry.blots.set(domNode, this), this.prev = null, this.next = null;
}
static create(rawValue) {
if (this.tagName == null)
throw new ParchmentError("Blot definition missing tagName");
let node, value;
return Array.isArray(this.tagName) ? (typeof rawValue == "string" ? (value = rawValue.toUpperCase(), parseInt(value, 10).toString() === value && (value = parseInt(value, 10))) : typeof rawValue == "number" && (value = rawValue), typeof value == "number" ? node = document.createElement(this.tagName[value - 1]) : value && this.tagName.indexOf(value) > -1 ? node = document.createElement(value) : node = document.createElement(this.tagName[0])) : node = document.createElement(this.tagName), this.className && node.classList.add(this.className), node;
}
// Hack for accessing inherited static methods
get statics() {
return this.constructor;
}
attach() {
}
clone() {
const domNode = this.domNode.cloneNode(!1);
return this.scroll.create(domNode);
}
detach() {
this.parent != null && this.parent.removeChild(this), Registry.blots.delete(this.domNode);
}
deleteAt(index, length) {
this.isolate(index, length).remove();
}
formatAt(index, length, name, value) {
const blot = this.isolate(index, length);
if (this.scroll.query(name, Scope.BLOT) != null && value)
blot.wrap(name, value);
else if (this.scroll.query(name, Scope.ATTRIBUTE) != null) {
const parent = this.scroll.create(this.statics.scope);
blot.wrap(parent), parent.format(name, value);
}
}
insertAt(index, value, def) {
const blot = def == null ? this.scroll.create("text", value) : this.scroll.create(value, def), ref = this.split(index);
this.parent.insertBefore(blot, ref || void 0);
}
isolate(index, length) {
const target = this.split(index);
if (target == null)
throw new Error("Attempt to isolate at end");
return target.split(length), target;
}
length() {
return 1;
}
offset(root = this.parent) {
return this.parent == null || this === root ? 0 : this.parent.children.offset(this) + this.parent.offset(root);
}
optimize(_context) {
this.statics.requiredContainer && !(this.parent instanceof this.statics.requiredContainer) && this.wrap(this.statics.requiredContainer.blotName);
}
remove() {
this.domNode.parentNode != null && this.domNode.parentNode.removeChild(this.domNode), this.detach();
}
replaceWith(name, value) {
const replacement = typeof name == "string" ? this.scroll.create(name, value) : name;
return this.parent != null && (this.parent.insertBefore(replacement, this.next || void 0), this.remove()), replacement;
}
split(index, _force) {
return index === 0 ? this : this.next;
}
update(_mutations, _context) {
}
wrap(name, value) {
const wrapper = typeof name == "string" ? this.scroll.create(name, value) : name;
if (this.parent != null && this.parent.insertBefore(wrapper, this.next || void 0), typeof wrapper.appendChild != "function")
throw new ParchmentError(`Cannot wrap ${name}`);
return wrapper.appendChild(this), wrapper;
}
};
_ShadowBlot.blotName = "abstract";
let ShadowBlot = _ShadowBlot;
const _LeafBlot = class _LeafBlot extends ShadowBlot {
/**
* Returns the value represented by domNode if it is this Blot's type
* No checking that domNode can represent this Blot type is required so
* applications needing it should check externally before calling.
*/
static value(_domNode) {
return !0;
}
/**
* Given location represented by node and offset from DOM Selection Range,
* return index to that location.
*/
index(node, offset) {
return this.domNode === node || this.domNode.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY ? Math.min(offset, 1) : -1;
}
/**
* Given index to location within blot, return node and offset representing
* that location, consumable by DOM Selection Range
*/
position(index, _inclusive) {
let offset = Array.from(this.parent.domNode.childNodes).indexOf(this.domNode);
return index > 0 && (offset += 1), [this.parent.domNode, offset];
}
/**
* Return value represented by this blot
* Should not change without interaction from API or
* user change detectable by update()
*/
value() {
return {
[this.statics.blotName]: this.statics.value(this.domNode) || !0
};
}
};
_LeafBlot.scope = Scope.INLINE_BLOT;
let LeafBlot = _LeafBlot;
const LeafBlot$1 = LeafBlot;
class LinkedList {
constructor() {
this.head = null, this.tail = null, this.length = 0;
}
append(...nodes) {
if (this.insertBefore(nodes[0], null), nodes.length > 1) {
const rest = nodes.slice(1);
this.append(...rest);
}
}
at(index) {
const next = this.iterator();
let cur = next();
for (; cur && index > 0; )
index -= 1, cur = next();
return cur;
}
contains(node) {
const next = this.iterator();
let cur = next();
for (; cur; ) {
if (cur === node)
return !0;
cur = next();
}
return !1;
}
indexOf(node) {
const next = this.iterator();
let cur = next(), index = 0;
for (; cur; ) {
if (cur === node)
return index;
index += 1, cur = next();
}
return -1;
}
insertBefore(node, refNode) {
node != null && (this.remove(node), node.next = refNode, refNode != null ? (node.prev = refNode.prev, refNode.prev != null && (refNode.prev.next = node), refNode.prev = node, refNode === this.head && (this.head = node)) : this.tail != null ? (this.tail.next = node, node.prev = this.tail, this.tail = node) : (node.prev = null, this.head = this.tail = node), this.length += 1);
}
offset(target) {
let index = 0, cur = this.head;
for (; cur != null; ) {
if (cur === target)
return index;
index += cur.length(), cur = cur.next;
}
return -1;
}
remove(node) {
this.contains(node) && (node.prev != null && (node.prev.next = node.next), node.next != null && (node.next.prev = node.prev), node === this.head && (this.head = node.next), node === this.tail && (this.tail = node.prev), this.length -= 1);
}
iterator(curNode = this.head) {
return () => {
const ret = curNode;
return curNode != null && (curNode = curNode.next), ret;
};
}
find(index, inclusive = !1) {
const next = this.iterator();
let cur = next();
for (; cur; ) {
const length = cur.length();
if (index < length || inclusive && index === length && (cur.next == null || cur.next.length() !== 0))
return [cur, index];
index -= length, cur = next();
}
return [null, 0];
}
forEach(callback) {
const next = this.iterator();
let cur = next();
for (; cur; )
callback(cur), cur = next();
}
forEachAt(index, length, callback) {
if (length <= 0)
return;
const [startNode, offset] = this.find(index);
let curIndex = index - offset;
const next = this.iterator(startNode);
let cur = next();
for (; cur && curIndex < index + length; ) {
const curLength = cur.length();
index > curIndex ? callback(
cur,
index - curIndex,
Math.min(length, curIndex + curLength - index)
) : callback(cur, 0, Math.min(curLength, index + length - curIndex)), curIndex += curLength, cur = next();
}
}
map(callback) {
return this.reduce((memo, cur) => (memo.push(callback(cur)), memo), []);
}
reduce(callback, memo) {
const next = this.iterator();
let cur = next();
for (; cur; )
memo = callback(memo, cur), cur = next();
return memo;
}
}
function makeAttachedBlot(node, scroll) {
const found = scroll.find(node);
if (found)
return found;
try {
return scroll.create(node);
} catch {
const blot = scroll.create(Scope.INLINE);
return Array.from(node.childNodes).forEach((child) => {
blot.domNode.appendChild(child);
}), node.parentNode && node.parentNode.replaceChild(blot.domNode, node), blot.attach(), blot;
}
}
const _ParentBlot = class _ParentBlot extends ShadowBlot {
constructor(scroll, domNode) {
super(scroll, domNode), this.uiNode = null, this.build();
}
appendChild(other) {
this.insertBefore(other);
}
attach() {
super.attach(), this.children.forEach((child) => {
child.attach();
});
}
attachUI(node) {
this.uiNode != null && this.uiNode.remove(), this.uiNode = node, _ParentBlot.uiClass && this.uiNode.classList.add(_ParentBlot.uiClass), this.uiNode.setAttribute("contenteditable", "false"), this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
}
/**
* Called during construction, should fill its own children LinkedList.
*/
build() {
this.children = new LinkedList(), Array.from(this.domNode.childNodes).filter((node) => node !== this.uiNode).reverse().forEach((node) => {
try {
const child = makeAttachedBlot(node, this.scroll);
this.insertBefore(child, this.children.head || void 0);
} catch (err) {
if (err instanceof ParchmentError)
return;
throw err;
}
});
}
deleteAt(index, length) {
if (index === 0 && length === this.length())
return this.remove();
this.children.forEachAt(index, length, (child, offset, childLength) => {
child.deleteAt(offset, childLength);
});
}
descendant(criteria, index = 0) {
const [child, offset] = this.children.find(index);
return criteria.blotName == null && criteria(child) || criteria.blotName != null && child instanceof criteria ? [child, offset] : child instanceof _ParentBlot ? child.descendant(criteria, offset) : [null, -1];
}
descendants(criteria, index = 0, length = Number.MAX_VALUE) {
let descendants = [], lengthLeft = length;
return this.children.forEachAt(
index,
length,
(child, childIndex, childLength) => {
(criteria.blotName == null && criteria(child) || criteria.blotName != null && child instanceof criteria) && descendants.push(child), child instanceof _ParentBlot && (descendants = descendants.concat(
child.descendants(criteria, childIndex, lengthLeft)
)), lengthLeft -= childLength;
}
), descendants;
}
detach() {
this.children.forEach((child) => {
child.detach();
}), super.detach();
}
enforceAllowedChildren() {
let done = !1;
this.children.forEach((child) => {
done || this.statics.allowedChildren.some(
(def) => child instanceof def
) || (child.statics.scope === Scope.BLOCK_BLOT ? (child.next != null && this.splitAfter(child), child.prev != null && this.splitAfter(child.prev), child.parent.unwrap(), done = !0) : child instanceof _ParentBlot ? child.unwrap() : child.remove());
});
}
formatAt(index, length, name, value) {
this.children.forEachAt(index, length, (child, offset, childLength) => {
child.formatAt(offset, childLength, name, value);
});
}
insertAt(index, value, def) {
const [child, offset] = this.children.find(index);
if (child)
child.insertAt(offset, value, def);
else {
const blot = def == null ? this.scroll.create("text", value) : this.scroll.create(value, def);
this.appendChild(blot);
}
}
insertBefore(childBlot, refBlot) {
childBlot.parent != null && childBlot.parent.children.remove(childBlot);
let refDomNode = null;
this.children.insertBefore(childBlot, refBlot || null), childBlot.parent = this, refBlot != null && (refDomNode = refBlot.domNode), (this.domNode.parentNode !== childBlot.domNode || this.domNode.nextSibling !== refDomNode) && this.domNode.insertBefore(childBlot.domNode, refDomNode), childBlot.attach();
}
length() {
return this.children.reduce((memo, child) => memo + child.length(), 0);
}
moveChildren(targetParent, refNode) {
this.children.forEach((child) => {
targetParent.insertBefore(child, refNode);
});
}
optimize(context) {
if (super.optimize(context), this.enforceAllowedChildren(), this.uiNode != null && this.uiNode !== this.domNode.firstChild && this.domNode.insertBefore(this.uiNode, this.domNode.firstChild), this.children.length === 0)
if (this.statics.defaultChild != null) {
const child = this.scroll.create(this.statics.defaultChild.blotName);
this.appendChild(child);
} else
this.remove();
}
path(index, inclusive = !1) {
const [child, offset] = this.children.find(index, inclusive), position = [[this, index]];
return child instanceof _ParentBlot ? position.concat(child.path(offset, inclusive)) : (child != null && position.push([child, offset]), position);
}
removeChild(child) {
this.children.remove(child);
}
replaceWith(name, value) {
const replacement = typeof name == "string" ? this.scroll.create(name, value) : name;
return replacement instanceof _ParentBlot && this.moveChildren(replacement), super.replaceWith(replacement);
}
split(index, force = !1) {
if (!force) {
if (index === 0)
return this;
if (index === this.length())
return this.next;
}
const after = this.clone();
return this.parent && this.parent.insertBefore(after, this.next || void 0), this.children.forEachAt(index, this.length(), (child, offset, _length) => {
const split = child.split(offset, force);
split != null && after.appendChild(split);
}), after;
}
splitAfter(child) {
const after = this.clone();
for (; child.next != null; )
after.appendChild(child.next);
return this.parent && this.parent.insertBefore(after, this.next || void 0), after;
}
unwrap() {
this.parent && this.moveChildren(this.parent, this.next || void 0), this.remove();
}
update(mutations, _context) {
const addedNodes = [], removedNodes = [];
mutations.forEach((mutation) => {
mutation.target === this.domNode && mutation.type === "childList" && (addedNodes.push(...mutation.addedNodes), removedNodes.push(...mutation.removedNodes));
}), removedNodes.forEach((node) => {
if (node.parentNode != null && // @ts-expect-error Fix me later
node.tagName !== "IFRAME" && document.body.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY)
return;
const blot = this.scroll.find(node);
blot != null && (blot.domNode.parentNode == null || blot.domNode.parentNode === this.domNode) && blot.detach();
}), addedNodes.filter((node) => node.parentNode === this.domNode && node !== this.uiNode).sort((a, b) => a === b ? 0 : a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? 1 : -1).forEach((node) => {
let refBlot = null;
node.nextSibling != null && (refBlot = this.scroll.find(node.nextSibling));
const blot = makeAttachedBlot(node, this.scroll);
(blot.next !== refBlot || blot.next == null) && (blot.parent != null && blot.parent.removeChild(this), this.insertBefore(blot, refBlot || void 0));
}), this.enforceAllowedChildren();
}
};
_ParentBlot.uiClass = "";
let ParentBlot = _ParentBlot;
const ParentBlot$1 = ParentBlot;
function isEqual(obj1, obj2) {
if (Object.keys(obj1).length !== Object.keys(obj2).length)
return !1;
for (const prop in obj1)
if (obj1[prop] !== obj2[prop])
return !1;
return !0;
}
const _InlineBlot = class _InlineBlot extends ParentBlot$1 {
static create(value) {
return super.create(value);
}
static formats(domNode, scroll) {
const match2 = scroll.query(_InlineBlot.blotName);
if (!(match2 != null && domNode.tagName === match2.tagName)) {
if (typeof this.tagName == "string")
return !0;
if (Array.isArray(this.tagName))
return domNode.tagName.toLowerCase();
}
}
constructor(scroll, domNode) {
super(scroll, domNode), this.attributes = new AttributorStore$1(this.domNode);
}
format(name, value) {
if (name === this.statics.blotName && !value)
this.children.forEach((child) => {
child instanceof _InlineBlot || (child = child.wrap(_InlineBlot.blotName, !0)), this.attributes.copy(child);
}), this.unwrap();
else {
const format = this.scroll.query(name, Scope.INLINE);
if (format == null)
return;
format instanceof Attributor ? this.attributes.attribute(format, value) : value && (name !== this.statics.blotName || this.formats()[name] !== value) && this.replaceWith(name, value);
}
}
formats() {
const formats = this.attributes.values(), format = this.statics.formats(this.domNode, this.scroll);
return format != null && (formats[this.statics.blotName] = format), formats;
}
formatAt(index, length, name, value) {
this.formats()[name] != null || this.scroll.query(name, Scope.ATTRIBUTE) ? this.isolate(index, length).format(name, value) : super.formatAt(index, length, name, value);
}
optimize(context) {
super.optimize(context);
const formats = this.formats();
if (Object.keys(formats).length === 0)
return this.unwrap();
const next = this.next;
next instanceof _InlineBlot && next.prev === this && isEqual(formats, next.formats()) && (next.moveChildren(this), next.remove());
}
replaceWith(name, value) {
const replacement = super.replaceWith(name, value);
return this.attributes.copy(replacement), replacement;
}
update(mutations, context) {
super.update(mutations, context), mutations.some(
(mutation) => mutation.target === this.domNode && mutation.type === "attributes"
) && this.attributes.build();
}
wrap(name, value) {
const wrapper = super.wrap(name, value);
return wrapper instanceof _InlineBlot && this.attributes.move(wrapper), wrapper;
}
};
_InlineBlot.allowedChildren = [_InlineBlot, LeafBlot$1], _InlineBlot.blotName = "inline", _InlineBlot.scope = Scope.INLINE_BLOT, _InlineBlot.tagName = "SPAN";
let InlineBlot = _InlineBlot;
const InlineBlot$1 = InlineBlot, _BlockBlot = class _BlockBlot extends ParentBlot$1 {
static create(value) {
return super.create(value);
}
static formats(domNode, scroll) {
const match2 = scroll.query(_BlockBlot.blotName);
if (!(match2 != null && domNode.tagName === match2.tagName)) {
if (typeof this.tagName == "string")
return !0;
if (Array.isArray(this.tagName))
return domNode.tagName.toLowerCase();
}
}
constructor(scroll, domNode) {
super(scroll, domNode), this.attributes = new AttributorStore$1(this.domNode);
}
format(name, value) {
const format = this.scroll.query(name, Scope.BLOCK);
format != null && (format instanceof Attributor ? this.attributes.attribute(format, value) : name === this.statics.blotName && !value ? this.replaceWith(_BlockBlot.blotName) : value && (name !== this.statics.blotName || this.formats()[name] !== value) && this.replaceWith(name, value));
}
formats() {
const formats = this.attributes.values(), format = this.statics.formats(this.domNode, this.scroll);
return format != null && (formats[this.statics.blotName] = format), formats;
}
formatAt(index, length, name, value) {
this.scroll.query(name, Scope.BLOCK) != null ? this.format(name, value) : super.formatAt(index, length, name, value);
}
insertAt(index, value, def) {
if (def == null || this.scroll.query(value, Scope.INLINE) != null)
super.insertAt(index, value, def);
else {
const after = this.split(index);
if (after != null) {
const blot = this.scroll.create(value, def);
after.parent.insertBefore(blot, after);
} else
throw new Error("Attempt to insertAt after block boundaries");
}
}
replaceWith(name, value) {
const replacement = super.replaceWith(name, value);
return this.attributes.copy(replacement), replacement;
}
update(mutations, context) {
super.update(mutations, context), mutations.some(
(mutation) => mutation.target === this.domNode && mutation.type === "attributes"
) && this.attributes.build();
}
};
_BlockBlot.blotName = "block", _BlockBlot.scope = Scope.BLOCK_BLOT, _BlockBlot.tagName = "P", _BlockBlot.allowedChildren = [
InlineBlot$1,
_BlockBlot,
LeafBlot$1
];
let BlockBlot = _BlockBlot;
const BlockBlot$1 = BlockBlot, _ContainerBlot = class _ContainerBlot extends ParentBlot$1 {
checkMerge() {
return this.next !== null && this.next.statics.blotName === this.statics.blotName;
}
deleteAt(index, length) {
super.deleteAt(index, length), this.enforceAllowedChildren();
}
formatAt(index, length, name, value) {
super.formatAt(index, length, name, value), this.enforceAllowedChildren();
}
insertAt(index, value, def) {
super.insertAt(index, value, def), this.enforceAllowedChildren();
}
optimize(context) {
super.optimize(context), this.children.length > 0 && this.next != null && this.checkMerge() && (this.next.moveChildren(this), this.next.remove());
}
};
_ContainerBlot.blotName = "container", _ContainerBlot.scope = Scope.BLOCK_BLOT;
let ContainerBlot = _ContainerBlot;
const ContainerBlot$1 = ContainerBlot;
class EmbedBlot extends LeafBlot$1 {
static formats(_domNode, _scroll) {
}
format(name, value) {
super.formatAt(0, this.length(), name, value);
}
formatAt(index, length, name, value) {
index === 0 && length === this.length() ? this.format(name, value) : super.formatAt(index, length, name, value);
}
formats() {
return this.statics.formats(this.domNode, this.scroll);
}
}
const EmbedBlot$1 = EmbedBlot, OBSERVER_CONFIG = {
attributes: !0,
characterData: !0,
characterDataOldValue: !0,
childList: !0,
subtree: !0
}, MAX_OPTIMIZE_ITERATIONS = 100, _ScrollBlot = class _ScrollBlot extends ParentBlot$1 {
constructor(registry, node) {
super(null, node), this.registry = registry, this.scroll = this, this.build(), this.observer = new MutationObserver((mutations) => {
this.update(mutations);
}), this.observer.observe(this.domNode, OBSERVER_CONFIG), this.attach();
}
create(input, value) {
return this.registry.create(this, input, value);
}
find(node, bubble = !1) {
const blot = this.registry.find(node, bubble);
return blot ? blot.scroll === this ? blot : bubble ? this.find(blot.scroll.domNode.parentNode, !0) : null : null;
}
query(query, scope = Scope.ANY) {
return this.registry.query(query, scope);
}
register(...definitions) {
return this.registry.register(...definitions);
}
build() {
this.scroll != null && super.build();
}
detach() {
super.detach(), this.observer.disconnect();
}
deleteAt(index, length) {
this.update(), index === 0 && length === this.length() ? this.children.forEach((child) => {
child.remove();
}) : super.deleteAt(index, length);
}
formatAt(index, length, name, value) {
this.update(), super.formatAt(index, length, name, value);
}
insertAt(index, value, def) {
this.update(), super.insertAt(index, value, def);
}
optimize(mutations = [], context = {}) {
super.optimize(context);
const mutationsMap = context.mutationsMap || /* @__PURE__ */ new WeakMap();
let records = Array.from(this.observer.takeRecords());
for (; records.length > 0; )
mutations.push(records.pop());
const mark = (blot, markParent = !0) => {
blot == null || blot === this || blot.domNode.parentNode != null && (mutationsMap.has(blot.domNode) || mutationsMap.set(blot.domNode, []), markParent && mark(blot.parent));
}, optimize = (blot) => {
mutationsMap.has(blot.domNode) && (blot instanceof ParentBlot$1 && blot.children.forEach(optimize), mutationsMap.delete(blot.domNode), blot.optimize(context));
};
let remaining = mutations;
for (let i = 0; remaining.length > 0; i += 1) {
if (i >= MAX_OPTIMIZE_ITERATIONS)
throw new Error("[Parchment] Maximum optimize iterations reached");
for (remaining.forEach((mutation) => {
const blot = this.find(mutation.target, !0);
blot != null && (blot.domNode === mutation.target && (mutation.type === "childList" ? (mark(this.find(mutation.previousSibling, !1)), Array.from(mutation.addedNodes).forEach((node) => {
const child = this.find(node, !1);
mark(child, !1), child instanceof ParentBlot$1 && child.children.forEach((grandChild) => {
mark(grandChild, !1);
});
})) : mutation.type === "attributes" && mark(blot.prev)), mark(blot));
}), this.children.forEach(optimize), remaining = Array.from(this.observer.takeRecords()), records = remaining.slice(); records.length > 0; )
mutations.push(records.pop());
}
}
update(mutations, context = {}) {
mutations = mutations || this.observer.takeRecords();
const mutationsMap = /* @__PURE__ */ new WeakMap();
mutations.map((mutation) => {
const blot = this.find(mutation.target, !0);
return blot == null ? null : mutationsMap.has(blot.domNode) ? (mutationsMap.get(blot.domNode).push(mutation), null) : (mutationsMap.set(blot.domNode, [mutation]), blot);
}).forEach((blot) => {
blot != null && blot !== this && mutationsMap.has(blot.domNode) && blot.update(mutationsMap.get(blot.domNode) || [], context);
}), context.mutationsMap = mutationsMap, mutationsMap.has(this.domNode) && super.update(mutationsMap.get(this.domNode), context), this.optimize(mutations, context);
}
};
_ScrollBlot.blotName = "scroll", _ScrollBlot.defaultChild = BlockBlot$1, _ScrollBlot.allowedChildren = [BlockBlot$1, ContainerBlot$1], _ScrollBlot.scope = Scope.BLOCK_BLOT, _ScrollBlot.tagName = "DIV";
let ScrollBlot = _ScrollBlot;
const ScrollBlot$1 = ScrollBlot, _TextBlot = class _TextBlot extends LeafBlot$1 {
static create(value) {
return document.createTextNode(value);
}
static value(domNode) {
return domNode.data;
}
constructor(scroll, node) {
super(scroll, node), this.text = this.statics.value(this.domNode);
}
deleteAt(index, length) {
this.domNode.data = this.text = this.text.slice(0, index) + this.text.slice(index + length);
}
index(node, offset) {
return this.domNode === node ? offset : -1;
}
insertAt(index, value, def) {
def == null ? (this.text = this.text.slice(0, index) + value + this.text.slice(index), this.domNode.data = this.text) : super.insertAt(index, value, def);
}
length() {
return this.text.length;
}
optimize(context) {
super.optimize(context), this.text = this.statics.value(this.domNode), this.text.length === 0 ? this.remove() : this.next instanceof _TextBlot && this.next.prev === this && (this.insertAt(this.length(), this.next.value()), this.next.remove());
}
position(index, _inclusive = !1) {
return [this.domNode, index];
}
split(index, force = !1) {
if (!force) {
if (index === 0)
return this;
if (index === this.length())
return this.next;
}
const after = this.scroll.create(this.domNode.splitText(index));
return this.parent.insertBefore(after, this.next || void 0), this.text = this.statics.value(this.domNode), after;
}
update(mutations, _context) {
mutations.some((mutation) => mutation.type === "characterData" && mutation.target === this.domNode) && (this.text = this.statics.value(this.domNode));
}
value() {
return this.text;
}
};
_TextBlot.blotName = "text", _TextBlot.scope = Scope.INLINE_BLOT;
let TextBlot = _TextBlot;
const TextBlot$1 = TextBlot;
export {
Attributor,
AttributorStore$1 as AttributorStore,
BlockBlot$1 as BlockBlot,
ClassAttributor$1 as ClassAttributor,
ContainerBlot$1 as ContainerBlot,
EmbedBlot$1 as EmbedBlot,
InlineBlot$1 as InlineBlot,
LeafBlot$1 as LeafBlot,
ParentBlot$1 as ParentBlot,
Registry,
Scope,
ScrollBlot$1 as ScrollBlot,
StyleAttributor$1 as StyleAttributor,
TextBlot$1 as TextBlot
};
//# sourceMappingURL=parchment.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,53 @@
{
"name": "parchment",
"version": "3.0.0",
"description": "A document model for rich text editors",
"author": "Jason Chen <jhchen7@gmail.com>",
"homepage": "http://quilljs.com/docs/parchment",
"main": "./dist/parchment.js",
"types": "./dist/parchment.d.ts",
"type": "module",
"sideEffects": false,
"files": [
"tsconfig.json",
"dist",
"src"
],
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.1",
"@microsoft/api-extractor": "^7.42.3",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitest/browser": "^1.4.0",
"del-cli": "^5.1.0",
"eslint": "^8.46.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-require-extensions": "^0.1.3",
"eslint-plugin-tree-shaking": "^1.12.1",
"playwright": "1.42.1",
"prettier": "^3.2.5",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"vitest": "^1.4.0"
},
"prettier": {
"singleQuote": true
},
"license": "BSD-3-Clause",
"repository": "github:quilljs/parchment",
"scripts": {
"build": "npm run build:bundle && npm run build:types",
"build:bundle": "vite build",
"build:types": "tsc --emitDeclarationOnly && api-extractor run && del-cli dist/typings",
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
"prepare": "npm run build",
"test": "npm run test:unit",
"test:unit": "vitest --typecheck",
"test:pkg": "attw $(npm pack)"
},
"bugs": {
"url": "https://github.com/quilljs/parchment/issues"
}
}

View File

@@ -0,0 +1,62 @@
import Scope from '../scope.js';
export interface AttributorOptions {
scope?: Scope;
whitelist?: string[];
}
export default class Attributor {
public static keys(node: HTMLElement): string[] {
return Array.from(node.attributes).map((item: Attr) => item.name);
}
public scope: Scope;
public whitelist: string[] | undefined;
constructor(
public readonly attrName: string,
public readonly keyName: string,
options: AttributorOptions = {},
) {
const attributeBit = Scope.TYPE & Scope.ATTRIBUTE;
this.scope =
options.scope != null
? // Ignore type bits, force attribute bit
(options.scope & Scope.LEVEL) | attributeBit
: Scope.ATTRIBUTE;
if (options.whitelist != null) {
this.whitelist = options.whitelist;
}
}
public add(node: HTMLElement, value: any): boolean {
if (!this.canAdd(node, value)) {
return false;
}
node.setAttribute(this.keyName, value);
return true;
}
public canAdd(_node: HTMLElement, value: any): boolean {
if (this.whitelist == null) {
return true;
}
if (typeof value === 'string') {
return this.whitelist.indexOf(value.replace(/["']/g, '')) > -1;
} else {
return this.whitelist.indexOf(value) > -1;
}
}
public remove(node: HTMLElement): void {
node.removeAttribute(this.keyName);
}
public value(node: HTMLElement): any {
const value = node.getAttribute(this.keyName);
if (this.canAdd(node, value) && value) {
return value;
}
return '';
}
}

View File

@@ -0,0 +1,43 @@
import Attributor from './attributor.js';
function match(node: HTMLElement, prefix: string): string[] {
const className = node.getAttribute('class') || '';
return className
.split(/\s+/)
.filter((name) => name.indexOf(`${prefix}-`) === 0);
}
class ClassAttributor extends Attributor {
public static keys(node: HTMLElement): string[] {
return (node.getAttribute('class') || '')
.split(/\s+/)
.map((name) => name.split('-').slice(0, -1).join('-'));
}
public add(node: HTMLElement, value: any): boolean {
if (!this.canAdd(node, value)) {
return false;
}
this.remove(node);
node.classList.add(`${this.keyName}-${value}`);
return true;
}
public remove(node: HTMLElement): void {
const matches = match(node, this.keyName);
matches.forEach((name) => {
node.classList.remove(name);
});
if (node.classList.length === 0) {
node.removeAttribute('class');
}
}
public value(node: HTMLElement): any {
const result = match(node, this.keyName)[0] || '';
const value = result.slice(this.keyName.length + 1); // +1 for hyphen
return this.canAdd(node, value) ? value : '';
}
}
export default ClassAttributor;

View File

@@ -0,0 +1,79 @@
import type { Formattable } from '../blot/abstract/blot.js';
import Registry from '../registry.js';
import Scope from '../scope.js';
import Attributor from './attributor.js';
import ClassAttributor from './class.js';
import StyleAttributor from './style.js';
class AttributorStore {
private attributes: { [key: string]: Attributor } = {};
private domNode: HTMLElement;
constructor(domNode: HTMLElement) {
this.domNode = domNode;
this.build();
}
public attribute(attribute: Attributor, value: any): void {
// verb
if (value) {
if (attribute.add(this.domNode, value)) {
if (attribute.value(this.domNode) != null) {
this.attributes[attribute.attrName] = attribute;
} else {
delete this.attributes[attribute.attrName];
}
}
} else {
attribute.remove(this.domNode);
delete this.attributes[attribute.attrName];
}
}
public build(): void {
this.attributes = {};
const blot = Registry.find(this.domNode);
if (blot == null) {
return;
}
const attributes = Attributor.keys(this.domNode);
const classes = ClassAttributor.keys(this.domNode);
const styles = StyleAttributor.keys(this.domNode);
attributes
.concat(classes)
.concat(styles)
.forEach((name) => {
const attr = blot.scroll.query(name, Scope.ATTRIBUTE);
if (attr instanceof Attributor) {
this.attributes[attr.attrName] = attr;
}
});
}
public copy(target: Formattable): void {
Object.keys(this.attributes).forEach((key) => {
const value = this.attributes[key].value(this.domNode);
target.format(key, value);
});
}
public move(target: Formattable): void {
this.copy(target);
Object.keys(this.attributes).forEach((key) => {
this.attributes[key].remove(this.domNode);
});
this.attributes = {};
}
public values(): { [key: string]: any } {
return Object.keys(this.attributes).reduce(
(attributes: { [key: string]: any }, name: string) => {
attributes[name] = this.attributes[name].value(this.domNode);
return attributes;
},
{},
);
}
}
export default AttributorStore;

View File

@@ -0,0 +1,44 @@
import Attributor from './attributor.js';
function camelize(name: string): string {
const parts = name.split('-');
const rest = parts
.slice(1)
.map((part: string) => part[0].toUpperCase() + part.slice(1))
.join('');
return parts[0] + rest;
}
class StyleAttributor extends Attributor {
public static keys(node: HTMLElement): string[] {
return (node.getAttribute('style') || '').split(';').map((value) => {
const arr = value.split(':');
return arr[0].trim();
});
}
public add(node: HTMLElement, value: any): boolean {
if (!this.canAdd(node, value)) {
return false;
}
// @ts-expect-error Fix me later
node.style[camelize(this.keyName)] = value;
return true;
}
public remove(node: HTMLElement): void {
// @ts-expect-error Fix me later
node.style[camelize(this.keyName)] = '';
if (!node.getAttribute('style')) {
node.removeAttribute('style');
}
}
public value(node: HTMLElement): any {
// @ts-expect-error Fix me later
const value = node.style[camelize(this.keyName)];
return this.canAdd(node, value) ? value : '';
}
}
export default StyleAttributor;

View File

@@ -0,0 +1,128 @@
import type LinkedList from '../../collection/linked-list.js';
import type LinkedNode from '../../collection/linked-node.js';
import type { RegistryDefinition } from '../../registry.js';
import Scope from '../../scope.js';
export interface BlotConstructor {
new (...args: any[]): Blot;
/**
* Creates corresponding DOM node
*/
create(value?: any): Node;
blotName: string;
tagName: string | string[];
scope: Scope;
className?: string;
requiredContainer?: BlotConstructor;
allowedChildren?: BlotConstructor[];
defaultChild?: BlotConstructor;
}
/**
* Blots are the basic building blocks of a Parchment document.
*
* Several basic implementations such as Block, Inline, and Embed are provided.
* In general you will want to extend one of these, instead of building from scratch.
* After implementation, blots need to be registered before usage.
*
* At the very minimum a Blot must be named with a static blotName and associated with either a tagName or className.
* If a Blot is defined with both a tag and class, the class takes precedence, but the tag may be used as a fallback.
* Blots must also have a scope, which determine if it is inline or block.
*/
export interface Blot extends LinkedNode {
scroll: Root;
parent: Parent;
prev: Blot | null;
next: Blot | null;
domNode: Node;
statics: BlotConstructor;
attach(): void;
clone(): Blot;
detach(): void;
isolate(index: number, length: number): Blot;
/**
* For leaves, length of blot's value()
* For parents, sum of children's values
*/
length(): number;
/**
* Returns offset between this blot and an ancestor's
*/
offset(root?: Blot): number;
remove(): void;
replaceWith(name: string, value: any): Blot;
replaceWith(replacement: Blot): Blot;
split(index: number, force?: boolean): Blot | null;
wrap(name: string, value?: any): Parent;
wrap(wrapper: Parent): Parent;
deleteAt(index: number, length: number): void;
formatAt(index: number, length: number, name: string, value: any): void;
insertAt(index: number, value: string, def?: any): void;
/**
* Called after update cycle completes. Cannot change the value or length
* of the document, and any DOM operation must reduce complexity of the DOM
* tree. A shared context object is passed through all blots.
*/
optimize(context: { [key: string]: any }): void;
optimize(mutations: MutationRecord[], context: { [key: string]: any }): void;
/**
* Called when blot changes, with the mutation records of its change.
* Internal records of the blot values can be updated, and modifications of
* the blot itself is permitted. Can be trigger from user change or API call.
* A shared context object is passed through all blots.
*/
update(mutations: MutationRecord[], context: { [key: string]: any }): void;
}
export interface Parent extends Blot {
children: LinkedList<Blot>;
domNode: HTMLElement;
appendChild(child: Blot): void;
descendant<T>(type: new () => T, index: number): [T, number];
descendant<T>(matcher: (blot: Blot) => boolean, index: number): [T, number];
descendants<T>(type: new () => T, index: number, length: number): T[];
descendants<T>(
matcher: (blot: Blot) => boolean,
index: number,
length: number,
): T[];
insertBefore(child: Blot, refNode?: Blot | null): void;
moveChildren(parent: Parent, refNode?: Blot | null): void;
path(index: number, inclusive?: boolean): [Blot, number][];
removeChild(child: Blot): void;
unwrap(): void;
}
export interface Root extends Parent {
create(input: Node | string | Scope, value?: any): Blot;
find(node: Node | null, bubble?: boolean): Blot | null;
query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null;
}
export interface Formattable extends Blot {
/**
* Apply format to blot. Should not pass onto child or other blot.
*/
format(name: string, value: any): void;
/**
* Return formats represented by blot, including from Attributors.
*/
formats(): { [index: string]: any };
}
export interface Leaf extends Blot {
index(node: Node, offset: number): number;
position(index: number, inclusive: boolean): [Node, number];
value(): any;
}

View File

@@ -0,0 +1,48 @@
import Scope from '../../scope.js';
import BlockBlot from '../block.js';
import ParentBlot from './parent.js';
class ContainerBlot extends ParentBlot {
public static blotName = 'container';
public static scope = Scope.BLOCK_BLOT;
public static tagName: string | string[];
public prev!: BlockBlot | ContainerBlot | null;
public next!: BlockBlot | ContainerBlot | null;
public checkMerge(): boolean {
return (
this.next !== null && this.next.statics.blotName === this.statics.blotName
);
}
public deleteAt(index: number, length: number): void {
super.deleteAt(index, length);
this.enforceAllowedChildren();
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
super.formatAt(index, length, name, value);
this.enforceAllowedChildren();
}
public insertAt(index: number, value: string, def?: any): void {
super.insertAt(index, value, def);
this.enforceAllowedChildren();
}
public optimize(context: { [key: string]: any }): void {
super.optimize(context);
if (this.children.length > 0 && this.next != null && this.checkMerge()) {
this.next.moveChildren(this);
this.next.remove();
}
}
}
export default ContainerBlot;

View File

@@ -0,0 +1,57 @@
import Scope from '../../scope.js';
import type { Leaf } from './blot.js';
import ShadowBlot from './shadow.js';
class LeafBlot extends ShadowBlot implements Leaf {
public static scope = Scope.INLINE_BLOT;
/**
* Returns the value represented by domNode if it is this Blot's type
* No checking that domNode can represent this Blot type is required so
* applications needing it should check externally before calling.
*/
public static value(_domNode: Node): any {
return true;
}
/**
* Given location represented by node and offset from DOM Selection Range,
* return index to that location.
*/
public index(node: Node, offset: number): number {
if (
this.domNode === node ||
this.domNode.compareDocumentPosition(node) &
Node.DOCUMENT_POSITION_CONTAINED_BY
) {
return Math.min(offset, 1);
}
return -1;
}
/**
* Given index to location within blot, return node and offset representing
* that location, consumable by DOM Selection Range
*/
public position(index: number, _inclusive?: boolean): [Node, number] {
const childNodes: Node[] = Array.from(this.parent.domNode.childNodes);
let offset = childNodes.indexOf(this.domNode);
if (index > 0) {
offset += 1;
}
return [this.parent.domNode, offset];
}
/**
* Return value represented by this blot
* Should not change without interaction from API or
* user change detectable by update()
*/
public value(): any {
return {
[this.statics.blotName]: this.statics.value(this.domNode) || true,
};
}
}
export default LeafBlot;

View File

@@ -0,0 +1,400 @@
import LinkedList from '../../collection/linked-list.js';
import ParchmentError from '../../error.js';
import Scope from '../../scope.js';
import type { Blot, BlotConstructor, Parent, Root } from './blot.js';
import ShadowBlot from './shadow.js';
function makeAttachedBlot(node: Node, scroll: Root): Blot {
const found = scroll.find(node);
if (found) return found;
try {
return scroll.create(node);
} catch (e) {
const blot = scroll.create(Scope.INLINE);
Array.from(node.childNodes).forEach((child: Node) => {
blot.domNode.appendChild(child);
});
if (node.parentNode) {
node.parentNode.replaceChild(blot.domNode, node);
}
blot.attach();
return blot;
}
}
class ParentBlot extends ShadowBlot implements Parent {
/**
* Whitelist array of Blots that can be direct children.
*/
public static allowedChildren?: BlotConstructor[];
/**
* Default child blot to be inserted if this blot becomes empty.
*/
public static defaultChild?: BlotConstructor;
public static uiClass = '';
public children!: LinkedList<Blot>;
public domNode!: HTMLElement;
public uiNode: HTMLElement | null = null;
constructor(scroll: Root, domNode: Node) {
super(scroll, domNode);
this.build();
}
public appendChild(other: Blot): void {
this.insertBefore(other);
}
public attach(): void {
super.attach();
this.children.forEach((child) => {
child.attach();
});
}
public attachUI(node: HTMLElement): void {
if (this.uiNode != null) {
this.uiNode.remove();
}
this.uiNode = node;
if (ParentBlot.uiClass) {
this.uiNode.classList.add(ParentBlot.uiClass);
}
this.uiNode.setAttribute('contenteditable', 'false');
this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
}
/**
* Called during construction, should fill its own children LinkedList.
*/
public build(): void {
this.children = new LinkedList<Blot>();
// Need to be reversed for if DOM nodes already in order
Array.from(this.domNode.childNodes)
.filter((node: Node) => node !== this.uiNode)
.reverse()
.forEach((node: Node) => {
try {
const child = makeAttachedBlot(node, this.scroll);
this.insertBefore(child, this.children.head || undefined);
} catch (err) {
if (err instanceof ParchmentError) {
return;
} else {
throw err;
}
}
});
}
public deleteAt(index: number, length: number): void {
if (index === 0 && length === this.length()) {
return this.remove();
}
this.children.forEachAt(index, length, (child, offset, childLength) => {
child.deleteAt(offset, childLength);
});
}
public descendant<T extends Blot>(
criteria: new (...args: any[]) => T,
index: number,
): [T | null, number];
public descendant(
criteria: (blot: Blot) => boolean,
index: number,
): [Blot | null, number];
public descendant(criteria: any, index = 0): [Blot | null, number] {
const [child, offset] = this.children.find(index);
if (
(criteria.blotName == null && criteria(child)) ||
(criteria.blotName != null && child instanceof criteria)
) {
return [child as any, offset];
} else if (child instanceof ParentBlot) {
return child.descendant(criteria, offset);
} else {
return [null, -1];
}
}
public descendants<T extends Blot>(
criteria: new (...args: any[]) => T,
index?: number,
length?: number,
): T[];
public descendants(
criteria: (blot: Blot) => boolean,
index?: number,
length?: number,
): Blot[];
public descendants(
criteria: any,
index = 0,
length: number = Number.MAX_VALUE,
): Blot[] {
let descendants: Blot[] = [];
let lengthLeft = length;
this.children.forEachAt(
index,
length,
(child: Blot, childIndex: number, childLength: number) => {
if (
(criteria.blotName == null && criteria(child)) ||
(criteria.blotName != null && child instanceof criteria)
) {
descendants.push(child);
}
if (child instanceof ParentBlot) {
descendants = descendants.concat(
child.descendants(criteria, childIndex, lengthLeft),
);
}
lengthLeft -= childLength;
},
);
return descendants;
}
public detach(): void {
this.children.forEach((child) => {
child.detach();
});
super.detach();
}
public enforceAllowedChildren(): void {
let done = false;
this.children.forEach((child: Blot) => {
if (done) {
return;
}
const allowed = this.statics.allowedChildren.some(
(def: BlotConstructor) => child instanceof def,
);
if (allowed) {
return;
}
if (child.statics.scope === Scope.BLOCK_BLOT) {
if (child.next != null) {
this.splitAfter(child);
}
if (child.prev != null) {
this.splitAfter(child.prev);
}
child.parent.unwrap();
done = true;
} else if (child instanceof ParentBlot) {
child.unwrap();
} else {
child.remove();
}
});
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
this.children.forEachAt(index, length, (child, offset, childLength) => {
child.formatAt(offset, childLength, name, value);
});
}
public insertAt(index: number, value: string, def?: any): void {
const [child, offset] = this.children.find(index);
if (child) {
child.insertAt(offset, value, def);
} else {
const blot =
def == null
? this.scroll.create('text', value)
: this.scroll.create(value, def);
this.appendChild(blot);
}
}
public insertBefore(childBlot: Blot, refBlot?: Blot | null): void {
if (childBlot.parent != null) {
childBlot.parent.children.remove(childBlot);
}
let refDomNode: Node | null = null;
this.children.insertBefore(childBlot, refBlot || null);
childBlot.parent = this;
if (refBlot != null) {
refDomNode = refBlot.domNode;
}
if (
this.domNode.parentNode !== childBlot.domNode ||
this.domNode.nextSibling !== refDomNode
) {
this.domNode.insertBefore(childBlot.domNode, refDomNode);
}
childBlot.attach();
}
public length(): number {
return this.children.reduce((memo, child) => {
return memo + child.length();
}, 0);
}
public moveChildren(targetParent: Parent, refNode?: Blot | null): void {
this.children.forEach((child) => {
targetParent.insertBefore(child, refNode);
});
}
public optimize(context?: { [key: string]: any }): void {
super.optimize(context);
this.enforceAllowedChildren();
if (this.uiNode != null && this.uiNode !== this.domNode.firstChild) {
this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
}
if (this.children.length === 0) {
if (this.statics.defaultChild != null) {
const child = this.scroll.create(this.statics.defaultChild.blotName);
this.appendChild(child);
// TODO double check if necessary
// child.optimize(context);
} else {
this.remove();
}
}
}
public path(index: number, inclusive = false): [Blot, number][] {
const [child, offset] = this.children.find(index, inclusive);
const position: [Blot, number][] = [[this, index]];
if (child instanceof ParentBlot) {
return position.concat(child.path(offset, inclusive));
} else if (child != null) {
position.push([child, offset]);
}
return position;
}
public removeChild(child: Blot): void {
this.children.remove(child);
}
public replaceWith(name: string | Blot, value?: any): Blot {
const replacement =
typeof name === 'string' ? this.scroll.create(name, value) : name;
if (replacement instanceof ParentBlot) {
this.moveChildren(replacement);
}
return super.replaceWith(replacement);
}
public split(index: number, force = false): Blot | null {
if (!force) {
if (index === 0) {
return this;
}
if (index === this.length()) {
return this.next;
}
}
const after = this.clone() as ParentBlot;
if (this.parent) {
this.parent.insertBefore(after, this.next || undefined);
}
this.children.forEachAt(index, this.length(), (child, offset, _length) => {
const split = child.split(offset, force);
if (split != null) {
after.appendChild(split);
}
});
return after;
}
public splitAfter(child: Blot): Parent {
const after = this.clone() as ParentBlot;
while (child.next != null) {
after.appendChild(child.next);
}
if (this.parent) {
this.parent.insertBefore(after, this.next || undefined);
}
return after;
}
public unwrap(): void {
if (this.parent) {
this.moveChildren(this.parent, this.next || undefined);
}
this.remove();
}
public update(
mutations: MutationRecord[],
_context: { [key: string]: any },
): void {
const addedNodes: Node[] = [];
const removedNodes: Node[] = [];
mutations.forEach((mutation) => {
if (mutation.target === this.domNode && mutation.type === 'childList') {
addedNodes.push(...mutation.addedNodes);
removedNodes.push(...mutation.removedNodes);
}
});
removedNodes.forEach((node: Node) => {
// Check node has actually been removed
// One exception is Chrome does not immediately remove IFRAMEs
// from DOM but MutationRecord is correct in its reported removal
if (
node.parentNode != null &&
// @ts-expect-error Fix me later
node.tagName !== 'IFRAME' &&
document.body.compareDocumentPosition(node) &
Node.DOCUMENT_POSITION_CONTAINED_BY
) {
return;
}
const blot = this.scroll.find(node);
if (blot == null) {
return;
}
if (
blot.domNode.parentNode == null ||
blot.domNode.parentNode === this.domNode
) {
blot.detach();
}
});
addedNodes
.filter((node) => {
return node.parentNode === this.domNode && node !== this.uiNode;
})
.sort((a, b) => {
if (a === b) {
return 0;
}
if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) {
return 1;
}
return -1;
})
.forEach((node) => {
let refBlot: Blot | null = null;
if (node.nextSibling != null) {
refBlot = this.scroll.find(node.nextSibling);
}
const blot = makeAttachedBlot(node, this.scroll);
if (blot.next !== refBlot || blot.next == null) {
if (blot.parent != null) {
blot.parent.removeChild(this);
}
this.insertBefore(blot, refBlot || undefined);
}
});
this.enforceAllowedChildren();
}
}
export default ParentBlot;

View File

@@ -0,0 +1,188 @@
import ParchmentError from '../../error.js';
import Registry from '../../registry.js';
import Scope from '../../scope.js';
import type {
Blot,
BlotConstructor,
Formattable,
Parent,
Root,
} from './blot.js';
class ShadowBlot implements Blot {
public static blotName = 'abstract';
public static className: string;
public static requiredContainer: BlotConstructor;
public static scope: Scope;
public static tagName: string | string[];
public static create(rawValue?: unknown): Node {
if (this.tagName == null) {
throw new ParchmentError('Blot definition missing tagName');
}
let node: HTMLElement;
let value: string | number | undefined;
if (Array.isArray(this.tagName)) {
if (typeof rawValue === 'string') {
value = rawValue.toUpperCase();
if (parseInt(value, 10).toString() === value) {
value = parseInt(value, 10);
}
} else if (typeof rawValue === 'number') {
value = rawValue;
}
if (typeof value === 'number') {
node = document.createElement(this.tagName[value - 1]);
} else if (value && this.tagName.indexOf(value) > -1) {
node = document.createElement(value);
} else {
node = document.createElement(this.tagName[0]);
}
} else {
node = document.createElement(this.tagName);
}
if (this.className) {
node.classList.add(this.className);
}
return node;
}
public prev: Blot | null;
public next: Blot | null;
// @ts-expect-error Fix me later
public parent: Parent;
// Hack for accessing inherited static methods
get statics(): any {
return this.constructor;
}
constructor(
public scroll: Root,
public domNode: Node,
) {
Registry.blots.set(domNode, this);
this.prev = null;
this.next = null;
}
public attach(): void {
// Nothing to do
}
public clone(): Blot {
const domNode = this.domNode.cloneNode(false);
return this.scroll.create(domNode);
}
public detach(): void {
if (this.parent != null) {
this.parent.removeChild(this);
}
Registry.blots.delete(this.domNode);
}
public deleteAt(index: number, length: number): void {
const blot = this.isolate(index, length);
blot.remove();
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
const blot = this.isolate(index, length);
if (this.scroll.query(name, Scope.BLOT) != null && value) {
blot.wrap(name, value);
} else if (this.scroll.query(name, Scope.ATTRIBUTE) != null) {
const parent = this.scroll.create(this.statics.scope) as Parent &
Formattable;
blot.wrap(parent);
parent.format(name, value);
}
}
public insertAt(index: number, value: string, def?: any): void {
const blot =
def == null
? this.scroll.create('text', value)
: this.scroll.create(value, def);
const ref = this.split(index);
this.parent.insertBefore(blot, ref || undefined);
}
public isolate(index: number, length: number): Blot {
const target = this.split(index);
if (target == null) {
throw new Error('Attempt to isolate at end');
}
target.split(length);
return target;
}
public length(): number {
return 1;
}
public offset(root: Blot = this.parent): number {
if (this.parent == null || this === root) {
return 0;
}
return this.parent.children.offset(this) + this.parent.offset(root);
}
public optimize(_context?: { [key: string]: any }): void {
if (
this.statics.requiredContainer &&
!(this.parent instanceof this.statics.requiredContainer)
) {
this.wrap(this.statics.requiredContainer.blotName);
}
}
public remove(): void {
if (this.domNode.parentNode != null) {
this.domNode.parentNode.removeChild(this.domNode);
}
this.detach();
}
public replaceWith(name: string | Blot, value?: any): Blot {
const replacement =
typeof name === 'string' ? this.scroll.create(name, value) : name;
if (this.parent != null) {
this.parent.insertBefore(replacement, this.next || undefined);
this.remove();
}
return replacement;
}
public split(index: number, _force?: boolean): Blot | null {
return index === 0 ? this : this.next;
}
public update(
_mutations: MutationRecord[],
_context: { [key: string]: any },
): void {
// Nothing to do by default
}
public wrap(name: string | Parent, value?: any): Parent {
const wrapper =
typeof name === 'string'
? (this.scroll.create(name, value) as Parent)
: name;
if (this.parent != null) {
this.parent.insertBefore(wrapper, this.next || undefined);
}
if (typeof wrapper.appendChild !== 'function') {
throw new ParchmentError(`Cannot wrap ${name}`);
}
wrapper.appendChild(this);
return wrapper;
}
}
export default ShadowBlot;

View File

@@ -0,0 +1,123 @@
import Attributor from '../attributor/attributor.js';
import AttributorStore from '../attributor/store.js';
import Scope from '../scope.js';
import type {
Blot,
BlotConstructor,
Formattable,
Root,
} from './abstract/blot.js';
import LeafBlot from './abstract/leaf.js';
import ParentBlot from './abstract/parent.js';
import InlineBlot from './inline.js';
class BlockBlot extends ParentBlot implements Formattable {
public static blotName = 'block';
public static scope = Scope.BLOCK_BLOT;
public static tagName: string | string[] = 'P';
public static allowedChildren: BlotConstructor[] = [
InlineBlot,
BlockBlot,
LeafBlot,
];
static create(value?: unknown) {
return super.create(value) as HTMLElement;
}
public static formats(domNode: HTMLElement, scroll: Root): any {
const match = scroll.query(BlockBlot.blotName);
if (
match != null &&
domNode.tagName === (match as BlotConstructor).tagName
) {
return undefined;
} else if (typeof this.tagName === 'string') {
return true;
} else if (Array.isArray(this.tagName)) {
return domNode.tagName.toLowerCase();
}
}
protected attributes: AttributorStore;
constructor(scroll: Root, domNode: Node) {
super(scroll, domNode);
this.attributes = new AttributorStore(this.domNode);
}
public format(name: string, value: any): void {
const format = this.scroll.query(name, Scope.BLOCK);
if (format == null) {
return;
} else if (format instanceof Attributor) {
this.attributes.attribute(format, value);
} else if (name === this.statics.blotName && !value) {
this.replaceWith(BlockBlot.blotName);
} else if (
value &&
(name !== this.statics.blotName || this.formats()[name] !== value)
) {
this.replaceWith(name, value);
}
}
public formats(): { [index: string]: any } {
const formats = this.attributes.values();
const format = this.statics.formats(this.domNode, this.scroll);
if (format != null) {
formats[this.statics.blotName] = format;
}
return formats;
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
if (this.scroll.query(name, Scope.BLOCK) != null) {
this.format(name, value);
} else {
super.formatAt(index, length, name, value);
}
}
public insertAt(index: number, value: string, def?: any): void {
if (def == null || this.scroll.query(value, Scope.INLINE) != null) {
// Insert text or inline
super.insertAt(index, value, def);
} else {
const after = this.split(index);
if (after != null) {
const blot = this.scroll.create(value, def);
after.parent.insertBefore(blot, after);
} else {
throw new Error('Attempt to insertAt after block boundaries');
}
}
}
public replaceWith(name: string | Blot, value?: any): Blot {
const replacement = super.replaceWith(name, value) as BlockBlot;
this.attributes.copy(replacement);
return replacement;
}
public update(
mutations: MutationRecord[],
context: { [key: string]: any },
): void {
super.update(mutations, context);
const attributeChanged = mutations.some(
(mutation) =>
mutation.target === this.domNode && mutation.type === 'attributes',
);
if (attributeChanged) {
this.attributes.build();
}
}
}
export default BlockBlot;

View File

@@ -0,0 +1,34 @@
import type { Formattable, Root } from './abstract/blot.js';
import LeafBlot from './abstract/leaf.js';
class EmbedBlot extends LeafBlot implements Formattable {
public static formats(_domNode: HTMLElement, _scroll: Root): any {
return undefined;
}
public format(name: string, value: any): void {
// super.formatAt wraps, which is what we want in general,
// but this allows subclasses to overwrite for formats
// that just apply to particular embeds
super.formatAt(0, this.length(), name, value);
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
if (index === 0 && length === this.length()) {
this.format(name, value);
} else {
super.formatAt(index, length, name, value);
}
}
public formats(): { [index: string]: any } {
return this.statics.formats(this.domNode, this.scroll);
}
}
export default EmbedBlot;

View File

@@ -0,0 +1,159 @@
import Attributor from '../attributor/attributor.js';
import AttributorStore from '../attributor/store.js';
import Scope from '../scope.js';
import type {
Blot,
BlotConstructor,
Formattable,
Parent,
Root,
} from './abstract/blot.js';
import LeafBlot from './abstract/leaf.js';
import ParentBlot from './abstract/parent.js';
// Shallow object comparison
function isEqual(
obj1: Record<string, unknown>,
obj2: Record<string, unknown>,
): boolean {
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (const prop in obj1) {
if (obj1[prop] !== obj2[prop]) {
return false;
}
}
return true;
}
class InlineBlot extends ParentBlot implements Formattable {
public static allowedChildren: BlotConstructor[] = [InlineBlot, LeafBlot];
public static blotName = 'inline';
public static scope = Scope.INLINE_BLOT;
public static tagName: string | string[] = 'SPAN';
static create(value?: unknown) {
return super.create(value) as HTMLElement;
}
public static formats(domNode: HTMLElement, scroll: Root): any {
const match = scroll.query(InlineBlot.blotName);
if (
match != null &&
domNode.tagName === (match as BlotConstructor).tagName
) {
return undefined;
} else if (typeof this.tagName === 'string') {
return true;
} else if (Array.isArray(this.tagName)) {
return domNode.tagName.toLowerCase();
}
return undefined;
}
protected attributes: AttributorStore;
constructor(scroll: Root, domNode: Node) {
super(scroll, domNode);
this.attributes = new AttributorStore(this.domNode);
}
public format(name: string, value: any): void {
if (name === this.statics.blotName && !value) {
this.children.forEach((child) => {
if (!(child instanceof InlineBlot)) {
child = child.wrap(InlineBlot.blotName, true);
}
this.attributes.copy(child as InlineBlot);
});
this.unwrap();
} else {
const format = this.scroll.query(name, Scope.INLINE);
if (format == null) {
return;
}
if (format instanceof Attributor) {
this.attributes.attribute(format, value);
} else if (
value &&
(name !== this.statics.blotName || this.formats()[name] !== value)
) {
this.replaceWith(name, value);
}
}
}
public formats(): { [index: string]: any } {
const formats = this.attributes.values();
const format = this.statics.formats(this.domNode, this.scroll);
if (format != null) {
formats[this.statics.blotName] = format;
}
return formats;
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
if (
this.formats()[name] != null ||
this.scroll.query(name, Scope.ATTRIBUTE)
) {
const blot = this.isolate(index, length) as InlineBlot;
blot.format(name, value);
} else {
super.formatAt(index, length, name, value);
}
}
public optimize(context: { [key: string]: any }): void {
super.optimize(context);
const formats = this.formats();
if (Object.keys(formats).length === 0) {
return this.unwrap(); // unformatted span
}
const next = this.next;
if (
next instanceof InlineBlot &&
next.prev === this &&
isEqual(formats, next.formats())
) {
next.moveChildren(this);
next.remove();
}
}
public replaceWith(name: string | Blot, value?: any): Blot {
const replacement = super.replaceWith(name, value) as InlineBlot;
this.attributes.copy(replacement);
return replacement;
}
public update(
mutations: MutationRecord[],
context: { [key: string]: any },
): void {
super.update(mutations, context);
const attributeChanged = mutations.some(
(mutation) =>
mutation.target === this.domNode && mutation.type === 'attributes',
);
if (attributeChanged) {
this.attributes.build();
}
}
public wrap(name: string | Parent, value?: any): Parent {
const wrapper = super.wrap(name, value);
if (wrapper instanceof InlineBlot) {
this.attributes.move(wrapper);
}
return wrapper;
}
}
export default InlineBlot;

View File

@@ -0,0 +1,216 @@
import Registry, { type RegistryDefinition } from '../registry.js';
import Scope from '../scope.js';
import type { Blot, BlotConstructor, Root } from './abstract/blot.js';
import ContainerBlot from './abstract/container.js';
import ParentBlot from './abstract/parent.js';
import BlockBlot from './block.js';
const OBSERVER_CONFIG = {
attributes: true,
characterData: true,
characterDataOldValue: true,
childList: true,
subtree: true,
};
const MAX_OPTIMIZE_ITERATIONS = 100;
class ScrollBlot extends ParentBlot implements Root {
public static blotName = 'scroll';
public static defaultChild = BlockBlot;
public static allowedChildren: BlotConstructor[] = [BlockBlot, ContainerBlot];
public static scope = Scope.BLOCK_BLOT;
public static tagName = 'DIV';
public observer: MutationObserver;
constructor(
public registry: Registry,
node: HTMLDivElement,
) {
// @ts-expect-error scroll is the root with no parent
super(null, node);
this.scroll = this;
this.build();
this.observer = new MutationObserver((mutations: MutationRecord[]) => {
this.update(mutations);
});
this.observer.observe(this.domNode, OBSERVER_CONFIG);
this.attach();
}
public create(input: Node | string | Scope, value?: any): Blot {
return this.registry.create(this, input, value);
}
public find(node: Node | null, bubble = false): Blot | null {
const blot = this.registry.find(node, bubble);
if (!blot) {
return null;
}
if (blot.scroll === this) {
return blot;
}
return bubble ? this.find(blot.scroll.domNode.parentNode, true) : null;
}
public query(
query: string | Node | Scope,
scope: Scope = Scope.ANY,
): RegistryDefinition | null {
return this.registry.query(query, scope);
}
public register(...definitions: RegistryDefinition[]) {
return this.registry.register(...definitions);
}
public build(): void {
if (this.scroll == null) {
return;
}
super.build();
}
public detach(): void {
super.detach();
this.observer.disconnect();
}
public deleteAt(index: number, length: number): void {
this.update();
if (index === 0 && length === this.length()) {
this.children.forEach((child) => {
child.remove();
});
} else {
super.deleteAt(index, length);
}
}
public formatAt(
index: number,
length: number,
name: string,
value: any,
): void {
this.update();
super.formatAt(index, length, name, value);
}
public insertAt(index: number, value: string, def?: any): void {
this.update();
super.insertAt(index, value, def);
}
public optimize(context?: { [key: string]: any }): void;
public optimize(
mutations: MutationRecord[],
context: { [key: string]: any },
): void;
public optimize(mutations: any = [], context: any = {}): void {
super.optimize(context);
const mutationsMap = context.mutationsMap || new WeakMap();
// We must modify mutations directly, cannot make copy and then modify
let records = Array.from(this.observer.takeRecords());
// Array.push currently seems to be implemented by a non-tail recursive function
// so we cannot just mutations.push.apply(mutations, this.observer.takeRecords());
while (records.length > 0) {
mutations.push(records.pop());
}
const mark = (blot: Blot | null, markParent = true): void => {
if (blot == null || blot === this) {
return;
}
if (blot.domNode.parentNode == null) {
return;
}
if (!mutationsMap.has(blot.domNode)) {
mutationsMap.set(blot.domNode, []);
}
if (markParent) {
mark(blot.parent);
}
};
const optimize = (blot: Blot): void => {
// Post-order traversal
if (!mutationsMap.has(blot.domNode)) {
return;
}
if (blot instanceof ParentBlot) {
blot.children.forEach(optimize);
}
mutationsMap.delete(blot.domNode);
blot.optimize(context);
};
let remaining = mutations;
for (let i = 0; remaining.length > 0; i += 1) {
if (i >= MAX_OPTIMIZE_ITERATIONS) {
throw new Error('[Parchment] Maximum optimize iterations reached');
}
remaining.forEach((mutation: MutationRecord) => {
const blot = this.find(mutation.target, true);
if (blot == null) {
return;
}
if (blot.domNode === mutation.target) {
if (mutation.type === 'childList') {
mark(this.find(mutation.previousSibling, false));
Array.from(mutation.addedNodes).forEach((node: Node) => {
const child = this.find(node, false);
mark(child, false);
if (child instanceof ParentBlot) {
child.children.forEach((grandChild: Blot) => {
mark(grandChild, false);
});
}
});
} else if (mutation.type === 'attributes') {
mark(blot.prev);
}
}
mark(blot);
});
this.children.forEach(optimize);
remaining = Array.from(this.observer.takeRecords());
records = remaining.slice();
while (records.length > 0) {
mutations.push(records.pop());
}
}
}
public update(
mutations?: MutationRecord[],
context: { [key: string]: any } = {},
): void {
mutations = mutations || this.observer.takeRecords();
const mutationsMap = new WeakMap();
mutations
.map((mutation: MutationRecord) => {
const blot = this.find(mutation.target, true);
if (blot == null) {
return null;
}
if (mutationsMap.has(blot.domNode)) {
mutationsMap.get(blot.domNode).push(mutation);
return null;
} else {
mutationsMap.set(blot.domNode, [mutation]);
return blot;
}
})
.forEach((blot: Blot | null) => {
if (blot != null && blot !== this && mutationsMap.has(blot.domNode)) {
blot.update(mutationsMap.get(blot.domNode) || [], context);
}
});
context.mutationsMap = mutationsMap;
if (mutationsMap.has(this.domNode)) {
super.update(mutationsMap.get(this.domNode), context);
}
this.optimize(mutations, context);
}
}
export default ScrollBlot;

View File

@@ -0,0 +1,100 @@
import Scope from '../scope.js';
import type { Blot, Leaf, Root } from './abstract/blot.js';
import LeafBlot from './abstract/leaf.js';
class TextBlot extends LeafBlot implements Leaf {
public static readonly blotName = 'text';
public static scope = Scope.INLINE_BLOT;
public static create(value: string): Text {
return document.createTextNode(value);
}
public static value(domNode: Text): string {
return domNode.data;
}
public domNode!: Text;
protected text: string;
constructor(scroll: Root, node: Node) {
super(scroll, node);
this.text = this.statics.value(this.domNode);
}
public deleteAt(index: number, length: number): void {
this.domNode.data = this.text =
this.text.slice(0, index) + this.text.slice(index + length);
}
public index(node: Node, offset: number): number {
if (this.domNode === node) {
return offset;
}
return -1;
}
public insertAt(index: number, value: string, def?: any): void {
if (def == null) {
this.text = this.text.slice(0, index) + value + this.text.slice(index);
this.domNode.data = this.text;
} else {
super.insertAt(index, value, def);
}
}
public length(): number {
return this.text.length;
}
public optimize(context: { [key: string]: any }): void {
super.optimize(context);
this.text = this.statics.value(this.domNode);
if (this.text.length === 0) {
this.remove();
} else if (this.next instanceof TextBlot && this.next.prev === this) {
this.insertAt(this.length(), (this.next as TextBlot).value());
this.next.remove();
}
}
public position(index: number, _inclusive = false): [Node, number] {
return [this.domNode, index];
}
public split(index: number, force = false): Blot | null {
if (!force) {
if (index === 0) {
return this;
}
if (index === this.length()) {
return this.next;
}
}
const after = this.scroll.create(this.domNode.splitText(index));
this.parent.insertBefore(after, this.next || undefined);
this.text = this.statics.value(this.domNode);
return after;
}
public update(
mutations: MutationRecord[],
_context: { [key: string]: any },
): void {
if (
mutations.some((mutation) => {
return (
mutation.type === 'characterData' && mutation.target === this.domNode
);
})
) {
this.text = this.statics.value(this.domNode);
}
}
public value(): string {
return this.text;
}
}
export default TextBlot;

View File

@@ -0,0 +1,201 @@
import type LinkedNode from './linked-node.js';
class LinkedList<T extends LinkedNode> {
public head: T | null;
public tail: T | null;
public length: number;
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
public append(...nodes: T[]): void {
this.insertBefore(nodes[0], null);
if (nodes.length > 1) {
const rest = nodes.slice(1);
this.append(...rest);
}
}
public at(index: number): T | null {
const next = this.iterator();
let cur = next();
while (cur && index > 0) {
index -= 1;
cur = next();
}
return cur;
}
public contains(node: T): boolean {
const next = this.iterator();
let cur = next();
while (cur) {
if (cur === node) {
return true;
}
cur = next();
}
return false;
}
public indexOf(node: T): number {
const next = this.iterator();
let cur = next();
let index = 0;
while (cur) {
if (cur === node) {
return index;
}
index += 1;
cur = next();
}
return -1;
}
public insertBefore(node: T | null, refNode: T | null): void {
if (node == null) {
return;
}
this.remove(node);
node.next = refNode;
if (refNode != null) {
node.prev = refNode.prev;
if (refNode.prev != null) {
refNode.prev.next = node;
}
refNode.prev = node;
if (refNode === this.head) {
this.head = node;
}
} else if (this.tail != null) {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
} else {
node.prev = null;
this.head = this.tail = node;
}
this.length += 1;
}
public offset(target: T): number {
let index = 0;
let cur = this.head;
while (cur != null) {
if (cur === target) {
return index;
}
index += cur.length();
cur = cur.next as T;
}
return -1;
}
public remove(node: T): void {
if (!this.contains(node)) {
return;
}
if (node.prev != null) {
node.prev.next = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
}
if (node === this.head) {
this.head = node.next as T;
}
if (node === this.tail) {
this.tail = node.prev as T;
}
this.length -= 1;
}
public iterator(curNode: T | null = this.head): () => T | null {
// TODO use yield when we can
return (): T | null => {
const ret = curNode;
if (curNode != null) {
curNode = curNode.next as T;
}
return ret;
};
}
public find(index: number, inclusive = false): [T | null, number] {
const next = this.iterator();
let cur = next();
while (cur) {
const length = cur.length();
if (
index < length ||
(inclusive &&
index === length &&
(cur.next == null || cur.next.length() !== 0))
) {
return [cur, index];
}
index -= length;
cur = next();
}
return [null, 0];
}
public forEach(callback: (cur: T) => void): void {
const next = this.iterator();
let cur = next();
while (cur) {
callback(cur);
cur = next();
}
}
public forEachAt(
index: number,
length: number,
callback: (cur: T, offset: number, length: number) => void,
): void {
if (length <= 0) {
return;
}
const [startNode, offset] = this.find(index);
let curIndex = index - offset;
const next = this.iterator(startNode);
let cur = next();
while (cur && curIndex < index + length) {
const curLength = cur.length();
if (index > curIndex) {
callback(
cur,
index - curIndex,
Math.min(length, curIndex + curLength - index),
);
} else {
callback(cur, 0, Math.min(curLength, index + length - curIndex));
}
curIndex += curLength;
cur = next();
}
}
public map(callback: (cur: T) => any): any[] {
return this.reduce((memo: T[], cur: T) => {
memo.push(callback(cur));
return memo;
}, []);
}
public reduce<M>(callback: (memo: M, cur: T) => M, memo: M): M {
const next = this.iterator();
let cur = next();
while (cur) {
memo = callback(memo, cur);
cur = next();
}
return memo;
}
}
export default LinkedList;

View File

@@ -0,0 +1,8 @@
interface LinkedNode {
prev: LinkedNode | null;
next: LinkedNode | null;
length(): number;
}
export type { LinkedNode as default };

View File

@@ -0,0 +1,12 @@
export default class ParchmentError extends Error {
public message: string;
public name: string;
public stack!: string;
constructor(message: string) {
message = '[Parchment] ' + message;
super(message);
this.message = message;
this.name = this.constructor.name;
}
}

View File

@@ -0,0 +1,48 @@
import ContainerBlot from './blot/abstract/container.js';
import LeafBlot from './blot/abstract/leaf.js';
import ParentBlot from './blot/abstract/parent.js';
import BlockBlot from './blot/block.js';
import EmbedBlot from './blot/embed.js';
import InlineBlot from './blot/inline.js';
import ScrollBlot from './blot/scroll.js';
import TextBlot from './blot/text.js';
import Attributor from './attributor/attributor.js';
import ClassAttributor from './attributor/class.js';
import AttributorStore from './attributor/store.js';
import StyleAttributor from './attributor/style.js';
import Registry from './registry.js';
import Scope from './scope.js';
export {
ParentBlot,
ContainerBlot,
LeafBlot,
EmbedBlot,
ScrollBlot,
BlockBlot,
InlineBlot,
TextBlot,
Attributor,
ClassAttributor,
StyleAttributor,
AttributorStore,
Registry,
Scope,
};
export type { RegistryInterface, RegistryDefinition } from './registry.js';
export type { default as ShadowBlot } from './blot/abstract/shadow.js';
export type { default as LinkedList } from './collection/linked-list.js';
export type { default as LinkedNode } from './collection/linked-node.js';
export type { AttributorOptions } from './attributor/attributor.js';
export type {
Blot,
BlotConstructor,
Formattable,
Leaf,
Parent,
Root,
} from './blot/abstract/blot.js';

View File

@@ -0,0 +1,155 @@
import Attributor from './attributor/attributor.js';
import {
type Blot,
type BlotConstructor,
type Root,
} from './blot/abstract/blot.js';
import ParchmentError from './error.js';
import Scope from './scope.js';
export type RegistryDefinition = Attributor | BlotConstructor;
export interface RegistryInterface {
create(scroll: Root, input: Node | string | Scope, value?: any): Blot;
query(query: string | Node | Scope, scope: Scope): RegistryDefinition | null;
register(...definitions: any[]): any;
}
export default class Registry implements RegistryInterface {
public static blots = new WeakMap<Node, Blot>();
public static find(node?: Node | null, bubble = false): Blot | null {
if (node == null) {
return null;
}
if (this.blots.has(node)) {
return this.blots.get(node) || null;
}
if (bubble) {
let parentNode: Node | null = null;
try {
parentNode = node.parentNode;
} catch (err) {
// Probably hit a permission denied error.
// A known case is in Firefox, event targets can be anonymous DIVs
// inside an input element.
// https://bugzilla.mozilla.org/show_bug.cgi?id=208427
return null;
}
return this.find(parentNode, bubble);
}
return null;
}
private attributes: { [key: string]: Attributor } = {};
private classes: { [key: string]: BlotConstructor } = {};
private tags: { [key: string]: BlotConstructor } = {};
private types: { [key: string]: RegistryDefinition } = {};
public create(scroll: Root, input: Node | string | Scope, value?: any): Blot {
const match = this.query(input);
if (match == null) {
throw new ParchmentError(`Unable to create ${input} blot`);
}
const blotClass = match as BlotConstructor;
const node =
// @ts-expect-error Fix me later
input instanceof Node || input.nodeType === Node.TEXT_NODE
? input
: blotClass.create(value);
const blot = new blotClass(scroll, node as Node, value);
Registry.blots.set(blot.domNode, blot);
return blot;
}
public find(node: Node | null, bubble = false): Blot | null {
return Registry.find(node, bubble);
}
public query(
query: string | Node | Scope,
scope: Scope = Scope.ANY,
): RegistryDefinition | null {
let match;
if (typeof query === 'string') {
match = this.types[query] || this.attributes[query];
// @ts-expect-error Fix me later
} else if (query instanceof Text || query.nodeType === Node.TEXT_NODE) {
match = this.types.text;
} else if (typeof query === 'number') {
if (query & Scope.LEVEL & Scope.BLOCK) {
match = this.types.block;
} else if (query & Scope.LEVEL & Scope.INLINE) {
match = this.types.inline;
}
} else if (query instanceof Element) {
const names = (query.getAttribute('class') || '').split(/\s+/);
names.some((name) => {
match = this.classes[name];
if (match) {
return true;
}
return false;
});
match = match || this.tags[query.tagName];
}
if (match == null) {
return null;
}
if (
'scope' in match &&
scope & Scope.LEVEL & match.scope &&
scope & Scope.TYPE & match.scope
) {
return match;
}
return null;
}
public register(...definitions: RegistryDefinition[]): RegistryDefinition[] {
return definitions.map((definition) => {
const isBlot = 'blotName' in definition;
const isAttr = 'attrName' in definition;
if (!isBlot && !isAttr) {
throw new ParchmentError('Invalid definition');
} else if (isBlot && definition.blotName === 'abstract') {
throw new ParchmentError('Cannot register abstract class');
}
const key = isBlot
? definition.blotName
: isAttr
? definition.attrName
: (undefined as never); // already handled by above checks
this.types[key] = definition;
if (isAttr) {
if (typeof definition.keyName === 'string') {
this.attributes[definition.keyName] = definition;
}
} else if (isBlot) {
if (definition.className) {
this.classes[definition.className] = definition;
}
if (definition.tagName) {
if (Array.isArray(definition.tagName)) {
definition.tagName = definition.tagName.map((tagName: string) => {
return tagName.toUpperCase();
});
} else {
definition.tagName = definition.tagName.toUpperCase();
}
const tagNames = Array.isArray(definition.tagName)
? definition.tagName
: [definition.tagName];
tagNames.forEach((tag: string) => {
if (this.tags[tag] == null || definition.className == null) {
this.tags[tag] = definition;
}
});
}
}
return definition;
});
}
}

View File

@@ -0,0 +1,18 @@
enum Scope {
TYPE = (1 << 2) - 1, // 0011 Lower two bits
LEVEL = ((1 << 2) - 1) << 2, // 1100 Higher two bits
ATTRIBUTE = (1 << 0) | LEVEL, // 1101
BLOT = (1 << 1) | LEVEL, // 1110
INLINE = (1 << 2) | TYPE, // 0111
BLOCK = (1 << 3) | TYPE, // 1011
BLOCK_BLOT = BLOCK & BLOT, // 1010
INLINE_BLOT = INLINE & BLOT, // 0110
BLOCK_ATTRIBUTE = BLOCK & ATTRIBUTE, // 1001
INLINE_ATTRIBUTE = INLINE & ATTRIBUTE, // 0101
ANY = TYPE | LEVEL,
}
export default Scope;

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"lib": ["es2015", "dom", "dom.iterable"],
"outDir": "./dist/esm",
"declaration": true,
"declarationDir": "./dist/typings",
"strict": true,
"moduleResolution": "Bundler",
"noUnusedLocals": true,
"noUnusedParameters": true,
"verbatimModuleSyntax": true
},
"include": ["src", "tests"]
}