To write a plugin that traverses an AST (Abstract Syntax Tree) in Remark, you can create a Remark plugin using its unified API. Here's a step-by-step guide:
Make sure you have remark
and related packages installed:
npm install remark
If you need additional utilities, install:
npm install unified visit
The AST generated by Remark follows the
MDAST specification. Nodes have a type
(e.g., paragraph
,
heading
, list
), and may contain children.
A Remark plugin is a function that takes a syntax tree
(ast
) and performs operations on it. Use the
visit
utility to traverse the tree.
Here's an example of a plugin:
const visit = require('unist-util-visit');
function myRemarkPlugin() {
return (tree) => {
// Traverse the AST
visit(tree, 'heading', (node) => {
console.log(`Found a heading with depth ${node.depth}`);
console.log(`Text: ${node.children.map((child) => child.value).join('')}`);
});
};
}
module.exports = myRemarkPlugin;
Apply the plugin to a Markdown file:
const remark = require('remark');
const myRemarkPlugin = require('./myRemarkPlugin');
const markdown = `
# Heading 1
This is a paragraph.
## Heading 2
Another paragraph.
`;
remark()
.use(myRemarkPlugin)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file));
});
unist-util-visit
for selective traversal. visit(tree, 'paragraph', (node) => {
console.log('Found a paragraph:', node);
});
Use unist-util-visit-parents
if you need to know
parent nodes.
Use unist-util-modify-children
for direct child
modification.
You can modify the AST within the visit
callback. For
example, adding text to all headings:
visit(tree, 'heading', (node) => {
node.children.push({
type: 'text',
value: ' (modified)',
});
});
You can chain multiple plugins using remark().use()
.
Use remark-cli
or integrate your plugin in a larger build
pipeline for testing.
npm install remark-cli
remark -u ./myRemarkPlugin input.md
unist-util-visit
for most traversal needs; it's the
go-to utility for working with unified ASTs.
Let me know if you'd like examples for specific use cases! ๐
Manipulating the output in a Remark plugin involves modifying the AST nodes during traversal. After you make changes to the AST, the updated structure will be reflected in the output Markdown.
Hereโs how to manipulate the output step-by-step:
When traversing the AST, you can directly change node properties (like
value
for text nodes) or structure (like adding,
removing, or reordering nodes).
For example, let's transform all headings to uppercase:
const visit = require('unist-util-visit');
function transformHeadingsToUppercase() {
return (tree) => {
visit(tree, 'heading', (node) => {
node.children.forEach((child) => {
if (child.type === 'text') {
child.value = child.value.toUpperCase();
}
});
});
};
}
module.exports = transformHeadingsToUppercase;
You can insert additional nodes into the AST. For example, let's add a note before every heading:
const visit = require('unist-util-visit');
function addNoteBeforeHeadings() {
return (tree) => {
visit(tree, 'heading', (node, index, parent) => {
const noteNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: 'Note: This section is important!',
},
],
};
parent.children.splice(index, 0, noteNode); // Insert the note before the heading
});
};
}
module.exports = addNoteBeforeHeadings;
To remove a node, simply splice it out of its parent:
const visit = require('unist-util-visit');
function removeParagraphs() {
return (tree) => {
visit(tree, 'paragraph', (node, index, parent) => {
parent.children.splice(index, 1); // Remove the paragraph node
});
};
}
module.exports = removeParagraphs;
Reordering involves rearranging nodes in the
parent.children
array. For example, reversing the order
of all headings:
const visit = require('unist-util-visit');
function reverseHeadings() {
return (tree) => {
const headings = [];
visit(tree, 'heading', (node, index, parent) => {
headings.push({ node, index, parent });
});
// Reverse headings in their parents
headings.reverse().forEach(({ node, index, parent }) => {
parent.children.splice(index, 1); // Remove the heading
parent.children.push(node); // Add it back at the end
});
};
}
module.exports = reverseHeadings;
You can store additional information or metadata in a custom format. For example, appending metadata to the tree:
function addMetadata() {
return (tree) => {
tree.children.push({
type: 'html', // Add an HTML comment as metadata
value: '<!-- Generated by My Remark Plugin -->',
});
};
}
module.exports = addMetadata;
If you need to generate specific output, such as converting the Markdown to HTML, integrate with rehype or stringify the Markdown.
const remark = require('remark');
const html = require('remark-html');
remark()
.use(html)
.process('# Hello World', (err, file) => {
if (err) throw err;
console.log(String(file)); // Outputs HTML
});
You can chain multiple changes in a single plugin or combine plugins:
const visit = require('unist-util-visit');
function myCustomTransformations() {
return (tree) => {
// Change headings to uppercase
visit(tree, 'heading', (node) => {
node.children.forEach((child) => {
if (child.type === 'text') {
child.value = child.value.toUpperCase();
}
});
});
// Remove all paragraphs
visit(tree, 'paragraph', (node, index, parent) => {
parent.children.splice(index, 1);
});
};
}
module.exports = myCustomTransformations;
Finally, process and retrieve the updated Markdown:
const remark = require('remark');
const myCustomTransformations = require('./myCustomTransformations');
const markdown = `
# Heading 1
This is a paragraph.
## Heading 2
Another paragraph.
`;
remark()
.use(myCustomTransformations)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file)); // Outputs the transformed Markdown
});
Let me know if youโd like specific help with any manipulation tasks! ๐
Adding a custom block to the Markdown content is straightforward when working with a Remark plugin. Here's how to do it:
Letโs add a custom block, like a "callout" or "notice" block, to the Markdown content. The new block will be inserted as a node in the AST, and Remark will render it when the tree is processed.
Hereโs a plugin that adds a custom block at the beginning of the Markdown document:
function addCustomBlock() {
return (tree) => {
const customBlockNode = {
type: 'paragraph', // Defines the block type; change this as needed
children: [
{
type: 'text',
value: '๐ This is a custom callout block added by the plugin!',
},
],
};
// Insert the custom block at the beginning of the document
tree.children.unshift(customBlockNode);
};
}
module.exports = addCustomBlock;
Apply the plugin to some Markdown content:
const remark = require('remark');
const addCustomBlock = require('./addCustomBlock');
const markdown = `
# Heading 1
This is some content.
`;
remark()
.use(addCustomBlock)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file)); // Outputs the modified Markdown
});
The output will be:
๐ This is a custom callout block added by the plugin!
# Heading 1
This is some content.
If you want to add the block at a specific location (e.g., after the first heading), modify the plugin logic:
const visit = require('unist-util-visit');
function addBlockAfterFirstHeading() {
return (tree) => {
let blockAdded = false;
visit(tree, 'heading', (node, index, parent) => {
if (!blockAdded) {
const customBlockNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: '๐ This block appears after the first heading.',
},
],
};
parent.children.splice(index + 1, 0, customBlockNode); // Insert after the heading
blockAdded = true;
}
});
};
}
module.exports = addBlockAfterFirstHeading;
This ensures that the custom block is added right after the first heading in the document.
If you want your custom block to include HTML, you can use an
html
node:
function addHtmlBlock() {
return (tree) => {
const htmlBlockNode = {
type: 'html',
value: '<div class="custom-block">๐ฅ Custom HTML Block</div>',
};
tree.children.push(htmlBlockNode); // Add it at the end
};
}
module.exports = addHtmlBlock;
The resulting Markdown file will include the custom HTML directly:
# Heading 1
This is some content.
<div class="custom-block">๐ฅ Custom HTML Block</div>
You can make the block content dynamic by examining the AST during traversal. For example, appending a block that lists all headings:
const visit = require('unist-util-visit');
function addDynamicBlock() {
return (tree) => {
const headings = [];
// Collect all headings
visit(tree, 'heading', (node) => {
headings.push(node.children.map((child) => child.value).join(''));
});
// Create a summary block
const summaryBlockNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: `๐ Summary of headings: ${headings.join(', ')}`,
},
],
};
// Add the block at the end of the document
tree.children.push(summaryBlockNode);
};
}
module.exports = addDynamicBlock;
For this Markdown input:
# Introduction
## Features
## Installation
The output will include:
# Introduction
## Features
## Installation
๐ Summary of headings: Introduction, Features, Installation
paragraph
, html
,
code
, etc.
unist-util-visit
to find the right location for
inserting your block.
children
.
Let me know if you want further assistance in building or extending the plugin! ๐
The blockAdded
variable in the code ensures that the
block is added only once, even if multiple nodes
match the condition (e.g., multiple headings in the document). Without
this safeguard, the block would be added after every matched node,
leading to unintended behavior.
blockAdded
Necessary?
Let's break it down:
Traversal with visit
:
visit
function recursively traverses all nodes
of the specified type in the tree.
heading
nodes), the callback will execute
for each of them.
Use Case:
How blockAdded
Works:
false
.true
.
!blockAdded
) will evaluate to false
.
blockAdded
If you remove the blockAdded
check, the block will be
added after every heading in the document. For example:
Input Markdown:
# Heading 1
## Heading 2
## Heading 3
Output without blockAdded
:
# Heading 1
๐ This block appears after the first heading.
## Heading 2
๐ This block appears after the first heading.
## Heading 3
๐ This block appears after the first heading.
Output with blockAdded
:
# Heading 1
๐ This block appears after the first heading.
## Heading 2
## Heading 3
blockAdded
?
Use a similar variable when:
In some cases, you can simplify the logic by "breaking out" of the
traversal early. However, unist-util-visit
does not
support an explicit break. Instead, you can return a custom signal to
indicate the traversal should stop, as shown below:
const visit = require('unist-util-visit-parents');
function addBlockAfterFirstHeading() {
return (tree) => {
visit(tree, 'heading', (node, index, parent) => {
const customBlockNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: '๐ This block appears after the first heading.',
},
],
};
parent.children.splice(index + 1, 0, customBlockNode); // Add the block
return false; // Signal to stop traversal
});
};
}
module.exports = addBlockAfterFirstHeading;
This works similarly but stops further traversal after adding the block.
blockAdded
ensures the block is added only once by
preventing further execution of the callback after the first match.
Yes, you can add a div
with content inside it by creating
a custom HTML block node. This is useful if you want
to insert HTML content directly into your Markdown document.
Hereโs how you can achieve it:
<div>
with Content
function addCustomDiv() {
return (tree) => {
const divNode = {
type: 'html', // Specifies this is an HTML block
value: '<div class="custom-div">This is a custom div with content inside.</div>',
};
// Add the custom div at the end of the document
tree.children.push(divNode);
};
}
module.exports = addCustomDiv;
const remark = require('remark');
const addCustomDiv = require('./addCustomDiv');
const markdown = `
# Heading 1
Some content here.
`;
remark()
.use(addCustomDiv)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file));
});
# Heading 1
Some content here.
<div class="custom-div">This is a custom div with content inside.</div>
<div>
You can dynamically generate the content for the
<div>
by traversing the AST or passing data into
your plugin. For example:
const visit = require('unist-util-visit');
function addDivWithDynamicContent() {
return (tree) => {
let headingTexts = [];
// Collect all heading text
visit(tree, 'heading', (node) => {
const text = node.children
.filter((child) => child.type === 'text')
.map((child) => child.value)
.join(' ');
headingTexts.push(text);
});
// Create the HTML div node
const divNode = {
type: 'html',
value: `<div class="custom-div">Headings: ${headingTexts.join(', ')}</div>`,
};
// Add the custom div at the end of the document
tree.children.push(divNode);
};
}
module.exports = addDivWithDynamicContent;
const remark = require('remark');
const addDivWithDynamicContent = require('./addDivWithDynamicContent');
const markdown = `
# Introduction
## Features
## Installation
`;
remark()
.use(addDivWithDynamicContent)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file));
});
# Introduction
## Features
## Installation
<div class="custom-div">Headings: Introduction, Features, Installation</div>
<div>
If you want to include nested Markdown or specific elements inside the
<div>
, you can generate Markdown or AST nodes and
convert them to HTML.
const visit = require('unist-util-visit');
function addNestedDiv() {
return (tree) => {
// Create nested content
const nestedMarkdown = `
### Nested Content
This is content inside the custom div.
- Item 1
- Item 2
`;
// Process the nested Markdown into an AST
const nestedAst = this.parse(nestedMarkdown);
// Convert the nested AST to HTML
const divContent = this.stringify(nestedAst);
// Create the div node with nested content
const divNode = {
type: 'html',
value: `<div class="custom-div">${divContent}</div>`,
};
// Add the div node at the end
tree.children.push(divNode);
};
}
module.exports = addNestedDiv;
# Heading
<div class="custom-div">
<h3>Nested Content</h3>
<p>This is content inside the custom div.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
html
node for adding raw HTML, such as a
<div>
.
<div>
can be static, dynamic,
or even nested Markdown converted to HTML.
Let me know if you want help with a specific example! ๐
If the dynamic content of the <div>
is going to be
tree nodes (i.e., AST nodes), you can embed those
nodes as children of a custom parent node (e.g., a wrapper node
representing a div
). You don't need to directly use raw
HTML; instead, you can structure the tree to represent the content.
Hereโs how you can create a div
node and embed other AST
nodes inside it:
You can create a custom mdast
-like node for your
<div>
and append or insert other nodes as its
children.
const visit = require('unist-util-visit');
function wrapContentInDiv() {
return (tree) => {
// Create a custom "div" node
const divNode = {
type: 'div', // Custom type
data: {
hName: 'div', // Specify the HTML tag name for rendering
hProperties: { className: 'custom-div' }, // Add class or other attributes
},
children: [], // Add tree nodes dynamically here
};
// Collect specific nodes to move into the div (e.g., all paragraphs)
const nodesToWrap = [];
visit(tree, 'paragraph', (node, index, parent) => {
nodesToWrap.push({ node, index, parent });
});
// Remove the collected nodes from the tree and add them to the div
nodesToWrap.forEach(({ node, index, parent }) => {
parent.children.splice(index, 1); // Remove the node from its parent
divNode.children.push(node); // Add the node to the div's children
});
// Add the div node at the end of the document
tree.children.push(divNode);
};
}
module.exports = wrapContentInDiv;
Remark and related tools (like remark-html
) will render
the custom node as a <div>
if you use the
data
properties (hName
and
hProperties
) in the node.
Input Markdown:
# Heading
This is a paragraph.
Another paragraph.
Output:
<h1>Heading</h1>
<div class="custom-div">
<p>This is a paragraph.</p>
<p>Another paragraph.</p>
</div>
If the content inside the <div>
is dynamically
generated tree nodes (not just existing nodes), you can create and
insert new nodes into the children
of your custom
div
node.
function addDynamicDivWithNodes() {
return (tree) => {
// Create the custom div node
const divNode = {
type: 'div',
data: {
hName: 'div',
hProperties: { className: 'dynamic-div' },
},
children: [
{
type: 'heading',
depth: 2,
children: [{ type: 'text', value: 'Dynamic Heading' }],
},
{
type: 'paragraph',
children: [{ type: 'text', value: 'This content is dynamically generated.' }],
},
],
};
// Add the div node to the end of the tree
tree.children.push(divNode);
};
}
module.exports = addDynamicDivWithNodes;
Input Markdown:
# Title
Output:
<h1>Title</h1>
<div class="dynamic-div">
<h2>Dynamic Heading</h2>
<p>This content is dynamically generated.</p>
</div>
If you want to selectively wrap nodes based on conditions (e.g., wrap
all list
and paragraph
nodes in a
<div>
), you can:
const visit = require('unist-util-visit');
function wrapSpecificNodesInDiv() {
return (tree) => {
// Custom wrapper node
const divNode = {
type: 'div',
data: {
hName: 'div',
hProperties: { className: 'custom-wrapper' },
},
children: [],
};
// Visit specific node types to wrap (e.g., paragraphs and lists)
const nodesToWrap = [];
visit(tree, (node) => {
if (['paragraph', 'list'].includes(node.type)) {
nodesToWrap.push(node);
}
});
// Add the collected nodes to the div's children
divNode.children.push(...nodesToWrap);
// Filter out the wrapped nodes from the tree and add the div
tree.children = tree.children.filter((node) => !nodesToWrap.includes(node));
tree.children.push(divNode);
};
}
module.exports = wrapSpecificNodesInDiv;
Custom Node Types:
type: 'div'
is not an official
mdast
node, but you can use it with
data.hName
to define how it should render (e.g., as
a <div>
).
remark-html
or
rehype
respect data.hName
and
data.hProperties
.
Dynamic Child Nodes:
children
array of your custom wrapper node.
Preserve Order:
<div>
at a specific
position in the tree, use the parentโs
children.splice
method.
Let me know if you'd like additional examples or a walkthrough for a specific use case! ๐
The different node types you can work with in Remark are defined by the MDAST specification (Markdown Abstract Syntax Tree). This specification is part of the syntax-tree ecosystem and provides a structured way to represent Markdown content as an AST.
The official MDAST documentation is the best place to learn about node types. It provides detailed descriptions of:
Hereโs an overview of some commonly used node types and their structure:
root
heading
,
paragraph
, list
, etc.
{
"type": "root",
"children": [...]
}
heading
#
,
##
, etc.).
depth
: Heading level (1
for
#
, 2
for ##
, etc.).
text
or
emphasis
.
{
"type": "heading",
"depth": 1,
"children": [
{ "type": "text", "value": "Heading Text" }
]
}
paragraph
text
,
emphasis
, or strong
.
{
"type": "paragraph",
"children": [
{ "type": "text", "value": "This is a paragraph." }
]
}
text
value
: The text content.{
"type": "text",
"value": "Plain text"
}
list
ordered
: true
for ordered lists
(1.
, 2.
), false
for
unordered lists (-
, *
).
start
: Starting number for ordered lists.listItem
nodes.{
"type": "list",
"ordered": true,
"children": [
{
"type": "listItem",
"children": [
{ "type": "paragraph", "children": [{ "type": "text", "value": "Item 1" }] }
]
}
]
}
listItem
paragraph
or
text
nodes.
{
"type": "listItem",
"children": [
{ "type": "paragraph", "children": [{ "type": "text", "value": "Item 1" }] }
]
}
blockquote
>
).
paragraph
nodes.
{
"type": "blockquote",
"children": [
{ "type": "paragraph", "children": [{ "type": "text", "value": "Quoted text." }] }
]
}
code
lang
: Language of the code block (e.g.,
javascript
).
value
: Code content.{
"type": "code",
"lang": "javascript",
"value": "console.log('Hello, world!');"
}
inlineCode
value
: Code content.{
"type": "inlineCode",
"value": "const x = 10;"
}
thematicBreak
---
or ***
).
{
"type": "thematicBreak"
}
link
url
: The hyperlink URL.title
: Optional title for the link.text
.
{
"type": "link",
"url": "https://example.com",
"title": "Example",
"children": [
{ "type": "text", "value": "Click here" }
]
}
image
url
: Image URL.alt
: Alternative text.title
: Optional title.{
"type": "image",
"url": "https://example.com/image.png",
"alt": "An example image",
"title": "Example Image"
}
You can inspect the structure of the AST for a specific Markdown file to see all available node types:
const remark = require('remark');
const markdown = `
# Heading
This is a paragraph.
- List item 1
- List item 2
> Blockquote
`;
remark().parse(markdown, (err, tree) => {
if (err) throw err;
console.log(JSON.stringify(tree, null, 2)); // View the AST
});
The output will include all node types and their structure.
You can also create custom nodes with a type
of your
choice. Just ensure you handle rendering appropriately using tools
like remark-html
or rehype
.
Let me know if you'd like more detailed help with a specific type! ๐
To replace a paragraph
node with a link
node
in the AST using a Remark plugin, you need to:
paragraph
nodes.paragraph
node with a
link
node.
Here's how you can do it step by step:
Below is an example plugin that replaces all
paragraph
nodes with a link
node:
const visit = require('unist-util-visit');
function replaceParagraphWithLink() {
return (tree) => {
visit(tree, 'paragraph', (node, index, parent) => {
// Replace the paragraph with a link node
const linkNode = {
type: 'link',
url: 'https://example.com',
title: 'Example Site',
children: [
{
type: 'text',
value: 'Click here to visit Example.com!',
},
],
};
// Replace the paragraph node with the link node
parent.children[index] = linkNode;
});
};
}
module.exports = replaceParagraphWithLink;
Traversal:
visit
function traverses the AST to find nodes
of type paragraph
.
Creating a link
Node:
link
node has:
type
: "link"
url
: The destination URL (e.g.,
https://example.com
).
title
: Optional title for the link.children
: An array of inline nodes, like
text
, which represents the visible link text.
Replacing the Node:
parent.children[index]
replaces the
paragraph
node with the newly created
link
node.
This is a paragraph.
Another paragraph.
const remark = require('remark');
const replaceParagraphWithLink = require('./replaceParagraphWithLink');
const markdown = `
This is a paragraph.
Another paragraph.
`;
remark()
.use(replaceParagraphWithLink)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file));
});
[Click here to visit Example.com!](https://example.com)
[Click here to visit Example.com!](https://example.com)
You can apply conditions to replace only certain paragraphs. For example, replace a paragraph only if it contains specific text:
function replaceSpecificParagraphWithLink() {
return (tree) => {
visit(tree, 'paragraph', (node, index, parent) => {
const paragraphText = node.children
.filter((child) => child.type === 'text')
.map((child) => child.value)
.join('');
if (paragraphText.includes('Replace me')) {
const linkNode = {
type: 'link',
url: 'https://example.com',
title: 'Example Site',
children: [
{
type: 'text',
value: 'Click here to visit Example.com!',
},
],
};
parent.children[index] = linkNode;
}
});
};
}
If you process the Markdown to HTML using remark-html
,
the link node will render as an HTML <a>
tag.
const html = require('remark-html');
remark()
.use(replaceParagraphWithLink)
.use(html)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file)); // Outputs the HTML
});
<a href="https://example.com" title="Example Site">Click here to visit Example.com!</a>
<a href="https://example.com" title="Example Site">Click here to visit Example.com!</a>
Node Types:
type: 'link'
for links in the AST.children
to define the link text.
Replace or Insert:
parent.children[index] = ...
replaces a node.
parent.children.splice(index, 0, newNode)
.
Selective Replacement:
if
to replace specific nodes.
Let me know if you need further clarifications or adjustments! ๐
To insert a new node at the beginning of the tree in Remark, you
modify the children
array of the root node. The
children
array represents all top-level nodes in the
document, so inserting at the beginning involves prepending the new
node.
Hereโs how you can do it:
This example inserts a paragraph
node at the start of the
Markdown document:
function insertAtBeginning() {
return (tree) => {
const newNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: 'This is a custom paragraph inserted at the beginning.',
},
],
};
// Insert the new node at the beginning of the tree
tree.children.unshift(newNode);
};
}
module.exports = insertAtBeginning;
const remark = require('remark');
const insertAtBeginning = require('./insertAtBeginning');
const markdown = `
# Heading
This is the original content.
`;
remark()
.use(insertAtBeginning)
.process(markdown, (err, file) => {
if (err) throw err;
console.log(String(file));
});
This is a custom paragraph inserted at the beginning.
# Heading
This is the original content.
To insert a link
node at the beginning, create the
link
node structure and prepend it to the
children
array:
function insertLinkAtBeginning() {
return (tree) => {
const linkNode = {
type: 'link',
url: 'https://example.com',
title: 'Example Site',
children: [
{
type: 'text',
value: 'Visit Example.com',
},
],
};
// Insert the link node at the beginning of the tree
tree.children.unshift(linkNode);
};
}
module.exports = insertLinkAtBeginning;
[Visit Example.com](https://example.com)
# Heading
This is the original content.
If you want to insert raw HTML (e.g., a custom
<div>
), use an html
node:
function insertHtmlAtBeginning() {
return (tree) => {
const htmlNode = {
type: 'html',
value: '<div class="intro">Welcome to the document!</div>',
};
// Insert the HTML block at the beginning
tree.children.unshift(htmlNode);
};
}
module.exports = insertHtmlAtBeginning;
<div class="intro">Welcome to the document!</div>
# Heading
This is the original content.
If you want to insert a custom node (e.g., a
blockquote
or a dynamic node), follow this approach:
function insertDynamicNodeAtBeginning() {
return (tree) => {
const blockquoteNode = {
type: 'blockquote',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'This is a blockquote inserted dynamically.' },
],
},
],
};
// Insert the blockquote node at the beginning
tree.children.unshift(blockquoteNode);
};
}
module.exports = insertDynamicNodeAtBeginning;
> This is a blockquote inserted dynamically.
# Heading
This is the original content.
Use unshift
:
unshift
method adds the new node at the start
of the children
array.
Node Structure:
Node Types:
paragraph
link
html
blockquote
Dynamic Content:
Order Preservation:
Let me know if you need further assistance with this or related transformations! ๐
The proper type for the tree
parameter in a
Remark plugin is typically the root node of an
MDAST (Markdown Abstract Syntax Tree). This root node
represents the entire Markdown document, and its type is
Root
.
If youโre working in a TypeScript project or want to explicitly type
it, you can use the Root
type from the
@types/mdast
package or define it yourself.
@types/mdast
The @types/mdast
package provides type definitions for
the MDAST structure, including Root
and other node types.
npm install @types/mdast
Root
Type
import { Root } from 'mdast';
function myRemarkPlugin() {
return (tree: Root) => {
// Work with the tree
console.log(tree.type); // Logs "root"
};
}
If youโre not using @types/mdast
, you can define a basic
type for the Root
node and extend it as needed.
Root
Typetype Root = {
type: 'root';
children: Node[];
};
type Node = {
type: string;
[key: string]: any; // Allow additional properties
};
function myRemarkPlugin() {
return (tree: Root) => {
tree.children.forEach((node) => {
console.log(node.type);
});
};
}
unist
Types
Since MDAST is part of the unist syntax tree
ecosystem, you can use types from the
@types/unist
package for generic AST typing.
@types/unist
npm install @types/unist
unist
Typesimport { Node } from 'unist';
function myRemarkPlugin() {
return (tree: Node) => {
if (tree.type === 'root') {
console.log('This is the root node.');
}
};
}
If you use the unified framework (which Remark is
part of), you can install and use @types/unified
for a
more complete typing system.
npm install @types/unified
import { Plugin } from 'unified';
import { Root } from 'mdast';
const myRemarkPlugin: Plugin<[], Root> = () => {
return (tree) => {
console.log('Root node type:', tree.type);
};
};
type Root = {
type: 'root';
children: Node[];
};
type Node = {
type: string;
children?: Node[];
[key: string]: any;
};
Root
and Node
from
@types/mdast
or @types/unist
.
Let me know if you want more detailed examples or explanations for any of these! ๐
The type from unist that represents a generic
Abstract Syntax Tree (AST) node is Node
. If you're
working with unist, you can use the
Node
interface and its extensions for more specific
types.
Hereโs a breakdown of the relevant types:
Node
Type
The Node
type from unist
serves as the base
type for all nodes in a syntax tree:
import { Node } from 'unist';
interface Node {
type: string; // The type of the node (e.g., "root", "paragraph", "heading")
data?: unknown; // Optional metadata about the node
position?: Position; // Optional positional info in the source document
}
type
: A string representing the type of the node
(e.g., 'root'
, 'paragraph'
, etc.).
data
: Optional metadata associated with the node
(e.g., custom attributes).
position
: Optional Position
object
indicating where the node is located in the source Markdown.
Parent
Type
For nodes that can contain child nodes (like root
,
paragraph
, or heading
), the
Parent
type extends Node
with the
children
property.
import { Node } from 'unist';
interface Parent extends Node {
children: Node[]; // Array of child nodes
}
Literal
Type
For nodes that contain a value (e.g., text nodes or code blocks), the
Literal
type extends Node
with a
value
property.
import { Node } from 'unist';
interface Literal extends Node {
value: unknown; // The content of the node (e.g., text content)
}
Position
and Point
Types
The Position
and Point
types describe the
location of a node in the source Markdown:
interface Position {
start: Point; // Start of the node
end: Point; // End of the node
indent?: number[]; // Optional indentation info
}
interface Point {
line: number; // Line number
column: number; // Column number
offset?: number; // Optional character offset in the document
}
Root
Type
The Root
node is a common entry point in MDAST (used for
the entire document). It extends Parent
since it contains
child nodes.
import { Parent } from 'unist';
interface Root extends Parent {
type: 'root';
}
When writing a Remark plugin, the
tree
parameter can be typed as a Node
or,
more specifically, as a Root
node if you're working at
the document level:
import { Node, Parent, Root } from 'unist';
function myPlugin() {
return (tree: Root) => {
console.log(tree.type); // Logs "root"
tree.children.forEach((node: Node) => {
console.log(node.type); // Logs types of child nodes (e.g., "paragraph")
});
};
}
unist
Type Definitions
To use these types in your project, install the
@types/unist
package:
npm install @types/unist
Type | Description |
---|---|
Node |
Base type for all syntax tree nodes (type ,
data , position ).
|
Parent |
Extends Node , includes a
children array (used for root ,
paragraph , heading , etc.).
|
Literal |
Extends Node , includes a
value property (used for text ,
code , etc.).
|
Position |
Represents the position of a node in the source Markdown
(start , end , indent ).
|
Root |
A specific Parent node representing the entire
document, with type: 'root' .
|
Let me know if you need more details or examples for specific nodes! ๐
Hereโs the updated code with proper
TypeScript types applied. I've added typings to the
tree
, node
, and other variables, leveraging
the unist
and mdast
type definitions.
import type { Node, Parent } from 'unist'; // Import types for Node and Parent
import type { Heading, Link, Text } from 'mdast'; // Import specific node types from mdast
import { visit } from 'unist-util-visit';
const toc: Record<string, unknown> = {}; // Add a type for `toc` if needed
export const remarkToc = () => {
return (tree: Parent) => {
// Ensure the tree is a Parent node (it has `children`)
visit(tree, 'heading', (node: Heading, index: number | null, parent: Parent | null) => {
// Define the div node with proper types
const divNode: Parent = {
type: 'div', // Custom node type
data: {
hName: 'div', // Rendered as <div> in HTML
hProperties: { className: 'dynamic-div' }, // Add attributes
},
children: [
{
type: 'link', // Link node
url: '#test', // Target URL
children: [
{
type: 'text', // Text node
value: 'This content is dynamically generated.', // Text content
} as Text, // Explicit type for text node
],
} as Link, // Explicit type for link node
],
};
console.log('here');
// Add the div node to the end of the tree
(tree as Parent).children.push(divNode); // Cast `tree` to Parent to access `children`
});
};
};
Node
and Parent
from
unist
:
Node
: The base type for all AST nodes.Parent
: Extends Node
, adding the
children
property (used for nodes like
root
, heading
, and
paragraph
).
Specific Types from mdast
:
Heading
: Represents a Markdown heading
(#
, ##
, etc.).
Link
: Represents a link [text](url)
.
Text
: Represents plain text within the AST.Types for Callback Parameters:
node
: Typed as Heading
since the
visit
function is targeting
heading
nodes.
index
: A number
or null
,
representing the position of the node in its parent's
children
array.
parent
: A Parent
node or
null
(if the node has no parent).
Custom Node (divNode
):
Parent
node, with custom
data
to ensure compatibility with
remark-html
or other plugins that handle HTML
attributes.
toc
Type:
Record<string, unknown>
as a
placeholder. Replace with a more specific type if necessary.
To use these types, ensure you have the following type definitions installed:
npm install @types/unist @types/mdast
Input Markdown:
# Heading
Generated Tree:
{
"type": "root",
"children": [
{
"type": "heading",
"depth": 1,
"children": [
{
"type": "text",
"value": "Heading"
}
]
},
{
"type": "div",
"data": {
"hName": "div",
"hProperties": {
"className": "dynamic-div"
}
},
"children": [
{
"type": "link",
"url": "#test",
"children": [
{
"type": "text",
"value": "This content is dynamically generated."
}
]
}
]
}
]
}
Let me know if you need further adjustments or explanations! ๐
To add a style
object to the link
in the
AST, you can include it in the data
property of the
link
node under hProperties
. The
data
property allows you to specify custom attributes
that will be used when rendering the AST, for example, by
remark-html or other compatible plugins.
Hereโs how you can add a style
object to the
link
:
style
to the Link
import type { Node, Parent } from 'unist'; // Import types for Node and Parent
import type { Heading, Link, Text } from 'mdast'; // Import specific node types from mdast
import { visit } from 'unist-util-visit';
const toc: Record<string, unknown> = {}; // Add a type for `toc` if needed
export const remarkToc = () => {
return (tree: Parent) => {
// Ensure the tree is a Parent node (it has `children`)
visit(tree, 'heading', (node: Heading, index: number | null, parent: Parent | null) => {
// Define the div node with proper types
const divNode: Parent = {
type: 'div', // Custom node type
data: {
hName: 'div', // Rendered as <div> in HTML
hProperties: { className: 'dynamic-div' }, // Add attributes
},
children: [
{
type: 'link', // Link node
url: '#test', // Target URL
data: {
hProperties: {
style: { color: 'red', textDecoration: 'none' }, // Add inline styles
},
},
children: [
{
type: 'text', // Text node
value: 'This content is dynamically generated.', // Text content
} as Text, // Explicit type for text node
],
} as Link, // Explicit type for link node
],
};
console.log('here');
// Add the div node to the end of the tree
(tree as Parent).children.push(divNode); // Cast `tree` to Parent to access `children`
});
};
};
Adding data.hProperties
to the Link Node:
data
property allows you to add rendering
metadata, such as hProperties
, which is used by
remark-html or other rendering plugins.
hProperties
, the style
attribute is
added as an object.
Style Object:
style
object can include any CSS properties:
style: { color: 'red', textDecoration: 'none' }
Rendering with remark-html
:
style
object as an inline
style
attribute in the HTML.
# Heading
If you pass the tree to remark-html
, the resulting HTML
will look like this:
<h1>Heading</h1>
<div class="dynamic-div">
<a href="#test" style="color: red; text-decoration: none;">This content is dynamically generated.</a>
</div>
Make sure you have remark-html installed if you want to convert the AST to HTML:
npm install remark-html
Use it like this:
import { remark } from 'remark';
import remarkHtml from 'remark-html';
import { remarkToc } from './path-to-your-plugin';
remark()
.use(remarkToc)
.use(remarkHtml)
.process('# Heading', (err, file) => {
if (err) throw err;
console.log(String(file)); // Outputs the final HTML
});
Let me know if you have any further questions or need more examples! ๐
Hereโs the fixed and improved version of your code. It includes proper
TypeScript type adjustments, fixes type mismatches, and ensures
div
is properly added to the tree.
import { visit } from 'unist-util-visit';
import type { Parent } from 'unist';
import type { Link, List, ListItem, Heading, Paragraph, Text } from 'mdast';
// Collect headings for Table of Contents
const toc: Heading[] = [];
// Define the custom DivNode type
type DivNode = {
type: 'div';
data: { hName: 'div'; hProperties: { className: string } };
children: List;
};
export const remarkToc = () => {
return (tree: Parent) => {
// Collect all headings
visit(tree, 'heading', (node: Heading) => {
toc.push(node);
});
// Define the div node with a nested list structure
const div: DivNode = {
type: 'div',
data: {
hName: 'div',
hProperties: { className: 'table-of-contents' },
},
children: {
type: 'list',
children: [
{
type: 'listItem',
children: [
{
type: 'paragraph',
children: [
{
type: 'link',
url: '#test',
children: [
{
type: 'text',
value: 'text',
} as Text,
],
} as Link,
],
} as Paragraph,
],
} as ListItem,
],
},
};
// Add the div node to the beginning of the tree
tree.children.unshift(div as unknown as Parent); // Cast div to Parent to satisfy TypeScript
};
};
Fixed Type for div.children
:
children
property of DivNode
is
defined as List
. Previously, there was a type
mismatch due to incorrect or missing child node definitions.
Typed Inner Nodes (Text
, Link
,
Paragraph
, ListItem
, and
List
):
Text
: Represents plain text.Link
: Represents links with
url
and children
.
Paragraph
: Wraps text or inline content.ListItem
: Represents list items.List
: Represents the list itself.
Proper Type for div
:
div
as a DivNode
and
ensured it adhered to the Parent
type with correct
children.
Casting div
for
tree.children.unshift
:
div
node is cast to Parent
to
satisfy TypeScript, as tree.children
expects
Parent
-typed nodes.
Simplified Logic:
div
.
# Heading 1
Some content.
## Heading 2
More content.
The resulting AST will include the div
node at the
beginning:
{
"type": "root",
"children": [
{
"type": "div",
"data": {
"hName": "div",
"hProperties": {
"className": "table-of-contents"
}
},
"children": {
"type": "list",
"children": [
{
"type": "listItem",
"children": [
{
"type": "paragraph",
"children": [
{
"type": "link",
"url": "#test",
"children": [
{
"type": "text",
"value": "text"
}
]
}
]
}
]
}
]
}
},
{
"type": "heading",
"depth": 1,
"children": [
{
"type": "text",
"value": "Heading 1"
}
]
},
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Heading 2"
}
]
}
]
}
remark-html
)
If rendered to HTML using remark-html
, the output will
be:
<div class="table-of-contents">
<ul>
<li>
<p><a href="#test">text</a></p>
</li>
</ul>
</div>
<h1>Heading 1</h1>
<p>Some content.</p>
<h2>Heading 2</h2>
<p>More content.</p>
url
in div.children
with
dynamically generated IDs based on the collected
toc
array.
toc
array to generate the list items
dynamically based on the headings collected.
Let me know if you need help with dynamic generation or further modifications! ๐
The issue where the Table of Contents (TOC) from previously visited
pages persists across navigations is likely due to the
toc
array being declared outside the
remarkToc
The issue arises because the toc
array is declared as a
global variable, and its content persists across navigations, causing
the Table of Contents (TOC) of previously visited pages to appear on
subsequent pages. To resolve this, ensure the toc
array
is reset or scoped properly within the function.
Here's the updated implementation where the toc
array is
scoped to the remarkToc
function:
import { visit } from 'unist-util-visit';
// * types
import type { Parent } from 'unist';
import type { List, Heading } from 'mdast';
type DepthRange = { min?: Heading['depth']; max?: Heading['depth'] };
type NavNode = {
type: 'nav';
children: [List];
data: { hName: 'nav'; hProperties: { className: string } };
};
export const remarkToc = (depthRange: DepthRange = {}) => {
const { min = 2, max = 3 } = depthRange;
return (tree: Parent) => {
const toc: Heading[] = []; // Declare toc inside the function to ensure it's reset for every page
visit(tree, 'heading', (node: Heading) => {
if (node.depth >= min && node.depth <= max) toc.push(node);
});
const navNode: NavNode = {
type: 'nav',
data: {
hName: 'nav',
hProperties: { className: 'table-of-contents' },
},
children: [
{
type: 'list',
children: toc.map((heading) => ({
type: 'listItem',
data: { hProperties: { className: `lvl-${heading.depth}` } },
children: [
{
type: 'paragraph',
children: [{ type: 'link', url: '#test', children: heading.children }],
},
],
})),
},
],
};
tree.children.unshift(navNode);
};
};
Scoped toc
Array:
toc
array declaration inside the
return
function of remarkToc
.
toc
is reset for every execution
of the plugin, resolving the issue of stale data.
Dynamic Depth Range:
depthRange
parameter with min
and
max
values works as expected. This code ensures
only headings within the specified range are added to the TOC.
Correct List Generation:
heading
is converted to a
listItem
with a paragraph
containing a
link
.
children
of the heading are reused for the link
text, and the url
can be dynamically generated
(e.g., based on an id
or slug).
# Title
## Section 1
Content here.
### Subsection 1.1
Content here.
## Section 2
Content here.
The TOC is correctly inserted as a nav
node at the
beginning:
{
"type": "root",
"children": [
{
"type": "nav",
"data": {
"hName": "nav",
"hProperties": {
"className": "table-of-contents"
}
},
"children": [
{
"type": "list",
"children": [
{
"type": "listItem",
"data": {
"hProperties": {
"className": "lvl-2"
}
},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "link",
"url": "#test",
"children": [
{
"type": "text",
"value": "Section 1"
}
]
}
]
}
]
},
{
"type": "listItem",
"data": {
"hProperties": {
"className": "lvl-3"
}
},
"children": [
{
"type": "paragraph",
"children": [
{
"type": "link",
"url": "#test",
"children": [
{
"type": "text",
"value": "Subsection 1.1"
}
]
}
]
}
]
}
]
}
]
},
{
"type": "heading",
"depth": 1,
"children": [
{
"type": "text",
"value": "Title"
}
]
}
]
}
toc
Scoped to Each Invocation:
remarkToc
(e.g., for a new
Markdown file) initializes a fresh toc
array,
avoiding stale data from previous invocations.
Independent State:
toc
locally, each page has an
independent Table of Contents, unaffected by previous pages.
Let me know if you have further questions or need additional assistance! ๐
Publishing your Remark plugin involves several steps to ensure it's available on npm and easily usable by others. Here's a step-by-step guide:
module.exports = remarkToc;
README.md
README.md
: # remark-toc
A Remark plugin to generate a Table of Contents (TOC) for Markdown files.
## Installation
bash
npm install remark-toc
## Usage
javascript
const remark = require('remark');
const remarkToc = require('remark-toc');
remark()
.use(remarkToc, { min: 2, max: 3 })
.process('# Title\n\n## Section 1\n\n### Subsection 1.1', (err, file)
=> {
if (err) throw err;
console.log(String(file));
});
## Options
- `min`: Minimum heading depth to include in the TOC.
- `max`: Maximum heading depth to include in the TOC.
package.json
If you don't already have one, create it using:
npm init
Key fields to include:
remark-toc
).
1.0.0
for an
initial release.
index.js
).
remark
, markdown
,
plugin
, toc
).
unist-util-visit
or other dependencies are listed.
Example package.json
:
{
"name": "remark-toc",
"version": "1.0.0",
"description": "A Remark plugin to generate a Table of Contents (TOC) for Markdown files.",
"main": "index.js",
"keywords": ["remark", "markdown", "plugin", "toc"],
"repository": {
"type": "git",
"url": "https://github.com/yourusername/remark-toc"
},
"author": "Your Name <your.email@example.com>",
"license": "MIT",
"dependencies": {
"unist-util-visit": "^4.0.0"
}
}
If you donโt already have an npm account, create one here.
Log in to npm:
npm login
Run:
npm publish
package.json
.
@yourusername/remark-toc
), prefix the name in
package.json
with @yourusername/
.
Search for your package on npm: https://www.npmjs.com/.
README.md
: 
1.0.1
: For patches.1.1.0
: For minor feature updates.2.0.0
: For breaking changes.
Add tests using a framework like jest
to ensure future
changes donโt introduce bugs.
README.md
for users to
contribute.
package.json
under
repository
.
"repository": {
"type": "git",
"url": "https://github.com/yourusername/remark-toc"
}
Let me know if you need help with any specific step! ๐
Whether your Remark plugin needs to be built depends on the language and tools you use to write it:
Pure JavaScript (No Build Required):
index.js
) and compatible with Node.js (ES5 or ES6
syntax), you donโt need to build it.
Example:
module.exports = function remarkToc() {
return (tree) => {
// Plugin logic
};
};
Steps:
main
in package.json
points to your
entry file (e.g., index.js
).
npm publish
.TypeScript (Build Required):
tsc
(TypeScript Compiler) handle this.
Steps:
tsconfig.json
to configure TypeScript: {
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"outDir": "dist",
"strict": true,
"declaration": true
},
"include": ["src"]
}
src/
โโโ index.ts (your plugin code)
dist/
โโโ (generated JavaScript files after build)
npx tsc
dist
folder to npm:
"main": "dist/index.js"
in
package.json
.
src
in the .npmignore
file
or ensure src
is not included in the npm
package.
Modern JavaScript (ESM or ESNext):
import/export
or ES modules), you may need to
transpile it for compatibility with older environments.
Use Babel or another build tool:
.babelrc
file: {
"presets": ["@babel/preset-env"]
}
npx babel src --out-dir dist
package.json
points to the transpiled files:
"main": "dist/index.js",
"type": "commonjs"
remark-*
) are written in plain JavaScript or
TypeScript. Typically, they donโt require extensive builds
unless you use advanced tooling.
rollup
or
esbuild
.
Language/Setup | Build Required | Action |
---|---|---|
Plain JavaScript | No | Publish directly. |
TypeScript | Yes | Transpile with tsc . |
Modern JavaScript (ESM) | Yes (optional) | Transpile with Babel if necessary. |
Using Advanced Bundlers | Yes | Use rollup or esbuild . |
Let me know if youโd like help setting up a build process! ๐