@anydigital/11ty-bricks 1.0.0-alpha → 1.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +613 -16
- package/package.json +8 -1
- package/src/bricks.js +125 -0
- package/src/byAttrFilter.js +35 -0
- package/src/byAttrFilter.test.js +105 -0
- package/src/cli/download-files.js +136 -0
- package/src/fragments.js +34 -0
- package/src/index.cjs +14 -5
- package/src/index.js +18 -9
- package/src/markdown.js +58 -0
- package/src/{autoRaw.test.js → markdown.test.js} +66 -1
- package/src/setAttrFilter.js +17 -0
- package/src/siteData.js +12 -0
- package/src/starter/admin/index.html +13 -0
- package/src/starter/eleventy.config.js +67 -0
- package/src/starter/tailwind.config.js +7 -0
- package/src/autoRaw.js +0 -28
package/src/bricks.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export function bricks(eleventyConfig) {
|
|
2
|
+
|
|
3
|
+
// Brick Registry System
|
|
4
|
+
// Global registry to track dependencies per page
|
|
5
|
+
const brickRegistry = new Map();
|
|
6
|
+
|
|
7
|
+
// Helper to get or create page registry
|
|
8
|
+
function getPageRegistry(page) {
|
|
9
|
+
const pageUrl = page.url || page.outputPath || 'default';
|
|
10
|
+
if (!brickRegistry.has(pageUrl)) {
|
|
11
|
+
brickRegistry.set(pageUrl, {
|
|
12
|
+
dependencies: new Set(), // Raw dependencies (URLs) - categorized later
|
|
13
|
+
inlineStyles: new Set(),
|
|
14
|
+
inlineScripts: new Set()
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return brickRegistry.get(pageUrl);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Clear registry before each build
|
|
21
|
+
eleventyConfig.on("eleventy.before", async () => {
|
|
22
|
+
brickRegistry.clear();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// brick shortcode: registers and renders a brick component
|
|
26
|
+
eleventyConfig.addShortcode("brick", function(brickModule, ...args) {
|
|
27
|
+
const registry = getPageRegistry(this.page);
|
|
28
|
+
|
|
29
|
+
if (!brickModule) return '';
|
|
30
|
+
|
|
31
|
+
// Register external dependencies (categorized later in transform)
|
|
32
|
+
if (brickModule.dependencies) {
|
|
33
|
+
brickModule.dependencies.forEach(dep => {
|
|
34
|
+
registry.dependencies.add(dep);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Register inline styles directly from style variable
|
|
39
|
+
if (brickModule.style && brickModule.style.trim()) {
|
|
40
|
+
registry.inlineStyles.add(brickModule.style);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Register inline scripts directly from script variable
|
|
44
|
+
if (brickModule.script && brickModule.script.trim()) {
|
|
45
|
+
registry.inlineScripts.add(brickModule.script);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Render the brick using render() macro
|
|
49
|
+
if (brickModule.render && typeof brickModule.render === 'function') {
|
|
50
|
+
return brickModule.render(...args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return '';
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// bricksRegistry shortcode: outputs placeholder and base dependencies
|
|
57
|
+
eleventyConfig.addShortcode("bricksDependencies", function(dependencies = []) {
|
|
58
|
+
const registry = getPageRegistry(this.page);
|
|
59
|
+
|
|
60
|
+
// Register root dependencies if provided (categorized later in transform)
|
|
61
|
+
if (dependencies && Array.isArray(dependencies)) {
|
|
62
|
+
dependencies.forEach(dep => {
|
|
63
|
+
registry.dependencies.add(dep);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Return placeholder comment that will be replaced by transform
|
|
68
|
+
return '<!-- BRICK_DEPENDENCIES_PLACEHOLDER -->';
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Transform to inject collected dependencies
|
|
72
|
+
eleventyConfig.addTransform("injectBrickDependencies", function(content, outputPath) {
|
|
73
|
+
if (!outputPath || !outputPath.endsWith(".html")) {
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pageUrl = this.page?.url || this.page?.outputPath || outputPath;
|
|
78
|
+
const registry = brickRegistry.get(pageUrl);
|
|
79
|
+
|
|
80
|
+
if (!registry || !content.includes('<!-- BRICK_DEPENDENCIES_PLACEHOLDER -->')) {
|
|
81
|
+
return content;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Categorize dependencies by type
|
|
85
|
+
const externalStyles = [];
|
|
86
|
+
const externalScripts = [];
|
|
87
|
+
|
|
88
|
+
registry.dependencies.forEach(dep => {
|
|
89
|
+
// Categorize by type
|
|
90
|
+
if (dep.endsWith('.css') || dep.includes('.css?')) {
|
|
91
|
+
externalStyles.push(dep);
|
|
92
|
+
} else if (dep.endsWith('.js') || dep.includes('.js?')) {
|
|
93
|
+
externalScripts.push(dep);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Build HTML for dependencies
|
|
98
|
+
let dependenciesHtml = '\n';
|
|
99
|
+
|
|
100
|
+
// Add external CSS links
|
|
101
|
+
externalStyles.forEach(href => {
|
|
102
|
+
dependenciesHtml += ` <link rel="stylesheet" href="${href}">\n`;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Add inline styles
|
|
106
|
+
registry.inlineStyles.forEach(style => {
|
|
107
|
+
dependenciesHtml += ` <style>${style}</style>\n`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Add external script links
|
|
111
|
+
externalScripts.forEach(src => {
|
|
112
|
+
dependenciesHtml += ` <script src="${src}"></script>\n`;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Add inline scripts
|
|
116
|
+
registry.inlineScripts.forEach(script => {
|
|
117
|
+
dependenciesHtml += ` <script>${script}</script>\n`;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
dependenciesHtml += ' ';
|
|
121
|
+
|
|
122
|
+
// Replace placeholder with actual dependencies
|
|
123
|
+
return content.replace('<!-- BRICK_DEPENDENCIES_PLACEHOLDER -->', dependenciesHtml);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* byAttr filter - Filter collection items by attribute value
|
|
3
|
+
*
|
|
4
|
+
* This filter takes a collection, an attribute name, and a target value,
|
|
5
|
+
* and returns items where the attribute matches the target value.
|
|
6
|
+
* If the attribute is an array, it checks if the array includes the target value.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
9
|
+
*/
|
|
10
|
+
export function byAttrFilter(eleventyConfig) {
|
|
11
|
+
eleventyConfig.addFilter("byAttr", function(collection, attrName, targetValue) {
|
|
12
|
+
if (!collection || !Array.isArray(collection)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return collection.filter(item => {
|
|
17
|
+
// Get the attribute value from the item's data
|
|
18
|
+
const attrValue = item?.data?.[attrName] ?? item?.[attrName];
|
|
19
|
+
|
|
20
|
+
// If attribute doesn't exist, skip this item
|
|
21
|
+
if (attrValue === undefined || attrValue === null) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If the attribute is an array, check if it includes the target value
|
|
26
|
+
if (Array.isArray(attrValue)) {
|
|
27
|
+
return attrValue.includes(targetValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Otherwise, do a direct comparison
|
|
31
|
+
return attrValue === targetValue;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { byAttrFilter } from './byAttrFilter.js';
|
|
4
|
+
|
|
5
|
+
describe('byAttr filter', () => {
|
|
6
|
+
let filterFn;
|
|
7
|
+
|
|
8
|
+
// Mock eleventyConfig to capture the filter function
|
|
9
|
+
const mockEleventyConfig = {
|
|
10
|
+
addFilter(name, fn) {
|
|
11
|
+
if (name === 'byAttr') {
|
|
12
|
+
filterFn = fn;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Register the filter
|
|
18
|
+
byAttrFilter(mockEleventyConfig);
|
|
19
|
+
|
|
20
|
+
it('should filter items by exact attribute match', () => {
|
|
21
|
+
const collection = [
|
|
22
|
+
{ data: { category: 'blog' }, title: 'Post 1' },
|
|
23
|
+
{ data: { category: 'news' }, title: 'Post 2' },
|
|
24
|
+
{ data: { category: 'blog' }, title: 'Post 3' }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const result = filterFn(collection, 'category', 'blog');
|
|
28
|
+
assert.strictEqual(result.length, 2);
|
|
29
|
+
assert.strictEqual(result[0].title, 'Post 1');
|
|
30
|
+
assert.strictEqual(result[1].title, 'Post 3');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should filter items when attribute is an array (includes check)', () => {
|
|
34
|
+
const collection = [
|
|
35
|
+
{ data: { tags: ['javascript', 'tutorial'] }, title: 'Post 1' },
|
|
36
|
+
{ data: { tags: ['python', 'tutorial'] }, title: 'Post 2' },
|
|
37
|
+
{ data: { tags: ['javascript', 'advanced'] }, title: 'Post 3' }
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const result = filterFn(collection, 'tags', 'javascript');
|
|
41
|
+
assert.strictEqual(result.length, 2);
|
|
42
|
+
assert.strictEqual(result[0].title, 'Post 1');
|
|
43
|
+
assert.strictEqual(result[1].title, 'Post 3');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return empty array when collection is not an array', () => {
|
|
47
|
+
const result = filterFn(null, 'category', 'blog');
|
|
48
|
+
assert.strictEqual(result.length, 0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should filter out items without the specified attribute', () => {
|
|
52
|
+
const collection = [
|
|
53
|
+
{ data: { category: 'blog' }, title: 'Post 1' },
|
|
54
|
+
{ data: {}, title: 'Post 2' },
|
|
55
|
+
{ data: { category: 'blog' }, title: 'Post 3' }
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const result = filterFn(collection, 'category', 'blog');
|
|
59
|
+
assert.strictEqual(result.length, 2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should work with attribute directly on item (not in data)', () => {
|
|
63
|
+
const collection = [
|
|
64
|
+
{ category: 'blog', title: 'Post 1' },
|
|
65
|
+
{ category: 'news', title: 'Post 2' },
|
|
66
|
+
{ category: 'blog', title: 'Post 3' }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const result = filterFn(collection, 'category', 'blog');
|
|
70
|
+
assert.strictEqual(result.length, 2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle mixed data structures', () => {
|
|
74
|
+
const collection = [
|
|
75
|
+
{ data: { category: 'blog' }, title: 'Post 1' },
|
|
76
|
+
{ category: 'blog', title: 'Post 2' },
|
|
77
|
+
{ data: { category: 'news' }, title: 'Post 3' }
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const result = filterFn(collection, 'category', 'blog');
|
|
81
|
+
assert.strictEqual(result.length, 2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle array that does not include target value', () => {
|
|
85
|
+
const collection = [
|
|
86
|
+
{ data: { tags: ['python', 'tutorial'] }, title: 'Post 1' },
|
|
87
|
+
{ data: { tags: ['ruby', 'guide'] }, title: 'Post 2' }
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const result = filterFn(collection, 'tags', 'javascript');
|
|
91
|
+
assert.strictEqual(result.length, 0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle different value types', () => {
|
|
95
|
+
const collection = [
|
|
96
|
+
{ data: { priority: 1 }, title: 'Post 1' },
|
|
97
|
+
{ data: { priority: 2 }, title: 'Post 2' },
|
|
98
|
+
{ data: { priority: 1 }, title: 'Post 3' }
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const result = filterFn(collection, 'priority', 1);
|
|
102
|
+
assert.strictEqual(result.length, 2);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
|
+
import { dirname, resolve, join } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse command line arguments
|
|
8
|
+
*
|
|
9
|
+
* @returns {Object} Parsed arguments
|
|
10
|
+
*/
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const parsed = {
|
|
14
|
+
outputDir: null
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
|
|
20
|
+
if (arg === '--output' || arg === '-o') {
|
|
21
|
+
if (i + 1 < args.length) {
|
|
22
|
+
parsed.outputDir = args[i + 1];
|
|
23
|
+
i++; // Skip next argument
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error(`${arg} requires a directory path`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Downloads files specified in package.json's _downloadFiles field
|
|
35
|
+
*
|
|
36
|
+
* @param {string|null} outputDir - Optional output directory to prepend to all paths
|
|
37
|
+
* @returns {Promise<boolean>} True if all downloads succeeded, false if any failed
|
|
38
|
+
*/
|
|
39
|
+
async function download(outputDir = null) {
|
|
40
|
+
try {
|
|
41
|
+
// Find and read package.json from the current working directory
|
|
42
|
+
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
43
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
|
44
|
+
|
|
45
|
+
const downloadFiles = packageJson._downloadFiles;
|
|
46
|
+
|
|
47
|
+
if (!downloadFiles || typeof downloadFiles !== 'object') {
|
|
48
|
+
console.log('No _downloadFiles field found in package.json');
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entries = Object.entries(downloadFiles);
|
|
53
|
+
|
|
54
|
+
if (entries.length === 0) {
|
|
55
|
+
console.log('No files to download (_downloadFiles is empty)');
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`Starting download of ${entries.length} file(s)...\n`);
|
|
60
|
+
|
|
61
|
+
let hasErrors = false;
|
|
62
|
+
|
|
63
|
+
// Process all downloads
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const url = entry[0];
|
|
66
|
+
let localPath = entry[1];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
console.log(`Downloading: ${url}`);
|
|
70
|
+
console.log(` To: ${localPath}`);
|
|
71
|
+
|
|
72
|
+
// Download the file
|
|
73
|
+
const response = await fetch(url);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get the file content
|
|
80
|
+
const content = await response.arrayBuffer();
|
|
81
|
+
const buffer = Buffer.from(content);
|
|
82
|
+
|
|
83
|
+
// Prepend output directory to local path if specified
|
|
84
|
+
if (outputDir) {
|
|
85
|
+
localPath = join(outputDir, localPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Resolve the full path
|
|
89
|
+
const fullPath = resolve(process.cwd(), localPath);
|
|
90
|
+
|
|
91
|
+
// Create directory if it doesn't exist
|
|
92
|
+
const dir = dirname(fullPath);
|
|
93
|
+
await mkdir(dir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
// Write the file
|
|
96
|
+
await writeFile(fullPath, buffer);
|
|
97
|
+
|
|
98
|
+
console.log(` Success: ${localPath}\n`);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
hasErrors = true;
|
|
101
|
+
console.error(` Error: ${error.message}`);
|
|
102
|
+
console.error(` URL: ${url}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Summary
|
|
107
|
+
if (hasErrors) {
|
|
108
|
+
console.log('Download completed with errors');
|
|
109
|
+
return false;
|
|
110
|
+
} else {
|
|
111
|
+
console.log('All downloads completed successfully');
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`Fatal error: ${error.message}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* CLI entry point
|
|
123
|
+
*/
|
|
124
|
+
async function main() {
|
|
125
|
+
try {
|
|
126
|
+
const args = parseArgs();
|
|
127
|
+
const success = await download(args.outputDir);
|
|
128
|
+
process.exit(success ? 0 : 1);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`Error: ${error.message}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main();
|
|
136
|
+
|
package/src/fragments.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* fragment shortcode - Include content from fragments
|
|
6
|
+
*
|
|
7
|
+
* This shortcode reads a file from the _fragments directory and includes
|
|
8
|
+
* its content. The content will be processed by the template engine.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
11
|
+
*/
|
|
12
|
+
export function fragments(eleventyConfig) {
|
|
13
|
+
eleventyConfig.addShortcode("fragment", function(path) {
|
|
14
|
+
// Get the input directory from Eleventy's context
|
|
15
|
+
const inputDir = this.page?.inputPath
|
|
16
|
+
? join(process.cwd(), eleventyConfig.dir?.input || ".")
|
|
17
|
+
: process.cwd();
|
|
18
|
+
|
|
19
|
+
// Construct the full path to the fragment file
|
|
20
|
+
const fragmentPath = join(inputDir, "_fragments", path);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Read the fragment file
|
|
24
|
+
const content = readFileSync(fragmentPath, "utf8");
|
|
25
|
+
|
|
26
|
+
// Return content to be processed by the template engine
|
|
27
|
+
return content;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`Error reading fragment at ${fragmentPath}:`, error.message);
|
|
30
|
+
return `<!-- Fragment not found: ${path} -->`;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
package/src/index.cjs
CHANGED
|
@@ -9,9 +9,18 @@ module.exports = async function eleventyBricksPlugin(eleventyConfig, options) {
|
|
|
9
9
|
return plugin(eleventyConfig, options);
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// Export individual helpers
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
// Export individual helpers for granular usage
|
|
13
|
+
['bricks', 'mdAutoRawTags', 'mdAutoNl2br', 'fragments', 'setAttrFilter', 'byAttrFilter', 'siteData'].forEach(name => {
|
|
14
|
+
module.exports[name] = async (eleventyConfig) => {
|
|
15
|
+
const module = await import('./index.js');
|
|
16
|
+
return module[name](eleventyConfig);
|
|
17
|
+
};
|
|
18
|
+
});
|
|
17
19
|
|
|
20
|
+
// Export transform functions for advanced usage
|
|
21
|
+
['transformAutoRaw', 'transformNl2br'].forEach(name => {
|
|
22
|
+
module.exports[name] = async (content) => {
|
|
23
|
+
const module = await import('./index.js');
|
|
24
|
+
return module[name](content);
|
|
25
|
+
};
|
|
26
|
+
});
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bricks } from "./bricks.js";
|
|
2
|
+
import { mdAutoRawTags, mdAutoNl2br, transformAutoRaw, transformNl2br } from "./markdown.js";
|
|
3
|
+
import { fragments } from "./fragments.js";
|
|
4
|
+
import { setAttrFilter } from "./setAttrFilter.js";
|
|
5
|
+
import { byAttrFilter } from "./byAttrFilter.js";
|
|
6
|
+
import { siteData } from "./siteData.js";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* 11ty Bricks Plugin
|
|
@@ -8,17 +13,21 @@ import { autoRaw } from "./autoRaw.js";
|
|
|
8
13
|
*
|
|
9
14
|
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
10
15
|
* @param {Object} options - Plugin options
|
|
11
|
-
* @param {boolean} options.
|
|
16
|
+
* @param {boolean} options.bricks - Enable bricks system with dependencies injection (default: false)
|
|
17
|
+
* @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
|
|
18
|
+
* @param {boolean} options.mdAutoNl2br - Enable mdAutoNl2br for \n to <br> conversion (default: false)
|
|
19
|
+
* @param {boolean} options.fragments - Enable fragment shortcode (default: false)
|
|
20
|
+
* @param {boolean} options.setAttrFilter - Enable setAttr filter (default: false)
|
|
21
|
+
* @param {boolean} options.byAttrFilter - Enable byAttr filter (default: false)
|
|
22
|
+
* @param {boolean} options.siteData - Enable site.year and site.isProd global data (default: false)
|
|
12
23
|
*/
|
|
13
24
|
export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
// Register helpers based on options
|
|
17
|
-
if (enableAutoRaw) {
|
|
18
|
-
autoRaw(eleventyConfig);
|
|
19
|
-
}
|
|
25
|
+
const plugins = { bricks, mdAutoRawTags, mdAutoNl2br, fragments, setAttrFilter, byAttrFilter, siteData };
|
|
26
|
+
Object.entries(options).forEach(([key, enabled]) => enabled && plugins[key]?.(eleventyConfig));
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
// Export individual helpers for granular usage
|
|
23
|
-
export {
|
|
30
|
+
export { bricks, mdAutoRawTags, mdAutoNl2br, fragments, setAttrFilter, byAttrFilter, siteData };
|
|
24
31
|
|
|
32
|
+
// Export transform functions for advanced usage
|
|
33
|
+
export { transformAutoRaw, transformNl2br };
|
package/src/markdown.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { transformAutoRaw } from "./
|
|
3
|
+
import { transformAutoRaw, transformNl2br } from "./markdown.js";
|
|
4
4
|
|
|
5
5
|
describe("transformAutoRaw", () => {
|
|
6
6
|
it("should wrap opening double curly braces with raw tags", () => {
|
|
@@ -85,3 +85,68 @@ Some text
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
describe("transformNl2br", () => {
|
|
89
|
+
it("should convert single \\n to <br>", () => {
|
|
90
|
+
const input = "Line 1\\nLine 2";
|
|
91
|
+
const expected = "Line 1<br>Line 2";
|
|
92
|
+
assert.equal(transformNl2br(input), expected);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should convert double \\n\\n to <br>", () => {
|
|
96
|
+
const input = "Line 1\\n\\nLine 2";
|
|
97
|
+
const expected = "Line 1<br>Line 2";
|
|
98
|
+
assert.equal(transformNl2br(input), expected);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should convert multiple \\n sequences", () => {
|
|
102
|
+
const input = "Line 1\\nLine 2\\nLine 3";
|
|
103
|
+
const expected = "Line 1<br>Line 2<br>Line 3";
|
|
104
|
+
assert.equal(transformNl2br(input), expected);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle mixed single and double \\n", () => {
|
|
108
|
+
const input = "Line 1\\n\\nLine 2\\nLine 3";
|
|
109
|
+
const expected = "Line 1<br>Line 2<br>Line 3";
|
|
110
|
+
assert.equal(transformNl2br(input), expected);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should handle text without \\n", () => {
|
|
114
|
+
const input = "Just plain text";
|
|
115
|
+
assert.equal(transformNl2br(input), input);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle empty content", () => {
|
|
119
|
+
assert.equal(transformNl2br(""), "");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should handle content with only \\n", () => {
|
|
123
|
+
const input = "\\n\\n\\n";
|
|
124
|
+
const expected = "<br><br>";
|
|
125
|
+
assert.equal(transformNl2br(input), expected);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should handle markdown table cell content with \\n", () => {
|
|
129
|
+
const input = "Cell 1\\nCell 1 Line 2\\n\\nCell 1 Line 3";
|
|
130
|
+
const expected = "Cell 1<br>Cell 1 Line 2<br>Cell 1 Line 3";
|
|
131
|
+
assert.equal(transformNl2br(input), expected);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should handle multiple consecutive double \\n\\n", () => {
|
|
135
|
+
const input = "Line 1\\n\\n\\n\\nLine 2";
|
|
136
|
+
const expected = "Line 1<br><br>Line 2";
|
|
137
|
+
assert.equal(transformNl2br(input), expected);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should preserve actual newlines (not literal \\n)", () => {
|
|
141
|
+
const input = "Line 1\nLine 2";
|
|
142
|
+
const expected = "Line 1\nLine 2";
|
|
143
|
+
assert.equal(transformNl2br(input), expected);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should only convert literal backslash-n sequences", () => {
|
|
147
|
+
const input = "Text with\\nbackslash-n and\nreal newline";
|
|
148
|
+
const expected = "Text with<br>backslash-n and\nreal newline";
|
|
149
|
+
assert.equal(transformNl2br(input), expected);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setAttr filter - Override an attribute and return the object
|
|
3
|
+
*
|
|
4
|
+
* This filter takes an object, a key, and a value, and returns a new object
|
|
5
|
+
* with the specified attribute set to the given value.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
8
|
+
*/
|
|
9
|
+
export function setAttrFilter(eleventyConfig) {
|
|
10
|
+
eleventyConfig.addFilter("setAttr", function(obj, key, value) {
|
|
11
|
+
return {
|
|
12
|
+
...obj,
|
|
13
|
+
[key]: value
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
package/src/siteData.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add site.year and site.isProd global data
|
|
3
|
+
* - site.isProd: Boolean indicating if running in production mode (build) vs development (serve)
|
|
4
|
+
* - site.year: Sets the current year to be available in all templates as {{ site.year }}
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} eleventyConfig - The Eleventy configuration object
|
|
7
|
+
*/
|
|
8
|
+
export function siteData(eleventyConfig) {
|
|
9
|
+
eleventyConfig.addGlobalData("site.isProd", () => process.env.ELEVENTY_RUN_MODE === "build");
|
|
10
|
+
eleventyConfig.addGlobalData("site.year", () => new Date().getFullYear());
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="dns-prefetch" href="https://unpkg.com">
|
|
7
|
+
<title>Sveltia CMS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!-- Include the script that builds the page and powers Sveltia CMS -->
|
|
11
|
+
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js" type="module"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|