Swap remarkable for markdown-it#4884
Merged
Merged
Conversation
remarkable is unmaintained (last release ~2020); markdown-it is the
de-facto successor and shares the same linkify-it engine, so URL detection
behavior is preserved. Mechanical near-drop-in: linkify plugin → linkify
option, unescapeMd → unescapeAll, state.push({obj}) → state.push(type,
tag, nesting). Custom image renderer added so backslash-escaped
punctuation in alt text is preserved (markdown-it's default
renderInlineAsText skips text_special tokens). Allowlist gains `s` for
~~strike~~; documented in a NOTE so future plugin additions and raw-HTML
pass-through are explicit. Markdown.spec.ts gains feature-coverage
regression tests (linkify, typographer, code highlight, tasklist,
DOMPurify, alt escapes).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4884 +/- ##
==========================================
- Coverage 46.55% 46.55% -0.01%
==========================================
Files 832 832
Lines 34131 34120 -11
Branches 5833 5833
==========================================
- Hits 15890 15884 -6
+ Misses 16239 16237 -2
+ Partials 2002 1999 -3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…nescapeAll Address Greptile inline review on PR #4884. - `imageAltHandler` now mirrors markdown-it's own `renderInlineAsText` semantics for multi-line alt text by mapping `softbreak`/`hardbreak` children to `'\n'` instead of dropping them. - Add a comment to `handleImage` explaining that `unescapeAll` is a no-op for markdown images (already decoded by `imageAltHandler`) but still does work for raw HTML `<img alt="...">` written directly in markdown source — preserving pre-migration behavior. - New regression test covering the softbreak case.
Member
Author
|
@greptileai please re-review and update the confidence score |
* parseTasklist: switch to `startsWith`, separate token types (`tasklist_checked`/`tasklist_unchecked`), add word-boundary check so `a[x]b` is left as text. Renamed exports to `parse`/`CHECKED`/`UNCHECKED`. * Drop `imageAltHandler` and the `unescapeAll(alt)` call in `handleImage`; use markdown-it defaults. Loses backslash-escape decoding inside alt text (rare edge case) but keeps DOMPurify XSS protection unchanged. * Update SANITIZE_OPTS comment: strikethrough is part of the default preset, and add `input` to the raw-HTML examples list.
Comments should describe current state, not contrast with prior libraries.
Member
Author
|
@greptileai please re-review. Notable changes since last review:
PR description updated to reflect the current state. |
Convert the eight feature-coverage tests added in this PR to the input/output template-literal + exact toBe() form used by the pre-existing tests. Names are capitalized verb phrases. Exact HTML output replaces partial toContain/toMatch checks, so future markdown-it upgrades will surface unintended rendering changes.
Test names read as the subject of an implicit "it" — they should agree in person/number. Convert "Process / Detect / Strip / ..." to "Processes / Detects / Strips / ..." across Markdown.spec.ts and parseTasklist.spec.ts. Negative-form names already use "Does not …", which is the correct third-person inflection of "do".
Pin exact HTML output via toMatchInlineSnapshot instead of hand-written output strings. Maintenance under markdown-it / highlight.js upgrades is now `vitest --update` plus a PR diff review, instead of editing the expectations by hand and risking copy-paste drift.
Member
Author
|
@greptileai please re-review. Changes since last review:
|
Drop the second `renderPlain` renderer. The test-fixture processors now return scheme-valid URLs (`https://link/...`, `https://image/...`) so a single `render` instance works for tests that exercise processor behavior and tests that don't — the URLs survive DOMPurify's URL validator either way. Snapshots regenerated.
remarkable for markdown-it
markdown-it calls inline rules with `silent=true` during lookahead via `ParserInline.skipToken` — used internally by `parseLinkLabel` when scanning inner tokens of image/link labels. Without the guard, the rule was pushing tokens during this scan, corrupting state.tokens: - `![ [x] image](url)` rendered `☑<img alt="image">` (spurious glyph before the image, alt content stripped). - `[ [x] inner](url)` rendered `☑[ ☑ inner](url)` (no link, doubled glyph). Fix follows the pattern used by markdown-it's own `link.mjs`: still advance `state.pos` and return true/false, but only `state.push(...)` when `!silent`. Adds two unit tests for the silent-mode contract and one integration snapshot for the nested-image-label case. Reported by greptile-apps on PR #4884.
Reframe the remarkable→markdown-it entry around CommonMark + GFM conformance and enumerate the non-standard Pandoc / PHP-Markdown-Extra shortcut syntaxes that no longer render as markdown (raw inline HTML for the corresponding tags still works).
Member
Author
|
@greptileai please re-review |
8 tasks
drernie
previously approved these changes
May 11, 2026
nl0
previously approved these changes
May 12, 2026
Co-authored-by: Alexei Mochalov <[email protected]>
nl0
approved these changes
May 12, 2026
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
remarkable(last release ~2020) formarkdown-it, the de-facto successor. Samelinkify-itengine, so URL detection is unchanged.linkifyplugin →linkify: trueoption,state.push({obj})→state.push(type, tag, nesting)then mutate.parseTasklistported to markdown-itStateInlineAPI; also cleaned up internally — string matching viastartsWith, two separate token types (tasklist_checked/tasklist_unchecked) instead of one mutable.checkedproperty, word-boundary check soa[x]bis left as text, andsilent-mode guard soparseLinkLabel's lookahead doesn't push spurious tokens.ALLOWED_TAGSgainss(markdown-it strikethrough →<s>). Existing tags retained, including those that only reach the sanitizer via raw HTML pass-through (input,abbr,dd,dl,dt,ins,mark,sub,sup,del). A NOTE comment now documents the two categories so the rationale survives future edits.remarkable@1async comment inPreview/loaders/Markdown.jsxremoved; underlying TODO preserved.Markdown.spec.tsextended with feature-coverage cases (code highlight, linkify, nofollow, tasklist glyphs incl. escaped-bracket, mid-word rejection, and nested-image-label regression, typographer, DOMPurify script-strip). All existing strict-string assertions preserved as inline snapshots — markdown-it produces byte-identical HTML for those cases.Note on alt text
Inside markdown image syntax
, escape sequences (\!), HTML entities (&), and inline`code`aren't reflected in the renderedaltattribute. This is markdown-it's default behavior (markdown-it/markdown-it#1142) — its image renderer concatenates a fixed subset of inline token types when building alt. Raw HTML<img alt="...">is left untouched; attribute values are inert text per HTML5 parsing rules. No workaround locally — the patterns are uncommon in real markdown, and the rendered output is well-formed in all cases.This PR also drops the
unescapeMd(alt)step previously run inside the DOMPurifyhandleImagehook. It was a remarkable-era leftover: remarkable stored the raw source slice inaltwithout decoding escapes, so the hook decoded them post-hoc. Both current paths now handle decoding themselves — markdown-it's inline tokenizer stores the decoded character intext_special.content(subject to #1142 above), and the browser's HTML5 parser decodes entities in raw<img alt="...">at parse time. The removed call additionally applied markdown backslash-escape rules to raw-HTML attribute values, which was a category error (markdown semantics on HTML-authored attributes).Test plan
npm run lint(Prettier + ESLint) passesnpm run test:only -- parseTasklist— 11 tests pass (incl. silent-mode contract)npm run test:only -- Markdown— 28 tests pass (Markdown 13 + parseTasklist 11 + Preview/quick/Markdown 4)npm run buildclean (only preexisting bundle-size warnings)<Bucket/Summarize>— render aquilt_summarize.jsonREADME block<Preview/loaders/Markdown>— open a.mdfile preview<Preview/renderers/Markdown>— render an inline preview<Chat/Message>— send a chat message with markdown<Assistant/UI/Chat/Chat>— assistant response with code fencesOut of scope
parseTasklistwithmarkdown-it-task-lists(would change rendered output:<input>checkboxes vs ☐/☑ glyphs; the maintained plugins are also stale, last updated 8+ years ago).abbr/mark/sub/sup/ins/deflist/footnoteparser features via the matchingmarkdown-it-*plugins (the corresponding tags are already inALLOWED_TAGS).unified/remark+rehypemigration (dropsdangerouslySetInnerHTMLbut is a much larger refactor across 5 consumers).<Markdown>for bundle-size reduction (~250 KB gzipped) — independent of this swap.Rollback
Single PR, single revert if anything regresses. No data migrations, no persisted state.
Greptile Summary
This PR replaces the unmaintained
remarkablelibrary withmarkdown-itacross the catalog's markdown rendering pipeline. The migration is near-drop-in, preserving all existing output for supported syntax while gaining proper CommonMark/GFM conformance.parseTasklistis ported to markdown-it'sStateInlineAPI with a correctsilent-mode guard (no token pushes during lookahead), a word-boundary check to prevent mid-word[x]matches, and two distinct token type constants (tasklist_checked/tasklist_unchecked) replacing the old single mutable token.Markdown.tsxdrops the remarkable-eraunescapeMdpost-processing ofaltattributes;linkifyandtypographerare now options rather than plugins;'s'is added toALLOWED_TAGSfor markdown-it's~~strikethrough~~output.Confidence Score: 5/5
Safe to merge — the migration is a clean swap with no functional regressions identified; all previously flagged concerns have been addressed in this branch.
The silent-mode contract in parseTasklist is correctly implemented: tokens are not pushed during lookahead, state.pos is advanced as markdown-it's skipToken requires, and the spec explicitly verifies both branches. The word-boundary guard closes the mid-word false-positive. The unescapeMd removal is justified by markdown-it decoding at the token level for parser-generated images and the browser's HTML5 parser handling entities in raw img alt attributes. The ALLOWED_TAGS addition of 's' is narrowly correct for strikethrough. Test coverage is meaningfully extended for every changed behaviour.
No files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["markdown source string"] --> B["MarkdownIt.render()"] B --> B1["inline rules chain"] B1 --> B2["link rule"] B1 --> B3["escape rule"] B1 --> B4["tasklist.parse (pushed last)"] B4 --> B4a{"isAtBoundary?"} B4a -- "no" --> B4b["return false (skip)"] B4a -- "yes" --> B4c{"silent mode?"} B4c -- "yes" --> B4d["advance pos, return true (no token pushed)"] B4c -- "no" --> B4e["push tasklist_checked or tasklist_unchecked token"] B --> C["renderer.rules: tasklist_checked→☑, tasklist_unchecked→☐"] C --> D["HTML string"] D --> E["DOMPurify.sanitize(html, SANITIZE_OPTS)"] E --> E1["uponSanitizeElement hook → handleLink / handleImage"] E --> F["sanitized HTML string"] F --> G["dangerouslySetInnerHTML in Container"]Reviews (5): Last reviewed commit: "Merge branch 'master' into worktree-rema..." | Re-trigger Greptile