@anyblades/eleventy-blades 0.28.0-beta.2
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/.prettierrc.json +3 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +46 -0
- package/src/admin/index.html +12 -0
- package/src/do/README.md +53 -0
- package/src/do/package.json +15 -0
- package/src/eleventy.config.js +178 -0
- package/src/filters/attr_concat.js +107 -0
- package/src/filters/attr_concat.test.js +205 -0
- package/src/filters/attr_includes.js +65 -0
- package/src/filters/attr_includes.test.js +145 -0
- package/src/filters/attr_set.js +42 -0
- package/src/filters/attr_set.test.js +71 -0
- package/src/filters/fetch.js +118 -0
- package/src/filters/if.js +79 -0
- package/src/filters/if.test.js +63 -0
- package/src/filters/merge.js +78 -0
- package/src/filters/merge.test.js +51 -0
- package/src/filters/remove_tag.js +70 -0
- package/src/filters/remove_tag.test.js +60 -0
- package/src/filters/section.js +125 -0
- package/src/filters/section.test.js +174 -0
- package/src/filters/strip_tag.js +83 -0
- package/src/filters/strip_tag.test.js +74 -0
- package/src/filters/unindent.js +35 -0
- package/src/filters/unindent.test.js +49 -0
- package/src/index.js +122 -0
- package/src/processors/autoLinkFavicons.js +147 -0
- package/src/processors/autoLinkFavicons.test.js +452 -0
- package/src/processors/markdown.js +79 -0
- package/src/processors/markdown.test.js +207 -0
- package/src/siteData.js +25 -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,65 @@
|
|
|
1
|
+
// <!--section:code-->```js
|
|
2
|
+
import lodash from "@11ty/lodash-custom";
|
|
3
|
+
const { get } = lodash;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Core logic for filtering collection items by attribute value
|
|
7
|
+
*
|
|
8
|
+
* This function takes a collection, an attribute name, and a target value,
|
|
9
|
+
* and returns items where the attribute matches the target value.
|
|
10
|
+
* If the attribute is an array, it checks if the array includes the target value.
|
|
11
|
+
*
|
|
12
|
+
* Supports nested attribute names using dot notation (e.g., "data.tags").
|
|
13
|
+
*
|
|
14
|
+
* @param {Array} collection - The collection to filter
|
|
15
|
+
* @param {string} attrName - The attribute name to check (supports dot notation for nested properties)
|
|
16
|
+
* @param {*} targetValue - The value to match against
|
|
17
|
+
* @returns {Array} Filtered collection
|
|
18
|
+
*/
|
|
19
|
+
export function attrIncludes(collection, attrName, targetValue) {
|
|
20
|
+
// If no targetValue, return original collection
|
|
21
|
+
if (!targetValue) {
|
|
22
|
+
return collection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return collection.filter((item) => {
|
|
26
|
+
// Get the attribute value from the item (supports nested paths like "data.tags")
|
|
27
|
+
const attrValue = get(item, attrName);
|
|
28
|
+
|
|
29
|
+
// If the attribute is an array, check if it includes the target value
|
|
30
|
+
if (Array.isArray(attrValue)) {
|
|
31
|
+
return attrValue.includes(targetValue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Otherwise skip this item
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Registers the attr_includes filter with Eleventy
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
43
|
+
*/
|
|
44
|
+
export function attrIncludesFilter(eleventyConfig) {
|
|
45
|
+
eleventyConfig.addFilter("attr_includes", attrIncludes);
|
|
46
|
+
}
|
|
47
|
+
/*```
|
|
48
|
+
|
|
49
|
+
<!--section:docs-->
|
|
50
|
+
### `attr_includes`
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
54
|
+
**Why use this?** When working with Eleventy collections, you often need to filter items based on tags or other array attributes in front matter. The `attr_includes` filter provides a flexible way to filter by any array attribute, with support for nested properties using dot notation.
|
|
55
|
+
|
|
56
|
+
#### Example: Get all posts that include `#javascript` tag
|
|
57
|
+
|
|
58
|
+
```jinja2 {data-caption="in .njk:"}
|
|
59
|
+
{% set js_posts = collections.all | attr_includes('data.tags', '#javascript') %}
|
|
60
|
+
|
|
61
|
+
{% for post in js_posts %}
|
|
62
|
+
<h2>{{ post.data.title }}</h2>
|
|
63
|
+
{% endfor %}
|
|
64
|
+
```
|
|
65
|
+
<!--section--> */
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { attrIncludes, attrIncludesFilter } from "./attr_includes.js";
|
|
4
|
+
|
|
5
|
+
describe("attrIncludes (core logic)", () => {
|
|
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
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("attrIncludesFilter (Eleventy integration)", () => {
|
|
128
|
+
it("should register the filter with eleventyConfig", () => {
|
|
129
|
+
let registeredName;
|
|
130
|
+
let registeredFn;
|
|
131
|
+
|
|
132
|
+
const mockEleventyConfig = {
|
|
133
|
+
addFilter(name, fn) {
|
|
134
|
+
registeredName = name;
|
|
135
|
+
registeredFn = fn;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
attrIncludesFilter(mockEleventyConfig);
|
|
140
|
+
|
|
141
|
+
assert.strictEqual(registeredName, "attr_includes");
|
|
142
|
+
assert.strictEqual(typeof registeredFn, "function");
|
|
143
|
+
assert.strictEqual(registeredFn, attrIncludes);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// <!--section:code-->```js
|
|
2
|
+
/**
|
|
3
|
+
* attr_set filter - Override an attribute and return the object
|
|
4
|
+
*
|
|
5
|
+
* This filter takes an object, a key, and a value, and returns a new object
|
|
6
|
+
* with the specified attribute set to the given value.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Core attr_set function - Override an attribute and return a new object
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} obj - The object to modify
|
|
15
|
+
* @param {string} key - The attribute name to set
|
|
16
|
+
* @param {*} value - The value to set for the attribute
|
|
17
|
+
* @returns {Object} A new object with the specified attribute set to the given value
|
|
18
|
+
*/
|
|
19
|
+
export function attrSet(obj, key, value) {
|
|
20
|
+
return {
|
|
21
|
+
...obj,
|
|
22
|
+
[key]: value,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function attrSetFilter(eleventyConfig) {
|
|
27
|
+
eleventyConfig.addFilter("attr_set", attrSet);
|
|
28
|
+
}
|
|
29
|
+
/*```
|
|
30
|
+
|
|
31
|
+
<!--section:docs-->
|
|
32
|
+
### `attr_set`
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
#### Example: How to pass object(s) as argument(s) to a filter in `.liquid`?
|
|
37
|
+
|
|
38
|
+
```liquid {data-caption="trick for '| renderContent' filter"}
|
|
39
|
+
{% assign _ctx = null | attr_set: 'collections', collections %}
|
|
40
|
+
{{ ... | renderContent: 'liquid,md', _ctx }}
|
|
41
|
+
```
|
|
42
|
+
<!--section--> */
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// <!--section:code-->```js
|
|
2
|
+
import EleventyFetch from "@11ty/eleventy-fetch";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* fetch filter - Fetch a URL or local file and return its raw content
|
|
8
|
+
*
|
|
9
|
+
* This filter takes a URL or local file path. For URLs, it downloads them
|
|
10
|
+
* using eleventy-fetch to the input directory's _downloads folder.
|
|
11
|
+
* For local paths, it reads them relative to the input directory.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
14
|
+
*/
|
|
15
|
+
export function fetchFilter(eleventyConfig) {
|
|
16
|
+
eleventyConfig.addFilter("fetch", async function (url) {
|
|
17
|
+
if (!url) {
|
|
18
|
+
throw new Error("fetch filter requires a URL or path");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get the input directory from Eleventy config
|
|
22
|
+
const inputDir = this.eleventy.directories.input;
|
|
23
|
+
|
|
24
|
+
// Check if it's a URL or local path
|
|
25
|
+
const isUrl = url.startsWith("http://") || url.startsWith("https://");
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (isUrl) {
|
|
29
|
+
// Handle remote URLs with eleventy-fetch
|
|
30
|
+
const cacheDirectory = path.join(inputDir, "_downloads");
|
|
31
|
+
const content = await EleventyFetch(url, {
|
|
32
|
+
duration: "1d", // Cache for 1 day by default
|
|
33
|
+
type: "text", // Return as text
|
|
34
|
+
directory: cacheDirectory,
|
|
35
|
+
});
|
|
36
|
+
return content.toString(); // toString() handles bytes cache (inside eleventyComputed)
|
|
37
|
+
} else {
|
|
38
|
+
// Handle local file paths relative to input directory
|
|
39
|
+
const filePath = path.join(inputDir, url);
|
|
40
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(`Failed to fetch ${url}: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/*```
|
|
49
|
+
|
|
50
|
+
<!--section:docs-->
|
|
51
|
+
### `fetch`
|
|
52
|
+
|
|
53
|
+
A filter that fetches content from remote URLs or local files. For remote URLs, it uses `@11ty/eleventy-fetch` to download and cache files. For local paths, it reads files relative to the input directory.
|
|
54
|
+
|
|
55
|
+
**Why use this?** When building static sites, you often need to include content from external sources or reuse content from local files. The `fetch` filter provides a unified way to retrieve content from both remote URLs and local files, with automatic caching for remote resources to improve build performance.
|
|
56
|
+
|
|
57
|
+
**Requirements:** This filter requires the `@11ty/eleventy-fetch` package to be installed:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install @11ty/eleventy-fetch
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> `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.
|
|
64
|
+
|
|
65
|
+
**Features:**
|
|
66
|
+
|
|
67
|
+
- Supports a URL (starting with `http://` or `https://`) or a local file path (relative to the input directory):
|
|
68
|
+
- **Remote URLs**: Downloads and caches content using `@11ty/eleventy-fetch`
|
|
69
|
+
- Caches files for 1 day by default
|
|
70
|
+
- Stores cached files in `[input-dir]/_downloads/` directory
|
|
71
|
+
- Automatically revalidates after cache expires
|
|
72
|
+
- **Local files**: Reads files relative to the Eleventy input directory
|
|
73
|
+
- No caching needed for local files
|
|
74
|
+
- Supports any file type that can be read as text
|
|
75
|
+
- **Error handling**: Throws descriptive errors if fetching fails
|
|
76
|
+
- **Conditional loading**: Only available when `@11ty/eleventy-fetch` is installed
|
|
77
|
+
|
|
78
|
+
**Use Cases:**
|
|
79
|
+
|
|
80
|
+
- Fetch content from external APIs during build time
|
|
81
|
+
- Include README files from GitHub repositories
|
|
82
|
+
- Reuse content from local files across multiple pages
|
|
83
|
+
- Download and inline external CSS or JavaScript
|
|
84
|
+
- Fetch data from headless CMS or external data sources
|
|
85
|
+
- Include shared content snippets without using Eleventy's include syntax
|
|
86
|
+
|
|
87
|
+
> `NOTE:` The filter returns raw text content. Use Eleventy's built-in filters like `| safe`, `| markdown`, or `| fromJson` to process the content as needed.
|
|
88
|
+
|
|
89
|
+
**Examples:**
|
|
90
|
+
|
|
91
|
+
```jinja2
|
|
92
|
+
{# Fetch and display remote content #}
|
|
93
|
+
{% set readme = "https://raw.githubusercontent.com/user/repo/main/README.md" | fetch %}
|
|
94
|
+
<div class="readme">
|
|
95
|
+
{{ readme | markdown | safe }}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{# Fetch JSON data from API #}
|
|
99
|
+
{% set data = "https://api.example.com/data.json" | fetch %}
|
|
100
|
+
{% set items = data | fromJson %}
|
|
101
|
+
{% for item in items %}
|
|
102
|
+
<p>{{ item.title }}</p>
|
|
103
|
+
{% endfor %}
|
|
104
|
+
|
|
105
|
+
{# Include local file content #}
|
|
106
|
+
{% set changelog = "CHANGELOG.md" | fetch %}
|
|
107
|
+
{{ changelog | markdown | safe }}
|
|
108
|
+
|
|
109
|
+
{# Fetch CSS from CDN and inline it #}
|
|
110
|
+
<style>
|
|
111
|
+
{{ "https://cdn.example.com/styles.css" | fetch }}
|
|
112
|
+
</style>
|
|
113
|
+
|
|
114
|
+
{# Reuse content across pages #}
|
|
115
|
+
{% set sharedContent = "_includes/shared/footer.html" | fetch %}
|
|
116
|
+
{{ sharedContent | safe }}
|
|
117
|
+
```
|
|
118
|
+
<!--section--> */
|