@anydigital/eleventy-bricks 0.22.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/.prettierrc.json +3 -0
- package/LICENSE +21 -0
- package/README.md +1064 -0
- package/package.json +49 -0
- package/src/admin/index.html +12 -0
- package/src/cli/download-files.js +136 -0
- package/src/do/package.json +15 -0
- package/src/eleventy.config.js +72 -0
- package/src/filters/attr.js +16 -0
- package/src/filters/attr_concat.js +65 -0
- package/src/filters/attr_concat.test.js +205 -0
- package/src/filters/if.js +39 -0
- package/src/filters/if.test.js +63 -0
- package/src/filters/merge.js +47 -0
- package/src/filters/merge.test.js +51 -0
- package/src/filters/remove_tag.js +42 -0
- package/src/filters/remove_tag.test.js +60 -0
- package/src/filters/where_in.js +49 -0
- package/src/filters/where_in.test.js +148 -0
- package/src/index.cjs +26 -0
- package/src/index.js +94 -0
- package/src/markdown.js +163 -0
- package/src/markdown.test.js +589 -0
- package/src/siteData.js +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anydigital/eleventy-bricks",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "A collection of helpful utilities and filters for Eleventy (11ty)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.js",
|
|
10
|
+
"require": "./src/index.cjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"download-files": "src/cli/download-files.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "npx download-files",
|
|
18
|
+
"test": "node --test src/**/*.test.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"11ty",
|
|
22
|
+
"eleventy",
|
|
23
|
+
"plugin",
|
|
24
|
+
"helpers",
|
|
25
|
+
"filters",
|
|
26
|
+
"markdown",
|
|
27
|
+
"nunjucks"
|
|
28
|
+
],
|
|
29
|
+
"author": "Anton Staroverov",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/anydigital/eleventy-bricks.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/anydigital/eleventy-bricks/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/anydigital/eleventy-bricks#readme",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@11ty/eleventy": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"_downloadFiles": {
|
|
46
|
+
"https://raw.githubusercontent.com/anydigital/bricks/refs/heads/main/.prettierrc.json": ".prettierrc.json",
|
|
47
|
+
"https://raw.githubusercontent.com/danurbanowicz/eleventy-sveltia-cms-starter/refs/heads/master/admin/index.html": "src/admin/index.html"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- https://sveltiacms.app/en/docs/start#manual-installation -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="robots" content="noindex" />
|
|
7
|
+
<title>Sveltia CMS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/@sveltia/cms@0.128/dist/sveltia-cms.min.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
|
+
import { dirname, resolve, join } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse command line arguments
|
|
8
|
+
*
|
|
9
|
+
* @returns {Object} Parsed arguments
|
|
10
|
+
*/
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const parsed = {
|
|
14
|
+
outputDir: null
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
|
|
20
|
+
if (arg === '--output' || arg === '-o') {
|
|
21
|
+
if (i + 1 < args.length) {
|
|
22
|
+
parsed.outputDir = args[i + 1];
|
|
23
|
+
i++; // Skip next argument
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error(`${arg} requires a directory path`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Downloads files specified in package.json's _downloadFiles field
|
|
35
|
+
*
|
|
36
|
+
* @param {string|null} outputDir - Optional output directory to prepend to all paths
|
|
37
|
+
* @returns {Promise<boolean>} True if all downloads succeeded, false if any failed
|
|
38
|
+
*/
|
|
39
|
+
async function download(outputDir = null) {
|
|
40
|
+
try {
|
|
41
|
+
// Find and read package.json from the current working directory
|
|
42
|
+
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
43
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
|
44
|
+
|
|
45
|
+
const downloadFiles = packageJson._downloadFiles;
|
|
46
|
+
|
|
47
|
+
if (!downloadFiles || typeof downloadFiles !== 'object') {
|
|
48
|
+
console.log('No _downloadFiles field found in package.json');
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entries = Object.entries(downloadFiles);
|
|
53
|
+
|
|
54
|
+
if (entries.length === 0) {
|
|
55
|
+
console.log('No files to download (_downloadFiles is empty)');
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`Starting download of ${entries.length} file(s)...\n`);
|
|
60
|
+
|
|
61
|
+
let hasErrors = false;
|
|
62
|
+
|
|
63
|
+
// Process all downloads
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const url = entry[0];
|
|
66
|
+
let localPath = entry[1];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
console.log(`Downloading: ${url}`);
|
|
70
|
+
console.log(` To: ${localPath}`);
|
|
71
|
+
|
|
72
|
+
// Download the file
|
|
73
|
+
const response = await fetch(url);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get the file content
|
|
80
|
+
const content = await response.arrayBuffer();
|
|
81
|
+
const buffer = Buffer.from(content);
|
|
82
|
+
|
|
83
|
+
// Prepend output directory to local path if specified
|
|
84
|
+
if (outputDir) {
|
|
85
|
+
localPath = join(outputDir, localPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Resolve the full path
|
|
89
|
+
const fullPath = resolve(process.cwd(), localPath);
|
|
90
|
+
|
|
91
|
+
// Create directory if it doesn't exist
|
|
92
|
+
const dir = dirname(fullPath);
|
|
93
|
+
await mkdir(dir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
// Write the file
|
|
96
|
+
await writeFile(fullPath, buffer);
|
|
97
|
+
|
|
98
|
+
console.log(` Success: ${localPath}\n`);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
hasErrors = true;
|
|
101
|
+
console.error(` Error: ${error.message}`);
|
|
102
|
+
console.error(` URL: ${url}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Summary
|
|
107
|
+
if (hasErrors) {
|
|
108
|
+
console.log('Download completed with errors');
|
|
109
|
+
return false;
|
|
110
|
+
} else {
|
|
111
|
+
console.log('All downloads completed successfully');
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`Fatal error: ${error.message}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* CLI entry point
|
|
123
|
+
*/
|
|
124
|
+
async function main() {
|
|
125
|
+
try {
|
|
126
|
+
const args = parseArgs();
|
|
127
|
+
const success = await download(args.outputDir);
|
|
128
|
+
process.exit(success ? 0 : 1);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`Error: ${error.message}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main();
|
|
136
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anydigital/eleventy-bricks-do",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"build": "npm run 11ty -- $ELTY_OPTIONS && npm run tw -- --minify",
|
|
6
|
+
"start": "npm run 11ty -- $ELTY_OPTIONS --serve & npm run tw -- --watch",
|
|
7
|
+
"prerestart": "npm run 11ty:clean",
|
|
8
|
+
"stage": "npm run 11ty:clean; npm run build && serve ../_site",
|
|
9
|
+
"11ty": "cd ../ && NODE_OPTIONS='--preserve-symlinks' eleventy",
|
|
10
|
+
"11ty:clean": "rm -r ../_site",
|
|
11
|
+
"11ty:debug": "DEBUG=* npm run 11ty --",
|
|
12
|
+
"tw": "tailwindcss -i ../src/_theme/styles.css -o ../_site/styles.css",
|
|
13
|
+
"tw:debug": "DEBUG=* npm run tw --"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* CLI */
|
|
2
|
+
import minimist from "minimist";
|
|
3
|
+
/* Plugins */
|
|
4
|
+
import { RenderPlugin } from "@11ty/eleventy";
|
|
5
|
+
import eleventyNavigationPlugin from "@11ty/eleventy-navigation";
|
|
6
|
+
import eleventyBricksPlugin from "@anydigital/eleventy-bricks";
|
|
7
|
+
/* Libraries */
|
|
8
|
+
import markdownIt from "markdown-it";
|
|
9
|
+
import markdownItAnchor from "markdown-it-anchor";
|
|
10
|
+
import markdownItAttrs from "markdown-it-attrs";
|
|
11
|
+
/* Data */
|
|
12
|
+
import yaml from "js-yaml";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Eleventy Configuration
|
|
16
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
17
|
+
* @returns {Object} The Eleventy configuration object
|
|
18
|
+
*/
|
|
19
|
+
export default function (eleventyConfig) {
|
|
20
|
+
/* CLI support */
|
|
21
|
+
const argv = minimist(process.argv.slice(2));
|
|
22
|
+
const inputDir = argv.input || "src";
|
|
23
|
+
|
|
24
|
+
/* Plugins */
|
|
25
|
+
eleventyConfig.addPlugin(RenderPlugin);
|
|
26
|
+
eleventyConfig.addPlugin(eleventyNavigationPlugin);
|
|
27
|
+
eleventyConfig.addPlugin(eleventyBricksPlugin, {
|
|
28
|
+
mdAutoNl2br: true,
|
|
29
|
+
mdAutoRawTags: true,
|
|
30
|
+
mdAutoLinkFavicons: true,
|
|
31
|
+
siteData: true,
|
|
32
|
+
filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/* Libraries */
|
|
36
|
+
eleventyConfig.setLibrary(
|
|
37
|
+
"md",
|
|
38
|
+
markdownIt({
|
|
39
|
+
html: true,
|
|
40
|
+
breaks: true,
|
|
41
|
+
linkify: true,
|
|
42
|
+
})
|
|
43
|
+
.use(markdownItAnchor, {
|
|
44
|
+
permalink: markdownItAnchor.permalink.headerLink(),
|
|
45
|
+
})
|
|
46
|
+
.use(markdownItAttrs),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/* Data */
|
|
50
|
+
eleventyConfig.addDataExtension("yml", (contents) => yaml.load(contents));
|
|
51
|
+
|
|
52
|
+
/* Build */
|
|
53
|
+
eleventyConfig.addPassthroughCopy(
|
|
54
|
+
{
|
|
55
|
+
"src/_public": ".",
|
|
56
|
+
...(inputDir !== "src" && { [`${inputDir}/_public`]: "." }),
|
|
57
|
+
},
|
|
58
|
+
{ expand: true }, // This follows/resolves symbolic links
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/* Dev tools */
|
|
62
|
+
// Follow symlinks in Chokidar used by 11ty to watch files
|
|
63
|
+
eleventyConfig.setChokidarConfig({ followSymlinks: true });
|
|
64
|
+
|
|
65
|
+
/* Config */
|
|
66
|
+
return {
|
|
67
|
+
dir: {
|
|
68
|
+
input: inputDir,
|
|
69
|
+
includes: "_theme",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* attr filter - Override an attribute and return the object
|
|
3
|
+
*
|
|
4
|
+
* This filter takes an object, a key, and a value, and returns a new object
|
|
5
|
+
* with the specified attribute set to the given value.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
8
|
+
*/
|
|
9
|
+
export function setAttrFilter(eleventyConfig) {
|
|
10
|
+
eleventyConfig.addFilter("attr", function (obj, key, value) {
|
|
11
|
+
return {
|
|
12
|
+
...obj,
|
|
13
|
+
[key]: value,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concatenate values to an attribute array
|
|
3
|
+
*
|
|
4
|
+
* This function takes an object, an attribute name, and values to append.
|
|
5
|
+
* It returns a new object with the attribute as a combined array of unique items.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} obj - The object to modify
|
|
8
|
+
* @param {string} attr - The attribute name
|
|
9
|
+
* @param {Array|string|*} values - Values to concatenate (array, JSON string array, or single value)
|
|
10
|
+
* @returns {Object} A new object with the combined unique array
|
|
11
|
+
*/
|
|
12
|
+
export function attrConcat(obj, attr, values) {
|
|
13
|
+
// Get the existing attribute value, default to empty array if not present
|
|
14
|
+
const existingArray = obj?.[attr] || [];
|
|
15
|
+
|
|
16
|
+
// Check if existing value is an array, convert if not
|
|
17
|
+
if (!Array.isArray(existingArray)) {
|
|
18
|
+
console.error(
|
|
19
|
+
`attrConcat: Expected ${attr} to be an array, got ${typeof existingArray}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Process the values argument
|
|
24
|
+
let valuesToAdd = [];
|
|
25
|
+
if (Array.isArray(values)) {
|
|
26
|
+
valuesToAdd = values;
|
|
27
|
+
} else if (typeof values === "string") {
|
|
28
|
+
// Try to parse as JSON array
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(values);
|
|
31
|
+
if (Array.isArray(parsed)) {
|
|
32
|
+
valuesToAdd = parsed;
|
|
33
|
+
} else {
|
|
34
|
+
valuesToAdd = [values];
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Not valid JSON, treat as single value
|
|
38
|
+
valuesToAdd = [values];
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
// If it's a single value, wrap it in an array
|
|
42
|
+
valuesToAdd = [values];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Combine arrays and remove duplicates using Set
|
|
46
|
+
const combinedArray = [...new Set([...existingArray, ...valuesToAdd])];
|
|
47
|
+
|
|
48
|
+
// Return a new object with the combined array
|
|
49
|
+
return {
|
|
50
|
+
...obj,
|
|
51
|
+
[attr]: combinedArray,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* attr_concat filter - Concatenate values to an attribute array
|
|
57
|
+
*
|
|
58
|
+
* This filter takes an object, an attribute name, and values to append.
|
|
59
|
+
* It returns a new object with the attribute as a combined array of unique items.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
62
|
+
*/
|
|
63
|
+
export function attrConcatFilter(eleventyConfig) {
|
|
64
|
+
eleventyConfig.addFilter("attr_concat", attrConcat);
|
|
65
|
+
}
|
|
@@ -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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* if utility function - Ternary/conditional helper
|
|
3
|
+
*
|
|
4
|
+
* Returns trueValue if condition is truthy, otherwise returns falseValue.
|
|
5
|
+
* Similar to Nunjucks' inline if: `value if condition else other_value`
|
|
6
|
+
*
|
|
7
|
+
* @param {*} trueValue - The value to return if condition is truthy
|
|
8
|
+
* @param {*} condition - The condition to evaluate
|
|
9
|
+
* @param {*} falseValue - The value to return if condition is falsy (default: empty string)
|
|
10
|
+
* @returns {*} Either trueValue or falseValue based on condition
|
|
11
|
+
*/
|
|
12
|
+
export function iff(trueValue, condition, falseValue = "") {
|
|
13
|
+
// Treat empty objects {} as falsy
|
|
14
|
+
if (
|
|
15
|
+
condition &&
|
|
16
|
+
typeof condition === "object" &&
|
|
17
|
+
!Array.isArray(condition) &&
|
|
18
|
+
Object.keys(condition).length === 0
|
|
19
|
+
) {
|
|
20
|
+
return falseValue;
|
|
21
|
+
}
|
|
22
|
+
return !!condition ? trueValue : falseValue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* if filter - Inline conditional/ternary operator for templates
|
|
27
|
+
*
|
|
28
|
+
* This filter provides a simple inline if/else similar to Nunjucks.
|
|
29
|
+
*
|
|
30
|
+
* Usage in Liquid templates:
|
|
31
|
+
* {{ "Active" | if: isActive, "Inactive" }}
|
|
32
|
+
* {{ "Yes" | if: condition }}
|
|
33
|
+
* {{ someValue | if: test, otherValue }}
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
36
|
+
*/
|
|
37
|
+
export function ifFilter(eleventyConfig) {
|
|
38
|
+
eleventyConfig.addFilter("if", iff);
|
|
39
|
+
}
|
|
@@ -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
|
+
});
|