comark-critic
Critic
Suggest an edit right in the text, comment on a phrase, and render the page with the changes shown, accepted, or rejected.
The deploy waits when more than twelvefifteen pigeons sit on the roof.
Count them afterbefore the stand-up, from the east windowThe west one faces the parking lot..
Pizza is on FridayPineapple still needs a vote..
The deploy waits when more than {--twelve--}{++fifteen++} pigeons sit on the roof.
Count them {~~after~>before~~} the stand-up, from the {==east window==}{>>The west one faces the parking lot.<<}.
Pizza is on Friday{>>Pineapple still needs a vote.<<}.Insertions render as <ins>, deletions as <del>, highlights as <mark>, and comments as a small muted span. Set mode to accept or reject to render the same Markdown with the changes made or undone.
| Mark | Markdown | Renders as |
|---|---|---|
| Insertion | {++added++} |
<ins class="ccm-ins">added</ins> |
| Deletion | {--removed--} |
<del class="ccm-del">removed</del> |
| Substitution | {~~old~>new~~} |
the deletion, then the insertion |
| Highlight | {==this==} |
<mark class="ccm-mark">this</mark> |
| Comment | {>>why<<} |
<span class="ccm-comment">why</span> |
A highlight followed by a comment, {==east window==}{>>Not the west one.<<}, reads as a comment on the highlighted text. Text inside a mark is Markdown, so it can hold **bold**, `code`, and links, and {.class} right after a mark adds its attributes, next to the mark's own class.
Each mark opens and closes in one paragraph. Inside a code span it stays as written.
| Mark | view |
accept |
reject |
|---|---|---|---|
| Insertion | <ins> |
its text | nothing |
| Deletion | <del> |
nothing | its text |
| Substitution | both | the new text | the old text |
| Highlight | <mark> |
its text | its text |
| Comment | the span | nothing | nothing |
Issues
Section titled "Issues"A mark with no closing half in its paragraph stays as written, and the plugin adds a sentence to meta.issues, as it does for a substitution without ~> and a mode it doesn't know. Each one has its own rule in Doctor:
critic `{++` has no closing `++}` in its paragraph; close it there, or write `\{++` to keep the text.
critic `{~~old~~}` has no `~>` between the old and the new text; write `{~~old~>new~~}`.Escape the brace, \{++, to write the characters themselves.
Examples
Section titled "Examples"Review, then publish
Section titled "Review, then publish"Keep the suggestions in the source while a draft is in review, and render it twice: once with mode: "view" for the reviewers, and once with mode: "accept" for the page you publish. The source stays one file, and nobody copies the accepted text by hand.
const review = await parseMarkdown(draft, { plugins: [critic()] });
const published = await parseMarkdown(draft, { plugins: [critic({ mode: "accept" })] });Removing a word leaves the spaces around it, so a {--big--} duck accepts as a duck. HTML collapses the two spaces into one.
Without the plugin
Section titled "Without the plugin"Comark reads a { in text as the start of attributes. Without this plugin, {++fifteen++} doesn't stay as text: the attribute rule takes the whole mark and renders nothing, so a suggestion vanishes from the page without a warning. The plugin's rule runs first and leaves every other brace, such as {.class} and {#id}, to Comark.
The options go under critic in comarkUi(), or to critic() when you use the plugin alone.
Options
Section titled "Options"critic({ mode: "accept" }) renders the document with every change made, and mode: "reject" with none of them. The default, "view", shows them. Modes has what each mode does to each mark.
Styles
Section titled "Styles"The package's styles tint insertions green, strike deletions in red, highlight in amber, and draw comments as chips. Without them, the browser underlines <ins>, strikes <del>, and paints <mark> yellow, and comments are plain text. Import them with yours:
import "comark-critic/style.css";Or inline the same styles from css:
import { css } from "comark-critic";The plugin has no component to add them, so comark-ui re-exports css as criticCss from every entry; put it in the page.
Package
Section titled "Package"comark-ui brings comark-critic and registers it. To use it alone, install it and register the plugin:
pnpm add comark @comark/html comark-criticimport { parseMarkdown } from "comark";
import critic from "comark-critic";
const document = await parseMarkdown(markdown, {
plugins: [critic()],
});It has no components: insertions, deletions, and highlights are native tags that every renderer outputs as they are. It works with registerDefaultPlugins: false and in streaming parses.
Each mark carries a class, so the styles reach only CriticMarkup and not a ~~strikethrough~~, which Comark also writes as <del>. An insertion is tinted green over a line, a deletion struck through in red, a highlight amber, and a comment a muted chip. They read these shared tokens:
| Token | Used for |
|---|---|
--comark-accent |
The comment's chip |
--comark-border |
The chip's edge |
--comark-foreground |
A deletion's text, mixed with its red |
--comark-muted |
The comment's text |
--comark-radius |
Corners |
The colors are variables of their own, read wherever they're set, on :root or any element around the marks. The deletion's red is shadcn/ui's --destructive when the page has it:
| Variable | Default |
|---|---|
--ccm-ins |
Green |
--ccm-del |
--destructive, else red |
--ccm-mark |
Amber |
The rules sit in a comark CSS layer, so any plain rule of your own wins. Recolor them as a pull request does, for instance:
:root {
--ccm-ins: #1a7f37;
--ccm-del: #cf222e;
}shadcn/ui before Tailwind 4 stores --destructive as bare HSL numbers, so map it as Theming does the shared tokens: --ccm-del: hsl(var(--destructive)).