@anydigital/eleventy-bricks 0.22.0
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 +1064 -0
- package/package.json +49 -0
- package/src/admin/index.html +12 -0
- package/src/cli/download-files.js +136 -0
- package/src/do/package.json +15 -0
- package/src/eleventy.config.js +72 -0
- package/src/filters/attr.js +16 -0
- package/src/filters/attr_concat.js +65 -0
- package/src/filters/attr_concat.test.js +205 -0
- package/src/filters/if.js +39 -0
- package/src/filters/if.test.js +63 -0
- package/src/filters/merge.js +47 -0
- package/src/filters/merge.test.js +51 -0
- package/src/filters/remove_tag.js +42 -0
- package/src/filters/remove_tag.test.js +60 -0
- package/src/filters/where_in.js +49 -0
- package/src/filters/where_in.test.js +148 -0
- package/src/index.cjs +26 -0
- package/src/index.js +94 -0
- package/src/markdown.js +163 -0
- package/src/markdown.test.js +589 -0
- package/src/siteData.js +12 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge objects together
|
|
3
|
+
*
|
|
4
|
+
* Shallow merges objects (later values override earlier ones)
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} first - The first object
|
|
7
|
+
* @param {...Object} rest - Additional objects to merge
|
|
8
|
+
* @returns {Object} The merged result
|
|
9
|
+
*/
|
|
10
|
+
export function merge(first, ...rest) {
|
|
11
|
+
// If first argument is null or undefined, treat as empty object
|
|
12
|
+
if (first === null || first === undefined) {
|
|
13
|
+
first = {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Only support objects
|
|
17
|
+
if (typeof first === "object" && !Array.isArray(first)) {
|
|
18
|
+
// Merge objects using spread operator (shallow merge)
|
|
19
|
+
return rest.reduce(
|
|
20
|
+
(acc, item) => {
|
|
21
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
22
|
+
return { ...acc, ...item };
|
|
23
|
+
}
|
|
24
|
+
return acc;
|
|
25
|
+
},
|
|
26
|
+
{ ...first }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If first is not an object, return empty object
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* merge filter - Merge objects together
|
|
36
|
+
*
|
|
37
|
+
* This filter merges objects, similar to Twig's merge filter.
|
|
38
|
+
*
|
|
39
|
+
* Usage in templates:
|
|
40
|
+
* {{ obj1 | merge(obj2) }}
|
|
41
|
+
* {{ obj1 | merge(obj2, obj3) }}
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
44
|
+
*/
|
|
45
|
+
export function mergeFilter(eleventyConfig) {
|
|
46
|
+
eleventyConfig.addFilter("merge", merge);
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { merge } from './merge.js';
|
|
4
|
+
|
|
5
|
+
test('merge - merges two objects', () => {
|
|
6
|
+
const result = merge({ a: 1, b: 2 }, { c: 3, d: 4 });
|
|
7
|
+
assert.deepStrictEqual(result, { a: 1, b: 2, c: 3, d: 4 });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('merge - merges objects with override', () => {
|
|
11
|
+
const result = merge({ a: 1, b: 2 }, { b: 3, c: 4 });
|
|
12
|
+
assert.deepStrictEqual(result, { a: 1, b: 3, c: 4 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('merge - merges multiple objects', () => {
|
|
16
|
+
const result = merge({ a: 1 }, { b: 2 }, { c: 3 });
|
|
17
|
+
assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('merge - handles null first argument', () => {
|
|
21
|
+
const result = merge(null, { a: 1 });
|
|
22
|
+
assert.deepStrictEqual(result, { a: 1 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('merge - handles undefined first argument', () => {
|
|
26
|
+
const result = merge(undefined, { a: 1 });
|
|
27
|
+
assert.deepStrictEqual(result, { a: 1 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('merge - does not modify original objects', () => {
|
|
31
|
+
const original = { a: 1 };
|
|
32
|
+
const result = merge(original, { b: 2 });
|
|
33
|
+
|
|
34
|
+
assert.deepStrictEqual(original, { a: 1 });
|
|
35
|
+
assert.deepStrictEqual(result, { a: 1, b: 2 });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('merge - returns empty object for arrays', () => {
|
|
39
|
+
const result = merge([1, 2], [3, 4]);
|
|
40
|
+
assert.deepStrictEqual(result, {});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('merge - returns empty object for primitives', () => {
|
|
44
|
+
const result = merge('string', { a: 1 });
|
|
45
|
+
assert.deepStrictEqual(result, {});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('merge - ignores non-object arguments in rest', () => {
|
|
49
|
+
const result = merge({ a: 1 }, 'string', { b: 2 }, null, { c: 3 });
|
|
50
|
+
assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
|
|
51
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove specified HTML element from provided HTML
|
|
3
|
+
*
|
|
4
|
+
* @param {string} html - The HTML content to process
|
|
5
|
+
* @param {string} tagName - The tag name to remove
|
|
6
|
+
* @returns {string} The HTML with the specified tag removed
|
|
7
|
+
*/
|
|
8
|
+
export function removeTag(html, tagName) {
|
|
9
|
+
if (!html || typeof html !== 'string') {
|
|
10
|
+
return html;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof tagName !== 'string' || !tagName) {
|
|
14
|
+
return html;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Escape special regex characters in tag name
|
|
18
|
+
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
|
|
20
|
+
// Remove opening and closing tags along with their content
|
|
21
|
+
// This regex matches: <tag attributes>content</tag>
|
|
22
|
+
const regex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>.*?<\\/${escapedTag}>`, 'gis');
|
|
23
|
+
let result = html.replace(regex, '');
|
|
24
|
+
|
|
25
|
+
// Also remove self-closing tags: <tag />
|
|
26
|
+
const selfClosingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?\\s*\\/?>`, 'gi');
|
|
27
|
+
result = result.replace(selfClosingRegex, '');
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* remove_tag filter - Remove specified HTML element from provided HTML
|
|
34
|
+
*
|
|
35
|
+
* Usage in templates:
|
|
36
|
+
* {{ htmlContent | remove_tag('script') }}
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
39
|
+
*/
|
|
40
|
+
export function removeTagFilter(eleventyConfig) {
|
|
41
|
+
eleventyConfig.addFilter("remove_tag", removeTag);
|
|
42
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { removeTag } from './remove_tag.js';
|
|
4
|
+
|
|
5
|
+
describe('removeTag', () => {
|
|
6
|
+
it('should remove a single tag with content', () => {
|
|
7
|
+
const html = '<div>Keep this</div><script>Remove this</script><p>Keep this too</p>';
|
|
8
|
+
const result = removeTag(html, 'script');
|
|
9
|
+
|
|
10
|
+
assert.strictEqual(result, '<div>Keep this</div><p>Keep this too</p>');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should remove multiple instances of the same tag', () => {
|
|
14
|
+
const html = '<p>First</p><script>One</script><p>Second</p><script>Two</script><p>Third</p>';
|
|
15
|
+
const result = removeTag(html, 'script');
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(result, '<p>First</p><p>Second</p><p>Third</p>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should handle tags with attributes', () => {
|
|
21
|
+
const html = '<div>Keep</div><script type="text/javascript" src="file.js">Code</script><p>Keep</p>';
|
|
22
|
+
const result = removeTag(html, 'script');
|
|
23
|
+
|
|
24
|
+
assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle self-closing tags', () => {
|
|
28
|
+
const html = '<div>Keep</div><br /><p>Keep</p>';
|
|
29
|
+
const result = removeTag(html, 'br');
|
|
30
|
+
|
|
31
|
+
assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return original HTML if tag does not exist', () => {
|
|
35
|
+
const html = '<div>Keep this</div><p>Keep this too</p>';
|
|
36
|
+
const result = removeTag(html, 'script');
|
|
37
|
+
|
|
38
|
+
assert.strictEqual(result, html);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should handle empty or null input', () => {
|
|
42
|
+
assert.strictEqual(removeTag('', 'script'), '');
|
|
43
|
+
assert.strictEqual(removeTag(null, 'script'), null);
|
|
44
|
+
assert.strictEqual(removeTag(undefined, 'script'), undefined);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should be case-insensitive', () => {
|
|
48
|
+
const html = '<div>Keep</div><SCRIPT>Remove</SCRIPT><Script>Remove</Script><p>Keep</p>';
|
|
49
|
+
const result = removeTag(html, 'script');
|
|
50
|
+
|
|
51
|
+
assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle nested content', () => {
|
|
55
|
+
const html = '<div>Keep</div><script><div>Nested</div></script><p>Keep</p>';
|
|
56
|
+
const result = removeTag(html, 'script');
|
|
57
|
+
|
|
58
|
+
assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import lodash from "@11ty/lodash-custom";
|
|
2
|
+
const { get } = lodash;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Core logic for filtering collection items by attribute value
|
|
6
|
+
*
|
|
7
|
+
* This function takes a collection, an attribute name, and a target value,
|
|
8
|
+
* and returns items where the attribute matches the target value.
|
|
9
|
+
* If the attribute is an array, it checks if the array includes the target value.
|
|
10
|
+
*
|
|
11
|
+
* Supports nested attribute names using dot notation (e.g., "data.tags").
|
|
12
|
+
*
|
|
13
|
+
* @param {Array} collection - The collection to filter
|
|
14
|
+
* @param {string} attrName - The attribute name to check (supports dot notation for nested properties)
|
|
15
|
+
* @param {*} targetValue - The value to match against
|
|
16
|
+
* @returns {Array} Filtered collection
|
|
17
|
+
*/
|
|
18
|
+
export function whereIn(collection, attrName, targetValue) {
|
|
19
|
+
if (!collection || !Array.isArray(collection)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return collection.filter((item) => {
|
|
24
|
+
// Get the attribute value from the item (supports nested paths like "data.tags")
|
|
25
|
+
const attrValue = get(item, attrName);
|
|
26
|
+
|
|
27
|
+
// If attribute doesn't exist, skip this item
|
|
28
|
+
if (attrValue === undefined || attrValue === null) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If the attribute is an array, check if it includes the target value
|
|
33
|
+
if (Array.isArray(attrValue)) {
|
|
34
|
+
return attrValue.includes(targetValue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Otherwise, do a direct comparison
|
|
38
|
+
return attrValue === targetValue;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registers the where_in filter with Eleventy
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
46
|
+
*/
|
|
47
|
+
export function whereInFilter(eleventyConfig) {
|
|
48
|
+
eleventyConfig.addFilter("where_in", whereIn);
|
|
49
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { whereIn, whereInFilter } from "./where_in.js";
|
|
4
|
+
|
|
5
|
+
describe("whereIn (core logic)", () => {
|
|
6
|
+
it("should filter items by exact attribute match", () => {
|
|
7
|
+
const collection = [
|
|
8
|
+
{ category: "blog", title: "Post 1" },
|
|
9
|
+
{ category: "news", title: "Post 2" },
|
|
10
|
+
{ category: "blog", title: "Post 3" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const result = whereIn(collection, "category", "blog");
|
|
14
|
+
assert.strictEqual(result.length, 2);
|
|
15
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
16
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should filter items when attribute is an array (includes check)", () => {
|
|
20
|
+
const collection = [
|
|
21
|
+
{ tags: ["javascript", "tutorial"], title: "Post 1" },
|
|
22
|
+
{ tags: ["python", "tutorial"], title: "Post 2" },
|
|
23
|
+
{ tags: ["javascript", "advanced"], title: "Post 3" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const result = whereIn(collection, "tags", "javascript");
|
|
27
|
+
assert.strictEqual(result.length, 2);
|
|
28
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
29
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return empty array when collection is not an array", () => {
|
|
33
|
+
const result = whereIn(null, "category", "blog");
|
|
34
|
+
assert.strictEqual(result.length, 0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should filter out items without the specified attribute", () => {
|
|
38
|
+
const collection = [
|
|
39
|
+
{ category: "blog", title: "Post 1" },
|
|
40
|
+
{ title: "Post 2" },
|
|
41
|
+
{ category: "blog", title: "Post 3" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const result = whereIn(collection, "category", "blog");
|
|
45
|
+
assert.strictEqual(result.length, 2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should work with attribute directly on item (not in data)", () => {
|
|
49
|
+
const collection = [
|
|
50
|
+
{ category: "blog", title: "Post 1" },
|
|
51
|
+
{ category: "news", title: "Post 2" },
|
|
52
|
+
{ category: "blog", title: "Post 3" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const result = whereIn(collection, "category", "blog");
|
|
56
|
+
assert.strictEqual(result.length, 2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle array that does not include target value", () => {
|
|
60
|
+
const collection = [
|
|
61
|
+
{ tags: ["python", "tutorial"], title: "Post 1" },
|
|
62
|
+
{ tags: ["ruby", "guide"], title: "Post 2" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const result = whereIn(collection, "tags", "javascript");
|
|
66
|
+
assert.strictEqual(result.length, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should handle different value types", () => {
|
|
70
|
+
const collection = [
|
|
71
|
+
{ priority: 1, title: "Post 1" },
|
|
72
|
+
{ priority: 2, title: "Post 2" },
|
|
73
|
+
{ priority: 1, title: "Post 3" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const result = whereIn(collection, "priority", 1);
|
|
77
|
+
assert.strictEqual(result.length, 2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should support nested attribute names with dot notation", () => {
|
|
81
|
+
const collection = [
|
|
82
|
+
{ data: { category: "blog" }, title: "Post 1" },
|
|
83
|
+
{ data: { category: "news" }, title: "Post 2" },
|
|
84
|
+
{ data: { category: "blog" }, title: "Post 3" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const result = whereIn(collection, "data.category", "blog");
|
|
88
|
+
assert.strictEqual(result.length, 2);
|
|
89
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
90
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should support nested arrays with dot notation", () => {
|
|
94
|
+
const collection = [
|
|
95
|
+
{ data: { tags: ["javascript", "tutorial"] }, title: "Post 1" },
|
|
96
|
+
{ data: { tags: ["python", "tutorial"] }, title: "Post 2" },
|
|
97
|
+
{ data: { tags: ["javascript", "advanced"] }, title: "Post 3" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const result = whereIn(collection, "data.tags", "javascript");
|
|
101
|
+
assert.strictEqual(result.length, 2);
|
|
102
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
103
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should handle deeply nested paths", () => {
|
|
107
|
+
const collection = [
|
|
108
|
+
{ data: { meta: { status: "published" } }, title: "Post 1" },
|
|
109
|
+
{ data: { meta: { status: "draft" } }, title: "Post 2" },
|
|
110
|
+
{ data: { meta: { status: "published" } }, title: "Post 3" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const result = whereIn(collection, "data.meta.status", "published");
|
|
114
|
+
assert.strictEqual(result.length, 2);
|
|
115
|
+
assert.strictEqual(result[0].title, "Post 1");
|
|
116
|
+
assert.strictEqual(result[1].title, "Post 3");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should return empty array when nested path does not exist", () => {
|
|
120
|
+
const collection = [
|
|
121
|
+
{ data: { category: "blog" }, title: "Post 1" },
|
|
122
|
+
{ title: "Post 2" },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const result = whereIn(collection, "data.nonexistent.path", "value");
|
|
126
|
+
assert.strictEqual(result.length, 0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("whereInFilter (Eleventy integration)", () => {
|
|
131
|
+
it("should register the filter with eleventyConfig", () => {
|
|
132
|
+
let registeredName;
|
|
133
|
+
let registeredFn;
|
|
134
|
+
|
|
135
|
+
const mockEleventyConfig = {
|
|
136
|
+
addFilter(name, fn) {
|
|
137
|
+
registeredName = name;
|
|
138
|
+
registeredFn = fn;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
whereInFilter(mockEleventyConfig);
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(registeredName, "where_in");
|
|
145
|
+
assert.strictEqual(typeof registeredFn, "function");
|
|
146
|
+
assert.strictEqual(registeredFn, whereIn);
|
|
147
|
+
});
|
|
148
|
+
});
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommonJS wrapper for 11ty Bricks Plugin
|
|
3
|
+
* Provides compatibility for projects using require()
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Dynamic import for ES modules
|
|
7
|
+
module.exports = async function eleventyBricksPlugin(eleventyConfig, options) {
|
|
8
|
+
const { default: plugin } = await import('./index.js');
|
|
9
|
+
return plugin(eleventyConfig, options);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Export individual helpers for granular usage
|
|
13
|
+
['mdAutoRawTags', 'mdAutoNl2br', 'setAttrFilter', 'whereInFilter', 'mergeFilter', 'removeTagFilter', 'ifFilter', 'attrConcatFilter', 'siteData'].forEach(name => {
|
|
14
|
+
module.exports[name] = async (eleventyConfig) => {
|
|
15
|
+
const module = await import('./index.js');
|
|
16
|
+
return module[name](eleventyConfig);
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Export transform/utility functions for advanced usage
|
|
21
|
+
['transformAutoRaw', 'transformNl2br', 'merge', 'removeTag', 'iff', 'attrConcat'].forEach(name => {
|
|
22
|
+
module.exports[name] = async (...args) => {
|
|
23
|
+
const module = await import('./index.js');
|
|
24
|
+
return module[name](...args);
|
|
25
|
+
};
|
|
26
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mdAutoRawTags,
|
|
3
|
+
mdAutoNl2br,
|
|
4
|
+
mdAutoLinkFavicons,
|
|
5
|
+
transformAutoRaw,
|
|
6
|
+
transformNl2br,
|
|
7
|
+
isPlainUrlText,
|
|
8
|
+
cleanLinkText,
|
|
9
|
+
buildFaviconLink,
|
|
10
|
+
transformLink,
|
|
11
|
+
} from "./markdown.js";
|
|
12
|
+
import { setAttrFilter } from "./filters/attr.js";
|
|
13
|
+
import { whereInFilter } from "./filters/where_in.js";
|
|
14
|
+
import { mergeFilter, merge } from "./filters/merge.js";
|
|
15
|
+
import { removeTagFilter, removeTag } from "./filters/remove_tag.js";
|
|
16
|
+
import { ifFilter, iff } from "./filters/if.js";
|
|
17
|
+
import { attrConcatFilter, attrConcat } from "./filters/attr_concat.js";
|
|
18
|
+
import { siteData } from "./siteData.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 11ty Bricks Plugin
|
|
22
|
+
*
|
|
23
|
+
* A collection of helpful utilities and filters for Eleventy (11ty).
|
|
24
|
+
* Can be used as a plugin or by importing individual helpers.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
27
|
+
* @param {Object} options - Plugin options
|
|
28
|
+
* @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
|
|
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)
|
|
31
|
+
* @param {Array<string>} options.filters - Array of filter names to enable: 'attr', 'where_in', 'merge', 'remove_tag', 'if', 'attr_concat' (default: [])
|
|
32
|
+
* @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
|
|
33
|
+
*/
|
|
34
|
+
export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
|
|
35
|
+
const plugins = {
|
|
36
|
+
mdAutoRawTags,
|
|
37
|
+
mdAutoNl2br,
|
|
38
|
+
mdAutoLinkFavicons,
|
|
39
|
+
siteData,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const filters = {
|
|
43
|
+
attr: setAttrFilter,
|
|
44
|
+
where_in: whereInFilter,
|
|
45
|
+
merge: mergeFilter,
|
|
46
|
+
remove_tag: removeTagFilter,
|
|
47
|
+
if: ifFilter,
|
|
48
|
+
attr_concat: attrConcatFilter,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Handle individual plugin options
|
|
52
|
+
Object.entries(options).forEach(([key, enabled]) => {
|
|
53
|
+
if (key !== "filters" && enabled && plugins[key]) {
|
|
54
|
+
plugins[key](eleventyConfig);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Handle filters array
|
|
59
|
+
if (Array.isArray(options.filters)) {
|
|
60
|
+
options.filters.forEach((filterName) => {
|
|
61
|
+
if (filters[filterName]) {
|
|
62
|
+
filters[filterName](eleventyConfig);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Export individual helpers for granular usage
|
|
69
|
+
export {
|
|
70
|
+
mdAutoRawTags,
|
|
71
|
+
mdAutoNl2br,
|
|
72
|
+
mdAutoLinkFavicons,
|
|
73
|
+
setAttrFilter,
|
|
74
|
+
whereInFilter,
|
|
75
|
+
mergeFilter,
|
|
76
|
+
removeTagFilter,
|
|
77
|
+
ifFilter,
|
|
78
|
+
attrConcatFilter,
|
|
79
|
+
siteData,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Export transform/utility functions for advanced usage
|
|
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
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Nunjucks syntax in content by wrapping it with raw tags
|
|
3
|
+
*
|
|
4
|
+
* This function wraps Nunjucks syntax ({{, }}, {%, %}) with {% raw %} tags
|
|
5
|
+
* to prevent them from being processed by the template engine.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} content - The content to transform
|
|
8
|
+
* @returns {string} The transformed content with Nunjucks syntax wrapped
|
|
9
|
+
*/
|
|
10
|
+
export function transformAutoRaw(content) {
|
|
11
|
+
// This regex looks for {{, }}, {%, or %} individually and wraps them
|
|
12
|
+
return content.replace(/({{|}}|{%|%})/g, "{% raw %}$1{% endraw %}");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* mdAutoRawTags - Forbid Nunjucks processing in Markdown files
|
|
17
|
+
*
|
|
18
|
+
* This preprocessor wraps Nunjucks syntax ({{, }}, {%, %}) with {% raw %} tags
|
|
19
|
+
* to prevent them from being processed by the template engine in Markdown files.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
22
|
+
*/
|
|
23
|
+
export function mdAutoRawTags(eleventyConfig) {
|
|
24
|
+
eleventyConfig.addPreprocessor("mdAutoRawTags", "md", (data, content) => {
|
|
25
|
+
return transformAutoRaw(content);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Transform \n sequences to <br> tags
|
|
31
|
+
*
|
|
32
|
+
* This function converts literal \n sequences (double backslash + n) to HTML <br> tags.
|
|
33
|
+
* It handles both double \n\n and single \n sequences, processing double ones first.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} content - The content to transform
|
|
36
|
+
* @returns {string} The transformed content with \n converted to <br>
|
|
37
|
+
*/
|
|
38
|
+
export function transformNl2br(content) {
|
|
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>");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* mdAutoNl2br - Auto convert \n to <br> in markdown (especially tables)
|
|
45
|
+
*
|
|
46
|
+
* This function amends the markdown library to automatically convert \n
|
|
47
|
+
* to <br> tags in text content, which is particularly useful for line breaks
|
|
48
|
+
* inside markdown tables where standard newlines don't work.
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
51
|
+
*/
|
|
52
|
+
export function mdAutoNl2br(eleventyConfig) {
|
|
53
|
+
eleventyConfig.amendLibrary("md", (mdLib) => {
|
|
54
|
+
mdLib.renderer.rules.text = (tokens, idx) => {
|
|
55
|
+
return transformNl2br(tokens[idx].content);
|
|
56
|
+
};
|
|
57
|
+
});
|
|
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} target="_blank"><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
|
+
* Replace all anchor links in HTML content with transformed versions
|
|
132
|
+
*
|
|
133
|
+
* This function searches for all anchor tags in HTML content and replaces them
|
|
134
|
+
* using the provided transform function. The regex captures:
|
|
135
|
+
* - Group 1: All attributes including href
|
|
136
|
+
* - Group 2: The URL from the href attribute
|
|
137
|
+
* - Group 3: The link text content
|
|
138
|
+
*
|
|
139
|
+
* @param {string} content - The HTML content to process
|
|
140
|
+
* @param {Function} transformer - Function to transform each link (receives match, attrs, url, linkText)
|
|
141
|
+
* @returns {string} The HTML content with transformed links
|
|
142
|
+
*/
|
|
143
|
+
export function replaceLinksInHtml(content, transformer) {
|
|
144
|
+
return content.replace(/<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([^<]+)<\/a>/gi, transformer);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* mdAutoLinkFavicons - Add favicon images to plain text links
|
|
149
|
+
*
|
|
150
|
+
* This transform automatically adds favicon images from Google's favicon service
|
|
151
|
+
* to links that display plain URLs or domain names. It processes all HTML output
|
|
152
|
+
* files and adds inline favicon images next to the link text.
|
|
153
|
+
*
|
|
154
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
155
|
+
*/
|
|
156
|
+
export function mdAutoLinkFavicons(eleventyConfig) {
|
|
157
|
+
eleventyConfig.addTransform("mdAutoLinkFavicons", function (content) {
|
|
158
|
+
if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
|
|
159
|
+
return replaceLinksInHtml(content, transformLink);
|
|
160
|
+
}
|
|
161
|
+
return content;
|
|
162
|
+
});
|
|
163
|
+
}
|