Skip to content

Allow frontmatter in included files.#228

Merged
panglesd merged 15 commits into
mainfrom
local-and-global-frontmatter
May 5, 2026
Merged

Allow frontmatter in included files.#228
panglesd merged 15 commits into
mainfrom
local-and-global-frontmatter

Conversation

@panglesd
Copy link
Copy Markdown
Owner

@panglesd panglesd commented May 4, 2026

Toplevel-attributes apply to the included file. Otherwise, fields are combined when applicable (and raise a warning when not).

Summary by CodeRabbit

  • New Features

    • Frontmatter is now allowed in included files; options from multiple files are merged and CSS links combined.
    • Dimension parsing supports presets (e.g. 16:9, 4:3) and widthxheight formats.
  • Bug Fixes / Warnings

    • Conflicting or duplicated frontmatter options now emit precise warnings with source locations.
  • Documentation

    • Added “Multifile presentations” guidance for frontmatter in included files.
  • Tests

    • Expanded tests for multi-file frontmatter, dimension parsing, themes, and related warnings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 962b5b6a-7b88-42b4-8182-af9738b32327

📥 Commits

Reviewing files that changed from the base of the PR and between 102589c and 181f652.

📒 Files selected for processing (8)
  • test/compiler/multi-frontmatter.t/single-file.md
  • test/compiler/theme.t/css.md
  • test/compiler/theme.t/default.md
  • test/compiler/theme.t/empty.md
  • test/compiler/theme.t/my_theme.md
  • test/compiler/theme.t/none.md
  • test/compiler/theme.t/remote.md
  • test/compiler/theme.t/vanier.md
✅ Files skipped from review due to trivial changes (4)
  • test/compiler/theme.t/none.md
  • test/compiler/multi-frontmatter.t/single-file.md
  • test/compiler/theme.t/remote.md
  • test/compiler/theme.t/vanier.md

📝 Walkthrough

Walkthrough

This change enables frontmatter in included files and refactors frontmatter into a unified Frontmatter.t with separate local and global components and loc-aware fields. Frontmatter is now extracted during parsing via Cmarkit_proxy.of_string, propagated as fm.global into compiled AST options, and merged from included files (including toplevel-attributes). CLI, previewer, and plugin APIs were simplified to accept optional ?options:Frontmatter.Global.t. A generic Fold_mapper was added to cmarkit and a new InconsistentOption diagnostic was introduced.

Possibly related PRs

  • panglesd/slipshow#213: Modifies frontmatter representation and related preview/plugin APIs, touching frontmatter parsing and warning propagation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Allow frontmatter in included files' directly and clearly summarizes the main change: enabling frontmatter parsing in included markdown files, which is the core feature added throughout the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Caution

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

⚠️ Outside diff range comments (1)
src/compiler/frontmatter.ml (1)

382-387: ⚠️ Potential issue | 🔴 Critical | 💤 Low value

Fix frontmatter opening delimiter detection for CRLF line endings.

The logic at lines 384-386 has a critical bug for "---\r\n" prefixes. Since s.[4] is always '\n' in that case, the condition will always be true, causing the function to always return Some 3 instead of the correct offset. This breaks frontmatter extraction on Windows systems using CRLF line endings.

The issue stems from checking s.[4] unconditionally despite the prefix length being different:

  • For "---\n" (4 bytes): s.[4] is the first content character
  • For "---\r\n" (5 bytes): s.[4] is always '\n' from the prefix, not content

Consider using prefix-aware logic or checking the appropriate position based on the matched prefix length.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/frontmatter.ml` around lines 382 - 387, The function
find_opening incorrectly checks s.[4] regardless of which prefix matched; change
the logic to test the CRLF prefix first and return the correct offset for each
prefix (i.e., if String.starts_with ~prefix:"---\r\n" then return Some 4, else
if String.starts_with ~prefix:"---\n" then return Some 3), ensuring the check is
prefix-aware and avoids indexing the wrong byte; update the function
find_opening to match the longer prefix first and return the appropriate offset
for the matched prefix.
🧹 Nitpick comments (3)
docs/frontmatter.rst (1)

53-55: ⚡ Quick win

Clarify precedence for conflicting single-value options.

Line 53–55 explains combine vs warning, but not which value wins on conflict. Please state precedence explicitly (and mention list options are appended) to prevent ambiguity in multifile setups.

Proposed doc tweak
-When you split your presentation in multiple files, you can define a frontmatter
-in the included files. All options can be duplicated, and will be combined when
-applicable. When not applicable, a warning will be raised.
-``toplevel-attributes`` refers to the included file.
+When you split your presentation in multiple files, you can define frontmatter
+in included files.
+
+Compatible options are combined (for example, ``css``, ``js``, and
+``external-ids`` are appended). For single-value options (for example,
+``theme`` or ``dimension``), conflicting values raise a warning and the value
+already defined in the including file is kept.
+
+``toplevel-attributes`` always refers to the included file.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/frontmatter.rst` around lines 53 - 55, Update the paragraph around the
"toplevel-attributes" description to explicitly state precedence: for
conflicting single-value options the value from the last-included file (i.e.,
the later include) overrides earlier values, list-valued options are
appended/concatenated across included files, and a warning is still emitted when
a single-value option is overwritten to make conflict visibility explicit.
src/compiler/cmarkit_proxy.ml (1)

1-19: ⚡ Quick win

Consider renaming the private Cmarkit.Doc.of_string wrapper to avoid name shadowing.

Having two let of_string bindings in the same file — where the second shadows the first and the internal call on line 18 silently resolves to the first — is non-obvious. Re-ordering the bindings, wrapping either in let rec, or renaming only one would silently break the cross-call without a compile error.

A simple rename makes the intent explicit:

♻️ Proposed rename
-let of_string ?loc_offset ~file =
+let parse_doc ?loc_offset ~file =
   let locs = Option.is_some file in
   Cmarkit.Doc.of_string ~heading_auto_ids:false ~strict:false ~locs ?loc_offset
     ?file

 let of_string ~read_file ~file s =
   ...
-  let doc = of_string ~loc_offset ~file s in
+  let doc = parse_doc ~loc_offset ~file s in
   (doc, frontmatter)

All external call sites already use the new ~read_file ~file signature, so this refactor is safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/cmarkit_proxy.ml` around lines 1 - 19, The private wrapper
Cmarkit.Doc.of_string called via the local let of_string is being shadowed by
the public of_string ~read_file ~file binding; rename the private wrapper (e.g.,
to of_string_raw or cmarkit_of_string) so the cross-call is explicit, update the
internal call site that currently does of_string ~loc_offset ~file s to call the
new name, and leave the public of_string ~read_file ~file signature unchanged so
external call sites aren’t affected.
src/compiler/compile.ml (1)

316-321: 💤 Low value

Incorrect parameter name in inline function signature.

The first parameter should be named m (for mapper) to be consistent with the Fold_mapper API and other functions in this module. Currently it's named i, which shadows the intended purpose.

♻️ Suggested fix for consistency
-    let inline i fm = function
+    let inline m fm = function
       | Inline.Image img as inl -> (
-          match handle_image_inlining i fm defs current_path img with
-          | `Default -> Ast.Fold_mapper.default.inline i fm inl
+          match handle_image_inlining m fm defs current_path img with
+          | `Default -> Ast.Fold_mapper.default.inline m fm inl
           | `Return (fm, i) -> (fm, Some i))
-      | inl -> Ast.Fold_mapper.default.inline i fm inl
+      | inl -> Ast.Fold_mapper.default.inline m fm inl
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/compile.ml` around lines 316 - 321, The inline function's first
parameter is misnamed `i` and should be `m` to match the Fold_mapper API and
other functions; rename the parameter in the inline function signature (and all
its occurrences inside the function) from `i` to `m`, keeping the match arms and
calls to handle_image_inlining and Ast.Fold_mapper.default.inline unchanged so
that the mapper argument is correctly passed through.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vendor/github.com/panglesd/cmarkit/src/cmarkit.ml`:
- Around line 3860-3880: The fold_map_doc function double-processes definitions
because it folds the whole Doc.block and then iterates Doc.defs; fix by
processing Doc.defs first (use Label.Map.fold with fold_map_def to update acc
and build new defs), then remove any Ext_footnote_definition /
Ext_attribute_definition nodes from the block before calling m.block so they are
not folded a second time. Concretely, in fold_map_doc call Label.Map.fold on
(Doc.defs d) using fold_map_def to produce (acc, defs), build a cleaned_block by
filtering out Block.Footnote.Def and Block.Attribute_definition.Def nodes from
(Doc.block d), then call m.block m acc cleaned_block and finally return acc, { d
with block = Option.value ~default:Block.empty block; defs } (using the same
symbols: fold_map_doc, fold_map_def, Doc.block, Doc.defs, Block.Footnote.Def,
Block.Attribute_definition.Def, m.block).
- Around line 3793-3810: The record-update uses invalid qualified field
shorthand in the Block.Ext_table branch and similarly for list items; replace `{
t with Block.Table.rows }` with an explicit assignment `{ t with
Block.Table.rows = rows }` in the Block.Ext_table handling (where t and rows are
in scope) and likewise replace `{ i with Block.List_item.block }` with `{ i with
Block.List_item.block = block }` in the list-item branch (using the local i and
block variables) so the qualified field updates are valid OCaml syntax.
- Around line 3824-3838: The pattern matching branch handling Block.List uses an
invalid qualified record update `{ i with Block.List_item.block }`; in the
map_item function you should assign the field explicitly by setting the
qualified field to the local value returned by m.block (i.e. use
`Block.List_item.block = block` in the record update). Locate the Block.List
branch and the map_item function that calls `m.block m acc
(Block.List_item.block i)` and change the record update so the qualified field
name is followed by `= block`, ensuring the Option.map constructs the updated
list item correctly.

---

Outside diff comments:
In `@src/compiler/frontmatter.ml`:
- Around line 382-387: The function find_opening incorrectly checks s.[4]
regardless of which prefix matched; change the logic to test the CRLF prefix
first and return the correct offset for each prefix (i.e., if String.starts_with
~prefix:"---\r\n" then return Some 4, else if String.starts_with ~prefix:"---\n"
then return Some 3), ensuring the check is prefix-aware and avoids indexing the
wrong byte; update the function find_opening to match the longer prefix first
and return the appropriate offset for the matched prefix.

---

Nitpick comments:
In `@docs/frontmatter.rst`:
- Around line 53-55: Update the paragraph around the "toplevel-attributes"
description to explicitly state precedence: for conflicting single-value options
the value from the last-included file (i.e., the later include) overrides
earlier values, list-valued options are appended/concatenated across included
files, and a warning is still emitted when a single-value option is overwritten
to make conflict visibility explicit.

In `@src/compiler/cmarkit_proxy.ml`:
- Around line 1-19: The private wrapper Cmarkit.Doc.of_string called via the
local let of_string is being shadowed by the public of_string ~read_file ~file
binding; rename the private wrapper (e.g., to of_string_raw or
cmarkit_of_string) so the cross-call is explicit, update the internal call site
that currently does of_string ~loc_offset ~file s to call the new name, and
leave the public of_string ~read_file ~file signature unchanged so external call
sites aren’t affected.

In `@src/compiler/compile.ml`:
- Around line 316-321: The inline function's first parameter is misnamed `i` and
should be `m` to match the Fold_mapper API and other functions; rename the
parameter in the inline function signature (and all its occurrences inside the
function) from `i` to `m`, keeping the match arms and calls to
handle_image_inlining and Ast.Fold_mapper.default.inline unchanged so that the
mapper argument is correctly passed through.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4ffc0848-2968-43e3-9127-1da24cb12aec

📥 Commits

Reviewing files that changed from the base of the PR and between 3d17d1a and 23c9831.

⛔ Files ignored due to path filters (1)
  • test/compiler/multi-frontmatter.t/chapter2/image_of_chapter_2.png is excluded by !**/*.png
📒 Files selected for processing (32)
  • CHANGELOG.md
  • docs/doc-repl/main.ml
  • docs/frontmatter.rst
  • src/cli/main.ml
  • src/cli/run.ml
  • src/cli/run.mli
  • src/cm_plugin/slipshow_communication.ml
  • src/cm_plugin/slipshow_communication.mli
  • src/compiler/ast.ml
  • src/compiler/cmarkit_proxy.ml
  • src/compiler/compile.ml
  • src/compiler/compile.mli
  • src/compiler/frontmatter.ml
  • src/compiler/frontmatter.mli
  • src/compiler/slipshow.ml
  • src/compiler/slipshow.mli
  • src/diagnosis/diagnosis.ml
  • src/diagnosis/diagnosis.mli
  • src/engine/previewer/previewer.ml
  • src/engine/previewer/previewer.mli
  • test/compiler/dimension.t/run.t
  • test/compiler/multi-frontmatter.t/chapter1.md
  • test/compiler/multi-frontmatter.t/chapter2/chapter2.md
  • test/compiler/multi-frontmatter.t/chapter2/parts/part1.md
  • test/compiler/multi-frontmatter.t/chapter2/parts/part2.md
  • test/compiler/multi-frontmatter.t/main.md
  • test/compiler/multi-frontmatter.t/run.t
  • test/compiler/simple.t/run.t
  • test/compiler/theme.t/run.t
  • test/compiler/warnings.t/run.t
  • vendor/github.com/panglesd/cmarkit/src/cmarkit.ml
  • vendor/github.com/panglesd/cmarkit/src/cmarkit.mli
💤 Files with no reviewable changes (1)
  • test/compiler/simple.t/run.t

Comment thread vendor/github.com/panglesd/cmarkit/src/cmarkit.ml
Comment thread vendor/github.com/panglesd/cmarkit/src/cmarkit.ml
Comment thread vendor/github.com/panglesd/cmarkit/src/cmarkit.ml
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: 1

🧹 Nitpick comments (2)
src/compiler/frontmatter.ml (2)

204-204: 💤 Low value

Slightly fragile: of_string ~to_asset:() only works while to_asset stays unused in Dimension.of_string.

let of_string' = of_string ~to_asset:() type-checks today only because Dimension.of_string matches ~to_asset:_ and stays polymorphic in that argument; the moment someone actually uses to_asset (turning it into string -> Asset.t), this line silently breaks compilation in a confusing way. A small comment explaining the intent, or a no-op default like ~to_asset:(fun _ -> assert false) (or Fun.id if Asset.t = string), makes the fragility explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/frontmatter.ml` at line 204, The binding let of_string' =
of_string ~to_asset:() is fragile because it relies on Dimension.of_string
remaining polymorphic in ~to_asset; change it to explicitly provide a no-op
placeholder for the to_asset argument (e.g. ~to_asset:(fun _ -> assert false) or
another sentinel) or add a clarifying comment, so the intent is explicit and the
code fails clearly if to_asset becomes used; update the occurrence of of_string'
and call site expectations around of_string and Dimension.of_string accordingly.

113-119: 💤 Low value

Design inconsistency: toplevel-attributes merges silently while other duplicate fields warn.

Unlike Global option fields (e.g., math-link, theme) that emit InconsistentOption diagnostics when encountered multiple times with conflicting values via combine_opt, Toplevel_attributes.update_frontmatter silently merges via Cmarkit.Attributes.merge with no diagnostic. This is visible in the commit "Raise warnings in case of duplicated frontmatter fields" which added warnings for other field conflicts, yet attributes bypass this.

The merge behavior is technically not a "silent override"—conflicting keys coexist in the attribute list with the newer value prepended and found first—but no collision warning is issued. This may be intentional given that attributes are inherently mergeable (unlike scalar options), but it creates an inconsistency with the stated goal of warning on duplicated fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/frontmatter.ml` around lines 113 - 119, The update_frontmatter
function currently merges attributes with Cmarkit.Attributes.merge without
emitting a diagnostic for duplicate keys; change update_frontmatter (in
Toplevel_attributes) to detect overlapping attribute keys between the existing
toplevel_attributes (a) and the incoming v, and when conflicts exist emit the
same InconsistentOption diagnostic used by combine_opt (or the module's
diagnostic constructor) before performing the merge; keep using
Cmarkit.Attributes.merge to produce the combined list but ensure you call the
diagnostics API with a clear message and context (same style as combine_opt)
referencing the conflicting key(s).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/compiler/multi-frontmatter.t/run.t`:
- Around line 20-21: The test currently asserts the exact concatenated
stylesheet string by running show_source main.html | grep "rel=\"stylesheet\""
which is fragile; change it to check for each stylesheet independently (e.g.,
assert presence of file.css and file2.css separately) by replacing the single
grep with two independent assertions that each look for the corresponding
href/rel pair (or grep for href="file.css" and href="file2.css" individually) so
the test no longer depends on the renderer's whitespace/ordering in main.html.

---

Nitpick comments:
In `@src/compiler/frontmatter.ml`:
- Line 204: The binding let of_string' = of_string ~to_asset:() is fragile
because it relies on Dimension.of_string remaining polymorphic in ~to_asset;
change it to explicitly provide a no-op placeholder for the to_asset argument
(e.g. ~to_asset:(fun _ -> assert false) or another sentinel) or add a clarifying
comment, so the intent is explicit and the code fails clearly if to_asset
becomes used; update the occurrence of of_string' and call site expectations
around of_string and Dimension.of_string accordingly.
- Around line 113-119: The update_frontmatter function currently merges
attributes with Cmarkit.Attributes.merge without emitting a diagnostic for
duplicate keys; change update_frontmatter (in Toplevel_attributes) to detect
overlapping attribute keys between the existing toplevel_attributes (a) and the
incoming v, and when conflicts exist emit the same InconsistentOption diagnostic
used by combine_opt (or the module's diagnostic constructor) before performing
the merge; keep using Cmarkit.Attributes.merge to produce the combined list but
ensure you call the diagnostics API with a clear message and context (same style
as combine_opt) referencing the conflicting key(s).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3416f9e3-838f-4289-be61-ea27e6e69f1b

📥 Commits

Reviewing files that changed from the base of the PR and between 23c9831 and 102589c.

📒 Files selected for processing (2)
  • src/compiler/frontmatter.ml
  • test/compiler/multi-frontmatter.t/run.t

Comment thread test/compiler/multi-frontmatter.t/run.t
@panglesd panglesd merged commit a787178 into main May 5, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant