feat(canvas): add MarkdownEmbedNode with FBS schema and .md file drops#627
feat(canvas): add MarkdownEmbedNode with FBS schema and .md file drops#627softmarshmallow merged 8 commits intocanaryfrom
Conversation
Replace the standalone 877-line markdown→Skia renderer (painter/markdown.rs) with the shared htmlcss pipeline: markdown is converted to HTML via pulldown-cmark, wrapped with grida-markdown.css, and rendered through Stylo CSS → Taffy layout → Skia paint — the same path as HTMLEmbedNode. Key changes: - Add `markdown_to_styled_html()` that converts GFM markdown to a self-contained HTML document with embedded CSS - Add `grida-markdown.css` fixture (element-selector-only stylesheet targeting pulldown-cmark's bare HTML output) - Add faux-table layout module: emulates CSS table via Taffy flex (table→flex-column, tr→flex-row, td→equal-width flex children) - Auto-height on resize: MarkdownEmbed and HTMLEmbed nodes re-measure content height when width changes in grida-dev - Measure content height at load time instead of using 100,000px - Rename MarkdownNodeRec → MarkdownEmbedNodeRec to align with HTMLEmbedNodeRec naming pattern - Delete painter/markdown.rs (877 lines removed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(canvas): unify MarkdownEmbed to use htmlcss render pipeline
… and .md file drops Introduces a first-class MarkdownEmbed node type that renders GFM markdown directly on the canvas via the existing Skia-backed markdown painter. Prior to this, the AI tool make_from_markdown fell back to creating a plain TextSpanNode, so styled markdown never actually made it to the renderer. - format/grida.fbs: add MarkdownEmbedNode + MarkdownEmbedNodeProperties tables, append to Node union and NodeType enum - packages/grida-canvas-schema: add MarkdownEmbedNode interface and wire into LayerNode / ComputedNode / NodePrototype unions - crates/grida-canvas: JSON bridge + FBS decoder for MarkdownEmbedNode - packages/grida-canvas-io: TS FBS encoder for markdown_embed - editor: createMarkdownEmbedNode command, initial-node factory case, DOM renderer background fill mapping - ai/tools/canvas-use: make_from_markdown now creates MarkdownEmbedNode - data-transfer: insertMarkdown handler + .md file drop/paste support (text/markdown MIME type added to ValidFileType) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces comprehensive support for a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a4d733311
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case "markdown_embed": { | ||
| const mdNode = node as grida.program.nodes.MarkdownEmbedNode; |
There was a problem hiding this comment.
Add MarkdownEmbed handling in TS FlatBuffers decoder
This new encoder path writes fbs.Node.MarkdownEmbedNode, but the TS FBS document decoder still has no case fbs.Node.MarkdownEmbedNode and falls back to its default group reconstruction. In any flow that round-trips through @grida/io decode (save/import/reopen), markdown embeds will be deserialized as groups and lose markdown-specific data (markdown, fill paints, corner radius), so this change currently introduces data loss for the new node type.
Useful? React with 👍 / 👎.
| } else if ( | ||
| type === "text/markdown" || | ||
| type === "md" || | ||
| type === "markdown" | ||
| ) { |
There was a problem hiding this comment.
Validate .md extension even when MIME is non-markdown
The new markdown detection checks type === "md" | "markdown", but type is assigned from file.type || extension, so those extension checks are skipped whenever the browser provides any MIME type. When .md files come through as text/plain (a common browser/OS behavior), they are rejected as unsupported and markdown file drop/paste silently fails in those environments.
Useful? React with 👍 / 👎.
create_packed_scene_document_from_prototype threw "Unsupported node prototype type: markdown_embed" when the AI tool or .md file drop tried to insert a markdown embed node. Adds the case alongside the other simple scalar prototypes (iframe, richtext, tspan, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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 (4)
packages/grida-canvas-io/index.ts (2)
332-340:⚠️ Potential issue | 🟠 Major
text/markdownis declared, but async clipboard decode never emits it.
DecodedItemnow includes"text/markdown", butdecodeFromClipboardItems()only checks SVG/image MIME types andtext/plain. Because there is notext/markdownprobe there, markdown clipboard items on the async Clipboard API path are skipped.💡 Suggested fix
@@ - // Check for text/plain (may be SVG text or plain text) + // Check for text/markdown file-like payload + if (types.includes("text/markdown")) { + try { + const blob = await clipboardItem.getType("text/markdown"); + const file = new File([blob], "clipboard.md", { + type: "text/markdown", + }); + decodedItems.push({ type: "text/markdown", file }); + continue; + } catch {} + } + + // Check for text/plain (may be SVG text or plain text) if (types.includes("text/plain")) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/grida-canvas-io/index.ts` around lines 332 - 340, The DecodedItem union includes "text/markdown" but decodeFromClipboardItems() only inspects image/svg+xml, image/png/jpeg/gif/webp and text/plain, so markdown clipboard items are never emitted; update decodeFromClipboardItems() to detect and handle "text/markdown" by probing clipboardItem types for "text/markdown" (or falling back to text/plain when markdown is expected), read the markdown payload (via getType/getAsString or the async clipboard API) and emit a DecodedItem with type "text/markdown" and the markdown string as the value so the declared DecodedItem variants and runtime decoding stay consistent.
324-330:⚠️ Potential issue | 🟠 MajorAdd round-trip test coverage for MarkdownEmbed union member.
The schema includes
MarkdownEmbedNodebut neither the Rust test suite (crates/grida-canvas/tests/fbs_roundtrip.rs) nor the TypeScript test suite (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) has a corresponding round-trip test. Add agen_markdown_embedfunction in Rust and a markdown embed test case in TypeScript following the same pattern as existing embed node tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/grida-canvas-io/index.ts` around lines 324 - 330, Add round-trip test coverage for the MarkdownEmbed union member by implementing a Rust generator function named gen_markdown_embed in the Rust test suite (used by crates/grida-canvas/tests/fbs_roundtrip.rs) that creates a MarkdownEmbedNode instance consistent with other embed generators, and add a corresponding test case in the TypeScript suite (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) that creates a markdown embed node (matching the MarkdownEmbedNode shape) and asserts successful round-trip using the existing test helpers; ensure field names and types match the schema and include the markdown content and any file/type fields so the node mirrors other embed tests.editor/grida-canvas-react/use-data-transfer.ts (1)
426-445:⚠️ Potential issue | 🟠 Major
text/plaincan preempt markdown embeds during paste.In mixed clipboard payloads, a
"text"item may be encountered before"text/markdown", causing plain text insertion and early return. This can bypass the new markdown-embed flow.Proposed fix
- for (const item of items) { + const prioritizedItems = [ + ...items.filter((item) => item.type !== "text"), + ...items.filter((item) => item.type === "text"), + ]; + for (const item of prioritizedItems) { try { switch (item.type) { case "svg-text": { insertSVG("SVG", item.svg, position); return true; } - case "text": { - insertText(item.text, position); - return true; - } case "image/gif": case "image/jpeg": case "image/png": case "image/svg+xml": case "image/webp": case "text/markdown": { insertFromFile(item.type, item.file, position); return true; } + case "text": { + insertText(item.text, position); + return true; + } } } catch {} }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@editor/grida-canvas-react/use-data-transfer.ts` around lines 426 - 445, The paste loop in use-data-transfer.ts currently handles a "text" item immediately which can preempt a later "text/markdown" item; update the switch in the for (const item of items) loop so that when encountering item.type === "text" you first check whether items contains any "text/markdown" (e.g., items.some(i => i.type === "text/markdown")), and if so skip handling the plain text (continue) so the markdown branch (which calls insertFromFile for "text/markdown") gets priority; keep existing handlers insertText, insertFromFile, and insertSVG unchanged and still return true after successful insertion.packages/grida-canvas-io/format.ts (1)
393-413:⚠️ Potential issue | 🟠 MajorAdd round-trip test coverage for
markdown_embedunion member.The codec mappings are correct, but
MarkdownEmbedis missing from both the TypeScript base objects (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) and Rust test generators (crates/grida-canvas/tests/fbs_roundtrip.rs). Add test cases for the new union member in both files to prevent silent codec drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/grida-canvas-io/format.ts` around lines 393 - 413, Add round-trip test coverage for the new MarkdownEmbed union member: update the TypeScript base objects used by the format roundtrip test (refer to the test file symbol format-roundtrip.test.ts and the base object fixtures used there) to include a node with type "markdown_embed" (matching the NODE_TYPE_DECODE/NODE_TYPE_ENCODE mapping) and update the Rust FBS roundtrip generator tests (refer to crates/grida-canvas tests/fbs_roundtrip.rs) to emit a corresponding fbs::MarkdownEmbed case so the serializer/deserializer path is exercised; ensure the test objects use the same identifying property names as the existing union members so the round-trip assertions will include markdown_embed.
🧹 Nitpick comments (4)
crates/grida-canvas/src/io/io_grida_fbs.rs (1)
2054-2088: Add explicit round-trip tests formarkdown_embed.The new decoder path should be covered by Rust round-trip tests in
crates/grida-canvas/tests/fbs_roundtrip.rsand TypeScript round-trip tests inpackages/grida-canvas-io/__tests__/format-roundtrip.test.tsto ensuremarkdown,fill_paints, andcorner_radiussurvive encode→decode→encode cycles.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/io/io_grida_fbs.rs` around lines 2054 - 2088, Add round-trip tests that exercise the new markdown_embed decoder: in Rust, add cases to crates/grida-canvas/tests/fbs_roundtrip.rs that construct an fbs MarkdownEmbed node with non-empty markdown text, non-empty fill_paints, and a corner_radius, encode it, decode via decode_markdown_embed_node (and related decode functions), then re-encode and assert markdown, fill_paints (Paints) and corner_radius remain equal; in TypeScript, add analogous tests to packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that build the same markdown_embed payload, run encode→decode→encode and assert the markdown string, fill_paints array and cornerRadius survive the round-trip. Ensure tests reference the MarkdownEmbedNodeRec shape and use the same helper encode/decode utilities used by other round-trip tests.crates/grida-canvas/src/io/io_grida.rs (1)
1669-1715: Consider adding a focused conversion test forJSONMarkdownEmbedNode.A small unit test asserting
markdown, fills, corner radius, and layout mapping would reduce regression risk for this new conversion path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/io/io_grida.rs` around lines 1669 - 1715, Add a focused unit test that constructs a JSONMarkdownEmbedNode with non-default values for markdown, base.fill and base.fill_paints, base.corner_radius and rectangular corner radii, and base.layout_positioning, then converts it via From<JSONMarkdownEmbedNode> (the impl for Node) and asserts the resulting Node is Node::MarkdownEmbed containing a MarkdownEmbedNodeRec whose markdown string, fills (check merge_paints result), corner_radius (check merge_corner_radius result), and layout_child.layout_positioning/layout_grow match the expected mapped values; target the conversion code paths referenced by JSONMarkdownEmbedNode, MarkdownEmbedNodeRec, merge_paints, merge_corner_radius, and LayoutChildStyle to catch regressions.editor/grida-canvas-react/use-data-transfer.ts (2)
177-177: Filename normalization for markdown nodes is fragile.
file.name.split(".md")[0]is extension- and case-sensitive and can produce unexpected names. Prefer extension-aware trimming with a case-insensitive suffix check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@editor/grida-canvas-react/use-data-transfer.ts` at line 177, The filename normalization using file.name.split(".md")[0] is brittle and case-sensitive; replace it with an extension-aware, case-insensitive trim (e.g., use a case-insensitive regex to strip a trailing ".md" or use path.parse(file.name).name) so the computed name variable correctly handles ".md", ".MD", and filenames containing ".md" in the middle; update the assignment to name (in use-data-transfer.ts where const name = file.name...) accordingly.
442-445: Add/refresh manual interaction tests for markdown paste/drop flows.This changes user interaction behavior in
grida-canvas; please add/update related manual test cases under the canvas test area for markdown paste and drag-drop.As per coding guidelines: When fixing UX bugs or changing interaction behavior in
grida-canvas*, check../test/for related manual test cases and add new ones when appropriate.Also applies to: 561-659
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@editor/grida-canvas-react/use-data-transfer.ts` around lines 442 - 445, The change adds handling for "text/markdown" by calling insertFromFile(item.type, item.file, position) during paste/drag-drop, so update the manual interaction tests for markdown paste and drag-drop in the canvas test suite: add/refresh tests that exercise pasting markdown clipboard content and dragging-dropping markdown files to verify insertFromFile is invoked and the markdown renders/positions correctly (cover both paste and drop flows referenced around the "text/markdown" branch and the surrounding useDataTransfer handler), and ensure any related cases between the other referenced region are updated to reflect the new UX behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/grida-canvas/src/io/id_converter.rs`:
- Line 172: Add round-trip coverage for the JSONNode::MarkdownEmbed ->
Node::from(JSONNode::MarkdownEmbed(...)) mapping by adding test cases that
construct a MarkdownEmbed union member and assert round-trip equality; update
the Rust round-trip test module (fbs_roundtrip.rs) to include a
JSONNode::MarkdownEmbed case in the same pattern as Image/BooleanOperation
tests, and add the corresponding case to the TypeScript format-roundtrip.test.ts
suite so the MarkdownEmbed payload is serialized and deserialized and compared
for equality.
In `@crates/grida-canvas/src/io/io_grida_fbs.rs`:
- Around line 378-384: The encode_node function is missing an arm to handle
fbs::Node::MarkdownEmbedNode so markdown-embed nodes get treated as UnknownNode
and lose their payload on re-save; add a matching encode arm for
fbs::Node::MarkdownEmbedNode that mirrors the decode path by calling the
corresponding encoder (the counterpart to decode_markdown_embed_node) and using
the same slot handling logic as other layer-node cases (see existing
decode_layer_node invocation and other encode arms) so markdown-specific fields
are preserved instead of falling back to UnknownNode.
In `@editor/grida-canvas-react-renderer-dom/nodes/node.tsx`:
- Line 221: The new mapping added for "markdown_embed" to behave like a fill is
incomplete because NodeElement's renderer switch lacks a "markdown_embed" branch
and thus throws "Unknown node type" at runtime; update the switch inside the
NodeElement renderer function (the switch handling node.type) to add a
"markdown_embed" case that delegates to the same rendering path used for other
background/fill nodes (or calls the existing background/fill render helper),
ensuring the node is rendered instead of falling through to the Unknown node
type error.
In `@format/grida.fbs`:
- Around line 1491-1508: The FlatBuffers addition of
MarkdownEmbedNode/MarkdownEmbedNodeProperties was not propagated to TypeScript
and tests: add a MarkdownEmbedNode type to packages/grida-canvas-schema/grida.ts
matching the schema (include properties with fill_paints, markdown,
corner_radius), implement encoder and decoder handling for the Node union
variant in packages/grida-canvas-io/format.ts (serialize/deserialize properties
using existing PaintStackItem and RectangularCornerRadiusTrait helpers and
register the union discriminant for MarkdownEmbedNode), bump the schema_version
constant where the FlatBuffers schema version is maintained to reflect the union
change, and add roundtrip tests: one FBS roundtrip in
crates/grida-canvas/tests/fbs_roundtrip.rs that exercises MarkdownEmbedNode and
one JS/TS roundtrip in
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that encodes and
decodes a MarkdownEmbedNode instance to ensure fidelity.
In `@packages/grida-canvas-io/format.ts`:
- Around line 1918-1971: The decode path for the encoded markdown_embed is
missing: update the fromFlatbuffer() switch to handle fbs.Node.MarkdownEmbedNode
(the MarkdownEmbedNode case) and reconstruct a
grida.program.nodes.MarkdownEmbedNode by reading properties added in format.ts
(fillPaints, markdown string, corner radius via
format.shape.decode.rectangularCornerRadiusTrait or equivalent decoder, and
system/layer traits). Ensure you map the flatbuffer fields back to the node
shape (markdown, fill paints, rectangular_corner_radius_* and corner_smoothing)
and return the correct node type instead of falling through to the default group
handling.
In `@packages/grida-canvas-io/index.ts`:
- Around line 309-315: The branch that detects markdown uses case-sensitive
checks on the local variable type, so uppercase extensions like ".MD" or "MD"
are missed; normalize type before comparisons (e.g., use a lowercased fallback
value and compare against "text/markdown", "md", and "markdown") by converting
type to lowercase once (e.g., const normalized = (type || "").toLowerCase()) and
use normalized in the existing conditional around the code that returns [true,
"text/markdown" as const].
In `@packages/grida-canvas-schema/grida.ts`:
- Around line 1390-1392: NodePrototype now supports markdown but
createNodeDataFromPrototypeWithoutChildren doesn’t handle it; add a
"markdown_embed" branch in the switch inside
createNodeDataFromPrototypeWithoutChildren to materialize MarkdownEmbedNode
prototypes into runtime node data. Implement the case to read the prototype
payload (the __TPrototypeNode / NodePrototype entry for MarkdownEmbedNode),
copy/merge relevant fields (id, type, metadata and the markdown-specific fields
from MarkdownEmbedNode), apply the same defaulting/omission logic used for other
node types, and return the constructed node object so prototype materialization
no longer throws.
---
Outside diff comments:
In `@editor/grida-canvas-react/use-data-transfer.ts`:
- Around line 426-445: The paste loop in use-data-transfer.ts currently handles
a "text" item immediately which can preempt a later "text/markdown" item; update
the switch in the for (const item of items) loop so that when encountering
item.type === "text" you first check whether items contains any "text/markdown"
(e.g., items.some(i => i.type === "text/markdown")), and if so skip handling the
plain text (continue) so the markdown branch (which calls insertFromFile for
"text/markdown") gets priority; keep existing handlers insertText,
insertFromFile, and insertSVG unchanged and still return true after successful
insertion.
In `@packages/grida-canvas-io/format.ts`:
- Around line 393-413: Add round-trip test coverage for the new MarkdownEmbed
union member: update the TypeScript base objects used by the format roundtrip
test (refer to the test file symbol format-roundtrip.test.ts and the base object
fixtures used there) to include a node with type "markdown_embed" (matching the
NODE_TYPE_DECODE/NODE_TYPE_ENCODE mapping) and update the Rust FBS roundtrip
generator tests (refer to crates/grida-canvas tests/fbs_roundtrip.rs) to emit a
corresponding fbs::MarkdownEmbed case so the serializer/deserializer path is
exercised; ensure the test objects use the same identifying property names as
the existing union members so the round-trip assertions will include
markdown_embed.
In `@packages/grida-canvas-io/index.ts`:
- Around line 332-340: The DecodedItem union includes "text/markdown" but
decodeFromClipboardItems() only inspects image/svg+xml, image/png/jpeg/gif/webp
and text/plain, so markdown clipboard items are never emitted; update
decodeFromClipboardItems() to detect and handle "text/markdown" by probing
clipboardItem types for "text/markdown" (or falling back to text/plain when
markdown is expected), read the markdown payload (via getType/getAsString or the
async clipboard API) and emit a DecodedItem with type "text/markdown" and the
markdown string as the value so the declared DecodedItem variants and runtime
decoding stay consistent.
- Around line 324-330: Add round-trip test coverage for the MarkdownEmbed union
member by implementing a Rust generator function named gen_markdown_embed in the
Rust test suite (used by crates/grida-canvas/tests/fbs_roundtrip.rs) that
creates a MarkdownEmbedNode instance consistent with other embed generators, and
add a corresponding test case in the TypeScript suite
(packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) that creates a
markdown embed node (matching the MarkdownEmbedNode shape) and asserts
successful round-trip using the existing test helpers; ensure field names and
types match the schema and include the markdown content and any file/type fields
so the node mirrors other embed tests.
---
Nitpick comments:
In `@crates/grida-canvas/src/io/io_grida_fbs.rs`:
- Around line 2054-2088: Add round-trip tests that exercise the new
markdown_embed decoder: in Rust, add cases to
crates/grida-canvas/tests/fbs_roundtrip.rs that construct an fbs MarkdownEmbed
node with non-empty markdown text, non-empty fill_paints, and a corner_radius,
encode it, decode via decode_markdown_embed_node (and related decode functions),
then re-encode and assert markdown, fill_paints (Paints) and corner_radius
remain equal; in TypeScript, add analogous tests to
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that build the same
markdown_embed payload, run encode→decode→encode and assert the markdown string,
fill_paints array and cornerRadius survive the round-trip. Ensure tests
reference the MarkdownEmbedNodeRec shape and use the same helper encode/decode
utilities used by other round-trip tests.
In `@crates/grida-canvas/src/io/io_grida.rs`:
- Around line 1669-1715: Add a focused unit test that constructs a
JSONMarkdownEmbedNode with non-default values for markdown, base.fill and
base.fill_paints, base.corner_radius and rectangular corner radii, and
base.layout_positioning, then converts it via From<JSONMarkdownEmbedNode> (the
impl for Node) and asserts the resulting Node is Node::MarkdownEmbed containing
a MarkdownEmbedNodeRec whose markdown string, fills (check merge_paints result),
corner_radius (check merge_corner_radius result), and
layout_child.layout_positioning/layout_grow match the expected mapped values;
target the conversion code paths referenced by JSONMarkdownEmbedNode,
MarkdownEmbedNodeRec, merge_paints, merge_corner_radius, and LayoutChildStyle to
catch regressions.
In `@editor/grida-canvas-react/use-data-transfer.ts`:
- Line 177: The filename normalization using file.name.split(".md")[0] is
brittle and case-sensitive; replace it with an extension-aware, case-insensitive
trim (e.g., use a case-insensitive regex to strip a trailing ".md" or use
path.parse(file.name).name) so the computed name variable correctly handles
".md", ".MD", and filenames containing ".md" in the middle; update the
assignment to name (in use-data-transfer.ts where const name = file.name...)
accordingly.
- Around line 442-445: The change adds handling for "text/markdown" by calling
insertFromFile(item.type, item.file, position) during paste/drag-drop, so update
the manual interaction tests for markdown paste and drag-drop in the canvas test
suite: add/refresh tests that exercise pasting markdown clipboard content and
dragging-dropping markdown files to verify insertFromFile is invoked and the
markdown renders/positions correctly (cover both paste and drop flows referenced
around the "text/markdown" branch and the surrounding useDataTransfer handler),
and ensure any related cases between the other referenced region are updated to
reflect the new UX behavior.
🪄 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: d920b64a-f906-4f71-88a9-412c93138933
⛔ Files ignored due to path filters (1)
crates/grida-canvas/src/io/generated/grida.rsis excluded by!**/generated/**
📒 Files selected for processing (13)
crates/grida-canvas/src/io/id_converter.rscrates/grida-canvas/src/io/io_grida.rscrates/grida-canvas/src/io/io_grida_fbs.rseditor/grida-canvas-hosted/ai/tools/canvas-use.tseditor/grida-canvas-react-renderer-dom/nodes/node.tsxeditor/grida-canvas-react/use-data-transfer.tseditor/grida-canvas/editor.i.tseditor/grida-canvas/editor.tseditor/grida-canvas/reducers/tools/initial-node.tsformat/grida.fbspackages/grida-canvas-io/format.tspackages/grida-canvas-io/index.tspackages/grida-canvas-schema/grida.ts
| JSONNode::TextSpan(text) => Node::TextSpan(TextSpanNodeRec::from(text)), | ||
| JSONNode::BooleanOperation(bool_op) => Node::from(JSONNode::BooleanOperation(bool_op)), | ||
| JSONNode::Image(image) => Node::from(JSONNode::Image(image)), | ||
| JSONNode::MarkdownEmbed(md) => Node::from(JSONNode::MarkdownEmbed(md)), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify MarkdownEmbed coverage exists in Rust and TS round-trip tests
rg -n --iglob '**/fbs_roundtrip.rs' --iglob '**/format-roundtrip.test.ts' 'MarkdownEmbed|markdown_embed'Repository: gridaco/grida
Length of output: 39
🏁 Script executed:
#!/bin/bash
# Check if MarkdownEmbed exists in the schema
find . -name "grida.fbs" -type fRepository: gridaco/grida
Length of output: 75
🏁 Script executed:
#!/bin/bash
# Look for round-trip test files
find . -name "fbs_roundtrip.rs" -o -name "format-roundtrip.test.ts" | head -20Repository: gridaco/grida
Length of output: 163
🏁 Script executed:
#!/bin/bash
# Search for MarkdownEmbed in the codebase to see where it's defined
rg -i "MarkdownEmbed" --type rust --type ts -B 2 -A 2 | head -100Repository: gridaco/grida
Length of output: 6170
🏁 Script executed:
#!/bin/bash
# Check the structure of the Rust round-trip test file
wc -l ./crates/grida-canvas/tests/fbs_roundtrip.rsRepository: gridaco/grida
Length of output: 106
🏁 Script executed:
#!/bin/bash
# Check the structure of the TS round-trip test file
wc -l ./packages/grida-canvas-io/__tests__/format-roundtrip.test.tsRepository: gridaco/grida
Length of output: 123
🏁 Script executed:
#!/bin/bash
# Look at the Rust round-trip test to understand coverage patterns
head -100 ./crates/grida-canvas/tests/fbs_roundtrip.rsRepository: gridaco/grida
Length of output: 3766
🏁 Script executed:
#!/bin/bash
# Check what union members ARE tested in the Rust round-trip test
rg -n "Node::" ./crates/grida-canvas/tests/fbs_roundtrip.rs | head -30Repository: gridaco/grida
Length of output: 1717
🏁 Script executed:
#!/bin/bash
# Check what node types ARE covered in TS round-trip test
rg -n "text_node|container_node|shape_node" ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -30Repository: gridaco/grida
Length of output: 39
🏁 Script executed:
#!/bin/bash
# Find the Node enum definition to see all union members
rg -A 50 "^pub enum Node" crates/grida-canvas/src/node/schema.rs | head -80Repository: gridaco/grida
Length of output: 2266
🏁 Script executed:
#!/bin/bash
# Look at the TS round-trip test file structure to understand what's being tested
head -200 ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | tail -100Repository: gridaco/grida
Length of output: 2098
🏁 Script executed:
#!/bin/bash
# Search for actual test cases in the TS file to see node coverage
rg "describe|it\(" ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -40Repository: gridaco/grida
Length of output: 2166
Add MarkdownEmbed coverage to round-trip tests.
The wiring at line 172 is correct, but MarkdownEmbed is missing from both the Rust (crates/grida-canvas/tests/fbs_roundtrip.rs) and TypeScript (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) round-trip test suites. Add test cases for this union member following the pattern of other node types (Image, BooleanOperation, etc.) already covered in those files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/grida-canvas/src/io/id_converter.rs` at line 172, Add round-trip
coverage for the JSONNode::MarkdownEmbed ->
Node::from(JSONNode::MarkdownEmbed(...)) mapping by adding test cases that
construct a MarkdownEmbed union member and assert round-trip equality; update
the Rust round-trip test module (fbs_roundtrip.rs) to include a
JSONNode::MarkdownEmbed case in the same pattern as Image/BooleanOperation
tests, and add the corresponding case to the TypeScript format-roundtrip.test.ts
suite so the MarkdownEmbed payload is serialized and deserialized and compared
for equality.
| fbs::Node::MarkdownEmbedNode => { | ||
| decode_layer_node!( | ||
| slot, | ||
| node_as_markdown_embed_node, | ||
| decode_markdown_embed_node | ||
| ); | ||
| } |
There was a problem hiding this comment.
Missing Node::MarkdownEmbed encode arm causes lossy saves.
Line 378 adds decode support, but encode_node still falls through to the UnknownNode fallback (Line 2538) for markdown embeds. That will drop markdown-specific payload on re-encode.
💡 Minimal fix direction
fn encode_node<'a, A: flatbuffers::Allocator + 'a>(
fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>,
node: &Node,
node_id: &str,
parent_id: &str,
position: &str,
) -> flatbuffers::WIPOffset<fbs::NodeSlot<'a>> {
match node {
+ Node::MarkdownEmbed(r) => {
+ encode_markdown_embed_node(fbb, r, node_id, parent_id, position)
+ }
Node::Container(r) => encode_container_node(fbb, r, node_id, parent_id, position),
// ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/grida-canvas/src/io/io_grida_fbs.rs` around lines 378 - 384, The
encode_node function is missing an arm to handle fbs::Node::MarkdownEmbedNode so
markdown-embed nodes get treated as UnknownNode and lose their payload on
re-save; add a matching encode arm for fbs::Node::MarkdownEmbedNode that mirrors
the decode path by calling the corresponding encoder (the counterpart to
decode_markdown_embed_node) and using the same slot handling logic as other
layer-node cases (see existing decode_layer_node invocation and other encode
arms) so markdown-specific fields are preserved instead of falling back to
UnknownNode.
| component: "background", | ||
| iframe: "background", | ||
| richtext: "color", | ||
| markdown_embed: "background", |
There was a problem hiding this comment.
markdown_embed is mapped for fill, but it is still unrenderable in NodeElement.
Line 221 adds fill behavior, but NodeElement still has no "markdown_embed" branch in the renderer switch, so it will hit Unknown node type at runtime.
Proposed fix
- case "richtext": {
+ case "richtext":
+ case "markdown_embed": {
return ReactNodeRenderers[node.type];
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@editor/grida-canvas-react-renderer-dom/nodes/node.tsx` at line 221, The new
mapping added for "markdown_embed" to behave like a fill is incomplete because
NodeElement's renderer switch lacks a "markdown_embed" branch and thus throws
"Unknown node type" at runtime; update the switch inside the NodeElement
renderer function (the switch handling node.type) to add a "markdown_embed" case
that delegates to the same rendering path used for other background/fill nodes
(or calls the existing background/fill render helper), ensuring the node is
rendered instead of falling through to the Unknown node type error.
| table MarkdownEmbedNodeProperties { | ||
| /// Background fill paints for the markdown container. | ||
| fill_paints:[PaintStackItem]; | ||
| /// The GitHub Flavored Markdown source text. | ||
| markdown:string; | ||
| /// Per-corner rectangular corner radius. | ||
| corner_radius:RectangularCornerRadiusTrait; | ||
| } | ||
|
|
||
| /// Node variant: Markdown embed (render-only). | ||
| /// | ||
| /// Stores GFM markdown source and renders it directly to a Skia Picture | ||
| /// at paint time. Not structurally editable — treated as a single text block. | ||
| table MarkdownEmbedNode { | ||
| node:SystemNodeTrait (required); | ||
| layer:LayerTrait (required); | ||
| properties:MarkdownEmbedNodeProperties; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Schema version definitions =="
rg -n "SCHEMA_VERSION|schema_version" --glob "*grida.ts" --glob "*document*" --glob "*format*"
echo
echo "== MarkdownEmbed round-trip coverage =="
rg -n "MarkdownEmbed|markdown_embed" --glob "*fbs_roundtrip.rs" --glob "*format-roundtrip.test.ts"Repository: gridaco/grida
Length of output: 762
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Finding the union definition for MarkdownEmbedNode =="
rg -n "union.*Node|MarkdownEmbed" format/grida.fbs | head -20
echo
echo "== Checking CanvasDocument.schema_version in grida.fbs =="
rg -n "schema_version" format/grida.fbs -A 2 -B 2
echo
echo "== Reading roundtrip test files for MarkdownEmbed coverage =="
echo "--- Rust roundtrip test ---"
rg -n "MarkdownEmbed\|markdown_embed\|test.*node" crates/grida-canvas/tests/fbs_roundtrip.rs | head -20
echo
echo "--- TS roundtrip test ---"
rg -n "MarkdownEmbed\|markdown_embed\|it\(" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -30Repository: gridaco/grida
Length of output: 1027
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Reading grida.fbs to find actual schema_version value =="
sed -n '1620,1650p' format/grida.fbs
echo
echo "== Looking for schema_version assignment in grida.fbs =="
rg -n "schema_version.*=|\"0\.91|version" format/grida.fbs -B 2 -A 2 | head -30
echo
echo "== Checking for other node roundtrip tests (Rust) =="
rg -n "fn test_|assert.*Node|SceneNode\|ContainerNode" crates/grida-canvas/tests/fbs_roundtrip.rs | head -15
echo
echo "== Checking for other node roundtrip tests (TS) =="
rg -n "describe\(|it\(|MarkdownEmbed|Node" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -20Repository: gridaco/grida
Length of output: 2062
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== List all files in roundtrip test directories =="
find crates/grida-canvas/tests -name "*roundtrip*" -o -name "*test*" | head -20
echo
find packages/grida-canvas-io/__tests__ -name "*roundtrip*" | head -20
echo
echo "== Check content and structure of Rust roundtrip test =="
wc -l crates/grida-canvas/tests/fbs_roundtrip.rs
head -100 crates/grida-canvas/tests/fbs_roundtrip.rs
echo
echo "== Check for node test patterns in Rust =="
rg -n "Node\|node" crates/grida-canvas/tests/fbs_roundtrip.rs | head -20
echo
echo "== Check TS roundtrip test structure =="
wc -l packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
head -80 packages/grida-canvas-io/__tests__/format-roundtrip.test.tsRepository: gridaco/grida
Length of output: 4135
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for actual schema_version value assignments =="
rg -n "schema_version\s*=" format/grida.fbs
echo
echo "== Get line 1623 context from grida.fbs to find value =="
sed -n '1615,1635p' format/grida.fbs
echo
echo "== Search complete TS test file for MarkdownEmbed =="
rg "MarkdownEmbed" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
echo
echo "== List all test/describe/it functions in TS roundtrip test =="
rg "describe\(|it\(" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -30
echo
echo "== List all test functions in Rust roundtrip test =="
rg "fn test_|fn gen_" crates/grida-canvas/tests/fbs_roundtrip.rs | head -30
echo
echo "== Search for markdown in Rust test (any variant) =="
rg -i "markdown" crates/grida-canvas/tests/fbs_roundtrip.rsRepository: gridaco/grida
Length of output: 113
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check where schema_version is actually set at encode time =="
rg -n "schema_version\|SCHEMA_VERSION" packages/grida-canvas-io/format.ts | head -20
echo
echo "== Get TS SCHEMA_VERSION value =="
sed -n '530,540p' packages/grida-canvas-schema/grida.ts
echo
echo "== List all gen_* or test_* functions in Rust roundtrip =="
rg "^fn (gen_|test_)" crates/grida-canvas/tests/fbs_roundtrip.rs
echo
echo "== Check if there's a recent change history in grida.ts SCHEMA_VERSION =="
git log --oneline -n 20 --all -- packages/grida-canvas-schema/grida.ts 2>/dev/null || echo "(git not available)"
echo
echo "== Check git diff to see if schema_version was changed in this PR =="
git diff HEAD~1 format/grida.fbs | grep -A 5 -B 5 "schema_version\|MarkdownEmbed" || echo "(no changes in schema_version area)"Repository: gridaco/grida
Length of output: 120
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Read TS SCHEMA_VERSION directly =="
cat packages/grida-canvas-schema/grida.ts | grep -n "SCHEMA_VERSION" | head -5
echo
echo "== Check format.ts for schema_version handling =="
cat packages/grida-canvas-io/format.ts | grep -n "schema_version\|SCHEMA_VERSION" | head -10
echo
echo "== List all test function names in fbs_roundtrip.rs =="
cat crates/grida-canvas/tests/fbs_roundtrip.rs | grep -n "^fn " | head -40
echo
echo "== Check for MarkdownEmbed/markdown tests explicitly =="
cat crates/grida-canvas/tests/fbs_roundtrip.rs | grep -in "markdown"
echo
echo "== Check TS roundtrip test file exists and list tests =="
cat packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | grep -n "describe\|it(" | head -30
echo
echo "== Search for MarkdownEmbed in TS test =="
cat packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | grep -in "markdown"Repository: gridaco/grida
Length of output: 2167
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check complete TS test file for any markdown pattern =="
wc -l packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
tail -100 packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
echo
echo "== Verify MarkdownEmbedNode is in the Node union =="
sed -n '1590,1610p' format/grida.fbs
echo
echo "== Check if there's a default value for schema_version in FBS =="
sed -n '1621,1626p' format/grida.fbs
echo
echo "== List how encode sets schema_version =="
rg -n "encode.*schema\|createCanvasDocument" packages/grida-canvas-io/format.ts -B 3 -A 5 | head -40Repository: gridaco/grida
Length of output: 3725
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for where schema_version is written during encode =="
rg -n "schema_version.*=" packages/grida-canvas-io/format.ts -B 2 -A 2 | head -30
echo
echo "== Check encoder implementation for CanvasDocument creation =="
rg -n "CanvasDocument\|builder.*schema" packages/grida-canvas-io/format.ts -B 1 -A 3 | head -40
echo
echo "== Verify no MarkdownEmbed test exists anywhere in roundtrip =="
echo "Rust test:"
grep -c "MarkdownEmbed\|markdown_embed\|gen_markdown" crates/grida-canvas/tests/fbs_roundtrip.rs || echo "0 matches"
echo
echo "TS test:"
grep -c "MarkdownEmbed\|markdown_embed\|markdown" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts || echo "0 matches"
echo
echo "== Check if this is the initial commit or if schema needs bump =="
git log --oneline -1 --all format/grida.fbs 2>/dev/null || echo "n/a"
git log --oneline -1 --all packages/grida-canvas-schema/grida.ts 2>/dev/null || echo "n/a"Repository: gridaco/grida
Length of output: 119
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Read the encoder function that creates CanvasDocument =="
rg -n "fn.*encode|impl.*Encode" packages/grida-canvas-io/format.ts -A 10 | head -50
echo
echo "== Check for any constant or version string in grida.fbs =="
grep -n "0\.91\|version\|Version" format/grida.fbs
echo
echo "== Search broadly for CanvasDocument builder in format.ts =="
rg -n "CanvasDocument\|CanvasDocumentBuilder" packages/grida-canvas-io/format.ts | head -20
echo
echo "== Check the io_grida_fbs.rs Rust encoder =="
find . -name "*io*grida*" -type f | grep -E "\.(rs|ts)$"
</parameter>Repository: gridaco/grida
Length of output: 116
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== List Rust encoder files =="
find crates/grida-canvas -name "*io*" -type f | head -10
echo
echo "== Check io_grida_fbs module =="
find . -path "*/io/io_grida_fbs*" -type f
echo
echo "== Read format.ts around line 4977 where SCHEMA_VERSION is used =="
sed -n '4970,4985p' packages/grida-canvas-io/format.ts
echo
echo "== Check if schema_version is set in the document creation =="
sed -n '4900,4980p' packages/grida-canvas-io/format.ts | grep -n "schemaVersion\|schema_version\|SCHEMA_VERSION" -B 5 -A 5Repository: gridaco/grida
Length of output: 1856
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for where schemaVersion is passed to builder =="
sed -n '4977,5050p' packages/grida-canvas-io/format.ts | grep -n "schemaVersion\|CanvasDocument\|addSchema" -B 3 -A 3
echo
echo "== Search for CanvasDocument builder calls =="
rg -n "CanvasDocumentStart\|CanvasDocumentAdd" packages/grida-canvas-io/format.ts | head -10
echo
echo "== Read the io_grida_fbs encoder to see what schema version it uses =="
sed -n '1,50p' crates/grida-canvas/src/io/io_grida_fbs.rs
echo
echo "== Check if MarkdownEmbed appears in io_grida_fbs.rs at all =="
grep -n "MarkdownEmbed\|markdown" crates/grida-canvas/src/io/io_grida_fbs.rsRepository: gridaco/grida
Length of output: 703
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for CanvasDocument builder in format.ts =="
rg -n "CanvasDocument\|CanvasDocumentStart\|CanvasDocumentAddSchemaVersion" packages/grida-canvas-io/format.ts -B 2 -A 5 | head -60
echo
echo "== Look for flatbuffers builder.finish and root =="
rg -n "builder.finish\|CanvasDocumentStart" packages/grida-canvas-io/format.ts -B 3 -A 3 | head -40
echo
echo "== Check what addSchemaVersion or similar is called =="
rg -n "addSchema|Schema.*Add" packages/grida-canvas-io/format.ts | head -10Repository: gridaco/grida
Length of output: 109
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Read more of the toFlatbuffer function to see complete implementation =="
sed -n '4977,5150p' packages/grida-canvas-io/format.ts
echo
echo "== Check the end of toFlatbuffer to see what it returns =="
sed -n '5100,5200p' packages/grida-canvas-io/format.tsRepository: gridaco/grida
Length of output: 10297
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check if MarkdownEmbed decoder exists in format.ts =="
rg -n "markdown\|MarkdownEmbed" packages/grida-canvas-io/format.ts -B 2 -A 8 | head -50
echo
echo "== Check Rust schema.rs for MarkdownEmbed =="
rg -n "MarkdownEmbed\|markdown" crates/grida-canvas/src/node/schema.rs | head -20
echo
echo "== Check TS grida.ts for MarkdownEmbed node type =="
rg -n "MarkdownEmbed" packages/grida-canvas-schema/grida.ts | head -20
echo
echo "== Confirm schema_version in TS model matches the constant =="
sed -n '530,545p' packages/grida-canvas-schema/grida.tsRepository: gridaco/grida
Length of output: 113
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check if MarkdownEmbedNode type is defined in TS =="
rg -n "MarkdownEmbedNode\|type MarkdownEmbed" packages/grida-canvas-schema/grida.ts | head -20
echo
echo "== List all node types defined in TS grida.ts =="
rg -n "export type.*Node\s*=" packages/grida-canvas-schema/grida.ts | tail -30
echo
echo "== Check what node types are in the union type =="
sed -n '500,700p' packages/grida-canvas-schema/grida.ts | grep -n "Node\|Union"
echo
echo "== Check if encoder/decoder code exists for MarkdownEmbed in format.ts =="
wc -l packages/grida-canvas-io/format.ts
tail -500 packages/grida-canvas-io/format.ts | head -100Repository: gridaco/grida
Length of output: 111
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== List ALL node type definitions in grida.ts =="
rg -n "export.*\bNode\b" packages/grida-canvas-schema/grida.ts | grep "type\|interface" | head -40
echo
echo "== Check the union type definition in grida.ts =="
sed -n '600,900p' packages/grida-canvas-schema/grida.ts
echo
echo "== Search for all 'Node' in grida.ts to see structure =="
grep -n "^\s*export type\|^\s*\| Node\s*\|^\s*interface" packages/grida-canvas-schema/grida.ts | tail -60Repository: gridaco/grida
Length of output: 12061
Add missing TypeScript codec and roundtrip test coverage for MarkdownEmbedNode.
The FlatBuffers schema adds MarkdownEmbedNode to the Node union (line 1604), but the TypeScript model in packages/grida-canvas-schema/grida.ts lacks a corresponding MarkdownEmbedNode type, and packages/grida-canvas-io/format.ts has no encoder/decoder for it. Additionally:
- No roundtrip test exists for
MarkdownEmbedNodeincrates/grida-canvas/tests/fbs_roundtrip.rsorpackages/grida-canvas-io/__tests__/format-roundtrip.test.ts schema_versionin the FlatBuffers schema was not bumped to reflect this union member addition
Implement the TS type, add encoder/decoder in format.ts, bump schema_version, and add explicit roundtrip coverage for the new variant.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@format/grida.fbs` around lines 1491 - 1508, The FlatBuffers addition of
MarkdownEmbedNode/MarkdownEmbedNodeProperties was not propagated to TypeScript
and tests: add a MarkdownEmbedNode type to packages/grida-canvas-schema/grida.ts
matching the schema (include properties with fill_paints, markdown,
corner_radius), implement encoder and decoder handling for the Node union
variant in packages/grida-canvas-io/format.ts (serialize/deserialize properties
using existing PaintStackItem and RectangularCornerRadiusTrait helpers and
register the union discriminant for MarkdownEmbedNode), bump the schema_version
constant where the FlatBuffers schema version is maintained to reflect the union
change, and add roundtrip tests: one FBS roundtrip in
crates/grida-canvas/tests/fbs_roundtrip.rs that exercises MarkdownEmbedNode and
one JS/TS roundtrip in
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that encodes and
decodes a MarkdownEmbedNode instance to ensure fidelity.
| case "markdown_embed": { | ||
| const mdNode = node as grida.program.nodes.MarkdownEmbedNode; | ||
|
|
||
| const fillPaintsFiltered = paints(mdNode, "fill"); | ||
| const fillPaintsOffset = format.paint.encode.fillPaints( | ||
| builder, | ||
| fillPaintsFiltered, | ||
| fbs.MarkdownEmbedNodeProperties.createFillPaintsVector | ||
| ); | ||
| const markdownOffset = builder.createString(mdNode.markdown ?? ""); | ||
| const mdCornerUniform = mdNode.corner_radius; | ||
| const cornerRadiusOffset = | ||
| format.shape.encode.rectangularCornerRadiusTrait(builder, { | ||
| rectangular_corner_radius_top_left: | ||
| mdNode.rectangular_corner_radius_top_left ?? mdCornerUniform, | ||
| rectangular_corner_radius_top_right: | ||
| mdNode.rectangular_corner_radius_top_right ?? mdCornerUniform, | ||
| rectangular_corner_radius_bottom_left: | ||
| mdNode.rectangular_corner_radius_bottom_left ?? | ||
| mdCornerUniform, | ||
| rectangular_corner_radius_bottom_right: | ||
| mdNode.rectangular_corner_radius_bottom_right ?? | ||
| mdCornerUniform, | ||
| corner_smoothing: mdNode.corner_smoothing, | ||
| }); | ||
|
|
||
| fbs.MarkdownEmbedNodeProperties.startMarkdownEmbedNodeProperties( | ||
| builder | ||
| ); | ||
| fbs.MarkdownEmbedNodeProperties.addFillPaints( | ||
| builder, | ||
| fillPaintsOffset | ||
| ); | ||
| fbs.MarkdownEmbedNodeProperties.addMarkdown( | ||
| builder, | ||
| markdownOffset | ||
| ); | ||
| fbs.MarkdownEmbedNodeProperties.addCornerRadius( | ||
| builder, | ||
| cornerRadiusOffset | ||
| ); | ||
| const propertiesOffset = | ||
| fbs.MarkdownEmbedNodeProperties.endMarkdownEmbedNodeProperties( | ||
| builder | ||
| ); | ||
|
|
||
| fbs.MarkdownEmbedNode.startMarkdownEmbedNode(builder); | ||
| fbs.MarkdownEmbedNode.addNode(builder, systemNodeTraitOffset); | ||
| fbs.MarkdownEmbedNode.addLayer(builder, layerOffset); | ||
| fbs.MarkdownEmbedNode.addProperties(builder, propertiesOffset); | ||
| nodeOffset = fbs.MarkdownEmbedNode.endMarkdownEmbedNode(builder); | ||
| nodeType = fbs.Node.MarkdownEmbedNode; | ||
| break; | ||
| } |
There was a problem hiding this comment.
markdown_embed is encoded at Line [1918], but TypeScript decode never reconstructs it.
fromFlatbuffer() has no fbs.Node.MarkdownEmbedNode case, so markdown nodes fall through to the default group path, dropping markdown content and style properties.
🔧 Proposed fix (add TS decode path for MarkdownEmbedNode)
@@ export namespace document.decode.nodeTypes {
+ export function markdownEmbed(
+ n: fbs.MarkdownEmbedNode,
+ id: string,
+ systemNode: fbs.SystemNodeTrait,
+ layer: fbs.LayerTrait | null,
+ opacity: number,
+ layoutFields: ReturnType<typeof format.layout.decode.nodeLayout>,
+ effects?: grida.program.nodes.i.IEffects
+ ): grida.program.nodes.MarkdownEmbedNode {
+ const props = n.properties();
+ const fillPaints = props ? format.paint.decode.fillPaints(props) : undefined;
+ const cr = format.shape.decode.rectangularCornerRadiusTrait(
+ props?.cornerRadius() ?? null
+ );
+
+ return {
+ type: "markdown_embed",
+ id,
+ name: systemNode.name() ?? "markdown",
+ active: systemNode.active() ?? true,
+ locked: systemNode.locked() ?? false,
+ opacity,
+ z_index: 0,
+ ...layoutFields,
+ markdown: props?.markdown() ?? "",
+ ...(fillPaints ? { fill_paints: fillPaints } : {}),
+ rectangular_corner_radius_top_left: cr.rectangular_corner_radius_top_left,
+ rectangular_corner_radius_top_right: cr.rectangular_corner_radius_top_right,
+ rectangular_corner_radius_bottom_left: cr.rectangular_corner_radius_bottom_left,
+ rectangular_corner_radius_bottom_right: cr.rectangular_corner_radius_bottom_right,
+ corner_smoothing: cr.corner_smoothing,
+ ...(effects || {}),
+ } satisfies grida.program.nodes.MarkdownEmbedNode;
+ }
@@ type NodeWithLayer =
- | fbs.GroupNode;
+ | fbs.GroupNode
+ | fbs.MarkdownEmbedNode;
@@ switch (nodeType) {
+ case fbs.Node.MarkdownEmbedNode:
+ nodes[id] = nodeTypes.markdownEmbed(
+ typedNode as fbs.MarkdownEmbedNode,
+ id,
+ systemNode,
+ layer,
+ opacity,
+ layoutFields,
+ decodedEffects
+ );
+ break;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/grida-canvas-io/format.ts` around lines 1918 - 1971, The decode path
for the encoded markdown_embed is missing: update the fromFlatbuffer() switch to
handle fbs.Node.MarkdownEmbedNode (the MarkdownEmbedNode case) and reconstruct a
grida.program.nodes.MarkdownEmbedNode by reading properties added in format.ts
(fillPaints, markdown string, corner radius via
format.shape.decode.rectangularCornerRadiusTrait or equivalent decoder, and
system/layer traits). Ensure you map the flatbuffer fields back to the node
shape (markdown, fill paints, rectangular_corner_radius_* and corner_smoothing)
and return the correct node type instead of falling through to the default group
handling.
| } else if ( | ||
| type === "text/markdown" || | ||
| type === "md" || | ||
| type === "markdown" | ||
| ) { | ||
| return [true, "text/markdown" as const]; | ||
| } else { |
There was a problem hiding this comment.
Normalize extension matching to avoid missing uppercase markdown extensions.
When file.type is empty, .MD currently won’t match because the extension checks are case-sensitive. Lowercasing the fallback type/extension avoids false negatives.
💡 Suggested fix
- const type = file.type || file.name.split(".").pop() || file.name;
+ const type = (
+ file.type ||
+ file.name.split(".").pop() ||
+ file.name
+ ).toLowerCase();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/grida-canvas-io/index.ts` around lines 309 - 315, The branch that
detects markdown uses case-sensitive checks on the local variable type, so
uppercase extensions like ".MD" or "MD" are missed; normalize type before
comparisons (e.g., use a lowercased fallback value and compare against
"text/markdown", "md", and "markdown") by converting type to lowercase once
(e.g., const normalized = (type || "").toLowerCase()) and use normalized in the
existing conditional around the code that returns [true, "text/markdown" as
const].
CI cargo build with --examples failed with non-exhaustive match in examples/tool_io_grida.rs classify_json_node after MarkdownEmbed variant was added to JSONNode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd edge cases Addresses CodeRabbit review findings on PR #627. Critical (prevents data loss on save/reopen): - Add Rust FBS encoder (encode_markdown_embed_node) so Node::MarkdownEmbed no longer falls through to the UnknownNode fallback in encode_node. - Add TS FBS decoder (nodeTypes.markdownEmbed) + fromFlatbuffer match arm so MarkdownEmbedNode round-trips through @grida/io without being reconstructed as a group. DOM renderer safety: - Add MarkdownEmbedWidget as a simple DOM fallback (monospace pre-wrap block) and wire markdown_embed into NodeElement's renderer switch. The canonical render path is still canvas/Skia — this just prevents "Unknown node type" in DOM-rendered subtrees (previews, tests). .md file detection: - filetype() now lowercases MIME + extension and checks extension FIRST, so .md files still resolve as text/markdown when browsers serve them as text/plain, application/octet-stream, or with empty MIME. Also accepts .MD, .markdown, .mdown, .mkd extensions. - File name trimming on drop uses a case-insensitive regex to strip trailing .md/.markdown/.mdown/.mkd instead of a brittle split(".md"). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
MarkdownEmbedNodetype that renders GFM markdown directly on the canvas via the existing Skia-backed markdown paintermake_from_markdownfell back to creating a plainTextSpanNode, so styled markdown never made it to the renderer — it now creates a properMarkdownEmbedNode.mdfile drop + paste support on the canvasWhat changed
Schema (
format/grida.fbs)MarkdownEmbedNode+MarkdownEmbedNodePropertiestables (markdown string, fill_paints, rectangular corner_radius)MarkdownEmbedNodeto theNodeunion andNodeTypeenum (additive, schema-safe)TS runtime (
packages/grida-canvas-schema/grida.ts)MarkdownEmbedNodeinterface (type: \"markdown_embed\")LayerNode/ComputedNode/UnknownNode/NodePrototypeunionsRust IO bridges (
crates/grida-canvas/src/io/)JSONMarkdownEmbedNode→MarkdownEmbedNodeRecdecode_markdown_embed_nodeTS FBS encoder (
packages/grida-canvas-io/format.ts)\"markdown_embed\"with corner radius + fill paintsEditor + AI tool
Editor.createMarkdownEmbedNode(markdown)commandmake_from_markdowntool now creates the styled embed nodeFile drops
\"text/markdown\"toValidFileType(detects MIME +.mdextension)insertMarkdownhandler wired throughinsertFromFileand paste handlerTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
.mdfiles.