From 1ad81933f458d1bd2527c9d4a7ab41eb45373304 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 6 Feb 2024 09:37:26 -0600 Subject: [PATCH 01/33] tsconfig setup --- packages/kg-default-nodes/.gitignore | 1 + .../lib/KoenigDecoratorNode.js | 11 ------ .../lib/KoenigDecoratorNode.ts | 12 +++++++ ...g-default-nodes.js => kg-default-nodes.ts} | 0 packages/kg-default-nodes/package.json | 11 +++--- packages/kg-default-nodes/tsconfig.json | 34 +++++++++++++++++++ yarn.lock | 2 +- 7 files changed, 54 insertions(+), 17 deletions(-) delete mode 100644 packages/kg-default-nodes/lib/KoenigDecoratorNode.js create mode 100644 packages/kg-default-nodes/lib/KoenigDecoratorNode.ts rename packages/kg-default-nodes/lib/{kg-default-nodes.js => kg-default-nodes.ts} (100%) create mode 100644 packages/kg-default-nodes/tsconfig.json diff --git a/packages/kg-default-nodes/.gitignore b/packages/kg-default-nodes/.gitignore index 8e6861d207..c502d7033e 100644 --- a/packages/kg-default-nodes/.gitignore +++ b/packages/kg-default-nodes/.gitignore @@ -1,2 +1,3 @@ cjs/ es/ +build/ \ No newline at end of file diff --git a/packages/kg-default-nodes/lib/KoenigDecoratorNode.js b/packages/kg-default-nodes/lib/KoenigDecoratorNode.js deleted file mode 100644 index f6a6537ade..0000000000 --- a/packages/kg-default-nodes/lib/KoenigDecoratorNode.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -/* c8 ignore start */ -import {DecoratorNode} from 'lexical'; - -export class KoenigDecoratorNode extends DecoratorNode { -} - -export function $isKoenigCard(node) { - return node instanceof KoenigDecoratorNode; -} -/* c8 ignore end */ diff --git a/packages/kg-default-nodes/lib/KoenigDecoratorNode.ts b/packages/kg-default-nodes/lib/KoenigDecoratorNode.ts new file mode 100644 index 0000000000..700310f23a --- /dev/null +++ b/packages/kg-default-nodes/lib/KoenigDecoratorNode.ts @@ -0,0 +1,12 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +/* c8 ignore start */ +import {DecoratorNode} from 'lexical'; +import type {LexicalNode} from 'lexical'; + +export class KoenigDecoratorNode extends DecoratorNode { +} + +export function $isKoenigCard(node: LexicalNode): boolean { + return node instanceof KoenigDecoratorNode; +} +/* c8 ignore end */ \ No newline at end of file diff --git a/packages/kg-default-nodes/lib/kg-default-nodes.js b/packages/kg-default-nodes/lib/kg-default-nodes.ts similarity index 100% rename from packages/kg-default-nodes/lib/kg-default-nodes.js rename to packages/kg-default-nodes/lib/kg-default-nodes.ts diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index 4dc97c1389..ac5ec34219 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -6,15 +6,15 @@ "license": "MIT", "main": "cjs/kg-default-nodes.js", "module": "es/kg-default-nodes.js", - "source": "lib/kg-default-nodes.js", + "source": "build/kg-default-nodes.js", "scripts": { - "dev": "rollup -c -w", - "build": "rollup -c", + "dev": "tsc && rollup -c -w", + "build": "tsc && rollup -c", "prepare": "NODE_ENV=production yarn build", "pretest": "yarn build", "test:unit": "NODE_ENV=testing c8 --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", "test": "yarn test:unit", - "lint:code": "eslint *.js lib/ --ext .js --cache", + "lint:code": "eslint *.js lib/ --ext .js --cache && eslint *.ts lib/ --ext .ts --cache", "lint": "yarn lint:code && yarn lint:test", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" }, @@ -41,7 +41,8 @@ "prettier": "^3.0.1", "rollup": "4.9.5", "should": "13.2.3", - "sinon": "17.0.1" + "sinon": "17.0.1", + "typescript": "^5.3.3" }, "dependencies": { "@lexical/clipboard": "0.12.2", diff --git a/packages/kg-default-nodes/tsconfig.json b/packages/kg-default-nodes/tsconfig.json new file mode 100644 index 0000000000..e4cbc7f878 --- /dev/null +++ b/packages/kg-default-nodes/tsconfig.json @@ -0,0 +1,34 @@ +{ + // required for tests to find module declarations in the src folder + "ts-node": { + "files": true + }, + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + + /* Modules */ + "rootDir": "lib", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "outDir": "build", /* Specify an output folder for all emitted files. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + }, + "include": ["lib/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 04a20f40da..47e6591164 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19043,7 +19043,7 @@ typescript@5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -typescript@5.3.3, "typescript@>=3 < 6": +typescript@5.3.3, "typescript@>=3 < 6", typescript@^5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== From 177295c331e5b11c2cdebad0e8c5fe98469f82ff Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 6 Feb 2024 11:19:02 -0600 Subject: [PATCH 02/33] update c8 coverage --- packages/kg-default-nodes/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index ac5ec34219..01956c7647 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -12,7 +12,7 @@ "build": "tsc && rollup -c", "prepare": "NODE_ENV=production yarn build", "pretest": "yarn build", - "test:unit": "NODE_ENV=testing c8 --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test:unit": "NODE_ENV=testing c8 --lib --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", "test": "yarn test:unit", "lint:code": "eslint *.js lib/ --ext .js --cache && eslint *.ts lib/ --ext .ts --cache", "lint": "yarn lint:code && yarn lint:test", From 508e191a2773aab4d5dc95e206aabbde5bd0bddb Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 6 Feb 2024 11:19:18 -0600 Subject: [PATCH 03/33] remove tsbuildinfo --- packages/kg-default-nodes/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kg-default-nodes/.gitignore b/packages/kg-default-nodes/.gitignore index c502d7033e..4837d66e23 100644 --- a/packages/kg-default-nodes/.gitignore +++ b/packages/kg-default-nodes/.gitignore @@ -1,3 +1,4 @@ cjs/ es/ -build/ \ No newline at end of file +build/ +tsconfig.tsbuildinfo \ No newline at end of file From 2a4cad35a83b02bce0fcf2799603abd540589380 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 6 Feb 2024 11:19:29 -0600 Subject: [PATCH 04/33] test converting a node --- .../aside/{AsideNode.js => AsideNode.ts} | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) rename packages/kg-default-nodes/lib/nodes/aside/{AsideNode.js => AsideNode.ts} (61%) diff --git a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.js b/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts similarity index 61% rename from packages/kg-default-nodes/lib/nodes/aside/AsideNode.js rename to packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts index 995ee83e59..eaecca86a7 100644 --- a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.js +++ b/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts @@ -1,13 +1,19 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {ElementNode} from 'lexical'; +import {ElementFormatType, ElementNode, LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; import {AsideParser} from './AsideParser'; +export type SerializedAsideNode = Spread<{ + format: ElementFormatType, + indent: number, + direction: 'ltr' | 'rtl' | null +}, SerializedLexicalNode>; + export class AsideNode extends ElementNode { - static getType() { + static getType(): string { return 'aside'; } - static clone(node) { + static clone(node: AsideNode): AsideNode { return new this( node.__key ); @@ -17,11 +23,11 @@ export class AsideNode extends ElementNode { return {}; } - constructor(key) { + constructor(key?: NodeKey) { super(key); } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedAsideNode): AsideNode { const node = new this(); node.setFormat(serializedNode.format); node.setIndent(serializedNode.indent); @@ -44,28 +50,28 @@ export class AsideNode extends ElementNode { } /* c8 ignore start */ - createDOM() { + createDOM(): HTMLElement { return document.createElement('div'); } - updateDOM() { + updateDOM(): false { return false; } - isInline() { + isInline(): false { return false; } - extractWithChild() { + extractWithChild(): true { return true; } /* c8 ignore stop */ } -export function $createAsideNode() { +export function $createAsideNode(): AsideNode { return new AsideNode(); } -export function $isAsideNode(node) { +export function $isAsideNode(node: LexicalNode | null): node is AsideNode { return node instanceof AsideNode; } From 120672011cad40caec3ce37f69abd3e9e968f5f7 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 6 Feb 2024 13:09:14 -0600 Subject: [PATCH 05/33] convert some nodes, need to resolve exportDOM --- ...dHeadingNode.js => ExtendedHeadingNode.ts} | 31 +++++++++++-------- ...endedQuoteNode.js => ExtendedQuoteNode.ts} | 16 +++++----- ...xtendedTextNode.js => ExtendedTextNode.ts} | 24 +++++++------- .../lib/nodes/{TKNode.js => TKNode.ts} | 25 ++++++++------- 4 files changed, 51 insertions(+), 45 deletions(-) rename packages/kg-default-nodes/lib/nodes/{ExtendedHeadingNode.js => ExtendedHeadingNode.ts} (63%) rename packages/kg-default-nodes/lib/nodes/{ExtendedQuoteNode.js => ExtendedQuoteNode.ts} (84%) rename packages/kg-default-nodes/lib/nodes/{ExtendedTextNode.js => ExtendedTextNode.ts} (82%) rename packages/kg-default-nodes/lib/nodes/{TKNode.js => TKNode.ts} (66%) diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.js b/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts similarity index 63% rename from packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.js rename to packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts index ca02a772ae..7bd659ba4a 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.js +++ b/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts @@ -1,5 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {HeadingNode} from '@lexical/rich-text'; +import {HeadingNode, SerializedHeadingNode} from '@lexical/rich-text'; +import { DOMConversion, DOMConversionFn, DOMConversionMap, DOMConversionOutput, Spread } from 'lexical'; // Since the HeadingNode is foundational to Lexical rich-text, only using a // custom HeadingNode is undesirable as it means every package would need to @@ -9,45 +10,48 @@ import {HeadingNode} from '@lexical/rich-text'; // // https://lexical.dev/docs/concepts/serialization#handling-extended-html-styling -export const extendedHeadingNodeReplacement = {replace: HeadingNode, with: node => new ExtendedHeadingNode(node.__tag)}; +export const extendedHeadingNodeReplacement = {replace: HeadingNode, with: (node: HeadingNode) => new ExtendedHeadingNode(node.__tag)}; + +type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; export class ExtendedHeadingNode extends HeadingNode { - constructor(tag, key) { + constructor(tag: HeadingTagType, key?: string) { super(tag, key); } - static getType() { + static getType(): string { return 'extended-heading'; } - static clone(node) { + static clone(node: ExtendedHeadingNode): ExtendedHeadingNode { return new ExtendedHeadingNode(node.__tag, node.__key); } - static importDOM() { + static importDOM(): DOMConversionMap | null { const importers = HeadingNode.importDOM(); + const originalParagraphImporter = importers?.p as DOMConversionFn; return { ...importers, - p: patchParagraphConversion(importers?.p) + p: patchParagraphConversion(originalParagraphImporter) }; } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedHeadingNode): ExtendedHeadingNode { return HeadingNode.importJSON(serializedNode); } - exportJSON() { + exportJSON(): SerializedHeadingNode { const json = super.exportJSON(); json.type = 'extended-heading'; return json; } } -function patchParagraphConversion(originalDOMConverter) { - return (node) => { +function patchParagraphConversion(originalDOMConverter: DOMConversionFn): DOMConversion | null { + return (node: HTMLElement) => { // Original matches Google Docs p node to a null conversion so it's // child span is parsed as a heading. Don't prevent that here - const original = originalDOMConverter?.(node); + const original = originalDOMConverter(node); if (original) { return original; } @@ -62,10 +66,11 @@ function patchParagraphConversion(originalDOMConverter) { if (hasAriaHeadingRole && hasAriaLevel) { const level = parseInt(hasAriaLevel, 10); if (level > 0 && level < 7) { + const tag: HeadingTagType = `h${level}` as HeadingTagType; return { conversion: () => { return { - node: new ExtendedHeadingNode(`h${level}`) + node: new ExtendedHeadingNode(tag) }; }, priority: 1 diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.js b/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts similarity index 84% rename from packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.js rename to packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts index 577cda211f..35c6d1c115 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.js +++ b/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {QuoteNode} from '@lexical/rich-text'; -import {$createLineBreakNode, $isParagraphNode} from 'lexical'; +import {QuoteNode, SerializedQuoteNode} from '@lexical/rich-text'; +import {$createLineBreakNode, $isParagraphNode, DOMConversionMap, NodeKey} from 'lexical'; // Since the QuoteNode is foundational to Lexical rich-text, only using a // custom QuoteNode is undesirable as it means every package would need to @@ -13,7 +13,7 @@ import {$createLineBreakNode, $isParagraphNode} from 'lexical'; export const extendedQuoteNodeReplacement = {replace: QuoteNode, with: () => new ExtendedQuoteNode()}; export class ExtendedQuoteNode extends QuoteNode { - constructor(key) { + constructor(key?: NodeKey) { super(key); } @@ -21,11 +21,11 @@ export class ExtendedQuoteNode extends QuoteNode { return 'extended-quote'; } - static clone(node) { + static clone(node: ExtendedQuoteNode): ExtendedQuoteNode { return new ExtendedQuoteNode(node.__key); } - static importDOM() { + static importDOM(): DOMConversionMap | null { const importers = QuoteNode.importDOM(); return { ...importers, @@ -33,18 +33,18 @@ export class ExtendedQuoteNode extends QuoteNode { }; } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedQuoteNode): ExtendedQuoteNode { return QuoteNode.importJSON(serializedNode); } - exportJSON() { + exportJSON(): SerializedQuoteNode { const json = super.exportJSON(); json.type = 'extended-quote'; return json; } /* c8 ignore start */ - extractWithChild() { + extractWithChild(): true { return true; } /* c8 ignore end */ diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.js b/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts similarity index 82% rename from packages/kg-default-nodes/lib/nodes/ExtendedTextNode.js rename to packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts index 07744fd710..ee3bad4e02 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.js +++ b/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {$isTextNode, TextNode} from 'lexical'; +import {$isTextNode, DOMConversion, DOMConversionFn, DOMConversionMap, LexicalNode, NodeKey, SerializedTextNode, TextNode} from 'lexical'; // Since the TextNode is foundational to all Lexical packages, including the // plain text use case. Handling any rich text logic is undesirable. This creates @@ -8,22 +8,22 @@ import {$isTextNode, TextNode} from 'lexical'; // // https://lexical.dev/docs/concepts/serialization#handling-extended-html-styling -export const extendedTextNodeReplacement = {replace: TextNode, with: node => new ExtendedTextNode(node.__text)}; +export const extendedTextNodeReplacement = {replace: TextNode, with: (node: TextNode) => new ExtendedTextNode(node.__text)}; export class ExtendedTextNode extends TextNode { - constructor(text, key) { + constructor(text: string, key?: NodeKey) { super(text, key); } - static getType() { + static getType(): string { return 'extended-text'; } - static clone(node) { + static clone(node: ExtendedTextNode): ExtendedTextNode { return new ExtendedTextNode(node.__text, node.__key); } - static importDOM() { + static importDOM(): DOMConversionMap | null { const importers = TextNode.importDOM(); return { ...importers, @@ -34,17 +34,17 @@ export class ExtendedTextNode extends TextNode { }; } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedTextNode): ExtendedTextNode { return TextNode.importJSON(serializedNode); } - exportJSON() { + exportJSON(): SerializedTextNode { const json = super.exportJSON(); json.type = 'extended-text'; return json; } - isSimpleText() { + isSimpleText(): boolean { return ( (this.__type === 'text' || this.__type === 'extended-text') && this.__mode === 0 @@ -52,8 +52,8 @@ export class ExtendedTextNode extends TextNode { } } -function patchConversion(originalDOMConverter, convertFn) { - return (node) => { +function patchConversion(originalDOMConverter: DOMConversion, convertFn: DOMConversionFn) { + return (node: HTMLElement) => { const original = originalDOMConverter?.(node); if (!original) { return null; @@ -78,7 +78,7 @@ function patchConversion(originalDOMConverter, convertFn) { }; } -function convertSpanElement(lexicalNode, domNode) { +function convertSpanElement(lexicalNode: ExtendedTextNode, domNode: HTMLElement): ExtendedTextNode { const span = domNode; // Word uses span tags + font-weight for bold text diff --git a/packages/kg-default-nodes/lib/nodes/TKNode.js b/packages/kg-default-nodes/lib/nodes/TKNode.ts similarity index 66% rename from packages/kg-default-nodes/lib/nodes/TKNode.js rename to packages/kg-default-nodes/lib/nodes/TKNode.ts index f39cd03734..5b1313972c 100644 --- a/packages/kg-default-nodes/lib/nodes/TKNode.js +++ b/packages/kg-default-nodes/lib/nodes/TKNode.ts @@ -1,28 +1,29 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {$applyNodeReplacement, TextNode} from 'lexical'; +import {$applyNodeReplacement, EditorConfig, LexicalNode, NodeKey, TextNode} from 'lexical'; +import {SerializedTextNode} from 'lexical/nodes/LexicalTextNode'; export class TKNode extends TextNode { - static getType() { + static getType(): string { return 'tk'; } - static clone(node) { + static clone(node: TKNode): TKNode { return new TKNode(node.__text, node.__key); } - constructor(text, key) { + constructor(text: string, key?: NodeKey) { super(text, key); } - createDOM(config) { + createDOM(config: EditorConfig) { const element = super.createDOM(config); const classes = config.theme.tk?.split(' ') || []; element.classList.add(...classes); - element.dataset.kgTk = true; + element.dataset.kgTk = "true"; return element; } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedTextNode): TKNode { const node = $createTKNode(serializedNode.text); node.setFormat(serializedNode.format); node.setDetail(serializedNode.detail); @@ -31,18 +32,18 @@ export class TKNode extends TextNode { return node; } - exportJSON() { + exportJSON(): SerializedTextNode { return { ...super.exportJSON(), type: 'tk' }; } - canInsertTextBefore() { + canInsertTextBefore(): false { return false; } - isTextEntity() { + isTextEntity(): true { return true; } } @@ -52,7 +53,7 @@ export class TKNode extends TextNode { * @param text - The text used inside the TKNode. * @returns - The TKNode with the embedded text. */ -export function $createTKNode(text) { +export function $createTKNode(text: string): TKNode { return $applyNodeReplacement(new TKNode(text)); } @@ -61,6 +62,6 @@ export function $createTKNode(text) { * @param node - The node to be checked. * @returns true if node is a TKNode, false otherwise. */ -export function $isTKNode(node) { +export function $isTKNode(node: LexicalNode): node is TKNode { return node instanceof TKNode; } From 4a88ac0e7f7f31b5fc0776b15a21705b27f88fe4 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 6 Feb 2024 18:13:06 -0600 Subject: [PATCH 06/33] patch up extended nodes - note: any type is used for some of the conversions for the moment as a convenience.. these are going to need refactoring --- .../lib/nodes/ExtendedHeadingNode.ts | 21 +++++++++---------- .../lib/nodes/ExtendedQuoteNode.ts | 9 ++++---- .../lib/nodes/ExtendedTextNode.ts | 13 +++++++----- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts b/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts index 7bd659ba4a..2fd169bbce 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts +++ b/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {HeadingNode, SerializedHeadingNode} from '@lexical/rich-text'; -import { DOMConversion, DOMConversionFn, DOMConversionMap, DOMConversionOutput, Spread } from 'lexical'; +import {DOMConversionMap} from 'lexical'; // Since the HeadingNode is foundational to Lexical rich-text, only using a // custom HeadingNode is undesirable as it means every package would need to @@ -29,11 +29,10 @@ export class ExtendedHeadingNode extends HeadingNode { static importDOM(): DOMConversionMap | null { const importers = HeadingNode.importDOM(); - const originalParagraphImporter = importers?.p as DOMConversionFn; return { ...importers, - p: patchParagraphConversion(originalParagraphImporter) - }; + p: patchParagraphConversion(importers?.p) + } } static importJSON(serializedNode: SerializedHeadingNode): ExtendedHeadingNode { @@ -47,17 +46,17 @@ export class ExtendedHeadingNode extends HeadingNode { } } -function patchParagraphConversion(originalDOMConverter: DOMConversionFn): DOMConversion | null { - return (node: HTMLElement) => { +// ignore no explicit any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function patchParagraphConversion(originalDOMConverter: any) { + return (node: Node) => { + const p = node as HTMLParagraphElement; // Original matches Google Docs p node to a null conversion so it's // child span is parsed as a heading. Don't prevent that here - const original = originalDOMConverter(node); - if (original) { - return original; + if (originalDOMConverter) { + return originalDOMConverter(p); } - const p = node; - // Word uses paragraphs with role="heading" to represent headings // and an aria-level="x" to represent the heading level const hasAriaHeadingRole = p.getAttribute('role') === 'heading'; diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts b/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts index 35c6d1c115..2dacb5ed59 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts +++ b/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {QuoteNode, SerializedQuoteNode} from '@lexical/rich-text'; -import {$createLineBreakNode, $isParagraphNode, DOMConversionMap, NodeKey} from 'lexical'; +import {$createLineBreakNode, $isParagraphNode, DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, NodeKey} from 'lexical'; // Since the QuoteNode is foundational to Lexical rich-text, only using a // custom QuoteNode is undesirable as it means every package would need to @@ -34,7 +34,8 @@ export class ExtendedQuoteNode extends QuoteNode { } static importJSON(serializedNode: SerializedQuoteNode): ExtendedQuoteNode { - return QuoteNode.importJSON(serializedNode); + const json = QuoteNode.importJSON(serializedNode) as ExtendedQuoteNode; + return json; } exportJSON(): SerializedQuoteNode { @@ -50,7 +51,7 @@ export class ExtendedQuoteNode extends QuoteNode { /* c8 ignore end */ } -function convertBlockquoteElement() { +function convertBlockquoteElement(): DOMConversion { return { conversion: () => { const node = new ExtendedQuoteNode(); @@ -61,7 +62,7 @@ function convertBlockquoteElement() { // editor we parsed all of the nested paragraphs into a single blockquote // separating each paragraph with two line breaks. We replicate that // here so we don't have a breaking change in conversion behaviour. - const newChildNodes = []; + const newChildNodes: LexicalNode[] = []; childNodes.forEach((child) => { if ($isParagraphNode(child)) { diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts b/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts index ee3bad4e02..8872d5516c 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts +++ b/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {$isTextNode, DOMConversion, DOMConversionFn, DOMConversionMap, LexicalNode, NodeKey, SerializedTextNode, TextNode} from 'lexical'; +import {$isTextNode, DOMChildConversion, DOMConversion, DOMConversionFn, DOMConversionMap, LexicalNode, NodeKey, SerializedTextNode, TextNode} from 'lexical'; // Since the TextNode is foundational to all Lexical packages, including the // plain text use case. Handling any rich text logic is undesirable. This creates @@ -25,10 +25,11 @@ export class ExtendedTextNode extends TextNode { static importDOM(): DOMConversionMap | null { const importers = TextNode.importDOM(); + const importersSpanConversion = importers?.span; return { ...importers, span: () => ({ - conversion: patchConversion(importers?.span, convertSpanElement), + conversion: patchConversion(importersSpanConversion, convertSpanElement), priority: 1 }) }; @@ -52,8 +53,10 @@ export class ExtendedTextNode extends TextNode { } } -function patchConversion(originalDOMConverter: DOMConversion, convertFn: DOMConversionFn) { - return (node: HTMLElement) => { +// ignore no implicit any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function patchConversion(originalDOMConverter: any, convertFn: any): DOMConversionFn { + return (node: Node) => { const original = originalDOMConverter?.(node); if (!original) { return null; @@ -67,7 +70,7 @@ function patchConversion(originalDOMConverter: DOMConversion, convertFn: DOMConv return { ...originalOutput, forChild: (lexicalNode, parent) => { - const originalForChild = originalOutput?.forChild ?? (x => x); + const originalForChild = originalOutput?.forChild; const result = originalForChild(lexicalNode, parent); if ($isTextNode(result)) { return convertFn(result, node); From 5e8a9051a3a25de7dd571c44fccc18c42f1b398a Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 08:22:32 -0600 Subject: [PATCH 07/33] upd nodes --- .../lib/nodes/aside/AsideNode.ts | 5 ++-- .../lib/nodes/aside/AsideParser.js | 24 ------------------- .../lib/nodes/aside/aside-parser.ts | 20 ++++++++++++++++ .../{button-parser.js => button-parser.ts} | 19 ++++++++++----- 4 files changed, 35 insertions(+), 33 deletions(-) delete mode 100644 packages/kg-default-nodes/lib/nodes/aside/AsideParser.js create mode 100644 packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts rename packages/kg-default-nodes/lib/nodes/button/{button-parser.js => button-parser.ts} (52%) diff --git a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts b/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts index eaecca86a7..c5cca7b964 100644 --- a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts +++ b/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {ElementFormatType, ElementNode, LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; -import {AsideParser} from './AsideParser'; +import {parseAsideNode} from './aside-parser'; export type SerializedAsideNode = Spread<{ format: ElementFormatType, @@ -45,8 +45,7 @@ export class AsideNode extends ElementNode { } static importDOM() { - const parser = new AsideParser(this); - return parser.DOMConversionMap; + return parseAsideNode(); } /* c8 ignore start */ diff --git a/packages/kg-default-nodes/lib/nodes/aside/AsideParser.js b/packages/kg-default-nodes/lib/nodes/aside/AsideParser.js deleted file mode 100644 index 662cf5234d..0000000000 --- a/packages/kg-default-nodes/lib/nodes/aside/AsideParser.js +++ /dev/null @@ -1,24 +0,0 @@ -export class AsideParser { - constructor(NodeClass) { - this.NodeClass = NodeClass; - } - - get DOMConversionMap() { - const self = this; - - return { - blockquote: () => ({ - conversion(domNode) { - const isBigQuote = domNode.classList?.contains('kg-blockquote-alt'); - if (domNode.tagName === 'BLOCKQUOTE' && isBigQuote) { - const node = new self.NodeClass(); - return {node}; - } - - return null; - }, - priority: 0 - }) - }; - } -} diff --git a/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts b/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts new file mode 100644 index 0000000000..b558cd7c3c --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts @@ -0,0 +1,20 @@ +import {$createAsideNode} from './AsideNode'; + +export function parseAsideNode() { + return { + blockquote: (nodeElem: HTMLElement) => { + const isBigQuote = nodeElem.classList?.contains('kg-blockquote-alt'); + if (nodeElem.tagName === 'BLOCKQUOTE' && isBigQuote) { + return { + conversion() { + const node = $createAsideNode(); + return {node}; + }, + priority: 0 + }; + } + + return null; + } + }; +} \ No newline at end of file diff --git a/packages/kg-default-nodes/lib/nodes/button/button-parser.js b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts similarity index 52% rename from packages/kg-default-nodes/lib/nodes/button/button-parser.js rename to packages/kg-default-nodes/lib/nodes/button/button-parser.ts index 7e6e326f1b..bb4a49f0d0 100644 --- a/packages/kg-default-nodes/lib/nodes/button/button-parser.js +++ b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts @@ -1,10 +1,17 @@ -export function parseButtonNode(ButtonNode) { +import { DOMConversion, DOMConversionMap, DOMConversionOutput } from "lexical"; +import { $createButtonNode } from "./ButtonNode"; +import { KoenigDecoratorNode } from "../../KoenigDecoratorNode"; + +// TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode +type ButtonNode = KoenigDecoratorNode; + +export const parseButtonNode = (ButtonNode: ButtonNode): DOMConversionMap | null => { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { const isButtonCard = nodeElem.classList?.contains('kg-button-card'); if (nodeElem.tagName === 'DIV' && isButtonCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement): DOMConversionOutput { const alignmentClass = nodeElem.className.match(/kg-align-(left|center)/); let alignment; @@ -13,8 +20,8 @@ export function parseButtonNode(ButtonNode) { } const buttonNode = domNode?.querySelector('.kg-btn'); - const buttonUrl = buttonNode.getAttribute('href'); - const buttonText = buttonNode.textContent; + const buttonUrl = buttonNode?.getAttribute('href'); + const buttonText = buttonNode?.textContent; const payload = { buttonText: buttonText, @@ -22,7 +29,7 @@ export function parseButtonNode(ButtonNode) { buttonUrl: buttonUrl }; - const node = new ButtonNode(payload); + const node = $createButtonNode(payload) as ButtonNode; return {node}; }, priority: 1 From 9482df121497c045bcf92be39fc3cbd4437f748f Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 08:37:15 -0600 Subject: [PATCH 08/33] update audio parser --- .../lib/nodes/audio/AudioNode.js | 2 +- .../{audio-parser.js => audio-parser.ts} | 30 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) rename packages/kg-default-nodes/lib/nodes/audio/{audio-parser.js => audio-parser.ts} (59%) diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js index cc4dce06dd..936c0232d1 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js @@ -13,7 +13,7 @@ export class AudioNode extends generateDecoratorNode({nodeType: 'audio', ]} ) { static importDOM() { - return parseAudioNode(this); + return parseAudioNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.js b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts similarity index 59% rename from packages/kg-default-nodes/lib/nodes/audio/audio-parser.js rename to packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts index 2ffcf27d22..d469af8afa 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.js +++ b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts @@ -1,19 +1,33 @@ -export function parseAudioNode(AudioNode) { +import { DOMConversion, DOMConversionMap, DOMConversionOutput } from "lexical"; +import { $createAudioNode } from "./AudioNode"; +import { KoenigDecoratorNode } from "../../KoenigDecoratorNode"; + +// TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode +type AudioNode = KoenigDecoratorNode; + +type AudioPayload = { + src: string | null; + title: string | null; + thumbnailSrc?: string; + duration?: number; +}; + +export function parseAudioNode(): DOMConversionMap | null { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { const isKgAudioCard = nodeElem.classList?.contains('kg-audio-card'); if (nodeElem.tagName === 'DIV' && isKgAudioCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement): DOMConversionOutput { const titleNode = domNode?.querySelector('.kg-audio-title'); - const audioNode = domNode?.querySelector('.kg-audio-player-container audio'); + const audioNode = domNode?.querySelector('.kg-audio-player-container audio') as HTMLAudioElement | null; const durationNode = domNode?.querySelector('.kg-audio-duration'); - const thumbnailNode = domNode?.querySelector('.kg-audio-thumbnail'); + const thumbnailNode = domNode?.querySelector('.kg-audio-thumbnail') as HTMLImageElement | null; const title = titleNode && titleNode.innerHTML.trim(); const audioSrc = audioNode && audioNode.src; const thumbnailSrc = thumbnailNode && thumbnailNode.src; const durationText = durationNode && durationNode.innerHTML.trim(); - const payload = { + const payload: AudioPayload = { src: audioSrc, title: title }; @@ -22,7 +36,7 @@ export function parseAudioNode(AudioNode) { } if (durationText) { - const [minutes, seconds = 0] = durationText.split(':'); + const [minutes, seconds = '0'] = durationText.split(':'); try { payload.duration = parseInt(minutes) * 60 + parseInt(seconds); } catch (e) { @@ -30,7 +44,7 @@ export function parseAudioNode(AudioNode) { } } - const node = new AudioNode(payload); + const node = $createAudioNode(payload) as AudioNode; return {node}; }, priority: 1 From 1f4e49fb9c2793a9edeb8d781deebf199be5b0af Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 09:10:24 -0600 Subject: [PATCH 09/33] wire up generator --- ...tor-node.js => generate-decorator-node.ts} | 139 ++++++++---------- .../audio/{AudioNode.js => AudioNode.ts} | 9 +- 2 files changed, 72 insertions(+), 76 deletions(-) rename packages/kg-default-nodes/lib/{generate-decorator-node.js => generate-decorator-node.ts} (58%) rename packages/kg-default-nodes/lib/nodes/audio/{AudioNode.js => AudioNode.ts} (62%) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.js b/packages/kg-default-nodes/lib/generate-decorator-node.ts similarity index 58% rename from packages/kg-default-nodes/lib/generate-decorator-node.js rename to packages/kg-default-nodes/lib/generate-decorator-node.ts index ba81ef8682..256257558f 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.js +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -1,55 +1,75 @@ +import {NodeKey} from 'lexical'; import {KoenigDecoratorNode} from './KoenigDecoratorNode'; import readTextContent from './utils/read-text-content'; /** * Validates the required arguments passed to `generateDecoratorNode` + * @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode */ -function validateArguments(nodeType, properties) { + +// NOTE: There's some liberal use of 'any' in this file despite moving it to typescript. This is a difficult one to get right +// because we're generating classes. + +export type KoenigDecoratorProperty = { + name: string; + default?: number | string | boolean | null; + urlType?: 'url'|'html'|'markdown'; + wordCount?: boolean; +}; + +export type KoenigDecoratorNodeProperties = KoenigDecoratorProperty[]; + +function validateArguments(nodeType: string, properties: KoenigDecoratorNodeProperties) { /* eslint-disable ghost/ghost-custom/no-native-error */ /* c8 ignore start */ if (!nodeType) { - throw new Error({message: '[generateDecoratorNode] A unique "nodeType" should be provided'}); + throw new Error('[generateDecoratorNode] A unique "nodeType" should be provided'); } properties.forEach((prop) => { if (!('name' in prop) || !('default' in prop)){ - throw new Error({message: '[generateDecoratorNode] Properties should have both "name" and "default" attributes.'}); + throw new Error('[generateDecoratorNode] Properties should have both "name" and "default" attributes.'); } if (prop.urlType && !['url', 'html', 'markdown'].includes(prop.urlType)) { - throw new Error({message: '[generateDecoratorNode] "urlType" should be either "url", "html" or "markdown"'}); + throw new Error('[generateDecoratorNode] "urlType" should be either "url", "html" or "markdown"'); } if ('wordCount' in prop && typeof prop.wordCount !== 'boolean') { - throw new Error({message: '[generateDecoratorNode] "wordCount" should be of boolean type.'}); + throw new Error('[generateDecoratorNode] "wordCount" should be of boolean type.'); } }); /* c8 ignore stop */ } -/** - * @typedef {Object} DecoratorNodeProperty - * @property {string} name - The property's name. - * @property {*} default - The property's default value - * @property {('url'|'html'|'markdown'|null)} urlType - If the property contains a URL, the URL's type: 'url', 'html' or 'markdown'. Use 'url' is the property contains only a URL, 'html' or 'markdown' if the property contains HTML or markdown code, that may contain URLs. - * @property {boolean} wordCount - Whether the property should be counted in the word count - * - * @param {string} nodeType – The node's type (must be unique) - * @param {DecoratorNodeProperty[]} properties - An array of properties for the generated class - * @returns {Object} - The generated class. - */ -export function generateDecoratorNode({nodeType, properties = [], version = 1}) { +// expand the properties to include a privateName field +type PrivateKoenigProperty = KoenigDecoratorProperty & {privateName: string}; + +type GenerateKoenigDecoratorNodeFn = (options: { + nodeType: string; + properties?: KoenigDecoratorNodeProperties; + version?: number; +}) => typeof KoenigDecoratorNode; + +type SerializedKoenigDecoratorNode = { + type: string; + version: number; + [key: string]: any; +}; + +export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, properties = [], version = 1}) => { validateArguments(nodeType, properties); // Adds a `privateName` field to the properties for convenience (e.g. `__name`): // properties: [{name: 'name', privateName: '__name', type: 'string', default: 'hello'}, {...}] - properties = properties.map((prop) => { + const __properties: PrivateKoenigProperty[] = properties.map((prop) => { return {...prop, privateName: `__${prop.name}`}; }); class GeneratedDecoratorNode extends KoenigDecoratorNode { - constructor(data = {}, key) { + // allow any type here for ease of use + constructor(data: any = {}, key?: NodeKey) { super(key); - properties.forEach((prop) => { + __properties.forEach((prop) => { if (typeof prop.default === 'boolean') { this[prop.privateName] = data[prop.name] ?? prop.default; } else { @@ -58,22 +78,11 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) }); } - /** - * Returns the node's unique type - * @extends DecoratorNode - * @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode - * @returns {string} - */ - static getType() { + static getType(): string { return nodeType; } - /** - * Creates a copy of an existing node with all its properties - * @extends DecoratorNode - * @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode - */ - static clone(node) { + static clone(node: KoenigDecoratorNode) { return new this(node.getDataset(), node.__key); } @@ -83,9 +92,9 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) * @see https://github.com/TryGhost/SDK/tree/main/packages/url-utils */ static get urlTransformMap() { - let map = {}; + let map: any = {}; - properties.forEach((prop) => { + __properties.forEach((prop) => { if (prop.urlType) { map[prop.name] = prop.urlType; } @@ -101,8 +110,8 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) getDataset() { const self = this.getLatest(); - let dataset = {}; - properties.forEach((prop) => { + let dataset: any = {}; + __properties.forEach((prop) => { dataset[prop.name] = self[prop.privateName]; }); @@ -115,10 +124,11 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) * @extends DecoratorNode * @param {Object} serializedNode - Lexical's representation of the node, in JSON format */ - static importJSON(serializedNode) { - const data = {}; - properties.forEach((prop) => { + static importJSON(serializedNode: SerializedKoenigDecoratorNode): KoenigDecoratorNode { + const data: any = {}; + + __properties.forEach((prop) => { data[prop.name] = serializedNode[prop.name]; }); @@ -130,11 +140,11 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) * @extends DecoratorNode * @see https://lexical.dev/docs/concepts/serialization#lexicalnodeexportjson */ - exportJSON() { + exportJSON(): SerializedKoenigDecoratorNode { const dataset = { type: nodeType, version: version, - ...properties.reduce((obj, prop) => { + ...__properties.reduce((obj: any, prop) => { obj[prop.name] = this[prop.name]; return obj; }, {}) @@ -143,56 +153,37 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) } /* c8 ignore start */ - /** - * Inserts node in the DOM. Required when extending the DecoratorNode. - * @extends DecoratorNode - * @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode - */ - createDOM() { + createDOM(): HTMLElement { return document.createElement('div'); } - /** - * Required when extending the DecoratorNode - * @extends DecoratorNode - * @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode - */ - updateDOM() { + updateDOM(): false { return false; } - /** - * Defines whether a node is a top-level block. - * @see https://lexical.dev/docs/api/classes/lexical.DecoratorNode#isinline - */ - isInline() { + // Defines whether a node is a top-level block. + isInline(): false { // All our cards are top-level blocks. Override if needed. return false; } /* c8 ignore stop */ - /** - * Defines whether a node has dynamic data that needs to be fetched from the server when rendering - */ - hasDynamicData() { + // Defines whether a node has dynamic data that needs to be fetched from the server when rendering + hasDynamicData(): false { return false; } - /** - * Defines whether a node has an edit mode in the editor UI - */ - hasEditMode() { + // Defines whether a node has an edit mode in the editor UI + hasEditMode(): true { // Most of our cards have an edit mode. Override if needed. return true; } - /* - * Returns the text content of the node, used by the editor to calculate the word count - * This method filters out properties without `wordCount: true` - */ - getTextContent() { + // Returns the text content of the node, used by the editor to calculate the word count + // This method filters out properties without `wordCount: true` + getTextContent(): string { const self = this.getLatest(); - const propertiesWithText = properties.filter(prop => !!prop.wordCount); + const propertiesWithText = __properties.filter(prop => !!prop.wordCount); const text = propertiesWithText.map( prop => readTextContent(self, prop.name) @@ -220,7 +211,7 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1}) * * They can be used as `node.content` (getter) and `node.content = 'new value'` (setter) */ - properties.forEach((prop) => { + __properties.forEach((prop) => { Object.defineProperty(GeneratedDecoratorNode.prototype, prop.name, { get: function () { const self = this.getLatest(); diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts similarity index 62% rename from packages/kg-default-nodes/lib/nodes/audio/AudioNode.js rename to packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index 936c0232d1..12a2c851a2 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -1,4 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ +import { LexicalNode } from 'lexical'; import {generateDecoratorNode} from '../../generate-decorator-node'; import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; @@ -21,10 +22,14 @@ export class AudioNode extends generateDecoratorNode({nodeType: 'audio', } } -export const $createAudioNode = (dataset) => { +// TODO: Not sure how to handle the 'any' here; it's a result of the `generateDecoratorNode` function +// and the fact that the constructor is obscured, so it's pulling from the DecoratorNode class +// +// Given that we call the $create methods in koenig-lexical, we really want to have type safety on the dataset properties +export const $createAudioNode = (dataset: any) => { return new AudioNode(dataset); }; -export function $isAudioNode(node) { +export function $isAudioNode(node: LexicalNode) { return node instanceof AudioNode; } From 4e789855796fb5d8b83d5c7360fe8d0f8328e1af Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 10:16:18 -0600 Subject: [PATCH 10/33] update generator class --- .../lib/generate-decorator-node.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index 256257558f..0f018cf5b8 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -44,11 +44,13 @@ function validateArguments(nodeType: string, properties: KoenigDecoratorNodeProp // expand the properties to include a privateName field type PrivateKoenigProperty = KoenigDecoratorProperty & {privateName: string}; -type GenerateKoenigDecoratorNodeFn = (options: { +type GenerateKoenigDecoratorNodeFn = (options: GenerateKoenigDecoratorNodeOptions) => typeof GeneratedKoenigDecoratorNode; + +type GenerateKoenigDecoratorNodeOptions = { nodeType: string; properties?: KoenigDecoratorNodeProperties; version?: number; -}) => typeof KoenigDecoratorNode; +}; type SerializedKoenigDecoratorNode = { type: string; @@ -56,6 +58,14 @@ type SerializedKoenigDecoratorNode = { [key: string]: any; }; +class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { + + constructor(data: GenerateKoenigDecoratorNodeOptions) { + super(); + this.generateDecoratorNode(data); + } +} + export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, properties = [], version = 1}) => { validateArguments(nodeType, properties); From 79426debd990fdef98292dee14fc3f1eef9fcefb Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 10:22:14 -0600 Subject: [PATCH 11/33] update create audio node --- .../lib/nodes/audio/AudioNode.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index 12a2c851a2..6f5f560220 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -1,9 +1,22 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import { LexicalNode } from 'lexical'; -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; +type AudioNodeProperties = KoenigDecoratorNodeProperties & { + duration: number; + mimeType: string; + src: string; + title: string; + thumbnailSrc: string; +}; + +type AudioNodeDataset = { + nodeType: 'audio'; + properties: AudioNodeProperties; +}; + export class AudioNode extends generateDecoratorNode({nodeType: 'audio', properties: [ {name: 'duration', default: 0}, @@ -26,7 +39,7 @@ export class AudioNode extends generateDecoratorNode({nodeType: 'audio', // and the fact that the constructor is obscured, so it's pulling from the DecoratorNode class // // Given that we call the $create methods in koenig-lexical, we really want to have type safety on the dataset properties -export const $createAudioNode = (dataset: any) => { +export const $createAudioNode = (dataset: AudioNodeDataset) => { return new AudioNode(dataset); }; From 6791f50f834e397b60d752b894399c0affc35188 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 10:50:01 -0600 Subject: [PATCH 12/33] adjust dataset prop --- .../lib/generate-decorator-node.ts | 2 -- .../kg-default-nodes/lib/nodes/audio/AudioNode.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index 0f018cf5b8..fa29ba0bcc 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -41,7 +41,6 @@ function validateArguments(nodeType: string, properties: KoenigDecoratorNodeProp /* c8 ignore stop */ } -// expand the properties to include a privateName field type PrivateKoenigProperty = KoenigDecoratorProperty & {privateName: string}; type GenerateKoenigDecoratorNodeFn = (options: GenerateKoenigDecoratorNodeOptions) => typeof GeneratedKoenigDecoratorNode; @@ -59,7 +58,6 @@ type SerializedKoenigDecoratorNode = { }; class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { - constructor(data: GenerateKoenigDecoratorNodeOptions) { super(); this.generateDecoratorNode(data); diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index 6f5f560220..c0769db560 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -5,16 +5,16 @@ import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; type AudioNodeProperties = KoenigDecoratorNodeProperties & { - duration: number; - mimeType: string; - src: string; - title: string; - thumbnailSrc: string; + duration?: number; + mimeType?: string; + src?: string; + title?: string; + thumbnailSrc?: string; }; type AudioNodeDataset = { nodeType: 'audio'; - properties: AudioNodeProperties; + properties?: AudioNodeProperties; }; export class AudioNode extends generateDecoratorNode({nodeType: 'audio', @@ -39,7 +39,7 @@ export class AudioNode extends generateDecoratorNode({nodeType: 'audio', // and the fact that the constructor is obscured, so it's pulling from the DecoratorNode class // // Given that we call the $create methods in koenig-lexical, we really want to have type safety on the dataset properties -export const $createAudioNode = (dataset: AudioNodeDataset) => { +export const $createAudioNode = (dataset: any) => { return new AudioNode(dataset); }; From 3fb90f2d2d40cdf0bb227184b873788152494d98 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 11:33:33 -0600 Subject: [PATCH 13/33] upd --- .../lib/generate-decorator-node.ts | 2 +- .../lib/nodes/audio/AudioNode.ts | 35 ++++++++++--------- .../lib/nodes/audio/audio-parser.ts | 16 ++++----- packages/kg-default-nodes/package.json | 1 + 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index fa29ba0bcc..a3302886c6 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -57,7 +57,7 @@ type SerializedKoenigDecoratorNode = { [key: string]: any; }; -class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { +export class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { constructor(data: GenerateKoenigDecoratorNodeOptions) { super(); this.generateDecoratorNode(data); diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index c0769db560..5bde4e4751 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -1,10 +1,10 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {LexicalNode} from 'lexical'; -import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {GeneratedKoenigDecoratorNode} from '../../generate-decorator-node'; import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; -type AudioNodeProperties = KoenigDecoratorNodeProperties & { +export type AudioNodeDataset = { duration?: number; mimeType?: string; src?: string; @@ -12,20 +12,23 @@ type AudioNodeProperties = KoenigDecoratorNodeProperties & { thumbnailSrc?: string; }; -type AudioNodeDataset = { - nodeType: 'audio'; - properties?: AudioNodeProperties; -}; +export class AudioNode extends GeneratedKoenigDecoratorNode { + + constructor(data: AudioNodeDataset) { + super( + { + nodeType: 'audio', + properties: [ + {name: 'duration', default: 0}, + {name: 'mimeType', default: ''}, + {name: 'src', default: '', urlType: 'url'}, + {name: 'title', default: ''}, + {name: 'thumbnailSrc', default: ''} + ] + } + ); + } -export class AudioNode extends generateDecoratorNode({nodeType: 'audio', - properties: [ - {name: 'duration', default: 0}, - {name: 'mimeType', default: ''}, - {name: 'src', default: '', urlType: 'url'}, - {name: 'title', default: ''}, - {name: 'thumbnailSrc', default: ''} - ]} -) { static importDOM() { return parseAudioNode(); } @@ -39,7 +42,7 @@ export class AudioNode extends generateDecoratorNode({nodeType: 'audio', // and the fact that the constructor is obscured, so it's pulling from the DecoratorNode class // // Given that we call the $create methods in koenig-lexical, we really want to have type safety on the dataset properties -export const $createAudioNode = (dataset: any) => { +export const $createAudioNode = (dataset: AudioNodeDataset) => { return new AudioNode(dataset); }; diff --git a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts index d469af8afa..7a5ca3c57b 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts @@ -1,16 +1,10 @@ import { DOMConversion, DOMConversionMap, DOMConversionOutput } from "lexical"; -import { $createAudioNode } from "./AudioNode"; +import { $createAudioNode, AudioNodeDataset } from "./AudioNode"; import { KoenigDecoratorNode } from "../../KoenigDecoratorNode"; // TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode type AudioNode = KoenigDecoratorNode; -type AudioPayload = { - src: string | null; - title: string | null; - thumbnailSrc?: string; - duration?: number; -}; export function parseAudioNode(): DOMConversionMap | null { return { @@ -27,10 +21,12 @@ export function parseAudioNode(): DOMConversionMap | null { const audioSrc = audioNode && audioNode.src; const thumbnailSrc = thumbnailNode && thumbnailNode.src; const durationText = durationNode && durationNode.innerHTML.trim(); - const payload: AudioPayload = { - src: audioSrc, - title: title + + const payload: AudioNodeDataset = { + src: audioSrc || undefined, + title: title || undefined }; + if (thumbnailSrc) { payload.thumbnailSrc = thumbnailSrc; } diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index 01956c7647..a0a49e8b1a 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -14,6 +14,7 @@ "pretest": "yarn build", "test:unit": "NODE_ENV=testing c8 --lib --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", "test": "yarn test:unit", + "test:noc8": "NODE_ENV=testing mocha './test/**/*.test.js'", "lint:code": "eslint *.js lib/ --ext .js --cache && eslint *.ts lib/ --ext .ts --cache", "lint": "yarn lint:code && yarn lint:test", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" From bc47e1e83fff024ebe9669e7b9474dfc76d92a33 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 12:01:42 -0600 Subject: [PATCH 14/33] update linter --- packages/kg-default-nodes/.eslintrc.js | 19 +++++++------------ .../lib/generate-decorator-node.ts | 13 +++++++++---- .../lib/nodes/ExtendedHeadingNode.ts | 2 +- .../lib/nodes/ExtendedQuoteNode.ts | 2 +- .../lib/nodes/ExtendedTextNode.ts | 2 +- packages/kg-default-nodes/lib/nodes/TKNode.ts | 2 +- .../lib/nodes/audio/AudioNode.ts | 2 +- .../lib/nodes/audio/audio-parser.ts | 7 +++---- .../lib/nodes/button/button-parser.ts | 10 +++++----- packages/kg-default-nodes/package.json | 2 +- 10 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/kg-default-nodes/.eslintrc.js b/packages/kg-default-nodes/.eslintrc.js index fafc2745fc..c1aed6b951 100644 --- a/packages/kg-default-nodes/.eslintrc.js +++ b/packages/kg-default-nodes/.eslintrc.js @@ -1,20 +1,15 @@ module.exports = { plugins: ['ghost'], extends: [ - 'plugin:ghost/node' + 'plugin:ghost/node', + 'plugin:ghost/ts' ], - parser: '@babel/eslint-parser', - parserOptions: { - sourceType: 'module', - requireConfigFile: false, - babelOptions: { - plugins: [ - '@babel/plugin-syntax-import-assertions' - ] - } - }, env: { browser: true, node: true + }, + // this fouls up with Lexical's need to prefix with '$' for lifecycle hooks + rules: { + 'ghost/filenames/match-exported-class': 'off' } -}; +}; \ No newline at end of file diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index a3302886c6..586cca534b 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -54,6 +54,7 @@ type GenerateKoenigDecoratorNodeOptions = { type SerializedKoenigDecoratorNode = { type: string; version: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; }; @@ -75,6 +76,7 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, class GeneratedDecoratorNode extends KoenigDecoratorNode { // allow any type here for ease of use + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(data: any = {}, key?: NodeKey) { super(key); __properties.forEach((prop) => { @@ -100,7 +102,8 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, * @see https://github.com/TryGhost/SDK/tree/main/packages/url-utils */ static get urlTransformMap() { - let map: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const map: any = {}; __properties.forEach((prop) => { if (prop.urlType) { @@ -117,8 +120,8 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, */ getDataset() { const self = this.getLatest(); - - let dataset: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dataset: any = {}; __properties.forEach((prop) => { dataset[prop.name] = self[prop.privateName]; }); @@ -134,6 +137,7 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, */ static importJSON(serializedNode: SerializedKoenigDecoratorNode): KoenigDecoratorNode { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const data: any = {}; __properties.forEach((prop) => { @@ -152,6 +156,7 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, const dataset = { type: nodeType, version: version, + // eslint-disable-next-line @typescript-eslint/no-explicit-any ...__properties.reduce((obj: any, prop) => { obj[prop.name] = this[prop.name]; return obj; @@ -233,4 +238,4 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, }); return GeneratedDecoratorNode; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts b/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts index 2fd169bbce..4598e40bf6 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts +++ b/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.ts @@ -32,7 +32,7 @@ export class ExtendedHeadingNode extends HeadingNode { return { ...importers, p: patchParagraphConversion(importers?.p) - } + }; } static importJSON(serializedNode: SerializedHeadingNode): ExtendedHeadingNode { diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts b/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts index 2dacb5ed59..68c0a58c56 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts +++ b/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {QuoteNode, SerializedQuoteNode} from '@lexical/rich-text'; -import {$createLineBreakNode, $isParagraphNode, DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, NodeKey} from 'lexical'; +import {$createLineBreakNode, $isParagraphNode, DOMConversion, DOMConversionMap, LexicalNode, NodeKey} from 'lexical'; // Since the QuoteNode is foundational to Lexical rich-text, only using a // custom QuoteNode is undesirable as it means every package would need to diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts b/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts index 8872d5516c..a5ad81d556 100644 --- a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts +++ b/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {$isTextNode, DOMChildConversion, DOMConversion, DOMConversionFn, DOMConversionMap, LexicalNode, NodeKey, SerializedTextNode, TextNode} from 'lexical'; +import {$isTextNode, DOMConversionFn, DOMConversionMap, NodeKey, SerializedTextNode, TextNode} from 'lexical'; // Since the TextNode is foundational to all Lexical packages, including the // plain text use case. Handling any rich text logic is undesirable. This creates diff --git a/packages/kg-default-nodes/lib/nodes/TKNode.ts b/packages/kg-default-nodes/lib/nodes/TKNode.ts index 5b1313972c..07e57b11c8 100644 --- a/packages/kg-default-nodes/lib/nodes/TKNode.ts +++ b/packages/kg-default-nodes/lib/nodes/TKNode.ts @@ -19,7 +19,7 @@ export class TKNode extends TextNode { const element = super.createDOM(config); const classes = config.theme.tk?.split(' ') || []; element.classList.add(...classes); - element.dataset.kgTk = "true"; + element.dataset.kgTk = 'true'; return element; } diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index 5bde4e4751..26bc344843 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -13,7 +13,7 @@ export type AudioNodeDataset = { }; export class AudioNode extends GeneratedKoenigDecoratorNode { - + // eslint-disable-next-line @typescript-eslint/no-unused-vars constructor(data: AudioNodeDataset) { super( { diff --git a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts index 7a5ca3c57b..30aa304bfd 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts @@ -1,11 +1,10 @@ -import { DOMConversion, DOMConversionMap, DOMConversionOutput } from "lexical"; -import { $createAudioNode, AudioNodeDataset } from "./AudioNode"; -import { KoenigDecoratorNode } from "../../KoenigDecoratorNode"; +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; +import {$createAudioNode, AudioNodeDataset} from './AudioNode'; +import {KoenigDecoratorNode} from '../../KoenigDecoratorNode'; // TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode type AudioNode = KoenigDecoratorNode; - export function parseAudioNode(): DOMConversionMap | null { return { div: (nodeElem: HTMLElement): DOMConversion | null => { diff --git a/packages/kg-default-nodes/lib/nodes/button/button-parser.ts b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts index bb4a49f0d0..1b4427deb5 100644 --- a/packages/kg-default-nodes/lib/nodes/button/button-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts @@ -1,11 +1,11 @@ -import { DOMConversion, DOMConversionMap, DOMConversionOutput } from "lexical"; -import { $createButtonNode } from "./ButtonNode"; -import { KoenigDecoratorNode } from "../../KoenigDecoratorNode"; +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; +import {$createButtonNode} from './ButtonNode'; +import {KoenigDecoratorNode} from '../../KoenigDecoratorNode'; // TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode type ButtonNode = KoenigDecoratorNode; -export const parseButtonNode = (ButtonNode: ButtonNode): DOMConversionMap | null => { +export const parseButtonNode = (): DOMConversionMap | null => { return { div: (nodeElem: HTMLElement): DOMConversion | null => { const isButtonCard = nodeElem.classList?.contains('kg-button-card'); @@ -38,4 +38,4 @@ export const parseButtonNode = (ButtonNode: ButtonNode): DOMConversionMap | null return null; } }; -} +}; diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index a0a49e8b1a..e36d6841c8 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -15,7 +15,7 @@ "test:unit": "NODE_ENV=testing c8 --lib --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", "test": "yarn test:unit", "test:noc8": "NODE_ENV=testing mocha './test/**/*.test.js'", - "lint:code": "eslint *.js lib/ --ext .js --cache && eslint *.ts lib/ --ext .ts --cache", + "lint:code": "eslint lib/ --ext .js --cache && eslint lib/ --ext .ts --cache", "lint": "yarn lint:code && yarn lint:test", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" }, From 796fc5082c37702078a3eb9a881040e2095a89fb Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 7 Feb 2024 12:02:29 -0600 Subject: [PATCH 15/33] drop to ts 5.2.0 to support ghost ts linter --- packages/kg-default-nodes/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index e36d6841c8..267924ad79 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -43,7 +43,7 @@ "rollup": "4.9.5", "should": "13.2.3", "sinon": "17.0.1", - "typescript": "^5.3.3" + "typescript": "^5.2.0" }, "dependencies": { "@lexical/clipboard": "0.12.2", diff --git a/yarn.lock b/yarn.lock index 47e6591164..fa97176d34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19043,7 +19043,7 @@ typescript@5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -typescript@5.3.3, "typescript@>=3 < 6", typescript@^5.3.3: +typescript@5.3.3, "typescript@>=3 < 6", typescript@^5.2.0: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== From 35941db48182139fa2e7394dd76ad63c34009387 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 8 Feb 2024 11:16:18 -0600 Subject: [PATCH 16/33] revert constructor changes, weren't actually working - tsc wasn't running with test:noc8 --- .../lib/nodes/audio/AudioNode.ts | 32 ++++++++----------- packages/kg-default-nodes/package.json | 2 +- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index 26bc344843..a5681aa6b0 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {LexicalNode} from 'lexical'; -import {GeneratedKoenigDecoratorNode} from '../../generate-decorator-node'; +import {generateDecoratorNode} from '../../generate-decorator-node'; import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; @@ -12,23 +12,18 @@ export type AudioNodeDataset = { thumbnailSrc?: string; }; -export class AudioNode extends GeneratedKoenigDecoratorNode { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - constructor(data: AudioNodeDataset) { - super( - { - nodeType: 'audio', - properties: [ - {name: 'duration', default: 0}, - {name: 'mimeType', default: ''}, - {name: 'src', default: '', urlType: 'url'}, - {name: 'title', default: ''}, - {name: 'thumbnailSrc', default: ''} - ] - } - ); +export class AudioNode extends generateDecoratorNode( + { + nodeType: 'audio', + properties: [ + {name: 'duration', default: 0}, + {name: 'mimeType', default: ''}, + {name: 'src', default: '', urlType: 'url'}, + {name: 'title', default: ''}, + {name: 'thumbnailSrc', default: ''} + ] } - +) { static importDOM() { return parseAudioNode(); } @@ -42,7 +37,8 @@ export class AudioNode extends GeneratedKoenigDecoratorNode { // and the fact that the constructor is obscured, so it's pulling from the DecoratorNode class // // Given that we call the $create methods in koenig-lexical, we really want to have type safety on the dataset properties -export const $createAudioNode = (dataset: AudioNodeDataset) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const $createAudioNode = (dataset: any) => { return new AudioNode(dataset); }; diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index 267924ad79..50b1463095 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -14,7 +14,7 @@ "pretest": "yarn build", "test:unit": "NODE_ENV=testing c8 --lib --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", "test": "yarn test:unit", - "test:noc8": "NODE_ENV=testing mocha './test/**/*.test.js'", + "test:noc8": "tsc && NODE_ENV=testing mocha './test/**/*.test.js'", "lint:code": "eslint lib/ --ext .js --cache && eslint lib/ --ext .ts --cache", "lint": "yarn lint:code && yarn lint:test", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" From fda4b132a5ac2885074e697c1dc4e88ac4a41104 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 8 Feb 2024 13:32:58 -0600 Subject: [PATCH 17/33] fix decorator node generation and dataset type --- .../lib/generate-decorator-node.ts | 6 ++- .../lib/nodes/audio/AudioNode.ts | 40 ++++++++++--------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index 586cca534b..8d19d0eb6e 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -59,7 +59,8 @@ type SerializedKoenigDecoratorNode = { }; export class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { - constructor(data: GenerateKoenigDecoratorNodeOptions) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(data: any) { super(); this.generateDecoratorNode(data); } @@ -70,7 +71,8 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, // Adds a `privateName` field to the properties for convenience (e.g. `__name`): // properties: [{name: 'name', privateName: '__name', type: 'string', default: 'hello'}, {...}] - const __properties: PrivateKoenigProperty[] = properties.map((prop) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const __properties: PrivateKoenigProperty[] = properties.map((prop: any) => { return {...prop, privateName: `__${prop.name}`}; }); diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index a5681aa6b0..71399d3a94 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -1,6 +1,6 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {LexicalNode} from 'lexical'; -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {generateDecoratorNode, KoenigDecoratorNodeProperties} from '../../generate-decorator-node'; import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; @@ -12,18 +12,27 @@ export type AudioNodeDataset = { thumbnailSrc?: string; }; -export class AudioNode extends generateDecoratorNode( - { - nodeType: 'audio', - properties: [ - {name: 'duration', default: 0}, - {name: 'mimeType', default: ''}, - {name: 'src', default: '', urlType: 'url'}, - {name: 'title', default: ''}, - {name: 'thumbnailSrc', default: ''} - ] +type AudioNodeProps = { + nodeType: 'audio'; + properties: KoenigDecoratorNodeProperties +}; + +const audioNodeProps: AudioNodeProps = { + nodeType: 'audio', + properties: [ + {name: 'duration', default: 0}, + {name: 'mimeType', default: ''}, + {name: 'src', default: '', urlType: 'url'}, + {name: 'title', default: ''}, + {name: 'thumbnailSrc', default: ''} + ] +}; + +export class AudioNode extends generateDecoratorNode(audioNodeProps) { + constructor(dataset: AudioNodeDataset) { + super(dataset); } -) { + static importDOM() { return parseAudioNode(); } @@ -33,12 +42,7 @@ export class AudioNode extends generateDecoratorNode( } } -// TODO: Not sure how to handle the 'any' here; it's a result of the `generateDecoratorNode` function -// and the fact that the constructor is obscured, so it's pulling from the DecoratorNode class -// -// Given that we call the $create methods in koenig-lexical, we really want to have type safety on the dataset properties -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const $createAudioNode = (dataset: any) => { +export const $createAudioNode = (dataset: AudioNodeDataset) => { return new AudioNode(dataset); }; From 8822d9c88e4766f9fbdbe7c2a20cedcdf9eee552 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 8 Feb 2024 13:41:26 -0600 Subject: [PATCH 18/33] remove any --- packages/kg-default-nodes/lib/generate-decorator-node.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index 8d19d0eb6e..a12edc8810 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -71,8 +71,7 @@ export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, // Adds a `privateName` field to the properties for convenience (e.g. `__name`): // properties: [{name: 'name', privateName: '__name', type: 'string', default: 'hello'}, {...}] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const __properties: PrivateKoenigProperty[] = properties.map((prop: any) => { + const __properties: PrivateKoenigProperty[] = properties.map((prop) => { return {...prop, privateName: `__${prop.name}`}; }); From 5e9a022e14507a6312c921bae131a9b17fbccd2c Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 8 Feb 2024 13:46:12 -0600 Subject: [PATCH 19/33] add notes --- packages/kg-default-nodes/lib/generate-decorator-node.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index a12edc8810..1b98ec2a1d 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -43,6 +43,9 @@ function validateArguments(nodeType: string, properties: KoenigDecoratorNodeProp type PrivateKoenigProperty = KoenigDecoratorProperty & {privateName: string}; +// NOTE: This is really what the return type is, but we wrap it in the GeneratedKoenigDecoratorNode class to make it a bit easier to interpret +// the 'magic' behind the scenes that generates the KoenigDecoratorNode classes. +// type GenerateKoenigDecoratorNodeFn = (options: GenerateKoenigDecoratorNodeOptions) => typeof generateDecoratorNode.prototype; type GenerateKoenigDecoratorNodeFn = (options: GenerateKoenigDecoratorNodeOptions) => typeof GeneratedKoenigDecoratorNode; type GenerateKoenigDecoratorNodeOptions = { @@ -57,15 +60,13 @@ type SerializedKoenigDecoratorNode = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; }; - -export class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { +class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(data: any) { super(); this.generateDecoratorNode(data); } } - export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, properties = [], version = 1}) => { validateArguments(nodeType, properties); From 406cece6ce1c4a959cfd12aebac5035c689f1376 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 8 Feb 2024 16:20:45 -0600 Subject: [PATCH 20/33] update nodes --- .../lib/generate-decorator-node.ts | 4 +- .../lib/nodes/audio/AudioNode.ts | 4 +- .../{BookmarkNode.js => BookmarkNode.ts} | 46 ++++++++++++----- .../lib/nodes/button/ButtonNode.js | 28 ----------- .../lib/nodes/button/ButtonNode.ts | 47 ++++++++++++++++++ .../lib/nodes/button/button-parser.ts | 6 +-- .../{CalloutNode.js => CalloutNode.ts} | 35 +++++++++---- .../{CodeBlockNode.js => CodeBlockNode.ts} | 27 +++++++--- .../{CollectionNode.js => CollectionNode.ts} | 33 ++++++++++--- .../lib/nodes/email-cta/EmailCtaNode.js | 27 ---------- .../lib/nodes/email-cta/EmailCtaNode.ts | 47 ++++++++++++++++++ .../lib/nodes/email/EmailNode.js | 21 -------- .../lib/nodes/email/EmailNode.ts | 34 +++++++++++++ .../embed/{EmbedNode.js => EmbedNode.ts} | 29 ++++++++--- .../nodes/file/{FileNode.js => FileNode.ts} | 40 +++++++++++---- .../{GalleryNode.js => GalleryNode.ts} | 30 +++++++++--- ...ontalRuleNode.js => HorizontalRuleNode.ts} | 13 ++++- .../lib/nodes/html/HtmlNode.js | 30 ------------ .../lib/nodes/html/HtmlNode.ts | 43 ++++++++++++++++ .../image/{ImageNode.js => ImageNode.ts} | 33 ++++++++++--- .../lib/nodes/markdown/MarkdownNode.js | 25 ---------- .../lib/nodes/markdown/MarkdownNode.ts | 38 ++++++++++++++ .../{PaywallNode.js => PaywallNode.ts} | 19 +++++-- .../{ProductNode.js => ProductNode.ts} | 36 +++++++++++--- .../signup/{SignupNode.js => SignupNode.ts} | 49 ++++++++++++++----- .../lib/nodes/toggle/ToggleNode.js | 27 ---------- .../lib/nodes/toggle/ToggleNode.ts | 41 ++++++++++++++++ .../video/{VideoNode.js => VideoNode.ts} | 38 +++++++++++--- .../kg-default-nodes/test/nodes/file.test.js | 1 + 29 files changed, 592 insertions(+), 259 deletions(-) rename packages/kg-default-nodes/lib/nodes/bookmark/{BookmarkNode.js => BookmarkNode.ts} (67%) delete mode 100644 packages/kg-default-nodes/lib/nodes/button/ButtonNode.js create mode 100644 packages/kg-default-nodes/lib/nodes/button/ButtonNode.ts rename packages/kg-default-nodes/lib/nodes/callout/{CalloutNode.js => CalloutNode.ts} (52%) rename packages/kg-default-nodes/lib/nodes/codeblock/{CodeBlockNode.js => CodeBlockNode.ts} (50%) rename packages/kg-default-nodes/lib/nodes/collection/{CollectionNode.js => CollectionNode.ts} (56%) delete mode 100644 packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js create mode 100644 packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.ts delete mode 100644 packages/kg-default-nodes/lib/nodes/email/EmailNode.js create mode 100644 packages/kg-default-nodes/lib/nodes/email/EmailNode.ts rename packages/kg-default-nodes/lib/nodes/embed/{EmbedNode.js => EmbedNode.ts} (52%) rename packages/kg-default-nodes/lib/nodes/file/{FileNode.js => FileNode.ts} (58%) rename packages/kg-default-nodes/lib/nodes/gallery/{GalleryNode.js => GalleryNode.ts} (54%) rename packages/kg-default-nodes/lib/nodes/horizontalrule/{HorizontalRuleNode.js => HorizontalRuleNode.ts} (65%) delete mode 100644 packages/kg-default-nodes/lib/nodes/html/HtmlNode.js create mode 100644 packages/kg-default-nodes/lib/nodes/html/HtmlNode.ts rename packages/kg-default-nodes/lib/nodes/image/{ImageNode.js => ImageNode.ts} (66%) delete mode 100644 packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js create mode 100644 packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.ts rename packages/kg-default-nodes/lib/nodes/paywall/{PaywallNode.js => PaywallNode.ts} (50%) rename packages/kg-default-nodes/lib/nodes/product/{ProductNode.js => ProductNode.ts} (69%) rename packages/kg-default-nodes/lib/nodes/signup/{SignupNode.js => SignupNode.ts} (72%) delete mode 100644 packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js create mode 100644 packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.ts rename packages/kg-default-nodes/lib/nodes/video/{VideoNode.js => VideoNode.ts} (71%) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.ts b/packages/kg-default-nodes/lib/generate-decorator-node.ts index 1b98ec2a1d..0c0f03789e 100644 --- a/packages/kg-default-nodes/lib/generate-decorator-node.ts +++ b/packages/kg-default-nodes/lib/generate-decorator-node.ts @@ -11,7 +11,7 @@ import readTextContent from './utils/read-text-content'; export type KoenigDecoratorProperty = { name: string; - default?: number | string | boolean | null; + default?: number | string | boolean | null | object; urlType?: 'url'|'html'|'markdown'; wordCount?: boolean; }; @@ -62,7 +62,7 @@ type SerializedKoenigDecoratorNode = { }; class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode { // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(data: any) { + constructor(data?: any) { super(); this.generateDecoratorNode(data); } diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts index 71399d3a94..2ad2d2fe7f 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/AudioNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {LexicalNode} from 'lexical'; +import {DOMConversionMap, LexicalNode} from 'lexical'; import {generateDecoratorNode, KoenigDecoratorNodeProperties} from '../../generate-decorator-node'; import {parseAudioNode} from './audio-parser'; import {renderAudioNode} from './audio-renderer'; @@ -33,7 +33,7 @@ export class AudioNode extends generateDecoratorNode(audioNodeProps) { super(dataset); } - static importDOM() { + static importDOM(): DOMConversionMap | null { return parseAudioNode(); } diff --git a/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js b/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts similarity index 67% rename from packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js rename to packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts index 2eb10fa9e3..711bbd35b8 100644 --- a/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js +++ b/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts @@ -1,9 +1,31 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseBookmarkNode} from './bookmark-parser'; import {renderBookmarkNode} from './bookmark-renderer'; -export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', +export type BookmarkNodeDataset = { + url?: string; + metadata?: { + icon?: string; + title?: string; + description?: string; + author?: string; + publisher?: string; + thumbnail?: string; + }; + caption?: string; +}; + +export type SerializedBookmarkNode = Spread; + +type BookmarkNodeProps = { + nodeType: 'bookmark'; + properties: KoenigDecoratorNodeProperties +}; + +const bookmarkNodeProps: BookmarkNodeProps = { + nodeType: 'bookmark', properties: [ {name: 'title', default: '', wordCount: true}, {name: 'description', default: '', wordCount: true}, @@ -13,8 +35,10 @@ export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', {name: 'publisher', default: ''}, {name: 'icon', default: '', urlType: 'url'}, {name: 'thumbnail', default: '', urlType: 'url'} - ]} -) { + ] +}; + +export class BookmarkNode extends generateDecoratorNode(bookmarkNodeProps) { static importDOM() { return parseBookmarkNode(this); } @@ -24,7 +48,7 @@ export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', } /* override */ - constructor({url, metadata, caption} = {}, key) { + constructor({url, metadata, caption}: BookmarkNodeDataset = {}, key?: NodeKey) { super(key); this.__url = url || ''; this.__icon = metadata?.icon || ''; @@ -37,7 +61,7 @@ export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', } /* @override */ - getDataset() { + getDataset(): BookmarkNodeDataset { const self = this.getLatest(); return { url: self.__url, @@ -54,7 +78,7 @@ export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', } /* @override */ - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedBookmarkNode): BookmarkNode { const {url, metadata, caption} = serializedNode; const node = new this({ url, @@ -65,7 +89,7 @@ export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', } /* @override */ - exportJSON() { + exportJSON(): SerializedBookmarkNode { const dataset = { type: 'bookmark', version: 1, @@ -83,15 +107,15 @@ export class BookmarkNode extends generateDecoratorNode({nodeType: 'bookmark', return dataset; } - isEmpty() { + isEmpty(): boolean { return !this.url; } } -export const $createBookmarkNode = (dataset) => { +export const $createBookmarkNode = (dataset: BookmarkNodeDataset) => { return new BookmarkNode(dataset); }; -export function $isBookmarkNode(node) { +export function $isBookmarkNode(node: LexicalNode) { return node instanceof BookmarkNode; } diff --git a/packages/kg-default-nodes/lib/nodes/button/ButtonNode.js b/packages/kg-default-nodes/lib/nodes/button/ButtonNode.js deleted file mode 100644 index 985e4bf4e9..0000000000 --- a/packages/kg-default-nodes/lib/nodes/button/ButtonNode.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseButtonNode} from './button-parser'; -import {renderButtonNode} from './button-renderer'; - -export class ButtonNode extends generateDecoratorNode({nodeType: 'button', - properties: [ - {name: 'buttonText', default: ''}, - {name: 'alignment', default: 'center'}, - {name: 'buttonUrl', default: '', urlType: 'url'} - ]} -) { - static importDOM() { - return parseButtonNode(this); - } - - exportDOM(options = {}) { - return renderButtonNode(this, options); - } -} - -export const $createButtonNode = (dataset) => { - return new ButtonNode(dataset); -}; - -export function $isButtonNode(node) { - return node instanceof ButtonNode; -} diff --git a/packages/kg-default-nodes/lib/nodes/button/ButtonNode.ts b/packages/kg-default-nodes/lib/nodes/button/ButtonNode.ts new file mode 100644 index 0000000000..6ff6ca7bb7 --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/button/ButtonNode.ts @@ -0,0 +1,47 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +import {DOMConversionMap, LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {parseButtonNode} from './button-parser'; +import {renderButtonNode} from './button-renderer'; + +export type ButtonNodeDataset = { + buttonText?: string | null; + alignment?: string | null; + buttonUrl?: string | null; +}; + +type ButtonNodeProps = { + nodeType: 'button'; + properties: KoenigDecoratorNodeProperties +}; + +const buttonNodeProps: ButtonNodeProps = { + nodeType: 'button', + properties: [ + {name: 'buttonText', default: ''}, + {name: 'alignment', default: 'center'}, + {name: 'buttonUrl', default: '', urlType: 'url'} + ] +}; + +export class ButtonNode extends generateDecoratorNode(buttonNodeProps) { + constructor(dataset: ButtonNodeDataset) { + super(dataset); + } + + static importDOM(): DOMConversionMap | null { + return parseButtonNode(); + } + + exportDOM(options = {}) { + return renderButtonNode(this, options); + } +} + +export const $createButtonNode = (dataset: ButtonNodeDataset) => { + return new ButtonNode(dataset); +}; + +export function $isButtonNode(node: LexicalNode) { + return node instanceof ButtonNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/button/button-parser.ts b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts index 1b4427deb5..861e28e521 100644 --- a/packages/kg-default-nodes/lib/nodes/button/button-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts @@ -1,9 +1,5 @@ import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; import {$createButtonNode} from './ButtonNode'; -import {KoenigDecoratorNode} from '../../KoenigDecoratorNode'; - -// TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode -type ButtonNode = KoenigDecoratorNode; export const parseButtonNode = (): DOMConversionMap | null => { return { @@ -29,7 +25,7 @@ export const parseButtonNode = (): DOMConversionMap | null => { buttonUrl: buttonUrl }; - const node = $createButtonNode(payload) as ButtonNode; + const node = $createButtonNode(payload); return {node}; }, priority: 1 diff --git a/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js b/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts similarity index 52% rename from packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js rename to packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts index 3caaae8fdc..4468ec51ec 100644 --- a/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js +++ b/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts @@ -1,17 +1,32 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {renderCalloutNode} from './callout-renderer'; import {parseCalloutNode} from './callout-parser'; +import {LexicalNode, NodeKey} from 'lexical'; -export class CalloutNode extends generateDecoratorNode({nodeType: 'callout', +export type CalloutNodeDataset = { + calloutText?: string; + calloutEmoji?: string; + backgroundColor?: string; +}; + +type CalloutNodeProps = { + nodeType: 'callout'; + properties: KoenigDecoratorNodeProperties +}; + +const calloutNodeProps: CalloutNodeProps = { + nodeType: 'callout', properties: [ {name: 'calloutText', default: '', wordCount: true}, {name: 'calloutEmoji', default: '💡'}, {name: 'backgroundColor', default: 'blue'} - ]} -) { + ] +}; + +export class CalloutNode extends generateDecoratorNode(calloutNodeProps) { /* override */ - constructor({calloutText, calloutEmoji, backgroundColor} = {}, key) { + constructor({calloutText, calloutEmoji, backgroundColor}: CalloutNodeDataset = {}, key?: NodeKey) { super(key); this.__calloutText = calloutText || ''; this.__calloutEmoji = calloutEmoji !== undefined ? calloutEmoji : '💡'; @@ -27,10 +42,10 @@ export class CalloutNode extends generateDecoratorNode({nodeType: 'callout', } } -export function $isCalloutNode(node) { - return node instanceof CalloutNode; -} - -export const $createCalloutNode = (dataset) => { +export const $createCalloutNode = (dataset: CalloutNodeDataset) => { return new CalloutNode(dataset); }; + +export function $isCalloutNode(node: LexicalNode) { + return node instanceof CalloutNode; +} \ No newline at end of file diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js b/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts similarity index 50% rename from packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js rename to packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts index 751d20531c..84a360ec8f 100644 --- a/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js +++ b/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts @@ -1,15 +1,30 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseCodeBlockNode} from './codeblock-parser'; import {renderCodeBlockNode} from './codeblock-renderer'; -export class CodeBlockNode extends generateDecoratorNode({nodeType: 'codeblock', +export type CodeBlockNodeDataset = { + code?: string; + language?: string; + caption?: string; +}; + +type CodeBlockNodeProps = { + nodeType: 'codeblock'; + properties: KoenigDecoratorNodeProperties +}; + +const codeBlockNodeProps: CodeBlockNodeProps = { + nodeType: 'codeblock', properties: [ {name: 'code', default: '', wordCount: true}, {name: 'language', default: ''}, {name: 'caption', default: '', urlType: 'html', wordCount: true} - ]} -) { + ] +}; + +export class CodeBlockNode extends generateDecoratorNode(codeBlockNodeProps) { static importDOM() { return parseCodeBlockNode(this); } @@ -23,10 +38,10 @@ export class CodeBlockNode extends generateDecoratorNode({nodeType: 'codeblock', } } -export function $createCodeBlockNode(dataset) { +export function $createCodeBlockNode(dataset: CodeBlockNodeDataset) { return new CodeBlockNode(dataset); } -export function $isCodeBlockNode(node) { +export function $isCodeBlockNode(node: LexicalNode) { return node instanceof CodeBlockNode; } diff --git a/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js b/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts similarity index 56% rename from packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js rename to packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts index c58d9a83ee..1f016ad081 100644 --- a/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js +++ b/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts @@ -1,17 +1,34 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {renderCollectionNode} from './collection-renderer'; import {collectionParser} from './collection-parser'; +import {LexicalNode} from 'lexical'; -export class CollectionNode extends generateDecoratorNode({nodeType: 'collection', +export type CollectionNodeDataset = { + collection?: string; + postCount?: number; + layout?: string; + columns?: number; + header?: string; +}; + +type CollectionNodeProps = { + nodeType: 'collection'; + properties: KoenigDecoratorNodeProperties +}; + +const collectionNodeProps: CollectionNodeProps = { + nodeType: 'collection', properties: [ {name: 'collection', default: 'latest'}, // start with empty object; might want to just store the slug {name: 'postCount', default: 3}, {name: 'layout', default: 'grid'}, {name: 'columns', default: 3}, {name: 'header', default: '', wordCount: true} - ]} -) { + ] +}; + +export class CollectionNode extends generateDecoratorNode(collectionNodeProps) { static importDOM() { return collectionParser(this); } @@ -24,7 +41,9 @@ export class CollectionNode extends generateDecoratorNode({nodeType: 'collection return true; } - async getDynamicData(options = {}) { + // TODO: build the options object + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getDynamicData(options: any = {}) { const key = this.getKey(); const collection = this.__collection; const postCount = this.__postCount; @@ -38,10 +57,10 @@ export class CollectionNode extends generateDecoratorNode({nodeType: 'collection } } -export const $createCollectionNode = (dataset) => { +export const $createCollectionNode = (dataset: CollectionNodeDataset) => { return new CollectionNode(dataset); }; -export function $isCollectionNode(node) { +export function $isCollectionNode(node: LexicalNode) { return node instanceof CollectionNode; } diff --git a/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js b/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js deleted file mode 100644 index 271d51d7c8..0000000000 --- a/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderEmailCtaNode} from './email-cta-renderer'; - -export class EmailCtaNode extends generateDecoratorNode({nodeType: 'email-cta', - properties: [ - {name: 'alignment', default: 'left'}, - {name: 'buttonText', default: ''}, - {name: 'buttonUrl', default: '', urlType: 'url'}, - {name: 'html', default: '', urlType: 'html'}, - {name: 'segment', default: 'status:free'}, - {name: 'showButton', default: false}, - {name: 'showDividers', default: true} - ]} -) { - exportDOM(options = {}) { - return renderEmailCtaNode(this, options); - } -} - -export const $createEmailCtaNode = (dataset) => { - return new EmailCtaNode(dataset); -}; - -export function $isEmailCtaNode(node) { - return node instanceof EmailCtaNode; -} diff --git a/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.ts b/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.ts new file mode 100644 index 0000000000..0b644e96cd --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.ts @@ -0,0 +1,47 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {renderEmailCtaNode} from './email-cta-renderer'; + +export type EmailCtaNodeDataset = { + alignment?: string; + buttonText?: string; + buttonUrl?: string; + html?: string; + segment?: string; + showButton?: boolean; + showDividers?: boolean; +}; + +type EmailCtaNodeProps = { + nodeType: 'email-cta'; + properties: KoenigDecoratorNodeProperties; +}; + +const emailCtaNodeProps: EmailCtaNodeProps = { + nodeType: 'email-cta', + properties: [ + {name: 'alignment', default: 'left'}, + {name: 'buttonText', default: ''}, + {name: 'buttonUrl', default: '', urlType: 'url'}, + {name: 'html', default: '', urlType: 'html'}, + {name: 'segment', default: 'status:free'}, + {name: 'showButton', default: false}, + {name: 'showDividers', default: true} + ] +}; + +export class EmailCtaNode extends generateDecoratorNode(emailCtaNodeProps) { + // TODO: build options + exportDOM(options = {}) { + return renderEmailCtaNode(this, options); + } +} + +export const $createEmailCtaNode = (dataset: EmailCtaNodeDataset) => { + return new EmailCtaNode(dataset); +}; + +export function $isEmailCtaNode(node: LexicalNode) { + return node instanceof EmailCtaNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/email/EmailNode.js b/packages/kg-default-nodes/lib/nodes/email/EmailNode.js deleted file mode 100644 index 39bd5067ff..0000000000 --- a/packages/kg-default-nodes/lib/nodes/email/EmailNode.js +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderEmailNode} from './email-renderer'; - -export class EmailNode extends generateDecoratorNode({nodeType: 'email', - properties: [ - {name: 'html', default: '', urlType: 'html'} - ]} -) { - exportDOM(options = {}) { - return renderEmailNode(this, options); - } -} - -export const $createEmailNode = (dataset) => { - return new EmailNode(dataset); -}; - -export function $isEmailNode(node) { - return node instanceof EmailNode; -} diff --git a/packages/kg-default-nodes/lib/nodes/email/EmailNode.ts b/packages/kg-default-nodes/lib/nodes/email/EmailNode.ts new file mode 100644 index 0000000000..65a7636cad --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/email/EmailNode.ts @@ -0,0 +1,34 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {renderEmailNode} from './email-renderer'; + +export type EmailNodeDataset = { + html?: string; +}; + +type EmailNodeProps = { + nodeType: 'email'; + properties: KoenigDecoratorNodeProperties; +}; + +const emailNodeProps: EmailNodeProps = { + nodeType: 'email', + properties: [ + {name: 'html', default: '', urlType: 'html'} + ] +}; + +export class EmailNode extends generateDecoratorNode(emailNodeProps) { + exportDOM(options = {}) { + return renderEmailNode(this, options); + } +} + +export const $createEmailNode = (dataset: EmailNodeDataset) => { + return new EmailNode(dataset); +}; + +export function $isEmailNode(node: LexicalNode) { + return node instanceof EmailNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js b/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts similarity index 52% rename from packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js rename to packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts index d6d4e05fd0..39608b2f87 100644 --- a/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js +++ b/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts @@ -1,17 +1,34 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseEmbedNode} from './embed-parser'; import {renderEmbedNode} from './embed-renderer'; -export class EmbedNode extends generateDecoratorNode({nodeType: 'embed', +export type EmbedNodeDataset = { + url?: string; + embedType?: string; + html?: string; + metadata?: object; + caption?: string; +}; + +type EmbedNodeProps = { + nodeType: 'embed'; + properties: KoenigDecoratorNodeProperties; +}; + +const embedNodeProps: EmbedNodeProps = { + nodeType: 'embed', properties: [ {name: 'url', default: '', urlType: 'url'}, {name: 'embedType', default: ''}, {name: 'html', default: ''}, {name: 'metadata', default: {}}, {name: 'caption', default: '', wordCount: true} - ]} -) { + ] +}; + +export class EmbedNode extends generateDecoratorNode(embedNodeProps) { static importDOM() { return parseEmbedNode(this); } @@ -25,10 +42,10 @@ export class EmbedNode extends generateDecoratorNode({nodeType: 'embed', } } -export const $createEmbedNode = (dataset) => { +export const $createEmbedNode = (dataset: EmbedNodeDataset) => { return new EmbedNode(dataset); }; -export function $isEmbedNode(node) { +export function $isEmbedNode(node: LexicalNode) { return node instanceof EmbedNode; } diff --git a/packages/kg-default-nodes/lib/nodes/file/FileNode.js b/packages/kg-default-nodes/lib/nodes/file/FileNode.ts similarity index 58% rename from packages/kg-default-nodes/lib/nodes/file/FileNode.js rename to packages/kg-default-nodes/lib/nodes/file/FileNode.ts index 00d37268cb..6a73d2350b 100644 --- a/packages/kg-default-nodes/lib/nodes/file/FileNode.js +++ b/packages/kg-default-nodes/lib/nodes/file/FileNode.ts @@ -1,25 +1,45 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {renderFileNode} from './file-renderer'; import {parseFileNode} from './file-parser'; import {bytesToSize} from '../../utils/size-byte-converter'; +import {LexicalNode, SerializedLexicalNode, Spread} from 'lexical'; -export class FileNode extends generateDecoratorNode({nodeType: 'file', +export type FileNodeDataset = { + src?: string; + fileTitle?: string; + fileCaption?: string; + fileName?: string; + fileSize?: string; +}; + +type FileNodeProps = { + nodeType: 'file'; + properties: KoenigDecoratorNodeProperties; +}; + +const fileNodeProps: FileNodeProps = { + nodeType: 'file', properties: [ {name: 'src', default: '', urlType: 'url'}, {name: 'fileTitle', default: '', wordCount: true}, {name: 'fileCaption', default: '', wordCount: true}, {name: 'fileName', default: ''}, {name: 'fileSize', default: ''} - ]} -) { + ] +}; + +export type SerializedFileNode = Spread; + +export class FileNode extends generateDecoratorNode(fileNodeProps) { /* @override */ - exportJSON() { + exportJSON(): SerializedFileNode { const {src, fileTitle, fileCaption, fileName, fileSize} = this; const isBlob = src.startsWith('data:'); return { type: 'file', + version: 1, src: isBlob ? '' : src, fileTitle, fileCaption, @@ -41,10 +61,10 @@ export class FileNode extends generateDecoratorNode({nodeType: 'file', } } -export function $isFileNode(node) { - return node instanceof FileNode; -} - -export const $createFileNode = (dataset) => { +export const $createFileNode = (dataset: FileNodeDataset) => { return new FileNode(dataset); }; + +export function $isFileNode(node: LexicalNode) { + return node instanceof FileNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js b/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts similarity index 54% rename from packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js rename to packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts index b99c958e4b..d97f9f72fd 100644 --- a/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js +++ b/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts @@ -1,13 +1,31 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseGalleryNode} from './gallery-parser'; import {renderGalleryNode} from './gallery-renderer'; -export class GalleryNode extends generateDecoratorNode({nodeType: 'gallery', + +export type GalleryNodeDataset = { + images?: Array<{ + src?: string; + caption?: string; + }>; + caption?: string; +}; + +type GalleryNodeProps = { + nodeType: 'gallery'; + properties: KoenigDecoratorNodeProperties; +}; + +const galleryNodeProps: GalleryNodeProps = { + nodeType: 'gallery', properties: [ {name: 'images', default: []}, {name: 'caption', default: '', wordCount: true} - ]} -) { + ] +}; + +export class GalleryNode extends generateDecoratorNode(galleryNodeProps) { /* override */ static get urlTransformMap() { return { @@ -32,10 +50,10 @@ export class GalleryNode extends generateDecoratorNode({nodeType: 'gallery', } } -export const $createGalleryNode = (dataset) => { +export const $createGalleryNode = (dataset: GalleryNodeDataset) => { return new GalleryNode(dataset); }; -export function $isGalleryNode(node) { +export function $isGalleryNode(node: LexicalNode) { return node instanceof GalleryNode; } diff --git a/packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js b/packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.ts similarity index 65% rename from packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js rename to packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.ts index e896f2bcd7..03b916b1bb 100644 --- a/packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js +++ b/packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.ts @@ -2,8 +2,17 @@ import {generateDecoratorNode} from '../../generate-decorator-node'; import {renderHorizontalRuleNode} from './horizontalrule-renderer'; import {parseHorizontalRuleNode} from './horizontalrule-parser'; +import {LexicalNode} from 'lexical'; -export class HorizontalRuleNode extends generateDecoratorNode({nodeType: 'horizontalrule'}) { +type HorizontalRuleNodeProps = { + nodeType: 'horizontalrule'; +}; + +const horizontalRuleNodeProps: HorizontalRuleNodeProps = { + nodeType: 'horizontalrule' +}; + +export class HorizontalRuleNode extends generateDecoratorNode(horizontalRuleNodeProps) { static importDOM() { return parseHorizontalRuleNode(this); } @@ -25,6 +34,6 @@ export function $createHorizontalRuleNode() { return new HorizontalRuleNode(); } -export function $isHorizontalRuleNode(node) { +export function $isHorizontalRuleNode(node: LexicalNode) { return node instanceof HorizontalRuleNode; } diff --git a/packages/kg-default-nodes/lib/nodes/html/HtmlNode.js b/packages/kg-default-nodes/lib/nodes/html/HtmlNode.js deleted file mode 100644 index 4d293cecfc..0000000000 --- a/packages/kg-default-nodes/lib/nodes/html/HtmlNode.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderHtmlNode} from './html-renderer'; -import {parseHtmlNode} from './html-parser'; - -export class HtmlNode extends generateDecoratorNode({nodeType: 'html', - properties: [ - {name: 'html', default: '', urlType: 'html', wordCount: true} - ]} -) { - static importDOM() { - return parseHtmlNode(this); - } - - exportDOM(options = {}) { - return renderHtmlNode(this, options); - } - - isEmpty() { - return !this.__html; - } -} - -export function $createHtmlNode(dataset) { - return new HtmlNode(dataset); -} - -export function $isHtmlNode(node) { - return node instanceof HtmlNode; -} diff --git a/packages/kg-default-nodes/lib/nodes/html/HtmlNode.ts b/packages/kg-default-nodes/lib/nodes/html/HtmlNode.ts new file mode 100644 index 0000000000..41dcbf770b --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/html/HtmlNode.ts @@ -0,0 +1,43 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {renderHtmlNode} from './html-renderer'; +import {parseHtmlNode} from './html-parser'; +import {LexicalNode} from 'lexical'; + +export type HtmlNodeDataset = { + html?: string; +}; + +type HtmlNodeProps = { + nodeType: 'html'; + properties: KoenigDecoratorNodeProperties; +}; + +const htmlNodeProps: HtmlNodeProps = { + nodeType: 'html', + properties: [ + {name: 'html', default: '', urlType: 'html', wordCount: true} + ] +}; + +export class HtmlNode extends generateDecoratorNode(htmlNodeProps) { + static importDOM() { + return parseHtmlNode(this); + } + + exportDOM(options = {}) { + return renderHtmlNode(this, options); + } + + isEmpty() { + return !this.__html; + } +} + +export function $createHtmlNode(dataset: HtmlNodeDataset) { + return new HtmlNode(dataset); +} + +export function $isHtmlNode(node: LexicalNode) { + return node instanceof HtmlNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/image/ImageNode.js b/packages/kg-default-nodes/lib/nodes/image/ImageNode.ts similarity index 66% rename from packages/kg-default-nodes/lib/nodes/image/ImageNode.js rename to packages/kg-default-nodes/lib/nodes/image/ImageNode.ts index 4f9c609e03..dea91f5eea 100644 --- a/packages/kg-default-nodes/lib/nodes/image/ImageNode.js +++ b/packages/kg-default-nodes/lib/nodes/image/ImageNode.ts @@ -1,8 +1,27 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseImageNode} from './image-parser'; import {renderImageNode} from './image-renderer'; -export class ImageNode extends generateDecoratorNode({nodeType: 'image', + +export type ImageNodeDataset = { + src?: string; + caption?: string; + title?: string; + alt?: string; + cardWidth?: string; + width?: number; + height?: number; + href?: string; +}; + +type ImageNodeProps = { + nodeType: 'image'; + properties: KoenigDecoratorNodeProperties; +}; + +const imageNodeProps: ImageNodeProps = { + nodeType: 'image', properties: [ {name: 'src', default: '', urlType: 'url'}, {name: 'caption', default: '', urlType: 'html', wordCount: true}, @@ -12,8 +31,10 @@ export class ImageNode extends generateDecoratorNode({nodeType: 'image', {name: 'width', default: null}, {name: 'height', default: null}, {name: 'href', default: '', urlType: 'url'} - ]} -) { + ] +}; + +export class ImageNode extends generateDecoratorNode(imageNodeProps) { /* @override */ exportJSON() { // checks if src is a data string @@ -48,10 +69,10 @@ export class ImageNode extends generateDecoratorNode({nodeType: 'image', } } -export const $createImageNode = (dataset) => { +export const $createImageNode = (dataset: ImageNodeDataset) => { return new ImageNode(dataset); }; -export function $isImageNode(node) { +export function $isImageNode(node: LexicalNode) { return node instanceof ImageNode; } diff --git a/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js b/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js deleted file mode 100644 index a57d64b817..0000000000 --- a/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderMarkdownNode} from './markdown-renderer'; - -export class MarkdownNode extends generateDecoratorNode({nodeType: 'markdown', - properties: [ - {name: 'markdown', default: '', urlType: 'markdown', wordCount: true} - ]} -) { - exportDOM(options = {}) { - return renderMarkdownNode(this, options); - } - - isEmpty() { - return !this.__markdown; - } -} - -export function $createMarkdownNode(dataset) { - return new MarkdownNode(dataset); -} - -export function $isMarkdownNode(node) { - return node instanceof MarkdownNode; -} diff --git a/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.ts b/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.ts new file mode 100644 index 0000000000..15f583ba8c --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.ts @@ -0,0 +1,38 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {renderMarkdownNode} from './markdown-renderer'; + +export type MarkdownNodeDataset = { + markdown?: string; +}; + +type MarkdownNodeProps = { + nodeType: 'markdown'; + properties: KoenigDecoratorNodeProperties; +}; + +const markdownNodeProps: MarkdownNodeProps = { + nodeType: 'markdown', + properties: [ + {name: 'markdown', default: '', urlType: 'markdown', wordCount: true} + ] +}; + +export class MarkdownNode extends generateDecoratorNode(markdownNodeProps) { + exportDOM(options = {}) { + return renderMarkdownNode(this, options); + } + + isEmpty() { + return !this.__markdown; + } +} + +export function $createMarkdownNode(dataset: MarkdownNodeDataset) { + return new MarkdownNode(dataset); +} + +export function $isMarkdownNode(node: LexicalNode) { + return node instanceof MarkdownNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js b/packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.ts similarity index 50% rename from packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js rename to packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.ts index b825b6d735..769a332f52 100644 --- a/packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js +++ b/packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.ts @@ -1,9 +1,20 @@ /* eslint-disable ghost/filenames/match-exported-class */ +import {LexicalNode} from 'lexical'; import {generateDecoratorNode} from '../../generate-decorator-node'; import {parsePaywallNode} from './paywall-parser'; import {renderPaywallNode} from './paywall-renderer'; -export class PaywallNode extends generateDecoratorNode({nodeType: 'paywall'}) { +export type PaywallNodeDataset = object; // {} is the default value + +type PaywallNodeProps = { + nodeType: 'paywall'; +}; + +const paywallNodeProps: PaywallNodeProps = { + nodeType: 'paywall' +}; + +export class PaywallNode extends generateDecoratorNode(paywallNodeProps) { static importDOM() { return parsePaywallNode(this); } @@ -13,10 +24,10 @@ export class PaywallNode extends generateDecoratorNode({nodeType: 'paywall'}) { } } -export const $createPaywallNode = (dataset) => { - return new PaywallNode(dataset); +export const $createPaywallNode = () => { + return new PaywallNode(); }; -export function $isPaywallNode(node) { +export function $isPaywallNode(node: LexicalNode) { return node instanceof PaywallNode; } diff --git a/packages/kg-default-nodes/lib/nodes/product/ProductNode.js b/packages/kg-default-nodes/lib/nodes/product/ProductNode.ts similarity index 69% rename from packages/kg-default-nodes/lib/nodes/product/ProductNode.js rename to packages/kg-default-nodes/lib/nodes/product/ProductNode.ts index a3b33c6222..c11c0811c7 100644 --- a/packages/kg-default-nodes/lib/nodes/product/ProductNode.js +++ b/packages/kg-default-nodes/lib/nodes/product/ProductNode.ts @@ -1,9 +1,29 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseProductNode} from './product-parser'; import {renderProductNode} from './product-renderer'; -export class ProductNode extends generateDecoratorNode({nodeType: 'product', +export type ProductNodeDataset = { + productImageSrc?: string; + productImageWidth?: number; + productImageHeight?: number; + productTitle?: string; + productDescription?: string; + productRatingEnabled?: boolean; + productStarRating?: number; + productButtonEnabled?: boolean; + productButton?: string; + productUrl?: string; +}; + +type ProductNodeProps = { + nodeType: 'product'; + properties: KoenigDecoratorNodeProperties; +}; + +const productNodeProps: ProductNodeProps = { + nodeType: 'product', properties: [ {name: 'productImageSrc', default: '', urlType: 'url'}, {name: 'productImageWidth', default: null}, @@ -15,8 +35,10 @@ export class ProductNode extends generateDecoratorNode({nodeType: 'product', {name: 'productButtonEnabled', default: false}, {name: 'productButton', default: ''}, {name: 'productUrl', default: ''} - ]} -) { + ] +}; + +export class ProductNode extends generateDecoratorNode(productNodeProps) { /* override */ exportJSON() { // checks if src is a data string @@ -49,16 +71,16 @@ export class ProductNode extends generateDecoratorNode({nodeType: 'product', return renderProductNode(this, options); } - isEmpty() { + isEmpty(): boolean { const isButtonFilled = this.__productButtonEnabled && this.__productUrl && this.__productButton; return !this.__productTitle && !this.__productDescription && !isButtonFilled && !this.__productImageSrc && !this.__productRatingEnabled; } } -export const $createProductNode = (dataset) => { +export const $createProductNode = (dataset: ProductNodeDataset) => { return new ProductNode(dataset); }; -export function $isProductNode(node) { +export function $isProductNode(node: LexicalNode) { return node instanceof ProductNode; } diff --git a/packages/kg-default-nodes/lib/nodes/signup/SignupNode.js b/packages/kg-default-nodes/lib/nodes/signup/SignupNode.ts similarity index 72% rename from packages/kg-default-nodes/lib/nodes/signup/SignupNode.js rename to packages/kg-default-nodes/lib/nodes/signup/SignupNode.ts index e1d802ca02..b7b054dccc 100644 --- a/packages/kg-default-nodes/lib/nodes/signup/SignupNode.js +++ b/packages/kg-default-nodes/lib/nodes/signup/SignupNode.ts @@ -1,9 +1,34 @@ /* eslint-disable ghost/filenames/match-exported-class */ import {signupParser} from './signup-parser'; import {renderSignupCardToDOM} from './signup-renderer'; -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode, NodeKey} from 'lexical'; -export class SignupNode extends generateDecoratorNode({nodeType: 'signup', +export type SignupNodeDataset = { + alignment?: string; + backgroundColor?: string; + backgroundImageSrc?: string; + backgroundSize?: string; + textColor?: string; + buttonColor?: string; + buttonTextColor?: string; + buttonText?: string; + disclaimer?: string; + header?: string; + labels?: string[]; + layout?: string; + subheader?: string; + successMessage?: string; + swapped?: boolean; +}; + +type SignupNodeProps = { + nodeType: 'signup'; + properties: KoenigDecoratorNodeProperties; +}; + +const signupNodeProps: SignupNodeProps = { + nodeType: 'signup', properties: [ {name: 'alignment', default: 'left'}, {name: 'backgroundColor', default: '#F0F0F0'}, @@ -20,9 +45,12 @@ export class SignupNode extends generateDecoratorNode({nodeType: 'signup', {name: 'subheader', default: '', wordCount: true}, {name: 'successMessage', default: 'Email sent! Check your inbox to complete your signup.'}, {name: 'swapped', default: false} - ]}) { + ] +}; + +export class SignupNode extends generateDecoratorNode(signupNodeProps) { /* override */ - constructor({alignment, backgroundColor, backgroundImageSrc, backgroundSize, textColor, buttonColor, buttonTextColor, buttonText, disclaimer, header, labels, layout, subheader, successMessage, swapped} = {}, key) { + constructor({alignment, backgroundColor, backgroundImageSrc, backgroundSize, textColor, buttonColor, buttonTextColor, buttonText, disclaimer, header, labels, layout, subheader, successMessage, swapped}: SignupNodeDataset = {}, key?: NodeKey) { super(key); this.__alignment = alignment || 'left'; this.__backgroundColor = backgroundColor || '#F0F0F0'; @@ -50,8 +78,7 @@ export class SignupNode extends generateDecoratorNode({nodeType: 'signup', } // keeping some custom methods for labels as it requires some special handling - - setLabels(labels) { + setLabels(labels: string[]) { if (!Array.isArray(labels) || !labels.every(item => typeof item === 'string')) { throw new Error('Invalid argument: Expected an array of strings.'); // eslint-disable-line } @@ -60,21 +87,21 @@ export class SignupNode extends generateDecoratorNode({nodeType: 'signup', writable.__labels = labels; } - addLabel(label) { + addLabel(label: string) { const writable = this.getWritable(); writable.__labels.push(label); } - removeLabel(label) { + removeLabel(label: string) { const writable = this.getWritable(); - writable.__labels = writable.__labels.filter(l => l !== label); + writable.__labels = writable.__labels.filter((l: string) => l !== label); } } -export const $createSignupNode = (dataset) => { +export const $createSignupNode = (dataset: SignupNodeDataset) => { return new SignupNode(dataset); }; -export function $isSignupNode(node) { +export function $isSignupNode(node: LexicalNode) { return node instanceof SignupNode; } diff --git a/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js b/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js deleted file mode 100644 index 7894441076..0000000000 --- a/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseToggleNode} from './toggle-parser'; -import {renderToggleNode} from './toggle-renderer'; - -export class ToggleNode extends generateDecoratorNode({nodeType: 'toggle', - properties: [ - {name: 'heading', default: '', urlType: 'html', wordCount: true}, - {name: 'content', default: '', urlType: 'html', wordCount: true} - ]} -) { - static importDOM() { - return parseToggleNode(this); - } - - exportDOM(options = {}) { - return renderToggleNode(this, options); - } -} - -export const $createToggleNode = (dataset) => { - return new ToggleNode(dataset); -}; - -export function $isToggleNode(node) { - return node instanceof ToggleNode; -} diff --git a/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.ts b/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.ts new file mode 100644 index 0000000000..7db36a0f64 --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.ts @@ -0,0 +1,41 @@ +/* eslint-disable ghost/filenames/match-exported-class */ +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; +import {parseToggleNode} from './toggle-parser'; +import {renderToggleNode} from './toggle-renderer'; + +export type ToggleNodeDataset = { + heading?: string; + content?: string; +}; + +type ToggleNodeProps = { + nodeType: 'toggle'; + properties: KoenigDecoratorNodeProperties; +}; + +const toggleNodeProps: ToggleNodeProps = { + nodeType: 'toggle', + properties: [ + {name: 'heading', default: '', urlType: 'html', wordCount: true}, + {name: 'content', default: '', urlType: 'html', wordCount: true} + ] +}; + +export class ToggleNode extends generateDecoratorNode(toggleNodeProps) { + static importDOM() { + return parseToggleNode(this); + } + + exportDOM(options = {}) { + return renderToggleNode(this, options); + } +} + +export const $createToggleNode = (dataset: ToggleNodeDataset) => { + return new ToggleNode(dataset); +}; + +export function $isToggleNode(node: LexicalNode) { + return node instanceof ToggleNode; +} diff --git a/packages/kg-default-nodes/lib/nodes/video/VideoNode.js b/packages/kg-default-nodes/lib/nodes/video/VideoNode.ts similarity index 71% rename from packages/kg-default-nodes/lib/nodes/video/VideoNode.js rename to packages/kg-default-nodes/lib/nodes/video/VideoNode.ts index 1a23a4389e..0ba07c4fbd 100644 --- a/packages/kg-default-nodes/lib/nodes/video/VideoNode.js +++ b/packages/kg-default-nodes/lib/nodes/video/VideoNode.ts @@ -1,8 +1,32 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {generateDecoratorNode} from '../../generate-decorator-node'; +import {LexicalNode} from 'lexical'; +import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseVideoNode} from './video-parser'; import {renderVideoNode} from './video-renderer'; -export class VideoNode extends generateDecoratorNode({nodeType: 'video', + +export type VideoNodeDataset = { + src?: string; + caption?: string; + fileName?: string; + mimeType?: string; + width?: number; + height?: number; + duration?: number; + thumbnailSrc?: string; + customThumbnailSrc?: string; + thumbnailWidth?: number; + thumbnailHeight?: number; + cardWidth?: string; + loop?: boolean; +}; + +type VideoNodeProps = { + nodeType: 'video'; + properties: KoenigDecoratorNodeProperties; +}; + +const videoNodeProps: VideoNodeProps = { + nodeType: 'video', properties: [ {name: 'src', default: '', urlType: 'url'}, {name: 'caption', default: '', urlType: 'html', wordCount: true}, @@ -17,8 +41,10 @@ export class VideoNode extends generateDecoratorNode({nodeType: 'video', {name: 'thumbnailHeight', default: null}, {name: 'cardWidth', default: 'regular'}, {name: 'loop', default: false} - ]} -) { + ] +}; + +export class VideoNode extends generateDecoratorNode(videoNodeProps) { /* override */ exportJSON() { // checks if src is a data string @@ -62,10 +88,10 @@ export class VideoNode extends generateDecoratorNode({nodeType: 'video', } } -export const $createVideoNode = (dataset) => { +export const $createVideoNode = (dataset: VideoNodeDataset) => { return new VideoNode(dataset); }; -export function $isVideoNode(node) { +export function $isVideoNode(node: LexicalNode) { return node instanceof VideoNode; } diff --git a/packages/kg-default-nodes/test/nodes/file.test.js b/packages/kg-default-nodes/test/nodes/file.test.js index b9ebdbe310..513e61ccb3 100644 --- a/packages/kg-default-nodes/test/nodes/file.test.js +++ b/packages/kg-default-nodes/test/nodes/file.test.js @@ -208,6 +208,7 @@ describe('FileNode', function () { const json = fileNode.exportJSON(); json.should.deepEqual({ type: 'file', + version: 1, ...dataset }); })); From f106a39e4b44208f08578fba18c2917a0e9563ce Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 8 Feb 2024 17:03:31 -0600 Subject: [PATCH 21/33] some parsers --- .../lib/nodes/audio/audio-parser.ts | 6 +- .../lib/nodes/bookmark/BookmarkNode.ts | 6 +- ...{bookmark-parser.js => bookmark-parser.ts} | 64 ++++++++++-------- .../lib/nodes/button/button-parser.ts | 10 +-- .../lib/nodes/callout/CalloutNode.ts | 6 +- .../{callout-parser.js => callout-parser.ts} | 19 +++--- .../lib/nodes/codeblock/CodeBlockNode.ts | 6 +- .../lib/nodes/codeblock/codeblock-parser.js | 63 ----------------- .../lib/nodes/codeblock/codeblock-parser.ts | 67 +++++++++++++++++++ .../lib/nodes/collection/CollectionNode.ts | 6 +- ...lection-parser.js => collection-parser.ts} | 19 +++--- .../lib/nodes/embed/EmbedNode.ts | 6 +- .../{embed-parser.js => embed-parser.ts} | 37 +++++----- .../lib/nodes/file/FileNode.ts | 8 +-- .../file/{file-parser.js => file-parser.ts} | 16 +++-- 15 files changed, 178 insertions(+), 161 deletions(-) rename packages/kg-default-nodes/lib/nodes/bookmark/{bookmark-parser.js => bookmark-parser.ts} (62%) rename packages/kg-default-nodes/lib/nodes/callout/{callout-parser.js => callout-parser.ts} (63%) delete mode 100644 packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js create mode 100644 packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.ts rename packages/kg-default-nodes/lib/nodes/collection/{collection-parser.js => collection-parser.ts} (71%) rename packages/kg-default-nodes/lib/nodes/embed/{embed-parser.js => embed-parser.ts} (67%) rename packages/kg-default-nodes/lib/nodes/file/{file-parser.js => file-parser.ts} (61%) diff --git a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts index 30aa304bfd..cac0608eab 100644 --- a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/audio/audio-parser.ts @@ -1,9 +1,5 @@ import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; import {$createAudioNode, AudioNodeDataset} from './AudioNode'; -import {KoenigDecoratorNode} from '../../KoenigDecoratorNode'; - -// TODO: This is a workaround for the moment until we can get the generator fn output to be recognized as an extended KoenigDecoratorNode -type AudioNode = KoenigDecoratorNode; export function parseAudioNode(): DOMConversionMap | null { return { @@ -39,7 +35,7 @@ export function parseAudioNode(): DOMConversionMap | null { } } - const node = $createAudioNode(payload) as AudioNode; + const node = $createAudioNode(payload); return {node}; }, priority: 1 diff --git a/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts b/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts index 711bbd35b8..14db3d4377 100644 --- a/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts +++ b/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; +import {DOMConversionMap, LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseBookmarkNode} from './bookmark-parser'; import {renderBookmarkNode} from './bookmark-renderer'; @@ -39,8 +39,8 @@ const bookmarkNodeProps: BookmarkNodeProps = { }; export class BookmarkNode extends generateDecoratorNode(bookmarkNodeProps) { - static importDOM() { - return parseBookmarkNode(this); + static importDOM(): DOMConversionMap | null { + return parseBookmarkNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.js b/packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.ts similarity index 62% rename from packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.js rename to packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.ts index 8d6a7e7fd3..4ef0c934c5 100644 --- a/packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.js +++ b/packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.ts @@ -1,31 +1,36 @@ -export function parseBookmarkNode(BookmarkNode) { +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; +import {$createBookmarkNode, BookmarkNodeDataset} from './BookmarkNode'; + +export function parseBookmarkNode(): DOMConversionMap | null { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement): DOMConversion | null => { const isKgBookmarkCard = nodeElem.classList?.contains('kg-bookmark-card'); if (nodeElem.tagName === 'FIGURE' && isKgBookmarkCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement): DOMConversionOutput { const url = domNode?.querySelector('.kg-bookmark-container')?.getAttribute('href'); - const icon = domNode?.querySelector('.kg-bookmark-icon')?.src; + const iconNode = domNode?.querySelector('.kg-bookmark-icon') as HTMLImageElement | null; + const icon = iconNode?.src; const title = domNode?.querySelector('.kg-bookmark-title')?.textContent; const description = domNode?.querySelector('.kg-bookmark-description')?.textContent; const author = domNode?.querySelector('.kg-bookmark-publisher')?.textContent; // NOTE: This is NOT in error. The classes are reversed for theme backwards-compatibility. const publisher = domNode?.querySelector('.kg-bookmark-author')?.textContent; // NOTE: This is NOT in error. The classes are reversed for theme backwards-compatibility. - const thumbnail = domNode?.querySelector('.kg-bookmark-thumbnail img')?.src; + const thumbnailNode = domNode?.querySelector('.kg-bookmark-thumbnail img') as HTMLImageElement | null; + const thumbnail = thumbnailNode?.src; const caption = domNode?.querySelector('figure.kg-bookmark-card figcaption')?.textContent; const payload = { - url: url, + url, metadata: { - icon: icon, - title: title, - description: description, - author: author, - publisher: publisher, - thumbnail: thumbnail + icon, + title, + description, + author, + publisher, + thumbnail }, - caption: caption - }; - const node = new BookmarkNode(payload); + caption + } as BookmarkNodeDataset; + const node = $createBookmarkNode(payload); return {node}; }, priority: 1 @@ -33,24 +38,24 @@ export function parseBookmarkNode(BookmarkNode) { } return null; }, - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { if (nodeElem.nodeType === 1 && nodeElem.tagName === 'DIV' && nodeElem.className.match(/graf--mixtapeEmbed/)) { return { - conversion(domNode) { + conversion(domNode: HTMLElement): DOMConversionOutput { // Grab the relevant elements - Anchor wraps most of the data - const anchorElement = domNode.querySelector('.markup--mixtapeEmbed-anchor'); + const anchorElement = domNode.querySelector('.markup--mixtapeEmbed-anchor') as HTMLAnchorElement; const titleElement = anchorElement.querySelector('.markup--mixtapeEmbed-strong'); const descElement = anchorElement.querySelector('.markup--mixtapeEmbed-em'); // Image is a top level field inside it's own a tag - const imgElement = domNode.querySelector('.mixtapeImage'); + const imgElement = domNode.querySelector('.mixtapeImage') as HTMLAnchorElement; - domNode.querySelector('br').remove(); + domNode.querySelector('br')?.remove(); // Grab individual values from the elements const url = anchorElement.getAttribute('href'); - let title = ''; - let description = ''; - let thumbnail = ''; + let title; + let description; + let thumbnail; if (titleElement && titleElement.innerHTML) { title = titleElement.innerHTML.trim(); @@ -65,22 +70,23 @@ export function parseBookmarkNode(BookmarkNode) { } // Publisher is the remaining text in the anchor, once title & desc are removed - let publisher = anchorElement.innerHTML.trim(); + const publisher = anchorElement.innerHTML.trim(); // Image is optional, // The element usually still exists with an additional has.mixtapeImage--empty class and has no background image - if (imgElement && imgElement.style['background-image']) { - thumbnail = imgElement.style['background-image'].match(/url\(([^)]*?)\)/)[1]; + if (imgElement) { + const imgElementStyle = imgElement.style.getPropertyValue('background-image'); + thumbnail = imgElementStyle?.match(/url\(([^)]*?)\)/)?.[1]; } - let payload = {url, + const payload = {url, metadata: { title, description, publisher, thumbnail - }}; - const node = new BookmarkNode(payload); + }} as BookmarkNodeDataset; + const node = $createBookmarkNode(payload); return {node}; }, priority: 1 diff --git a/packages/kg-default-nodes/lib/nodes/button/button-parser.ts b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts index 861e28e521..5c18bdb792 100644 --- a/packages/kg-default-nodes/lib/nodes/button/button-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/button/button-parser.ts @@ -1,5 +1,5 @@ import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; -import {$createButtonNode} from './ButtonNode'; +import {$createButtonNode, ButtonNodeDataset} from './ButtonNode'; export const parseButtonNode = (): DOMConversionMap | null => { return { @@ -20,10 +20,10 @@ export const parseButtonNode = (): DOMConversionMap | null => { const buttonText = buttonNode?.textContent; const payload = { - buttonText: buttonText, - alignment: alignment, - buttonUrl: buttonUrl - }; + buttonText, + alignment, + buttonUrl + } as ButtonNodeDataset; const node = $createButtonNode(payload); return {node}; diff --git a/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts b/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts index 4468ec51ec..e8295c6dcf 100644 --- a/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts +++ b/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.ts @@ -2,7 +2,7 @@ import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {renderCalloutNode} from './callout-renderer'; import {parseCalloutNode} from './callout-parser'; -import {LexicalNode, NodeKey} from 'lexical'; +import {DOMConversionMap, LexicalNode, NodeKey} from 'lexical'; export type CalloutNodeDataset = { calloutText?: string; @@ -33,8 +33,8 @@ export class CalloutNode extends generateDecoratorNode(calloutNodeProps) { this.__backgroundColor = backgroundColor || 'blue'; } - static importDOM() { - return parseCalloutNode(this); + static importDOM(): DOMConversionMap | null { + return parseCalloutNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/callout/callout-parser.js b/packages/kg-default-nodes/lib/nodes/callout/callout-parser.ts similarity index 63% rename from packages/kg-default-nodes/lib/nodes/callout/callout-parser.js rename to packages/kg-default-nodes/lib/nodes/callout/callout-parser.ts index b55e80d202..5d5a5c2dbd 100644 --- a/packages/kg-default-nodes/lib/nodes/callout/callout-parser.js +++ b/packages/kg-default-nodes/lib/nodes/callout/callout-parser.ts @@ -1,26 +1,29 @@ -const getColorTag = (nodeElem) => { +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; +import {$createCalloutNode, CalloutNodeDataset} from './CalloutNode'; + +const getColorTag = (nodeElem: HTMLElement) => { const colorClass = nodeElem.classList?.value?.match(/kg-callout-card-(\w+)/); return colorClass && colorClass[1]; }; -export function parseCalloutNode(CalloutNode) { +export function parseCalloutNode(): DOMConversionMap | null { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { const isKgCalloutCard = nodeElem.classList?.contains('kg-callout-card'); if (nodeElem.tagName === 'DIV' && isKgCalloutCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement): DOMConversionOutput { const textNode = domNode?.querySelector('.kg-callout-text'); const emojiNode = domNode?.querySelector('.kg-callout-emoji'); const color = getColorTag(domNode); const payload = { - calloutText: textNode && textNode.innerHTML.trim() || '', - calloutEmoji: emojiNode && emojiNode.innerHTML.trim() || '', + calloutText: textNode && textNode.innerHTML.trim(), + calloutEmoji: emojiNode && emojiNode.innerHTML.trim(), backgroundColor: color - }; + } as CalloutNodeDataset; - const node = new CalloutNode(payload); + const node = $createCalloutNode(payload); return {node}; }, priority: 1 diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts b/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts index 84a360ec8f..a4b8aba633 100644 --- a/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts +++ b/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {LexicalNode} from 'lexical'; +import {DOMConversionMap, LexicalNode} from 'lexical'; import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseCodeBlockNode} from './codeblock-parser'; import {renderCodeBlockNode} from './codeblock-renderer'; @@ -25,8 +25,8 @@ const codeBlockNodeProps: CodeBlockNodeProps = { }; export class CodeBlockNode extends generateDecoratorNode(codeBlockNodeProps) { - static importDOM() { - return parseCodeBlockNode(this); + static importDOM(): DOMConversionMap | null { + return parseCodeBlockNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js b/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js deleted file mode 100644 index 8c22de773b..0000000000 --- a/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js +++ /dev/null @@ -1,63 +0,0 @@ -import {readCaptionFromElement} from '../../utils/read-caption-from-element'; - -export function parseCodeBlockNode(CodeBlockNode) { - return { - figure: (nodeElem) => { - const pre = nodeElem.querySelector('pre'); - if (nodeElem.tagName === 'FIGURE' && pre) { - return { - conversion(domNode) { - let code = pre.querySelector('code'); - let figcaption = domNode.querySelector('figcaption'); - - // if there's no caption the pre key should pick it up - if (!code || !figcaption) { - return null; - } - - let payload = { - code: code.textContent, - caption: readCaptionFromElement(domNode) - }; - - let preClass = pre.getAttribute('class') || ''; - let codeClass = code.getAttribute('class') || ''; - let langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; - let languageMatches = preClass.match(langRegex) || codeClass.match(langRegex); - if (languageMatches) { - payload.language = languageMatches[1].toLowerCase(); - } - - const node = new CodeBlockNode(payload); - return {node}; - }, - priority: 2 // falls back to pre if no caption - }; - } - return null; - }, - pre: () => ({ - conversion(domNode) { - if (domNode.tagName === 'PRE') { - let [codeElement] = domNode.children; - - if (codeElement && codeElement.tagName === 'CODE') { - let payload = {code: codeElement.textContent}; - let preClass = domNode.getAttribute('class') || ''; - let codeClass = codeElement.getAttribute('class') || ''; - let langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; - let languageMatches = preClass.match(langRegex) || codeClass.match(langRegex); - if (languageMatches) { - payload.language = languageMatches[1].toLowerCase(); - } - const node = new CodeBlockNode(payload); - return {node}; - } - } - - return null; - }, - priority: 1 - }) - }; -} diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.ts b/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.ts new file mode 100644 index 0000000000..bb9c3682e2 --- /dev/null +++ b/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.ts @@ -0,0 +1,67 @@ +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; +import {readCaptionFromElement} from '../../utils/read-caption-from-element'; +import {$createCodeBlockNode, CodeBlockNodeDataset} from './CodeBlockNode'; + +export function parseCodeBlockNode(): DOMConversionMap | null { + return { + figure: (nodeElem: HTMLElement): DOMConversion | null => { + const pre = nodeElem.querySelector('pre'); + if (nodeElem.tagName === 'FIGURE' && pre) { + return { + conversion(domNode: HTMLElement): DOMConversionOutput | null { + const code = pre.querySelector('code'); + const figcaption = domNode.querySelector('figcaption'); + + // if there's no caption the pre key should pick it up + if (!code || !figcaption) { + return null; + } + + const payload = { + code: code.textContent, + caption: readCaptionFromElement(domNode) + } as CodeBlockNodeDataset; + + const preClass = pre.getAttribute('class') || ''; + const codeClass = code.getAttribute('class') || ''; + const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; + const languageMatches = preClass.match(langRegex) || codeClass.match(langRegex); + if (languageMatches) { + payload.language = languageMatches[1].toLowerCase(); + } + + const node = $createCodeBlockNode(payload); + return {node}; + }, + priority: 2 // falls back to pre if no caption + }; + } + return null; + }, + pre: (): DOMConversion | null => ({ + conversion(domNode: HTMLElement): DOMConversionOutput | null { + if (domNode.tagName === 'PRE') { + const [codeElement] = domNode.children; + + if (codeElement && codeElement.tagName === 'CODE') { + const payload = { + code: codeElement.textContent + } as CodeBlockNodeDataset; + const preClass = domNode.getAttribute('class') || ''; + const codeClass = codeElement.getAttribute('class') || ''; + const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; + const languageMatches = preClass.match(langRegex) || codeClass.match(langRegex); + if (languageMatches) { + payload.language = languageMatches[1].toLowerCase(); + } + const node = $createCodeBlockNode(payload); + return {node}; + } + } + + return null; + }, + priority: 1 + }) + }; +} diff --git a/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts b/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts index 1f016ad081..221f90c90f 100644 --- a/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts +++ b/packages/kg-default-nodes/lib/nodes/collection/CollectionNode.ts @@ -2,7 +2,7 @@ import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {renderCollectionNode} from './collection-renderer'; import {collectionParser} from './collection-parser'; -import {LexicalNode} from 'lexical'; +import {DOMConversionMap, LexicalNode} from 'lexical'; export type CollectionNodeDataset = { collection?: string; @@ -29,8 +29,8 @@ const collectionNodeProps: CollectionNodeProps = { }; export class CollectionNode extends generateDecoratorNode(collectionNodeProps) { - static importDOM() { - return collectionParser(this); + static importDOM(): DOMConversionMap | null { + return collectionParser(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/collection/collection-parser.js b/packages/kg-default-nodes/lib/nodes/collection/collection-parser.ts similarity index 71% rename from packages/kg-default-nodes/lib/nodes/collection/collection-parser.js rename to packages/kg-default-nodes/lib/nodes/collection/collection-parser.ts index 156bebd840..059b4ac9b4 100644 --- a/packages/kg-default-nodes/lib/nodes/collection/collection-parser.js +++ b/packages/kg-default-nodes/lib/nodes/collection/collection-parser.ts @@ -1,4 +1,7 @@ -function getLayout(domNode) { +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; +import {$createCollectionNode, CollectionNodeDataset} from './CollectionNode'; + +function getLayout(domNode: HTMLElement) { if (domNode.classList.contains('kg-collection-card-list')) { return 'list'; } else { // should have kg-collection-card-grid @@ -6,7 +9,7 @@ function getLayout(domNode) { } } -function getColumns(domNode) { +function getColumns(domNode: HTMLElement) { if (domNode.classList.contains('columns-1')) { return 1; } @@ -21,14 +24,14 @@ function getColumns(domNode) { } } -export function collectionParser(CollectionNode) { +export function collectionParser(): DOMConversionMap | null { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { const isCollectionNode = nodeElem.classList?.contains('kg-collection-card'); if (nodeElem.tagName === 'DIV' && isCollectionNode) { return { - conversion(domNode) { - const postCount = parseInt(domNode.getAttribute('data-kg-collection-limit')); + conversion(domNode: HTMLElement): DOMConversionOutput { + const postCount = parseInt(domNode.getAttribute('data-kg-collection-limit') || ''); const collection = domNode.getAttribute('data-kg-collection-slug'); const layout = getLayout(domNode); const header = domNode.querySelector('.kg-collection-card-title')?.textContent || ''; @@ -40,9 +43,9 @@ export function collectionParser(CollectionNode) { layout, columns, header - }; + } as CollectionNodeDataset; - const node = new CollectionNode(payload); + const node = $createCollectionNode(payload); return {node}; }, priority: 1 diff --git a/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts b/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts index 39608b2f87..c99dad9ab7 100644 --- a/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts +++ b/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {LexicalNode} from 'lexical'; +import {DOMConversionMap, LexicalNode} from 'lexical'; import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseEmbedNode} from './embed-parser'; import {renderEmbedNode} from './embed-renderer'; @@ -29,8 +29,8 @@ const embedNodeProps: EmbedNodeProps = { }; export class EmbedNode extends generateDecoratorNode(embedNodeProps) { - static importDOM() { - return parseEmbedNode(this); + static importDOM(): DOMConversionMap | null { + return parseEmbedNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/embed/embed-parser.js b/packages/kg-default-nodes/lib/nodes/embed/embed-parser.ts similarity index 67% rename from packages/kg-default-nodes/lib/nodes/embed/embed-parser.js rename to packages/kg-default-nodes/lib/nodes/embed/embed-parser.ts index ecde2910ed..74c3e3d68e 100644 --- a/packages/kg-default-nodes/lib/nodes/embed/embed-parser.js +++ b/packages/kg-default-nodes/lib/nodes/embed/embed-parser.ts @@ -1,15 +1,17 @@ +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical/LexicalNode.js'; import {readCaptionFromElement} from '../../utils/read-caption-from-element.js'; +import {$createEmbedNode, EmbedNodeDataset} from './EmbedNode.js'; // TODO: add NFT card parser -export function parseEmbedNode(EmbedNode) { +export function parseEmbedNode(): DOMConversionMap | null { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement): DOMConversion | null => { if (nodeElem.nodeType === 1 && nodeElem.tagName === 'FIGURE') { const iframe = nodeElem.querySelector('iframe'); if (iframe) { return { - conversion(domNode) { - const payload = _createPayloadForIframe(iframe); + conversion(domNode: HTMLElement): DOMConversionOutput | null { + const payload = _createPayloadForIframe(iframe) as EmbedNodeDataset; if (!payload) { return null; @@ -17,7 +19,7 @@ export function parseEmbedNode(EmbedNode) { payload.caption = readCaptionFromElement(domNode); - const node = new EmbedNode(payload); + const node = $createEmbedNode(payload); return {node}; }, priority: 1 @@ -26,29 +28,29 @@ export function parseEmbedNode(EmbedNode) { const blockquote = nodeElem.querySelector('blockquote'); if (blockquote) { return { - conversion(domNode) { + conversion(domNode: HTMLElement): DOMConversionOutput | null { const link = domNode.querySelector('a'); if (!link) { return null; } - let url = link.getAttribute('href'); + const url = link.getAttribute('href'); // If we don't have a url, or it's not an absolute URL, we can't handle this if (!url || !url.match(/^https?:\/\//i)) { return null; } - let payload = {url: url}; + const payload = {url: url} as EmbedNodeDataset; // append caption, remove element from blockquote payload.caption = readCaptionFromElement(domNode); - let figcaption = domNode.querySelector('figcaption'); + const figcaption = domNode.querySelector('figcaption'); figcaption?.remove(); payload.html = domNode.innerHTML; - const node = new EmbedNode(payload); + const node = $createEmbedNode(payload); return {node}; }, priority: 1 @@ -57,17 +59,17 @@ export function parseEmbedNode(EmbedNode) { } return null; }, - iframe: (nodeElem) => { + iframe: (nodeElem: HTMLElement): DOMConversion | null => { if (nodeElem.nodeType === 1 && nodeElem.tagName === 'IFRAME') { return { - conversion(domNode) { - const payload = _createPayloadForIframe(domNode); + conversion(domNode: HTMLElement): DOMConversionOutput | null { + const payload = _createPayloadForIframe(domNode as HTMLIFrameElement); if (!payload) { return null; } - const node = new EmbedNode(payload); + const node = $createEmbedNode(payload); return {node}; }, priority: 1 @@ -78,7 +80,7 @@ export function parseEmbedNode(EmbedNode) { }; } -function _createPayloadForIframe(iframe) { +function _createPayloadForIframe(iframe: HTMLIFrameElement): EmbedNodeDataset | undefined { // If we don't have a src Or it's not an absolute URL, we can't handle this // This regex handles http://, https:// or // if (!iframe.src || !iframe.src.match(/^(https?:)?\/\//i)) { @@ -90,8 +92,9 @@ function _createPayloadForIframe(iframe) { iframe.src = `https:${iframe.src}`; } - let payload = { - url: iframe.src + const payload = { + url: iframe.src, + html: '' }; payload.html = iframe.outerHTML; diff --git a/packages/kg-default-nodes/lib/nodes/file/FileNode.ts b/packages/kg-default-nodes/lib/nodes/file/FileNode.ts index 6a73d2350b..8e07dcf027 100644 --- a/packages/kg-default-nodes/lib/nodes/file/FileNode.ts +++ b/packages/kg-default-nodes/lib/nodes/file/FileNode.ts @@ -3,14 +3,14 @@ import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../genera import {renderFileNode} from './file-renderer'; import {parseFileNode} from './file-parser'; import {bytesToSize} from '../../utils/size-byte-converter'; -import {LexicalNode, SerializedLexicalNode, Spread} from 'lexical'; +import {DOMConversionMap, LexicalNode, SerializedLexicalNode, Spread} from 'lexical'; export type FileNodeDataset = { src?: string; fileTitle?: string; fileCaption?: string; fileName?: string; - fileSize?: string; + fileSize?: number; }; type FileNodeProps = { @@ -48,8 +48,8 @@ export class FileNode extends generateDecoratorNode(fileNodeProps) { }; } - static importDOM() { - return parseFileNode(this); + static importDOM(): DOMConversionMap | null { + return parseFileNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/file/file-parser.js b/packages/kg-default-nodes/lib/nodes/file/file-parser.ts similarity index 61% rename from packages/kg-default-nodes/lib/nodes/file/file-parser.js rename to packages/kg-default-nodes/lib/nodes/file/file-parser.ts index c3740879ff..ea93a6acbd 100644 --- a/packages/kg-default-nodes/lib/nodes/file/file-parser.js +++ b/packages/kg-default-nodes/lib/nodes/file/file-parser.ts @@ -1,27 +1,29 @@ +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; import {sizeToBytes} from '../../utils/size-byte-converter'; +import {$createFileNode, FileNodeDataset} from './FileNode'; -export function parseFileNode(FileNode) { +export function parseFileNode(): DOMConversionMap | null { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { const isKgFileCard = nodeElem.classList?.contains('kg-file-card'); if (nodeElem.tagName === 'DIV' && isKgFileCard) { return { - conversion(domNode) { - const link = domNode.querySelector('a'); + conversion(domNode: HTMLElement): DOMConversionOutput { + const link = domNode.querySelector('a') as HTMLAnchorElement; const src = link.getAttribute('href'); const fileTitle = domNode.querySelector('.kg-file-card-title')?.textContent || ''; const fileCaption = domNode.querySelector('.kg-file-card-caption')?.textContent || ''; const fileName = domNode.querySelector('.kg-file-card-filename')?.textContent || ''; - let fileSize = sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || ''); + const fileSize = sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || ''); const payload = { src, fileTitle, fileCaption, fileName, fileSize - }; + } as FileNodeDataset; - const node = new FileNode(payload); + const node = $createFileNode(payload); return {node}; }, priority: 1 From 841b7525fb7bb53cf108b6f125bb8dd4258ee84b Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 12 Feb 2024 13:50:34 -0600 Subject: [PATCH 22/33] update parsers to ts --- .../lib/nodes/aside/AsideNode.ts | 4 +- .../lib/nodes/aside/aside-parser.ts | 7 +- .../lib/nodes/gallery/GalleryNode.ts | 6 +- .../{gallery-parser.js => gallery-parser.ts} | 73 ++++++++++--------- .../horizontalrule/HorizontalRuleNode.ts | 6 +- .../horizontalrule/horizontalrule-parser.js | 11 --- .../horizontalrule/horizontalrule-parser.ts | 14 ++++ .../lib/nodes/html/HtmlNode.ts | 6 +- .../lib/nodes/html/html-parser.js | 47 ------------ .../lib/nodes/html/html-parser.ts | 51 +++++++++++++ .../lib/nodes/image/ImageNode.ts | 6 +- .../{image-parser.js => image-parser.ts} | 21 +++--- .../lib/nodes/paywall/PaywallNode.ts | 6 +- .../lib/nodes/paywall/paywall-parser.js | 16 ---- .../lib/nodes/paywall/paywall-parser.ts | 19 +++++ .../lib/nodes/product/ProductNode.ts | 10 +-- .../{product-parser.js => product-parser.ts} | 27 +++---- .../lib/nodes/signup/SignupNode.ts | 6 +- .../{signup-parser.js => signup-parser.ts} | 40 ++++++---- .../lib/nodes/toggle/ToggleNode.ts | 6 +- .../{toggle-parser.js => toggle-parser.ts} | 17 +++-- .../lib/nodes/video/VideoNode.ts | 6 +- .../{video-parser.js => video-parser.ts} | 19 +++-- .../read-image-attributes-from-element.js | 43 ----------- .../read-image-attributes-from-element.ts | 58 +++++++++++++++ 25 files changed, 290 insertions(+), 235 deletions(-) rename packages/kg-default-nodes/lib/nodes/gallery/{gallery-parser.js => gallery-parser.ts} (58%) delete mode 100644 packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js create mode 100644 packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.ts delete mode 100644 packages/kg-default-nodes/lib/nodes/html/html-parser.js create mode 100644 packages/kg-default-nodes/lib/nodes/html/html-parser.ts rename packages/kg-default-nodes/lib/nodes/image/{image-parser.js => image-parser.ts} (63%) delete mode 100644 packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js create mode 100644 packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.ts rename packages/kg-default-nodes/lib/nodes/product/{product-parser.js => product-parser.ts} (69%) rename packages/kg-default-nodes/lib/nodes/signup/{signup-parser.js => signup-parser.ts} (64%) rename packages/kg-default-nodes/lib/nodes/toggle/{toggle-parser.js => toggle-parser.ts} (50%) rename packages/kg-default-nodes/lib/nodes/video/{video-parser.js => video-parser.ts} (79%) delete mode 100644 packages/kg-default-nodes/lib/utils/read-image-attributes-from-element.js create mode 100644 packages/kg-default-nodes/lib/utils/read-image-attributes-from-element.ts diff --git a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts b/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts index c5cca7b964..f001e0b6d9 100644 --- a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts +++ b/packages/kg-default-nodes/lib/nodes/aside/AsideNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {ElementFormatType, ElementNode, LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; +import {DOMConversionMap, ElementFormatType, ElementNode, LexicalNode, NodeKey, SerializedLexicalNode, Spread} from 'lexical'; import {parseAsideNode} from './aside-parser'; export type SerializedAsideNode = Spread<{ @@ -44,7 +44,7 @@ export class AsideNode extends ElementNode { return dataset; } - static importDOM() { + static importDOM(): DOMConversionMap | null { return parseAsideNode(); } diff --git a/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts b/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts index b558cd7c3c..ef204fd9e2 100644 --- a/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts +++ b/packages/kg-default-nodes/lib/nodes/aside/aside-parser.ts @@ -1,12 +1,13 @@ +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical'; import {$createAsideNode} from './AsideNode'; -export function parseAsideNode() { +export function parseAsideNode(): DOMConversionMap | null { return { - blockquote: (nodeElem: HTMLElement) => { + blockquote: (nodeElem: HTMLElement): DOMConversion | null => { const isBigQuote = nodeElem.classList?.contains('kg-blockquote-alt'); if (nodeElem.tagName === 'BLOCKQUOTE' && isBigQuote) { return { - conversion() { + conversion(): DOMConversionOutput { const node = $createAsideNode(); return {node}; }, diff --git a/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts b/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts index d97f9f72fd..68338384bb 100644 --- a/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts +++ b/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.ts @@ -1,5 +1,5 @@ /* eslint-disable ghost/filenames/match-exported-class */ -import {LexicalNode} from 'lexical'; +import {DOMConversionMap, LexicalNode} from 'lexical'; import {KoenigDecoratorNodeProperties, generateDecoratorNode} from '../../generate-decorator-node'; import {parseGalleryNode} from './gallery-parser'; import {renderGalleryNode} from './gallery-renderer'; @@ -37,8 +37,8 @@ export class GalleryNode extends generateDecoratorNode(galleryNodeProps) { }; } - static importDOM() { - return parseGalleryNode(this); + static importDOM(): DOMConversionMap | null { + return parseGalleryNode(); } exportDOM(options = {}) { diff --git a/packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.js b/packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.ts similarity index 58% rename from packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.js rename to packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.ts index 0b0b555e66..dbf37b6bb7 100644 --- a/packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.js +++ b/packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.ts @@ -1,29 +1,31 @@ +import {DOMConversion, DOMConversionMap, DOMConversionOutput} from 'lexical/LexicalNode.js'; import {readCaptionFromElement} from '../../utils/read-caption-from-element.js'; import {readImageAttributesFromElement} from '../../utils/read-image-attributes-from-element.js'; +import {$createGalleryNode, GalleryNodeDataset} from './GalleryNode.js'; -function readGalleryImageAttributesFromElement(element, imgNum) { +function readGalleryImageAttributesFromElement(element: HTMLImageElement, imgNum: number) { const image = readImageAttributesFromElement(element); - image.fileName = element.src.match(/[^/]*$/)[0]; + image.fileName = element.src?.match(/[^/]*$/)?.[0]; image.row = Math.floor(imgNum / 3); return image; } -export function parseGalleryNode(GalleryNode) { +export function parseGalleryNode(): DOMConversionMap | null { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement): DOMConversion | null => { // Koenig gallery card if (nodeElem.classList?.contains('kg-gallery-card')) { return { - conversion(domNode) { - const payload = {}; + conversion(domNode): DOMConversionOutput { + const payload: GalleryNodeDataset = {}; const imgs = Array.from(domNode.querySelectorAll('img')); payload.images = imgs.map(readGalleryImageAttributesFromElement); payload.caption = readCaptionFromElement(domNode); - const node = new GalleryNode(payload); + const node = $createGalleryNode(payload); return {node}; }, priority: 1 @@ -32,18 +34,19 @@ export function parseGalleryNode(GalleryNode) { return null; }, - div: (nodeElem) => { + div: (nodeElem: HTMLElement): DOMConversion | null => { // Medium "graf" galleries - function isGrafGallery(node) { - return node.tagName === 'DIV' - && node.dataset?.paragraphCount - && node.querySelectorAll('img').length > 0; + function isGrafGallery(node: HTMLElement): boolean { + if (node.tagName === 'DIV' && node.dataset?.paragraphCount && node.querySelectorAll('img').length > 0) { + return true; + } + return false; } if (isGrafGallery(nodeElem)) { return { - conversion(domNode) { - const payload = { + conversion(domNode): DOMConversionOutput { + const payload: GalleryNodeDataset = { caption: readCaptionFromElement(domNode) }; @@ -52,9 +55,9 @@ export function parseGalleryNode(GalleryNode) { let imgs = Array.from(domNode.querySelectorAll('img')); // ...and then iterate over any remaining divs until we run out of matches - let nextNode = domNode.nextElementSibling; + let nextNode = domNode.nextElementSibling as HTMLElement; while (nextNode && isGrafGallery(nextNode)) { - let currentNode = nextNode; + const currentNode = nextNode; imgs = imgs.concat(Array.from(currentNode.querySelectorAll('img'))); const currentNodeCaption = readCaptionFromElement(currentNode); @@ -62,7 +65,7 @@ export function parseGalleryNode(GalleryNode) { payload.caption = `${payload.caption} / ${currentNodeCaption}`; } - nextNode = currentNode.nextElementSibling; + nextNode = currentNode.nextElementSibling as HTMLElement; // remove nodes as we go so that they don't go through the parser currentNode.remove(); @@ -70,7 +73,7 @@ export function parseGalleryNode(GalleryNode) { payload.images = imgs.map(readGalleryImageAttributesFromElement); - const node = new GalleryNode(payload); + const node = $createGalleryNode(payload); return {node}; }, priority: 1 @@ -78,42 +81,46 @@ export function parseGalleryNode(GalleryNode) { } // Squarespace SQS galleries - function isSqsGallery(node) { - return node.tagName === 'DIV' - && node.className.match(/sqs-gallery-container/) - && !node.className.match(/summary-/); + function isSqsGallery(node: HTMLElement): boolean { + if (node.tagName === 'DIV' && node.className.match(/sqs-gallery-container/) && !node.className.match(/summary-/)) { + return true; + } + return false; } if (isSqsGallery(nodeElem)) { return { - conversion(domNode) { - const payload = {}; + conversion(domNode): DOMConversionOutput { + const payload: GalleryNodeDataset = {}; // Each image exists twice... // The first image is wrapped in `