@anydigital/eleventy-bricks 0.24.0 → 0.26.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anydigital/eleventy-bricks",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "A collection of helpful utilities and filters for Eleventy (11ty)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -11,8 +11,8 @@
11
11
  }
12
12
  },
13
13
  "scripts": {
14
- "build": "curl -O https://raw.githubusercontent.com/anydigital/bricks/refs/heads/main/.prettierrc.json",
15
- "test": "node --test src/**/*.test.js"
14
+ "test": "node --test src/**/*.test.js",
15
+ "prepublishOnly": "npm run test"
16
16
  },
17
17
  "keywords": [
18
18
  "11ty",
@@ -4,6 +4,13 @@ import minimist from "minimist";
4
4
  import { RenderPlugin } from "@11ty/eleventy";
5
5
  import eleventyNavigationPlugin from "@11ty/eleventy-navigation";
6
6
  import eleventyBricksPlugin from "@anydigital/eleventy-bricks";
7
+ /* Conditional imports */
8
+ let pluginTOC;
9
+ try {
10
+ pluginTOC = (await import("@uncenter/eleventy-plugin-toc")).default;
11
+ } catch (e) {
12
+ // @uncenter/eleventy-plugin-toc not installed
13
+ }
7
14
  /* Libraries */
8
15
  import markdownIt from "markdown-it";
9
16
  import markdownItAnchor from "markdown-it-anchor";
@@ -27,10 +34,16 @@ export default function (eleventyConfig) {
27
34
  eleventyConfig.addPlugin(eleventyBricksPlugin, {
28
35
  mdAutoNl2br: true,
29
36
  mdAutoRawTags: true,
30
- mdAutoLinkFavicons: true,
37
+ autoLinkFavicons: true,
31
38
  siteData: true,
32
- filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat", "fetch"],
39
+ filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "fetch"],
33
40
  });
41
+ if (pluginTOC) {
42
+ eleventyConfig.addPlugin(pluginTOC, {
43
+ ignoredElements: ["sub", ".header-anchor"],
44
+ ul: true,
45
+ });
46
+ }
34
47
 
35
48
  /* Libraries */
36
49
  eleventyConfig.setLibrary(
@@ -15,7 +15,7 @@ const { get } = lodash;
15
15
  * @param {*} targetValue - The value to match against
16
16
  * @returns {Array} Filtered collection
17
17
  */
18
- export function whereIn(collection, attrName, targetValue) {
18
+ export function attrIncludes(collection, attrName, targetValue) {
19
19
  // If no targetValue, return original collection
20
20
  if (!targetValue) {
21
21
  return collection;
@@ -36,10 +36,10 @@ export function whereIn(collection, attrName, targetValue) {
36
36
  }
37
37
 
38
38
  /**
39
- * Registers the where_in filter with Eleventy
39
+ * Registers the attr_includes filter with Eleventy
40
40
  *
41
41
  * @param {Object} eleventyConfig - The Eleventy configuration object
42
42
  */
43
- export function whereInFilter(eleventyConfig) {
44
- eleventyConfig.addFilter("where_in", whereIn);
43
+ export function attrIncludesFilter(eleventyConfig) {
44
+ eleventyConfig.addFilter("attr_includes", attrIncludes);
45
45
  }
@@ -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,27 @@
1
+ /**
2
+ * attr_set 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
+
10
+ /**
11
+ * Core attr_set function - Override an attribute and return a new object
12
+ *
13
+ * @param {Object} obj - The object to modify
14
+ * @param {string} key - The attribute name to set
15
+ * @param {*} value - The value to set for the attribute
16
+ * @returns {Object} A new object with the specified attribute set to the given value
17
+ */
18
+ export function attrSet(obj, key, value) {
19
+ return {
20
+ ...obj,
21
+ [key]: value,
22
+ };
23
+ }
24
+
25
+ export function attrSetFilter(eleventyConfig) {
26
+ eleventyConfig.addFilter("attr_set", attrSet);
27
+ }
@@ -0,0 +1,71 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { attrSet } from "./attr_set.js";
4
+
5
+ test("attrSet - sets a single attribute", () => {
6
+ const result = attrSet({ a: 1 }, "b", 2);
7
+ assert.deepStrictEqual(result, { a: 1, b: 2 });
8
+ });
9
+
10
+ test("attrSet - overrides an existing attribute", () => {
11
+ const result = attrSet({ a: 1, b: 2 }, "b", 3);
12
+ assert.deepStrictEqual(result, { a: 1, b: 3 });
13
+ });
14
+
15
+ test("attrSet - sets attribute on empty object", () => {
16
+ const result = attrSet({}, "key", "value");
17
+ assert.deepStrictEqual(result, { key: "value" });
18
+ });
19
+
20
+ test("attrSet - does not modify original object", () => {
21
+ const original = { a: 1, b: 2 };
22
+ const result = attrSet(original, "c", 3);
23
+
24
+ assert.deepStrictEqual(original, { a: 1, b: 2 });
25
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
26
+ });
27
+
28
+ test("attrSet - works with different value types", () => {
29
+ const obj = { name: "test" };
30
+
31
+ // String
32
+ assert.deepStrictEqual(attrSet(obj, "str", "value"), { name: "test", str: "value" });
33
+
34
+ // Number
35
+ assert.deepStrictEqual(attrSet(obj, "num", 42), { name: "test", num: 42 });
36
+
37
+ // Boolean
38
+ assert.deepStrictEqual(attrSet(obj, "bool", true), { name: "test", bool: true });
39
+
40
+ // Array
41
+ assert.deepStrictEqual(attrSet(obj, "arr", [1, 2, 3]), { name: "test", arr: [1, 2, 3] });
42
+
43
+ // Object
44
+ assert.deepStrictEqual(attrSet(obj, "nested", { x: 1 }), { name: "test", nested: { x: 1 } });
45
+
46
+ // Null
47
+ assert.deepStrictEqual(attrSet(obj, "nil", null), { name: "test", nil: null });
48
+
49
+ // Undefined
50
+ assert.deepStrictEqual(attrSet(obj, "undef", undefined), { name: "test", undef: undefined });
51
+ });
52
+
53
+ test("attrSet - can chain multiple calls", () => {
54
+ const original = { a: 1 };
55
+ const step1 = attrSet(original, "b", 2);
56
+ const step2 = attrSet(step1, "c", 3);
57
+ const step3 = attrSet(step2, "d", 4);
58
+
59
+ assert.deepStrictEqual(original, { a: 1 });
60
+ assert.deepStrictEqual(step3, { a: 1, b: 2, c: 3, d: 4 });
61
+ });
62
+
63
+ test("attrSet - handles special characters in keys", () => {
64
+ const result = attrSet({}, "data-id", 123);
65
+ assert.deepStrictEqual(result, { "data-id": 123 });
66
+ });
67
+
68
+ test("attrSet - works with numeric keys", () => {
69
+ const result = attrSet({ a: 1 }, "0", "zero");
70
+ assert.deepStrictEqual(result, { a: 1, 0: "zero" });
71
+ });
package/src/index.cjs CHANGED
@@ -5,22 +5,47 @@
5
5
 
6
6
  // Dynamic import for ES modules
7
7
  module.exports = async function eleventyBricksPlugin(eleventyConfig, options) {
8
- const { default: plugin } = await import('./index.js');
8
+ const { default: plugin } = await import("./index.js");
9
9
  return plugin(eleventyConfig, options);
10
10
  };
11
11
 
12
12
  // Export individual helpers for granular usage
13
- ['mdAutoRawTags', 'mdAutoNl2br', 'setAttrFilter', 'whereInFilter', 'mergeFilter', 'removeTagFilter', 'ifFilter', 'attrConcatFilter', 'siteData'].forEach(name => {
14
- module.exports[name] = async (eleventyConfig) => {
15
- const module = await import('./index.js');
16
- return module[name](eleventyConfig);
13
+ [
14
+ "mdAutoRawTags",
15
+ "mdAutoNl2br",
16
+ "autoLinkFavicons",
17
+ "attrSetFilter",
18
+ "attrIncludesFilter",
19
+ "mergeFilter",
20
+ "removeTagFilter",
21
+ "ifFilter",
22
+ "attrConcatFilter",
23
+ "fetchFilter",
24
+ "siteData",
25
+ ].forEach((name) => {
26
+ module.exports[name] = async (...args) => {
27
+ const module = await import("./index.js");
28
+ return module[name](...args);
17
29
  };
18
30
  });
19
31
 
20
32
  // Export transform/utility functions for advanced usage
21
- ['transformAutoRaw', 'transformNl2br', 'merge', 'removeTag', 'iff', 'attrConcat'].forEach(name => {
33
+ [
34
+ "transformAutoRaw",
35
+ "transformNl2br",
36
+ "isPlainUrlText",
37
+ "cleanLinkText",
38
+ "buildFaviconLink",
39
+ "transformLink",
40
+ "replaceLinksInHtml",
41
+ "merge",
42
+ "removeTag",
43
+ "iff",
44
+ "attrConcat",
45
+ "attrSet",
46
+ ].forEach((name) => {
22
47
  module.exports[name] = async (...args) => {
23
- const module = await import('./index.js');
48
+ const module = await import("./index.js");
24
49
  return module[name](...args);
25
50
  };
26
51
  });
package/src/index.js CHANGED
@@ -1,16 +1,14 @@
1
+ import { mdAutoRawTags, mdAutoNl2br, transformAutoRaw, transformNl2br } from "./transforms/markdown.js";
1
2
  import {
2
- mdAutoRawTags,
3
- mdAutoNl2br,
4
- mdAutoLinkFavicons,
5
- transformAutoRaw,
6
- transformNl2br,
3
+ autoLinkFavicons,
7
4
  isPlainUrlText,
8
5
  cleanLinkText,
9
6
  buildFaviconLink,
10
7
  transformLink,
11
- } from "./markdown.js";
12
- import { setAttrFilter } from "./filters/attr.js";
13
- import { whereInFilter } from "./filters/where_in.js";
8
+ replaceLinksInHtml,
9
+ } from "./transforms/autoLinkFavicons.js";
10
+ import { attrSetFilter, attrSet } from "./filters/attr_set.js";
11
+ import { attrIncludesFilter } from "./filters/attr_includes.js";
14
12
  import { mergeFilter, merge } from "./filters/merge.js";
15
13
  import { removeTagFilter, removeTag } from "./filters/remove_tag.js";
16
14
  import { ifFilter, iff } from "./filters/if.js";
@@ -37,21 +35,21 @@ try {
37
35
  * @param {Object} options - Plugin options
38
36
  * @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
39
37
  * @param {boolean} options.mdAutoNl2br - Enable mdAutoNl2br for \n to <br> conversion (default: false)
40
- * @param {boolean} options.mdAutoLinkFavicons - Enable mdAutoLinkFavicons to add favicons to plain text links (default: false)
41
- * @param {Array<string>} options.filters - Array of filter names to enable: 'attr', 'where_in', 'merge', 'remove_tag', 'if', 'attr_concat', 'fetch' (default: [])
38
+ * @param {boolean} options.autoLinkFavicons - Enable autoLinkFavicons to add favicons to plain text links (default: false)
39
+ * @param {Array<string>} options.filters - Array of filter names to enable: 'attr_set', 'attr_includes', 'merge', 'remove_tag', 'if', 'attr_concat', 'fetch' (default: [])
42
40
  * @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
43
41
  */
44
42
  export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
45
43
  const plugins = {
46
44
  mdAutoRawTags,
47
45
  mdAutoNl2br,
48
- mdAutoLinkFavicons,
46
+ autoLinkFavicons,
49
47
  siteData,
50
48
  };
51
49
 
52
50
  const filters = {
53
- attr: setAttrFilter,
54
- where_in: whereInFilter,
51
+ attr_set: attrSetFilter,
52
+ attr_includes: attrIncludesFilter,
55
53
  merge: mergeFilter,
56
54
  remove_tag: removeTagFilter,
57
55
  if: ifFilter,
@@ -81,9 +79,9 @@ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
81
79
  export {
82
80
  mdAutoRawTags,
83
81
  mdAutoNl2br,
84
- mdAutoLinkFavicons,
85
- setAttrFilter,
86
- whereInFilter,
82
+ autoLinkFavicons,
83
+ attrSetFilter,
84
+ attrIncludesFilter,
87
85
  mergeFilter,
88
86
  removeTagFilter,
89
87
  ifFilter,
@@ -100,8 +98,10 @@ export {
100
98
  cleanLinkText,
101
99
  buildFaviconLink,
102
100
  transformLink,
101
+ replaceLinksInHtml,
103
102
  merge,
104
103
  removeTag,
105
104
  iff,
106
105
  attrConcat,
106
+ attrSet,
107
107
  };
@@ -1,62 +1,3 @@
1
- /**
2
- * Transform Nunjucks syntax in content by wrapping it with raw tags
3
- *
4
- * This function wraps Nunjucks syntax ({{, }}, {%, %}) with {% raw %} tags
5
- * to prevent them from being processed by the template engine.
6
- *
7
- * @param {string} content - The content to transform
8
- * @returns {string} The transformed content with Nunjucks syntax wrapped
9
- */
10
- export function transformAutoRaw(content) {
11
- // This regex looks for {{, }}, {%, or %} individually and wraps them
12
- return content.replace(/({{|}}|{%|%})/g, "{% raw %}$1{% endraw %}");
13
- }
14
-
15
- /**
16
- * mdAutoRawTags - Forbid Nunjucks processing in Markdown files
17
- *
18
- * This preprocessor wraps Nunjucks syntax ({{, }}, {%, %}) with {% raw %} tags
19
- * to prevent them from being processed by the template engine in Markdown files.
20
- *
21
- * @param {Object} eleventyConfig - The Eleventy configuration object
22
- */
23
- export function mdAutoRawTags(eleventyConfig) {
24
- eleventyConfig.addPreprocessor("mdAutoRawTags", "md", (data, content) => {
25
- return transformAutoRaw(content);
26
- });
27
- }
28
-
29
- /**
30
- * Transform \n sequences to <br> tags
31
- *
32
- * This function converts literal \n sequences (double backslash + n) to HTML <br> tags.
33
- * It handles both double \n\n and single \n sequences, processing double ones first.
34
- *
35
- * @param {string} content - The content to transform
36
- * @returns {string} The transformed content with \n converted to <br>
37
- */
38
- export function transformNl2br(content) {
39
- // Replace double \n\n first, then single \n to avoid double conversion
40
- return content.replace(/\\n\\n/g, "<br>").replace(/\\n/g, "<br>");
41
- }
42
-
43
- /**
44
- * mdAutoNl2br - Auto convert \n to <br> in markdown (especially tables)
45
- *
46
- * This function amends the markdown library to automatically convert \n
47
- * to <br> tags in text content, which is particularly useful for line breaks
48
- * inside markdown tables where standard newlines don't work.
49
- *
50
- * @param {Object} eleventyConfig - The Eleventy configuration object
51
- */
52
- export function mdAutoNl2br(eleventyConfig) {
53
- eleventyConfig.amendLibrary("md", (mdLib) => {
54
- mdLib.renderer.rules.text = (tokens, idx) => {
55
- return transformNl2br(tokens[idx].content);
56
- };
57
- });
58
- }
59
-
60
1
  /**
61
2
  * Check if link text looks like a plain URL or domain
62
3
  *
@@ -157,7 +98,7 @@ export function replaceLinksInHtml(content, transformer) {
157
98
  }
158
99
 
159
100
  /**
160
- * mdAutoLinkFavicons - Add favicon images to plain text links
101
+ * autoLinkFavicons - Add favicon images to plain text links
161
102
  *
162
103
  * This transform automatically adds favicon images from Google's favicon service
163
104
  * to links that display plain URLs or domain names. It processes all HTML output
@@ -165,8 +106,8 @@ export function replaceLinksInHtml(content, transformer) {
165
106
  *
166
107
  * @param {Object} eleventyConfig - The Eleventy configuration object
167
108
  */
168
- export function mdAutoLinkFavicons(eleventyConfig) {
169
- eleventyConfig.addTransform("mdAutoLinkFavicons", function (content) {
109
+ export function autoLinkFavicons(eleventyConfig) {
110
+ eleventyConfig.addTransform("autoLinkFavicons", function (content) {
170
111
  if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
171
112
  return replaceLinksInHtml(content, transformLink);
172
113
  }