Remark AST Plugin Tutorial

how to write a plugin to traverse an ast tree in remark

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:


1. Set up a new project

Make sure you have remark and related packages installed:

npm install remark

If you need additional utilities, install:

npm install unified visit

2. Understand the AST structure

The AST generated by Remark follows the MDAST specification. Nodes have a type (e.g., paragraph, heading, list), and may contain children.


3. Write the Plugin

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:

Example: Logging all headings in a Markdown file

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;

4. Use the Plugin

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));
  });

5. Advanced Traversal

  visit(tree, 'paragraph', (node) => {
    console.log('Found a paragraph:', node);
  });

6. Modify the AST

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)',
  });
});

7. Combine Plugins

You can chain multiple plugins using remark().use().


8. Testing

Use remark-cli or integrate your plugin in a larger build pipeline for testing.

npm install remark-cli
remark -u ./myRemarkPlugin input.md

Tips:

Let me know if you'd like examples for specific use cases! ๐Ÿš€

how to then maniuplate the output ?

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:


1. Modify Nodes

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;

2. Add Nodes

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;

3. Remove Nodes

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;

4. Reorder Nodes

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;

5. Update Metadata

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;

6. Change Output Format

If you need to generate specific output, such as converting the Markdown to HTML, integrate with rehype or stringify the Markdown.

Markdown to HTML Example:

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
  });

7. Multiple Changes

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;

8. Save or Use the Manipulated Output

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
  });

Final Notes

Let me know if youโ€™d like specific help with any manipulation tasks! ๐Ÿš€

No I intend to add a block to it

Adding a custom block to the Markdown content is straightforward when working with a Remark plugin. Here's how to do it:


Example: Adding a Custom Block

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.


1. Define the Plugin

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;

2. Use the Plugin

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.

3. Add a Custom Block in a Specific Location

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.


4. Custom Block as HTML

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>

5. Customizing Block Based on Node Context

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

Tips for Block Manipulation

  1. Node Types: Familiarize yourself with MDAST node types like paragraph, html, code, etc.
  2. Tree Location: Use traversal utilities like unist-util-visit to find the right location for inserting your block.
  3. Complex Blocks: Build more complex blocks by nesting nodes within children.

Let me know if you want further assistance in building or extending the plugin! ๐Ÿš€

what is the use for the blockAdded variable?

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.

Why is blockAdded Necessary?

Let's break it down:

  1. Traversal with visit:

    • The visit function recursively traverses all nodes of the specified type in the tree.
    • If there are multiple nodes of the type being targeted (e.g., multiple heading nodes), the callback will execute for each of them.
  2. Use Case:

    • The example intends to add a block only after the first heading. Therefore, we need to ensure the operation runs only once.
  3. How blockAdded Works:

    • It starts as false.
    • When the block is added (inside the callback), itโ€™s set to true.
    • Subsequent matches will skip the operation because the condition (!blockAdded) will evaluate to false.

Without 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

When to Use blockAdded?

Use a similar variable when:

  1. You need to ensure an operation occurs only once during traversal.
  2. You are adding or modifying nodes based on conditions that could match multiple times.

Alternative Approach: Exiting Early

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.


Summary

is it possible to add a div with content inside of it?

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:


Example: Adding a <div> with Content

Plugin Code:

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;

Usage:

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));
  });

Output:

# Heading 1
Some content here.

<div class="custom-div">This is a custom div with content inside.</div>

Adding Dynamic Content Inside the <div>

You can dynamically generate the content for the <div> by traversing the AST or passing data into your plugin. For example:

Plugin Code:

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;

Usage:

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));
  });

Output:

# Introduction

## Features

## Installation

<div class="custom-div">Headings: Introduction, Features, Installation</div>

Adding Nested Content Inside the <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.

Plugin Code:

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;

Output:

# 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>

Summary

Let me know if you want help with a specific example! ๐Ÿš€

