@anyblades/buildawesome-kit-plugin 1.3.0-alpha

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.
Files changed (39) hide show
  1. package/features/autoLinkFavicons.js +71 -0
  2. package/features/autoLinkFavicons.test.js +694 -0
  3. package/features/mdAutoNl2br.js +22 -0
  4. package/features/mdAutoNl2br.test.js +68 -0
  5. package/features/mdAutoRawTags.js +17 -0
  6. package/features/mdAutoRawTags.test.js +80 -0
  7. package/features/mdAutoUncommentAttrs.js +26 -0
  8. package/features/mdAutoUncommentAttrs.test.js +65 -0
  9. package/features/siteData.js +43 -0
  10. package/features/siteData.test.js +120 -0
  11. package/features/virtualPages.js +18 -0
  12. package/filters/attr_concat.js +55 -0
  13. package/filters/attr_concat.test.js +205 -0
  14. package/filters/attr_includes.js +41 -0
  15. package/filters/attr_includes.test.js +125 -0
  16. package/filters/attr_set.js +22 -0
  17. package/filters/attr_set.test.js +71 -0
  18. package/filters/date.js +11 -0
  19. package/filters/date.test.js +33 -0
  20. package/filters/fetch.js +80 -0
  21. package/filters/if.js +24 -0
  22. package/filters/if.test.js +63 -0
  23. package/filters/merge.js +51 -0
  24. package/filters/merge.test.js +51 -0
  25. package/filters/remove_tag.js +42 -0
  26. package/filters/remove_tag.test.js +60 -0
  27. package/filters/section.js +82 -0
  28. package/filters/section.test.js +174 -0
  29. package/filters/split.js +9 -0
  30. package/filters/split.test.js +39 -0
  31. package/filters/strip_tag.js +39 -0
  32. package/filters/strip_tag.test.js +74 -0
  33. package/filters/unindent.js +11 -0
  34. package/filters/unindent.test.js +49 -0
  35. package/package.json +48 -0
  36. package/plugin.js +53 -0
  37. package/plugin.test.js +8 -0
  38. package/scripts/README.md +59 -0
  39. package/scripts/package.json +17 -0
