@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.
- package/.prettierrc.json +3 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +46 -0
- package/src/admin/index.html +12 -0
- package/src/do/README.md +53 -0
- package/src/do/package.json +15 -0
- package/src/eleventy.config.js +178 -0
- package/src/filters/attr_concat.js +107 -0
- package/src/filters/attr_concat.test.js +205 -0
- package/src/filters/attr_includes.js +65 -0
- package/src/filters/attr_includes.test.js +145 -0
- package/src/filters/attr_set.js +42 -0
- package/src/filters/attr_set.test.js +71 -0
- package/src/filters/fetch.js +118 -0
- package/src/filters/if.js +79 -0
- package/src/filters/if.test.js +63 -0
- package/src/filters/merge.js +78 -0
- package/src/filters/merge.test.js +51 -0
- package/src/filters/remove_tag.js +70 -0
- package/src/filters/remove_tag.test.js +60 -0
- package/src/filters/section.js +125 -0
- package/src/filters/section.test.js +174 -0
- package/src/filters/strip_tag.js +83 -0
- package/src/filters/strip_tag.test.js +74 -0
- package/src/filters/unindent.js +35 -0
- package/src/filters/unindent.test.js +49 -0
- package/src/index.js +122 -0
- package/src/processors/autoLinkFavicons.js +147 -0
- package/src/processors/autoLinkFavicons.test.js +452 -0
- package/src/processors/markdown.js +79 -0
- package/src/processors/markdown.test.js +207 -0
- package/src/siteData.js +25 -0
|
@@ -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
|
+
*/
|