A "Semantic-First" Markdown processing toolkit based on unified, remark and rehype.
@isdk/mdast-plus is a powerful extension to the unified ecosystem, designed to provide consistent, semantic-first normalization and transformation of Markdown content. It simplifies complex processing pipelines with a fluent API and a staged plugin system.
- Fluent API: Chainable interface
mdast(input).use(plugin).toHTML(). - Staged Plugins: Organize transformations into
parse,normalize,compile,finalize, andstringifystages. - Semantic Normalization:
- Directives: Canonicalizes admonition names and extracts titles from labels.
- Table Spans: Support for
rowspanandcolspanin HTML output. - Code Meta: Structured parsing of code block metadata strings.
- Image Sizing: URL "sugar" support (e.g.,
image.png#=500x300) for image dimensions. - Inline Styles: Built-in support for
==Highlight==,~Subscript~, and^Superscript^.
- Deeply Typed: Built on TypeScript with full support for unist/mdast module augmentation.
npm install @isdk/mdast-plus
# or
pnpm add @isdk/mdast-plusimport { mdast } from '@isdk/mdast-plus';
const html = await mdast(':::warning[Special Note]\nBe careful!\n:::')
.toHTML();
// Result: <div title="Special Note" class="warning"><p>Be careful!</p></div>You can pass options to input plugins (like remark-gfm or remark-parse) using the second argument of .from():
// Enable single tilde strikethrough (~text~)
const md = await mdast('Hello ~world~')
.from('markdown', { remarkGfm: { singleTilde: true } })
.toMarkdown();You can request metadata (like that from html-readability) to be attached directly to the returned string object.
const result = await mdast(htmlInput)
.useAt('parse', htmlReadabilityPlugins)
.toMarkdown({ attachMetadata: true });
// result is a String object
console.log(result.toString()); // The Markdown content
console.log((result as any).title); // The extracted title
console.log((result as any).author); // The extracted author
console.log((result as any).metadata); // The collection metadata which include title, author etcconst html = await mdast('').toHTML();
// Result: <img src="cat.png" alt="Cat" width="500" height="300">// Get the fully processed AST (after normalization)
const ast = await mdast('==Highlighted==').toAST();
// Get the raw AST (after parsing, before normalization)
const rawAst = await mdast('==Highlighted==').toAST({ stage: 'parse' });You can stop the pipeline at any stage to inspect the intermediate AST, even when targeting a specific output format like html or markdown.
// Run 'markdown' pipeline but stop after 'parse' stage
// Returns the VFile with the AST at that point
const vfile = await mdast(input).to('markdown', { stage: 'parse' });
const ast = vfile.result;You can override plugin options at the moment of execution using the overrides option in .to(). This is useful for adjusting behavior dynamically without rebuilding the pipeline.
await mdast(input)
.use({ name: 'myPlugin', plugin: myPlugin, options: [{ foo: 'bar' }] }) // Default option
.to('html', {
overrides: {
myPlugin: { foo: 'baz' } // Override for this run only
}
});import { htmlReadabilityPlugins } from '@isdk/mdast-plus';
const vfile = await mdast(myInput)
.data({ myGlobal: 'value' })
// Add multiple plugins as an array at the 'compile' stage
.use([pluginA, pluginB])
// Or add a set of plugins at a specific stage with options
.useAt('parse', htmlReadabilityPlugins, {
url: 'https://example.com/article',
frontmatter: true, // Inject metadata as YAML frontmatter
sourceLink: true // Append source link at the bottom
})
.priority(10) // Run later than default plugins
.to('markdown');
console.log(vfile.value); // The serialized Markdown with frontmattermdast-plus uses unified internally. If you add the same plugin function multiple times, the last configuration overrides the previous ones.
Warning: Passing
falseas a plugin option (e.g.,.use(myPlugin, false)) will disable the plugin entirely. For regular plugins, this means they simply won't run. For plugins marked asmain: true(like replacements for the default parser), if they are disabled withfalse, they will not replace the default plugin of that stage, providing a safe fallback to the default behavior. If you want to bypass a plugin's logic while keeping it active (e.g. to maintain its parser), use an options object like{ enable: false }instead.
// The plugin will run ONCE with option: 2
pipeline.use(myPlugin, { option: 1 });
pipeline.use(myPlugin, { option: 2 });To run the same plugin logic multiple times (e.g., for different purposes), provide a distinct function reference:
// The plugin will run TWICE
pipeline.use(myPlugin, { option: 1 });
pipeline.use(myPlugin.bind({}), { option: 2 });You can register custom input or output formats:
import { MdastPipeline, mdast, PipelineStage } from '@isdk/mdast-plus';
// Register a custom output format
MdastPipeline.register({
id: 'reverse',
output: [{
plugin: function() {
this.Compiler = (tree) => {
// your custom stringification logic
return '...';
};
},
stage: PipelineStage.stringify
}]
});
const result = await mdast('Hello').to('reverse');Note: Format names are case-insensitive (always converted to lowercase internally).
Plugins are executed based on their stage, order, and semantic constraints (before/after):
- parse (0): Input parsing (e.g.,
remark-parse). - normalize (100): Cleanup and canonicalize the tree.
- compile (200): High-level semantic transformations.
- finalize (300): Final preparation before output (e.g.
rehype-sanitize). - stringify (400): Output generation.
Each stage can have one "main" plugin. If a plugin is marked with main: true, it will replace the first plugin in that same stage. This is useful for swapping out default parsers or compilers while keeping the rest of the pipeline intact.
Note: Only one main plugin is allowed per stage. If multiple plugins are marked as main, only the last one defined will take effect as the replacement.
| Plugin | Stage | Description |
|---|---|---|
normalize-directive |
normalize | Handles aliases (warn -> warning) and extracts titles. |
normalize-table-span |
normalize | Migrates table cell spans to hProperties. |
extract-code-meta |
normalize | Parses title="foo" from code block meta. |
image-size |
normalize | Parses #=WxH from image URLs. |
normalize-inline-styles |
normalize | Standardizes ==mark==, ~sub~, and ^sup^. |
html-readability |
parse | Uses Mozilla's Readability to extract main content from HTML. Supports frontmatter injection, sourceLink (source link) footer, and smartExcerpt for intelligent summary management. Use htmlReadabilityPlugins array for easier setup. |
url: (string) The URL of the HTML document.frontmatter: (boolean | 'yaml' | 'toml') Whether to inject metadata as frontmatter. Default:false.sourceLink: (boolean) Whether to append source link at the bottom. The link will be generated based on the original title/URL even if they are filtered or renamed in thefieldsoption. Default:false.fields: (string[] | object) Control which metadata fields are kept or how they are renamed.- If an array: acts as an allowlist (e.g.,
['title', 'excerpt']). - If an object: maps original keys to new names (e.g.,
{ title: 'headline' }). Only keys in the map are kept (Projection).
- If an array: acts as an allowlist (e.g.,
extraMetadata: (object) Extra key-value pairs to inject into the frontmatter. These will be merged with the readability metadata.smartExcerpt: (boolean | object) Whether to remove the excerpt if it is a duplicate or near-duplicate of the main content. Default:true.threshold: (number) The ratio of excerpt length to content length (0.0 to 1.0). Default:0.6.minContentLength: (number) Minimum length of the main content required to keep the excerpt. Default:300.
Please see CONTRIBUTING.md for guidelines on how to contribute to this project.