what if the dynamic content of the div is going to be tree nodes

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:


1. Adding a Div Node with Tree Nodes as Content

You can create a custom mdast-like node for your <div> and append or insert other nodes as its children.

Example Plugin:

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;

2. Rendering the Custom Node

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.

Output:

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>

3. Dynamic Content as Nodes

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.

Example: Generating Dynamic Content

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;

Output:

Input Markdown:

# Title

Output:

<h1>Title</h1>
<div class="dynamic-div">
  <h2>Dynamic Heading</h2>
  <p>This content is dynamically generated.</p>
</div>

4. Advanced: Wrapping Nodes in Div Based on Conditions

If you want to selectively wrap nodes based on conditions (e.g., wrap all list and paragraph nodes in a <div>), you can:

Example Plugin:

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;

5. Key Notes

  1. 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>).
    • Tools like remark-html or rehype respect data.hName and data.hProperties.
  2. Dynamic Child Nodes:

    • Dynamically generate new nodes and add them to the children array of your custom wrapper node.
  3. Preserve Order:

    • If you want to insert the <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! ๐Ÿš€

where to find different node types?

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.


1. Primary Resource: MDAST Specification

The official MDAST documentation is the best place to learn about node types. It provides detailed descriptions of:


2. Common Node Types

Hereโ€™s an overview of some commonly used node types and their structure:

Root

{
  "type": "root",
  "children": [...]
}

Heading

{
  "type": "heading",
  "depth": 1,
  "children": [
    { "type": "text", "value": "Heading Text" }
  ]
}

Paragraph

{
  "type": "paragraph",
  "children": [
    { "type": "text", "value": "This is a paragraph." }
  ]
}

Text

{
  "type": "text",
  "value": "Plain text"
}

List

{
  "type": "list",
  "ordered": true,
  "children": [
    {
      "type": "listItem",
      "children": [
        { "type": "paragraph", "children": [{ "type": "text", "value": "Item 1" }] }
      ]
    }
  ]
}

ListItem

{
  "type": "listItem",
  "children": [
    { "type": "paragraph", "children": [{ "type": "text", "value": "Item 1" }] }
  ]
}

Blockquote

{
  "type": "blockquote",
  "children": [
    { "type": "paragraph", "children": [{ "type": "text", "value": "Quoted text." }] }
  ]
}

Code

{
  "type": "code",
  "lang": "javascript",
  "value": "console.log('Hello, world!');"
}

InlineCode

{
  "type": "inlineCode",
  "value": "const x = 10;"
}

ThematicBreak

{
  "type": "thematicBreak"
}

{
  "type": "link",
  "url": "https://example.com",
  "title": "Example",
  "children": [
    { "type": "text", "value": "Click here" }
  ]
}

Image

{
  "type": "image",
  "url": "https://example.com/image.png",
  "alt": "An example image",
  "title": "Example Image"
}

3. Find Node Types Programmatically

You can inspect the structure of the AST for a specific Markdown file to see all available node types:

Example:

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.


4. Custom Node Types

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.


5. Tools and Utilities


Let me know if you'd like more detailed help with a specific type! ๐Ÿš€

How to insert a link instead of a paragraph?

To replace a paragraph node with a link node in the AST using a Remark plugin, you need to:

  1. Traverse the AST to find the paragraph nodes.
  2. Replace the 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;

2. Explanation of the Code

  1. Traversal:

    • The visit function traverses the AST to find nodes of type paragraph.
  2. Creating a link Node:

    • The 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.
  3. Replacing the Node:

    • parent.children[index] replaces the paragraph node with the newly created link node.

3. Usage

Input Markdown:

This is a paragraph.

Another paragraph.

Plugin Application:

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));
  });

Output Markdown:

[Click here to visit Example.com!](https://example.com)

[Click here to visit Example.com!](https://example.com)

4. Conditional Replacement

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;
      }
    });
  };
}

5. Output as HTML

If you process the Markdown to HTML using remark-html, the link node will render as an HTML <a> tag.

Example:

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
  });

Output 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>

Key Notes:

  1. Node Types:

    • Use the type: 'link' for links in the AST.
    • Links should have children to define the link text.
  2. Replace or Insert:

    • parent.children[index] = ... replaces a node.
    • To add instead of replace, use parent.children.splice(index, 0, newNode).
  3. Selective Replacement:

    • Use conditions like if to replace specific nodes.

Let me know if you need further clarifications or adjustments! ๐Ÿš€

How to insert something at the beginning of the tree?

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:


Example 1: Insert a Paragraph Node at the Beginning

This example inserts a paragraph node at the start of the Markdown document:

Plugin Code:

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;

Usage:

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));
  });

Output Markdown:

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:

Plugin Code:

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;

Output Markdown:

[Visit Example.com](https://example.com)

# Heading

This is the original content.

Example 3: Insert an HTML Block at the Beginning

If you want to insert raw HTML (e.g., a custom <div>), use an html node:

Plugin Code:

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;

Output Markdown:

<div class="intro">Welcome to the document!</div>

# Heading

This is the original content.

Example 4: Insert a Custom Node at the Beginning

If you want to insert a custom node (e.g., a blockquote or a dynamic node), follow this approach:

Plugin Code:

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;

Output Markdown:

> This is a blockquote inserted dynamically.

# Heading

This is the original content.

Key Points

  1. Use unshift:

    • The unshift method adds the new node at the start of the children array.
  2. Node Structure:

    • Ensure the inserted node follows the MDAST specification for its type.
  3. Node Types:

    • Common types for insertion:
      • paragraph
      • link
      • html
      • blockquote
  4. Dynamic Content:

    • Generate dynamic nodes (e.g., headings or lists) by creating appropriate child structures.
  5. Order Preservation:

    • Inserting at the beginning doesnโ€™t disrupt the order of other nodes.

Let me know if you need further assistance with this or related transformations! ๐Ÿš€

what is the proper type for the tree parameter?

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.


1. Using @types/mdast

The @types/mdast package provides type definitions for the MDAST structure, including Root and other node types.

Install the Type Definitions

npm install @types/mdast

Import and Use the Root Type

import { Root } from 'mdast';

function myRemarkPlugin() {
  return (tree: Root) => {
    // Work with the tree
    console.log(tree.type); // Logs "root"
  };
}

2. Manually Typing the Tree Parameter

If youโ€™re not using @types/mdast, you can define a basic type for the Root node and extend it as needed.

Basic Root Type

type Root = {
  type: 'root';
  children: Node[];
};

type Node = {
  type: string;
  [key: string]: any; // Allow additional properties
};

Plugin Example

function myRemarkPlugin() {
  return (tree: Root) => {
    tree.children.forEach((node) => {
      console.log(node.type);
    });
  };
}

3. Using 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.

Install @types/unist

npm install @types/unist

Use unist Types

import { 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.

Install Unified Types

npm install @types/unified

Type the Plugin

import { Plugin } from 'unified';
import { Root } from 'mdast';

const myRemarkPlugin: Plugin<[], Root> = () => {
  return (tree) => {
    console.log('Root node type:', tree.type);
  };
};

Summary of Types

  type Root = {
    type: 'root';
    children: Node[];
  };
  type Node = {
    type: string;
    children?: Node[];
    [key: string]: any;
  };

Let me know if you want more detailed examples or explanations for any of these! ๐Ÿš€

give me the type from unist

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:


1. 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
}

2. 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
}

3. 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)
}

4. 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
}

5. Example: 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';
}

6. Using the Types in a Plugin

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")
    });
  };
}

7. Install unist Type Definitions

To use these types in your project, install the @types/unist package:

npm install @types/unist

Summary of Core Types

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! ๐Ÿš€

add typescript types to this for me import type { Node } from 'unist'; import { visit } from 'unist-util-visit'; const toc = {}; export const remarkToc = () => { return (tree: Node) => { visit(tree, 'heading', (node, index, parent) => { const divNode = { type: 'div', data: { hName: 'div', hProperties: { className: 'dynamic-div' }, }, children: [ { type: 'link', url: '#test', children: [{ type: 'text', value: 'This content is dynamically generated.' }], }, ], }; console.log('here'); // Add the div node to the end of the tree tree.children.push(divNode); }); }; };

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.


Updated Code

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`
    });
  };
};

Key Updates and Explanation

  1. 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).
  2. Specific Types from mdast:

    • Heading: Represents a Markdown heading (#, ##, etc.).
    • Link: Represents a link [text](url).
    • Text: Represents plain text within the AST.
  3. 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).
  4. Custom Node (divNode):

    • Defined as a Parent node, with custom data to ensure compatibility with remark-html or other plugins that handle HTML attributes.
  5. toc Type:

    • Used Record<string, unknown> as a placeholder. Replace with a more specific type if necessary.

Dependencies

To use these types, ensure you have the following type definitions installed:

npm install @types/unist @types/mdast

Example Tree Modification

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! ๐Ÿš€

How to add a style object to the link?

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:


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`
    });
  };
};

Explanation

  1. Adding data.hProperties to the Link Node:

    • The data property allows you to add rendering metadata, such as hProperties, which is used by remark-html or other rendering plugins.
    • In hProperties, the style attribute is added as an object.
  2. Style Object:

    • The style object can include any CSS properties:
     style: { color: 'red', textDecoration: 'none' }
  3. Rendering with remark-html:

    • If you use remark-html, it will render the style object as an inline style attribute in the HTML.

Example Input Markdown:

# Heading

Example Output HTML:

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>

Dependencies

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! ๐Ÿš€

fix this code for me import { visit } from 'unist-util-visit'; import type { Parent } from 'unist'; import type { Link, List, ListItem, Heading } from 'mdast'; const toc: Heading[] = []; type DivNode = { type: 'div'; data: { hName: 'div'; hProperties: { className: string } }; children: List; }; export const remarkToc = () => { return (tree: Parent) => { visit(tree, 'heading', (node: Heading) => { toc.push(node); }); // ! nav 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' }] }, ], }, ], }, ], }, }; tree.children.unshift(div); }; };

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.

Fixed Code

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
  };
};

Key Changes and Fixes

  1. Fixed Type for div.children:

    • The children property of DivNode is defined as List. Previously, there was a type mismatch due to incorrect or missing child node definitions.
  2. Typed Inner Nodes (Text, Link, Paragraph, ListItem, and List):

    • Explicitly defined types for each child node to ensure correct structure:
      • 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.
  3. Proper Type for div:

    • Defined the div as a DivNode and ensured it adhered to the Parent type with correct children.
  4. Casting div for tree.children.unshift:

    • The div node is cast to Parent to satisfy TypeScript, as tree.children expects Parent-typed nodes.
  5. Simplified Logic:

    • Focused on creating a valid MDAST-compliant structure for the div.

Example Input Markdown

# Heading 1

Some content.

## Heading 2

More content.

Example Output AST

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"
        }
      ]
    }
  ]
}

Example Output Markdown (with 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>

Notes

Let me know if you need help with dynamic generation or further modifications! ๐Ÿš€

This is my implementation and the problem is that the TOC of the previously visited pages gets shown upon navigation to another page

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

import { visit } from 'unist-util-visit'; // * data const toc: Heading[] = []; // * 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; toc.length = 0; return (tree: Parent) => { 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); }; };

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.

Fixed Implementation

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);
  };
};

