unified

Learn/Recipe/Remove a node

How to remove a node

Once you have found the node(s) you want to remove (see tree traversal), you can remove them.

Contents

Prerequisites

Removing a node

For the most part, removing nodes has to do with finding them first (see tree traversal), so let’s say we already have some code to find all emphasis nodes.

First, our example.md file:

Some text with *emphasis*.

Another paragraph with **importance** (and *more emphasis*).

And a module, example.js:

import fs from 'node:fs/promises'
import remarkParse from 'remark-parse'
import {unified} from 'unified'
import {visit} from 'unist-util-visit'

const document = await fs.readFile('example.md', 'utf8')

const tree = unified().use(remarkParse).parse(document)

visit(tree, 'emphasis', function (node) {
  console.log(node)
})
(alias) module "node:fs/promises"
import fs
(alias) const remarkParse: Plugin<[(Readonly<Options> | null | undefined)?], string, Root>
import remarkParse

Add support for parsing from markdown.

  • @this processor.
  • @param Configuration (optional).
  • @returns Nothing.
(alias) const unified: Processor<undefined, undefined, undefined, undefined, undefined>
import unified

Create a new processor.

  • @example This example shows how a new processor can be created (from remark) and linked to stdin(4) and stdout(4).
    import process from 'node:process'
    import concatStream from 'concat-stream'
    import {remark} from 'remark'
    
    process.stdin.pipe(
      concatStream(function (buf) {
        process.stdout.write(String(remark().processSync(buf)))
      })
    )
    
  • @returns New unfrozen processor (processor). This processor is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor.
(alias) function visit<Tree extends Node, Check extends Test>(tree: Tree, check: Check, visitor: BuildVisitor<Tree, Check>, reverse?: boolean | null | undefined): undefined (+1 overload)
import visit
const document: string
(alias) module "node:fs/promises"
import fs
function readFile(path: PathLike | fs.FileHandle, options: ({
    encoding: BufferEncoding;
    flag?: OpenMode | undefined;
} & EventEmitter<T extends EventMap<T> = DefaultEventMap>.Abortable) | BufferEncoding): Promise<string> (+2 overloads)

Asynchronously reads the entire contents of a file.

  • @param path A path to a file. If a URL is provided, it must use the file: protocol. If a FileHandle is provided, the underlying file will not be closed automatically.
  • @param options An object that may contain an optional flag. If a flag is not provided, it defaults to 'r'.
const tree: Root
(alias) unified(): Processor<undefined, undefined, undefined, undefined, undefined>
import unified

Create a new processor.

  • @example This example shows how a new processor can be created (from remark) and linked to stdin(4) and stdout(4).
    import process from 'node:process'
    import concatStream from 'concat-stream'
    import {remark} from 'remark'
    
    process.stdin.pipe(
      concatStream(function (buf) {
        process.stdout.write(String(remark().processSync(buf)))
      })
    )
    
  • @returns New unfrozen processor (processor). This processor is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor.
(method) Processor<undefined, undefined, undefined, undefined, undefined>.use<[], string, Root>(plugin: Plugin<[], string, Root>, ...parameters: [] | [boolean]): Processor<Root, undefined, undefined, undefined, undefined> (+2 overloads)

Configure the processor to use a plugin, a list of usable values, or a preset.

If the processor is already using a plugin, the previous plugin configuration is changed based on the options that are passed in. In other words, the plugin is not added a second time.

Note: use cannot be called on frozen processors. Call the processor first to create a new unfrozen processor.

  • @example There are many ways to pass plugins to .use(). This example gives an overview:
    import {unified} from 'unified'
    
    unified()
      // Plugin with options:
      .use(pluginA, {x: true, y: true})
      // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`):
      .use(pluginA, {y: false, z: true})
      // Plugins:
      .use([pluginB, pluginC])
      // Two plugins, the second with options:
      .use([pluginD, [pluginE, {}]])
      // Preset with plugins and settings:
      .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}})
      // Settings only:
      .use({settings: {position: false}})
    
  • @template {Array} [Parameters=[]]
  • @template {Node | string | undefined} [Input=undefined]
  • @template [Output=Input]
  • @overload
  • @overload
  • @overload
  • @param value Usable value.
  • @param parameters Parameters, when a plugin is given as a usable value.
  • @returns Current processor.
(alias) const remarkParse: Plugin<[(Readonly<Options> | null | undefined)?], string, Root>
import remarkParse

Add support for parsing from markdown.

  • @this processor.
  • @param Configuration (optional).
  • @returns Nothing.
(method) Processor<Root, undefined, undefined, undefined, undefined>.parse(file?: Compatible | undefined): Root

Parse text to a syntax tree.

Note: parse freezes the processor if not already frozen.

Note: parse performs the parse phase, not the run phase or other phases.

  • @param file file to parse (optional); typically string or VFile; any value accepted as x in new VFile(x).
  • @returns Syntax tree representing file.
const document: string
(alias) visit<Root, "emphasis">(tree: Root, check: "emphasis", visitor: BuildVisitor<Root, "emphasis">, reverse?: boolean | null | undefined): undefined (+1 overload)
import visit
const tree: Root
(parameter) node: Emphasis
namespace console
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
//   Error: Whoops, something bad happened
//     at [eval]:5:15
//     at Script.runInThisContext (node:vm:132:18)
//     at Object.runInThisContext (node:vm:309:38)
//     at node:internal/process/execution:77:19
//     at [eval]-wrapper:6:22
//     at evalScript (node:internal/process/execution:76:60)
//     at node:internal/main/eval_string:23:3

const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);

myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
(method) Console.log(message?: any, ...optionalParams: any[]): void

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

  • @since v0.1.100
(parameter) node: Emphasis

Now, running node example.js yields (ignoring positions for brevity):

{
  type: 'emphasis',
  children: [ { type: 'text', value: 'emphasis', position: [Object] } ]
}
{
  type: 'emphasis',
  children: [ { type: 'text', value: 'more emphasis', position: [Object] } ]
}

As the above log shows, nodes are objects. Each node is inside an array at the children property of another node. In other words, to remove a node, it must be removed from its parents children.

The problem then is to remove a value from an array. Standard JavaScript Array functions can be used: namely, splice.

We have the emphasis nodes, but we don’t have their parent, or the position in the parent’s children field they are in. Luckily, the function given to visit gets not only node, but also that index and parent:

--- a/example.js
+++ b/example.js
@@ -7,6 +7,6 @@ const document = await fs.readFile('example.md', 'utf8')

 const tree = unified().use(remarkParse).parse(document)

-visit(tree, 'emphasis', function (node) {
-  console.log(node)
+visit(tree, 'emphasis', function (node, index, parent) {
+  console.log(node.type, index, parent?.type)
 })

Yields:

emphasis 1 paragraph
emphasis 3 paragraph

parent is a reference to the parent of node, index is the position at which node is in parent’s children. With this information, and splice, we can now remove emphasis nodes:

--- a/example.js
+++ b/example.js
@@ -8,5 +8,9 @@ const document = await fs.readFile('example.md', 'utf8')
 const tree = unified().use(remarkParse).parse(document)

 visit(tree, 'emphasis', function (node, index, parent) {
-  console.log(node.type, index, parent?.type)
+  if (typeof index !== 'number' || !parent) return
+  // Note: this is buggy, see next section.
+  parent.children.splice(index, 1)
 })
+
+console.log(tree)

Yields:

{
  type: 'root',
  children: [
    {
      type: 'paragraph',
      children: [
        {type: 'text', value: 'Some text with '},
        {type: 'text', value: '.'}
      ]
    },
    {
      type: 'paragraph',
      children: [
        {type: 'text', value: 'Another paragraph with '},
        {type: 'strong', children: [Array]},
        {type: 'text', value: ' (and '},
        {type: 'text', value: ').'}
      ]
    }
  ]
}

This looks great, but beware of bugs. We are now changing the tree, while traversing it. That can cause bugs and performance problems.

When changing the tree, in most cases you should signal to visit how it should continue. More information on how to signal what to do next, is documented in unist-util-visit-parents.

In this case, we don’t want the removed node to be traversed (we want to skip it). And we want to continue with the node that is now at the position where our removed node was. To do that: return that information from visitor:

--- a/example.js
+++ b/example.js
@@ -1,7 +1,7 @@
 import fs from 'node:fs/promises'
 import remarkParse from 'remark-parse'
 import {unified} from 'unified'
-import {visit} from 'unist-util-visit'
+import {SKIP, visit} from 'unist-util-visit'

 const document = await fs.readFile('example.md', 'utf8')

@@ -9,8 +9,9 @@ const tree = unified().use(remarkParse).parse(document)

 visit(tree, 'emphasis', function (node, index, parent) {
   if (typeof index !== 'number' || !parent) return
-  // Note: this is buggy, see next section.
   parent.children.splice(index, 1)
+  // Do not traverse `node`, continue at the node *now* at `index`.
+  return [SKIP, index]
 })

 console.log(tree)

This yields the same output as before, but there’s no bug anymore. Nice, we can now remove nodes!

Replacing a node with its children

One more thing to make this example more useful: instead of dropping emphasis and its children, it might make more sense to replace the emphasis with its children.

To do that, we can do the following:

--- a/example.js
+++ b/example.js
@@ -9,7 +9,7 @@ const tree = unified().use(remarkParse).parse(document)

 visit(tree, 'emphasis', function (node, index, parent) {
   if (typeof index !== 'number' || !parent) return
-  parent.children.splice(index, 1)
+  parent.children.splice(index, 1, ...node.children)
   // Do not traverse `node`, continue at the node *now* at `index`.
   return [SKIP, index]
 })

Yields:

{
  type: 'root',
  children: [
    {
      type: 'paragraph',
      children: [
        {type: 'text', value: 'Some text with '},
        {type: 'text', value: 'emphasis'},
        {type: 'text', value: '.'}
      ]
    },
    {
      type: 'paragraph',
      children: [
        {type: 'text', value: 'Another paragraph with '},
        {type: 'strong', children: [Array]},
        {type: 'text', value: ' (and '},
        {type: 'text', value: 'more emphasis'},
        {type: 'text', value: ').'}
      ]
    }
  ]
}