@anydigital/11ty-bricks 1.0.0-alpha → 1.0.0-alpha.10

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 CHANGED
@@ -21,8 +21,8 @@ Import and use the entire plugin. You can configure which helpers to enable usin
21
21
  import eleventyBricks from "@anydigital/11ty-bricks";
22
22
 
23
23
  export default function(eleventyConfig) {
24
- eleventyBricks(eleventyConfig, {
25
- autoRaw: true // Enable autoRaw preprocessor (default: false)
24
+ eleventyConfig.addPlugin(eleventyBricks, {
25
+ mdAutoRawTags: true // Enable mdAutoRawTags preprocessor (default: false)
26
26
  });
27
27
 
28
28
  // Your other configuration...
@@ -34,8 +34,8 @@ export default function(eleventyConfig) {
34
34
  const eleventyBricks = require("@anydigital/11ty-bricks");
35
35
 
36
36
  module.exports = function(eleventyConfig) {
37
- eleventyBricks(eleventyConfig, {
38
- autoRaw: true // Enable autoRaw preprocessor (default: false)
37
+ eleventyConfig.addPlugin(eleventyBricks, {
38
+ mdAutoRawTags: true // Enable mdAutoRawTags preprocessor (default: false)
39
39
  });
40
40
 
41
41
  // Your other configuration...
@@ -48,10 +48,16 @@ Import only the specific helpers you need without using the plugin:
48
48
 
49
49
  **ES Modules:**
50
50
  ```javascript
51
- import { autoRaw } from "@anydigital/11ty-bricks";
51
+ import { bricks, mdAutoRawTags, mdAutoNl2br, fragments, setAttrFilter, byAttrFilter, siteData } from "@anydigital/11ty-bricks";
52
52
 
53
53
  export default function(eleventyConfig) {
54
- autoRaw(eleventyConfig);
54
+ bricks(eleventyConfig);
55
+ mdAutoRawTags(eleventyConfig);
56
+ mdAutoNl2br(eleventyConfig);
57
+ fragments(eleventyConfig);
58
+ setAttrFilter(eleventyConfig);
59
+ byAttrFilter(eleventyConfig);
60
+ siteData(eleventyConfig);
55
61
 
56
62
  // Your other configuration...
57
63
  }
@@ -59,10 +65,16 @@ export default function(eleventyConfig) {
59
65
 
60
66
  **CommonJS:**
61
67
  ```javascript
62
- const { autoRaw } = require("@anydigital/11ty-bricks");
68
+ const { bricks, mdAutoRawTags, mdAutoNl2br, fragments, setAttrFilter, byAttrFilter, siteData } = require("@anydigital/11ty-bricks");
63
69
 
64
70
  module.exports = function(eleventyConfig) {
65
- autoRaw(eleventyConfig);
71
+ bricks(eleventyConfig);
72
+ mdAutoRawTags(eleventyConfig);
73
+ mdAutoNl2br(eleventyConfig);
74
+ fragments(eleventyConfig);
75
+ setAttrFilter(eleventyConfig);
76
+ byAttrFilter(eleventyConfig);
77
+ siteData(eleventyConfig);
66
78
 
67
79
  // Your other configuration...
68
80
  };
@@ -74,33 +86,560 @@ When using the plugin (Option 1), you can configure which helpers to enable:
74
86
 
75
87
  | Option | Type | Default | Description |
76
88
  |--------|------|---------|-------------|
77
- | `autoRaw` | boolean | `false` | Enable the autoRaw preprocessor for Markdown files |
89
+ | `bricks` | boolean | `false` | Enable the bricks system for dependency management |
90
+ | `mdAutoRawTags` | boolean | `false` | Enable the mdAutoRawTags preprocessor for Markdown files |
91
+ | `mdAutoNl2br` | boolean | `false` | Enable the mdAutoNl2br preprocessor to convert \n to `<br>` tags |
92
+ | `fragments` | boolean | `false` | Enable the fragment shortcode for including content from fragments |
93
+ | `setAttrFilter` | boolean | `false` | Enable the setAttr filter for overriding object attributes |
94
+ | `byAttrFilter` | boolean | `false` | Enable the byAttr filter for filtering collections by attribute values |
95
+ | `siteData` | boolean | `false` | Enable site.year and site.isProd global data |
78
96
 
79
97
  **Example:**
80
98
  ```javascript
81
- eleventyBricks(eleventyConfig, {
82
- autoRaw: true
99
+ eleventyConfig.addPlugin(eleventyBricks, {
100
+ bricks: true,
101
+ mdAutoRawTags: true,
102
+ byAttrFilter: true,
103
+ siteData: true
83
104
  });
84
105
  ```
85
106
 
86
- ## Available Helpers
107
+ ## Available 11ty Helpers
87
108
 
88
- ### autoRaw
109
+ ### bricks
110
+
111
+ A dependency management system for Eleventy that automatically collects and injects CSS and JavaScript dependencies (both external and inline) per page. This allows brick components to declare their dependencies, and the system will inject them in the correct location in your HTML.
112
+
113
+ **Why use this?**
114
+
115
+ When building reusable components (bricks) in Eleventy, you often need to include CSS and JavaScript dependencies. Instead of manually adding these to every page, `bricks` automatically:
116
+ - Collects dependencies from all bricks used on a page
117
+ - Categorizes them (external CSS, external JS, inline styles, inline scripts)
118
+ - Injects them in the correct location in your HTML output
119
+
120
+ **How it works:**
121
+
122
+ 1. Use the `bricksDependencies` shortcode in your base template to mark where dependencies should be injected
123
+ 2. Use the `brick` shortcode to register and render brick components that declare their dependencies
124
+ 3. The system automatically collects all dependencies and injects them when the page is built
125
+
126
+ **Usage:**
127
+
128
+ 1. Enable `bricks` in your Eleventy config:
129
+
130
+ ```javascript
131
+ import { bricks } from "@anydigital/11ty-bricks";
132
+
133
+ export default function(eleventyConfig) {
134
+ bricks(eleventyConfig);
135
+ // Or use as plugin:
136
+ // eleventyConfig.addPlugin(eleventyBricks, { bricks: true });
137
+ }
138
+ ```
139
+
140
+ 2. Add the `bricksDependencies` shortcode in your base template (typically in the `<head>` section):
141
+
142
+ ```njk
143
+ <head>
144
+ <meta charset="UTF-8">
145
+ <title>My Site</title>
146
+ {% bricksDependencies [
147
+ ... (global dependencies can be set here) ...
148
+ ] %}
149
+ <!-- Other head content -->
150
+ </head>
151
+ ```
152
+
153
+ 3. Create brick components that declare their dependencies:
154
+
155
+ ```javascript
156
+ // myBrick.js
157
+ export default {
158
+ dependencies: [
159
+ 'https://cdn.example.com/library.css',
160
+ 'https://cdn.example.com/library.js'
161
+ ],
162
+ style: `
163
+ .my-component { color: blue; }
164
+ `,
165
+ script: `
166
+ console.log('Component initialized');
167
+ `,
168
+ render: function() {
169
+ return '<div class="my-component">Hello World</div>';
170
+ }
171
+ };
172
+ ```
173
+
174
+ 4. Use the `brick` shortcode in your templates:
175
+
176
+ ```njk
177
+ {% set myBrick = require('./myBrick.js') %}
178
+ {% brick myBrick %}
179
+ ```
180
+
181
+ **Brick Component Structure:**
182
+
183
+ A brick component is a JavaScript object with the following optional properties:
184
+
185
+ - `dependencies`: Array of URLs to external CSS or JavaScript files (e.g., `['https://cdn.example.com/style.css', 'https://cdn.example.com/script.js']`)
186
+ - `style`: String containing inline CSS
187
+ - `script`: String containing inline JavaScript
188
+ - `render`: Function that returns the HTML markup for the component
189
+
190
+ **Output:**
191
+
192
+ The system will automatically inject all dependencies in the order they were registered:
193
+
194
+ ```html
195
+ <head>
196
+ <meta charset="UTF-8">
197
+ <title>My Site</title>
198
+ <link rel="stylesheet" href="https://cdn.example.com/library.css">
199
+ <style>.my-component { color: blue; }</style>
200
+ <script src="https://cdn.example.com/library.js"></script>
201
+ <script>console.log('Component initialized');</script>
202
+ <!-- Other head content -->
203
+ </head>
204
+ ```
205
+
206
+ **Features:**
207
+
208
+ - Automatic dependency collection per page
209
+ - Categorizes dependencies (CSS vs JS, external vs inline)
210
+ - Deduplicates dependencies (using Sets internally)
211
+ - Works with both external URLs and inline code
212
+ - Clears registry before each build to prevent stale data
213
+
214
+ ### mdAutoRawTags
89
215
 
90
216
  Prevents Nunjucks syntax from being processed in Markdown files by automatically wrapping `{{`, `}}`, `{%`, and `%}` with `{% raw %}` tags.
91
217
 
92
218
  **Why use this?**
93
219
 
94
- When writing documentation or tutorials about templating in Markdown files, you often want to show Nunjucks/Liquid syntax as literal text. This helper automatically escapes these special characters so they display as-is instead of being processed by the template engine.
220
+ When writing documentation or tutorials about templating in Markdown files, you often want to show Nunjucks/Liquid syntax as literal text. This preprocessor automatically escapes these special characters so they display as-is instead of being processed by the template engine.
221
+
222
+ **Usage:**
223
+
224
+ 1. Enable `mdAutoRawTags` in your Eleventy config:
225
+
226
+ ```javascript
227
+ import { mdAutoRawTags } from "@anydigital/11ty-bricks";
228
+
229
+ export default function(eleventyConfig) {
230
+ mdAutoRawTags(eleventyConfig);
231
+ // Or use as plugin:
232
+ // eleventyConfig.addPlugin(eleventyBricks, { mdAutoRawTags: true });
233
+ }
234
+ ```
95
235
 
96
236
  **Example:**
97
237
 
98
- Before `autoRaw`, writing this in Markdown:
238
+ Before `mdAutoRawTags`, writing this in Markdown:
99
239
  ```markdown
100
240
  Use {{ variable }} to output variables.
101
241
  ```
102
242
 
103
- Would try to process `{{ variable }}` as a template variable. With `autoRaw`, it displays exactly as written.
243
+ Would try to process `{{ variable }}` as a template variable. With `mdAutoRawTags`, it displays exactly as written.
244
+
245
+ ### mdAutoNl2br
246
+
247
+ Automatically converts `\n` sequences to `<br>` tags in Markdown content. This is particularly useful for adding line breaks inside Markdown tables where standard newlines don't work.
248
+
249
+ **Why use this?**
250
+
251
+ Markdown tables don't support multi-line content in cells. By using `\n` in your content, this preprocessor will convert it to `<br>` tags, allowing you to display line breaks within table cells and other content.
252
+
253
+ **Usage:**
254
+
255
+ 1. Enable `mdAutoNl2br` in your Eleventy config:
256
+
257
+ ```javascript
258
+ import { mdAutoNl2br } from "@anydigital/11ty-bricks";
259
+
260
+ export default function(eleventyConfig) {
261
+ mdAutoNl2br(eleventyConfig);
262
+ // Or use as plugin:
263
+ // eleventyConfig.addPlugin(eleventyBricks, { mdAutoNl2br: true });
264
+ }
265
+ ```
266
+
267
+ **Example:**
268
+
269
+ In your Markdown file:
270
+ ```markdown
271
+ | Column 1 | Column 2 |
272
+ |----------|----------|
273
+ | Line 1\nLine 2\nLine 3 | Another cell\nWith multiple lines |
274
+ ```
275
+
276
+ Will render as:
277
+ ```html
278
+ <td>Line 1<br>Line 2<br>Line 3</td>
279
+ <td>Another cell<br>With multiple lines</td>
280
+ ```
281
+
282
+ **Note:** This processes literal `\n` sequences (backslash followed by 'n'), not actual newline characters. Type `\n` in your source files where you want line breaks.
283
+
284
+ ### fragment
285
+
286
+ A shortcode that includes content from fragment files stored in the `_fragments` directory. The content will be processed by the template engine.
287
+
288
+ **Why use this?**
289
+
290
+ Fragments allow you to organize reusable content snippets in a dedicated directory and include them in your templates. This is useful for:
291
+ - Reusable content blocks
292
+ - Shared template sections
293
+ - Component-like content organization
294
+
295
+ **Usage:**
296
+
297
+ 1. Enable `fragments` in your Eleventy config:
298
+
299
+ ```javascript
300
+ import { fragments } from "@anydigital/11ty-bricks";
301
+
302
+ export default function(eleventyConfig) {
303
+ fragments(eleventyConfig);
304
+ // Or use as plugin:
305
+ // eleventyConfig.addPlugin(eleventyBricks, { fragments: true });
306
+ }
307
+ ```
308
+
309
+ 2. Create fragment files in the `_fragments` directory (relative to your input directory):
310
+
311
+ ```
312
+ your-project/
313
+ _fragments/
314
+ header.njk
315
+ footer.njk
316
+ callout.md
317
+ ```
318
+
319
+ 3. Use the `fragment` shortcode in your templates:
320
+
321
+ ```njk
322
+ {% fragment "header.njk" %}
323
+
324
+ <main>
325
+ <!-- Your content -->
326
+ </main>
327
+
328
+ {% fragment "footer.njk" %}
329
+ ```
330
+
331
+ **Parameters:**
332
+
333
+ - `path`: The path to the fragment file relative to the `_fragments` directory
334
+
335
+ **Features:**
336
+
337
+ - Reads files from `_fragments` directory in your input directory
338
+ - Content is processed by the template engine
339
+ - Supports any template language that Eleventy supports
340
+ - Shows helpful error comment if fragment is not found
341
+
342
+ **Example:**
343
+
344
+ Create `_fragments/callout.njk`:
345
+ ```njk
346
+ <div class="callout callout-{{ type | default('info') }}">
347
+ {{ content }}
348
+ </div>
349
+ ```
350
+
351
+ Use it in your template:
352
+ ```njk
353
+ {% set type = "warning" %}
354
+ {% set content = "This is important!" %}
355
+ {% fragment "callout.njk" %}
356
+ ```
357
+
358
+ ### setAttr
359
+
360
+ A filter that creates a new object with an overridden attribute value. This is useful for modifying data objects in templates without mutating the original.
361
+
362
+ **Why use this?**
363
+
364
+ When working with Eleventy data, you sometimes need to modify an object's properties for a specific use case. The `setAttr` filter provides a clean way to create a modified copy of an object without affecting the original.
365
+
366
+ **Usage:**
367
+
368
+ 1. Enable `setAttr` in your Eleventy config:
369
+
370
+ ```javascript
371
+ import { setAttrFilter } from "@anydigital/11ty-bricks";
372
+
373
+ export default function(eleventyConfig) {
374
+ setAttrFilter(eleventyConfig);
375
+ // Or use as plugin:
376
+ // eleventyConfig.addPlugin(eleventyBricks, { setAttrFilter: true });
377
+ }
378
+ ```
379
+
380
+ 2. Use the filter in your templates:
381
+
382
+ ```njk
383
+ {# Create a modified version of a page object #}
384
+ {% set modifiedPage = page | setAttr('title', 'New Title') %}
385
+
386
+ <h1>{{ modifiedPage.title }}</h1>
387
+ <p>Original title: {{ page.title }}</p>
388
+ ```
389
+
390
+ **Parameters:**
391
+
392
+ - `obj`: The object to modify
393
+ - `key`: The attribute name to set (string)
394
+ - `value`: The value to set for the attribute (any type)
395
+
396
+ **Returns:**
397
+
398
+ A new object with the specified attribute set to the given value. The original object is not modified.
399
+
400
+ **Features:**
401
+
402
+ - Non-mutating: Creates a new object, leaving the original unchanged
403
+ - Works with any object type
404
+ - Supports any attribute name and value type
405
+ - Can be chained with other filters
406
+
407
+ **Examples:**
408
+
409
+ ```njk
410
+ {# Override a single attribute #}
411
+ {% set updatedPost = post | setAttr('featured', true) %}
412
+
413
+ {# Chain multiple setAttr filters #}
414
+ {% set modifiedPost = post
415
+ | setAttr('category', 'blog')
416
+ | setAttr('priority', 1)
417
+ %}
418
+
419
+ {# Use in loops #}
420
+ {% for item in collection %}
421
+ {% set enhancedItem = item | setAttr('processed', true) %}
422
+ {# ... use enhancedItem ... #}
423
+ {% endfor %}
424
+ ```
425
+
426
+ ### byAttr
427
+
428
+ A filter that filters collection items by attribute value. It checks if an item's attribute matches a target value. If the attribute is an array, it checks if the array includes the target value.
429
+
430
+ **Why use this?**
431
+
432
+ When working with Eleventy collections, you often need to filter items based on front matter data. The `byAttr` filter provides a flexible way to filter by any attribute, with special handling for array attributes (like tags).
433
+
434
+ **Usage:**
435
+
436
+ 1. Enable `byAttr` in your Eleventy config:
437
+
438
+ ```javascript
439
+ import { byAttrFilter } from "@anydigital/11ty-bricks";
440
+
441
+ export default function(eleventyConfig) {
442
+ byAttrFilter(eleventyConfig);
443
+ // Or use as plugin:
444
+ // eleventyConfig.addPlugin(eleventyBricks, { byAttrFilter: true });
445
+ }
446
+ ```
447
+
448
+ 2. Use the filter in your templates:
449
+
450
+ **Filter by exact attribute match:**
451
+ ```njk
452
+ {# Get all posts with category 'blog' #}
453
+ {% set blogPosts = collections.all | byAttr('category', 'blog') %}
454
+
455
+ {% for post in blogPosts %}
456
+ <h2>{{ post.data.title }}</h2>
457
+ {% endfor %}
458
+ ```
459
+
460
+ **Filter by array attribute (tags):**
461
+ ```njk
462
+ {# Get all posts that include 'javascript' tag #}
463
+ {% set jsPosts = collections.all | byAttr('tags', 'javascript') %}
464
+
465
+ {% for post in jsPosts %}
466
+ <h2>{{ post.data.title }}</h2>
467
+ {% endfor %}
468
+ ```
469
+
470
+ **Parameters:**
471
+
472
+ - `collection`: The collection to filter (array of items)
473
+ - `attrName`: The attribute name to check (string)
474
+ - `targetValue`: The value to match against (any type)
475
+
476
+ **Features:**
477
+
478
+ - Works with any attribute in front matter
479
+ - Handles both `item.data.attrName` and `item.attrName` patterns
480
+ - Special handling for array attributes (uses `includes()` check)
481
+ - Returns empty array if collection is invalid
482
+ - Filters out items without the specified attribute
483
+
484
+ **Examples:**
485
+
486
+ Front matter:
487
+ ```yaml
488
+ ---
489
+ title: My Post
490
+ category: blog
491
+ tags: [javascript, tutorial, beginner]
492
+ priority: 1
493
+ ---
494
+ ```
495
+
496
+ Template usage:
497
+ ```njk
498
+ {# Filter by category #}
499
+ {% set blogPosts = collections.all | byAttr('category', 'blog') %}
500
+
501
+ {# Filter by tag (array) #}
502
+ {% set jsTutorials = collections.all | byAttr('tags', 'javascript') %}
503
+
504
+ {# Filter by numeric value #}
505
+ {% set highPriority = collections.all | byAttr('priority', 1) %}
506
+
507
+ {# Chain filters #}
508
+ {% set recentBlogPosts = collections.all | byAttr('category', 'blog') | reverse | limit(5) %}
509
+ ```
510
+
511
+ ### siteData
512
+
513
+ Adds global site data to your Eleventy project, providing commonly needed values that can be accessed in all templates.
514
+
515
+ **Why use this?**
516
+
517
+ Many websites need access to the current year (for copyright notices) and environment information (to conditionally enable features based on production vs development). This helper provides these as global `site` data without manually setting them up.
518
+
519
+ **Usage:**
520
+
521
+ 1. Enable `siteData` in your Eleventy config:
522
+
523
+ ```javascript
524
+ import { siteData } from "@anydigital/11ty-bricks";
525
+
526
+ export default function(eleventyConfig) {
527
+ siteData(eleventyConfig);
528
+ // Or use as plugin:
529
+ // eleventyConfig.addPlugin(eleventyBricks, { siteData: true });
530
+ }
531
+ ```
532
+
533
+ 2. Use the global data in your templates:
534
+
535
+ **Current Year:**
536
+ ```njk
537
+ <footer>
538
+ <p>&copy; {{ site.year }} Your Company Name. All rights reserved.</p>
539
+ </footer>
540
+ ```
541
+
542
+ **Environment Check:**
543
+ ```njk
544
+ {% if site.isProd %}
545
+ <!-- Production-only features -->
546
+ <script async src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID"></script>
547
+ {% else %}
548
+ <!-- Development-only features -->
549
+ <div class="dev-toolbar">Development Mode</div>
550
+ {% endif %}
551
+ ```
552
+
553
+ **Available Data:**
554
+
555
+ - `site.year`: The current year as a number (e.g., `2026`)
556
+ - `site.isProd`: Boolean indicating if running in production mode (`true` for `eleventy build`, `false` for `eleventy serve`)
557
+
558
+ **Features:**
559
+
560
+ - Automatically updates the year value
561
+ - Detects production vs development mode based on `ELEVENTY_RUN_MODE` environment variable
562
+ - Available globally in all templates without manual setup
563
+ - No configuration required
564
+
565
+ **Examples:**
566
+
567
+ ```njk
568
+ {# Copyright notice #}
569
+ <p>Copyright &copy; {{ site.year }} My Site</p>
570
+
571
+ {# Conditional loading of analytics #}
572
+ {% if site.isProd %}
573
+ <script src="/analytics.js"></script>
574
+ {% endif %}
575
+
576
+ {# Different behavior in dev vs prod #}
577
+ {% if site.isProd %}
578
+ <link rel="stylesheet" href="/css/styles.min.css">
579
+ {% else %}
580
+ <link rel="stylesheet" href="/css/styles.css">
581
+ <script src="/live-reload.js"></script>
582
+ {% endif %}
583
+ ```
584
+
585
+ ### Additional Exports
586
+
587
+ The plugin also exports the following for advanced usage:
588
+
589
+ - `transformAutoRaw(content)`: The transform function used by `mdAutoRawTags` preprocessor. Can be used programmatically to wrap Nunjucks syntax with raw tags.
590
+ - `transformNl2br(content)`: The transform function used by `mdAutoNl2br` preprocessor. Can be used programmatically to convert `\n` sequences to `<br>` tags.
591
+
592
+ ## CLI Helper Commands
593
+
594
+ After installing this package, the `download-files` command becomes available:
595
+
596
+ ### download-files
597
+
598
+ A CLI command that downloads external files to your project based on URLs specified in your `package.json`.
599
+
600
+ **Usage:**
601
+
602
+ 1. Add a `_downloadFiles` field to your project's `package.json` with URL-to-path mappings:
603
+
604
+ ```json
605
+ {
606
+ "_downloadFiles": {
607
+ "https://example.com/library.js": "src/vendor/library.js",
608
+ "https://cdn.example.com/styles.css": "public/css/external.css"
609
+ }
610
+ }
611
+ ```
612
+
613
+ 2. Run the download command:
614
+
615
+ ```bash
616
+ npx download-files
617
+ ```
618
+
619
+ **Options:**
620
+
621
+ - `-o, --output <dir>`: Specify an output directory where all files will be downloaded (relative paths in `_downloadFiles` will be resolved relative to this directory)
622
+
623
+ ```bash
624
+ # Download all files to a specific directory
625
+ npx download-files --output public
626
+ ```
627
+
628
+ **Features:**
629
+
630
+ - Downloads multiple files from external URLs
631
+ - Automatically creates directories if they don't exist
632
+ - Overwrites existing files
633
+ - Continues downloading remaining files even if some fail
634
+ - Provides clear progress and error messages
635
+ - Returns appropriate exit codes for CI/CD integration
636
+
637
+ **Use Cases:**
638
+
639
+ - Download third-party libraries and assets
640
+ - Fetch external resources during build processes
641
+ - Keep vendored files up to date
642
+ - Automate dependency downloads that aren't available via npm
104
643
 
105
644
  ## Requirements
106
645
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anydigital/11ty-bricks",
3
- "version": "1.0.0-alpha",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "A collection of helpful utilities and filters for Eleventy (11ty)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -10,10 +10,14 @@
10
10
  "require": "./src/index.cjs"
11
11
  }
12
12
  },
13
+ "bin": {
14
+ "download-files": "src/cli/download-files.js"
15
+ },
13
16
  "files": [
14
17
  "src"
15
18
  ],
16
19
  "scripts": {
20
+ "build": "npx download-files",
17
21
  "test": "node --test src/**/*.test.js"
18
22
  },
19
23
  "keywords": [
@@ -40,5 +44,8 @@
40
44
  },
41
45
  "engines": {
42
46
  "node": ">=18.0.0"
47
+ },
48
+ "_downloadFiles": {
49
+ "https://raw.githubusercontent.com/danurbanowicz/eleventy-sveltia-cms-starter/refs/heads/master/admin/index.html": "src/sveltia-admin/index.html"
43
50
  }
44
51
  }
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
+
@@ -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
- module.exports.autoRaw = async function(eleventyConfig) {
14
- const { autoRaw } = await import('./index.js');
15
- return autoRaw(eleventyConfig);
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 { autoRaw } from "./autoRaw.js";
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.autoRaw - Enable autoRaw preprocessor (default: false)
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 global data (default: false)
12
23
  */
13
24
  export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
14
- const { autoRaw: enableAutoRaw = false } = options;
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 { autoRaw };
30
+ export { bricks, mdAutoRawTags, mdAutoNl2br, fragments, setAttrFilter, byAttrFilter, siteData };
24
31
 
32
+ // Export transform functions for advanced usage
33
+ export { transformAutoRaw, transformNl2br };
@@ -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 "./autoRaw.js";
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
+
@@ -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>
package/src/autoRaw.js DELETED
@@ -1,28 +0,0 @@
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
- * autoRaw - 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 autoRaw(eleventyConfig) {
24
- eleventyConfig.addPreprocessor("autoRaw", "md", (data, content) => {
25
- return transformAutoRaw(content);
26
- });
27
- }
28
-