Key Changes and Fixes

  1. Scoped toc Array:

    • Moved the toc array declaration inside the return function of remarkToc.
    • This ensures the toc is reset for every execution of the plugin, resolving the issue of stale data.
  2. Dynamic Depth Range:

    • The depthRange parameter with min and max values works as expected. This code ensures only headings within the specified range are added to the TOC.
  3. Correct List Generation:

    • Each heading is converted to a listItem with a paragraph containing a link.
    • The 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).

Example Usage

Input Markdown:

# Title

## Section 1

Content here.

### Subsection 1.1

Content here.

## Section 2

Content here.

Output AST:

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"
        }
      ]
    }
  ]
}

Why This Fix Works

  1. toc Scoped to Each Invocation:

    • Each invocation of remarkToc (e.g., for a new Markdown file) initializes a fresh toc array, avoiding stale data from previous invocations.
  2. Independent State:

    • By scoping 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! ๐Ÿš€

My plugin is complete how to then publish it?

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:


1. Prepare Your Plugin for Publishing

a. Ensure Your Code is Ready

  module.exports = remarkToc;

b. Add a 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.

c. Add a package.json

If you don't already have one, create it using:

npm init

Key fields to include:

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"
  }
}

2. Publish to npm

a. Login to npm

If you donโ€™t already have an npm account, create one here.

Log in to npm:

npm login

b. Publish Your Plugin

Run:

npm publish

c. Verify the Package

Search for your package on npm: https://www.npmjs.com/.


3. Share and Maintain Your Plugin

a. Share Your Plugin

  ![npm](https://img.shields.io/npm/v/remark-toc)

b. Fix Bugs and Update

c. Automate Tests

Add tests using a framework like jest to ensure future changes donโ€™t introduce bugs.


4. Optional: Publish on GitHub

a. Create a GitHub Repository

  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/remark-toc"
  }

Let me know if you need help with any specific step! ๐Ÿš€

Does it need to be built?

Whether your Remark plugin needs to be built depends on the language and tools you use to write it:

  1. Pure JavaScript (No Build Required):

    • If your plugin is written in plain JavaScript (e.g., index.js) and compatible with Node.js (ES5 or ES6 syntax), you donโ€™t need to build it.
    • Simply publish the code as-is to npm.

    Example:

   module.exports = function remarkToc() {
     return (tree) => {
       // Plugin logic
     };
   };

Steps:


  1. TypeScript (Build Required):

    • If your plugin is written in TypeScript, it needs to be transpiled to JavaScript before publishing.
    • Tools like tsc (TypeScript Compiler) handle this.

    Steps:

    1. Add a tsconfig.json to configure TypeScript:
      {
        "compilerOptions": {
          "module": "commonjs",
          "target": "es2017",
          "outDir": "dist",
          "strict": true,
          "declaration": true
        },
        "include": ["src"]
      }
    1. Organize your project:
      src/
      โ””โ”€โ”€ index.ts (your plugin code)
      dist/
      โ””โ”€โ”€ (generated JavaScript files after build)
    1. Build the plugin:
      npx tsc
    1. Publish the dist folder to npm:
      • Add "main": "dist/index.js" in package.json.
      • Exclude src in the .npmignore file or ensure src is not included in the npm package.

  1. Modern JavaScript (ESM or ESNext):

    • If your plugin uses modern JavaScript features (e.g., import/export or ES modules), you may need to transpile it for compatibility with older environments.

    Use Babel or another build tool:

    • Add a .babelrc file:
     {
       "presets": ["@babel/preset-env"]
     }
    • Transpile the code:
     npx babel src --out-dir dist
    • Ensure package.json points to the transpiled files:
     "main": "dist/index.js",
     "type": "commonjs"

  1. Unified Plugins and Build Systems:
    • Many unified ecosystem plugins (e.g., remark-*) are written in plain JavaScript or TypeScript. Typically, they donโ€™t require extensive builds unless you use advanced tooling.
    • If your plugin uses advanced bundling (e.g., for web compatibility), you might use tools like rollup or esbuild.

Summary

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! ๐Ÿš€