@anydigital/eleventy-bricks 0.23.2 → 0.24.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 CHANGED
@@ -1,3 +1,12 @@
1
1
  {
2
- "printWidth": 120
2
+ "printWidth": 120,
3
+ "plugins": ["prettier-plugin-jinja-template"],
4
+ "overrides": [
5
+ {
6
+ "files": ["*.njk", "*.html"],
7
+ "options": {
8
+ "parser": "jinja-template"
9
+ }
10
+ }
11
+ ]
3
12
  }
package/README.md CHANGED
@@ -27,7 +27,7 @@ export default function (eleventyConfig) {
27
27
  mdAutoNl2br: true,
28
28
  mdAutoLinkFavicons: true,
29
29
  siteData: true,
30
- filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
30
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat", "fetch"],
31
31
  });
32
32
 
33
33
  // Your other configuration...
@@ -45,7 +45,7 @@ module.exports = function (eleventyConfig) {
45
45
  mdAutoNl2br: true,
46
46
  mdAutoLinkFavicons: true,
47
47
  siteData: true,
48
- filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
48
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat", "fetch"],
49
49
  });
50
50
 
51
51
  // Your other configuration...
@@ -71,6 +71,7 @@ import {
71
71
  removeTagFilter,
72
72
  ifFilter,
73
73
  attrConcatFilter,
74
+ fetchFilter,
74
75
  siteData,
75
76
  } from "@anydigital/eleventy-bricks";
76
77
 
