diff --git a/README.md b/README.md
index 5ab9140..edc1024 100644
--- a/README.md
+++ b/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('
Hello, {name}!
'),
+// Get our HTML fragment.
+const ast = htmlToAst('Hello,
{name}
!', { 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,28 +74,54 @@ 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 [transformations documentation](./docs/transformations.md) for more
- information.
+See the [interpolation documentation](./docs/interpolation.md) for more
+detailed information.
- Transform the AST according to plugins.
+```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
+detailed information.
## Glossary
### ast
- The AST to be transformed. This can be any AST that conforms to the
- Unified.js specification.
+The AST to be transformed. This can be any AST that conforms to the
+Unified.js specification.
## Contributing to Loom
- 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
- This project contains a `.vimrc` file to help with development and
- maintainance. See that file for more information and instructions for use.
+This project contains a `.vimrc` file to help with development and
+maintainance. See that file for more information and instructions for use.
diff --git a/docs/interpolation.md b/docs/interpolation.md
new file mode 100644
index 0000000..11fd4c9
--- /dev/null
+++ b/docs/interpolation.md
@@ -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 ` 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
+
+ Hello, Othello!
+ Hello, Margaret!
+
+```
+
+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
+
+ Hello, Samwise!
+ Hello, Samwise!
+
+```
+
diff --git a/docs/transformations.md b/docs/transformations.md
index 5059925..c17ec6c 100644
--- a/docs/transformations.md
+++ b/docs/transformations.md
@@ -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
+
+
+
+ Fun Facts about Stuff
+
+ - The Northern Lights are beautiful at night.
+ - Grizzly bears are scary.
+ - Ancient megaliths cover the valley.
+
+
+
+
+```
-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.
\ No newline at end of file
+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);
+ }
+ },
+);
+```
\ No newline at end of file
diff --git a/package.json b/package.json
index f9f5a5f..306d855 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/interpolate.mjs b/src/interpolate.mjs
index feedd26..7810299 100644
--- a/src/interpolate.mjs
+++ b/src/interpolate.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.
diff --git a/src/transform.mjs b/src/transform.mjs
index 3dcb312..dd9b93c 100644
--- a/src/transform.mjs
+++ b/src/transform.mjs
@@ -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;
}
diff --git a/src/util.mjs b/src/util.mjs
index c0e216d..9f6cf20 100644
--- a/src/util.mjs
+++ b/src/util.mjs
@@ -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);
}
diff --git a/test/interpolate.test.mjs b/test/interpolate.test.mjs
index 9ffab68..e3eeaef 100644
--- a/test/interpolate.test.mjs
+++ b/test/interpolate.test.mjs
@@ -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.
diff --git a/test/util.test.mjs b/test/util.test.mjs
index ca48287..22c4298 100644
--- a/test/util.test.mjs
+++ b/test/util.test.mjs
@@ -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);
});
});