@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.
@@ -0,0 +1,79 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * if utility function - Ternary/conditional helper
4
+ *
5
+ * Returns trueValue if condition is truthy, otherwise returns falseValue.
6
+ * Similar to Nunjucks' inline if: `value if condition else other_value`
7
+ *
8
+ * @param {*} trueValue - The value to return if condition is truthy
9
+ * @param {*} condition - The condition to evaluate
10
+ * @param {*} falseValue - The value to return if condition is falsy (default: empty string)
11
+ * @returns {*} Either trueValue or falseValue based on condition
12
+ */
13
+ export function iff(trueValue, condition, falseValue = "") {
14
+ // Treat empty objects {} as falsy
15
+ if (condition && typeof condition === "object" && !Array.isArray(condition) && Object.keys(condition).length === 0) {
16
+ return falseValue;
17
+ }
18
+ return !!condition ? trueValue : falseValue;
19
+ }
20
+
21
+ /**
22
+ * if filter - Inline conditional/ternary operator for templates
23
+ *
24
+ * This filter provides a simple inline if/else similar to Nunjucks.
25
+ *
26
+ * Usage in Liquid templates:
27
+ * {{ "Active" | if: isActive, "Inactive" }}
28
+ * {{ "Yes" | if: condition }}
29
+ * {{ someValue | if: test, otherValue }}
30
+ *
31
+ * @param {Object} eleventyConfig - The Eleventy configuration object
32
+ */
33
+ export function ifFilter(eleventyConfig) {
34
+ eleventyConfig.addFilter("if", iff);
35
+ }
36
+ /*```
37
+
38
+ <!--section:docs-->
39
+ ### `if`
40
+
41
+ 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.
42
+
43
+ **Features:**
44
+
45
+ - Returns `trueValue` if condition is truthy, otherwise returns `falseValue`
46
+ - Treats empty objects `{}` as falsy
47
+ - Default `falseValue` is an empty string if not provided
48
+ - Works with any data type for values
49
+
50
+ **Examples:** <!-- @TODO: better examples -->
51
+
52
+ ```jinja2
53
+ {# Basic usage (defaults to empty string) #}
54
+ <div class="{{ 'active' | if: isActive | default: 'inactive' }}">Status</div>
55
+
56
+ {# Toggle CSS classes #}
57
+ <button class="{{ 'btn-primary' | if: isPrimary | default: 'btn-secondary' }}">
58
+ Click me
59
+ </button>
60
+
61
+ {# Display different text #}
62
+ <p>{{ 'Online' | if: user.isOnline, 'Offline' }}</p>
63
+
64
+ {# Use with boolean values #}
65
+ {% set isEnabled = true %}
66
+ <div>{{ 'Enabled' | if: isEnabled, 'Disabled' }}</div>
67
+
68
+ {# Conditional attribute values #}
69
+ <input type="checkbox" {{ 'checked' | if: isChecked }}>
70
+
71
+ {# With numeric values #}
72
+ <span class="{{ 'has-items' | if: items.length }}">
73
+ {{ items.length }} items
74
+ </span>
75
+
76
+ {# Chain with other filters #}
77
+ {% set cssClass = 'featured' | if: post.featured | upper %}
78
+ ```
79
+ <!--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
+ });
@@ -0,0 +1,78 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * Merge objects together
4
+ *
5
+ * Shallow merges objects (later values override earlier ones)
6
+ *
7
+ * @param {Object} first - The first object
8
+ * @param {...Object} rest - Additional objects to merge
9
+ * @returns {Object} The merged result
10
+ */
11
+ export function merge(first, ...rest) {
12
+ // If first argument is null or undefined, treat as empty object
13
+ if (first === null || first === undefined) {
14
+ first = {};
15
+ }
16
+
17
+ // Only support objects
18
+ if (typeof first === "object" && !Array.isArray(first)) {
19
+ // Merge objects using spread operator (shallow merge)
20
+ return rest.reduce(
21
+ (acc, item) => {
22
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
23
+ return { ...acc, ...item };
24
+ }
25
+ return acc;
26
+ },
27
+ { ...first },
28
+ );
29
+ }
30
+
31
+ // If first is not an object, return empty object
32
+ return {};
33
+ }
34
+
35
+ /**
36
+ * merge filter - Merge objects together
37
+ *
38
+ * This filter merges objects, similar to Twig's merge filter.
39
+ *
40
+ * Usage in templates:
41
+ * {{ obj1 | merge(obj2) }}
42
+ * {{ obj1 | merge(obj2, obj3) }}
43
+ *
44
+ * @param {Object} eleventyConfig - The Eleventy configuration object
45
+ */
46
+ export function mergeFilter(eleventyConfig) {
47
+ eleventyConfig.addFilter("merge", merge);
48
+ }
49
+ /*```
50
+
51
+ <!--section:docs-->
52
+ ### `merge`
53
+
54
+ A filter that merges arrays or objects together, similar to Twig's merge filter. For arrays, it concatenates them. For objects, it performs a shallow merge where later values override earlier ones.
55
+
56
+ **Why use this?** When working with data in templates, you often need to combine multiple arrays or objects. The `merge` filter provides a clean way to merge data structures without writing custom JavaScript, making it easy to combine collections, merge configuration objects, or aggregate data from multiple sources.
57
+
58
+ **Examples:** <!-- @TODO: better examples -->
59
+
60
+ ```jinja2
61
+ {# Merge configuration objects #}
62
+ {% set defaultConfig = { theme: 'light', lang: 'en' } %}
63
+ {% set userConfig = { theme: 'dark' } %}
64
+ {% set finalConfig = defaultConfig | merge(userConfig) %}
65
+
66
+ {# Result: { theme: 'dark', lang: 'en' } #}
67
+ ```
68
+
69
+ ```jinja2
70
+ {# Merge page metadata with defaults #}
71
+ {% set defaultMeta = {
72
+ author: 'Site Admin',
73
+ category: 'general',
74
+ comments: false
75
+ } %}
76
+ {% set pageMeta = defaultMeta | merge(page.data) %}
77
+ ```
78
+ <!--section--> */
@@ -0,0 +1,51 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { merge } from './merge.js';
4
+
5
+ test('merge - merges two objects', () => {
6
+ const result = merge({ a: 1, b: 2 }, { c: 3, d: 4 });
7
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3, d: 4 });
8
+ });
9
+
10
+ test('merge - merges objects with override', () => {
11
+ const result = merge({ a: 1, b: 2 }, { b: 3, c: 4 });
12
+ assert.deepStrictEqual(result, { a: 1, b: 3, c: 4 });
13
+ });
14
+
15
+ test('merge - merges multiple objects', () => {
16
+ const result = merge({ a: 1 }, { b: 2 }, { c: 3 });
17
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
18
+ });
19
+
20
+ test('merge - handles null first argument', () => {
21
+ const result = merge(null, { a: 1 });
22
+ assert.deepStrictEqual(result, { a: 1 });
23
+ });
24
+
25
+ test('merge - handles undefined first argument', () => {
26
+ const result = merge(undefined, { a: 1 });
27
+ assert.deepStrictEqual(result, { a: 1 });
28
+ });
29
+
30
+ test('merge - does not modify original objects', () => {
31
+ const original = { a: 1 };
32
+ const result = merge(original, { b: 2 });
33
+
34
+ assert.deepStrictEqual(original, { a: 1 });
35
+ assert.deepStrictEqual(result, { a: 1, b: 2 });
36
+ });
37
+
38
+ test('merge - returns empty object for arrays', () => {
39
+ const result = merge([1, 2], [3, 4]);
40
+ assert.deepStrictEqual(result, {});
41
+ });
42
+
43
+ test('merge - returns empty object for primitives', () => {
44
+ const result = merge('string', { a: 1 });
45
+ assert.deepStrictEqual(result, {});
46
+ });
47
+
48
+ test('merge - ignores non-object arguments in rest', () => {
49
+ const result = merge({ a: 1 }, 'string', { b: 2 }, null, { c: 3 });
50
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
51
+ });
@@ -0,0 +1,70 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * Remove specified HTML element from provided HTML
4
+ *
5
+ * @param {string} html - The HTML content to process
6
+ * @param {string} tagName - The tag name to remove
7
+ * @returns {string} The HTML with the specified tag removed
8
+ */
9
+ export function removeTag(html, tagName) {
10
+ if (!html || typeof html !== "string") {
11
+ return html;
12
+ }
13
+
14
+ if (typeof tagName !== "string" || !tagName) {
15
+ return html;
16
+ }
17
+
18
+ // Escape special regex characters in tag name
19
+ const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+
21
+ // Remove opening and closing tags along with their content
22
+ // This regex matches: <tag attributes>content</tag>
23
+ const regex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>.*?<\\/${escapedTag}>`, "gis");
24
+ let result = html.replace(regex, "");
25
+
26
+ // Also remove self-closing tags: <tag />
27
+ const selfClosingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?\\s*\\/?>`, "gi");
28
+ result = result.replace(selfClosingRegex, "");
29
+
30
+ return result;
31
+ }
32
+
33
+ /**
34
+ * remove_tag filter - Remove specified HTML element from provided HTML
35
+ *
36
+ * Usage in templates:
37
+ * {{ htmlContent | remove_tag('script') }}
38
+ *
39
+ * @param {Object} eleventyConfig - The Eleventy configuration object
40
+ */
41
+ export function removeTagFilter(eleventyConfig) {
42
+ eleventyConfig.addFilter("remove_tag", removeTag);
43
+ }
44
+ /*```
45
+
46
+ <!--section:docs-->
47
+ ### `remove_tag`
48
+
49
+ A filter that removes a specified HTML element from provided HTML content. It removes the tag along with its content, including self-closing tags.
50
+
51
+ **Why use this?** When working with content from external sources or user-generated content, you may need to strip certain HTML tags for security or presentation purposes. The `remove_tag` filter provides a simple way to remove unwanted tags like `<script>`, `<style>`, or any other HTML elements from your content.
52
+
53
+ **Features:**
54
+
55
+ - Removes both opening and closing tags along with their content
56
+ - Handles self-closing tags (e.g., `<br />`, `<img />`)
57
+ - Handles tags with attributes
58
+ - Case-insensitive matching
59
+ - Non-destructive: Returns new string, doesn't modify original
60
+
61
+ **Security note:** While this filter can help sanitize HTML content, it should not be relied upon as the sole security measure. For critical security requirements, use a dedicated HTML sanitization library on the server side before content reaches your templates.
62
+
63
+ #### Example: Remove all script tags from content <!-- @TODO: better examples -->
64
+
65
+ ```jinja2
66
+ {% set cleanContent = htmlContent | remove_tag('script') %}
67
+
68
+ {{ cleanContent | safe }}
69
+ ```
70
+ <!--section--> */
@@ -0,0 +1,60 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { removeTag } from './remove_tag.js';
4
+
5
+ describe('removeTag', () => {
6
+ it('should remove a single tag with content', () => {
7
+ const html = '<div>Keep this</div><script>Remove this</script><p>Keep this too</p>';
8
+ const result = removeTag(html, 'script');
9
+
10
+ assert.strictEqual(result, '<div>Keep this</div><p>Keep this too</p>');
11
+ });
12
+
13
+ it('should remove multiple instances of the same tag', () => {
14
+ const html = '<p>First</p><script>One</script><p>Second</p><script>Two</script><p>Third</p>';
15
+ const result = removeTag(html, 'script');
16
+
17
+ assert.strictEqual(result, '<p>First</p><p>Second</p><p>Third</p>');
18
+ });
19
+
20
+ it('should handle tags with attributes', () => {
21
+ const html = '<div>Keep</div><script type="text/javascript" src="file.js">Code</script><p>Keep</p>';
22
+ const result = removeTag(html, 'script');
23
+
24
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
25
+ });
26
+
27
+ it('should handle self-closing tags', () => {
28
+ const html = '<div>Keep</div><br /><p>Keep</p>';
29
+ const result = removeTag(html, 'br');
30
+
31
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
32
+ });
33
+
34
+ it('should return original HTML if tag does not exist', () => {
35
+ const html = '<div>Keep this</div><p>Keep this too</p>';
36
+ const result = removeTag(html, 'script');
37
+
38
+ assert.strictEqual(result, html);
39
+ });
40
+
41
+ it('should handle empty or null input', () => {
42
+ assert.strictEqual(removeTag('', 'script'), '');
43
+ assert.strictEqual(removeTag(null, 'script'), null);
44
+ assert.strictEqual(removeTag(undefined, 'script'), undefined);
45
+ });
46
+
47
+ it('should be case-insensitive', () => {
48
+ const html = '<div>Keep</div><SCRIPT>Remove</SCRIPT><Script>Remove</Script><p>Keep</p>';
49
+ const result = removeTag(html, 'script');
50
+
51
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
52
+ });
53
+
54
+ it('should handle nested content', () => {
55
+ const html = '<div>Keep</div><script><div>Nested</div></script><p>Keep</p>';
56
+ const result = removeTag(html, 'script');
57
+
58
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
59
+ });
60
+ });
@@ -0,0 +1,125 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * Extract a named section from content marked with HTML comments
4
+ *
5
+ * @param {string} content - The content to process
6
+ * @param {string} sectionName - The section name(s) to extract
7
+ * @returns {string} The extracted section content
8
+ */
9
+ export function section(content, sectionName) {
10
+ if (!content || typeof content !== "string") {
11
+ return content;
12
+ }
13
+
14
+ if (typeof sectionName !== "string" || !sectionName) {
15
+ return "";
16
+ }
17
+
18
+ // Normalize section name for comparison (trim whitespace)
19
+ const targetName = sectionName.trim().toLowerCase();
20
+
21
+ // Regex to match section markers with content up to the next section or end of string
22
+ // Captures: (1) section names, (2) content until next section marker or end
23
+ const sectionRegex = /<[!]--section:([^>]+)-->([\s\S]*?)(?=<[!]--section|$)/g;
24
+
25
+ let results = [];
26
+ let match;
27
+
28
+ // Find all sections
29
+ while ((match = sectionRegex.exec(content)) !== null) {
30
+ const namesStr = match[1];
31
+ const sectionContent = match[2];
32
+ const names = namesStr.split(",").map((n) => n.trim().toLowerCase());
33
+
34
+ // Check if any of the names match the target
35
+ if (names.includes(targetName)) {
36
+ results.push(sectionContent);
37
+ }
38
+ }
39
+
40
+ // Join all matching sections
41
+ return results.join("");
42
+ }
43
+
44
+ /**
45
+ * section filter - Extract a named section from content
46
+ *
47
+ * Usage in templates:
48
+ * {{ content | section('intro') }}
49
+ * {{ content | section('footer') }}
50
+ *
51
+ * Content format:
52
+ * <¡--section:intro-->
53
+ * This is the intro content
54
+ * <¡--section:main-->
55
+ * This is the main content
56
+ * <¡--section:footer,sidebar-->
57
+ * This appears in both footer and sidebar sections
58
+ *
59
+ * @param {Object} eleventyConfig - The Eleventy configuration object
60
+ */
61
+ export function sectionFilter(eleventyConfig) {
62
+ eleventyConfig.addFilter("section", section);
63
+ }
64
+ /*```
65
+
66
+ <!--section:docs-->
67
+ ### `section`
68
+
69
+ A filter that extracts a named section from content marked with HTML comments. This is useful for splitting a single content file (like a Markdown post) into multiple parts that can be displayed and styled independently in your templates.
70
+
71
+ **Usage:**
72
+
73
+ 1. Mark sections in your content file (e.g., `post.md`):
74
+
75
+ ⚠️ `NOTE:` The `¡` symbol is used instead of `!` only to give examples below. Use `!` in your actual content files.
76
+
77
+ ```markdown
78
+ # My Post
79
+
80
+ <¡--section:intro-->
81
+
82
+ This is the introduction that appears at the top of the page.
83
+
84
+ <¡--section:main-->
85
+
86
+ This is the main body of the post with all the details.
87
+
88
+ <¡--section:summary,sidebar-->
89
+
90
+ This content appears in both the summary and the sidebar!
91
+ ```
92
+
93
+ 2. Use the filter in your templates: <!-- @TODO: better examples -->
94
+
95
+ ```jinja2
96
+ {# Get the intro section #}
97
+ <div class="page-intro">
98
+ {{ content | section('intro') | safe }}
99
+ </div>
100
+
101
+ {# Get the main section #}
102
+ <article>
103
+ {{ content | section('main') | safe }}
104
+ </article>
105
+
106
+ {# Get the sidebar section #}
107
+ <aside>
108
+ {{ content | section('sidebar') | safe }}
109
+ </aside>
110
+ ```
111
+
112
+ **Features:**
113
+
114
+ - **Multiple names**: A single section can have multiple names separated by commas: `<¡--section:name1,name2-->`
115
+ - **Case-insensitive**: Section names are matched without regard to case
116
+ - **Multiple occurrences**: If a section name appears multiple times, the filter concatenates all matching sections
117
+ - **Non-destructive**: Returns extracted content without modifying the original input
118
+ - **EOF support**: Sections continue until the next `<¡--section*-->` marker or the end of the file
119
+
120
+ **Syntax Rules:**
121
+
122
+ - Sections start with: `<¡--section:NAME-->` or `<¡--section:NAME1,NAME2-->`
123
+ - Sections end at the next `<¡--section*-->` marker or end of file
124
+ - Whitespace around names and inside comments is automatically trimmed
125
+ <!--section--> */