@anyblades/buildawesome-kit-plugin 1.3.0-alpha
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/features/autoLinkFavicons.js +71 -0
- package/features/autoLinkFavicons.test.js +694 -0
- package/features/mdAutoNl2br.js +22 -0
- package/features/mdAutoNl2br.test.js +68 -0
- package/features/mdAutoRawTags.js +17 -0
- package/features/mdAutoRawTags.test.js +80 -0
- package/features/mdAutoUncommentAttrs.js +26 -0
- package/features/mdAutoUncommentAttrs.test.js +65 -0
- package/features/siteData.js +43 -0
- package/features/siteData.test.js +120 -0
- package/features/virtualPages.js +18 -0
- package/filters/attr_concat.js +55 -0
- package/filters/attr_concat.test.js +205 -0
- package/filters/attr_includes.js +41 -0
- package/filters/attr_includes.test.js +125 -0
- package/filters/attr_set.js +22 -0
- package/filters/attr_set.test.js +71 -0
- package/filters/date.js +11 -0
- package/filters/date.test.js +33 -0
- package/filters/fetch.js +80 -0
- package/filters/if.js +24 -0
- package/filters/if.test.js +63 -0
- package/filters/merge.js +51 -0
- package/filters/merge.test.js +51 -0
- package/filters/remove_tag.js +42 -0
- package/filters/remove_tag.test.js +60 -0
- package/filters/section.js +82 -0
- package/filters/section.test.js +174 -0
- package/filters/split.js +9 -0
- package/filters/split.test.js +39 -0
- package/filters/strip_tag.js +39 -0
- package/filters/strip_tag.test.js +74 -0
- package/filters/unindent.js +11 -0
- package/filters/unindent.test.js +49 -0
- package/package.json +48 -0
- package/plugin.js +53 -0
- package/plugin.test.js +8 -0
- package/scripts/README.md +59 -0
- package/scripts/package.json +17 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import attrConcat from "./attr_concat.js";
|
|
4
|
+
|
|
5
|
+
describe("attrConcat", () => {
|
|
6
|
+
it("should concatenate array values to existing array attribute", () => {
|
|
7
|
+
const obj = { classes: ["foo", "bar"] };
|
|
8
|
+
const result = attrConcat(obj, "classes", ["baz", "qux"]);
|
|
9
|
+
|
|
10
|
+
assert.deepStrictEqual(result, {
|
|
11
|
+
classes: ["foo", "bar", "baz", "qux"],
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should parse JSON array string", () => {
|
|
16
|
+
const obj = { classes: ["foo", "bar"] };
|
|
17
|
+
const result = attrConcat(obj, "classes", '["baz", "qux", "quux"]');
|
|
18
|
+
|
|
19
|
+
assert.deepStrictEqual(result, {
|
|
20
|
+
classes: ["foo", "bar", "baz", "qux", "quux"],
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should handle single string value (not JSON)", () => {
|
|
25
|
+
const obj = { classes: ["foo"] };
|
|
26
|
+
const result = attrConcat(obj, "classes", "bar baz");
|
|
27
|
+
|
|
28
|
+
assert.deepStrictEqual(result, {
|
|
29
|
+
classes: ["foo", "bar baz"],
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should create new array if attribute doesn't exist", () => {
|
|
34
|
+
const obj = { name: "test" };
|
|
35
|
+
const result = attrConcat(obj, "classes", ["foo", "bar"]);
|
|
36
|
+
|
|
37
|
+
assert.deepStrictEqual(result, {
|
|
38
|
+
name: "test",
|
|
39
|
+
classes: ["foo", "bar"],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should handle empty object", () => {
|
|
44
|
+
const obj = {};
|
|
45
|
+
const result = attrConcat(obj, "classes", ["foo", "bar"]);
|
|
46
|
+
|
|
47
|
+
assert.deepStrictEqual(result, {
|
|
48
|
+
classes: ["foo", "bar"],
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle null object", () => {
|
|
53
|
+
const result = attrConcat(null, "classes", ["foo", "bar"]);
|
|
54
|
+
|
|
55
|
+
assert.deepStrictEqual(result, {
|
|
56
|
+
classes: ["foo", "bar"],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should handle single value (non-array, non-string)", () => {
|
|
61
|
+
const obj = { ids: [1, 2] };
|
|
62
|
+
const result = attrConcat(obj, "ids", 3);
|
|
63
|
+
|
|
64
|
+
assert.deepStrictEqual(result, {
|
|
65
|
+
ids: [1, 2, 3],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should not mutate original object", () => {
|
|
70
|
+
const obj = { classes: ["foo", "bar"] };
|
|
71
|
+
const result = attrConcat(obj, "classes", ["baz"]);
|
|
72
|
+
|
|
73
|
+
assert.deepStrictEqual(obj, { classes: ["foo", "bar"] });
|
|
74
|
+
assert.deepStrictEqual(result, { classes: ["foo", "bar", "baz"] });
|
|
75
|
+
assert.notStrictEqual(obj, result);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should preserve other attributes", () => {
|
|
79
|
+
const obj = { classes: ["foo"], id: "test", data: { value: 42 } };
|
|
80
|
+
const result = attrConcat(obj, "classes", ["bar"]);
|
|
81
|
+
|
|
82
|
+
assert.deepStrictEqual(result, {
|
|
83
|
+
classes: ["foo", "bar"],
|
|
84
|
+
id: "test",
|
|
85
|
+
data: { value: 42 },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should handle empty array values", () => {
|
|
90
|
+
const obj = { classes: ["foo"] };
|
|
91
|
+
const result = attrConcat(obj, "classes", []);
|
|
92
|
+
|
|
93
|
+
assert.deepStrictEqual(result, {
|
|
94
|
+
classes: ["foo"],
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should handle empty string values", () => {
|
|
99
|
+
const obj = { classes: ["foo"] };
|
|
100
|
+
const result = attrConcat(obj, "classes", "");
|
|
101
|
+
|
|
102
|
+
assert.deepStrictEqual(result, {
|
|
103
|
+
classes: ["foo", ""],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle multiline strings as single value (not JSON)", () => {
|
|
108
|
+
const obj = { classes: ["foo"] };
|
|
109
|
+
const result = attrConcat(obj, "classes", "bar\n\nbaz\n\n\nqux");
|
|
110
|
+
|
|
111
|
+
assert.deepStrictEqual(result, {
|
|
112
|
+
classes: ["foo", "bar\n\nbaz\n\n\nqux"],
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should remove duplicate values from existing array", () => {
|
|
117
|
+
const obj = { classes: ["foo", "bar"] };
|
|
118
|
+
const result = attrConcat(obj, "classes", ["bar", "baz"]);
|
|
119
|
+
|
|
120
|
+
assert.deepStrictEqual(result, {
|
|
121
|
+
classes: ["foo", "bar", "baz"],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should remove duplicate values from new array", () => {
|
|
126
|
+
const obj = { classes: ["foo"] };
|
|
127
|
+
const result = attrConcat(obj, "classes", ["bar", "bar", "baz", "baz"]);
|
|
128
|
+
|
|
129
|
+
assert.deepStrictEqual(result, {
|
|
130
|
+
classes: ["foo", "bar", "baz"],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should handle duplicates in JSON array strings", () => {
|
|
135
|
+
const obj = { classes: ["foo", "bar"] };
|
|
136
|
+
const result = attrConcat(obj, "classes", '["bar", "baz", "bar", "qux"]');
|
|
137
|
+
|
|
138
|
+
assert.deepStrictEqual(result, {
|
|
139
|
+
classes: ["foo", "bar", "baz", "qux"],
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should preserve order and remove only duplicates", () => {
|
|
144
|
+
const obj = { classes: ["a", "b", "c"] };
|
|
145
|
+
const result = attrConcat(obj, "classes", ["b", "d", "e", "a"]);
|
|
146
|
+
|
|
147
|
+
assert.deepStrictEqual(result, {
|
|
148
|
+
classes: ["a", "b", "c", "d", "e"],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should parse valid JSON array string", () => {
|
|
153
|
+
const obj = { classes: ["foo"] };
|
|
154
|
+
const result = attrConcat(obj, "classes", '["bar", "baz", "qux"]');
|
|
155
|
+
|
|
156
|
+
assert.deepStrictEqual(result, {
|
|
157
|
+
classes: ["foo", "bar", "baz", "qux"],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should treat non-JSON string as single value", () => {
|
|
162
|
+
const obj = { classes: ["foo"] };
|
|
163
|
+
const result = attrConcat(obj, "classes", "bar\nbaz\nqux");
|
|
164
|
+
|
|
165
|
+
assert.deepStrictEqual(result, {
|
|
166
|
+
classes: ["foo", "bar\nbaz\nqux"],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should keep plain string as single value", () => {
|
|
171
|
+
const obj = { tags: ["existing"] };
|
|
172
|
+
const result = attrConcat(obj, "tags", "single value");
|
|
173
|
+
|
|
174
|
+
assert.deepStrictEqual(result, {
|
|
175
|
+
tags: ["existing", "single value"],
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should parse JSON with numbers", () => {
|
|
180
|
+
const obj = { ids: [1, 2] };
|
|
181
|
+
const result = attrConcat(obj, "ids", "[3, 4, 5]");
|
|
182
|
+
|
|
183
|
+
assert.deepStrictEqual(result, {
|
|
184
|
+
ids: [1, 2, 3, 4, 5],
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should treat invalid JSON as single string value", () => {
|
|
189
|
+
const obj = { classes: ["foo"] };
|
|
190
|
+
const result = attrConcat(obj, "classes", '["bar", "baz"'); // Invalid JSON
|
|
191
|
+
|
|
192
|
+
assert.deepStrictEqual(result, {
|
|
193
|
+
classes: ["foo", '["bar", "baz"'],
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should treat JSON non-array as single value", () => {
|
|
198
|
+
const obj = { tags: ["foo"] };
|
|
199
|
+
const result = attrConcat(obj, "tags", '{"key": "value"}');
|
|
200
|
+
|
|
201
|
+
assert.deepStrictEqual(result, {
|
|
202
|
+
tags: ["foo", '{"key": "value"}'],
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*<!--section:docs-->
|
|
2
|
+
|
|
3
|
+
A filter that filters a list of items by checking if an attribute array includes a target value. Supports nested attribute names using dot notation.
|
|
4
|
+
|
|
5
|
+
<!--section:code-->```js */
|
|
6
|
+
import lodash from "@11ty/lodash-custom";
|
|
7
|
+
const { get } = lodash;
|
|
8
|
+
|
|
9
|
+
export default function (collection, attrName, targetValue) {
|
|
10
|
+
// If no targetValue, return original collection
|
|
11
|
+
if (!targetValue) {
|
|
12
|
+
return collection;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return collection.filter((item) => {
|
|
16
|
+
// Get the attribute value from the item (supports nested paths like "data.tags")
|
|
17
|
+
const attrValue = get(item, attrName);
|
|
18
|
+
|
|
19
|
+
// If the attribute is an array, check if it includes the target value
|
|
20
|
+
if (Array.isArray(attrValue)) {
|
|
21
|
+
return attrValue.includes(targetValue);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Otherwise skip this item
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/*```<!--section:docs-->
|
|
29
|
+
|
|
30
|
+
### Examples
|
|
31
|
+
|
|
32
|
+
Get all posts that include `#javascript` tag
|
|
33
|
+
|
|
34
|
+
```jinja2 {data-caption="in .njk:"}
|
|
35
|
+
{% set js_posts = collections.all | attr_includes('data.tags', '#javascript') %}
|
|
36
|
+
|
|
37
|
+
{% for post in js_posts %}
|
|
38
|
+
<h2>{{ post.data.title }}</h2>
|
|
39
|
+
{% endfor %}
|
|
40
|
+
```
|
|
41
|
+
*/
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import attrIncludes from "./attr_includes.js";
|
|
4
|
+
|
|
5
|
+
describe("attrIncludes", () => {
|
|
6
|
+
it("should only work with array attributes, not exact matches", () => {
|
|
7
|
+
const collection = [
|
|
8
|
+
{ category: "blog", title: "Post 1" },
|
|
9
|
+
{ category: "news", title: "Post 2" },
|
|
10
|
+
{ category: "blog", title: "Post 3" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Non-array attributes are skipped
|
|
14
|
+
const result = attrIncludes(collection, "category", "blog");
|
|
15
|
+
assert.strictEqual(result.length, 0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should filter items when attribute is an array (includes check)", () => {
|
|
19
|
+
const collection = [
|
|
20
|
+
{ tags: ["javascript", "tutorial"], title: "Post 1" },
|
|
21
|
+
{ tags: ["python", "tutorial"], title: "Post 2" },
|
|
22
|
+
{ tags: ["javascript", "advanced"], title: "Post 3" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const result = attrIncludes(collection, "tags", "javascript");
|
|
26
|
+
assert.strictEqual(result.length, 2);
|
|
27
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
28
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should throw error when collection is not an array", () => {
|
|
32
|
+
assert.throws(() => {
|
|
33
|
+
attrIncludes(null, "category", "blog");
|
|
34
|
+
}, TypeError);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should filter out items without array attributes", () => {
|
|
38
|
+
const collection = [
|
|
39
|
+
{ tags: ["javascript"], title: "Post 1" },
|
|
40
|
+
{ title: "Post 2" },
|
|
41
|
+
{ tags: ["javascript"], title: "Post 3" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const result = attrIncludes(collection, "tags", "javascript");
|
|
45
|
+
assert.strictEqual(result.length, 2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should work with array attribute directly on item (not in data)", () => {
|
|
49
|
+
const collection = [
|
|
50
|
+
{ tags: ["blog"], title: "Post 1" },
|
|
51
|
+
{ tags: ["news"], title: "Post 2" },
|
|
52
|
+
{ tags: ["blog"], title: "Post 3" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const result = attrIncludes(collection, "tags", "blog");
|
|
56
|
+
assert.strictEqual(result.length, 2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle array that does not include target value", () => {
|
|
60
|
+
const collection = [
|
|
61
|
+
{ tags: ["python", "tutorial"], title: "Post 1" },
|
|
62
|
+
{ tags: ["ruby", "guide"], title: "Post 2" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const result = attrIncludes(collection, "tags", "javascript");
|
|
66
|
+
assert.strictEqual(result.length, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should handle different value types in arrays", () => {
|
|
70
|
+
const collection = [
|
|
71
|
+
{ priorities: [1, 3], title: "Post 1" },
|
|
72
|
+
{ priorities: [2, 4], title: "Post 2" },
|
|
73
|
+
{ priorities: [1, 5], title: "Post 3" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const result = attrIncludes(collection, "priorities", 1);
|
|
77
|
+
assert.strictEqual(result.length, 2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should support nested array attributes with dot notation", () => {
|
|
81
|
+
const collection = [
|
|
82
|
+
{ data: { categories: ["blog"] }, title: "Post 1" },
|
|
83
|
+
{ data: { categories: ["news"] }, title: "Post 2" },
|
|
84
|
+
{ data: { categories: ["blog"] }, title: "Post 3" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const result = attrIncludes(collection, "data.categories", "blog");
|
|
88
|
+
assert.strictEqual(result.length, 2);
|
|
89
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
90
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should support nested arrays with dot notation", () => {
|
|
94
|
+
const collection = [
|
|
95
|
+
{ data: { tags: ["javascript", "tutorial"] }, title: "Post 1" },
|
|
96
|
+
{ data: { tags: ["python", "tutorial"] }, title: "Post 2" },
|
|
97
|
+
{ data: { tags: ["javascript", "advanced"] }, title: "Post 3" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const result = attrIncludes(collection, "data.tags", "javascript");
|
|
101
|
+
assert.strictEqual(result.length, 2);
|
|
102
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
103
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should handle deeply nested array paths", () => {
|
|
107
|
+
const collection = [
|
|
108
|
+
{ data: { meta: { statuses: ["published"] } }, title: "Post 1" },
|
|
109
|
+
{ data: { meta: { statuses: ["draft"] } }, title: "Post 2" },
|
|
110
|
+
{ data: { meta: { statuses: ["published"] } }, title: "Post 3" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const result = attrIncludes(collection, "data.meta.statuses", "published");
|
|
114
|
+
assert.strictEqual(result.length, 2);
|
|
115
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
116
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should return empty array when nested array path does not exist", () => {
|
|
120
|
+
const collection = [{ data: { tags: ["blog"] }, title: "Post 1" }, { title: "Post 2" }];
|
|
121
|
+
|
|
122
|
+
const result = attrIncludes(collection, "data.nonexistent.path", "value");
|
|
123
|
+
assert.strictEqual(result.length, 0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*<!--section:docs-->
|
|
2
|
+
|
|
3
|
+
A filter that creates a new object with an overridden attribute value. This is useful for modifying data objects in templates without mutating the original. Or even constructing an object from scratch.
|
|
4
|
+
|
|
5
|
+
<!--section:code-->```js */
|
|
6
|
+
export default function (obj, key, value) {
|
|
7
|
+
return {
|
|
8
|
+
...obj,
|
|
9
|
+
[key]: value,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/*```<!--section:docs-->
|
|
13
|
+
|
|
14
|
+
### Examples
|
|
15
|
+
|
|
16
|
+
How to pass object(s) as argument(s) to a filter in `.liquid`?
|
|
17
|
+
|
|
18
|
+
```liquid {data-caption="trick for '| renderContent' filter"}
|
|
19
|
+
{% assign _ctx = null | attr_set: 'collections', collections %}
|
|
20
|
+
{{ ... | renderContent: 'liquid,md', _ctx }}
|
|
21
|
+
```
|
|
22
|
+
*/
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import attrSet from "./attr_set.js";
|
|
4
|
+
|
|
5
|
+
test("attrSet - sets a single attribute", () => {
|
|
6
|
+
const result = attrSet({ a: 1 }, "b", 2);
|
|
7
|
+
assert.deepStrictEqual(result, { a: 1, b: 2 });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("attrSet - overrides an existing attribute", () => {
|
|
11
|
+
const result = attrSet({ a: 1, b: 2 }, "b", 3);
|
|
12
|
+
assert.deepStrictEqual(result, { a: 1, b: 3 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("attrSet - sets attribute on empty object", () => {
|
|
16
|
+
const result = attrSet({}, "key", "value");
|
|
17
|
+
assert.deepStrictEqual(result, { key: "value" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("attrSet - does not modify original object", () => {
|
|
21
|
+
const original = { a: 1, b: 2 };
|
|
22
|
+
const result = attrSet(original, "c", 3);
|
|
23
|
+
|
|
24
|
+
assert.deepStrictEqual(original, { a: 1, b: 2 });
|
|
25
|
+
assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("attrSet - works with different value types", () => {
|
|
29
|
+
const obj = { name: "test" };
|
|
30
|
+
|
|
31
|
+
// String
|
|
32
|
+
assert.deepStrictEqual(attrSet(obj, "str", "value"), { name: "test", str: "value" });
|
|
33
|
+
|
|
34
|
+
// Number
|
|
35
|
+
assert.deepStrictEqual(attrSet(obj, "num", 42), { name: "test", num: 42 });
|
|
36
|
+
|
|
37
|
+
// Boolean
|
|
38
|
+
assert.deepStrictEqual(attrSet(obj, "bool", true), { name: "test", bool: true });
|
|
39
|
+
|
|
40
|
+
// Array
|
|
41
|
+
assert.deepStrictEqual(attrSet(obj, "arr", [1, 2, 3]), { name: "test", arr: [1, 2, 3] });
|
|
42
|
+
|
|
43
|
+
// Object
|
|
44
|
+
assert.deepStrictEqual(attrSet(obj, "nested", { x: 1 }), { name: "test", nested: { x: 1 } });
|
|
45
|
+
|
|
46
|
+
// Null
|
|
47
|
+
assert.deepStrictEqual(attrSet(obj, "nil", null), { name: "test", nil: null });
|
|
48
|
+
|
|
49
|
+
// Undefined
|
|
50
|
+
assert.deepStrictEqual(attrSet(obj, "undef", undefined), { name: "test", undef: undefined });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("attrSet - can chain multiple calls", () => {
|
|
54
|
+
const original = { a: 1 };
|
|
55
|
+
const step1 = attrSet(original, "b", 2);
|
|
56
|
+
const step2 = attrSet(step1, "c", 3);
|
|
57
|
+
const step3 = attrSet(step2, "d", 4);
|
|
58
|
+
|
|
59
|
+
assert.deepStrictEqual(original, { a: 1 });
|
|
60
|
+
assert.deepStrictEqual(step3, { a: 1, b: 2, c: 3, d: 4 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("attrSet - handles special characters in keys", () => {
|
|
64
|
+
const result = attrSet({}, "data-id", 123);
|
|
65
|
+
assert.deepStrictEqual(result, { "data-id": 123 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("attrSet - works with numeric keys", () => {
|
|
69
|
+
const result = attrSet({ a: 1 }, "0", "zero");
|
|
70
|
+
assert.deepStrictEqual(result, { a: 1, 0: "zero" });
|
|
71
|
+
});
|
package/filters/date.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*<!--section:docs-->
|
|
2
|
+
|
|
3
|
+
Format a date value as an ISO 8601 date string (YYYY-MM-DD).
|
|
4
|
+
Accepts any value accepted by the Date constructor and returns
|
|
5
|
+
the date portion of the resulting ISO string.
|
|
6
|
+
|
|
7
|
+
<!--section:code-->```js */
|
|
8
|
+
export default function (dateVal) {
|
|
9
|
+
return new Date(dateVal).toISOString().split("T")[0];
|
|
10
|
+
}
|
|
11
|
+
//```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import date from "./date.js";
|
|
4
|
+
|
|
5
|
+
test("date - formats a date string as YYYY-MM-DD", () => {
|
|
6
|
+
assert.strictEqual(date("2024-03-15"), "2024-03-15");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("date - formats a Date object", () => {
|
|
10
|
+
assert.strictEqual(date(new Date("2024-03-15T00:00:00Z")), "2024-03-15");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("date - formats a timestamp number", () => {
|
|
14
|
+
// 2024-01-01T00:00:00.000Z in ms
|
|
15
|
+
assert.strictEqual(date(1704067200000), "2024-01-01");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("date - strips time portion from a datetime string", () => {
|
|
19
|
+
assert.strictEqual(date("2024-06-21T23:59:59Z"), "2024-06-21");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("date - handles year boundaries (Dec 31)", () => {
|
|
23
|
+
assert.strictEqual(date("2023-12-31T23:59:59Z"), "2023-12-31");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("date - handles leap day", () => {
|
|
27
|
+
assert.strictEqual(date("2024-02-29T00:00:00Z"), "2024-02-29");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("date - returns NaN string for invalid date", () => {
|
|
31
|
+
// Invalid Date produces "Invalid Date" whose ISO string throws; just assert it throws
|
|
32
|
+
assert.throws(() => date("not-a-date"));
|
|
33
|
+
});
|
package/filters/fetch.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*<!--section:docs-->
|
|
2
|
+
|
|
3
|
+
A filter that fetches content from remote URLs or local files. For local paths, it reads files relative to the input directory.
|
|
4
|
+
|
|
5
|
+
For remote URLs, it uses `@11ty/eleventy-fetch` to download and cache files:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @11ty/eleventy-fetch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> `NOTE:` If `@11ty/eleventy-fetch` is not installed, this filter will not be available. The plugin automatically detects whether the package is installed and only enables the filter if it's present.
|
|
12
|
+
|
|
13
|
+
<!--section:code-->```js */
|
|
14
|
+
import EleventyFetch from "@11ty/eleventy-fetch";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import fs from "fs/promises";
|
|
17
|
+
|
|
18
|
+
export default async function (url) {
|
|
19
|
+
if (!url) {
|
|
20
|
+
throw new Error("fetch filter requires a URL or path");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get the input directory from Eleventy config
|
|
24
|
+
const inputDir = this.eleventy.directories.input;
|
|
25
|
+
|
|
26
|
+
// Check if it's a URL or local path
|
|
27
|
+
const isUrl = url.startsWith("http://") || url.startsWith("https://");
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (isUrl) {
|
|
31
|
+
// Handle remote URLs with eleventy-fetch
|
|
32
|
+
const cacheDirectory = path.join(inputDir, "_downloads");
|
|
33
|
+
const content = await EleventyFetch(url, {
|
|
34
|
+
duration: "1d", // Cache for 1 day by default
|
|
35
|
+
type: "text", // Return as text
|
|
36
|
+
directory: cacheDirectory,
|
|
37
|
+
});
|
|
38
|
+
return content.toString(); // toString() handles bytes cache (inside eleventyComputed)
|
|
39
|
+
} else {
|
|
40
|
+
// Handle local file paths relative to input directory
|
|
41
|
+
const filePath = path.join(inputDir, url);
|
|
42
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Failed to fetch ${url}: ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/*```<!--section:docs-->
|
|
50
|
+
|
|
51
|
+
### Examples
|
|
52
|
+
|
|
53
|
+
```jinja2
|
|
54
|
+
{# Fetch and display remote content #}
|
|
55
|
+
{% set readme = "https://raw.githubusercontent.com/user/repo/main/README.md" | fetch %}
|
|
56
|
+
<div class="readme">
|
|
57
|
+
{{ readme | markdown | safe }}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{# Fetch JSON data from API #}
|
|
61
|
+
{% set data = "https://api.example.com/data.json" | fetch %}
|
|
62
|
+
{% set items = data | fromJson %}
|
|
63
|
+
{% for item in items %}
|
|
64
|
+
<p>{{ item.title }}</p>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
|
|
67
|
+
{# Include local file content #}
|
|
68
|
+
{% set changelog = "CHANGELOG.md" | fetch %}
|
|
69
|
+
{{ changelog | markdown | safe }}
|
|
70
|
+
|
|
71
|
+
{# Fetch CSS from CDN and inline it #}
|
|
72
|
+
<style>
|
|
73
|
+
{{ "https://cdn.example.com/styles.css" | fetch }}
|
|
74
|
+
</style>
|
|
75
|
+
|
|
76
|
+
{# Reuse content across pages #}
|
|
77
|
+
{% set sharedContent = "_includes/shared/footer.html" | fetch %}
|
|
78
|
+
{{ sharedContent | safe }}
|
|
79
|
+
```
|
|
80
|
+
*/
|
package/filters/if.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*<!--section:docs-->
|
|
2
|
+
|
|
3
|
+
An inline conditional/ternary operator filter that returns one value if a condition is truthy, and another if it's falsy. Similar to Nunjucks' inline if syntax, it is especially useful in `.liquid` templates.
|
|
4
|
+
|
|
5
|
+
<!--section:code-->```js */
|
|
6
|
+
export default function (trueValue, condition, falseValue = "") {
|
|
7
|
+
// Treat empty objects {} as falsy
|
|
8
|
+
if (condition && typeof condition === "object" && !Array.isArray(condition) && Object.keys(condition).length === 0) {
|
|
9
|
+
return falseValue;
|
|
10
|
+
}
|
|
11
|
+
return !!condition ? trueValue : falseValue;
|
|
12
|
+
}
|
|
13
|
+
/*```<!--section:docs-->
|
|
14
|
+
|
|
15
|
+
### Examples <!-- @TODO: better examples -->
|
|
16
|
+
|
|
17
|
+
```liquid
|
|
18
|
+
<button class="{{ 'btn-primary' | if: isPrimary | default: 'btn-secondary' }}">
|
|
19
|
+
Click me
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" {{ 'checked' | if: isChecked }}>
|
|
23
|
+
```
|
|
24
|
+
<!--section--> */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import iff from "./if.js";
|
|
4
|
+
|
|
5
|
+
test("iff returns trueValue when condition is truthy", () => {
|
|
6
|
+
assert.strictEqual(iff("yes", true, "no"), "yes");
|
|
7
|
+
assert.strictEqual(iff("yes", 1, "no"), "yes");
|
|
8
|
+
assert.strictEqual(iff("yes", "truthy", "no"), "yes");
|
|
9
|
+
assert.strictEqual(iff("yes", { a: 1 }, "no"), "yes"); // non-empty object is truthy
|
|
10
|
+
assert.strictEqual(iff("yes", [], "no"), "yes");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("iff returns falseValue when condition is falsy", () => {
|
|
14
|
+
assert.strictEqual(iff("yes", false, "no"), "no");
|
|
15
|
+
assert.strictEqual(iff("yes", 0, "no"), "no");
|
|
16
|
+
assert.strictEqual(iff("yes", "", "no"), "no");
|
|
17
|
+
assert.strictEqual(iff("yes", null, "no"), "no");
|
|
18
|
+
assert.strictEqual(iff("yes", undefined, "no"), "no");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("iff treats empty objects as falsy", () => {
|
|
22
|
+
assert.strictEqual(iff("yes", {}, "no"), "no");
|
|
23
|
+
assert.strictEqual(iff("yes", {}, ""), "");
|
|
24
|
+
assert.strictEqual(iff(100, {}, 200), 200);
|
|
25
|
+
|
|
26
|
+
// Non-empty objects should still be truthy
|
|
27
|
+
assert.strictEqual(iff("yes", { a: 1 }, "no"), "yes");
|
|
28
|
+
assert.strictEqual(iff("yes", { nested: {} }, "no"), "yes");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("iff returns falseValue when condition is undefined", () => {
|
|
32
|
+
assert.strictEqual(iff("yes", undefined, "no"), "no");
|
|
33
|
+
assert.strictEqual(iff("yes", undefined), "");
|
|
34
|
+
assert.strictEqual(iff(100, undefined, 200), 200);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("iff returns empty string as default falseValue", () => {
|
|
38
|
+
assert.strictEqual(iff("yes", false), "");
|
|
39
|
+
assert.strictEqual(iff("yes", 0), "");
|
|
40
|
+
assert.strictEqual(iff("yes", null), "");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("iff works with various value types", () => {
|
|
44
|
+
assert.strictEqual(iff(100, true, 200), 100);
|
|
45
|
+
assert.strictEqual(iff(100, false, 200), 200);
|
|
46
|
+
|
|
47
|
+
const obj1 = { a: 1 };
|
|
48
|
+
const obj2 = { b: 2 };
|
|
49
|
+
assert.strictEqual(iff(obj1, true, obj2), obj1);
|
|
50
|
+
assert.strictEqual(iff(obj1, false, obj2), obj2);
|
|
51
|
+
|
|
52
|
+
const arr1 = [1, 2];
|
|
53
|
+
const arr2 = [3, 4];
|
|
54
|
+
assert.strictEqual(iff(arr1, true, arr2), arr1);
|
|
55
|
+
assert.strictEqual(iff(arr1, false, arr2), arr2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("iff evaluates condition correctly without type coercion confusion", () => {
|
|
59
|
+
// Common edge cases
|
|
60
|
+
assert.strictEqual(iff("yes", "false", "no"), "yes"); // string 'false' is truthy
|
|
61
|
+
assert.strictEqual(iff("yes", "0", "no"), "yes"); // string '0' is truthy
|
|
62
|
+
assert.strictEqual(iff("yes", NaN, "no"), "no"); // NaN is falsy
|
|
63
|
+
});
|