@anydigital/eleventy-bricks 1.0.0-alpha.19 → 1.0.0-alpha.20

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/README.md CHANGED
@@ -25,6 +25,7 @@ export default function (eleventyConfig) {
25
25
  eleventyConfig.addPlugin(eleventyBricks, {
26
26
  mdAutoRawTags: true,
27
27
  mdAutoNl2br: true,
28
+ mdAutoLinkFavicons: true,
28
29
  siteData: true,
29
30
  filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
30
31
  });
@@ -42,6 +43,7 @@ module.exports = function (eleventyConfig) {
42
43
  eleventyConfig.addPlugin(eleventyBricks, {
43
44
  mdAutoRawTags: true,
44
45
  mdAutoNl2br: true,
46
+ mdAutoLinkFavicons: true,
45
47
  siteData: true,
46
48
  filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
47
49
  });
@@ -62,6 +64,7 @@ Import only the specific helpers you need without using the plugin:
62
64
  import {
63
65
  mdAutoRawTags,
64
66
  mdAutoNl2br,
67
+ mdAutoLinkFavicons,
65
68
  setAttrFilter,
66
69
  whereInFilter,
67
70
  mergeFilter,
@@ -74,6 +77,7 @@ import {
74
77
  export default function (eleventyConfig) {
75
78
  mdAutoRawTags(eleventyConfig);
76
79
  mdAutoNl2br(eleventyConfig);
80
+ mdAutoLinkFavicons(eleventyConfig);
77
81
  setAttrFilter(eleventyConfig);
78
82
  whereInFilter(eleventyConfig);
79
83
  mergeFilter(eleventyConfig);
@@ -92,6 +96,7 @@ export default function (eleventyConfig) {
92
96
  const {
93
97
  mdAutoRawTags,
94
98
  mdAutoNl2br,
99
+ mdAutoLinkFavicons,
95
100
  setAttrFilter,
96
101
  whereInFilter,
97
102
  mergeFilter,
@@ -104,6 +109,7 @@ const {
104
109
  module.exports = async function (eleventyConfig) {
105
110
  await mdAutoRawTags(eleventyConfig);
106
111
  await mdAutoNl2br(eleventyConfig);
112
+ await mdAutoLinkFavicons(eleventyConfig);
107
113
  await setAttrFilter(eleventyConfig);
108
114
  await whereInFilter(eleventyConfig);
109
115
  await mergeFilter(eleventyConfig);
@@ -122,12 +128,13 @@ module.exports = async function (eleventyConfig) {
122
128
 
123
129
  When using the plugin (Option 1), you can configure which helpers to enable:
124
130
 
125
- | Option | Type | Default | Description |
126
- | --------------- | --------------- | ------- | ---------------------------------------------------------------- |
127
- | `mdAutoRawTags` | boolean | `false` | Enable the mdAutoRawTags preprocessor for Markdown files |
128
- | `mdAutoNl2br` | boolean | `false` | Enable the mdAutoNl2br preprocessor to convert \n to `<br>` tags |
129
- | `siteData` | boolean | `false` | Enable site.year and site.prod global data |
130
- | `filters` | array of string | `[]` | Array of filter names to enable (see Available Filters section) |
131
+ | Option | Type | Default | Description |
132
+ | -------------------- | --------------- | ------- | ---------------------------------------------------------------- |
133
+ | `mdAutoRawTags` | boolean | `false` | Enable the mdAutoRawTags preprocessor for Markdown files |
134
+ | `mdAutoNl2br` | boolean | `false` | Enable the mdAutoNl2br preprocessor to convert \n to `<br>` tags |
135
+ | `mdAutoLinkFavicons` | boolean | `false` | Enable the mdAutoLinkFavicons transform to add favicons to links |
136
+ | `siteData` | boolean | `false` | Enable site.year and site.prod global data |
137
+ | `filters` | array of string | `[]` | Array of filter names to enable (see Available Filters section) |
131
138
 
132
139
  **Available filter names for the `filters` array:**
133
140
 
@@ -144,6 +151,7 @@ When using the plugin (Option 1), you can configure which helpers to enable:
144
151
  eleventyConfig.addPlugin(eleventyBricks, {
145
152
  mdAutoRawTags: true,
146
153
  mdAutoNl2br: true,
154
+ mdAutoLinkFavicons: true,
147
155
  siteData: true,
148
156
  filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
149
157
  });
@@ -224,6 +232,84 @@ Will render as:
224
232
 
225
233
  **Note:** This processes literal `\n` sequences (backslash followed by 'n'), not actual newline characters. Type `\n` in your source files where you want line breaks.
226
234
 
235
+ ### mdAutoLinkFavicons
236
+
237
+ Automatically adds favicon images from Google's favicon service to links that display plain URLs or domain names. This transform processes all HTML output files and adds inline favicon images next to link text that appears to be a plain URL.
238
+
239
+ **Why use this?**
240
+
241
+ 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 transform automatically detects these plain-text URL links and enhances them with favicon images, making them more visually appealing and easier to recognize.
242
+
243
+ **Usage:**
244
+
245
+ 1. Enable `mdAutoLinkFavicons` in your Eleventy config:
246
+
247
+ ```javascript
248
+ import { mdAutoLinkFavicons } from "@anydigital/eleventy-bricks";
249
+
250
+ export default function (eleventyConfig) {
251
+ mdAutoLinkFavicons(eleventyConfig);
252
+ // Or use as plugin:
253
+ // eleventyConfig.addPlugin(eleventyBricks, { mdAutoLinkFavicons: true });
254
+ }
255
+ ```
256
+
257
+ **How it works:**
258
+
259
+ The transform:
260
+
261
+ 1. Scans all HTML output files for `<a>` tags
262
+ 2. Checks if the link text appears to be a plain URL or domain
263
+ 3. Extracts the domain from the URL
264
+ 4. Removes the domain from the link text (keeping only the path)
265
+ 5. Adds a favicon image from Google's favicon service inline with the remaining text
266
+
267
+ **Example:**
268
+
269
+ Before transformation:
270
+
271
+ ```html
272
+ <a href="https://github.com/anydigital/eleventy-bricks">https://github.com/anydigital/eleventy-bricks</a>
273
+ ```
274
+
275
+ After transformation:
276
+
277
+ ```html
278
+ <a href="https://github.com/anydigital/eleventy-bricks">
279
+ <i><img src="https://www.google.com/s2/favicons?domain=github.com&sz=32" /></i>
280
+ anydigital/eleventy-bricks
281
+ </a>
282
+ ```
283
+
284
+ **Rules:**
285
+
286
+ - Only applies to links where the text looks like a plain URL (contains the domain or starts with `http://`/`https://`)
287
+ - Removes the protocol and domain from the display text
288
+ - Only applies if at least 3 characters remain after removing the domain (to avoid showing favicons for bare domain links)
289
+ - Uses Google's favicon service at `https://www.google.com/s2/favicons?domain=DOMAIN&sz=32`
290
+ - The favicon is wrapped in an `<i>` tag for easy styling
291
+
292
+ **Styling:**
293
+
294
+ You can style the favicon icons with CSS:
295
+
296
+ ```css
297
+ /* Style the favicon wrapper */
298
+ a i {
299
+ display: inline-block;
300
+ margin-right: 0.25em;
301
+ }
302
+
303
+ /* Style the favicon image */
304
+ a i img {
305
+ width: 16px;
306
+ height: 16px;
307
+ vertical-align: middle;
308
+ }
309
+ ```
310
+
311
+ **Note:** This transform only processes HTML output files (those ending in `.html`). It does not modify the original content files.
312
+
227
313
  ### attr
228
314
 
229
315
  A filter that creates a new object with an overridden attribute value. This is useful for modifying data objects in templates without mutating the original.
@@ -781,6 +867,10 @@ The plugin also exports the following utility functions for advanced usage:
781
867
 
782
868
  - `transformAutoRaw(content)`: The transform function used by `mdAutoRawTags` preprocessor. Can be used programmatically to wrap Nunjucks syntax with raw tags.
783
869
  - `transformNl2br(content)`: The transform function used by `mdAutoNl2br` preprocessor. Can be used programmatically to convert `\n` sequences to `<br>` tags.
870
+ - `isPlainUrlText(linkText, domain)`: Helper function that checks if link text looks like a plain URL or domain.
871
+ - `cleanLinkText(linkText, domain)`: Helper function that cleans link text by removing protocol, domain, and leading slash.
872
+ - `buildFaviconLink(attrs, domain, text)`: Helper function that builds HTML for a link with favicon.
873
+ - `transformLink(match, attrs, url, linkText)`: The transform function used by `mdAutoLinkFavicons` that transforms a single link to include a favicon.
784
874
  - `merge(first, ...rest)`: The core merge function used by the `merge` filter. Can be used programmatically to merge arrays or objects.
785
875
  - `removeTag(html, tagName)`: The core function used by the `remove_tag` filter. Can be used programmatically to remove HTML tags from content.
786
876
  - `iff(trueValue, condition, falseValue)`: The core conditional function used by the `if` filter. Can be used programmatically as a ternary operator.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anydigital/eleventy-bricks",
3
- "version": "1.0.0-alpha.19",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "A collection of helpful utilities and filters for Eleventy (11ty)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -27,6 +27,7 @@ export default function (eleventyConfig) {
27
27
  eleventyConfig.addPlugin(eleventyBricksPlugin, {
28
28
  mdAutoNl2br: true,
29
29
  mdAutoRawTags: true,
30
+ mdAutoLinkFavicons: true,
30
31
  siteData: true,
31
32
  filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
32
33
  });
package/src/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import {
2
2
  mdAutoRawTags,
3
3
  mdAutoNl2br,
4
+ mdAutoLinkFavicons,
4
5
  transformAutoRaw,
5
6
  transformNl2br,
7
+ isPlainUrlText,
8
+ cleanLinkText,
9
+ buildFaviconLink,
10
+ transformLink,
6
11
  } from "./markdown.js";
7
12
  import { setAttrFilter } from "./filters/attr.js";
8
13
  import { whereInFilter } from "./filters/where_in.js";
@@ -22,6 +27,7 @@ import { siteData } from "./siteData.js";
22
27
  * @param {Object} options - Plugin options
23
28
  * @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
24
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)
25
31
  * @param {Array<string>} options.filters - Array of filter names to enable: 'attr', 'where_in', 'merge', 'remove_tag', 'if', 'attr_concat' (default: [])
26
32
  * @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
27
33
  */
@@ -29,6 +35,7 @@ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
29
35
  const plugins = {
30
36
  mdAutoRawTags,
31
37
  mdAutoNl2br,
38
+ mdAutoLinkFavicons,
32
39
  siteData,
33
40
  };
34
41
 
@@ -62,6 +69,7 @@ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
62
69
  export {
63
70
  mdAutoRawTags,
64
71
  mdAutoNl2br,
72
+ mdAutoLinkFavicons,
65
73
  setAttrFilter,
66
74
  whereInFilter,
67
75
  mergeFilter,
@@ -72,4 +80,15 @@ export {
72
80
  };
73
81
 
74
82
  // Export transform/utility functions for advanced usage
75
- export { transformAutoRaw, transformNl2br, merge, removeTag, iff, attrConcat };
83
+ export {
84
+ transformAutoRaw,
85
+ transformNl2br,
86
+ isPlainUrlText,
87
+ cleanLinkText,
88
+ buildFaviconLink,
89
+ transformLink,
90
+ merge,
91
+ removeTag,
92
+ iff,
93
+ attrConcat,
94
+ };
package/src/markdown.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Transform Nunjucks syntax in content by wrapping it with raw tags
3
- *
3
+ *
4
4
  * This function wraps Nunjucks syntax ({{, }}, {%, %}) with {% raw %} tags
5
5
  * to prevent them from being processed by the template engine.
6
- *
6
+ *
7
7
  * @param {string} content - The content to transform
8
8
  * @returns {string} The transformed content with Nunjucks syntax wrapped
9
9
  */
@@ -14,10 +14,10 @@ export function transformAutoRaw(content) {
14
14
 
15
15
  /**
16
16
  * mdAutoRawTags - Forbid Nunjucks processing in Markdown files
17
- *
17
+ *
18
18
  * This preprocessor wraps Nunjucks syntax ({{, }}, {%, %}) with {% raw %} tags
19
19
  * to prevent them from being processed by the template engine in Markdown files.
20
- *
20
+ *
21
21
  * @param {Object} eleventyConfig - The Eleventy configuration object
22
22
  */
23
23
  export function mdAutoRawTags(eleventyConfig) {
@@ -28,31 +28,119 @@ export function mdAutoRawTags(eleventyConfig) {
28
28
 
29
29
  /**
30
30
  * Transform \n sequences to <br> tags
31
- *
31
+ *
32
32
  * This function converts literal \n sequences (double backslash + n) to HTML <br> tags.
33
33
  * It handles both double \n\n and single \n sequences, processing double ones first.
34
- *
34
+ *
35
35
  * @param {string} content - The content to transform
36
36
  * @returns {string} The transformed content with \n converted to <br>
37
37
  */
38
38
  export function transformNl2br(content) {
39
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>');
40
+ return content.replace(/\\n\\n/g, "<br>").replace(/\\n/g, "<br>");
41
41
  }
42
42
 
43
43
  /**
44
44
  * mdAutoNl2br - Auto convert \n to <br> in markdown (especially tables)
45
- *
45
+ *
46
46
  * This function amends the markdown library to automatically convert \n
47
47
  * to <br> tags in text content, which is particularly useful for line breaks
48
48
  * inside markdown tables where standard newlines don't work.
49
- *
49
+ *
50
50
  * @param {Object} eleventyConfig - The Eleventy configuration object
51
51
  */
52
52
  export function mdAutoNl2br(eleventyConfig) {
53
- eleventyConfig.amendLibrary("md", mdLib => {
53
+ eleventyConfig.amendLibrary("md", (mdLib) => {
54
54
  mdLib.renderer.rules.text = (tokens, idx) => {
55
55
  return transformNl2br(tokens[idx].content);
56
56
  };
57
57
  });
58
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}><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
+ * mdAutoLinkFavicons - Add favicon images to plain text links
132
+ *
133
+ * This transform automatically adds favicon images from Google's favicon service
134
+ * to links that display plain URLs or domain names. It processes all HTML output
135
+ * files and adds inline favicon images next to the link text.
136
+ *
137
+ * @param {Object} eleventyConfig - The Eleventy configuration object
138
+ */
139
+ export function mdAutoLinkFavicons(eleventyConfig) {
140
+ eleventyConfig.addTransform("mdAutoLinkFavicons", function (content) {
141
+ if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
142
+ return content.replace(/<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([^<]+)<\/a>/gi, transformLink);
143
+ }
144
+ return content;
145
+ });
146
+ }
@@ -1,33 +1,36 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { transformAutoRaw, transformNl2br } from "./markdown.js";
3
+ import {
4
+ transformAutoRaw,
5
+ transformNl2br,
6
+ isPlainUrlText,
7
+ cleanLinkText,
8
+ buildFaviconLink,
9
+ transformLink,
10
+ } from "./markdown.js";
4
11
 
5
12
  describe("transformAutoRaw", () => {
6
13
  it("should wrap opening double curly braces with raw tags", () => {
7
14
  const input = "Use {{ variable }} to output.";
8
- const expected =
9
- "Use {% raw %}{{{% endraw %} variable {% raw %}}}{% endraw %} to output.";
15
+ const expected = "Use {% raw %}{{{% endraw %} variable {% raw %}}}{% endraw %} to output.";
10
16
  assert.equal(transformAutoRaw(input), expected);
11
17
  });
12
18
 
13
19
  it("should wrap closing double curly braces with raw tags", () => {
14
20
  const input = "{{ name }}";
15
- const expected =
16
- "{% raw %}{{{% endraw %} name {% raw %}}}{% endraw %}";
21
+ const expected = "{% raw %}{{{% endraw %} name {% raw %}}}{% endraw %}";
17
22
  assert.equal(transformAutoRaw(input), expected);
18
23
  });
19
24
 
20
25
  it("should wrap opening template tags with raw tags", () => {
21
26
  const input = "{% if condition %}";
22
- const expected =
23
- "{% raw %}{%{% endraw %} if condition {% raw %}%}{% endraw %}";
27
+ const expected = "{% raw %}{%{% endraw %} if condition {% raw %}%}{% endraw %}";
24
28
  assert.equal(transformAutoRaw(input), expected);
25
29
  });
26
30
 
27
31
  it("should wrap closing template tags with raw tags", () => {
28
32
  const input = "{% endif %}";
29
- const expected =
30
- "{% raw %}{%{% endraw %} endif {% raw %}%}{% endraw %}";
33
+ const expected = "{% raw %}{%{% endraw %} endif {% raw %}%}{% endraw %}";
31
34
  assert.equal(transformAutoRaw(input), expected);
32
35
  });
33
36
 
@@ -65,15 +68,13 @@ Some text
65
68
 
66
69
  it("should handle content with only Nunjucks syntax", () => {
67
70
  const input = "{{}}";
68
- const expected =
69
- "{% raw %}{{{% endraw %}{% raw %}}}{% endraw %}";
71
+ const expected = "{% raw %}{{{% endraw %}{% raw %}}}{% endraw %}";
70
72
  assert.equal(transformAutoRaw(input), expected);
71
73
  });
72
74
 
73
75
  it("should handle consecutive Nunjucks patterns", () => {
74
76
  const input = "{{{{}}}}";
75
- const expected =
76
- "{% raw %}{{{% endraw %}{% raw %}{{{% endraw %}{% raw %}}}{% endraw %}{% raw %}}}{% endraw %}";
77
+ const expected = "{% raw %}{{{% endraw %}{% raw %}{{{% endraw %}{% raw %}}}{% endraw %}{% raw %}}}{% endraw %}";
77
78
  assert.equal(transformAutoRaw(input), expected);
78
79
  });
79
80
 
@@ -150,3 +151,223 @@ describe("transformNl2br", () => {
150
151
  });
151
152
  });
152
153
 
154
+ describe("isPlainUrlText", () => {
155
+ it("should return true when linkText contains domain", () => {
156
+ assert.equal(isPlainUrlText("example.com", "example.com"), true);
157
+ assert.equal(isPlainUrlText("https://example.com/path", "example.com"), true);
158
+ assert.equal(isPlainUrlText("Visit example.com for more", "example.com"), true);
159
+ });
160
+
161
+ it("should return true when linkText starts with http://", () => {
162
+ assert.equal(isPlainUrlText("http://example.com", "example.com"), true);
163
+ assert.equal(isPlainUrlText("http://other.com/path", "other.com"), true);
164
+ });
165
+
166
+ it("should return true when linkText starts with https://", () => {
167
+ assert.equal(isPlainUrlText("https://example.com", "example.com"), true);
168
+ assert.equal(isPlainUrlText("https://other.com/path", "other.com"), true);
169
+ });
170
+
171
+ it("should return false for custom link text without domain", () => {
172
+ assert.equal(isPlainUrlText("Click here", "example.com"), false);
173
+ assert.equal(isPlainUrlText("Read more", "example.com"), false);
174
+ assert.equal(isPlainUrlText("Documentation", "example.com"), false);
175
+ });
176
+
177
+ it("should handle whitespace in linkText", () => {
178
+ assert.equal(isPlainUrlText(" example.com ", "example.com"), true);
179
+ assert.equal(isPlainUrlText(" https://example.com ", "example.com"), true);
180
+ });
181
+
182
+ it("should return false for empty linkText", () => {
183
+ assert.equal(isPlainUrlText("", "example.com"), false);
184
+ });
185
+ });
186
+
187
+ describe("cleanLinkText", () => {
188
+ it("should remove protocol, domain, and leading slash", () => {
189
+ assert.equal(cleanLinkText("https://example.com/docs", "example.com"), "docs");
190
+ assert.equal(cleanLinkText("http://example.com/docs", "example.com"), "docs");
191
+ assert.equal(cleanLinkText("https://example.com/docs/guide", "example.com"), "docs/guide");
192
+ });
193
+
194
+ it("should handle links without protocol", () => {
195
+ assert.equal(cleanLinkText("example.com/docs", "example.com"), "docs");
196
+ assert.equal(cleanLinkText("example.com/path/to/page", "example.com"), "path/to/page");
197
+ });
198
+
199
+ it("should remove leading slash after domain removal", () => {
200
+ assert.equal(cleanLinkText("https://example.com/docs", "example.com"), "docs");
201
+ assert.equal(cleanLinkText("example.com/docs", "example.com"), "docs");
202
+ });
203
+
204
+ it("should return empty string for root domain", () => {
205
+ assert.equal(cleanLinkText("example.com/", "example.com"), "");
206
+ assert.equal(cleanLinkText("example.com", "example.com"), "");
207
+ assert.equal(cleanLinkText("https://example.com", "example.com"), "");
208
+ });
209
+
210
+ it("should handle whitespace", () => {
211
+ assert.equal(cleanLinkText(" https://example.com/docs ", "example.com"), "docs");
212
+ assert.equal(cleanLinkText("\nhttps://example.com/docs\n", "example.com"), "docs");
213
+ });
214
+
215
+ it("should preserve path after domain", () => {
216
+ assert.equal(cleanLinkText("https://example.com/api/v1/docs", "example.com"), "api/v1/docs");
217
+ });
218
+
219
+ it("should handle query parameters", () => {
220
+ const result = cleanLinkText("https://example.com/search?q=test", "example.com");
221
+ assert.equal(result, "search?q=test");
222
+ });
223
+
224
+ it("should handle hash fragments", () => {
225
+ const result = cleanLinkText("https://example.com/page#section", "example.com");
226
+ assert.equal(result, "page#section");
227
+ });
228
+ });
229
+
230
+ describe("buildFaviconLink", () => {
231
+ it("should create correct HTML with favicon", () => {
232
+ const result = buildFaviconLink('href="https://example.com/docs"', "example.com", "docs");
233
+ assert.equal(
234
+ result,
235
+ '<a href="https://example.com/docs"><i><img src="https://www.google.com/s2/favicons?domain=example.com&sz=32"></i>docs</a>'
236
+ );
237
+ });
238
+
239
+ it("should handle complex attributes", () => {
240
+ const result = buildFaviconLink('href="https://example.com" class="link" target="_blank"', "example.com", "docs");
241
+ assert.equal(
242
+ result,
243
+ '<a href="https://example.com" class="link" target="_blank"><i><img src="https://www.google.com/s2/favicons?domain=example.com&sz=32"></i>docs</a>'
244
+ );
245
+ });
246
+
247
+ it("should use sz=32 parameter for favicon size", () => {
248
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "text");
249
+ assert.match(result, /sz=32/);
250
+ });
251
+
252
+ it("should wrap img in <i> tag", () => {
253
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "text");
254
+ assert.match(result, /<i><img[^>]*><\/i>/);
255
+ });
256
+
257
+ it("should handle different domains", () => {
258
+ const result = buildFaviconLink('href="https://github.com/repo"', "github.com", "repo");
259
+ assert.equal(
260
+ result,
261
+ '<a href="https://github.com/repo"><i><img src="https://www.google.com/s2/favicons?domain=github.com&sz=32"></i>repo</a>'
262
+ );
263
+ });
264
+
265
+ it("should preserve link text as provided", () => {
266
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "custom text");
267
+ assert.match(result, />custom text<\/a>$/);
268
+ });
269
+ });
270
+
271
+ describe("transformLink", () => {
272
+ it("should transform plain URL links with sufficient length", () => {
273
+ const result = transformLink(
274
+ '<a href="https://example.com/docs">https://example.com/docs</a>',
275
+ 'href="https://example.com/docs"',
276
+ "https://example.com/docs",
277
+ "https://example.com/docs"
278
+ );
279
+ assert.match(result, /<i><img src="https:\/\/www\.google\.com\/s2\/favicons\?domain=example\.com&sz=32"><\/i>docs/);
280
+ });
281
+
282
+ it("should not transform if cleaned text is too short (2 chars or less)", () => {
283
+ const match = '<a href="https://example.com/ab">https://example.com/ab</a>';
284
+ const result = transformLink(
285
+ match,
286
+ 'href="https://example.com/ab"',
287
+ "https://example.com/ab",
288
+ "https://example.com/ab"
289
+ );
290
+ assert.equal(result, match);
291
+ });
292
+
293
+ it("should not transform custom link text without URL", () => {
294
+ const match = '<a href="https://example.com/docs">Click here</a>';
295
+ const result = transformLink(match, 'href="https://example.com/docs"', "https://example.com/docs", "Click here");
296
+ assert.equal(result, match);
297
+ });
298
+
299
+ it("should not transform root domain links", () => {
300
+ const match = '<a href="https://example.com">example.com</a>';
301
+ const result = transformLink(match, 'href="https://example.com"', "https://example.com", "example.com");
302
+ assert.equal(result, match);
303
+ });
304
+
305
+ it("should not transform links ending with slash only", () => {
306
+ const match = '<a href="https://example.com/">https://example.com/</a>';
307
+ const result = transformLink(match, 'href="https://example.com/"', "https://example.com/", "https://example.com/");
308
+ assert.equal(result, match);
309
+ });
310
+
311
+ it("should handle invalid URLs gracefully", () => {
312
+ const match = '<a href="not-a-url">not-a-url</a>';
313
+ const result = transformLink(match, 'href="not-a-url"', "not-a-url", "not-a-url");
314
+ assert.equal(result, match);
315
+ });
316
+
317
+ it("should work with http:// protocol", () => {
318
+ const result = transformLink(
319
+ '<a href="http://example.com/docs">http://example.com/docs</a>',
320
+ 'href="http://example.com/docs"',
321
+ "http://example.com/docs",
322
+ "http://example.com/docs"
323
+ );
324
+ assert.match(result, /<i><img[^>]*><\/i>docs/);
325
+ });
326
+
327
+ it("should work with https:// protocol", () => {
328
+ const result = transformLink(
329
+ '<a href="https://example.com/docs">https://example.com/docs</a>',
330
+ 'href="https://example.com/docs"',
331
+ "https://example.com/docs",
332
+ "https://example.com/docs"
333
+ );
334
+ assert.match(result, /<i><img[^>]*><\/i>docs/);
335
+ });
336
+
337
+ it("should handle longer paths correctly", () => {
338
+ const result = transformLink(
339
+ '<a href="https://example.com/path/to/document">https://example.com/path/to/document</a>',
340
+ 'href="https://example.com/path/to/document"',
341
+ "https://example.com/path/to/document",
342
+ "https://example.com/path/to/document"
343
+ );
344
+ assert.match(result, /<i><img[^>]*><\/i>path\/to\/document/);
345
+ });
346
+
347
+ it("should not transform when linkText doesn't look like URL", () => {
348
+ const match = '<a href="https://example.com/page">Read the documentation</a>';
349
+ const result = transformLink(
350
+ match,
351
+ 'href="https://example.com/page"',
352
+ "https://example.com/page",
353
+ "Read the documentation"
354
+ );
355
+ assert.equal(result, match);
356
+ });
357
+
358
+ it("should transform when linkText contains domain even without protocol", () => {
359
+ const result = transformLink(
360
+ '<a href="https://example.com/docs">example.com/docs</a>',
361
+ 'href="https://example.com/docs"',
362
+ "https://example.com/docs",
363
+ "example.com/docs"
364
+ );
365
+ assert.match(result, /<i><img[^>]*><\/i>docs/);
366
+ });
367
+
368
+ it("should handle malformed URLs by returning original match", () => {
369
+ const match = '<a href="ht!tp://bad-url">ht!tp://bad-url</a>';
370
+ const result = transformLink(match, 'href="ht!tp://bad-url"', "ht!tp://bad-url", "ht!tp://bad-url");
371
+ assert.equal(result, match);
372
+ });
373
+ });