@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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Merge objects together
3
+ *
4
+ * Shallow merges objects (later values override earlier ones)
5
+ *
6
+ * @param {Object} first - The first object
7
+ * @param {...Object} rest - Additional objects to merge
8
+ * @returns {Object} The merged result
9
+ */
10
+ export function merge(first, ...rest) {
11
+ // If first argument is null or undefined, treat as empty object
12
+ if (first === null || first === undefined) {
13
+ first = {};
14
+ }
15
+
16
+ // Only support objects
17
+ if (typeof first === "object" && !Array.isArray(first)) {
18
+ // Merge objects using spread operator (shallow merge)
19
+ return rest.reduce(
20
+ (acc, item) => {
21
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
22
+ return { ...acc, ...item };
23
+ }
24
+ return acc;
25
+ },
26
+ { ...first }
27
+ );
28
+ }
29
+
30
+ // If first is not an object, return empty object
31
+ return {};
32
+ }
33
+
34
+ /**
35
+ * merge filter - Merge objects together
36
+ *
37
+ * This filter merges objects, similar to Twig's merge filter.
38
+ *
39
+ * Usage in templates:
40
+ * {{ obj1 | merge(obj2) }}
41
+ * {{ obj1 | merge(obj2, obj3) }}
42
+ *
43
+ * @param {Object} eleventyConfig - The Eleventy configuration object
44
+ */
45
+ export function mergeFilter(eleventyConfig) {
46
+ eleventyConfig.addFilter("merge", merge);
47
+ }
@@ -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,42 @@
1
+ /**
2
+ * Remove specified HTML element from provided HTML
3
+ *
4
+ * @param {string} html - The HTML content to process
5
+ * @param {string} tagName - The tag name to remove
6
+ * @returns {string} The HTML with the specified tag removed
7
+ */
8
+ export function removeTag(html, tagName) {
9
+ if (!html || typeof html !== 'string') {
10
+ return html;
11
+ }
12
+
13
+ if (typeof tagName !== 'string' || !tagName) {
14
+ return html;
15
+ }
16
+
17
+ // Escape special regex characters in tag name
18
+ const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
19
+
20
+ // Remove opening and closing tags along with their content
21
+ // This regex matches: <tag attributes>content</tag>
22
+ const regex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>.*?<\\/${escapedTag}>`, 'gis');
23
+ let result = html.replace(regex, '');
24
+
25
+ // Also remove self-closing tags: <tag />
26
+ const selfClosingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?\\s*\\/?>`, 'gi');
27
+ result = result.replace(selfClosingRegex, '');
28
+
29
+ return result;
30
+ }
31
+
32
+ /**
33
+ * remove_tag filter - Remove specified HTML element from provided HTML
34
+ *
35
+ * Usage in templates:
36
+ * {{ htmlContent | remove_tag('script') }}
37
+ *
38
+ * @param {Object} eleventyConfig - The Eleventy configuration object
39
+ */
40
+ export function removeTagFilter(eleventyConfig) {
41
+ eleventyConfig.addFilter("remove_tag", removeTag);
42
+ }
@@ -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,49 @@
1
+ import lodash from "@11ty/lodash-custom";
2
+ const { get } = lodash;
3
+
4
+ /**
5
+ * Core logic for filtering collection items by attribute value
6
+ *
7
+ * This function takes a collection, an attribute name, and a target value,
8
+ * and returns items where the attribute matches the target value.
9
+ * If the attribute is an array, it checks if the array includes the target value.
10
+ *
11
+ * Supports nested attribute names using dot notation (e.g., "data.tags").
12
+ *
13
+ * @param {Array} collection - The collection to filter
14
+ * @param {string} attrName - The attribute name to check (supports dot notation for nested properties)
15
+ * @param {*} targetValue - The value to match against
16
+ * @returns {Array} Filtered collection
17
+ */
18
+ export function whereIn(collection, attrName, targetValue) {
19
+ if (!collection || !Array.isArray(collection)) {
20
+ return [];
21
+ }
22
+
23
+ return collection.filter((item) => {
24
+ // Get the attribute value from the item (supports nested paths like "data.tags")
25
+ const attrValue = get(item, attrName);
26
+
27
+ // If attribute doesn't exist, skip this item
28
+ if (attrValue === undefined || attrValue === null) {
29
+ return false;
30
+ }
31
+
32
+ // If the attribute is an array, check if it includes the target value
33
+ if (Array.isArray(attrValue)) {
34
+ return attrValue.includes(targetValue);
35
+ }
36
+
37
+ // Otherwise, do a direct comparison
38
+ return attrValue === targetValue;
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Registers the where_in filter with Eleventy
44
+ *
45
+ * @param {Object} eleventyConfig - The Eleventy configuration object
46
+ */
47
+ export function whereInFilter(eleventyConfig) {
48
+ eleventyConfig.addFilter("where_in", whereIn);
49
+ }
@@ -0,0 +1,148 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { whereIn, whereInFilter } from "./where_in.js";
4
+
5
+ describe("whereIn (core logic)", () => {
6
+ it("should filter items by exact attribute match", () => {
7
+ const collection = [
8
+ { category: "blog", title: "Post 1" },
9
+ { category: "news", title: "Post 2" },
10
+ { category: "blog", title: "Post 3" },
11
+ ];
12
+
13
+ const result = whereIn(collection, "category", "blog");
14
+ assert.strictEqual(result.length, 2);
15
+ assert.strictEqual(result[0].title, "Post 1");
16
+ assert.strictEqual(result[1].title, "Post 3");
17
+ });
18
+
19
+ it("should filter items when attribute is an array (includes check)", () => {
20
+ const collection = [
21
+ { tags: ["javascript", "tutorial"], title: "Post 1" },
22
+ { tags: ["python", "tutorial"], title: "Post 2" },
23
+ { tags: ["javascript", "advanced"], title: "Post 3" },
24
+ ];
25
+
26
+ const result = whereIn(collection, "tags", "javascript");
27
+ assert.strictEqual(result.length, 2);
28
+ assert.strictEqual(result[0].title, "Post 1");
29
+ assert.strictEqual(result[1].title, "Post 3");
30
+ });
31
+
32
+ it("should return empty array when collection is not an array", () => {
33
+ const result = whereIn(null, "category", "blog");
34
+ assert.strictEqual(result.length, 0);
35
+ });
36
+
37
+ it("should filter out items without the specified attribute", () => {
38
+ const collection = [
39
+ { category: "blog", title: "Post 1" },
40
+ { title: "Post 2" },
41
+ { category: "blog", title: "Post 3" },
42
+ ];
43
+
44
+ const result = whereIn(collection, "category", "blog");
45
+ assert.strictEqual(result.length, 2);
46
+ });
47
+
48
+ it("should work with attribute directly on item (not in data)", () => {
49
+ const collection = [
50
+ { category: "blog", title: "Post 1" },
51
+ { category: "news", title: "Post 2" },
52
+ { category: "blog", title: "Post 3" },
53
+ ];
54
+
55
+ const result = whereIn(collection, "category", "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 = whereIn(collection, "tags", "javascript");
66
+ assert.strictEqual(result.length, 0);
67
+ });
68
+
69
+ it("should handle different value types", () => {
70
+ const collection = [
71
+ { priority: 1, title: "Post 1" },
72
+ { priority: 2, title: "Post 2" },
73
+ { priority: 1, title: "Post 3" },
74
+ ];
75
+
76
+ const result = whereIn(collection, "priority", 1);
77
+ assert.strictEqual(result.length, 2);
78
+ });
79
+
80
+ it("should support nested attribute names with dot notation", () => {
81
+ const collection = [
82
+ { data: { category: "blog" }, title: "Post 1" },
83
+ { data: { category: "news" }, title: "Post 2" },
84
+ { data: { category: "blog" }, title: "Post 3" },
85
+ ];
86
+
87
+ const result = whereIn(collection, "data.category", "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 = whereIn(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 paths", () => {
107
+ const collection = [
108
+ { data: { meta: { status: "published" } }, title: "Post 1" },
109
+ { data: { meta: { status: "draft" } }, title: "Post 2" },
110
+ { data: { meta: { status: "published" } }, title: "Post 3" },
111
+ ];
112
+
113
+ const result = whereIn(collection, "data.meta.status", "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 path does not exist", () => {
120
+ const collection = [
121
+ { data: { category: "blog" }, title: "Post 1" },
122
+ { title: "Post 2" },
123
+ ];
124
+
125
+ const result = whereIn(collection, "data.nonexistent.path", "value");
126
+ assert.strictEqual(result.length, 0);
127
+ });
128
+ });
129
+
130
+ describe("whereInFilter (Eleventy integration)", () => {
131
+ it("should register the filter with eleventyConfig", () => {
132
+ let registeredName;
133
+ let registeredFn;
134
+
135
+ const mockEleventyConfig = {
136
+ addFilter(name, fn) {
137
+ registeredName = name;
138
+ registeredFn = fn;
139
+ },
140
+ };
141
+
142
+ whereInFilter(mockEleventyConfig);
143
+
144
+ assert.strictEqual(registeredName, "where_in");
145
+ assert.strictEqual(typeof registeredFn, "function");
146
+ assert.strictEqual(registeredFn, whereIn);
147
+ });
148
+ });
package/src/index.cjs ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * CommonJS wrapper for 11ty Bricks Plugin
3
+ * Provides compatibility for projects using require()
4
+ */
5
+
6
+ // Dynamic import for ES modules
7
+ module.exports = async function eleventyBricksPlugin(eleventyConfig, options) {
8
+ const { default: plugin } = await import('./index.js');
9
+ return plugin(eleventyConfig, options);
10
+ };
11
+
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);
17
+ };
18
+ });
19
+
20
+ // Export transform/utility functions for advanced usage
21
+ ['transformAutoRaw', 'transformNl2br', 'merge', 'removeTag', 'iff', 'attrConcat'].forEach(name => {
22
+ module.exports[name] = async (...args) => {
23
+ const module = await import('./index.js');
24
+ return module[name](...args);
25
+ };
26
+ });
package/src/index.js ADDED
@@ -0,0 +1,94 @@
1
+ import {
2
+ mdAutoRawTags,
3
+ mdAutoNl2br,
4
+ mdAutoLinkFavicons,
5
+ transformAutoRaw,
6
+ transformNl2br,
7
+ isPlainUrlText,
8
+ cleanLinkText,
9
+ buildFaviconLink,
10
+ transformLink,
11
+ } from "./markdown.js";
12
+ import { setAttrFilter } from "./filters/attr.js";
13
+ import { whereInFilter } from "./filters/where_in.js";
14
+ import { mergeFilter, merge } from "./filters/merge.js";
15
+ import { removeTagFilter, removeTag } from "./filters/remove_tag.js";
16
+ import { ifFilter, iff } from "./filters/if.js";
17
+ import { attrConcatFilter, attrConcat } from "./filters/attr_concat.js";
18
+ import { siteData } from "./siteData.js";
19
+
20
+ /**
21
+ * 11ty Bricks Plugin
22
+ *
23
+ * A collection of helpful utilities and filters for Eleventy (11ty).
24
+ * Can be used as a plugin or by importing individual helpers.
25
+ *
26
+ * @param {Object} eleventyConfig - The Eleventy configuration object
27
+ * @param {Object} options - Plugin options
28
+ * @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
29
+ * @param {boolean} options.mdAutoNl2br - Enable mdAutoNl2br for \n to <br> conversion (default: false)
30
+ * @param {boolean} options.mdAutoLinkFavicons - Enable mdAutoLinkFavicons to add favicons to plain text links (default: false)
31
+ * @param {Array<string>} options.filters - Array of filter names to enable: 'attr', 'where_in', 'merge', 'remove_tag', 'if', 'attr_concat' (default: [])
32
+ * @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
33
+ */
34
+ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
35
+ const plugins = {
36
+ mdAutoRawTags,
37
+ mdAutoNl2br,
38
+ mdAutoLinkFavicons,
39
+ siteData,
40
+ };
41
+
42
+ const filters = {
43
+ attr: setAttrFilter,
44
+ where_in: whereInFilter,
45
+ merge: mergeFilter,
46
+ remove_tag: removeTagFilter,
47
+ if: ifFilter,
48
+ attr_concat: attrConcatFilter,
49
+ };
50
+
51
+ // Handle individual plugin options
52
+ Object.entries(options).forEach(([key, enabled]) => {
53
+ if (key !== "filters" && enabled && plugins[key]) {
54
+ plugins[key](eleventyConfig);
55
+ }
56
+ });
57
+
58
+ // Handle filters array
59
+ if (Array.isArray(options.filters)) {
60
+ options.filters.forEach((filterName) => {
61
+ if (filters[filterName]) {
62
+ filters[filterName](eleventyConfig);
63
+ }
64
+ });
65
+ }
66
+ }
67
+
68
+ // Export individual helpers for granular usage
69
+ export {
70
+ mdAutoRawTags,
71
+ mdAutoNl2br,
72
+ mdAutoLinkFavicons,
73
+ setAttrFilter,
74
+ whereInFilter,
75
+ mergeFilter,
76
+ removeTagFilter,
77
+ ifFilter,
78
+ attrConcatFilter,
79
+ siteData,
80
+ };
81
+
82
+ // Export transform/utility functions for advanced usage
83
+ export {
84
+ transformAutoRaw,
85
+ transformNl2br,
86
+ isPlainUrlText,
87
+ cleanLinkText,
88
+ buildFaviconLink,
89
+ transformLink,
90
+ merge,
91
+ removeTag,
92
+ iff,
93
+ attrConcat,
94
+ };
@@ -0,0 +1,163 @@
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
+ /**
61
+ * Check if link text looks like a plain URL or domain
62
+ *
63
+ * @param {string} linkText - The text content of the link
64
+ * @param {string} domain - The domain extracted from the URL
65
+ * @returns {boolean} True if the link text appears to be a plain URL
66
+ */
67
+ export function isPlainUrlText(linkText, domain) {
68
+ return linkText.trim().includes(domain) || linkText.trim().match(/^https?:\/\//) !== null;
69
+ }
70
+
71
+ /**
72
+ * Clean link text by removing protocol, domain, and leading slash
73
+ *
74
+ * @param {string} linkText - The original link text
75
+ * @param {string} domain - The domain to remove
76
+ * @returns {string} The cleaned text
77
+ */
78
+ export function cleanLinkText(linkText, domain) {
79
+ return linkText
80
+ .trim()
81
+ .replace(/^https?:\/\//, "")
82
+ .replace(domain, "")
83
+ .replace(/\/$/, "");
84
+ }
85
+
86
+ /**
87
+ * Build HTML for a link with favicon
88
+ *
89
+ * @param {string} attrs - The link attributes (including href)
90
+ * @param {string} domain - The domain for the favicon
91
+ * @param {string} text - The text to display
92
+ * @returns {string} The HTML string
93
+ */
94
+ export function buildFaviconLink(attrs, domain, text) {
95
+ return `<a ${attrs} target="_blank"><i><img src="https://www.google.com/s2/favicons?domain=${domain}&sz=32"></i>${text}</a>`;
96
+ }
97
+
98
+ /**
99
+ * Transform a single link to include a favicon
100
+ *
101
+ * @param {string} match - The full matched link HTML
102
+ * @param {string} attrs - The link attributes
103
+ * @param {string} url - The URL from the href attribute
104
+ * @param {string} linkText - The text content of the link
105
+ * @returns {string} The transformed link or original match if not applicable
106
+ */
107
+ export function transformLink(match, attrs, url, linkText) {
108
+ try {
109
+ // Extract domain from URL
110
+ const urlObj = new URL(url, "http://dummy.com");
111
+ const domain = urlObj.hostname;
112
+
113
+ // Only add favicon if link text looks like a plain URL/domain
114
+ if (isPlainUrlText(linkText, domain)) {
115
+ // Remove domain from link text
116
+ const cleanedText = cleanLinkText(linkText, domain);
117
+
118
+ // Only apply if there are at least 2 letters remaining after domain
119
+ if (cleanedText.length > 2) {
120
+ return buildFaviconLink(attrs, domain, cleanedText);
121
+ }
122
+ }
123
+ return match;
124
+ } catch (e) {
125
+ // If URL parsing fails, return original match
126
+ return match;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Replace all anchor links in HTML content with transformed versions
132
+ *
133
+ * This function searches for all anchor tags in HTML content and replaces them
134
+ * using the provided transform function. The regex captures:
135
+ * - Group 1: All attributes including href
136
+ * - Group 2: The URL from the href attribute
137
+ * - Group 3: The link text content
138
+ *
139
+ * @param {string} content - The HTML content to process
140
+ * @param {Function} transformer - Function to transform each link (receives match, attrs, url, linkText)
141
+ * @returns {string} The HTML content with transformed links
142
+ */
143
+ export function replaceLinksInHtml(content, transformer) {
144
+ return content.replace(/<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([^<]+)<\/a>/gi, transformer);
145
+ }
146
+
147
+ /**
148
+ * mdAutoLinkFavicons - Add favicon images to plain text links
149
+ *
150
+ * This transform automatically adds favicon images from Google's favicon service
151
+ * to links that display plain URLs or domain names. It processes all HTML output
152
+ * files and adds inline favicon images next to the link text.
153
+ *
154
+ * @param {Object} eleventyConfig - The Eleventy configuration object
155
+ */
156
+ export function mdAutoLinkFavicons(eleventyConfig) {
157
+ eleventyConfig.addTransform("mdAutoLinkFavicons", function (content) {
158
+ if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
159
+ return replaceLinksInHtml(content, transformLink);
160
+ }
161
+ return content;
162
+ });
163
+ }