Skip to content

Render LaTeX math with KaTeX - #318

Merged
HamptonMakes merged 5 commits into
block:mainfrom
ThePumpingLemma:dgrochowski/math-rendering
Nov 9, 2026
Merged

HamptonMakes merged 5 commits into
block:mainfrom
ThePumpingLemma:dgrochowski/math-rendering

Conversation

@ThePumpingLemma

@ThePumpingLemma ThePumpingLemma commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

What changed and why

Plans and comments can now contain LaTeX math. Before this change, $x^2$ showed as plain text.

Parsing. Commonmarker 2.6.3 exposes comrak's math_dollars and math_code options, so they are now on in MarkdownHelper::EXTENSION_OPTIONS. Every parser already shares that constant: CoPlan::Markdown, MarkdownTextExtractor, SourceTargets, the slide classifier and splitter, changed sections, and content regions. Supported forms:

  • $inline$
  • $$display$$, inline or as its own multi-line paragraph
  • $`inline`$
  • ```math fences

comrak's dollar rules keep currency such as "costs $5 and $10" as text. A spec pins this.

Sanitizer. comrak marks math with data-math-style="inline|display" on span, code (for $`…`$), or pre lang="math" (for fences). data-math-style is the only attribute added to the allowlist. After sanitizing, prepare_math keeps it only on those elements and values at a unique parser-confirmed math source position. Authored HTML imitations lose the attribute and stay anchorable text. Each element that keeps it is wired to the math controller. Because this runs after sanitization, authored markup still can't set data-controller.

Rendering. A new Stimulus controller, coplan--math, typesets each math element with katex.render(source, el, { displayMode, throwOnError: false, trust: false, strict: "warn" }). KaTeX 0.19.1 is pinned in the importmap from jsDelivr with preload: false, the same way Mermaid is pinned. Its stylesheet is version-pinned and loaded with a SHA-384 integrity hash. Fonts load relative to that stylesheet.

  • Lazy loading. The controller is attached to the math elements themselves, so a page without math never loads KaTeX.
  • Coverage. Math renders on every surface that uses render_markdown: the plan page, comments and comment previews, slides and decks, footnote and references sections, version pages, and editor previews.
  • Fallbacks. The TeX source stays visible until both KaTeX and its CSS have loaded, and stays visible if either fails to load. KaTeX shows an expression it can't parse as its source in the theme's danger color. Any other error restores the source text.
  • Other renderers. Math fences are left out of highlight.js. The writing-provenance inspector skips math.

Comment anchors. Math is an atomic, non-anchorable region, like Mermaid source. The TeX glyphs that KaTeX draws don't spell anything in the source, so keeping the source mappable would mean the server and the browser disagree about the text. Instead, both sides leave math out:

  • Server: MarkdownTextExtractor skips :math nodes and math code blocks.
  • Browser: text_selection_controller adds [data-math-style] to the elements it leaves out of the anchor text model.

The effect on selections:

  • A selection across a formula anchors on the text around it, and its source range covers the formula.
  • A selection that starts or ends inside a formula snaps to the nearest text outside it.
  • A selection wholly inside one formula offers no comment.

Agents that anchor on raw TeX through the API still resolve to the source range, but they get no highlight. The guide tells them to anchor on the surrounding text.

Rich editor. A markdown-it inline rule follows comrak's dollar rules. A spec checks it against Commonmarker on about 40 tricky lines.

  • Representation. Math becomes a math mark over raw TeX, delimiters included. It is editable inline and serializes verbatim with escape: false.
  • Round-trip. Unchanged and edited paragraphs both keep their math byte for byte.
  • Fences. ```math fences stay code blocks and are not syntax-highlighted.

Docs and demo. The agent Markdown guide has a new "Math" section. A new demo, math-gallery ("Math: estimating a retry budget"), appears under Tables and rich text and in docs/DEMO_LIBRARY.md. It shows inline math, display math, a math fence, math in lists and tables, a six-row retry-budget slide, dollar amounts that stay text, and an invalid expression that falls back to its source.

Cache. RENDER_CACHE_VERSION goes from 24 to 28, reserving a distinct value from #311 (25), #313 (26), and #314 (27). The final combined-render merge must use a fresh version above every previously deployed value.

No Google Docs or other exporters needed changes.

Evidence

Light and dark screenshots are attached below. Updated gallery, dense-slide evidence, and maintenance validation.

How to try it

  1. Run bin/setup --skip-server, bin/rails db:seed, and bin/rails server -p 3105 (set COPLAN_DEMO_BASE_URL=http://localhost:3105 before the first seed).
  2. Sign in at http://localhost:3105/sign_in as hampton@example.com.
  3. Open http://localhost:3105/hampton/demo-content/rich-text/math-estimating-a-retry-budget, or Demo library → Tables and rich text → Math: estimating a retry budget.
  4. You should see the following:
    • inline formulas in prose, list items, and table cells;
    • two centered display formulas (a $$ block and a math fence);
    • "$0.002 … $5 … $10 and $25" as plain text;
    • \frac{1}{ in red as readable source.
  5. Select from "probability" across $p^n$ to "With" and press C. The comment anchors on the surrounding text, and the formula stays typeset.
  6. Toggle the theme. The math follows the text color.
  7. Choose Edit. Math shows as editable TeX. Change a word in a paragraph that contains math, choose Done, and check that the math source is unchanged in Raw mode or History.

Testing

  • bundle exec rspec spec/helpers/markdown_helper_spec.rb spec/models/comment_thread_anchor_spec.rb spec/helpers/slideshows_helper_spec.rb spec/lib/development_seed_spec.rb spec/requests/agent_instructions_spec.rb spec/services/plans/writing_provenance_spec.rb spec/services/coplan/markdown_spec.rb spec/lib/stylesheets_spec.rb spec/system/math_rendering_spec.rb spec/system/comment_anchoring_spec.rb spec/system/mermaid_anchor_spec.rb spec/system/writing_provenance_spec.rb passed. Its failures happened only while another run shared the same test database, and they passed when rerun alone.

  • bundle exec rspec spec/system/editor_math_spec.rb spec/system/editor_*_spec.rb spec/system/document_editor_modes_spec.rb spec/system/human_editing_spec.rb spec/system/inline_editor_spec.rb had 228 examples. Its one failure was Selenium's "failed to close window" in editor_caret_spec.rb:16, and that file passes on its own.

  • The full suite, bundle exec rspec, had 3431 examples, 1 failure: spec/system/agent_connections_spec.rb:16, a known flake where Chrome times out closing a window. It is unrelated to this change.

  • bin/rubocop on the changed Ruby files, bin/importmap audit, and bin/brakeman all passed.

  • I checked the demo plan in light and dark mode in headless Chrome on port 3105; the screenshots are listed under Evidence. The system specs drive the comment popover, the outline and § preview for a heading that contains math, and a page without math, which loads no KaTeX.

  • Added or updated real JavaScript-enabled browser specs in spec/system/ for changed UI behavior, covering the user interaction and visible result; or explained why no browser UI is involved.

  • Added model, service, or request specs where needed for domain behavior, authorization, or API cases.

  • Ran bundle exec rspec, or explained why it could not run and listed the specs that did run.

  • Tested affected UI behavior in both light and dark mode, or explained why mode testing does not apply.

  • Attached a video or images demonstrating the change, if this PR adds or changes a feature or fixes a UI bug.

demo-dark demo-light

Enable comrak's math_dollars and math_code extensions everywhere plan
Markdown is parsed, allow only data-math-style on comrak's math
elements, and typeset them on the client with a lazily loaded KaTeX
Stimulus controller. Math is atomic for comment anchoring, round-trips
through the rich editor, and has a demo-library example and agent guide
section.
@ThePumpingLemma
ThePumpingLemma marked this pull request as ready for review November 7, 2026 23:58
@HamptonMakes

Copy link
Copy Markdown
Collaborator

🤖 @codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-08T00:07:12.536058Z 42c8899 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Signed-off-by: Hampton Lintorn-Catlin <hampton@squareup.com>

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

Copy link
Copy Markdown

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: 42c88994e8

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

ThePumpingLemma and others added 2 commits November 7, 2026 23:54
- readableText reads a section whose root is a math fence as TeX source.
- The slide classifier bills inline math as text and display math by
  rows; math fences are display math, not code slides. SLIDE_SPEC.md
  documents the cost with conformance examples.
- Only parser-generated math keeps data-math-style: an element must sit
  at the unique source position of a math node, so authored HTML that
  imitates math stays ordinary, anchorable text.
Signed-off-by: Hampton Lintorn-Catlin <hampton@squareup.com>
@HamptonMakes

HamptonMakes commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 Maintenance preparation requested by @HamptonMakes; product acceptance, approval, and merge remain his decision.

  • 8b0c5c02 reserves render cache 28 and adds browser coverage for stylesheet failure/recovery and untrusted TeX.
  • Adopted the contributor’s 52e4873b fixes for fence previews, slide sizing, and authored math attributes. All actionable Codex threads are resolved. The delimiter finding was disproved using the pinned serializer, browser regressions, and a manual prose edit; evidence is in that thread. The original review covered 42c88994; these bounded corrections do not need a duplicate review request.
  • dcb4c142 waits for exactly two highlights before asserting their original texts, and adds the six-row slide to the shared math gallery with reseeding coverage and agent guidance. The failed CI artifact visibly contained both highlights after the assertion: this synchronization fix is separate from the product fixes above. CI-seed 8307 replay passed: 148 examples.
  • Reconciled validation: 167 focused examples plus the demo reseeding example passed, and RuboCop passed. The earlier full local run was stopped to reconcile review fixes after 2,305 passing examples, not reported as a completed suite. Both originally reported flaky spec files passed in the initial 239-example focused run.
  • Current head: dcb4c142. All 15 checks passed on this exact head (CI run). GitHub reports MERGEABLE / BLOCKED / REVIEW_REQUIRED.

Exercised the actual browser in both themes: prose edits preserve raw TeX, currency stays literal, table selection waits for an intentional comment action, and the six-row formula fits in presentation mode. Browser regressions cover selections across/inside formulas, heading previews, lazy loading, failure recovery, and trust boundaries.

Local preview stays running: http://localhost:3318/hampton/demo-content/rich-text/math-estimating-a-retry-budget — sign in as hampton@example.com, no password. Use Math on slides → Present, or Edit → Raw to inspect source. The formula-only prose comment limitation remains a product choice: anchor surrounding text; table cells support their own discussions.

Integration note: cache 28 avoids #311=25, #313=26, and #314=27. The final combined-render merge must use a fresh value above every previously deployed value.

Light Dark
Math gallery in light mode Math gallery in dark mode

Six-row retry-budget formula in presentation mode

@HamptonMakes
HamptonMakes merged commit af594b7 into block:main Nov 9, 2026
15 checks passed
@HamptonMakes

Copy link
Copy Markdown
Collaborator

🤖 Post-merge verification and release-note evidence

Merged by Hampton as af594b7. Post-merge CI completed successfully on that exact merge commit.

Release-note text: CoPlan now renders inline and display LaTeX with KaTeX across plans, comments, previews, tables, and slide decks. The rich editor preserves editable TeX through prose edits; currency and code stay literal. Math loads lazily and retains readable source if loading or parsing fails. Prose discussions can anchor surrounding text across formulas, while table cells support independent discussions.

Review, focused test results, and light/dark and dense-slide screenshots document the validation. Render cache version is 28; subsequent combined rendering changes must use a fresh value above deployed versions.

The local preview remains running at http://localhost:3318/hampton/demo-content/rich-text/math-estimating-a-retry-budget. No further acceptance gate or release publication is part of this housekeeping.

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