@@ -0,0 +1,51 @@
1
+ /*<!--section:docs-->
2
+
3
+ A filter that shallow-merges objects together, similar to Twig's merge filter. Later values override earlier ones. Non-object arguments are ignored.
4
+
5
+ <!--section:code-->```js */
6
+ export default function (first, ...rest) {
7
+ // If first argument is null or undefined, treat as empty object
8
+ if (first === null || first === undefined) {
9
+ first = {};
10
+ }
11
+
12
+ // Only support objects
13
+ if (typeof first === "object" && !Array.isArray(first)) {
14
+ // Merge objects using spread operator (shallow merge)
15
+ return rest.reduce(
16
+ (acc, item) => {
17
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
18
+ return { ...acc, ...item };
19
+ }
20
+ return acc;
21
+ },
22
+ { ...first },
23
+ );
24
+ }
25
+
26
+ // If first is not an object, return empty object
27
+ return {};
28
+ }
29
+ /*```<!--section:docs-->
30
+
31
+ ### Examples <!-- @TODO: better examples -->
32
+
33
+ ```jinja2
34
+ {# Merge configuration objects #}
35
+ {% set defaultConfig = { theme: 'light', lang: 'en' } %}
36
+ {% set userConfig = { theme: 'dark' } %}
37
+ {% set finalConfig = defaultConfig | merge(userConfig) %}
38
+
39
+ {# Result: { theme: 'dark', lang: 'en' } #}
40
+ ```
41
+
42
+ ```jinja2
43
+ {# Merge page metadata with defaults #}
44
+ {% set defaultMeta = {
45
+ author: 'Site Admin',
46
+ category: 'general',
47
+ comments: false
48
+ } %}
49
+ {% set pageMeta = defaultMeta | merge(page.data) %}
50
+ ```
51
+ */
@@ -0,0 +1,51 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import merge from './merge.js';
4
+
5
+ test('merge - merges two objects', () => {
6
+ const result = merge({ a: 1, b: 2 }, { c: 3, d: 4 });
7
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3, d: 4 });
8
+ });
9
+
10
+ test('merge - merges objects with override', () => {
11
+ const result = merge({ a: 1, b: 2 }, { b: 3, c: 4 });
12
+ assert.deepStrictEqual(result, { a: 1, b: 3, c: 4 });
13
+ });
14
+
15
+ test('merge - merges multiple objects', () => {
16
+ const result = merge({ a: 1 }, { b: 2 }, { c: 3 });
17
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
18
+ });
19
+
20
+ test('merge - handles null first argument', () => {
21
+ const result = merge(null, { a: 1 });
22
+ assert.deepStrictEqual(result, { a: 1 });
23
+ });
24
+
25
+ test('merge - handles undefined first argument', () => {
26
+ const result = merge(undefined, { a: 1 });
27
+ assert.deepStrictEqual(result, { a: 1 });
28
+ });
29
+
30
+ test('merge - does not modify original objects', () => {
31
+ const original = { a: 1 };
32
+ const result = merge(original, { b: 2 });
33
+
34
+ assert.deepStrictEqual(original, { a: 1 });
35
+ assert.deepStrictEqual(result, { a: 1, b: 2 });
36
+ });
37
+
38
+ test('merge - returns empty object for arrays', () => {
39
+ const result = merge([1, 2], [3, 4]);
40
+ assert.deepStrictEqual(result, {});
41
+ });
42
+
43
+ test('merge - returns empty object for primitives', () => {
44
+ const result = merge('string', { a: 1 });
45
+ assert.deepStrictEqual(result, {});
46
+ });
47
+
48
+ test('merge - ignores non-object arguments in rest', () => {
49
+ const result = merge({ a: 1 }, 'string', { b: 2 }, null, { c: 3 });
50
+ assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
51
+ });
@@ -0,0 +1,42 @@
1
+ /*<!--section:docs-->
2
+
3
+ A filter that removes a specified HTML element from provided HTML content. It removes the tag along with its content, including self-closing tags.
4
+
5
+ **Security note:** 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.
6
+
7
+ <!--section:code-->```js */
8
+ export default function (html, tagName) {
9
+ if (!html || typeof html !== "string") {
10
+ return html;
11
+ }
12
+
13
+ if (typeof tagName !== "string" || !tagName) {
14
+ return html;
15
+ }
16
+
17
+ // Escape special regex characters in tag name
18
+ const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+
20
+ // Remove opening and closing tags along with their content
21
+ // This regex matches: <tag attributes>content</tag>
22
+ const regex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>.*?<\\/${escapedTag}>`, "gis");
23
+ let result = html.replace(regex, "");
24
+
25
+ // Also remove self-closing tags: <tag />
26
+ const selfClosingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?\\s*\\/?>`, "gi");
27
+ result = result.replace(selfClosingRegex, "");
28
+
29
+ return result;
30
+ }
31
+ /*```<!--section:docs-->
32
+
33
+ ### Examples <!-- @TODO: better examples -->
34
+
35
+ Remove all script tags from content
36
+
37
+ ```jinja2
38
+ {% set cleanContent = htmlContent | remove_tag('script') %}
39
+
40
+ {{ cleanContent | safe }}
41
+ ```
42
+ */
@@ -0,0 +1,60 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import removeTag from './remove_tag.js';
4
+
5
+ describe('removeTag', () => {
6
+ it('should remove a single tag with content', () => {
7
+ const html = '<div>Keep this</div><script>Remove this</script><p>Keep this too</p>';
8
+ const result = removeTag(html, 'script');
9
+
10
+ assert.strictEqual(result, '<div>Keep this</div><p>Keep this too</p>');
11
+ });
12
+
13
+ it('should remove multiple instances of the same tag', () => {
14
+ const html = '<p>First</p><script>One</script><p>Second</p><script>Two</script><p>Third</p>';
15
+ const result = removeTag(html, 'script');
16
+
17
+ assert.strictEqual(result, '<p>First</p><p>Second</p><p>Third</p>');
18
+ });
19
+
20
+ it('should handle tags with attributes', () => {
21
+ const html = '<div>Keep</div><script type="text/javascript" src="file.js">Code</script><p>Keep</p>';
22
+ const result = removeTag(html, 'script');
23
+
24
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
25
+ });
26
+
27
+ it('should handle self-closing tags', () => {
28
+ const html = '<div>Keep</div><br /><p>Keep</p>';
29
+ const result = removeTag(html, 'br');
30
+
31
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
32
+ });
33
+
34
+ it('should return original HTML if tag does not exist', () => {
35
+ const html = '<div>Keep this</div><p>Keep this too</p>';
36
+ const result = removeTag(html, 'script');
37
+
38
+ assert.strictEqual(result, html);
39
+ });
40
+
41
+ it('should handle empty or null input', () => {
42
+ assert.strictEqual(removeTag('', 'script'), '');
43
+ assert.strictEqual(removeTag(null, 'script'), null);
44
+ assert.strictEqual(removeTag(undefined, 'script'), undefined);
45
+ });
46
+
47
+ it('should be case-insensitive', () => {
48
+ const html = '<div>Keep</div><SCRIPT>Remove</SCRIPT><Script>Remove</Script><p>Keep</p>';
49
+ const result = removeTag(html, 'script');
50
+
51
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
52
+ });
53
+
54
+ it('should handle nested content', () => {
55
+ const html = '<div>Keep</div><script><div>Nested</div></script><p>Keep</p>';
56
+ const result = removeTag(html, 'script');
57
+
58
+ assert.strictEqual(result, '<div>Keep</div><p>Keep</p>');
59
+ });
60
+ });
@@ -0,0 +1,82 @@
1
+ /*<!--section:docs-->
2
+
3
+ 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.
4
+
5
+ <!--section:code-->```js */
6
+ export default function (content, sectionName) {
7
+ if (!content || typeof content !== "string") {
8
+ return content;
9
+ }
10
+
11
+ if (typeof sectionName !== "string" || !sectionName) {
12
+ return "";
13
+ }
14
+
15
+ // Normalize section name for comparison (trim whitespace)
16
+ const targetName = sectionName.trim().toLowerCase();
17
+
18
+ // Regex to match section markers with content up to the next section or end of string
19
+ // Captures: (1) section names, (2) content until next section marker or end
20
+ const sectionRegex = /<!--section:([^>]+)-->([\s\S]*?)(?=<!--section|$)/g;
21
+
22
+ let results = [];
23
+ let match;
24
+
25
+ // Find all sections
26
+ while ((match = sectionRegex.exec(content)) !== null) {
27
+ const namesStr = match[1];
28
+ const sectionContent = match[2];
29
+ const names = namesStr.split(",").map((n) => n.trim().toLowerCase());
30
+
31
+ // Check if any of the names match the target
32
+ if (names.includes(targetName)) {
33
+ results.push(sectionContent);
34
+ }
35
+ }
36
+
37
+ // Join all matching sections
38
+ return results.join("");
39
+ }
40
+ /*```<!--section:docs-->
41
+
42
+ ## Usage <!-- @TODO: better examples -->
43
+
44
+ 1. Mark sections in your content file (e.g., `post.md`):
45
+
46
+ ⚠️ `NOTE:` The `¡` symbol is used instead of `!` only to give examples below. Use `!` in your actual content files.
47
+
48
+ ```markdown
49
+ # My Post
50
+
51
+ <¡--section:intro-->
52
+
53
+ This is the introduction that appears at the top of the page.
54
+
55
+ <¡--section:main-->
56
+
57
+ This is the main body of the post with all the details.
58
+
59
+ <¡--section:summary,sidebar-->
60
+
61
+ This content appears in both the summary and the sidebar!
62
+ ```
63
+
64
+ 2. Use the filter in your templates:
65
+
66
+ ```jinja2
67
+ {# Get the intro section #}
68
+ <div class="page-intro">
69
+ {{ content | section('intro') | safe }}
70
+ </div>
71
+
72
+ {# Get the main section #}
73
+ <article>
74
+ {{ content | section('main') | safe }}
75
+ </article>
76
+
77
+ {# Get the sidebar section #}
78
+ <aside>
79
+ {{ content | section('sidebar') | safe }}
80
+ </aside>
81
+ ```
82
+ */
@@ -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
+ });
@@ -0,0 +1,9 @@
1
+ /*<!--section:docs-->
2
+
3
+ Split a string into an array by a separator.
4
+
5
+ <!--section:code-->```js */
6
+ export default function (str, sep) {
7
+ return String(str ?? "").split(sep);
8
+ }
9
+ //```
@@ -0,0 +1,39 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import split from "./split.js";
4
+
5
+ test("split - splits by a single character separator", () => {
6
+ assert.deepStrictEqual(split("a,b,c", ","), ["a", "b", "c"]);
7
+ });
8
+
9
+ test("split - splits by a string separator", () => {
10
+ assert.deepStrictEqual(split("foo::bar::baz", "::"), ["foo", "bar", "baz"]);
11
+ });
12
+
13
+ test("split - splits by empty string into characters", () => {
14
+ assert.deepStrictEqual(split("abc", ""), ["a", "b", "c"]);
15
+ });
16
+
17
+ test("split - returns full string in array when separator not found", () => {
18
+ assert.deepStrictEqual(split("hello", ","), ["hello"]);
19
+ });
20
+
21
+ test("split - handles empty string input", () => {
22
+ assert.deepStrictEqual(split("", ","), [""]);
23
+ });
24
+
25
+ test("split - handles null input", () => {
26
+ assert.deepStrictEqual(split(null, ","), [""]);
27
+ });
28
+
29
+ test("split - handles undefined input", () => {
30
+ assert.deepStrictEqual(split(undefined, ","), [""]);
31
+ });
32
+
33
+ test("split - splits on whitespace", () => {
34
+ assert.deepStrictEqual(split("foo bar baz", " "), ["foo", "bar", "baz"]);
35
+ });
36
+
37
+ test("split - splits newlines", () => {
38
+ assert.deepStrictEqual(split("line1\nline2\nline3", "\n"), ["line1", "line2", "line3"]);
39
+ });
@@ -0,0 +1,39 @@
1
+ /*<!--section:docs-->
2
+
3
+ A filter that strips a specified HTML element from content while keeping its inner content intact. Only the opening and closing tags are removed; everything inside the tag is preserved in place.
4
+
5
+ <!--section:code-->```js */
6
+ export default function (html, tagName) {
7
+ if (!html || typeof html !== "string") {
8
+ return html;
9
+ }
10
+
11
+ if (typeof tagName !== "string" || !tagName) {
12
+ return html;
13
+ }
14
+
15
+ // Escape special regex characters in tag name
16
+ const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+
18
+ // Remove opening tags (with optional attributes): <tag> or <tag attr="val">
19
+ const openingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>`, "gi");
20
+ let result = html.replace(openingRegex, "");
21
+
22
+ // Remove closing tags: </tag>
23
+ const closingRegex = new RegExp(`<\\/${escapedTag}>`, "gi");
24
+ result = result.replace(closingRegex, "");
25
+
26
+ return result;
27
+ }
28
+ /*```<!--section:docs-->
29
+
30
+ ### Examples
31
+
32
+ Unwrap a wrapping `<div>` from content:
33
+
34
+ ```jinja2
35
+ {% set unwrapped = htmlContent | strip_tag('div') %}
36
+
37
+ {{ unwrapped | safe }}
38
+ ```
39
+ */
@@ -0,0 +1,74 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import stripTag from "./strip_tag.js";
4
+
5
+ describe("stripTag", () => {
6
+ it("should strip a tag but keep its inner content", () => {
7
+ const html = "<div><p>Keep this</p></div>";
8
+ const result = stripTag(html, "div");
9
+
10
+ assert.strictEqual(result, "<p>Keep this</p>");
11
+ });
12
+
13
+ it("should strip multiple instances of the same tag", () => {
14
+ const html = "<div>First</div><p>Middle</p><div>Second</div>";
15
+ const result = stripTag(html, "div");
16
+
17
+ assert.strictEqual(result, "First<p>Middle</p>Second");
18
+ });
19
+
20
+ it("should handle tags with attributes", () => {
21
+ const html = '<div class="wrapper" id="main">Content</div>';
22
+ const result = stripTag(html, "div");
23
+
24
+ assert.strictEqual(result, "Content");
25
+ });
26
+
27
+ it("should only strip the specified tag, leaving others intact", () => {
28
+ const html = "<div><span>Keep span</span><em>Keep em</em></div>";
29
+ const result = stripTag(html, "div");
30
+
31
+ assert.strictEqual(result, "<span>Keep span</span><em>Keep em</em>");
32
+ });
33
+
34
+ it("should handle nested content with the same tag", () => {
35
+ const html = "<div>outer <div>inner</div> text</div>";
36
+ const result = stripTag(html, "div");
37
+
38
+ assert.strictEqual(result, "outer inner text");
39
+ });
40
+
41
+ it("should return original HTML if tag does not exist", () => {
42
+ const html = "<p>Some text</p>";
43
+ const result = stripTag(html, "div");
44
+
45
+ assert.strictEqual(result, html);
46
+ });
47
+
48
+ it("should handle empty or null input", () => {
49
+ assert.strictEqual(stripTag("", "div"), "");
50
+ assert.strictEqual(stripTag(null, "div"), null);
51
+ assert.strictEqual(stripTag(undefined, "div"), undefined);
52
+ });
53
+
54
+ it("should handle missing or invalid tagName", () => {
55
+ const html = "<div>Content</div>";
56
+ assert.strictEqual(stripTag(html, ""), html);
57
+ assert.strictEqual(stripTag(html, null), html);
58
+ assert.strictEqual(stripTag(html, undefined), html);
59
+ });
60
+
61
+ it("should be case-insensitive", () => {
62
+ const html = '<DIV class="foo">Content</DIV>';
63
+ const result = stripTag(html, "div");
64
+
65
+ assert.strictEqual(result, "Content");
66
+ });
67
+
68
+ it("should preserve whitespace and newlines inside the tag", () => {
69
+ const html = "<div>\n <p>Line 1</p>\n <p>Line 2</p>\n</div>";
70
+ const result = stripTag(html, "div");
71
+
72
+ assert.strictEqual(result, "\n <p>Line 1</p>\n <p>Line 2</p>\n");
73
+ });
74
+ });
@@ -0,0 +1,11 @@
1
+ /*<!--section:docs-->
2
+
3
+ Remove the minimal common indentation from a multi-line string.
4
+
5
+ <!--section:code-->```js */
6
+ export default function (content) {
7
+ const lines = String(content ?? "").split("\n");
8
+ const minIndent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^(\s*)/)[1].length));
9
+ return lines.map((l) => l.slice(minIndent)).join("\n");
10
+ }
11
+ //```
@@ -0,0 +1,49 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import unindent from "./unindent.js";
4
+
5
+ test("unindent - removes common leading spaces", () => {
6
+ const input = " hello\n world";
7
+ assert.strictEqual(unindent(input), "hello\nworld");
8
+ });
9
+
10
+ test("unindent - removes common leading tabs", () => {
11
+ const input = "\tfoo\n\tbar";
12
+ assert.strictEqual(unindent(input), "foo\nbar");
13
+ });
14
+
15
+ test("unindent - preserves relative indentation", () => {
16
+ const input = " if true\n inner\n end";
17
+ assert.strictEqual(unindent(input), "if true\n inner\nend");
18
+ });
19
+
20
+ test("unindent - ignores blank lines when computing min indent", () => {
21
+ const input = " line1\n\n line2";
22
+ assert.strictEqual(unindent(input), "line1\n\nline2");
23
+ });
24
+
25
+ test("unindent - does nothing when already at zero indent", () => {
26
+ const input = "hello\nworld";
27
+ assert.strictEqual(unindent(input), "hello\nworld");
28
+ });
29
+
30
+ test("unindent - handles single line", () => {
31
+ assert.strictEqual(unindent(" hello"), "hello");
32
+ });
33
+
34
+ test("unindent - handles null input", () => {
35
+ assert.strictEqual(unindent(null), "");
36
+ });
37
+
38
+ test("unindent - handles undefined input", () => {
39
+ assert.strictEqual(unindent(undefined), "");
40
+ });
41
+
42
+ test("unindent - handles empty string", () => {
43
+ assert.strictEqual(unindent(""), "");
44
+ });
45
+
46
+ test("unindent - mixed indent levels, strips only the minimum", () => {
47
+ const input = " a\n b\n c";
48
+ assert.strictEqual(unindent(input), "a\n b\nc");
49
+ });