From ec3ad86f1607df5032e3e983f16849be372014f2 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 17 Apr 2026 09:59:02 +0100 Subject: [PATCH 1/2] Renamed files for kg-default-nodes TypeScript migration --- .../generate-decorator-node.js => src/generate-decorator-node.ts} | 0 .../{lib/kg-default-nodes.js => src/kg-default-nodes.ts} | 0 .../ExtendedHeadingNode.js => src/nodes/ExtendedHeadingNode.ts} | 0 .../nodes/ExtendedQuoteNode.js => src/nodes/ExtendedQuoteNode.ts} | 0 .../nodes/ExtendedTextNode.js => src/nodes/ExtendedTextNode.ts} | 0 .../kg-default-nodes/{lib/nodes/TKNode.js => src/nodes/TKNode.ts} | 0 .../nodes/aside/AsideNode.js => src/nodes/aside/AsideNode.ts} | 0 .../nodes/aside/AsideParser.js => src/nodes/aside/AsideParser.ts} | 0 .../at-link/AtLinkNode.js => src/nodes/at-link/AtLinkNode.ts} | 0 .../AtLinkSearchNode.js => src/nodes/at-link/AtLinkSearchNode.ts} | 0 .../{lib/nodes/at-link/index.js => src/nodes/at-link/index.ts} | 0 packages/kg-default-nodes/{lib => src}/nodes/at-link/kg-link.svg | 0 .../nodes/audio/AudioNode.js => src/nodes/audio/AudioNode.ts} | 0 .../audio/audio-parser.js => src/nodes/audio/audio-parser.ts} | 0 .../audio/audio-renderer.js => src/nodes/audio/audio-renderer.ts} | 0 .../BookmarkNode.js => src/nodes/bookmark/BookmarkNode.ts} | 0 .../bookmark-parser.js => src/nodes/bookmark/bookmark-parser.ts} | 0 .../nodes/bookmark/bookmark-renderer.ts} | 0 .../nodes/button/ButtonNode.js => src/nodes/button/ButtonNode.ts} | 0 .../button/button-parser.js => src/nodes/button/button-parser.ts} | 0 .../button-renderer.js => src/nodes/button/button-renderer.ts} | 0 .../nodes/call-to-action/CallToActionNode.ts} | 0 .../nodes/call-to-action/calltoaction-parser.ts} | 0 .../nodes/call-to-action/calltoaction-renderer.ts} | 0 .../callout/CalloutNode.js => src/nodes/callout/CalloutNode.ts} | 0 .../callout-parser.js => src/nodes/callout/callout-parser.ts} | 0 .../callout-renderer.js => src/nodes/callout/callout-renderer.ts} | 0 .../CodeBlockNode.js => src/nodes/codeblock/CodeBlockNode.ts} | 0 .../nodes/codeblock/codeblock-parser.ts} | 0 .../nodes/codeblock/codeblock-renderer.ts} | 0 .../EmailCtaNode.js => src/nodes/email-cta/EmailCtaNode.ts} | 0 .../nodes/email-cta/email-cta-renderer.ts} | 0 .../nodes/email/EmailNode.js => src/nodes/email/EmailNode.ts} | 0 .../email/email-renderer.js => src/nodes/email/email-renderer.ts} | 0 .../nodes/embed/EmbedNode.js => src/nodes/embed/EmbedNode.ts} | 0 .../embed/embed-parser.js => src/nodes/embed/embed-parser.ts} | 0 .../embed/embed-renderer.js => src/nodes/embed/embed-renderer.ts} | 0 .../embed/types/twitter.js => src/nodes/embed/types/twitter.ts} | 0 .../{lib/nodes/file/FileNode.js => src/nodes/file/FileNode.ts} | 0 .../nodes/file/file-parser.js => src/nodes/file/file-parser.ts} | 0 .../file/file-renderer.js => src/nodes/file/file-renderer.ts} | 0 .../gallery/GalleryNode.js => src/nodes/gallery/GalleryNode.ts} | 0 .../gallery-parser.js => src/nodes/gallery/gallery-parser.ts} | 0 .../gallery-renderer.js => src/nodes/gallery/gallery-renderer.ts} | 0 .../nodes/header/HeaderNode.js => src/nodes/header/HeaderNode.ts} | 0 .../nodes/header/parsers/header-parser.ts} | 0 .../nodes/header/renderers/v1/header-renderer.ts} | 0 .../nodes/header/renderers/v2/header-renderer.ts} | 0 .../nodes/horizontalrule/HorizontalRuleNode.ts} | 0 .../nodes/horizontalrule/horizontalrule-parser.ts} | 0 .../nodes/horizontalrule/horizontalrule-renderer.ts} | 0 .../{lib/nodes/html/HtmlNode.js => src/nodes/html/HtmlNode.ts} | 0 .../nodes/html/html-parser.js => src/nodes/html/html-parser.ts} | 0 .../html/html-renderer.js => src/nodes/html/html-renderer.ts} | 0 .../nodes/image/ImageNode.js => src/nodes/image/ImageNode.ts} | 0 .../image/image-parser.js => src/nodes/image/image-parser.ts} | 0 .../image/image-renderer.js => src/nodes/image/image-renderer.ts} | 0 .../MarkdownNode.js => src/nodes/markdown/MarkdownNode.ts} | 0 .../nodes/markdown/markdown-renderer.ts} | 0 .../paywall/PaywallNode.js => src/nodes/paywall/PaywallNode.ts} | 0 .../paywall-parser.js => src/nodes/paywall/paywall-parser.ts} | 0 .../paywall-renderer.js => src/nodes/paywall/paywall-renderer.ts} | 0 .../product/ProductNode.js => src/nodes/product/ProductNode.ts} | 0 .../product-parser.js => src/nodes/product/product-parser.ts} | 0 .../product-renderer.js => src/nodes/product/product-renderer.ts} | 0 .../nodes/signup/SignupNode.js => src/nodes/signup/SignupNode.ts} | 0 .../signup/signup-parser.js => src/nodes/signup/signup-parser.ts} | 0 .../signup-renderer.js => src/nodes/signup/signup-renderer.ts} | 0 .../nodes/toggle/ToggleNode.js => src/nodes/toggle/ToggleNode.ts} | 0 .../toggle/toggle-parser.js => src/nodes/toggle/toggle-parser.ts} | 0 .../toggle-renderer.js => src/nodes/toggle/toggle-renderer.ts} | 0 .../TransistorNode.js => src/nodes/transistor/TransistorNode.ts} | 0 .../nodes/transistor/transistor-renderer.ts} | 0 .../nodes/video/VideoNode.js => src/nodes/video/VideoNode.ts} | 0 .../video/video-parser.js => src/nodes/video/video-parser.ts} | 0 .../video/video-renderer.js => src/nodes/video/video-renderer.ts} | 0 .../{lib/nodes/zwnj/ZWNJNode.js => src/nodes/zwnj/ZWNJNode.ts} | 0 .../serializers/linebreak.js => src/serializers/linebreak.ts} | 0 .../serializers/paragraph.js => src/serializers/paragraph.ts} | 0 .../utils/add-create-document-option.ts} | 0 .../utils/build-clean-basic-html-for-element.ts} | 0 .../{lib/utils/clean-dom.js => src/utils/clean-dom.ts} | 0 .../{lib/utils/escape-html.js => src/utils/escape-html.ts} | 0 .../utils/get-available-image-widths.ts} | 0 .../utils/get-resized-image-dimensions.ts} | 0 .../utils/is-local-content-image.ts} | 0 .../utils/is-unsplash-image.js => src/utils/is-unsplash-image.ts} | 0 .../utils/read-caption-from-element.ts} | 0 .../utils/read-image-attributes-from-element.ts} | 0 .../utils/read-text-content.js => src/utils/read-text-content.ts} | 0 .../utils/render-empty-container.ts} | 0 .../email-button.js => src/utils/render-helpers/email-button.ts} | 0 .../replacement-strings.js => src/utils/replacement-strings.ts} | 0 .../{lib/utils/rgb-to-hex.js => src/utils/rgb-to-hex.ts} | 0 .../utils/set-src-background-from-parent.ts} | 0 .../size-byte-converter.js => src/utils/size-byte-converter.ts} | 0 .../{lib/utils/slugify.js => src/utils/slugify.ts} | 0 .../utils/srcset-attribute.js => src/utils/srcset-attribute.ts} | 0 .../tagged-template-fns.mjs => src/utils/tagged-template-fns.ts} | 0 .../{lib/utils/truncate.js => src/utils/truncate.ts} | 0 .../{lib/utils/visibility.js => src/utils/visibility.ts} | 0 ...ate-decorator-node.test.js => generate-decorator-node.test.ts} | 0 .../kg-default-nodes/test/nodes/{aside.test.js => aside.test.ts} | 0 .../test/nodes/{at-link-search.test.js => at-link-search.test.ts} | 0 .../test/nodes/{at-link.test.js => at-link.test.ts} | 0 .../kg-default-nodes/test/nodes/{audio.test.js => audio.test.ts} | 0 .../test/nodes/{bookmark.test.js => bookmark.test.ts} | 0 .../test/nodes/{button.test.js => button.test.ts} | 0 .../test/nodes/{call-to-action.test.js => call-to-action.test.ts} | 0 .../test/nodes/{callout.test.js => callout.test.ts} | 0 .../test/nodes/{codeblock.test.js => codeblock.test.ts} | 0 .../test/nodes/{email-cta.test.js => email-cta.test.ts} | 0 .../kg-default-nodes/test/nodes/{email.test.js => email.test.ts} | 0 .../kg-default-nodes/test/nodes/{embed.test.js => embed.test.ts} | 0 .../kg-default-nodes/test/nodes/{file.test.js => file.test.ts} | 0 .../test/nodes/{gallery.test.js => gallery.test.ts} | 0 .../test/nodes/{header.test.js => header.test.ts} | 0 .../test/nodes/{horizontalrule.test.js => horizontalrule.test.ts} | 0 .../kg-default-nodes/test/nodes/{html.test.js => html.test.ts} | 0 .../kg-default-nodes/test/nodes/{image.test.js => image.test.ts} | 0 .../test/nodes/{markdown.test.js => markdown.test.ts} | 0 .../test/nodes/{paywall.test.js => paywall.test.ts} | 0 .../test/nodes/{product.test.js => product.test.ts} | 0 .../test/nodes/{signup.test.js => signup.test.ts} | 0 packages/kg-default-nodes/test/nodes/{tk.test.js => tk.test.ts} | 0 .../test/nodes/{toggle.test.js => toggle.test.ts} | 0 .../test/nodes/{transistor.test.js => transistor.test.ts} | 0 .../kg-default-nodes/test/nodes/{video.test.js => video.test.ts} | 0 .../kg-default-nodes/test/nodes/{zwnj.test.js => zwnj.test.ts} | 0 .../test/serializers/{linebreak.test.js => linebreak.test.ts} | 0 .../test/serializers/{paragraph.test.js => paragraph.test.ts} | 0 .../test/test-utils/{assertions.js => assertions.ts} | 0 packages/kg-default-nodes/test/test-utils/{index.js => index.ts} | 0 .../test/test-utils/{overrides.js => overrides.ts} | 0 .../test/utils/{rgb-to-hex.test.js => rgb-to-hex.test.ts} | 0 .../{tagged-template-fns.test.mjs => tagged-template-fns.test.ts} | 0 .../test/utils/{visibility.test.js => visibility.test.ts} | 0 137 files changed, 0 insertions(+), 0 deletions(-) rename packages/kg-default-nodes/{lib/generate-decorator-node.js => src/generate-decorator-node.ts} (100%) rename packages/kg-default-nodes/{lib/kg-default-nodes.js => src/kg-default-nodes.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/ExtendedHeadingNode.js => src/nodes/ExtendedHeadingNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/ExtendedQuoteNode.js => src/nodes/ExtendedQuoteNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/ExtendedTextNode.js => src/nodes/ExtendedTextNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/TKNode.js => src/nodes/TKNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/aside/AsideNode.js => src/nodes/aside/AsideNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/aside/AsideParser.js => src/nodes/aside/AsideParser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/at-link/AtLinkNode.js => src/nodes/at-link/AtLinkNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/at-link/AtLinkSearchNode.js => src/nodes/at-link/AtLinkSearchNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/at-link/index.js => src/nodes/at-link/index.ts} (100%) rename packages/kg-default-nodes/{lib => src}/nodes/at-link/kg-link.svg (100%) rename packages/kg-default-nodes/{lib/nodes/audio/AudioNode.js => src/nodes/audio/AudioNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/audio/audio-parser.js => src/nodes/audio/audio-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/audio/audio-renderer.js => src/nodes/audio/audio-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/bookmark/BookmarkNode.js => src/nodes/bookmark/BookmarkNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/bookmark/bookmark-parser.js => src/nodes/bookmark/bookmark-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/bookmark/bookmark-renderer.js => src/nodes/bookmark/bookmark-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/button/ButtonNode.js => src/nodes/button/ButtonNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/button/button-parser.js => src/nodes/button/button-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/button/button-renderer.js => src/nodes/button/button-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/call-to-action/CallToActionNode.js => src/nodes/call-to-action/CallToActionNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/call-to-action/calltoaction-parser.js => src/nodes/call-to-action/calltoaction-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/call-to-action/calltoaction-renderer.js => src/nodes/call-to-action/calltoaction-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/callout/CalloutNode.js => src/nodes/callout/CalloutNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/callout/callout-parser.js => src/nodes/callout/callout-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/callout/callout-renderer.js => src/nodes/callout/callout-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/codeblock/CodeBlockNode.js => src/nodes/codeblock/CodeBlockNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/codeblock/codeblock-parser.js => src/nodes/codeblock/codeblock-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/codeblock/codeblock-renderer.js => src/nodes/codeblock/codeblock-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/email-cta/EmailCtaNode.js => src/nodes/email-cta/EmailCtaNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/email-cta/email-cta-renderer.js => src/nodes/email-cta/email-cta-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/email/EmailNode.js => src/nodes/email/EmailNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/email/email-renderer.js => src/nodes/email/email-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/embed/EmbedNode.js => src/nodes/embed/EmbedNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/embed/embed-parser.js => src/nodes/embed/embed-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/embed/embed-renderer.js => src/nodes/embed/embed-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/embed/types/twitter.js => src/nodes/embed/types/twitter.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/file/FileNode.js => src/nodes/file/FileNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/file/file-parser.js => src/nodes/file/file-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/file/file-renderer.js => src/nodes/file/file-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/gallery/GalleryNode.js => src/nodes/gallery/GalleryNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/gallery/gallery-parser.js => src/nodes/gallery/gallery-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/gallery/gallery-renderer.js => src/nodes/gallery/gallery-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/header/HeaderNode.js => src/nodes/header/HeaderNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/header/parsers/header-parser.js => src/nodes/header/parsers/header-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/header/renderers/v1/header-renderer.js => src/nodes/header/renderers/v1/header-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/header/renderers/v2/header-renderer.js => src/nodes/header/renderers/v2/header-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/horizontalrule/HorizontalRuleNode.js => src/nodes/horizontalrule/HorizontalRuleNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/horizontalrule/horizontalrule-parser.js => src/nodes/horizontalrule/horizontalrule-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/horizontalrule/horizontalrule-renderer.js => src/nodes/horizontalrule/horizontalrule-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/html/HtmlNode.js => src/nodes/html/HtmlNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/html/html-parser.js => src/nodes/html/html-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/html/html-renderer.js => src/nodes/html/html-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/image/ImageNode.js => src/nodes/image/ImageNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/image/image-parser.js => src/nodes/image/image-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/image/image-renderer.js => src/nodes/image/image-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/markdown/MarkdownNode.js => src/nodes/markdown/MarkdownNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/markdown/markdown-renderer.js => src/nodes/markdown/markdown-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/paywall/PaywallNode.js => src/nodes/paywall/PaywallNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/paywall/paywall-parser.js => src/nodes/paywall/paywall-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/paywall/paywall-renderer.js => src/nodes/paywall/paywall-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/product/ProductNode.js => src/nodes/product/ProductNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/product/product-parser.js => src/nodes/product/product-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/product/product-renderer.js => src/nodes/product/product-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/signup/SignupNode.js => src/nodes/signup/SignupNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/signup/signup-parser.js => src/nodes/signup/signup-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/signup/signup-renderer.js => src/nodes/signup/signup-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/toggle/ToggleNode.js => src/nodes/toggle/ToggleNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/toggle/toggle-parser.js => src/nodes/toggle/toggle-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/toggle/toggle-renderer.js => src/nodes/toggle/toggle-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/transistor/TransistorNode.js => src/nodes/transistor/TransistorNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/transistor/transistor-renderer.js => src/nodes/transistor/transistor-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/video/VideoNode.js => src/nodes/video/VideoNode.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/video/video-parser.js => src/nodes/video/video-parser.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/video/video-renderer.js => src/nodes/video/video-renderer.ts} (100%) rename packages/kg-default-nodes/{lib/nodes/zwnj/ZWNJNode.js => src/nodes/zwnj/ZWNJNode.ts} (100%) rename packages/kg-default-nodes/{lib/serializers/linebreak.js => src/serializers/linebreak.ts} (100%) rename packages/kg-default-nodes/{lib/serializers/paragraph.js => src/serializers/paragraph.ts} (100%) rename packages/kg-default-nodes/{lib/utils/add-create-document-option.js => src/utils/add-create-document-option.ts} (100%) rename packages/kg-default-nodes/{lib/utils/build-clean-basic-html-for-element.js => src/utils/build-clean-basic-html-for-element.ts} (100%) rename packages/kg-default-nodes/{lib/utils/clean-dom.js => src/utils/clean-dom.ts} (100%) rename packages/kg-default-nodes/{lib/utils/escape-html.js => src/utils/escape-html.ts} (100%) rename packages/kg-default-nodes/{lib/utils/get-available-image-widths.js => src/utils/get-available-image-widths.ts} (100%) rename packages/kg-default-nodes/{lib/utils/get-resized-image-dimensions.js => src/utils/get-resized-image-dimensions.ts} (100%) rename packages/kg-default-nodes/{lib/utils/is-local-content-image.js => src/utils/is-local-content-image.ts} (100%) rename packages/kg-default-nodes/{lib/utils/is-unsplash-image.js => src/utils/is-unsplash-image.ts} (100%) rename packages/kg-default-nodes/{lib/utils/read-caption-from-element.js => src/utils/read-caption-from-element.ts} (100%) rename packages/kg-default-nodes/{lib/utils/read-image-attributes-from-element.js => src/utils/read-image-attributes-from-element.ts} (100%) rename packages/kg-default-nodes/{lib/utils/read-text-content.js => src/utils/read-text-content.ts} (100%) rename packages/kg-default-nodes/{lib/utils/render-empty-container.js => src/utils/render-empty-container.ts} (100%) rename packages/kg-default-nodes/{lib/utils/render-helpers/email-button.js => src/utils/render-helpers/email-button.ts} (100%) rename packages/kg-default-nodes/{lib/utils/replacement-strings.js => src/utils/replacement-strings.ts} (100%) rename packages/kg-default-nodes/{lib/utils/rgb-to-hex.js => src/utils/rgb-to-hex.ts} (100%) rename packages/kg-default-nodes/{lib/utils/set-src-background-from-parent.js => src/utils/set-src-background-from-parent.ts} (100%) rename packages/kg-default-nodes/{lib/utils/size-byte-converter.js => src/utils/size-byte-converter.ts} (100%) rename packages/kg-default-nodes/{lib/utils/slugify.js => src/utils/slugify.ts} (100%) rename packages/kg-default-nodes/{lib/utils/srcset-attribute.js => src/utils/srcset-attribute.ts} (100%) rename packages/kg-default-nodes/{lib/utils/tagged-template-fns.mjs => src/utils/tagged-template-fns.ts} (100%) rename packages/kg-default-nodes/{lib/utils/truncate.js => src/utils/truncate.ts} (100%) rename packages/kg-default-nodes/{lib/utils/visibility.js => src/utils/visibility.ts} (100%) rename packages/kg-default-nodes/test/{generate-decorator-node.test.js => generate-decorator-node.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{aside.test.js => aside.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{at-link-search.test.js => at-link-search.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{at-link.test.js => at-link.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{audio.test.js => audio.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{bookmark.test.js => bookmark.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{button.test.js => button.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{call-to-action.test.js => call-to-action.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{callout.test.js => callout.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{codeblock.test.js => codeblock.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{email-cta.test.js => email-cta.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{email.test.js => email.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{embed.test.js => embed.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{file.test.js => file.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{gallery.test.js => gallery.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{header.test.js => header.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{horizontalrule.test.js => horizontalrule.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{html.test.js => html.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{image.test.js => image.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{markdown.test.js => markdown.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{paywall.test.js => paywall.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{product.test.js => product.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{signup.test.js => signup.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{tk.test.js => tk.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{toggle.test.js => toggle.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{transistor.test.js => transistor.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{video.test.js => video.test.ts} (100%) rename packages/kg-default-nodes/test/nodes/{zwnj.test.js => zwnj.test.ts} (100%) rename packages/kg-default-nodes/test/serializers/{linebreak.test.js => linebreak.test.ts} (100%) rename packages/kg-default-nodes/test/serializers/{paragraph.test.js => paragraph.test.ts} (100%) rename packages/kg-default-nodes/test/test-utils/{assertions.js => assertions.ts} (100%) rename packages/kg-default-nodes/test/test-utils/{index.js => index.ts} (100%) rename packages/kg-default-nodes/test/test-utils/{overrides.js => overrides.ts} (100%) rename packages/kg-default-nodes/test/utils/{rgb-to-hex.test.js => rgb-to-hex.test.ts} (100%) rename packages/kg-default-nodes/test/utils/{tagged-template-fns.test.mjs => tagged-template-fns.test.ts} (100%) rename packages/kg-default-nodes/test/utils/{visibility.test.js => visibility.test.ts} (100%) diff --git a/packages/kg-default-nodes/lib/generate-decorator-node.js b/packages/kg-default-nodes/src/generate-decorator-node.ts similarity index 100% rename from packages/kg-default-nodes/lib/generate-decorator-node.js rename to packages/kg-default-nodes/src/generate-decorator-node.ts diff --git a/packages/kg-default-nodes/lib/kg-default-nodes.js b/packages/kg-default-nodes/src/kg-default-nodes.ts similarity index 100% rename from packages/kg-default-nodes/lib/kg-default-nodes.js rename to packages/kg-default-nodes/src/kg-default-nodes.ts diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.js b/packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/ExtendedHeadingNode.js rename to packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.js b/packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/ExtendedQuoteNode.js rename to packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/ExtendedTextNode.js b/packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/ExtendedTextNode.js rename to packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/TKNode.js b/packages/kg-default-nodes/src/nodes/TKNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/TKNode.js rename to packages/kg-default-nodes/src/nodes/TKNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/aside/AsideNode.js b/packages/kg-default-nodes/src/nodes/aside/AsideNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/aside/AsideNode.js rename to packages/kg-default-nodes/src/nodes/aside/AsideNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/aside/AsideParser.js b/packages/kg-default-nodes/src/nodes/aside/AsideParser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/aside/AsideParser.js rename to packages/kg-default-nodes/src/nodes/aside/AsideParser.ts diff --git a/packages/kg-default-nodes/lib/nodes/at-link/AtLinkNode.js b/packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/at-link/AtLinkNode.js rename to packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/at-link/AtLinkSearchNode.js b/packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/at-link/AtLinkSearchNode.js rename to packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/at-link/index.js b/packages/kg-default-nodes/src/nodes/at-link/index.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/at-link/index.js rename to packages/kg-default-nodes/src/nodes/at-link/index.ts diff --git a/packages/kg-default-nodes/lib/nodes/at-link/kg-link.svg b/packages/kg-default-nodes/src/nodes/at-link/kg-link.svg similarity index 100% rename from packages/kg-default-nodes/lib/nodes/at-link/kg-link.svg rename to packages/kg-default-nodes/src/nodes/at-link/kg-link.svg diff --git a/packages/kg-default-nodes/lib/nodes/audio/AudioNode.js b/packages/kg-default-nodes/src/nodes/audio/AudioNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/audio/AudioNode.js rename to packages/kg-default-nodes/src/nodes/audio/AudioNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/audio/audio-parser.js b/packages/kg-default-nodes/src/nodes/audio/audio-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/audio/audio-parser.js rename to packages/kg-default-nodes/src/nodes/audio/audio-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/audio/audio-renderer.js b/packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/audio/audio-renderer.js rename to packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js b/packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js rename to packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.js b/packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/bookmark/bookmark-parser.js rename to packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/bookmark/bookmark-renderer.js b/packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/bookmark/bookmark-renderer.js rename to packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/button/ButtonNode.js b/packages/kg-default-nodes/src/nodes/button/ButtonNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/button/ButtonNode.js rename to packages/kg-default-nodes/src/nodes/button/ButtonNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/button/button-parser.js b/packages/kg-default-nodes/src/nodes/button/button-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/button/button-parser.js rename to packages/kg-default-nodes/src/nodes/button/button-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/button/button-renderer.js b/packages/kg-default-nodes/src/nodes/button/button-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/button/button-renderer.js rename to packages/kg-default-nodes/src/nodes/button/button-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js b/packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js rename to packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-parser.js b/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-parser.js rename to packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js b/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js rename to packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js b/packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js rename to packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/callout/callout-parser.js b/packages/kg-default-nodes/src/nodes/callout/callout-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/callout/callout-parser.js rename to packages/kg-default-nodes/src/nodes/callout/callout-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/callout/callout-renderer.js b/packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/callout/callout-renderer.js rename to packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js b/packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js rename to packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js b/packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js rename to packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/codeblock/codeblock-renderer.js b/packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/codeblock/codeblock-renderer.js rename to packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js b/packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js rename to packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/email-cta/email-cta-renderer.js b/packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/email-cta/email-cta-renderer.js rename to packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/email/EmailNode.js b/packages/kg-default-nodes/src/nodes/email/EmailNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/email/EmailNode.js rename to packages/kg-default-nodes/src/nodes/email/EmailNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/email/email-renderer.js b/packages/kg-default-nodes/src/nodes/email/email-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/email/email-renderer.js rename to packages/kg-default-nodes/src/nodes/email/email-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js b/packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js rename to packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/embed/embed-parser.js b/packages/kg-default-nodes/src/nodes/embed/embed-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/embed/embed-parser.js rename to packages/kg-default-nodes/src/nodes/embed/embed-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js b/packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js rename to packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/embed/types/twitter.js b/packages/kg-default-nodes/src/nodes/embed/types/twitter.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/embed/types/twitter.js rename to packages/kg-default-nodes/src/nodes/embed/types/twitter.ts diff --git a/packages/kg-default-nodes/lib/nodes/file/FileNode.js b/packages/kg-default-nodes/src/nodes/file/FileNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/file/FileNode.js rename to packages/kg-default-nodes/src/nodes/file/FileNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/file/file-parser.js b/packages/kg-default-nodes/src/nodes/file/file-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/file/file-parser.js rename to packages/kg-default-nodes/src/nodes/file/file-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/file/file-renderer.js b/packages/kg-default-nodes/src/nodes/file/file-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/file/file-renderer.js rename to packages/kg-default-nodes/src/nodes/file/file-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js b/packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js rename to packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.js b/packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/gallery/gallery-parser.js rename to packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/gallery/gallery-renderer.js b/packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/gallery/gallery-renderer.js rename to packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/header/HeaderNode.js b/packages/kg-default-nodes/src/nodes/header/HeaderNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/header/HeaderNode.js rename to packages/kg-default-nodes/src/nodes/header/HeaderNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/header/parsers/header-parser.js b/packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/header/parsers/header-parser.js rename to packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/header/renderers/v1/header-renderer.js b/packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/header/renderers/v1/header-renderer.js rename to packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/header/renderers/v2/header-renderer.js b/packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/header/renderers/v2/header-renderer.js rename to packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js b/packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js rename to packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js b/packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js rename to packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js b/packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js rename to packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/html/HtmlNode.js b/packages/kg-default-nodes/src/nodes/html/HtmlNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/html/HtmlNode.js rename to packages/kg-default-nodes/src/nodes/html/HtmlNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/html/html-parser.js b/packages/kg-default-nodes/src/nodes/html/html-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/html/html-parser.js rename to packages/kg-default-nodes/src/nodes/html/html-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/html/html-renderer.js b/packages/kg-default-nodes/src/nodes/html/html-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/html/html-renderer.js rename to packages/kg-default-nodes/src/nodes/html/html-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/image/ImageNode.js b/packages/kg-default-nodes/src/nodes/image/ImageNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/image/ImageNode.js rename to packages/kg-default-nodes/src/nodes/image/ImageNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/image/image-parser.js b/packages/kg-default-nodes/src/nodes/image/image-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/image/image-parser.js rename to packages/kg-default-nodes/src/nodes/image/image-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/image/image-renderer.js b/packages/kg-default-nodes/src/nodes/image/image-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/image/image-renderer.js rename to packages/kg-default-nodes/src/nodes/image/image-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js b/packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js rename to packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js b/packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js rename to packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js b/packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js rename to packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js b/packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js rename to packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/paywall/paywall-renderer.js b/packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/paywall/paywall-renderer.js rename to packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/product/ProductNode.js b/packages/kg-default-nodes/src/nodes/product/ProductNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/product/ProductNode.js rename to packages/kg-default-nodes/src/nodes/product/ProductNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/product/product-parser.js b/packages/kg-default-nodes/src/nodes/product/product-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/product/product-parser.js rename to packages/kg-default-nodes/src/nodes/product/product-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/product/product-renderer.js b/packages/kg-default-nodes/src/nodes/product/product-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/product/product-renderer.js rename to packages/kg-default-nodes/src/nodes/product/product-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/signup/SignupNode.js b/packages/kg-default-nodes/src/nodes/signup/SignupNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/signup/SignupNode.js rename to packages/kg-default-nodes/src/nodes/signup/SignupNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/signup/signup-parser.js b/packages/kg-default-nodes/src/nodes/signup/signup-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/signup/signup-parser.js rename to packages/kg-default-nodes/src/nodes/signup/signup-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/signup/signup-renderer.js b/packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/signup/signup-renderer.js rename to packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js b/packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js rename to packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/toggle/toggle-parser.js b/packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/toggle/toggle-parser.js rename to packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/toggle/toggle-renderer.js b/packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/toggle/toggle-renderer.js rename to packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/transistor/TransistorNode.js b/packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/transistor/TransistorNode.js rename to packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/transistor/transistor-renderer.js b/packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/transistor/transistor-renderer.js rename to packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/video/VideoNode.js b/packages/kg-default-nodes/src/nodes/video/VideoNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/video/VideoNode.js rename to packages/kg-default-nodes/src/nodes/video/VideoNode.ts diff --git a/packages/kg-default-nodes/lib/nodes/video/video-parser.js b/packages/kg-default-nodes/src/nodes/video/video-parser.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/video/video-parser.js rename to packages/kg-default-nodes/src/nodes/video/video-parser.ts diff --git a/packages/kg-default-nodes/lib/nodes/video/video-renderer.js b/packages/kg-default-nodes/src/nodes/video/video-renderer.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/video/video-renderer.js rename to packages/kg-default-nodes/src/nodes/video/video-renderer.ts diff --git a/packages/kg-default-nodes/lib/nodes/zwnj/ZWNJNode.js b/packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts similarity index 100% rename from packages/kg-default-nodes/lib/nodes/zwnj/ZWNJNode.js rename to packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts diff --git a/packages/kg-default-nodes/lib/serializers/linebreak.js b/packages/kg-default-nodes/src/serializers/linebreak.ts similarity index 100% rename from packages/kg-default-nodes/lib/serializers/linebreak.js rename to packages/kg-default-nodes/src/serializers/linebreak.ts diff --git a/packages/kg-default-nodes/lib/serializers/paragraph.js b/packages/kg-default-nodes/src/serializers/paragraph.ts similarity index 100% rename from packages/kg-default-nodes/lib/serializers/paragraph.js rename to packages/kg-default-nodes/src/serializers/paragraph.ts diff --git a/packages/kg-default-nodes/lib/utils/add-create-document-option.js b/packages/kg-default-nodes/src/utils/add-create-document-option.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/add-create-document-option.js rename to packages/kg-default-nodes/src/utils/add-create-document-option.ts diff --git a/packages/kg-default-nodes/lib/utils/build-clean-basic-html-for-element.js b/packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/build-clean-basic-html-for-element.js rename to packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts diff --git a/packages/kg-default-nodes/lib/utils/clean-dom.js b/packages/kg-default-nodes/src/utils/clean-dom.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/clean-dom.js rename to packages/kg-default-nodes/src/utils/clean-dom.ts diff --git a/packages/kg-default-nodes/lib/utils/escape-html.js b/packages/kg-default-nodes/src/utils/escape-html.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/escape-html.js rename to packages/kg-default-nodes/src/utils/escape-html.ts diff --git a/packages/kg-default-nodes/lib/utils/get-available-image-widths.js b/packages/kg-default-nodes/src/utils/get-available-image-widths.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/get-available-image-widths.js rename to packages/kg-default-nodes/src/utils/get-available-image-widths.ts diff --git a/packages/kg-default-nodes/lib/utils/get-resized-image-dimensions.js b/packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/get-resized-image-dimensions.js rename to packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts diff --git a/packages/kg-default-nodes/lib/utils/is-local-content-image.js b/packages/kg-default-nodes/src/utils/is-local-content-image.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/is-local-content-image.js rename to packages/kg-default-nodes/src/utils/is-local-content-image.ts diff --git a/packages/kg-default-nodes/lib/utils/is-unsplash-image.js b/packages/kg-default-nodes/src/utils/is-unsplash-image.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/is-unsplash-image.js rename to packages/kg-default-nodes/src/utils/is-unsplash-image.ts diff --git a/packages/kg-default-nodes/lib/utils/read-caption-from-element.js b/packages/kg-default-nodes/src/utils/read-caption-from-element.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/read-caption-from-element.js rename to packages/kg-default-nodes/src/utils/read-caption-from-element.ts diff --git a/packages/kg-default-nodes/lib/utils/read-image-attributes-from-element.js b/packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/read-image-attributes-from-element.js rename to packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts diff --git a/packages/kg-default-nodes/lib/utils/read-text-content.js b/packages/kg-default-nodes/src/utils/read-text-content.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/read-text-content.js rename to packages/kg-default-nodes/src/utils/read-text-content.ts diff --git a/packages/kg-default-nodes/lib/utils/render-empty-container.js b/packages/kg-default-nodes/src/utils/render-empty-container.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/render-empty-container.js rename to packages/kg-default-nodes/src/utils/render-empty-container.ts diff --git a/packages/kg-default-nodes/lib/utils/render-helpers/email-button.js b/packages/kg-default-nodes/src/utils/render-helpers/email-button.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/render-helpers/email-button.js rename to packages/kg-default-nodes/src/utils/render-helpers/email-button.ts diff --git a/packages/kg-default-nodes/lib/utils/replacement-strings.js b/packages/kg-default-nodes/src/utils/replacement-strings.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/replacement-strings.js rename to packages/kg-default-nodes/src/utils/replacement-strings.ts diff --git a/packages/kg-default-nodes/lib/utils/rgb-to-hex.js b/packages/kg-default-nodes/src/utils/rgb-to-hex.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/rgb-to-hex.js rename to packages/kg-default-nodes/src/utils/rgb-to-hex.ts diff --git a/packages/kg-default-nodes/lib/utils/set-src-background-from-parent.js b/packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/set-src-background-from-parent.js rename to packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts diff --git a/packages/kg-default-nodes/lib/utils/size-byte-converter.js b/packages/kg-default-nodes/src/utils/size-byte-converter.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/size-byte-converter.js rename to packages/kg-default-nodes/src/utils/size-byte-converter.ts diff --git a/packages/kg-default-nodes/lib/utils/slugify.js b/packages/kg-default-nodes/src/utils/slugify.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/slugify.js rename to packages/kg-default-nodes/src/utils/slugify.ts diff --git a/packages/kg-default-nodes/lib/utils/srcset-attribute.js b/packages/kg-default-nodes/src/utils/srcset-attribute.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/srcset-attribute.js rename to packages/kg-default-nodes/src/utils/srcset-attribute.ts diff --git a/packages/kg-default-nodes/lib/utils/tagged-template-fns.mjs b/packages/kg-default-nodes/src/utils/tagged-template-fns.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/tagged-template-fns.mjs rename to packages/kg-default-nodes/src/utils/tagged-template-fns.ts diff --git a/packages/kg-default-nodes/lib/utils/truncate.js b/packages/kg-default-nodes/src/utils/truncate.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/truncate.js rename to packages/kg-default-nodes/src/utils/truncate.ts diff --git a/packages/kg-default-nodes/lib/utils/visibility.js b/packages/kg-default-nodes/src/utils/visibility.ts similarity index 100% rename from packages/kg-default-nodes/lib/utils/visibility.js rename to packages/kg-default-nodes/src/utils/visibility.ts diff --git a/packages/kg-default-nodes/test/generate-decorator-node.test.js b/packages/kg-default-nodes/test/generate-decorator-node.test.ts similarity index 100% rename from packages/kg-default-nodes/test/generate-decorator-node.test.js rename to packages/kg-default-nodes/test/generate-decorator-node.test.ts diff --git a/packages/kg-default-nodes/test/nodes/aside.test.js b/packages/kg-default-nodes/test/nodes/aside.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/aside.test.js rename to packages/kg-default-nodes/test/nodes/aside.test.ts diff --git a/packages/kg-default-nodes/test/nodes/at-link-search.test.js b/packages/kg-default-nodes/test/nodes/at-link-search.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/at-link-search.test.js rename to packages/kg-default-nodes/test/nodes/at-link-search.test.ts diff --git a/packages/kg-default-nodes/test/nodes/at-link.test.js b/packages/kg-default-nodes/test/nodes/at-link.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/at-link.test.js rename to packages/kg-default-nodes/test/nodes/at-link.test.ts diff --git a/packages/kg-default-nodes/test/nodes/audio.test.js b/packages/kg-default-nodes/test/nodes/audio.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/audio.test.js rename to packages/kg-default-nodes/test/nodes/audio.test.ts diff --git a/packages/kg-default-nodes/test/nodes/bookmark.test.js b/packages/kg-default-nodes/test/nodes/bookmark.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/bookmark.test.js rename to packages/kg-default-nodes/test/nodes/bookmark.test.ts diff --git a/packages/kg-default-nodes/test/nodes/button.test.js b/packages/kg-default-nodes/test/nodes/button.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/button.test.js rename to packages/kg-default-nodes/test/nodes/button.test.ts diff --git a/packages/kg-default-nodes/test/nodes/call-to-action.test.js b/packages/kg-default-nodes/test/nodes/call-to-action.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/call-to-action.test.js rename to packages/kg-default-nodes/test/nodes/call-to-action.test.ts diff --git a/packages/kg-default-nodes/test/nodes/callout.test.js b/packages/kg-default-nodes/test/nodes/callout.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/callout.test.js rename to packages/kg-default-nodes/test/nodes/callout.test.ts diff --git a/packages/kg-default-nodes/test/nodes/codeblock.test.js b/packages/kg-default-nodes/test/nodes/codeblock.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/codeblock.test.js rename to packages/kg-default-nodes/test/nodes/codeblock.test.ts diff --git a/packages/kg-default-nodes/test/nodes/email-cta.test.js b/packages/kg-default-nodes/test/nodes/email-cta.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/email-cta.test.js rename to packages/kg-default-nodes/test/nodes/email-cta.test.ts diff --git a/packages/kg-default-nodes/test/nodes/email.test.js b/packages/kg-default-nodes/test/nodes/email.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/email.test.js rename to packages/kg-default-nodes/test/nodes/email.test.ts diff --git a/packages/kg-default-nodes/test/nodes/embed.test.js b/packages/kg-default-nodes/test/nodes/embed.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/embed.test.js rename to packages/kg-default-nodes/test/nodes/embed.test.ts diff --git a/packages/kg-default-nodes/test/nodes/file.test.js b/packages/kg-default-nodes/test/nodes/file.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/file.test.js rename to packages/kg-default-nodes/test/nodes/file.test.ts diff --git a/packages/kg-default-nodes/test/nodes/gallery.test.js b/packages/kg-default-nodes/test/nodes/gallery.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/gallery.test.js rename to packages/kg-default-nodes/test/nodes/gallery.test.ts diff --git a/packages/kg-default-nodes/test/nodes/header.test.js b/packages/kg-default-nodes/test/nodes/header.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/header.test.js rename to packages/kg-default-nodes/test/nodes/header.test.ts diff --git a/packages/kg-default-nodes/test/nodes/horizontalrule.test.js b/packages/kg-default-nodes/test/nodes/horizontalrule.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/horizontalrule.test.js rename to packages/kg-default-nodes/test/nodes/horizontalrule.test.ts diff --git a/packages/kg-default-nodes/test/nodes/html.test.js b/packages/kg-default-nodes/test/nodes/html.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/html.test.js rename to packages/kg-default-nodes/test/nodes/html.test.ts diff --git a/packages/kg-default-nodes/test/nodes/image.test.js b/packages/kg-default-nodes/test/nodes/image.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/image.test.js rename to packages/kg-default-nodes/test/nodes/image.test.ts diff --git a/packages/kg-default-nodes/test/nodes/markdown.test.js b/packages/kg-default-nodes/test/nodes/markdown.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/markdown.test.js rename to packages/kg-default-nodes/test/nodes/markdown.test.ts diff --git a/packages/kg-default-nodes/test/nodes/paywall.test.js b/packages/kg-default-nodes/test/nodes/paywall.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/paywall.test.js rename to packages/kg-default-nodes/test/nodes/paywall.test.ts diff --git a/packages/kg-default-nodes/test/nodes/product.test.js b/packages/kg-default-nodes/test/nodes/product.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/product.test.js rename to packages/kg-default-nodes/test/nodes/product.test.ts diff --git a/packages/kg-default-nodes/test/nodes/signup.test.js b/packages/kg-default-nodes/test/nodes/signup.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/signup.test.js rename to packages/kg-default-nodes/test/nodes/signup.test.ts diff --git a/packages/kg-default-nodes/test/nodes/tk.test.js b/packages/kg-default-nodes/test/nodes/tk.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/tk.test.js rename to packages/kg-default-nodes/test/nodes/tk.test.ts diff --git a/packages/kg-default-nodes/test/nodes/toggle.test.js b/packages/kg-default-nodes/test/nodes/toggle.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/toggle.test.js rename to packages/kg-default-nodes/test/nodes/toggle.test.ts diff --git a/packages/kg-default-nodes/test/nodes/transistor.test.js b/packages/kg-default-nodes/test/nodes/transistor.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/transistor.test.js rename to packages/kg-default-nodes/test/nodes/transistor.test.ts diff --git a/packages/kg-default-nodes/test/nodes/video.test.js b/packages/kg-default-nodes/test/nodes/video.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/video.test.js rename to packages/kg-default-nodes/test/nodes/video.test.ts diff --git a/packages/kg-default-nodes/test/nodes/zwnj.test.js b/packages/kg-default-nodes/test/nodes/zwnj.test.ts similarity index 100% rename from packages/kg-default-nodes/test/nodes/zwnj.test.js rename to packages/kg-default-nodes/test/nodes/zwnj.test.ts diff --git a/packages/kg-default-nodes/test/serializers/linebreak.test.js b/packages/kg-default-nodes/test/serializers/linebreak.test.ts similarity index 100% rename from packages/kg-default-nodes/test/serializers/linebreak.test.js rename to packages/kg-default-nodes/test/serializers/linebreak.test.ts diff --git a/packages/kg-default-nodes/test/serializers/paragraph.test.js b/packages/kg-default-nodes/test/serializers/paragraph.test.ts similarity index 100% rename from packages/kg-default-nodes/test/serializers/paragraph.test.js rename to packages/kg-default-nodes/test/serializers/paragraph.test.ts diff --git a/packages/kg-default-nodes/test/test-utils/assertions.js b/packages/kg-default-nodes/test/test-utils/assertions.ts similarity index 100% rename from packages/kg-default-nodes/test/test-utils/assertions.js rename to packages/kg-default-nodes/test/test-utils/assertions.ts diff --git a/packages/kg-default-nodes/test/test-utils/index.js b/packages/kg-default-nodes/test/test-utils/index.ts similarity index 100% rename from packages/kg-default-nodes/test/test-utils/index.js rename to packages/kg-default-nodes/test/test-utils/index.ts diff --git a/packages/kg-default-nodes/test/test-utils/overrides.js b/packages/kg-default-nodes/test/test-utils/overrides.ts similarity index 100% rename from packages/kg-default-nodes/test/test-utils/overrides.js rename to packages/kg-default-nodes/test/test-utils/overrides.ts diff --git a/packages/kg-default-nodes/test/utils/rgb-to-hex.test.js b/packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts similarity index 100% rename from packages/kg-default-nodes/test/utils/rgb-to-hex.test.js rename to packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts diff --git a/packages/kg-default-nodes/test/utils/tagged-template-fns.test.mjs b/packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts similarity index 100% rename from packages/kg-default-nodes/test/utils/tagged-template-fns.test.mjs rename to packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts diff --git a/packages/kg-default-nodes/test/utils/visibility.test.js b/packages/kg-default-nodes/test/utils/visibility.test.ts similarity index 100% rename from packages/kg-default-nodes/test/utils/visibility.test.js rename to packages/kg-default-nodes/test/utils/visibility.test.ts From be6e93464990c187d7ab41aeb901fc2c1e32b56c Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 17 Apr 2026 09:57:05 +0100 Subject: [PATCH 2/2] Migrated kg-default-nodes to TypeScript no issue This migrates kg-default-nodes from JavaScript to TypeScript, switches the package to a tsc-based ESM/CJS build with emitted type declarations, and tightens node, parser, and renderer typing so the package keeps its existing runtime behavior under stricter compile-time guarantees. --- packages/kg-default-nodes/eslint.config.mjs | 78 +- packages/kg-default-nodes/index.js | 1 - packages/kg-default-nodes/package.json | 60 +- packages/kg-default-nodes/rollup.config.mjs | 50 - .../src/KoenigDecoratorNode.ts | 17 + packages/kg-default-nodes/src/export-dom.ts | 40 + .../src/generate-decorator-node.ts | 199 +++- packages/kg-default-nodes/src/index.ts | 1 + .../kg-default-nodes/src/kg-default-nodes.ts | 122 +- .../src/nodes/ExtendedHeadingNode.ts | 26 +- .../src/nodes/ExtendedQuoteNode.ts | 16 +- .../src/nodes/ExtendedTextNode.ts | 34 +- packages/kg-default-nodes/src/nodes/TKNode.ts | 23 +- .../src/nodes/aside/AsideNode.ts | 13 +- .../src/nodes/aside/AsideParser.ts | 14 +- .../src/nodes/at-link/AtLinkNode.ts | 28 +- .../src/nodes/at-link/AtLinkSearchNode.ts | 38 +- .../src/nodes/at-link/index.ts | 4 +- .../src/nodes/audio/AudioNode.ts | 30 +- .../src/nodes/audio/audio-parser.ts | 27 +- .../src/nodes/audio/audio-renderer.ts | 60 +- .../src/nodes/bookmark/BookmarkNode.ts | 84 +- .../src/nodes/bookmark/bookmark-parser.ts | 44 +- .../src/nodes/bookmark/bookmark-renderer.ts | 58 +- .../src/nodes/button/ButtonNode.ts | 26 +- .../src/nodes/button/button-parser.ts | 16 +- .../src/nodes/button/button-renderer.ts | 37 +- .../nodes/call-to-action/CallToActionNode.ts | 52 +- .../call-to-action/calltoaction-parser.ts | 23 +- .../call-to-action/calltoaction-renderer.ts | 78 +- .../src/nodes/callout/CalloutNode.ts | 40 +- .../src/nodes/callout/callout-parser.ts | 13 +- .../src/nodes/callout/callout-renderer.ts | 19 +- .../src/nodes/codeblock/CodeBlockNode.ts | 26 +- .../src/nodes/codeblock/codeblock-parser.ts | 49 +- .../src/nodes/codeblock/codeblock-renderer.ts | 25 +- .../src/nodes/email-cta/EmailCtaNode.ts | 32 +- .../src/nodes/email-cta/email-cta-renderer.ts | 29 +- .../src/nodes/email/EmailNode.ts | 20 +- .../src/nodes/email/email-renderer.ts | 22 +- .../src/nodes/embed/EmbedNode.ts | 35 +- .../src/nodes/embed/embed-parser.ts | 37 +- .../src/nodes/embed/embed-renderer.ts | 36 +- .../src/nodes/embed/types/twitter.ts | 86 +- .../src/nodes/file/FileNode.ts | 35 +- .../src/nodes/file/file-parser.ts | 17 +- .../src/nodes/file/file-renderer.ts | 56 +- .../src/nodes/gallery/GalleryNode.ts | 25 +- .../src/nodes/gallery/gallery-parser.ts | 54 +- .../src/nodes/gallery/gallery-renderer.ts | 111 +- .../src/nodes/header/HeaderNode.ts | 66 +- .../src/nodes/header/parsers/header-parser.ts | 18 +- .../header/renderers/v1/header-renderer.ts | 28 +- .../header/renderers/v2/header-renderer.ts | 77 +- .../horizontalrule/HorizontalRuleNode.ts | 8 +- .../horizontalrule/horizontalrule-parser.ts | 6 +- .../horizontalrule/horizontalrule-renderer.ts | 13 +- .../src/nodes/html/HtmlNode.ts | 22 +- .../src/nodes/html/html-parser.ts | 64 +- .../src/nodes/html/html-renderer.ts | 27 +- .../src/nodes/image/ImageNode.ts | 37 +- .../src/nodes/image/image-parser.ts | 17 +- .../src/nodes/image/image-renderer.ts | 64 +- .../src/nodes/markdown/MarkdownNode.ts | 20 +- .../src/nodes/markdown/markdown-renderer.ts | 19 +- .../src/nodes/paywall/PaywallNode.ts | 15 +- .../src/nodes/paywall/paywall-parser.ts | 10 +- .../src/nodes/paywall/paywall-renderer.ts | 15 +- .../src/nodes/product/ProductNode.ts | 40 +- .../src/nodes/product/product-parser.ts | 27 +- .../src/nodes/product/product-renderer.ts | 66 +- .../src/nodes/signup/SignupNode.ts | 138 ++- .../src/nodes/signup/signup-parser.ts | 25 +- .../src/nodes/signup/signup-renderer.ts | 61 +- .../src/nodes/toggle/ToggleNode.ts | 24 +- .../src/nodes/toggle/toggle-parser.ts | 18 +- .../src/nodes/toggle/toggle-renderer.ts | 24 +- .../src/nodes/transistor/TransistorNode.ts | 35 +- .../nodes/transistor/transistor-renderer.ts | 26 +- .../src/nodes/video/VideoNode.ts | 47 +- .../src/nodes/video/video-parser.ts | 30 +- .../src/nodes/video/video-renderer.ts | 82 +- .../src/nodes/zwnj/ZWNJNode.ts | 7 +- .../src/serializers/linebreak.ts | 6 +- .../src/serializers/paragraph.ts | 2 +- packages/kg-default-nodes/src/svg.d.ts | 4 + .../src/utils/add-create-document-option.ts | 11 +- .../build-clean-basic-html-for-element.ts | 4 +- .../kg-default-nodes/src/utils/clean-dom.ts | 8 +- .../kg-default-nodes/src/utils/escape-html.ts | 2 +- .../src/utils/get-available-image-widths.ts | 2 +- .../src/utils/get-resized-image-dimensions.ts | 9 +- .../src/utils/is-local-content-image.ts | 2 +- .../src/utils/is-unsplash-image.ts | 2 +- .../src/utils/read-caption-from-element.ts | 11 +- .../read-image-attributes-from-element.ts | 17 +- .../src/utils/read-text-content.ts | 6 +- .../src/utils/render-empty-container.ts | 8 +- .../src/utils/render-helpers/email-button.ts | 2 +- .../src/utils/replacement-strings.ts | 20 +- .../kg-default-nodes/src/utils/rgb-to-hex.ts | 10 +- .../utils/set-src-background-from-parent.ts | 23 +- .../src/utils/size-byte-converter.ts | 8 +- .../kg-default-nodes/src/utils/slugify.ts | 2 +- .../src/utils/srcset-attribute.ts | 31 +- .../src/utils/tagged-template-fns.ts | 8 +- .../kg-default-nodes/src/utils/truncate.ts | 12 +- .../kg-default-nodes/src/utils/visibility.ts | 82 +- packages/kg-default-nodes/src/visibility.ts | 13 + .../test/generate-decorator-node.test.ts | 153 +-- .../kg-default-nodes/test/nodes/aside.test.ts | 22 +- .../test/nodes/at-link-search.test.ts | 87 +- .../test/nodes/at-link.test.ts | 78 +- .../kg-default-nodes/test/nodes/audio.test.ts | 94 +- .../test/nodes/bookmark.test.ts | 160 ++- .../test/nodes/button.test.ts | 42 +- .../test/nodes/call-to-action.test.ts | 147 ++- .../test/nodes/callout.test.ts | 55 +- .../test/nodes/codeblock.test.ts | 59 +- .../test/nodes/email-cta.test.ts | 53 +- .../kg-default-nodes/test/nodes/email.test.ts | 69 +- .../kg-default-nodes/test/nodes/embed.test.ts | 109 +- .../kg-default-nodes/test/nodes/file.test.ts | 73 +- .../test/nodes/gallery.test.ts | 165 ++- .../test/nodes/header.test.ts | 103 +- .../test/nodes/horizontalrule.test.ts | 23 +- .../kg-default-nodes/test/nodes/html.test.ts | 136 ++- .../kg-default-nodes/test/nodes/image.test.ts | 116 +- .../test/nodes/markdown.test.ts | 38 +- .../test/nodes/paywall.test.ts | 26 +- .../test/nodes/product.test.ts | 104 +- .../test/nodes/signup.test.ts | 154 ++- .../kg-default-nodes/test/nodes/tk.test.ts | 25 +- .../test/nodes/toggle.test.ts | 40 +- .../test/nodes/transistor.test.ts | 101 +- .../kg-default-nodes/test/nodes/video.test.ts | 203 +++- .../kg-default-nodes/test/nodes/zwnj.test.ts | 11 +- .../test/serializers/linebreak.test.ts | 49 +- .../test/serializers/paragraph.test.ts | 18 +- .../test/test-utils/assertions.ts | 19 +- .../test/test-utils/html-minifier.d.ts | 3 + .../kg-default-nodes/test/test-utils/index.ts | 32 +- .../test/test-utils/overrides.ts | 10 +- .../test/test-utils/should-assertions.d.ts | 5 + .../test/test-utils/should.d.ts | 13 + .../utils/read-caption-from-element.test.ts | 19 + .../test/utils/rgb-to-hex.test.ts | 5 +- .../test/utils/srcset-attribute.test.ts | 22 + .../test/utils/tagged-template-fns.test.ts | 2 +- .../test/utils/visibility.test.ts | 49 +- .../test/visibility-smoke.test.ts | 24 + packages/kg-default-nodes/tsconfig.cjs.json | 13 + packages/kg-default-nodes/tsconfig.json | 21 + packages/kg-default-nodes/tsconfig.test.json | 10 + .../koenig-lexical/src/utils/visibility.js | 6 +- .../unit/hooks/useVisibilityToggle.test.js | 19 +- yarn.lock | 1048 +++-------------- 157 files changed, 4277 insertions(+), 3103 deletions(-) delete mode 100644 packages/kg-default-nodes/index.js delete mode 100644 packages/kg-default-nodes/rollup.config.mjs create mode 100644 packages/kg-default-nodes/src/KoenigDecoratorNode.ts create mode 100644 packages/kg-default-nodes/src/export-dom.ts create mode 100644 packages/kg-default-nodes/src/index.ts create mode 100644 packages/kg-default-nodes/src/svg.d.ts create mode 100644 packages/kg-default-nodes/src/visibility.ts create mode 100644 packages/kg-default-nodes/test/test-utils/html-minifier.d.ts create mode 100644 packages/kg-default-nodes/test/test-utils/should-assertions.d.ts create mode 100644 packages/kg-default-nodes/test/test-utils/should.d.ts create mode 100644 packages/kg-default-nodes/test/utils/read-caption-from-element.test.ts create mode 100644 packages/kg-default-nodes/test/utils/srcset-attribute.test.ts create mode 100644 packages/kg-default-nodes/test/visibility-smoke.test.ts create mode 100644 packages/kg-default-nodes/tsconfig.cjs.json create mode 100644 packages/kg-default-nodes/tsconfig.json create mode 100644 packages/kg-default-nodes/tsconfig.test.json diff --git a/packages/kg-default-nodes/eslint.config.mjs b/packages/kg-default-nodes/eslint.config.mjs index 9a583ec14c..e7a814e84d 100644 --- a/packages/kg-default-nodes/eslint.config.mjs +++ b/packages/kg-default-nodes/eslint.config.mjs @@ -1,45 +1,41 @@ -import {fixupPluginRules} from '@eslint/compat'; import eslint from '@eslint/js'; +import {defineConfig} from 'eslint/config'; import ghostPlugin from 'eslint-plugin-ghost'; -import globals from 'globals'; +import tseslint from 'typescript-eslint'; -const ghost = fixupPluginRules(ghostPlugin); - -export default [ - {ignores: ['build/**', 'cjs/**', 'es/**']}, - eslint.configs.recommended, - { - files: ['**/*.{js,mjs}'], - plugins: {ghost}, - languageOptions: { - globals: { - ...globals.node, - ...globals.browser - } - }, - rules: { - ...ghostPlugin.configs.node.rules, - // match ESLint 8 behavior for catch clause variables - 'no-unused-vars': ['error', {caughtErrors: 'none'}], - // disable rules incompatible with ESLint 9 flat config - 'ghost/filenames/match-exported-class': 'off', - 'ghost/filenames/match-exported': 'off', - 'ghost/filenames/match-regex': 'off' - } +export default defineConfig([ + { ignores: ['build/**'] }, + { + files: ['**/*.ts'], + extends: [ + eslint.configs.recommended, + tseslint.configs.recommended, + ], + languageOptions: { + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, + }, + plugins: { ghost: ghostPlugin }, + rules: { + ...ghostPlugin.configs.ts.rules, + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + { + files: ['src/**/*.ts'], + rules: { + '@typescript-eslint/no-unsafe-declaration-merging': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + }, + }, + { + files: ['test/**/*.ts'], + rules: { + ...ghostPlugin.configs['ts-test'].rules, + 'ghost/mocha/no-global-tests': 'off', + 'ghost/mocha/handle-done-callback': 'off', + 'ghost/mocha/no-mocha-arrows': 'off', + 'ghost/mocha/max-top-level-suites': 'off', + 'ghost/mocha/no-setup-in-describe': 'off', }, - { - files: ['test/**/*.{js,mjs}'], - plugins: {ghost}, - languageOptions: { - globals: { - ...globals.node, - ...globals.mocha, - should: true, - sinon: true - } - }, - rules: { - ...ghostPlugin.configs.test.rules - } - } -]; + }, +]); diff --git a/packages/kg-default-nodes/index.js b/packages/kg-default-nodes/index.js deleted file mode 100644 index de6039568e..0000000000 --- a/packages/kg-default-nodes/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/kg-default-nodes'; diff --git a/packages/kg-default-nodes/package.json b/packages/kg-default-nodes/package.json index 074460866e..0a3ef8244a 100644 --- a/packages/kg-default-nodes/package.json +++ b/packages/kg-default-nodes/package.json @@ -4,45 +4,65 @@ "repository": "https://github.com/TryGhost/Koenig/tree/main/packages/kg-default-nodes", "author": "Ghost Foundation", "license": "MIT", - "main": "cjs/kg-default-nodes.js", - "module": "es/kg-default-nodes.js", - "source": "lib/kg-default-nodes.js", + "engines": { + "node": "^22.13.1 || ^24.0.0" + }, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/esm/index.d.ts", + "exports": { + ".": { + "types": "./build/esm/index.d.ts", + "import": "./build/esm/index.js", + "require": "./build/cjs/index.js" + }, + "./visibility": { + "types": "./build/esm/visibility.d.ts", + "import": "./build/esm/visibility.js", + "require": "./build/cjs/visibility.js" + } + }, "scripts": { - "dev": "rollup -c -w", - "build": "rollup -c", - "prepare": "NODE_ENV=production yarn build", - "pretest": "yarn build", - "test:unit": "NODE_ENV=testing c8 --src lib --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.*js'", + "dev": "tsc --watch --preserveWatchOutput", + "clean": "rm -rf build", + "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "prepare": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "pretest": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json", + "test:unit": "NODE_ENV=testing c8 --src src --all --check-coverage --reporter text --reporter cobertura mocha --require tsx './test/**/*.test.ts'", "test": "yarn test:unit", - "test:no-coverage": "yarn pretest && mocha './test/**/*.test.*js'", - "lint": "eslint . --cache" + "lint:code": "eslint src/ --cache", + "lint": "yarn lint:code && yarn lint:test", + "lint:test": "eslint test/ --cache" }, "files": [ "LICENSE", "README.md", - "cjs/", - "es/", - "lib/" + "build" ], "publishConfig": { "access": "public" }, "devDependencies": { - "@babel/eslint-parser": "7.28.6", - "@babel/plugin-syntax-import-assertions": "7.28.6", - "@babel/preset-env": "7.29.2", + "@eslint/js": "9.39.4", "@lexical/headless": "0.13.1", "@lexical/html": "0.13.1", "@prettier/sync": "0.6.1", - "@rollup/plugin-babel": "7.0.0", + "@types/jsdom": "^21.1.7", + "@types/lodash": "^4.17.24", + "@types/luxon": "^3.6.2", + "@types/mocha": "10.0.10", + "@types/node": "24.12.0", + "@types/should": "^13.0.0", + "@types/sinon": "21.0.0", "c8": "11.0.0", "html-minifier": "4.0.0", "mocha": "11.7.5", "prettier": "3.8.2", - "rollup": "4.60.1", - "rollup-plugin-svg": "2.0.0", "should": "13.2.3", - "sinon": "21.1.2" + "sinon": "21.1.2", + "tsx": "4.21.0", + "typescript": "5.8.3", + "typescript-eslint": "8.57.0" }, "dependencies": { "@lexical/clipboard": "0.13.1", diff --git a/packages/kg-default-nodes/rollup.config.mjs b/packages/kg-default-nodes/rollup.config.mjs deleted file mode 100644 index 38ef3247b1..0000000000 --- a/packages/kg-default-nodes/rollup.config.mjs +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-env node */ -import pkg from './package.json' with { type: 'json' }; -import babel from '@rollup/plugin-babel'; -import svg from 'rollup-plugin-svg'; - -const dependencies = Object.keys(pkg.dependencies); - -export default [ - // Node build. - // No transpilation or bundling other than conversion from es modules to cjs - { - input: pkg.source, - output: { - file: pkg.main, - format: 'cjs', - sourcemap: true - }, - plugins: [svg()], - external: id => /lodash/.test(id) || dependencies.includes(id) - }, - - // ES module build - // Transpiles to target browser support for use in client apps - { - input: pkg.source, - output: { - file: pkg.module, - format: 'es', - sourcemap: true - }, - plugins: [ - svg(), - babel({ - babelHelpers: 'bundled', - presets: [ - ['@babel/preset-env', { - modules: false, - targets: [ - 'last 2 Chrome versions', - 'last 2 Firefox versions', - 'last 2 Safari versions' - ].join(', ') - }] - ], - exclude: ['node_modules/**', '../../node_modules/**'] - }) - ], - external: id => /lodash/.test(id) || dependencies.includes(id) - } -]; diff --git a/packages/kg-default-nodes/src/KoenigDecoratorNode.ts b/packages/kg-default-nodes/src/KoenigDecoratorNode.ts new file mode 100644 index 0000000000..3b42f6878b --- /dev/null +++ b/packages/kg-default-nodes/src/KoenigDecoratorNode.ts @@ -0,0 +1,17 @@ +/* c8 ignore start */ +import {DecoratorNode} from 'lexical'; + +export class KoenigDecoratorNode extends DecoratorNode { + static transform() { + return null; + } + + decorate(): unknown { + return null; + } +} + +export function $isKoenigCard(node: unknown): node is KoenigDecoratorNode { + return node instanceof KoenigDecoratorNode; +} +/* c8 ignore end */ diff --git a/packages/kg-default-nodes/src/export-dom.ts b/packages/kg-default-nodes/src/export-dom.ts new file mode 100644 index 0000000000..1785e22711 --- /dev/null +++ b/packages/kg-default-nodes/src/export-dom.ts @@ -0,0 +1,40 @@ +export type ExportDOMOutputType = 'inner' | 'outer' | 'value' | 'html'; + +export interface ExportDOMOutput { + element: TElement; + type: TType; +} + +export interface ExportDOMFeatureOptions { + emailCustomization?: boolean; + emailCustomizationAlpha?: boolean; + [key: string]: unknown; +} + +export interface ExportDOMDom { + window: {document: Document}; +} + +export interface ExportDOMOptionsBase { + createDocument?: () => Document; + dom?: ExportDOMDom; + target?: string; + postUrl?: string; + siteUrl?: string; + siteUuid?: string; + canTransformImage?: (src: string) => boolean; + imageOptimization?: Record; + feature?: ExportDOMFeatureOptions; + design?: Record; + [key: string]: unknown; +} + +export type ExportDOMRenderer = (node: TNode, options: TOptions) => TOutput; + +export type VersionedExportDOMRenderer = Record>; + +export type ExportDOMNodeRenderers = Record | VersionedExportDOMRenderer>; + +export interface ExportDOMOptions extends ExportDOMOptionsBase { + nodeRenderers?: ExportDOMNodeRenderers; +} diff --git a/packages/kg-default-nodes/src/generate-decorator-node.ts b/packages/kg-default-nodes/src/generate-decorator-node.ts index fa9192953b..309e364bee 100644 --- a/packages/kg-default-nodes/src/generate-decorator-node.ts +++ b/packages/kg-default-nodes/src/generate-decorator-node.ts @@ -1,27 +1,37 @@ -import {KoenigDecoratorNode} from './KoenigDecoratorNode'; -import readTextContent from './utils/read-text-content'; -import {buildDefaultVisibility, isVisibilityRestricted, migrateOldVisibilityFormat} from './utils/visibility'; +import {KoenigDecoratorNode} from './KoenigDecoratorNode.js'; +import type {ExportDOMOptions, ExportDOMOutput} from './export-dom.js'; +import readTextContent from './utils/read-text-content.js'; +import {buildDefaultVisibility, isVisibilityRestricted, migrateOldVisibilityFormat} from './utils/visibility.js'; +import type {Visibility} from './utils/visibility.js'; + +type RenderFn = (node: any, options: ExportDOMOptions) => TOutput; +type VersionedRenderFn = Record>; +type WidenLiteral = + T extends string ? string : + T extends number ? number : + T extends boolean ? boolean : + T extends readonly (infer U)[] ? U[] : + T; /** * Validates the required arguments passed to `generateDecoratorNode` */ -function validateArguments(nodeType, properties) { - /* eslint-disable ghost/ghost-custom/no-native-error */ +function validateArguments(nodeType: string, properties: readonly DecoratorNodeProperty[]) { /* 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) => { + properties.forEach((prop: DecoratorNodeProperty) => { 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 */ @@ -40,19 +50,114 @@ function validateArguments(nodeType, properties) { * @param {Function} defaultRenderFn - A function that returns a @tryghost/kg-lexical-html-renderer compatible object, e.g. {element: Div, type: 'inner} * @returns {Object} - The generated class. */ -export function generateDecoratorNode({nodeType, properties = [], defaultRenderFn, version = 1, hasVisibility = false}) { +export interface DecoratorNodeProperty { + name: Name; + default: Default; + urlType?: string; + urlPath?: string; + wordCount?: boolean; + privateName?: string; +} + +export type DecoratorNodeValueMap = { + [Prop in Props[number] as Prop['name']]: WidenLiteral; +} & (HasVisibility extends true ? {visibility: Visibility} : {}); + +export type DecoratorNodeData = Partial>; + +type GeneratedDecoratorNodeInstance, TOutput extends ExportDOMOutput = ExportDOMOutput> = GeneratedDecoratorNodeBase & TDataset & { + exportDOM(options?: ExportDOMOptions): TOutput; +}; + +export interface GeneratedDecoratorNodeClass, TOutput extends ExportDOMOutput = ExportDOMOutput> { + new (data?: Partial, key?: string): GeneratedDecoratorNodeInstance; + prototype: GeneratedDecoratorNodeInstance; + getType(): string; + clone(node: GeneratedDecoratorNodeInstance): GeneratedDecoratorNodeInstance; + transform(): null; + getPropertyDefaults(): TDataset; + readonly urlTransformMap: Record>; + importJSON(serializedNode: Record): GeneratedDecoratorNodeInstance; +} + +// Type-only base class used as the return type of generateDecoratorNode. +// This ensures TypeScript recognizes generated nodes as LexicalNode subclasses +// while preserving the dynamic property index signature. +export class GeneratedDecoratorNodeBase = Record> extends KoenigDecoratorNode { + [key: string]: unknown; + + constructor(data?: Partial, key?: string) { + super(key); + } + + getDataset(): Record { + return {}; + } + + exportJSON(): {type: string; version: number; [key: string]: unknown} { + return {type: '', version: 1}; + } + + static getPropertyDefaults(): Record { + return {}; + } + + static get urlTransformMap(): Record> { + return {}; + } + + static importJSON(_serializedNode: Record): GeneratedDecoratorNodeBase> { + return new GeneratedDecoratorNodeBase(); + } + + static transform() { + return null; + } + + hasDynamicData(): boolean { + return false; + } + + hasEditMode(): boolean { + return true; + } + + getIsVisibilityActive(): boolean { + return false; + } +} + +export function generateDecoratorNode< + Props extends readonly DecoratorNodeProperty[] = readonly [], + HasVisibility extends boolean = false, + TOutput extends ExportDOMOutput = ExportDOMOutput +>({nodeType, properties = [] as unknown as Props, defaultRenderFn, version = 1, hasVisibility = false as HasVisibility}: { + nodeType: string; + properties?: Props; + defaultRenderFn?: RenderFn | VersionedRenderFn; + version?: number; + hasVisibility?: HasVisibility; +}): GeneratedDecoratorNodeClass, TOutput> { 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) => { - return {...prop, privateName: `__${prop.name}`}; + const internalProps = properties.map((prop) => { + return Object.defineProperties({}, { + ...Object.getOwnPropertyDescriptors(prop), + privateName: { + configurable: true, + enumerable: true, + value: `__${prop.name}`, + writable: true + } + }) as DecoratorNodeProperty & {privateName: string}; }); // Adds `visibility` property to the properties array if `hasVisibility` is true // uses a getter for `default` to avoid problems with mutation of nested objects if (hasVisibility) { - properties.push({ + internalProps.push({ name: 'visibility', get default() { return buildDefaultVisibility(); @@ -62,13 +167,16 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF } class GeneratedDecoratorNode extends KoenigDecoratorNode { - constructor(data = {}, key) { + [key: string]: unknown; + + constructor(data: Partial> = {}, key?: string) { super(key); - properties.forEach((prop) => { + const dataset = data as Record; + internalProps.forEach((prop) => { if (typeof prop.default === 'boolean') { - this[prop.privateName] = data[prop.name] ?? prop.default; + this[prop.privateName] = dataset[prop.name] ?? prop.default; } else { - this[prop.privateName] = data[prop.name] || prop.default; + this[prop.privateName] = dataset[prop.name] || prop.default; } }); } @@ -88,8 +196,8 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * @extends DecoratorNode * @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode */ - static clone(node) { - return new this(node.getDataset(), node.__key); + static clone(node: GeneratedDecoratorNodeInstance, TOutput>) { + return new this(node.getDataset() as Partial>, node.__key); } /** @@ -97,10 +205,10 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * to detect when a property has been changed */ static getPropertyDefaults() { - return properties.reduce((obj, prop) => { + return internalProps.reduce((obj: Record, prop) => { obj[prop.name] = prop.default; return obj; - }, {}); + }, {}) as DecoratorNodeValueMap; } /** @@ -109,9 +217,9 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * @see https://github.com/TryGhost/SDK/tree/main/packages/url-utils */ static get urlTransformMap() { - let map = {}; + const map: Record = {}; - properties.forEach((prop) => { + internalProps.forEach((prop) => { if (prop.urlType) { if (prop.urlPath) { map[prop.urlPath] = prop.urlType; @@ -131,8 +239,8 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF getDataset() { const self = this.getLatest(); - let dataset = {}; - properties.forEach((prop) => { + const dataset: Record = {}; + internalProps.forEach((prop) => { dataset[prop.name] = self[prop.privateName]; }); @@ -145,17 +253,17 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * @extends DecoratorNode * @param {Object} serializedNode - Lexical's representation of the node, in JSON format */ - static importJSON(serializedNode) { - const data = {}; + static importJSON(serializedNode: Record) { + const data: Record = {}; // migrate older nodes that were saved with an earlier version of the visibility format - serializedNode.visibility = migrateOldVisibilityFormat(serializedNode.visibility); + serializedNode.visibility = migrateOldVisibilityFormat(serializedNode.visibility as Visibility); - properties.forEach((prop) => { + internalProps.forEach((prop) => { data[prop.name] = serializedNode[prop.name]; }); - return new this(data); + return new this(data as Partial>); } /** @@ -163,11 +271,12 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * @extends DecoratorNode * @see https://lexical.dev/docs/concepts/serialization#lexicalnodeexportjson */ + // @ts-expect-error -- strict mode migration exportJSON() { - const dataset = { + const dataset: Record = { type: nodeType, version: version, - ...properties.reduce((obj, prop) => { + ...internalProps.reduce((obj: Record, prop) => { obj[prop.name] = this[prop.name]; return obj; }, {}) @@ -175,27 +284,29 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF return dataset; } - exportDOM(options = {}) { + // @ts-expect-error - custom exportDOM signature for Ghost rendering + exportDOM(options: ExportDOMOptions = {}): TOutput { // this.__version is used when a node has a version property which // means it's set from the serialized version data at runtime const nodeVersion = this.__version || version; - if (options.nodeRenderers?.[nodeType]) { - const render = options.nodeRenderers[nodeType]; + const nodeRenderers = options.nodeRenderers as Record | VersionedRenderFn> | undefined; + if (nodeRenderers?.[nodeType]) { + const render = nodeRenderers[nodeType]; if (typeof render === 'object') { - const versionRenderer = render[nodeVersion]; + const versionRenderer = (render as VersionedRenderFn)[nodeVersion as number]; if (!versionRenderer) { throw new Error(`[generateDecoratorNode] ${nodeType}: options.nodeRenderers['${nodeType}'] for version ${nodeVersion} is required`); } return versionRenderer(this, options); } else { - return render(this, options); + return (render as RenderFn)(this, options); } } if (typeof defaultRenderFn === 'object') { - const render = defaultRenderFn[nodeVersion]; + const render = (defaultRenderFn as VersionedRenderFn)[nodeVersion as number]; if (!render) { throw new Error(`[generateDecoratorNode] ${nodeType}: "defaultRenderFn" for version ${nodeVersion} is required`); } @@ -206,7 +317,7 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF throw new Error(`[generateDecoratorNode] ${nodeType}: "defaultRenderFn" is required`); } - const render = defaultRenderFn; + const render = defaultRenderFn as RenderFn; return render(this, options); } @@ -276,14 +387,14 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * @returns {boolean} */ getIsVisibilityActive() { - if (!properties.some(prop => prop.name === 'visibility')) { + if (!internalProps.some(prop => prop.name === 'visibility')) { return false; } const self = this.getLatest(); const visibility = self.__visibility; - return isVisibilityRestricted(visibility); + return isVisibilityRestricted(visibility as Visibility); } } @@ -305,7 +416,7 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF * * They can be used as `node.content` (getter) and `node.content = 'new value'` (setter) */ - properties.forEach((prop) => { + internalProps.forEach((prop) => { Object.defineProperty(GeneratedDecoratorNode.prototype, prop.name, { get: function () { const self = this.getLatest(); @@ -318,5 +429,5 @@ export function generateDecoratorNode({nodeType, properties = [], defaultRenderF }); }); - return GeneratedDecoratorNode; + return GeneratedDecoratorNode as unknown as GeneratedDecoratorNodeClass, TOutput>; } diff --git a/packages/kg-default-nodes/src/index.ts b/packages/kg-default-nodes/src/index.ts new file mode 100644 index 0000000000..1c910a46ae --- /dev/null +++ b/packages/kg-default-nodes/src/index.ts @@ -0,0 +1 @@ +export * from './kg-default-nodes.js'; diff --git a/packages/kg-default-nodes/src/kg-default-nodes.ts b/packages/kg-default-nodes/src/kg-default-nodes.ts index b503a98310..56e393566c 100644 --- a/packages/kg-default-nodes/src/kg-default-nodes.ts +++ b/packages/kg-default-nodes/src/kg-default-nodes.ts @@ -1,71 +1,73 @@ -import * as image from './nodes/image/ImageNode'; -import * as codeblock from './nodes/codeblock/CodeBlockNode'; -import * as markdown from './nodes/markdown/MarkdownNode'; -import * as video from './nodes/video/VideoNode'; -import * as audio from './nodes/audio/AudioNode'; -import * as callout from './nodes/callout/CalloutNode'; -import * as callToAction from './nodes/call-to-action/CallToActionNode'; -import * as aside from './nodes/aside/AsideNode'; -import * as horizontalrule from './nodes/horizontalrule/HorizontalRuleNode'; -import * as html from './nodes/html/HtmlNode'; -import * as toggle from './nodes/toggle/ToggleNode'; -import * as button from './nodes/button/ButtonNode'; -import * as bookmark from './nodes/bookmark/BookmarkNode'; -import * as file from './nodes/file/FileNode'; -import * as header from './nodes/header/HeaderNode'; -import * as paywall from './nodes/paywall/PaywallNode'; -import * as product from './nodes/product/ProductNode'; -import * as embed from './nodes/embed/EmbedNode'; -import * as email from './nodes/email/EmailNode'; -import * as gallery from './nodes/gallery/GalleryNode'; -import * as emailCta from './nodes/email-cta/EmailCtaNode'; -import * as signup from './nodes/signup/SignupNode'; -import * as transistor from './nodes/transistor/TransistorNode'; -import * as textnode from './nodes/ExtendedTextNode'; -import * as headingnode from './nodes/ExtendedHeadingNode'; -import * as quotenode from './nodes/ExtendedQuoteNode'; -import * as tk from './nodes/TKNode'; +export {GeneratedDecoratorNodeBase} from './generate-decorator-node.js'; +export * from './export-dom.js'; +import * as image from './nodes/image/ImageNode.js'; +import * as codeblock from './nodes/codeblock/CodeBlockNode.js'; +import * as markdown from './nodes/markdown/MarkdownNode.js'; +import * as video from './nodes/video/VideoNode.js'; +import * as audio from './nodes/audio/AudioNode.js'; +import * as callout from './nodes/callout/CalloutNode.js'; +import * as callToAction from './nodes/call-to-action/CallToActionNode.js'; +import * as aside from './nodes/aside/AsideNode.js'; +import * as horizontalrule from './nodes/horizontalrule/HorizontalRuleNode.js'; +import * as html from './nodes/html/HtmlNode.js'; +import * as toggle from './nodes/toggle/ToggleNode.js'; +import * as button from './nodes/button/ButtonNode.js'; +import * as bookmark from './nodes/bookmark/BookmarkNode.js'; +import * as file from './nodes/file/FileNode.js'; +import * as header from './nodes/header/HeaderNode.js'; +import * as paywall from './nodes/paywall/PaywallNode.js'; +import * as product from './nodes/product/ProductNode.js'; +import * as embed from './nodes/embed/EmbedNode.js'; +import * as email from './nodes/email/EmailNode.js'; +import * as gallery from './nodes/gallery/GalleryNode.js'; +import * as emailCta from './nodes/email-cta/EmailCtaNode.js'; +import * as signup from './nodes/signup/SignupNode.js'; +import * as transistor from './nodes/transistor/TransistorNode.js'; +import * as textnode from './nodes/ExtendedTextNode.js'; +import * as headingnode from './nodes/ExtendedHeadingNode.js'; +import * as quotenode from './nodes/ExtendedQuoteNode.js'; +import * as tk from './nodes/TKNode.js'; import * as atLink from './nodes/at-link/index.js'; import * as zwnj from './nodes/zwnj/ZWNJNode.js'; -import linebreakSerializers from './serializers/linebreak'; -import paragraphSerializers from './serializers/paragraph'; +import linebreakSerializers from './serializers/linebreak.js'; +import paragraphSerializers from './serializers/paragraph.js'; // re-export everything for easier importing -export * from './KoenigDecoratorNode'; -export * from './nodes/image/ImageNode'; -export * from './nodes/codeblock/CodeBlockNode'; -export * from './nodes/markdown/MarkdownNode'; -export * from './nodes/video/VideoNode'; -export * from './nodes/audio/AudioNode'; -export * from './nodes/callout/CalloutNode'; -export * from './nodes/aside/AsideNode'; -export * from './nodes/horizontalrule/HorizontalRuleNode'; -export * from './nodes/html/HtmlNode'; -export * from './nodes/toggle/ToggleNode'; -export * from './nodes/button/ButtonNode'; -export * from './nodes/bookmark/BookmarkNode'; -export * from './nodes/file/FileNode'; -export * from './nodes/header/HeaderNode'; -export * from './nodes/paywall/PaywallNode'; -export * from './nodes/product/ProductNode'; -export * from './nodes/embed/EmbedNode'; -export * from './nodes/email/EmailNode'; -export * from './nodes/gallery/GalleryNode'; -export * from './nodes/email-cta/EmailCtaNode'; -export * from './nodes/signup/SignupNode'; -export * from './nodes/transistor/TransistorNode'; -export * from './nodes/call-to-action/CallToActionNode'; -export * from './nodes/ExtendedTextNode'; -export * from './nodes/ExtendedHeadingNode'; -export * from './nodes/ExtendedQuoteNode'; -export * from './nodes/TKNode'; +export * from './KoenigDecoratorNode.js'; +export * from './nodes/image/ImageNode.js'; +export * from './nodes/codeblock/CodeBlockNode.js'; +export * from './nodes/markdown/MarkdownNode.js'; +export * from './nodes/video/VideoNode.js'; +export * from './nodes/audio/AudioNode.js'; +export * from './nodes/callout/CalloutNode.js'; +export * from './nodes/aside/AsideNode.js'; +export * from './nodes/horizontalrule/HorizontalRuleNode.js'; +export * from './nodes/html/HtmlNode.js'; +export * from './nodes/toggle/ToggleNode.js'; +export * from './nodes/button/ButtonNode.js'; +export * from './nodes/bookmark/BookmarkNode.js'; +export * from './nodes/file/FileNode.js'; +export * from './nodes/header/HeaderNode.js'; +export * from './nodes/paywall/PaywallNode.js'; +export * from './nodes/product/ProductNode.js'; +export * from './nodes/embed/EmbedNode.js'; +export * from './nodes/email/EmailNode.js'; +export * from './nodes/gallery/GalleryNode.js'; +export * from './nodes/email-cta/EmailCtaNode.js'; +export * from './nodes/signup/SignupNode.js'; +export * from './nodes/transistor/TransistorNode.js'; +export * from './nodes/call-to-action/CallToActionNode.js'; +export * from './nodes/ExtendedTextNode.js'; +export * from './nodes/ExtendedHeadingNode.js'; +export * from './nodes/ExtendedQuoteNode.js'; +export * from './nodes/TKNode.js'; export * from './nodes/at-link/index.js'; -export * from './nodes/zwnj/ZWNJNode'; +export * from './nodes/zwnj/ZWNJNode.js'; // export utility functions that are useful in other packages or tests -import * as visibilityUtils from './utils/visibility'; -import * as taggedTemplateFns from './utils/tagged-template-fns.mjs'; +import * as visibilityUtils from './utils/visibility.js'; +import * as taggedTemplateFns from './utils/tagged-template-fns.js'; import {generateDecoratorNode} from './generate-decorator-node.js'; import {rgbToHex} from './utils/rgb-to-hex.js'; export const utils = { diff --git a/packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts b/packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts index 63b3883d50..9dbe10ca3d 100644 --- a/packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts +++ b/packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts @@ -1,4 +1,6 @@ import {HeadingNode} from '@lexical/rich-text'; +import type {HeadingTagType, SerializedHeadingNode} from '@lexical/rich-text'; +import type {DOMConversion} 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 @@ -8,10 +10,10 @@ 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)}; export class ExtendedHeadingNode extends HeadingNode { - constructor(tag, key) { + constructor(tag: HeadingTagType, key?: string) { super(tag, key); } @@ -19,7 +21,7 @@ export class ExtendedHeadingNode extends HeadingNode { return 'extended-heading'; } - static clone(node) { + static clone(node: ExtendedHeadingNode) { return new ExtendedHeadingNode(node.__tag, node.__key); } @@ -31,8 +33,12 @@ export class ExtendedHeadingNode extends HeadingNode { }; } - static importJSON(serializedNode) { - return HeadingNode.importJSON(serializedNode); + static importJSON(serializedNode: SerializedHeadingNode): ExtendedHeadingNode { + const node = new ExtendedHeadingNode(serializedNode.tag); + node.setFormat(serializedNode.format); + node.setIndent(serializedNode.indent); + node.setDirection(serializedNode.direction); + return node; } exportJSON() { @@ -42,8 +48,10 @@ export class ExtendedHeadingNode extends HeadingNode { } } -function patchParagraphConversion(originalDOMConverter) { - return (node) => { +type DOMConverterFn = ((node: HTMLElement) => DOMConversion | null) | undefined; + +function patchParagraphConversion(originalDOMConverter: DOMConverterFn) { + 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); @@ -64,10 +72,10 @@ function patchParagraphConversion(originalDOMConverter) { return { conversion: () => { return { - node: new ExtendedHeadingNode(`h${level}`) + node: new ExtendedHeadingNode(`h${level}` as HeadingTagType) }; }, - priority: 1 + priority: 1 as const }; } } diff --git a/packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts b/packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts index 7d2f14be90..c61869d13f 100644 --- a/packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts +++ b/packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts @@ -1,5 +1,7 @@ import {QuoteNode} from '@lexical/rich-text'; +import type {SerializedQuoteNode} from '@lexical/rich-text'; import {$createLineBreakNode, $isParagraphNode} from 'lexical'; +import type {LexicalNode} 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 @@ -12,7 +14,7 @@ import {$createLineBreakNode, $isParagraphNode} from 'lexical'; export const extendedQuoteNodeReplacement = {replace: QuoteNode, with: () => new ExtendedQuoteNode()}; export class ExtendedQuoteNode extends QuoteNode { - constructor(key) { + constructor(key?: string) { super(key); } @@ -20,7 +22,7 @@ export class ExtendedQuoteNode extends QuoteNode { return 'extended-quote'; } - static clone(node) { + static clone(node: ExtendedQuoteNode) { return new ExtendedQuoteNode(node.__key); } @@ -32,7 +34,7 @@ export class ExtendedQuoteNode extends QuoteNode { }; } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedQuoteNode) { return QuoteNode.importJSON(serializedNode); } @@ -55,14 +57,14 @@ function convertBlockquoteElement() { const node = new ExtendedQuoteNode(); return { node, - after: (childNodes) => { + after: (childNodes: LexicalNode[]) => { // Blockquotes can have nested paragraphs. In our original mobiledoc // 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) => { + childNodes.forEach((child: LexicalNode) => { if ($isParagraphNode(child)) { if (newChildNodes.length > 0) { newChildNodes.push($createLineBreakNode()); @@ -79,6 +81,6 @@ function convertBlockquoteElement() { } }; }, - priority: 1 + priority: 1 as const }; } diff --git a/packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts b/packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts index c2144dc0ee..85b5ab95a3 100644 --- a/packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts +++ b/packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts @@ -1,4 +1,5 @@ import {$isTextNode, TextNode} from 'lexical'; +import type {DOMConversion, DOMConversionOutput, LexicalNode, SerializedTextNode} 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 @@ -7,10 +8,10 @@ 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?: string) { super(text, key); } @@ -18,7 +19,7 @@ export class ExtendedTextNode extends TextNode { return 'extended-text'; } - static clone(node) { + static clone(node: ExtendedTextNode) { return new ExtendedTextNode(node.__text, node.__key); } @@ -28,13 +29,18 @@ export class ExtendedTextNode extends TextNode { ...importers, span: () => ({ conversion: patchConversion(importers?.span, convertSpanElement), - priority: 1 + priority: 1 as const }) }; } - static importJSON(serializedNode) { - return TextNode.importJSON(serializedNode); + static importJSON(serializedNode: SerializedTextNode): ExtendedTextNode { + const node = new ExtendedTextNode(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; } exportJSON() { @@ -43,7 +49,7 @@ export class ExtendedTextNode extends TextNode { return json; } - isSimpleText() { + isSimpleText(): boolean { return ( (this.__type === 'text' || this.__type === 'extended-text') && this.__mode === 0 @@ -55,13 +61,15 @@ export class ExtendedTextNode extends TextNode { } } -function patchConversion(originalDOMConverter, convertFn) { - return (node) => { +type DOMConverterFn = ((node: HTMLElement) => DOMConversion | null) | undefined; + +function patchConversion(originalDOMConverter: DOMConverterFn, convertFn: (lexicalNode: TextNode, domNode: HTMLElement) => TextNode) { + return (node: HTMLElement) => { const original = originalDOMConverter?.(node); if (!original) { return null; } - const originalOutput = original.conversion(node); + const originalOutput = original.conversion(node) as DOMConversionOutput; if (!originalOutput) { return originalOutput; @@ -69,8 +77,8 @@ function patchConversion(originalDOMConverter, convertFn) { return { ...originalOutput, - forChild: (lexicalNode, parent) => { - const originalForChild = originalOutput?.forChild ?? (x => x); + forChild: (lexicalNode: LexicalNode, parent: LexicalNode | null | undefined) => { + const originalForChild = originalOutput?.forChild ?? ((x: LexicalNode) => x); const result = originalForChild(lexicalNode, parent); if ($isTextNode(result)) { return convertFn(result, node); @@ -81,7 +89,7 @@ function patchConversion(originalDOMConverter, convertFn) { }; } -function convertSpanElement(lexicalNode, domNode) { +function convertSpanElement(lexicalNode: TextNode, domNode: HTMLElement) { const span = domNode; // Word uses span tags + font-weight for bold text diff --git a/packages/kg-default-nodes/src/nodes/TKNode.ts b/packages/kg-default-nodes/src/nodes/TKNode.ts index 27f7a841e3..3ce9f39db4 100644 --- a/packages/kg-default-nodes/src/nodes/TKNode.ts +++ b/packages/kg-default-nodes/src/nodes/TKNode.ts @@ -1,31 +1,32 @@ import {$applyNodeReplacement, TextNode} from 'lexical'; +import type {EditorConfig, SerializedTextNode, TextModeType} from 'lexical'; export class TKNode extends TextNode { static getType() { return 'tk'; } - static clone(node) { + static clone(node: TKNode) { return new TKNode(node.__text, node.__key); } - constructor(text, key) { + constructor(text: string, key?: string) { 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) { - const node = $createTKNode(serializedNode.text); - node.setFormat(serializedNode.format); - node.setDetail(serializedNode.detail); - node.setMode(serializedNode.mode); + static importJSON(serializedNode: SerializedTextNode): TKNode { + const node = new TKNode(serializedNode.text); + node.setFormat(serializedNode.format as number); + node.setDetail(serializedNode.detail as number); + node.setMode(serializedNode.mode as TextModeType); node.setStyle(serializedNode.style); return node; } @@ -51,7 +52,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) { return $applyNodeReplacement(new TKNode(text)); } @@ -60,6 +61,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: unknown): node is TKNode { return node instanceof TKNode; } diff --git a/packages/kg-default-nodes/src/nodes/aside/AsideNode.ts b/packages/kg-default-nodes/src/nodes/aside/AsideNode.ts index 7f65528319..94ec517be9 100644 --- a/packages/kg-default-nodes/src/nodes/aside/AsideNode.ts +++ b/packages/kg-default-nodes/src/nodes/aside/AsideNode.ts @@ -1,12 +1,13 @@ import {ElementNode} from 'lexical'; -import {AsideParser} from './AsideParser'; +import type {EditorConfig, LexicalEditor, SerializedElementNode} from 'lexical'; +import {AsideParser} from './AsideParser.js'; export class AsideNode extends ElementNode { static getType() { return 'aside'; } - static clone(node) { + static clone(node: AsideNode) { return new this( node.__key ); @@ -16,11 +17,11 @@ export class AsideNode extends ElementNode { return {}; } - constructor(key) { + constructor(key?: string) { super(key); } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedElementNode) { const node = new this(); node.setFormat(serializedNode.format); node.setIndent(serializedNode.indent); @@ -43,7 +44,7 @@ export class AsideNode extends ElementNode { } /* c8 ignore start */ - createDOM() { + createDOM(_config?: EditorConfig, _editor?: LexicalEditor): HTMLElement { return document.createElement('div'); } @@ -65,6 +66,6 @@ export function $createAsideNode() { return new AsideNode(); } -export function $isAsideNode(node) { +export function $isAsideNode(node: unknown): node is AsideNode { return node instanceof AsideNode; } diff --git a/packages/kg-default-nodes/src/nodes/aside/AsideParser.ts b/packages/kg-default-nodes/src/nodes/aside/AsideParser.ts index 662cf5234d..c1218ef6e3 100644 --- a/packages/kg-default-nodes/src/nodes/aside/AsideParser.ts +++ b/packages/kg-default-nodes/src/nodes/aside/AsideParser.ts @@ -1,23 +1,25 @@ +import type {LexicalNode} from 'lexical'; + export class AsideParser { - constructor(NodeClass) { + NodeClass: {new (): LexicalNode}; + + constructor(NodeClass: {new (): LexicalNode}) { this.NodeClass = NodeClass; } get DOMConversionMap() { - const self = this; - return { blockquote: () => ({ - conversion(domNode) { + conversion: (domNode: HTMLElement) => { const isBigQuote = domNode.classList?.contains('kg-blockquote-alt'); if (domNode.tagName === 'BLOCKQUOTE' && isBigQuote) { - const node = new self.NodeClass(); + const node = new this.NodeClass(); return {node}; } return null; }, - priority: 0 + priority: 0 as const }) }; } diff --git a/packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts b/packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts index 0b98d9d024..44c21b681c 100644 --- a/packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts +++ b/packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts @@ -1,30 +1,31 @@ import {$applyNodeReplacement, ElementNode} from 'lexical'; -import linkSVG from './kg-link.svg'; +import type {EditorConfig} from 'lexical'; +const linkSVG = ' '; // Container element for a link search query. Temporary node used only inside // the editor that will be replaced with a LinkNode when the search is complete. export class AtLinkNode extends ElementNode { // We keep track of the format that was applied to the original '@' character // so we can re-apply that when converting to a LinkNode - __linkFormat = null; + __linkFormat: number | null = null; static getType() { return 'at-link'; } - constructor(linkFormat, key) { + constructor(linkFormat: number | null, key?: string) { super(key); this.__linkFormat = linkFormat; } - static clone(node) { + static clone(node: AtLinkNode) { return new AtLinkNode(node.__linkFormat, node.__key); } // This is a temporary node, it should never be serialized but we need // to implement just in case and to match expected types. The AtLinkPlugin // should take care of replacing this node with it's children when needed. - static importJSON({linkFormat}) { - return $createAtLinkNode(linkFormat); + static importJSON(serializedNode: ReturnType) { + return $createAtLinkNode(serializedNode.linkFormat); } exportJSON() { @@ -36,7 +37,7 @@ export class AtLinkNode extends ElementNode { }; } - createDOM(config) { + createDOM(config: EditorConfig) { const span = document.createElement('span'); const atLinkClasses = (config.theme.atLink || '').split(' ').filter(Boolean); const atLinkIconClasses = (config.theme.atLinkIcon || '').split(' ').filter(Boolean); @@ -55,9 +56,12 @@ export class AtLinkNode extends ElementNode { return false; } - // should not render anything - this is a placeholder node + // This is an editor-only placeholder node. Return an empty element and + // `type: 'inner'` so downstream serializers emit no HTML while still + // receiving a non-null DOM element. exportDOM() { - return null; + const span = document.createElement('span'); + return {element: span, type: 'inner' as const}; } /* c8 ignore next 3 */ @@ -77,7 +81,7 @@ export class AtLinkNode extends ElementNode { return false; } - setLinkFormat(linkFormat) { + setLinkFormat(linkFormat: number | null) { const self = this.getWritable(); self.__linkFormat = linkFormat; } @@ -88,10 +92,10 @@ export class AtLinkNode extends ElementNode { } } -export function $createAtLinkNode(linkFormat) { +export function $createAtLinkNode(linkFormat: number | null = null): AtLinkNode { return $applyNodeReplacement(new AtLinkNode(linkFormat)); } -export function $isAtLinkNode(node) { +export function $isAtLinkNode(node: unknown): node is AtLinkNode { return node instanceof AtLinkNode; } diff --git a/packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts b/packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts index e7f93c1a67..2d7b6ad5d6 100644 --- a/packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts +++ b/packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts @@ -1,9 +1,10 @@ import {$applyNodeReplacement, TextNode} from 'lexical'; +import type {EditorConfig} from 'lexical'; // Represents the search query string inside an AtLinkNode. Used in place of a // regular TextNode to allow for :after styling to be applied to work as a placeholder export class AtLinkSearchNode extends TextNode { - __placeholder = null; + __placeholder: string | null = null; defaultPlaceholder = 'Find a post, tag or author'; @@ -11,12 +12,12 @@ export class AtLinkSearchNode extends TextNode { return 'at-link-search'; } - constructor(text, placeholder, key) { + constructor(text: string, placeholder: string | null, key?: string) { super(text, key); this.__placeholder = placeholder; } - static clone(node) { + static clone(node: AtLinkSearchNode) { return new AtLinkSearchNode( node.__text, node.__placeholder, @@ -27,8 +28,8 @@ export class AtLinkSearchNode extends TextNode { // This is a temporary node, it should never be serialized but we need // to implement just in case and to match expected types. The AtLinkPlugin // should take care of replacing this node when needed. - static importJSON({text, placeholder}) { - return $createAtLinkSearchNode(text, placeholder); + static importJSON(serializedNode: ReturnType) { + return $createAtLinkSearchNode(serializedNode.text, serializedNode.placeholder); } exportJSON() { @@ -40,7 +41,7 @@ export class AtLinkSearchNode extends TextNode { }; } - createDOM(config) { + createDOM(config: EditorConfig) { const span = super.createDOM(config); span.dataset.placeholder = ''; if (!this.__text) { @@ -54,17 +55,22 @@ export class AtLinkSearchNode extends TextNode { return span; } - updateDOM(prevNode, dom) { - if (this.__text) { - dom.dataset.placeholder = this.__placeholder ?? ''; - } + updateDOM(prevNode: AtLinkSearchNode, dom: HTMLElement, config: EditorConfig) { + dom.dataset.placeholder = this.__placeholder !== null + ? this.__placeholder + : this.__text + ? '' + : this.defaultPlaceholder; - return super.updateDOM(...arguments); + return super.updateDOM(prevNode, dom, config); } - // should not render anything - this is a placeholder node + // This is an editor-only placeholder node. Return an empty element and + // `type: 'inner'` so downstream serializers emit no HTML while still + // receiving a non-null DOM element. exportDOM() { - return null; + const span = document.createElement('span'); + return {element: span, type: 'inner' as const}; } /* c8 ignore next 3 */ @@ -76,7 +82,7 @@ export class AtLinkSearchNode extends TextNode { return false; } - setPlaceholder(text) { + setPlaceholder(text: string | null) { const self = this.getWritable(); self.__placeholder = text; } @@ -99,10 +105,10 @@ export class AtLinkSearchNode extends TextNode { } } -export function $createAtLinkSearchNode(text = '', placeholder = null) { +export function $createAtLinkSearchNode(text = '', placeholder: string | null = null): AtLinkSearchNode { return $applyNodeReplacement(new AtLinkSearchNode(text, placeholder)); } -export function $isAtLinkSearchNode(node) { +export function $isAtLinkSearchNode(node: unknown): node is AtLinkSearchNode { return node instanceof AtLinkSearchNode; } diff --git a/packages/kg-default-nodes/src/nodes/at-link/index.ts b/packages/kg-default-nodes/src/nodes/at-link/index.ts index e369eae5c5..9ae2921d68 100644 --- a/packages/kg-default-nodes/src/nodes/at-link/index.ts +++ b/packages/kg-default-nodes/src/nodes/at-link/index.ts @@ -1,4 +1,4 @@ /* c8 ignore start */ -export * from './AtLinkNode'; -export * from './AtLinkSearchNode'; +export * from './AtLinkNode.js'; +export * from './AtLinkSearchNode.js'; /* c8 ignore end */ diff --git a/packages/kg-default-nodes/src/nodes/audio/AudioNode.ts b/packages/kg-default-nodes/src/nodes/audio/AudioNode.ts index 94c6cd2a0a..f8a0f2fe69 100644 --- a/packages/kg-default-nodes/src/nodes/audio/AudioNode.ts +++ b/packages/kg-default-nodes/src/nodes/audio/AudioNode.ts @@ -1,16 +1,22 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseAudioNode} from './audio-parser'; -import {renderAudioNode} from './audio-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {parseAudioNode} from './audio-parser.js'; +import {renderAudioNode} from './audio-renderer.js'; + +const audioProperties = [ + {name: 'duration', default: 0}, + {name: 'mimeType', default: ''}, + {name: 'src', default: '', urlType: 'url'}, + {name: 'title', default: ''}, + {name: 'thumbnailSrc', default: ''} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type AudioData = DecoratorNodeData; + +export interface AudioNode extends DecoratorNodeValueMap {} 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: ''} - ], + properties: audioProperties, defaultRenderFn: renderAudioNode }) { static importDOM() { @@ -18,10 +24,10 @@ export class AudioNode extends generateDecoratorNode({ } } -export const $createAudioNode = (dataset) => { +export const $createAudioNode = (dataset: AudioData = {}) => { return new AudioNode(dataset); }; -export function $isAudioNode(node) { +export function $isAudioNode(node: unknown): node is AudioNode { return node instanceof AudioNode; } diff --git a/packages/kg-default-nodes/src/nodes/audio/audio-parser.ts b/packages/kg-default-nodes/src/nodes/audio/audio-parser.ts index 2ffcf27d22..8c0134eb64 100644 --- a/packages/kg-default-nodes/src/nodes/audio/audio-parser.ts +++ b/packages/kg-default-nodes/src/nodes/audio/audio-parser.ts @@ -1,19 +1,21 @@ -export function parseAudioNode(AudioNode) { +import type {LexicalNode} from 'lexical'; + +export function parseAudioNode(AudioNode: new (data: Record) => LexicalNode) { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { const isKgAudioCard = nodeElem.classList?.contains('kg-audio-card'); if (nodeElem.tagName === 'DIV' && isKgAudioCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { 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: Record = { src: audioSrc, title: title }; @@ -22,18 +24,19 @@ export function parseAudioNode(AudioNode) { } if (durationText) { - const [minutes, seconds = 0] = durationText.split(':'); - try { - payload.duration = parseInt(minutes) * 60 + parseInt(seconds); - } catch (e) { - // ignore duration + const [rawMinutes, rawSeconds = '0'] = durationText.split(':'); + const minutes = Number(rawMinutes.trim()); + const seconds = Number(rawSeconds.trim()); + + if (Number.isInteger(minutes) && Number.isInteger(seconds)) { + payload.duration = minutes * 60 + seconds; } } const node = new AudioNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; diff --git a/packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts b/packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts index d7b163c4c4..9a876ef880 100644 --- a/packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts @@ -1,9 +1,36 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions, ExportDOMOutput} from '../../export-dom.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; + +interface AudioNodeData { + src: string; + title: string; + thumbnailSrc: string; + duration: number; +} + +interface RenderOptions extends ExportDOMOptions {} + +interface EmailAudioRenderOptions extends RenderOptions { + target: 'email'; + postUrl: string; +} + +interface DefaultAudioRenderOptions extends RenderOptions { + target?: string; + postUrl?: string; +} + +type AudioRenderOptions = EmailAudioRenderOptions | DefaultAudioRenderOptions; -export function renderAudioNode(node, options = {}) { +export type AudioExportDOMOutput = + | ExportDOMOutput + | ExportDOMOutput + | ExportDOMOutput; + +export function renderAudioNode(node: AudioNodeData, options: AudioRenderOptions = {}): AudioExportDOMOutput { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); if (!node.src || node.src.trim() === '') { return renderEmptyContainer(document); @@ -13,13 +40,21 @@ export function renderAudioNode(node, options = {}) { const emptyThumbnailCls = getEmptyThumbnailCls(node); if (options.target === 'email') { + if (!isEmailRenderOptions(options)) { + throw new Error('renderAudioNode requires options.postUrl when options.target is "email"'); + } + return emailTemplate(node, document, options, thumbnailCls, emptyThumbnailCls); } else { return frontendTemplate(node, document, thumbnailCls, emptyThumbnailCls); } } -function frontendTemplate(node, document, thumbnailCls, emptyThumbnailCls) { +function isEmailRenderOptions(options: AudioRenderOptions): options is EmailAudioRenderOptions { + return options.target === 'email' && typeof options.postUrl === 'string' && options.postUrl.trim() !== ''; +} + +function frontendTemplate(node: AudioNodeData, document: Document, thumbnailCls: string, emptyThumbnailCls: string) { const element = document.createElement('div'); element.setAttribute('class', 'kg-card kg-audio-card'); const img = document.createElement('img'); @@ -113,7 +148,7 @@ function frontendTemplate(node, document, thumbnailCls, emptyThumbnailCls) { audioDurationTotal.textContent = '/'; const audioDUrationNode = document.createElement('span'); audioDUrationNode.setAttribute('class', 'kg-audio-duration'); - audioDUrationNode.textContent = node.duration; + audioDUrationNode.textContent = String(node.duration); audioDurationTotal.appendChild(audioDUrationNode); audioPlayer.appendChild(audioDurationTotal); @@ -163,10 +198,10 @@ function frontendTemplate(node, document, thumbnailCls, emptyThumbnailCls) { audioPlayerContainer.appendChild(audioPlayer); element.appendChild(audioPlayerContainer); - return {element}; + return {element, type: 'outer' as const}; } -function emailTemplate(node, document, options, thumbnailCls, emptyThumbnailCls) { +function emailTemplate(node: AudioNodeData, document: Document, options: EmailAudioRenderOptions, thumbnailCls: string, emptyThumbnailCls: string) { const html = (` @@ -215,11 +250,10 @@ function emailTemplate(node, document, options, thumbnailCls, emptyThumbnailCls) const container = document.createElement('div'); container.innerHTML = html.trim(); - - return {element: container.firstElementChild}; + return {element: container.firstElementChild as HTMLTableElement, type: 'outer' as const}; } -function getThumbnailCls(node) { +function getThumbnailCls(node: AudioNodeData) { let thumbnailCls = 'kg-audio-thumbnail'; if (!node.thumbnailSrc) { @@ -229,7 +263,7 @@ function getThumbnailCls(node) { return thumbnailCls; } -function getEmptyThumbnailCls(node) { +function getEmptyThumbnailCls(node: AudioNodeData) { let emptyThumbnailCls = 'kg-audio-thumbnail placeholder'; if (node.thumbnailSrc) { @@ -245,4 +279,4 @@ function getFormattedDuration(duration = 200) { const paddedSeconds = String(seconds).padStart(2, '0'); const formattedDuration = `${minutes}:${paddedSeconds}`; return formattedDuration; -} \ No newline at end of file +} diff --git a/packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts b/packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts index 122aed6a09..d5b3912e9f 100644 --- a/packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts +++ b/packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts @@ -1,19 +1,47 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseBookmarkNode} from './bookmark-parser'; -import {renderBookmarkNode} from './bookmark-renderer'; +import {generateDecoratorNode, type DecoratorNodeProperty} from '../../generate-decorator-node.js'; +import {parseBookmarkNode} from './bookmark-parser.js'; +import {renderBookmarkNode} from './bookmark-renderer.js'; + +interface BookmarkMetadata { + icon?: string; + title?: string; + description?: string; + author?: string; + publisher?: string; + thumbnail?: string; +} + +export interface BookmarkData { + url?: string; + metadata?: BookmarkMetadata; + caption?: string; +} + +export interface BookmarkNode { + title: string; + description: string; + url: string; + caption: string; + author: string; + publisher: string; + icon: string; + thumbnail: string; +} + +const bookmarkProperties = [ + {name: 'title', default: '', wordCount: true}, + {name: 'description', default: '', wordCount: true}, + {name: 'url', default: '', urlType: 'url', wordCount: true}, + {name: 'caption', default: '', wordCount: true}, + {name: 'author', default: ''}, + {name: 'publisher', default: ''}, + {name: 'icon', urlPath: 'metadata.icon', default: '', urlType: 'url'}, + {name: 'thumbnail', urlPath: 'metadata.thumbnail', default: '', urlType: 'url'} +] as const satisfies readonly DecoratorNodeProperty[]; export class BookmarkNode extends generateDecoratorNode({ nodeType: 'bookmark', - properties: [ - {name: 'title', default: '', wordCount: true}, - {name: 'description', default: '', wordCount: true}, - {name: 'url', default: '', urlType: 'url', wordCount: true}, - {name: 'caption', default: '', wordCount: true}, - {name: 'author', default: ''}, - {name: 'publisher', default: ''}, - {name: 'icon', urlPath: 'metadata.icon', default: '', urlType: 'url'}, - {name: 'thumbnail', urlPath: 'metadata.thumbnail', default: '', urlType: 'url'} - ], + properties: bookmarkProperties, defaultRenderFn: renderBookmarkNode }) { static importDOM() { @@ -21,8 +49,8 @@ export class BookmarkNode extends generateDecoratorNode({ } /* override */ - constructor({url, metadata, caption} = {}, key) { - super(key); + constructor({url, metadata, caption}: BookmarkData = {}, key?: string) { + super({}, key); this.__url = url || ''; this.__icon = metadata?.icon || ''; this.__title = metadata?.title || ''; @@ -34,25 +62,25 @@ export class BookmarkNode extends generateDecoratorNode({ } /* @override */ - getDataset() { + getDataset(): Record { const self = this.getLatest(); return { - url: self.__url, + url: self.__url as string, metadata: { - icon: self.__icon, - title: self.__title, - description: self.__description, - author: self.__author, - publisher: self.__publisher, - thumbnail: self.__thumbnail + icon: self.__icon as string, + title: self.__title as string, + description: self.__description as string, + author: self.__author as string, + publisher: self.__publisher as string, + thumbnail: self.__thumbnail as string }, - caption: self.__caption + caption: self.__caption as string }; } /* @override */ - static importJSON(serializedNode) { - const {url, metadata, caption} = serializedNode; + static importJSON(serializedNode: Record) { + const {url, metadata, caption} = serializedNode as BookmarkData; const node = new this({ url, metadata, @@ -85,10 +113,10 @@ export class BookmarkNode extends generateDecoratorNode({ } } -export const $createBookmarkNode = (dataset) => { +export const $createBookmarkNode = (dataset: BookmarkData = {}) => { return new BookmarkNode(dataset); }; -export function $isBookmarkNode(node) { +export function $isBookmarkNode(node: unknown): node is BookmarkNode { return node instanceof BookmarkNode; } diff --git a/packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts b/packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts index c626cba9f4..212755912b 100644 --- a/packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts +++ b/packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts @@ -1,19 +1,21 @@ -export function parseBookmarkNode(BookmarkNode) { +import type {LexicalNode} from 'lexical'; + +export function parseBookmarkNode(BookmarkNode: new (data: Record) => LexicalNode) { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement) => { const isKgBookmarkCard = nodeElem.classList?.contains('kg-bookmark-card'); if (nodeElem.tagName === 'FIGURE' && isKgBookmarkCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { const url = domNode?.querySelector('.kg-bookmark-container')?.getAttribute('href'); - const icon = domNode?.querySelector('.kg-bookmark-icon')?.src; + const icon = domNode?.querySelector('.kg-bookmark-icon')?.getAttribute('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 thumbnail = domNode?.querySelector('.kg-bookmark-thumbnail img')?.getAttribute('src'); const caption = domNode?.querySelector('figure.kg-bookmark-card figcaption')?.textContent; - const payload = { + const payload: Record = { url: url, metadata: { icon: icon, @@ -28,26 +30,26 @@ export function parseBookmarkNode(BookmarkNode) { const node = new BookmarkNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; }, - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { if (nodeElem.nodeType === 1 && nodeElem.tagName === 'DIV' && nodeElem.className.match(/graf--mixtapeEmbed/)) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { // Grab the relevant elements - Anchor wraps most of the data const anchorElement = domNode.querySelector('.markup--mixtapeEmbed-anchor'); - const titleElement = anchorElement.querySelector('.markup--mixtapeEmbed-strong'); - const descElement = anchorElement.querySelector('.markup--mixtapeEmbed-em'); + 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 HTMLElement | null; - domNode.querySelector('br').remove(); + domNode.querySelector('br')?.remove(); // Grab individual values from the elements - const url = anchorElement.getAttribute('href'); + const url = anchorElement?.getAttribute('href') ?? ''; let title = ''; let description = ''; let thumbnail = ''; @@ -55,28 +57,28 @@ export function parseBookmarkNode(BookmarkNode) { if (titleElement && titleElement.innerHTML) { title = titleElement.innerHTML.trim(); // Cleanup anchor so we can see what's left now that we've processed title - anchorElement.removeChild(titleElement); + titleElement.remove(); } if (descElement && descElement.innerHTML) { description = descElement.innerHTML.trim(); // Cleanup anchor so we can see what's left now that we've processed description - anchorElement.removeChild(descElement); + descElement.remove(); } // 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']) { - const match = imgElement.style['background-image'].match(/url\(([^)]*?)\)/); + if (imgElement && imgElement.style.backgroundImage) { + const match = imgElement.style.backgroundImage.match(/url\(([^)]*?)\)/); if (match?.[1]) { thumbnail = match[1].replace(/^['"]|['"]$/g, ''); } } - let payload = {url, + const payload: Record = {url, metadata: { title, description, @@ -86,7 +88,7 @@ export function parseBookmarkNode(BookmarkNode) { const node = new BookmarkNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; diff --git a/packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts b/packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts index 762f62e773..a804a7e9d0 100644 --- a/packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts @@ -1,12 +1,26 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; -import {escapeHtml} from '../../utils/escape-html'; -import {truncateHtml} from '../../utils/truncate'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; +import {escapeHtml} from '../../utils/escape-html.js'; +import {truncateHtml} from '../../utils/truncate.js'; + +interface BookmarkNodeData { + url: string; + title: string; + description: string; + icon: string; + author: string; + publisher: string; + thumbnail: string; + caption: string; +} + +interface RenderOptions extends ExportDOMOptions {} -export function renderBookmarkNode(node, options = {}) { +export function renderBookmarkNode(node: BookmarkNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); if (!node.url || node.url.trim() === '') { return renderEmptyContainer(document); @@ -19,7 +33,7 @@ export function renderBookmarkNode(node, options = {}) { } } -function emailTemplate(node, document) { +function emailTemplate(node: BookmarkNodeData, document: Document) { const title = escapeHtml(node.title); const publisher = escapeHtml(node.publisher); const author = escapeHtml(node.author); @@ -103,10 +117,10 @@ function emailTemplate(node, document) { `; element.innerHTML = html; - return {element}; + return {element, type: 'outer' as const}; } -function frontendTemplate(node, document) { +function frontendTemplate(node: BookmarkNodeData, document: Document) { const element = document.createElement('figure'); const caption = node.caption; let cardClass = 'kg-card kg-bookmark-card'; @@ -138,39 +152,39 @@ function frontendTemplate(node, document) { metadata.setAttribute('class','kg-bookmark-metadata'); content.appendChild(metadata); - metadata.icon = node.icon; - if (metadata.icon) { + const nodeIcon = node.icon; + if (nodeIcon) { const icon = document.createElement('img'); icon.setAttribute('class','kg-bookmark-icon'); - icon.src = metadata.icon; + icon.src = nodeIcon; icon.alt = ''; metadata.appendChild(icon); } - metadata.publisher = node.publisher; - if (metadata.publisher) { + const nodePublisher = node.publisher; + if (nodePublisher) { const publisher = document.createElement('span'); publisher.setAttribute('class','kg-bookmark-author'); // NOTE: This is NOT in error. The classes are reversed for theme backwards-compatibility. - publisher.textContent = metadata.publisher; + publisher.textContent = nodePublisher; metadata.appendChild(publisher); } - metadata.author = node.author; - if (metadata.author) { + const nodeAuthor = node.author; + if (nodeAuthor) { const author = document.createElement('span'); author.setAttribute('class','kg-bookmark-publisher'); // NOTE: This is NOT in error. The classes are reversed for theme backwards-compatibility. - author.textContent = metadata.author; + author.textContent = nodeAuthor; metadata.appendChild(author); } - metadata.thumbnail = node.thumbnail; - if (metadata.thumbnail) { + const nodeThumbnail = node.thumbnail; + if (nodeThumbnail) { const thumbnailDiv = document.createElement('div'); thumbnailDiv.setAttribute('class','kg-bookmark-thumbnail'); container.appendChild(thumbnailDiv); const thumbnail = document.createElement('img'); - thumbnail.src = metadata.thumbnail; + thumbnail.src = nodeThumbnail; thumbnail.alt = ''; thumbnail.setAttribute('onerror',`this.style.display = 'none'`); // Hide thumbnail div if image fails to load thumbnailDiv.appendChild(thumbnail); @@ -182,5 +196,5 @@ function frontendTemplate(node, document) { element.appendChild(figCaption); } - return {element}; + return {element, type: 'outer' as const}; } diff --git a/packages/kg-default-nodes/src/nodes/button/ButtonNode.ts b/packages/kg-default-nodes/src/nodes/button/ButtonNode.ts index 5b1c3adb2d..d21987a74d 100644 --- a/packages/kg-default-nodes/src/nodes/button/ButtonNode.ts +++ b/packages/kg-default-nodes/src/nodes/button/ButtonNode.ts @@ -1,14 +1,20 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseButtonNode} from './button-parser'; -import {renderButtonNode} from './button-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {parseButtonNode} from './button-parser.js'; +import {renderButtonNode} from './button-renderer.js'; + +const buttonProperties = [ + {name: 'buttonText', default: ''}, + {name: 'alignment', default: 'center'}, + {name: 'buttonUrl', default: '', urlType: 'url'} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type ButtonData = DecoratorNodeData; + +export interface ButtonNode extends DecoratorNodeValueMap {} export class ButtonNode extends generateDecoratorNode({ nodeType: 'button', - properties: [ - {name: 'buttonText', default: ''}, - {name: 'alignment', default: 'center'}, - {name: 'buttonUrl', default: '', urlType: 'url'} - ], + properties: buttonProperties, defaultRenderFn: renderButtonNode }) { static importDOM() { @@ -16,10 +22,10 @@ export class ButtonNode extends generateDecoratorNode({ } } -export const $createButtonNode = (dataset) => { +export const $createButtonNode = (dataset: ButtonData = {}) => { return new ButtonNode(dataset); }; -export function $isButtonNode(node) { +export function $isButtonNode(node: unknown): node is ButtonNode { return node instanceof ButtonNode; } diff --git a/packages/kg-default-nodes/src/nodes/button/button-parser.ts b/packages/kg-default-nodes/src/nodes/button/button-parser.ts index 7e6e326f1b..3972425514 100644 --- a/packages/kg-default-nodes/src/nodes/button/button-parser.ts +++ b/packages/kg-default-nodes/src/nodes/button/button-parser.ts @@ -1,10 +1,12 @@ -export function parseButtonNode(ButtonNode) { +import type {LexicalNode} from 'lexical'; + +export function parseButtonNode(ButtonNode: new (data: Record) => LexicalNode) { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { const isButtonCard = nodeElem.classList?.contains('kg-button-card'); if (nodeElem.tagName === 'DIV' && isButtonCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { const alignmentClass = nodeElem.className.match(/kg-align-(left|center)/); let alignment; @@ -13,10 +15,10 @@ 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 = { + const payload: Record = { buttonText: buttonText, alignment: alignment, buttonUrl: buttonUrl @@ -25,7 +27,7 @@ export function parseButtonNode(ButtonNode) { const node = new ButtonNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; diff --git a/packages/kg-default-nodes/src/nodes/button/button-renderer.ts b/packages/kg-default-nodes/src/nodes/button/button-renderer.ts index d72ee8cc56..e23c508dcd 100644 --- a/packages/kg-default-nodes/src/nodes/button/button-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/button/button-renderer.ts @@ -1,11 +1,20 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; -import {renderEmailButton} from '../../utils/render-helpers/email-button'; -import {html} from '../../utils/tagged-template-fns.mjs'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; +import {renderEmailButton} from '../../utils/render-helpers/email-button.js'; +import {html} from '../../utils/tagged-template-fns.js'; + +interface ButtonNodeData { + buttonUrl: string; + buttonText: string; + alignment: string; +} + +interface RenderOptions extends ExportDOMOptions {} -export function renderButtonNode(node, options = {}) { +export function renderButtonNode(node: ButtonNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); if (!node.buttonUrl || node.buttonUrl.trim() === '') { return renderEmptyContainer(document); @@ -18,7 +27,7 @@ export function renderButtonNode(node, options = {}) { } } -function frontendTemplate(node, document) { +function frontendTemplate(node: ButtonNodeData, document: Document) { const cardClasses = getCardClasses(node); const cardDiv = document.createElement('div'); @@ -30,10 +39,10 @@ function frontendTemplate(node, document) { button.textContent = node.buttonText || 'Button Title'; cardDiv.appendChild(button); - return {element: cardDiv}; + return {element: cardDiv, type: 'outer' as const}; } -function emailTemplate(node, options, document) { +function emailTemplate(node: ButtonNodeData, options: RenderOptions, document: Document) { const {buttonUrl, buttonText} = node; let cardHtml; @@ -55,7 +64,7 @@ function emailTemplate(node, options, document) { const element = document.createElement('p'); element.innerHTML = cardHtml; - return {element}; + return {element, type: 'outer' as const}; } else if (options.feature?.emailCustomizationAlpha) { const buttonHtml = renderEmailButton({ alignment: node.alignment, @@ -78,7 +87,7 @@ function emailTemplate(node, options, document) { const element = document.createElement('div'); element.innerHTML = cardHtml; - return {element, type: 'inner'}; + return {element, type: 'inner' as const}; } else { cardHtml = html`
@@ -94,12 +103,12 @@ function emailTemplate(node, options, document) { const element = document.createElement('p'); element.innerHTML = cardHtml; - return {element}; + return {element, type: 'outer' as const}; } } -function getCardClasses(node) { - let cardClasses = ['kg-card kg-button-card']; +function getCardClasses(node: ButtonNodeData) { + const cardClasses = ['kg-card kg-button-card']; if (node.alignment) { cardClasses.push(`kg-align-${node.alignment}`); diff --git a/packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts b/packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts index 335527d0b6..ebd400b114 100644 --- a/packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts +++ b/packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts @@ -1,28 +1,34 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderCallToActionNode} from './calltoaction-renderer'; -import {parseCallToActionNode} from './calltoaction-parser'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {renderCallToActionNode} from './calltoaction-renderer.js'; +import {parseCallToActionNode} from './calltoaction-parser.js'; + +const callToActionProperties = [ + {name: 'layout', default: 'minimal'}, + {name: 'alignment', default: 'left'}, + {name: 'textValue', default: '', wordCount: true}, + {name: 'showButton', default: true}, + {name: 'showDividers', default: true}, + {name: 'buttonText', default: 'Learn more'}, + {name: 'buttonUrl', default: ''}, + {name: 'buttonColor', default: '#000000'}, + {name: 'buttonTextColor', default: '#ffffff'}, + {name: 'hasSponsorLabel', default: true}, + {name: 'sponsorLabel', default: '

SPONSORED

'}, + {name: 'backgroundColor', default: 'grey'}, + {name: 'linkColor', default: 'text'}, + {name: 'imageUrl', default: '' as string | null}, + {name: 'imageWidth', default: null as number | null}, + {name: 'imageHeight', default: null as number | null} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type CallToActionData = DecoratorNodeData; + +export interface CallToActionNode extends DecoratorNodeValueMap {} export class CallToActionNode extends generateDecoratorNode({ nodeType: 'call-to-action', hasVisibility: true, - properties: [ - {name: 'layout', default: 'minimal'}, - {name: 'alignment', default: 'left'}, - {name: 'textValue', default: '', wordCount: true}, - {name: 'showButton', default: true}, - {name: 'showDividers', default: true}, - {name: 'buttonText', default: 'Learn more'}, - {name: 'buttonUrl', default: ''}, - {name: 'buttonColor', default: '#000000'}, // Where colour is customisable, we should use hex values - {name: 'buttonTextColor', default: '#ffffff'}, - {name: 'hasSponsorLabel', default: true}, - {name: 'sponsorLabel', default: '

SPONSORED

'}, - {name: 'backgroundColor', default: 'grey'}, // Since this is one of a few fixed options, we stick to colour names. - {name: 'linkColor', default: 'text'}, - {name: 'imageUrl', default: ''}, - {name: 'imageWidth', default: null}, - {name: 'imageHeight', default: null} - ], + properties: callToActionProperties, defaultRenderFn: renderCallToActionNode }) { static importDOM() { @@ -30,10 +36,10 @@ export class CallToActionNode extends generateDecoratorNode({ } } -export const $createCallToActionNode = (dataset) => { +export const $createCallToActionNode = (dataset?: CallToActionData) => { return new CallToActionNode(dataset); }; -export const $isCallToActionNode = (node) => { +export const $isCallToActionNode = (node: unknown): node is CallToActionNode => { return node instanceof CallToActionNode; }; diff --git a/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts b/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts index f8e93f0324..c6d3e1e8c1 100644 --- a/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts +++ b/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts @@ -1,19 +1,20 @@ -import {rgbToHex} from '../../utils/rgb-to-hex'; +import type {LexicalNode} from 'lexical'; +import {rgbToHex} from '../../utils/rgb-to-hex.js'; import {readImageAttributesFromElement} from '../../utils/read-image-attributes-from-element.js'; -export function parseCallToActionNode(CallToActionNode) { +export function parseCallToActionNode(CallToActionNode: new (data: Record) => LexicalNode) { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { const isCallToActionElement = nodeElem.classList?.contains('kg-cta-card'); if (isCallToActionElement) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { const div = domNode; const layout = div.getAttribute('data-layout') || 'minimal'; const alignment = div.getAttribute('data-alignment') || 'left'; const textValueElement = domNode.querySelector('.kg-cta-text'); - const buttonElement = domNode.querySelector('.kg-cta-button'); - const buttonStyles = buttonElement?.style || {}; + const buttonElement = domNode.querySelector('.kg-cta-button') as HTMLElement | null; + const buttonStyles = buttonElement?.style || {} as CSSStyleDeclaration; const buttonColor = buttonStyles.backgroundColor || '#000000'; const buttonTextColor = buttonStyles.color || '#ffffff'; const sponsorLabelElement = domNode.querySelector('.kg-cta-sponsor-label'); @@ -23,7 +24,7 @@ export function parseCallToActionNode(CallToActionNode) { const showDividers = div.classList.contains('kg-cta-has-dividers'); const imageContainer = domNode.querySelector('.kg-cta-image-container'); const imageElement = imageContainer?.querySelector('img'); - let imageData = { + const imageData: {imageUrl: string | number; imageWidth: string | number | null; imageHeight: string | number | null} = { imageUrl: '', imageWidth: null, imageHeight: null @@ -43,13 +44,13 @@ export function parseCallToActionNode(CallToActionNode) { sponsorLabelElement.innerHTML = `

${sponsorLabelElement.innerHTML.trim()}

`; } - const payload = { + const payload: Record = { layout: layout, alignment: alignment, - textValue: textValueElement.textContent.trim() || '', + textValue: textValueElement?.textContent?.trim() || '', showButton: buttonElement ? true : false, showDividers: showDividers, - buttonText: buttonElement?.textContent.trim() || '', + buttonText: buttonElement?.textContent?.trim() || '', buttonUrl: buttonElement?.getAttribute('href'), buttonColor: rgbToHex(buttonColor), buttonTextColor: rgbToHex(buttonTextColor), @@ -64,7 +65,7 @@ export function parseCallToActionNode(CallToActionNode) { const node = new CallToActionNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; diff --git a/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts b/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts index 1be610bbd4..f4fa0df4e7 100644 --- a/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts @@ -1,12 +1,41 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderWithVisibility} from '../../utils/visibility'; -import {getResizedImageDimensions} from '../../utils/get-resized-image-dimensions'; -import {isLocalContentImage} from '../../utils/is-local-content-image'; -import {buildCleanBasicHtmlForElement} from '../../utils/build-clean-basic-html-for-element'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {renderWithVisibility, type RenderOutput, type Visibility} from '../../utils/visibility.js'; +import {getResizedImageDimensions} from '../../utils/get-resized-image-dimensions.js'; +import {isLocalContentImage} from '../../utils/is-local-content-image.js'; +import {buildCleanBasicHtmlForElement} from '../../utils/build-clean-basic-html-for-element.js'; -const showButton = dataset => dataset.showButton && dataset.buttonUrl && dataset.buttonText; +interface CTADataset { + layout: string; + alignment: string; + textValue: string; + showButton: boolean; + showDividers: boolean; + buttonText: string; + buttonUrl: string; + buttonColor: string; + buttonTextColor: string; + hasSponsorLabel: boolean; + backgroundColor: string; + sponsorLabel: string; + imageUrl: string; + imageWidth: number; + imageHeight: number; + linkColor: string; +} + +interface CTARenderOptions extends ExportDOMOptions { + design?: { buttonStyle?: string }; + imageOptimization?: { internalImageSizes?: Record }; +} + +interface CTANodeData extends CTADataset { + visibility?: Visibility; +} + +const showButton = (dataset: CTADataset) => dataset.showButton && dataset.buttonUrl && dataset.buttonText; -const wrapWithLink = (dataset, content) => { +const wrapWithLink = (dataset: CTADataset, content: string) => { if (!showButton(dataset)) { return content; } @@ -14,9 +43,9 @@ const wrapWithLink = (dataset, content) => { return `${content}`; }; -function ctaCardTemplate(dataset) { +function ctaCardTemplate(dataset: CTADataset) { // Add validation for buttonColor - if (!dataset.buttonColor || !dataset.buttonColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { + if (!dataset.buttonColor || !dataset.buttonColor.match(/^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/)) { dataset.buttonColor = 'accent'; } const buttonAccent = dataset.buttonColor === 'accent' ? 'kg-style-accent' : ''; @@ -58,7 +87,7 @@ function ctaCardTemplate(dataset) { `; } -function emailCTATemplate(dataset, options = {}) { +function emailCTATemplate(dataset: CTADataset, options: CTARenderOptions = {}) { // accent button color backgrounds are set in main template styles, // for other button colors we need to set the background color explicitly let buttonStyle = dataset.buttonColor === 'accent' @@ -85,7 +114,7 @@ function emailCTATemplate(dataset, options = {}) { `; } - let imageDimensions; + let imageDimensions: { width: number; height: number } | undefined; if (dataset.imageUrl && dataset.imageWidth && dataset.imageHeight) { imageDimensions = { @@ -100,9 +129,12 @@ function emailCTATemplate(dataset, options = {}) { if (dataset.layout === 'minimal' && dataset.imageUrl) { if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) { - const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/); - const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; // default to 256 since we know the image is a square - dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`; + const match = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/); + if (match) { + const [, imagesPath, filename] = match; + const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; // default to 256 since we know the image is a square + dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`; + } } } @@ -335,9 +367,9 @@ function emailCTATemplate(dataset, options = {}) { } } -export function renderCallToActionNode(node, options = {}) { +export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); const dataset = { layout: node.layout, alignment: node.alignment, @@ -364,24 +396,24 @@ export function renderCallToActionNode(node, options = {}) { } if (options.target === 'email') { - const emailDoc = options.createDocument(); + const emailDoc = options.createDocument!(); const emailDiv = emailDoc.createElement('div'); emailDiv.innerHTML = emailCTATemplate(dataset, options); - return renderWithVisibility({element: emailDiv.firstElementChild}, node.visibility, options); + return renderWithVisibility({element: emailDiv.firstElementChild as RenderOutput['element'], type: 'outer' as const}, node.visibility, options); } - const element = document.createElement('div'); - if (dataset.hasSponsorLabel) { - const cleanBasicHtml = buildCleanBasicHtmlForElement(element); + const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div')); const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); - dataset.sponsorLabel = cleanedHtml; + dataset.sponsorLabel = cleanedHtml || ''; } + + const element = document.createElement('div'); const htmlString = ctaCardTemplate(dataset); element.innerHTML = htmlString?.trim(); - return renderWithVisibility({element: element.firstElementChild}, node.visibility, options); + return renderWithVisibility({element: element.firstElementChild as RenderOutput['element'], type: 'outer' as const}, node.visibility, options); } diff --git a/packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts b/packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts index 304329f9a1..94bdbda9e4 100644 --- a/packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts +++ b/packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts @@ -1,21 +1,35 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderCalloutNode} from './callout-renderer'; -import {parseCalloutNode} from './callout-parser'; +import {generateDecoratorNode, type DecoratorNodeProperty} from '../../generate-decorator-node.js'; +import {renderCalloutNode} from './callout-renderer.js'; +import {parseCalloutNode} from './callout-parser.js'; + +export interface CalloutData { + calloutText?: string; + calloutEmoji?: string; + backgroundColor?: string; +} + +export interface CalloutNode { + calloutText: string; + calloutEmoji: string; + backgroundColor: string; +} + +const calloutProperties = [ + {name: 'calloutText', default: '', wordCount: true}, + {name: 'calloutEmoji', default: '💡'}, + {name: 'backgroundColor', default: 'blue'} +] as const satisfies readonly DecoratorNodeProperty[]; export class CalloutNode extends generateDecoratorNode({ nodeType: 'callout', - properties: [ - {name: 'calloutText', default: '', wordCount: true}, - {name: 'calloutEmoji', default: '💡'}, - {name: 'backgroundColor', default: 'blue'} - ], + properties: calloutProperties, defaultRenderFn: renderCalloutNode }) { /* override */ - constructor({calloutText, calloutEmoji, backgroundColor} = {}, key) { - super(key); + constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {}, key?: string) { + super({}, key); this.__calloutText = calloutText || ''; - this.__calloutEmoji = calloutEmoji !== undefined ? calloutEmoji : '💡'; + this.__calloutEmoji = calloutEmoji ?? '💡'; this.__backgroundColor = backgroundColor || 'blue'; } @@ -24,10 +38,10 @@ export class CalloutNode extends generateDecoratorNode({ } } -export function $isCalloutNode(node) { +export function $isCalloutNode(node: unknown): node is CalloutNode { return node instanceof CalloutNode; } -export const $createCalloutNode = (dataset) => { +export const $createCalloutNode = (dataset: CalloutData = {}) => { return new CalloutNode(dataset); }; diff --git a/packages/kg-default-nodes/src/nodes/callout/callout-parser.ts b/packages/kg-default-nodes/src/nodes/callout/callout-parser.ts index b55e80d202..6de92c3cd7 100644 --- a/packages/kg-default-nodes/src/nodes/callout/callout-parser.ts +++ b/packages/kg-default-nodes/src/nodes/callout/callout-parser.ts @@ -1,20 +1,21 @@ -const getColorTag = (nodeElem) => { +import type {LexicalNode} from 'lexical'; +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(CalloutNode: new (data: Record) => LexicalNode) { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { const isKgCalloutCard = nodeElem.classList?.contains('kg-callout-card'); if (nodeElem.tagName === 'DIV' && isKgCalloutCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { const textNode = domNode?.querySelector('.kg-callout-text'); const emojiNode = domNode?.querySelector('.kg-callout-emoji'); const color = getColorTag(domNode); - const payload = { + const payload: Record = { calloutText: textNode && textNode.innerHTML.trim() || '', calloutEmoji: emojiNode && emojiNode.innerHTML.trim() || '', backgroundColor: color @@ -23,7 +24,7 @@ export function parseCalloutNode(CalloutNode) { const node = new CalloutNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; diff --git a/packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts b/packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts index 948c0eedbb..2978fc9741 100644 --- a/packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts @@ -1,9 +1,18 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {cleanDOM} from '../../utils/clean-dom'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {cleanDOM} from '../../utils/clean-dom.js'; + +interface CalloutNodeData { + backgroundColor: string; + calloutEmoji: string; + calloutText: string; +} + +interface RenderOptions extends ExportDOMOptions {} -export function renderCalloutNode(node, options = {}) { +export function renderCalloutNode(node: CalloutNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); const element = document.createElement('div'); // backgroundColor can end up with `rgba(0, 0, 0, 0)` from old mobiledoc copy/paste @@ -34,5 +43,5 @@ export function renderCalloutNode(node, options = {}) { textElement.innerHTML = temporaryContainer.innerHTML; element.appendChild(textElement); - return {element}; + return {element, type: 'outer' as const}; } diff --git a/packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts b/packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts index 650175af3f..ca08aee0f2 100644 --- a/packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts +++ b/packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts @@ -1,14 +1,20 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseCodeBlockNode} from './codeblock-parser'; -import {renderCodeBlockNode} from './codeblock-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {parseCodeBlockNode} from './codeblock-parser.js'; +import {renderCodeBlockNode} from './codeblock-renderer.js'; + +const codeBlockProperties = [ + {name: 'code', default: '', wordCount: true}, + {name: 'language', default: ''}, + {name: 'caption', default: '', urlType: 'html', wordCount: true} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type CodeBlockData = DecoratorNodeData; + +export interface CodeBlockNode extends DecoratorNodeValueMap {} export class CodeBlockNode extends generateDecoratorNode({ nodeType: 'codeblock', - properties: [ - {name: 'code', default: '', wordCount: true}, - {name: 'language', default: ''}, - {name: 'caption', default: '', urlType: 'html', wordCount: true} - ], + properties: codeBlockProperties, defaultRenderFn: renderCodeBlockNode }) { static importDOM() { @@ -20,10 +26,10 @@ export class CodeBlockNode extends generateDecoratorNode({ } } -export function $createCodeBlockNode(dataset) { +export function $createCodeBlockNode(dataset: CodeBlockData = {}) { return new CodeBlockNode(dataset); } -export function $isCodeBlockNode(node) { +export function $isCodeBlockNode(node: unknown): node is CodeBlockNode { return node instanceof CodeBlockNode; } diff --git a/packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts b/packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts index 8c22de773b..bfcb7de559 100644 --- a/packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts +++ b/packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts @@ -1,52 +1,53 @@ -import {readCaptionFromElement} from '../../utils/read-caption-from-element'; +import type {LexicalNode} from 'lexical'; +import {readCaptionFromElement} from '../../utils/read-caption-from-element.js'; -export function parseCodeBlockNode(CodeBlockNode) { +export function parseCodeBlockNode(CodeBlockNode: new (data: Record) => LexicalNode) { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement) => { const pre = nodeElem.querySelector('pre'); if (nodeElem.tagName === 'FIGURE' && pre) { return { - conversion(domNode) { - let code = pre.querySelector('code'); - let figcaption = domNode.querySelector('figcaption'); - + conversion(domNode: HTMLElement) { + 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; } - - let payload = { + + const payload: Record = { 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); + + 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 = new CodeBlockNode(payload); return {node}; }, - priority: 2 // falls back to pre if no caption + priority: 2 as const // falls back to pre if no caption }; } return null; }, pre: () => ({ - conversion(domNode) { + conversion(domNode: HTMLElement) { if (domNode.tagName === 'PRE') { - let [codeElement] = domNode.children; + const [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); + const payload: Record = {code: codeElement.textContent}; + 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(); } @@ -57,7 +58,7 @@ export function parseCodeBlockNode(CodeBlockNode) { return null; }, - priority: 1 + priority: 1 as const }) }; } diff --git a/packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts b/packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts index 1889b7d926..a1e3fe7719 100644 --- a/packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts @@ -1,9 +1,18 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; + +interface CodeBlockNodeData { + code: string; + language: string; + caption: string; +} + +interface RenderOptions extends ExportDOMOptions {} -export function renderCodeBlockNode(node, options = {}) { +export function renderCodeBlockNode(node: CodeBlockNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); if (!node.code || node.code.trim() === '') { return renderEmptyContainer(document); @@ -20,16 +29,16 @@ export function renderCodeBlockNode(node, options = {}) { pre.appendChild(code); if (node.caption) { - let figure = document.createElement('figure'); + const figure = document.createElement('figure'); figure.setAttribute('class', 'kg-card kg-code-card'); figure.appendChild(pre); - let figcaption = document.createElement('figcaption'); + const figcaption = document.createElement('figcaption'); figcaption.innerHTML = node.caption; figure.appendChild(figcaption); - return {element: figure}; + return {element: figure, type: 'outer' as const}; } else { - return {element: pre}; + return {element: pre, type: 'outer' as const}; } } diff --git a/packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts b/packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts index 6fd156c93d..5d6468bfc8 100644 --- a/packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts +++ b/packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts @@ -1,25 +1,31 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderEmailCtaNode} from './email-cta-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {renderEmailCtaNode} from './email-cta-renderer.js'; + +const emailCtaProperties = [ + {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} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type EmailCtaData = DecoratorNodeData; + +export interface EmailCtaNode extends DecoratorNodeValueMap {} 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} - ], + properties: emailCtaProperties, defaultRenderFn: renderEmailCtaNode }) { } -export const $createEmailCtaNode = (dataset) => { +export const $createEmailCtaNode = (dataset: EmailCtaData = {}) => { return new EmailCtaNode(dataset); }; -export function $isEmailCtaNode(node) { +export function $isEmailCtaNode(node: unknown): node is EmailCtaNode { return node instanceof EmailCtaNode; } diff --git a/packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts b/packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts index 6019b35ef5..8136c949f0 100644 --- a/packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts @@ -1,12 +1,25 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {removeCodeWrappersFromHelpers, removeSpaces, wrapReplacementStrings} from '../../utils/replacement-strings'; -import {escapeHtml} from '../../utils/escape-html'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {removeCodeWrappersFromHelpers, removeSpaces, wrapReplacementStrings} from '../../utils/replacement-strings.js'; +import {escapeHtml} from '../../utils/escape-html.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; -export function renderEmailCtaNode(node, options = {}) { +interface EmailCtaNodeData { + html: string; + buttonText: string; + buttonUrl: string; + showButton: boolean; + alignment: string; + segment: string; + showDividers: boolean; +} + +interface RenderOptions extends ExportDOMOptions {} + +export function renderEmailCtaNode(node: EmailCtaNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); const {html, buttonText, buttonUrl, showButton, alignment, segment, showDividers} = node; const hasButton = showButton && !!buttonText && !!buttonUrl; @@ -55,5 +68,5 @@ export function renderEmailCtaNode(node, options = {}) { element.appendChild(document.createElement('hr')); } - return {element}; -} \ No newline at end of file + return {element, type: 'outer' as const}; +} diff --git a/packages/kg-default-nodes/src/nodes/email/EmailNode.ts b/packages/kg-default-nodes/src/nodes/email/EmailNode.ts index 550b67d28d..e3b173f83c 100644 --- a/packages/kg-default-nodes/src/nodes/email/EmailNode.ts +++ b/packages/kg-default-nodes/src/nodes/email/EmailNode.ts @@ -1,19 +1,25 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderEmailNode} from './email-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {renderEmailNode} from './email-renderer.js'; + +const emailProperties = [ + {name: 'html', default: '', urlType: 'html'} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type EmailData = DecoratorNodeData; + +export interface EmailNode extends DecoratorNodeValueMap {} export class EmailNode extends generateDecoratorNode({ nodeType: 'email', - properties: [ - {name: 'html', default: '', urlType: 'html'} - ], + properties: emailProperties, defaultRenderFn: renderEmailNode }) { } -export const $createEmailNode = (dataset) => { +export const $createEmailNode = (dataset: EmailData = {}) => { return new EmailNode(dataset); }; -export function $isEmailNode(node) { +export function $isEmailNode(node: unknown): node is EmailNode { return node instanceof EmailNode; } diff --git a/packages/kg-default-nodes/src/nodes/email/email-renderer.ts b/packages/kg-default-nodes/src/nodes/email/email-renderer.ts index 127e49ba33..e88bf2f2a7 100644 --- a/packages/kg-default-nodes/src/nodes/email/email-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/email/email-renderer.ts @@ -1,10 +1,18 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {removeSpaces, removeCodeWrappersFromHelpers, wrapReplacementStrings} from '../../utils/replacement-strings'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions, ExportDOMOutput} from '../../export-dom.js'; +import {removeSpaces, removeCodeWrappersFromHelpers, wrapReplacementStrings} from '../../utils/replacement-strings.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; +import type {EmptyContainerOutput} from '../../utils/render-empty-container.js'; -export function renderEmailNode(node, options = {}) { +interface EmailNodeData { + html: string; +} + +interface RenderOptions extends ExportDOMOptions {} + +export function renderEmailNode(node: EmailNodeData, options: RenderOptions = {}): EmptyContainerOutput | ExportDOMOutput { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); const html = node.html; @@ -19,5 +27,5 @@ export function renderEmailNode(node, options = {}) { // `type: 'inner'` will render only the innerHTML of the element // @see @tryghost/kg-lexical-html-renderer package - return {element, type: 'inner'}; -} \ No newline at end of file + return {element, type: 'inner' as const}; +} diff --git a/packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts b/packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts index 87b96f1b66..197bea55d0 100644 --- a/packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts +++ b/packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts @@ -1,16 +1,27 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseEmbedNode} from './embed-parser'; -import {renderEmbedNode} from './embed-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {parseEmbedNode} from './embed-parser.js'; +import {renderEmbedNode} from './embed-renderer.js'; + +const embedProperties = [ + {name: 'url', default: '', urlType: 'url'}, + {name: 'embedType', default: ''}, + {name: 'html', default: ''}, + { + name: 'metadata', + get default() { + return {} as Record; + } + }, + {name: 'caption', default: '', wordCount: true} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type EmbedData = DecoratorNodeData; + +export interface EmbedNode extends DecoratorNodeValueMap {} export class EmbedNode extends generateDecoratorNode({ nodeType: 'embed', - properties: [ - {name: 'url', default: '', urlType: 'url'}, - {name: 'embedType', default: ''}, - {name: 'html', default: ''}, - {name: 'metadata', default: {}}, - {name: 'caption', default: '', wordCount: true} - ], + properties: embedProperties, defaultRenderFn: renderEmbedNode }) { static importDOM() { @@ -22,10 +33,10 @@ export class EmbedNode extends generateDecoratorNode({ } } -export const $createEmbedNode = (dataset) => { +export const $createEmbedNode = (dataset: EmbedData = {}) => { return new EmbedNode(dataset); }; -export function $isEmbedNode(node) { +export function $isEmbedNode(node: unknown): node is EmbedNode { return node instanceof EmbedNode; } diff --git a/packages/kg-default-nodes/src/nodes/embed/embed-parser.ts b/packages/kg-default-nodes/src/nodes/embed/embed-parser.ts index ecde2910ed..e05d0e0a63 100644 --- a/packages/kg-default-nodes/src/nodes/embed/embed-parser.ts +++ b/packages/kg-default-nodes/src/nodes/embed/embed-parser.ts @@ -1,14 +1,15 @@ +import type {LexicalNode} from 'lexical'; import {readCaptionFromElement} from '../../utils/read-caption-from-element.js'; // TODO: add NFT card parser -export function parseEmbedNode(EmbedNode) { +export function parseEmbedNode(EmbedNode: new (data: Record) => LexicalNode) { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement) => { if (nodeElem.nodeType === 1 && nodeElem.tagName === 'FIGURE') { const iframe = nodeElem.querySelector('iframe'); if (iframe) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { const payload = _createPayloadForIframe(iframe); if (!payload) { @@ -20,30 +21,30 @@ export function parseEmbedNode(EmbedNode) { const node = new EmbedNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } const blockquote = nodeElem.querySelector('blockquote'); if (blockquote) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { 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: Record = {url: url}; // 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; @@ -51,17 +52,21 @@ export function parseEmbedNode(EmbedNode) { const node = new EmbedNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } } return null; }, - iframe: (nodeElem) => { + iframe: (nodeElem: HTMLElement) => { if (nodeElem.nodeType === 1 && nodeElem.tagName === 'IFRAME') { return { - conversion(domNode) { - const payload = _createPayloadForIframe(domNode); + conversion(domNode: HTMLElement) { + if (domNode.tagName !== 'IFRAME') { + return null; + } + + const payload = _createPayloadForIframe(domNode as HTMLIFrameElement); if (!payload) { return null; @@ -70,7 +75,7 @@ export function parseEmbedNode(EmbedNode) { const node = new EmbedNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; @@ -78,7 +83,7 @@ export function parseEmbedNode(EmbedNode) { }; } -function _createPayloadForIframe(iframe) { +function _createPayloadForIframe(iframe: HTMLIFrameElement) { // 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,11 +95,11 @@ function _createPayloadForIframe(iframe) { iframe.src = `https:${iframe.src}`; } - let payload = { + const payload: Record = { url: iframe.src }; payload.html = iframe.outerHTML; return payload; -} \ No newline at end of file +} diff --git a/packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts b/packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts index 9592f36b5f..d9d2cb234f 100644 --- a/packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts @@ -1,11 +1,29 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; -import twitterRenderer from './types/twitter'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; +import twitterRenderer from './types/twitter.js'; -export function renderEmbedNode(node, options = {}) { +interface EmbedNodeData { + embedType: string; + html: string; + url: string; + caption: string; + metadata: { + thumbnail_url?: string; + thumbnail_width?: number; + thumbnail_height?: number; + tweet_data?: Record; + [key: string]: unknown; + }; + isEmpty: () => boolean; +} + +interface RenderOptions extends ExportDOMOptions {} + +export function renderEmbedNode(node: EmbedNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); const embedType = node.embedType; if (embedType === 'twitter') { @@ -15,7 +33,7 @@ export function renderEmbedNode(node, options = {}) { return renderTemplate(node, document, options); } -function renderTemplate(node, document, options) { +function renderTemplate(node: EmbedNodeData, document: Document, options: RenderOptions) { if (node.isEmpty()) { return renderEmptyContainer(document); } @@ -26,7 +44,7 @@ function renderTemplate(node, document, options) { const figure = document.createElement('figure'); figure.setAttribute('class', 'kg-card kg-embed-card'); - if (isEmail && isVideoWithThumbnail) { + if (isEmail && isVideoWithThumbnail && metadata.thumbnail_width && metadata.thumbnail_height) { const emailTemplateMaxWidth = 600; const thumbnailAspectRatio = metadata.thumbnail_width / metadata.thumbnail_height; const spacerWidth = Math.round(emailTemplateMaxWidth / 4); @@ -69,5 +87,5 @@ function renderTemplate(node, document, options) { figure.setAttribute('class', `${figure.getAttribute('class')} kg-card-hascaption`); } - return {element: figure}; -} \ No newline at end of file + return {element: figure, type: 'outer' as const}; +} diff --git a/packages/kg-default-nodes/src/nodes/embed/types/twitter.ts b/packages/kg-default-nodes/src/nodes/embed/types/twitter.ts index 9f8cedfed2..ccc435c217 100644 --- a/packages/kg-default-nodes/src/nodes/embed/types/twitter.ts +++ b/packages/kg-default-nodes/src/nodes/embed/types/twitter.ts @@ -1,7 +1,63 @@ import {DateTime} from 'luxon'; -import toArray from 'lodash/toArray'; -export default function render(node, document, options) { +interface TweetPublicMetrics { + retweet_count: number; + like_count: number; +} + +interface TweetEntities { + mentions?: TwitterEntity[]; + urls?: TwitterEntity[]; + hashtags?: TwitterEntity[]; +} + +interface TweetAttachments { + media_keys?: string[]; + poll_ids?: string[]; +} + +interface TweetIncludes { + media: Array<{ preview_image_url?: string; url?: string }>; +} + +interface TweetData { + id?: string; + text?: string; + created_at?: string; + author_id?: string; + public_metrics?: TweetPublicMetrics; + users?: TwitterUser[]; + entities?: TweetEntities; + attachments?: TweetAttachments; + includes?: TweetIncludes; + [key: string]: unknown; +} + +interface TwitterNode { + html: string; + caption?: string; + metadata?: { + tweet_data?: TweetData; + }; +} + +interface TwitterUser { + id: string; + name?: string; + username?: string; + profile_image_url?: string; +} + +interface TwitterEntity { + start: number; + end: number; + url?: string; + display_url?: string; + username?: string; + tag?: string; +} + +export default function render(node: TwitterNode, document: Document, options: Record) { const metadata = node.metadata; const figure = document.createElement('figure'); @@ -12,7 +68,7 @@ export default function render(node, document, options) { const tweetData = metadata && metadata.tweet_data; const isEmail = options.target === 'email'; - if (tweetData && isEmail) { + if (tweetData && isEmail && tweetData.id) { const tweetId = tweetData.id; const numberFormatter = new Intl.NumberFormat('en-US', { style: 'decimal', @@ -20,29 +76,31 @@ export default function render(node, document, options) { unitDisplay: 'narrow', maximumFractionDigits: 1 }); - const retweetCount = numberFormatter.format(tweetData.public_metrics.retweet_count); - const likeCount = numberFormatter.format(tweetData.public_metrics.like_count); - const authorUser = tweetData.users && tweetData.users.find(user => user.id === tweetData.author_id); - const tweetTime = DateTime.fromISO(tweetData.created_at).toLocaleString(DateTime.TIME_SIMPLE); - const tweetDate = DateTime.fromISO(tweetData.created_at).toLocaleString(DateTime.DATE_MED); + const retweetCount = numberFormatter.format(tweetData.public_metrics?.retweet_count ?? 0); + const likeCount = numberFormatter.format(tweetData.public_metrics?.like_count ?? 0); + const authorUser = tweetData.users && tweetData.users.find((user: TwitterUser) => user.id === tweetData.author_id); + const parsedCreatedAt = tweetData.created_at ? DateTime.fromISO(tweetData.created_at) : null; + const tweetTime = parsedCreatedAt?.isValid ? parsedCreatedAt.toLocaleString(DateTime.TIME_SIMPLE) : ''; + const tweetDate = parsedCreatedAt?.isValid ? parsedCreatedAt.toLocaleString(DateTime.DATE_MED) : ''; const mentions = tweetData.entities && tweetData.entities.mentions || []; const urls = tweetData.entities && tweetData.entities.urls || []; const hashtags = tweetData.entities && tweetData.entities.hashtags || []; - const entities = mentions.concat(urls).concat(hashtags).sort((a, b) => a.start - b.start); + const entities = mentions.concat(urls).concat(hashtags).sort((a: TwitterEntity, b: TwitterEntity) => a.start - b.start); let tweetContent = tweetData.text; let tweetImageUrl = null; - const hasImageOrVideo = tweetData.attachments && tweetData.attachments && tweetData.attachments.media_keys; + const firstMedia = tweetData.includes?.media?.[0]; + const hasImageOrVideo = tweetData.attachments?.media_keys && firstMedia; if (hasImageOrVideo) { - tweetImageUrl = tweetData.includes.media[0].preview_image_url || tweetData.includes.media[0].url; + tweetImageUrl = firstMedia.preview_image_url || firstMedia.url; } const hasPoll = tweetData.attachments && tweetData.attachments && tweetData.attachments.poll_ids; if (mentions) { let last = 0; - let parts = []; - let content = toArray(tweetContent); + const parts = []; + const content = Array.from(tweetContent ?? ''); for (const entity of entities) { let type = 'text'; let data = content.slice(entity.start, entity.end + 1).join('').replace(/\n/g, '
'); @@ -165,5 +223,5 @@ export default function render(node, document, options) { figure.setAttribute('class', `${figure.getAttribute('class')} kg-card-hascaption`); } - return {element: figure}; + return {element: figure, type: 'outer' as const}; } diff --git a/packages/kg-default-nodes/src/nodes/file/FileNode.ts b/packages/kg-default-nodes/src/nodes/file/FileNode.ts index a0a9800c2b..3732fd1864 100644 --- a/packages/kg-default-nodes/src/nodes/file/FileNode.ts +++ b/packages/kg-default-nodes/src/nodes/file/FileNode.ts @@ -1,17 +1,23 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {renderFileNode} from './file-renderer'; -import {parseFileNode} from './file-parser'; -import {bytesToSize} from '../../utils/size-byte-converter'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {renderFileNode} from './file-renderer.js'; +import {parseFileNode} from './file-parser.js'; +import {bytesToSize} from '../../utils/size-byte-converter.js'; + +const fileProperties = [ + {name: 'src', default: '', urlType: 'url'}, + {name: 'fileTitle', default: '', wordCount: true}, + {name: 'fileCaption', default: '', wordCount: true}, + {name: 'fileName', default: ''}, + {name: 'fileSize', default: 0} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type FileData = DecoratorNodeData; + +export interface FileNode extends DecoratorNodeValueMap {} export class FileNode extends generateDecoratorNode({ nodeType: 'file', - properties: [ - {name: 'src', default: '', urlType: 'url'}, - {name: 'fileTitle', default: '', wordCount: true}, - {name: 'fileCaption', default: '', wordCount: true}, - {name: 'fileName', default: ''}, - {name: 'fileSize', default: ''} - ], + properties: fileProperties, defaultRenderFn: renderFileNode }) { /* @override */ @@ -20,7 +26,8 @@ export class FileNode extends generateDecoratorNode({ const isBlob = src && src.startsWith('data:'); return { - type: 'file', + type: 'file' as const, + version: 1, src: isBlob ? '' : src, fileTitle, fileCaption, @@ -38,10 +45,10 @@ export class FileNode extends generateDecoratorNode({ } } -export function $isFileNode(node) { +export function $isFileNode(node: unknown): node is FileNode { return node instanceof FileNode; } -export const $createFileNode = (dataset) => { +export const $createFileNode = (dataset: FileData = {}) => { return new FileNode(dataset); }; diff --git a/packages/kg-default-nodes/src/nodes/file/file-parser.ts b/packages/kg-default-nodes/src/nodes/file/file-parser.ts index c3740879ff..f7f2cb9ac0 100644 --- a/packages/kg-default-nodes/src/nodes/file/file-parser.ts +++ b/packages/kg-default-nodes/src/nodes/file/file-parser.ts @@ -1,19 +1,20 @@ -import {sizeToBytes} from '../../utils/size-byte-converter'; +import type {LexicalNode} from 'lexical'; +import {sizeToBytes} from '../../utils/size-byte-converter.js'; -export function parseFileNode(FileNode) { +export function parseFileNode(FileNode: new (data: Record) => LexicalNode) { return { - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { const isKgFileCard = nodeElem.classList?.contains('kg-file-card'); if (nodeElem.tagName === 'DIV' && isKgFileCard) { return { - conversion(domNode) { + conversion(domNode: HTMLElement) { const link = domNode.querySelector('a'); - const src = link.getAttribute('href'); + 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 payload = { + const fileSize = sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || ''); + const payload: Record = { src, fileTitle, fileCaption, @@ -24,7 +25,7 @@ export function parseFileNode(FileNode) { const node = new FileNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; diff --git a/packages/kg-default-nodes/src/nodes/file/file-renderer.ts b/packages/kg-default-nodes/src/nodes/file/file-renderer.ts index e5570b1588..1e0638ad4c 100644 --- a/packages/kg-default-nodes/src/nodes/file/file-renderer.ts +++ b/packages/kg-default-nodes/src/nodes/file/file-renderer.ts @@ -1,11 +1,23 @@ -import {addCreateDocumentOption} from '../../utils/add-create-document-option'; -import {renderEmptyContainer} from '../../utils/render-empty-container'; -import {escapeHtml} from '../../utils/escape-html'; -import {bytesToSize} from '../../utils/size-byte-converter'; +import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; +import type {ExportDOMOptions} from '../../export-dom.js'; +import {renderEmptyContainer} from '../../utils/render-empty-container.js'; +import {escapeHtml} from '../../utils/escape-html.js'; +import {bytesToSize} from '../../utils/size-byte-converter.js'; + +interface FileNodeData { + src: string; + fileTitle: string; + fileCaption: string; + fileName: string; + fileSize: number; + formattedFileSize: string; +} + +interface RenderOptions extends ExportDOMOptions {} -export function renderFileNode(node, options = {}) { +export function renderFileNode(node: FileNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument(); + const document = options.createDocument!(); if (!node.src || node.src.trim() === '') { return renderEmptyContainer(document); @@ -18,7 +30,15 @@ export function renderFileNode(node, options = {}) { } } -function emailTemplate(node, document, options) { +function wrapWithAnchor(content: string, href: string | undefined, cls: string, style?: string) { + if (href) { + const styleAttr = style ? ` style="${style}"` : ''; + return `${content}`; + } + return `${content}`; +} + +function emailTemplate(node: FileNodeData, document: Document, options: RenderOptions) { let iconCls; if (!node.fileTitle && !node.fileCaption) { iconCls = 'margin-top: 6px; height: 20px; width: 20px; max-width: 20px; padding-top: 4px; padding-bottom: 4px;'; @@ -26,6 +46,8 @@ function emailTemplate(node, document, options) { iconCls = 'margin-top: 6px; height: 24px; width: 24px; max-width: 24px;'; } + const href = options.postUrl || node.src || undefined; + const html = (`
@@ -35,22 +57,24 @@ function emailTemplate(node, document, options) {
${node.fileTitle ? `
- ${escapeHtml(node.fileTitle)} + ${wrapWithAnchor(escapeHtml(node.fileTitle), href, 'kg-file-title')}
` : ``} ${node.fileCaption ? `
- ${escapeHtml(node.fileCaption)} + ${wrapWithAnchor(escapeHtml(node.fileCaption), href, 'kg-file-description')}
` : ``}
- ${escapeHtml(node.fileName)} • ${bytesToSize(node.fileSize)} + ${wrapWithAnchor(`${escapeHtml(node.fileName)} • ${bytesToSize(node.fileSize)}`, href, 'kg-file-meta')}
- + ${href + ? ` - + ` + : ``}
@@ -62,10 +86,10 @@ function emailTemplate(node, document, options) { const container = document.createElement('div'); container.innerHTML = html.trim(); - return {element: container.firstElementChild}; + return {element: container.firstElementChild, type: 'outer' as const}; } -function cardTemplate(node, document) { +function cardTemplate(node: FileNodeData, document: Document) { const card = document.createElement('div'); card.setAttribute('class', 'kg-card kg-file-card'); @@ -149,5 +173,5 @@ function cardTemplate(node, document) { container.appendChild(icon); card.appendChild(container); - return {element: card}; -} \ No newline at end of file + return {element: card, type: 'outer' as const}; +} diff --git a/packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts b/packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts index cfef4fa4fe..9a1e49416d 100644 --- a/packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts +++ b/packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts @@ -1,12 +1,19 @@ -import {generateDecoratorNode} from '../../generate-decorator-node'; -import {parseGalleryNode} from './gallery-parser'; -import {renderGalleryNode} from './gallery-renderer'; +import {generateDecoratorNode, type DecoratorNodeData, type DecoratorNodeProperty, type DecoratorNodeValueMap} from '../../generate-decorator-node.js'; +import {parseGalleryNode} from './gallery-parser.js'; +import {renderGalleryNode} from './gallery-renderer.js'; + +const galleryProperties = [ + {name: 'images', default: [] as unknown[]}, + {name: 'caption', default: '', wordCount: true} +] as const satisfies readonly DecoratorNodeProperty[]; + +export type GalleryData = DecoratorNodeData; + +export interface GalleryNode extends DecoratorNodeValueMap {} + export class GalleryNode extends generateDecoratorNode({ nodeType: 'gallery', - properties: [ - {name: 'images', default: []}, - {name: 'caption', default: '', wordCount: true} - ], + properties: galleryProperties, defaultRenderFn: renderGalleryNode }) { /* override */ @@ -29,10 +36,10 @@ export class GalleryNode extends generateDecoratorNode({ } } -export const $createGalleryNode = (dataset) => { +export const $createGalleryNode = (dataset?: GalleryData) => { return new GalleryNode(dataset); }; -export function $isGalleryNode(node) { +export function $isGalleryNode(node: unknown): node is GalleryNode { return node instanceof GalleryNode; } diff --git a/packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts b/packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts index 0b0b555e66..be669cc9c6 100644 --- a/packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts +++ b/packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts @@ -1,23 +1,24 @@ +import type {LexicalNode} from 'lexical'; import {readCaptionFromElement} from '../../utils/read-caption-from-element.js'; import {readImageAttributesFromElement} from '../../utils/read-image-attributes-from-element.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(GalleryNode: new (data: Record) => LexicalNode) { return { - figure: (nodeElem) => { + figure: (nodeElem: HTMLElement) => { // Koenig gallery card if (nodeElem.classList?.contains('kg-gallery-card')) { return { - conversion(domNode) { - const payload = {}; + conversion(domNode: HTMLElement) { + const payload: Record = {}; const imgs = Array.from(domNode.querySelectorAll('img')); payload.images = imgs.map(readGalleryImageAttributesFromElement); @@ -26,15 +27,15 @@ export function parseGalleryNode(GalleryNode) { const node = new GalleryNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } return null; }, - div: (nodeElem) => { + div: (nodeElem: HTMLElement) => { // Medium "graf" galleries - function isGrafGallery(node) { + function isGrafGallery(node: HTMLElement) { return node.tagName === 'DIV' && node.dataset?.paragraphCount && node.querySelectorAll('img').length > 0; @@ -42,43 +43,46 @@ export function parseGalleryNode(GalleryNode) { if (isGrafGallery(nodeElem)) { return { - conversion(domNode) { - const payload = { - caption: readCaptionFromElement(domNode) - }; + conversion(domNode: HTMLElement) { + const payload: Record = {}; + const captions = [readCaptionFromElement(domNode)].filter((caption): caption is string => Boolean(caption)); // These galleries exist as a series of divs containing multiple figure+img. // Grab the first set of imgs... 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 | null; while (nextNode && isGrafGallery(nextNode)) { - let currentNode = nextNode; + const currentNode = nextNode; imgs = imgs.concat(Array.from(currentNode.querySelectorAll('img'))); const currentNodeCaption = readCaptionFromElement(currentNode); if (currentNodeCaption) { - payload.caption = `${payload.caption} / ${currentNodeCaption}`; + captions.push(currentNodeCaption); } - nextNode = currentNode.nextElementSibling; + nextNode = currentNode.nextElementSibling as HTMLElement | null; // remove nodes as we go so that they don't go through the parser currentNode.remove(); } + if (captions.length > 0) { + payload.caption = captions.join(' / '); + } + payload.images = imgs.map(readGalleryImageAttributesFromElement); const node = new GalleryNode(payload); return {node}; }, - priority: 1 + priority: 1 as const }; } // Squarespace SQS galleries - function isSqsGallery(node) { + function isSqsGallery(node: HTMLElement) { return node.tagName === 'DIV' && node.className.match(/sqs-gallery-container/) && !node.className.match(/summary-/); @@ -86,19 +90,19 @@ export function parseGalleryNode(GalleryNode) { if (isSqsGallery(nodeElem)) { return { - conversion(domNode) { - const payload = {}; + conversion(domNode: HTMLElement) { + const payload: Record = {}; // Each image exists twice... // The first image is wrapped in `