Skip to content

Conversation

@kevinansfield
Copy link
Member

no issue

First part of a refactor to allow node renderers to be provided by the consumer. This change centralizes our exportDOM() definition in our generateDecoratorNode() factory which means if needed we can select a different renderer function in one place rather than needing to spread knowledge across every node.

  • added a required defaultRenderFn option to generateDecoratorNode()
    • takes a function or an object, the object version should be keyed by version numbers with the appropriate render function as the value
    • adds default exportDOM() method that calls defaultRenderFn or the appropriate versioned render function
  • removed exportDOM() overrides from all of our decorator nodes
  • standardized on nodeType: property being defined on the first line of the properties list in each node which helps with regex search & replace and AI agents

@coderabbitai
Copy link

coderabbitai bot commented May 26, 2025

Caution

Review failed

The pull request is closed.

"""

Walkthrough

This change refactors the node class definitions in the kg-default-nodes package to centralize rendering logic through a defaultRenderFn property. The generateDecoratorNode function is updated to accept this new parameter, which can be a function or an object mapping versions to functions. All affected node classes now specify their rendering function via defaultRenderFn in the decorator configuration, replacing previous exportDOM methods. The explicit exportDOM methods are removed from these classes, streamlining the node configuration and rendering process without altering core functionality.

Possibly related PRs

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Failed to load parser '@babel/eslint-parser' declared in 'packages/kg-default-nodes/.eslintrc.js': Cannot find module '@babel/eslint-parser'
Require stack:

  • /packages/kg-default-nodes/.eslintrc.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at require.resolve (node:internal/modules/helpers:145:19)
    at Object.resolve (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2346:46)
    at ConfigArrayFactory._loadParser (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3325:39)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3099:43)
    at _normalizeObjectConfigDataBody.next ()
    at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3040:20)
    at _normalizeObjectConfigData.next ()
    at ConfigArrayFactory.loadInDirectory (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2886:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3871:46)
packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Failed to load parser '@babel/eslint-parser' declared in 'packages/kg-default-nodes/.eslintrc.js': Cannot find module '@babel/eslint-parser'
Require stack:

  • /packages/kg-default-nodes/.eslintrc.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at require.resolve (node:internal/modules/helpers:145:19)
    at Object.resolve (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2346:46)
    at ConfigArrayFactory._loadParser (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3325:39)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3099:43)
    at _normalizeObjectConfigDataBody.next ()
    at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3040:20)
    at _normalizeObjectConfigData.next ()
    at ConfigArrayFactory.loadInDirectory (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2886:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3871:46)
packages/kg-default-nodes/lib/nodes/audio/AudioNode.js

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Failed to load parser '@babel/eslint-parser' declared in 'packages/kg-default-nodes/.eslintrc.js': Cannot find module '@babel/eslint-parser'
Require stack:

  • /packages/kg-default-nodes/.eslintrc.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at require.resolve (node:internal/modules/helpers:145:19)
    at Object.resolve (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2346:46)
    at ConfigArrayFactory._loadParser (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3325:39)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3099:43)
    at _normalizeObjectConfigDataBody.next ()
    at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3040:20)
    at _normalizeObjectConfigData.next ()
    at ConfigArrayFactory.loadInDirectory (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2886:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3871:46)
  • 20 others

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4ae59 and 6991c15.

📒 Files selected for processing (23)
  • packages/kg-default-nodes/lib/generate-decorator-node.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/audio/AudioNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/button/ButtonNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/email/EmailNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/file/FileNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/header/HeaderNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/html/HtmlNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/image/ImageNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/product/ProductNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/signup/SignupNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/video/VideoNode.js (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (1)

29-29: Remove the unnecessary blank line.

This extra blank line doesn't add value to code readability.

        const collection = this.__collection;
        const postCount = this.__postCount;
-

        if (!options?.getCollectionPosts) {
packages/kg-default-nodes/lib/generate-decorator-node.js (1)

40-40: Fix JSDoc typo - missing closing brace.

The example in the JSDoc is incomplete.

- * @param {Function} defaultRenderFn - A function that returns a @tryghost/kg-lexical-html-renderer compatible object, e.g. {element: Div, type: 'inner}
+ * @param {Function} defaultRenderFn - A function that returns a @tryghost/kg-lexical-html-renderer compatible object, e.g. {element: 'div', type: 'inner'}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06ca688 and 64b395f.

📒 Files selected for processing (23)
  • packages/kg-default-nodes/lib/generate-decorator-node.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/audio/AudioNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/button/ButtonNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/email/EmailNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/file/FileNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/header/HeaderNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/html/HtmlNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/image/ImageNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/product/ProductNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/signup/SignupNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/video/VideoNode.js (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Node 20.11.1
  • GitHub Check: Node 22.13.1
🔇 Additional comments (22)
packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (1)

6-16: LGTM! Refactoring correctly implements the centralized rendering pattern.

The changes properly consolidate the decorator configuration with nodeType as the first property and defaultRenderFn set to the imported render function, aligning with the PR objectives.

packages/kg-default-nodes/lib/nodes/audio/AudioNode.js (1)

6-16: LGTM! Consistent implementation of the centralized rendering pattern.

The refactoring correctly follows the established pattern with nodeType first and defaultRenderFn properly configured.

packages/kg-default-nodes/lib/nodes/image/ImageNode.js (1)

5-18: LGTM! Clean implementation of the centralized rendering pattern.

The refactoring maintains consistency with other nodes, properly configuring nodeType and defaultRenderFn.

packages/kg-default-nodes/lib/nodes/file/FileNode.js (1)

7-17: LGTM! Refactoring correctly implemented.

The changes properly implement the new pattern:

  • nodeType: 'file' is correctly placed as the first property
  • defaultRenderFn is properly assigned to the imported renderFileNode
  • The removal of exportDOM() aligns with the PR's goal of centralizing this functionality
packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js (1)

6-13: LGTM! Consistent with the refactoring pattern.

The changes correctly follow the established pattern:

  • nodeType: 'toggle' as the first property
  • defaultRenderFn properly references the imported renderToggleNode
  • Clean implementation without the exportDOM() method
packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js (1)

6-19: LGTM! Clean refactoring with commented code removed.

The changes correctly implement the pattern:

  • nodeType: 'bookmark' properly positioned first
  • defaultRenderFn correctly assigned to the imported renderBookmarkNode
  • Good cleanup by removing the previously commented-out exportDOM() method
packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js (1)

26-27: Changes correctly implement the centralized rendering pattern.

The addition of defaultRenderFn: renderCallToActionNode properly integrates with the refactored generateDecoratorNode factory, successfully removing the need for individual exportDOM overrides.

packages/kg-default-nodes/lib/nodes/button/ButtonNode.js (1)

6-14: Refactoring successfully implements the centralized rendering approach.

The changes properly:

  • Place nodeType on the first line of properties as per standardization goals
  • Add the required defaultRenderFn property with the correct render function
  • Maintain clean multi-line formatting for better readability
packages/kg-default-nodes/lib/nodes/product/ProductNode.js (1)

6-21: Excellent implementation of the centralized rendering pattern.

The refactoring successfully:

  • Removes the explicit exportDOM override as intended
  • Adds the defaultRenderFn property pointing to the appropriate render function
  • Maintains consistency with the standardized nodeType placement
  • Preserves other class methods (exportJSON, isEmpty) that are still needed
packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js (1)

6-14: LGTM! Refactoring correctly implements the new pattern.

The changes properly refactor the CodeBlockNode to use the new defaultRenderFn pattern, removing the need for an exportDOM override. The nodeType property is correctly positioned on the first line as specified in the PR objectives.

packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js (1)

5-11: LGTM! Clean refactoring to the new pattern.

The MarkdownNode has been correctly refactored to use defaultRenderFn instead of an exportDOM method, with nodeType properly positioned on the first line.

packages/kg-default-nodes/lib/nodes/header/HeaderNode.js (1)

9-41: Excellent implementation of versioned rendering pattern!

The HeaderNode correctly demonstrates the versioned defaultRenderFn pattern using an object keyed by version numbers. This cleanly replaces the previous exportDOM method that contained version-specific logic, centralizing the rendering configuration as intended.

packages/kg-default-nodes/lib/nodes/email/EmailNode.js (1)

5-11: Refactoring looks good!

The changes correctly implement the centralized rendering pattern by:

  • Moving to a multi-line format for better readability
  • Placing nodeType as the first property (standardization)
  • Using defaultRenderFn instead of an exportDOM override
packages/kg-default-nodes/lib/nodes/signup/SignupNode.js (1)

6-26: Consistent refactoring implementation!

The SignupNode correctly follows the new pattern with:

  • nodeType as the first property
  • Extensive properties array maintaining all defaults
  • defaultRenderFn properly set to renderSignupCardToDOM

The custom constructor and label management methods remain properly intact.

packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js (1)

6-9: Clean and minimal refactoring!

The HorizontalRuleNode correctly implements the new pattern with the minimal required properties:

  • nodeType as the first property
  • defaultRenderFn pointing to the appropriate renderer

The remaining methods (importDOM, getTextContent, hasEditMode) are properly preserved.

packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js (1)

6-14: LGTM! Clean refactoring of the CalloutNode class.

The changes correctly implement the new pattern:

  • nodeType is properly placed on the first line
  • defaultRenderFn correctly references the imported renderCalloutNode
  • The removal of the exportDOM() method aligns with the PR's goal of centralizing rendering logic
packages/kg-default-nodes/lib/nodes/html/HtmlNode.js (1)

6-13: LGTM! Consistent refactoring of the HtmlNode class.

The changes correctly follow the established pattern:

  • nodeType is on the first line as required
  • hasVisibility property is properly preserved
  • defaultRenderFn correctly references the imported renderHtmlNode
packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js (1)

5-18: LGTM! EmailCtaNode successfully refactored.

The changes are consistent with the refactoring pattern:

  • nodeType is properly placed on the first line
  • defaultRenderFn correctly references the imported renderEmailCtaNode
  • The empty class body is appropriate since no methods need to be overridden
packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js (1)

6-16: LGTM! Clean refactoring to use centralized rendering logic.

The changes correctly implement the new pattern:

  • nodeType is placed on the first line of properties for better searchability
  • defaultRenderFn properly delegates rendering to renderEmbedNode
  • Removal of exportDOM aligns with centralizing this logic in generateDecoratorNode
packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js (1)

5-12: LGTM! Consistent implementation of the centralized rendering pattern.

The refactoring follows the same pattern as other nodes, properly delegating rendering responsibility to the factory function.

packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js (1)

6-9: LGTM! Refactoring aligns with PR objectives.

The changes correctly implement the centralized rendering pattern by:

  • Adding the required defaultRenderFn parameter with renderPaywallNode
  • Placing nodeType on the first line for consistency
  • Removing the exportDOM method (now handled by the factory)
packages/kg-default-nodes/lib/nodes/video/VideoNode.js (1)

5-23: LGTM! Consistent implementation of the rendering refactor.

The changes correctly implement the centralized rendering pattern:

  • defaultRenderFn is properly set to renderVideoNode
  • nodeType placement follows the new standard (first line)
  • All existing properties are preserved
  • exportDOM method removal aligns with the factory-based approach

@kevinansfield kevinansfield force-pushed the centralize-export-dom branch from 64b395f to 3e4ae59 Compare May 26, 2025 14:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (1)

29-29: Remove unnecessary blank line.

         const postCount = this.__postCount;
-
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64b395f and 3e4ae59.

📒 Files selected for processing (23)
  • packages/kg-default-nodes/lib/generate-decorator-node.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/audio/AudioNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/button/ButtonNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/email/EmailNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/file/FileNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/header/HeaderNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/html/HtmlNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/image/ImageNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/product/ProductNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/signup/SignupNode.js (2 hunks)
  • packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/video/VideoNode.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/kg-default-nodes/lib/nodes/markdown/MarkdownNode.js
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Node 20.11.1
  • GitHub Check: Node 22.13.1
🔇 Additional comments (27)
packages/kg-default-nodes/lib/generate-decorator-node.js (2)

40-40: LGTM: Clear documentation for the new parameter.

The JSDoc documentation clearly explains the defaultRenderFn parameter and its expected return type.


43-43: LGTM: Function signature correctly updated.

The function signature properly includes the new defaultRenderFn parameter as required by the refactor.

packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js (1)

26-27: LGTM: Correctly implements the centralized rendering pattern.

The addition of defaultRenderFn: renderCallToActionNode properly centralizes the rendering logic as intended by the refactor. The nodeType is correctly positioned on the first line as per the standardization requirements.

packages/kg-default-nodes/lib/nodes/toggle/ToggleNode.js (1)

6-13: LGTM: Clean implementation of the centralized rendering pattern.

The refactor correctly:

  • Positions nodeType on the first line as per standardization requirements
  • Adds defaultRenderFn: renderToggleNode to centralize rendering logic
  • Uses multiline formatting for improved readability

This aligns perfectly with the PR objectives.

packages/kg-default-nodes/lib/nodes/image/ImageNode.js (1)

5-18: LGTM: Consistent implementation of the rendering refactor.

The changes properly implement the centralized rendering pattern:

  • nodeType is correctly positioned on the first line
  • defaultRenderFn: renderImageNode centralizes the rendering logic
  • Multiline formatting enhances readability

The refactor maintains all existing functionality while following the established pattern.

packages/kg-default-nodes/lib/nodes/email-cta/EmailCtaNode.js (1)

5-17: LGTM! Refactor correctly implemented.

The refactor successfully centralizes the rendering logic by:

  • Moving nodeType to the first line for consistency
  • Adding defaultRenderFn configuration pointing to the imported renderer
  • Maintaining clean, declarative node configuration

This follows the intended pattern described in the PR objectives.

packages/kg-default-nodes/lib/nodes/paywall/PaywallNode.js (1)

6-9: LGTM! Consistent refactor implementation.

The configuration correctly:

  • Places nodeType on the first line for standardization
  • Assigns the imported renderPaywallNode to defaultRenderFn
  • Maintains the same clean pattern across node types

The preservation of the static importDOM() method is appropriate since this refactor only targets exportDOM centralization.

packages/kg-default-nodes/lib/nodes/audio/AudioNode.js (1)

6-16: LGTM! Refactor pattern consistently applied.

The AudioNode configuration correctly implements the centralized rendering approach:

  • nodeType standardized on first line
  • Audio-specific properties maintained in the properties array
  • defaultRenderFn properly assigned to the imported renderer function

The implementation maintains consistency with the other refactored nodes.

packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js (1)

6-16: LGTM! Refactor completed successfully.

The EmbedNode configuration correctly implements the centralized rendering pattern:

  • nodeType placed on first line for consistency
  • Embed-specific properties preserved in the configuration
  • defaultRenderFn properly assigned to imported renderer function

This completes the consistent application of the refactor pattern across all reviewed node types.

packages/kg-default-nodes/lib/nodes/header/HeaderNode.js (2)

9-11: LGTM: Clean refactor implementation.

The addition of nodeType parameter to generateDecoratorNode() follows the standardization objective and maintains consistency across the codebase.


37-40:

✅ Verification successful

LGTM: Elegant version-based rendering solution.

The version mapping approach for defaultRenderFn is well-designed, allowing different render functions for v1 and v2 headers. This centralizes the version logic that was previously handled in the exportDOM method.

Please verify that the generateDecoratorNode function has been updated to support the new defaultRenderFn parameter:


🏁 Script executed:

#!/bin/bash
# Description: Verify generateDecoratorNode supports defaultRenderFn parameter
# Expected: Function should handle defaultRenderFn and create appropriate exportDOM method

# Search for generateDecoratorNode implementation
ast-grep --pattern 'function generateDecoratorNode($PARAMS) {
  $$$
}'

# Also search for the function as arrow function or export
rg -A 20 "generateDecoratorNode.*=" 

Length of output: 26970


Confirmed: generateDecoratorNode supports the defaultRenderFn parameter

Verified that in packages/kg-default-nodes/lib/generate-decorator-node.js the function signature includes defaultRenderFn, and its exportDOM method correctly handles both object‐based (versioned) and single‐function renderers:

  • Signature at line 43:
    export function generateDecoratorNode({nodeType, properties = [], defaultRenderFn, version = 1, hasVisibility = false}) {
  • exportDOM implementation at lines 178–184 and 187–193 uses defaultRenderFn[this.__version] when it’s an object and falls back to a direct function call otherwise.

No further changes required.

packages/kg-default-nodes/lib/nodes/product/ProductNode.js (2)

6-7: LGTM: Consistent with refactor pattern.

The nodeType standardization and positioning aligns with the PR objectives for easier regex operations and AI agent compatibility.


19-21: LGTM: Proper rendering function assignment.

The defaultRenderFn implementation correctly replaces the previous exportDOM method with centralized rendering logic.

packages/kg-default-nodes/lib/nodes/signup/SignupNode.js (2)

6-7: LGTM: Standardized node type declaration.

The positioning of nodeType as the first property in the generateDecoratorNode call follows the standardization pattern across all nodes.


24-26: LGTM: Centralized rendering approach.

The defaultRenderFn assignment properly replaces the previous rendering logic, maintaining functionality while simplifying the node definition.

packages/kg-default-nodes/lib/nodes/file/FileNode.js (2)

7-8: LGTM: Consistent standardization.

The nodeType placement follows the established pattern, contributing to the overall consistency of the refactor.


15-17: LGTM: Successful rendering centralization.

The defaultRenderFn implementation completes the refactor pattern, removing the need for individual exportDOM method overrides while maintaining rendering functionality.

packages/kg-default-nodes/lib/nodes/button/ButtonNode.js (1)

6-14: LGTM! The refactoring correctly implements the centralized rendering pattern.

The changes properly:

  • Place nodeType as the first property for consistency
  • Add defaultRenderFn: renderButtonNode to centralize the rendering logic
  • Maintain clean multi-line formatting for better readability
packages/kg-default-nodes/lib/nodes/callout/CalloutNode.js (1)

6-14: LGTM! The CalloutNode refactoring follows the new pattern correctly.

The implementation properly:

  • Standardizes nodeType placement as the first property
  • Adds defaultRenderFn: renderCalloutNode for centralized rendering
  • Maintains consistency with the custom constructor's default values
packages/kg-default-nodes/lib/nodes/bookmark/BookmarkNode.js (1)

6-19: LGTM! The BookmarkNode refactoring successfully implements the centralized rendering pattern.

The changes correctly:

  • Place nodeType as the first property following the standardization
  • Add defaultRenderFn: renderBookmarkNode to delegate rendering to the centralized handler
  • Preserve the complex metadata structure with urlPath properties
packages/kg-default-nodes/lib/nodes/codeblock/CodeBlockNode.js (1)

6-14: LGTM! Clean refactoring that aligns with PR objectives.

The refactoring correctly moves the rendering logic to defaultRenderFn and places nodeType on the first line for better consistency and searchability.

packages/kg-default-nodes/lib/nodes/email/EmailNode.js (1)

5-11: LGTM! Consistent implementation of the refactoring pattern.

The changes correctly implement the centralized rendering approach using defaultRenderFn.

packages/kg-default-nodes/lib/nodes/horizontalrule/HorizontalRuleNode.js (1)

6-9: LGTM! Correct implementation for a node without properties.

The refactoring properly handles the case where a node doesn't require properties, while still implementing the defaultRenderFn pattern.

packages/kg-default-nodes/lib/nodes/collection/CollectionNode.js (1)

6-16: LGTM! Refactoring correctly implemented.

The changes properly implement the centralized rendering pattern with defaultRenderFn.

packages/kg-default-nodes/lib/nodes/html/HtmlNode.js (1)

6-13: Refactoring looks good!

The changes correctly implement the centralized rendering pattern by:

  • Moving nodeType to the first property position for consistency
  • Adding defaultRenderFn: renderHtmlNode to replace the removed exportDOM method
  • Maintaining all existing functionality
packages/kg-default-nodes/lib/nodes/gallery/GalleryNode.js (1)

5-12: Consistent implementation of the refactoring pattern.

The changes follow the same pattern as other nodes:

  • nodeType property standardized to first position
  • defaultRenderFn: renderGalleryNode replaces the removed exportDOM method
  • All functionality preserved
packages/kg-default-nodes/lib/nodes/video/VideoNode.js (1)

5-23: Well-executed refactoring with proper handling of complex properties.

The implementation correctly applies the refactoring pattern while maintaining the node's complexity:

  • nodeType properly positioned first
  • defaultRenderFn: renderVideoNode correctly replaces the removed exportDOM method
  • All properties and custom methods (like exportJSON) remain intact

Comment on lines +6 to +16
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}
]}
) {
],
defaultRenderFn: renderEmbedNode
}) {
Copy link

@coderabbitai coderabbitai bot May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify refactor completion across the entire codebase.

Since this is a systematic refactor affecting multiple node types, let's ensure all nodes have been updated consistently and that no exportDOM methods remain.


🏁 Script executed:

#!/bin/bash
# Description: Verify that the refactor has been applied consistently across all node files
# Expected: All node files should have defaultRenderFn and no exportDOM methods

echo "=== Checking for remaining exportDOM methods ==="
rg -n "exportDOM" packages/kg-default-nodes/lib/nodes/ || echo "✓ No exportDOM methods found"

echo -e "\n=== Checking for defaultRenderFn usage ==="
rg -n "defaultRenderFn:" packages/kg-default-nodes/lib/nodes/ | head -10

echo -e "\n=== Checking generateDecoratorNode usage pattern ==="
rg -A 5 "generateDecoratorNode\(\{" packages/kg-default-nodes/lib/nodes/ | grep -E "(nodeType|defaultRenderFn)" | head -20

echo -e "\n=== Verifying that generateDecoratorNode supports defaultRenderFn ==="
rg -A 10 -B 5 "defaultRenderFn" packages/kg-default-nodes/lib/generate-decorator-node.js

Length of output: 5820


Remove remaining exportDOM overrides from AtLink nodes

We’ve confirmed that two node files still define custom exportDOM methods instead of using defaultRenderFn. Please update them to pass a render function into generateDecoratorNode and remove the overrides:

• packages/kg-default-nodes/lib/nodes/at-link/AtLinkNode.js (line 60)
• packages/kg-default-nodes/lib/nodes/at-link/AtLinkSearchNode.js (line 67)

Suggested steps:

  1. Remove the exportDOM method in each class.
  2. Add the appropriate defaultRenderFn (e.g. renderAtLinkNode / renderAtLinkSearchNode) to the decorator config.
  3. Migrate any custom export logic into those render functions so the built-in exportDOM implementation can be used.

Once done, confirm no other exportDOM overrides remain across packages/kg-default-nodes/lib/nodes.

🤖 Prompt for AI Agents
In packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js lines 6 to 16, the
class EmbedNode uses generateDecoratorNode with a defaultRenderFn but does not
include any exportDOM method override. The review comment suggests removing
custom exportDOM methods from other nodes and ensuring they use
generateDecoratorNode with the appropriate render function. To fix this, verify
that EmbedNode correctly passes the renderEmbedNode function as defaultRenderFn,
remove any existing exportDOM override, and ensure the class relies solely on
generateDecoratorNode for rendering and exporting. Confirm that no exportDOM
methods remain in this file after the update.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of those nodes are decorator nodes and so don't use generateDecoratorNode()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@kevinansfield Thank you for the clarification! Let me verify the structure of those nodes to better understand the distinction between decorator nodes and other node types.


🏁 Script executed:

#!/bin/bash
# Check the structure of AtLinkNode and AtLinkSearchNode to confirm they don't use generateDecoratorNode
echo "=== AtLinkNode.js structure ==="
head -20 packages/kg-default-nodes/lib/nodes/at-link/AtLinkNode.js

echo -e "\n=== AtLinkSearchNode.js structure ==="
head -20 packages/kg-default-nodes/lib/nodes/at-link/AtLinkSearchNode.js

echo -e "\n=== Checking what these nodes extend instead ==="
rg -A 3 "class.*Node extends" packages/kg-default-nodes/lib/nodes/at-link/

echo -e "\n=== Double-checking no generateDecoratorNode usage in at-link nodes ==="
rg "generateDecoratorNode" packages/kg-default-nodes/lib/nodes/at-link/ || echo "✓ No generateDecoratorNode usage found in at-link nodes"

Length of output: 3016


@kevinansfield You're absolutely right! I can see from the code structure that:

  • AtLinkNode extends ElementNode (from Lexical)
  • AtLinkSearchNode extends TextNode (from Lexical)

These are standard Lexical nodes, not decorator nodes created by generateDecoratorNode(). The refactor is specifically for decorator nodes that extend generateDecoratorNode() (which creates classes extending KoenigDecoratorNode).

The AtLink nodes legitimately need their own exportDOM methods and should not be part of this refactor. Thank you for the clarification!


🧬 Code Graph Analysis Results

Relevant snippet from packages/kg-default-nodes/lib/generate-decorator-node.js (lines 43-304)

export function generateDecoratorNode({nodeType, properties = [], defaultRenderFn, version = 1, hasVisibility = false}) {
    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}`};
    });

    // 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({
            name: 'visibility',
            get default() {
                return buildDefaultVisibility();
            },
            privateName: '__visibility'
        });
    }

    class GeneratedDecoratorNode extends KoenigDecoratorNode {
        constructor(data = {}, key) {
            super(key);
            properties.forEach((prop) => {
                if (typeof prop.default === 'boolean') {
                    this[prop.privateName] = data[prop.name] ?? prop.default;
                } else {
                    this[prop.privateName] = data[prop.name] || prop.default;
                }
            });
        }

        static getType() {
            return nodeType;
        }

        static clone(node) {
            return new this(node.getDataset(), node.__key);
        }

        static getPropertyDefaults() {
            return properties.reduce((obj, prop) => {
                obj[prop.name] = prop.default;
                return obj;
            }, {});
        }

        static get urlTransformMap() {
            let map = {};

            properties.forEach((prop) => {
                if (prop.urlType) {
                    if (prop.urlPath) {
                        map[prop.urlPath] = prop.urlType;
                    } else {
                        map[prop.name] = prop.urlType;
                    }
                }
            });

            return map;
        }

        getDataset() {
            const self = this.getLatest();

            let dataset = {};
            properties.forEach((prop) => {
                dataset[prop.name] = self[prop.privateName];
            });

            return dataset;
        }

        static importJSON(serializedNode) {
            const data = {};

            // migrate older nodes that were saved with an earlier version of the visibility format
            serializedNode.visibility = migrateOldVisibilityFormat(serializedNode.visibility);

            properties.forEach((prop) => {
                data[prop.name] = serializedNode[prop.name];
            });

            return new this(data);
        }

        exportJSON() {
            const dataset = {
                type: nodeType,
                version: version,
                ...properties.reduce((obj, prop) => {
                    obj[prop.name] = this[prop.name];
                    return obj;
                }, {})
            };
            return dataset;
        }

        exportDOM(options = {}) {
            if (typeof defaultRenderFn === 'object') {
                const render = defaultRenderFn[this.__version];
                if (!render) {
                    throw new Error(`[generateDecoratorNode] ${nodeType}: "defaultRenderFn" for version ${this.__version} is required`);
                }
                return render(this, options);
            }

            if (!defaultRenderFn) {
                throw new Error(`[generateDecoratorNode] ${nodeType}: "defaultRenderFn" is required`);
            }

            const render = defaultRenderFn;

            return render(this, options);
        }

        createDOM() {
            return document.createElement('div');
        }

        updateDOM() {
            return false;
        }

        isInline() {
            // All our cards are top-level blocks. Override if needed.
            return false;
        }

        hasDynamicData() {
            return false;
        }

        hasEditMode() {
            // Most of our cards have an edit mode. Override if needed.
            return true;
        }

        getTextContent() {
            const self = this.getLatest();
            const propertiesWithText = properties.filter(prop => !!prop.wordCount);

            const text = propertiesWithText.map(
                prop => readTextContent(self, prop.name)
            ).filter(Boolean).join('\n');

            return text ? `${text}\n\n` : '';
        }

        getIsVisibilityActive() {
            if (!properties.some(prop => prop.name === 'visibility')) {
                return false;
            }

            const self = this.getLatest();
            const visibility = self.__visibility;

            return isVisibilityRestricted(visibility);
        }
    }

    properties.forEach((prop) => {
        Object.defineProperty(GeneratedDecoratorNode.prototype, prop.name, {
            get: function () {
                const self = this.getLatest();
                return self[prop.privateName];
            },
            set: function (newVal) {
                const writable = this.getWritable();
                writable[prop.privateName] = newVal;
            }
        });
    });

    return GeneratedDecoratorNode;
}

Relevant snippet from packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js (full file)

/* eslint-disable ghost/filenames/match-exported-class */
import {generateDecoratorNode} from '../../generate-decorator-node';
import {parseEmbedNode} from './embed-parser';
import {renderEmbedNode} from './embed-renderer';

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}
    ],
    defaultRenderFn: renderEmbedNode
}) {
    static importDOM() {
        return parseEmbedNode(this);
    }

    isEmpty() {
        return !this.__url && !this.__html;
    }
}

export const $createEmbedNode = (dataset) => {
    return new EmbedNode(dataset);
};

export function $isEmbedNode(node) {
    return node instanceof EmbedNode;
}

Relevant snippet from packages/kg-default-nodes/lib/nodes/embed/embed-parser.js (lines 4-79)

export function parseEmbedNode(EmbedNode) {
    return {
        figure: (nodeElem) => {
            if (nodeElem.nodeType === 1 && nodeElem.tagName === 'FIGURE') {
                const iframe = nodeElem.querySelector('iframe');
                if (iframe) {
                    return {
                        conversion(domNode) {
                            const payload = _createPayloadForIframe(iframe);

                            if (!payload) {
                                return null;
                            }

                            payload.caption = readCaptionFromElement(domNode);

                            const node = new EmbedNode(payload);
                            return {node};
                        },
                        priority: 1
                    };
                }
                const blockquote = nodeElem.querySelector('blockquote');
                if (blockquote) {
                    return {
                        conversion(domNode) {
                            const link = domNode.querySelector('a');
                            if (!link) {
                                return null;
                            }

                            let 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};

                            // append caption, remove element from blockquote
                            payload.caption = readCaptionFromElement(domNode);
                            let figcaption = domNode.querySelector('figcaption');
                            figcaption?.remove();

                            payload.html = domNode.innerHTML;

                            const node = new EmbedNode(payload);
                            return {node};
                        },
                        priority: 1
                    };
                }
            }
            return null;
        },
        iframe: (nodeElem) => {
            if (nodeElem.nodeType === 1 && nodeElem.tagName === 'IFRAME') {
                return {
                    conversion(domNode) {
                        const payload = _createPayloadForIframe(domNode);

                        if (!payload) {
                            return null;
                        }

                        const node = new EmbedNode(payload);
                        return {node};
                    },
                    priority: 1
                };
            }
            return null;
        }
    };
}

Relevant snippet from packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js (lines 5-16)

export function renderEmbedNode(node, options = {}) {
    addCreateDocumentOption(options);

    const document = options.createDocument();
    const embedType = node.embedType;

    if (embedType === 'twitter') {
        return twitterRenderer(node, document, options);
    }

    return renderTemplate(node, document, options);
}

These snippets provide context on:

  • The EmbedNode class extending a node generated by generateDecoratorNode().
  • The implementation and behavior of generateDecoratorNode(), which creates a subclass of KoenigDecoratorNode.
  • The parsing and rendering logic for embed nodes.
  • The user comment about nodes not being decorator nodes and not using generateDecoratorNode() relates directly to the fact that EmbedNode is created by generateDecoratorNode().

✏️ Learnings added
Learnt from: kevinansfield
PR: TryGhost/Koenig#1531
File: packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js:6-16
Timestamp: 2025-05-26T14:57:48.106Z
Learning: AtLinkNode and AtLinkSearchNode in packages/kg-default-nodes/lib/nodes/at-link/ are not decorator nodes and don't use generateDecoratorNode(). They are a different type of node that legitimately maintains their own exportDOM methods and should not be part of the decorator node refactor.

Learnt from: kevinansfield
PR: TryGhost/Koenig#1531
File: packages/kg-default-nodes/lib/nodes/embed/EmbedNode.js:6-16
Timestamp: 2025-05-26T14:57:48.106Z
Learning: The decorator node refactor in packages/kg-default-nodes only applies to nodes that extend generateDecoratorNode() (which creates KoenigDecoratorNode subclasses). Nodes like AtLinkNode and AtLinkSearchNode that extend standard Lexical nodes (ElementNode, TextNode) are not decorator nodes and legitimately maintain their own exportDOM methods outside of this refactor scope.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

no issue

First part of a refactor to allow node renderers to be provided by the consumer. This change centralizes our `exportDOM()` definition in our `generateDecoratorNode()` factory which means if needed we can select a different renderer function in one place rather than needing to spread knowledge across every node.

- added a required `defaultRenderFn` option to `generateDecoratorNode()`
  - takes a function or an object, the object version should be keyed by version numbers with the appropriate render function as the value
  - adds default `exportDOM()` method that calls `defaultRenderFn` or the appropriate versioned render function
- removed `exportDOM()` overrides from all of our decorator nodes
- standardized on `nodeType:` property being defined on the first line of the properties list in each node which helps with regex search & replace and AI agents
@kevinansfield kevinansfield force-pushed the centralize-export-dom branch from 3e4ae59 to 6991c15 Compare May 26, 2025 14:59
@kevinansfield kevinansfield merged commit 06f4c80 into main May 26, 2025
2 of 3 checks passed
@kevinansfield kevinansfield deleted the centralize-export-dom branch May 26, 2025 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant