Add a buncha docs and change isAstNode to isNode

master
Sir Robert Burbridge 2023-11-30 10:30:50 -05:00
parent 1f4e4cef51
commit 0d78790413
9 changed files with 335 additions and 59 deletions

View File

@ -23,22 +23,47 @@ or reading from files.
## Usage Example
For more thorough usage examples, see the `examples` directory and refer to
the [documentation](./docs/usage.md).
the [transformers documentation](./docs/usage.md) and the [interpolation
documentation](./docs/interpolation.md).
### Simple Example
In this simple example, we will make two changes. First, we will replace
the `div` tag in our html fragment with a `span` tag. Second, we will
replace the `name` variable in our template with the value `Othello`.
```js
import {
interpolateTree,
transform,
} from '@thefarce/loom'
import { interpolateTree, transform } from '@thefarce/loom'
import { astToHtml, htmlToAst } from '@thefarce/loom-html5'
const initial = htmlToAst('<p>Hello, {name}!</p>'),
// Get our HTML fragment.
const ast = htmlToAst('<p>Hello, <div>{name}</div>!</p>', { fragment: true });
const transformed = interpolateTree(
transform(initial, ...transformers),
{ name: 'Othello' },
// Now we have an abstract syntax tree (AST) representation of our HTML
// fragment. First, let's transform the div to a span.
let updated = transform(
// our initial syntax tree
ast,
// A transformer that checks if the node is an element and has a tag name
// of "div". If so, we just change the tag name to "span". It is
// important that we return the updated node (or a new one).
(node) => {
if (node.type === 'element' && node.tagName === 'div') {
node.tagName = 'span';
return node;
}
}
);
console.log(astToHtml(transformed));
// Now let's interpolate the value `name`. There is another major mechanism
// for providing interpolation values; see the for more detailed information.
let interpolated = interpolateTree(
updated,
{ name: 'Othello' }
);
console.log(astToHtml(interpolated));
```
Running this script produces the following output:
@ -49,14 +74,35 @@ Running this script produces the following output:
## API
### interpolate(ast, data)
### Value Interpolation
### transform(ast, plugins)
Interpolation is the process of executing code within the AST's values.
This uses a syntax identical to JavaScript's "backtick" interpolation. For
example, in the following script, JavaScript will replace `{name}` with the
value `Othello`:
See the [interpolation documentation](./docs/interpolation.md) for more
detailed information.
```js
const name = 'Othello';
console.log(`Hello, ${name}!`);
```
This interpolation uses variables in scope to expand values. Loom
interpolation works the same way: any string[^1] in the AST will be replaced
with the value in the data.
[^1]: There are a few exceptions. The `type`, `data`, and `position` values of a node will not be interpolated. You may interpolate them manually, of course.
### AST Transformation
Transformation is the process of making systematic changes to an AST. This
is accomplished by creating "transformer functions" (or "transformers") that
apply changes to a node based on its content.
See the [transformations documentation](./docs/transformations.md) for more
information.
Transform the AST according to plugins.
detailed information.
## Glossary
@ -69,6 +115,11 @@ Running this script produces the following output:
Contributions to Loom are welcome.
### Community Guidelines
Contribute good code with full tests and docs, then submit a pull request.
I don't care about any of your other behaviors.
### VIM
This project contains a `.vimrc` file to help with development and

View File

@ -0,0 +1,123 @@
# Interpolation
In Loom, *interpolation* refers to the process of modifying text values in
an abstract syntax tree (AST) by applying data held in scope in code to the strings.
The mechanism provided by loom for these interpolations are the
`interpolate*()` functions:
interpolate: tree, node, string, value, object, array
* [`interpolateNode(...)`](#interpolatenode) - Interpolate a single node
* [`interpolateArray(...)`](#interpolatearray) - Interpolate an array's values
* [`interpolateObject(...)`](#interpolateobject) - Interpolate an object's properties
* [`interpolateString(...)`](#interpolatestring) - interpolate a string
* [`interpolateTree(...)`](#interpolatetree) - interpolate an AST
* [`interpolateValue(...)`](#interpolatevalue) - interpolate a value of unknown type
## Interpolation data
The most simple interface to interpolation is using a *context mask*. See
[that section](#context-masks) for more details.
### Context
As you traverse a tree or subtree of nodes, you can assign data to the node
for later interpolation. This data is called the node's *context*. Each
node's context is stored in its `.data.context` property. Note that the
Unified.js AST convention provides the `.data` property and other things may
modify its contents. Use `.data.context` for a safe place to store the
node's data for interpolation purposes.
When a node is processed, it gets sees the context of all of its parents.
Nearer contexts *shadow* farther contexts. This works in the same way that
shadowing works in JavaScript, where the innermost value is used. In
JavaScript, this is seen this way:
```js
(foo) => {
let a = 1;
[10,20].forEach((a) => console.log(a));
console.log(a);
}
```
Notice that inside the interior function, the parent's value of `a` is
shadowed by the child's value of `a` from the function's signature. This
script will output:
```
10
20
1
```
In a loom AST, the functionality is very similar. Consider the following
HTML AST (abbreviated for clarity):
```
{
"tagName": "personal-greetings",
data: { context: { name: "Othello" } },
"children": [
{
"tagName": "p",
"children": [
{ type: "text", value: "Hello, ${name}!" }
]
},
{
"tagName": "p",
data: { context: { name: "Margaret" } },
"children": [
{ type: "text", value: "Hello, ${name}!" }
]
},
]
}
```
Notice that the `<personal-greetings> tag has context associated with it:
`.data.context.name` is set to `"Othello"`.
Of the two children, the former item does not have its own data, while the
latter item does; it's context sets the `name` to `"Margaret"`.
The code to interpolate and render the AST could look like this:
```js
console.log(astToHtml(interpolate(tree)));
```
The rendered version of this AST to HTML would look like this:
```html
<personal-greetings>
<p>Hello, Othello!</p>
<p>Hello, Margaret!</p>
</personal-greetings>
```
In the first node, `name` is available through the parent's context. In the
second node, `name` is shadowed by the child's own value, "Margaret".
### Context Masks
You may interpolate any value with a context mask by passing it in as a
second parameter to the `interpolate\*()` functions.
Using the example provided in the Contexts section, you could write:
```js
console.log(astToHtml(interpolate(tree, {name: "Samwise"})));
```
The rendered version of this AST to HTML would look like this:
```html
<personal-greetings>
<p>Hello, Samwise!</p>
<p>Hello, Samwise!</p>
</personal-greetings>
```

View File

@ -1,7 +1,8 @@
# Transformation
In Loom, *transformation* refers to the process of modifying an abstract
syntax tree (AST), especially through the use of *transformation functions*.
syntax tree (AST), especially through the use of *transformers* (or
*transformer functions*).
The mechanism provided by loom for these transformations is the
`transform()` function.
@ -10,45 +11,152 @@ 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` function. Each of these should be a *transformation function*
or an array of such.
`transform(ast, ...transformers)` function. Each of these should be a
*transformer*.
## Transformation functions
## Transformerss
Transformer functions take _a copy_ of an AST node. Because it is a copy,
the changes made to the node received by the transformation function will
have no effect on the tree. To change the tree, return a new AST node.
### Signature
`transformer(node)`
* `node` - AST node to be transformed. The node includes information about
its children, but not its parent.
### Return values
Transformation functions may return one of three recognized value types. If
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 transformation function returns an AST node, that node will be used
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.createAstNode()` or `util.cloneNode(node)` function. 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.isAstNode()` utility function.
using the `util.isNode(obj)` utility function.
#### null
#### _other values_
If the transformation function returns `null`, the original node will be
removed from the AST. No further transformation functions will be called on
the same node.
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.
#### undefined
## Examples
If the transformation function returns `undefined`, the original node will
be left in the AST unchanged. The next transformation function will be
called on the same node.
In the following examples, we will use the AST corresponding to the
following HTML for high intelligibility.
### _other values_
// 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
<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>
```
In the event that a transformation function returns any other value than
`null`, `undefined`, or an AST node--as determined by `util.isAstNode()`,
the `transform()` function will throw an error.
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.
```js
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.
```js
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.
```js
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);
}
},
);
```

View File

@ -1,6 +1,6 @@
{
"name": "@thefarce/loom",
"version": "0.1.1",
"version": "0.1.2",
"description": "A module for weaving form data with content data",
"type": "module",
"main": "index.mjs",

View File

@ -7,7 +7,6 @@
import { visitParents } from 'unist-util-visit-parents';
import { createContext, runInContext } from 'vm';
import { cloneNode } from './util.mjs';
import { is } from 'unist-util-is';
/**
* Accumulates context from a lineage of nodes.

View File

@ -5,8 +5,7 @@
*/
import { visit, SKIP } from 'unist-util-visit';
import { isAstNode } from './util.mjs';
import { is } from 'unist-util-is';
import { isNode } from './util.mjs';
/**
* Transforms an AST using a series of transformer functions.
@ -54,7 +53,7 @@ export function transform (ast, ...transformers) {
}
// If the function returned an AST node, we replace the working node
// with that AST node and continue processing with the new node.
else if (isAstNode(result)) {
else if (isNode(result)) {
replaced = true;
return result;
}

View File

@ -40,6 +40,6 @@ export function createNode({
* @returns {boolean} True if the value is an object with a "type" property,
* false otherwise.
*/
export function isAstNode(node) {
export function isNode(node) {
return (typeof node === 'object') && (node !== null) && ('type' in node);
}

View File

@ -8,10 +8,6 @@ import {
interpolateValue,
} from '../src/interpolate.mjs'
import {
createNode,
} from '../src/util.mjs'
/**
* The minimal information on an AST node. 'data' and 'position' are
* optional.

View File

@ -1,7 +1,7 @@
import {
cloneNode,
createNode,
isAstNode,
isNode,
} from '../src/util.mjs';
describe("cloneNode()", () => {
@ -66,17 +66,17 @@ describe("createNode()", () => {
});
describe("isAstNode()", () => {
describe("isNode()", () => {
it("should import isAstNode()", () => {
expect(isAstNode).toBeDefined();
it("should import isNode()", () => {
expect(isNode).toBeDefined();
});
it("should return true if the node is an AST node", () => {
[
createNode(),
].forEach(value => {
expect(isAstNode(value)).toBe(true);
expect(isNode(value)).toBe(true);
});
});
@ -90,7 +90,7 @@ describe("isAstNode()", () => {
{a:1},
{children:[]},
].forEach(value => {
expect(isAstNode(value)).toBe(false);
expect(isNode(value)).toBe(false);
});
});