@@ -84,6 +85,7 @@ export default function (eleventyConfig) {
84
85
  removeTagFilter(eleventyConfig);
85
86
  ifFilter(eleventyConfig);
86
87
  attrConcatFilter(eleventyConfig);
88
+ fetchFilter(eleventyConfig); // Only if @11ty/eleventy-fetch is installed
87
89
  siteData(eleventyConfig);
88
90
 
89
91
  // Your other configuration...
@@ -103,6 +105,7 @@ const {
103
105
  removeTagFilter,
104
106
  ifFilter,
105
107
  attrConcatFilter,
108
+ fetchFilter,
106
109
  siteData,
107
110
  } = require("@anydigital/eleventy-bricks");
108
111
 
@@ -116,6 +119,7 @@ module.exports = async function (eleventyConfig) {
116
119
  await removeTagFilter(eleventyConfig);
117
120
  await ifFilter(eleventyConfig);
118
121
  await attrConcatFilter(eleventyConfig);
122
+ await fetchFilter(eleventyConfig); // Only if @11ty/eleventy-fetch is installed
119
123
  await siteData(eleventyConfig);
120
124
 
121
125
  // Your other configuration...
@@ -144,6 +148,7 @@ When using the plugin (Option 1), you can configure which helpers to enable:
144
148
  - `'remove_tag'` - Remove HTML elements from content
145
149
  - `'if'` - Inline conditional/ternary operator
146
150
  - `'attr_concat'` - Concatenate values to an attribute array
151
+ - `'fetch'` - Fetch remote URLs or local files (requires `@11ty/eleventy-fetch`)
147
152
 
148
153
  **Example:**
149
154
 
@@ -153,7 +158,7 @@ eleventyConfig.addPlugin(eleventyBricks, {
153
158
  mdAutoNl2br: true,
154
159
  mdAutoLinkFavicons: true,
155
160
  siteData: true,
156
- filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
161
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat", "fetch"],
157
162
  });
158
163
  ```
159
164
 
@@ -789,6 +794,139 @@ A new object with the specified attribute containing the combined unique array.
789
794
  {% endfor %}
790
795
  ```
791
796
 
797
+ ### fetch
798
+
799
+ A filter that fetches content from remote URLs or local files. For remote URLs, it uses `@11ty/eleventy-fetch` to download and cache files. For local paths, it reads files relative to the input directory.
800
+
801
+ **Why use this?**
802
+
803
+ When building static sites, you often need to include content from external sources or reuse content from local files. The `fetch` filter provides a unified way to retrieve content from both remote URLs and local files, with automatic caching for remote resources to improve build performance.
804
+
805
+ **Requirements:**
806
+
807
+ This filter requires the `@11ty/eleventy-fetch` package to be installed:
808
+
809
+ ```bash
810
+ npm install @11ty/eleventy-fetch
811
+ ```
812
+
813
+ > **Note:** If `@11ty/eleventy-fetch` is not installed, this filter will not be available. The plugin automatically detects whether the package is installed and only enables the filter if it's present.
814
+
815
+ **Usage:**
816
+
817
+ 1. Install the required dependency:
818
+
819
+ ```bash
820
+ npm install @11ty/eleventy-fetch
821
+ ```
822
+
823
+ 2. Enable the `fetch` filter in your Eleventy config:
824
+
825
+ ```javascript
826
+ import { fetchFilter } from "@anydigital/eleventy-bricks";
827
+
828
+ export default function (eleventyConfig) {
829
+ fetchFilter(eleventyConfig);
830
+ // Or use as plugin:
831
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['fetch'] });
832
+ }
833
+ ```
834
+
835
+ 3. Use the filter in your templates:
836
+
837
+ **Fetch remote URLs:**
838
+
839
+ ```njk
840
+ {# Fetch content from a remote URL #}
841
+ {% set externalContent = "https://example.com/data.json" | fetch %}
842
+ {{ externalContent }}
843
+
844
+ {# Fetch and parse JSON #}
845
+ {% set apiData = "https://api.example.com/posts" | fetch %}
846
+ {% set posts = apiData | fromJson %}
847
+ ```
848
+
849
+ **Fetch local files:**
850
+
851
+ ```njk
852
+ {# Fetch content from a local file (relative to input directory) #}
853
+ {% set localData = "_data/content.txt" | fetch %}
854
+ {{ localData }}
855
+
856
+ {# Include content from another file #}
857
+ {% set snippet = "_includes/snippets/example.md" | fetch %}
858
+ {{ snippet | markdown | safe }}
859
+ ```
860
+
861
+ **Parameters:**
862
+
863
+ - `url`: A URL (starting with `http://` or `https://`) or a local file path (relative to the input directory)
864
+
865
+ **Features:**
866
+
867
+ - **Remote URLs**: Downloads and caches content using `@11ty/eleventy-fetch`
868
+ - Caches files for 1 day by default
869
+ - Stores cached files in `[input-dir]/_downloads/` directory
870
+ - Automatically revalidates after cache expires
871
+ - **Local files**: Reads files relative to the Eleventy input directory
872
+ - No caching needed for local files
873
+ - Supports any file type that can be read as text
874
+ - **Error handling**: Throws descriptive errors if fetching fails
875
+ - **Conditional loading**: Only available when `@11ty/eleventy-fetch` is installed
876
+
877
+ **Examples:**
878
+
879
+ ```njk
880
+ {# Fetch and display remote content #}
881
+ {% set readme = "https://raw.githubusercontent.com/user/repo/main/README.md" | fetch %}
882
+ <div class="readme">
883
+ {{ readme | markdown | safe }}
884
+ </div>
885
+
886
+ {# Fetch JSON data from API #}
887
+ {% set data = "https://api.example.com/data.json" | fetch %}
888
+ {% set items = data | fromJson %}
889
+ {% for item in items %}
890
+ <p>{{ item.title }}</p>
891
+ {% endfor %}
892
+
893
+ {# Include local file content #}
894
+ {% set changelog = "CHANGELOG.md" | fetch %}
895
+ {{ changelog | markdown | safe }}
896
+
897
+ {# Fetch CSS from CDN and inline it #}
898
+ <style>
899
+ {{ "https://cdn.example.com/styles.css" | fetch }}
900
+ </style>
901
+
902
+ {# Reuse content across pages #}
903
+ {% set sharedContent = "_includes/shared/footer.html" | fetch %}
904
+ {{ sharedContent | safe }}
905
+ ```
906
+
907
+ **Cache Directory:**
908
+
909
+ Remote files are cached in the `_downloads` folder within your input directory:
910
+
911
+ ```
912
+ your-project/
913
+ ├── src/ (or your input directory)
914
+ │ ├── _downloads/ (cached remote files)
915
+ │ ├── index.njk
916
+ │ └── ...
917
+ ```
918
+
919
+ **Use Cases:**
920
+
921
+ - Fetch content from external APIs during build time
922
+ - Include README files from GitHub repositories
923
+ - Reuse content from local files across multiple pages
924
+ - Download and inline external CSS or JavaScript
925
+ - Fetch data from headless CMS or external data sources
926
+ - Include shared content snippets without using Eleventy's include syntax
927
+
928
+ **Note:** The filter returns raw text content. Use Eleventy's built-in filters like `| safe`, `| markdown`, or `| fromJson` to process the content as needed.
929
+
792
930
  ### siteData
793
931
 
794
932
  Adds global site data to your Eleventy project, providing commonly needed values that can be accessed in all templates.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anydigital/eleventy-bricks",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "description": "A collection of helpful utilities and filters for Eleventy (11ty)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -10,11 +10,8 @@
10
10
  "require": "./src/index.cjs"
11
11
  }
12
12
  },
13
- "bin": {
14
- "download-files": "src/cli/download-files.js"
15
- },
16
13
  "scripts": {
17
- "build": "npx download-files",
14
+ "build": "curl -O https://raw.githubusercontent.com/anydigital/bricks/refs/heads/main/.prettierrc.json",
18
15
  "test": "node --test src/**/*.test.js"
19
16
  },
20
17
  "keywords": [
@@ -41,9 +38,5 @@
41
38
  },
42
39
  "engines": {
43
40
  "node": ">=18.0.0"
44
- },
45
- "_downloadFiles": {
46
- "https://raw.githubusercontent.com/anydigital/bricks/refs/heads/main/.prettierrc.json": ".prettierrc.json",
47
- "https://raw.githubusercontent.com/danurbanowicz/eleventy-sveltia-cms-starter/refs/heads/master/admin/index.html": "src/admin/index.html"
48
41
  }
49
42
  }
@@ -29,7 +29,7 @@ export default function (eleventyConfig) {
29
29
  mdAutoRawTags: true,
30
30
  mdAutoLinkFavicons: true,
31
31
  siteData: true,
32
- filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat"],
32
+ filters: ["attr", "where_in", "merge", "remove_tag", "if", "attr_concat", "fetch"],
33
33
  });
34
34
 
35
35
  /* Libraries */
