Skip to content

Frontmatter Bugfixes#71

Merged
dannysmith merged 6 commits intomainfrom
frontmatter-bugfixes
Dec 4, 2025
Merged

Frontmatter Bugfixes#71
dannysmith merged 6 commits intomainfrom
frontmatter-bugfixes

Conversation

@dannysmith
Copy link
Copy Markdown
Owner

@dannysmith dannysmith commented Dec 4, 2025

Closes #68

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced schema support for nullable arrays, enums, and objects with correct field type rendering.
    • Document formatting (metadata field order, spacing, and styling) now preserved when editing content.
  • Tests

    • Added comprehensive test coverage for nullable schema types and content-only edits.

✏️ Tip: You can customize this high-level summary in your review settings.

When Zod schemas use `.nullish()` or `.optional().nullable()` on non-primitive types, Astro generates JSON Schema with `anyOf: [type, null]`. The current `handle_anyof_type()` function only handles:
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 4, 2025

Walkthrough

This PR implements two cohesive improvements: (1) nullable schema type handling in JSON Schema generation, adding helpers to extract and properly render nullable arrays, enums, and objects from anyOf unions; (2) frontmatter preservation during save by tracking frontmatter edits separately, conditionally passing raw or parsed frontmatter to maintain original formatting when only content changes.

Changes

Cohort / File(s) Summary
Documentation & Task Tracking
docs/tasks-done/task-2025-12-04-fix-nullable-schema-types.md
Comprehensive task documentation covering nullable schema types feature (array/enum/object extraction), frontmatter dirty-state tracking, save flow adjustments, and test scaffolding for both features with success criteria and edge cases.
Backend: Frontmatter Preservation Logic
src-tauri/src/commands/files.rs
Updated save_markdown_content signature to accept optional parsed and raw frontmatter; added helpers rebuild_markdown_with_raw_frontmatter and rebuild_markdown_content_only to conditionally reconstruct markdown preserving raw frontmatter or handling content-only scenarios; introduced parse_frontmatter_internal public utility; expanded unit tests for raw-frontmatter and content-only rebuilds.
Backend: Schema Processing
src-tauri/src/schema_merger.rs
Extended handle_anyof_type with dedicated helpers (extract_nullable_primitive_type, extract_nullable_array_type, extract_nullable_enum_type) to recognise and correctly assign field types for nullable patterns in anyOf; added regression tests validating nullable number/integer/boolean/enum/array handling with order-independence.
Frontend: Editor State Management
src/store/editorStore.ts
Introduced isFrontmatterDirty: boolean flag to track frontmatter modifications separately; initialised to false; marked true when frontmatter or individual fields update; updated isDirty semantics documentation.
Frontend: File Content Sync
src/hooks/useEditorFileContent.ts
Added isFrontmatterDirty: false reset when loading content from disk to clear frontmatter-dirty flag during file sync.
Frontend: Save Action Flow
src/hooks/editor/useEditorActions.ts
Integrated rawFrontmatter and isFrontmatterDirty from editor store; conditionally pass frontmatter only if dirty, otherwise include rawFrontmatter; clear isFrontmatterDirty alongside isDirty after successful save.
Frontend: Test Setup
src/hooks/editor/useEditorHandlers.test.ts
Added isFrontmatterDirty: boolean property to mock store state type; initialised to false in test setup.
Test Data: Schema Definitions
test/dummy-astro-project/.astro/collections/notes.schema.json
Added three new nullable properties: status (enum anyOf), keywords (array of strings anyOf), scores (array of numbers anyOf) to notes schema definitions.
Test Data: Astro Configuration
test/dummy-astro-project/src/content.config.ts
Added three nullable fields to notes collection: status (z.enum with .nullish()), keywords (z.array(z.string()).nullish()), scores (z.array(z.number()).nullish()).
Test Data: Test Files
test/dummy-astro-project/src/content/notes/frontmatter-preservation-test.md,
test/dummy-astro-project/src/content/notes/nullable-schema-test.md
New test markdown files documenting frontmatter preservation and nullable schema type verification scenarios with expected UI behaviour and formatting preservation.
Test Data: Settings Update
test/dummy-astro-project/.astro/settings.json
Updated lastUpdateCheck timestamp.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Store as Editor Store
    participant Hook as useEditorActions
    participant Backend as save_markdown_content
    participant FS as File System

    User->>Store: Edit frontmatter
    Store->>Store: Set isFrontmatterDirty = true
    
    User->>Store: Edit content only
    Store->>Store: isFrontmatterDirty remains true/false

    User->>Hook: Trigger save
    Hook->>Store: Read isFrontmatterDirty, frontmatter, rawFrontmatter
    
    alt Frontmatter was edited
        Hook->>Backend: Pass frontmatter (parsed), rawFrontmatter = null
        Backend->>Backend: Reorder/normalise frontmatter
    else Frontmatter unchanged
        Hook->>Backend: Pass frontmatter = null, rawFrontmatter (raw)
        Backend->>Backend: Preserve raw frontmatter exactly
    end
    
    Backend->>FS: Write markdown with conditional frontmatter handling
    FS-->>Backend: Success
    Backend-->>Hook: Confirm
    Hook->>Store: Set isFrontmatterDirty = false, isDirty = false
    Store-->>User: Save complete, state cleared
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25–30 minutes

