@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/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
+ });