Stable commit. Missing a few branch tests.
parent
0bc3d52fac
commit
ce403c65e6
15
README.md
15
README.md
|
@ -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
|
||||
|
||||
|
|
23
index.mjs
23
index.mjs
|
@ -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,
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { toHtml } from 'hast-util-to-html';
|
||||
|
||||
export function htmlToAst(ast) {
|
||||
return toHtml(ast);
|
||||
}
|
||||
|
||||
export default htmlToAst;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
14
src/util.mjs
14
src/util.mjs
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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"})
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue