Skip to content

Rough Markdown

@rougher-stuff/markdown adds first-class hand-drawn annotations to CommonMark and GFM without taking over existing Markdown delimiters.

Directive syntax

Visual shorthand and the generic mark form normalize to the same AST node:

md
:underline[Important text]{color=#e5484d seed=42}
:mark[An API]{type=circle padding=4}

:::box{padding=8}

## An annotated section

:::

Semantic directives let the runtime theme decide how an idea appears:

md
:important[Database migration]
:warning[Breaking API]
:question[Why does this happen?]
:todo[Implement the cache]
:changed[New behavior]

Parse and render

ts
import { parse, renderMarkdown } from '@rougher-stuff/markdown';

const tree = parse(markdown);
const html = renderMarkdown(markdown);

parse() produces mdast with roughAnnotation, roughSemanticAnnotation, roughAnnotationContainer, roughReference, and roughCallout nodes. renderMarkdown() emits safe semantic HTML with data-rough-* enhancement hooks.

Mount annotations

ts
import { mountRoughAnnotations, renderMarkdown } from '@rougher-stuff/markdown';

article.innerHTML = renderMarkdown(markdown);

const controller = mountRoughAnnotations(article, {
  theme: {
    warning: { type: 'box', color: '#e5484d', roughness: 1.8 },
  },
});

controller.refresh();
await controller.hide().finished;
controller.remove();

The DOM runtime delegates geometry, shared surfaces, drawing, and animation to @rougher-stuff/rough-notation.

Callouts

Link an inline target and a block note with a safe identifier:

md
:ref[reconciliation]{id=reconciliation}

:::callout{for=reconciliation color=#3b82f6}
React compares the old and new trees here.
:::

The reference remains ordinary readable text without JavaScript. When mounted, the callout gains a marker and connector.

See the package README for the complete attribute schema and unified plugin API.

Released under the MIT License.