Skip to content

Migrate kg-default-nodes to TypeScript#1797

Open
kevinansfield wants to merge 5 commits intomainfrom
ts/kg-default-nodes
Open

Migrate kg-default-nodes to TypeScript#1797
kevinansfield wants to merge 5 commits intomainfrom
ts/kg-default-nodes

Conversation

@kevinansfield
Copy link
Copy Markdown
Member

Summary

  • Renames JS files to TS and migrates kg-default-nodes to TypeScript
  • Removes all any types and eslint-disable comments from source (Visibility interface, set-src-background-from-parent browser types, generate-decorator-node exportJSON)
  • Adds tsconfig.test.json with full test type-checking — all 30+ test files converted from build/cjs to src imports with proper type annotations
  • Fixes pre-existing issues: calltoaction-parser null check, markdown-renderer RenderOptions conflict, typeRoots for hoisted @types
  • Fixes exports map (removes "node" condition), moduleResolutionNodeNext, tseslint.config()defineConfig()
  • Fixes overrides.ts typing with as unknown as pattern
  • Pins sinon to 21.0.3, typescript to 5.8.3, typescript-eslint to 8.57.0

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

Walkthrough

The PR migrates the kg-default-nodes package from a JS/lib+Rollup layout to a TypeScript-first src/build layout. It removes numerous legacy lib files and Rollup config, adds a complete typed src/ tree (nodes, parsers, renderers, serializers, utils), updates package.json and tsconfig files for tsc/tsx builds, replaces the ESLint config with a TypeScript-targeted defineConfig form, and migrates tests and test-utils to ESM/TypeScript (including new type declarations and test bootstrap changes). Several public entrypoint re-exports were moved/removed and many runtime APIs gained explicit TypeScript signatures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ts/kg-default-nodes

Copy link
Copy Markdown

@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: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
packages/kg-default-nodes/src/utils/size-byte-converter.ts (1)

1-25: ⚠️ Potential issue | 🟡 Minor

Handle malformed and out-of-range sizes before formatting.

sizeToBytes('foo KB') currently returns NaN, and bytesToSize(1024 ** 5) returns 1 undefined because the unit index runs past TB. The new type annotations do not protect either runtime value, so imported file metadata can still leak broken text into rendering.

Possible fix
 export function sizeToBytes(size: string) {
     if (!size) {
         return 0;
     }
     const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-    const sizeParts = size.split(' ');
-    const sizeNumber = parseFloat(sizeParts[0]);
-    const sizeUnit = sizeParts[1];
+    const [rawNumber = '', rawUnit = ''] = size.trim().split(/\s+/, 2);
+    const sizeNumber = Number(rawNumber);
+    const sizeUnit = rawUnit === 'Byte' ? 'Bytes' : rawUnit;
     const sizeUnitIndex = sizes.indexOf(sizeUnit);
-    if (sizeUnitIndex === -1) {
+    if (!Number.isFinite(sizeNumber) || sizeNumber < 0 || sizeUnitIndex === -1) {
         return 0;
     }
     return Math.round(sizeNumber * Math.pow(1024, sizeUnitIndex));
 }
 
 export function bytesToSize(bytes: number) {
-    if (!bytes) {
+    if (!Number.isFinite(bytes) || bytes < 1) {
         return '0 Byte';
     }
     const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-    if (bytes === 0) {
-        return '0 Byte';
-    }
-    const i = Math.floor(Math.log(bytes) / Math.log(1024));
+    const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
     return Math.round((bytes / Math.pow(1024, i))) + ' ' + sizes[i];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/size-byte-converter.ts` around lines 1 -
25, sizeToBytes: validate the input string and parsed number before
computing—ensure size is a non-empty string, split yields at least 2 parts,
parseFloat returns a finite number, and the unit is one of the allowed values
(sizes array); if any check fails return 0. bytesToSize: validate bytes is a
finite non-negative number and handle the unit index overflow by clamping the
computed index i to the last index of sizes (so very large values map to 'TB'
instead of undefined); also treat 0 or negative/invalid bytes as '0 Byte'. Use
the existing function names sizeToBytes and bytesToSize and the sizes array to
locate where to add these guards and clamping logic.
packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts (1)

18-40: ⚠️ Potential issue | 🟡 Minor

Coerce nullable DOM reads back to strings before new HeaderNode(payload).

getAttribute() and textContent can still return null, but packages/kg-default-nodes/src/nodes/header/HeaderNode.ts declares these fields as strings. Packing them into Record<string, unknown> only hides the mismatch and lets malformed imports store null into string properties. Use || '' (or omit the key) for the nullable reads before constructing the node.

Also applies to: 57-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts` around
lines 18 - 40, The DOM reads for backgroundImageSrc, header, subheader,
buttonUrl and buttonText can be null but are passed into new HeaderNode(payload)
where those fields are declared as strings; coerce each nullable value to a
string (e.g. using value || '') before building the payload in header-parser.ts
so HeaderNode never receives nulls (apply same fix for the other block
referenced around lines 57-84); ensure you update variables backgroundImageSrc,
header, subheader, buttonUrl and buttonText (or omit keys) prior to constructing
the HeaderNode instance.
packages/kg-default-nodes/src/nodes/button/button-renderer.ts (1)

50-72: ⚠️ Potential issue | 🟠 Major

Escape buttonText and buttonUrl before interpolating email HTML.

packages/kg-default-nodes/src/utils/tagged-template-fns.ts is raw string concatenation, so these branches inject node data straight into innerHTML. A button label containing </& or a crafted URL with quotes will break the markup and creates an HTML-injection sink. Escape the dynamic values first, and ideally whitelist alignment rather than interpolating it raw.

Possible fix
+import {escapeHtml} from '../../utils/escape-html.js';
+
 function emailTemplate(node: ButtonNodeData, options: RenderOptions, document: Document) {
     const {buttonUrl, buttonText} = node;
+    const safeButtonUrl = escapeHtml(buttonUrl);
+    const safeButtonText = escapeHtml(buttonText);
@@
-                                <a href="${buttonUrl}">${buttonText}</a>
+                                <a href="${safeButtonUrl}">${safeButtonText}</a>
@@
-                        <a href="${buttonUrl}">${buttonText}</a>
+                        <a href="${safeButtonUrl}">${safeButtonText}</a>

Also applies to: 97-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/button/button-renderer.ts` around lines
50 - 72, In emailTemplate, avoid injecting raw node.buttonText and
node.buttonUrl into cardHtml and element.innerHTML: HTML-escape buttonText and
properly validate/encode buttonUrl before interpolation, and whitelist
node.alignment to a small set of allowed values (e.g., "left","center","right")
instead of interpolating it raw; then build the string using the escaped values
(or set element.textContent / anchor.href on created elements instead of
innerHTML) and replace the existing innerHTML assignments in emailTemplate and
the other identical branch that also sets innerHTML so dynamic values cannot
break the markup or create an HTML-injection sink.
packages/kg-default-nodes/test/nodes/markdown.test.ts (1)

40-43: ⚠️ Potential issue | 🟡 Minor

Misleading test description: refers to $isImageNode instead of $isMarkdownNode.

The test description says "matches node with $isImageNode" but the test actually verifies $isMarkdownNode. This appears to be a copy-paste error.

📝 Suggested fix
-    it('matches node with $isImageNode', editorTest(function () {
+    it('matches node with $isMarkdownNode', editorTest(function () {
         const markdownNode = $createMarkdownNode(dataset);
         $isMarkdownNode(markdownNode).should.be.true();
     }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/markdown.test.ts` around lines 40 - 43,
The test description is incorrect: update the it(...) description string that
currently reads 'matches node with $isImageNode' to correctly refer to
$isMarkdownNode (or a neutral description like 'matches node with
$isMarkdownNode') so it matches the assertion using $createMarkdownNode and
$isMarkdownNode in the test; ensure the test name reflects the behavior being
verified in the it block.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts (1)

27-31: ⚠️ Potential issue | 🟡 Minor

Type mismatch: imageData types don't align with CallToActionNode interface.

The inline type for imageData specifies imageUrl: string | number and dimensions as string | number | null, but the CallToActionNode interface (from the relevant snippet) defines imageUrl: string | null and dimensions as number | null. This inconsistency could cause type errors downstream.

🔧 Proposed fix to align types
-                        const imageData: {imageUrl: string | number; imageWidth: string | number | null; imageHeight: string | number | null} = {
+                        const imageData: {imageUrl: string; imageWidth: number | null; imageHeight: number | null} = {
                             imageUrl: '',
                             imageWidth: null,
                             imageHeight: null
                         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts`
around lines 27 - 31, The inline type and initial values for the imageData
constant in calltoaction-parser.ts don't match the CallToActionNode interface;
change imageData's type to imageUrl: string | null and imageWidth/imageHeight:
number | null and update the initializer values accordingly (use null for
missing values rather than '' or null-for-numeric mismatches) so imageData
conforms to CallToActionNode.
packages/kg-default-nodes/test/nodes/signup.test.ts (1)

688-694: ⚠️ Potential issue | 🟡 Minor

Potential test bug: Test creates a PaywallNode instead of SignupNode.

The getTextContent test for SignupNode creates a PaywallNode instead of a SignupNode. This appears to be unintentional and tests the wrong node type.

🐛 Proposed fix
 describe('getTextContent', function () {
     it('returns contents', editorTest(function () {
-        const node = $createPaywallNode({});
+        const node = $createSignupNode({});

         // signup nodes don't have text content
         node.getTextContent().should.equal('');
     }));
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/signup.test.ts` around lines 688 - 694,
The test in the describe('getTextContent') block incorrectly instantiates a
PaywallNode via $createPaywallNode; replace that with a SignupNode creation (use
$createSignupNode or the factory used elsewhere for SignupNode) so the test
exercises SignupNode.getTextContent(), keep the assertion that getTextContent()
returns an empty string and leave the surrounding editorTest wrapper intact.
packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts (1)

47-55: ⚠️ Potential issue | 🟡 Minor

Potential runtime error: thumbnail_width and thumbnail_height may be undefined.

The isVideoWithThumbnail check on line 47 only validates thumbnail_url, but line 53 uses non-null assertions on thumbnail_width and thumbnail_height. If these are undefined, the division will produce NaN, causing invalid spacer dimensions.

🛡️ Proposed fix to guard thumbnail dimensions
-        const isVideoWithThumbnail = node.embedType === 'video' && metadata && metadata.thumbnail_url;
+        const isVideoWithThumbnail = node.embedType === 'video' && metadata && metadata.thumbnail_url && metadata.thumbnail_width && metadata.thumbnail_height;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts` around lines 47
- 55, The code computes spacer dimensions using metadata.thumbnail_width and
metadata.thumbnail_height with non-null assertions even though
isVideoWithThumbnail only checked metadata.thumbnail_url; update the logic in
embed-renderer.ts (the isVideoWithThumbnail / email thumbnail branch where
spacerWidth/spacerHeight are computed) to first verify that
metadata.thumbnail_width and metadata.thumbnail_height are present and are
finite numbers before using them, and if not present fall back to safe defaults
or skip creating the thumbnail spacer (e.g., use a default aspect ratio or skip
the spacer calculation) so the division (thumbnail_width/thumbnail_height)
cannot produce NaN or throw at runtime.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

52-60: ⚠️ Potential issue | 🔴 Critical

Fix unanchored color regex allowing attribute injection.

The regex /^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/ lacks proper grouping around the alternation. The first alternative ^[a-zA-Z\d-]+ has no end anchor, so it matches any string starting with alphanumeric characters. This allows payloads like accent" onclick="..." to pass validation and be directly interpolated into HTML attributes (e.g., class="kg-cta-bg-${dataset.backgroundColor}"), creating an attribute injection vulnerability.

Apply the fix to both locations (lines 52-60 and 395-399):

Suggested fix
+const VALID_COLOR_PATTERN = /^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/;
+
 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 || !VALID_COLOR_PATTERN.test(dataset.buttonColor)) {
         dataset.buttonColor = 'accent';
     }

Also apply to the backgroundColor validation (lines 395-399):

-    if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) {
+    if (!dataset.backgroundColor || !VALID_COLOR_PATTERN.test(dataset.backgroundColor)) {
         dataset.backgroundColor = 'white';
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 52 - 60, The regex used to validate dataset.buttonColor is
unanchored around the alternation, allowing values like 'accent" onclick="...'
to pass and enabling attribute injection; update the validation in
ctaCardTemplate to use a single anchored grouped alternation (e.g., require the
whole string to match either a simple token or a hex color) and keep the
existing fallback to 'accent' when invalid, then mirror the same correction for
dataset.backgroundColor validation elsewhere; specifically fix the validation
logic that assigns dataset.buttonColor and dataset.backgroundColor so they use a
grouped/anchored pattern (ensuring the full string is matched) before using the
value in class or style interpolation.
🟡 Minor comments (14)
packages/kg-default-nodes/src/utils/truncate.ts-3-5 (1)

3-5: ⚠️ Potential issue | 🟡 Minor

Guard non-positive length inputs to avoid malformed truncation output.

At Line 5 and in truncateHtml, non-positive maxLength / maxLengthMobile can lead to unintended substring behavior and output that violates expected max-length semantics.

💡 Proposed fix
 export function truncateText(text: string, maxLength: number) {
+    if (maxLength <= 0) {
+        return '';
+    }
     if (text && text.length > maxLength) {
         return text.substring(0, maxLength - 1).trim() + '\u2026';
     } else {
         return text ?? '';
     }
 }

 export function truncateHtml(text: string, maxLength: number, maxLengthMobile?: number) {
+    if (maxLength <= 0) {
+        return '';
+    }
+    if (typeof maxLengthMobile === 'number' && maxLengthMobile <= 0) {
+        return escapeHtml(truncateText(text, maxLength));
+    }
     // If no mobile length specified or mobile length is larger than desktop,
     // just do a simple truncate
     if (!maxLengthMobile || maxLength <= maxLengthMobile) {
         return escapeHtml(truncateText(text, maxLength));
     }

Also applies to: 11-15

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/truncate.ts` around lines 3 - 5, Guard
against non-positive length inputs in truncateText and truncateHtml by returning
an empty string when maxLength (or maxLengthMobile) is <= 0 to avoid calling
substring with a negative length or producing malformed output; add an early
check in truncateText (parameter maxLength) and in truncateHtml (parameter
maxLengthMobile and any other maxLength usages) that returns '' for non-positive
values, otherwise keep the existing substring/ellipsis logic (or use Math.max(0,
maxLength - 1) if you prefer a defensive clamp before substring).
packages/kg-default-nodes/src/utils/rgb-to-hex.ts-8-12 (1)

8-12: ⚠️ Potential issue | 🟡 Minor

Good null check, but incomplete validation for malformed RGB strings.

The null check handles the case where no digits are found, but a malformed string like "rgb(255)" would pass this check and result in g and b being undefined, producing invalid hex output like "#ffNaNNaN".

🛡️ Proposed fix to validate component count
         const match = rgb.match(/\d+/g);
-        if (!match) {
+        if (!match || match.length < 3) {
             return null;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/rgb-to-hex.ts` around lines 8 - 12, The
current null check after const match = rgb.match(/\d+/g) only ensures digits
exist but doesn't validate the count, so malformed inputs like "rgb(255)" yield
undefined g/b; update the rgb-to-hex validation to verify match.length >= 3
before destructuring [r, g, b] (and return null on failure), and ideally
coerce/validate each component is a number within 0–255 (in the function
handling conversion, e.g., the rgbToHex helper that uses match and [r,g,b]) so
you never produce NaN or invalid hex strings.
packages/kg-default-nodes/src/utils/visibility.ts-43-49 (1)

43-49: ⚠️ Potential issue | 🟡 Minor

Potential runtime error from non-null assertions when web or email are undefined.

The isOldVisibilityFormat function uses non-null assertions (visibility.web!, visibility.email!) on lines 46-48, but these are only safe if visibility.web and visibility.email exist. The function checks for their presence using hasOwnProperty on lines 44-45, but if either check fails, the function should short-circuit and return true before reaching the assertions.

However, due to the || chain, if visibility.web exists but visibility.email doesn't, line 48 (visibility.email!.memberSegment) will throw.

🐛 Proposed fix using optional chaining
 export function isOldVisibilityFormat(visibility: Visibility) {
     return !Object.prototype.hasOwnProperty.call(visibility, 'web')
         || !Object.prototype.hasOwnProperty.call(visibility, 'email')
-        || !Object.prototype.hasOwnProperty.call(visibility.web!, 'nonMember')
-        || isNullish(visibility.web!.memberSegment)
-        || isNullish(visibility.email!.memberSegment);
+        || !Object.prototype.hasOwnProperty.call(visibility.web, 'nonMember')
+        || isNullish(visibility.web?.memberSegment)
+        || isNullish(visibility.email?.memberSegment);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/visibility.ts` around lines 43 - 49, The
function isOldVisibilityFormat uses non-null assertions (visibility.web! and
visibility.email!) that can still throw when one of those props is missing;
change the checks to safely access nested props with optional chaining and/or
reorder to short-circuit: keep the hasOwnProperty checks for 'web' and 'email'
and replace uses of visibility.web! and visibility.email! with visibility.web?
and visibility.email? (e.g., check hasOwnProperty.call(visibility.web,
'nonMember') replaced or guarded and use
isNullish(visibility.web?.memberSegment) and
isNullish(visibility.email?.memberSegment)) so the function returns true when
web/email are absent without dereferencing undefined in isOldVisibilityFormat.
packages/kg-default-nodes/src/nodes/image/ImageNode.ts-4-13 (1)

4-13: ⚠️ Potential issue | 🟡 Minor

Type mismatch: width/height are nullable here but non-nullable in ImageNodeData.

The ImageNode interface defines width and height as number | null, but ImageNodeData in image-renderer.ts (lines 10-11) defines them as number. This inconsistency can lead to type errors when passing ImageNode data to the renderer, since the renderer assumes non-null values.

Consider aligning these types—either make ImageNodeData.width/height nullable, or ensure the renderer handles null values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/image/ImageNode.ts` around lines 4 - 13,
The types for image dimensions are inconsistent: ImageNode declares width and
height as number | null while ImageNodeData (in image-renderer.ts) declares them
as number; align them by making ImageNodeData.width and ImageNodeData.height
nullable (number | null) or update the renderer to handle nulls before using
ImageNodeData (e.g., provide fallback values or conditional logic). Locate the
ImageNode interface and the ImageNodeData type in image-renderer.ts and choose
one approach—either change ImageNodeData to number | null to match ImageNode, or
add null-checks/defaults in the renderer where ImageNodeData.width/height are
accessed.
packages/kg-default-nodes/src/nodes/image/image-renderer.ts-132-133 (1)

132-133: ⚠️ Potential issue | 🟡 Minor

Non-null assertion on regex match risks runtime error.

If node.src doesn't match the expected pattern /(.*\/content\/images)\/(.*)/, the ! assertion will cause a runtime error when destructuring. While isLocalContentImage at line 124 provides some validation, it uses a different regex pattern that may not guarantee this match succeeds.

🛡️ Proposed defensive check
-                const [, imagesPath, filename] = node.src.match(/(.*\/content\/images)\/(.*)/)!;
-                img.setAttribute('src', `${imagesPath}/size/w${srcWidth}/${filename}`);
+                const match = node.src.match(/(.*\/content\/images)\/(.*)/);
+                if (match) {
+                    const [, imagesPath, filename] = match;
+                    img.setAttribute('src', `${imagesPath}/size/w${srcWidth}/${filename}`);
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts` around lines 132
- 133, The destructuring uses a non-null assertion on node.src.match(...) which
can crash if the regex doesn't match; replace it by storing the match result in
a variable (e.g., const m = node.src.match(/(.*\/content\/images)\/(.*)/)) and
add a defensive check (if (!m) { handle gracefully — e.g., skip size rewrite,
log a warning, or leave img.src as node.src }) before using m[1]/m[2]; update
the code around the image-rendering logic (the lines that set
img.setAttribute('src', ...)) to use the validated match parts so no runtime
error occurs when the pattern isn't present.
packages/kg-default-nodes/test/serializers/paragraph.test.ts-8-8 (1)

8-8: ⚠️ Potential issue | 🟡 Minor

Describe block name doesn't match filename.

The describe block says 'Serializers: linebreak' but the file is named paragraph.test.ts. This mismatch could cause confusion. Consider renaming either the describe block to match the filename or vice versa.

Suggested fix
-describe('Serializers: linebreak', function () {
+describe('Serializers: paragraph', function () {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts` at line 8, The
describe block string 'Serializers: linebreak' does not match the test filename
paragraph.test.ts; rename the describe block to 'Serializers: paragraph' (or
rename the file to match if intended) so the describe label and filename are
consistent; update the string in the describe(...) call to match
paragraph.test.ts and run tests to confirm no other references need changing.
packages/kg-default-nodes/src/utils/tagged-template-fns.ts-8-10 (1)

8-10: ⚠️ Potential issue | 🟡 Minor

Preserve falsy interpolation values.

Current interpolation on Line 9 converts 0/false to empty string. Use nullish coalescing instead.

🐛 Proposed fix
-    const result = strings.reduce((acc: string, str: string, i: number) => {
-        return acc + str + (values[i] || '');
+    const result = strings.reduce((acc: string, str: string, i: number) => {
+        return acc + str + String(values[i] ?? '');
     }, '');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/tagged-template-fns.ts` around lines 8 -
10, The interpolation reducer in the tagged-template function uses (values[i] ||
'') which drops falsy but valid values like 0 or false; update the reduction
logic in the strings.reduce callback (the expression that computes result) to
use nullish coalescing (values[i] ?? '') so null/undefined become empty strings
while preserving 0 and false.
packages/kg-default-nodes/src/nodes/embed/types/twitter.ts-79-83 (1)

79-83: ⚠️ Potential issue | 🟡 Minor

Non-null assertions on optional fields may cause runtime errors.

public_metrics and created_at are typed as optional in TweetData, but the code uses ! to assert they exist. If Twitter API returns incomplete data, this will throw at runtime.

Consider adding defensive checks or providing fallback values:

🛡️ Suggested defensive handling
-        const retweetCount = numberFormatter.format(tweetData.public_metrics!.retweet_count);
-        const likeCount = numberFormatter.format(tweetData.public_metrics!.like_count);
+        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 tweetTime = DateTime.fromISO(tweetData.created_at!).toLocaleString(DateTime.TIME_SIMPLE);
-        const tweetDate = DateTime.fromISO(tweetData.created_at!).toLocaleString(DateTime.DATE_MED);
+        const tweetTime = tweetData.created_at ? DateTime.fromISO(tweetData.created_at).toLocaleString(DateTime.TIME_SIMPLE) : '';
+        const tweetDate = tweetData.created_at ? DateTime.fromISO(tweetData.created_at).toLocaleString(DateTime.DATE_MED) : '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts` around lines 79 -
83, The code uses non-null assertions on tweetData.public_metrics and
tweetData.created_at when computing retweetCount, likeCount, tweetTime and
tweetDate; replace these assertions with defensive checks: verify
tweetData.public_metrics exists before formatting retweet_count/like_count and
provide default values (e.g., 0 or "0") for retweetCount/likeCount, and verify
tweetData.created_at is present before calling DateTime.fromISO, providing a
fallback string (e.g., empty or "Unknown date") for tweetTime/tweetDate; also
keep the authorUser lookup as-is but handle a potential undefined authorUser
when later used. Ensure you update the logic around retweetCount, likeCount,
tweetTime, and tweetDate to use optional chaining and defaults instead of the !
operator.
packages/kg-default-nodes/src/nodes/embed/types/twitter.ts-92-94 (1)

92-94: ⚠️ Potential issue | 🟡 Minor

Non-null assertion on includes may fail if attachments exist but media is missing.

The condition checks tweetData.attachments?.media_keys but then assumes tweetData.includes!.media[0] exists. If includes is undefined or media is empty, this will throw.

🛡️ Suggested defensive handling
         let tweetImageUrl = null;
         const hasImageOrVideo = tweetData.attachments && tweetData.attachments && tweetData.attachments.media_keys;
-        if (hasImageOrVideo) {
-            tweetImageUrl = tweetData.includes!.media[0].preview_image_url || tweetData.includes!.media[0].url;
+        if (hasImageOrVideo && tweetData.includes?.media?.[0]) {
+            tweetImageUrl = tweetData.includes.media[0].preview_image_url || tweetData.includes.media[0].url;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts` around lines 92 -
94, The code assigns tweetImageUrl using tweetData.includes!.media[0] while only
checking tweetData.attachments; update the check and assignment around
hasImageOrVideo and tweetImageUrl to defensively verify that tweetData.includes
exists and includes.media is a non-empty array (and ideally that a media entry
matching an attachments.media_keys exists) before accessing media[0]; change
references in this block (hasImageOrVideo, tweetImageUrl, and the
tweetData.includes usage) to use optional chaining and length checks or lookup
by media_key so you never dereference includes or media when they are
undefined/empty.
packages/kg-default-nodes/test/nodes/file.test.ts-67-68 (1)

67-68: ⚠️ Potential issue | 🟡 Minor

Type mismatch: fileSize is declared as string but should be number for consistency.

The FileNode interface declares fileSize: string (line 11 of FileNode.ts), yet the file-renderer interface expects fileSize: number (line 11 of file-renderer.ts). The test assignment at line 67 uses a double cast as unknown as string to bypass type checking and assign a number, then asserts it equals the number 123456 at line 68. Additionally, FileNode.ts line 46 converts fileSize to a number before use (Number(this.fileSize)). This type inconsistency should be resolved by either changing the FileNode interface to declare fileSize: number or ensuring all usages treat it consistently as a string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 67 - 68, The
test is forcing a number into FileNode.fileSize despite FileNode declaring
fileSize as string; make fileSize consistently a number across the codebase by
changing the FileNode interface/implementation to declare fileSize: number
(update any constructors or parsing logic that currently converts via
Number(this.fileSize)), update file-renderer expectations if necessary, and
adjust the test in file.test.ts to assign 123456 without casts and assert
equality; ensure all uses of FileNode.fileSize (e.g., in FileNode class methods
and file-renderer) treat it as a number so no runtime Number(...) conversions
are required.
packages/kg-default-nodes/src/nodes/file/FileNode.ts-6-12 (1)

6-12: ⚠️ Potential issue | 🟡 Minor

Type inconsistency: fileSize declared as string but used as number throughout.

The interface declares fileSize: string, but the actual usage suggests it should be number:

  • formattedFileSize calls Number(this.fileSize) (line 46)
  • Tests assign numeric values
  • The property represents byte count, which is naturally numeric

Consider updating the type to number or number | string to match actual usage.

♻️ Suggested interface update
 export interface FileNode {
     src: string;
     fileTitle: string;
     fileCaption: string;
     fileName: string;
-    fileSize: string;
+    fileSize: number;
 }

Note: This would also require updating the property default from '' to 0 or null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/file/FileNode.ts` around lines 6 - 12,
The FileNode interface incorrectly types fileSize as string while code
(formattedFileSize) and tests treat it as a numeric byte count; change the
FileNode declaration to use fileSize: number (or number | string if you need
backward compatibility), update any default initialization from '' to 0 or null,
and adjust related constructors/assignments and tests to pass numeric values;
ensure formattedFileSize no longer needs Number(this.fileSize) if you make it a
number and update any places that assumed a string.
packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts-237-239 (1)

237-239: ⚠️ Potential issue | 🟡 Minor

Redundant createDocument!() call.

A document is already created at Line 215. Creating another emailDoc is unnecessary overhead.

🔧 Proposed fix
     if (options.target === 'email') {
-        const emailDoc = options.createDocument!();
-        const emailDiv = emailDoc.createElement('div');
+        const emailDiv = document.createElement('div');

         emailDiv.innerHTML = emailTemplate(node, options)?.trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts`
around lines 237 - 239, The new call to options.createDocument!() is redundant
because this renderer already creates a document earlier; replace usage of the
newly introduced emailDoc with the existing document variable (the document
created earlier in this renderer) when target === 'email', i.e. create emailDiv
via the existing document.createElement instead of calling
options.createDocument(), and remove the emailDoc variable and its
createDocument invocation (adjust references to emailDiv accordingly); keep
using options.target and options.createDocument where needed only for initial
document creation.
packages/kg-default-nodes/src/nodes/TKNode.ts-17-20 (1)

17-20: ⚠️ Potential issue | 🟡 Minor

Filter empty theme class tokens before classList.add().

config.theme.tk?.split(' ') produces empty strings for cases like '' or 'a b', and classList.add('') throws a SyntaxError per the DOM specification. packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts already filters these tokens (lines 42-43), so TKNode should do the same.

Suggested fix
-        const classes = config.theme.tk?.split(' ') || [];
-        element.classList.add(...classes);
+        const classes = config.theme.tk?.split(' ').filter(Boolean) ?? [];
+        element.classList.add(...classes);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/TKNode.ts` around lines 17 - 20, The
TKNode.createDOM method currently builds classes with config.theme.tk?.split('
') then calls element.classList.add(...classes), which can pass empty strings
and throw; update createDOM (TKNode.createDOM) to filter out empty tokens from
the split (e.g., use .filter(Boolean) or equivalent) before calling
element.classList.add so only non-empty class names are added to the element.
packages/kg-default-nodes/src/nodes/audio/audio-parser.ts-26-30 (1)

26-30: ⚠️ Potential issue | 🟡 Minor

Reject malformed durations instead of storing NaN.

The catch block here won't catch parseInt() failures with string inputs—it silently returns NaN instead. Malformed input like 1:xx will assign NaN to payload.duration. Validate that both parsed values are numbers before assigning.

Suggested fix
                        if (durationText) {
                            const [minutes, seconds = '0'] = durationText.split(':');
-                           try {
-                               payload.duration = parseInt(minutes) * 60 + parseInt(seconds);
-                           } catch {
-                               // ignore duration
-                           }
+                           const parsedMinutes = Number.parseInt(minutes, 10);
+                           const parsedSeconds = Number.parseInt(seconds, 10);
+
+                           if (!Number.isNaN(parsedMinutes) && !Number.isNaN(parsedSeconds)) {
+                               payload.duration = parsedMinutes * 60 + parsedSeconds;
+                           }
                        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/audio/audio-parser.ts` around lines 26 -
30, The current parsing of durationText (in the audio parsing logic where
durationText is split and parsed) can yield NaN because parseInt does not throw;
replace the try/catch approach by explicitly validating the numeric results:
split durationText into minutes and seconds, parse them, then check both parsed
values with Number.isFinite (or !Number.isNaN) before assigning
payload.duration; if validation fails, do not set payload.duration (or remove
it) and handle the malformed input path instead of assigning NaN. Ensure you
update the code around the durationText handling and payload.duration assignment
(the block that destructures [minutes, seconds = '0'] and sets
payload.duration).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4d28d24-3b2a-4673-8324-4dde83b5928c

📥 Commits

Reviewing files that changed from the base of the PR and between 27187b7 and ef572c1.

⛔ Files ignored due to path filters (1)
  • packages/kg-default-nodes/src/nodes/at-link/kg-link.svg is excluded by !**/*.svg
📒 Files selected for processing (161)
  • packages/kg-default-nodes/eslint.config.mjs
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/lib/kg-default-nodes.js
  • packages/kg-default-nodes/lib/nodes/at-link/index.js
  • packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
  • packages/kg-default-nodes/lib/nodes/html/html-parser.js
  • packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
  • packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
  • packages/kg-default-nodes/lib/utils/is-unsplash-image.js
  • packages/kg-default-nodes/package.json
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-parser.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/src/nodes/product/product-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/test/generate-decorator-node.test.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/call-to-action.test.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/test/test-utils/assertions.js
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/index.js
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/test/test-utils/overrides.js
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/test/utils/visibility.test.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/tsconfig.test.json
💤 Files with no reviewable changes (14)
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/lib/nodes/at-link/index.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
  • packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
  • packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
  • packages/kg-default-nodes/lib/utils/is-unsplash-image.js
  • packages/kg-default-nodes/lib/nodes/html/html-parser.js
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/test/test-utils/overrides.js
  • packages/kg-default-nodes/test/test-utils/index.js
  • packages/kg-default-nodes/test/test-utils/assertions.js
  • packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
  • packages/kg-default-nodes/lib/kg-default-nodes.js

Comment thread packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/file/file-renderer.ts
Comment thread packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts
Comment thread packages/kg-default-nodes/src/nodes/image/image-renderer.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
Comment on lines +1 to +2
export const isUnsplashImage = function (url: string) {
return /images\.unsplash\.com/.test(url);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use hostname validation instead of substring matching.

This regex matches anywhere in the string, so non-Unsplash URLs can be misclassified if they contain images.unsplash.com in query/path text. Parse the URL and validate hostname directly.

Proposed fix
 export const isUnsplashImage = function (url: string) {
-    return /images\.unsplash\.com/.test(url);
+    try {
+        return new URL(url).hostname === 'images.unsplash.com';
+    } catch {
+        return false;
+    }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const isUnsplashImage = function (url: string) {
return /images\.unsplash\.com/.test(url);
export const isUnsplashImage = function (url: string) {
try {
return new URL(url).hostname === 'images.unsplash.com';
} catch {
return false;
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/is-unsplash-image.ts` around lines 1 - 2,
Replace the substring/regex check in isUnsplashImage with proper URL hostname
validation: in the isUnsplashImage function, attempt to construct a new URL(url)
inside a try/catch and return false for invalid URLs, then compare
urlObj.hostname strictly to "images.unsplash.com" (or use an appropriate
endsWith check if you want subdomains) instead of testing the string with
/images\.unsplash\.com/ so only actual hosts match.

Comment thread packages/kg-default-nodes/test/nodes/at-link.test.ts Outdated
Comment thread packages/kg-default-nodes/test/nodes/product.test.ts Outdated
Comment thread packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
Copy link
Copy Markdown

@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: 4

♻️ Duplicate comments (4)
packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts (2)

180-182: ⚠️ Potential issue | 🔴 Critical

Avoid unsanitized innerHTML for captions.

Line 181 writes node.caption directly to innerHTML, which is an XSS sink. Sanitize with the shared allowlist utility (preferred) or fall back to textContent if markup is not required.

Minimal safe fallback
-        figcaption.innerHTML = node.caption;
+        figcaption.textContent = node.caption;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines
180 - 182, Replace the unsafe figcaption.innerHTML = node.caption assignment in
gallery-renderer.ts: import and use the shared allowlist sanitizer utility to
sanitize node.caption before setting innerHTML on the created figcaption element
(e.g., sanitized = allowlistSanitize(node.caption); figcaption.innerHTML =
sanitized), and if the allowlist sanitizer is not available or returns
null/empty, fall back to assigning the plain text via figcaption.textContent =
node.caption; ensure the sanitizer function is referenced and imported where the
figcaption is created and handle undefined/null captions safely.

145-147: ⚠️ Potential issue | 🟠 Major

Guard contentImageSizes before retina-size lookup.

Line 147 dereferences options.imageOptimization!.contentImageSizes! even though both are optional in GalleryRenderOptions; this can throw at runtime in email rendering when transform is enabled without size metadata.

Suggested fix
-                if (isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage && options.canTransformImage(image.src)) {
+                const contentImageSizes = options.imageOptimization?.contentImageSizes;
+                if (contentImageSizes && isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage?.(image.src)) {
                     // find available image size next up from 2x600 so we can use it for the "retina" src
-                    const availableImageWidths = getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!);
+                    const availableImageWidths = getAvailableImageWidths(image, contentImageSizes);
                     const srcWidth = availableImageWidths.find(width => width >= 1200);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines
145 - 147, The code in GalleryRenderOptions assumes options.imageOptimization
and contentImageSizes exist when calling getAvailableImageWidths(image,
options.imageOptimization!.contentImageSizes!), which can throw; update the
gallery-renderer.ts flow (around the block using isLocalContentImage(image.src,
options.siteUrl) and getAvailableImageWidths) to first check
options.imageOptimization and options.imageOptimization.contentImageSizes are
defined before performing the retina-size lookup, and if missing skip or
short-circuit the retina logic (use the non-retina src fallback) so
getAvailableImageWidths is never called with undefined; reference the image
variable, options, getAvailableImageWidths and the retina-size lookup in your
change.
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)

12-18: ⚠️ Potential issue | 🟠 Major

Unsafe cast to Element — text/comment nodes lack outerHTML.

The loop assumes all siblings are Elements, but text nodes (whitespace, line breaks) or other comment nodes between the markers would cause outerHTML to return undefined, polluting the HTML output with "undefined" strings.

This issue was flagged in a previous review but appears unresolved.

🐛 Proposed fix
                         while (nextNode && !isHtmlEndComment(nextNode)) {
                             const currentNode = nextNode;
-                            html.push((currentNode as Element).outerHTML);
+                            if (currentNode.nodeType === 1) {
+                                html.push((currentNode as Element).outerHTML);
+                            }
                             nextNode = currentNode.nextSibling;
                             // remove nodes as we go so that they don't go through the parser
                             currentNode.remove();
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 12 -
18, The loop in html-parser.ts assumes every sibling is an Element and uses
(currentNode as Element).outerHTML, which produces "undefined" for text/comment
nodes; update the while loop that uses nextNode/currentNode and isHtmlEndComment
to guard by node type: if currentNode.nodeType === Node.ELEMENT_NODE (or
currentNode instanceof Element) push its outerHTML, else if currentNode.nodeType
=== Node.TEXT_NODE push its textContent (trim or skip if only whitespace), and
skip comment nodes (Node.COMMENT_NODE); keep removing currentNode after handling
it so nodes still don't reach the parser.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

401-416: ⚠️ Potential issue | 🟠 Major

Sanitize sponsorLabel before the email early-return.

The email rendering path (lines 401-408) returns before sponsorLabel is sanitized (lines 412-416). This means the email template at line 405 receives raw, unsanitized sponsorLabel content, while the web template gets sanitized content.

Move the sanitization before the email branch:

🐛 Proposed fix
+    if (dataset.hasSponsorLabel) {
+        const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div'));
+        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
+        dataset.sponsorLabel = cleanedHtml || '';
+    }
+
     if (options.target === 'email') {
         const emailDoc = options.createDocument!();
         const emailDiv = emailDoc.createElement('div');

         emailDiv.innerHTML = emailCTATemplate(dataset, options);

         return renderWithVisibility({element: emailDiv.firstElementChild as RenderOutput['element']}, node.visibility as Visibility, options);
     }

     const element = document.createElement('div');

-    if (dataset.hasSponsorLabel) {
-        const cleanBasicHtml = buildCleanBasicHtmlForElement(element);
-        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
-        dataset.sponsorLabel = cleanedHtml || '';
-    }
     const htmlString = ctaCardTemplate(dataset);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 401 - 416, The email rendering path returns before sanitizing
dataset.sponsorLabel, so the emailCTATemplate receives unsanitized HTML; move
the sponsorLabel sanitization (use buildCleanBasicHtmlForElement and
cleanBasicHtml) to occur before the options.target === 'email' branch and assign
the sanitized string back to dataset.sponsorLabel, then proceed to call
emailCTATemplate and renderWithVisibility (preserving node.visibility and
options) so both email and web paths use the cleaned sponsorLabel.
🧹 Nitpick comments (10)
packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts (1)

47-50: Consistent DOMConverterFn type pattern.

This type definition mirrors the one in ExtendedTextNode.ts. Consider extracting this shared type to a common utilities module if more extended nodes are added in the future.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts` around lines 47 -
50, The DOMConverterFn type is duplicated (see DOMConverterFn and
patchParagraphConversion) and should be extracted to a shared utility to keep
types consistent across extended nodes; create a new exported type (e.g.,
DOMConverterFn) in a common utils/types module, replace the inline
DOMConverterFn declaration in this file and in ExtendedTextNode by importing
that shared type, and update any references in patchParagraphConversion and
related conversion functions to use the imported type.
packages/kg-default-nodes/src/nodes/product/product-renderer.ts (2)

54-61: Consider the innerHTML security context.

The static analysis flags the innerHTML assignment as a potential XSS vector. In this CMS renderer context, the risk is mitigated because:

  1. Data originates from the Ghost editor/database (trusted input)
  2. Content sanitization typically occurs at write time in CMS architectures

However, if any of the template data fields (productUrl, productTitle, productButton, productDescription, productImageSrc) could contain user-controlled content that bypasses editor sanitization, this would be exploitable.

If stricter security is desired, consider sanitizing URL attributes with a URL validator or encoding HTML entities for text content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines
54 - 61, The assignment to element.innerHTML using htmlString (produced by
emailCardTemplate or cardTemplate) is flagged as a potential XSS vector; ensure
template data fields (productUrl, productTitle, productButton,
productDescription, productImageSrc) are sanitized/validated before rendering by
either validating/normalizing URLs (e.g., allow only http(s) and data:image for
productImageSrc/productUrl) and HTML-encoding any free-text fields, or by
switching to safe DOM creation rather than innerHTML; update the rendering flow
in product-renderer.ts so that htmlString is produced from sanitized
templateData (or replace element.innerHTML usage with
element.appendChild/fromDocumentFragment built using
createElement/setAttribute/textContent) while keeping selection between
emailCardTemplate and cardTemplate.

96-105: Type assertions are acceptable but consider stronger typing in future.

The as number assertions on lines 98-99 and 102 are necessary because the truthiness check doesn't narrow Record<string, unknown> to a specific type. This pattern is common when working with loosely-typed data structures.

For future improvement, consider defining a more specific interface for template data (e.g., ProductTemplateData with productImageWidth?: number) to eliminate the need for runtime type assertions. This would be a broader refactor across renderers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines
96 - 105, The code uses runtime type assertions (data.productImageWidth as
number, data.productImageHeight as number) because Record<string, unknown>
prevents proper type narrowing; to fix, introduce a stronger type for the
template data (e.g., interface ProductTemplateData { productImageWidth?: number;
productImageHeight?: number; ... }) and annotate the renderer function parameter
so `data` is ProductTemplateData, then remove the `as number` casts and rely on
the existing truthiness checks before using `imageDimensions` and calling
getResizedImageDimensions; alternatively, if changing the parameter type is not
possible right now, add a small type guard function (e.g., isNumber) and use it
to narrow data.productImageWidth/data.productImageHeight before assigning to
imageDimensions to avoid unsafe assertions.
packages/kg-default-nodes/src/nodes/signup/SignupNode.ts (1)

108-110: Consider tightening the parameter type for better type safety.

The function accepts Record<string, unknown> but passes it to a constructor expecting SignupData. This type mismatch allows invalid property types to slip through at compile time (e.g., labels as a non-array).

If the loose type is intentional for parsing flexibility from external sources, consider documenting it or adding minimal runtime validation for critical fields.

💡 Option: Use SignupData for stricter typing
-export const $createSignupNode = (dataset: Record<string, unknown>) => {
+export const $createSignupNode = (dataset: SignupData) => {
     return new SignupNode(dataset);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts` around lines 108 -
110, The factory $createSignupNode currently types its parameter as
Record<string, unknown> but immediately passes it to new SignupNode which
expects SignupData; tighten the parameter to SignupData (replace the dataset
type) to ensure compile-time safety, or if loose parsing is required, add
minimal runtime validation inside $createSignupNode to coerce/verify critical
fields (e.g., ensure labels is an array) and document why Record<string,
unknown> is used; update references to SignupData, $createSignupNode, and
SignupNode accordingly so the constructor always receives a valid SignupData
object.
packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts (1)

114-116: Consider stricter parameter typing for $createBookmarkNode.

The function accepts Record<string, unknown> but passes it to a constructor expecting BookmarkData. This reduces type safety—callers can pass any shape without compile-time validation.

If you want to preserve flexibility for deserialization scenarios while improving type safety for direct usage:

♻️ Suggested improvement
-export const $createBookmarkNode = (dataset: Record<string, unknown>) => {
+export const $createBookmarkNode = (dataset: BookmarkData) => {
     return new BookmarkNode(dataset);
 };

Callers with Record<string, unknown> data can cast explicitly: $createBookmarkNode(data as BookmarkData).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines
114 - 116, The $createBookmarkNode function currently accepts a loose
Record<string, unknown> but passes it to the BookmarkNode constructor which
expects BookmarkData; tighten the signature to accept BookmarkData (or
BookmarkData | Record<string, unknown> if you must preserve deserialization
flexibility) and, if allowing the union, perform an explicit cast/validation
before calling new BookmarkNode(dataset) so callers get compile-time safety;
update the $createBookmarkNode parameter type and any callers that construct
nodes from known BookmarkData to pass BookmarkData (or cast deserialized objects
with `as BookmarkData`) and ensure the constructor argument to BookmarkNode is
the properly typed BookmarkData.
packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts (1)

8-13: The recommendation to use the renderer's exported options type is not feasible. The @tryghost/kg-markdown-html-renderer package does not export a TypeScript options type—RenderOptions is internal and inaccessible. The cast to Record<string, unknown> on line 19 is necessary because MarkdownRenderOptions includes properties (createDocument, dom, target) that the render() function doesn't accept (it only supports ghostVersion). A better alternative would be to extract only the relevant options before passing them to render(), or to acknowledge the type boundary and document the cast intent with a comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts` around
lines 8 - 13, The custom MarkdownRenderOptions includes createDocument, dom, and
target which aren't accepted by the external render() (internal RenderOptions
only accepts ghostVersion), so remove the unsafe broad cast: extract only the
supported options (e.g., const { ghostVersion } = options) or build a new object
containing just the allowed keys before calling render(), and update the call
site that currently casts to Record<string, unknown> to pass this filtered
object; if you must keep the cast, replace it with a short comment explaining
the intentional type boundary and why createDocument/dom/target are excluded.
packages/kg-default-nodes/test/test-utils/index.ts (1)

6-16: Consider adding error handling for Prettier formatting.

Prettier.format() can throw if the input HTML is severely malformed. While test inputs are typically controlled, wrapping this in a try-catch or documenting this behavior would improve robustness.

That said, for test utilities where inputs are controlled, this is acceptable as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/test-utils/index.ts` around lines 6 - 16, The
html test helper calls Prettier.format(output, {parser: 'html'}) which can throw
on malformed input; wrap the Prettier.format call in a try-catch inside the html
function (or explicitly document the potential exception) so tests don't crash
on formatting errors—on catch, return the raw output or a safe fallback and
optionally log or rethrow with additional context mentioning the html helper and
the failing output so the failure is clear.
packages/kg-default-nodes/test/nodes/paywall.test.ts (1)

90-94: Consider a more targeted type assertion for exportDOM.

The as unknown as LexicalEditor cast is a broad escape hatch. The exportDOM method signature expects a LexicalEditor, but you're passing render options. This works at runtime because the decorator node's exportDOM implementation uses these options, but the type mismatch suggests the base type definition may need adjustment upstream.

For now, this is acceptable as a migration workaround, but consider creating a dedicated type or interface for export options to avoid the double cast pattern across test files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/paywall.test.ts` around lines 90 - 94,
The test is using a broad double-cast (as unknown as LexicalEditor) when calling
paywallNode.exportDOM; instead create a narrow mock or interface that matches
what exportDOM actually reads instead of pretending to be a full LexicalEditor:
either define a small ExportDOMOptions/RenderExportOptions type that describes
the properties exportDOM uses and cast exportOptions to that, or construct a
minimal mock object implementing just the methods/properties the
$createPaywallNode().exportDOM(exportOptions) implementation accesses, then pass
that instead of casting to LexicalEditor; update the test to call exportDOM with
this targeted mock/interface so you remove the double-cast while keeping
behavior unchanged.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

136-142: Non-null assertion on match() at line 138 may throw if regex doesn't match.

The ! assertion assumes the regex will always match. While isLocalContentImage likely validates that the URL contains /content/images/, if that assumption is violated, this will throw a runtime error.

Consider adding a defensive check:

♻️ Suggested defensive fix
         if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) {
-            const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/)!;
+            const match = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
+            if (!match) {
+                // URL doesn't match expected format, skip transformation
+                return;
+            }
+            const [, imagesPath, filename] = match;
             const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256};
             dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 136 - 142, The code uses a non-null assertion on
dataset.imageUrl.match(...) inside the block that checks
isLocalContentImage(...) and options.canTransformImage(...), which can still
throw if the regex doesn't match; update the transformation in
calltoaction-renderer.ts to first assign the match result to a variable, verify
it's not null before destructuring (or bail out/leave dataset.imageUrl
unchanged), and only build the optimized URL when the match succeeds; reference
the dataset.imageUrl match, the isLocalContentImage(...) guard, and the iconSize
calculation so the logic flow remains the same but safe against missing regex
matches.
packages/kg-default-nodes/src/generate-decorator-node.ts (1)

241-253: Consider adding a type guard for versioned renderer lookup.

The version-based renderer lookup uses number indexing but nodeVersion could be a number or come from this.__version. The current cast as number works but the logic assumes version is always a valid key.

💡 Optional: Add defensive check for missing versioned renderer

The current code throws a helpful error when the versioned renderer is missing, which is good. The implementation handles this case properly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 241 -
253, The node renderer lookup assumes nodeVersion is a valid numeric key; add a
type guard and normalize nodeVersion before indexing into nodeRenderers:
retrieve nodeVersion from the local variable or this.__version, coerce/validate
it to a finite number or string key (e.g., ensure typeof nodeVersion ===
'number' && Number.isFinite(nodeVersion) or convert from string safely), then
use that normalized key when accessing (render as
Record<string|number,RenderFn>)[normalizedVersion]; keep the existing error
throw if the versioned renderer is missing and update references to
nodeVersion/this.__version and the local render variable in
generateDecoratorNode to use the validated normalizedVersion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The package.json scripts ("build", "prepare", "pretest")
run tsc but do not clean stale artifacts; modify each script to remove/clean the
build directory before running tsc (e.g., run a cross-platform cleaner like
rimraf build or rm -rf build && mkdir -p build) so old JS/.d.ts files are
deleted before emitting, then run the existing tsc commands and the echo step;
update "build", "prepare", and "pretest" to include this pre-clean step (or add
a separate "clean" script and invoke it from those scripts).

In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts`:
- Around line 51-55: The code in the isEmail && isVideoWithThumbnail branch uses
non-null assertions on metadata.thumbnail_width and metadata.thumbnail_height
(line computing thumbnailAspectRatio) which can be missing or zero and lead to
division-by-zero or invalid spacerHeight; update the guard in that block to
verify metadata.thumbnail_width and metadata.thumbnail_height are present and >
0 before computing thumbnailAspectRatio and spacerHeight, and if they are
absent/invalid either skip thumbnail sizing logic (avoid computing spacerHeight)
or use a safe default aspect ratio (e.g., 16/9) so spacerHeight and spacerWidth
are always valid; adjust the calculations that set spacerHeight/spacerWidth (and
any downstream use) accordingly to reference the validated values.

In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts`:
- Around line 19-23: The code assigns unsanitized HTML from
render(node.markdown) directly to element.innerHTML in markdown-renderer.ts,
creating an XSS risk; fix by sanitizing the rendered HTML (e.g., run the output
of render(...) through a sanitizer like DOMPurify before assigning to
element.innerHTML) or, if only plain text is needed, set element.textContent to
node.markdown instead; update the code paths around render, node.markdown and
element.innerHTML to use the sanitizer API (or textContent) and add a short
comment documenting the trust assumption if you opt to keep raw HTML.

In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts`:
- Around line 16-21: The VideoRenderOptions interface currently makes postUrl
optional which allows target === 'email' with postUrl undefined; change
VideoRenderOptions to a discriminated union (e.g., one variant where target:
'email' and postUrl: string, and another for other targets where postUrl?:
string) so TypeScript enforces postUrl when target is 'email'; update any
references or function signatures that accept VideoRenderOptions (e.g., the
function that uses emailCardTemplate) to use the new union type so the places
that interpolate postUrl into href must have a defined string.

---

Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 401-416: The email rendering path returns before sanitizing
dataset.sponsorLabel, so the emailCTATemplate receives unsanitized HTML; move
the sponsorLabel sanitization (use buildCleanBasicHtmlForElement and
cleanBasicHtml) to occur before the options.target === 'email' branch and assign
the sanitized string back to dataset.sponsorLabel, then proceed to call
emailCTATemplate and renderWithVisibility (preserving node.visibility and
options) so both email and web paths use the cleaned sponsorLabel.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts`:
- Around line 180-182: Replace the unsafe figcaption.innerHTML = node.caption
assignment in gallery-renderer.ts: import and use the shared allowlist sanitizer
utility to sanitize node.caption before setting innerHTML on the created
figcaption element (e.g., sanitized = allowlistSanitize(node.caption);
figcaption.innerHTML = sanitized), and if the allowlist sanitizer is not
available or returns null/empty, fall back to assigning the plain text via
figcaption.textContent = node.caption; ensure the sanitizer function is
referenced and imported where the figcaption is created and handle
undefined/null captions safely.
- Around line 145-147: The code in GalleryRenderOptions assumes
options.imageOptimization and contentImageSizes exist when calling
getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!),
which can throw; update the gallery-renderer.ts flow (around the block using
isLocalContentImage(image.src, options.siteUrl) and getAvailableImageWidths) to
first check options.imageOptimization and
options.imageOptimization.contentImageSizes are defined before performing the
retina-size lookup, and if missing skip or short-circuit the retina logic (use
the non-retina src fallback) so getAvailableImageWidths is never called with
undefined; reference the image variable, options, getAvailableImageWidths and
the retina-size lookup in your change.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 12-18: The loop in html-parser.ts assumes every sibling is an
Element and uses (currentNode as Element).outerHTML, which produces "undefined"
for text/comment nodes; update the while loop that uses nextNode/currentNode and
isHtmlEndComment to guard by node type: if currentNode.nodeType ===
Node.ELEMENT_NODE (or currentNode instanceof Element) push its outerHTML, else
if currentNode.nodeType === Node.TEXT_NODE push its textContent (trim or skip if
only whitespace), and skip comment nodes (Node.COMMENT_NODE); keep removing
currentNode after handling it so nodes still don't reach the parser.

---

Nitpick comments:
In `@packages/kg-default-nodes/src/generate-decorator-node.ts`:
- Around line 241-253: The node renderer lookup assumes nodeVersion is a valid
numeric key; add a type guard and normalize nodeVersion before indexing into
nodeRenderers: retrieve nodeVersion from the local variable or this.__version,
coerce/validate it to a finite number or string key (e.g., ensure typeof
nodeVersion === 'number' && Number.isFinite(nodeVersion) or convert from string
safely), then use that normalized key when accessing (render as
Record<string|number,RenderFn>)[normalizedVersion]; keep the existing error
throw if the versioned renderer is missing and update references to
nodeVersion/this.__version and the local render variable in
generateDecoratorNode to use the validated normalizedVersion.

In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts`:
- Around line 114-116: The $createBookmarkNode function currently accepts a
loose Record<string, unknown> but passes it to the BookmarkNode constructor
which expects BookmarkData; tighten the signature to accept BookmarkData (or
BookmarkData | Record<string, unknown> if you must preserve deserialization
flexibility) and, if allowing the union, perform an explicit cast/validation
before calling new BookmarkNode(dataset) so callers get compile-time safety;
update the $createBookmarkNode parameter type and any callers that construct
nodes from known BookmarkData to pass BookmarkData (or cast deserialized objects
with `as BookmarkData`) and ensure the constructor argument to BookmarkNode is
the properly typed BookmarkData.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 136-142: The code uses a non-null assertion on
dataset.imageUrl.match(...) inside the block that checks
isLocalContentImage(...) and options.canTransformImage(...), which can still
throw if the regex doesn't match; update the transformation in
calltoaction-renderer.ts to first assign the match result to a variable, verify
it's not null before destructuring (or bail out/leave dataset.imageUrl
unchanged), and only build the optimized URL when the match succeeds; reference
the dataset.imageUrl match, the isLocalContentImage(...) guard, and the iconSize
calculation so the logic flow remains the same but safe against missing regex
matches.

In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts`:
- Around line 47-50: The DOMConverterFn type is duplicated (see DOMConverterFn
and patchParagraphConversion) and should be extracted to a shared utility to
keep types consistent across extended nodes; create a new exported type (e.g.,
DOMConverterFn) in a common utils/types module, replace the inline
DOMConverterFn declaration in this file and in ExtendedTextNode by importing
that shared type, and update any references in patchParagraphConversion and
related conversion functions to use the imported type.

In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts`:
- Around line 8-13: The custom MarkdownRenderOptions includes createDocument,
dom, and target which aren't accepted by the external render() (internal
RenderOptions only accepts ghostVersion), so remove the unsafe broad cast:
extract only the supported options (e.g., const { ghostVersion } = options) or
build a new object containing just the allowed keys before calling render(), and
update the call site that currently casts to Record<string, unknown> to pass
this filtered object; if you must keep the cast, replace it with a short comment
explaining the intentional type boundary and why createDocument/dom/target are
excluded.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 54-61: The assignment to element.innerHTML using htmlString
(produced by emailCardTemplate or cardTemplate) is flagged as a potential XSS
vector; ensure template data fields (productUrl, productTitle, productButton,
productDescription, productImageSrc) are sanitized/validated before rendering by
either validating/normalizing URLs (e.g., allow only http(s) and data:image for
productImageSrc/productUrl) and HTML-encoding any free-text fields, or by
switching to safe DOM creation rather than innerHTML; update the rendering flow
in product-renderer.ts so that htmlString is produced from sanitized
templateData (or replace element.innerHTML usage with
element.appendChild/fromDocumentFragment built using
createElement/setAttribute/textContent) while keeping selection between
emailCardTemplate and cardTemplate.
- Around line 96-105: The code uses runtime type assertions
(data.productImageWidth as number, data.productImageHeight as number) because
Record<string, unknown> prevents proper type narrowing; to fix, introduce a
stronger type for the template data (e.g., interface ProductTemplateData {
productImageWidth?: number; productImageHeight?: number; ... }) and annotate the
renderer function parameter so `data` is ProductTemplateData, then remove the
`as number` casts and rely on the existing truthiness checks before using
`imageDimensions` and calling getResizedImageDimensions; alternatively, if
changing the parameter type is not possible right now, add a small type guard
function (e.g., isNumber) and use it to narrow
data.productImageWidth/data.productImageHeight before assigning to
imageDimensions to avoid unsafe assertions.

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 108-110: The factory $createSignupNode currently types its
parameter as Record<string, unknown> but immediately passes it to new SignupNode
which expects SignupData; tighten the parameter to SignupData (replace the
dataset type) to ensure compile-time safety, or if loose parsing is required,
add minimal runtime validation inside $createSignupNode to coerce/verify
critical fields (e.g., ensure labels is an array) and document why
Record<string, unknown> is used; update references to SignupData,
$createSignupNode, and SignupNode accordingly so the constructor always receives
a valid SignupData object.

In `@packages/kg-default-nodes/test/nodes/paywall.test.ts`:
- Around line 90-94: The test is using a broad double-cast (as unknown as
LexicalEditor) when calling paywallNode.exportDOM; instead create a narrow mock
or interface that matches what exportDOM actually reads instead of pretending to
be a full LexicalEditor: either define a small
ExportDOMOptions/RenderExportOptions type that describes the properties
exportDOM uses and cast exportOptions to that, or construct a minimal mock
object implementing just the methods/properties the
$createPaywallNode().exportDOM(exportOptions) implementation accesses, then pass
that instead of casting to LexicalEditor; update the test to call exportDOM with
this targeted mock/interface so you remove the double-cast while keeping
behavior unchanged.

In `@packages/kg-default-nodes/test/test-utils/index.ts`:
- Around line 6-16: The html test helper calls Prettier.format(output, {parser:
'html'}) which can throw on malformed input; wrap the Prettier.format call in a
try-catch inside the html function (or explicitly document the potential
exception) so tests don't crash on formatting errors—on catch, return the raw
output or a safe fallback and optionally log or rethrow with additional context
mentioning the html helper and the failing output so the failure is clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 135dc1ea-e8d5-49ae-bf9a-a3a3d15f4faf

📥 Commits

Reviewing files that changed from the base of the PR and between ef572c1 and 25f1493.

📒 Files selected for processing (150)
  • packages/kg-default-nodes/eslint.config.mjs
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/package.json
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-parser.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/src/nodes/product/product-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/test/generate-decorator-node.test.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/call-to-action.test.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/test/utils/visibility.test.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/tsconfig.test.json
  • packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js
💤 Files with no reviewable changes (2)
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/rollup.config.mjs
✅ Files skipped from review due to trivial changes (27)
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/tsconfig.test.json
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
🚧 Files skipped from review as they are similar to previous changes (90)
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts

Comment thread packages/kg-default-nodes/package.json
Comment thread packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts
Comment thread packages/kg-default-nodes/src/nodes/video/video-renderer.ts Outdated
Copy link
Copy Markdown

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/kg-default-nodes/test/nodes/signup.test.ts (1)

688-695: ⚠️ Potential issue | 🟡 Minor

Bug: Wrong node type tested in getTextContent test.

The test creates a PaywallNode via $createPaywallNode({}) but the comment says "signup nodes don't have text content" and this is within the SignupNode test suite. This should likely be testing SignupNode:

🐛 Proposed fix
     describe('getTextContent', function () {
         it('returns contents', editorTest(function () {
-            const node = $createPaywallNode({});
+            const node = $createSignupNode(dataset);
 
             // signup nodes don't have text content
             node.getTextContent().should.equal('');
         }));
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/signup.test.ts` around lines 688 - 695,
The test in the SignupNode suite is creating the wrong node type: replace the
call to $createPaywallNode({}) with the Signup node factory
($createSignupNode({})) so the getTextContent test actually instantiates a
SignupNode (refer to symbols $createPaywallNode and $createSignupNode and the
getTextContent test block) and keep the existing assertion that signup nodes
return an empty string.
packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)

22-31: ⚠️ Potential issue | 🟠 Major

Parse image dimensions to numbers in the parser.

The getAttribute('width') and getAttribute('height') methods return string | null, but the ProductNode interface declares productImageWidth and productImageHeight as number | null. With TypeScript strict mode enabled, this type mismatch will cause compilation errors when the payload is passed to the ProductNode constructor.

Use parseInt() to convert the string values to numbers before assignment:

Fix: Parse to numbers in the parser
                         if (img.getAttribute('width')) {
-                            payload.productImageWidth = img.getAttribute('width');
+                            payload.productImageWidth = parseInt(img.getAttribute('width')!, 10);
                         }

                         if (img.getAttribute('height')) {
-                            payload.productImageHeight = img.getAttribute('height');
+                            payload.productImageHeight = parseInt(img.getAttribute('height')!, 10);
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` around lines
22 - 31, The parser in product-parser.ts assigns
img.getAttribute('width'/'height') (string|null) directly to
payload.productImageWidth/productImageHeight which are typed as number|null;
update the assignment to parse the attribute strings to numbers (e.g., use
parseInt or Number) and handle null/NaN by setting the fields to null when no
valid numeric value exists so the payload matches the ProductNode types (locate
assignments where img.getAttribute(...) is read and replace with
parsed-and-validated numeric values).
♻️ Duplicate comments (4)
packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts (2)

145-148: ⚠️ Potential issue | 🟠 Major

Guard contentImageSizes before retina-src computation.

At Line 147, getAvailableImageWidths can throw when options.imageOptimization?.contentImageSizes is absent.

🔧 Suggested fix
-                if (isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage && options.canTransformImage(image.src)) {
+                const contentImageSizes = options.imageOptimization?.contentImageSizes;
+                if (contentImageSizes && isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage?.(image.src)) {
                     // find available image size next up from 2x600 so we can use it for the "retina" src
-                    const availableImageWidths = getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!);
+                    const availableImageWidths = getAvailableImageWidths(image, contentImageSizes);
                     const srcWidth = availableImageWidths.find(width => width >= 1200);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines
145 - 148, The code calls getAvailableImageWidths(image,
options.imageOptimization!.contentImageSizes!) without guarding that
options.imageOptimization and its contentImageSizes exist, which can throw;
update the condition around the retina-src computation (the block using
isLocalContentImage(...) and calculating srcWidth) to first check
options.imageOptimization && options.imageOptimization.contentImageSizes (or use
optional chaining) before calling getAvailableImageWidths, and if absent either
skip the retina-src logic or use a safe fallback so getAvailableImageWidths is
never invoked with undefined; refer to isLocalContentImage,
getAvailableImageWidths, options.imageOptimization.contentImageSizes and the
srcWidth usage when making the change.

1-8: ⚠️ Potential issue | 🔴 Critical

Sanitize caption before assigning to innerHTML.

Line 181 inserts node.caption directly as HTML, which can enable scriptable markup injection.

🔧 Suggested fix
 import {renderEmptyContainer} from '../../utils/render-empty-container.js';
+import {buildCleanBasicHtmlForElement} from '../../utils/build-clean-basic-html-for-element.js';
@@
     if (node.caption) {
         const figcaption = document.createElement('figcaption');
-        figcaption.innerHTML = node.caption;
+        const cleanBasicHtml = buildCleanBasicHtmlForElement(figcaption);
+        const cleanedCaption = cleanBasicHtml(node.caption);
+        figcaption.innerHTML = cleanedCaption || '';
         figure.appendChild(figcaption);
         figure.setAttribute('class', `${figure.getAttribute('class')} kg-card-hascaption`);
     }

Also applies to: 179-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines
1 - 8, The caption string (node.caption) is being inserted directly into
element.innerHTML (around the gallery renderer logic), which allows scriptable
markup injection; change the code that assigns node.caption to the DOM so it
either sets element.textContent / uses createTextNode or passes node.caption
through a trusted HTML sanitizer (e.g., DOMPurify.sanitize) before assigning to
innerHTML; implement a small helper (e.g., sanitizeCaption) and use it where
node.caption is currently used so all caption insertions are sanitized
consistently.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

401-407: ⚠️ Potential issue | 🔴 Critical

Sanitize sponsorLabel before the email early-return path.

At Line 401, the email branch returns before the sanitizer at Line 412, so Line 405 can render raw dataset.sponsorLabel.

🔧 Suggested fix
 export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) {
     addCreateDocumentOption(options);
     const document = options.createDocument!();
@@
     if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) {
         dataset.backgroundColor = 'white';
     }
 
+    if (dataset.hasSponsorLabel) {
+        const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div'));
+        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
+        dataset.sponsorLabel = cleanedHtml || '';
+    }
+
     if (options.target === 'email') {
         const emailDoc = options.createDocument!();
         const emailDiv = emailDoc.createElement('div');
@@
-    if (dataset.hasSponsorLabel) {
-        const cleanBasicHtml = buildCleanBasicHtmlForElement(element);
-        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
-        dataset.sponsorLabel = cleanedHtml || '';
-    }
     const htmlString = ctaCardTemplate(dataset);

Also applies to: 412-416

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 401 - 407, The email branch returns before the sponsor label
sanitizer runs, so ensure dataset.sponsorLabel is sanitized before you build the
email HTML and return: call the existing sanitizer (the one currently used later
in this file) to produce a safeSponsorLabel and use that when calling
emailCTATemplate(datasetWithSafeSponsor, options) or otherwise substitute
dataset.sponsorLabel with the sanitized value; do the same fix for the other
branch that uses raw sponsorLabel (the block around renderWithVisibility/DOM
rendering) so all paths use the sanitized sponsor label instead of the raw
dataset.sponsorLabel.
packages/kg-default-nodes/package.json (1)

19-21: ⚠️ Potential issue | 🟠 Major

Still unresolved: clean build/ before recompiling.

tsc will not remove outputs for renamed or deleted sources. Because this package now publishes the whole build/ tree, stale JS or .d.ts files can survive a local rebuild and get packed.

🧹 Suggested script update
   "scripts": {
     "dev": "tsc --watch --preserveWatchOutput",
-    "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
-    "prepare": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
-    "pretest": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json",
+    "clean": "node -e \"require('node:fs').rmSync('build', {recursive: true, force: true})\"",
+    "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
+    "prepare": "yarn build",
+    "pretest": "yarn build && tsc -p tsconfig.test.json",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/package.json` around lines 19 - 21, The build
scripts ("build", "prepare", "pretest") leave stale artifacts because tsc
doesn't delete removed/renamed outputs; update each script to remove the
existing build/ directory before running the TypeScript compiles (e.g., call a
"clean" step or invoke rimraf/rm -rf build) so the sequence becomes clean then
tsc && tsc -p tsconfig.cjs.json ...; add a "clean" script if using rimraf and
ensure "build", "prepare", and "pretest" all invoke that clean step first to
guarantee a fresh build tree.
🧹 Nitpick comments (10)
packages/kg-default-nodes/src/nodes/aside/AsideParser.ts (1)

4-7: Consider making NodeClass immutable.

Line 4 can be readonly since it is only assigned in the constructor, which tightens class invariants.

♻️ Suggested tweak
-    NodeClass: {new (): LexicalNode};
+    readonly NodeClass: {new (): LexicalNode};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/aside/AsideParser.ts` around lines 4 - 7,
Make the NodeClass property immutable: change the class field declaration of
NodeClass to be readonly (e.g., readonly NodeClass: { new(): LexicalNode })
since it is only assigned in the constructor; update the class field declaration
and keep the assignment in the constructor unchanged (refer to NodeClass and the
constructor in AsideParser).
packages/kg-default-nodes/test/serializers/linebreak.test.ts (1)

47-50: Type assertions are correct but repetitive.

The as ElementNode casts are necessary since $generateNodesFromDOM returns LexicalNode[], and getChildren() is only available on ElementNode. However, casting the same node multiple times is verbose.

Consider extracting to a typed variable for readability:

♻️ Optional: Extract repeated casts to a variable
                should.equal(nodes.length, 1);
                should.equal(nodes[0].getType(), 'paragraph');
-               should.equal((nodes[0] as ElementNode).getChildren().length, 3);
-               should.equal((nodes[0] as ElementNode).getChildren()[0].getType(), 'extended-text');
-               should.equal((nodes[0] as ElementNode).getChildren()[1].getType(), 'linebreak');
-               should.equal((nodes[0] as ElementNode).getChildren()[2].getType(), 'extended-text');
+               const paragraph = nodes[0] as ElementNode;
+               should.equal(paragraph.getChildren().length, 3);
+               should.equal(paragraph.getChildren()[0].getType(), 'extended-text');
+               should.equal(paragraph.getChildren()[1].getType(), 'linebreak');
+               should.equal(paragraph.getChildren()[2].getType(), 'extended-text');

This same pattern applies to other test cases (lines 63-65, 70-72, 82-84, 87-89) where the cast is repeated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/serializers/linebreak.test.ts` around lines 47
- 50, The repeated casts like (nodes[0] as ElementNode) are verbose; replace
each group of repeated casts by extracting a typed variable (e.g., const element
= nodes[0] as ElementNode) and use element.getChildren() and
element.getChildren()[i].getType() in the assertions; do the same refactor for
the other test blocks that repeat the cast (the subsequent assertions
referencing getChildren()), keeping the same assertion logic but using the
single typed variable to improve readability.
packages/kg-default-nodes/test/nodes/email.test.ts (1)

158-159: Consider defining a dedicated type for export options.

The as unknown as LexicalEditor double-cast pattern is used repeatedly to pass exportOptions to exportDOM(). While this works at runtime and is consistent with similar test files in this PR, it completely bypasses type safety.

If this pattern is prevalent across many test files, consider defining a shared type (e.g., ExportDOMOptions) that accurately describes the options object, and updating the exportDOM method signature or creating a test helper to avoid the double-cast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/email.test.ts` around lines 158 - 159,
The test uses a double-cast (as unknown as LexicalEditor) when calling
emailNode.exportDOM({...exportOptions, ...options}) which bypasses type safety;
create a shared type (e.g., ExportDOMOptions) that models the options passed to
exportDOM and update the test to cast to that type (or add a small test helper
that returns a properly typed object) and, if appropriate, adjust the exportDOM
signature to accept ExportDOMOptions instead of forcing a LexicalEditor cast;
update references to exportOptions, emailNode.exportDOM, and any other tests
using this pattern to use the new ExportDOMOptions or helper so the double-cast
is removed.
packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts (1)

47-47: Consider adding a brief comment explaining the double cast.

The as unknown as pattern is used twice here to bridge DOM type differences between the standard Document and the BrowserDocument type expected by buildSrcBackgroundScript. While necessary for this migration, a brief inline comment would help future maintainers understand why the workaround exists.

💡 Suggested comment
-    figure.insertAdjacentElement('beforeend', buildSrcBackgroundScript(document as unknown as Parameters<typeof buildSrcBackgroundScript>[0]) as unknown as Element);
+    // Cast needed: document type differs from BrowserDocument expected by buildSrcBackgroundScript
+    figure.insertAdjacentElement('beforeend', buildSrcBackgroundScript(document as unknown as Parameters<typeof buildSrcBackgroundScript>[0]) as unknown as Element);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts` at
line 47, The double cast using "as unknown as" when calling
buildSrcBackgroundScript(document ...) is a necessary DOM-type workaround but
lacks explanation; add a succinct inline comment next to the call that explains
why Document is being cast to the BrowserDocument type expected by
buildSrcBackgroundScript (e.g., to bridge differing DOM typings during
migration) and reference the involved symbols (buildSrcBackgroundScript and the
insertion into figure via insertAdjacentElement) so future maintainers
understand the type workaround.
packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)

90-92: Mutating the original DOM element's src may cause side effects.

Assigning to iframe.src modifies the passed DOM element in place. If the parser is called during live DOM processing (e.g., drag-and-drop or copy-paste), this could unexpectedly alter the original element.

Consider working with the value instead:

♻️ Suggested refactor
-    // if it's a schemaless URL, convert to https
-    if (iframe.src.match(/^\/\//)) {
-        iframe.src = `https:${iframe.src}`;
-    }
+    // if it's a schemaless URL, convert to https
+    let src = iframe.src;
+    if (src.match(/^\/\//)) {
+        src = `https:${src}`;
+    }

     const payload: Record<string, unknown> = {
-        url: iframe.src
+        url: src
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 90 -
92, The code mutates the passed DOM element by assigning to iframe.src; instead,
read and normalize the value into a local variable (e.g., const src = iframe.src
or let normalizedSrc = iframe.src), if it matches /^\/\// prefix prepend
"https:" to that local variable, and use the normalizedSrc for further
parsing/return values without writing back to iframe.src; update the logic in
embed-parser.ts (the block handling iframe and iframe.src) to avoid modifying
the original iframe element.
packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts (1)

9-15: Consolidate duplicated RenderOptions into a shared type.

This RenderOptions shape is repeated in multiple renderers (for example, packages/kg-default-nodes/src/nodes/button/button-renderer.ts Lines 12-18). Centralizing it would reduce drift and maintenance overhead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts` around lines 9
- 15, The RenderOptions interface declared in toggle-renderer.ts is duplicated
across renderers (e.g., button-renderer.ts); extract it to a single shared
exported type (e.g., export type RenderOptions = { createDocument?: () =>
Document; dom?: { window: { document: Document } }; target?: string; feature?: {
emailCustomization?: boolean; emailCustomizationAlpha?: boolean }; [key:
string]: unknown }) in a common module (shared types file) and update
toggle-renderer.ts and other renderers to import and use this shared
RenderOptions type instead of declaring it inline (ensure the symbol name
RenderOptions is used where referenced).
packages/kg-default-nodes/test/nodes/tk.test.ts (2)

109-115: Same pattern as noted above.

The cast as TKNode on line 111 follows the same pattern as lines 36/41 and could similarly be removed if the return type of $createTKNode is correctly inferred as TKNode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/tk.test.ts` around lines 109 - 115,
Remove the unnecessary cast on the test's $createTKNode call: instead of casting
the result to TKNode when calling TKNode.clone, make $createTKNode's return type
correctly inferred as TKNode (or update its type signature) so the cast can be
dropped; locate uses in this test (the $createTKNode call and TKNode.clone
invocation) and remove the "as TKNode" cast, or adjust the $createTKNode
declaration so its return type is TKNode to avoid needing casts.

34-42: Remove redundant type casts.

The $createTKNode function returns TKNode (inferred from $applyNodeReplacement(new TKNode(text))), so the casts on lines 36 and 41 are unnecessary. This is evident from line 47, where tkNode.exportJSON() is called without any cast.

♻️ Remove redundant casts
     it('is a text entity', editorTest(function () {
         const tkNode = $createTKNode('TK');
-        (tkNode as TKNode).isTextEntity().should.be.true();
+        tkNode.isTextEntity().should.be.true();
     }));

     it('can not insert text before', editorTest(function () {
         const tkNode = $createTKNode('TK');
-        (tkNode as TKNode).canInsertTextBefore().should.be.false();
+        tkNode.canInsertTextBefore().should.be.false();
     }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/tk.test.ts` around lines 34 - 42, Remove
the redundant casts to TKNode: $createTKNode already returns a TKNode so drop
the (tkNode as TKNode) casts in the tests where isTextEntity() and
canInsertTextBefore() are called; change calls to tkNode.isTextEntity() and
tkNode.canInsertTextBefore() (leave other usages like tkNode.exportJSON()
untouched).
packages/kg-default-nodes/src/generate-decorator-node.ts (2)

32-52: JSDoc is missing the urlPath property.

The DecoratorNodeProperty interface includes urlPath?: string (line 49), but the JSDoc above doesn't document this property. Consider updating the JSDoc for completeness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 32 -
52, The JSDoc for DecoratorNodeProperty is missing the urlPath property; update
the comment block above the interface to add an `@property` for urlPath (e.g.,
`@property` {string} [urlPath] - optional path segment used when the property
contains a URL or to resolve relative URLs) so the documented properties match
the DecoratorNodeProperty interface (which currently declares urlPath?: string).

222-233: Acceptable use of @ts-expect-error for migration.

The comment indicates this is part of the strict mode migration. The return type Record<string, unknown> with type and version properties may not perfectly match the parent class signature. Consider documenting what the expected mismatch is or planning to resolve this post-migration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 222 -
233, The exportJSON method uses `@ts-expect-error` because its declared return
Record<string, unknown> (built from nodeType, version and internalProps) doesn't
match the parent signature; update exportJSON in the class to return the correct
type or add an explicit, narrow cast to the parent return type to remove the
error (referencing exportJSON, nodeType, version, and internalProps), or add a
short inline comment explaining the specific shape mismatch and plan to fix the
parent type post-migration so the `@ts-expect-error` can be removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/package.json`:
- Line 18: The "dev" script currently only runs "tsc --watch
--preserveWatchOutput" which performs a simple TS transpile and misses emitting
the published layout (build/cjs/index.js and build/esm/package.json); update the
"dev" script entry named "dev" in package.json to run the TypeScript build mode
in watch (e.g. use "tsc --build --watch --preserveWatchOutput" or "tsc -b
--watch --preserveWatchOutput") so project references and the packaging steps
run on start and produce the correct build/cjs and build/esm outputs immediately
after a clean checkout.

In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts`:
- Around line 40-41: The figcaption assignment currently uses
figcaption.innerHTML = node.caption which is an XSS sink; change it to set plain
text instead (e.g., use figcaption.textContent or document.createTextNode) so
captions are rendered as text rather than raw HTML, and if HTML captions must be
supported ensure they are explicitly sanitized before assignment; update the
code in codeblock-renderer.ts where figcaption is created and assigned (look for
figcaption.innerHTML and replace with a safe text assignment or sanitized HTML).

In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts`:
- Around line 79-95: Several optional Tweet fields are being dereferenced
directly; guard them first to avoid runtime errors by using null/undefined
checks or optional chaining and sensible fallbacks: read
tweetData.public_metrics via tweetData.public_metrics?.retweet_count and
tweetData.public_metrics?.like_count with defaults before calling
numberFormatter (affecting retweetCount and likeCount), find authorUser only if
tweetData.users and tweetData.author_id exist, call DateTime.fromISO for
tweetTime/tweetDate only when tweetData.created_at is present, derive
mentions/urls/hashtags from tweetData.entities with default empty arrays as
currently done, and compute tweetImageUrl only after verifying
tweetData.attachments/media and tweetData.includes?.media[0] exist (affecting
hasImageOrVideo and tweetImageUrl); update those expressions in this file
(variables: retweetCount, likeCount, authorUser, tweetTime, tweetDate,
hasImageOrVideo, tweetImageUrl) to use optional chaining/null checks and
defaults.

In `@packages/kg-default-nodes/src/nodes/file/file-parser.ts`:
- Line 16: The value assigned to fileSize in file-parser.ts is a number from
sizeToBytes but FileNode.fileSize is typed as string and the formattedFileSize
getter expects a string; change the assignment used when constructing the
FileNode (the const fileSize local and/or the value passed into the FileNode) to
String(sizeToBytes(...)) so FileNode.fileSize receives a string (refer to the
fileSize const and the FileNode/interface and formattedFileSize getter to locate
the assignment).

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 96-104: The code assumes data.productImageWidth and
data.productImageHeight are numbers by using "as number", which can hide string
values from the parser and cause runtime bugs; update the product renderer logic
(references: productImageWidth, productImageHeight, imageDimensions,
getResizedImageDimensions) to defensively coerce and validate these values
(e.g., Number(...) or parseInt/parseFloat) before using them, check for NaN or
non-finite results and skip/handle assignment if invalid, and use the validated
numeric value when comparing to 560 and when calling getResizedImageDimensions
so comparisons and resizing operate on real numbers.
- Around line 56-61: The template interpolation in product-renderer.ts currently
injects unescaped user fields (productTitle, productDescription, productUrl,
productButton) into htmlString before assigning to element.innerHTML; import
escapeHtml from ../../utils/escape-html.js at the top and wrap every
user-supplied value passed into the cardTemplate and email template with
escapeHtml (use escaped productTitle, productDescription, productButton for text
nodes and escapeHtml(productUrl) for href attributes) so the returned element
(from the function that builds htmlString and returns {element:
element.firstElementChild}) receives safe, escaped content consistent with
file-renderer.ts.

In `@packages/kg-default-nodes/src/utils/tagged-template-fns.ts`:
- Around line 8-10: The reduce callback building `result` currently uses
(values[i] || '') which treats valid falsy values like 0 and false as empty;
change that fallback to nullish coalescing so only null/undefined become '' —
i.e., update the reducer expression in the strings.reduce callback (the code
that computes `result`) to use (values[i] ?? '') instead of (values[i] || '').

In `@packages/kg-default-nodes/test/nodes/toggle.test.ts`:
- Line 168: The exportDOM calls on toggleNode are incorrectly casting
exportOptions to LexicalEditor (e.g., toggleNode.exportDOM(exportOptions as
unknown as LexicalEditor)); remove the unnecessary and unsafe "as unknown as
LexicalEditor" casts and call exportDOM with the options object directly
(toggleNode.exportDOM(exportOptions)), keeping the final type assertion to
{element: HTMLElement} as needed; update all occurrences where exportDOM is
called with that double-cast (the calls using exportOptions) to use the direct
call instead.

---

Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts`:
- Around line 22-31: The parser in product-parser.ts assigns
img.getAttribute('width'/'height') (string|null) directly to
payload.productImageWidth/productImageHeight which are typed as number|null;
update the assignment to parse the attribute strings to numbers (e.g., use
parseInt or Number) and handle null/NaN by setting the fields to null when no
valid numeric value exists so the payload matches the ProductNode types (locate
assignments where img.getAttribute(...) is read and replace with
parsed-and-validated numeric values).

In `@packages/kg-default-nodes/test/nodes/signup.test.ts`:
- Around line 688-695: The test in the SignupNode suite is creating the wrong
node type: replace the call to $createPaywallNode({}) with the Signup node
factory ($createSignupNode({})) so the getTextContent test actually instantiates
a SignupNode (refer to symbols $createPaywallNode and $createSignupNode and the
getTextContent test block) and keep the existing assertion that signup nodes
return an empty string.

---

Duplicate comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The build scripts ("build", "prepare", "pretest") leave
stale artifacts because tsc doesn't delete removed/renamed outputs; update each
script to remove the existing build/ directory before running the TypeScript
compiles (e.g., call a "clean" step or invoke rimraf/rm -rf build) so the
sequence becomes clean then tsc && tsc -p tsconfig.cjs.json ...; add a "clean"
script if using rimraf and ensure "build", "prepare", and "pretest" all invoke
that clean step first to guarantee a fresh build tree.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 401-407: The email branch returns before the sponsor label
sanitizer runs, so ensure dataset.sponsorLabel is sanitized before you build the
email HTML and return: call the existing sanitizer (the one currently used later
in this file) to produce a safeSponsorLabel and use that when calling
emailCTATemplate(datasetWithSafeSponsor, options) or otherwise substitute
dataset.sponsorLabel with the sanitized value; do the same fix for the other
branch that uses raw sponsorLabel (the block around renderWithVisibility/DOM
rendering) so all paths use the sanitized sponsor label instead of the raw
dataset.sponsorLabel.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts`:
- Around line 145-148: The code calls getAvailableImageWidths(image,
options.imageOptimization!.contentImageSizes!) without guarding that
options.imageOptimization and its contentImageSizes exist, which can throw;
update the condition around the retina-src computation (the block using
isLocalContentImage(...) and calculating srcWidth) to first check
options.imageOptimization && options.imageOptimization.contentImageSizes (or use
optional chaining) before calling getAvailableImageWidths, and if absent either
skip the retina-src logic or use a safe fallback so getAvailableImageWidths is
never invoked with undefined; refer to isLocalContentImage,
getAvailableImageWidths, options.imageOptimization.contentImageSizes and the
srcWidth usage when making the change.
- Around line 1-8: The caption string (node.caption) is being inserted directly
into element.innerHTML (around the gallery renderer logic), which allows
scriptable markup injection; change the code that assigns node.caption to the
DOM so it either sets element.textContent / uses createTextNode or passes
node.caption through a trusted HTML sanitizer (e.g., DOMPurify.sanitize) before
assigning to innerHTML; implement a small helper (e.g., sanitizeCaption) and use
it where node.caption is currently used so all caption insertions are sanitized
consistently.

---

Nitpick comments:
In `@packages/kg-default-nodes/src/generate-decorator-node.ts`:
- Around line 32-52: The JSDoc for DecoratorNodeProperty is missing the urlPath
property; update the comment block above the interface to add an `@property` for
urlPath (e.g., `@property` {string} [urlPath] - optional path segment used when
the property contains a URL or to resolve relative URLs) so the documented
properties match the DecoratorNodeProperty interface (which currently declares
urlPath?: string).
- Around line 222-233: The exportJSON method uses `@ts-expect-error` because its
declared return Record<string, unknown> (built from nodeType, version and
internalProps) doesn't match the parent signature; update exportJSON in the
class to return the correct type or add an explicit, narrow cast to the parent
return type to remove the error (referencing exportJSON, nodeType, version, and
internalProps), or add a short inline comment explaining the specific shape
mismatch and plan to fix the parent type post-migration so the `@ts-expect-error`
can be removed.

In `@packages/kg-default-nodes/src/nodes/aside/AsideParser.ts`:
- Around line 4-7: Make the NodeClass property immutable: change the class field
declaration of NodeClass to be readonly (e.g., readonly NodeClass: { new():
LexicalNode }) since it is only assigned in the constructor; update the class
field declaration and keep the assignment in the constructor unchanged (refer to
NodeClass and the constructor in AsideParser).

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts`:
- Around line 90-92: The code mutates the passed DOM element by assigning to
iframe.src; instead, read and normalize the value into a local variable (e.g.,
const src = iframe.src or let normalizedSrc = iframe.src), if it matches /^\/\//
prefix prepend "https:" to that local variable, and use the normalizedSrc for
further parsing/return values without writing back to iframe.src; update the
logic in embed-parser.ts (the block handling iframe and iframe.src) to avoid
modifying the original iframe element.

In `@packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts`:
- Around line 9-15: The RenderOptions interface declared in toggle-renderer.ts
is duplicated across renderers (e.g., button-renderer.ts); extract it to a
single shared exported type (e.g., export type RenderOptions = {
createDocument?: () => Document; dom?: { window: { document: Document } };
target?: string; feature?: { emailCustomization?: boolean;
emailCustomizationAlpha?: boolean }; [key: string]: unknown }) in a common
module (shared types file) and update toggle-renderer.ts and other renderers to
import and use this shared RenderOptions type instead of declaring it inline
(ensure the symbol name RenderOptions is used where referenced).

In `@packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts`:
- Line 47: The double cast using "as unknown as" when calling
buildSrcBackgroundScript(document ...) is a necessary DOM-type workaround but
lacks explanation; add a succinct inline comment next to the call that explains
why Document is being cast to the BrowserDocument type expected by
buildSrcBackgroundScript (e.g., to bridge differing DOM typings during
migration) and reference the involved symbols (buildSrcBackgroundScript and the
insertion into figure via insertAdjacentElement) so future maintainers
understand the type workaround.

In `@packages/kg-default-nodes/test/nodes/email.test.ts`:
- Around line 158-159: The test uses a double-cast (as unknown as LexicalEditor)
when calling emailNode.exportDOM({...exportOptions, ...options}) which bypasses
type safety; create a shared type (e.g., ExportDOMOptions) that models the
options passed to exportDOM and update the test to cast to that type (or add a
small test helper that returns a properly typed object) and, if appropriate,
adjust the exportDOM signature to accept ExportDOMOptions instead of forcing a
LexicalEditor cast; update references to exportOptions, emailNode.exportDOM, and
any other tests using this pattern to use the new ExportDOMOptions or helper so
the double-cast is removed.

In `@packages/kg-default-nodes/test/nodes/tk.test.ts`:
- Around line 109-115: Remove the unnecessary cast on the test's $createTKNode
call: instead of casting the result to TKNode when calling TKNode.clone, make
$createTKNode's return type correctly inferred as TKNode (or update its type
signature) so the cast can be dropped; locate uses in this test (the
$createTKNode call and TKNode.clone invocation) and remove the "as TKNode" cast,
or adjust the $createTKNode declaration so its return type is TKNode to avoid
needing casts.
- Around line 34-42: Remove the redundant casts to TKNode: $createTKNode already
returns a TKNode so drop the (tkNode as TKNode) casts in the tests where
isTextEntity() and canInsertTextBefore() are called; change calls to
tkNode.isTextEntity() and tkNode.canInsertTextBefore() (leave other usages like
tkNode.exportJSON() untouched).

In `@packages/kg-default-nodes/test/serializers/linebreak.test.ts`:
- Around line 47-50: The repeated casts like (nodes[0] as ElementNode) are
verbose; replace each group of repeated casts by extracting a typed variable
(e.g., const element = nodes[0] as ElementNode) and use element.getChildren()
and element.getChildren()[i].getType() in the assertions; do the same refactor
for the other test blocks that repeat the cast (the subsequent assertions
referencing getChildren()), keeping the same assertion logic but using the
single typed variable to improve readability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c072553-4f9f-416a-bd25-5546754776be

📥 Commits

Reviewing files that changed from the base of the PR and between 25f1493 and c1fdfd3.

⛔ Files ignored due to path filters (1)
  • packages/kg-default-nodes/src/nodes/at-link/kg-link.svg is excluded by !**/*.svg
📒 Files selected for processing (161)
  • packages/kg-default-nodes/eslint.config.mjs
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/lib/kg-default-nodes.js
  • packages/kg-default-nodes/lib/nodes/at-link/index.js
  • packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
  • packages/kg-default-nodes/lib/nodes/html/html-parser.js
  • packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
  • packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
  • packages/kg-default-nodes/lib/utils/is-unsplash-image.js
  • packages/kg-default-nodes/package.json
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-parser.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/src/nodes/product/product-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/test/generate-decorator-node.test.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/call-to-action.test.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/test/test-utils/assertions.js
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/index.js
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/test/test-utils/overrides.js
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/test/utils/visibility.test.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/tsconfig.test.json
💤 Files with no reviewable changes (14)
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
  • packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
  • packages/kg-default-nodes/lib/utils/is-unsplash-image.js
  • packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/lib/nodes/at-link/index.js
  • packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
  • packages/kg-default-nodes/test/test-utils/assertions.js
  • packages/kg-default-nodes/test/test-utils/index.js
  • packages/kg-default-nodes/lib/kg-default-nodes.js
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/lib/nodes/html/html-parser.js
  • packages/kg-default-nodes/test/test-utils/overrides.js
✅ Files skipped from review due to trivial changes (40)
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/tsconfig.test.json
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
🚧 Files skipped from review as they are similar to previous changes (75)
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/test/generate-decorator-node.test.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts

Comment thread packages/kg-default-nodes/package.json
Comment thread packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts
Comment thread packages/kg-default-nodes/src/nodes/embed/types/twitter.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/file/file-parser.ts
Comment thread packages/kg-default-nodes/src/nodes/product/product-renderer.ts
Comment thread packages/kg-default-nodes/src/nodes/product/product-renderer.ts Outdated
Comment thread packages/kg-default-nodes/src/utils/tagged-template-fns.ts
Comment thread packages/kg-default-nodes/test/nodes/toggle.test.ts
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 96.81296% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.66%. Comparing base (85a0f34) to head (134858b).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
...ges/kg-default-nodes/src/nodes/ExtendedTextNode.ts 53.33% 7 Missing ⚠️
...-nodes/src/utils/set-src-background-from-parent.ts 73.91% 6 Missing ⚠️
...s/kg-default-nodes/src/nodes/file/file-renderer.ts 86.20% 2 Missing and 2 partials ⚠️
.../kg-default-nodes/src/nodes/embed/types/twitter.ts 95.52% 3 Missing ⚠️
...lt-nodes/src/utils/get-resized-image-dimensions.ts 62.50% 2 Missing and 1 partial ⚠️
packages/kg-default-nodes/src/utils/truncate.ts 50.00% 3 Missing ⚠️
.../kg-default-nodes/src/nodes/ExtendedHeadingNode.ts 83.33% 2 Missing ⚠️
...efault-nodes/src/nodes/bookmark/bookmark-parser.ts 92.00% 2 Missing ⚠️
...kg-default-nodes/src/nodes/button/button-parser.ts 80.00% 2 Missing ⚠️
...ges/kg-default-nodes/src/nodes/file/file-parser.ts 80.00% 2 Missing ⚠️
... and 24 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1797      +/-   ##
==========================================
+ Coverage   86.13%   86.66%   +0.52%     
==========================================
  Files         194      195       +1     
  Lines       13177    14042     +865     
  Branches     1962     2085     +123     
==========================================
+ Hits        11350    12169     +819     
- Misses       1810     1844      +34     
- Partials       17       29      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)

25-31: ⚠️ Potential issue | 🟠 Major

Parse width/height attributes into numbers before assigning.

Lines 26 and 30 assign string DOM attributes (img.getAttribute('width') and img.getAttribute('height')) directly into fields typed as number | null in the ProductNode interface. This creates a runtime type mismatch and leaks incorrect types into the node payload.

💡 Proposed fix
                        if (img && img.getAttribute('src')) {
                            payload.productImageSrc = img.getAttribute('src');

-                            if (img.getAttribute('width')) {
-                                payload.productImageWidth = img.getAttribute('width');
-                            }
+                            const widthAttr = img.getAttribute('width');
+                            if (widthAttr) {
+                                const width = Number.parseInt(widthAttr, 10);
+                                if (!Number.isNaN(width)) {
+                                    payload.productImageWidth = width;
+                                }
+                            }

-                            if (img.getAttribute('height')) {
-                                payload.productImageHeight = img.getAttribute('height');
-                            }
+                            const heightAttr = img.getAttribute('height');
+                            if (heightAttr) {
+                                const height = Number.parseInt(heightAttr, 10);
+                                if (!Number.isNaN(height)) {
+                                    payload.productImageHeight = height;
+                                }
+                            }
                        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` around lines
25 - 31, The code assigns string DOM attributes directly to numeric payload
fields: when reading img.getAttribute('width') and img.getAttribute('height') in
product-parser.ts (the logic that sets payload.productImageWidth and
payload.productImageHeight), parse the attribute values into numbers (e.g., via
Number(...) or parseInt(..., 10)) and only assign if the result is a finite
number; otherwise set the fields to null to preserve the ProductNode type
(number | null) and avoid NaN leaks.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

52-56: ⚠️ Potential issue | 🔴 Critical

Fix color-token validation: current regex is bypassable and email path skips buttonColor validation.

Line 54 and Line 397 use a partially anchored regex, so invalid strings can still pass if they start with allowed chars. Those values are then interpolated into HTML attributes/styles (Line 99+, Line 220, Line 333, Line 353), which can enable attribute/style injection. Also, buttonColor validation runs only in ctaCardTemplate(), so the email path never gets that check.

Suggested hardening patch
+const COLOR_TOKEN_RE = /^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/;
+
+function sanitizeColorToken(value: string, fallback: string) {
+    return COLOR_TOKEN_RE.test(value) ? value : fallback;
+}
+
 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})$/)) {
-        dataset.buttonColor = 'accent';
-    }
     const buttonAccent = dataset.buttonColor === 'accent' ? 'kg-style-accent' : '';
@@
 export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) {
@@
-    if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) {
-        dataset.backgroundColor = 'white';
-    }
+    dataset.buttonColor = sanitizeColorToken(dataset.buttonColor, 'accent');
+    dataset.backgroundColor = sanitizeColorToken(dataset.backgroundColor, 'white');

Also applies to: 96-102, 397-399

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 52 - 56, The buttonColor validation is insecure and inconsistently
applied: replace the partially-anchored regex in ctaCardTemplate with a
fully-anchored pattern that only allows either an allowed token (letters,
digits, hyphen) or a hex color (`#abc` or `#aabbcc`) (e.g.
^(?:[A-Za-z0-9-]+|#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))$), and centralize the
check into a single helper (e.g., validateButtonColor) called from
ctaCardTemplate and the email-generation path so all interpolations (places
referencing dataset.buttonColor in the renderer and email code) use the
sanitized/validated value before being placed into attributes/styles to prevent
attribute/style injection. Ensure fallback to a safe default ('accent') when
validation fails.
packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)

82-101: ⚠️ Potential issue | 🟡 Minor

Avoid mutating the DOM element's src attribute.

Line 91 mutates iframe.src directly, which modifies the original DOM element. While unlikely to cause issues in typical import scenarios, mutating passed-in DOM elements can lead to subtle side effects if the element is still live in the document.

Suggested fix using a local variable
 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)) {
         return;
     }

+    let url = iframe.src;
     // if it's a schemaless URL, convert to https
-    if (iframe.src.match(/^\/\//)) {
-        iframe.src = `https:${iframe.src}`;
+    if (url.match(/^\/\//)) {
+        url = `https:${url}`;
     }

     const payload: Record<string, unknown> = {
-        url: iframe.src
+        url: url
     };

     payload.html = iframe.outerHTML;

     return payload;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 82 -
101, The _createPayloadForIframe function currently mutates the passed-in DOM
element by assigning iframe.src = `https:${iframe.src}` for schemaless URLs;
change this to use a local variable (e.g., const src = iframe.src || '') and
normalize that local src (prefix with "https:" when it starts with "//") and use
that local src when building payload.url and payload.html, leaving iframe.src
unchanged; keep existing validation using the local src when checking the regex.
♻️ Duplicate comments (5)
packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts (1)

19-23: ⚠️ Potential issue | 🔴 Critical

Sanitize rendered markdown HTML before innerHTML assignment.

Line 22 writes renderer output directly to innerHTML. If node.markdown is untrusted, this is an XSS sink. Please sanitize html before assignment (or explicitly enforce/document trusted-only markdown upstream).

🔧 Example fix (sanitization before DOM injection)
+import DOMPurify from 'isomorphic-dompurify';
 import {addCreateDocumentOption} from '../../utils/add-create-document-option.js';
 import {render} from '@tryghost/kg-markdown-html-renderer';
@@
-    const html = render(node.markdown || '', options as Record<string, unknown>);
+    const unsafeHtml = render(node.markdown || '', options as Record<string, unknown>);
+    const html = DOMPurify.sanitize(unsafeHtml);
@@
     const element = document.createElement('div');
     element.innerHTML = html;
In the currently used version of `@tryghost/kg-markdown-html-renderer`, does render() sanitize HTML output by default? Please cite the official source or implementation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts` around
lines 19 - 23, The renderer currently writes untrusted HTML directly into the
DOM (render(node.markdown || '') → element.innerHTML = html), creating an XSS
sink; fix by sanitizing the output of render() before assigning to
element.innerHTML (e.g. call a sanitizer like DOMPurify or your project's
sanitizeHtml helper on the html string and assign the sanitized result), update
the import/usage in markdown-renderer.ts to use the sanitizer function and
ensure tests cover untrusted input; alternatively, if you decide to enforce
trusted-only input, add explicit validation/documentation and defensive checks
around node.markdown and render() output instead of direct assignment.
packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts (1)

40-41: ⚠️ Potential issue | 🔴 Critical

Replace HTML caption injection with text assignment.

Line 41 uses innerHTML with node.caption, which is an XSS sink for untrusted caption content. Use plain text (or explicitly sanitize first).

🔒 Safe fix
-        figcaption.innerHTML = node.caption;
+        figcaption.textContent = node.caption;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts` around
lines 40 - 41, The figcaption is currently populated using innerHTML with
node.caption in codeblock-renderer.ts which creates an XSS sink; replace that
with a safe text assignment (e.g., set figcaption.textContent or append a text
node) or explicitly sanitize node.caption before injection so untrusted caption
content cannot execute HTML—locate the figcaption creation and the line setting
figcaption.innerHTML and change it to use textContent or a sanitizer.
packages/kg-default-nodes/src/nodes/image/image-renderer.ts (1)

124-127: ⚠️ Potential issue | 🟠 Major

Non-null assertions may cause runtime errors if imageOptimization is undefined.

The assertions options.imageOptimization!.contentImageSizes! assume both properties exist, but imageOptimization is optional in the interface. The guard at line 124 checks isLocalContentImage and canTransformImage but does not verify that imageOptimization or contentImageSizes are defined.

🛡️ Proposed defensive check
         if (isLocalContentImage(node.src, options.siteUrl) && options.canTransformImage?.(node.src)) {
+            const contentImageSizes = options.imageOptimization?.contentImageSizes;
+            if (!contentImageSizes) {
+                // Cannot determine retina src without image size configuration
+                return {element: figure};
+            }
             // find available image size next up from 2x600 so we can use it for the "retina" src
-            const availableImageWidths = getAvailableImageWidths(node, options.imageOptimization!.contentImageSizes!);
+            const availableImageWidths = getAvailableImageWidths(node, contentImageSizes);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts` around lines 124
- 127, The code uses non-null assertions
options.imageOptimization!.contentImageSizes! when calling
getAvailableImageWidths inside the isLocalContentImage/canTransformImage branch;
add a defensive check that options.imageOptimization and
options.imageOptimization.contentImageSizes are defined before using them (or
bail/skip the transformation if not), and then pass the verified
contentImageSizes to getAvailableImageWidths; update the condition that starts
with isLocalContentImage(...) && options.canTransformImage?.(node.src) to also
require options.imageOptimization && options.imageOptimization.contentImageSizes
to avoid runtime errors.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

401-407: ⚠️ Potential issue | 🟠 Major

Sanitize sponsorLabel before the email early-return path.

Line 401 returns for target === 'email' before the sanitizer at Line 412 runs, so emailCTATemplate() still gets raw dataset.sponsorLabel.

Suggested fix
 export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) {
@@
     const dataset = {
@@
         linkColor: node.linkColor
     };

+    if (dataset.hasSponsorLabel) {
+        const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div'));
+        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
+        dataset.sponsorLabel = cleanedHtml || '';
+    }
+
     if (options.target === 'email') {
         const emailDoc = options.createDocument!();
         const emailDiv = emailDoc.createElement('div');
@@
-    if (dataset.hasSponsorLabel) {
-        const cleanBasicHtml = buildCleanBasicHtmlForElement(element);
-        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
-        dataset.sponsorLabel = cleanedHtml || '';
-    }
     const htmlString = ctaCardTemplate(dataset);

Also applies to: 412-416

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 401 - 407, The email branch returns early when options.target ===
'email' and calls emailCTATemplate(dataset, options) before dataset.sponsorLabel
is sanitized; move or apply the same sanitizer that is currently executed after
the email branch so dataset.sponsorLabel is sanitized before calling
emailCTATemplate (either sanitize dataset.sponsorLabel in place or create a
sanitized copy of dataset and pass that to emailCTATemplate), ensuring you
reference options.target, emailCTATemplate, and dataset.sponsorLabel when making
the change.
packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts (1)

51-55: ⚠️ Potential issue | 🟠 Major

Guard thumbnail dimensions before computing aspect ratio.

The non-null assertions on metadata.thumbnail_width! and metadata.thumbnail_height! are unsafe. The guard on line 51 only checks for thumbnail_url, not the width/height fields. If these are missing or zero, this will produce NaN or Infinity for thumbnailAspectRatio, generating invalid spacerHeight and broken email HTML.

Suggested fix
-    if (isEmail && isVideoWithThumbnail) {
+    const hasValidThumbnailSize =
+        typeof metadata.thumbnail_width === 'number' &&
+        typeof metadata.thumbnail_height === 'number' &&
+        metadata.thumbnail_width > 0 &&
+        metadata.thumbnail_height > 0;
+
+    if (isEmail && isVideoWithThumbnail && hasValidThumbnailSize) {
         const emailTemplateMaxWidth = 600;
-        const thumbnailAspectRatio = metadata.thumbnail_width! / metadata.thumbnail_height!;
+        const thumbnailAspectRatio = metadata.thumbnail_width / metadata.thumbnail_height;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts` around lines 51
- 55, The code currently computes thumbnailAspectRatio using
metadata.thumbnail_width! and metadata.thumbnail_height! without verifying those
fields are present and non-zero; update the isEmail && isVideoWithThumbnail
branch to first check that metadata.thumbnail_width and
metadata.thumbnail_height are defined and > 0 before computing
thumbnailAspectRatio (and only then compute spacerHeight), and provide a safe
fallback path (e.g., skip thumbnail sizing or use a default aspect ratio/height)
if either value is missing or zero so spacerHeight cannot become NaN/Infinity;
refer to the isEmail && isVideoWithThumbnail conditional,
metadata.thumbnail_width/metadata.thumbnail_height, thumbnailAspectRatio, and
spacerHeight to find and change the logic.
🧹 Nitpick comments (14)
packages/kg-default-nodes/test/serializers/paragraph.test.ts (1)

23-23: Add type annotation to DEFAULT_CONFIG to eliminate assertions at callsites.

The as HTMLConfig casts on lines 23 and 24 exist because DEFAULT_CONFIG is declared as an untyped object literal in packages/kg-default-nodes/src/kg-default-nodes.ts. Adding a source-level type annotation—such as satisfies HTMLConfig—would allow the compiler to enforce type compatibility at the definition site, eliminating the need for assertions in both test files.

export const DEFAULT_CONFIG = {
    html: {
        import: {
            ...serializers.linebreak.import,
            ...serializers.paragraph.import
        }
    }
} satisfies { html: HTMLConfig };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts` at line 23,
DEFAULT_CONFIG is declared without a type which forces callers (e.g.,
createHeadlessEditor in tests) to use `as HTMLConfig` casts; update the
DEFAULT_CONFIG declaration in kg-default-nodes.ts to include a source-level type
annotation (for example use `satisfies { html: HTMLConfig }` or explicitly type
the exported DEFAULT_CONFIG) so the compiler enforces shape at the definition
site, then remove the `as HTMLConfig` assertions at callsites like the
createHeadlessEditor(...) usages that currently pass DEFAULT_CONFIG.html.
packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)

4-4: Tighten constructor typing to return LexicalNode and drop the cast.

Line 4 allows unknown, then Line 58 force-casts to LexicalNode. This weakens type guarantees in the parser boundary; type the constructor as producing LexicalNode and return directly.

♻️ Proposed refactor
-export function parseProductNode(ProductNode: new (data: Record<string, unknown>) => unknown) {
+export function parseProductNode(ProductNode: new (data: Record<string, unknown>) => LexicalNode) {
@@
-                        const node = new ProductNode(payload);
-                        return {node: node as LexicalNode};
+                        const node = new ProductNode(payload);
+                        return {node};

Also applies to: 57-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` at line 4, The
parseProductNode signature currently accepts ProductNode as new (data:
Record<string, unknown>) => unknown and then force-casts instances to
LexicalNode; tighten this by changing the constructor type to new (data:
Record<string, unknown>) => LexicalNode in the parseProductNode parameter so you
can construct and return the instance as a LexicalNode without casting, and
update the other similar spot referenced (lines 57-59) to the same constructor
type to remove the cast there as well.
packages/kg-default-nodes/test/nodes/tk.test.ts (1)

36-36: Remove redundant TKNode casts for cleaner tests.

On Line 36, Line 41, and Line 111, tkNode is already TKNode from $createTKNode('TK'), so these casts add noise without extra safety.

♻️ Suggested cleanup
-        (tkNode as TKNode).isTextEntity().should.be.true();
+        tkNode.isTextEntity().should.be.true();

-        (tkNode as TKNode).canInsertTextBefore().should.be.false();
+        tkNode.canInsertTextBefore().should.be.false();

-        const clonedNode = TKNode.clone(tkNode as TKNode);
+        const clonedNode = TKNode.clone(tkNode);

Also applies to: 41-41, 111-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/tk.test.ts` at line 36, Remove the
redundant type assertions of tkNode; since tkNode is created via
$createTKNode('TK') it's already a TKNode, so replace occurrences of (tkNode as
TKNode).isTextEntity() (and similar casts around tkNode at places where you
created it) with tkNode.isTextEntity(), and remove the extra (tkNode as TKNode)
casts in the tests referencing tkNode, $createTKNode and isTextEntity.
packages/kg-default-nodes/src/nodes/product/product-renderer.ts (2)

96-104: Type assertions assume dimensions are already numbers.

The as number assertions on lines 98-99 and 102 bypass runtime type checking. If getDataset() returns string values (e.g., from DOM attributes), these assertions will silently pass but cause incorrect behavior at runtime.

Consider adding defensive numeric coercion:

🛡️ Proposed fix for defensive numeric coercion
 if (data.productImageWidth && data.productImageHeight) {
+    const width = Number(data.productImageWidth);
+    const height = Number(data.productImageHeight);
+    if (!Number.isFinite(width) || !Number.isFinite(height)) {
+        // Skip dimension calculations if values aren't valid numbers
+    } else {
         imageDimensions = {
-            width: data.productImageWidth as number,
-            height: data.productImageHeight as number
+            width,
+            height
         };
 
-        if ((data.productImageWidth as number) >= 560) {
+        if (width >= 560) {
             imageDimensions = getResizedImageDimensions(imageDimensions, {width: 560});
         }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines
96 - 104, The code uses type assertions (data.productImageWidth as number /
data.productImageHeight as number) which skip runtime validation; change to
defensively coerce and validate values before using them: read numeric values
from data.productImageWidth and data.productImageHeight via Number(...) or
parseFloat, check isFinite (or !Number.isNaN) and fall back to a safe default or
skip imageDimensions if invalid, then use those validated numeric variables for
the imageDimensions assignment and for the comparison that decides whether to
call getResizedImageDimensions; update references to imageDimensions,
getResizedImageDimensions, and the productImageWidth/productImageHeight reads so
all numeric operations use the validated coerced numbers.

5-9: Interface may be incomplete compared to actual usage.

The ProductNodeData interface only declares productStarRating, isEmpty(), and getDataset(), but the template functions access many more properties (like productTitle, productDescription, productImageSrc, etc.) through getDataset(). Consider documenting that the full data shape is returned by getDataset().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines
5 - 9, ProductNodeData currently only lists productStarRating, isEmpty(), and
getDataset(), but template code expects many specific fields inside the dataset
(e.g., productTitle, productDescription, productImageSrc, etc.); update the type
returned by getDataset() to a concrete ProductDataset interface (or expand
ProductNodeData) that lists all expected keys (productTitle, productDescription,
productImageSrc, productPrice, productUrl, etc.) and use that type for
getDataset(): () => ProductDataset so callers and template functions have proper
typing and documentation; ensure ProductDataset is exported/placed near
ProductNodeData and update any usages of getDataset() accordingly.
packages/kg-default-nodes/test/nodes/video.test.ts (1)

96-100: Inconsistent should invocation pattern.

Line 96 uses an explicit cast (should as unknown as (obj: unknown) => should.Assertion)(videoNode.width) while Line 100 uses should(videoNode.height) directly. Both work, but the inconsistency is confusing. Consider using the same pattern for both null checks.

♻️ Suggested consistency fix
-            (should as unknown as (obj: unknown) => should.Assertion)(videoNode.width).equal(null);
+            should(videoNode.width).equal(null);
             videoNode.width = 600;
             videoNode.width.should.equal(600);

             should(videoNode.height).equal(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/video.test.ts` around lines 96 - 100,
The null assertions for videoNode.width and videoNode.height use mixed should
invocation styles; make them consistent by changing the explicit cast invocation
for videoNode.width to the same pattern used for videoNode.height (use
should(videoNode.width).equal(null)), or vice versa—apply the chosen style to
both checks around the videoNode.width and videoNode.height assertions in the
video.test.ts test block so both null checks use the identical should(...) form.
packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts (1)

4-7: Consider stronger typing for images array in future iteration.

The images: unknown[] type removes any as intended, but the GalleryImage interface defined in gallery-renderer.ts could be extracted and reused here for better type safety. This is a minor improvement opportunity, not a blocker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts` around lines 4 -
7, The GalleryNode interface currently types images as unknown[]; replace this
with the concrete GalleryImage type used in gallery-renderer.ts to improve type
safety by importing or extracting the GalleryImage interface and updating
GalleryNode.images to GalleryImage[] (ensure you update any references or
imports for GalleryNode to compile).
packages/kg-default-nodes/test/nodes/gallery.test.ts (1)

637-639: Consider creating a typed interface for export options.

The exportOptions as unknown as LexicalEditor cast is used throughout the test file. While this works, it suggests the exportDOM method signature might benefit from a union type or a dedicated options interface to improve type safety. This pattern is consistent across test files, so it's not blocking, but worth noting for future improvement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 637 - 639,
The test is using a double-cast "exportOptions as unknown as LexicalEditor" when
calling galleryNode.exportDOM; replace this with a proper typed interface or
union: define a dedicated ExportDOMOptions type (or union type that includes
LexicalEditor) and type the exportOptions variable accordingly, or update the
exportDOM signature to accept ExportDOMOptions | LexicalEditor so you can pass
exportOptions without unsafe casting; ensure references use exportDOM,
galleryNode, exportOptions and LexicalEditor so the new type is used across this
test file (and replicable in other tests).
packages/kg-default-nodes/test/nodes/bookmark.test.ts (4)

397-401: Extract node variable to avoid repeated casts.

The cast nodes[0] as BookmarkNode is repeated for each assertion. Extract it once for consistency with the pattern used in line 375.

♻️ Suggested refactor
 nodes.length.should.equal(1);
-(nodes[0] as BookmarkNode).url.should.equal('https://slack.engineering/typescript-at-slack-a81307fa288d');
-(nodes[0] as BookmarkNode).title.should.equal('TypeScript at Slack');
-(nodes[0] as BookmarkNode).description.should.equal('Or, How I Learned to Stop Worrying &amp; Trust the Compiler');
-(nodes[0] as BookmarkNode).publisher.should.equal('slack.engineering');
-(nodes[0] as BookmarkNode).thumbnail.should.equal('https://cdn-images-1.medium.com/fit/c/160/160/1*-h1bH8gB3I7gPh5AG1HmsQ.png');
+const node = nodes[0] as BookmarkNode;
+node.url.should.equal('https://slack.engineering/typescript-at-slack-a81307fa288d');
+node.title.should.equal('TypeScript at Slack');
+node.description.should.equal('Or, How I Learned to Stop Worrying &amp; Trust the Compiler');
+node.publisher.should.equal('slack.engineering');
+node.thumbnail.should.equal('https://cdn-images-1.medium.com/fit/c/160/160/1*-h1bH8gB3I7gPh5AG1HmsQ.png');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` around lines 397 -
401, Extract nodes[0] once into a local typed variable to avoid repeated casts:
assign const node = nodes[0] as BookmarkNode (or similar) and then use node.url,
node.title, node.description, node.publisher, node.thumbnail in the assertions;
update the assertions that currently reference (nodes[0] as BookmarkNode) to use
the new node variable and keep all assertion texts unchanged.

166-166: Consider adding a clarifying comment for the double cast pattern.

The as unknown as LexicalEditor cast is used because exportDOM expects a LexicalEditor per Lexical's signature, but this implementation actually consumes render options. A brief comment explaining this type mismatch would help future maintainers understand why the cast is necessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` at line 166, Add a
brief inline comment next to the double-cast on the exportDOM call
(bookmarkNode.exportDOM(exportOptions as unknown as LexicalEditor)) explaining
that exportDOM's TypeScript signature expects a LexicalEditor but this
implementation actually passes render/export options, so the "as unknown as
LexicalEditor" cast is intentional to satisfy the signature; reference
exportOptions and bookmarkNode.exportDOM in the comment for clarity.

375-383: Reuse the metadata variable already declared on line 352.

The metadata variable is extracted on line 352 but not reused for these assertions. This would eliminate the repeated inline casts.

♻️ Suggested refactor
 const node = nodes[0] as BookmarkNode;
 node.url.should.equal(dataset.url);
-node.icon.should.equal((dataset.metadata as Record<string, unknown>).icon);
-node.title.should.equal((dataset.metadata as Record<string, unknown>).title);
-node.description.should.equal((dataset.metadata as Record<string, unknown>).description);
-node.author.should.equal((dataset.metadata as Record<string, unknown>).author);
-node.publisher.should.equal((dataset.metadata as Record<string, unknown>).publisher);
-node.thumbnail.should.equal((dataset.metadata as Record<string, unknown>).thumbnail);
+node.icon.should.equal(metadata.icon);
+node.title.should.equal(metadata.title);
+node.description.should.equal(metadata.description);
+node.author.should.equal(metadata.author);
+node.publisher.should.equal(metadata.publisher);
+node.thumbnail.should.equal(metadata.thumbnail);
 node.caption.should.equal(dataset.caption);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` around lines 375 -
383, In the assertions block for the BookmarkNode (variables node and dataset),
replace the repeated inline casts of (dataset.metadata as Record<string,
unknown>) with the already-extracted metadata variable declared earlier (line
352) — e.g., use metadata.icon, metadata.title, metadata.description,
metadata.author, metadata.publisher, metadata.thumbnail — while keeping node.url
and node.caption comparisons against dataset.url and dataset.caption; update
references in the test so all metadata-based assertions use the metadata
identifier instead of re-casting dataset.metadata.

320-326: Extract metadata variable to reduce repetition.

The cast (dataset.metadata as Record<string, unknown>) is repeated 6 times here. Extract it once for cleaner assertions, similar to the pattern used in line 62.

♻️ Suggested refactor
 editor.getEditorState().read(() => {
     try {
         const [bookmarkNode] = $getRoot().getChildren() as BookmarkNode[];
+        const metadata = dataset.metadata as Record<string, unknown>;

         bookmarkNode.url.should.equal(dataset.url);
-        bookmarkNode.icon.should.equal((dataset.metadata as Record<string, unknown>).icon);
-        bookmarkNode.title.should.equal((dataset.metadata as Record<string, unknown>).title);
-        bookmarkNode.description.should.equal((dataset.metadata as Record<string, unknown>).description);
-        bookmarkNode.author.should.equal((dataset.metadata as Record<string, unknown>).author);
-        bookmarkNode.publisher.should.equal((dataset.metadata as Record<string, unknown>).publisher);
-        bookmarkNode.thumbnail.should.equal((dataset.metadata as Record<string, unknown>).thumbnail);
+        bookmarkNode.icon.should.equal(metadata.icon);
+        bookmarkNode.title.should.equal(metadata.title);
+        bookmarkNode.description.should.equal(metadata.description);
+        bookmarkNode.author.should.equal(metadata.author);
+        bookmarkNode.publisher.should.equal(metadata.publisher);
+        bookmarkNode.thumbnail.should.equal(metadata.thumbnail);
         bookmarkNode.caption.should.equal(dataset.caption);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` around lines 320 -
326, Replace the repeated casts by extracting metadata once: create a local
const (e.g., meta) assigned to dataset.metadata cast as Record<string, unknown>,
then update the assertions to use bookmarkNode.icon.should.equal(meta.icon),
bookmarkNode.title.should.equal(meta.title),
bookmarkNode.description.should.equal(meta.description),
bookmarkNode.author.should.equal(meta.author),
bookmarkNode.publisher.should.equal(meta.publisher), and
bookmarkNode.thumbnail.should.equal(meta.thumbnail) while leaving
bookmarkNode.caption.should.equal(dataset.caption) unchanged; this removes the
repeated (dataset.metadata as Record<string, unknown>) occurrences and mirrors
the pattern used earlier.
packages/kg-default-nodes/test/nodes/transistor.test.ts (1)

49-52: Consider defining a local Visibility type to reduce casting verbosity.

The nested casts work but are repetitive. A local type alias could improve readability:

type Visibility = {
    web: { nonMember: boolean; memberSegment: string };
    email: { memberSegment: string };
};

Then use transistorNode.visibility as Visibility once per assertion block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/transistor.test.ts` around lines 49 -
52, Define a local type alias (e.g., Visibility) that matches the expected shape
of transistorNode.visibility and use it to reduce repetitive casts: declare type
Visibility with web and email subtypes, cast transistorNode.visibility once to
Visibility (assign to the existing visibility const or a new typed const) and
update the three assertions to reference the typed properties without repeated
Record<string, unknown> casts; adjust the assertions that use
visibility.web.nonMember, visibility.web.memberSegment, and
visibility.email.memberSegment accordingly.
packages/kg-default-nodes/test/nodes/header.test.ts (1)

447-451: Same as unknown as LexicalEditor cast pattern as other tests.

This is consistent with the approach across the migration, but the same recommendation applies: consider defining a proper ExportOptions type to improve type safety in tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 447 - 451,
Tests use a repeated unsafe double-cast "as unknown as LexicalEditor" when
calling headerNode.exportDOM(exportOptions), so define a proper ExportOptions
type used by exportDOM calls (or import the existing ExportOptions interface
from the node implementation) and construct exportOptions with that type in the
test; update the call to headerNode.exportDOM(exportOptions) to use the strongly
typed ExportOptions instead of the cast, referencing the
headerNode.exportDOM/exportDOM signature and the ExportOptions type to locate
where to change the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/package.json`:
- Line 45: The package declares "@types/node": "24.12.0" but lacks an
engines.node field; either add a matching engines.node entry or lower the
`@types/node` version to match supported Node targets. Edit
packages/kg-default-nodes/package.json and do one of: (A) add "engines": {
"node": "^22.13.1 || ^24.0.0" } to align with the other packages, or (B) change
"@types/node" to the same lower version used across the monorepo, then run
install/build to ensure types and CI pass. Ensure package.json's dependency
entry "@types/node": and the new "engines" key are updated consistently.

In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts`:
- Around line 36-37: importJSON currently returns a base HeadingNode (via
HeadingNode.importJSON) which breaks the round-trip because exportJSON emits
type 'extended-heading'; change ExtendedHeadingNode.importJSON to construct and
return an ExtendedHeadingNode instance instead of delegating to
HeadingNode.importJSON — e.g., call HeadingNode.importJSON(serializedNode) to
read base properties if useful, then create new ExtendedHeadingNode(...) and
copy over the necessary serialized fields (level/tag/format/children/keys or
other props) so the deserialized object is an ExtendedHeadingNode whose type
matches exportJSON.

In `@packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts`:
- Around line 37-38: The importJSON implementation currently delegates to
TextNode.importJSON and returns a base TextNode, losing the ExtendedTextNode
subclass; update ExtendedTextNode.importJSON(serializedNode: SerializedTextNode)
to construct and return an ExtendedTextNode instance, restore any serialized
properties (text content, format, and any ExtendedTextNode-specific fields) onto
that instance, and avoid calling TextNode.importJSON; follow the pattern used by
TKNode (instantiate the subclass, set state from serializedNode, and return the
subclass instance) so deserialization preserves the extended node type and
behavior.

In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts`:
- Around line 132-133: The code uses a non-null assertion on
node.src.match(/(.*\/content\/images)\/(.*)/) which can throw if the regex
doesn't match; update the image handling in the image-renderer (the block around
isLocalContentImage, node.src.match, and img.setAttribute) to capture the match
result into a variable (e.g., const match = node.src.match(...)), verify match
is truthy before destructuring imagesPath and filename, and handle the failure
case (skip setting the sized src, fall back to node.src, or log/error) so
img.setAttribute('src', ...) is only called when imagesPath/filename are valid.
Ensure the regex used matches the earlier isLocalContentImage check or make both
consistent.

In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 151-160: The test "renders nothing when header and subheader is
undefined and the button is disabled" currently no-ops via `void element` after
calling $createHeaderNode(...).exportDOM(exportOptions as unknown as
LexicalEditor), so it doesn't validate behavior; replace the no-op with a real
assertion or add a TODO and explicit assertion depending on desired approach:
either (A) if you want to track the renderer bug, add a TODO/TICKET comment
above the test and assert that element is null (or assert that exportDOM returns
null) so the intended behavior is documented, or (B) if you want the test to
reflect current behavior, remove the `null as unknown as string` casts and
assert that `element` is an HTMLElement (or not null) returned from
node.exportDOM; reference the test function name and the $createHeaderNode,
node.exportDOM, and exportOptions/LexicalEditor usage when making the change.

In `@packages/kg-default-nodes/test/nodes/transistor.test.ts`:
- Around line 165-166: The test uses an unnecessary cast when calling
transistorNode.exportDOM: remove the "as unknown as LexicalEditor" cast and call
transistorNode.exportDOM(exportOptions) directly (exportOptions is already a
Record<string, unknown> and matches exportDOM's parameter), updating all
occurrences where exportOptions is cast to LexicalEditor so the call uses the
typed exportOptions variable without defeating type safety.

In `@packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js`:
- Around line 8-14: The mock created in the vi.mock call incorrectly spreads
actual.default (which is undefined for the 'lexical' module) causing only
$getNodeByKey to be present; update the mock to spread the real module object
returned by vi.importActual('lexical') (i.e. use ...actual) and then override
$getNodeByKey with vi.fn() so all named Lexical exports remain available while
mocking $getNodeByKey.

---

Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 52-56: The buttonColor validation is insecure and inconsistently
applied: replace the partially-anchored regex in ctaCardTemplate with a
fully-anchored pattern that only allows either an allowed token (letters,
digits, hyphen) or a hex color (`#abc` or `#aabbcc`) (e.g.
^(?:[A-Za-z0-9-]+|#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))$), and centralize the
check into a single helper (e.g., validateButtonColor) called from
ctaCardTemplate and the email-generation path so all interpolations (places
referencing dataset.buttonColor in the renderer and email code) use the
sanitized/validated value before being placed into attributes/styles to prevent
attribute/style injection. Ensure fallback to a safe default ('accent') when
validation fails.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts`:
- Around line 82-101: The _createPayloadForIframe function currently mutates the
passed-in DOM element by assigning iframe.src = `https:${iframe.src}` for
schemaless URLs; change this to use a local variable (e.g., const src =
iframe.src || '') and normalize that local src (prefix with "https:" when it
starts with "//") and use that local src when building payload.url and
payload.html, leaving iframe.src unchanged; keep existing validation using the
local src when checking the regex.

In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts`:
- Around line 25-31: The code assigns string DOM attributes directly to numeric
payload fields: when reading img.getAttribute('width') and
img.getAttribute('height') in product-parser.ts (the logic that sets
payload.productImageWidth and payload.productImageHeight), parse the attribute
values into numbers (e.g., via Number(...) or parseInt(..., 10)) and only assign
if the result is a finite number; otherwise set the fields to null to preserve
the ProductNode type (number | null) and avoid NaN leaks.

---

Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 401-407: The email branch returns early when options.target ===
'email' and calls emailCTATemplate(dataset, options) before dataset.sponsorLabel
is sanitized; move or apply the same sanitizer that is currently executed after
the email branch so dataset.sponsorLabel is sanitized before calling
emailCTATemplate (either sanitize dataset.sponsorLabel in place or create a
sanitized copy of dataset and pass that to emailCTATemplate), ensuring you
reference options.target, emailCTATemplate, and dataset.sponsorLabel when making
the change.

In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts`:
- Around line 40-41: The figcaption is currently populated using innerHTML with
node.caption in codeblock-renderer.ts which creates an XSS sink; replace that
with a safe text assignment (e.g., set figcaption.textContent or append a text
node) or explicitly sanitize node.caption before injection so untrusted caption
content cannot execute HTML—locate the figcaption creation and the line setting
figcaption.innerHTML and change it to use textContent or a sanitizer.

In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts`:
- Around line 51-55: The code currently computes thumbnailAspectRatio using
metadata.thumbnail_width! and metadata.thumbnail_height! without verifying those
fields are present and non-zero; update the isEmail && isVideoWithThumbnail
branch to first check that metadata.thumbnail_width and
metadata.thumbnail_height are defined and > 0 before computing
thumbnailAspectRatio (and only then compute spacerHeight), and provide a safe
fallback path (e.g., skip thumbnail sizing or use a default aspect ratio/height)
if either value is missing or zero so spacerHeight cannot become NaN/Infinity;
refer to the isEmail && isVideoWithThumbnail conditional,
metadata.thumbnail_width/metadata.thumbnail_height, thumbnailAspectRatio, and
spacerHeight to find and change the logic.

In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts`:
- Around line 124-127: The code uses non-null assertions
options.imageOptimization!.contentImageSizes! when calling
getAvailableImageWidths inside the isLocalContentImage/canTransformImage branch;
add a defensive check that options.imageOptimization and
options.imageOptimization.contentImageSizes are defined before using them (or
bail/skip the transformation if not), and then pass the verified
contentImageSizes to getAvailableImageWidths; update the condition that starts
with isLocalContentImage(...) && options.canTransformImage?.(node.src) to also
require options.imageOptimization && options.imageOptimization.contentImageSizes
to avoid runtime errors.

In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts`:
- Around line 19-23: The renderer currently writes untrusted HTML directly into
the DOM (render(node.markdown || '') → element.innerHTML = html), creating an
XSS sink; fix by sanitizing the output of render() before assigning to
element.innerHTML (e.g. call a sanitizer like DOMPurify or your project's
sanitizeHtml helper on the html string and assign the sanitized result), update
the import/usage in markdown-renderer.ts to use the sanitizer function and
ensure tests cover untrusted input; alternatively, if you decide to enforce
trusted-only input, add explicit validation/documentation and defensive checks
around node.markdown and render() output instead of direct assignment.

---

Nitpick comments:
In `@packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts`:
- Around line 4-7: The GalleryNode interface currently types images as
unknown[]; replace this with the concrete GalleryImage type used in
gallery-renderer.ts to improve type safety by importing or extracting the
GalleryImage interface and updating GalleryNode.images to GalleryImage[] (ensure
you update any references or imports for GalleryNode to compile).

In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts`:
- Line 4: The parseProductNode signature currently accepts ProductNode as new
(data: Record<string, unknown>) => unknown and then force-casts instances to
LexicalNode; tighten this by changing the constructor type to new (data:
Record<string, unknown>) => LexicalNode in the parseProductNode parameter so you
can construct and return the instance as a LexicalNode without casting, and
update the other similar spot referenced (lines 57-59) to the same constructor
type to remove the cast there as well.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 96-104: The code uses type assertions (data.productImageWidth as
number / data.productImageHeight as number) which skip runtime validation;
change to defensively coerce and validate values before using them: read numeric
values from data.productImageWidth and data.productImageHeight via Number(...)
or parseFloat, check isFinite (or !Number.isNaN) and fall back to a safe default
or skip imageDimensions if invalid, then use those validated numeric variables
for the imageDimensions assignment and for the comparison that decides whether
to call getResizedImageDimensions; update references to imageDimensions,
getResizedImageDimensions, and the productImageWidth/productImageHeight reads so
all numeric operations use the validated coerced numbers.
- Around line 5-9: ProductNodeData currently only lists productStarRating,
isEmpty(), and getDataset(), but template code expects many specific fields
inside the dataset (e.g., productTitle, productDescription, productImageSrc,
etc.); update the type returned by getDataset() to a concrete ProductDataset
interface (or expand ProductNodeData) that lists all expected keys
(productTitle, productDescription, productImageSrc, productPrice, productUrl,
etc.) and use that type for getDataset(): () => ProductDataset so callers and
template functions have proper typing and documentation; ensure ProductDataset
is exported/placed near ProductNodeData and update any usages of getDataset()
accordingly.

In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts`:
- Around line 397-401: Extract nodes[0] once into a local typed variable to
avoid repeated casts: assign const node = nodes[0] as BookmarkNode (or similar)
and then use node.url, node.title, node.description, node.publisher,
node.thumbnail in the assertions; update the assertions that currently reference
(nodes[0] as BookmarkNode) to use the new node variable and keep all assertion
texts unchanged.
- Line 166: Add a brief inline comment next to the double-cast on the exportDOM
call (bookmarkNode.exportDOM(exportOptions as unknown as LexicalEditor))
explaining that exportDOM's TypeScript signature expects a LexicalEditor but
this implementation actually passes render/export options, so the "as unknown as
LexicalEditor" cast is intentional to satisfy the signature; reference
exportOptions and bookmarkNode.exportDOM in the comment for clarity.
- Around line 375-383: In the assertions block for the BookmarkNode (variables
node and dataset), replace the repeated inline casts of (dataset.metadata as
Record<string, unknown>) with the already-extracted metadata variable declared
earlier (line 352) — e.g., use metadata.icon, metadata.title,
metadata.description, metadata.author, metadata.publisher, metadata.thumbnail —
while keeping node.url and node.caption comparisons against dataset.url and
dataset.caption; update references in the test so all metadata-based assertions
use the metadata identifier instead of re-casting dataset.metadata.
- Around line 320-326: Replace the repeated casts by extracting metadata once:
create a local const (e.g., meta) assigned to dataset.metadata cast as
Record<string, unknown>, then update the assertions to use
bookmarkNode.icon.should.equal(meta.icon),
bookmarkNode.title.should.equal(meta.title),
bookmarkNode.description.should.equal(meta.description),
bookmarkNode.author.should.equal(meta.author),
bookmarkNode.publisher.should.equal(meta.publisher), and
bookmarkNode.thumbnail.should.equal(meta.thumbnail) while leaving
bookmarkNode.caption.should.equal(dataset.caption) unchanged; this removes the
repeated (dataset.metadata as Record<string, unknown>) occurrences and mirrors
the pattern used earlier.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Around line 637-639: The test is using a double-cast "exportOptions as unknown
as LexicalEditor" when calling galleryNode.exportDOM; replace this with a proper
typed interface or union: define a dedicated ExportDOMOptions type (or union
type that includes LexicalEditor) and type the exportOptions variable
accordingly, or update the exportDOM signature to accept ExportDOMOptions |
LexicalEditor so you can pass exportOptions without unsafe casting; ensure
references use exportDOM, galleryNode, exportOptions and LexicalEditor so the
new type is used across this test file (and replicable in other tests).

In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 447-451: Tests use a repeated unsafe double-cast "as unknown as
LexicalEditor" when calling headerNode.exportDOM(exportOptions), so define a
proper ExportOptions type used by exportDOM calls (or import the existing
ExportOptions interface from the node implementation) and construct
exportOptions with that type in the test; update the call to
headerNode.exportDOM(exportOptions) to use the strongly typed ExportOptions
instead of the cast, referencing the headerNode.exportDOM/exportDOM signature
and the ExportOptions type to locate where to change the test.

In `@packages/kg-default-nodes/test/nodes/tk.test.ts`:
- Line 36: Remove the redundant type assertions of tkNode; since tkNode is
created via $createTKNode('TK') it's already a TKNode, so replace occurrences of
(tkNode as TKNode).isTextEntity() (and similar casts around tkNode at places
where you created it) with tkNode.isTextEntity(), and remove the extra (tkNode
as TKNode) casts in the tests referencing tkNode, $createTKNode and
isTextEntity.

In `@packages/kg-default-nodes/test/nodes/transistor.test.ts`:
- Around line 49-52: Define a local type alias (e.g., Visibility) that matches
the expected shape of transistorNode.visibility and use it to reduce repetitive
casts: declare type Visibility with web and email subtypes, cast
transistorNode.visibility once to Visibility (assign to the existing visibility
const or a new typed const) and update the three assertions to reference the
typed properties without repeated Record<string, unknown> casts; adjust the
assertions that use visibility.web.nonMember, visibility.web.memberSegment, and
visibility.email.memberSegment accordingly.

In `@packages/kg-default-nodes/test/nodes/video.test.ts`:
- Around line 96-100: The null assertions for videoNode.width and
videoNode.height use mixed should invocation styles; make them consistent by
changing the explicit cast invocation for videoNode.width to the same pattern
used for videoNode.height (use should(videoNode.width).equal(null)), or vice
versa—apply the chosen style to both checks around the videoNode.width and
videoNode.height assertions in the video.test.ts test block so both null checks
use the identical should(...) form.

In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts`:
- Line 23: DEFAULT_CONFIG is declared without a type which forces callers (e.g.,
createHeadlessEditor in tests) to use `as HTMLConfig` casts; update the
DEFAULT_CONFIG declaration in kg-default-nodes.ts to include a source-level type
annotation (for example use `satisfies { html: HTMLConfig }` or explicitly type
the exported DEFAULT_CONFIG) so the compiler enforces shape at the definition
site, then remove the `as HTMLConfig` assertions at callsites like the
createHeadlessEditor(...) usages that currently pass DEFAULT_CONFIG.html.

Comment thread packages/kg-default-nodes/package.json
Comment thread packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/image/image-renderer.ts Outdated
Comment thread packages/kg-default-nodes/test/nodes/header.test.ts
Comment thread packages/kg-default-nodes/test/nodes/transistor.test.ts
Comment thread packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js Outdated
Copy link
Copy Markdown

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)

14-18: ⚠️ Potential issue | 🟠 Major

Normalize imported image dimensions to numbers.

getAttribute('width'/'height') returns strings, but packages/kg-default-nodes/src/nodes/product/ProductNode.ts defines these fields as number | null, and packages/kg-default-nodes/src/nodes/product/product-renderer.ts later uses them numerically. Leaving them as strings makes imported product cards depend on runtime coercion and can break the resize path.

🔧 Suggested fix
                         const img = domNode.querySelector('.kg-product-card-image');
                         if (img && img.getAttribute('src')) {
                             payload.productImageSrc = img.getAttribute('src');
 
-                            if (img.getAttribute('width')) {
-                                payload.productImageWidth = img.getAttribute('width');
-                            }
+                            const widthAttr = img.getAttribute('width');
+                            if (widthAttr) {
+                                const width = Number(widthAttr);
+                                if (Number.isFinite(width)) {
+                                    payload.productImageWidth = width;
+                                }
+                            }
 
-                            if (img.getAttribute('height')) {
-                                payload.productImageHeight = img.getAttribute('height');
-                            }
+                            const heightAttr = img.getAttribute('height');
+                            if (heightAttr) {
+                                const height = Number(heightAttr);
+                                if (Number.isFinite(height)) {
+                                    payload.productImageHeight = height;
+                                }
+                            }
                         }

Also applies to: 21-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` around lines
14 - 18, The imported product image width/height are currently taken from
element.getAttribute('width'/'height') which yields strings but ProductNode.ts
declares those fields as number | null and product-renderer.ts expects numeric
values; update the parsing in product-parser.ts where you build the payload (the
properties for product image dimensions) to convert the attribute strings to
numbers (e.g., using Number(...) or parseInt(...)) and set them to null when the
attribute is missing or not a valid number so
payload.productImageWidth/productImageHeight are proper number|null values and
avoid runtime coercion issues.
packages/kg-default-nodes/src/nodes/video/video-parser.ts (2)

23-27: ⚠️ Potential issue | 🟠 Major

Read cardWidth from the <figure>, not the inner <video>.

Line 26 passes videoNode into getCardWidth(), but this package renders kg-width-* on the outer <figure> in packages/kg-default-nodes/src/nodes/video/video-renderer.ts at Line 55 and Lines 166-171. Wide/full video cards will round-trip through HTML import as regular.

🔧 Suggested fix
-                            cardWidth: getCardWidth(videoNode)
+                            cardWidth: getCardWidth(domNode)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts` around lines 23 -
27, The payload is reading cardWidth from the inner video node (videoNode) but
the kg-width-* attribute is placed on the outer <figure>, so change the code
that builds payload (where payload: { src, loop, cardWidth } is created) to
obtain the card width from the figure wrapper instead of videoNode — e.g.,
locate where getCardWidth(videoNode) is called and call getCardWidth with the
outer figure node (or read the kg-width-* attribute from the parent/figure
element) so getCardWidth receives the element that actually carries the width
class and returns correct width for wide/full cards.

29-35: ⚠️ Potential issue | 🟡 Minor

Reject malformed durations before assigning them.

parseInt() returns NaN on invalid input and does not throw an exception, so the catch block never executes. This allows NaN to be assigned to payload.duration, which later surfaces as NaN:NaN in the formattedDuration getter when Math.floor(NaN) produces NaN and String(NaN) renders as "NaN".

Validate parsed values with Number.isFinite() before assignment:

Suggested fix
                         if (durationText) {
                             const [minutes, seconds] = durationText.split(':');
-                            try {
-                                payload.duration = parseInt(minutes) * 60 + parseInt(seconds);
-                            } catch {
-                                // ignore duration
+                            const parsedMinutes = Number.parseInt(minutes, 10);
+                            const parsedSeconds = Number.parseInt(seconds, 10);
+
+                            if (Number.isFinite(parsedMinutes) && Number.isFinite(parsedSeconds)) {
+                                payload.duration = (parsedMinutes * 60) + parsedSeconds;
                             }
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts` around lines 29 -
35, The duration parsing currently uses parseInt on durationText parts and
assigns directly to payload.duration, but parseInt returns NaN for bad input and
never throws, so invalid values get assigned; update the logic in
video-parser.ts (the block handling durationText and payload.duration) to parse
minutes and seconds, validate both with Number.isFinite (or
Number.isFinite(Number(...))) and only assign payload.duration when both parts
are finite numbers; if validation fails, do not set payload.duration (or leave
it undefined/null) so formattedDuration getter won't produce "NaN:NaN".
packages/kg-default-nodes/src/generate-decorator-node.ts (1)

121-129: ⚠️ Potential issue | 🟠 Major

Constructor and getPropertyDefaults() share mutable defaults and collapse falsy values.

The constructor at line 121 uses || for non-boolean defaults, which collapses valid falsy values ('', 0, null). Additionally, getPropertyDefaults() at line 155 returns raw prop.default values without cloning, causing object and array defaults to be shared by reference across instances. For example, GalleryNode defines images with default [], so two gallery nodes will share the same backing array until reassigned. Switch constructor to ?? (matching the boolean case at line 124) and clone non-primitive defaults in getPropertyDefaults().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 121 -
129, The constructor currently uses || for non-boolean defaults which collapses
valid falsy values; change the fallback logic in the constructor (inside the
class constructor that iterates internalProps and assigns
this[prop.privateName]) to use the nullish coalescing operator (??) for
non-boolean defaults just like the boolean branch, and ensure
getPropertyDefaults() clones non-primitive default values (arrays/objects)
before returning them so instances do not share references (e.g.,
GalleryNode.images default [] should be cloned per instance); use
structuredClone or an appropriate deep-clone utility for prop.default when
typeof prop.default is 'object' or Array.isArray(prop.default).
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

53-60: ⚠️ Potential issue | 🔴 Critical

Fully anchor the color validators before interpolating them into HTML attributes.

The regex /^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/ only anchors the hex pattern with $; the first alternative lacks an end anchor. This allows values like red" onclick="..." to pass validation and break out of style and class attributes, creating a stored-XSS vulnerability affecting both buttonColor (line 60) and backgroundColor (line 63).

Fix
-    if (!dataset.buttonColor || !dataset.buttonColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) {
+    if (!dataset.buttonColor || !/^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/.test(dataset.buttonColor)) {
         dataset.buttonColor = 'accent';
     }
@@
-    if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) {
+    if (!dataset.backgroundColor || !/^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/.test(dataset.backgroundColor)) {
         dataset.backgroundColor = 'white';
     }

Also applies to: 397-399

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 53 - 60, The current validator for dataset.buttonColor uses an
alternation where only the hex branch is fully anchored, letting values like
`red" onclick="..."` bypass validation; update the check in
calltoaction-renderer.ts so the entire alternation is anchored (e.g. use a
single regex that wraps both branches with ^...$ such as
/^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/) and apply the same
anchored regex to the backgroundColor validation; keep the fallback to 'accent'
when validation fails and ensure buttonAccent and buttonStyle continue to derive
from the sanitized dataset.buttonColor and dataset.buttonTextColor.
♻️ Duplicate comments (6)
packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts (1)

36-38: ⚠️ Potential issue | 🟠 Major

importJSON still returns base HeadingNode instead of ExtendedHeadingNode.

This issue was flagged in a previous review but remains unaddressed. Delegating to HeadingNode.importJSON(serializedNode) returns a HeadingNode instance, while exportJSON() at line 42 sets type to 'extended-heading'. This breaks serialization round-trip integrity.

🐛 Proposed fix
 static importJSON(serializedNode: SerializedHeadingNode) {
-    return HeadingNode.importJSON(serializedNode);
+    const node = new ExtendedHeadingNode(serializedNode.tag);
+    node.setDirection(serializedNode.direction);
+    node.setFormat(serializedNode.format);
+    node.setIndent(serializedNode.indent);
+    return node;
 }

Alternatively, if updateFromJSON is available in the Lexical version being used:

 static importJSON(serializedNode: SerializedHeadingNode) {
-    return HeadingNode.importJSON(serializedNode);
+    return new ExtendedHeadingNode(serializedNode.tag).updateFromJSON(serializedNode);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts` around lines 36 -
38, The importJSON implementation for ExtendedHeadingNode currently delegates to
HeadingNode.importJSON and returns a HeadingNode, breaking round-trip
serialization with exportJSON (which sets type 'extended-heading'); change
ExtendedHeadingNode.importJSON to construct and return an ExtendedHeadingNode
instance by either calling ExtendedHeadingNode.updateFromJSON(serializedNode,
node) if updateFromJSON is available or by creating a new ExtendedHeadingNode
and copying relevant properties from serializedNode (level, text/content, and
any extended props) so the returned object is an ExtendedHeadingNode that
matches exportJSON's type.
packages/kg-default-nodes/src/nodes/file/file-parser.ts (1)

16-22: ⚠️ Potential issue | 🟡 Minor

Keep fileSize as a string to match FileNode’s contract.

On Line [16], sizeToBytes() returns number, but packages/kg-default-nodes/src/nodes/file/FileNode.ts defines fileSize as string. This still introduces a type-contract mismatch in the parser payload.

Proposed fix
-                        const fileSize = sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || '');
+                        const fileSize = String(sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || ''));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/file/file-parser.ts` around lines 16 -
22, The parser sets fileSize by calling
sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || '')
which returns a number, but FileNode.ts defines fileSize as a string; change the
parser (in file-parser.ts) to preserve fileSize as a string (e.g., keep the
original textContent or convert the numeric result back to the same string
format expected by FileNode) so the payload sent from the parser uses a string
for fileSize; update the variable fileSize and the payload construction (src,
fileTitle, fileCaption, fileName, fileSize) accordingly to match FileNode's
contract.
packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts (1)

37-39: ⚠️ Potential issue | 🟠 Major

importJSON still delegates to TextNode.importJSON(), breaking round-trip serialization.

This was previously flagged. The implementation returns a base TextNode instead of ExtendedTextNode, so deserialized nodes lose their subclass type. Since exportJSON() sets type: 'extended-text', importJSON should return an ExtendedTextNode instance.

Suggested fix
-    static importJSON(serializedNode: SerializedTextNode) {
-        return TextNode.importJSON(serializedNode);
+    static importJSON(serializedNode: SerializedTextNode): ExtendedTextNode {
+        return new ExtendedTextNode(serializedNode.text).updateFromJSON(serializedNode);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts` around lines 37 -
39, The current importJSON delegates to TextNode.importJSON and returns a base
TextNode, losing subclass identity; change ExtendedTextNode.importJSON to
construct and return an ExtendedTextNode using the incoming SerializedTextNode
payload (copy text, format/formatting/children/any custom extended properties)
instead of calling TextNode.importJSON, and ensure the returned object has type
'extended-text' to match exportJSON; reference ExtendedTextNode.importJSON,
ExtendedTextNode.exportJSON, and SerializedTextNode when implementing the
conversion so all extended fields are restored during deserialization.
packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts (1)

40-42: ⚠️ Potential issue | 🔴 Critical

Replace raw caption innerHTML assignment (XSS sink).

Line 41 injects node.caption directly into HTML. If this value is not sanitized at render time, it can execute script content.

🔒 Safer assignment
-        figcaption.innerHTML = node.caption;
+        figcaption.textContent = node.caption;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts` around
lines 40 - 42, The code in codeblock-renderer.ts directly assigns node.caption
to figcaption.innerHTML (XSS sink); change this to a safe assignment by using
textContent (or a sanitizer) instead of innerHTML so arbitrary HTML/scripts in
node.caption cannot execute. Locate the figcaption creation/assignment in the
codeblock renderer (the block that creates figcaption and uses node.caption) and
replace the innerHTML assignment with figcaption.textContent = node.caption or
pass node.caption through your project's HTML sanitizer before setting innerHTML
if rich HTML is intended.
packages/kg-default-nodes/src/nodes/product/product-renderer.ts (1)

54-60: ⚠️ Potential issue | 🔴 Critical

Escape the button label and sanitize the href before templating.

cardTemplate/emailCardTemplate still interpolate data.productButton and data.productUrl directly into the HTML string, and Line 59 materializes that string with innerHTML. That leaves an XSS path for any product data that reaches these fields without prior escaping/URL validation. If productTitle/productDescription are intentionally sanitized HTML, keep those as-is, but the button text and link should not be emitted raw.

Also applies to: 64-90, 93-196

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines
54 - 60, The cardTemplate/emailCardTemplate flow currently injects unescaped
productButton and productUrl into htmlString which is then assigned via
element.innerHTML, creating XSS risk; update the code that prepares templateData
(used by cardTemplate and emailCardTemplate) to HTML-escape the productButton
label and to sanitize/validate productUrl (e.g., allow only http/https and
remove javascript: or data: schemes) before passing into templates, or
alternatively stop embedding those values in the raw template string and instead
set the button text via textContent and set the anchor href via a validated
value on the created element after element.innerHTML is assigned; refer to
templateData, productButton, productUrl, cardTemplate, emailCardTemplate and
element.innerHTML to locate where to apply escaping/URL validation and where to
set safe DOM properties.
packages/kg-default-nodes/src/nodes/video/video-renderer.ts (1)

16-21: ⚠️ Potential issue | 🟠 Major

postUrl is still optional on the email branch.

When renderVideoNode() takes the target === 'email' path, emailCardTemplate() interpolates options.postUrl into the href attributes on Line 129 and Line 153. Because VideoRenderOptions still makes postUrl optional, the type system still allows href="undefined". This matches the earlier review comment and still looks unresolved.

🔧 Suggested fix
-interface VideoRenderOptions {
+interface BaseVideoRenderOptions {
     createDocument?: () => Document;
     dom?: { window: { document: Document } };
-    target?: string;
-    postUrl?: string;
     [key: string]: unknown;
 }
+
+type VideoRenderOptions =
+    | (BaseVideoRenderOptions & {target: 'email'; postUrl: string})
+    | (BaseVideoRenderOptions & {target?: string; postUrl?: string});
+
+type EmailVideoRenderOptions = Extract<VideoRenderOptions, {target: 'email'}>;
@@
-export function emailCardTemplate({node, options, cardClasses}: {node: VideoNodeData, options: VideoRenderOptions, cardClasses: string}) {
+export function emailCardTemplate({node, options, cardClasses}: {node: VideoNodeData, options: EmailVideoRenderOptions, cardClasses: string}) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts` around lines 16
- 21, VideoRenderOptions currently declares postUrl as optional which allows
renderVideoNode's email path (emailCardTemplate) to interpolate undefined into
hrefs; make postUrl required for the email path by either (A) changing
VideoRenderOptions.postUrl to mandatory (remove the ?) so callers always provide
it, or (B) narrow types in renderVideoNode/emailCardTemplate to accept a new
EmailVideoRenderOptions with postUrl: string and validate/throw if missing
before building the email template; update function signatures (renderVideoNode
and emailCardTemplate) to use the stricter type or add a runtime check that
throws a clear error when target === 'email' and options.postUrl is undefined to
prevent href="undefined".
🧹 Nitpick comments (14)
packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts (1)

6-21: Consider consolidating RenderOptions into a shared type.

The EmailCtaNodeData interface correctly types all the node properties used in this renderer. The RenderOptions interface with its index signature is compatible with addCreateDocumentOption's expected Record<string, unknown> parameter.

However, similar RenderOptions interfaces likely exist across multiple renderers (bookmark, email, file, audio, button, etc.). Consider extracting this to a shared type in a common location to reduce duplication and ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts` around
lines 6 - 21, Extract the local RenderOptions interface into a shared type and
replace the renderer-local declaration with an import: create a common types
file (e.g., a shared renderer-types module) that exports the RenderOptions shape
used across renderers, update EmailCtaNodeRenderer to import and use that shared
RenderOptions type instead of its own declaration, and update other renderer
files (bookmark, email, file, audio, button, etc.) to consume the same shared
type so the signature matches the expected Record<string, unknown> for
addCreateDocumentOption and avoids duplication.
packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts (2)

129-132: Typo in variable name: audioDUrationNode

The variable has an inconsistent capital U - should be audioDurationNode to match the camelCase convention used elsewhere in this file (e.g., audioDuration, audioDurationTotal).

✏️ Suggested fix
-    const audioDUrationNode = document.createElement('span');
-    audioDUrationNode.setAttribute('class', 'kg-audio-duration');
-    audioDUrationNode.textContent = String(node.duration);
-    audioDurationTotal.appendChild(audioDUrationNode);
+    const audioDurationNode = document.createElement('span');
+    audioDurationNode.setAttribute('class', 'kg-audio-duration');
+    audioDurationNode.textContent = String(node.duration);
+    audioDurationTotal.appendChild(audioDurationNode);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 129
- 132, Rename the mistakenly cased variable audioDUrationNode to
audioDurationNode to follow camelCase and match surrounding identifiers; update
all occurrences in audio-renderer.ts where audioDUrationNode is declared and
referenced (e.g., the const declaration, setAttribute, textContent assignment,
and appendChild call) so they consistently use audioDurationNode.

184-235: Pre-existing consideration: HTML escaping in email template.

The email template interpolates node.title directly into HTML (line 205) without escaping. If title contains HTML special characters or malicious content, it could cause rendering issues or XSS in email clients that execute JavaScript.

This appears to be pre-existing behavior from before the TypeScript migration. If the input data is trusted/sanitized upstream, this may be acceptable—just flagging for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 184
- 235, The emailTemplate function in audio-renderer.ts inserts node.title
directly into the HTML string, risking broken rendering or XSS; fix it by
escaping the title before interpolation (e.g. add or reuse an escapeHtml(text)
helper and use escapeHtml(node.title) when building the html) or avoid string
interpolation for the title by creating the title element via DOM APIs
(createElement / createTextNode) and inserting the text node into the container
after container.innerHTML is set; update the emailTemplate implementation
(reference: function emailTemplate, symbol node.title) to ensure the title is
safely escaped/inserted.
packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts (2)

213-216: createDocument() is called twice on the email path; first call is unused.

On the email branch, Line 215 creates a document that is never used, then Line 238 creates another one. This can introduce avoidable side effects and extra work.

Refactor to one call per execution path
 export function renderHeaderNodeV2(dataset: HeaderV2DatasetNode, options: HeaderV2RenderOptions = {}) {
     addCreateDocumentOption(options);
-    const document = options.createDocument!();
+    const createDocument = options.createDocument!();

     const node = {
       // ...
     };

     if (options.target === 'email') {
-        const emailDoc = options.createDocument!();
+        const emailDoc = createDocument();
         const emailDiv = emailDoc.createElement('div');

         emailDiv.innerHTML = emailTemplate(node, options)?.trim();

         return {element: emailDiv.firstElementChild};
     }

+    const document = createDocument();
     const htmlString = cardTemplate(node, options);

Also applies to: 237-239

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts`
around lines 213 - 216, The renderHeaderNodeV2 currently invokes
options.createDocument() eagerly (after addCreateDocumentOption) and again later
on the email branch, causing an unused/duplicate call; update renderHeaderNodeV2
to only call options.createDocument() once per control-flow path by removing the
initial unused call at the top and invoking options.createDocument() lazily
inside each branch that actually needs a Document (e.g., the email branch where
the later call currently exists), or reuse a single local variable if both
branches need the same Document; keep references to addCreateDocumentOption and
options.createDocument to locate and fix the logic.

71-71: Replace the broad type assertion with a structurally compatible approach.

The cast options as Parameters<typeof getSrcsetAttribute>[0]['options'] masks structural incompatibility between HeaderV2RenderOptions and ImageRenderOptions. HeaderV2RenderOptions lacks the required fields (siteUrl, canTransformImage, imageOptimization) and relies on its index signature [key: string]: unknown to permit the cast. This pattern can hide future type drift.

Extract a typed subset from options that matches ImageRenderOptions shape, or align HeaderV2RenderOptions to include the expected fields.

Suggested approach
-const srcsetValue = getSrcsetAttribute({...bgImage, options: options as Parameters<typeof getSrcsetAttribute>[0]['options']});
+type SrcsetOptions = Parameters<typeof getSrcsetAttribute>[0]['options'];
+const srcsetValue = getSrcsetAttribute({
+    ...bgImage,
+    options: {
+        siteUrl: options.siteUrl,
+        canTransformImage: options.canTransformImage,
+        imageOptimization: options.imageOptimization
+    } as SrcsetOptions
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts`
at line 71, The current broad cast of `options as Parameters<typeof
getSrcsetAttribute>[0]['options']` hides incompatibilities between
`HeaderV2RenderOptions` and `ImageRenderOptions`; instead, create a properly
typed subset object with the fields `siteUrl`, `canTransformImage`, and
`imageOptimization` (and any other properties required by `ImageRenderOptions`)
sourced from `options`, then pass that subset into `getSrcsetAttribute` (i.e.,
construct `const imgOptions: ImageRenderOptions = { siteUrl: options.siteUrl,
canTransformImage: options.canTransformImage, imageOptimization:
options.imageOptimization, ... }` and call `getSrcsetAttribute({...bgImage,
options: imgOptions})`), or alternatively extend/adjust `HeaderV2RenderOptions`
to include the same properties so a direct pass is type-safe.
packages/kg-default-nodes/src/nodes/image/image-parser.ts (2)

28-30: Remove unreachable if (!img) inside figure conversion.

img is already validated before the conversion object is created, so this branch is dead code.

Proposed diff
-                        if (!img) {
-                            return null;
-                        }
-
                         const payload = readImageAttributesFromElement(img);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/image/image-parser.ts` around lines 28 -
30, Remove the unreachable branch that checks if (!img) inside the figure
conversion: since img is validated earlier, delete the entire conditional and
its null return so the conversion logic flows without dead code; locate the
block that constructs the figure conversion (the code referencing img inside the
image->figure conversion in image-parser.ts) and remove the if (!img) { return
null; } branch, leaving the rest of the conversion intact.

5-5: Tighten parser constructor typing to remove unsafe casts and unreachable code.

The parseImageNode function accepts constructors returning unknown and force-casts nodes to LexicalNode. Change the constructor parameter to return LexicalNode directly, eliminating both casts. Additionally, remove the unreachable guard at lines 28-30 (the img variable is already checked at line 21 before entering the conversion function).

This pattern appears across all parser functions in the codebase—apply broadly or consider a systematic refactor.

Proposed diff
-export function parseImageNode(ImageNode: new (data: Record<string, unknown>) => unknown) {
+export function parseImageNode(ImageNode: new (data: Record<string, unknown>) => LexicalNode) {
     return {
         img: () => ({
             conversion(domNode: HTMLElement) {
                 if (domNode.tagName === 'IMG') {
                     const {src, width, height, alt, title, href} = readImageAttributesFromElement(domNode as HTMLImageElement);
 
                     const node = new ImageNode({alt, src, title, width, height, href});
-                    return {node: node as LexicalNode};
+                    return {node};
                 }
 
                 return null;
             },
             priority: 1 as const
         }),
         figure: (nodeElem: HTMLElement) => {
             const img = nodeElem.querySelector('img');
             if (img) {
                 return {
                     conversion(domNode: HTMLElement) {
                         const kgClass = domNode.className.match(/kg-width-(wide|full)/);
                         const grafClass = domNode.className.match(/graf--layout(FillWidth|OutsetCenter)/);
 
-                        if (!img) {
-                            return null;
-                        }
-
                         const payload = readImageAttributesFromElement(img);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/image/image-parser.ts` at line 5, Change
the parseImageNode signature so the ImageNode constructor returns LexicalNode
instead of unknown (i.e., ImageNode: new (data: Record<string, unknown>) =>
LexicalNode), then remove any force-casts of the created node to LexicalNode
inside parseImageNode and adjust usages to use the correct type directly; also
delete the redundant/unreachable guard that checks img again inside the
conversion function (the img is already validated before calling the converter),
and propagate the same tighter constructor typing and guard removal pattern to
the other parser functions that follow this pattern (so you eliminate unsafe
casts and unreachable code).
packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts (1)

24-30: Deduplicate language extraction to avoid parser drift.

The class-to-language parsing logic is duplicated in both conversion paths. Extracting it to a small helper keeps both paths behaviorally identical over time.

♻️ Refactor sketch
+function extractLanguageFromClasses(preClass: string, codeClass: string): string | undefined {
+    const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i;
+    const match = preClass.match(langRegex) || codeClass.match(langRegex);
+    return match?.[1]?.toLowerCase();
+}
...
-                        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 language = extractLanguageFromClasses(
+                            pre.getAttribute('class') || '',
+                            code.getAttribute('class') || ''
+                        );
+                        if (language) {
+                            payload.language = language;
+                        }
...
-                        const preClass = domNode.getAttribute('class') || '';
-                        const codeClass = codeElement.getAttribute('class') || '';
-                        const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i;
-                        const languageMatches = preClass.match(langRegex) || codeClass.match(langRegex);
-                        if (languageMatches) {
-                            payload.language = languageMatches[1].toLowerCase();
-                        }
+                        const language = extractLanguageFromClasses(
+                            domNode.getAttribute('class') || '',
+                            codeElement.getAttribute('class') || ''
+                        );
+                        if (language) {
+                            payload.language = language;
+                        }

Also applies to: 49-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts` around
lines 24 - 30, The language extraction logic in codeblock-parser.ts (the
langRegex, preClass/codeClass matching and setting payload.language) is
duplicated; extract it into a small helper (e.g., getLanguageFromNodes or
extractLanguageFromClasses) that accepts the pre and code elements or their
class strings, runs the existing regex match, returns the lowercased language
(or null/undefined) and then use that helper in both places where
preClass/codeClass are currently handled (the block that sets payload.language
at the first occurrence and the second occurrence around lines 49-53) so both
conversion paths share identical logic.
packages/kg-default-nodes/test/nodes/file.test.ts (1)

67-69: Remove unnecessary double-cast and align test assertion with fileSize string type.

The double-cast as unknown as string on line 67 defeats TypeScript's strict typing. Since fileSize is defined as string in the interface (see FileNode.ts), the assertion on line 68 comparing against a numeric value is inconsistent. The formattedFileSize getter converts the string to a number internally, so the fix below maintains correctness while improving type safety:

♻️ Proposed cleanup
-            node.fileSize = 123456 as unknown as string;
-            node.fileSize.should.equal(123456);
+            node.fileSize = '123456';
+            node.fileSize.should.equal('123456');
             node.formattedFileSize.should.equal('121 KB');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 67 - 69,
Remove the unnecessary "as unknown as string" double-cast and make the test set
node.fileSize to a string (e.g., node.fileSize = '123456') to match the
FileNode.ts type; update the assertion that currently checks
node.fileSize.should.equal(123456) to assert the string value (e.g.,
node.fileSize.should.equal('123456')) while keeping the existing
node.formattedFileSize.should.equal('121 KB') check unchanged so the
formattedFileSize getter still converts the string to a number internally.
packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)

64-65: Simplify the type cast chain.

The double cast domNode as unknown as HTMLIFrameElement can be simplified. Since the iframe check already confirms this is an IFRAME element, consider typing the conversion callback parameter more specifically:

-                   conversion(domNode: HTMLElement) {
-                       const payload = _createPayloadForIframe(domNode as unknown as HTMLIFrameElement);
+                   conversion(domNode: HTMLIFrameElement) {
+                       const payload = _createPayloadForIframe(domNode);

This matches the guard at line 62 that confirms nodeElem.tagName === 'IFRAME'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 64 -
65, The conversion callback currently types its parameter as HTMLElement and
then does a double cast when calling _createPayloadForIframe; change the
conversion parameter to a more specific HTMLIFrameElement (e.g.,
conversion(domNode: HTMLIFrameElement)) so the iframe guard (nodeElem.tagName
=== 'IFRAME') aligns with the parameter type and you can call
_createPayloadForIframe(domNode) without the unnecessary "as unknown as" cast.
packages/kg-default-nodes/test/nodes/email.test.ts (1)

158-159: Type cast pattern for exportDOM is repeated throughout tests.

The as unknown as LexicalEditor cast works around a type mismatch where exportDOM expects LexicalEditor but tests pass render options. This pattern appears consistently across all test files in this migration.

Consider adding a test utility type or wrapper to encapsulate this cast, improving readability and centralizing the workaround:

// In test-utils/index.ts
export type ExportDOMOptions = Record<string, unknown>;
export function asEditorForExport(options: ExportDOMOptions): LexicalEditor {
    return options as unknown as LexicalEditor;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/email.test.ts` around lines 158 - 159,
Tests repeatedly use the unsafe cast "as unknown as LexicalEditor" when calling
exportDOM; centralize this workaround by adding a small test utility: define a
type ExportDOMOptions and a helper function asEditorForExport(options) that
returns options cast to LexicalEditor, then replace occurrences of "... as
unknown as LexicalEditor" in tests (e.g., in emailNode.exportDOM calls) with a
call to asEditorForExport(options) to improve readability and maintainability.
packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts (2)

10-15: Consider centralizing RenderOptions interface.

The RenderOptions interface pattern (with createDocument, dom, target, and index signature) is duplicated across multiple renderers in this migration. This could be extracted to a shared type in a common location to reduce duplication and ensure consistency.

Also applies to: 4-8

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines
10 - 15, Extract the duplicated RenderOptions interface (with createDocument,
dom.window.document, target and the index signature) into a single
exported/shared type (e.g., RenderOptions) in a common rendering types module,
export it, then replace the local interface declarations in callout-renderer.ts
and the other renderers with imports of that shared RenderOptions; ensure the
shared type preserves createDocument?: () => Document, dom?: { window: {
document: Document } }, target?: string and [key: string]: unknown so existing
call sites (e.g., functions referencing createDocument, dom, or target) remain
type-compatible.

25-27: Mutating input parameter node.backgroundColor is a side effect.

The function modifies the incoming node object directly. If callers don't expect this mutation, it could cause subtle bugs. Consider working with a local copy:

-    if (!node.backgroundColor || !node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) {
-        node.backgroundColor = 'white';
-    }
+    let backgroundColor = node.backgroundColor;
+    if (!backgroundColor || !backgroundColor.match(/^[a-zA-Z\d-]+$/)) {
+        backgroundColor = 'white';
+    }

Then use backgroundColor in the class assignment on line 29.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines
25 - 27, The code mutates the incoming node by assigning node.backgroundColor =
'white'; instead create a local const (e.g. const backgroundColor =
node.backgroundColor && node.backgroundColor.match(/^[a-zA-Z\d-]+$/) ?
node.backgroundColor : 'white') and use that local backgroundColor for the class
assignment instead of mutating node.backgroundColor; update any references in
this file that currently read node.backgroundColor (notably the className
construction in the callout renderer) to use the new local variable.
packages/kg-default-nodes/eslint.config.mjs (1)

23-29: Disabling no-unsafe-declaration-merging warrants documentation.

The rule is disabled for all source files. If this is specifically needed for Lexical's decorator node patterns (interface + class merging), consider adding a comment explaining why, or narrowing the scope to specific files that require it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/eslint.config.mjs` around lines 23 - 29, The rule
'@typescript-eslint/no-unsafe-declaration-merging' is globally disabled in the
ESLint config; either document why it's disabled (e.g., a brief comment stating
it's required for Lexical's decorator node patterns where interface+class
merging is intentional) or restrict the disablement to the specific files that
need it (for example narrow the files pattern from 'src/**/*.ts' to the Lexical
node files). Update the config entry for the rule in the object containing
files: ['src/**/*.ts'] so it includes a concise comment referencing Lexical
decorator node merging or change the files array to only target the module(s)
that require declaration merging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60da2381-4d9e-4c8c-86fa-ee9bf596a84d

📥 Commits

Reviewing files that changed from the base of the PR and between 134858b and b83e572.

⛔ Files ignored due to path filters (2)
  • packages/kg-default-nodes/src/nodes/at-link/kg-link.svg is excluded by !**/*.svg
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (162)
  • packages/kg-default-nodes/eslint.config.mjs
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/lib/kg-default-nodes.js
  • packages/kg-default-nodes/lib/nodes/at-link/index.js
  • packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
  • packages/kg-default-nodes/lib/nodes/html/html-parser.js
  • packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
  • packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
  • packages/kg-default-nodes/lib/utils/is-unsplash-image.js
  • packages/kg-default-nodes/package.json
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-parser.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/src/nodes/product/product-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/test/generate-decorator-node.test.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/call-to-action.test.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/test/test-utils/assertions.js
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/index.js
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/test/test-utils/overrides.js
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/test/utils/visibility.test.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/tsconfig.test.json
  • packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js
💤 Files with no reviewable changes (14)
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
  • packages/kg-default-nodes/lib/utils/is-unsplash-image.js
  • packages/kg-default-nodes/lib/nodes/at-link/index.js
  • packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
  • packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
  • packages/kg-default-nodes/lib/nodes/html/html-parser.js
  • packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/test/test-utils/assertions.js
  • packages/kg-default-nodes/test/test-utils/index.js
  • packages/kg-default-nodes/test/test-utils/overrides.js
  • packages/kg-default-nodes/lib/kg-default-nodes.js
✅ Files skipped from review due to trivial changes (33)
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/tsconfig.test.json
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
🚧 Files skipped from review as they are similar to previous changes (72)
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/package.json
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts

Comment thread packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
Comment thread packages/kg-default-nodes/src/nodes/video/video-renderer.ts
Comment thread packages/kg-default-nodes/test/nodes/file.test.ts
Comment thread packages/kg-default-nodes/test/nodes/gallery.test.ts Outdated
@kevinansfield kevinansfield force-pushed the ts/kg-default-nodes branch 5 times, most recently from 06e801f to e38ff89 Compare March 31, 2026 14:52
@kevinansfield kevinansfield force-pushed the ts/kg-default-nodes branch 3 times, most recently from 09cfc78 to ad10088 Compare April 16, 2026 12:45
@kevinansfield kevinansfield force-pushed the ts/kg-default-nodes branch 3 times, most recently from 6985fcd to f995f5e Compare April 16, 2026 13:01
Copy link
Copy Markdown

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts (1)

184-217: ⚠️ Potential issue | 🟠 Major

Guard email rendering when postUrl is missing.

RenderOptions.postUrl is optional, but this template unconditionally injects it into href. If target is email without postUrl, output contains broken href="undefined" links.

Proposed fix
 export function renderAudioNode(node: AudioNodeData, options: RenderOptions = {}) {
     addCreateDocumentOption(options);
     const document = options.createDocument!();

     if (!node.src || node.src.trim() === '') {
         return renderEmptyContainer(document);
     }

     const thumbnailCls = getThumbnailCls(node);
     const emptyThumbnailCls = getEmptyThumbnailCls(node);

     if (options.target === 'email') {
-        return emailTemplate(node, document, options, thumbnailCls, emptyThumbnailCls);
+        if (!options.postUrl) {
+            return renderEmptyContainer(document);
+        }
+        return emailTemplate(node, document, options.postUrl, thumbnailCls, emptyThumbnailCls);
     } else {
         return frontendTemplate(node, document, thumbnailCls, emptyThumbnailCls);
     }
 }
 
-function emailTemplate(node: AudioNodeData, document: Document, options: RenderOptions, thumbnailCls: string, emptyThumbnailCls: string) {
+function emailTemplate(node: AudioNodeData, document: Document, postUrl: string, thumbnailCls: string, emptyThumbnailCls: string) {
     const html = (`
         <table cellspacing="0" cellpadding="0" border="0" class="kg-audio-card">
                 <tr>
                     <td>
                         <table cellspacing="0" cellpadding="0" border="0" width="100%">
                             <tr>
                                 <td width="60">
-                                    <a href="${options.postUrl}" style="display: block; width: 60px; height: 60px; padding-top: 4px; padding-right: 16px; padding-bottom: 4px; padding-left: 4px; border-radius: 2px;">
+                                    <a href="${postUrl}" style="display: block; width: 60px; height: 60px; padding-top: 4px; padding-right: 16px; padding-bottom: 4px; padding-left: 4px; border-radius: 2px;">
...
-                                    <a href="${options.postUrl}" style="position: absolute; display: block; top: 0; right: 0; bottom: 0; left: 0;"></a>
+                                    <a href="${postUrl}" style="position: absolute; display: block; top: 0; right: 0; bottom: 0; left: 0;"></a>
...
-                                                <a href="${options.postUrl}" class="kg-audio-title">${node.title}</a>
+                                                <a href="${postUrl}" class="kg-audio-title">${node.title}</a>
...
-                                                            <a href="${options.postUrl}" class="kg-audio-play-button"></a>
+                                                            <a href="${postUrl}" class="kg-audio-play-button"></a>
...
-                                                            <a href="${options.postUrl}" class="kg-audio-duration">${getFormattedDuration(node.duration)}<span class="kg-audio-link"> • Click to play audio</span></a>
+                                                            <a href="${postUrl}" class="kg-audio-duration">${getFormattedDuration(node.duration)}<span class="kg-audio-link"> • Click to play audio</span></a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 184
- 217, The emailTemplate function injects options.postUrl into multiple anchor
hrefs regardless of presence, causing href="undefined"; update emailTemplate to
guard all uses of RenderOptions.postUrl (e.g. the anchors around the thumbnail,
title, play button and duration) by either conditionally omitting the href
attribute when postUrl is falsy or substituting a safe fallback (e.g. '#' or
empty string) so anchors are not rendered with "undefined"; check occurrences in
emailTemplate (thumbnail anchor, title anchor, play-button anchor, duration
anchor) and use a single pattern like postUrl ? `href="${options.postUrl}"` : ''
to keep markup valid while preserving classes and
getFormattedDuration(node.duration) usage.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts (1)

27-31: ⚠️ Potential issue | 🟡 Minor

Type for imageUrl allows number which seems incorrect.

The imageData type declares imageUrl: string | number, but image URLs should always be strings. The readImageAttributesFromElement function returns src as a string, and line 35 assigns it directly. The | number appears to be a copy-paste from the dimension fields.

Suggested fix
-                        const imageData: {imageUrl: string | number; imageWidth: string | number | null; imageHeight: string | number | null} = {
+                        const imageData: {imageUrl: string; imageWidth: string | number | null; imageHeight: string | number | null} = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts`
around lines 27 - 31, The imageData type incorrectly allows imageUrl to be a
number; update the declaration so imageUrl is string only (remove | number) to
match readImageAttributesFromElement which returns a string and the assignment
at imageData.imageUrl; adjust the type signature where imageData is defined and
any related uses to ensure imageUrl is typed as string while leaving imageWidth
and imageHeight as string | number | null.
♻️ Duplicate comments (6)
packages/kg-default-nodes/src/nodes/file/file-renderer.ts (1)

55-68: ⚠️ Potential issue | 🟠 Major

Avoid emitting empty email href values when postUrl is unset.

Line 55, Line 60, Line 64, and Line 68 currently fall back to '', which renders broken clickable links in email output. Use options.postUrl || node.src (the latter is already guaranteed by Line 27).

Suggested fix
 function emailTemplate(node: FileNodeData, document: Document, options: RenderOptions) {
+    const href = escapeHtml(options.postUrl || node.src);
     let iconCls;
@@
-                                    <a href="${escapeHtml(options.postUrl || '')}" class="kg-file-title">${escapeHtml(node.fileTitle)}</a>
+                                    <a href="${href}" class="kg-file-title">${escapeHtml(node.fileTitle)}</a>
@@
-                                    <a href="${escapeHtml(options.postUrl || '')}" class="kg-file-description">${escapeHtml(node.fileCaption)}</a>
+                                    <a href="${href}" class="kg-file-description">${escapeHtml(node.fileCaption)}</a>
@@
-                                    <a href="${escapeHtml(options.postUrl || '')}" class="kg-file-meta"><span class="kg-file-name">${escapeHtml(node.fileName)}</span> &bull; ${bytesToSize(node.fileSize)}</a>
+                                    <a href="${href}" class="kg-file-meta"><span class="kg-file-name">${escapeHtml(node.fileName)}</span> &bull; ${bytesToSize(node.fileSize)}</a>
@@
-                                <a href="${escapeHtml(options.postUrl || '')}" style="display: block; top: 0; right: 0; bottom: 0; left: 0;">
+                                <a href="${href}" style="display: block; top: 0; right: 0; bottom: 0; left: 0;">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/file/file-renderer.ts` around lines 55 -
68, The template emits empty href attributes when options.postUrl is unset;
update all href uses that currently use escapeHtml(options.postUrl || '') to
fall back to node.src instead (use escapeHtml(options.postUrl || node.src)) so
links for file title, caption, meta and thumbnail point to node.src when postUrl
is missing; locate these occurrences in file-renderer.ts where hrefs wrap
node.fileTitle, node.fileCaption, node.fileName and the thumbnail anchor and
change the fallback accordingly.
packages/kg-default-nodes/src/nodes/product/product-renderer.ts (1)

64-90: ⚠️ Potential issue | 🔴 Critical

Escape interpolated product fields before HTML generation (XSS risk).

The templates still interpolate raw data values into HTML strings and those strings are inserted via innerHTML (Line 59). This re-opens an injection path for text and attribute fields (productTitle, productDescription, productButton, productUrl, and similarly productImageSrc).

🔒 Suggested hardening
+import {escapeHtml} from '../../utils/escape-html.js';
 import {addCreateDocumentOption} from '../../utils/add-create-document-option.js';
 import {renderEmptyContainer} from '../../utils/render-empty-container.js';
 import {getResizedImageDimensions} from '../../utils/get-resized-image-dimensions.js';

 export function cardTemplate({data}: {data: Record<string, unknown>}) {
+    const productImageSrc = escapeHtml(String(data.productImageSrc ?? ''));
+    const productTitle = escapeHtml(String(data.productTitle ?? ''));
+    const productDescription = escapeHtml(String(data.productDescription ?? ''));
+    const productUrl = escapeHtml(String(data.productUrl ?? ''));
+    const productButton = escapeHtml(String(data.productButton ?? ''));
+
     return (
         `
         <div class="kg-card kg-product-card">
             <div class="kg-product-card-container">
-                ${data.productImageSrc ? `<img src="${data.productImageSrc}" ${data.productImageWidth ? `width="${data.productImageWidth}"` : ''} ${data.productImageHeight ? `height="${data.productImageHeight}"` : ''} class="kg-product-card-image" loading="lazy" />` : ''}
+                ${data.productImageSrc ? `<img src="${productImageSrc}" ${data.productImageWidth ? `width="${data.productImageWidth}"` : ''} ${data.productImageHeight ? `height="${data.productImageHeight}"` : ''} class="kg-product-card-image" loading="lazy" />` : ''}
                 <div class="kg-product-card-title-container">
-                    <h4 class="kg-product-card-title">${data.productTitle}</h4>
+                    <h4 class="kg-product-card-title">${productTitle}</h4>
                 </div>
 ...
-                <div class="kg-product-card-description">${data.productDescription}</div>
+                <div class="kg-product-card-description">${productDescription}</div>
                 ${data.productButtonEnabled ? `
-                    <a href="${data.productUrl}" class="kg-product-card-button kg-product-card-btn-accent" target="_blank" rel="noopener noreferrer"><span>${data.productButton}</span></a>
+                    <a href="${productUrl}" class="kg-product-card-button kg-product-card-btn-accent" target="_blank" rel="noopener noreferrer"><span>${productButton}</span></a>
                 ` : ''}
             </div>
         </div>
     `
     );
 }

Also applies to: 93-194

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines
64 - 90, The cardTemplate function is inserting raw data into an HTML string
which is then used with innerHTML, creating an XSS risk; update cardTemplate to
sanitize/escape all interpolated values (productTitle, productDescription,
productButton, productUrl, productImageSrc, productImageWidth,
productImageHeight and any starIcon/star class strings) before building the
markup or, better, switch to constructing DOM nodes programmatically and set
textContent/attribute values rather than embedding raw values in a template
string; add or reuse a single escape/sanitize helper (e.g., escapeHtml or
sanitizeUrl) and apply it to attribute and text interpolations used by
cardTemplate (and the other template blocks referenced in the comment).
packages/kg-default-nodes/test/nodes/gallery.test.ts (1)

627-629: ⚠️ Potential issue | 🟡 Minor

Avoid indexing nodes[0] when empty results are valid.

This assertion can fail for the wrong reason when parsing returns []. Prefer checking that no returned node is a gallery.

Suggested fix
-                nodes.length.should.not.equal(1);
-                nodes[0].getType().should.not.equal('gallery');
+                nodes.some(node => node.getType() === 'gallery').should.be.false();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 627 - 629,
The test currently indexes nodes[0] which can throw when
$generateNodesFromDOM(editor, document) returns an empty array; instead assert
that no returned node is a gallery by replacing the length/index checks with a
check on the nodes array (e.g., using Array.prototype.every or
Array.prototype.some) to ensure every GalleryNode from $generateNodesFromDOM has
getType() !== 'gallery' — update the assertions around the nodes variable and
references to GalleryNode and getType() accordingly.
packages/kg-default-nodes/package.json (1)

19-21: ⚠️ Potential issue | 🟠 Major

Clean build/ before emitting artifacts.

tsc will not remove outputs for renamed or deleted source files. Since this package now publishes the whole build directory, stale JS or .d.ts files can survive local rebuilds and get shipped unless build/ is cleaned first.

Suggested script update
   "scripts": {
+    "clean": "node -e \"require('node:fs').rmSync('build', {recursive: true, force: true})\"",
     "dev": "tsc --watch --preserveWatchOutput",
-    "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
-    "prepare": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
-    "pretest": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json",
+    "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
+    "prepare": "yarn build",
+    "pretest": "yarn build && tsc -p tsconfig.test.json",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/package.json` around lines 19 - 21, The build,
prepare, and pretest npm scripts ("build", "prepare", "pretest") do not clean
the build/ directory first, which allows stale artifacts to be published; update
each script to remove or clean build/ before running tsc (e.g., run a cleanup
command such as rimraf build or rm -rf build or call an existing "clean" npm
script) so that build/ is freshly created prior to the existing tsc && tsc -p
... && echo ... sequence.
packages/kg-default-nodes/test/nodes/header.test.ts (2)

151-159: ⚠️ Potential issue | 🟡 Minor

Replace this no-op with a real assertion.

void element makes the test pass regardless of what exportDOM() returns, so the empty-render case is no longer validated. packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts:23-78 currently returns an element, not null, so this should assert the current empty-container behavior explicitly instead of silently skipping it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 151 - 159,
Replace the no-op "void element" with a real assertion that verifies the
renderer's current empty-container behavior: call $createHeaderNode(...) and
node.exportDOM(...), then assert that the returned element is a DOM Element (not
null) and that it contains no meaningful content (e.g., has no child elements
and empty/textContent trimmed is empty) to explicitly validate the behavior of
the header renderer in
packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts and
the exportDOM path.

468-476: ⚠️ Potential issue | 🟡 Minor

This v2 "renders nothing" test also stopped asserting behavior.

Same issue here: void element turns the test into a no-op. packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts:213-266 still returns an element value, so please assert the actual result for the empty-content case rather than leaving it unverified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 468 - 476,
Test currently discards the exportDOM result with "void element" making the case
a no-op; instead, update the spec using $createHeaderNode and
node.exportDOM(exportOptions) to assert the actual empty-content behavior
produced by the v2 header renderer (the code path in the header renderer that
handles header/subheader null + buttonEnabled=false). Replace the no-op with an
assertion that matches the renderer's real return for empty content (for example
assert that element is a DocumentFragment or Element with no children and no
textContent / innerHTML), or explicitly assert the expected element type and
that its childNodes.length === 0.
🧹 Nitpick comments (12)
packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)

90-92: Minor: mutating the input iframe.src directly.

This modifies the original DOM element's src attribute during parsing. If the parser ever runs on a live document (rather than a cloned/temporary one), this could cause unintended side effects. Consider storing the normalized URL in a local variable instead:

♻️ Optional refactor
     // if it's a schemaless URL, convert to https
+    let src = iframe.src;
     if (iframe.src.match(/^\/\//)) {
-        iframe.src = `https:${iframe.src}`;
+        src = `https:${iframe.src}`;
     }
 
     const payload: Record<string, unknown> = {
-        url: iframe.src
+        url: src
     };
 
-    payload.html = iframe.outerHTML;
+    // Note: outerHTML still reflects original src; if normalized URL is needed in html,
+    // additional handling would be required
+    payload.html = iframe.outerHTML;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 90 -
92, The parser currently mutates the input DOM by assigning to iframe.src when
normalizing protocol-relative URLs; instead, read iframe.src into a local
variable (e.g., normalizedSrc), normalize that variable (if it matches /^\/\//
prefix, prepend "https:"), and use the local normalizedSrc for any further
processing or returned values rather than writing back to iframe.src in
embed-parser.ts (look for the iframe.src handling block).
packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)

22-25: Consider extracting the complex createElement return type into a separate interface.

The inline return type for createElement is quite dense and spans multiple nested properties. Extracting it would improve readability.

♻️ Suggested refactor
+interface BrowserCanvasContext {
+    fillStyle: string;
+    fillRect(x: number, y: number, w: number, h: number): void;
+    getImageData(x: number, y: number, w: number, h: number): { data: number[] };
+}
+
+interface BrowserCanvasElement extends BrowserElement {
+    innerHTML: string;
+    width: number;
+    height: number;
+    getContext(id: string): BrowserCanvasContext;
+}
+
 interface BrowserDocument {
     currentScript: { parentElement: BrowserElement } | null;
-    createElement(tag: string): BrowserElement & { innerHTML: string; width: number; height: number; getContext(id: string): { fillStyle: string; fillRect(x: number, y: number, w: number, h: number): void; getImageData(x: number, y: number, w: number, h: number): { data: number[] } } };
+    createElement(tag: string): BrowserCanvasElement;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around
lines 22 - 25, The inline complex return type of BrowserDocument.createElement
should be extracted into a named interface to improve readability and reuse:
define a new interface (e.g., CanvasElement or CreateElementReturn) that extends
BrowserElement and declares innerHTML, width, height, getContext(...) returning
the typed context and getImageData(...) returning the data array, then replace
the inline return type in the BrowserDocument interface with this new interface;
update any other uses of the same inline shape (if present) to reference the new
interface and keep names BrowserDocument and createElement unchanged.
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)

3-3: Tighten constructor typing to remove unsafe unknown -> LexicalNode casts

Line 3 currently allows non-Lexical constructors and defers failure to runtime. Prefer a generic constructor constrained to LexicalNode so Line 26 and Line 40 can return node directly.

#!/bin/bash
# Verify parser usage and HtmlNode inheritance before applying the generic type refactor.
cat -n packages/kg-default-nodes/src/nodes/html/html-parser.ts
cat -n packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
rg -n "parseHtmlNode\\(" packages/kg-default-nodes/src packages/kg-default-nodes/test

Also applies to: 25-26, 39-40

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` at line 3, The
parseHtmlNode function currently accepts a constructor typed as new (data:
Record<string, unknown>) => unknown which allows non-LexicalNode classes and
forces unsafe casts; change the signature to be generic (e.g. function
parseHtmlNode<T extends LexicalNode>(HtmlNode: new (data: Record<string,
unknown>) => T) ) so the constructor is constrained to produce a LexicalNode
subtype, then update internal variables and return types so the values returned
at the places that currently cast/return node (the code paths around the current
Line 26 and Line 40) are returned as T directly without unknown casts; ensure
any local helpers or variables referencing the constructed node use
T/LexicalNode types (and update imports if needed) so type-checking prevents
non-Lexical constructors from being passed to parseHtmlNode.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

410-410: firstElementChild cast may hide null reference issues.

Both lines cast firstElementChild to RenderOutput['element'], but firstElementChild can be null if the template produces no elements. While this is unlikely given the template structure, the cast bypasses TypeScript's null safety.

Suggested defensive check
-    return renderWithVisibility({element: emailDiv.firstElementChild as RenderOutput['element']}, node.visibility, options);
+    const firstChild = emailDiv.firstElementChild;
+    if (!firstChild) {
+        return renderEmptyContainer(emailDoc);
+    }
+    return renderWithVisibility({element: firstChild as RenderOutput['element']}, node.visibility, options);

Also applies to: 424-424

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
at line 410, The call to renderWithVisibility currently casts
emailDiv.firstElementChild to RenderOutput['element'], which hides potential
null refs; update where renderWithVisibility is called (using
emailDiv.firstElementChild and the other similar occurrence) to first check if
firstElementChild is non-null and handle the null case explicitly (e.g., return
a fallback RenderOutput, throw a descriptive error, or skip rendering) before
calling renderWithVisibility with node.visibility and options so TypeScript
null-safety is preserved and runtime NPEs are avoided.
packages/kg-default-nodes/src/utils/visibility.ts (1)

38-41: RenderOutput type is well-defined but may be overly strict for some callers.

The RenderOutput interface requires element to have innerHTML, outerHTML, and optionally value. This works for most DOM elements, but note that callers using firstElementChild (like calltoaction-renderer.ts) may pass null if the template produces no elements—the cast to RenderOutput['element'] bypasses this check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/visibility.ts` around lines 38 - 41, The
RenderOutput.element type is too strict and disallows null values returned by
DOM lookups (e.g., firstElementChild in calltoaction-renderer.ts); change the
RenderOutput interface so element can be null (make element type "(Element &
{innerHTML: string; outerHTML: string; value?: string}) | null") and then update
callers that cast to RenderOutput['element'] (such as calltoaction-renderer) to
handle the null case safely before accessing innerHTML/outerHTML/value.
packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts (1)

53-67: Consider sanitizing header/subheader innerHTML assignments.

Lines 57 and 65 assign templateData.header and templateData.subheader directly to innerHTML. If these values can contain untrusted user input, this creates XSS sinks. This appears to be a pre-existing pattern, but worth noting for a future sanitization pass across renderers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts`
around lines 53 - 67, The code assigns untrusted content to innerHTML in
header-renderer.ts (in the block using
templateData.hasHeader/templateData.hasSubheader), creating XSS risk; update the
assignments for headerElement and subheaderElement (currently using
headerElement.innerHTML = templateData.header and subheaderElement.innerHTML =
templateData.subheader) to a safe approach—either set textContent instead of
innerHTML if HTML is not needed, or run
templateData.header/templateData.subheader through a trusted sanitizer (e.g.,
DOMPurify or the project's sanitize utility) before assigning to innerHTML; make
the same change for both headerElement and subheaderElement.
packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts (1)

140-142: Non-null assertion on getResizedImageDimensions return value.

Line 140 uses ! to assert the result is non-null. While getResizedImageDimensions should always return dimensions when given valid input, if image has invalid dimensions (e.g., zero width/height), the function behavior is unclear.

Consider adding a guard or verifying the function's contract:

Suggested improvement
-                    const newImageDimensions = getResizedImageDimensions(image, {width: 600})!;
-                    img.setAttribute('width', String(newImageDimensions.width));
-                    img.setAttribute('height', String(newImageDimensions.height));
+                    const newImageDimensions = getResizedImageDimensions(image, {width: 600});
+                    if (newImageDimensions) {
+                        img.setAttribute('width', String(newImageDimensions.width));
+                        img.setAttribute('height', String(newImageDimensions.height));
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines
140 - 142, The code currently uses a non-null assertion on
getResizedImageDimensions(image, {width: 600}) in gallery-renderer.ts; instead,
call getResizedImageDimensions and store the result in a variable (e.g., const
newImageDimensions = getResizedImageDimensions(...)), check if
newImageDimensions is truthy before using it, and handle the null case by either
skipping setting img.width/height, using a safe fallback size, or logging a
warning; update the img.setAttribute calls to only run when newImageDimensions
is valid to avoid runtime errors.
packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts (1)

8-8: Non-null assertion on regex match is fragile.

While the regex /[^/]*$/ technically always matches (it can match an empty string at the end), using ! makes the code less defensive. If the regex were ever changed, this could throw.

Suggested improvement
-    image.fileName = element.src.match(/[^/]*$/)![0];
+    image.fileName = element.src.match(/[^/]*$/)?.[0] ?? '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts` at line 8,
Replace the non-null assertion on the regex match by capturing the result of
element.src.match(/[^/]*$/) into a variable, check for null/undefined before
indexing, and fall back to a safe default (e.g., the original element.src or an
empty string) when setting image.fileName; update the assignment that sets
image.fileName (the line using element.src.match and image.fileName) to use this
guarded logic so it won't throw if the match changes or fails.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts (1)

17-17: Fallback to empty object may cause unexpected behavior.

When buttonElement is null, buttonStyles becomes {} as CSSStyleDeclaration. Accessing buttonStyles.backgroundColor and buttonStyles.color on an empty object will return undefined, which then correctly falls back to the defaults on lines 18-19. However, casting an empty object to CSSStyleDeclaration is misleading and could cause issues if additional style properties are accessed without fallbacks.

Consider using optional chaining consistently:

Suggested improvement
-                        const buttonStyles = buttonElement?.style || {} as CSSStyleDeclaration;
-                        const buttonColor = buttonStyles.backgroundColor || '#000000';
-                        const buttonTextColor = buttonStyles.color || '#ffffff';
+                        const buttonColor = buttonElement?.style.backgroundColor || '#000000';
+                        const buttonTextColor = buttonElement?.style.color || '#ffffff';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts` at
line 17, The current line casts an empty object to CSSStyleDeclaration which is
misleading and can hide missing properties; change the usage so buttonStyles is
either the real CSSStyleDeclaration or undefined (e.g., const buttonStyles =
buttonElement?.style) and use optional chaining/nullish coalescing when reading
properties (references: buttonStyles, buttonElement in calltoaction-parser.ts)
so accesses like buttonStyles?.backgroundColor and buttonStyles?.color safely
fall back to defaults without casting an empty object.
packages/kg-default-nodes/src/generate-decorator-node.ts (1)

124-128: Falsy check may incorrectly use default for 0 or empty string values.

The condition data[prop.name] || prop.default on line 127 will use the default value when data[prop.name] is 0, '', or other falsy values. While booleans are correctly handled with ?? on line 125, numeric properties with value 0 or string properties with empty string '' would incorrectly fall back to defaults.

This is pre-existing behavior, but the TypeScript migration is a good opportunity to address it:

Suggested fix
             internalProps.forEach((prop) => {
-                if (typeof prop.default === 'boolean') {
-                    this[prop.privateName] = data[prop.name] ?? prop.default;
-                } else {
-                    this[prop.privateName] = data[prop.name] || prop.default;
-                }
+                // Use nullish coalescing for all types to preserve explicit 0, '', false values
+                this[prop.privateName] = data[prop.name] ?? prop.default;
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 124 -
128, The assignment branch that sets this[prop.privateName] uses a falsy check
(data[prop.name] || prop.default) which incorrectly overrides valid falsy values
like 0 or ''. Change the non-boolean branch to use a nullish coalescing-style
check so that undefined/null fall back to prop.default but valid falsy values
(0, '') are preserved; update the conditional handling around prop.default,
prop.name and prop.privateName accordingly (mirror the existing boolean branch's
use of ?? for consistency).
packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts (1)

25-27: Mutating input parameter node.backgroundColor.

The function modifies node.backgroundColor directly, which mutates the caller's object. This could cause unexpected side effects if the caller expects the node data to remain unchanged after rendering.

Consider either documenting this behavior or working with a local copy:

Suggested improvement
+    let backgroundColor = node.backgroundColor;
     // backgroundColor can end up with `rgba(0, 0, 0, 0)` from old mobiledoc copy/paste
     // that is invalid when used in a class name so fall back to `white` when we don't have
     // something that looks like a valid class
-    if (!node.backgroundColor || !node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) {
-        node.backgroundColor = 'white';
+    if (!backgroundColor || !backgroundColor.match(/^[a-zA-Z\d-]+$/)) {
+        backgroundColor = 'white';
     }

-    element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${node.backgroundColor}`);
+    element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${backgroundColor}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines
25 - 27, The code directly mutates node.backgroundColor; avoid mutating the
caller's object by validating into a local value or a shallow copy instead:
e.g., create a local variable (const backgroundColor = (node.backgroundColor &&
node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) ? node.backgroundColor : 'white')
or clone the node (const safeNode = { ...node }) and update
safeNode.backgroundColor, then use backgroundColor/safeNode for rendering;
update the logic around node.backgroundColor validation so no assignment is made
to the original node object.
packages/kg-default-nodes/test/nodes/gallery.test.ts (1)

637-1030: Reduce repeated as unknown as LexicalEditor casts in exportDOM calls.

These casts hide type mismatches and make the tests harder to trust. Prefer passing correctly typed export options (or a helper wrapper with a single cast point) instead of per-call double-casts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 637 -
1030, Tests call galleryNode.exportDOM(... as unknown as LexicalEditor) many
times; instead create a single correctly typed export options value or a small
test helper to centralize the cast so individual calls don't repeat "as unknown
as LexicalEditor". Update references to exportOptions and calls to exportDOM to
use the properly typed variable (or a helper function like renderExport(node,
opts) that performs one cast), and remove the per-call double-casts in tests
that call exportDOM.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 12-22: Pre-scan for the closing kg-card-end comment before
mutating: starting from nextNode, walk siblings using isHtmlEndComment to find
the matching end comment and if none is found, skip the destructive while loop
entirely so you don't accumulate unrelated nodes into html; only after
confirming the end marker exists, run the existing loop that uses currentNode,
checks nodeType, pushes into the html array, and calls currentNode.remove().

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 108-110: The factory $createSignupNode currently accepts
Record<string, unknown> while it constructs new SignupNode with a SignupData;
change $createSignupNode's parameter type to SignupData (or a compatible
narrower type/interface) so the factory signature matches SignupNode's
constructor, and update any callers if needed to pass the correct typed object;
refer to $createSignupNode and the SignupNode constructor/SignupData type when
making the change.

In `@packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts`:
- Around line 58-60: Update the type guard function $isTransistorNode so it
returns a type predicate (node is TransistorNode) instead of plain boolean;
locate the exported function $isTransistorNode and change its signature to use
the TypeScript type predicate while keeping the parameter as node: unknown and
the runtime check node instanceof TransistorNode unchanged so TypeScript will
narrow types after the guard.
- Line 1: Replace the root lodash named import with a deep-path default import
to reduce bundle size: change the import of cloneDeep in TransistorNode.ts (the
current "import {cloneDeep} from 'lodash'") to the deep path default form
("import cloneDeep from 'lodash/cloneDeep'") so only the needed utility is
bundled; leave all usages of cloneDeep in the file (e.g., anywhere it's called)
unchanged.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts`:
- Line 39: The code retrieves baseSrc via el.getAttribute('data-src') and then
uses new URL(baseSrc!) and other non-null assertions; change this to explicitly
check that baseSrc is a non-empty string (e.g., if (!baseSrc) return or handle
fallback) before calling new URL(baseSrc) and remove the non-null assertions;
update all usages in this module (the baseSrc variable and the locations
currently asserting with !) so you validate/guard once and then safely use
baseSrc without the ! operator.

---

Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts`:
- Around line 184-217: The emailTemplate function injects options.postUrl into
multiple anchor hrefs regardless of presence, causing href="undefined"; update
emailTemplate to guard all uses of RenderOptions.postUrl (e.g. the anchors
around the thumbnail, title, play button and duration) by either conditionally
omitting the href attribute when postUrl is falsy or substituting a safe
fallback (e.g. '#' or empty string) so anchors are not rendered with
"undefined"; check occurrences in emailTemplate (thumbnail anchor, title anchor,
play-button anchor, duration anchor) and use a single pattern like postUrl ?
`href="${options.postUrl}"` : '' to keep markup valid while preserving classes
and getFormattedDuration(node.duration) usage.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts`:
- Around line 27-31: The imageData type incorrectly allows imageUrl to be a
number; update the declaration so imageUrl is string only (remove | number) to
match readImageAttributesFromElement which returns a string and the assignment
at imageData.imageUrl; adjust the type signature where imageData is defined and
any related uses to ensure imageUrl is typed as string while leaving imageWidth
and imageHeight as string | number | null.

---

Duplicate comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The build, prepare, and pretest npm scripts ("build",
"prepare", "pretest") do not clean the build/ directory first, which allows
stale artifacts to be published; update each script to remove or clean build/
before running tsc (e.g., run a cleanup command such as rimraf build or rm -rf
build or call an existing "clean" npm script) so that build/ is freshly created
prior to the existing tsc && tsc -p ... && echo ... sequence.

In `@packages/kg-default-nodes/src/nodes/file/file-renderer.ts`:
- Around line 55-68: The template emits empty href attributes when
options.postUrl is unset; update all href uses that currently use
escapeHtml(options.postUrl || '') to fall back to node.src instead (use
escapeHtml(options.postUrl || node.src)) so links for file title, caption, meta
and thumbnail point to node.src when postUrl is missing; locate these
occurrences in file-renderer.ts where hrefs wrap node.fileTitle,
node.fileCaption, node.fileName and the thumbnail anchor and change the fallback
accordingly.

In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 64-90: The cardTemplate function is inserting raw data into an
HTML string which is then used with innerHTML, creating an XSS risk; update
cardTemplate to sanitize/escape all interpolated values (productTitle,
productDescription, productButton, productUrl, productImageSrc,
productImageWidth, productImageHeight and any starIcon/star class strings)
before building the markup or, better, switch to constructing DOM nodes
programmatically and set textContent/attribute values rather than embedding raw
values in a template string; add or reuse a single escape/sanitize helper (e.g.,
escapeHtml or sanitizeUrl) and apply it to attribute and text interpolations
used by cardTemplate (and the other template blocks referenced in the comment).

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Around line 627-629: The test currently indexes nodes[0] which can throw when
$generateNodesFromDOM(editor, document) returns an empty array; instead assert
that no returned node is a gallery by replacing the length/index checks with a
check on the nodes array (e.g., using Array.prototype.every or
Array.prototype.some) to ensure every GalleryNode from $generateNodesFromDOM has
getType() !== 'gallery' — update the assertions around the nodes variable and
references to GalleryNode and getType() accordingly.

In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 151-159: Replace the no-op "void element" with a real assertion
that verifies the renderer's current empty-container behavior: call
$createHeaderNode(...) and node.exportDOM(...), then assert that the returned
element is a DOM Element (not null) and that it contains no meaningful content
(e.g., has no child elements and empty/textContent trimmed is empty) to
explicitly validate the behavior of the header renderer in
packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts and
the exportDOM path.
- Around line 468-476: Test currently discards the exportDOM result with "void
element" making the case a no-op; instead, update the spec using
$createHeaderNode and node.exportDOM(exportOptions) to assert the actual
empty-content behavior produced by the v2 header renderer (the code path in the
header renderer that handles header/subheader null + buttonEnabled=false).
Replace the no-op with an assertion that matches the renderer's real return for
empty content (for example assert that element is a DocumentFragment or Element
with no children and no textContent / innerHTML), or explicitly assert the
expected element type and that its childNodes.length === 0.

---

Nitpick comments:
In `@packages/kg-default-nodes/src/generate-decorator-node.ts`:
- Around line 124-128: The assignment branch that sets this[prop.privateName]
uses a falsy check (data[prop.name] || prop.default) which incorrectly overrides
valid falsy values like 0 or ''. Change the non-boolean branch to use a nullish
coalescing-style check so that undefined/null fall back to prop.default but
valid falsy values (0, '') are preserved; update the conditional handling around
prop.default, prop.name and prop.privateName accordingly (mirror the existing
boolean branch's use of ?? for consistency).

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts`:
- Line 17: The current line casts an empty object to CSSStyleDeclaration which
is misleading and can hide missing properties; change the usage so buttonStyles
is either the real CSSStyleDeclaration or undefined (e.g., const buttonStyles =
buttonElement?.style) and use optional chaining/nullish coalescing when reading
properties (references: buttonStyles, buttonElement in calltoaction-parser.ts)
so accesses like buttonStyles?.backgroundColor and buttonStyles?.color safely
fall back to defaults without casting an empty object.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Line 410: The call to renderWithVisibility currently casts
emailDiv.firstElementChild to RenderOutput['element'], which hides potential
null refs; update where renderWithVisibility is called (using
emailDiv.firstElementChild and the other similar occurrence) to first check if
firstElementChild is non-null and handle the null case explicitly (e.g., return
a fallback RenderOutput, throw a descriptive error, or skip rendering) before
calling renderWithVisibility with node.visibility and options so TypeScript
null-safety is preserved and runtime NPEs are avoided.

In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts`:
- Around line 25-27: The code directly mutates node.backgroundColor; avoid
mutating the caller's object by validating into a local value or a shallow copy
instead: e.g., create a local variable (const backgroundColor =
(node.backgroundColor && node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) ?
node.backgroundColor : 'white') or clone the node (const safeNode = { ...node })
and update safeNode.backgroundColor, then use backgroundColor/safeNode for
rendering; update the logic around node.backgroundColor validation so no
assignment is made to the original node object.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts`:
- Around line 90-92: The parser currently mutates the input DOM by assigning to
iframe.src when normalizing protocol-relative URLs; instead, read iframe.src
into a local variable (e.g., normalizedSrc), normalize that variable (if it
matches /^\/\// prefix, prepend "https:"), and use the local normalizedSrc for
any further processing or returned values rather than writing back to iframe.src
in embed-parser.ts (look for the iframe.src handling block).

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts`:
- Line 8: Replace the non-null assertion on the regex match by capturing the
result of element.src.match(/[^/]*$/) into a variable, check for null/undefined
before indexing, and fall back to a safe default (e.g., the original element.src
or an empty string) when setting image.fileName; update the assignment that sets
image.fileName (the line using element.src.match and image.fileName) to use this
guarded logic so it won't throw if the match changes or fails.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts`:
- Around line 140-142: The code currently uses a non-null assertion on
getResizedImageDimensions(image, {width: 600}) in gallery-renderer.ts; instead,
call getResizedImageDimensions and store the result in a variable (e.g., const
newImageDimensions = getResizedImageDimensions(...)), check if
newImageDimensions is truthy before using it, and handle the null case by either
skipping setting img.width/height, using a safe fallback size, or logging a
warning; update the img.setAttribute calls to only run when newImageDimensions
is valid to avoid runtime errors.

In `@packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts`:
- Around line 53-67: The code assigns untrusted content to innerHTML in
header-renderer.ts (in the block using
templateData.hasHeader/templateData.hasSubheader), creating XSS risk; update the
assignments for headerElement and subheaderElement (currently using
headerElement.innerHTML = templateData.header and subheaderElement.innerHTML =
templateData.subheader) to a safe approach—either set textContent instead of
innerHTML if HTML is not needed, or run
templateData.header/templateData.subheader through a trusted sanitizer (e.g.,
DOMPurify or the project's sanitize utility) before assigning to innerHTML; make
the same change for both headerElement and subheaderElement.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Line 3: The parseHtmlNode function currently accepts a constructor typed as
new (data: Record<string, unknown>) => unknown which allows non-LexicalNode
classes and forces unsafe casts; change the signature to be generic (e.g.
function parseHtmlNode<T extends LexicalNode>(HtmlNode: new (data:
Record<string, unknown>) => T) ) so the constructor is constrained to produce a
LexicalNode subtype, then update internal variables and return types so the
values returned at the places that currently cast/return node (the code paths
around the current Line 26 and Line 40) are returned as T directly without
unknown casts; ensure any local helpers or variables referencing the constructed
node use T/LexicalNode types (and update imports if needed) so type-checking
prevents non-Lexical constructors from being passed to parseHtmlNode.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts`:
- Around line 22-25: The inline complex return type of
BrowserDocument.createElement should be extracted into a named interface to
improve readability and reuse: define a new interface (e.g., CanvasElement or
CreateElementReturn) that extends BrowserElement and declares innerHTML, width,
height, getContext(...) returning the typed context and getImageData(...)
returning the data array, then replace the inline return type in the
BrowserDocument interface with this new interface; update any other uses of the
same inline shape (if present) to reference the new interface and keep names
BrowserDocument and createElement unchanged.

In `@packages/kg-default-nodes/src/utils/visibility.ts`:
- Around line 38-41: The RenderOutput.element type is too strict and disallows
null values returned by DOM lookups (e.g., firstElementChild in
calltoaction-renderer.ts); change the RenderOutput interface so element can be
null (make element type "(Element & {innerHTML: string; outerHTML: string;
value?: string}) | null") and then update callers that cast to
RenderOutput['element'] (such as calltoaction-renderer) to handle the null case
safely before accessing innerHTML/outerHTML/value.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Around line 637-1030: Tests call galleryNode.exportDOM(... as unknown as
LexicalEditor) many times; instead create a single correctly typed export
options value or a small test helper to centralize the cast so individual calls
don't repeat "as unknown as LexicalEditor". Update references to exportOptions
and calls to exportDOM to use the properly typed variable (or a helper function
like renderExport(node, opts) that performs one cast), and remove the per-call
double-casts in tests that call exportDOM.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread packages/kg-default-nodes/src/nodes/html/html-parser.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/signup/SignupNode.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts Outdated
Comment thread packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts Outdated
@kevinansfield kevinansfield force-pushed the ts/kg-default-nodes branch 3 times, most recently from 3ab9780 to 7bfcd68 Compare April 16, 2026 13:28
- Remove Rollup build (rollup.config.mjs, rollup-plugin-svg, @babel/*)
- Move lib/ to src/, rename .js to .ts
- Add tsconfig.json (strict, NodeNext, ESM)
- Add "type": "module" to package.json
- Convert 100 source files and 36 test files to ESM with Lexical types
- Inline SVG import in AtLinkNode (replaces rollup-plugin-svg)
- Replace .eslintrc.js with eslint.config.js (flat config)
- Output to build/ via tsc (replaces cjs/ and es/ dirs)
- 668 tests passing
Copy link
Copy Markdown

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/kg-default-nodes/src/nodes/video/video-parser.ts (1)

29-35: ⚠️ Potential issue | 🟡 Minor

catch here never filters invalid durations.

parseInt() does not throw, so malformed values still assign payload.duration = NaN at Line 32. Parse both parts explicitly and only write the field when both numbers are finite.

Proposed fix
                         if (durationText) {
                             const [minutes, seconds] = durationText.split(':');
-                            try {
-                                payload.duration = parseInt(minutes) * 60 + parseInt(seconds);
-                            } catch {
-                                // ignore duration
+                            const parsedMinutes = Number.parseInt(minutes ?? '', 10);
+                            const parsedSeconds = Number.parseInt(seconds ?? '', 10);
+
+                            if (Number.isFinite(parsedMinutes) && Number.isFinite(parsedSeconds)) {
+                                payload.duration = (parsedMinutes * 60) + parsedSeconds;
                             }
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts` around lines 29 -
35, The current duration parsing uses parseInt inside a try/catch but parseInt
never throws, so invalid inputs set payload.duration to NaN; update the block
that reads durationText (the variables minutes/seconds and assignment to
payload.duration) to explicitly parse and validate both parts (e.g., use Number
or parseInt then check !Number.isNaN and Number.isFinite for minutes and
seconds) and only set payload.duration when both parsed values are valid numbers
(otherwise leave payload.duration unset or handle as invalid). Ensure you
reference durationText, payload.duration, and the minutes/seconds variables when
making the change.
♻️ Duplicate comments (8)
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)

12-22: ⚠️ Potential issue | 🟠 Major

Guard for missing kg-card-end before destructive sibling removal.

If no end marker exists, Line 12-Line 22 removes unrelated trailing siblings and folds them into one HTML card. Pre-scan for an end node first, then mutate only when found.

Proposed fix
                     conversion(domNode: Node) {
                         const html = [];
                         let nextNode = domNode.nextSibling;
+                        let endNode = nextNode;
+
+                        while (endNode && !isHtmlEndComment(endNode)) {
+                            endNode = endNode.nextSibling;
+                        }
+
+                        if (!endNode) {
+                            return null;
+                        }
 
-                        while (nextNode && !isHtmlEndComment(nextNode)) {
+                        while (nextNode && nextNode !== endNode) {
                             const currentNode = nextNode;
                             nextNode = currentNode.nextSibling;
                             if (currentNode.nodeType === 1) {
                                 html.push((currentNode as Element).outerHTML);
                             } else if (currentNode.nodeType === 3 && currentNode.textContent) {
                                 html.push(currentNode.textContent);
                             }
                             // remove nodes as we go so that they don't go through the parser
                             currentNode.remove();
                         }
+                        endNode.remove();
 
                         const payload: Record<string, unknown> = {html: html.join('\n').trim()};
                         const node = new HtmlNode(payload);
                         return {node: node as LexicalNode};
                     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 12 -
22, The loop in html-parser.ts that collects and removes siblings (using
nextNode/currentNode, currentNode.remove(), (currentNode as Element).outerHTML
and textContent) can run past a missing kg-card-end and destructively remove
unrelated nodes; pre-scan from the starting sibling using isHtmlEndComment to
find the end marker first, and only if an end node is found perform the existing
collection/removal loop; otherwise leave siblings untouched and skip creating
the fused HTML card.
packages/kg-default-nodes/src/nodes/signup/SignupNode.ts (1)

108-110: ⚠️ Potential issue | 🟠 Major

Factory input type still bypasses the stricter node input contract.

Line 108 uses Record<string, unknown>, which is broader than the constructor’s SignupData and weakens field-level safety for node creation.

Suggested fix
-export const $createSignupNode = (dataset: Record<string, unknown>) => {
+export const $createSignupNode = (dataset: SignupData = {}) => {
     return new SignupNode(dataset);
 };
#!/bin/bash
# Verify the mismatch and check for similar factory signatures in nodes.
rg -nP --type=ts '\$createSignupNode\s*=\s*\(dataset:\s*Record<string,\s*unknown>\)' packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
rg -nP --type=ts 'constructor\s*\(\{[^)]*\}\s*:\s*SignupData' packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
rg -nP --type=ts '\$create[A-Za-z0-9_]*Node\s*=\s*\([^)]*Record<string,\s*unknown>' packages/kg-default-nodes/src/nodes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts` around lines 108 -
110, The factory $createSignupNode currently accepts a broad Record<string,
unknown>, which bypasses the constructor's stricter SignupData contract; change
the parameter type of $createSignupNode from Record<string, unknown> to
SignupData (importing SignupData if needed) so it matches SignupNode's
constructor signature, update any call sites/tests that pass loose objects to
satisfy the stronger type, and run a repo-wide grep for other $create*Node
factory signatures to make similar fixes for consistency.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

404-418: ⚠️ Potential issue | 🟠 Major

Sanitize sponsorLabel before the email early return.

The cleanup at Lines 415-418 only runs on the web path. When options.target === 'email', Line 410 returns first, so emailCTATemplate() still interpolates the raw dataset.sponsorLabel.

Proposed fix
 export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) {
     addCreateDocumentOption(options);
     const document = options.createDocument!();
     const dataset = {
@@
         linkColor: node.linkColor
     };
+
+    if (dataset.hasSponsorLabel) {
+        const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div'));
+        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
+        dataset.sponsorLabel = cleanedHtml || '';
+    }

     // Add validation for backgroundColor
@@
     if (options.target === 'email') {
         const emailDoc = options.createDocument!();
         const emailDiv = emailDoc.createElement('div');
@@
-    if (dataset.hasSponsorLabel) {
-        const cleanBasicHtml = buildCleanBasicHtmlForElement(element);
-        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
-        dataset.sponsorLabel = cleanedHtml || '';
-    }
     const htmlString = ctaCardTemplate(dataset);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 404 - 418, The email path returns early using emailCTATemplate but
doesn't sanitize dataset.sponsorLabel; move or duplicate the cleaning logic so
sponsorLabel is sanitized before the email branch executes: when options.target
=== 'email', call buildCleanBasicHtmlForElement(element) and
cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}) (same flow
used later), assign the cleaned string back to dataset.sponsorLabel (or a local
sanitized copy) prior to calling
emailCTATemplate/createDocument/renderWithVisibility so the email output uses
the cleaned sponsorLabel.
packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts (2)

47-49: ⚠️ Potential issue | 🟡 Minor

Handle undefined caption to avoid string concatenation issues.

readCaptionFromElement(domNode) can return undefined. When the initial caption is undefined and a subsequent sibling has a caption, line 63 produces "undefined / actualCaption".

Initialize the caption to an empty string or filter before concatenating:

Proposed fix
                 conversion(domNode: HTMLElement) {
                     const payload: Record<string, unknown> = {
-                        caption: readCaptionFromElement(domNode)
+                        caption: readCaptionFromElement(domNode) ?? ''
                     };

Or use conditional concatenation:

                         const currentNodeCaption = readCaptionFromElement(currentNode);
                         if (currentNodeCaption) {
-                            payload.caption = `${payload.caption} / ${currentNodeCaption}`;
+                            payload.caption = payload.caption ? `${payload.caption} / ${currentNodeCaption}` : currentNodeCaption;
                         }

Also applies to: 61-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts` around lines
47 - 49, The payload construction uses readCaptionFromElement(domNode) which can
return undefined, causing later concatenation in gallery-caption-building logic
(see readCaptionFromElement and the payload variable) to produce "undefined /
actualCaption"; fix by normalizing the caption to an empty string (or filtering
out falsy values) before storing in payload.caption so subsequent concatenation
or joining code never sees undefined—update the payload assignment and any
concatenation that uses payload.caption (or siblingCaption assembly logic) to
use the normalized string or skip empty parts.

99-110: ⚠️ Potential issue | 🟡 Minor

Empty src fallback may create invalid gallery images.

When data-src is missing and there's a <noscript> sibling, line 102 sets src to an empty string. This passes the undefined filter at line 110 but results in an invalid image URL being processed by readGalleryImageAttributesFromElement.

Consider extracting the actual src from the <noscript> image or returning undefined to skip the entry:

Proposed fix
                         if (img.previousElementSibling?.tagName === 'NOSCRIPT' && img.previousElementSibling.getElementsByTagName('img').length) {
                             const prevNode = img.previousElementSibling;
-                            img.setAttribute('src', img.getAttribute('data-src') ?? '');
+                            const noscriptImg = prevNode.getElementsByTagName('img')[0];
+                            const src = img.getAttribute('data-src') ?? noscriptImg?.getAttribute('src');
+                            if (!src) {
+                                return undefined;
+                            }
+                            img.setAttribute('src', src);
                             prevNode.remove();
                         } else {
                             return undefined;
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts` around lines
99 - 110, The current map/filter over gallery images may set img.src to an empty
string when data-src is missing, producing invalid entries for downstream
readGalleryImageAttributesFromElement; update the logic in the gallery-parser
mapping: when img.getAttribute('src') is falsy and
img.previousElementSibling?.tagName === 'NOSCRIPT', attempt to find an <img>
inside that NOSCRIPT and extract its actual src (e.g.,
previousElementSibling.getElementsByTagName('img')[0].getAttribute('src')) and
set that as img.src only if present; if no valid src can be obtained from
data-src or the NOSCRIPT image, return undefined so the subsequent .filter(img
=> img !== undefined) will drop it. Ensure references to img,
previousElementSibling, and readGalleryImageAttributesFromElement remain intact.
packages/kg-default-nodes/test/nodes/header.test.ts (1)

151-160: ⚠️ Potential issue | 🟡 Minor

These “renders nothing” tests still don’t validate behavior.

Both blocks end in void element, so they pass without asserting expected output.

Proposed fix (assert current behavior explicitly)
-const {element} = node.exportDOM(exportOptions as unknown as LexicalEditor);
-// NOTE: pre-existing issue — renderer returns an element, not null.
-// Original JS test used `element.should.be.null` (a no-op getter).
-void element;
+const {element} = node.exportDOM(exportOptions as unknown as LexicalEditor);
+(element as HTMLElement).outerHTML.should.containEql('kg-header-card');

Also applies to: 468-477

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 151 - 160,
The tests currently end with "void element" so they don't assert anything;
update the two "renders nothing" cases (the one using $createHeaderNode and the
duplicate at lines ~468-477) to explicitly assert the current renderer behavior
instead of no-oping: call node.exportDOM(exportOptions as unknown as
LexicalEditor) and then assert that the returned element is not null and matches
the expected shape (e.g., is an Element/HTMLElement and either has no children
or the expected empty structure) so the test actually verifies the outcome of
exportDOM; use the $createHeaderNode, node.header/node.subheader/buttonEnabled,
exportOptions and exportDOM symbols to locate and modify the assertions.
packages/kg-default-nodes/package.json (1)

19-21: ⚠️ Potential issue | 🟠 Major

Clean build/ before compiling.

tsc will not remove outputs for renamed or deleted sources. Since this package now publishes the whole build directory, stale JS / .d.ts files can survive local rebuilds and get shipped.

🧹 Suggested script update
   "scripts": {
+    "clean": "node -e \"require('node:fs').rmSync('build', {recursive: true, force: true})\"",
-    "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
-    "prepare": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
-    "pretest": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json",
+    "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
+    "prepare": "yarn build",
+    "pretest": "yarn build && tsc -p tsconfig.test.json",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/package.json` around lines 19 - 21, The build/
directory isn't cleaned before running tsc so stale JS/.d.ts files can persist;
update the package.json scripts ("build", "prepare", and "pretest") to remove
the existing build directory first (e.g., run a clean step such as rm -rf build
or use rimraf) before running your tsc commands and creating
build/esm/package.json so each build starts from a clean output tree and won't
publish leftover artifacts.
packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts (1)

51-55: ⚠️ Potential issue | 🟡 Minor

Keep the thumbnail dimension guard strictly positive.

This branch now filters out missing values, but negative numbers are still truthy and will produce a negative spacerHeight/VML size. Please keep the width/height check as > 0 before dividing.

Suggested fix
-    if (isEmail && isVideoWithThumbnail && metadata.thumbnail_width && metadata.thumbnail_height) {
+    if (
+        isEmail &&
+        isVideoWithThumbnail &&
+        typeof metadata.thumbnail_width === 'number' &&
+        typeof metadata.thumbnail_height === 'number' &&
+        metadata.thumbnail_width > 0 &&
+        metadata.thumbnail_height > 0
+    ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts` around lines 51
- 55, The thumbnail dimension guard currently checks
thumbnail_width/thumbnail_height truthiness which allows negative values; update
the condition in the isEmail && isVideoWithThumbnail branch to require
metadata.thumbnail_width > 0 and metadata.thumbnail_height > 0 before computing
thumbnailAspectRatio, spacerWidth, and spacerHeight so you never divide by or
produce negative VML sizes (reference variables: metadata.thumbnail_width,
metadata.thumbnail_height, thumbnailAspectRatio, spacerWidth, spacerHeight).
🧹 Nitpick comments (8)
packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)

22-25: Split createElement typing by tag instead of using one broad intersection.

Line 24 currently implies every created element has both canvas and script members, which can mask invalid usage. Prefer overloads for 'canvas' and 'script' to keep type checks meaningful.

Refactor sketch
 interface BrowserDocument {
     currentScript: { parentElement: BrowserElement } | null;
-    createElement(tag: string): BrowserElement & { innerHTML: string; width: number; height: number; getContext(id: string): { fillStyle: string; fillRect(x: number, y: number, w: number, h: number): void; getImageData(x: number, y: number, w: number, h: number): { data: number[] } } };
+    createElement(tag: 'script'): BrowserElement & {innerHTML: string};
+    createElement(tag: 'canvas'): BrowserElement & {
+        width: number;
+        height: number;
+        getContext(id: string): {
+            fillStyle: string;
+            fillRect(x: number, y: number, w: number, h: number): void;
+            getImageData(x: number, y: number, w: number, h: number): {data: number[]};
+        };
+    };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around
lines 22 - 25, The createElement signature on the BrowserDocument interface
currently returns a broad intersection that makes every element look like both a
canvas and a script; update BrowserDocument.createElement to use overloads (or a
discriminated union) so calling createElement('canvas') returns a canvas-like
BrowserElement with canvas-specific members (width, height, getContext(...),
getImageData(...)) and createElement('script') returns a script-like
BrowserElement with currentScript/parentElement and innerHTML, keeping the
BrowserElement base separate; change the type for createElement in the
BrowserDocument interface and adjust any dependent code to rely on the specific
overloads rather than the combined intersection.
packages/kg-default-nodes/test/nodes/email-cta.test.ts (1)

213-214: Remove incorrect LexicalEditor input cast from exportDOM calls; assert return type instead.

These 10 call sites cast an options object to LexicalEditor, which is semantically incorrect—the method expects Record<string, unknown>, not an editor instance. Removing the input cast and relying on the method's actual signature improves type clarity.

Suggested pattern:

-const {element} = emailNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor);
-const el = element as HTMLElement;
+const {element} = emailNode.exportDOM({...exportOptions, ...options});
+const el = element as HTMLElement;

The return type is { element: Element | null; type?: string }, so the as HTMLElement cast on the second line remains necessary to narrow the element type.

Also applies to: 240–241, 262–263, 284–285, 306–307, 334–335, 362–363, 402–403, 440–441, 477–478

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` around lines 213 -
214, Remove the incorrect cast of the options object to LexicalEditor when
calling emailNode.exportDOM; exportDOM expects a Record<string, unknown> so stop
casting {...exportOptions, ...options} as unknown as LexicalEditor and pass the
merged object directly, and instead assert/narrow the return using the known
shape ({ element: Element | null; type?: string }) by keeping the subsequent `as
HTMLElement` cast on the `element` variable; update all similar call sites (the
ones around lines referenced) to call exportDOM(...) without the LexicalEditor
cast and rely on the exportDOM return type assertion.
packages/kg-default-nodes/test/nodes/video.test.ts (1)

245-247: Remove incorrect LexicalEditor cast from exportDOM calls in video tests.

The pattern exportDOM(exportOptions as unknown as LexicalEditor) misrepresents the parameter type. Since exportOptions is already correctly typed as Record<string, unknown> to match the exportDOM(options: Record<string, unknown>) signature, the cast hides type issues rather than resolving them.

Suggested pattern
-const {element} = videoNode.exportDOM(exportOptions as unknown as LexicalEditor);
+const {element} = videoNode.exportDOM(exportOptions) as {element: HTMLElement};

Applies to lines 245, 317, 337, 353, 369.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/video.test.ts` around lines 245 - 247,
The tests are using an incorrect cast by calling exportDOM(exportOptions as
unknown as LexicalEditor); remove the bogus LexicalEditor cast and pass
exportOptions directly to exportDOM since the method signature is
exportDOM(options: Record<string, unknown>); update all occurrences (e.g., calls
on videoNode.exportDOM and related nodes at the locations flagged) to call
exportDOM(exportOptions) so the real types are used and any underlying type
issues surface.
packages/kg-default-nodes/test/nodes/horizontalrule.test.ts (1)

47-49: Cast the result of exportDOM, not the parameter, for consistency with other tests.

Line 47 casts exportOptions to LexicalEditor which contradicts the actual parameter type. Most test files already cast the return value instead. Apply the same pattern here:

Proposed fix
-const {element} = hrNode.exportDOM(exportOptions as unknown as LexicalEditor);
-(element as HTMLElement).outerHTML.should.prettifyTo(html`
+const {element} = hrNode.exportDOM(exportOptions as unknown as LexicalEditor) as {element: HTMLElement};
+element.outerHTML.should.prettifyTo(html`

This aligns with the majority pattern in the codebase (bookmark.test.ts, button.test.ts, etc.) and removes the unnecessary element cast on line 48.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/horizontalrule.test.ts` around lines 47
- 49, The test currently casts the exportOptions argument to LexicalEditor when
calling hrNode.exportDOM; instead, cast the return value of hrNode.exportDOM to
the expected type (e.g., { element } as unknown as { element: HTMLElement }) to
match other tests, remove the unnecessary (element as HTMLElement) cast on the
next line, and ensure the constructed element variable is used directly in the
prettify assertion; reference hrNode.exportDOM, exportOptions, and element to
locate and update the code.
packages/kg-default-nodes/src/nodes/callout/callout-parser.ts (1)

7-25: Tighten the constructor signature to eliminate the unnecessary cast.

new (...) => unknown paired with node as LexicalNode discards the type guarantee that this conversion produces a Lexical node. Since all node classes created by generateDecoratorNode() extend LexicalNode (through KoenigDecoratorNodeDecoratorNode), the parameter type can be tightened to new (data: Record<string, unknown>) => LexicalNode and the cast removed. This pattern applies consistently across all parser functions in the codebase.

♻️ Suggested signature change
-export function parseCalloutNode(CalloutNode: new (data: Record<string, unknown>) => unknown) {
+export function parseCalloutNode(CalloutNode: new (data: Record<string, unknown>) => LexicalNode) {
     return {
         div: (nodeElem: HTMLElement) => {
             const isKgCalloutCard = nodeElem.classList?.contains('kg-callout-card');
             if (nodeElem.tagName === 'DIV' && isKgCalloutCard) {
                 return {
                     conversion(domNode: HTMLElement) {
                         const textNode = domNode?.querySelector('.kg-callout-text');
                         const emojiNode = domNode?.querySelector('.kg-callout-emoji');
                         const color = getColorTag(domNode);
 
                         const payload: Record<string, unknown> = {
                             calloutText: textNode && textNode.innerHTML.trim() || '',
                             calloutEmoji: emojiNode && emojiNode.innerHTML.trim() || '',
                             backgroundColor: color
                         };
 
                         const node = new CalloutNode(payload);
-                        return {node: node as LexicalNode};
+                        return {node};
                     },
                     priority: 1 as const
                 };
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/callout-parser.ts` around lines 7
- 25, The constructor signature for CalloutNode in parseCalloutNode is too loose
and forces a runtime cast to LexicalNode; change the parameter type from "new
(data: Record<string, unknown>) => unknown" to "new (data: Record<string,
unknown>) => LexicalNode" so the created instance is statically known to be a
LexicalNode and remove the "as LexicalNode" cast; apply the same tightening to
other parser functions that construct nodes produced by generateDecoratorNode /
KoenigDecoratorNode / DecoratorNode so all constructors return LexicalNode
types.
packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts (2)

50-51: Inconsistent super() call — same pattern issue as CalloutNode.

Since BookmarkNode manually initializes all properties (lines 52-59), the empty object passed to super({}, key) is unused. Consider calling super(key) directly for consistency with the base class.

Suggested change
     constructor({url, metadata, caption}: BookmarkData = {}, key?: string) {
-        super({}, key);
+        super(key);
         this.__url = url || '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 50
- 51, The BookmarkNode constructor currently calls super({}, key) even though
BookmarkNode manually initializes all properties; change the constructor to call
super(key) instead so the base class receives the intended key parameter and the
unused empty props object is removed—update the BookmarkNode constructor
(constructor({url, metadata, caption}: BookmarkData = {}, key?: string)) to pass
only key to super and leave the existing manual initialization of url, metadata,
caption intact.

114-116: Factory function parameter type mismatch with constructor.

Same pattern as CalloutNode: the constructor accepts BookmarkData, but the factory function is typed as Record<string, unknown>. Consider aligning the types for better type safety.

Suggested fix
-export const $createBookmarkNode = (dataset: Record<string, unknown>) => {
+export const $createBookmarkNode = (dataset: BookmarkData = {}) => {
     return new BookmarkNode(dataset);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines
114 - 116, The factory $createBookmarkNode currently types its parameter as
Record<string, unknown> but constructs a new BookmarkNode which expects
BookmarkData; update the factory signature to accept BookmarkData (or make
BookmarkNode constructor accept Record<string, unknown>) so types
align—specifically change the $createBookmarkNode parameter type to BookmarkData
to match the BookmarkNode constructor and ensure callers pass the correct shape.
packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts (1)

27-28: Inconsistent super() call — consider passing key only.

The base class constructor in generate-decorator-node.ts is constructor(data: Record<string, unknown> = {}, key?: string) and calls super(key). Since CalloutNode manually initializes all properties (lines 29-31), the empty object passed to super({}, key) is effectively ignored. For consistency with the base class pattern, consider calling super(key) directly.

Suggested change
     constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {}, key?: string) {
-        super({}, key);
+        super(key);
         this.__calloutText = calloutText || '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts` around lines 27 -
28, The CalloutNode constructor currently calls super({}, key) which is
inconsistent with the base class constructor pattern in
generate-decorator-node.ts (constructor(data: Record<string, unknown> = {},
key?: string) that itself calls super(key)); update the CalloutNode constructor
(the constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {},
key?: string) in CalloutNode.ts) to call super(key) instead of super({}, key) so
you only forward the key to the base class while still initializing calloutText,
calloutEmoji and backgroundColor locally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts`:
- Around line 59-61: exportDOM() currently returns {element: null} in AtLinkNode
(and similarly in AtLinkSearchNode), which violates the expected return type
(HTMLElement | HTMLInputElement | HTMLTextAreaElement) and causes consumers to
dereference a null. Update exportDOM in both AtLinkNode and AtLinkSearchNode to
return a real HTMLElement fallback (e.g., create a span or input element via
document.createElement and set any needed attributes or text/ARIA/data-
attributes to represent the placeholder) so callers like
convert-to-html-string.ts can safely access element.innerHTML/outerHTML without
null checks; ensure the returned object shape matches the declared contract.

In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts`:
- Around line 11-17: RenderOptions currently marks postUrl optional but the
email template interpolates options.postUrl, causing broken hrefs; update the
types and runtime to enforce it: make postUrl a required string on RenderOptions
(change postUrl?: string to postUrl: string) and add a defensive check in the
render function (e.g., renderAudioNode or the renderer function that reads
options.target) that throws or returns a clear error when options.target ===
'email' and options.postUrl is missing/empty, referencing RenderOptions and
options.postUrl/target so callers and the compiler both guarantee a valid
postUrl before composing the email links.

In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts`:
- Around line 23-33: The TweetData interface currently marks id as optional
which allows generating malformed Twitter URLs; either make TweetData.id
required (change interface TweetData to id: string) and update any callers to
provide it, or (preferred) add a guard in the email-generation logic that builds
the Twitter status URL: check tweetData?.id (or the local tweetId variable)
before interpolating into "https://twitter.com/.../status/{tweetId}" and if
missing fall back to node.html or skip rendering that URL block; update any
helper that references TweetData.id (e.g., the email template generator that
uses tweetId) to perform this null/undefined check.

In `@packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts`:
- Around line 20-25: The regex and assignment in
read-image-attributes-from-element accept empty width/height (using (\d*) ) and
write NaN into attrs; change the match to require digits on both sides (e.g.,
use (\d+)x(\d+)) or, if keeping the current regex, validate that match[1] and
match[2] are non-empty before calling parseInt and assigning to
attrs.width/attrs.height so both dimensions must be present. Ensure you
reference the element.getAttribute('data-image-dimensions') check, the match
variable, and the attrs.width/attrs.height assignments when making the change.

In `@packages/kg-default-nodes/src/utils/visibility.ts`:
- Around line 51-60: The function isVisibilityRestricted currently checks old
and new visibility shapes separately, causing legacy objects to be classified as
restricted even though migrateOldVisibilityFormat would normalize them to
unrestricted defaults; fix by first normalizing the input via
migrateOldVisibilityFormat(visibility) and then only evaluate the new-format
conditions (using web?.nonMember, web?.memberSegment !== ALL_MEMBERS_SEGMENT,
email?.memberSegment !== ALL_MEMBERS_SEGMENT), removing the
isOldVisibilityFormat branch so all checks operate on the migrated shape.

---

Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts`:
- Around line 29-35: The current duration parsing uses parseInt inside a
try/catch but parseInt never throws, so invalid inputs set payload.duration to
NaN; update the block that reads durationText (the variables minutes/seconds and
assignment to payload.duration) to explicitly parse and validate both parts
(e.g., use Number or parseInt then check !Number.isNaN and Number.isFinite for
minutes and seconds) and only set payload.duration when both parsed values are
valid numbers (otherwise leave payload.duration unset or handle as invalid).
Ensure you reference durationText, payload.duration, and the minutes/seconds
variables when making the change.

---

Duplicate comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The build/ directory isn't cleaned before running tsc so
stale JS/.d.ts files can persist; update the package.json scripts ("build",
"prepare", and "pretest") to remove the existing build directory first (e.g.,
run a clean step such as rm -rf build or use rimraf) before running your tsc
commands and creating build/esm/package.json so each build starts from a clean
output tree and won't publish leftover artifacts.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 404-418: The email path returns early using emailCTATemplate but
doesn't sanitize dataset.sponsorLabel; move or duplicate the cleaning logic so
sponsorLabel is sanitized before the email branch executes: when options.target
=== 'email', call buildCleanBasicHtmlForElement(element) and
cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}) (same flow
used later), assign the cleaned string back to dataset.sponsorLabel (or a local
sanitized copy) prior to calling
emailCTATemplate/createDocument/renderWithVisibility so the email output uses
the cleaned sponsorLabel.

In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts`:
- Around line 51-55: The thumbnail dimension guard currently checks
thumbnail_width/thumbnail_height truthiness which allows negative values; update
the condition in the isEmail && isVideoWithThumbnail branch to require
metadata.thumbnail_width > 0 and metadata.thumbnail_height > 0 before computing
thumbnailAspectRatio, spacerWidth, and spacerHeight so you never divide by or
produce negative VML sizes (reference variables: metadata.thumbnail_width,
metadata.thumbnail_height, thumbnailAspectRatio, spacerWidth, spacerHeight).

In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts`:
- Around line 47-49: The payload construction uses
readCaptionFromElement(domNode) which can return undefined, causing later
concatenation in gallery-caption-building logic (see readCaptionFromElement and
the payload variable) to produce "undefined / actualCaption"; fix by normalizing
the caption to an empty string (or filtering out falsy values) before storing in
payload.caption so subsequent concatenation or joining code never sees
undefined—update the payload assignment and any concatenation that uses
payload.caption (or siblingCaption assembly logic) to use the normalized string
or skip empty parts.
- Around line 99-110: The current map/filter over gallery images may set img.src
to an empty string when data-src is missing, producing invalid entries for
downstream readGalleryImageAttributesFromElement; update the logic in the
gallery-parser mapping: when img.getAttribute('src') is falsy and
img.previousElementSibling?.tagName === 'NOSCRIPT', attempt to find an <img>
inside that NOSCRIPT and extract its actual src (e.g.,
previousElementSibling.getElementsByTagName('img')[0].getAttribute('src')) and
set that as img.src only if present; if no valid src can be obtained from
data-src or the NOSCRIPT image, return undefined so the subsequent .filter(img
=> img !== undefined) will drop it. Ensure references to img,
previousElementSibling, and readGalleryImageAttributesFromElement remain intact.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 12-22: The loop in html-parser.ts that collects and removes
siblings (using nextNode/currentNode, currentNode.remove(), (currentNode as
Element).outerHTML and textContent) can run past a missing kg-card-end and
destructively remove unrelated nodes; pre-scan from the starting sibling using
isHtmlEndComment to find the end marker first, and only if an end node is found
perform the existing collection/removal loop; otherwise leave siblings untouched
and skip creating the fused HTML card.

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 108-110: The factory $createSignupNode currently accepts a broad
Record<string, unknown>, which bypasses the constructor's stricter SignupData
contract; change the parameter type of $createSignupNode from Record<string,
unknown> to SignupData (importing SignupData if needed) so it matches
SignupNode's constructor signature, update any call sites/tests that pass loose
objects to satisfy the stronger type, and run a repo-wide grep for other
$create*Node factory signatures to make similar fixes for consistency.

In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 151-160: The tests currently end with "void element" so they don't
assert anything; update the two "renders nothing" cases (the one using
$createHeaderNode and the duplicate at lines ~468-477) to explicitly assert the
current renderer behavior instead of no-oping: call node.exportDOM(exportOptions
as unknown as LexicalEditor) and then assert that the returned element is not
null and matches the expected shape (e.g., is an Element/HTMLElement and either
has no children or the expected empty structure) so the test actually verifies
the outcome of exportDOM; use the $createHeaderNode,
node.header/node.subheader/buttonEnabled, exportOptions and exportDOM symbols to
locate and modify the assertions.

---

Nitpick comments:
In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts`:
- Around line 50-51: The BookmarkNode constructor currently calls super({}, key)
even though BookmarkNode manually initializes all properties; change the
constructor to call super(key) instead so the base class receives the intended
key parameter and the unused empty props object is removed—update the
BookmarkNode constructor (constructor({url, metadata, caption}: BookmarkData =
{}, key?: string)) to pass only key to super and leave the existing manual
initialization of url, metadata, caption intact.
- Around line 114-116: The factory $createBookmarkNode currently types its
parameter as Record<string, unknown> but constructs a new BookmarkNode which
expects BookmarkData; update the factory signature to accept BookmarkData (or
make BookmarkNode constructor accept Record<string, unknown>) so types
align—specifically change the $createBookmarkNode parameter type to BookmarkData
to match the BookmarkNode constructor and ensure callers pass the correct shape.

In `@packages/kg-default-nodes/src/nodes/callout/callout-parser.ts`:
- Around line 7-25: The constructor signature for CalloutNode in
parseCalloutNode is too loose and forces a runtime cast to LexicalNode; change
the parameter type from "new (data: Record<string, unknown>) => unknown" to "new
(data: Record<string, unknown>) => LexicalNode" so the created instance is
statically known to be a LexicalNode and remove the "as LexicalNode" cast; apply
the same tightening to other parser functions that construct nodes produced by
generateDecoratorNode / KoenigDecoratorNode / DecoratorNode so all constructors
return LexicalNode types.

In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts`:
- Around line 27-28: The CalloutNode constructor currently calls super({}, key)
which is inconsistent with the base class constructor pattern in
generate-decorator-node.ts (constructor(data: Record<string, unknown> = {},
key?: string) that itself calls super(key)); update the CalloutNode constructor
(the constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {},
key?: string) in CalloutNode.ts) to call super(key) instead of super({}, key) so
you only forward the key to the base class while still initializing calloutText,
calloutEmoji and backgroundColor locally.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts`:
- Around line 22-25: The createElement signature on the BrowserDocument
interface currently returns a broad intersection that makes every element look
like both a canvas and a script; update BrowserDocument.createElement to use
overloads (or a discriminated union) so calling createElement('canvas') returns
a canvas-like BrowserElement with canvas-specific members (width, height,
getContext(...), getImageData(...)) and createElement('script') returns a
script-like BrowserElement with currentScript/parentElement and innerHTML,
keeping the BrowserElement base separate; change the type for createElement in
the BrowserDocument interface and adjust any dependent code to rely on the
specific overloads rather than the combined intersection.

In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts`:
- Around line 213-214: Remove the incorrect cast of the options object to
LexicalEditor when calling emailNode.exportDOM; exportDOM expects a
Record<string, unknown> so stop casting {...exportOptions, ...options} as
unknown as LexicalEditor and pass the merged object directly, and instead
assert/narrow the return using the known shape ({ element: Element | null;
type?: string }) by keeping the subsequent `as HTMLElement` cast on the
`element` variable; update all similar call sites (the ones around lines
referenced) to call exportDOM(...) without the LexicalEditor cast and rely on
the exportDOM return type assertion.

In `@packages/kg-default-nodes/test/nodes/horizontalrule.test.ts`:
- Around line 47-49: The test currently casts the exportOptions argument to
LexicalEditor when calling hrNode.exportDOM; instead, cast the return value of
hrNode.exportDOM to the expected type (e.g., { element } as unknown as {
element: HTMLElement }) to match other tests, remove the unnecessary (element as
HTMLElement) cast on the next line, and ensure the constructed element variable
is used directly in the prettify assertion; reference hrNode.exportDOM,
exportOptions, and element to locate and update the code.

In `@packages/kg-default-nodes/test/nodes/video.test.ts`:
- Around line 245-247: The tests are using an incorrect cast by calling
exportDOM(exportOptions as unknown as LexicalEditor); remove the bogus
LexicalEditor cast and pass exportOptions directly to exportDOM since the method
signature is exportDOM(options: Record<string, unknown>); update all occurrences
(e.g., calls on videoNode.exportDOM and related nodes at the locations flagged)
to call exportDOM(exportOptions) so the real types are used and any underlying
type issues surface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2751eb43-7ca8-4957-a935-5db41ba66289

📥 Commits

Reviewing files that changed from the base of the PR and between f995f5e and fda8a96.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (149)
  • packages/kg-default-nodes/eslint.config.mjs
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/package.json
  • packages/kg-default-nodes/rollup.config.mjs
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/kg-default-nodes.ts
  • packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/TKNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-parser.ts
  • packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/html/html-renderer.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/src/nodes/product/product-renderer.ts
  • packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-parser.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/utils/visibility.ts
  • packages/kg-default-nodes/test/generate-decorator-node.test.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/call-to-action.test.ts
  • packages/kg-default-nodes/test/nodes/callout.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/test/nodes/email-cta.test.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/test/nodes/file.test.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/test/nodes/header.test.ts
  • packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/test/nodes/product.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/nodes/toggle.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/test/test-utils/assertions.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/test/utils/visibility.test.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/tsconfig.test.json
💤 Files with no reviewable changes (2)
  • packages/kg-default-nodes/index.js
  • packages/kg-default-nodes/rollup.config.mjs
✅ Files skipped from review due to trivial changes (30)
  • packages/kg-default-nodes/src/index.ts
  • packages/kg-default-nodes/src/nodes/at-link/index.ts
  • packages/kg-default-nodes/src/svg.d.ts
  • packages/kg-default-nodes/src/utils/render-empty-container.ts
  • packages/kg-default-nodes/src/utils/escape-html.ts
  • packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
  • packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
  • packages/kg-default-nodes/src/KoenigDecoratorNode.ts
  • packages/kg-default-nodes/src/utils/get-available-image-widths.ts
  • packages/kg-default-nodes/src/utils/is-local-content-image.ts
  • packages/kg-default-nodes/src/utils/slugify.ts
  • packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
  • packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
  • packages/kg-default-nodes/tsconfig.test.json
  • packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
  • packages/kg-default-nodes/src/serializers/paragraph.ts
  • packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
  • packages/kg-default-nodes/src/utils/clean-dom.ts
  • packages/kg-default-nodes/tsconfig.cjs.json
  • packages/kg-default-nodes/test/serializers/linebreak.test.ts
  • packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
  • packages/kg-default-nodes/test/test-utils/index.ts
  • packages/kg-default-nodes/tsconfig.json
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
  • packages/kg-default-nodes/src/utils/truncate.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
🚧 Files skipped from review as they are similar to previous changes (69)
  • packages/kg-default-nodes/src/serializers/linebreak.ts
  • packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
  • packages/kg-default-nodes/src/utils/is-unsplash-image.ts
  • packages/kg-default-nodes/src/utils/read-caption-from-element.ts
  • packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/file/file-parser.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
  • packages/kg-default-nodes/src/nodes/button/button-parser.ts
  • packages/kg-default-nodes/src/nodes/button/button-renderer.ts
  • packages/kg-default-nodes/src/utils/read-text-content.ts
  • packages/kg-default-nodes/src/utils/add-create-document-option.ts
  • packages/kg-default-nodes/src/utils/replacement-strings.ts
  • packages/kg-default-nodes/src/utils/rgb-to-hex.ts
  • packages/kg-default-nodes/test/nodes/aside.test.ts
  • packages/kg-default-nodes/test/test-utils/should.d.ts
  • packages/kg-default-nodes/src/nodes/email/email-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/image/image-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
  • packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
  • packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
  • packages/kg-default-nodes/test/nodes/button.test.ts
  • packages/kg-default-nodes/test/nodes/zwnj.test.ts
  • packages/kg-default-nodes/test/test-utils/overrides.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
  • packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
  • packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
  • packages/kg-default-nodes/test/nodes/email.test.ts
  • packages/kg-default-nodes/test/nodes/image.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/markdown.test.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
  • packages/kg-default-nodes/src/nodes/product/product-parser.ts
  • packages/kg-default-nodes/src/utils/size-byte-converter.ts
  • packages/kg-default-nodes/test/nodes/paywall.test.ts
  • packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/utils/tagged-template-fns.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
  • packages/kg-default-nodes/test/nodes/tk.test.ts
  • packages/kg-default-nodes/test/serializers/paragraph.test.ts
  • packages/kg-default-nodes/src/nodes/image/image-renderer.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/utils/srcset-attribute.ts
  • packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
  • packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
  • packages/kg-default-nodes/src/nodes/file/file-renderer.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
  • packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/test/nodes/call-to-action.test.ts
  • packages/kg-default-nodes/test/nodes/codeblock.test.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts

Comment on lines +59 to +61
// should not render anything - this is a placeholder node
exportDOM() {
return null;
return {element: null};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "AtLinkNode exportDOM():"
sed -n '55,70p' packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts

echo
echo "Related AtLinkSearchNode exportDOM():"
sed -n '60,75p' packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts

echo
echo "HTML renderer caller:"
sed -n '32,60p' packages/kg-lexical-html-renderer/lib/convert-to-html-string.ts

echo
echo "Declared kg-default-nodes exportDOM contract:"
sed -n '12,24p' packages/kg-lexical-html-renderer/lib/kg-default-nodes.d.ts

Repository: TryGhost/Koenig

Length of output: 2658


Return a real DOM export payload here.

exportDOM() returns {element: null}, which violates the declared type contract (element: HTMLElement | HTMLInputElement | HTMLTextAreaElement). The HTML renderer in convert-to-html-string.ts calls element.innerHTML, element.outerHTML, or checks properties on element without null guards—if this node reaches that code path, it crashes with TypeError: Cannot read property of null.

The same issue exists in AtLinkSearchNode.

🛠️ Safer fallback
     // should not render anything - this is a placeholder node
     exportDOM() {
-        return {element: null};
+        return {
+            element: document.createElement('span'),
+            type: 'outer'
+        };
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// should not render anything - this is a placeholder node
exportDOM() {
return null;
return {element: null};
// should not render anything - this is a placeholder node
exportDOM() {
return {
element: document.createElement('span'),
type: 'outer'
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts` around lines 59 -
61, exportDOM() currently returns {element: null} in AtLinkNode (and similarly
in AtLinkSearchNode), which violates the expected return type (HTMLElement |
HTMLInputElement | HTMLTextAreaElement) and causes consumers to dereference a
null. Update exportDOM in both AtLinkNode and AtLinkSearchNode to return a real
HTMLElement fallback (e.g., create a span or input element via
document.createElement and set any needed attributes or text/ARIA/data-
attributes to represent the placeholder) so callers like
convert-to-html-string.ts can safely access element.innerHTML/outerHTML without
null checks; ensure the returned object shape matches the declared contract.

Comment thread packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
Comment thread packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
Comment thread packages/kg-default-nodes/src/utils/visibility.ts
Copy link
Copy Markdown

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts (1)

6-12: ⚠️ Potential issue | 🟡 Minor

Guard invalid dimensions before ratio calculation.

Line 8 can produce Infinity/NaN when image.height <= 0 (or invalid width), which can leak invalid resized values into HTML attributes.

Suggested fix
 export const getResizedImageDimensions = function (image: ImageDimensions, {width: desiredWidth, height: desiredHeight}: {width?: number; height?: number} = {}): ImageDimensions {
     const {width, height} = image;
+    if (width <= 0 || height <= 0) {
+        return {width, height};
+    }
     const ratio = width / height;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts` around
lines 6 - 12, The function getResizedImageDimensions currently computes ratio =
width / height before validating image dimensions, which can produce
NaN/Infinity when image.width or image.height are non-finite or <= 0; add a
guard at the top that checks Number.isFinite(image.width) &&
Number.isFinite(image.height) && image.width > 0 && image.height > 0, and if the
check fails, avoid computing ratio and simply return the original image (or a
safe fallback) so resizedHeight/width calculations using ratio (and the
variables width, height, desiredWidth, desiredHeight) cannot produce invalid
values for HTML attributes.
♻️ Duplicate comments (7)
packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts (1)

43-45: ⚠️ Potential issue | 🟡 Minor

Align $createCalloutNode() parameter type with constructor.

The constructor accepts CalloutData, but the factory function parameter is typed as Record<string, unknown>. This forces callers to cast and loses type safety benefits of the CalloutData interface.

♻️ Suggested fix
-export const $createCalloutNode = (dataset: Record<string, unknown>) => {
+export const $createCalloutNode = (dataset: CalloutData = {}) => {
     return new CalloutNode(dataset);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts` around lines 43 -
45, The factory $createCalloutNode currently types its parameter as
Record<string, unknown> which conflicts with the CalloutNode constructor that
expects CalloutData; change the parameter type of $createCalloutNode to
CalloutData so callers get proper type safety and avoid casts—update the
signature of $createCalloutNode(dataset: CalloutData) and ensure CalloutData is
imported/available in the module alongside the CalloutNode reference.
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)

12-22: ⚠️ Potential issue | 🟠 Major

Guard against missing kg-card-end before destructive removal.

If the end comment marker is absent, this loop removes all following siblings and folds unrelated content into one HTML card. Consider pre-scanning for the matching end comment before mutating/removing nodes.

🛡️ Suggested fix
                 conversion(domNode: Node) {
                     const html = [];
                     let nextNode = domNode.nextSibling;
+                    
+                    // Pre-scan for end marker
+                    let endNode = nextNode;
+                    while (endNode && !isHtmlEndComment(endNode)) {
+                        endNode = endNode.nextSibling;
+                    }
+                    
+                    if (!endNode) {
+                        return null;
+                    }

-                    while (nextNode && !isHtmlEndComment(nextNode)) {
+                    while (nextNode && nextNode !== endNode) {
                         const currentNode = nextNode;
                         nextNode = currentNode.nextSibling;
                         if (currentNode.nodeType === 1) {
                             html.push((currentNode as Element).outerHTML);
                         } else if (currentNode.nodeType === 3 && currentNode.textContent) {
                             html.push(currentNode.textContent);
                         }
                         // remove nodes as we go so that they don't go through the parser
                         currentNode.remove();
                     }
+                    endNode.remove();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 12 -
22, The loop that removes siblings using isHtmlEndComment can destructively
remove unrelated content if a matching `kg-card-end` comment is missing; before
mutating nodes in the while loop (where `nextNode`, `currentNode`, and `html`
are used), pre-scan the sibling chain for a `isHtmlEndComment` match and only
run the removal/aggregation if a matching end comment is found; if not found,
abort the operation (skip building the card or return early) to avoid removing
nodes.
packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts (1)

11-16: ⚠️ Potential issue | 🟠 Major

Guard email rendering when postUrl is missing.

Line 15 keeps postUrl optional, but Lines 192/201/205/213/216 require it for email links. This can render href="undefined" when target === 'email'.

Proposed fix
 export function renderAudioNode(node: AudioNodeData, options: RenderOptions = {}) {
     addCreateDocumentOption(options);
     const document = options.createDocument!();

     if (!node.src || node.src.trim() === '') {
         return renderEmptyContainer(document);
     }
+
+    if (options.target === 'email' && (!options.postUrl || options.postUrl.trim() === '')) {
+        return renderEmptyContainer(document);
+    }

     const thumbnailCls = getThumbnailCls(node);
     const emptyThumbnailCls = getEmptyThumbnailCls(node);

     if (options.target === 'email') {

Also applies to: 30-31, 184-216

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 11
- 16, RenderOptions.postUrl is optional but code paths that render email links
when target === 'email' assume it exists and can produce href="undefined";
update the email link rendering branches in audio-renderer.ts (the code that
checks target === 'email' around the rendering logic lines ~184-216) to first
verify options.postUrl is a non-empty string and only construct an anchor href
using postUrl when present; if postUrl is missing, either skip creating the
anchor (render plain text/disabled link) or fall back to a safe default (e.g.,
omit href) so no href="undefined" is emitted. Ensure all spots that build email
hrefs reference RenderOptions.postUrl and perform the guard.
packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)

404-419: ⚠️ Potential issue | 🟠 Major

Sanitize sponsorLabel before the email early return.

Line 404 returns through the email path before Line 415 sanitization runs, so dataset.sponsorLabel is still raw when interpolated into emailCTATemplate(...).

Suggested fix
 export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) {
     addCreateDocumentOption(options);
     const document = options.createDocument!();
     const dataset = {
@@
         linkColor: node.linkColor
     };

+    if (dataset.hasSponsorLabel) {
+        const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div'));
+        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
+        dataset.sponsorLabel = cleanedHtml || '';
+    }
+
     // Add validation for backgroundColor
@@
     if (options.target === 'email') {
         const emailDoc = options.createDocument!();
         const emailDiv = emailDoc.createElement('div');
@@
     const element = document.createElement('div');
-
-    if (dataset.hasSponsorLabel) {
-        const cleanBasicHtml = buildCleanBasicHtmlForElement(element);
-        const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true});
-        dataset.sponsorLabel = cleanedHtml || '';
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`
around lines 404 - 419, The email branch returns early before sanitizing
dataset.sponsorLabel so raw HTML can be injected into emailCTATemplate; move or
duplicate the sanitization logic using buildCleanBasicHtmlForElement(...) and
cleanBasicHtml(...) to run before the options.target === 'email' return (or
sanitize unconditionally at the top of the function), ensure you update
dataset.sponsorLabel = cleanedHtml || '' prior to calling emailCTATemplate(...)
and then continue to call renderWithVisibility(..., node.visibility, options) as
before.
packages/kg-default-nodes/src/nodes/video/video-renderer.ts (2)

27-32: ⚠️ Potential issue | 🟠 Major

target: 'email' still isn't safely enforced with postUrl.

Current union + cast still permits an email path with missing postUrl at compile time (target?: string in default options plus as EmailVideoRenderOptions). This can still render broken email links.

🔧 Suggested fix (type guard + no unsafe cast)
 type VideoRenderOptions = EmailVideoRenderOptions | DefaultVideoRenderOptions;
+
+function isEmailRenderOptions(options: VideoRenderOptions): options is EmailVideoRenderOptions {
+    return options.target === 'email' && typeof options.postUrl === 'string' && options.postUrl.trim() !== '';
+}
@@
-    const htmlString = options.target === 'email'
-        ? emailCardTemplate({node, options: options as EmailVideoRenderOptions, cardClasses})
-        : cardTemplate({node, cardClasses});
+    const htmlString = isEmailRenderOptions(options)
+        ? emailCardTemplate({node, options, cardClasses})
+        : cardTemplate({node, cardClasses});
#!/bin/bash
set -euo pipefail

# Verify that email branch currently depends on a cast instead of a safe narrowing guard
rg -n "interface DefaultVideoRenderOptions|target\\?: string|postUrl\\?: string|options as EmailVideoRenderOptions" \
  packages/kg-default-nodes/src/nodes/video/video-renderer.ts

# Optional call-site audit: email-target render calls should all include postUrl
ast-grep --lang ts --pattern "renderVideoNode($NODE, {$$$, target: 'email', $$$})"
ast-grep --lang ts --pattern "renderVideoNode($NODE, {$$$, target: 'email', $$$, postUrl: $URL, $$$})"

Also applies to: 45-47

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts` around lines 27
- 32, The union types currently allow an email path without postUrl because
DefaultVideoRenderOptions has target?: string and callers cast to
EmailVideoRenderOptions; fix by making target a discriminant (e.g., in
DefaultVideoRenderOptions set target?: 'default' or remove overlapping target)
and remove unsafe casts, then add a type guard function
isEmailVideoOptions(options): options is EmailVideoRenderOptions that checks
options.target === 'email' and options.postUrl exists; update renderVideoNode
and any callers to use this guard before treating options as
EmailVideoRenderOptions so the compiler enforces that postUrl is present for the
email branch.

4-7: ⚠️ Potential issue | 🟠 Major

Keep width/height nullable to match the node model.

VideoNodeData narrows dimensions to number, but downstream logic assumes valid numeric dimensions for spacer URL/aspect math. If upstream node data allows null dimensions, this can produce invalid output (NaN/broken spacer sizing).

🔧 Suggested fix
 interface VideoNodeData {
     src: string;
-    width: number;
-    height: number;
+    width: number | null;
+    height: number | null;
     caption: string;
@@
 }
+
+type SizedVideoNodeData = VideoNodeData & {width: number; height: number};
+
+function hasVideoDimensions(node: VideoNodeData): node is SizedVideoNodeData {
+    return node.width != null && node.height != null && node.height > 0;
+}
@@
 export function renderVideoNode(node: VideoNodeData, options: VideoRenderOptions = {}) {
@@
-    if (!node.src || node.src.trim() === '') {
+    if (!node.src || node.src.trim() === '' || !hasVideoDimensions(node)) {
         return renderEmptyContainer(document);
     }
@@
-        ? emailCardTemplate({node, options: options as EmailVideoRenderOptions, cardClasses})
-        : cardTemplate({node, cardClasses});
+        ? emailCardTemplate({node, options: options as EmailVideoRenderOptions, cardClasses})
+        : cardTemplate({node, cardClasses});
#!/bin/bash
set -euo pipefail

# Verify nullable source model vs renderer narrowing
rg -n "width: number \\| null|height: number \\| null" packages/kg-default-nodes/src/nodes/video/VideoNode.ts
rg -n "interface VideoNodeData|width: number;|height: number;" packages/kg-default-nodes/src/nodes/video/video-renderer.ts
rg -n "aspectRatio|spacergif|emailSpacerHeight" packages/kg-default-nodes/src/nodes/video/video-renderer.ts

Also applies to: 126-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts` around lines 4 -
7, VideoNodeData currently declares width and height as number but upstream
model allows null; change the VideoNodeData interface to allow width: number |
null and height: number | null, then update any downstream calculations in this
module that assume numeric dimensions (look for aspectRatio, spacergif,
emailSpacerHeight and any spacer URL construction) to guard against nulls — use
conditional checks or fallbacks (e.g., skip aspect math or default to 1/0
values) before dividing/multiplying so you never produce NaN or malformed spacer
URLs when width/height are null.
packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)

39-77: ⚠️ Potential issue | 🟡 Minor

Add an explicit baseSrc guard before fallback assignment and URL construction.

Line 39 can still produce null/empty values, while Lines 66, 72, and 77 rely on baseSrc!. This can still fail at runtime in edge cases and hides the real precondition.

🛠️ Suggested fix
         const baseSrc = el.getAttribute('data-src');
+        if (!baseSrc) {
+            return;
+        }
@@
-            el.src = baseSrc!;
+            el.src = baseSrc;
             return;
@@
-            el.src = baseSrc!;
+            el.src = baseSrc;
             return;
@@
-        const u = new URL(baseSrc!);
+        const u = new URL(baseSrc);
#!/bin/bash
# Verify unresolved non-null assertions and missing guard around baseSrc in this file
rg -n -C2 "const baseSrc = el.getAttribute\('data-src'\)|baseSrc!" packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around
lines 39 - 77, The code uses baseSrc (const baseSrc =
el.getAttribute('data-src')) with non-null assertions in multiple places (el.src
= baseSrc! and new URL(baseSrc!)) without an explicit guard; add an early check
that baseSrc is present and non-empty (e.g., if (!baseSrc) { return; } or set a
safe fallback) before any use, and replace the fallback assignments and the new
URL(baseSrc!) call in this function (references: baseSrc, colorToRgb, the while
loop that searches parent bg, the el.src assignments and new URL(...) call) so
runtime errors are avoided when data-src is null/empty. Ensure the guard is
placed before the bg-check branch exits and before constructing the URL.
🧹 Nitpick comments (20)
packages/kg-default-nodes/test/serializers/linebreak.test.ts (1)

47-50: Reduce repeated ElementNode casts in assertions.

There are many repeated (nodes[i] as ElementNode).getChildren() calls. Consider extracting the casted node/children once per block to improve readability and reduce assertion noise.

♻️ Example refactor pattern
-should.equal((nodes[0] as ElementNode).getChildren().length, 3);
-should.equal((nodes[0] as ElementNode).getChildren()[0].getType(), 'extended-text');
-should.equal((nodes[0] as ElementNode).getChildren()[1].getType(), 'linebreak');
-should.equal((nodes[0] as ElementNode).getChildren()[2].getType(), 'extended-text');
+const paragraphChildren = (nodes[0] as ElementNode).getChildren();
+should.equal(paragraphChildren.length, 3);
+should.equal(paragraphChildren[0].getType(), 'extended-text');
+should.equal(paragraphChildren[1].getType(), 'linebreak');
+should.equal(paragraphChildren[2].getType(), 'extended-text');

Also applies to: 63-65, 70-72, 82-84, 87-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/serializers/linebreak.test.ts` around lines 47
- 50, Extract the casted ElementNode and/or its children into a local variable
to avoid repeating `(nodes[i] as ElementNode).getChildren()` in the assertions;
for example, create a local `const el = nodes[0] as ElementNode` (or `const
children = el.getChildren()`) and then assert on `children.length` and
`children[0/1/2].getType()` instead of repeating the cast. Apply the same change
for the other blocks referenced (lines with nodes[1], nodes[2], etc.) so each
assertion group uses a single `ElementNode`/`children` variable for clarity and
reduced duplication.
packages/kg-default-nodes/test/nodes/horizontalrule.test.ts (1)

11-13: Remove stale dataset usage and rename asideNode for clarity.

Line 69 mutates dataset.cardWidth, but dataset is no longer used by $createHorizontalRuleNode() at Line 71; also asideNode is misleading in a horizontal-rule test.

Proposed cleanup
-    let dataset: Record<string, unknown>;
     let exportOptions: {dom: typeof dom};
@@
-        dataset = {};
-
         exportOptions = {
             dom
         };
@@
-            dataset.cardWidth = 'wide';
-
-            const asideNode = $createHorizontalRuleNode();
-            const json = asideNode.exportJSON();
+            const hrNode = $createHorizontalRuleNode();
+            const json = hrNode.exportJSON();

Also applies to: 69-73

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/horizontalrule.test.ts` around lines 11
- 13, Remove the stale dataset usage and rename the misleading asideNode
variable: delete the dataset variable mutation (dataset.cardWidth) and any
references to dataset since $createHorizontalRuleNode() no longer consumes it,
and rename asideNode to hrNode or horizontalRuleNode in horizontalrule.test.ts
(references to asideNode, creation, and assertions) so the test reads clearly
and only passes necessary args to $createHorizontalRuleNode().
packages/kg-default-nodes/test/nodes/button.test.ts (1)

117-117: Optional: centralize repeated exportDOM casting in a small helper.

This would reduce repetition and keep test intent clearer without changing behavior.

♻️ Proposed refactor
+    const exportButtonElement = (buttonNode: ButtonNode, options: Record<string, unknown>) =>
+        buttonNode.exportDOM(options as unknown as LexicalEditor) as {element: HTMLElement};
+
     describe('exportDOM', function () {
         it('creates a button card', editorTest(function () {
             const buttonNode = $createButtonNode(dataset);
-            const {element} = buttonNode.exportDOM(exportOptions as unknown as LexicalEditor) as {element: HTMLElement};
+            const {element} = exportButtonElement(buttonNode, exportOptions);
@@
-            const {element} = buttonNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) as {element: HTMLElement};
+            const {element} = exportButtonElement(buttonNode, {...exportOptions, ...options});
@@
-            const {element} = buttonNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) as {element: HTMLElement};
+            const {element} = exportButtonElement(buttonNode, {...exportOptions, ...options});
@@
-            const {element} = buttonNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) as {element: HTMLElement};
+            const {element} = exportButtonElement(buttonNode, {...exportOptions, ...options});

Also applies to: 127-127, 144-144, 176-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/button.test.ts` at line 117, Create a
small test helper that centralizes the repeated cast of exportDOM results
(currently written as buttonNode.exportDOM(exportOptions as unknown as
LexicalEditor) as {element: HTMLElement}) and use it wherever that pattern
appears (e.g., the calls around the occurrences referencing buttonNode.exportDOM
with exportOptions and the cast to {element: HTMLElement}); implement a function
like getExportedElement(node, options) that invokes node.exportDOM(options as
unknown as LexicalEditor) and returns the typed {element: HTMLElement}, then
replace each inline cast (including the other occurrences noted) with calls to
that helper to reduce duplication and make intent clearer.
packages/kg-default-nodes/test/nodes/call-to-action.test.ts (1)

718-719: Fix local variable typo (docuementdocument).

This is harmless at runtime, but it hurts readability and searchability.

✏️ Suggested patch
-            const docuement = createDocument(htmlTemplate`${element.outerHTML.toString()}`);
-            const nodes = $generateNodesFromDOM(editor, docuement) as CallToActionNode[];
+            const document = createDocument(htmlTemplate`${element.outerHTML.toString()}`);
+            const nodes = $generateNodesFromDOM(editor, document) as CallToActionNode[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/call-to-action.test.ts` around lines 718
- 719, Rename the misspelled local variable "docuement" to "document" where it's
assigned via createDocument(htmlTemplate`${element.outerHTML.toString()}`) and
subsequently passed into $generateNodesFromDOM(editor, document) as
CallToActionNode[] so the code reads const document = createDocument(...); const
nodes = $generateNodesFromDOM(editor, document) as CallToActionNode[]; update
all occurrences in this test block to use the corrected identifier for
readability and searchability.
packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts (1)

3-23: Tighten constructor typing to avoid unnecessary LexicalNode cast

parseToggleNode accepts a constructor returning unknown and then casts the result to LexicalNode. Since all callers pass LexicalNode-compatible classes (e.g., ToggleNode extends GeneratedDecoratorNodeBaseKoenigDecoratorNodeDecoratorNode), the parameter type and cast can be tightened:

Proposed change
-export function parseToggleNode(ToggleNode: new (data: Record<string, unknown>) => unknown) {
+export function parseToggleNode(ToggleNode: new (data: Record<string, unknown>) => LexicalNode) {
...
-                        const node = new ToggleNode(payload);
-                        return {node: node as LexicalNode};
+                        const node = new ToggleNode(payload);
+                        return {node};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts` around lines 3 -
23, The parseToggleNode currently types its ToggleNode parameter as returning
unknown and then casts new ToggleNode(payload) to LexicalNode; tighten the
constructor signature to reflect that ToggleNode produces a
LexicalNode-compatible instance (e.g., change the parameter type from new (data:
Record<string, unknown>) => unknown to new (data: Record<string, unknown>) =>
LexicalNode or the specific node class) and remove the unnecessary cast where
the node is returned; update the parseToggleNode signature and any related
generics so the created node (via new ToggleNode(payload)) is already correctly
typed.
packages/kg-default-nodes/test/utils/visibility.test.ts (1)

209-220: Tighten target parameter to prevent invalid test inputs.

runRender accepts target: string, but the implementation only handles 'web' and 'email'. Narrowing to 'web' | 'email' will catch accidental invalid targets at compile time. All six test cases consistently use only these two values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/utils/visibility.test.ts` around lines 209 -
220, The runRender helper allows any string for the target but the code and
callers only support 'web' and 'email'; tighten its signature by changing the
target parameter type to the string literal union 'web' | 'email' so invalid
targets are caught at compile time; update the runRender declaration (and any
related tests calling it) and keep using buildVisibility and
renderWithVisibility as-is to ensure the visibilityWithDefaults and render call
remain unchanged.
packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts (1)

3-3: Use recommended LexicalNode constructor typing to eliminate unsafe casts.

Line 3 currently types HeaderNode as returning unknown, requiring force-casts to LexicalNode at Lines 40 and 84. Lexical v0.13.1 documentation recommends constraining node constructors to new (...) => LexicalNode directly in conversion callbacks. This removes type unsafety and eliminates unnecessary casts.

Suggested change
 import type {LexicalNode} from 'lexical';

-export function parseHeaderNode(HeaderNode: new (data: Record<string, unknown>) => unknown) {
+export function parseHeaderNode(HeaderNode: new (data: Record<string, unknown>) => LexicalNode) {

Also applies to: 40-40, 84-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts` at line
3, The parseHeaderNode function currently types HeaderNode as returning unknown
which forces unsafe casts; change the constructor signature in parseHeaderNode
to HeaderNode: new (data: Record<string, unknown>) => LexicalNode
(importing/using LexicalNode type) and then remove the force-casts where
instances are created/returned (the locations around the current casts at the
conversions used near the previous lines ~40 and ~84), so the returned instances
are already typed as LexicalNode and no "as LexicalNode" casts are needed.
packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts (1)

213-215: Refactor to validate createDocument once instead of using repeated non-null assertions.

The helper addCreateDocumentOption() conditionally sets options.createDocument but doesn't guarantee it will be non-null—if neither createDocument nor dom are provided in options, the assertion will fail at runtime. The function uses options.createDocument!() at both lines 215 and 238, making it more robust to capture and validate once upfront.

♻️ Proposed refactor
 export function renderHeaderNodeV2(dataset: HeaderV2DatasetNode, options: HeaderV2RenderOptions = {}) {
     addCreateDocumentOption(options);
-    const document = options.createDocument!();
+    const createDocument = options.createDocument;
+    if (!createDocument) {
+        throw new Error('renderHeaderNodeV2 requires options.createDocument');
+    }
+    const document = createDocument();
@@
     if (options.target === 'email') {
-        const emailDoc = options.createDocument!();
+        const emailDoc = createDocument();
         const emailDiv = emailDoc.createElement('div');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts`
around lines 213 - 215, renderHeaderNodeV2 calls addCreateDocumentOption then
uses options.createDocument!() at multiple sites (lines shown) which can still
be undefined and crash; update renderHeaderNodeV2 to capture and validate the
createDocument function once after addCreateDocumentOption by assigning const
createDocument = options.createDocument and throwing/logging a clear error if
it's falsy, then replace subsequent uses of options.createDocument!() with
createDocument() to avoid repeated non-null assertions (references:
renderHeaderNodeV2, addCreateDocumentOption, options.createDocument).
packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts (1)

25-27: Avoid mutating node during render.

Line 26 mutates input state inside the renderer. Use a local sanitized backgroundColor instead, so rendering stays side-effect free.

Proposed refactor
-    if (!node.backgroundColor || !node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) {
-        node.backgroundColor = 'white';
-    }
-
-    element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${node.backgroundColor}`);
+    const backgroundColor = (node.backgroundColor && /^[a-zA-Z\d-]+$/.test(node.backgroundColor))
+        ? node.backgroundColor
+        : 'white';
+
+    element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${backgroundColor}`);

Also applies to: 29-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines
25 - 27, The renderer is mutating the input `node` (setting
node.backgroundColor) which breaks render side-effect guarantees; instead,
compute a local sanitized value (e.g., const backgroundColorSanitized) by
validating node.backgroundColor with the existing /^[a-zA-Z\d-]+$/ check and
defaulting to 'white' when invalid, then use that local variable in the render
output (update usages in the callout renderer where node.backgroundColor is
referenced, including the other occurrence noted) so the `node` object is never
modified during rendering.
packages/kg-default-nodes/test/nodes/email-cta.test.ts (3)

213-217: Extract repeated export/assert setup into one helper.

The repeated arrange/act block makes this section harder to maintain. A tiny helper returning outerHTML will remove duplication and keep each case focused on expected output.

Also applies to: 240-244, 262-266, 284-288, 306-310, 334-338, 362-366, 402-406, 440-444, 477-481

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` around lines 213 -
217, Multiple tests repeat the same arrange/act of calling
emailNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor)
and asserting element.outerHTML; extract that into a small helper (e.g.
getExportedOuterHTML) that accepts the emailNode plus optional options, calls
emailNode.exportDOM({...exportOptions, ...opts} as unknown as LexicalEditor),
casts element to HTMLElement and returns element.outerHTML, then replace the
repeated blocks in the tests (the blocks around emailNode.exportDOM,
exportOptions, options, LexicalEditor and element.outerHTML) with calls to this
helper so each test only asserts the expected HTML.

11-12: Use concrete fixture types instead of Record<string, unknown>.

dataset and exportOptions are broad enough to hide field typos and wrong value types in tests. A small local type keeps strict mode useful here.

♻️ Suggested typing refinement
+type EmailCtaDataset = {
+    alignment: string;
+    buttonText: string;
+    buttonUrl: string;
+    html: string;
+    segment: string;
+    showButton: boolean;
+    showDividers: boolean;
+};
+
+type EmailExportOptions = {
+    exportFormat: 'html';
+    dom: typeof dom;
+};
+
 let editor: LexicalEditor;
-let dataset: Record<string, unknown>;
-let exportOptions: Record<string, unknown>;
+let dataset: EmailCtaDataset;
+let exportOptions: EmailExportOptions;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` around lines 11 - 12,
Replace the overly-broad types for the test fixtures by declaring small concrete
interfaces/types for dataset and exportOptions instead of Record<string,
unknown>; locate the top-level variables dataset and exportOptions in
packages/kg-default-nodes/test/nodes/email-cta.test.ts, define a minimal
FixtureDataset (e.g., fields used by the tests like subject, body, recipients,
etc.) and ExportOptions (e.g., format, includeMetadata, locale) that match
actual usages in the tests, then update the dataset and exportOptions
declarations and any fixture values to use those types so TypeScript catches
typos and incorrect value shapes.

179-179: Avoid blind cast when reading root children.

Casting children directly to EmailCtaNode[] can mask an unexpected node type. Assert first, then narrow.

✅ Safer narrowing in test
-const [emailNode] = $getRoot().getChildren() as EmailCtaNode[];
+const [firstChild] = $getRoot().getChildren();
+$isEmailCtaNode(firstChild).should.be.true();
+const emailNode = firstChild as EmailCtaNode;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` at line 179, The test
is blindly casting root children to EmailCtaNode[] which can hide wrong node
types; instead read children via $getRoot().getChildren(), assert the child
exists and is an EmailCtaNode (e.g., via a runtime type check, instanceof, or
node.type property), then narrow to EmailCtaNode before assigning to emailNode
(use Array.prototype.find/filter to locate the EmailCtaNode and add an
expect/assert to fail the test if not found) so that EmailCtaNode is only used
after a safe runtime assertion.
packages/kg-default-nodes/src/nodes/signup/signup-parser.ts (1)

16-16: Tighten constructor typing to remove the unsafe LexicalNode cast.

At line 16, the constructor parameter is typed to return unknown, forcing an unsafe cast at line 62. Since SignupNode extends the result of generateDecoratorNode() (which extends KoenigDecoratorNodeDecoratorNode from lexical), instances are guaranteed to be LexicalNode. Change the constructor return type to LexicalNode to let TypeScript enforce this at compile time and eliminate the cast.

Proposed diff
-export function signupParser(SignupNode: new (data: Record<string, unknown>) => unknown) {
+export function signupParser(SignupNode: new (data: Record<string, unknown>) => LexicalNode) {
@@
-                        const node = new SignupNode(payload);
-                        return {node: node as LexicalNode};
+                        const node = new SignupNode(payload);
+                        return {node};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/signup/signup-parser.ts` at line 16, The
constructor parameter of signupParser is currently typed to return unknown,
forcing an unsafe cast elsewhere; change the SignupNode constructor signature in
signupParser to return LexicalNode (not unknown) so TypeScript knows instances
are LexicalNode (since SignupNode extends the result of generateDecoratorNode(),
which extends KoenigDecoratorNode/DecoratorNode), then remove the unsafe cast
where SignupNode is instantiated (the cast to LexicalNode) and rely on the
tightened type to satisfy the compiler; update the type import if necessary to
reference LexicalNode used by generateDecoratorNode()/KoenigDecoratorNode.
packages/kg-default-nodes/test/nodes/file.test.ts (3)

121-123: Assert img existence before casting.

The direct cast can hide null until property access; an existence assertion gives cleaner failure output in test runs.

♻️ Proposed refactor
-                const icon = el.querySelector('img') as HTMLImageElement;
-                icon.src.should.equal('https://static.ghost.org/v4.0.0/images/download-icon-darkmode.png');
-                icon.style.height.should.equal('24px');
+                const icon = el.querySelector('img');
+                should.exist(icon);
+                (icon as HTMLImageElement).src.should.equal('https://static.ghost.org/v4.0.0/images/download-icon-darkmode.png');
+                (icon as HTMLImageElement).style.height.should.equal('24px');
...
-                const icon = el.querySelector('img') as HTMLImageElement;
-                icon.style.height.should.equal('20px');
+                const icon = el.querySelector('img');
+                should.exist(icon);
+                (icon as HTMLImageElement).style.height.should.equal('20px');

Also applies to: 143-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 121 - 123,
The test currently casts el.querySelector('img') directly to HTMLImageElement
(variable icon) which can be null; update the assertions to first check
existence (e.g., assert/icon.should.exist or expect(icon).to.not.be.null) before
accessing icon.src and icon.style.height, and apply the same change to the
similar assertions around lines 143-144 so failures show a clear "missing img"
message rather than a null property access.

12-13: Prefer explicit fixture types over Record<string, unknown>.

dataset/exportOptions are broad and force extra casts later; a small local test type improves signal and catches misspelled fixture keys earlier.

♻️ Proposed refactor
+type FileDataset = {
+    src?: string;
+    fileTitle?: string;
+    fileSize?: number;
+    fileCaption?: string;
+    fileName?: string;
+};
+
+type FileExportOptions = {
+    exportFormat: 'html';
+    dom: typeof dom;
+    target?: 'email';
+    postUrl?: string;
+};

 let editor: LexicalEditor;
-let dataset: Record<string, unknown>;
-let exportOptions: Record<string, unknown>;
+let dataset: FileDataset;
+let exportOptions: FileExportOptions;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 12 - 13, The
test declares dataset and exportOptions with broad types (Record<string,
unknown>), which forces casts and hides typos; replace these with small explicit
local fixture types (e.g., define DatasetFixture and ExportOptionsFixture
interfaces/types above the tests that enumerate the exact keys used by the
tests) and change the variable declarations to use those types (dataset:
DatasetFixture, exportOptions: ExportOptionsFixture) so callers in file.test.ts
(references to dataset and exportOptions) get proper compile-time checks and
remove need for ad-hoc casts.

86-87: Extract a helper for repeated exportDOM setup.

The same create/export/cast sequence appears multiple times. A helper would reduce duplication and keep test intent focused.

♻️ Proposed refactor
+    const exportFileElement = (inputDataset = dataset) => {
+        const fileNode = $createFileNode(inputDataset);
+        const {element} = fileNode.exportDOM(exportOptions as unknown as LexicalEditor);
+        return element as HTMLElement;
+    };

     it('creates a file card', editorTest(function () {
-            const fileNode = $createFileNode(dataset);
-            const {element} = fileNode.exportDOM(exportOptions as unknown as LexicalEditor);
-            (element as HTMLElement).outerHTML.should.equal(`<div class="kg-card kg-file-card"><a class="kg-file-card-container" href="https://github.com/content/files/2023/03/IMG_0196.jpeg" title="Download" download=""><div class="kg-file-card-contents"><div class="kg-file-card-title">Cool image to download</div><div class="kg-file-card-caption">This is a description</div><div class="kg-file-card-metadata"><div class="kg-file-card-filename">IMG_0196.jpeg</div><div class="kg-file-card-filesize">121 KB</div></div></div><div class="kg-file-card-icon"><svg viewBox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"></polyline><line class="a" x1="12" y1="6.75" x2="12" y2="18"></line><circle class="a" cx="12" cy="12" r="11.25"></circle></svg></div></a></div>`);
+            const el = exportFileElement();
+            el.outerHTML.should.equal(`<div class="kg-card kg-file-card"><a class="kg-file-card-container" href="https://github.com/content/files/2023/03/IMG_0196.jpeg" title="Download" download=""><div class="kg-file-card-contents"><div class="kg-file-card-title">Cool image to download</div><div class="kg-file-card-caption">This is a description</div><div class="kg-file-card-metadata"><div class="kg-file-card-filename">IMG_0196.jpeg</div><div class="kg-file-card-filesize">121 KB</div></div></div><div class="kg-file-card-icon"><svg viewBox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"></polyline><line class="a" x1="12" y1="6.75" x2="12" y2="18"></line><circle class="a" cx="12" cy="12" r="11.25"></circle></svg></div></a></div>`);
     }));

Also applies to: 98-99, 133-134, 155-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 86 - 87,
Extract a small helper in the test (e.g., exportElement or
getExportedHTMLElement) that accepts a node and exportOptions, calls
node.exportDOM(exportOptions as unknown as LexicalEditor), and returns the
element cast to an HTMLElement; then replace the repeated sequences that call
fileNode.exportDOM(exportOptions as unknown as LexicalEditor) and (element as
HTMLElement) with this helper to remove duplication and make assertions more
readable (update all occurrences where exportDOM + cast is repeated).
packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)

5-5: Tighten EmbedNode typing and remove unsafe casts.

The function signature uses new (...) => unknown which prevents TypeScript from catching incompatible node classes. The as LexicalNode casts at lines 22, 53, and 72, combined with the double cast as unknown as HTMLIFrameElement at line 65, further weaken type safety.

Change the constructor parameter from new (data: Record<string, unknown>) => unknown to new (data: Record<string, unknown>) => LexicalNode, remove the unsafe as LexicalNode casts from lines 22, 53, and 72, and replace the double cast at line 65 with a runtime type guard:

♻️ Proposed refactor
 import type {LexicalNode} from 'lexical';
@@
-export function parseEmbedNode(EmbedNode: new (data: Record<string, unknown>) => unknown) {
+export function parseEmbedNode(EmbedNode: new (data: Record<string, unknown>) => LexicalNode) {
@@
                             const node = new EmbedNode(payload);
-                            return {node: node as LexicalNode};
+                            return {node};
@@
                             const node = new EmbedNode(payload);
-                            return {node: node as LexicalNode};
+                            return {node};
@@
                     conversion(domNode: HTMLElement) {
-                        const payload = _createPayloadForIframe(domNode as unknown as HTMLIFrameElement);
+                        if (!(domNode instanceof HTMLIFrameElement)) {
+                            return null;
+                        }
+                        const payload = _createPayloadForIframe(domNode);
@@
                         const node = new EmbedNode(payload);
-                        return {node: node as LexicalNode};
+                        return {node};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` at line 5, Change
the parseEmbedNode signature to accept EmbedNode constructors that produce
LexicalNode (i.e., new (data: Record<string, unknown>) => LexicalNode) instead
of unknown, remove the unsafe casts to LexicalNode currently applied to
instances created from EmbedNode (the casts at the spots referenced in the
review), and replace the double cast to HTMLIFrameElement with a runtime type
guard: after creating/receiving the element check its runtime type (e.g.,
element instanceof HTMLIFrameElement or element.tagName === 'IFRAME' and
required properties exist) before treating it as an iframe; update usages in
parseEmbedNode/EmbedNode creation to rely on the stronger constructor return
type so the explicit "as LexicalNode" casts are unnecessary.
packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)

22-25: Tighten BrowserDocument.createElement typing with tag-specific overloads.

The current signature incorrectly claims every created element has both canvas members (getContext, width, height) and script members (innerHTML), which forces downstream as unknown as casts (see transistor-renderer.ts:47). Only 'canvas' and 'script' tags are actually used.

♻️ Suggested typing refactor
 interface BrowserElement {
     parentElement: BrowserElement | null;
     querySelector(selector: string): BrowserElement | null;
     getAttribute(name: string): string | null;
     src: string;
 }
 
+interface BrowserScriptElement extends BrowserElement {
+    innerHTML: string;
+}
+
+interface BrowserCanvasContext {
+    fillStyle: string;
+    fillRect(x: number, y: number, w: number, h: number): void;
+    getImageData(x: number, y: number, w: number, h: number): { data: number[] };
+}
+
+interface BrowserCanvasElement extends BrowserElement {
+    width: number;
+    height: number;
+    getContext(id: string): BrowserCanvasContext;
+}
+
 interface BrowserDocument {
     currentScript: { parentElement: BrowserElement } | null;
-    createElement(tag: string): BrowserElement & { innerHTML: string; width: number; height: number; getContext(id: string): { fillStyle: string; fillRect(x: number, y: number, w: number, h: number): void; getImageData(x: number, y: number, w: number, h: number): { data: number[] } } };
+    createElement(tag: 'script'): BrowserScriptElement;
+    createElement(tag: 'canvas'): BrowserCanvasElement;
+    createElement(tag: string): BrowserElement;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around
lines 22 - 25, The createElement signature in BrowserDocument is too broad—every
element is typed as having both canvas APIs and script innerHTML forcing unsafe
casts (see transistor-renderer.ts:47); change createElement to tag-specific
overloads: add distinct element types (e.g., CanvasElement with
width/height/getContext/getImageData and ScriptElement with innerHTML and
parentElement) and declare createElement(tag: 'canvas'): CanvasElement and
createElement(tag: 'script'): ScriptElement (plus a generic createElement(tag:
string): BrowserElement fallback); update BrowserDocument.currentScript typing
to use ScriptElement | null and replace the single combined type with these
narrower types so downstream code no longer needs as unknown as casts.
packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts (1)

3-3: Tighten constructor typing to eliminate unsafe LexicalNode assertions.

Typing BookmarkNode as returning unknown forces as LexicalNode casts. Change the constructor parameter to return LexicalNode directly and remove the casts.

Proposed fix
-export function parseBookmarkNode(BookmarkNode: new (data: Record<string, unknown>) => unknown) {
+export function parseBookmarkNode(BookmarkNode: new (data: Record<string, unknown>) => LexicalNode) {
...
-                        return {node: node as LexicalNode};
+                        return {node};
...
-                        return {node: node as LexicalNode};
+                        return {node};

Also applies to: 31-31, 89-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts` at line 3,
The constructor type for the BookmarkNode parameter in parseBookmarkNode is too
loose (returns unknown) causing unsafe casts to LexicalNode; update the function
signature to accept BookmarkNode: new (data: Record<string, unknown>) =>
LexicalNode (or the appropriate LexicalNode subclass type) and remove all `as
LexicalNode` assertions inside parseBookmarkNode and the other similar factories
in this file (the other constructor-typed parameters referenced in the review).
Ensure imports include LexicalNode if needed and update any call sites or tests
that relied on the previous signature.
packages/kg-default-nodes/test/nodes/at-link.test.ts (1)

73-75: Use a real serialized payload in the importJSON() test.

{linkFormat: 1} as ReturnType<AtLinkNode['exportJSON']> sidesteps the stricter test typing added in this PR, so this case no longer validates the actual serialized shape. Seed from exportJSON() or pass the full object instead.

Suggested cleanup
-        const atLinkNode = AtLinkNode.importJSON({linkFormat: 1} as ReturnType<AtLinkNode['exportJSON']>);
+        const serialized: ReturnType<AtLinkNode['exportJSON']> = {
+            children: [],
+            direction: null,
+            format: '',
+            indent: 0,
+            linkFormat: 1,
+            type: 'at-link',
+            version: 1
+        };
+        const atLinkNode = AtLinkNode.importJSON(serialized);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/at-link.test.ts` around lines 73 - 75,
The test currently passes a casted partial literal to AtLinkNode.importJSON
which bypasses strict serialized-shape checks; instead obtain a real serialized
payload by calling exportJSON() on a real AtLinkNode instance (e.g., create or
obtain an AtLinkNode, call its exportJSON() to produce the serialized object)
and then pass that object into AtLinkNode.importJSON(...), then assert on
getLinkFormat(); replace the `{linkFormat: 1} as
ReturnType<AtLinkNode['exportJSON']>` literal with the exportJSON-derived
payload so the test validates the actual serialized shape.

Comment thread packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
Comment thread packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts Outdated
Comment on lines +3 to +9
export function parseHorizontalRuleNode(HorizontalRuleNode: new (data?: Record<string, unknown>) => unknown) {
return {
hr: () => ({
conversion() {
const node = new HorizontalRuleNode();
return {node: node as LexicalNode};
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking parser constructor signatures and LexicalNode casts..."
rg -nP --type=ts -C2 'new\s*\([^)]*\)\s*=>\s*unknown|as\s+LexicalNode' packages/kg-default-nodes/src/nodes

Repository: TryGhost/Koenig

Length of output: 24008


Tighten constructor typing to avoid masking invalid node types.

Line 3 allows a constructor returning unknown, and Line 8 force-casts to LexicalNode. This weakens TypeScript's static guarantees and can hide incorrect constructor inputs. Type the constructor to return LexicalNode and remove the cast.

This same pattern appears in all parser files across packages/kg-default-nodes/src/nodes/. Consider a coordinated refactor across all parsers for consistency.

Proposed fix
 import type {LexicalNode} from 'lexical';
 
-export function parseHorizontalRuleNode(HorizontalRuleNode: new (data?: Record<string, unknown>) => unknown) {
+export function parseHorizontalRuleNode(
+    HorizontalRuleNode: new (data?: Record<string, unknown>) => LexicalNode
+) {
     return {
         hr: () => ({
             conversion() {
                 const node = new HorizontalRuleNode();
-                return {node: node as LexicalNode};
+                return {node};
             },
             priority: 0 as const
         })
     };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts`
around lines 3 - 9, The constructor parameter for parseHorizontalRuleNode is
currently typed to return unknown and the code force-casts to LexicalNode;
change the constructor signature to new (data?: Record<string, unknown>) =>
LexicalNode (i.e. require it actually constructs a LexicalNode) and remove the
cast in the conversion() implementation (use the constructed node directly);
apply the same tightening to other parser factories in
packages/kg-default-nodes/src/nodes/ so all parser constructors return
LexicalNode rather than unknown for consistent static guarantees.

Comment on lines +98 to +102
it('returns null element for exportDOM()', editorTest(function () {
const atLinkNode = $createAtLinkNode(null);
const atLinkSearchNode = $createAtLinkSearchNode('test');
atLinkNode.append(atLinkSearchNode);
should.equal(atLinkNode.exportDOM(), null);
should.equal(atLinkNode.exportDOM().element, null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "AtLinkNode exportDOM definition:"
sed -n '/exportDOM\s*(/,/^[[:space:]]*}/p' packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts

echo
echo "Current test call site:"
sed -n '98,103p' packages/kg-default-nodes/test/nodes/at-link.test.ts

Repository: TryGhost/Koenig

Length of output: 476


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== AtLinkNode class definition and parent ==="
head -50 packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts

echo
echo "=== exportDOM method in AtLinkNode ==="
sed -n '/exportDOM/,/^[[:space:]]*}/p' packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts

echo
echo "=== Check parent class definition ==="
grep -n "extends\|class AtLinkNode" packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts | head -5

echo
echo "=== Check other node exportDOM implementations ==="
rg "exportDOM\s*\(" packages/kg-default-nodes/src/nodes/ -A 2 | head -40

Repository: TryGhost/Koenig

Length of output: 3153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All exportDOM implementations in the codebase ==="
rg "exportDOM\s*\(" packages/kg-default-nodes/src/nodes/ -B 2 -A 4

echo
echo "=== Check node_modules for Lexical ElementNode type definitions ==="
find . -name "*.d.ts" -type f | grep -i lexical | grep -i node | head -5

Repository: TryGhost/Koenig

Length of output: 1396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check ElementNode type definitions ==="
find . -type f \( -name "*.d.ts" -o -name "*.ts" \) | xargs grep -l "ElementNode" | grep -E "lexical|node_modules" | head -5

echo
echo "=== Check if lexical is installed and check its export signature ==="
if [ -d "node_modules/lexical" ]; then
    echo "Found lexical package"
    find node_modules/lexical -name "*.d.ts" -type f | head -10
    echo
    echo "=== Lexical ElementNode.exportDOM signature ==="
    grep -r "exportDOM" node_modules/lexical/LexicalNode.d.ts 2>/dev/null | head -10
fi

echo
echo "=== Check package.json for Lexical version ==="
grep -i lexical packages/kg-default-nodes/package.json

Repository: TryGhost/Koenig

Length of output: 435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Lexical dependency in kg-default-nodes ==="
cat packages/kg-default-nodes/package.json | grep -A 2 -B 2 lexical

echo
echo "=== Check node_modules/lexical for type definitions ==="
ls -la node_modules/lexical/LexicalNode.d.ts 2>/dev/null || echo "Type definitions file not found, checking alternatives..."
ls -la node_modules/lexical/index.d.ts 2>/dev/null || echo "Index.d.ts not found"

echo
echo "=== Try to find exportDOM method signature in node_modules ==="
grep -r "exportDOM" node_modules/lexical/*.d.ts 2>/dev/null | head -20

Repository: TryGhost/Koenig

Length of output: 873


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search all test files for exportDOM calls ==="
rg "exportDOM\(" packages/kg-default-nodes/test/ -B 2 -A 2

echo
echo "=== Check how exportDOM is called in main code ==="
rg "exportDOM\(" packages/kg-default-nodes/src/ --not-after-path test -B 2 -A 2

echo
echo "=== Check the actual method signature by looking at the full test context ==="
sed -n '90,110p' packages/kg-default-nodes/test/nodes/at-link.test.ts

Repository: TryGhost/Koenig

Length of output: 50807


Pass the test editor into exportDOM().

All other node tests in this suite pass exportOptions (cast as LexicalEditor) to exportDOM(). The test here creates a real editor through editorTest() but doesn't pass it to the method call. While AtLinkNode.exportDOM() currently returns {element: null} (and doesn't use the parameter), the method should be called according to the Lexical library's standard contract.

Suggested fix
-        should.equal(atLinkNode.exportDOM().element, null);
+        should.equal(atLinkNode.exportDOM(editor).element, null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/at-link.test.ts` around lines 98 - 102,
The test calls atLinkNode.exportDOM() without the editor argument; update the
test inside editorTest to pass the test editor instance (the exportOptions /
editor provided by editorTest) into exportDOM so it follows the Lexical contract
— e.g., change the call on the $createAtLinkNode instance from exportDOM() to
exportDOM(exportOptions as unknown as LexicalEditor) (or whatever local editor
variable the editorTest callback provides) so exportDOM receives the editor
parameter.

nodes[0].imageHeight!.should.equal(100);
}));

it('image width and height falls back to nulll if not provided', editorTest(function () {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Correct test description typo (nulllnull).

Small, but this keeps test output clean and professional.

✏️ Suggested patch
-        it('image width and height falls back to nulll if not provided', editorTest(function () {
+        it('image width and height falls back to null if not provided', editorTest(function () {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('image width and height falls back to nulll if not provided', editorTest(function () {
it('image width and height falls back to null if not provided', editorTest(function () {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/call-to-action.test.ts` at line 805,
Update the test description string in the test case currently named "image width
and height falls back to nulll if not provided" to correct the typo by replacing
"nulll" with "null" (the test wrapped with editorTest(...) / the it(...) call
that asserts image width/height fallback). Keep the rest of the test logic
unchanged.

import {DEFAULT_CONFIG, DEFAULT_NODES} from '../../src/index.js';
import type {HTMLConfig, LexicalEditor} from 'lexical';

describe('Serializers: linebreak', function () {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix test suite title to match file intent.

Line 8 labels this suite as linebreak, but this file tests paragraph behavior. This makes test reporting confusing.

🛠️ Suggested fix
-describe('Serializers: linebreak', function () {
+describe('Serializers: paragraph', function () {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('Serializers: linebreak', function () {
describe('Serializers: paragraph', function () {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts` at line 8,
Update the test suite title in the describe call used in paragraph.test.ts: the
current suite name string is "Serializers: linebreak" but the file and tests
target paragraph behavior, so change that describe(...) title to "Serializers:
paragraph" (locate the describe invocation in paragraph.test.ts and edit the
first argument string accordingly).

no issue

This groups the verified parser, renderer, factory, and type-guard fixes so the node APIs stay aligned with their runtime behavior and edge-case tests.
no issue

This preserves property-defined dataset shapes in generateDecoratorNode so generated node factories and constructors stop erasing their input types back to Record<string, unknown>.
Copy link
Copy Markdown

@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: 4

♻️ Duplicate comments (1)
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)

24-40: ⚠️ Potential issue | 🟠 Major

Return null when the closing HTML marker is missing.

If kg-card-end: html is not found, this still creates an empty HtmlNode, which turns malformed marker input into a synthetic empty card. The safer behavior is to bail out early and not convert.

Suggested fix
-                        if (hasEndComment) {
-                            while (nextNode && !isHtmlEndComment(nextNode)) {
-                                const currentNode = nextNode;
-                                nextNode = currentNode.nextSibling;
-                                if (currentNode.nodeType === 1) {
-                                    html.push((currentNode as Element).outerHTML);
-                                } else if (currentNode.nodeType === 3 && currentNode.textContent) {
-                                    html.push(currentNode.textContent);
-                                }
-                                // remove nodes as we go so that they don't go through the parser
-                                currentNode.remove();
-                            }
-                        }
+                        if (!hasEndComment) {
+                            return null;
+                        }
+
+                        while (nextNode && !isHtmlEndComment(nextNode)) {
+                            const currentNode = nextNode;
+                            nextNode = currentNode.nextSibling;
+                            if (currentNode.nodeType === 1) {
+                                html.push((currentNode as Element).outerHTML);
+                            } else if (currentNode.nodeType === 3 && currentNode.textContent) {
+                                html.push(currentNode.textContent);
+                            }
+                            // remove nodes as we go so that they don't go through the parser
+                            currentNode.remove();
+                        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 24 -
40, The code currently creates an empty HtmlNode even when the closing marker is
missing; change the logic in html-parser.ts to bail out when hasEndComment is
false by returning null (or the parser's expected "no node" value) instead of
constructing a payload/HtmlNode. Concretely, use the existing hasEndComment flag
(and the isHtmlEndComment logic) to early-return null before building the html
array/payload and instantiating HtmlNode so malformed/missing closing
`kg-card-end: html` markers are not turned into synthetic empty cards.
🧹 Nitpick comments (8)
packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts (1)

19-21: Make $createPaywallNode dataset optional for this empty-schema node.

Since this node has no declared properties, requiring a dataset argument adds avoidable call-site noise.

Suggested tweak
-export const $createPaywallNode = (dataset: PaywallData) => {
-    return new PaywallNode(dataset);
+export const $createPaywallNode = (dataset: PaywallData = {}) => {
+    return new PaywallNode(dataset);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts` around lines 19 -
21, Make the dataset parameter optional on the factory: change
$createPaywallNode to accept dataset?: PaywallData and call new
PaywallNode(dataset ?? {}) (or otherwise allow undefined) so callers can omit
the argument for this empty-schema node; update the function signature and the
single instantiation (new PaywallNode(...)) accordingly, referencing
$createPaywallNode and PaywallNode.
packages/kg-default-nodes/src/nodes/image/ImageNode.ts (1)

26-43: Reduce exportJSON() drift risk by deriving from super.exportJSON().

exportJSON() manually mirrors all properties, so future imageProperties changes can silently desync serialization. Prefer extending the base export and only redacting src.

♻️ Proposed refactor
     exportJSON() {
-        // checks if src is a data string
-        const {src, width, height, title, alt, caption, cardWidth, href} = this;
-        const isBlob = src && src.startsWith('data:');
-
-        const dataset = {
-            type: 'image',
-            version: 1,
-            src: isBlob ? '<base64String>' : src,
-            width,
-            height,
-            title,
-            alt,
-            caption,
-            cardWidth,
-            href
-        };
-        return dataset;
+        const dataset = super.exportJSON() as Record<string, unknown> & {src?: string};
+        if (typeof dataset.src === 'string' && dataset.src.startsWith('data:')) {
+            dataset.src = '<base64String>';
+        }
+        return dataset;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/image/ImageNode.ts` around lines 26 - 43,
The exportJSON method duplicates all image fields instead of deriving them from
the base class, risking drift; modify ImageNode.exportJSON to call
super.exportJSON() to get the canonical dataset (so it inherits any future
imageProperties changes), then detect if this.src startsWith('data:') and
replace dataset.src with '<base64String>' when needed, otherwise leave
dataset.src as-is, and finally return that dataset (preserving all other
properties from super.exportJSON()).
packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts (1)

26-27: Consider trimming values in isEmpty() to avoid whitespace-only false negatives.

At Lines 26-27, whitespace-only __url/__html will mark the node as non-empty.

Suggested tweak
     isEmpty() {
-        return !this.__url && !this.__html;
+        return !this.__url.trim() && !this.__html.trim();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts` around lines 26 - 27,
The isEmpty() method currently checks __url and __html raw values which treats
whitespace-only strings as non-empty; update EmbedNode.isEmpty() to trim both
this.__url and this.__html (e.g., use String.prototype.trim on each) before
evaluating emptiness so whitespace-only values are treated as empty, and ensure
you reference the __url and __html properties in the check.
packages/kg-default-nodes/src/nodes/header/HeaderNode.ts (1)

16-41: Consider replacing version magic numbers with shared constants.

Using shared constants for render version keys would reduce drift risk between properties.version and defaultRenderFn.

♻️ Proposed refactor
+const HEADER_VERSION = {
+    V1: 1,
+    V2: 2
+} as const;
+
 const headerProperties = [
@@
-    {name: 'version', default: 1},
+    {name: 'version', default: HEADER_VERSION.V1},
@@
 export class HeaderNode extends generateDecoratorNode({
@@
     defaultRenderFn: {
-        1: renderHeaderNodeV1,
-        2: renderHeaderNodeV2
+        [HEADER_VERSION.V1]: renderHeaderNodeV1,
+        [HEADER_VERSION.V2]: renderHeaderNodeV2
     }
 }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/header/HeaderNode.ts` around lines 16 -
41, The header node currently uses literal version numbers in headerProperties
(property 'version' default: 1) and in the HeaderNode class defaultRenderFn
mapping (keys 1 and 2), which can drift; introduce shared constants (e.g.,
HEADER_RENDER_V1, HEADER_RENDER_V2 or a HeaderRenderVersion enum) and replace
the numeric literals used in headerProperties and the defaultRenderFn keys, then
update the mapping to reference those constants alongside renderHeaderNodeV1 and
renderHeaderNodeV2 so the version key is defined in one place and reused.
packages/kg-default-nodes/test/nodes/gallery.test.ts (2)

1061-1061: Prefer explicit existence assertions over non-null assertions on regex matches.

! can throw before giving a clear assertion message when the match is missing.

✅ Suggested assertion style
-                sizes!.length.should.equal(2);
+                should.exist(sizes);
+                sizes.length.should.equal(2);
@@
-                output.match(/width="600"/g)!.length.should.equal(3);
+                const widthMatches = output.match(/width="600"/g);
+                should.exist(widthMatches);
+                widthMatches.length.should.equal(3);

Also applies to: 1170-1170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` at line 1061, The test
currently uses a non-null assertion when reading a regex match result (e.g., the
sizes variable from a .match call) which can throw before a clear failure
message; change it to first assert the match exists (e.g.,
assert.exists/sizes.should.exist or expect(sizes).to.not.be.null/undefined) and
only then assert on its length (e.g., have.lengthOf/length.should.equal(2)),
updating both the use of sizes and the other similar match assertion so failures
show a clear assertion message instead of a runtime error.

638-639: Extract repeated outerHTML assertion plumbing into a helper.

The repeated cast-and-read pattern adds noise and makes future assertion updates harder.

♻️ Suggested cleanup
 describe('GalleryNode', function () {
+    const getOuterHTML = (element: Node): string => (element as HTMLElement).outerHTML;
+
     let editor: LexicalEditor;
     let dataset: Record<string, unknown>;
     let exportOptions: Record<string, unknown>;
@@
-            (element as HTMLElement).outerHTML.should.equal('<span></span>');
+            getOuterHTML(element).should.equal('<span></span>');
@@
-            const output = (element as HTMLElement).outerHTML;
+            const output = getOuterHTML(element);

Also applies to: 645-646, 652-653, 693-694, 720-721, 759-760, 794-795, 883-884, 933-934, 967-968, 994-995, 1011-1012, 1028-1029, 1058-1059, 1080-1081, 1096-1097, 1125-1126, 1167-1168, 1202-1203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 638 - 639,
Extract the repeated cast-and-outerHTML assertion into a small helper (e.g.,
assertOuterHTML or expectOuterHTML) inside the test file and replace every
occurrence of "(element as HTMLElement).outerHTML.should.equal(...)" with a call
to that helper; the helper should accept the Element and expected string and
perform the cast and .outerHTML.should.equal check so you only change the
assertion calls in tests (update all listed occurrences and any similar lines
such as at 645-646, 652-653, 693-694, 720-721, 759-760, 794-795, 883-884,
933-934, 967-968, 994-995, 1011-1012, 1028-1029, 1058-1059, 1080-1081,
1096-1097, 1125-1126, 1167-1168, 1202-1203).
packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts (2)

116-118: Make $createBookmarkNode parameter optional for parity with constructor defaults.

Line 116 currently requires dataset, but constructor already supports empty input. Defaulting this to {} avoids extra caller boilerplate.

✨ Suggested small API polish
-export const $createBookmarkNode = (dataset: BookmarkData) => {
+export const $createBookmarkNode = (dataset: BookmarkData = {}) => {
     return new BookmarkNode(dataset);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines
116 - 118, The factory function $createBookmarkNode currently requires a
BookmarkData parameter even though BookmarkNode's constructor accepts empty
input; change $createBookmarkNode to make its dataset parameter optional and
default it to {} so callers can invoke $createBookmarkNode() without
boilerplate, and ensure the function still forwards the (possibly empty) object
into new BookmarkNode(dataset) to preserve behavior.

82-89: Avoid blind casting in importJSON.

Line 83 trusts unknown JSON with as BookmarkData. A malformed payload can still populate non-string runtime values. Normalize/guard fields before constructing the node.

♻️ Suggested runtime-safe import normalization
 static importJSON(serializedNode: Record<string, unknown>) {
-    const {url, metadata, caption} = serializedNode as BookmarkData;
-    const node = new this({
-        url,
-        metadata,
-        caption
-    });
-    return node;
+    const asString = (value: unknown) => typeof value === 'string' ? value : '';
+    const metadata = (typeof serializedNode.metadata === 'object' && serializedNode.metadata !== null)
+        ? serializedNode.metadata as Record<string, unknown>
+        : {};
+
+    return new this({
+        url: asString(serializedNode.url),
+        metadata: {
+            icon: asString(metadata.icon),
+            title: asString(metadata.title),
+            description: asString(metadata.description),
+            author: asString(metadata.author),
+            publisher: asString(metadata.publisher),
+            thumbnail: asString(metadata.thumbnail)
+        },
+        caption: asString(serializedNode.caption)
+    });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 82
- 89, The importJSON method currently casts the incoming serializedNode to
BookmarkData blindly; change importJSON to safely extract and normalize fields
(url, metadata, caption) from serializedNode by checking types and providing
defaults — e.g., ensure url and caption are strings (or empty string/default),
ensure metadata is an object (or null/{}), and coerce or ignore invalid values —
then pass these sanitized values into the constructor call (the new this({...})
invocation) instead of using the unchecked as BookmarkData cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts`:
- Around line 29-33: The constructor in CalloutNode uses a strict !== undefined
check for calloutEmoji which lets null slip through; update the assignment in
the constructor (the CalloutNode constructor that sets this.__calloutEmoji) to
use nullish coalescing (?? '💡') so null and undefined both fall back to the
default emoji while preserving explicit empty-string values.

In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts`:
- Line 9: The metadata property currently uses a shared mutable default ({})
causing all EmbedNode instances to share the same object; update the metadata
property descriptor in EmbedNode to return a fresh object per instance (e.g.,
use get default() { return {}; } or a factory) so that the constructor code in
generate-decorator-node.ts (which sets this.__metadata = dataset['metadata'] ||
prop.default) gets a new object for each node instead of a shared
reference—mirror the pattern used by visibility (get default() { return
buildDefaultVisibility(); }) to fix the leak.

In `@packages/kg-default-nodes/src/nodes/file/FileNode.ts`:
- Around line 26-31: The sanitization only treats data: URIs as transient;
update the logic that computes isBlob (in FileNode.ts, where isBlob is derived
from src) to also detect blob: URLs (e.g., src.startsWith('data:') ||
src.startsWith('blob:')) so the returned object for the file node (type: 'file',
version: 1, src: ...) masks both data and blob URLs (still using the placeholder
'<base64String>' or your chosen placeholder) before export.

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 65-77: The constructor and setLabels are storing the incoming
labels array by reference (assigning to this.__labels), which allows external
mutation to affect node state; update both the SignupNode constructor and the
setLabels method to clone the incoming labels array (e.g., use a shallow copy
like [...labels] or labels.slice()) before assigning to this.__labels so the
node keeps its own copy and is immune to external mutations.

---

Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 24-40: The code currently creates an empty HtmlNode even when the
closing marker is missing; change the logic in html-parser.ts to bail out when
hasEndComment is false by returning null (or the parser's expected "no node"
value) instead of constructing a payload/HtmlNode. Concretely, use the existing
hasEndComment flag (and the isHtmlEndComment logic) to early-return null before
building the html array/payload and instantiating HtmlNode so malformed/missing
closing `kg-card-end: html` markers are not turned into synthetic empty cards.

---

Nitpick comments:
In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts`:
- Around line 116-118: The factory function $createBookmarkNode currently
requires a BookmarkData parameter even though BookmarkNode's constructor accepts
empty input; change $createBookmarkNode to make its dataset parameter optional
and default it to {} so callers can invoke $createBookmarkNode() without
boilerplate, and ensure the function still forwards the (possibly empty) object
into new BookmarkNode(dataset) to preserve behavior.
- Around line 82-89: The importJSON method currently casts the incoming
serializedNode to BookmarkData blindly; change importJSON to safely extract and
normalize fields (url, metadata, caption) from serializedNode by checking types
and providing defaults — e.g., ensure url and caption are strings (or empty
string/default), ensure metadata is an object (or null/{}), and coerce or ignore
invalid values — then pass these sanitized values into the constructor call (the
new this({...}) invocation) instead of using the unchecked as BookmarkData cast.

In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts`:
- Around line 26-27: The isEmpty() method currently checks __url and __html raw
values which treats whitespace-only strings as non-empty; update
EmbedNode.isEmpty() to trim both this.__url and this.__html (e.g., use
String.prototype.trim on each) before evaluating emptiness so whitespace-only
values are treated as empty, and ensure you reference the __url and __html
properties in the check.

In `@packages/kg-default-nodes/src/nodes/header/HeaderNode.ts`:
- Around line 16-41: The header node currently uses literal version numbers in
headerProperties (property 'version' default: 1) and in the HeaderNode class
defaultRenderFn mapping (keys 1 and 2), which can drift; introduce shared
constants (e.g., HEADER_RENDER_V1, HEADER_RENDER_V2 or a HeaderRenderVersion
enum) and replace the numeric literals used in headerProperties and the
defaultRenderFn keys, then update the mapping to reference those constants
alongside renderHeaderNodeV1 and renderHeaderNodeV2 so the version key is
defined in one place and reused.

In `@packages/kg-default-nodes/src/nodes/image/ImageNode.ts`:
- Around line 26-43: The exportJSON method duplicates all image fields instead
of deriving them from the base class, risking drift; modify ImageNode.exportJSON
to call super.exportJSON() to get the canonical dataset (so it inherits any
future imageProperties changes), then detect if this.src startsWith('data:') and
replace dataset.src with '<base64String>' when needed, otherwise leave
dataset.src as-is, and finally return that dataset (preserving all other
properties from super.exportJSON()).

In `@packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts`:
- Around line 19-21: Make the dataset parameter optional on the factory: change
$createPaywallNode to accept dataset?: PaywallData and call new
PaywallNode(dataset ?? {}) (or otherwise allow undefined) so callers can omit
the argument for this empty-schema node; update the function signature and the
single instantiation (new PaywallNode(...)) accordingly, referencing
$createPaywallNode and PaywallNode.

In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Line 1061: The test currently uses a non-null assertion when reading a regex
match result (e.g., the sizes variable from a .match call) which can throw
before a clear failure message; change it to first assert the match exists
(e.g., assert.exists/sizes.should.exist or
expect(sizes).to.not.be.null/undefined) and only then assert on its length
(e.g., have.lengthOf/length.should.equal(2)), updating both the use of sizes and
the other similar match assertion so failures show a clear assertion message
instead of a runtime error.
- Around line 638-639: Extract the repeated cast-and-outerHTML assertion into a
small helper (e.g., assertOuterHTML or expectOuterHTML) inside the test file and
replace every occurrence of "(element as
HTMLElement).outerHTML.should.equal(...)" with a call to that helper; the helper
should accept the Element and expected string and perform the cast and
.outerHTML.should.equal check so you only change the assertion calls in tests
(update all listed occurrences and any similar lines such as at 645-646,
652-653, 693-694, 720-721, 759-760, 794-795, 883-884, 933-934, 967-968, 994-995,
1011-1012, 1028-1029, 1058-1059, 1080-1081, 1096-1097, 1125-1126, 1167-1168,
1202-1203).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3cf6a4b6-5aaf-43ad-8abd-c7a05c01336b

📥 Commits

Reviewing files that changed from the base of the PR and between f7a112c and a8b9821.

📒 Files selected for processing (31)
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
  • packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/email/EmailNode.ts
  • packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
  • packages/kg-default-nodes/src/nodes/file/FileNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
  • packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
  • packages/kg-default-nodes/src/nodes/html/html-parser.ts
  • packages/kg-default-nodes/src/nodes/image/ImageNode.ts
  • packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
  • packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/signup/SignupNode.ts
  • packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
  • packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
  • packages/kg-default-nodes/src/nodes/video/VideoNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/gallery.test.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/kg-default-nodes/test/nodes/signup.test.ts
  • packages/kg-default-nodes/src/nodes/product/ProductNode.ts
  • packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
  • packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
  • packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
  • packages/kg-default-nodes/test/nodes/html.test.ts
  • packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
  • packages/kg-default-nodes/test/nodes/video.test.ts
  • packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
  • packages/kg-default-nodes/test/nodes/transistor.test.ts
  • packages/kg-default-nodes/src/generate-decorator-node.ts
  • packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
  • packages/kg-default-nodes/src/nodes/video/video-renderer.ts

Comment on lines +29 to +33
constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {}, key?: string) {
super({}, key);
this.__calloutText = calloutText || '';
this.__calloutEmoji = calloutEmoji !== undefined ? calloutEmoji : '💡';
this.__backgroundColor = backgroundColor || 'blue';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether null can reach calloutEmoji from parser/fixtures/serialization paths
rg -nP --type=ts -C3 'calloutEmoji\s*:\s*null|calloutEmoji\s*==\s*null|calloutEmoji\s*===\s*null'
rg -nP --type=ts -C3 'getAttribute\([^)]*callout-emoji|dataset\.[A-Za-z_]*calloutEmoji|calloutEmoji'

Repository: TryGhost/Koenig

Length of output: 20893


Use nullish coalescing for calloutEmoji default.

The current !== undefined check allows null to pass through at runtime. While the TypeScript interface and all parsers ensure calloutEmoji is always a string (never null), using nullish coalescing is safer and more idiomatic:

Proposed fix
-        this.__calloutEmoji = calloutEmoji !== undefined ? calloutEmoji : '💡';
+        this.__calloutEmoji = calloutEmoji ?? '💡';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts` around lines 29 -
33, The constructor in CalloutNode uses a strict !== undefined check for
calloutEmoji which lets null slip through; update the assignment in the
constructor (the CalloutNode constructor that sets this.__calloutEmoji) to use
nullish coalescing (?? '💡') so null and undefined both fall back to the default
emoji while preserving explicit empty-string values.

{name: 'url', default: '', urlType: 'url'},
{name: 'embedType', default: ''},
{name: 'html', default: ''},
{name: 'metadata', default: {} as Record<string, unknown>},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

TARGET="packages/kg-default-nodes/src/generate-decorator-node.ts"

echo "Inspecting constructor/default handling in $TARGET"
nl -ba "$TARGET" | sed -n '1,260p'

echo
echo "Searching for property default assignment patterns"
rg -n --type=ts -C3 '\bconstructor\b|\bdefault\b|internalProps|dataset|privateName|__' "$TARGET"

echo
echo "Embed metadata default declaration"
nl -ba packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts | sed -n '5,15p'

Repository: TryGhost/Koenig

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Inspecting generate-decorator-node.ts (first 300 lines) ==="
if [ -f "packages/kg-default-nodes/src/generate-decorator-node.ts" ]; then
  cat -n "packages/kg-default-nodes/src/generate-decorator-node.ts" | head -300
else
  echo "File not found"
fi

echo ""
echo "=== Searching for default assignment and cloning patterns ==="
rg -n --type=ts 'default|clone|spread|\.\.\.|assign|Object\.assign' packages/kg-default-nodes/src/generate-decorator-node.ts -A 2 -B 1

echo ""
echo "=== Full EmbedNode.ts for context ==="
if [ -f "packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts" ]; then
  cat -n "packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts"
else
  echo "File not found"
fi

Repository: TryGhost/Koenig

Length of output: 21180


Mutable default object in metadata is shared across node instances.

Line 9 uses a mutable object literal as default. The constructor (lines 152–156 of generate-decorator-node.ts) assigns defaults by reference without cloning: this.__metadata = dataset['metadata'] || prop.default. When a new EmbedNode is created without metadata data, all instances will reference the same {} object, causing mutations in one node to leak to others.

The codebase already demonstrates the correct pattern: the visibility property (lines 134–142) uses a getter (get default() { return buildDefaultVisibility(); }) to avoid this issue. Apply the same pattern to metadata:

{name: 'metadata', get default() { return {}; }, urlType?: undefined}

Or provide a factory function to create fresh objects per instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts` at line 9, The
metadata property currently uses a shared mutable default ({}) causing all
EmbedNode instances to share the same object; update the metadata property
descriptor in EmbedNode to return a fresh object per instance (e.g., use get
default() { return {}; } or a factory) so that the constructor code in
generate-decorator-node.ts (which sets this.__metadata = dataset['metadata'] ||
prop.default) gets a new object for each node instead of a shared
reference—mirror the pattern used by visibility (get default() { return
buildDefaultVisibility(); }) to fix the leak.

Comment on lines +26 to +31
const isBlob = src && src.startsWith('data:');

return {
type: 'file' as const,
version: 1,
src: isBlob ? '<base64String>' : src,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle blob: URLs in src sanitization too.

Line 26 only checks data: URIs. blob: URLs are also transient and should be masked before JSON export to avoid persisting non-portable values.

Suggested patch
-        const isBlob = src && src.startsWith('data:');
+        const isBlob = src.startsWith('data:') || src.startsWith('blob:');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isBlob = src && src.startsWith('data:');
return {
type: 'file' as const,
version: 1,
src: isBlob ? '<base64String>' : src,
const isBlob = src && (src.startsWith('data:') || src.startsWith('blob:'));
return {
type: 'file' as const,
version: 1,
src: isBlob ? '<base64String>' : src,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/file/FileNode.ts` around lines 26 - 31,
The sanitization only treats data: URIs as transient; update the logic that
computes isBlob (in FileNode.ts, where isBlob is derived from src) to also
detect blob: URLs (e.g., src.startsWith('data:') || src.startsWith('blob:')) so
the returned object for the file node (type: 'file', version: 1, src: ...) masks
both data and blob URLs (still using the placeholder '<base64String>' or your
chosen placeholder) before export.

Comment on lines +65 to +77
constructor({alignment, backgroundColor, backgroundImageSrc, backgroundSize, textColor, buttonColor, buttonTextColor, buttonText, disclaimer, header, labels, layout, subheader, successMessage, swapped} : SignupData = {}, key?: string) {
super({}, key);
this.__alignment = alignment || 'left';
this.__backgroundColor = backgroundColor || '#F0F0F0';
this.__backgroundImageSrc = backgroundImageSrc || '';
this.__backgroundSize = backgroundSize || 'cover';
this.__textColor = (backgroundColor === 'transparent' && (layout === 'split' || !backgroundImageSrc)) ? '' : textColor || '#000000'; // text color should inherit with a transparent bg color unless we're using an image for the background (which supercedes the bg color)
this.__buttonColor = buttonColor || 'accent';
this.__buttonTextColor = buttonTextColor || '#FFFFFF';
this.__buttonText = buttonText || 'Subscribe';
this.__disclaimer = disclaimer || '';
this.__header = header || '';
this.__labels = labels || [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clone incoming labels arrays to prevent external mutation leaks.

labels is stored by reference in both the constructor and setLabels. If the caller mutates the original array later, node state can change unintentionally outside expected update flow.

Suggested fix
 constructor({alignment, backgroundColor, backgroundImageSrc, backgroundSize, textColor, buttonColor, buttonTextColor, buttonText, disclaimer, header, labels, layout, subheader, successMessage, swapped} : SignupData = {}, key?: string) {
     super({}, key);
@@
-    this.__labels = labels || [];
+    this.__labels = Array.isArray(labels) ? [...labels] : [];
@@
 setLabels(labels: string[]) {
@@
     const writable = this.getWritable();
-    writable.__labels = labels;
+    writable.__labels = [...labels];
 }

Also applies to: 90-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts` around lines 65 -
77, The constructor and setLabels are storing the incoming labels array by
reference (assigning to this.__labels), which allows external mutation to affect
node state; update both the SignupNode constructor and the setLabels method to
clone the incoming labels array (e.g., use a shallow copy like [...labels] or
labels.slice()) before assigning to this.__labels so the node keeps its own copy
and is immune to external mutations.

no issue

This groups the verified fixes for serializer contracts and defensive parser and renderer handling so malformed or partial node data no longer produces broken output or DOM errors.
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts (1)

58-61: ⚠️ Potential issue | 🟡 Minor

Restore default placeholder when text becomes empty.

Line 59 always resolves to this.__placeholder ?? '', so after text is cleared with no custom placeholder, the default placeholder is never restored. updateDOM should mirror createDOM’s empty-text behavior.

💡 Suggested fix
     updateDOM(prevNode: AtLinkSearchNode, dom: HTMLElement, config: EditorConfig) {
-        dom.dataset.placeholder = this.__placeholder ?? '';
+        dom.dataset.placeholder = this.__text
+            ? (this.__placeholder ?? '')
+            : (this.__placeholder ?? this.defaultPlaceholder);
 
         return super.updateDOM(prevNode, dom, config);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts` around lines
58 - 61, updateDOM currently always sets dom.dataset.placeholder =
this.__placeholder ?? '' so the default placeholder is never restored when text
is cleared; change updateDOM to mirror createDOM's empty-text behavior by
computing the placeholder based on whether the node's text is empty, e.g. if the
node is empty use this.__placeholder if present otherwise use the same default
placeholder used in createDOM (refer to AtLinkSearchNode.createDOM and the
default placeholder constant or getter), and if not empty clear the placeholder;
update the dom.dataset.placeholder assignment in updateDOM accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts`:
- Around line 58-61: updateDOM currently always sets dom.dataset.placeholder =
this.__placeholder ?? '' so the default placeholder is never restored when text
is cleared; change updateDOM to mirror createDOM's empty-text behavior by
computing the placeholder based on whether the node's text is empty, e.g. if the
node is empty use this.__placeholder if present otherwise use the same default
placeholder used in createDOM (refer to AtLinkSearchNode.createDOM and the
default placeholder constant or getter), and if not empty clear the placeholder;
update the dom.dataset.placeholder assignment in updateDOM accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1741f484-8b16-4db0-9335-fcaf14e5426d

📥 Commits

Reviewing files that changed from the base of the PR and between a8b9821 and ac30eb2.

📒 Files selected for processing (11)
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
  • packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
  • packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/test/nodes/at-link-search.test.ts
  • packages/kg-default-nodes/test/nodes/at-link.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
  • packages/kg-default-nodes/test/nodes/embed.test.ts
  • packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
  • packages/kg-default-nodes/test/nodes/bookmark.test.ts
  • packages/kg-default-nodes/test/nodes/audio.test.ts
  • packages/kg-default-nodes/src/nodes/embed/types/twitter.ts

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.

2 participants