feat(prose): add @comark/prose — framework-agnostic prose components (WIP) - #386
Open
atinux wants to merge 7 commits into
Open
feat(prose): add @comark/prose — framework-agnostic prose components (WIP)#386atinux wants to merge 7 commits into
atinux wants to merge 7 commits into
Conversation
User plugin post hooks previously ran before the default plugins' post
hooks, so tree-consuming plugins saw an un-normalized tree (e.g. GFM
alerts were still plain blockquotes because `alert` had not rewritten
them into `['blockquote', { as }]` yet).
Post hooks now run defaults first, then user plugins. Registration
order is unchanged: user plugins still come first so same-name entries
override defaults via dedupePlugins, and a user override keeps the
default's execution slot. `pre` hooks and `markdownItPlugins` keep
their existing order.
htmlAttributes collapsed any 'true' value to a bare attribute. That is correct for HTML boolean attributes (disabled, hidden) but wrong for ARIA: aria-* attributes are enumerated, and an empty value means absent/false (aria-hidden='' is not hidden, aria-selected='' is not selected). aria-* values now stay literal, including boolean false.
New package lowering docs components into plain HTML at parse time, so
every renderer benefits (Vue, React, Svelte, Angular, @comark/html):
- callouts (::note family, ::callout{color}, GFM alerts) ->
<div role=note data-variant> with CSS mask icons
- ::tabs / ::code-group -> <prose-tabs> WAI-ARIA tablist; panels render
stacked without JavaScript
- ::accordion -> native <details name> groups (exclusive by default)
- ::steps -> CSS counters on child headings
- code fences -> figure with filename header + <prose-copy> button
- heading anchor links (h2-h4) and table scroll wrappers
- optional classes/mergeClass map for utility-class design systems and
a per-tag transform escape hatch
Styling and interactivity are decoupled layers:
- components.css / typography.css: token-driven (size/leading/flow),
:where() scoped to .comark-content, append-stable for streaming;
partials exported under styles/*.css (built with lightningcss)
- client runtime: two dependency-free custom elements (<prose-tabs>
with keyboard nav + localStorage group sync, <prose-copy>), safe to
import on the server
Includes docs page, a framework-free example (examples/3.plugins/
html-prose), 27 tests, and the bundle snapshot update.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deriving the hoisted names from the registered defaultPlugins array instead of a hardcoded set removes the duplicated list and makes the hoisting a natural no-op with registerDefaultPlugins: false, where the user's explicit registration order rules. Adds a test for that case.
Same rationale as the post-hook ordering: among the defaults only frontmatter has a pre hook, and running it first gives user pre hooks a normalized contract — state.markdown without the frontmatter block, state.frontmatter and state.frontmatterText already populated — instead of raw text a rewriting plugin could corrupt. One partitioned list now drives both lifecycle phases; markdownItPlugins keep registration order.
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.
What
New package
@comark/prose: makes docs components framework-agnostic by lowering them into plain HTML at parse time, so every renderer benefits — Vue, React, Svelte, Angular, and@comark/htmlstring output. Discussion context: making comark prose work with any styling system (Nuxt UI utility classes, Tailwind Typography, shadcn Typeset) with minimal client JS.Three independent layers, each optional:
1. Plugin (
@comark/prose)::notefamily,::callout{color},> [!NOTE]<div class="prose-callout" role="note" data-variant>(CSS mask icons;role=noteavoids landmark noise from<aside>)::tabs+::tab-item{label}<prose-tabs>WAI-ARIA tablist, deterministic ids::code-group[filename]/language::steps{level}::accordion+::accordion-item{label}<details name>(exclusive;{multiple}opt-out)<figure class="prose-pre">+ filename header +<prose-copy><a href="#id">+ hash icon (skips headings containing links — no nested anchors)Also:
classes/mergeClassmap to bake utility-class design systems into plain tags, and a per-tagtransformescape hatch. Streaming-safe: re-parses never double-wrap, ids stay deterministic.2. CSS (
components.css,typography.css,styles/*.css)--prose-size/--prose-leading/--prose-flow+light-dark()color hooks:where()scoped to.comark-content; plain utilities override without!importantmargin-block-start, no:last-child/:has())typography.cssis optional — Tailwind Typography / shadcn Typeset work as-is on the lowered markup3. Client runtime (
client,client/register)Two dependency-free custom elements, SSR-safe, ~2 kB total:
<prose-tabs>: WAI-APG keyboard nav (arrows/Home/End, automatic activation),data-syncgroup sync persisted inlocalStorage, MutationObserver for streamed panels<prose-copy>: clipboard copy withdata-copiedicon swap and a live-region announcementNo-JS story: tab panels render stacked via
prose-tabs:not(:defined)(content always reachable), copy buttons stay hidden, everything else is pure HTML/CSS.Also included
fix(comark): keep literal aria-* attribute values in HTML output—htmlAttributescollapsed'true'to bare attributes; for ARIA that inverts the meaning (aria-hidden=""is not hidden). Found while building this; separate commit./plugins/prose), framework-free example (examples/3.plugins/html-prose:renderHtml+ one script tag = fully interactive docs page), AGENTS.md section, bundle snapshot (@comark/prose: 74.2k published).Tests
27 new tests: 21 lowering (exact node assertions, streaming re-parse guards, options) + 6 client (happy-dom: click/keyboard/sync/copy). All package suites pass;
pnpm lint+pnpm typecheckclean.Known pre-existing flake (unrelated):
packages/comark/test/index.test.tsSPEC fixtures with 5–10 ms perf-budget hook timeouts occasionally time out under full parallel runs, also onmain.Follow-ups (out of scope)
template-slot (#slot) form of tabsprose: falserenderer option so Nuxt UI apps don't need a components-map overrideclasses/mergeClass)🤖 Opened by an AI coding agent (OpenCode) on behalf of @atinux, who reviewed the changes.