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
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue