CS-10572: ProseMirror WYSIWYG editor for RichMarkdownField#4349
Draft
CS-10572: ProseMirror WYSIWYG editor for RichMarkdownField#4349
Conversation
Support `:card[URL]` (inline) and `::card[URL]` (block) Boxel Flavored Markdown syntax in MarkdownDef files. Inline references render as atom-format cards, block references as embedded-format. The parsing utility is keyword- generic to support future `:file` syntax (CS-10583). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import BaseDef instead of missing CardDef in markdown.gts - Wrap querySelectorAll with Array.from() for NodeListOf iteration - Use code submode (not card stack) to render file defs in tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import both BaseDef and CardDef from card-api (CardDef has .id) - Remove early-return in captureCardSlots when linkedCards is empty, so fallback text is still set on unresolvable BFM placeholders Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The modifier was setting el.textContent for unresolved BFM references, but Glimmer re-renders would wipe the text. Now the marked extensions include the URL as text content in the placeholder elements, so fallback text is part of the initial HTML and survives re-renders. Also fixes prettier formatting in bfm-card-references.ts and unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip text content from card-type BFM elements in renderedHtml using DOMParser so no URL text is ever in the DOM. The modifier uses a _modifierHasRun flag to skip fallback injection on the first run (when linkedCards is still loading as an empty []), and only injects fallback text on subsequent runs for truly unresolvable refs. Also adds MutationObserver + scheduleOnce to handle back-navigation re-rendering, and a new acceptance test for that scenario. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _modifierHasRun flag prevented fallback text from appearing when the modifier only runs once (all card refs unresolvable). Since text content is already stripped from the HTML, the afterRender fallback injection is brief enough to not cause a visible flash. Also adds @ember/runloop to the expected card references in the indexing tests since the markdown template now imports scheduleOnce. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document why scheduleOnce, MutationObserver, pendingUpdate, and didChange are all load-bearing in the captureCardSlots modifier. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restore _modifierHasRun gate to skip fallback text injection on the first modifier run (when linkedCards is still loading as empty []) - Clean up leftover fallback text nodes when a card later resolves - Guard fallback injection with textContent !== url to prevent infinite MutationObserver loop - Update fallback test to waitUntil the text appears (second modifier run is async) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The loadLinksForResource call caused timeouts for non-markdown files and relativizeDocument broke adoptsFrom URL assertions. The client deserialization pipeline also can't handle included resources in file-meta docs. Instead, cardReferenceUrls is stored in indexed attributes via extractAttributes, letting client-side queries work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eouts The new card-refs.md test fixture was missing from the expected directory listing assertion. Also increased waitUntil timeouts in the back-navigation test from the default 1000ms to 5000ms to avoid CI flakiness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The back-navigation test clicked the inline card reference before the Overlays component had bound its click handler. ElementTracker schedules afterRender, then Overlays re-renders and binds handlers — two render cycles after the card component appears. Wait for cursor:pointer (set by Overlays) as a signal the handler is ready. Also add card-refs.md to the directory listing assertion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cursor:pointer waitUntil was at 5000ms but CI runners under load (widespread Failed to fetch errors for icons/modules) can need longer for the two render cycles before the Overlays click handler binds. Increase to 10000ms to match the other waitFor timeouts in this test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The back-navigation test has three waitUntil calls — overlay handler bind, URL bar after click, and URL bar after history.back(). All involve async card search queries that can be slow on CI runners under load. Bumped all three from 5-10s to 15s and added timeoutMessage to each so future failures identify which step timed out. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code-mode navigation uses replaceState (not pushState), so history.back() has no entry to return to — causing the test to time out on CI and close the browser tab locally. Navigate back via the URL bar instead, which still tests that card references re-render correctly after returning to a markdown file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix document-order preservation in extractBfmReferences(): collect all matches with their index and sort before deduplicating, instead of grouping by keyword which reorders references. - Handle multi-backtick inline code spans (e.g. ``code``) in extraction regex, not just single-backtick spans. - Update doc comment for bfmExtensionsForKeyword() to reflect that placeholders include URL as fallback text content. - Add test for multi-backtick inline code case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Card IDs in the index don't include .json, but users may write :card[http://example.com/my-card.json] in markdown. Strip the .json extension in extractBfmReferences() (so the linksToMany query matches) and in the resolveUrl helper (so slot matching finds the card). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… heading IDs, mermaid Register 5 community marked extensions and 1 custom extension for BFM (Boxel Flavored Markdown) Layer 3 features: - marked-alert: GFM alert/admonition syntax (> [!NOTE], > [!WARNING], etc.) - marked-footnote: footnote syntax ([^1] references + definitions) - marked-extended-tables: colspan/rowspan in markdown tables - marked-gfm-heading-id: auto-generated slug IDs on headings - bfm-math (custom): LaTeX math placeholders ($...$, $$...$$) that emit lightweight placeholder HTML instead of bundling KaTeX (~268KB min + 1.1MB fonts), enabling lazy client-side rendering - Mermaid code blocks (```mermaid) emit a <pre class="mermaid"> placeholder for client-side rendering Each feature has smoke tests covering both markedSync output and DOMPurify sanitization pass-through. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The gfmHeadingId extension now adds id attributes to headings, so test assertions need to match the new output format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lazy-load Mermaid.js (~2MB) to render diagram placeholders emitted by the BFM markdown pipeline. Mermaid is never included in the initial bundle — it's loaded on-demand only when pre.mermaid elements are present in the rendered markdown. Implementation: - Add `mermaid` dependency to host package (webpack code-splits it) - Register `__loadMermaid` global loader in application route (same pattern as Monaco) - Add `renderMermaidDiagrams` modifier to markdown template that queries pre.mermaid elements, lazy-loads mermaid, and calls mermaid.run() with securityLevel: 'strict' - Add CSS for rendered diagrams (transparent background, centered, responsive SVG sizing) - Add acceptance test verifying mermaid code blocks render as SVG On failure, the placeholder <pre> with raw diagram source remains visible as readable fallback text. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lazy-load KaTeX to render math placeholders emitted by the BFM markdown pipeline. KaTeX JS (~268KB) is loaded on-demand only when .math-placeholder elements are present in the rendered markdown. Implementation: - Add `katex` dependency to host package (webpack code-splits the JS) - Import katex.min.css statically in app.ts (24KB, needed for font declarations so glyphs render correctly on first paint) - Register `__loadKatex` global loader in application route (same pattern as Monaco/Mermaid) - Add `renderMathExpressions` modifier to markdown template that queries .math-placeholder elements, lazy-loads KaTeX, and calls katex.render() with throwOnError: false for graceful degradation - Add acceptance test verifying math placeholders render with KaTeX On failure, the placeholder with raw LaTeX source ($E = mc^2$) remains visible as readable fallback text. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
KaTeX CSS references both .woff2 and .woff font files. The webpack config only had a rule for .woff2, causing parse failures on .woff files. Widen the regex to match both extensions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exercises all Layer 3 features: card references, GFM alerts, math/LaTeX, mermaid diagrams, footnotes, extended tables, heading IDs, and code blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Split logical && in assert.true into separate assertions (qunit/no-assert-logical-expression) - Fix prettier formatting across test files - Use eslint-disable block for require() in marked-sync.ts to survive prettier line-wrapping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Once The modifiers were creating new arrow functions on each invocation and passing null as the scheduleOnce target. This differs from the working captureCardSlots pattern which uses this (component instance) and a stable method reference. The changes: - Use this as scheduleOnce target (consistent with captureCardSlots) - Use stable method references (_renderMath, _renderMermaid) for proper scheduleOnce deduplication - Query DOM fresh inside async methods instead of capturing a NodeList that may become stale during async loading - Check el.isConnected before rendering each node Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g ids The modifier-based DOM mutation approach for KaTeX and Mermaid was unreliable because Glimmer re-renders overwrite imperative DOM changes. Refactored to process math and mermaid at the HTML-string level inside the `renderedHtml` getter so autotracking sees the final content. Also fixes: - DOMPurify stripping heading ids by adding `user-content-` prefix to `gfmHeadingId()` (avoids DOM clobbering protection for names like "title", "body", "links") - Missing type declaration for `marked-gfm-heading-id` (CJS resolution in `nodenext` mode) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deep path imports like `@cardstack/runtime-common/bfm-mermaid-render` are not resolved by the card runtime's module loader. Move imports to the main barrel export so they resolve correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…efix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The declaration in runtime-common/global.d.ts wasn't being resolved on CI due to pnpm's module layout differing from the local workspace. Add the declaration directly to local-types where all packages find it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The declare module in local-types/index.d.ts was treated as a module augmentation (not an ambient declaration) because index.d.ts has top-level imports making it a module. Ambient declarations must live in script files with no top-level import/export. Created a separate marked-gfm-heading-id.d.ts with only the declare module block (using import() type expressions to avoid top-level imports), following the same pattern as matrix-js-sdk.d.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These packages declare "type": "module" which triggers TS1479 under Node16/nodenext module resolution since runtime-common lacks "type": "module". Use require() with type assertions, matching the existing pattern for marked-extended-tables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
marked.use({ renderer }) wraps the previous renderer in a new closure on
each call. Since markedSync() called use() on every invocation, this created
an ever-growing closure chain that leaked memory (~MB per test), eventually
crashing Chrome on CI. Fix by using a dedicated Marked instance with the
code renderer registered once at module load time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
esbuild (used by workspace-sync-cli) preserves ESM default exports as
{ default: fn } when bundling to CJS, unlike webpack which unwraps them.
Add unwrapDefault() helper to handle both bundlers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New composite FieldDef that stores markdown content and exposes structured card relationships via query-based linkedCards. Includes integration tests for markdown rendering, BFM card references, footnotes, and edit mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Conflicts resolved by taking the base branch changes: - bfm-showcase.md: user-content- prefix for heading IDs - bfm-math.ts: dynamic displayMode detection - bfm-mermaid-render.ts: DOMPurify sanitization and robust regex Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate ProseMirror as a platform feature for rich markdown editing, following the established lazy-loading pattern used by KaTeX, Mermaid, and Monaco. New files: - packages/host/app/lib/prosemirror-context.ts: lazy-loaded module with ProseMirror schema (doc, paragraph, heading, blockquote, code_block, lists, hr, hard_break, card atom/block placeholders), marks (strong, em, code, link), markdown parser, and markdown serializer - packages/base/prosemirror-editor.gts: Glimmer component with ember-concurrency lazy loading, ember-modifier for EditorView mounting, keyboard bindings, debounced onUpdate, and scoped CSS - packages/host/tests/integration/components/prosemirror-editor-test.gts: 25 tests covering schema, parsing, serialization, round-trip, EditorView mounting, card placeholder rendering, and lazy loading Modified files: - packages/host/app/routes/application.ts: register __loadProseMirror - packages/base/rich-markdown.gts: use ProseMirrorEditor in edit mode - pnpm-workspace.yaml + packages/host/package.json: add prosemirror-* dependencies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Preview deployments |
Host Test Results 1 files ± 0 1 suites ±0 2h 19m 46s ⏱️ - 2m 26s For more details on these errors, see this check. Results for commit ce5f918. ± Comparison against base commit e45d4de. |
Strengthen parser and serializer for proper round-trip fidelity: - Fix combined bold+italic mark serialization (***text***) - Add link title support in parse and serialize - Guard against ::card[URL] being misinterpreted as inline card atom - Auto-derive card atom labels from URL path segment - Overhaul blockquote handling: bare > lines, multi-paragraph support - Fix empty code block serialization - Add 26 comprehensive round-trip tests covering all standard elements, card references with relative/absolute URLs, and edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add card nodeView infrastructure that renders live card components inside
the ProseMirror editing surface using custom nodeViews and Glimmer's
{{in-element}}. Card references (:card[URL] / ::card[URL]) now display
as resolved card components when available, with fallback text otherwise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement /card slash command in ProseMirror editor that lets users insert card references. Typing "/" triggers a contextual command menu, selecting "Card" opens a search/URL-paste popup, then a format picker (inline/block) inserts the reference at the cursor position. The slash command system is extensible for future commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RichMarkdownField, following the established lazy-loading pattern (KaTeX, Mermaid, Monaco)prosemirror-context.tslazy-loaded module: schema with 14 node types (includingboxel_card_atom/boxel_card_blockplaceholders), 4 mark types, hand-rolled markdown parser and serializerProseMirrorEditorGlimmer component in base package: ember-concurrency lazy loading, ember-modifier for EditorView, keyboard shortcuts (bold, italic, code, undo/redo, list operations), debounced saveRichMarkdownFieldedit template now renders ProseMirror instead of plain textareaTest plan
ember test --filter prosemirror-context)🤖 Generated with Claude Code