Key areas requiring attention:

  • Backend conditional logic in save_markdown_content: three branching paths (edited frontmatter, raw preservation, content-only) with distinct rebuild strategies
  • Schema type extraction helpers in schema_merger.rs: validate correct handling of nullable arrays/enums within anyOf patterns and order-independence
  • Frontend state propagation: ensure isFrontmatterDirty is correctly set during frontmatter edits and cleared on save/file load
  • Test coverage: verify raw-frontmatter preservation and schema type regression tests cover edge cases (nested arrays, enum values, null ordering)

Possibly related PRs

  • Refactor-zustand-calls #55: Modifies useEditorFileContent.ts subscriptions; overlaps with this PR's file-load logic and isFrontmatterDirty reset functionality.

Poem

🐰 With schemas squared and frontmatter fair,
I've tracked which edits need my care,
Nullable arrays now shine bright,
Raw formatting preserved just right!
One save, two paths, and all is well—
Hop hop, the story's mine to tell! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes a secondary feature (frontmatter preservation/dirty tracking) beyond the stated issue #68 requirement. While related to frontmatter handling, this feature extends scope significantly with new store state, save logic, and tests not required by the linked issue. Consider separating the frontmatter preservation feature into a separate PR. The current PR should focus on fixing the nullable number type coercion bug in issue #68.
Title check ❓ Inconclusive The title 'Frontmatter Bugfixes' is vague and does not clearly convey the primary change. While frontmatter is involved, the title does not specify the main bug being addressed (nullable schema type handling or frontmatter preservation). Clarify the title to specify the primary bug fix, such as 'Fix nullable number types in frontmatter sidebar' or 'Preserve frontmatter formatting and fix type coercion'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR addresses issue #68 by implementing nullable schema type handling (extract_nullable_primitive_type for numbers) to preserve numeric types, preventing string coercion. Tests validate nullable arrays and enums alongside the core fix.
Description check ✅ Passed The PR provides clear objectives linked to issue #68, describing the bug (numeric values converted to strings) and the solution (preserve numeric types via schema awareness).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch frontmatter-bugfixes

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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (2)
src-tauri/src/schema_merger.rs (1)

511-516: Consider logging when handle_array_type fails.

The .ok() on line 514 silently discards any error from handle_array_type. While this allows the function to fall through to other handlers, it may mask issues during debugging. A log::debug! call when an error occurs could aid troubleshooting.

     if has_null {
         if let Some(schema) = array_schema {
             // Delegate to existing handle_array_type logic
-            return handle_array_type(schema).ok();
+            match handle_array_type(schema) {
+                Ok(info) => return Some(info),
+                Err(e) => {
+                    log::debug!("[Schema] Nullable array extraction failed: {e}");
+                }
+            }
         }
     }
     None
src-tauri/src/commands/files.rs (1)

383-385: Consider removing redundant public wrapper.

The parse_frontmatter_internal function simply delegates to the private parse_frontmatter function without adding any value:

pub fn parse_frontmatter_internal(content: &str) -> Result<MarkdownContent, String> {
    parse_frontmatter(content)
}

If external modules need to call parse_frontmatter, consider making the original function public directly rather than adding a wrapper. If this wrapper serves a specific purpose (e.g., testing or future extensibility), please document it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de06201 and 67482c0.

📒 Files selected for processing (12)
  • docs/tasks-done/task-2025-12-04-fix-nullable-schema-types.md (1 hunks)
  • src-tauri/src/commands/files.rs (4 hunks)
  • src-tauri/src/schema_merger.rs (4 hunks)
  • src/hooks/editor/useEditorActions.ts (3 hunks)
  • src/hooks/editor/useEditorHandlers.test.ts (2 hunks)
  • src/hooks/useEditorFileContent.ts (1 hunks)
  • src/store/editorStore.ts (6 hunks)
  • test/dummy-astro-project/.astro/collections/notes.schema.json (1 hunks)
  • test/dummy-astro-project/.astro/settings.json (1 hunks)
  • test/dummy-astro-project/src/content.config.ts (1 hunks)
  • test/dummy-astro-project/src/content/notes/frontmatter-preservation-test.md (1 hunks)
  • test/dummy-astro-project/src/content/notes/nullable-schema-test.md (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src-tauri/src/commands/files.rs (1)
src-tauri/src/commands/mdx_components.rs (1)
  • validate_project_path (17-32)
🪛 LanguageTool
test/dummy-astro-project/src/content/notes/nullable-schema-test.md

[grammar] ~23-~23: The plural noun “keywords” cannot be used with the article “a”.
Context: ...lished']).nullish()- Should render as a dropdown 2. **keywords** -z.array(z.string()).nullish()` - S...

(A_NNS)


[duplication] ~30-~30: Possible typo: you repeated a word.
Context: ...eld should show a tag input, not a text field - scores field should show a number array input

(ENGLISH_WORD_REPEAT_RULE)

docs/tasks-done/task-2025-12-04-fix-nullable-schema-types.md

[uncategorized] ~652-~652: A comma may be missing after the conjunctive/linking adverb ‘Currently’.
Context: ...s) saves editor state when save fails. Currently it saves frontmatterbut notisFront...

(SENT_START_CONJUNCTIVE_LINKING_ADVERB_COMMA)


[uncategorized] ~656-~656: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...s is acceptable because recovery is rare and data integrity matters more than format...

(COMMA_COMPOUND_SENTENCE_2)

🪛 markdownlint-cli2 (0.18.1)
docs/tasks-done/task-2025-12-04-fix-nullable-schema-types.md

299-299: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (20)
test/dummy-astro-project/.astro/settings.json (1)

3-3: Verify the necessity of committing this test fixture change.

This is an auto-generated settings file in the test project directory. The timestamp modification appears to be a touch update rather than a functional change. Test fixtures and configuration files that are auto-generated by the build or test tooling should typically not be committed to version control, or the PR description should clearly explain why this file has been modified.

Confirm whether this change is intentional and necessary for the PR to function correctly, or if it should be excluded from the commit.

src-tauri/src/schema_merger.rs (4)

418-444: LGTM! Well-structured nullable type handling.

The prioritisation of nullable primitive, array, and enum extraction before falling back to string is correct. This directly addresses issue #68 where z.number().nullish() was incorrectly typed as "string".


446-486: Solid implementation with appropriate guards.

The logic correctly identifies nullable primitives while properly deferring strings with enum values to the dedicated enum handler. The explicit 2-element check and type matching are robust.


520-556: LGTM! Correctly extracts nullable enums.

The logic properly identifies nullable enum patterns by checking for both a null type and a string type with enum values. The catch-all _ => {} is appropriate here since the function needs to examine all schemas in the union.


1264-1304: Excellent regression test for issue #68.

The test explicitly documents the bug and verifies both lat and lng fields are correctly typed as "number" regardless of the order of elements in the anyOf array. This directly validates the fix for the reported issue.

src/hooks/editor/useEditorHandlers.test.ts (1)

28-28: LGTM! Mock state correctly extended.

The addition of isFrontmatterDirty to the mock store state and its initialisation to false aligns with the new editor store field. This ensures tests remain compatible with the updated store interface.

Also applies to: 74-74

src/hooks/useEditorFileContent.ts (1)

39-45: LGTM! Correctly resets frontmatter dirty state on load.

Resetting isFrontmatterDirty: false when syncing from disk ensures the editor correctly treats loaded content as clean. This is essential for the save flow to preserve original frontmatter formatting when only content has changed.

test/dummy-astro-project/src/content/notes/nullable-schema-test.md (1)

1-31: LGTM! Useful test fixture for nullable schema types.

The test file provides concrete test data for verifying nullable enum and array handling. The frontmatter values correctly exercise the new nullable schema paths (status, keywords, scores).

Note: The static analysis warnings are false positives — the grammar check misunderstands the technical documentation context, and "tag input" vs "text field" are distinct UI element types, not duplications.

src/hooks/editor/useEditorActions.ts (3)

32-39: LGTM! Clean destructuring of frontmatter state.

The addition of rawFrontmatter and isFrontmatterDirty to the destructured state is well-organised and follows existing patterns.


73-82: Core fix for issue #68 — preserves raw frontmatter when unedited.

The conditional logic correctly determines which frontmatter representation to send:

  • When isFrontmatterDirty is true: sends the parsed frontmatter object (user made sidebar edits)
  • When false: sends rawFrontmatter to preserve original YAML formatting and type information

This ensures that numeric values like lat/lng remain as numbers when only the content body is edited.


91-95: LGTM! Correctly resets dirty flags after save.

Resetting both isDirty and isFrontmatterDirty to false after a successful save ensures the editor state accurately reflects that all changes have been persisted.

test/dummy-astro-project/src/content/notes/frontmatter-preservation-test.md (1)

1-36: LGTM! Clear test documentation.

This test file provides clear instructions and expected behaviours for frontmatter preservation testing. The documentation of both preserved and re-serialised scenarios is helpful for validation.

src/store/editorStore.ts (1)

19-20: LGTM! Correct dirty state tracking implementation.

The isFrontmatterDirty flag is properly integrated:

  • Initialised to false and reset on file open/close operations
  • Set to true when frontmatter is updated via updateFrontmatter or updateFrontmatterField
  • Intentionally NOT modified in setEditorContent, which correctly preserves the distinction between content-only and frontmatter edits

This implementation aligns with the PR's objective to track frontmatter modifications separately from content changes.

Also applies to: 47-47, 70-70, 101-101, 125-125, 147-147

test/dummy-astro-project/.astro/collections/notes.schema.json (1)

50-90: LGTM! Nullable schema types correctly defined.

The three new properties follow the correct anyOf: [type, null] pattern for nullable types:

  • status: nullable enum with valid values
  • keywords: nullable array of strings
  • scores: nullable array of numbers

These additions align with the PR's objective to support nullable schema types and match the Zod definitions in content.config.ts.

test/dummy-astro-project/src/content.config.ts (1)

48-51: LGTM! Zod schemas correctly defined for testing.

The nullable Zod schemas match the JSON Schema definitions and provide comprehensive test coverage for:

  • Nullable enums (status)
  • Nullable string arrays (keywords)
  • Nullable number arrays (scores)

These test schemas align with the PR's objective to support nullable schema types properly.

src-tauri/src/commands/files.rs (4)

364-378: Consider defensive check for impossible state.

The match logic correctly handles the three scenarios:

  1. Frontmatter edited → use parsed and normalize
  2. Frontmatter unchanged → preserve raw formatting
  3. No frontmatter → content only

However, consider adding a defensive check for the impossible case where both frontmatter and raw_frontmatter are Some:

 let new_content = match (frontmatter, raw_frontmatter) {
     // Frontmatter was edited - reorder and normalize
     (Some(fm), _) => rebuild_markdown_with_frontmatter_and_imports_ordered(
         &fm,
         &imports,
         &content,
         schema_field_order,
     )?,
+    // Defensive: If both are present, prefer parsed (shouldn't happen from frontend)
+    (None, Some(_)) => {
+        return Err("Invalid state: received both frontmatter and raw_frontmatter".to_string());
+    },
     // Frontmatter unchanged - preserve original (non-empty)
     (None, Some(ref raw)) if !raw.trim().is_empty() => {
         rebuild_markdown_with_raw_frontmatter(raw, &imports, &content)?
     }
     // No frontmatter at all (None, None, or empty string)
     _ => rebuild_markdown_content_only(&imports, &content)?,
 };

Actually, on second thought, the current pattern (Some(fm), _) already handles this by giving precedence to parsed frontmatter. The code is correct as-is.


716-755: LGTM! Raw frontmatter preservation correctly implemented.

The rebuild_markdown_with_raw_frontmatter function correctly:

  • Preserves the raw frontmatter string exactly as-is (no normalisation)
  • Handles imports and content with proper spacing
  • Ensures consistent newline handling

The blank line before imports (line 733) is always added because this function is only called when raw frontmatter is non-empty (guarded in save_markdown_content line 373), ensuring consistency with the normalised path.


758-786: LGTM! Content-only rebuild correctly implemented.

The rebuild_markdown_content_only function properly handles the case where there's no frontmatter:

  • Includes imports if present
  • Adds appropriate spacing between imports and content
  • Ensures trailing newline

The spacing logic is consistent with the other rebuild functions.


1589-1649: Excellent test coverage for new rebuild functions.

The test suite comprehensively covers:

  • Raw frontmatter preservation including datetime format (lines 1589-1605)
  • Raw frontmatter with imports (lines 1607-1620)
  • Content-only rebuild without imports (lines 1622-1635)
  • Content-only rebuild with imports (lines 1637-1649)

These tests validate the key behaviours of the frontmatter preservation feature.

docs/tasks-done/task-2025-12-04-fix-nullable-schema-types.md (1)

1-688: Comprehensive documentation of implemented features.

This documentation thoroughly explains:

  • The nullable schema types problem and solution approach
  • The frontmatter preservation feature with architecture diagrams
  • Implementation details with code examples
  • Test plans and success criteria
  • Edge cases and gotchas

The documented implementation aligns with the code changes reviewed in editorStore.ts and files.rs. This serves as excellent reference documentation for future maintainers.

@dannysmith dannysmith merged commit 2a33ae1 into main Dec 4, 2025
8 checks passed
@dannysmith dannysmith deleted the frontmatter-bugfixes branch December 4, 2025 02:01
@coderabbitai coderabbitai bot mentioned this pull request Dec 4, 2025
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.

frontmatter sidebar changes my lat/lng number into a string

1 participant