remark-rehype-github-markdown-theme-aware-assets
remark-rehype-github-markdown-theme-aware-assets
libraryRender 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/renderAssetsproduce 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 β
remarkAssetEmbedandrehypeAssetEmbedtransform nodes exposingdata.assetsinto rendered content (replace, append, or wrap in-place). - π‘οΈ Structured runtime errors β Validation problems surface as
AssetValidationErrorinstances; plugins downgrade them tovfilemessages 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)β validatedAsset[]buildAssetNodes(assets, options?)βElementContent[]renderAsset(asset, options?)β HTML stringrenderAssets(assets, options?)β HTML stringrenderAssetDetailed(asset, options?)β{ html, node, asset, options }renderAssetsDetailed(assets, options?)β{ html, nodes, assets, options }getFallbackSrc(asset, baseTheme)β helper to pick the right fallback imageisAssetValidationError(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,srcfor markdown assets) are missing, - theme-specific constraints are violated (e.g.
srcLightwithoutincludeThemedPicture).
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
bitf
A tiny and robust library to manage bitflags / bitsets / optionsets in TypeScript & JavaScript
faviconize
π Library and CLI responsible for generating favicons in all formats and providing easy way to integrate them into your application
image-square-wizard
image square wizard pads rectangular or panoramic images to a square canvas using libvips, auto measuring the domimant color with options to customize