Files
Marcos Paulo e8c8c06d1e
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: smart folder model, media time fragments, link card URL guard (MiniMax); specs for 10 more gaps (Codex) and the registry with bloat measurements
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 14:41:26 -03:00

8.7 KiB

Markdown slides mode (agy gap 16, roadmap 41)

Part of specs/plugins-parity/. Supersedes roadmap 19 “Slides view”: one implementation, one acceptance set; do not create a second slides plugin. SPEC 6.1 still calls Slides deferred; implementation completion must reconcile that status. Existing Markdown, engine and native UI; no new crate/dependency.

Research (2026-10-21; agy claims checked)

  • Obsidian's core Slides documents note-tab/command entry, --- slide separators, arrow/Space navigation and Escape exit. Verified directly; prefer current lowercase help URL over agy's legacy path. https://help.obsidian.md/plugins/slides
  • Pandoc documents presentations and browser-backed slide output. That is inspiration, not NotesCraft's runtime: NotesCraft remains Rust/egui, without reveal.js, JavaScript or webview. https://pandoc.org/MANUAL.html#slide-shows
  • agy's Marp and SiYuan comparisons are not necessary to this design and are dropped rather than treated as verified feature parity. Effort “S” is unverified: parser ambiguity, buffer consistency and focus/fullscreen restoration require tests.

Problem / goal

Present an existing Markdown note as navigable slides without copying it into another format. Text, local images, lists, tables, math and links reuse native note rendering. Enter and exit without modifying source, losing editor selection or changing normal reading mode.

Non-goals

HTML/browser export, animation, speaker notes, remote control, presenter second display, arbitrary CSS/Marp directives, slide editing during presentation, automatic PDF slide pagination, new renderer.

Design decisions

D1 Deck model and separators

Pure model in crates/markdown/src/slides.rs uses pulldown-cmark already available in that crate, offset events plus existing frontmatter boundaries; do not split the raw string on ---. Deck { slides: Vec<Slide> }, Slide { range: Range<usize> }, parse_deck(&str) -> Result<Deck>. Ranges are source UTF-8 byte ranges, excludes frontmatter and separator lines, preserve contents/newline bytes; source always remains unchanged. A separator is a root-level CommonMark thematic-break event whose complete line, after optional CR removal, is exactly ---, with a blank line or body boundary on each side. Not ***, ___, ----, indented/list/quote breaks, setext underlines, code-fence contents, HTML/Obsidian comments, math blocks or frontmatter delimiters. Existing protected spans plus parser container context determine eligibility. Unclosed fenced/comment regions never create slides inside their content.

Empty/body-only/frontmatter-only note yields one possibly empty slide; explicit leading/trailing/repeated valid separators preserve empty slides (no silent dropping). Cap input 2 MiB and 500 slides, reject excess (too_large) instead of truncating. No unchecked slicing; ranges refer to original source and all get accesses checked. Keep module publicly re-exported from crates/markdown/src/lib.rs.

D2 Engine session and command surface

id params result effect
slides.start {path, slide?} 1-based default 1 {path, slide, total, generation} capture latest open-buffer source into session presentation
slides.status none {active:false} or active path/slide/total/generation no mutation
slides.goto {slide} 1-based active status exact validated slide index
slides.stop none {active:false} release snapshot; idempotent

Presentation state in engine slides/{mod,state,commands}.rs, one optional field in Session. Commands do not write disk or dirty buffers and work read-only. Start uses buffer source including unsaved edits when open, guarded note read otherwise. Invalid deck/path/index returns structured error and preserves prior active state. Goto outside 1..total returns error, never clamps; UI disables boundary arrows. Only UI maps Next/Previous to a known-valid index. Store one bounded immutable Arc<str> snapshot plus ranges and a generation; view obtains snapshot via internal session accessor (not huge JSON in status). If source changes externally during a show, current snapshot stays fixed; Restart presentation explicitly builds a fresh snapshot. Switching/closing vault clears presentation. Assistant gate grants status READ; start/goto/stop use the existing WRITE-class gate because they alter session state (this does not mean the commands write files). Existing write grants/review policy applies; no new permission system. Do not let assistant status reads start fullscreen; if review cannot represent a presentation-only change, return the existing unsupported-preview refusal instead of bypassing review. CLI/MCP/control callers can inspect and navigate the same state; desktop shell observes generation and active status.