@@ -15,16 +15,14 @@ export function attrConcat(obj, attr, values) {
15
15
 
16
16
  // Check if existing value is an array, convert if not
17
17
  if (!Array.isArray(existingArray)) {
18
- console.error(
19
- `attrConcat: Expected ${attr} to be an array, got ${typeof existingArray}`
20
- );
18
+ console.error(`attrConcat: Expected ${attr} to be an array, got ${typeof existingArray}`);
21
19
  }
22
20
 
23
21
  // Process the values argument
24
22
  let valuesToAdd = [];
25
23
  if (Array.isArray(values)) {
26
24
  valuesToAdd = values;
27
- } else if (typeof values === "string") {
25
+ } else if (typeof values === "string" && values.length >= 2 && values.at(0) == "[" && values.at(-1) == "]") {
28
26
  // Try to parse as JSON array
29
27
  try {
30
28
  const parsed = JSON.parse(values);
@@ -0,0 +1,46 @@
1
+ import EleventyFetch from "@11ty/eleventy-fetch";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+
5
+ /**
6
+ * fetch filter - Fetch a URL or local file and return its raw content
7
+ *
8
+ * This filter takes a URL or local file path. For URLs, it downloads them
9
+ * using eleventy-fetch to the input directory's _downloads folder.
10
+ * For local paths, it reads them relative to the input directory.
11
+ *
12
+ * @param {Object} eleventyConfig - The Eleventy configuration object
13
+ */
14
+ export function fetchFilter(eleventyConfig) {
15
+ eleventyConfig.addFilter("fetch", async function (url) {
16
+ if (!url) {
17
+ throw new Error("fetch filter requires a URL or path");
18
+ }
19
+
20
+ // Get the input directory from Eleventy config
21
+ const inputDir = eleventyConfig.dir?.input || ".";
22
+
23
+ // Check if it's a URL or local path
24
+ const isUrl = url.startsWith("http://") || url.startsWith("https://");
25
+
26
+ try {
27
+ if (isUrl) {
28
+ // Handle remote URLs with eleventy-fetch
29
+ const cacheDirectory = path.join(inputDir, "_downloads");
30
+ const content = await EleventyFetch(url, {
31
+ duration: "1d", // Cache for 1 day by default
32
+ type: "text", // Return as text
33
+ directory: cacheDirectory,
34
+ });
35
+ return content;
36
+ } else {
37
+ // Handle local file paths relative to input directory
38
+ const filePath = path.join(inputDir, url);
39
+ const content = await fs.readFile(filePath, "utf-8");
40
+ return content;
41
+ }
42
+ } catch (error) {
43
+ throw new Error(`Failed to fetch ${url}: ${error.message}`);
44
+ }
45
+ });
46
+ }
@@ -16,26 +16,22 @@ const { get } = lodash;
16
16
  * @returns {Array} Filtered collection
17
17
  */
18
18
  export function whereIn(collection, attrName, targetValue) {
19
- if (!collection || !Array.isArray(collection)) {
20
- return [];
19
+ // If no targetValue, return original collection
20
+ if (!targetValue) {
21
+ return collection;
21
22
  }
22
23
 
23
24
  return collection.filter((item) => {
24
25
  // Get the attribute value from the item (supports nested paths like "data.tags")
25
26
  const attrValue = get(item, attrName);
26
27
 
27
- // If attribute doesn't exist, skip this item
28
- if (attrValue === undefined || attrValue === null) {
29
- return false;
30
- }
31
-
32
28
  // If the attribute is an array, check if it includes the target value
33
29
  if (Array.isArray(attrValue)) {
34
30
  return attrValue.includes(targetValue);
35
31
  }
36
32
 
37
- // Otherwise, do a direct comparison
38
- return attrValue === targetValue;
33
+ // Otherwise skip this item
34
+ return false;
39
35
  });
40
36
  }
41
37
 
package/src/index.js CHANGED
@@ -17,6 +17,16 @@ import { ifFilter, iff } from "./filters/if.js";
17
17
  import { attrConcatFilter, attrConcat } from "./filters/attr_concat.js";
18
18
  import { siteData } from "./siteData.js";
19
19
 
20
+ // Conditionally import fetchFilter only if @11ty/eleventy-fetch is available
21
+ let fetchFilter = null;
22
+ try {
23
+ await import("@11ty/eleventy-fetch");
24
+ const fetchModule = await import("./filters/fetch.js");
25
+ fetchFilter = fetchModule.fetchFilter;
26
+ } catch (error) {
27
+ // @11ty/eleventy-fetch not available, fetch filter will be disabled
28
+ }
29
+
20
30
  /**
21
31
  * 11ty Bricks Plugin
22
32
  *
@@ -28,7 +38,7 @@ import { siteData } from "./siteData.js";
28
38
  * @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
29
39
  * @param {boolean} options.mdAutoNl2br - Enable mdAutoNl2br for \n to <br> conversion (default: false)
30
40
  * @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: [])
41
+ * @param {Array<string>} options.filters - Array of filter names to enable: 'attr', 'where_in', 'merge', 'remove_tag', 'if', 'attr_concat', 'fetch' (default: [])
32
42
  * @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
33
43
  */
34
44
  export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
@@ -46,6 +56,7 @@ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
46
56
  remove_tag: removeTagFilter,
47
57
  if: ifFilter,
48
58
  attr_concat: attrConcatFilter,
59
+ ...(fetchFilter && { fetch: fetchFilter }),
49
60
  };
50
61
 
51
62
  // Handle individual plugin options
@@ -66,6 +77,7 @@ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
66
77
  }
67
78
 
68
79
  // Export individual helpers for granular usage
80
+ // Note: fetchFilter will be null/undefined if @11ty/eleventy-fetch is not installed
69
81
  export {
70
82
  mdAutoRawTags,
71
83
  mdAutoNl2br,
@@ -76,6 +88,7 @@ export {
76
88
  removeTagFilter,
77
89
  ifFilter,
78
90
  attrConcatFilter,
91
+ fetchFilter,
79
92
  siteData,
80
93
  };
81
94
 
package/src/markdown.js CHANGED
@@ -76,11 +76,12 @@ export function isPlainUrlText(linkText, domain) {
76
76
  * @returns {string} The cleaned text
77
77
  */
78
78
  export function cleanLinkText(linkText, domain) {
79
- return linkText
79
+ const cleanedText = linkText
80
80
  .trim()
81
81
  .replace(/^https?:\/\//, "")
82
- .replace(domain, "")
83
82
  .replace(/\/$/, "");
83
+ const withoutDomain = cleanedText.replace(domain, "");
84
+ return withoutDomain.length > 2 ? withoutDomain : cleanedText;
84
85
  }
85
86
 
86
87
  /**
@@ -128,15 +129,10 @@ export function transformLink(match, attrs, url, linkText) {
128
129
 
129
130
  // Only add favicon if link text looks like a plain URL/domain
130
131
  if (isPlainUrlText(linkText, domain)) {
131
- // Remove domain from link text
132
132
  const cleanedText = cleanLinkText(linkText, domain);
133
-
134
- // Only apply if there are at least 2 letters remaining after domain
135
- if (cleanedText.length > 2) {
136
- return buildFaviconLink(attrs, domain, cleanedText);
137
- }
133
+ return buildFaviconLink(attrs, domain, cleanedText);
138
134
  }
139
- return match;
135
+ return match; // @TODO: throw?
140
136
  } catch (e) {
141
137
  // If URL parsing fails, return original match
142
138
  return match;
@@ -1,136 +0,0 @@
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
-