Add a buncha docs and change isAstNode to isNode
parent
1f4e4cef51
commit
0d78790413
81
README.md
81
README.md
|
@ -23,22 +23,47 @@ or reading from files.
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
For more thorough usage examples, see the `examples` directory and refer to
|
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
|
```js
|
||||||
import {
|
import { interpolateTree, transform } from '@thefarce/loom'
|
||||||
interpolateTree,
|
import { astToHtml, htmlToAst } from '@thefarce/loom-html5'
|
||||||
transform,
|
|
||||||
} from '@thefarce/loom'
|
|
||||||
|
|
||||||
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(
|
// Now we have an abstract syntax tree (AST) representation of our HTML
|
||||||
transform(initial, ...transformers),
|
// fragment. First, let's transform the div to a span.
|
||||||
{ name: 'Othello' },
|
|
||||||
|
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:
|
Running this script produces the following output:
|
||||||
|
@ -49,14 +74,35 @@ Running this script produces the following output:
|
||||||
|
|
||||||
## API
|
## 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
|
See the [transformations documentation](./docs/transformations.md) for more
|
||||||
information.
|
detailed information.
|
||||||
|
|
||||||
Transform the AST according to plugins.
|
|
||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
|
|
||||||
|
@ -69,6 +115,11 @@ Running this script produces the following output:
|
||||||
|
|
||||||
Contributions to Loom are welcome.
|
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
|
### VIM
|
||||||
|
|
||||||
This project contains a `.vimrc` file to help with development and
|
This project contains a `.vimrc` file to help with development and
|
||||||
|
|
|
@ -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>
|
||||||
|
```
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Transformation
|
# Transformation
|
||||||
|
|
||||||
In Loom, *transformation* refers to the process of modifying an abstract
|
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
|
The mechanism provided by loom for these transformations is the
|
||||||
`transform()` function.
|
`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.
|
AST will be considered for transformation.
|
||||||
|
|
||||||
After the AST, any number of additional arguments may be passed to the
|
After the AST, any number of additional arguments may be passed to the
|
||||||
`transform` function. Each of these should be a *transformation function*
|
`transform(ast, ...transformers)` function. Each of these should be a
|
||||||
or an array of such.
|
*transformer*.
|
||||||
|
|
||||||
## Transformation functions
|
## Transformerss
|
||||||
|
|
||||||
Transformer functions take _a copy_ of an AST node. Because it is a copy,
|
### Signature
|
||||||
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.
|
`transformer(node)`
|
||||||
|
|
||||||
|
* `node` - AST node to be transformed. The node includes information about
|
||||||
|
its children, but not its parent.
|
||||||
|
|
||||||
### Return values
|
### 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.
|
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
|
#### 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
|
to replace the original node in the original tree. Subsequent transformer
|
||||||
functions will recieve a copy of the _new_ node, not the original.
|
functions will recieve a copy of the _new_ node, not the original.
|
||||||
|
|
||||||
For convenience, new tree nodes can be created with the
|
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
|
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
|
In the event that a transformers returns any other value than
|
||||||
removed from the AST. No further transformation functions will be called on
|
`null`, `undefined`, or an AST node--as determined by `util.isNode()`,
|
||||||
the same node.
|
the `transform(ast, ...transformers)` function will throw an error.
|
||||||
|
|
||||||
#### undefined
|
## Examples
|
||||||
|
|
||||||
If the transformation function returns `undefined`, the original node will
|
In the following examples, we will use the AST corresponding to the
|
||||||
be left in the AST unchanged. The next transformation function will be
|
following HTML for high intelligibility.
|
||||||
called on the same node.
|
|
||||||
|
|
||||||
### _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
|
We will use the variable `ast` to refer to the root of the AST.
|
||||||
`null`, `undefined`, or an AST node--as determined by `util.isAstNode()`,
|
|
||||||
the `transform()` function will throw an error.
|
### 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@thefarce/loom",
|
"name": "@thefarce/loom",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "A module for weaving form data with content data",
|
"description": "A module for weaving form data with content data",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import { visitParents } from 'unist-util-visit-parents';
|
import { visitParents } from 'unist-util-visit-parents';
|
||||||
import { createContext, runInContext } from 'vm';
|
import { createContext, runInContext } from 'vm';
|
||||||
import { cloneNode } from './util.mjs';
|
import { cloneNode } from './util.mjs';
|
||||||
import { is } from 'unist-util-is';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accumulates context from a lineage of nodes.
|
* Accumulates context from a lineage of nodes.
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { visit, SKIP } from 'unist-util-visit';
|
import { visit, SKIP } from 'unist-util-visit';
|
||||||
import { isAstNode } from './util.mjs';
|
import { isNode } from './util.mjs';
|
||||||
import { is } from 'unist-util-is';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms an AST using a series of transformer functions.
|
* 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
|
// If the function returned an AST node, we replace the working node
|
||||||
// with that AST node and continue processing with the new node.
|
// with that AST node and continue processing with the new node.
|
||||||
else if (isAstNode(result)) {
|
else if (isNode(result)) {
|
||||||
replaced = true;
|
replaced = true;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,6 @@ export function createNode({
|
||||||
* @returns {boolean} True if the value is an object with a "type" property,
|
* @returns {boolean} True if the value is an object with a "type" property,
|
||||||
* false otherwise.
|
* false otherwise.
|
||||||
*/
|
*/
|
||||||
export function isAstNode(node) {
|
export function isNode(node) {
|
||||||
return (typeof node === 'object') && (node !== null) && ('type' in node);
|
return (typeof node === 'object') && (node !== null) && ('type' in node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,6 @@ import {
|
||||||
interpolateValue,
|
interpolateValue,
|
||||||
} from '../src/interpolate.mjs'
|
} from '../src/interpolate.mjs'
|
||||||
|
|
||||||
import {
|
|
||||||
createNode,
|
|
||||||
} from '../src/util.mjs'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimal information on an AST node. 'data' and 'position' are
|
* The minimal information on an AST node. 'data' and 'position' are
|
||||||
* optional.
|
* optional.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
cloneNode,
|
cloneNode,
|
||||||
createNode,
|
createNode,
|
||||||
isAstNode,
|
isNode,
|
||||||
} from '../src/util.mjs';
|
} from '../src/util.mjs';
|
||||||
|
|
||||||
describe("cloneNode()", () => {
|
describe("cloneNode()", () => {
|
||||||
|
@ -66,17 +66,17 @@ describe("createNode()", () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isAstNode()", () => {
|
describe("isNode()", () => {
|
||||||
|
|
||||||
it("should import isAstNode()", () => {
|
it("should import isNode()", () => {
|
||||||
expect(isAstNode).toBeDefined();
|
expect(isNode).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true if the node is an AST node", () => {
|
it("should return true if the node is an AST node", () => {
|
||||||
[
|
[
|
||||||
createNode(),
|
createNode(),
|
||||||
].forEach(value => {
|
].forEach(value => {
|
||||||
expect(isAstNode(value)).toBe(true);
|
expect(isNode(value)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ describe("isAstNode()", () => {
|
||||||
{a:1},
|
{a:1},
|
||||||
{children:[]},
|
{children:[]},
|
||||||
].forEach(value => {
|
].forEach(value => {
|
||||||
expect(isAstNode(value)).toBe(false);
|
expect(isNode(value)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue