The design of the data model is the core foundation of the editor, which directly affects the selection model,DOM
Design of modules such as model and state management. For examplequill
The selection model inindex + len
expression, andslate
In the middle, it isanchor + focus
These are all based on the design of data models. Therefore, our rich text editor that is implemented from zero needs to start with the design of the data model, and then we can gradually implement other modules.
- Open source address:/WindRunnerMax/BlockKit
- Online editing:/BlockKit/
- Project Notes:/WindRunnerMax/BlockKit/blob/master/
Articles on implementing rich text editor projects from scratch:
- Feeling like nothing, I'm ready to try to write a rich text editor from scratch
- Implementing Rich Text Editor from Zero #2 - Editor Architecture Design Based on MVC Mode
- Implementing Rich Text Editor #3 from Zero - Linear Data Structure Model Based on Delta
Delta
In previous architectural designs, we have mentioned that the flat data structure and independent subcontracting design we implement can be more conveniently processed whether it is operated in the editor or data analysis on the server side. In comparison, nested data structures can be better alignedDOM
Expression, however, the operation of data becomes more complicated.
Therefore, in the design of data structure, we are based onquill
ofdelta
The structure has been transformed. The most important part is to transform it intoimmutable
The implementation of the editor is actually a state that is maintained rather than itselfdelta
structure. And simplifies the expression of the entire data model, making it complexinsert
andAttribute
If the type is reduced, the complexity of its operation logic will also be reduced.
delta
It is a simple and powerful format used to describe the content of the document and its changes. This format is basedJSON
, not only easy to read, but also easy to analyze by machine. By usingdelta
The content described can accurately describe the content and format information of any rich text document, avoidingHTML
common ambiguity and complexity in.
delta
It consists of a series of operations that describe changes made to the document. Common operations includeinsert
、delete
、retain
. It should be noted that these operations do not depend on the specific index position, which always describes the changes in the current index position and can beretain
To move the pointer position.
delta
It can represent both the entire document and the changes made to the document. Then here we willdelta
The main class objects and related operation logic are described, especially in the editor's actual application scenarios, as well as the main transformations and related type declarations.
insert
insert
The method is to insert the data intodelta
This is the operationdelta
. When describing the entire document content, the content of the entire data should beinsert
. The first parameter is the text content to be inserted, and the second parameter is an optional attribute object that describes the format information of the text.
const delta = new Delta();
("123").insert("567", { a: "1" });
// [{"insert":"123"},{"insert":"567","attributes":{"a":"1"}}]
Originalinsert
Parameters are object typesEmbed
Structure, this structure can be expressedImage
、Video
、Mention
etc. non-text structure data, and attributesAttributeMap
The parameters areRecord<string, unknown>
Type, which is used to describe complex attribute values.
Here we have streamlined it,insert
Parameters are only supportedstring
Type, and specificschema
Then it is defined when the editor is initialized, and the format information is collected fromAttrs
Description in. andAttributeMap
Change toRecord<string, string>
type, and can avoid suchCloneDeep
、isEqual
etc. for the implementation of complex data structures.
ActuallyEtherPad
Zhong means generalAttribute
that is[string, string]
Type, we also used a similar design here. In this infrastructure design, we recommend placing the attribute value flat onattributes
In attributes, instead of using a single attribute value askey
, nested all attribute values invalue
middle.
export interface AttributeMap {
[key: string]: string;
}
delta
The entire name is usually used to describe changes, so in addition to describing the entire document content, it is of course also possible to describe changes in the document content. However, the application changes need to be usedcompose
, Let's look at the description of this method later.
const delta1 = new Delta().insert("123");
const delta2 = new Delta().insert("456");
(delta2); // [{"insert":"456123"}]
delete
delete
The method describes the length of the deleted content. Since the above definition, our current content is all text and embedded in the original data definition.Embed
The length of1
。
const delta = new Delta().insert("123");
(new Delta().delete(1)); // [{"insert":"23"}]
Actually, this is a relatively interesting thing, throughdelete
When describing the change, it is impossible to know what exactly was deleted. Then in this case,invert
When additional data is needed to constructinsert
operate. Similar scenes inOT-JSON
The content description is written directly to theop
, so it can be directly based onop
Come oninvert
operate.
const delta = new Delta().insert("123");
const del = new Delta().delete(1);
const invert1 = (delta); // [{"insert":"1"}]
const delta2 = (del); // [{"insert":"23"}]
(invert1); // [{"insert":"123"}]
retain
retain
The method describes the length of the content reserved, in other words, this operation can be used to move the pointer.
const delta = new Delta().insert("123");
(new Delta().retain(1).insert("a")); // [{"insert":"1a23"}]
at the same timeretain
Operations can also be used to modify the property value of the content. In addition, if you want to delete a certain property, you only need tovalue
Set as""
Just do it.
const delta = new Delta().insert("123");
const d2 = new Delta().retain(1).retain(1, { "a": "1" });
const d3 = (d2); // [{"insert":"1"},{"insert":"2","attributes":{"a":"1"}},{"insert":"3"}]
(new Delta().retain(1).retain(1, { "a": "" })); // [{"insert":"123"}]
push
push
The method is as mentioned aboveinsert
、delete
、retain
The basic method of dependency is to push content intodelta
Maintained in the array. The very important part of the implementation here isop
Merge of the attribute value, when the attribute value is the same, it needs to be merged into a singleop
。
const delta = new Delta();
({ insert: "123" }).push({ insert: "456" }); // [{"insert": "123456"}]
Of course it's not justinsert
The operations will be merged, fordelete
、retain
The same goes for the operation. The merge operation here is based onop
ofattributes
attribute value, ifattributes
Different attribute values will be considered differentop
, will not be merged automatically.
const delta = new Delta();
({ delete: 1 }).push({ delete: 1 }); // [{"delete": 2}]
const delta2 = new Delta();
({ retain: 1 }).push({ retain: 1 }); // [{"retain": 2}]
const delta3 = new Delta();
({ retain: 1 }).push({ retain: 1, attributes: { a: "1"} }); // [{"retain": 1}, {"retain": 1, "attributes": {"a": "1"}}]
slice
slice
The method is used to interceptdelta
This method is based onop
oflength
The attribute value is intercepted.
const delta = new Delta().insert("123").insert("456", {a: "1"});
(2, 4); // [{"insert":"3"},{"insert":"4","attributes":{"a":"1"}}]
eachLine
eachLine
Methods are used to iterate the entire line by rowdelta
, Our overall data structure is linear, but the editorDOM
It needs to be divided by row, so it is based on\n
It is a relatively common operation to divide the lines.
This method is very important for the initialization of the editor. After the initialization is completed, our changes need to be implemented based on the state, rather than through this method every time. Here we have also transformed it, originaleachLine
Method is not to carry\n
node.
const delta = new Delta().insert("123\n456\n789");
((line, attributes) => {
(line, attributes);
});
// [{insert:"123"},{insert:"\n"}] {}
// [{insert:"456"},{insert:"\n"}] {}
// [{insert:"789"},{insert:"\n"}] {}
diff
diff
Methods are used to compare twodelta
The difference between this method is actually based on plain textmyers diff
To achieve it. Bydelta
Convert to plain text, indiff
Afterwards, we continue to select shorter operating parts to achievedelta
Betweendiff
。
In fact, in our implementation, we can completelydiff
The method is independent, the only references to the external one herefast-diff
rely. existquill
middlediff
is necessary because it is a completely uncontrollable input method, and the input of text depends on theDOM
Textdiff
to implement, and our input is dependentbeforeinput
Semi-controlled input of events, so they are not strongly dependentdiff
。
const delta1 = new Delta().insert("123");
const delta2 = new Delta().insert("126");
(delta2); // [{"retain":2},{"insert":"6"},{"delete":1}]
chop
chop
Method for cutting the endretain
Operation, when there is the endretain
And when there is no attribute operation, it itself has no meaning, so this method can be called to check and remove.
const delta = new Delta().insert("123").retain(1);
(); // [{"insert":"123"}]
compose
compose
Methods can be used to transfer twodelta
Merge into onedelta
Specifically,B
ofdelta
Operations apply toA
ofdelta
On, the return is a new onedelta
Object. Of course, in our implementation, the originalDelta
Class rewrittencompose
Method, doneimmutable
。
compose
There are many application scenarios in the editor, such as input events, content pasting, historical operations, etc., similar to the editorapply
Method, equivalent to changing the application content.
const delta1 = new Delta().insert("123");
const delta2 = new Delta().retain(1).delete(1);
(delta2); // [{"insert":"13"}]
invert
invert
The method is todelta
This method is very important in historical operations because it is itselfundo
It is necessary to reverse the current operation. In addition, in realizingOT
oflocal-cs
middle,invert
It is also a very important method.
It is worth noting that the above mentioneddelete
Operation andretain
The original content will not be recorded when the operation is executed, soinvert
It needs originaldelta
As a data source, please note thatdelta
It was the originaldelta
, notinvert
The latterdelta
。
const delta = new Delta().insert("123");
const del = new Delta().delete(1);
const invert1 = (delta); // [{"insert":"1"}]
const delta2 = (del); // [{"insert":"23"}]
(invert1); // [{"insert":"123"}]
concat
concat
Methods can connect twodelta
Go to the new onedelta
middle. This operation iscompose
different,compose
YesB
The operation ofA
Up, andconcat
Then it isB
The operation is added toA
After the operation.
const delta1 = new Delta().insert("123");
const delta2 = new Delta().insert("456");
(delta2); // [{"insert":"456123"}]
(delta2); // [{"insert":"123456"}]
transform
transform
The method is to implement the operationOT
The basis of collaboration, even if collaborative editing is not implemented, this part of the implementation will be required in the history operation module in the editor. Suppose we have users nowA[uid:1]
And usersB[uid:2]
, at this time weuid
Define priority,A
The operation priority is higher thanB
, and the current document content is12
。
If it is in collaboration,b'=(b)
It means, assumptiona
andb
All from the samedraft
Branched out, thenb'
It's assumptiona
It has been applied, at this timeb
Need to be ina
Transformed fromb'
Only directly apply, we can also understand it astransform
Solveda
Operation is correctb
The impact of operation.
Then let's assumeA
exist12
The rear position has been insertedA
character,B
exist12
The rear position has been insertedB
character. If you perform a coordinated operation, then the two are equivalent to inserting characters at the same position at the same time. If you apply them directly without performing operation transformation, the data of the two will conflict.A
The data is12BA
,andB
The data is12AB
, so you need to convert first and then apply.
// User A
const base = new Delta().insert("12");
const delta = new Delta().retain(2).insert("A");
let current = (delta); // 12A
// Accept Remote B
const remote = new Delta().retain(2).insert("B");
// ob1=OT(oa, ob)
const remote2 = (remote, true); // [{"retain":3},{"insert":"B"}]
current = (remote2); // 12AB
// User B
const base = new Delta().insert("12");
const delta = new Delta().retain(2).insert("B");
let current = (delta); // 12B
// Accept Remote A
const remote = new Delta().retain(2).insert("A");
// oa2=OT(ob, oa)
const remote2 = (remote, false); // [{"retain":2},{"insert":"A"}]
current = (remote2); // 12AB
transformPosition
transformPosition
The method is used to convert the specified position. The main scenario of this method is to transform the position of the selection/cursor in the editor, for example, the cursor is at this time.1
Later, the structuredelta
exist1
If the content has been added before, the cursor needs to follow the movement.
const delta = new Delta().retain(5).insert("a");
(4); // 4
(5); // 6
OpIterator
OpIterator
The class defines an iterator for iteratingdelta
In-houseop
operate. Iterators are used in large quantitiesdiff
、compose
、transform
In other methods, it is important to note that the iterator is callednext
No crossing timeop
, even if passedlength
Greater than the current oneop
length.
const delta = new Delta()
.insert("Hello", { bold: "true" })
.insert(" World", { italic: "true" });
.retain(3);
(2); // { insert: "He", attributes: { bold: "true" } }
(10); // { insert: "llo", attributes: { bold: "true" } }
EtherPad
EtherPad
It is also a very excellent collaborative editor, and the built-in data structure is also linear, and the overall description of the document is calledClientVars
, data structure changes are calledChangeSet
. The implementation of the collaborative algorithm isEasySync
, and the documentation also provides a detailed description of how to perform server scheduling.
ClientVars/Changeset
Also based onJSON
The data format is used to describe the content of the document and its changes. But it doesn't look likeDelta
So clearly expressed,JSON
The structure is mainlyAttributePool
Inside, the expression of text content is a plain text structure.
Document description
Document content is usedClientVars
The data structure ofapool
Text attribute pool,text
Text content,attribs
Attribute description. In the following example, we describe the content of the title, bold, italic, plain text, so the contents of this are shown below.
({
initialAttributedText: {
text: "short description\n*Heading1\ntext\n*Heading2\nbold italic\nplain text\n\n",
attribs: "*0|1+i*0*1*2*3+1*0|2+e*0*4*2*3+1*0|1+9*0*5+4*0+1*0*6+6*0|2+c|1+1",
},
apool: {
numToAttrib: {
"0": ["author", "a.XYe86foM7oYgmpuu"],
"1": ["heading", "h1"],
"2": ["insertorder", "first"],
"3": ["lmkr", "1"],
"4": ["heading", "h2"],
"5": ["bold", "true"],
"6": ["italic", "true"],
},
nextNum: 7,
},
});
This content looks more complicated directly, and of course it is actually quite complicated.apool
It is a property pool where all decorations for text content are stored here, that is, in itnumToAttrib
Property stored[string, string]
value,nextNum
It is the next index to be placed.text
It is the content of plain text, which is equivalent to the content of the plain text of the document at this time.attribs
It is based ontext
plain text content and obtainapool
The attribute value in the decorative text content is equivalent to the encoding.
thereforeattribs
Need to be taken out separately to analyze.*n
Indicates taking the firstn
Applying attributes to text usually requires cooperation|n
and+n
Use attributes,|n
Indicates impactn
OK, only for\n
The attribute needs to be used.+n
It means taking outn
The number of characters is equivalent toretain
Operations can not only move the pointer, but also use it to hold property changes. It is particularly important to note that|m
It will not appear alone, it will always be with+n
Express them together, express themn
Exist in charactersm
A newline, and the last applied character must be\n
。
also,EasySync
The numbers inside are all36
Category, so here+i/+e
None of them are special symbols, they need to be used0-9
To represent the numbers0-9
characters, and10-35
That meansa-z
,For example+i
that isi - a = 8 => 8 + 10 = 18
。
-
*0
Indicates taking outauthor
attributes,|1+i
It means it is applied toi
Length is18
, the characters areshort description\n
, due to its inclusion\n
Then define|1
。 -
*0*1*2*3
Indicates before removal4
attributes,+1
Indicates applying it1
characters, i.e.*
Characters, inEasySync
This character at the beginning of the line bears the line attribute, not placed\n
middle. -
*0
Indicates taking outauthor
attributes,|2+e
It means it has been appliede
Length is14
, the characters areHeading1\ntext\n
, which contains two\n
Then define|2
。 -
*0*4*2*3
Indicates the removal of relevant attributes.+1
Indicates applying it1
characters, i.e.*
Characters represent line attribute content. -
*0|1+9
Indicates taking outauthor
attributes,+9
Indicates applying it9
characters, i.e.Heading2\n
, at the end is\n
Then define|1
。 -
*0*5+4
Indicates the removal of bold attributes, and apply4
characters, i.e.bold
。 -
*0+1
Indicates taking outauthor
properties, application1
A single character is a space. -
*0*6+6
Indicates the removal of italics and other properties, apply6
characters, i.e.italic
。 -
*0|2+c
Indicates the removal of relevant attributes and apply12
One character is\nplain text\n
There are two\n
Then define|2
。 -
|1+1
Indicates the end\n
Attributes, inEasySync
This character at the end of the line needs to be defined separately.
Change description
OT
The reference atomic implementation of operation transformation isinsert
、delete
、retain
Three operations, thenChangeSet
The content description is naturally similar, but the data change description is not the samedelta
The structure is so clear, but a set of data structure descriptions are specially designed.
Documents are initially created or importedClientVars
, and then every time the document content is modified, it will be generatedChangeSet
. For the above three operations, three symbols correspond to=
expressretain
、+
expressinsert
、-
expressdelete
, the combination of these three symbols can describe changes in the document content, in addition to additional definitions:
-
Z
: The first letter isMagicNumber
, denoted as a sign bit. -
:N
: The original content length of the document isN
。 -
>N
: The final document length will be longer than the original document lengthN
。 -
<N
: The final document length will be shorter than the original document lengthN
。 -
+N
: Actual execution of the operation means that it has been insertedN
characters. -
-N
: Actual operation means that the operation has been deletedN
characters. -
=N
: Actual execution of the operation means that the operation is retainedN
characters, move pointer or apply attributes. -
|N
: It means it has affectedN
line, consistent with the above document description, needs to be+/-/=N
The length of the operation includesN
indivual\n
, and the end operation must be\n
. The end of the document\n
If you need to express it, you must use it|1=1
express. -
*I
: Indicates the application attribute,I
forapool
The index value in a+
、=
or|
There could be any number before*
operate. -
$
: Indicates the end symbol, used to markOperation
End of part. -
char bank
: for storageinsert
Operation of specific character contents and use them in sequence when performing the insertion operation.
The same example as above already exists in the current documentexist text\n\n
The text content of the above content is then pasted into the document, thenUser ChangeSet
The changes are described as follows:
({
changeset:
"Z:c>1t|1=b*0|1+i*0*1*2*3+1*0|2+e*0*4*2*3+1*0|1+9*0+5*0*5+6*0|1+1*0+a$short description\n*Heading1\ntext\n*Heading2\nbold italic\nplain text",
apool: {
numToAttrib: {
"0": ["author", "a.XYe86foM7oYgmpuu"],
"1": ["heading", "h1"],
"2": ["insertorder", "first"],
"3": ["lmkr", "1"],
"4": ["heading", "h2"],
"5": ["italic", "true"],
},
nextNum: 6,
},
});
-
Z
expressMagicNumber
, that is, the symbol bit. -
c
Indicates that the original content of the document is12
,Right nowexist text\n\n
Content length. -
>1t
It means that the final document will be longer than the original content1t
,36
Priority conversion1t
for64
, specificallychar bank
index. -
|1=b
Indicates that the length of the moving pointer isb
, convert length to11
, the text content isexist text\n
, at the end\n
definition|1
。 -
*0|1+i
Indicates fromapool
Take out0
Properties, applicationi
Convert length to18
, the text content isshort description\n
, at the end\n
definition|1
。 -
*0*1*2*3+1
Indicates taking out4
attributes applied to1
, the text content is*
, specifically, it is the start mark of the line attribute. -
*0|2+e
Indicates taking out0
Properties, applicatione
Convert length to14
, the text content isHeading1\ntext\n
, at the end\n
And includes two\n
definition|2
。 -
*0*4*2*3+1
Indicates taking out4
attributes applied to1
, the text content is*
, also is the start mark of the row attribute. -
*0|1+9
Indicates taking out0
Attribute, the application length is9
, the text content isHeading2\n
, at the end\n
definition|1
。 -
*0+5
Indicates taking out0
Attribute, the application length is5
, the text content isbold
。 -
*0*5+6
It indicates that the properties such as italics are removed, and the application length is6
, the text content isitalic
。 -
*0|1+1
Indicates taking out0
Attribute, the application length is1
, at the end\n
Then define|1
, the text content is\n
。 -
*0+a
Indicates taking out0
Attribute, the application length isa
Right now10
, the text content isplain text
。 -
$
Indicates the end symbol, and subsequent content symbols arechar bank
, the last\n
Usually no representation is needed, even if it is expressed, it is required.|1=1
Individually indicated.
Slate
slate
The data structure and the design of the selection are almost completely alignedDOM
The structure, and the data structure design is not independent, and is also based onJSON
The structure of , very similar to the low-code structural design. The operation transformation is directly inslate
Core modulesTransform
and the implementation of position-related operation transformation is scattered inPoint
、Path
in object.
[
{
type: "paragraph",
children: [
{ text: "This is editable " },
{ text: "rich", bold: true },
{ text: " text." },
],
},
{ type: "block-quote", children: [{ text: "A wise quote." }] },
];
Operation
Also based onOT
Implement operation transformation algorithm, linear data structure only requiresinsert
、delete
、retain
Three basic operations can be achieved, andslate
The middle is realized9
Atomic operations are used to describe changes, including text processing, node processing, selection transformation operations, etc.
-
insert_node
: Insert node. -
insert_text
: Insert text. -
merge_node
: Merge nodes. -
move_node
: Mobile node. -
remove_node
: Remove node. -
remove_text
: Remove text. -
set_node
: Set the node. -
set_selection
: Set selection. -
split_node
: Split nodes.
In fact, it's okay to only implement the application, the correspondinginvert
、transform
It will be more complicated. existslate
In-houseinverse
Related operations areImplemented, position-related
transform
exist、
There are related implementations in this.
In fact, these operations are usually not called directly in the editor.slate
These most basic operations are encapsulated and implementedTransforms
Module. Many specific operations are implemented in this module, such asinsertNodes
、liftNodes
、mergeNodes
、moveNodes
、removeNodes
Wait, the operation here is far more than9
Types.
-
insertFragment
: Insert a fragment of a node at the specified location. -
insertNodes
: Insert node at the specified location. -
removeNodes
: Delete nodes at the location specified in the document. -
mergeNodes
: Merge with the previous node of the same level at a certain node. -
splitNodes
: Split nodes at a specified location in a node. -
wrapNodes
: Wrap a layer of nodes at a specified location in a node. -
unwrapNodes
: Unpack a layer of wrapping node at a specified location in a node. -
setNodes
: Set node properties at a specified location in a node. -
unsetNodes
: Cancel the node attribute at a specified location in a node. -
liftNodes
: Raise a layer of node at a specified location in a node. -
moveNodes
: Move the node at the specified location in the document. -
collapse
: Collapses the selection into caret. -
select
: Actively set the selection position. -
deselect
: Cancel the selection location. -
move
: Move the selection location. -
setPoint
: Sets the one-sided position of the selection. -
setSelection
: Set the new selection location. -
delete
: Delete the selection content. -
insertText
: Insert text in the selection position. -
transform
: On the editorimmutable
Executeop
。
OT-JSON
Similarly, inOT-JSON(json0)
Implemented11
In rich text scenesSubType
If you still need to expand, then naturally more operations are needed to describe the changes. Therefore, in fact,JSON
Nested data formats to describe content changes are much more complicated than linear operations.
existslate
The basics of encapsulating the editor by itselfop
If it is inOT-JSON
Packaging based onTransforms
If so, for implementationOT
The collaboration will be more convenient.ShareDB
All collaborative frameworks need to be referencedOTTypes
The definition of . Of course, basedCRDT
The implementation of collaboration looks easier to handle.
-
{p:[path], na:x}
: In the specified path[path]
Add valuex
Value. -
{p:[path,idx], li:obj}
: On the list[path]
Index ofidx
Insert object beforeobj
。 -
{p:[path,idx], ld:obj}
: From the list[path]
Index ofidx
Delete the object inobj
。 -
{p:[path,idx], ld:before, li:after}
: Use objectsafter
Replacement list[path]
Indicesidx
Object ofbefore
。 -
{p:[path,idx1], lm:idx2}
: Add the list[path]
Indicesidx1
The object to the indexidx2
place. -
{p:[path,key], oi:obj}
: toward the path[path]
Add keys to the object inkey
and objectsobj
。 -
{p:[path,key], od:obj}
: From the path[path]
Delete key in object inkey
and valueobj
。 -
{p:[path,key], od:before, oi:after}
: Use objectsafter
Replace path[path]
Middle keykey
Object ofbefore
。 -
{p:[path], t:subtype, o:subtypeOp}
: For path[path]
The object application type int
Suboperation ofo
, subtype operation. -
{p:[path,offset], si:s}
: On the path[path]
Offset of stringoffset
Insert strings ats
, use subtypes internally. -
{p:[path,offset], sd:s}
: From the path[path]
Offset of stringoffset
Delete stringss
, use subtypes internally.
Summarize
The design of data structure is very important. For the editor, the design of data structure directly affects the selection model,DOM
Design of modules such as model and state management. Here we talked about a lot of data structure design.Delta
、Changeset
linear structure,Slate
The nested structure of each data has its own design and considerations.
Then after selecting the data structure, you can implement various modules of the editor on this basis. Next, we will start from the data model, design the representation of the selection model, and then synchronize the browser selection and the editor selection model on this basis. The selection model is used as the goal of the operation to realize the basic operations of the editor, such as insertion, deletion, formatting and other operations.
One question every day
- /WindRunnerMax/EveryDay
refer to
- /slab/delta/blob/main/src/
- /slab/delta/blob/main/src/
- /ether/etherpad-lite/tree/develop/doc/public/easysync
- /ether/etherpad-lite/blob/develop/src/static/js/
- /ether/etherpad-lite/blob/develop/src/static/js/
- /ianstormtaylor/slate/blob/main/packages/slate/src/interfaces/
- /ianstormtaylor/slate/blob/main/packages/slate/src/interfaces/transforms/