@anyblades/buildawesome-kit-plugin 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 (39) hide show
  1. package/features/autoLinkFavicons.js +71 -0
  2. package/features/autoLinkFavicons.test.js +694 -0
  3. package/features/mdAutoNl2br.js +22 -0
  4. package/features/mdAutoNl2br.test.js +68 -0
  5. package/features/mdAutoRawTags.js +17 -0
  6. package/features/mdAutoRawTags.test.js +80 -0
  7. package/features/mdAutoUncommentAttrs.js +26 -0
  8. package/features/mdAutoUncommentAttrs.test.js +65 -0
  9. package/features/siteData.js +43 -0
  10. package/features/siteData.test.js +120 -0
  11. package/features/virtualPages.js +18 -0
  12. package/filters/attr_concat.js +55 -0
  13. package/filters/attr_concat.test.js +205 -0
  14. package/filters/attr_includes.js +41 -0
  15. package/filters/attr_includes.test.js +125 -0
  16. package/filters/attr_set.js +22 -0
  17. package/filters/attr_set.test.js +71 -0
  18. package/filters/date.js +11 -0
  19. package/filters/date.test.js +33 -0
  20. package/filters/fetch.js +80 -0
  21. package/filters/if.js +24 -0
  22. package/filters/if.test.js +63 -0
  23. package/filters/merge.js +51 -0
  24. package/filters/merge.test.js +51 -0
  25. package/filters/remove_tag.js +42 -0
  26. package/filters/remove_tag.test.js +60 -0
  27. package/filters/section.js +82 -0
  28. package/filters/section.test.js +174 -0
  29. package/filters/split.js +9 -0
  30. package/filters/split.test.js +39 -0
  31. package/filters/strip_tag.js +39 -0
  32. package/filters/strip_tag.test.js +74 -0
  33. package/filters/unindent.js +11 -0
  34. package/filters/unindent.test.js +49 -0
  35. package/package.json +48 -0
  36. package/plugin.js +53 -0
  37. package/plugin.test.js +8 -0
  38. package/scripts/README.md +59 -0
  39. package/scripts/package.json +17 -0
@@ -0,0 +1,694 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ isExternalUrl,
5
+ cleanLinkText,
6
+ getExtraAttrs,
7
+ buildFaviconLink,
8
+ transformLink,
9
+ replaceLinksInHtml,
10
+ } from "./autoLinkFavicons.js";
11
+
12
+ describe("isExternalUrl", () => {
13
+ it("should return true for http:// URLs", () => {
14
+ assert.equal(isExternalUrl("http://example.com"), true);
15
+ assert.equal(isExternalUrl("http://example.com/path"), true);
16
+ });
17
+
18
+ it("should return true for https:// URLs", () => {
19
+ assert.equal(isExternalUrl("https://example.com"), true);
20
+ assert.equal(isExternalUrl("https://example.com/path"), true);
21
+ });
22
+
23
+ it("should return false for relative URLs", () => {
24
+ assert.equal(isExternalUrl("/docs/guide"), false);
25
+ assert.equal(isExternalUrl("./relative"), false);
26
+ assert.equal(isExternalUrl("page.html"), false);
27
+ });
28
+
29
+ it("should return false for empty string", () => {
30
+ assert.equal(isExternalUrl(""), false);
31
+ });
32
+
33
+ it("should return false for protocol-relative and other schemes", () => {
34
+ assert.equal(isExternalUrl("//example.com"), false);
35
+ assert.equal(isExternalUrl("mailto:user@example.com"), false);
36
+ });
37
+ });
38
+
39
+ describe("cleanLinkText", () => {
40
+ it("should remove protocol prefix", () => {
41
+ assert.equal(cleanLinkText("https://example.com/docs"), "example.com/docs");
42
+ assert.equal(cleanLinkText("http://example.com/docs"), "example.com/docs");
43
+ assert.equal(cleanLinkText("https://example.com/docs/guide"), "example.com/docs/guide");
44
+ });
45
+
46
+ it("should handle links without protocol", () => {
47
+ assert.equal(cleanLinkText("example.com/docs"), "example.com/docs");
48
+ assert.equal(cleanLinkText("example.com/path/to/page"), "example.com/path/to/page");
49
+ });
50
+
51
+ it("should remove trailing slash", () => {
52
+ assert.equal(cleanLinkText("example.com/"), "example.com");
53
+ assert.equal(cleanLinkText("https://example.com/"), "example.com");
54
+ });
55
+
56
+ it("should handle root domain (no path)", () => {
57
+ assert.equal(cleanLinkText("example.com"), "example.com");
58
+ assert.equal(cleanLinkText("https://example.com"), "example.com");
59
+ });
60
+
61
+ it("should handle whitespace", () => {
62
+ assert.equal(cleanLinkText(" https://example.com/docs "), "example.com/docs");
63
+ assert.equal(cleanLinkText("\nhttps://example.com/docs\n"), "example.com/docs");
64
+ });
65
+
66
+ it("should preserve path after domain", () => {
67
+ assert.equal(cleanLinkText("https://example.com/api/v1/docs"), "example.com/api/v1/docs");
68
+ });
69
+
70
+ it("should handle query parameters", () => {
71
+ assert.equal(cleanLinkText("https://example.com/search?q=test"), "example.com/search?q=test");
72
+ });
73
+
74
+ it("should handle hash fragments", () => {
75
+ assert.equal(cleanLinkText("https://example.com/page#section"), "example.com/page#section");
76
+ });
77
+ });
78
+
79
+ describe("getExtraAttrs", () => {
80
+ it("should return default attributes when none are present", () => {
81
+ const defaults = {
82
+ title: "example.com",
83
+ target: "_blank",
84
+ rel: "noopener noreferrer",
85
+ };
86
+ const result = getExtraAttrs("", defaults);
87
+ assert.equal(result, ' title="example.com" target="_blank" rel="noopener noreferrer"');
88
+ });
89
+
90
+ it("should return only missing attributes when some are present", () => {
91
+ const defaults = {
92
+ title: "example.com",
93
+ target: "_blank",
94
+ rel: "noopener noreferrer",
95
+ };
96
+ const result = getExtraAttrs('target="_self"', defaults);
97
+ assert.equal(result, ' title="example.com" rel="noopener noreferrer"');
98
+ });
99
+
100
+ it("should be case-insensitive when checking attributes", () => {
101
+ const defaults = {
102
+ title: "example.com",
103
+ };
104
+ const result = getExtraAttrs('TITLE="custom"', defaults);
105
+ assert.equal(result, "");
106
+ });
107
+
108
+ it("should handle empty default dictionary", () => {
109
+ const result = getExtraAttrs('href="https://example.com"', {});
110
+ assert.equal(result, "");
111
+ });
112
+
113
+ it("should match attributes respect word boundaries", () => {
114
+ const defaults = {
115
+ title: "example.com",
116
+ };
117
+ // 'customtitle' contains 'title', but is not the 'title' attribute itself
118
+ const result = getExtraAttrs('customtitle="foo"', defaults);
119
+ assert.equal(result, ' title="example.com"');
120
+ });
121
+ });
122
+
123
+ describe("buildFaviconLink", () => {
124
+ it("should create correct HTML with favicon", () => {
125
+ const result = buildFaviconLink('href="https://example.com/docs"', "example.com", "/docs");
126
+ assert.equal(
127
+ result,
128
+ '<a href="https://example.com/docs" title="example.com" target="_blank" rel="noopener noreferrer"><i><img src="https://www.google.com/s2/favicons?domain=example.com&sz=64"></i> /docs</a>',
129
+ );
130
+ });
131
+
132
+ it("should handle complex attributes", () => {
133
+ const result = buildFaviconLink('href="https://example.com" class="link"', "example.com", "text");
134
+ assert.equal(
135
+ result,
136
+ '<a href="https://example.com" class="link" title="example.com" target="_blank" rel="noopener noreferrer"><i><img src="https://www.google.com/s2/favicons?domain=example.com&sz=64"></i> text</a>',
137
+ );
138
+ });
139
+
140
+ it("should use sz=64 parameter for favicon size", () => {
141
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "text");
142
+ assert.match(result, /sz=64/);
143
+ });
144
+
145
+ it("should handle different domains", () => {
146
+ const result = buildFaviconLink('href="https://github.com/repo"', "github.com", "/repo");
147
+ assert.equal(
148
+ result,
149
+ '<a href="https://github.com/repo" title="github.com" target="_blank" rel="noopener noreferrer"><i><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64"></i> /repo</a>',
150
+ );
151
+ });
152
+
153
+ it("should preserve link text as provided", () => {
154
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "custom text");
155
+ assert.match(result, /> custom text<\/a>$/);
156
+ });
157
+
158
+ it("should not wrap plain text in a span", () => {
159
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "/docs");
160
+ assert.match(result, /> \/docs<\/a>$/);
161
+ assert.doesNotMatch(result, /<span>/);
162
+ });
163
+
164
+ it("should wrap text containing HTML tags in a span", () => {
165
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "<em>Example</em>");
166
+ assert.match(result, /> <span><em>Example<\/em><\/span><\/a>$/);
167
+ });
168
+
169
+ it("should wrap text containing strong tags in a span", () => {
170
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "<strong>Bold</strong>");
171
+ assert.match(result, /> <span><strong>Bold<\/strong><\/span><\/a>$/);
172
+ });
173
+
174
+ it("should wrap text containing code tags in a span", () => {
175
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "<code>npm install</code>");
176
+ assert.match(result, /> <span><code>npm install<\/code><\/span><\/a>$/);
177
+ });
178
+
179
+ it("should wrap text with mixed HTML and plain text in a span", () => {
180
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "Visit <em>Example</em> site");
181
+ assert.match(result, /> <span>Visit <em>Example<\/em> site<\/span><\/a>$/);
182
+ });
183
+
184
+ it("should add domain as title attribute when not present", () => {
185
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "text");
186
+ assert.match(result, /title="example\.com"/);
187
+ });
188
+
189
+ it("should not add title attribute when already present in attrs", () => {
190
+ const result = buildFaviconLink('href="https://example.com" title="Custom Title"', "example.com", "text");
191
+ assert.doesNotMatch(result, /title="example\.com"/);
192
+ assert.match(result, /title="Custom Title"/);
193
+ // Ensure title appears exactly once
194
+ assert.equal((result.match(/title=/g) ?? []).length, 1);
195
+ });
196
+
197
+ it("should not add title when existing title uses single quotes", () => {
198
+ const result = buildFaviconLink("href='https://example.com' title='My Link'", "example.com", "text");
199
+ assert.equal((result.match(/title=/gi) ?? []).length, 1);
200
+ });
201
+
202
+ it("should add target=_blank when not already present", () => {
203
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "text");
204
+ assert.match(result, /target="_blank"/);
205
+ });
206
+
207
+ it("should not add target when already present in attrs", () => {
208
+ const result = buildFaviconLink('href="https://example.com" target="_self"', "example.com", "text");
209
+ assert.doesNotMatch(result, /target="_blank"/);
210
+ assert.match(result, /target="_self"/);
211
+ assert.equal((result.match(/target=/g) ?? []).length, 1);
212
+ });
213
+
214
+ it("should add rel=noopener noreferrer when not already present", () => {
215
+ const result = buildFaviconLink('href="https://example.com"', "example.com", "text");
216
+ assert.match(result, /rel="noopener noreferrer"/);
217
+ });
218
+
219
+ it("should not add rel when already present in attrs", () => {
220
+ const result = buildFaviconLink('href="https://example.com" rel="nofollow"', "example.com", "text");
221
+ assert.doesNotMatch(result, /rel="noopener noreferrer"/);
222
+ assert.match(result, /rel="nofollow"/);
223
+ assert.equal((result.match(/rel=/g) ?? []).length, 1);
224
+ });
225
+
226
+ it("should not add target when existing target uses single quotes", () => {
227
+ const result = buildFaviconLink("href='https://example.com' target='_self'", "example.com", "text");
228
+ assert.equal((result.match(/target=/gi) ?? []).length, 1);
229
+ });
230
+
231
+ it("should not add rel when existing rel uses single quotes", () => {
232
+ const result = buildFaviconLink("href='https://example.com' rel='nofollow'", "example.com", "text");
233
+ assert.equal((result.match(/rel=/gi) ?? []).length, 1);
234
+ });
235
+ });
236
+
237
+ describe("transformLink", () => {
238
+ it("should transform plain URL links with sufficient length", () => {
239
+ const result = transformLink(
240
+ '<a href="https://example.com/docs">https://example.com/docs</a>',
241
+ 'href="https://example.com/docs"',
242
+ "https://example.com/docs",
243
+ "https://example.com/docs",
244
+ );
245
+ assert.match(
246
+ result,
247
+ /<i><img src="https:\/\/www\.google\.com\/s2\/favicons\?domain=example\.com&sz=64"><\/i> \/docs<\/a>/,
248
+ );
249
+ });
250
+
251
+ it("should transform links with short paths (shows domain)", () => {
252
+ // When path is short (<=2 chars), cleanLinkText returns cleanedText (with domain)
253
+ // The link still gets transformed and shows the domain
254
+ const result = transformLink(
255
+ '<a href="https://example.com/a">https://example.com/a</a>',
256
+ 'href="https://example.com/a"',
257
+ "https://example.com/a",
258
+ "https://example.com/a",
259
+ );
260
+ assert.match(result, /<i><img[^>]*><\/i> example\.com\/a<\/a>/);
261
+ });
262
+
263
+ it("should not transform links when URL is not external", () => {
264
+ const match = '<a href="/docs">Click here</a>';
265
+ const result = transformLink(match, 'href="/docs"', "/docs", "Click here");
266
+ assert.equal(result, match);
267
+ });
268
+
269
+ it("should transform root domain links (shows domain)", () => {
270
+ // Root domains are transformed and display the domain name
271
+ const result = transformLink(
272
+ '<a href="https://example.com">example.com</a>',
273
+ 'href="https://example.com"',
274
+ "https://example.com",
275
+ "example.com",
276
+ );
277
+ assert.match(result, /<i><img[^>]*><\/i> example\.com<\/a>/);
278
+ });
279
+
280
+ it("should transform links ending with slash only (shows domain)", () => {
281
+ // Links with trailing slash are transformed and display the domain name
282
+ const result = transformLink(
283
+ '<a href="https://example.com/">https://example.com/</a>',
284
+ 'href="https://example.com/"',
285
+ "https://example.com/",
286
+ "https://example.com/",
287
+ );
288
+ assert.match(result, /<i><img[^>]*><\/i> example\.com<\/a>/);
289
+ });
290
+
291
+ it("should handle invalid URLs gracefully", () => {
292
+ const match = '<a href="not-a-url">not-a-url</a>';
293
+ const result = transformLink(match, 'href="not-a-url"', "not-a-url", "not-a-url");
294
+ assert.equal(result, match);
295
+ });
296
+
297
+ it("should not transform links whose text already contains ↗", () => {
298
+ const match = '<a href="https://example.com/docs">example.com ↗</a>';
299
+ const result = transformLink(match, 'href="https://example.com/docs"', "https://example.com/docs", "example.com ↗");
300
+ assert.equal(result, match);
301
+ });
302
+
303
+ it("should work with http:// protocol", () => {
304
+ const result = transformLink(
305
+ '<a href="http://example.com/docs">http://example.com/docs</a>',
306
+ 'href="http://example.com/docs"',
307
+ "http://example.com/docs",
308
+ "http://example.com/docs",
309
+ );
310
+ assert.match(result, /<i><img[^>]*><\/i> \/docs<\/a>/);
311
+ });
312
+
313
+ it("should work with https:// protocol", () => {
314
+ const result = transformLink(
315
+ '<a href="https://example.com/docs">https://example.com/docs</a>',
316
+ 'href="https://example.com/docs"',
317
+ "https://example.com/docs",
318
+ "https://example.com/docs",
319
+ );
320
+ assert.match(result, /<i><img[^>]*><\/i> \/docs<\/a>/);
321
+ });
322
+
323
+ it("should handle longer paths correctly", () => {
324
+ const result = transformLink(
325
+ '<a href="https://example.com/path/to/document">https://example.com/path/to/document</a>',
326
+ 'href="https://example.com/path/to/document"',
327
+ "https://example.com/path/to/document",
328
+ "https://example.com/path/to/document",
329
+ );
330
+ assert.match(result, /<i><img[^>]*><\/i> \/path\/to\/document<\/a>/);
331
+ });
332
+
333
+ it("should transform any external URL regardless of link text", () => {
334
+ const result = transformLink(
335
+ '<a href="https://example.com/page">Read the documentation</a>',
336
+ 'href="https://example.com/page"',
337
+ "https://example.com/page",
338
+ "Read the documentation",
339
+ );
340
+ assert.match(result, /<i><img[^>]*><\/i>/);
341
+ });
342
+
343
+ it("should transform external URL even when linkText contains domain without protocol", () => {
344
+ const result = transformLink(
345
+ '<a href="https://example.com/docs">example.com/docs</a>',
346
+ 'href="https://example.com/docs"',
347
+ "https://example.com/docs",
348
+ "example.com/docs",
349
+ );
350
+ assert.match(result, /<i><img[^>]*><\/i>/);
351
+ });
352
+
353
+ it("should handle malformed URLs by returning original match", () => {
354
+ const match = '<a href="ht!tp://bad-url">ht!tp://bad-url</a>';
355
+ const result = transformLink(match, 'href="ht!tp://bad-url"', "ht!tp://bad-url", "ht!tp://bad-url");
356
+ assert.equal(result, match);
357
+ });
358
+
359
+ describe("when linkText.trim() === url (via replaceLinksInHtml)", () => {
360
+ it("should strip domain when path is significant (stripped length > 2)", () => {
361
+ const html = '<a href="https://example.com/docs">https://example.com/docs</a>';
362
+ const result = replaceLinksInHtml(html, transformLink);
363
+ assert.match(result, /<i><img[^>]*><\/i> \/docs<\/a>/);
364
+ });
365
+
366
+ it("should strip domain and handle whitespace in link text", () => {
367
+ const html = '<a href="https://example.com/docs"> https://example.com/docs </a>';
368
+ const result = replaceLinksInHtml(html, transformLink);
369
+ assert.match(result, /<i><img[^>]*><\/i> \/docs<\/a>/);
370
+ });
371
+
372
+ it("should not strip domain when path is short (stripped length <= 2)", () => {
373
+ const html = '<a href="https://example.com/a">https://example.com/a</a>';
374
+ const result = replaceLinksInHtml(html, transformLink);
375
+ assert.match(result, /<i><img[^>]*><\/i> example\.com\/a<\/a>/);
376
+ });
377
+
378
+ it("should not strip domain for root domain links", () => {
379
+ const html = '<a href="https://example.com/">https://example.com/</a>';
380
+ const result = replaceLinksInHtml(html, transformLink);
381
+ assert.match(result, /<i><img[^>]*><\/i> example\.com<\/a>/);
382
+ });
383
+
384
+ it("should not strip domain when link text is not equal to url", () => {
385
+ const html = '<a href="https://example.com/docs">Visit https://example.com/docs</a>';
386
+ const result = replaceLinksInHtml(html, transformLink);
387
+ assert.match(result, /<i><img[^>]*><\/i> Visit https:\/\/example\.com\/docs<\/a>/);
388
+ });
389
+
390
+ it("should not strip domain when link text is a different URL", () => {
391
+ const html = '<a href="https://example.com/docs">https://example.com/docs/extra</a>';
392
+ const result = replaceLinksInHtml(html, transformLink);
393
+ assert.match(result, /<i><img[^>]*><\/i> example\.com\/docs\/extra<\/a>/);
394
+ });
395
+ });
396
+ });
397
+
398
+
399
+ describe("replaceLinksInHtml", () => {
400
+ it("should replace a single anchor link with transformer function", () => {
401
+ const html = '<a href="https://example.com">Click here</a>';
402
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
403
+ const result = replaceLinksInHtml(html, transformer);
404
+ assert.equal(result, "[Click here](https://example.com)");
405
+ });
406
+
407
+ it("should replace multiple anchor links in HTML", () => {
408
+ const html = '<a href="https://example.com">Link 1</a> and <a href="https://other.com">Link 2</a>';
409
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
410
+ const result = replaceLinksInHtml(html, transformer);
411
+ assert.equal(result, "[Link 1](https://example.com) and [Link 2](https://other.com)");
412
+ });
413
+
414
+ it("should handle links with single quotes in href", () => {
415
+ const html = "<a href='https://example.com'>Link</a>";
416
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
417
+ const result = replaceLinksInHtml(html, transformer);
418
+ assert.equal(result, "[Link](https://example.com)");
419
+ });
420
+
421
+ it("should handle links with double quotes in href", () => {
422
+ const html = '<a href="https://example.com">Link</a>';
423
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
424
+ const result = replaceLinksInHtml(html, transformer);
425
+ assert.equal(result, "[Link](https://example.com)");
426
+ });
427
+
428
+ it("should capture all attributes in first group", () => {
429
+ const html = '<a href="https://example.com" class="link" target="_blank">Link</a>';
430
+ let capturedAttrs = "";
431
+ const transformer = (match, attrs, url, linkText) => {
432
+ capturedAttrs = attrs;
433
+ return match;
434
+ };
435
+ replaceLinksInHtml(html, transformer);
436
+ assert.equal(capturedAttrs, 'href="https://example.com" class="link" target="_blank"');
437
+ });
438
+
439
+ it("should capture URL in second group", () => {
440
+ const html = '<a href="https://example.com/path">Link</a>';
441
+ let capturedUrl = "";
442
+ const transformer = (match, attrs, url, linkText) => {
443
+ capturedUrl = url;
444
+ return match;
445
+ };
446
+ replaceLinksInHtml(html, transformer);
447
+ assert.equal(capturedUrl, "https://example.com/path");
448
+ });
449
+
450
+ it("should capture link text in third group", () => {
451
+ const html = '<a href="https://example.com">Click here</a>';
452
+ let capturedText = "";
453
+ const transformer = (match, attrs, url, linkText) => {
454
+ capturedText = linkText;
455
+ return match;
456
+ };
457
+ replaceLinksInHtml(html, transformer);
458
+ assert.equal(capturedText, "Click here");
459
+ });
460
+
461
+ it("should preserve content that is not an anchor link", () => {
462
+ const html = '<p>Some text</p><a href="https://example.com">Link</a><div>More text</div>';
463
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
464
+ const result = replaceLinksInHtml(html, transformer);
465
+ assert.equal(result, "<p>Some text</p>[Link](https://example.com)<div>More text</div>");
466
+ });
467
+
468
+ it("should handle empty HTML content", () => {
469
+ const html = "";
470
+ const transformer = (match) => match;
471
+ const result = replaceLinksInHtml(html, transformer);
472
+ assert.equal(result, "");
473
+ });
474
+
475
+ it("should handle HTML with no anchor links", () => {
476
+ const html = "<p>No links here</p><div>Just text</div>";
477
+ const transformer = (match) => "REPLACED";
478
+ const result = replaceLinksInHtml(html, transformer);
479
+ assert.equal(result, html);
480
+ });
481
+
482
+ it("should handle links with query parameters", () => {
483
+ const html = '<a href="https://example.com/search?q=test&lang=en">Search</a>';
484
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
485
+ const result = replaceLinksInHtml(html, transformer);
486
+ assert.equal(result, "[Search](https://example.com/search?q=test&lang=en)");
487
+ });
488
+
489
+ it("should handle links with hash fragments", () => {
490
+ const html = '<a href="https://example.com/page#section">Section</a>';
491
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
492
+ const result = replaceLinksInHtml(html, transformer);
493
+ assert.equal(result, "[Section](https://example.com/page#section)");
494
+ });
495
+
496
+ it("should be case-insensitive for anchor tag", () => {
497
+ const html = '<A HREF="https://example.com">Link</A>';
498
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
499
+ const result = replaceLinksInHtml(html, transformer);
500
+ assert.equal(result, "[Link](https://example.com)");
501
+ });
502
+
503
+ it("should handle links with additional attributes before href", () => {
504
+ const html = '<a class="link" href="https://example.com" id="mylink">Link</a>';
505
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
506
+ const result = replaceLinksInHtml(html, transformer);
507
+ assert.equal(result, "[Link](https://example.com)");
508
+ });
509
+
510
+ it("should handle links with additional attributes after href", () => {
511
+ const html = '<a href="https://example.com" class="link" id="mylink">Link</a>';
512
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
513
+ const result = replaceLinksInHtml(html, transformer);
514
+ assert.equal(result, "[Link](https://example.com)");
515
+ });
516
+
517
+ it("should handle relative URLs", () => {
518
+ const html = '<a href="/docs/guide">Guide</a>';
519
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
520
+ const result = replaceLinksInHtml(html, transformer);
521
+ assert.equal(result, "[Guide](/docs/guide)");
522
+ });
523
+
524
+ it("should handle root-relative URLs", () => {
525
+ const html = '<a href="/">Home</a>';
526
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
527
+ const result = replaceLinksInHtml(html, transformer);
528
+ assert.equal(result, "[Home](/)");
529
+ });
530
+
531
+ it("should not match anchor tags with nested HTML in link text", () => {
532
+ const html = '<a href="https://example.com"><span>Link</span></a>';
533
+ const transformer = (match) => "REPLACED";
534
+ const result = replaceLinksInHtml(html, transformer);
535
+ // Only <em> and <strong> are allowed inside <a>; other tags are not matched
536
+ assert.equal(result, html);
537
+ });
538
+
539
+ it("should handle transformer that returns original match unchanged", () => {
540
+ const html = '<a href="https://example.com">Link</a>';
541
+ const transformer = (match) => match;
542
+ const result = replaceLinksInHtml(html, transformer);
543
+ assert.equal(result, html);
544
+ });
545
+
546
+ it("should work with transformLink function for real-world usage", () => {
547
+ const html = '<a href="https://example.com/docs">https://example.com/docs</a>';
548
+ const result = replaceLinksInHtml(html, transformLink);
549
+ // transformLink should add favicon for plain URL links
550
+ assert.match(result, /<i><img src="https:\/\/www\.google\.com\/s2\/favicons\?domain=example\.com&sz=64"><\/i> /);
551
+ assert.match(result, /\/docs<\/a>/);
552
+ });
553
+
554
+ it("should transform external links even with custom text when using transformLink", () => {
555
+ const html = '<a href="https://example.com/docs">Click here</a>';
556
+ const result = replaceLinksInHtml(html, transformLink);
557
+ // transformLink now adds favicon for any external URL
558
+ assert.match(result, /img src=/);
559
+ });
560
+
561
+ it("should handle multiple links with mixed transformation results", () => {
562
+ const html =
563
+ '<a href="https://example.com/docs">https://example.com/docs</a> and <a href="https://other.com">Click</a>';
564
+ const result = replaceLinksInHtml(html, transformLink);
565
+ // Both links should be transformed (any external URL gets a favicon)
566
+ assert.match(result, /img src=/);
567
+ assert.match(result, /Click<\/a>/);
568
+ });
569
+
570
+ it("should handle link text with special characters", () => {
571
+ const html = '<a href="https://example.com">Link & More!</a>';
572
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
573
+ const result = replaceLinksInHtml(html, transformer);
574
+ assert.equal(result, "[Link & More!](https://example.com)");
575
+ });
576
+
577
+ it("should handle URLs with ports", () => {
578
+ const html = '<a href="https://example.com:8080/page">Link</a>';
579
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
580
+ const result = replaceLinksInHtml(html, transformer);
581
+ assert.equal(result, "[Link](https://example.com:8080/page)");
582
+ });
583
+
584
+ it("should handle http protocol links", () => {
585
+ const html = '<a href="http://example.com">Link</a>';
586
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
587
+ const result = replaceLinksInHtml(html, transformer);
588
+ assert.equal(result, "[Link](http://example.com)");
589
+ });
590
+
591
+ it("should handle links in multiline HTML", () => {
592
+ const html = `<div>
593
+ <a href="https://example.com">Link 1</a>
594
+ <p>Some text</p>
595
+ <a href="https://other.com">Link 2</a>
596
+ </div>`;
597
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
598
+ const result = replaceLinksInHtml(html, transformer);
599
+ assert.match(result, /\[Link 1\]\(https:\/\/example\.com\)/);
600
+ assert.match(result, /\[Link 2\]\(https:\/\/other\.com\)/);
601
+ });
602
+
603
+ it("should handle adjacent anchor links", () => {
604
+ const html = '<a href="https://example.com">Link1</a><a href="https://other.com">Link2</a>';
605
+ const transformer = (match, attrs, url, linkText) => `[${linkText}](${url})`;
606
+ const result = replaceLinksInHtml(html, transformer);
607
+ assert.equal(result, "[Link1](https://example.com)[Link2](https://other.com)");
608
+ });
609
+ });
610
+
611
+ describe("replaceLinksInHtml - em, strong, and code inside link text", () => {
612
+ const passThrough = (match, attrs, url, linkText) => `[${linkText}](${url})`;
613
+
614
+ it("should match link text wrapped in <em>", () => {
615
+ const html = '<a href="https://example.com"><em>Example</em></a>';
616
+ const result = replaceLinksInHtml(html, passThrough);
617
+ assert.equal(result, "[<em>Example</em>](https://example.com)");
618
+ });
619
+
620
+ it("should match link text wrapped in <strong>", () => {
621
+ const html = '<a href="https://example.com"><strong>Example</strong></a>';
622
+ const result = replaceLinksInHtml(html, passThrough);
623
+ assert.equal(result, "[<strong>Example</strong>](https://example.com)");
624
+ });
625
+
626
+ it("should match link text wrapped in <code>", () => {
627
+ const html = '<a href="https://example.com"><code>npm install</code></a>';
628
+ const result = replaceLinksInHtml(html, passThrough);
629
+ assert.equal(result, "[<code>npm install</code>](https://example.com)");
630
+ });
631
+
632
+ it("should match plain text mixed with <em>", () => {
633
+ const html = '<a href="https://example.com">Visit <em>Example</em> site</a>';
634
+ const result = replaceLinksInHtml(html, passThrough);
635
+ assert.equal(result, "[Visit <em>Example</em> site](https://example.com)");
636
+ });
637
+
638
+ it("should match plain text mixed with <strong>", () => {
639
+ const html = '<a href="https://example.com">Visit <strong>Example</strong> site</a>';
640
+ const result = replaceLinksInHtml(html, passThrough);
641
+ assert.equal(result, "[Visit <strong>Example</strong> site](https://example.com)");
642
+ });
643
+
644
+ it("should match plain text mixed with <code>", () => {
645
+ const html = '<a href="https://example.com">Run <code>npm install</code> first</a>';
646
+ const result = replaceLinksInHtml(html, passThrough);
647
+ assert.equal(result, "[Run <code>npm install</code> first](https://example.com)");
648
+ });
649
+
650
+ it("should match link text with both <em> and <strong>", () => {
651
+ const html = '<a href="https://example.com"><em>Foo</em> and <strong>Bar</strong></a>';
652
+ const result = replaceLinksInHtml(html, passThrough);
653
+ assert.equal(result, "[<em>Foo</em> and <strong>Bar</strong>](https://example.com)");
654
+ });
655
+
656
+ it("should match link text with <code> and <em>", () => {
657
+ const html = '<a href="https://example.com"><code>foo</code> or <em>bar</em></a>';
658
+ const result = replaceLinksInHtml(html, passThrough);
659
+ assert.equal(result, "[<code>foo</code> or <em>bar</em>](https://example.com)");
660
+ });
661
+
662
+ it("should not match <code> with attributes", () => {
663
+ const html = '<a href="https://example.com"><code class="lang-js">foo()</code></a>';
664
+ const result = replaceLinksInHtml(html, passThrough);
665
+ // <code class="lang-js"> is not a bare <code> tag — should not be matched
666
+ assert.equal(result, html);
667
+ });
668
+
669
+ it("should not match <em> with attributes", () => {
670
+ const html = '<a href="https://example.com"><em class="x">Example</em></a>';
671
+ const result = replaceLinksInHtml(html, passThrough);
672
+ // <em class="x"> is not a bare <em> tag — should not be matched
673
+ assert.equal(result, html);
674
+ });
675
+
676
+ it("should not match other nested tags such as <span>", () => {
677
+ const html = '<a href="https://example.com"><span>Example</span></a>';
678
+ const result = replaceLinksInHtml(html, passThrough);
679
+ assert.equal(result, html);
680
+ });
681
+
682
+ it("should not match <img> inside link text", () => {
683
+ const html = '<a href="https://example.com"><img src="icon.png"> Example</a>';
684
+ const result = replaceLinksInHtml(html, passThrough);
685
+ assert.equal(result, html);
686
+ });
687
+
688
+ it("should transform <code> link text via transformLink (end-to-end)", () => {
689
+ const html = '<a href="https://example.com/docs"><code>npm install pkg</code></a>';
690
+ const result = replaceLinksInHtml(html, transformLink);
691
+ assert.match(result, /<i><img[^>]*><\/i>/);
692
+ assert.match(result, /<span><code>npm install pkg<\/code><\/span>/);
693
+ });
694
+ });