@anyblades/buildawesome-kit 1.3.0-alpha

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.
Files changed (47) hide show
  1. package/.gitmodules +3 -0
  2. package/.prettierrc.json +3 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +218 -0
  5. package/eleventy.config.js +141 -0
  6. package/package.json +49 -0
  7. package/plugin/features/autoLinkFavicons.js +71 -0
  8. package/plugin/features/autoLinkFavicons.test.js +694 -0
  9. package/plugin/features/mdAutoNl2br.js +22 -0
  10. package/plugin/features/mdAutoNl2br.test.js +68 -0
  11. package/plugin/features/mdAutoRawTags.js +17 -0
  12. package/plugin/features/mdAutoRawTags.test.js +80 -0
  13. package/plugin/features/mdAutoUncommentAttrs.js +26 -0
  14. package/plugin/features/mdAutoUncommentAttrs.test.js +65 -0
  15. package/plugin/features/siteData.js +43 -0
  16. package/plugin/features/siteData.test.js +120 -0
  17. package/plugin/features/virtualPages.js +18 -0
  18. package/plugin/filters/attr_concat.js +55 -0
  19. package/plugin/filters/attr_concat.test.js +205 -0
  20. package/plugin/filters/attr_includes.js +41 -0
  21. package/plugin/filters/attr_includes.test.js +125 -0
  22. package/plugin/filters/attr_set.js +22 -0
  23. package/plugin/filters/attr_set.test.js +71 -0
  24. package/plugin/filters/date.js +11 -0
  25. package/plugin/filters/date.test.js +33 -0
  26. package/plugin/filters/fetch.js +80 -0
  27. package/plugin/filters/if.js +24 -0
  28. package/plugin/filters/if.test.js +63 -0
  29. package/plugin/filters/merge.js +51 -0
  30. package/plugin/filters/merge.test.js +51 -0
  31. package/plugin/filters/remove_tag.js +42 -0
  32. package/plugin/filters/remove_tag.test.js +60 -0
  33. package/plugin/filters/section.js +82 -0
  34. package/plugin/filters/section.test.js +174 -0
  35. package/plugin/filters/split.js +9 -0
  36. package/plugin/filters/split.test.js +39 -0
  37. package/plugin/filters/strip_tag.js +39 -0
  38. package/plugin/filters/strip_tag.test.js +74 -0
  39. package/plugin/filters/unindent.js +11 -0
  40. package/plugin/filters/unindent.test.js +49 -0
  41. package/plugin/node_modules/.package-lock.json +22 -0
  42. package/plugin/package-lock.json +42 -0
  43. package/plugin/package.json +48 -0
  44. package/plugin/plugin.js +53 -0
  45. package/plugin/plugin.test.js +8 -0
  46. package/plugin/scripts/README.md +59 -0
  47. package/plugin/scripts/package.json +16 -0
package/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "starters"]
2
+ path = starters
3
+ url = https://github.com/anyblades/buildawesome-micro-starters.git
@@ -0,0 +1,3 @@
1
+ {
2
+ "printWidth": 120
3
+ }
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anton Staroverov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # Eleventy *Bl*ades &nbsp;<img src="https://img.shields.io/npm/v/@anyblades/eleventy-blades?label=plugin&color=white"> <img src="https://img.shields.io/npm/v/@anyblades/eleventy-blades-base?label=base&color=white">
2
+
3
+ <!--section:summary-->
4
+
5
+ <h1><mark>Ultimate blade kit</mark> <small>for</small> 11ty / Build&nbsp;Awesome</h1>
6
+
7
+ <big>Essential 11ty [filters](//11ty.blades.ninja/filters/), transforms, and other toggleable [features](//11ty.blades.ninja/features/) as a simple, configurable plugin. Reusable [npm scripts](//11ty.blades.ninja/scripts/) included.</big>
8
+
9
+ <nav>
10
+ <a href="//11ty.blades.ninja/plugin/" role="button">Get started&nbsp;&nbsp;›</a>
11
+ <span hidden>•</span>
12
+ <a href="//11ty.blades.ninja/base/" role="button" class="outline">
13
+ Base <span hidden>package</span> <i class="fa-brands fa-eleventy"></i>&nbsp;&nbsp;›
14
+ </a>
15
+ <span hidden>•</span>
16
+ <a href="//11ty.blades.ninja/starters/" role="button" class="outline">Micro-starters&nbsp;&nbsp;🌱</a>
17
+ </nav>
18
+
19
+ <!--section:gh-only-->
20
+
21
+ ---
22
+
23
+ ## Quick start
24
+
25
+ <!--section:install-->
26
+
27
+ There are 3 ways to get started:
28
+
29
+ ### <mark>A.</mark> Plugin install
30
+
31
+ Plugin gives you all the features and filters in one go without interfering with the rest of your 11ty config:
32
+
33
+ ```sh
34
+ npm install @anyblades/eleventy-blades
35
+ ```
36
+
37
+ Then `addPlugin` to your 11ty config:
38
+
39
+ ```js
40
+ import eleventyBladesPlugin from "@anyblades/eleventy-blades";
41
+
42
+ export default function (eleventyConfig) {
43
+ eleventyConfig.addPlugin(eleventyBladesPlugin);
44
+ }
45
+ ```
46
+
47
+ You can toggle features/filters like this:
48
+
49
+ ```js
50
+ eleventyConfig.addPlugin(eleventyBladesPlugin, {
51
+ mdAutoRawTags: false,
52
+ filters: { attr_set: false },
53
+ });
54
+ ```
55
+
56
+ Live examples:
57
+
58
+ - https://github.com/anyblades/eleventy-blades/tree/main/examples/plugin-usage
59
+ - https://github.com/anyblades/eleventy-blades/tree/main/packages/eleventy-blades-base
60
+
61
+ <!--section:install,install-starters-->
62
+
63
+ ---
64
+
65
+ ### <mark>B.</mark> Starter projects
66
+
67
+ Eleventy *Bl*ades plugin and base package are included out-of-the-box with:
68
+
69
+ `BA v4` [6-in-1 Starter](https://github.com/anyblades/buildawesome-starters)
70
+ : Modern, lightweight Eleventy v4 multi-site starter showcasing Eleventy Blades Kit and Tailwind CLI for: [1] Tailwind CSS + Typography [2] Blades CSS [3] Bootstrap CSS [4] Pico CSS [5] Simple CSS [6] Liquid Templates.
71
+
72
+ `11ty v4` [*S*ubtle](https://github.com/anyblades/subtle)
73
+ : The most subtle Eleventy v4 micro-starter for content-first sites. Powered by Eleventy Blades Kit
74
+
75
+ Built-in bare-minimum examples
76
+ : https://github.com/anyblades/eleventy-blades/tree/main/examples
77
+
78
+ <!--section:install-->
79
+
80
+ ---
81
+
82
+ ### <mark>C.</mark> Base package
83
+
84
+ <!--section:install,install-base-->
85
+
86
+ Base package bundles Eleventy *Bl*ades with other popular 11ty plugins, providing a ready-to-go reusable, zero-maintenance config:
87
+
88
+ ```sh
89
+ npm install @anyblades/eleventy-blades-base
90
+
91
+ # Link base templates:
92
+ cd _includes/
93
+ ln -s ../node_modules/@anyblades/blades/_includes/blades
94
+
95
+ # Run Eleventy:
96
+ eleventy --config=node_modules/@anyblades/eleventy-blades-base/eleventy.config.js
97
+ ```
98
+
99
+ Live examples:
100
+
101
+ - https://github.com/anyblades/buildawesome-starters
102
+ - https://github.com/anyblades/subtle/tree/main/.11ty
103
+
104
+ <div><hr></div>
105
+
106
+ If you don't want to type `--config=...` every time, symlink it once:
107
+
108
+ ```sh
109
+ ln -s node_modules/@anyblades/eleventy-blades-base/eleventy.config.js
110
+ eleventy
111
+ ```
112
+
113
+ Or save it in your `package.json` scripts:
114
+
115
+ ```json
116
+ "scripts": {
117
+ "build": "eleventy --config=node_modules/@anyblades/eleventy-blades-base/eleventy.config.js"
118
+ }
119
+ ```
120
+
121
+ <div><hr></div>
122
+
123
+ Alternatively, import it as a base config in your 11ty config:
124
+
125
+ ```js
126
+ import baseConfig from "@anyblades/eleventy-blades-base";
127
+
128
+ export default async function (eleventyConfig) {
129
+ await baseConfig(eleventyConfig);
130
+ }
131
+ ```
132
+
133
+ You can toggle features/filters like this:
134
+
135
+ ```js
136
+ await baseConfig(eleventyConfig, {
137
+ plugins: {
138
+ "@anyblades/eleventy-blades": {
139
+ mdAutoRawTags: false,
140
+ filters: { attr_set: false },
141
+ },
142
+ },
143
+ });
144
+ ```
145
+
146
+ Live examples:
147
+
148
+ - https://github.com/johnheenan/minform/blob/main/eleventy.config.js
149
+ - https://github.com/hostfurl/minformhf/blob/main/eleventy.config.js
150
+
151
+ <!--section:gh-only-->
152
+
153
+ ## Documentation
154
+
155
+ <ul class="columns">
156
+
157
+ <li>
158
+ <strong><a href="/plugin/">Get started</a></strong>
159
+ <ul><li><a href="https://11ty.blades.ninja/plugin/">Plugin</a></li>
160
+ <li><a href="https://11ty.blades.ninja/base/">Base package</a></li>
161
+ <li><a href="https://11ty.blades.ninja/scripts/">Useful scripts</a></li>
162
+ <li><a href="https://11ty.blades.ninja/starters/">Starter projects</a></li></ul>
163
+ </li>
164
+
165
+ <li>
166
+ <strong><a href="/features/">Features</a></strong>
167
+ <ul><li><a href="https://11ty.blades.ninja/features/">Overview</a></li>
168
+ <li><a href="https://11ty.blades.ninja/features/site-globals/">Site globals</a></li>
169
+ <li><a href="https://11ty.blades.ninja/features/link-favicons/">Automatic link favicons</a></li>
170
+ <li><a href="https://11ty.blades.ninja/features/markdown-auto-raw/">Markdown auto-raw tags</a></li>
171
+ <li><a href="https://11ty.blades.ninja/features/markdown-hidden-attrs/">Markdown hidden attrs</a></li>
172
+ <li><a href="https://11ty.blades.ninja/features/markdown-newlines/">Markdown newlines</a></li></ul>
173
+ </li>
174
+
175
+ <li>
176
+ <strong><a href="/templates/">Templates</a></strong>
177
+ <ul><li><a href="https://blades.ninja/html/starter/">HTML base ↗</a></li>
178
+ <li><a href="https://blades.ninja/html/links/">Links ↗</a></li>
179
+ <li><a href="https://blades.ninja/html/sitemap/">Sitemap ↗</a></li>
180
+ <li><a href="https://11ty.blades.ninja/templates/">More</a></li></ul>
181
+ </li>
182
+
183
+ <li>
184
+ <strong><a href="/filters/">Filters</a></strong>
185
+ <ul><li><a href="https://11ty.blades.ninja/filters/attr_concat/">attr_concat</a></li>
186
+ <li><a href="https://11ty.blades.ninja/filters/attr_includes/">attr_includes</a></li>
187
+ <li><a href="https://11ty.blades.ninja/filters/attr_set/">attr_set</a></li>
188
+ <li><a href="https://11ty.blades.ninja/filters/date/">date</a></li>
189
+ <li><a href="https://11ty.blades.ninja/filters/fetch/">fetch</a></li>
190
+ <li><a href="https://11ty.blades.ninja/filters/if/">if</a></li>
191
+ <li><a href="https://11ty.blades.ninja/filters/markdownify/">markdownify</a></li>
192
+ <li><a href="https://11ty.blades.ninja/filters/merge/">merge</a></li>
193
+ <li><a href="https://11ty.blades.ninja/filters/remove_tag/">remove_tag</a></li>
194
+ <li><a href="https://11ty.blades.ninja/filters/section/">section</a></li>
195
+ <li><a href="https://11ty.blades.ninja/filters/strip_tag/">strip_tag</a></li>
196
+ <li><a href="https://11ty.blades.ninja/filters/unindent/">unindent</a></li></ul>
197
+ </li>
198
+
199
+ <li>
200
+ <strong><a href="/awesome/">Awesome</a></strong>
201
+
202
+ </li>
203
+
204
+ </ul>
205
+
206
+ ---
207
+
208
+ <!--section:featured-->
209
+
210
+ ## <sup style>Featured by</sup><!--A-Z sites, then @users-->
211
+
212
+ - [11ty.dev](https://www.11ty.dev/docs/starter/#:~:text=blades) [↗](https://www.11ty.dev/docs/plugins/community/#:~:text=blades)
213
+ - [11tybundle.dev](https://11tybundle.dev/blog/11ty-bundle-88/#:~:text=blades)
214
+ - [sveltiacms.app](https://sveltiacms.app/en/docs/frameworks/eleventy#:~:text=anyblades)
215
+ - [@hamatti](https://hamatti.org/posts/markdown-content-split-to-sections-in-eleventy-and-nunjucks/#:~:text=section%20filter)
216
+ - [@johnheenan](https://github.com/johnheenan/minform#css-and-performance)
217
+
218
+ <!--{.markerless .columns}-->
@@ -0,0 +1,141 @@
1
+ //<!--section:code-->```js
2
+
3
+ /* Plugins (core > official > contrib) */
4
+ import { RenderPlugin } from "@awesome.me/buildawesome";
5
+ import { feedPlugin } from "@11ty/eleventy-plugin-rss";
6
+ import eleventyNavigationPlugin from "@11ty/eleventy-navigation";
7
+ import kitPlugin from "@anyblades/buildawesome-kit-plugin";
8
+ import pluginTOC from "@uncenter/eleventy-plugin-toc";
9
+ import { siteData } from "@anyblades/buildawesome-kit-plugin/features/siteData.js";
10
+ /* Libraries (A-Z) */
11
+ import markdownIt from "markdown-it";
12
+ import markdownItAnchor from "markdown-it-anchor";
13
+ import markdownItAttrs from "markdown-it-attrs";
14
+ import slugify from "@sindresorhus/slugify";
15
+ import YAML from "yaml";
16
+ /* System (A-Z) */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+
20
+ /**
21
+ * Eleventy Configuration
22
+ * @param {Object} eleventyConfig - The Eleventy configuration object
23
+ * @returns {Object} The Eleventy configuration object
24
+ */
25
+ export default async function (eleventyConfig, pluginOptions = {}) {
26
+ /* Dirs */
27
+ const inputDir = eleventyConfig.directories.input;
28
+ const outputDir = eleventyConfig.directories.output;
29
+ const cwdIsDotEleventy = path.basename(process.cwd()) === ".11ty";
30
+ if (cwdIsDotEleventy) {
31
+ // Per https://www.11ty.dev/docs/config/#directory-for-includes
32
+ // Order matters, put this at the top of your configuration file.
33
+ // This is relative to your input directory!
34
+ eleventyConfig.setIncludesDirectory("./.11ty/_includes/");
35
+ }
36
+
37
+ /* Plugins */
38
+ eleventyConfig.addBundle("css", { bundleHtmlContentFromSelector: "style" }); // per https://www.11ty.dev/docs/plugins/bundle/#bundling-html-node-content
39
+ eleventyConfig.addTransform("inject-css-bundle", function (content) {
40
+ const isHtml = typeof this.page.outputPath === "string" && this.page.outputPath.endsWith(".html");
41
+ const css = isHtml && eleventyConfig.getFilter("getBundle").call(this, "css");
42
+ return css ? content.replace("</head>", `<style>${css}</style></head>`) : content;
43
+ });
44
+ eleventyConfig.addPlugin(RenderPlugin);
45
+ eleventyConfig.addPlugin(eleventyNavigationPlugin);
46
+ eleventyConfig.addPlugin(
47
+ kitPlugin,
48
+ pluginOptions.plugins?.["@anyblades/buildawesome-kit-plugin"] ?? { mdAutoRawTags: true },
49
+ );
50
+ eleventyConfig.addPlugin(pluginTOC, {
51
+ ignoredElements: [".header-anchor", "sub"],
52
+ ul: true,
53
+ wrapper: (toc) => `${toc}`,
54
+ });
55
+ // Feed plugin
56
+ eleventyConfig.addCollection("feed", (collectionApi) =>
57
+ collectionApi.getAll().filter((item) => item.data.date || item.data.revised),
58
+ );
59
+ eleventyConfig.addPlugin(feedPlugin, {
60
+ // per https://www.11ty.dev/docs/plugins/rss/#virtual-template
61
+ type: "atom", // or "rss", "json"
62
+ outputPath: "/feed.xml",
63
+ collection: {
64
+ name: "feed",
65
+ limit: 100, // 0 means no limit
66
+ },
67
+ templateData: {
68
+ eleventyComputed: {
69
+ metadata: (data) => {
70
+ return {
71
+ base: "https://example.com/", // sample/fallback value required by Feed plugin
72
+ ...siteData(data),
73
+ };
74
+ },
75
+ },
76
+ },
77
+ });
78
+
79
+ /* Libraries */
80
+ let md = markdownIt({
81
+ html: true,
82
+ linkify: true,
83
+ })
84
+ .use(markdownItAnchor, {
85
+ slugify: slugify, // @TODO: TRICKS
86
+ permalink: markdownItAnchor.permalink.ariaHidden(),
87
+ })
88
+ .use(markdownItAttrs);
89
+ await import("markdown-it-deflist")
90
+ .then(({ default: markdownItDeflist }) => {
91
+ md.use(markdownItDeflist);
92
+ })
93
+ .catch(() => {
94
+ /* optional – skip if not installed */
95
+ });
96
+ eleventyConfig.setLibrary("md", md);
97
+ //```<!--section:code,markdownify-->```js
98
+ eleventyConfig.addFilter("markdownify", (content) => md.render(String(content ?? "")));
99
+ //```<!--section:code-->```js
100
+
101
+ /* Data */
102
+ eleventyConfig.addDataExtension("yml,yaml", (contents) => YAML.parse(contents));
103
+ eleventyConfig.addGlobalData("layout", "default");
104
+ // Sitemap
105
+ eleventyConfig.addTemplate("sitemap.xml.njk", "", {
106
+ permalink: "/sitemap.xml",
107
+ layout: "blades/sitemap.xml.njk",
108
+ eleventyExcludeFromCollections: true,
109
+ });
110
+
111
+ /* Build */
112
+ eleventyConfig.addPassthroughCopy(
113
+ {
114
+ // From current working directory
115
+ _public: "./",
116
+ media: "./media/",
117
+ // Additionally from input dirs like `../` or `./site-1`
118
+ [`${inputDir}/_public/`]: "./",
119
+ [`${inputDir}/media/`]: "./media/",
120
+ },
121
+ { expand: true }, // This follows/resolves symbolic links
122
+ );
123
+
124
+ /* Internal */
125
+ // Jekyll templates compatibility
126
+ eleventyConfig.addFilter("relative_url", (content) => content); // dummy
127
+ eleventyConfig.setLiquidOptions({
128
+ dynamicPartials: false, // allows unquoted Jekyll-style includes
129
+ root: [
130
+ eleventyConfig.directories.includes,
131
+ fs.realpathSync(path.resolve("./node_modules/@anyblades/blades/_includes")), // for symlinks to work after https://github.com/harttle/liquidjs/pull/870
132
+ ],
133
+ });
134
+ // Dev tools
135
+ eleventyConfig.setChokidarConfig({ followSymlinks: true }); // follow symlinks in Chokidar used by 11ty to watch files
136
+ if (cwdIsDotEleventy) {
137
+ eleventyConfig.watchIgnores.add(`../.11ty/${outputDir}`); // !!! avoid circular watching
138
+ eleventyConfig.watchIgnores.add("../.11ty/node_modules/"); // avoid performance issues
139
+ }
140
+ }
141
+ //```
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@anyblades/buildawesome-kit",
3
+ "version": "1.3.0-alpha",
4
+ "type": "module",
5
+ "main": "./eleventy.config.js",
6
+ "dependencies": {
7
+ "@11ty/eleventy-navigation": "^1.0.5",
8
+ "@11ty/eleventy-plugin-rss": "^3.0.0",
9
+ "@anyblades/blades": "^2.4.6",
10
+ "@anyblades/buildawesome-kit-plugin": "^1.3.0-0",
11
+ "@uncenter/eleventy-plugin-toc": "^2.1.1",
12
+ "markdown-it": "^14.1.0",
13
+ "markdown-it-anchor": "^9.2.0",
14
+ "markdown-it-attrs": "^4.3.1",
15
+ "yaml": "^2.9.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@11ty/eleventy": "^3.0.0 || ^4.0.0-0",
19
+ "@awesome.me/buildawesome": "^3.0.0 || ^4.0.0-0"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "@11ty/eleventy": {
23
+ "optional": true
24
+ },
25
+ "@awesome.me/buildawesome": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "=== DEV ONLY": "===",
30
+ "workspaces": [
31
+ "plugin"
32
+ ],
33
+ "=== METADATA": "===",
34
+ "author": "Anton Staroverov",
35
+ "description": "A ready-to-go `eleventy.config.js` for popular 11ty plugins, bundled with npm scripts in one reusable, zero-maintenance package.",
36
+ "homepage": "https://build.blades.ninja/",
37
+ "keywords": [
38
+ "11ty",
39
+ "buildawesome",
40
+ "eleventy",
41
+ "config"
42
+ ],
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/anyblades/buildawesome-kit.git"
47
+ },
48
+ "===": "==="
49
+ }
@@ -0,0 +1,71 @@
1
+ /*<!--section:docs-->
2
+
3
+ `autoLinkFavicons` feature prepends a favicon image (from Google's favicon service) to external links whose text is a plain URL or contains only inline emphasis (`<em>`/`<strong>`) — skipping any link already decorated with an `↗` marker.
4
+
5
+ For each qualifying link the link text is cleaned (protocol prefix and trailing slash stripped; domain portion removed when significant path text remains), then wrapped in the original `<a>` tag preceded by `<i><img src="https://www.google.com/s2/favicons?…"></i>`. Any HTML already present in the link text is wrapped in a `<span>` so the favicon image stays a direct child of `<a>`. Only `.html` output files are processed.
6
+
7
+ > Compatible with: https://blades.ninja/css/link-icon/ <i class="faded">← this link is a live example!</i>
8
+
9
+ <!--section:code-->```js */
10
+ export function isExternalUrl(url) {
11
+ return /^https?:\/\//.test(url);
12
+ }
13
+
14
+ export function cleanLinkText(linkText) {
15
+ return linkText
16
+ .trim()
17
+ .replace(/^https?:\/\//, "")
18
+ .replace(/\/$/, "");
19
+ }
20
+
21
+ export function getExtraAttrs(attrs, defaults) {
22
+ return Object.entries(defaults)
23
+ .map(([key, value]) => {
24
+ const regex = new RegExp(`\\b${key}=`, "i");
25
+ return regex.test(attrs) ? "" : ` ${key}="${value}"`;
26
+ })
27
+ .join("");
28
+ }
29
+
30
+ export function buildFaviconLink(attrs, domain, text) {
31
+ const extraAttrs = getExtraAttrs(attrs, {
32
+ title: domain,
33
+ target: "_blank",
34
+ rel: "noopener noreferrer",
35
+ });
36
+ const wrappedText = /<[a-z]/i.test(text) ? `<span>${text}</span>` : text;
37
+ return `<a ${attrs}${extraAttrs}><i><img src="https://www.google.com/s2/favicons?domain=${domain}&sz=64"></i> ${wrappedText}</a>`;
38
+ }
39
+
40
+ export function transformLink(match, attrs, url, linkText) {
41
+ try {
42
+ const domain = new URL(url).hostname;
43
+
44
+ if (isExternalUrl(url) && !linkText.includes("↗")) {
45
+ const cleaned = cleanLinkText(linkText);
46
+ const stripped = cleaned.replace(domain, "");
47
+ const label = linkText.trim() === url && stripped.length > 2 ? stripped : cleaned;
48
+ return buildFaviconLink(attrs, domain, label);
49
+ }
50
+ } catch (e) {
51
+ // URL parsing failed — fall through and return original match
52
+ }
53
+ return match;
54
+ }
55
+
56
+ export function replaceLinksInHtml(content, transformer) {
57
+ return content.replace(
58
+ /<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([^<]*(?:<\/?(?:em|strong|code)>[^<]*)*)<\/a>/gi,
59
+ transformer,
60
+ );
61
+ }
62
+
63
+ export default function (eleventyConfig) {
64
+ eleventyConfig.addTransform("autoLinkFavicons", function (content) {
65
+ if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
66
+ return replaceLinksInHtml(content, transformLink);
67
+ }
68
+ return content;
69
+ });
70
+ }
71
+ //```