remark-rehype-github-markdown-theme-aware-assets

library
1
TypeScript
Oct 12, 2025
Overview

Render precomputed asset links into <picture> / <img> HTML backed by HAST nodes that honour light/dark themes - especially useful when embedding theme-aware media resources inside GitHub's README. The core builder produces hastscript elements first, then offers an optional stringifier for consumers outside the Unified pipeline.

Features

  • 🎯 Asset-first API – renderAsset / renderAssets produce stable markup without any collection/section abstractions.
  • 🧩 HTML + HAST helpers – Build reusable hastscript nodes and optionally stringify them for environments outside Unified.
  • πŸ”Œ Unified-ready plugins – remarkAssetEmbed and rehypeAssetEmbed transform nodes exposing data.assets into rendered content (replace, append, or wrap in-place).
  • πŸ›‘οΈ Structured runtime errors – Validation problems surface as AssetValidationError instances; plugins downgrade them to vfile messages so pipelines keep running.
  • πŸ§ͺ Snapshot-tested – Vitest coverage verifies renderer output and integration scenarios.

Installation

pnpm add remark-rehype-github-markdown-theme-aware-assets
# or
npm install remark-rehype-github-markdown-theme-aware-assets

Data Model

type ThemedAsset = {
  alt: string
  href: string
  metadata?: Record<string, unknown>
  includeThemedPicture?: true // default: themed <picture>
  srcLight?: string
  srcDark?: string
  baseTheme?: 'light' | 'dark' // required when only one themed asset exists
}

type MarkdownAsset = {
  alt: string
  href: string
  includeThemedPicture: false
  src: string
  metadata?: Record<string, unknown>
}

type Asset = ThemedAsset | MarkdownAsset

type AssetRenderOptions = {
  includeThemedPicture?: boolean // default true
  baseTheme?: 'light' | 'dark' // default 'dark'
  singleLineOutput?: boolean // default false
  indent?: string // for multi-line HTML output
}

When includeThemedPicture is omitted (the default), provide themed sources via srcLight and/or srcDark. If only one variant exists, set baseTheme so fallback images are picked correctly. Trying to mix themed/markdown fields is caught at runtime by the built-in validator.

Input safety
The library does not escape or sanitize strings. Provide already-safe values for alt, href, and src* fields that are suitable for direct insertion into HTML.

Core Usage

import {
  buildAssetNodes,
  renderAsset,
  renderAssets,
  renderAssetDetailed,
  renderAssetsDetailed,
} from 'remark-rehype-github-markdown-theme-aware-assets'

const ci = {
  alt: 'CI Status',
  href: 'https://github.com/acme/project/actions',
  srcLight: 'https://img.shields.io/github/actions/workflow/status/acme/project/ci.yml?theme=light',
  srcDark: 'https://img.shields.io/github/actions/workflow/status/acme/project/ci.yml?theme=dark',
}

const docs = {
  alt: 'Documentation',
  href: 'https://acme.dev/docs',
  src: 'https://img.shields.io/badge/docs-success.svg',
  includeThemedPicture: false,
}

renderAsset(ci)
// => themed <picture> markup string (multi-line by default)

renderAssets([ci, docs], { singleLineOutput: true })
// => both assets rendered on a single line, separated by a space

renderAssetDetailed(ci, { baseTheme: 'light' })
// => { html, node, asset, options } without recomputing

const result = renderAssetsDetailed([ci, docs])
// result.html -> combined output
// result.nodes -> HAST nodes (including separators)
// result.assets[1].options.includeThemedPicture === false

const nodes = buildAssetNodes([ci, docs])
// => ElementContent[] suitable for manual HAST manipulation

API at a glance

  • normalizeAssets(assets) β†’ validated Asset[]
  • buildAssetNodes(assets, options?) β†’ ElementContent[]
  • renderAsset(asset, options?) β†’ HTML string
  • renderAssets(assets, options?) β†’ HTML string
  • renderAssetDetailed(asset, options?) β†’ { html, node, asset, options }
  • renderAssetsDetailed(assets, options?) β†’ { html, nodes, assets, options }
  • getFallbackSrc(asset, baseTheme) – helper to pick the right fallback image
  • isAssetValidationError(error) β†’ boolean

Unified Integration

remark

Transform placeholder nodes that carry an assets array in their data into rendered HTML strings:

import { remark } from 'remark'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import { remarkAssetEmbed } from 'remark-rehype-github-markdown-theme-aware-assets'

const processor = remark()
  .use(remarkParse)
  .use(() => tree => {
    tree.children.push({
      type: 'asset-placeholder',
      data: { assets: [ci, docs] },
      children: [],
    })
  })
  .use(
    remarkAssetEmbed({
      injectionMode: 'wrap', // 'replace' | 'append' | 'wrap'
      wrapTagName: 'div',
      wrapClassName: 'asset-grid',
    })
  )
  .use(remarkStringify)

The plugin swaps the placeholder for raw HTML string output. Validation issues show up as vfile.message entries so your pipeline can decide how to proceed.

rehype

Inject <picture> / <img> nodes directly into HAST while keeping render metadata on the original node:

import { rehype } from 'rehype'
import rehypeStringify from 'rehype-stringify'
import { rehypeAssetEmbed } from 'remark-rehype-github-markdown-theme-aware-assets'

const processor = rehype()
  .data('settings', { fragment: true })
  .use(() => tree => {
    tree.children.push({
      type: 'element',
      tagName: 'div',
      data: { assets: [ci, docs] },
      children: [],
    })
  })
  .use(
    rehypeAssetEmbed({
      injectionMode: 'replace',
      wrapTagName: 'div',
      wrapProperties: { className: ['asset-wrapper'] },
    })
  )
  .use(rehypeStringify)

Each processed node receives an assetRenderResult with the combined markup and per-asset metadata, which can be inspected by downstream plugins.

Error Handling

normalizeAssets (and every renderer built on top of it) throws an AssetValidationError when:

  • the asset list is empty or undefined,
  • an entry is not an object,
  • required fields (alt, href, src for markdown assets) are missing,
  • theme-specific constraints are violated (e.g. srcLight without includeThemedPicture).

Each error carries a machine-readable code plus a path pointing to the offending field. Unified plugins catch these errors and emit human-friendly messages instead of crashing.

License

MIT

More Projects

Explore other projects that might interest you

View all