From 0d78790413f91b5c93f5c8a2cf465beb378ec832 Mon Sep 17 00:00:00 2001 From: Sir Robert Burbridge Date: Thu, 30 Nov 2023 10:30:50 -0500 Subject: [PATCH] Add a buncha docs and change isAstNode to isNode --- README.md | 91 +++++++++++++++++----- docs/interpolation.md | 123 ++++++++++++++++++++++++++++++ docs/transformations.md | 154 ++++++++++++++++++++++++++++++++------ package.json | 2 +- src/interpolate.mjs | 1 - src/transform.mjs | 5 +- src/util.mjs | 2 +- test/interpolate.test.mjs | 4 - test/util.test.mjs | 12 +-- 9 files changed, 335 insertions(+), 59 deletions(-) create mode 100644 docs/interpolation.md 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); }); });