D3 Native UI

New views/slides_view/{mod,controls}.rs renders current slide through existing note_view::show_body_with_fences hooks with editable:false. Relative paths resolve against original note, never a copied temporary note. Slide subsections are parsed separately for painting but all link/asset lookup uses source note path. Disable task toggles, toolbar mutations and editable transclusions; links can open a note only through explicit exit/open action. Missing media uses existing placeholder; diagram/media resources use existing bounded caches. No IO/engine calls while painting a block.

Presentation occupies active window content and requests fullscreen using eframe viewport commands; preserve prior fullscreen flag, focused tab/pane, cursor/selection, scroll and panel layout. Exit restores exactly previous fullscreen state (already-fullscreen windows remain fullscreen). Shortcut input only consumed while presentation active and no modal/input owns it: Right/PageDown/Space next, Left/PageUp previous, Home/End first/last, Escape exit. One keypress makes one state transition. On-window controls offer previous/next, current/total, exit and restart; localized labels and accessible focus. Panel chrome hidden for show, not persisted to workspace layout. No global hotkey registration.

Default 16:9 content area fits viewport with themed letterboxing. Reuse theme tokens and note type scale; scroll tall slide vertically with clear overflow affordance rather than clipping unreadable content or silently reducing text below readable size. Scroll resets on navigation, preserved while repainting same slide. Frame remains bounded by same source/render caps. Native widgets only; no browser engine. Entry via note tab menu and command palette; no mandatory new chord.

Acceptance criteria

  1. Frontmatter, setext headings, fenced ---, quoted/list rules, comments/math and CRLF fixtures produce documented slide counts/ranges and lossless original source.
  2. Empty, repeated-boundary and no-final-newline sources work; non-UTF-8 offsets cannot occur; input/slide overflow errors preserve current deck.
  3. Start reads unsaved buffer; navigation and exit leave buffer bytes, dirty flag and undo history unchanged; read-only vault supports full presentation.
  4. Engine/control and UI use same slide index; invalid start/goto does not replace active state; stop idempotent; no filesystem writes.
  5. Arrow/Space/Home/End/Escape, pointer controls, modal focus and repeated key events behave predictably; editor caret/focus/scroll, panels and former fullscreen state restore after exit.
  6. Local asset relative resolution, code/table/math, tall-slide overflow and missing image render correctly; source edits during presentation affect only Restart, never shift current deck unexpectedly.
  7. Mocha/Latte screenshots at 1440x900 and narrow/2x viewport read; no raw user strings or per-block IO; parsing/rendering bounded; regression gates green.

Open owner questions

  • Q1: accept exact --- with blank-line boundaries as v1 compatibility rule, including explicit empty slides? Proposal yes; other thematic-break markers stay ordinary note content.
  • Q2: approve fullscreen in existing active window rather than second presenter window? Proposal yes; capture/restore previous fullscreen and editor state.
  • Q3: tall-slide handling: proposal scroll with visible overflow affordance; automatic fit-to-text and paginated PDF export remain later features.

Feature ideas (researched, ranked)

  1. Source-position-aware “Start from current slide”, using same deck ranges: https://help.obsidian.md/plugins/slides
  2. Export slides through optional installed Pandoc after a separate native-to-export compatibility contract: https://pandoc.org/MANUAL.html#slide-shows
  3. Speaker notes and incremental reveal require an explicit syntax and native rendering policy; compare documented Pandoc presentation conventions before extending grammar: https://pandoc.org/MANUAL.html#slide-shows