@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,174 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { section } from "./section.js";
4
+
5
+ describe("section", () => {
6
+ it("should extract a single named section", () => {
7
+ const content = `Before
8
+ <!--section:intro-->
9
+ This is the intro
10
+ <!--section:main-->
11
+ This is the main`;
12
+
13
+ const result = section(content, "intro");
14
+ assert.strictEqual(result, "\nThis is the intro\n");
15
+ });
16
+
17
+ it("should extract section up to the next section marker", () => {
18
+ const content = `<!--section:first-->
19
+ First content
20
+ <!--section:second-->
21
+ Second content
22
+ <!--section:third-->
23
+ Third content`;
24
+
25
+ const result = section(content, "second");
26
+ assert.strictEqual(result, "\nSecond content\n");
27
+ });
28
+
29
+ it("should extract section up to EOF when no next marker", () => {
30
+ const content = `<!--section:intro-->
31
+ Intro content
32
+ <!--section:main-->
33
+ Main content that goes to the end`;
34
+
35
+ const result = section(content, "main");
36
+ assert.strictEqual(result, "\nMain content that goes to the end");
37
+ });
38
+
39
+ it("should handle section with multiple names", () => {
40
+ const content = `<!--section:header,nav-->
41
+ Shared content
42
+ <!--section:main-->
43
+ Main content`;
44
+
45
+ const resultHeader = section(content, "header");
46
+ const resultNav = section(content, "nav");
47
+
48
+ assert.strictEqual(resultHeader, "\nShared content\n");
49
+ assert.strictEqual(resultNav, "\nShared content\n");
50
+ });
51
+
52
+ it("should handle section with multiple names (spaces around commas)", () => {
53
+ const content = `<!--section:header, nav , top-->
54
+ Shared content
55
+ <!--section:main-->
56
+ Main content`;
57
+
58
+ const resultHeader = section(content, "header");
59
+ const resultNav = section(content, "nav");
60
+ const resultTop = section(content, "top");
61
+
62
+ assert.strictEqual(resultHeader, "\nShared content\n");
63
+ assert.strictEqual(resultNav, "\nShared content\n");
64
+ assert.strictEqual(resultTop, "\nShared content\n");
65
+ });
66
+
67
+ it("should return empty string for non-existent section", () => {
68
+ const content = `<!--section:intro-->
69
+ Content here
70
+ <!--section:main-->
71
+ More content`;
72
+
73
+ const result = section(content, "footer");
74
+ assert.strictEqual(result, "");
75
+ });
76
+
77
+ it("should handle empty or null input", () => {
78
+ assert.strictEqual(section("", "test"), "");
79
+ assert.strictEqual(section(null, "test"), null);
80
+ assert.strictEqual(section(undefined, "test"), undefined);
81
+ });
82
+
83
+ it("should handle missing section name", () => {
84
+ const content = `<!--section:intro-->Content`;
85
+
86
+ assert.strictEqual(section(content, ""), "");
87
+ assert.strictEqual(section(content, null), "");
88
+ assert.strictEqual(section(content, undefined), "");
89
+ });
90
+
91
+ it("should be case-insensitive for section names", () => {
92
+ const content = `<!--section:INTRO-->
93
+ Content here
94
+ <!--section:Main-->
95
+ More content`;
96
+
97
+ const result1 = section(content, "intro");
98
+ const result2 = section(content, "INTRO");
99
+ const result3 = section(content, "main");
100
+ const result4 = section(content, "MAIN");
101
+
102
+ assert.strictEqual(result1, "\nContent here\n");
103
+ assert.strictEqual(result2, "\nContent here\n");
104
+ assert.strictEqual(result3, "\nMore content");
105
+ assert.strictEqual(result4, "\nMore content");
106
+ });
107
+
108
+ it("should handle multiple sections with the same name", () => {
109
+ const content = `<!--section:note-->
110
+ First note
111
+ <!--section:main-->
112
+ Main content
113
+ <!--section:note-->
114
+ Second note
115
+ <!--section:footer-->
116
+ Footer`;
117
+
118
+ const result = section(content, "note");
119
+ assert.strictEqual(result, "\nFirst note\n\nSecond note\n");
120
+ });
121
+
122
+ it("should handle sections with no content", () => {
123
+ const content = `<!--section:empty--><!--section:main-->
124
+ Main content`;
125
+
126
+ const result = section(content, "empty");
127
+ assert.strictEqual(result, "");
128
+ });
129
+
130
+ it("should handle content before first section", () => {
131
+ const content = `Some preamble
132
+ <!--section:intro-->
133
+ Intro content`;
134
+
135
+ const result = section(content, "intro");
136
+ assert.strictEqual(result, "\nIntro content");
137
+ });
138
+
139
+ it("should handle complex real-world example", () => {
140
+ const content = `# Document Title
141
+
142
+ <!--section:summary,abstract-->
143
+ This is a summary that can be used as an abstract.
144
+ <!--section:introduction-->
145
+ This is the introduction.
146
+ <!--section:methods-->
147
+ These are the methods.
148
+ <!--section:conclusion,summary-->
149
+ This is the conclusion and also part of summary.`;
150
+
151
+ const summary = section(content, "summary");
152
+ const introduction = section(content, "introduction");
153
+ const methods = section(content, "methods");
154
+ const conclusion = section(content, "conclusion");
155
+
156
+ assert.strictEqual(
157
+ summary,
158
+ "\nThis is a summary that can be used as an abstract.\n\nThis is the conclusion and also part of summary.",
159
+ );
160
+ assert.strictEqual(introduction, "\nThis is the introduction.\n");
161
+ assert.strictEqual(methods, "\nThese are the methods.\n");
162
+ assert.strictEqual(conclusion, "\nThis is the conclusion and also part of summary.");
163
+ });
164
+
165
+ it("should handle section markers with extra whitespace", () => {
166
+ const content = `<!--section: intro -->
167
+ Content
168
+ <!--section: main -->
169
+ More`;
170
+
171
+ const result = section(content, "intro");
172
+ assert.strictEqual(result, "\nContent\n");
173
+ });
174
+ });
@@ -0,0 +1,83 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * Strip a specified HTML element from provided HTML, keeping its inner content
4
+ *
5
+ * @param {string} html - The HTML content to process
6
+ * @param {string} tagName - The tag name to strip (opening/closing tags removed, inner content kept)
7
+ * @returns {string} The HTML with the specified tag stripped but its inner content preserved
8
+ */
9
+ export function stripTag(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 tags (with optional attributes): <tag> or <tag attr="val">
22
+ const openingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>`, "gi");
23
+ let result = html.replace(openingRegex, "");
24
+
25
+ // Remove closing tags: </tag>
26
+ const closingRegex = new RegExp(`<\\/${escapedTag}>`, "gi");
27
+ result = result.replace(closingRegex, "");
28
+
29
+ return result;
30
+ }
31
+
32
+ /**
33
+ * strip_tag filter - Strip a specified HTML element, keeping its inner content
34
+ *
35
+ * Usage in templates:
36
+ * {{ htmlContent | strip_tag('div') }}
37
+ *
38
+ * @param {Object} eleventyConfig - The Eleventy configuration object
39
+ */
40
+ export function stripTagFilter(eleventyConfig) {
41
+ eleventyConfig.addFilter("strip_tag", stripTag);
42
+ }
43
+ /*```
44
+
45
+ <!--section:docs-->
46
+ ### `strip_tag`
47
+
48
+ A filter that strips a specified HTML element from content while keeping its inner content intact. Only the opening and closing tags are removed; everything inside the tag is preserved in place.
49
+
50
+ **Why use this?** When rendering HTML from a CMS or external source you sometimes need to unwrap a specific element (e.g. remove a wrapping `<div>` or `<section>`) without losing the content it contains. Unlike `remove_tag`, which discards the entire element and its content, `strip_tag` surgically removes only the tags themselves.
51
+
52
+ **Features:**
53
+
54
+ - Removes only the opening and closing tags — inner content is preserved
55
+ - Handles tags with any attributes
56
+ - Strips all occurrences of the tag, including nested ones
57
+ - Case-insensitive matching
58
+ - Non-destructive: Returns a new string, leaves the original unchanged
59
+
60
+ #### Example: Unwrap a wrapping `<div>` from content
61
+
62
+ ```jinja2
63
+ {% set unwrapped = htmlContent | strip_tag('div') %}
64
+
65
+ {{ unwrapped | safe }}
66
+ ```
67
+
68
+ Input:
69
+
70
+ ```html
71
+ <div class="wrapper">
72
+ <p>Hello</p>
73
+ <p>World</p>
74
+ </div>
75
+ ```
76
+
77
+ Output:
78
+
79
+ ```html
80
+ <p>Hello</p>
81
+ <p>World</p>
82
+ ```
83
+ <!--section--> */
@@ -0,0 +1,74 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { stripTag } from "./strip_tag.js";
4
+
5
+ describe("stripTag", () => {
6
+ it("should strip a tag but keep its inner content", () => {
7
+ const html = "<div><p>Keep this</p></div>";
8
+ const result = stripTag(html, "div");
9
+
10
+ assert.strictEqual(result, "<p>Keep this</p>");
11
+ });
12
+
13
+ it("should strip multiple instances of the same tag", () => {
14
+ const html = "<div>First</div><p>Middle</p><div>Second</div>";
15
+ const result = stripTag(html, "div");
16
+
17
+ assert.strictEqual(result, "First<p>Middle</p>Second");
18
+ });
19
+
20
+ it("should handle tags with attributes", () => {
21
+ const html = '<div class="wrapper" id="main">Content</div>';
22
+ const result = stripTag(html, "div");
23
+
24
+ assert.strictEqual(result, "Content");
25
+ });
26
+
27
+ it("should only strip the specified tag, leaving others intact", () => {
28
+ const html = "<div><span>Keep span</span><em>Keep em</em></div>";
29
+ const result = stripTag(html, "div");
30
+
31
+ assert.strictEqual(result, "<span>Keep span</span><em>Keep em</em>");
32
+ });
33
+
34
+ it("should handle nested content with the same tag", () => {
35
+ const html = "<div>outer <div>inner</div> text</div>";
36
+ const result = stripTag(html, "div");
37
+
38
+ assert.strictEqual(result, "outer inner text");
39
+ });
40
+
41
+ it("should return original HTML if tag does not exist", () => {
42
+ const html = "<p>Some text</p>";
43
+ const result = stripTag(html, "div");
44
+
45
+ assert.strictEqual(result, html);
46
+ });
47
+
48
+ it("should handle empty or null input", () => {
49
+ assert.strictEqual(stripTag("", "div"), "");
50
+ assert.strictEqual(stripTag(null, "div"), null);
51
+ assert.strictEqual(stripTag(undefined, "div"), undefined);
52
+ });
53
+
54
+ it("should handle missing or invalid tagName", () => {
55
+ const html = "<div>Content</div>";
56
+ assert.strictEqual(stripTag(html, ""), html);
57
+ assert.strictEqual(stripTag(html, null), html);
58
+ assert.strictEqual(stripTag(html, undefined), html);
59
+ });
60
+
61
+ it("should be case-insensitive", () => {
62
+ const html = '<DIV class="foo">Content</DIV>';
63
+ const result = stripTag(html, "div");
64
+
65
+ assert.strictEqual(result, "Content");
66
+ });
67
+
68
+ it("should preserve whitespace and newlines inside the tag", () => {
69
+ const html = "<div>\n <p>Line 1</p>\n <p>Line 2</p>\n</div>";
70
+ const result = stripTag(html, "div");
71
+
72
+ assert.strictEqual(result, "\n <p>Line 1</p>\n <p>Line 2</p>\n");
73
+ });
74
+ });
@@ -0,0 +1,35 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * Remove the minimal common indentation from a multi-line string
4
+ *
5
+ * Finds the smallest leading-whitespace count across all non-empty lines
6
+ * and strips that many characters from the beginning of every line.
7
+ *
8
+ * @param {string} content - The input string
9
+ * @returns {string} The unindented string
10
+ */
11
+ export function unindent(content) {
12
+ const lines = String(content ?? "").split("\n");
13
+ const minIndent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^(\s*)/)[1].length));
14
+ return lines.map((l) => l.slice(minIndent)).join("\n");
15
+ }
16
+
17
+ /**
18
+ * unindent filter - Remove minimal common indentation
19
+ *
20
+ * Strips the smallest leading-whitespace indent shared by all non-empty
21
+ * lines, useful for dedenting captured or indented template blocks.
22
+ *
23
+ * Usage in templates:
24
+ * {{ content | unindent }}
25
+ *
26
+ * @param {Object} eleventyConfig - The Eleventy configuration object
27
+ */
28
+ export function unindentFilter(eleventyConfig) {
29
+ eleventyConfig.addFilter("unindent", unindent);
30
+ }
31
+ /*```
32
+
33
+ <!--section:docs-->
34
+ ### `unindent`
35
+ <!--section--> */
@@ -0,0 +1,49 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { unindent } from "./unindent.js";
4
+
5
+ test("unindent - removes common leading spaces", () => {
6
+ const input = " hello\n world";
7
+ assert.strictEqual(unindent(input), "hello\nworld");
8
+ });
9
+
10
+ test("unindent - removes common leading tabs", () => {
11
+ const input = "\tfoo\n\tbar";
12
+ assert.strictEqual(unindent(input), "foo\nbar");
13
+ });
14
+
15
+ test("unindent - preserves relative indentation", () => {
16
+ const input = " if true\n inner\n end";
17
+ assert.strictEqual(unindent(input), "if true\n inner\nend");
18
+ });
19
+
20
+ test("unindent - ignores blank lines when computing min indent", () => {
21
+ const input = " line1\n\n line2";
22
+ assert.strictEqual(unindent(input), "line1\n\nline2");
23
+ });
24
+
25
+ test("unindent - does nothing when already at zero indent", () => {
26
+ const input = "hello\nworld";
27
+ assert.strictEqual(unindent(input), "hello\nworld");
28
+ });
29
+
30
+ test("unindent - handles single line", () => {
31
+ assert.strictEqual(unindent(" hello"), "hello");
32
+ });
33
+
34
+ test("unindent - handles null input", () => {
35
+ assert.strictEqual(unindent(null), "");
36
+ });
37
+
38
+ test("unindent - handles undefined input", () => {
39
+ assert.strictEqual(unindent(undefined), "");
40
+ });
41
+
42
+ test("unindent - handles empty string", () => {
43
+ assert.strictEqual(unindent(""), "");
44
+ });
45
+
46
+ test("unindent - mixed indent levels, strips only the minimum", () => {
47
+ const input = " a\n b\n c";
48
+ assert.strictEqual(unindent(input), "a\n b\nc");
49
+ });
package/src/index.js ADDED
@@ -0,0 +1,122 @@
1
+ import { mdAutoRawTags, mdAutoNl2br, mdAutoUncommentAttrs, transformAutoRaw, transformNl2br, transformUncommentAttrs } from "./processors/markdown.js";
2
+ import {
3
+ autoLinkFavicons,
4
+ isPlainUrlText,
5
+ cleanLinkText,
6
+ buildFaviconLink,
7
+ transformLink,
8
+ replaceLinksInHtml,
9
+ } from "./processors/autoLinkFavicons.js";
10
+ import { attrSetFilter, attrSet } from "./filters/attr_set.js";
11
+ import { attrIncludesFilter } from "./filters/attr_includes.js";
12
+ import { mergeFilter, merge } from "./filters/merge.js";
13
+ import { removeTagFilter, removeTag } from "./filters/remove_tag.js";
14
+ import { stripTagFilter, stripTag } from "./filters/strip_tag.js";
15
+ import { ifFilter, iff } from "./filters/if.js";
16
+ import { attrConcatFilter, attrConcat } from "./filters/attr_concat.js";
17
+ import { sectionFilter, section as sectionFn } from "./filters/section.js";
18
+ import { unindentFilter, unindent } from "./filters/unindent.js";
19
+ import { siteData } from "./siteData.js";
20
+
21
+ // Conditionally import fetchFilter only if @11ty/eleventy-fetch is available
22
+ let fetchFilter = null;
23
+ try {
24
+ await import("@11ty/eleventy-fetch");
25
+ const fetchModule = await import("./filters/fetch.js");
26
+ fetchFilter = fetchModule.fetchFilter;
27
+ } catch (error) {
28
+ // @11ty/eleventy-fetch not available, fetch filter will be disabled
29
+ }
30
+
31
+ /**
32
+ * 11ty Blades Plugin
33
+ *
34
+ * A collection of helpful utilities and filters for Eleventy (11ty).
35
+ * Can be used as a plugin or by importing individual helpers.
36
+ *
37
+ * @param {Object} eleventyConfig - The Eleventy configuration object
38
+ * @param {Object} options - Plugin options
39
+ * @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
40
+ * @param {boolean} options.mdAutoNl2br - Enable mdAutoNl2br for \n to <br> conversion (default: false)
41
+ * @param {boolean} options.mdAutoUncommentAttrs - Enable mdAutoUncommentAttrs to expand <!--{...}--> to {...} (default: false)
42
+ * @param {boolean} options.autoLinkFavicons - Enable autoLinkFavicons to add favicons to plain text links (default: false)
43
+ * @param {Array<string>} options.filters - Array of filter names to enable: 'attr_set', 'attr_includes', 'merge', 'remove_tag', 'strip_tag', 'if', 'attr_concat', 'section', 'fetch' (default: [])
44
+ * @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
45
+ */
46
+ export default function eleventyBladesPlugin(eleventyConfig, options = {}) {
47
+ const plugins = {
48
+ mdAutoRawTags,
49
+ mdAutoNl2br,
50
+ mdAutoUncommentAttrs,
51
+ autoLinkFavicons,
52
+ siteData,
53
+ };
54
+ Object.entries(options).forEach(([key, enabled]) => {
55
+ if (key !== "filters" && enabled && plugins[key]) {
56
+ plugins[key](eleventyConfig);
57
+ }
58
+ });
59
+
60
+ const filters = {
61
+ attr_set: attrSetFilter,
62
+ attr_includes: attrIncludesFilter,
63
+ merge: mergeFilter,
64
+ remove_tag: removeTagFilter,
65
+ strip_tag: stripTagFilter,
66
+ if: ifFilter,
67
+ attr_concat: attrConcatFilter,
68
+ section: sectionFilter,
69
+ unindent: unindentFilter,
70
+ ...(fetchFilter && { fetch: fetchFilter }),
71
+ date: (eleventyConfig) => {
72
+ eleventyConfig.addFilter("date", (dateVal) => new Date(dateVal).toISOString().split("T")[0]);
73
+ },
74
+ };
75
+ if (Array.isArray(options.filters)) {
76
+ options.filters.forEach((filterName) => {
77
+ if (filters[filterName]) {
78
+ filters[filterName](eleventyConfig);
79
+ }
80
+ });
81
+ }
82
+ }
83
+
84
+ // Export individual helpers for granular usage
85
+ // Note: fetchFilter will be null/undefined if @11ty/eleventy-fetch is not installed
86
+ export {
87
+ mdAutoRawTags,
88
+ mdAutoNl2br,
89
+ mdAutoUncommentAttrs,
90
+ autoLinkFavicons,
91
+ attrSetFilter,
92
+ attrIncludesFilter,
93
+ mergeFilter,
94
+ removeTagFilter,
95
+ stripTagFilter,
96
+ ifFilter,
97
+ attrConcatFilter,
98
+ sectionFilter,
99
+ unindentFilter,
100
+ fetchFilter,
101
+ siteData,
102
+ };
103
+
104
+ // Export transform/utility functions for advanced usage
105
+ export {
106
+ transformAutoRaw,
107
+ transformNl2br,
108
+ transformUncommentAttrs,
109
+ isPlainUrlText,
110
+ cleanLinkText,
111
+ buildFaviconLink,
112
+ transformLink,
113
+ replaceLinksInHtml,
114
+ merge,
115
+ removeTag,
116
+ stripTag,
117
+ iff,
118
+ attrConcat,
119
+ attrSet,
120
+ sectionFn as section,
121
+ unindent,
122
+ };
@@ -0,0 +1,147 @@
1
+ // <!--section:code-->```js
2
+ /**
3
+ * Check if link text looks like a plain URL or domain
4
+ *
5
+ * @param {string} linkText - The text content of the link
6
+ * @param {string} domain - The domain extracted from the URL
7
+ * @returns {boolean} True if the link text appears to be a plain URL
8
+ */
9
+ export function isPlainUrlText(linkText, domain) {
10
+ return linkText.trim().includes(domain) || linkText.trim().match(/^https?:\/\//) !== null;
11
+ }
12
+
13
+ /**
14
+ * Clean link text by removing protocol, domain, and leading slash
15
+ *
16
+ * @param {string} linkText - The original link text
17
+ * @param {string} domain - The domain to remove
18
+ * @returns {string} The cleaned text
19
+ */
20
+ export function cleanLinkText(linkText, domain) {
21
+ const cleanedText = linkText
22
+ .trim()
23
+ .replace(/^https?:\/\//, "")
24
+ .replace(/\/$/, "");
25
+ const withoutDomain = cleanedText.replace(domain, "");
26
+ return withoutDomain.length > 2 ? withoutDomain : cleanedText;
27
+ }
28
+
29
+ /**
30
+ * Build HTML for a link with favicon
31
+ *
32
+ * @param {string} attrs - The link attributes (including href)
33
+ * @param {string} domain - The domain for the favicon
34
+ * @param {string} text - The text to display
35
+ * @returns {string} The HTML string
36
+ */
37
+ export function buildFaviconLink(attrs, domain, text) {
38
+ return `<a ${attrs}><i><img src="https://www.google.com/s2/favicons?domain=${domain}&sz=64"></i> ${text}</a>`;
39
+ }
40
+
41
+ /**
42
+ * Transform a single link to include a favicon
43
+ *
44
+ * @param {string} match - The full matched link HTML
45
+ * @param {string} attrs - The link attributes
46
+ * @param {string} url - The URL from the href attribute
47
+ * @param {string} linkText - The text content of the link
48
+ * @returns {string} The transformed link or original match if not applicable
49
+ */
50
+ export function transformLink(match, attrs, url, linkText) {
51
+ try {
52
+ // Extract domain from URL
53
+ const urlObj = new URL(url, "http://dummy.com");
54
+ const domain = urlObj.hostname;
55
+
56
+ // Only add favicon if link text looks like a plain URL/domain
57
+ if (isPlainUrlText(linkText, domain)) {
58
+ const cleanedText = cleanLinkText(linkText, domain);
59
+ return buildFaviconLink(attrs, domain, cleanedText);
60
+ }
61
+ return match; // @TODO: throw?
62
+ } catch (e) {
63
+ // If URL parsing fails, return original match
64
+ return match;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Replace all anchor links in HTML content with transformed versions
70
+ *
71
+ * This function searches for all anchor tags in HTML content and replaces them
72
+ * using the provided transform function. The regex captures:
73
+ * - Group 1: All attributes including href
74
+ * - Group 2: The URL from the href attribute
75
+ * - Group 3: The link text content
76
+ *
77
+ * @param {string} content - The HTML content to process
78
+ * @param {Function} transformer - Function to transform each link (receives match, attrs, url, linkText)
79
+ * @returns {string} The HTML content with transformed links
80
+ */
81
+ export function replaceLinksInHtml(content, transformer) {
82
+ return content.replace(/<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([^<]+)<\/a>/gi, transformer);
83
+ }
84
+
85
+ /**
86
+ * autoLinkFavicons - Add favicon images to plain text links
87
+ *
88
+ * This transform automatically adds favicon images from Google's favicon service
89
+ * to links that display plain URLs or domain names. It processes all HTML output
90
+ * files and adds inline favicon images next to the link text.
91
+ *
92
+ * @param {Object} eleventyConfig - The Eleventy configuration object
93
+ */
94
+ export function autoLinkFavicons(eleventyConfig) {
95
+ eleventyConfig.addTransform("autoLinkFavicons", function (content) {
96
+ if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
97
+ return replaceLinksInHtml(content, transformLink);
98
+ }
99
+ return content;
100
+ });
101
+ }
102
+ /*```
103
+
104
+ <!--section:docs-->
105
+ ### `autoLinkFavicons` postprocessor (transformer) {#auto-link-favicons}
106
+
107
+ Automatically adds favicon images from Google's favicon service to links that display plain URLs or domain names. This processor processes all HTML output files and adds inline favicon images next to link text that appears to be a plain URL.
108
+
109
+ **Why use this?** When you have links in your content that display raw URLs or domain names (like `https://example.com/page`), adding favicons provides a visual indicator of the external site. This processor automatically detects these plain-text URL links and enhances them with favicon images, making them more visually appealing and easier to recognize.
110
+
111
+ **How it works:**
112
+
113
+ 1. Scans all HTML output files for `<a>` tags
114
+ 2. Checks if the link text appears to be a plain URL or domain
115
+ 3. Extracts the domain from the URL
116
+ 4. Removes the domain from the link text (keeping only the path)
117
+ 5. Adds a favicon image from Google's favicon service inline with the remaining text
118
+
119
+ **Example:**
120
+
121
+ Before processing:
122
+
123
+ ```html
124
+ <a href="https://github.com/anyblades/eleventy-blades">https://github.com/anyblades/eleventy-blades</a>
125
+ ```
126
+
127
+ After processing:
128
+
129
+ ```html
130
+ <a href="https://github.com/anyblades/eleventy-blades" class="whitespace-nowrap" target="_blank">
131
+ <i><img src="https://www.google.com/s2/favicons?domain=github.com&sz=32" /></i>
132
+ <span>/anyblades/eleventy-blades</span>
133
+ </a>
134
+ ```
135
+
136
+ **Rules:**
137
+
138
+ - Only applies to links where the text looks like a plain URL (contains the domain or starts with `http://`/`https://`)
139
+ - Removes the protocol and domain from the display text
140
+ - Removes the trailing slash from the display text
141
+ - Only applies if at least 3 characters remain after removing the domain (to avoid showing favicons for bare domain links)
142
+ - Uses Google's favicon service at `https://www.google.com/s2/favicons?domain=DOMAIN&sz=32`
143
+ - Adds `target="_blank"` to the processed links (only if not already present)
144
+ - Adds `whitespace-nowrap` class to the link
145
+ - Wraps the link text in a `<span>` element
146
+ - The favicon is wrapped in an `<i>` tag for easy styling
147
+ */