@anydigital/eleventy-bricks 0.26.0 → 0.27.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/README.md CHANGED
@@ -27,7 +27,7 @@ export default function (eleventyConfig) {
27
27
  mdAutoNl2br: true,
28
28
  autoLinkFavicons: true,
29
29
  siteData: true,
30
- filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "fetch"],
30
+ filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "section", "fetch"],
31
31
  });
32
32
 
33
33
  // Your other configuration...
@@ -45,7 +45,7 @@ module.exports = function (eleventyConfig) {
45
45
  mdAutoNl2br: true,
46
46
  autoLinkFavicons: true,
47
47
  siteData: true,
48
- filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "fetch"],
48
+ filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "section", "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
+ sectionFilter,
74
75
  fetchFilter,
75
76
  siteData,
76
77
  } from "@anydigital/eleventy-bricks";
@@ -85,6 +86,7 @@ export default function (eleventyConfig) {
85
86
  removeTagFilter(eleventyConfig);
86
87
  ifFilter(eleventyConfig);
87
88
  attrConcatFilter(eleventyConfig);
89
+ sectionFilter(eleventyConfig);
88
90
  // fetchFilter is only available if @11ty/eleventy-fetch is installed
89
91
  if (fetchFilter) {
90
92
  fetchFilter(eleventyConfig);
@@ -108,6 +110,7 @@ const {
108
110
  removeTagFilter,
109
111
  ifFilter,
110
112
  attrConcatFilter,
113
+ sectionFilter,
111
114
  fetchFilter,
112
115
  siteData,
113
116
  } = require("@anydigital/eleventy-bricks");
@@ -122,6 +125,7 @@ module.exports = async function (eleventyConfig) {
122
125
  await removeTagFilter(eleventyConfig);
123
126
  await ifFilter(eleventyConfig);
124
127
  await attrConcatFilter(eleventyConfig);
128
+ await sectionFilter(eleventyConfig);
125
129
  // fetchFilter is only available if @11ty/eleventy-fetch is installed
126
130
  if (fetchFilter) {
127
131
  await fetchFilter(eleventyConfig);
@@ -154,6 +158,7 @@ When using the plugin (Option 1), you can configure which helpers to enable:
154
158
  - `'remove_tag'` - Remove HTML elements from content
155
159
  - `'if'` - Inline conditional/ternary operator
156
160
  - `'attr_concat'` - Concatenate values to an attribute array
161
+ - `'section'` - Extract named sections from content marked with HTML comments
157
162
  - `'fetch'` - Fetch remote URLs or local files (requires `@11ty/eleventy-fetch`)
158
163
 
159
164
  **Example:**
@@ -164,7 +169,7 @@ eleventyConfig.addPlugin(eleventyBricks, {
164
169
  mdAutoNl2br: true,
165
170
  autoLinkFavicons: true,
166
171
  siteData: true,
167
- filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "fetch"],
172
+ filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "section", "fetch"],
168
173
  });
169
174
  ```
170
175
 
@@ -185,8 +190,9 @@ The plugin also exports the following utility functions for advanced usage:
185
190
  - `iff(trueValue, condition, falseValue)`: The core conditional function used by the `if` filter. Can be used programmatically as a ternary operator.
186
191
  - `attrConcat(obj, attr, values)`: The core function used by the `attr_concat` filter. Can be used programmatically to concatenate values to an attribute array.
187
192
  - `attrSet(obj, key, value)`: The core function used by the `attr_set` filter. Can be used programmatically to override object attributes.
193
+ - `section(content, sectionName)`: The core function used by the `section` filter. Can be used programmatically to extract named sections from content.
188
194
 
189
- <!--TRICKS-->
195
+ <!--section:11ty-->
190
196
 
191
197
  ## Tricks from [Eleventy Bricks](https://github.com/anydigital/eleventy-bricks) {#eleventy-bricks}
192
198
 
@@ -208,6 +214,7 @@ The plugin also exports the following utility functions for advanced usage:
208
214
  | {.divider} | Textual |
209
215
  | `HTML \|` | `striptags` | `strip_html` |
210
216
  | `HTML \|` | [`remove_tag(TAG)`](#remove_tag) | [`remove_tag: TAG`](#remove_tag) |
217
+ | `HTML \|` | [`section(NAME)`](#section) | [`section: NAME`](#section) |
211
218
  | `STR \|` | `remove: STR2` | `remove: STR2` |
212
219
  | {.divider} | Other |
213
220
  | `URL \|` | [`fetch`](#fetch) | [`fetch`](#fetch) |
@@ -520,6 +527,103 @@ export default function (eleventyConfig) {
520
527
 
521
528
  While this filter can help sanitize HTML content, it should not be relied upon as the sole security measure. For critical security requirements, use a dedicated HTML sanitization library on the server side before content reaches your templates.
522
529
 
530
+ #### `section`
531
+
532
+ A filter that extracts a named section from content marked with HTML comments. This is useful for splitting a single content file (like a Markdown post) into multiple parts that can be displayed and styled independently in your templates.
533
+
534
+ **Why use this?**
535
+
536
+ When working with Markdown content in Eleventy, you're usually limited to a single `content` variable. The `section` filter allows you to define multiple named sections within your content using simple HTML comments, giving you granular control over where different parts of your content appear in your layout.
537
+
538
+ **Usage:**
539
+
540
+ 1. Enable the `section` filter in your Eleventy config:
541
+
542
+ ```javascript
543
+ import { sectionFilter } from "@anydigital/eleventy-bricks";
544
+
545
+ export default function (eleventyConfig) {
546
+ sectionFilter(eleventyConfig);
547
+ // Or use as plugin:
548
+ // eleventyConfig.addPlugin(eleventyBricks, { filters: ['section'] });
549
+ }
550
+ ```
551
+
552
+ 2. Mark sections in your content file (e.g., `post.md`):
553
+
554
+ ```markdown
555
+ # My Post
556
+
557
+ &lt;!--section:intro-->
558
+
559
+ This is the introduction that appears at the top of the page.
560
+
561
+ &lt;!--section:main-->
562
+
563
+ This is the main body of the post with all the details.
564
+
565
+ &lt;!--section:summary,sidebar-->
566
+
567
+ This content appears in both the summary and the sidebar!
568
+ ```
569
+
570
+ 3. Use the filter in your templates:
571
+
572
+ ```njk
573
+ {# Get the intro section #}
574
+ <div class="page-intro">
575
+ {{ content | section('intro') | safe }}
576
+ </div>
577
+
578
+ {# Get the main section #}
579
+ <article>
580
+ {{ content | section('main') | safe }}
581
+ </article>
582
+
583
+ {# Get the sidebar section #}
584
+ <aside>
585
+ {{ content | section('sidebar') | safe }}
586
+ </aside>
587
+ ```
588
+
589
+ **Parameters:**
590
+
591
+ - `content`: The string content to process (usually `content` variable)
592
+ - `sectionName`: The name(s) of the section to extract (string)
593
+
594
+ **Features:**
595
+
596
+ - **Multiple names**: A single section can have multiple names separated by commas: `&lt;!--section:name1,name2-->`
597
+ - **Case-insensitive**: Section names are matched without regard to case
598
+ - **Multiple occurrences**: If a section name appears multiple times, the filter concatenates all matching sections
599
+ - **Non-destructive**: Returns extracted content without modifying the original input
600
+ - **EOF support**: Sections continue until the next `&lt;!--section*-->` marker or the end of the file
601
+
602
+ **Examples:**
603
+
604
+ ```njk
605
+ {# Extract multiple sections with same name #}
606
+ {# Example content has two &lt;!--section:note--> blocks #}
607
+ <div class="notes-box">
608
+ {{ content | section('note') | safe }}
609
+ </div>
610
+
611
+ {# Use case-insensitive names #}
612
+ {{ content | section('INTRO') | safe }}
613
+
614
+ {# Handle missing sections gracefully (returns empty string) #}
615
+ {% set footer = content | section('non-existent-section') %}
616
+ {% if footer %}
617
+ <footer>{{ footer | safe }}</footer>
618
+ {% endif %}
619
+ ```
620
+
621
+ **Syntax Rules:**
622
+
623
+ - Sections start with: `&lt;!--section:NAME-->` or `&lt;!--section:NAME1,NAME2-->`
624
+ - Sections end at the next `&lt;!--section*-->` marker or end of file
625
+ - Whitespace around names and inside comments is automatically trimmed
626
+
523
627
  #### `if`
524
628
 
525
629
  An inline conditional/ternary operator filter that returns one value if a condition is truthy, and another if it's falsy. Similar to Nunjucks' inline if syntax.
@@ -1101,6 +1205,8 @@ mkdir -p admin
1101
1205
  ln -s ../node_modules/@anydigital/eleventy-bricks/src/admin/index.html admin/index.html
1102
1206
  ```
1103
1207
 
1208
+ <!--section:npm,11ty-->
1209
+
1104
1210
  ### Using the `do` Folder Pattern
1105
1211
 
1106
1212
  This package provides a pre-configured `do` folder setup that helps organize your development workflow using npm workspaces. The `do` folder contains scripts for building and running your Eleventy project.
@@ -0,0 +1,96 @@
1
+ # Section Filter Examples
2
+
3
+ ## Basic Usage
4
+
5
+ ```markdown
6
+ <!--section:intro-->
7
+
8
+ This is the introduction section.
9
+
10
+ <!--section:main-->
11
+
12
+ This is the main content.
13
+
14
+ <!--section:footer-->
15
+
16
+ This is the footer.
17
+ ```
18
+
19
+ In your template:
20
+
21
+ ```nunjucks
22
+ {{ content | section('intro') }}
23
+ ```
24
+
25
+ Output:
26
+
27
+ ```
28
+ This is the introduction section.
29
+ ```
30
+
31
+ ## Multiple Names per Section
32
+
33
+ ```markdown
34
+ <!--section:summary,abstract-->
35
+
36
+ This content appears in both 'summary' and 'abstract' sections.
37
+
38
+ <!--section:conclusion-->
39
+
40
+ Final thoughts.
41
+ ```
42
+
43
+ Both of these work:
44
+
45
+ ```nunjucks
46
+ {{ content | section('summary') }}
47
+ {{ content | section('abstract') }}
48
+ ```
49
+
50
+ ## Real-World Example
51
+
52
+ ```markdown
53
+ # Research Paper
54
+
55
+ <!--section:abstract,summary-->
56
+
57
+ A brief overview of the research findings.
58
+
59
+ <!--section:introduction-->
60
+
61
+ Background and context for the research.
62
+
63
+ <!--section:methodology-->
64
+
65
+ How the research was conducted.
66
+
67
+ <!--section:results-->
68
+
69
+ Key findings from the study.
70
+
71
+ <!--section:conclusion,summary-->
72
+
73
+ Summary and implications of the research.
74
+ ```
75
+
76
+ Usage:
77
+
78
+ ```nunjucks
79
+ <!-- Get full summary (abstract + conclusion) -->
80
+ <div class="summary">
81
+ {{ content | section('summary') }}
82
+ </div>
83
+
84
+ <!-- Get just introduction -->
85
+ <div class="intro">
86
+ {{ content | section('introduction') }}
87
+ </div>
88
+ ```
89
+
90
+ ## Notes
91
+
92
+ - Section names are **case-insensitive**: `intro`, `INTRO`, and `Intro` are all the same
93
+ - Multiple names can be comma-separated: `<!--section:name1,name2,name3-->`
94
+ - Whitespace around names is trimmed
95
+ - Content extends from the section marker to the next `<!--section*>` or EOF
96
+ - If a section name appears multiple times, all matching sections are concatenated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anydigital/eleventy-bricks",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "A collection of helpful utilities and filters for Eleventy (11ty)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -36,7 +36,7 @@ export default function (eleventyConfig) {
36
36
  mdAutoRawTags: true,
37
37
  autoLinkFavicons: true,
38
38
  siteData: true,
39
- filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "fetch"],
39
+ filters: ["attr_set", "attr_includes", "merge", "remove_tag", "if", "attr_concat", "fetch", "section"],
40
40
  });
41
41
  if (pluginTOC) {
42
42
  eleventyConfig.addPlugin(pluginTOC, {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Extract a named section from content marked with HTML comments
3
+ *
4
+ * @param {string} content - The content to process
5
+ * @param {string} sectionName - The section name(s) to extract
6
+ * @returns {string} The extracted section content
7
+ */
8
+ export function section(content, sectionName) {
9
+ if (!content || typeof content !== "string") {
10
+ return content;
11
+ }
12
+
13
+ if (typeof sectionName !== "string" || !sectionName) {
14
+ return "";
15
+ }
16
+
17
+ // Normalize section name for comparison (trim whitespace)
18
+ const targetName = sectionName.trim().toLowerCase();
19
+
20
+ // Regex to match section markers with content up to the next section or end of string
21
+ // Captures: (1) section names, (2) content until next section marker or end
22
+ const sectionRegex = /<!--section:([^>]+)-->([\s\S]*?)(?=<!--section|$)/g;
23
+
24
+ let results = [];
25
+ let match;
26
+
27
+ // Find all sections
28
+ while ((match = sectionRegex.exec(content)) !== null) {
29
+ const namesStr = match[1];
30
+ const sectionContent = match[2];
31
+ const names = namesStr.split(",").map((n) => n.trim().toLowerCase());
32
+
33
+ // Check if any of the names match the target
34
+ if (names.includes(targetName)) {
35
+ results.push(sectionContent);
36
+ }
37
+ }
38
+
39
+ // Join all matching sections
40
+ return results.join("");
41
+ }
42
+
43
+ /**
44
+ * section filter - Extract a named section from content
45
+ *
46
+ * Usage in templates:
47
+ * {{ content | section('intro') }}
48
+ * {{ content | section('footer') }}
49
+ *
50
+ * Content format:
51
+ * <!--section:intro-->
52
+ * This is the intro content
53
+ * <!--section:main-->
54
+ * This is the main content
55
+ * <!--section:footer,sidebar-->
56
+ * This appears in both footer and sidebar sections
57
+ *
58
+ * @param {Object} eleventyConfig - The Eleventy configuration object
59
+ */
60
+ export function sectionFilter(eleventyConfig) {
61
+ eleventyConfig.addFilter("section", section);
62
+ }
@@ -0,0 +1,174 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { section } from "./section.js";
4
+
5
+ describe("section", () => {
6
+ it("should extract a single named section", () => {
7
+ const content = `Before
8
+ <!--section:intro-->
9
+ This is the intro
10
+ <!--section:main-->
11
+ This is the main`;
12
+
13
+ const result = section(content, "intro");
14
+ assert.strictEqual(result, "\nThis is the intro\n");
15
+ });
16
+
17
+ it("should extract section up to the next section marker", () => {
18
+ const content = `<!--section:first-->
19
+ First content
20
+ <!--section:second-->
21
+ Second content
22
+ <!--section:third-->
23
+ Third content`;
24
+
25
+ const result = section(content, "second");
26
+ assert.strictEqual(result, "\nSecond content\n");
27
+ });
28
+
29
+ it("should extract section up to EOF when no next marker", () => {
30
+ const content = `<!--section:intro-->
31
+ Intro content
32
+ <!--section:main-->
33
+ Main content that goes to the end`;
34
+
35
+ const result = section(content, "main");
36
+ assert.strictEqual(result, "\nMain content that goes to the end");
37
+ });
38
+
39
+ it("should handle section with multiple names", () => {
40
+ const content = `<!--section:header,nav-->
41
+ Shared content
42
+ <!--section:main-->
43
+ Main content`;
44
+
45
+ const resultHeader = section(content, "header");
46
+ const resultNav = section(content, "nav");
47
+
48
+ assert.strictEqual(resultHeader, "\nShared content\n");
49
+ assert.strictEqual(resultNav, "\nShared content\n");
50
+ });
51
+
52
+ it("should handle section with multiple names (spaces around commas)", () => {
53
+ const content = `<!--section:header, nav , top-->
54
+ Shared content
55
+ <!--section:main-->
56
+ Main content`;
57
+
58
+ const resultHeader = section(content, "header");
59
+ const resultNav = section(content, "nav");
60
+ const resultTop = section(content, "top");
61
+
62
+ assert.strictEqual(resultHeader, "\nShared content\n");
63
+ assert.strictEqual(resultNav, "\nShared content\n");
64
+ assert.strictEqual(resultTop, "\nShared content\n");
65
+ });
66
+
67
+ it("should return empty string for non-existent section", () => {
68
+ const content = `<!--section:intro-->
69
+ Content here
70
+ <!--section:main-->
71
+ More content`;
72
+
73
+ const result = section(content, "footer");
74
+ assert.strictEqual(result, "");
75
+ });
76
+
77
+ it("should handle empty or null input", () => {
78
+ assert.strictEqual(section("", "test"), "");
79
+ assert.strictEqual(section(null, "test"), null);
80
+ assert.strictEqual(section(undefined, "test"), undefined);
81
+ });
82
+
83
+ it("should handle missing section name", () => {
84
+ const content = `<!--section:intro-->Content`;
85
+
86
+ assert.strictEqual(section(content, ""), "");
87
+ assert.strictEqual(section(content, null), "");
88
+ assert.strictEqual(section(content, undefined), "");
89
+ });
90
+
91
+ it("should be case-insensitive for section names", () => {
92
+ const content = `<!--section:INTRO-->
93
+ Content here
94
+ <!--section:Main-->
95
+ More content`;
96
+
97
+ const result1 = section(content, "intro");
98
+ const result2 = section(content, "INTRO");
99
+ const result3 = section(content, "main");
100
+ const result4 = section(content, "MAIN");
101
+
102
+ assert.strictEqual(result1, "\nContent here\n");
103
+ assert.strictEqual(result2, "\nContent here\n");
104
+ assert.strictEqual(result3, "\nMore content");
105
+ assert.strictEqual(result4, "\nMore content");
106
+ });
107
+
108
+ it("should handle multiple sections with the same name", () => {
109
+ const content = `<!--section:note-->
110
+ First note
111
+ <!--section:main-->
112
+ Main content
113
+ <!--section:note-->
114
+ Second note
115
+ <!--section:footer-->
116
+ Footer`;
117
+
118
+ const result = section(content, "note");
119
+ assert.strictEqual(result, "\nFirst note\n\nSecond note\n");
120
+ });
121
+
122
+ it("should handle sections with no content", () => {
123
+ const content = `<!--section:empty--><!--section:main-->
124
+ Main content`;
125
+
126
+ const result = section(content, "empty");
127
+ assert.strictEqual(result, "");
128
+ });
129
+
130
+ it("should handle content before first section", () => {
131
+ const content = `Some preamble
132
+ <!--section:intro-->
133
+ Intro content`;
134
+
135
+ const result = section(content, "intro");
136
+ assert.strictEqual(result, "\nIntro content");
137
+ });
138
+
139
+ it("should handle complex real-world example", () => {
140
+ const content = `# Document Title
141
+
142
+ <!--section:summary,abstract-->
143
+ This is a summary that can be used as an abstract.
144
+ <!--section:introduction-->
145
+ This is the introduction.
146
+ <!--section:methods-->
147
+ These are the methods.
148
+ <!--section:conclusion,summary-->
149
+ This is the conclusion and also part of summary.`;
150
+
151
+ const summary = section(content, "summary");
152
+ const introduction = section(content, "introduction");
153
+ const methods = section(content, "methods");
154
+ const conclusion = section(content, "conclusion");
155
+
156
+ assert.strictEqual(
157
+ summary,
158
+ "\nThis is a summary that can be used as an abstract.\n\nThis is the conclusion and also part of summary.",
159
+ );
160
+ assert.strictEqual(introduction, "\nThis is the introduction.\n");
161
+ assert.strictEqual(methods, "\nThese are the methods.\n");
162
+ assert.strictEqual(conclusion, "\nThis is the conclusion and also part of summary.");
163
+ });
164
+
165
+ it("should handle section markers with extra whitespace", () => {
166
+ const content = `<!--section: intro -->
167
+ Content
168
+ <!--section: main -->
169
+ More`;
170
+
171
+ const result = section(content, "intro");
172
+ assert.strictEqual(result, "\nContent\n");
173
+ });
174
+ });
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import { mergeFilter, merge } from "./filters/merge.js";
13
13
  import { removeTagFilter, removeTag } from "./filters/remove_tag.js";
14
14
  import { ifFilter, iff } from "./filters/if.js";
15
15
  import { attrConcatFilter, attrConcat } from "./filters/attr_concat.js";
16
+ import { sectionFilter, section as sectionFn } from "./filters/section.js";
16
17
  import { siteData } from "./siteData.js";
17
18
 
18
19
  // Conditionally import fetchFilter only if @11ty/eleventy-fetch is available
@@ -36,7 +37,7 @@ try {
36
37
  * @param {boolean} options.mdAutoRawTags - Enable mdAutoRawTags preprocessor (default: false)
37
38
  * @param {boolean} options.mdAutoNl2br - Enable mdAutoNl2br for \n to <br> conversion (default: false)
38
39
  * @param {boolean} options.autoLinkFavicons - Enable autoLinkFavicons to add favicons to plain text links (default: false)
39
- * @param {Array<string>} options.filters - Array of filter names to enable: 'attr_set', 'attr_includes', 'merge', 'remove_tag', 'if', 'attr_concat', 'fetch' (default: [])
40
+ * @param {Array<string>} options.filters - Array of filter names to enable: 'attr_set', 'attr_includes', 'merge', 'remove_tag', 'if', 'attr_concat', 'section', 'fetch' (default: [])
40
41
  * @param {boolean} options.siteData - Enable site.year and site.prod global data (default: false)
41
42
  */
42
43
  export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
@@ -54,6 +55,7 @@ export default function eleventyBricksPlugin(eleventyConfig, options = {}) {
54
55
  remove_tag: removeTagFilter,
55
56
  if: ifFilter,
56
57
  attr_concat: attrConcatFilter,
58
+ section: sectionFilter,
57
59
  ...(fetchFilter && { fetch: fetchFilter }),
58
60
  };
59
61
 
@@ -86,6 +88,7 @@ export {
86
88
  removeTagFilter,
87
89
  ifFilter,
88
90
  attrConcatFilter,
91
+ sectionFilter,
89
92
  fetchFilter,
90
93
  siteData,
91
94
  };
@@ -104,4 +107,5 @@ export {
104
107
  iff,
105
108
  attrConcat,
106
109
  attrSet,
110
+ sectionFn as section,
107
111
  };