loom/docs/transformations.md

4.3 KiB

Transformation

In Loom, transformation refers to the process of modifying an abstract syntax tree (AST), especially through the use of transformers (or transformer functions).

The mechanism provided by loom for these transformations is the transform() function.

The first argument to the transform function is an AST. Each node in the AST will be considered for transformation.

After the AST, any number of additional arguments may be passed to the transform(ast, ...transformers) function. Each of these should be a transformer.

Transformerss

Signature

transformer(node)

  • node - AST node to be transformed. The node includes information about its children, but not its parent.

Return values

Transformerss may return one of three recognized value types. If a different value is returned, the transform function will throw an error.

null

Returning null from a transformer will cause the node to be removed from the tree. No further transformers will be run against the node.

undefined

Returning undefined from a transformer will leave the node in the tree unchanged. The next transformer will be called on the same node.

AST node

If the transformers returns an AST node, that node will be used to replace the original node in the original tree. Subsequent transformer functions will recieve a copy of the new node, not the original.

For convenience, new tree nodes can be created with the util.createNode(data) or util.cloneNode(node) function. The determination of whether or not the returned value is an AST node is made using the util.isNode(obj) utility function.

other values

In the event that a transformers returns any other value than null, undefined, or an AST node--as determined by util.isNode(), the transform(ast, ...transformers) function will throw an error.

Examples

In the following examples, we will use the AST corresponding to the following HTML for high intelligibility.

// An HTML document with fixture data that includes an article, a header, // several paragraphs, and an unordered list. Some of the elements have ids // or class names, or other data attributes. // The paragraphs are about fauna

<html>
	<body>
		<article>
			<h1 id="title">Fun Facts about Stuff</h1>
			<ul id="fun-facts">
				<li id="lights">The Northern Lights are beautiful at night.</li>
				<li id="bears">Grizzly bears are scary.</li>
				<li id="stones">Ancient megaliths cover the valley.</li>
			</ul>
		</article>
	</body>
</html>

We will use the variable ast to refer to the root of the AST.

Deleting nodes

When a transformer returns null, it means the node should be deleted. No further transformations are performed on the same node.

Let's delete any fun facts about bears.

import ast from './my-html-ast.mjs';

ast = transform(
	ast,
	(node) => (node.id === 'bears') ? null : undefined,
);

Adding nodes from a remote data source

Let's add more fun facts from an API.

import ast from './my-html-ast.mjs';

ast = transform(
	ast,
	(node) => {
		if (node.property.id === 'fun-facts') {
			// These have a structure like ["...", "...", ...]
			let facts = JSON.parse(await fetch('https://example.com/facts'));

			facts.forEach((fact) => {
				// Clone the first existing fact.
				let newNode = cloneNode(facts[0]);
				// Replace the text
				newNode.children[0].value = fact;
				// Remove the id
				delete newNode.children[0].property.id;
				// Append it to the list
				node.children.push(newNode);
			});

		}
	},
)

The resulting AST will have additional facts attached to the ul element.

Multiple transformers

In this script we will use the hastscript library to create new html nodes conveniently.

import { h as htmlTag } from 'hastscript';
import ast from './my-html-ast.mjs';

ast = transform(
	ast,
	// Delete any node with id 'bears'.  No other transformers will be run
	// against bear nodes.  Other nodes are left unchanged.
	(node) => (node.id === 'bears') ? null : undefined,

	// Change the article tag to a section tag.  Leave the node in place.
	(node) => {
		if (node.tagName === 'article') {
			node.tagName = 'section';
		}
	}

	// Add a new paragraph talking about fun facts.
	(node) => {
		if (node.tagName === 'article') {
			let intro = htmlTag('p', { id: 'intro' }, 'Fun facts about stuff');
			node.children.push(intro);
		}
	},
);