Stable commit. Missing a few branch tests.

master
Sir Robert Burbridge 2023-11-29 11:20:51 -05:00
parent 0bc3d52fac
commit ce403c65e6
14 changed files with 961 additions and 5155 deletions

View File

@ -27,15 +27,13 @@ the [documentation](./docs/usage.md).
```js
import {
interpolate, // interpolate data into the AST
htmlToAst, // parse HTML to an AST
astToHtml, // render HTML from an AST
transform, // transform an AST according to plugins
interpolateTree,
transform,
} from '@thefarce/loom'
const initial = htmlToAst('<p>Hello, {name}!</p>'),
const transformed = interpolate(
const transformed = interpolateTree(
transform(initial, ...transformers),
{ name: 'Othello' },
);
@ -51,10 +49,6 @@ Running this script produces the following output:
## API
### astToHtml(ast)
### htmlToAst(html)
### interpolate(ast, data)
### transform(ast, plugins)
@ -69,8 +63,7 @@ Running this script produces the following output:
### ast
The AST to be transformed. This can be any AST that conforms to the
Unified.js specification, especially the
[hast](https://github.com/syntax-tree/hast) AST.
Unified.js specification.
## Contributing to Loom

View File

@ -1,5 +1,24 @@
import htmlToAst from './src/html-to-ast.mjs'
import {
getUnifiedContext,
interpolateArray,
interpolateNode,
interpolateObject,
interpolateString,
interpolateTree,
interpolateValue,
} from './src/interpolate.mjs'
import {
transform,
} from './src/transform.mjs'
export default {
htmlToAst,
getUnifiedContext,
interpolateArray,
interpolateNode,
interpolateObject,
interpolateString,
interpolateTree,
interpolateValue,
transform,
}

4849
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@thefarce/loom",
"version": "0.0.2",
"version": "0.1.0",
"description": "A module for weaving form data with content data",
"type": "module",
"main": "index.mjs",
@ -15,9 +15,7 @@
"jest": "^29.7.0"
},
"dependencies": {
"hast-util-from-html": "^2.0.1",
"hast-util-to-html": "^9.0.0",
"unist-util-parents": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.1"
}
}

View File

@ -1,7 +0,0 @@
import { toHtml } from 'hast-util-to-html';
export function htmlToAst(ast) {
return toHtml(ast);
}
export default htmlToAst;

View File

@ -1,10 +0,0 @@
import { fromHtml } from 'hast-util-from-html';
export function htmlToAst(source) {
return fromHtml(source, {
fragment : true,
verbose : false,
});
}
export default htmlToAst;

View File

@ -1,79 +1,86 @@
import { visitParents } from 'unist-util-visit-parents';
import { createContext, runInContext } from 'vm';
import { cloneNode } from './util.mjs';
import { is } from 'unist-util-is';
export function interpolateTree(tree, data, mask = {}, options = {}) {
tree.data = tree.data || {};
export function getUnifiedContext (lineage = []) {
return lineage.reduce((context, node) => {
return Object.assign({}, context, node?.data?.context || {});
}, {});
}
if (data) {
tree.data.interp = data;
}
export function interpolateTree(tree, contextMask = {}) {
let result = cloneNode(tree);
const opt = Object.assign({
rootNodeOnly: false,
}, options);
visitParents(result, (node, ancestors) => {
let context = Object.assign(
{},
getUnifiedContext([...ancestors, node]),
contextMask
);
let interpolatedNode = interpolateNode(node, context);
Object.assign(node, interpolatedNode);
visitParents(tree, (node, ancestors) => {
if (opt.rootNodeOnly && node !== tree) {
return;
}
const data = getMaskedData(node, ancestors);
if (node.properties && typeof node.properties === 'object') {
for (const key in node.properties) {
if (typeof node.properties[key] === 'string') {
node.properties[key] = interpolateString(
node.properties[key],
data
);
} else if (Array.isArray(node.properties[key])) {
node.properties[key] = node.properties[key].map(
(value) => interpolateString(value, data)
);
}
else {
node.properties[key] = '' + node.properties[key]
}
}
}
if (node.type === 'text' && node.value) {
node.value = interpolateString(node.value, data);
}
})
return tree;
return result;
}
function getMaskedData(node, ancestors, mask = {}) {
var foo = Object.assign(
{},
...(ancestors.map((ancestor) => (ancestor?.data?.interp || {}))),
node?.data?.interp || {},
mask
);
export function interpolateNode(node, contextMask = {}) {
const result = cloneNode(node);
return foo;
let _context = Object.assign({}, node?.data?.context || {}, contextMask);
// Loop through the node object's properties. Interpolate any values that
// are strings _unless_ it is the 'type', 'data', or 'position' property.
// Just skip those.
for (const key in node) {
if (['type', 'data', 'position', 'children'].includes(key)) {
continue;
}
result[key] = interpolateValue(node[key], _context);
}
return result;
}
function interpolateNode(node, data, mask) {
return interpolateTree(node, data, mask, { rootNodeOnly: true });
export function interpolateValue (value, context = {}) {
if (typeof value === 'string') {
return interpolateString(value, context);
}
else if (Array.isArray(value)) {
return interpolateArray(value, context);
}
else if (typeof value === 'object') {
return interpolateObject(value, context);
}
else {
return value;
}
}
function interpolateString(str, data) {
const context = createContext({ ...data, ...global });
const interpolatedStr = runInContext(`\`${str}\``, context);
return interpolatedStr;
export function interpolateString(str, data) {
try {
const context = createContext({ ...data, ...global });
return runInContext(`\`${str}\``, context);
} catch (e) {
return str;
}
}
export const __testExports = {
getMaskedData,
interpolateNode,
interpolateString,
export function interpolateArray(arr, context = {}) {
let result = [];
for (let i = 0; i < arr.length; i++) {
result[i] = interpolateValue(arr[i], context);
}
return result;
}
export default interpolateTree;
export function interpolateObject(obj, context = {}) {
for (const key in obj) {
obj[key] = interpolateValue(obj[key], context);
}
return obj;
}

View File

@ -1,6 +1,84 @@
import { visitParents } from 'unist-util-visit-parents';
import { isAstNode } from './util.mjs';
import { is } from 'unist-util-is';
export function transform () {
export function transform (ast, ...transformers) {
let tree = JSON.parse(JSON.stringify(ast));
// Visit every node. For each node, pass it into each function in the
// fxns array. If the function returns undefined, leave the node as-is
// and continue. If the function returns null, delete the node from the
// AST and stop processing it. If the function returns an AST node,
// replace the node with that AST node and continue processing with the
// new node.
visitParents(tree, (node, ancestors) => {
let transformed = transformers.reduce((workingNode, transformer) => {
// If the current node is null, just skip any further processing.
if (workingNode === null) {
return null;
}
// Process the working node through the current function. Make sure
// the transformer function knows not only the node but also its
// ancestors.
let result = transformer(workingNode, ancestors);
// If the function returned null, just skip any further processing.
if (result === null) {
return null;
}
// If the function returned 'undefined', this means that there's no
// specific definition for how the node should be transformed.
// Therefore, we do not transform it. We leave it as-is and pass it
// to the next transformer.
else if (result === undefined) {
return workingNode;
}
// 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)) {
return result;
}
// For anything else, throw an error.
throw new Error(`Invalid transformer result: ${JSON.stringify(result)}`);
}, node);
let parent = ancestors[ancestors.length - 1];
if (!parent) {
return;
}
// Loop through the first most recent ancestor and replace the current
// node with the transformed node. If transformed is null, just delete
// the current node.
if (transformed === null) {
let index = -1;
parent.children.forEach((child, i) => {
if (is(child, node)) {
index = i;
}
})
parent.children.splice(index, 1);
}
// If the transformed node is the same as the current node, do nothing.
else if (is(node, transformed)) {
// do nothing
}
// Finally, replace the current node with the transformed node.
else {
parent.children.forEach((child, index) => {
if (is(child, node)) {
parent.children[index] = transformed;
}
});
}
});
return tree;
}
export default transform;

View File

@ -2,12 +2,18 @@ export function cloneNode (node) {
return JSON.parse(JSON.stringify(node));
}
export function createAstNode (type, props, children) {
export function createNode ({
type = 'created-node',
context = {},
} = {}) {
const ast = {
type: type,
props: props,
children: children || [],
type,
data: {
context,
},
};
return ast;
}

View File

@ -1,15 +0,0 @@
import astToHtml from '../src/ast-to-html.mjs';
import htmlAstMapping from './fixtures/ast-html.fixture.mjs';
describe("ast-to-html", () => {
it("a UnifiedJS AST should be converted to html", () => {
expect(astToHtml).toBeDefined();
// Loop through all the mappings of html to AST and make sure they work.
for (const [html, ast] of Object.entries(htmlAstMapping)) {
expect(astToHtml(ast)).toEqual(html);
}
});
});

View File

@ -1,15 +0,0 @@
import htmlToAst from '../src/html-to-ast.mjs';
import htmlAstMapping from './fixtures/html-ast.fixture.mjs';
describe("html-to-ast", () => {
it("html should be converted to a UnifiedJS AST", () => {
expect(htmlToAst).toBeDefined();
// Loop through all the mappings of html to AST and make sure they work.
for (const [html, ast] of Object.entries(htmlAstMapping)) {
expect(htmlToAst(html)).toEqual(ast);
}
});
});

View File

@ -1,275 +1,388 @@
import interpolateTree, { __testExports } from '../src/interpolate.mjs';
import htmlToAst from '../src/html-to-ast.mjs';
import astToHtml from '../src/ast-to-html.mjs';
import {
interpolateNode,
interpolateString,
interpolateTree,
interpolateValue,
getUnifiedContext,
} from '../src/interpolate.mjs'
import {
createNode,
} from '../src/util.mjs'
/**
* The minimal information on an AST node. 'data' and 'position' are
* optional.
*
* interface Node {
* type : string
* data : Data?
* position : Position?
* }
*/
describe("importing interpolation", () => {
it("should import", () => {
expect(interpolateTree).toBeDefined();
expect(interpolateString).toBeDefined();
expect(interpolateNode).toBeDefined();
});
});
describe("interpolateNode()", () => {
it("should not interpolate 'type', 'data', or 'position'", () => {
const node = {
type: '${type}',
value: 'Hello, ${name}!',
data: {
context: {
name: "World",
},
},
position: {},
};
expect(interpolateNode(node)).toEqual({
type: '${type}',
value: 'Hello, World!',
data: {
context: {
name: "World",
},
},
position: {},
});
});
it("should interpolate strings deeply in non-children attributes", () => {
const node = {
type: '${type}',
str: 'Hello, ${name}!',
arr: [
'Another ${name}',
[
'Yet another ${name}',
],
],
obj: {
a: 'my ${name}',
b: {
c: 'some ${name}',
},
},
data: {
context: {
name: "World",
},
},
position: {},
};
expect(interpolateNode(node)).toEqual({
type: '${type}',
str: 'Hello, World!',
arr: [
'Another World',
[
'Yet another World',
],
],
obj: {
a: 'my World',
b: {
c: 'some World',
},
},
data: {
context: {
name: "World",
},
},
position: {},
});
});
it("should handle nodes without data", () => {
const node = {
type: '${type}',
str: 'Hello, ${name}!',
arr: [
'Another ${name}',
[
'Yet another ${game}',
],
],
obj: {
a: 'my ${game}',
b: {
c: 'some ${name}',
},
},
data: {
context: {
name: "World",
},
},
position: {},
};
expect(interpolateNode(node)).toEqual({
type: '${type}',
str: 'Hello, World!',
arr: [
'Another World',
[
'Yet another ${game}',
],
],
obj: {
a: 'my ${game}',
b: {
c: 'some World',
},
},
data: {
context: {
name: "World",
},
},
position: {},
});
});
it("should handle context masking", () => {
const node = {
type: '${type}',
str: 'Hello, ${name}!',
arr: [
'Another ${name}',
[
'Yet another ${game}',
],
],
obj: {
a: 'my ${game}',
b: {
c: 'some ${name}',
},
},
data: {
context: {
name: "World",
},
},
position: {},
};
expect(interpolateNode(node, {name: "Charles", game: "croquet"})).toEqual({
type: '${type}',
str: 'Hello, Charles!',
arr: [
'Another Charles',
[
'Yet another croquet',
],
],
obj: {
a: 'my croquet',
b: {
c: 'some Charles',
},
},
data: {
context: {
name: "World",
},
},
position: {},
});
});
});
describe("interpolateTree()", () => {
it("should import", () => {
expect(interpolateTree).toBeDefined();
});
it("should handle nodes without properties", () => {
it("should handle implicit context masking", () => {
const node = {
type: 'text',
value: 'Test',
data: {},
};
const data = {};
expect(() => interpolateTree(node, data)).not.toThrow();
});
it("should handle nodes without data", () => {
const node = {
type: 'text',
value: 'Test',
data: {},
};
expect(() => interpolateTree(node)).not.toThrow();
});
it("should handle nodes with array properties", () => {
const node = {
properties: {
test: ['${value}'],
type: '${type}',
data: {
context: {
name: "World",
},
},
data: {},
};
const data = { value: 'interpolated' };
const interpolated = interpolateTree(node, data);
expect(interpolated.properties.test[0]).toEqual('interpolated');
});
it("should handle nodes without data", () => {
const node = {
properties: {
test: '${value}',
},
};
const data = { value: 'interpolated' };
const interpolated = interpolateTree(node, data);
expect(interpolated.properties.test).toEqual('interpolated');
});
describe("getMaskedData()", () => {
it("should return masked data from ancestors", () => {
const node = { data: { interp: { test: 'value' } } };
const ancestors = [{ data: { interp: { parentValue: 'parent' } } }];
const maskedData = __testExports.getMaskedData(node, ancestors);
expect(maskedData).toEqual({ parentValue: 'parent', test: 'value' });
});
});
// interpolateNode() tests
describe("interpolateNode()", () => {
it("should interpolate node properties", () => {
const node = {
properties: {
test: '${value}',
position: {},
children: [
{
type: 'text',
value: 'It is the new ${name}!',
},
data: {
interp: {
value: 'interpolated',
},
},
};
const interpolatedNode = __testExports.interpolateNode(node);
expect(interpolatedNode.properties.test).toEqual('interpolated');
});
it("should abandon all its children with disdain", () => {
const node = {
properties: {
test: '${value}',
},
data: {
interp: {
value: 'interpolated',
},
},
children: [{
properties: {
test: '${piggly}',
},
{
type: 'element',
tagName: 'div',
data: {
interp: {
value: 'wiggly',
context: {
name: "Jasmine",
},
},
}],
};
const interpolatedNode = __testExports.interpolateNode(node);
expect(interpolatedNode.children[0].properties.test).toEqual('${piggly}');
});
it("should handle node properties that are not strings or arrays", () => {
const node = {
properties: {
prop: 45,
children: [
{
type: 'some-${name}',
value: 'Hello, ${name}!',
},
{
type: 'some-${name}',
value: 'Hello, ${name}!',
data: {
context: {
name: "Charles",
},
},
},
],
},
data: {},
};
const interpolatedNode = __testExports.interpolateNode(node);
expect(interpolatedNode.properties.prop).toEqual('45');
});
{
type: 'text',
value: 'Another ${game}',
},
],
};
let interpolated = interpolateTree(node);
expect(interpolated.children[0].value).toBe("It is the new World!");
expect(interpolated.children[1].children[0].value).toBe("Hello, Jasmine!");
expect(interpolated.children[1].children[1].value).toBe("Hello, Charles!");
expect(interpolated.children[2].value).toBe("Another ${game}");
});
describe("interpolateString()", () => {
it("should interpolate string with data", () => {
const str = '${value}';
const data = { value: 'interpolated' };
const interpolatedStr = __testExports.interpolateString(str, data);
expect(interpolatedStr).toEqual('interpolated');
});
it("should throw on an undefined variable", () => {
expect(() => __testExports.interpolateString("${f}", {})).toThrow();
expect(() => __testExports.interpolateString("${f||'foo'}", {})).toThrow();
});
});
it("should interpolate a single node", () => {
const uninterpolated = '<div class="${classname}"></div>';
const expected = '<div class="foo"></div>';
const data = { classname: "foo" };
const ast = htmlToAst(uninterpolated);
const interpolated = interpolateTree(ast, data);
const rendered = astToHtml(interpolated);
expect(rendered).toEqual(expected);
})
it("should interpolate a nested tree", () => {
const uninterpolated = `
<div class="\${classname}">
<p>This is a \${data.stringInPTag} in a p tag</p>
<p id="\${data.id}">This is a p with an interpolated id</p>
<ul class="\${data.listClass}">
<li>\${data.listItem}</li>
</ul>
</div>
`;
const expected = `
<div class="foo">
<p>This is a string in a p tag</p>
<p id="foo">This is a p with an interpolated id</p>
<ul class="bar">
<li>baz</li>
</ul>
</div>
`;
const data = {
classname: "foo",
it("should handle explicit context masking", () => {
const node = {
type: '${type}',
data: {
stringInPTag: "string",
id: "foo",
listClass: "bar",
listItem: "baz",
context: {
name: "World",
},
},
position: {},
children: [
{
type: 'element',
tagName: 'div',
children: [
{
type: 'text',
value: 'Hello, ${name}!',
},
],
},
{
type: 'text',
value: 'Another ${game}',
},
],
};
const ast = htmlToAst(uninterpolated);
const interpolated = interpolateTree(ast, data);
const rendered = astToHtml(interpolated);
expect(rendered).toEqual(expected);
})
let interpolated = interpolateTree(node, {name: "Charles", game: "croquet"});
expect(interpolated.children[0].children[0].value).toBe("Hello, Charles!");
expect(interpolated.children[1].value).toBe("Another croquet");
});
});
/**
* interpolateString() is where the rubber hits the road. Everything
* that gets interpolated eventually does it through interpolateString().
*/
describe("interpolateString()", () => {
it("should handle arbitrary javascript", () => {
const uninterpolated = `
<div class="\${classname}">
<p>simple: 3 * 12 = \${3 * 12}</p>
<p>named function: 3^2 = \${square(3)}</p>
<p>arrow function: 5*2 = \${(num => (num * 2))(5)}</p>
<p>arrow function: 3-8 = \${(num => (num - 8))(3)}</p>
</div>
`;
const expected = `
<div class="foo">
<p>simple: 3 * 12 = 36</p>
<p>named function: 3^2 = 9</p>
<p>arrow function: 5*2 = 10</p>
<p>arrow function: 3-8 = -5</p>
</div>
`;
const str = "sum: ${count + 5}";
const context = { count: 9 };
expect(interpolateString(str, context)).toEqual("sum: 14");
});
const data = {
classname: "foo",
square: (num) => Math.pow(num, 2),
it("should handle javascript globals", () => {
const str = "total: ${Math.pow(5,3)}";
expect(interpolateString(str, {})).toEqual("total: 125");
});
it("should handle multiple interpolations", () => {
const str = "${count} squared: ${Math.pow(count, 2)}";
const context = { count: 9 };
expect(interpolateString(str, context)).toEqual("9 squared: 81");
});
it("should handle function interpolations", () => {
const str = "${count} squared: ${(() => (count * count))()}";
const context = { count: 9 };
expect(interpolateString(str, context)).toEqual("9 squared: 81");
});
})
describe("interpolateValue()", () => {
it("should interpolate strings", () => {
const str = "sum: ${count + 5}";
const context = { count: 9 };
expect(interpolateValue(str, context)).toEqual("sum: 14");
});
it("should interpolate arrays", () => {
const arr = ["${count}"];
const context = { count: 9 };
expect(interpolateValue(arr, context)).toEqual(["9"]);
expect(interpolateValue([1,2,3])).toEqual([1,2,3]);
});
it("should interpolate objects", () => {
const obj = {
a: "${count}",
};
const ast = htmlToAst(uninterpolated);
const interpolated = interpolateTree(ast, data);
const rendered = astToHtml(interpolated);
expect(rendered).toEqual(expected);
})
const context = { count: 9 };
expect(interpolateValue(obj, context)).toEqual({a:"9"});
});
it("should pass through other values", () => {
const val = 12;
const context = { count: 9 };
expect(interpolateValue(val, context)).toEqual(12);
});
});
describe("getMaskedData()", () => {
it("should import", () => {
expect(__testExports.getMaskedData).toBeDefined();
})
it("should shadow ancestor data with current node data", () => {
const maskedData = __testExports.getMaskedData(
describe("getUnifiedContext()", () => {
it("should return the unified context", () => {
const ancestry = [
{
data: { interp: { foo: "child" } },
type: 'element',
tagName: 'div',
data: {
context: {
name: "Jasmine",
game: "croquet",
},
},
},
// Give a list of ancestors.
[
{ data: { interp: { foo: "root" } } },
{ data: { interp: { foo: "parent" } } },
],
);
expect(maskedData).toEqual({foo: "child"})
});
it("should shadow ancestor data with current node data", () => {
const maskedData = __testExports.getMaskedData(
{
data: { interp: { foo: "child" } },
type: 'element',
tagName: 'div',
data: {
context: {
name: "Charles",
same: "things",
},
},
},
// Give a list of ancestors.
[
{ data: { interp: { foo: "root" } } },
{ data: { interp: { foo: "parent" } } },
],
);
expect(maskedData).toEqual({foo: "child"})
});
it("should shadow earlier ancestors with later ancestor data", () => {
const maskedData = __testExports.getMaskedData(
{
data: { interp: {} },
type: 'text',
value: "pigs",
},
// Give a list of ancestors.
[
{ data: { interp: { foo: "root" } } },
{ data: { interp: { foo: "parent" } } },
],
);
];
expect(maskedData).toEqual({foo: "parent"})
expect(getUnifiedContext(ancestry)).toEqual({
name: "Charles",
game: "croquet",
same: "things",
});
});
it("should shadow earlier ancestors without current data", () => {
const maskedData = __testExports.getMaskedData(
{ data: {} },
// Give a list of ancestors.
[
{ data: { interp: { foo: "root" } } },
{ data: { interp: { foo: "parent" } } },
],
);
expect(maskedData).toEqual({foo: "parent"})
});
});

View File

@ -1,7 +1,17 @@
import transform from '../src/transform.mjs';
import htmlToAst from '../src/html-to-ast.mjs';
describe("transform", () => {
const ast_emptyDiv = {
"type": "root",
"children": [
{
"type": "element",
"tagName": "div",
"properties": {},
"children": [],
},
],
};
it("should import transform()", () => {
expect(transform).toBeDefined();
@ -13,10 +23,7 @@ describe("transform", () => {
});
it("should act as an identity function if there are no tx fxns", () => {
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(ast)).toEqual({
expect(transform(ast_emptyDiv)).toEqual({
"type": "root",
"children": [
{
@ -24,28 +31,13 @@ describe("transform", () => {
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
});
});
it("should leave the ast untouched for null", () => {
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(ast, () => null)).toEqual({
it("should leave the ast untouched for undefined", () => {
expect(transform(ast_emptyDiv, () => undefined)).toEqual({
"type": "root",
"children": [
{
@ -53,209 +45,130 @@ describe("transform", () => {
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
});
});
it("should remove the node for undefined", () => {
const html = '<div></div>';
const ast = htmlToAst(html);
it("should remove the node for null", () => {
expect(transform(ast_emptyDiv, () => null)).toEqual({
"type": "root",
"children": [],
});
});
expect(transform(ast, () => undefined)).toEqual({
it("should barf if non-AST is returned", () => {
expect(() => {transform(ast_emptyDiv, () => "pigs!")}).toThrow();
});
it("should replace old nodes with transformed nodes", () => {
expect(transform(
ast_emptyDiv,
() => ({type: "element", tagName: "div", children: []})
)).toEqual({
"type": "root",
"children": [
{
type: "element",
tagName: "div",
children: [],
},
],
});
});
it("should transform nodes anywhere in the child list", () => {
expect(transform(
{
"type": "root",
"children": [
{
type: "element",
tagName: "div",
children: [],
},
{
type: "element",
tagName: "span",
children: [],
},
],
},
(node) => {
if (node.tagName === "span") {
return {
type: "element",
tagName: "p",
children: [],
};
}
}
)).toEqual({
"type": "root",
"children": [
{
type: "element",
tagName: "div",
children: [],
},
{
type: "element",
tagName: "p",
children: [],
},
],
});
});
it("should delete nodes anywhere in the child list", () => {
expect(transform(
{
"type": "root",
"children": [
{
type: "element",
tagName: "div",
children: [],
},
{
type: "element",
tagName: "span",
children: [],
},
],
},
(node) => {
if (node.tagName === "span") {
return null;
}
}
)).toEqual({
"type": "root",
"children": [
{
type: "element",
tagName: "div",
children: [],
},
],
});
});
it("should stop transforming after a transformer returns null", () => {
expect(transform(
ast_emptyDiv,
() => null,
() => ({type: "element", tagName: "div", children: []})
)).toEqual({
"type": "root",
"children": [],
});
});
it("should handle multiple tx fxns", () => {
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(
ast,
() => ({
"type": "root",
"children": [
{
"type": "element",
"tagName": "span",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
}),
() => null,
)).toEqual({
"type": "root",
"children": [
{
"type": "element",
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
});
});
it("should handle tx fxns that return: null, node ", () => {
expect(transform).toBeDefined();
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(ast, () => null)).toEqual({
"type": "root",
"children": [
{
"type": "element",
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
});
});
it("should handle tx fxns that return: node, null ", () => {
expect(transform).toBeDefined();
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(ast, () => null)).toEqual({
"type": "root",
"children": [
{
"type": "element",
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
});
});
it("should handle tx fxns that return: undefined, node ", () => {
expect(transform).toBeDefined();
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(ast, () => null)).toEqual({
"type": "root",
"children": [
{
"type": "element",
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
});
});
it("should handle tx fxns that return: node, undefined", () => {
expect(transform).toBeDefined();
const html = '<div></div>';
const ast = htmlToAst(html);
expect(transform(ast, () => null)).toEqual({
"type": "root",
"children": [
{
"type": "element",
"tagName": "div",
"properties": {},
"children": [],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0,
},
"end": {
"line": 1,
"column": 12,
"offset": 11,
},
},
},
],
const output = transform(ast_emptyDiv, () => {return}, () => null);
expect(output).toEqual({
type: "root",
children: [],
});
});

View File

@ -1,6 +1,6 @@
import {
cloneNode,
createAstNode,
createNode,
isAstNode,
} from '../src/util.mjs';
@ -40,17 +40,18 @@ describe("cloneNode()", () => {
});
});
describe("createAstNode()", () => {
describe("createNode()", () => {
it("should import createAstNode()", () => {
expect(createAstNode).toBeDefined();
it("should import createNode()", () => {
expect(createNode).toBeDefined();
})
it("should create a generic AST node with no arguments", () => {
expect(createAstNode()).toEqual({
type: undefined,
value: undefined,
children: [],
expect(createNode()).toEqual({
type: 'created-node',
data: {
context: {},
},
})
})
@ -64,7 +65,7 @@ describe("isAstNode()", () => {
it("should return true if the node is an AST node", () => {
[
createAstNode(),
createNode(),
].forEach(value => {
expect(isAstNode(value)).toBe(true);
});