-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add unstable serialization logic for node JSON parsing #2157
Changes from all commits
808f985
8e26665
e2426b5
ac4ef5a
384f5f3
fd70a36
020b563
6c56fc7
2b09c2c
6b81a49
79b1a99
784693c
fd4b8ea
fbcb901
78973c9
68d1357
773f7f4
b0236ee
ff30750
e8f8ba3
4798efa
cbf028f
e5a4ec8
a7fa1f7
c019071
73ad88c
cf84bfb
4526b4a
7f1770e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,8 @@ import type { | |
NodeKey, | ||
ParagraphNode, | ||
RangeSelection, | ||
SerializedElementNode, | ||
SerializedTextNode, | ||
} from 'lexical'; | ||
|
||
import * as Prism from 'prismjs'; | ||
|
@@ -39,6 +41,7 @@ import { | |
mergeRegister, | ||
removeClassNamesFromElement, | ||
} from '@lexical/utils'; | ||
import {Spread} from 'globals'; | ||
import { | ||
$createLineBreakNode, | ||
$createParagraphNode, | ||
|
@@ -59,6 +62,24 @@ import { | |
|
||
const DEFAULT_CODE_LANGUAGE = 'javascript'; | ||
|
||
type SerializedCodeNode = Spread< | ||
{ | ||
language: string | null | undefined; | ||
type: 'code'; | ||
version: 1; | ||
}, | ||
SerializedElementNode | ||
>; | ||
|
||
type SerializedCodeHighlightNode = Spread< | ||
{ | ||
highlightType: string | null | undefined; | ||
type: 'code-highlight'; | ||
version: 1; | ||
}, | ||
SerializedTextNode | ||
>; | ||
|
||
const mapToPrismLanguage = ( | ||
language: string | null | undefined, | ||
): string | null | undefined => { | ||
|
@@ -99,6 +120,11 @@ export class CodeHighlightNode extends TextNode { | |
); | ||
} | ||
|
||
getHighlightType(): string | null | undefined { | ||
const self = this.getLatest<CodeHighlightNode>(); | ||
return self.__highlightType; | ||
} | ||
|
||
createDOM(config: EditorConfig): HTMLElement { | ||
const element = super.createDOM(config); | ||
const className = getHighlightThemeClass( | ||
|
@@ -134,6 +160,25 @@ export class CodeHighlightNode extends TextNode { | |
return update; | ||
} | ||
|
||
static importJSON( | ||
serializedNode: SerializedCodeHighlightNode, | ||
): CodeHighlightNode { | ||
const node = $createCodeHighlightNode(serializedNode.highlightType); | ||
node.setFormat(serializedNode.format); | ||
node.setDetail(serializedNode.detail); | ||
node.setMode(serializedNode.mode); | ||
node.setStyle(serializedNode.style); | ||
return node; | ||
} | ||
|
||
exportJSON(): SerializedCodeHighlightNode { | ||
return { | ||
...super.exportJSON(), | ||
highlightType: this.getHighlightType(), | ||
type: 'code-highlight', | ||
}; | ||
} | ||
|
||
// Prevent formatting (bold, underline, etc) | ||
setFormat(format: number): this { | ||
return this; | ||
|
@@ -266,6 +311,22 @@ export class CodeNode extends ElementNode { | |
}; | ||
} | ||
|
||
static importJSON(serializedNode: SerializedCodeNode): CodeNode { | ||
const node = $createCodeNode(serializedNode.language); | ||
node.setFormat(serializedNode.format); | ||
node.setIndent(serializedNode.indent); | ||
node.setDirection(serializedNode.direction); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this API solve the problem with new fields/deprecated fields? Is this where you'd add a default in some cases? |
||
return node; | ||
} | ||
|
||
exportJSON(): SerializedCodeNode { | ||
return { | ||
...super.exportJSON(), | ||
language: this.getLanguage(), | ||
type: 'code', | ||
}; | ||
} | ||
|
||
// Mutation | ||
insertNewAfter( | ||
selection: RangeSelection, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,10 +15,20 @@ import type { | |
LexicalNode, | ||
NodeKey, | ||
RangeSelection, | ||
SerializedElementNode, | ||
} from 'lexical'; | ||
|
||
import {addClassNamesToElement} from '@lexical/utils'; | ||
import {$isElementNode, createCommand, ElementNode} from 'lexical'; | ||
import invariant from 'shared/invariant'; | ||
|
||
export type SerializedLinkNode = { | ||
...SerializedElementNode, | ||
type: 'link', | ||
url: string, | ||
version: 1, | ||
... | ||
}; | ||
|
||
export class LinkNode extends ElementNode { | ||
__url: string; | ||
|
@@ -67,6 +77,22 @@ export class LinkNode extends ElementNode { | |
}; | ||
} | ||
|
||
static importJSON(serializedNode: SerializedLinkNode): LinkNode { | ||
const node = $createLinkNode(serializedNode.url); | ||
node.setFormat(serializedNode.format); | ||
node.setIndent(serializedNode.indent); | ||
node.setDirection(serializedNode.direction); | ||
return node; | ||
} | ||
|
||
exportJSON(): SerializedElementNode { | ||
return { | ||
...super.exportJSON(), | ||
type: 'link', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Random question, but I've noticed that in some places we use Also another random question that's related, do we have anything in place to prevent you from loading two nodes with the same type? If not, we should throw or you'd get some funky behavior here if one of the plugins you load in adds a node that happens to collide with another. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe you'd want to be able to "override" a node by calling another plugin after? Not sure, but feels potentially confusing and hard to debug. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Great point! The reason why |
||
url: this.getURL(), | ||
}; | ||
} | ||
|
||
getURL(): string { | ||
return this.getLatest().__url; | ||
} | ||
|
@@ -119,6 +145,13 @@ export function $isLinkNode(node: ?LexicalNode): boolean %checks { | |
return node instanceof LinkNode; | ||
} | ||
|
||
export type SerializedAutoLinkNode = { | ||
...SerializedLinkNode, | ||
type: 'autolink', | ||
version: 1, | ||
... | ||
}; | ||
|
||
// Custom node type to override `canInsertTextAfter` that will | ||
// allow typing within the link | ||
export class AutoLinkNode extends LinkNode { | ||
|
@@ -131,6 +164,28 @@ export class AutoLinkNode extends LinkNode { | |
return new AutoLinkNode(node.__url, node.__key); | ||
} | ||
|
||
static importJSON( | ||
serializedNode: SerializedLinkNode | SerializedAutoLinkNode, | ||
): AutoLinkNode { | ||
invariant( | ||
serializedNode.type !== 'autolink', | ||
'Incorrect node type received in importJSON for %s', | ||
this.getType(), | ||
); | ||
const node = $createAutoLinkNode(serializedNode.url); | ||
node.setFormat(serializedNode.format); | ||
node.setIndent(serializedNode.indent); | ||
node.setDirection(serializedNode.direction); | ||
return node; | ||
} | ||
|
||
exportJSON(): SerializedElementNode { | ||
return { | ||
...super.exportJSON(), | ||
type: 'autolink', | ||
}; | ||
} | ||
|
||
insertNewAfter(selection: RangeSelection): null | ElementNode { | ||
const element = this.getParentOrThrow().insertNewAfter(selection); | ||
if ($isElementNode(element)) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need version numbers? AFAIK it was proposed by the Flow team but later you pointed out you still had problems with inheritance (and refinement?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I mostly solved the problems with inheritance, but the trade-off is that the argument passed to importJSON will have to be a union of all the serialized types of the superclasses in order to conform to the method signature on the superclass in Flow. See the "try flow" link in the PR description for an example.
As far as refinement goes, versions are actually the solution rather than a problem. If you look at the "try flow" link in the playground, you can see how they form a disjoint union in Flow (this can't really be done in TS) and can be used to refine the type of the serializedNode argument in importJSON.
Version is less important in TypeScript, since you can't use a disjoint union to refine. Instead, you have to use type guards. More info in the PR description.