@weborigami/async-tree 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/shared.js +3 -0
- package/src/drivers/ObjectTree.js +8 -2
- package/src/operations/indent.js +120 -0
- package/src/operations/json.js +20 -0
- package/test/operations/indent.test.js +44 -0
- package/test/operations/json.test.js +18 -0
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
7
7
|
"browser": "./browser.js",
|
|
8
8
|
"types": "./index.ts",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@weborigami/types": "0.
|
|
10
|
+
"@weborigami/types": "0.5.0"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@types/node": "
|
|
14
|
-
"puppeteer": "24.
|
|
15
|
-
"typescript": "5.
|
|
13
|
+
"@types/node": "24.3.0",
|
|
14
|
+
"puppeteer": "24.17.0",
|
|
15
|
+
"typescript": "5.9.2"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"headlessTest": "node test/browser/headlessTest.js",
|
package/shared.js
CHANGED
|
@@ -22,8 +22,11 @@ export { default as deepValues } from "./src/operations/deepValues.js";
|
|
|
22
22
|
export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
|
|
23
23
|
export { default as extensionKeyFunctions } from "./src/operations/extensionKeyFunctions.js";
|
|
24
24
|
export { default as filter } from "./src/operations/filter.js";
|
|
25
|
+
export { default as globKeys } from "./src/operations/globKeys.js";
|
|
25
26
|
export { default as group } from "./src/operations/group.js";
|
|
27
|
+
export { default as indent } from "./src/operations/indent.js";
|
|
26
28
|
export { default as invokeFunctions } from "./src/operations/invokeFunctions.js";
|
|
29
|
+
export { default as json } from "./src/operations/json.js";
|
|
27
30
|
export { default as map } from "./src/operations/map.js";
|
|
28
31
|
export { default as mask } from "./src/operations/mask.js";
|
|
29
32
|
export { default as merge } from "./src/operations/merge.js";
|
|
@@ -56,8 +56,14 @@ export default class ObjectTree {
|
|
|
56
56
|
|
|
57
57
|
setParent(value, this);
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Is value an instance method? The first clause sees if the object is a
|
|
60
|
+
// constructor, in which case the method is likely a static method.
|
|
61
|
+
const isInstanceMethod =
|
|
62
|
+
!(this.object instanceof Function) &&
|
|
63
|
+
value instanceof Function &&
|
|
64
|
+
!Object.hasOwn(this.object, key);
|
|
65
|
+
if (isInstanceMethod) {
|
|
66
|
+
// Bind it to the object
|
|
61
67
|
value = value.bind(this.object);
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { deepText, toString, Tree } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
const lastLineWhitespaceRegex = /\n(?<indent>[ \t]*)$/;
|
|
4
|
+
|
|
5
|
+
const mapStringsToModifications = new Map();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalize indentation in a tagged template string
|
|
9
|
+
*
|
|
10
|
+
* @param {TemplateStringsArray} strings
|
|
11
|
+
* @param {...any} values
|
|
12
|
+
* @returns {Promise<string>}
|
|
13
|
+
*/
|
|
14
|
+
export default async function indent(strings, ...values) {
|
|
15
|
+
let modified = mapStringsToModifications.get(strings);
|
|
16
|
+
if (!modified) {
|
|
17
|
+
modified = modifyStrings(strings);
|
|
18
|
+
mapStringsToModifications.set(strings, modified);
|
|
19
|
+
}
|
|
20
|
+
const { blockIndentations, strings: modifiedStrings } = modified;
|
|
21
|
+
const valueTexts = await Promise.all(
|
|
22
|
+
values.map((value) => (Tree.isTreelike(value) ? deepText(value) : value))
|
|
23
|
+
);
|
|
24
|
+
return joinBlocks(modifiedStrings, valueTexts, blockIndentations);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Join strings and values, applying the given block indentation to the lines of
|
|
28
|
+
// values for block placholders.
|
|
29
|
+
function joinBlocks(strings, values, blockIndentations) {
|
|
30
|
+
let result = strings[0];
|
|
31
|
+
for (let i = 0; i < values.length; i++) {
|
|
32
|
+
let text = toString(values[i]);
|
|
33
|
+
if (text) {
|
|
34
|
+
const blockIndentation = blockIndentations[i];
|
|
35
|
+
if (blockIndentation) {
|
|
36
|
+
const lines = text.split("\n");
|
|
37
|
+
text = "";
|
|
38
|
+
if (lines.at(-1) === "") {
|
|
39
|
+
// Drop empty last line
|
|
40
|
+
lines.pop();
|
|
41
|
+
}
|
|
42
|
+
for (let line of lines) {
|
|
43
|
+
text += blockIndentation + line + "\n";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
result += text;
|
|
47
|
+
}
|
|
48
|
+
result += strings[i + 1];
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Given an array of template boilerplate strings, return an object { modified,
|
|
54
|
+
// blockIndentations } where `strings` is the array of strings with indentation
|
|
55
|
+
// removed, and `blockIndentations` is an array of indentation strings for each
|
|
56
|
+
// block placeholder.
|
|
57
|
+
function modifyStrings(strings) {
|
|
58
|
+
// Phase one: Identify the indentation based on the first real line of the
|
|
59
|
+
// first string (skipping the initial newline), and remove this indentation
|
|
60
|
+
// from all lines of all strings.
|
|
61
|
+
let indent;
|
|
62
|
+
if (strings.length > 0 && strings[0].startsWith("\n")) {
|
|
63
|
+
// Look for indenttation
|
|
64
|
+
const firstLineWhitespaceRegex = /^\n(?<indent>[ \t]*)/;
|
|
65
|
+
const match = strings[0].match(firstLineWhitespaceRegex);
|
|
66
|
+
indent = match?.groups.indent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Determine the modified strings. If this invoked as a JS tagged template
|
|
70
|
+
// literal, the `strings` argument will be an odd array-ish object that we'll
|
|
71
|
+
// want to convert to a real array.
|
|
72
|
+
let modified;
|
|
73
|
+
if (indent) {
|
|
74
|
+
// De-indent the strings.
|
|
75
|
+
const indentationRegex = new RegExp(`\n${indent}`, "g");
|
|
76
|
+
// The `replaceAll` also converts strings to a real array.
|
|
77
|
+
modified = strings.map((string) =>
|
|
78
|
+
string.replaceAll(indentationRegex, "\n")
|
|
79
|
+
);
|
|
80
|
+
// Remove indentation from last line of last string
|
|
81
|
+
modified[modified.length - 1] = modified
|
|
82
|
+
.at(-1)
|
|
83
|
+
.replace(lastLineWhitespaceRegex, "\n");
|
|
84
|
+
} else {
|
|
85
|
+
// No indentation; just copy the strings so we have a real array
|
|
86
|
+
modified = strings.slice();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Phase two: Identify any block placholders, identify and remove their
|
|
90
|
+
// preceding indentation, and remove the following newline. Work backward from
|
|
91
|
+
// the end towards the start because we're modifying the strings in place and
|
|
92
|
+
// our pattern matching won't work going forward from start to end.
|
|
93
|
+
let blockIndentations = [];
|
|
94
|
+
for (let i = modified.length - 2; i >= 0; i--) {
|
|
95
|
+
// Get the modified before and after substitution with index `i`
|
|
96
|
+
const beforeString = modified[i];
|
|
97
|
+
const afterString = modified[i + 1];
|
|
98
|
+
const match = beforeString.match(lastLineWhitespaceRegex);
|
|
99
|
+
if (match && afterString.startsWith("\n")) {
|
|
100
|
+
// The substitution between these strings is a block substitution
|
|
101
|
+
let blockIndentation = match.groups.indent;
|
|
102
|
+
blockIndentations[i] = blockIndentation;
|
|
103
|
+
// Trim the before and after strings
|
|
104
|
+
if (blockIndentation) {
|
|
105
|
+
modified[i] = beforeString.slice(0, -blockIndentation.length);
|
|
106
|
+
}
|
|
107
|
+
modified[i + 1] = afterString.slice(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Remove newline from start of first string *after* removing indentation.
|
|
112
|
+
if (modified[0].startsWith("\n")) {
|
|
113
|
+
modified[0] = modified[0].slice(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
blockIndentations,
|
|
118
|
+
strings: modified,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
|
|
2
|
+
import { Tree } from "../internal.js";
|
|
3
|
+
import { isUnpackable, toPlainValue } from "../utilities.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render the given tree in JSON format.
|
|
7
|
+
*
|
|
8
|
+
* @param {import("../../index.ts").Treelike} [treelike]
|
|
9
|
+
*/
|
|
10
|
+
export default async function json(treelike) {
|
|
11
|
+
let tree = Tree.from(treelike);
|
|
12
|
+
if (tree === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (isUnpackable(tree)) {
|
|
16
|
+
tree = await tree.unpack();
|
|
17
|
+
}
|
|
18
|
+
const value = await toPlainValue(tree);
|
|
19
|
+
return JSON.stringify(value, null, 2);
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import indent from "../../src/operations/indent.js";
|
|
4
|
+
|
|
5
|
+
describe("taggedTemplateIndent", () => {
|
|
6
|
+
test("joins strings and values together if template isn't a block template", async () => {
|
|
7
|
+
const result = await indent`a ${"b"} c`;
|
|
8
|
+
assert.equal(result, "a b c");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("removes first and last lines if template is a block template", async () => {
|
|
12
|
+
const actual = await indent`
|
|
13
|
+
<p>
|
|
14
|
+
Hello, ${"Alice"}!
|
|
15
|
+
</p>
|
|
16
|
+
`;
|
|
17
|
+
const expected = `
|
|
18
|
+
<p>
|
|
19
|
+
Hello, Alice!
|
|
20
|
+
</p>
|
|
21
|
+
`.trimStart();
|
|
22
|
+
assert.equal(actual, expected);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("indents all lines in a block substitution", async () => {
|
|
26
|
+
const lines = `
|
|
27
|
+
Line 1
|
|
28
|
+
Line 2
|
|
29
|
+
Line 3`.trimStart();
|
|
30
|
+
const actual = await indent`
|
|
31
|
+
<main>
|
|
32
|
+
${lines}
|
|
33
|
+
</main>
|
|
34
|
+
`;
|
|
35
|
+
const expected = `
|
|
36
|
+
<main>
|
|
37
|
+
Line 1
|
|
38
|
+
Line 2
|
|
39
|
+
Line 3
|
|
40
|
+
</main>
|
|
41
|
+
`.trimStart();
|
|
42
|
+
assert.equal(actual, expected);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
|
+
import json from "../../src/operations/json.js";
|
|
5
|
+
|
|
6
|
+
describe("json", () => {
|
|
7
|
+
test("renders a tree in JSON format", async () => {
|
|
8
|
+
const tree = Tree.from({ person1: "Alice", person2: "Bob" });
|
|
9
|
+
const result = await json(tree);
|
|
10
|
+
assert.equal(
|
|
11
|
+
result,
|
|
12
|
+
`{
|
|
13
|
+
"person1": "Alice",
|
|
14
|
+
"person2": "Bob"
|
|
15
|
+
}`
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
});
|