Skip to content

feat(slides): import slidey markdown decks#587

Merged
steipete merged 30 commits into
mainfrom
land/583-slidey-import
May 15, 2026
Merged

feat(slides): import slidey markdown decks#587
steipete merged 30 commits into
mainfrom
land/583-slidey-import

Conversation

@steipete
Copy link
Copy Markdown
Collaborator

Summary:

  • import slidey-flavored Markdown decks in slides create-from-markdown
  • support per-slide YAML frontmatter, notes, multi-column directives, Font Awesome icons, Mermaid images, and dry-run request previews
  • document slidey Markdown conventions and add a full fixture/regression suite

Testing:

  • make ci
  • go test ./internal/cmd -run 'Test(ParseMarkdownToSlides|RenderSlides|AssetPipeline|Build|SlidesToAPIRequests|Layout|Blocks|Inlines|Frontmatter|Slidey)'
  • go test ./internal/cmd -run 'TestRenderSlide_CenterLayoutWithOnlyTitleDoesNotStyleEmptyBody|TestRenderSlide_HeroLayoutLargeTitleNoTitleBox|TestRenderSlide_HeroStyleRangeUsesUTF16|TestParseMarkdownToSlides_ShorthandColumnsWithColsMentionInsideFence' -count=1
  • dry-run E2E: bin/gog --account clawdbot@gmail.com --json --no-input slides create-from-markdown 'Slidey Dry Run Smoke' --content-file testdata/slidey/index.md --dry-run --mmdc '' produced 35 slides, 15 image requests, 15 placeholder URLs
  • codex-review: clean, no accepted/actionable findings

Live E2E:

  • attempted with keychain account clawdbot@gmail.com; blocked by local OAuth refresh token failure: invalid_grant from Google before deck creation

Credits:

Co-authored-by: Nicholas Reid njreid@gmail.com

njreid and others added 28 commits May 13, 2026 12:22
Captures grammar additions (per-slide frontmatter, ## Notes, FA shortcodes,
mermaid fences, columns, ::boxes::/::arrows::), AST shape, layout mapping,
asset pipeline (jsDelivr SVG-direct, mmdc PNG), and test strategy for the
upcoming gog slides create-from-markdown rework.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Breaks the spec into 20 bite-sized TDD tasks: AST scaffold, frontmatter
splitter, inline/block parsers, layout helpers, asset pipeline (FA
fetch + mmdc + Drive upload + cleanup), renderer per layout, end-to-end
fixture, docs + CHANGELOG. Each task is self-contained with failing-test,
implementation, verification, and commit steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces Slide, Block, Inline, IconRef, ImageRef and per-block types
in slides_markdown_ast.go. Replaces the legacy types/parser/renderer in
slides_markdown.go and slides_formatter.go with compilable stubs so the
package builds; Tasks 2-8 fill the parser, Tasks 15-18 fill the renderer.
Updates slides.go call site to match new ParseMarkdownToSlides signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review feedback: Go convention drops type-prefix repetition on
struct fields. Field has zero callers yet, so rename is risk-free.
Plan updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the §4.1 disambiguation rule: a "---" opens frontmatter only
when followed by a YAML key line; otherwise it is a slide separator.
Unclosed frontmatter is a fatal error with a line-numbered message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review hardening: strip \r\n at the entry point so downstream
TrimSpace and yaml.Unmarshal calls don't see lingering \r bytes from
Windows-authored decks. Pure addition; tests unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tokenizes a line of markdown into TextRun and IconRef inlines. Style
derivation matches §4.6 (fa→default, fas→solid, far→regular, fab→brands,
fal/fad→solid with free-tier substitution).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks slide body line-by-line and emits Block AST nodes. Inline parsing
delegates to parseInlines from Task 3. Column, boxes/arrows, and mermaid
marker handling is added in subsequent tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rkers

Recursive call into parseBlocks per column body. Accepts ::right:: as a
synonym for ::col2:: (slidey allows both for the 2-column case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each row may begin with a leading FA shortcode (boxes) or a heading
prefix (arrows). Trailing text becomes the row label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Other languages remain CodeBlock. Each diagram gets a stable ID so
the asset pipeline (Task 11) can pair it with an uploaded image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ParseMarkdownToSlides now: splits frontmatter blocks, parses each
slide's body via parseBlocks, hoists the first h1 (or h2 fallback) as
the slide title for non-hero/title/statement layouts, extracts the
trailing "## Notes" section as raw speaker notes (FA shortcodes stripped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MapSlideyLayout collapses the slidey frontmatter values into a small
LayoutKind enum. ColumnBoxes/SingleBodyBox/TitleBox compute box
rectangles in points from the presentation page size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AssetMap pairs AST IconRefs/DiagramBlocks with their Drive ImageRefs.
DefaultAssetPipelineConfig sets a 30s HTTP timeout and the standard
mmdc binary. The FA URL builder targets the FA Free 6.x jsDelivr path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fetchFAIconFromURL performs a GET against jsDelivr (or any URL injected
in tests via httptest) and returns the SVG bytes. 404/non-200 are wrapped
errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mmdcCommandArgs builds the standardized invocation; renderMermaidWithBinary
writes a temp .mmd, runs mmdc with a transparent background and 2x scale,
and returns the rendered PNG. Temp files are cleaned up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AssetPipeline.Resolve walks all slides for unique IconRefs and
DiagramBlocks, fetches/renders each, and uploads via the Uploader
abstraction. Per-asset failures warn-and-skip unless Strict. Cleanup
deletes the tracked Drive files unless KeepTempImages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DriveUploader wraps drive.Service to upload bytes, set anyone-reader
permission, and return the WebContentLink — mirrors the pattern in
slides_add_slide.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RenderSlides emits CreateSlide + title box + body box for default and
center layouts. Returns a SlideNotesPlan slice consumed by the second
BatchUpdate (Task 18). Body text is the legacy "flatten blocks to one
string" form; columns and image insertion follow in Tasks 16/17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SectionHeader-kind layouts emit one centered body box with the first
line styled at 44pt bold. two-cols/three-cols emit N side-by-side body
boxes positioned via the geometry helpers from Task 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagram blocks render as full-width images below the title. Bullets
whose first inline is an IconRef get a small (18pt) icon image rendered
to the left of the body box, vertically aligned to the bullet line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreatePresentationFromMarkdownV2 runs the full pipeline:
create presentation → derive geometry → resolve assets → first
BatchUpdate → re-fetch → second BatchUpdate for notes → cleanup.

SlidesCreateFromMarkdownCmd gains --fa-style, --mmdc, --strict,
--keep-temp-images, --no-notes. Existing --dry-run prints the
would-be BatchUpdate JSON without any network for fetch/render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copies univrs/slidey/slides/index.md into testdata and asserts the
parser produces all expected layout kinds, notes, icons, and diagrams,
then runs the renderer with a fake asset map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the synthetic 30-slide fallback fixture with the actual
univrs/slidey/slides/index.md deck (969 lines). Tests still pass —
this gives the e2e check real-world content shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds reference for per-slide frontmatter, ## Notes, FA shortcodes,
mermaid blocks, columns, and ::boxes::/::arrows::. Updates the flag
table for the new --fa-style/--mmdc/--strict/--keep-temp-images/
--no-notes flags. CHANGELOG entry under 0.17.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drive uploader now best-effort deletes the uploaded file when
  Permissions.Create or Files.Get fails, so a permission failure
  no longer orphans assets in the user's Drive.
- Drop the legacy CreatePresentationFromMarkdown stub and the unused
  slideElementTitle const; the CLI now calls
  CreatePresentationFromMarkdownV2 directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues surfaced by running the import against a live Google
account on the univrs/slidey/slides/index.md deck:

- Notes BatchUpdate failed with "Invalid requests[0].deleteText: The
  startIndex 0 must be less than the endIndex 0" because freshly-created
  slides have empty notes boxes and DeleteText{ALL} rejects that. Drop
  the leading DeleteText and just InsertText — notes are always empty
  on a slide we just created.
- Font Awesome shortcodes like `:fa-dev:` 404 because "dev" is only
  published under brands/, not solid/. Add a fallback that re-tries
  the brands/regular/solid styles when the requested style 404s. Other
  errors (network, 5xx) still fail immediately without guessing.
- mmdc errors only surfaced as bare exit codes ("exit status 1"). Use
  CombinedOutput so stderr (puppeteer chromium download issues, mermaid
  syntax errors, etc.) ends up in the warning message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Nicholas Reid <njreid@gmail.com>
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 15, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgolang/​github.com/​yuin/​goldmark@​v1.8.299100100100100

View full report

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 535633b941

ℹ️ 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".

Comment on lines +55 to +56
if (layout == LayoutKindDefault || layout == LayoutKindCenter) && explicitColumnCount > 0 {
if explicitColumnCount == 2 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve third column content when layout is forced to two-cols

RenderSlides only upgrades to LayoutKindThreeCols when the frontmatter layout is default or center, so a slide declared as layout: two-cols but containing an explicit 3-column block keeps n=2. That causes the third parsed column to be dropped by findColumnsBlock (it truncates to n), silently losing user content whenever ::col3:: is present with mismatched frontmatter. Please either honor explicit column count regardless of base layout or fail fast on the mismatch.

Useful? React with 👍 / 👎.

Comment on lines +131 to +132
if columnStart < len(lines) && headingRE.MatchString(lines[columnStart]) {
columnStart++
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize non-ATX headings before shorthand column wrapping

normalizeShorthandColumns treats only headingRE matches as the pre-column title line, which misses valid Markdown title forms like indented ATX headings and setext headings. In two-cols/three-cols shorthand slides, those titles get wrapped inside ::cols::, and later hoistTitle cannot extract them because it only inspects top-level blocks, leaving slides without the expected title box text.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41cb77912e

ℹ️ 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".

Comment on lines +593 to +595
for i, b := range blocks {
out[i%n] = append(out[i%n], b)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep fallback two-column content in source order

When a slide uses layout: two-cols/three-cols but has no explicit ColumnsBlock, this fallback round-robins blocks with out[i%n]. That interleaves sequential content across columns (for example, paragraphs 1/3 in col1 and 2/4 in col2), which reorders the author’s narrative and can separate related blocks unexpectedly. The fallback should preserve order (e.g., keep content in the first column unless explicit markers exist) or return a validation error instead of repartitioning by index.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e3e14ac84

ℹ️ 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".

Comment on lines +74 to +75
if !inFence && isBareDelimiter(lines[i]) {
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Ignore indented code when splitting on slide delimiters

splitMarkdownIntoSlideBlocks treats any trimmed --- as a slide boundary unless it is inside a fenced block, but it does not account for indented code blocks. In vanilla Markdown, a line like --- is code content, yet this logic will split the slide there, which can truncate code snippets and create unintended extra slides whenever indented code examples include ---.

Useful? React with 👍 / 👎.

Comment on lines +65 to +67
t := strings.TrimSpace(line)
if t == "## Notes" || t == "### Notes" {
b := strings.Join(lines[:i], "\n")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip notes heading detection inside indented code blocks

splitOutNotes only suppresses ## Notes detection while inside fenced code, so an indented code line such as ## Notes is still matched after TrimSpace and incorrectly treated as the start of speaker notes. That removes the rest of the slide body and moves code content into notes for decks that use indented code blocks.

Useful? React with 👍 / 👎.

Comment on lines +339 to +341
if v.Ordered {
fmt.Fprintf(&b, "%d. ", j+1)
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve ordered list start numbers in rendered text

Ordered list numbering is always regenerated as j+1 during rendering, so Markdown lists that intentionally start at another number (for example 3., 4.) lose their original numbering in the produced slide text. Because the parser also drops the list start value, this behavior is deterministic and affects any non-1 ordered list input.

Useful? React with 👍 / 👎.

@steipete steipete merged commit 8a14a04 into main May 15, 2026
11 checks passed
@steipete steipete deleted the land/583-slidey-import branch May 15, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants