astro-helmet 0.1.2 → 0.2.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 +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +34 -8
- package/dist/index.js.map +1 -1
- package/dist/renderAttrs.test.d.ts +1 -0
- package/dist/renderAttrs.test.js +109 -0
- package/dist/renderAttrs.test.js.map +1 -0
- package/dist/renderHead.test.d.ts +1 -0
- package/dist/renderHead.test.js +124 -0
- package/dist/renderHead.test.js.map +1 -0
- package/package.json +4 -2
- package/src/index.ts +36 -9
- package/src/renderAttrs.test.ts +113 -0
- package/src/renderHead.test.ts +131 -0
- package/src/index.test.ts +0 -72
package/README.md
CHANGED
|
@@ -58,8 +58,9 @@ Then add the rendered `head` string to your Astro layout:
|
|
|
58
58
|
|
|
59
59
|
| priority | item |
|
|
60
60
|
| -------- | --------------------------------------------- |
|
|
61
|
-
| \-
|
|
62
|
-
| \-
|
|
61
|
+
| \-4 | `<meta charset="">` |
|
|
62
|
+
| \-3 | `<meta name="viewport">` |
|
|
63
|
+
| \-2 | `<base ="">` |
|
|
63
64
|
| \-1 | `<meta http-equiv="">` |
|
|
64
65
|
| 0 | `<title>` |
|
|
65
66
|
| 10 | `<link rel="preconnect" />` |
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,12 +3,20 @@ const DEFAULT_VIEWPORT = {
|
|
|
3
3
|
name: 'viewport',
|
|
4
4
|
content: 'width=device-width, initial-scale=1'
|
|
5
5
|
};
|
|
6
|
+
const tagNames = [
|
|
7
|
+
'base',
|
|
8
|
+
'meta',
|
|
9
|
+
'link',
|
|
10
|
+
'style',
|
|
11
|
+
'script',
|
|
12
|
+
'noscript'
|
|
13
|
+
];
|
|
14
|
+
// TODO: takes either a single HeadItems object or an array of HeadItems
|
|
6
15
|
export function renderHead(headItems) {
|
|
7
16
|
const items = mergeHeadItems(headItems);
|
|
8
17
|
if (!items.title?.length)
|
|
9
18
|
throw new Error('Missing title tag.');
|
|
10
19
|
const tags = [];
|
|
11
|
-
const tagNames = ['meta', 'link', 'style', 'script', 'noscript'];
|
|
12
20
|
tagNames.forEach((tag) => {
|
|
13
21
|
tags.push(...items[tag].map((item) => ({ ...item, tagName: tag })));
|
|
14
22
|
});
|
|
@@ -30,6 +38,7 @@ export function renderHead(headItems) {
|
|
|
30
38
|
function mergeHeadItems(items) {
|
|
31
39
|
const mergedHeadItems = {
|
|
32
40
|
title: '',
|
|
41
|
+
base: [],
|
|
33
42
|
meta: [],
|
|
34
43
|
link: [],
|
|
35
44
|
style: [],
|
|
@@ -39,6 +48,8 @@ function mergeHeadItems(items) {
|
|
|
39
48
|
items.forEach((item) => {
|
|
40
49
|
if (item.title && item.title.length)
|
|
41
50
|
mergedHeadItems.title = item.title;
|
|
51
|
+
if (item.base)
|
|
52
|
+
mergedHeadItems.base.push(...item.base);
|
|
42
53
|
if (item.meta)
|
|
43
54
|
mergedHeadItems.meta.push(...item.meta);
|
|
44
55
|
if (item.link)
|
|
@@ -51,6 +62,7 @@ function mergeHeadItems(items) {
|
|
|
51
62
|
mergedHeadItems.noscript.push(...item.noscript);
|
|
52
63
|
});
|
|
53
64
|
mergedHeadItems.meta = deduplicateMetaItems(mergedHeadItems.meta);
|
|
65
|
+
mergedHeadItems.base = mergedHeadItems.base.slice(-1);
|
|
54
66
|
return mergedHeadItems;
|
|
55
67
|
}
|
|
56
68
|
function applyDefaultPriorities(tags) {
|
|
@@ -65,11 +77,14 @@ function applyDefaultPriorities(tags) {
|
|
|
65
77
|
unprioritisedTags.forEach((tag) => {
|
|
66
78
|
let priority;
|
|
67
79
|
switch (tag.tagName) {
|
|
80
|
+
case 'base':
|
|
81
|
+
priority = -2;
|
|
82
|
+
break;
|
|
68
83
|
case 'meta':
|
|
69
84
|
if (tag.charset)
|
|
70
|
-
priority = -
|
|
85
|
+
priority = -4;
|
|
71
86
|
else if (tag.name === 'viewport')
|
|
72
|
-
priority = -
|
|
87
|
+
priority = -3;
|
|
73
88
|
else if (tag['http-equiv'])
|
|
74
89
|
priority = -1;
|
|
75
90
|
else
|
|
@@ -107,9 +122,9 @@ function applyDefaultPriorities(tags) {
|
|
|
107
122
|
}
|
|
108
123
|
function applyDefaultTags(tags) {
|
|
109
124
|
if (!tags.some((tag) => tag.tagName === 'meta' && tag.charset))
|
|
110
|
-
tags.push({ ...DEFAULT_CHARSET, tagName: 'meta', priority: -
|
|
125
|
+
tags.push({ ...DEFAULT_CHARSET, tagName: 'meta', priority: -4 });
|
|
111
126
|
if (!tags.some((tag) => tag.tagName === 'meta' && tag.name === 'viewport'))
|
|
112
|
-
tags.push({ ...DEFAULT_VIEWPORT, tagName: 'meta', priority: -
|
|
127
|
+
tags.push({ ...DEFAULT_VIEWPORT, tagName: 'meta', priority: -3 });
|
|
113
128
|
return tags;
|
|
114
129
|
}
|
|
115
130
|
function deduplicateMetaItems(metaItems) {
|
|
@@ -123,8 +138,8 @@ function deduplicateMetaItems(metaItems) {
|
|
|
123
138
|
}
|
|
124
139
|
function renderHeadTag(item) {
|
|
125
140
|
const attrs = renderAttrs(item);
|
|
126
|
-
return ['meta', 'link'].includes(item.tagName)
|
|
127
|
-
? `<${item.tagName} ${attrs}
|
|
141
|
+
return ['meta', 'link', 'base'].includes(item.tagName)
|
|
142
|
+
? `<${item.tagName} ${attrs}>`
|
|
128
143
|
: `<${item.tagName}${attrs && ' '}${attrs}>${item.innerHTML || ''}</${item.tagName}>`;
|
|
129
144
|
}
|
|
130
145
|
export function renderAttrs(item) {
|
|
@@ -133,10 +148,21 @@ export function renderAttrs(item) {
|
|
|
133
148
|
.map(([key, value]) => {
|
|
134
149
|
if (typeof value === 'boolean')
|
|
135
150
|
return value ? key : '';
|
|
151
|
+
else if (value === null || value === undefined)
|
|
152
|
+
return '';
|
|
136
153
|
else
|
|
137
|
-
return `${key}="${value}"`;
|
|
154
|
+
return `${key}="${escapeHtml(String(value))}"`;
|
|
138
155
|
})
|
|
139
156
|
.filter((attr) => attr !== '')
|
|
140
157
|
.join(' ');
|
|
141
158
|
}
|
|
159
|
+
function escapeHtml(str) {
|
|
160
|
+
const escapeMap = {
|
|
161
|
+
'"': '"',
|
|
162
|
+
'&': '&',
|
|
163
|
+
'<': '<',
|
|
164
|
+
'>': '>'
|
|
165
|
+
};
|
|
166
|
+
return str.replace(/["&<>]/g, (match) => escapeMap[match] || match);
|
|
167
|
+
}
|
|
142
168
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;AAC5C,MAAM,gBAAgB,GAAG;IACxB,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,qCAAqC;CAC9C,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { renderAttrs } from './index';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
const testCases = [
|
|
4
|
+
{
|
|
5
|
+
description: 'Preload a font with standard attributes',
|
|
6
|
+
attributes: {
|
|
7
|
+
rel: 'preload',
|
|
8
|
+
as: 'font',
|
|
9
|
+
type: 'font/woff2',
|
|
10
|
+
crossorigin: 'anonymous',
|
|
11
|
+
href: '/fonts/InterVariable.woff2'
|
|
12
|
+
},
|
|
13
|
+
expected: 'rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href="/fonts/InterVariable.woff2"'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
description: 'Link a stylesheet with standard attributes',
|
|
17
|
+
attributes: {
|
|
18
|
+
rel: 'stylesheet',
|
|
19
|
+
href: '/css/styles.css'
|
|
20
|
+
},
|
|
21
|
+
expected: 'rel="stylesheet" href="/css/styles.css"'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
description: "Preload font with missing 'as' attribute",
|
|
25
|
+
attributes: {
|
|
26
|
+
rel: 'preload',
|
|
27
|
+
type: 'font/woff2',
|
|
28
|
+
crossorigin: 'anonymous',
|
|
29
|
+
href: '/fonts/InterVariable.woff2'
|
|
30
|
+
},
|
|
31
|
+
expected: 'rel="preload" type="font/woff2" crossorigin="anonymous" href="/fonts/InterVariable.woff2"'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: 'Use non-standard rel attribute',
|
|
35
|
+
attributes: {
|
|
36
|
+
rel: 'data',
|
|
37
|
+
href: '/data/config.json'
|
|
38
|
+
},
|
|
39
|
+
expected: 'rel="data" href="/data/config.json"'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
description: 'Link tag with invalid type attribute for CSS',
|
|
43
|
+
attributes: {
|
|
44
|
+
rel: 'stylesheet',
|
|
45
|
+
type: 'text/plain',
|
|
46
|
+
href: '/css/styles.css'
|
|
47
|
+
},
|
|
48
|
+
expected: 'rel="stylesheet" type="text/plain" href="/css/styles.css"'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description: 'Attributes with boolean values',
|
|
52
|
+
attributes: {
|
|
53
|
+
async: true,
|
|
54
|
+
defer: false,
|
|
55
|
+
src: '/js/script.js'
|
|
56
|
+
},
|
|
57
|
+
expected: 'async src="/js/script.js"'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
description: 'Attributes with special characters',
|
|
61
|
+
attributes: {
|
|
62
|
+
'data-test': 'value with spaces',
|
|
63
|
+
'data-test2': 'value with "quotes"',
|
|
64
|
+
src: '/js/script.js'
|
|
65
|
+
},
|
|
66
|
+
expected: 'data-test="value with spaces" data-test2="value with "quotes"" src="/js/script.js"'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
description: 'Attributes with empty values',
|
|
70
|
+
attributes: {
|
|
71
|
+
rel: '',
|
|
72
|
+
href: '/css/styles.css'
|
|
73
|
+
},
|
|
74
|
+
expected: 'rel="" href="/css/styles.css"'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
description: 'Attributes with undefined values',
|
|
78
|
+
attributes: {
|
|
79
|
+
rel: undefined,
|
|
80
|
+
href: '/css/styles.css'
|
|
81
|
+
},
|
|
82
|
+
expected: 'href="/css/styles.css"'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
description: 'Attributes with null values',
|
|
86
|
+
attributes: {
|
|
87
|
+
rel: null,
|
|
88
|
+
href: '/css/styles.css'
|
|
89
|
+
},
|
|
90
|
+
expected: 'href="/css/styles.css"'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
description: 'Attributes with number values',
|
|
94
|
+
attributes: {
|
|
95
|
+
width: 100,
|
|
96
|
+
height: 200,
|
|
97
|
+
src: '/img/image.jpg'
|
|
98
|
+
},
|
|
99
|
+
expected: 'width="100" height="200" src="/img/image.jpg"'
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
describe('renderAttrs', () => {
|
|
103
|
+
testCases.forEach(({ description, attributes, expected }) => {
|
|
104
|
+
it(description, () => {
|
|
105
|
+
expect(renderAttrs(attributes)).toEqual(expected);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=renderAttrs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderAttrs.test.js","sourceRoot":"","sources":["../src/renderAttrs.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE7C,MAAM,SAAS,GAAG;IACjB;QACC,WAAW,EAAE,yCAAyC;QACtD,UAAU,EAAE;YACX,GAAG,EAAE,SAAS;YACd,EAAE,EAAE,MAAM;YACV,IAAI,EAAE,YAAY;YAClB,WAAW,EAAE,WAAW;YACxB,IAAI,EAAE,4BAA4B;SAClC;QACD,QAAQ,EACP,qGAAqG;KACtG;IACD;QACC,WAAW,EAAE,4CAA4C;QACzD,UAAU,EAAE;YACX,GAAG,EAAE,YAAY;YACjB,IAAI,EAAE,iBAAiB;SACvB;QACD,QAAQ,EAAE,yCAAyC;KACnD;IACD;QACC,WAAW,EAAE,0CAA0C;QACvD,UAAU,EAAE;YACX,GAAG,EAAE,SAAS;YACd,IAAI,EAAE,YAAY;YAClB,WAAW,EAAE,WAAW;YACxB,IAAI,EAAE,4BAA4B;SAClC;QACD,QAAQ,EACP,2FAA2F;KAC5F;IACD;QACC,WAAW,EAAE,gCAAgC;QAC7C,UAAU,EAAE;YACX,GAAG,EAAE,MAAM;YACX,IAAI,EAAE,mBAAmB;SACzB;QACD,QAAQ,EAAE,qCAAqC;KAC/C;IACD;QACC,WAAW,EAAE,8CAA8C;QAC3D,UAAU,EAAE;YACX,GAAG,EAAE,YAAY;YACjB,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,iBAAiB;SACvB;QACD,QAAQ,EAAE,2DAA2D;KACrE;IACD;QACC,WAAW,EAAE,gCAAgC;QAC7C,UAAU,EAAE;YACX,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,KAAK;YACZ,GAAG,EAAE,eAAe;SACpB;QACD,QAAQ,EAAE,2BAA2B;KACrC;IACD;QACC,WAAW,EAAE,oCAAoC;QACjD,UAAU,EAAE;YACX,WAAW,EAAE,mBAAmB;YAChC,YAAY,EAAE,qBAAqB;YACnC,GAAG,EAAE,eAAe;SACpB;QACD,QAAQ,EACP,8FAA8F;KAC/F;IACD;QACC,WAAW,EAAE,8BAA8B;QAC3C,UAAU,EAAE;YACX,GAAG,EAAE,EAAE;YACP,IAAI,EAAE,iBAAiB;SACvB;QACD,QAAQ,EAAE,+BAA+B;KACzC;IACD;QACC,WAAW,EAAE,kCAAkC;QAC/C,UAAU,EAAE;YACX,GAAG,EAAE,SAAS;YACd,IAAI,EAAE,iBAAiB;SACvB;QACD,QAAQ,EAAE,wBAAwB;KAClC;IACD;QACC,WAAW,EAAE,6BAA6B;QAC1C,UAAU,EAAE;YACX,GAAG,EAAE,IAAI;YACT,IAAI,EAAE,iBAAiB;SACvB;QACD,QAAQ,EAAE,wBAAwB;KAClC;IACD;QACC,WAAW,EAAE,+BAA+B;QAC5C,UAAU,EAAE;YACX,KAAK,EAAE,GAAG;YACV,MAAM,EAAE,GAAG;YACX,GAAG,EAAE,gBAAgB;SACrB;QACD,QAAQ,EAAE,+CAA+C;KACzD;CACD,CAAA;AAED,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC5B,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC3D,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;YACpB,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;IACH,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { renderHead } from './index';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
const testCases = [
|
|
4
|
+
{
|
|
5
|
+
description: 'Render head with minimal content',
|
|
6
|
+
params: [
|
|
7
|
+
{
|
|
8
|
+
title: 'My Site Title'
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
expected: `<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13
|
+
<title>My Site Title</title>`
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
description: 'Render head with base tag',
|
|
17
|
+
params: [
|
|
18
|
+
{
|
|
19
|
+
title: 'My Site Title',
|
|
20
|
+
base: [{ href: 'https://example.com' }]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
expected: `<meta charset="UTF-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
25
|
+
<base href="https://example.com">
|
|
26
|
+
<title>My Site Title</title>`
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
description: 'Render head with meta tags',
|
|
30
|
+
params: [
|
|
31
|
+
{
|
|
32
|
+
title: 'My Site Title',
|
|
33
|
+
meta: [{ name: 'description', content: 'My site description' }]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
expected: `<meta charset="UTF-8">
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
38
|
+
<title>My Site Title</title>
|
|
39
|
+
<meta name="description" content="My site description">`
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
description: 'Render head with link tags',
|
|
43
|
+
params: [
|
|
44
|
+
{
|
|
45
|
+
title: 'My Site Title',
|
|
46
|
+
link: [{ rel: 'stylesheet', href: 'styles.css' }]
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
expected: `<meta charset="UTF-8">
|
|
50
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
51
|
+
<title>My Site Title</title>
|
|
52
|
+
<link rel="stylesheet" href="styles.css">`
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
description: 'Render head with style tags',
|
|
56
|
+
params: [
|
|
57
|
+
{
|
|
58
|
+
title: 'My Site Title',
|
|
59
|
+
style: [{ innerHTML: 'body { color: red; }' }]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
expected: `<meta charset="UTF-8">
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
64
|
+
<title>My Site Title</title>
|
|
65
|
+
<style>body { color: red; }</style>`
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
description: 'Render head with script tags',
|
|
69
|
+
params: [
|
|
70
|
+
{
|
|
71
|
+
title: 'My Site Title',
|
|
72
|
+
script: [{ innerHTML: 'console.log("Hello, world!")' }]
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
expected: `<meta charset="UTF-8">
|
|
76
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
77
|
+
<title>My Site Title</title>
|
|
78
|
+
<script>console.log("Hello, world!")</script>`
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
description: 'Render head with noscript tags',
|
|
82
|
+
params: [
|
|
83
|
+
{
|
|
84
|
+
title: 'My Site Title',
|
|
85
|
+
noscript: [{ innerHTML: 'Please enable JavaScript' }]
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
expected: `<meta charset="UTF-8">
|
|
89
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
90
|
+
<title>My Site Title</title>
|
|
91
|
+
<noscript>Please enable JavaScript</noscript>`
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
description: 'Render head with all tags',
|
|
95
|
+
params: [
|
|
96
|
+
{
|
|
97
|
+
title: 'My Site Title',
|
|
98
|
+
base: [{ href: 'https://example.com' }],
|
|
99
|
+
meta: [{ name: 'description', content: 'My site description' }],
|
|
100
|
+
link: [{ rel: 'stylesheet', href: 'styles.css' }],
|
|
101
|
+
style: [{ innerHTML: 'body { color: red; }' }],
|
|
102
|
+
script: [{ innerHTML: 'console.log("Hello, world!")' }],
|
|
103
|
+
noscript: [{ innerHTML: 'Please enable JavaScript' }]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
expected: `<meta charset="UTF-8">
|
|
107
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
108
|
+
<base href="https://example.com">
|
|
109
|
+
<title>My Site Title</title>
|
|
110
|
+
<script>console.log("Hello, world!")</script>
|
|
111
|
+
<link rel="stylesheet" href="styles.css">
|
|
112
|
+
<style>body { color: red; }</style>
|
|
113
|
+
<meta name="description" content="My site description">
|
|
114
|
+
<noscript>Please enable JavaScript</noscript>`
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
describe('renderHead', () => {
|
|
118
|
+
testCases.forEach(({ description, params, expected }) => {
|
|
119
|
+
it(description, () => {
|
|
120
|
+
expect(renderHead(params)).toEqual(expected);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=renderHead.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderHead.test.js","sourceRoot":"","sources":["../src/renderHead.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-helmet",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "A document head manager for astro.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc",
|
|
10
|
-
"test": "vitest"
|
|
10
|
+
"test": "vitest",
|
|
11
|
+
"coverage": "vitest run --coverage"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"astro",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"author": "Ryan Voitiskis",
|
|
23
24
|
"license": "ISC",
|
|
24
25
|
"devDependencies": {
|
|
26
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
25
27
|
"prettier": "^3.3.0",
|
|
26
28
|
"typescript": "^5.4.5",
|
|
27
29
|
"vitest": "^1.6.0"
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,15 @@ const DEFAULT_VIEWPORT = {
|
|
|
4
4
|
content: 'width=device-width, initial-scale=1'
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
type TagName = 'meta' | 'link' | 'style' | 'script' | 'noscript'
|
|
7
|
+
type TagName = 'base' | 'meta' | 'link' | 'style' | 'script' | 'noscript'
|
|
8
|
+
const tagNames: TagName[] = [
|
|
9
|
+
'base',
|
|
10
|
+
'meta',
|
|
11
|
+
'link',
|
|
12
|
+
'style',
|
|
13
|
+
'script',
|
|
14
|
+
'noscript'
|
|
15
|
+
]
|
|
8
16
|
|
|
9
17
|
type BaseItem = {
|
|
10
18
|
[key: string]: any
|
|
@@ -25,6 +33,7 @@ type PrioritisedTag = Tag & {
|
|
|
25
33
|
|
|
26
34
|
export type HeadItems = {
|
|
27
35
|
title?: string
|
|
36
|
+
base?: BaseItem[]
|
|
28
37
|
meta?: BaseItem[]
|
|
29
38
|
link?: BaseItem[]
|
|
30
39
|
style?: ContentItem[]
|
|
@@ -34,12 +43,12 @@ export type HeadItems = {
|
|
|
34
43
|
|
|
35
44
|
type MergedHeadItems = Required<HeadItems>
|
|
36
45
|
|
|
46
|
+
// TODO: takes either a single HeadItems object or an array of HeadItems
|
|
37
47
|
export function renderHead(headItems: HeadItems[]): string {
|
|
38
48
|
const items = mergeHeadItems(headItems)
|
|
39
49
|
if (!items.title?.length) throw new Error('Missing title tag.')
|
|
40
50
|
|
|
41
51
|
const tags: Tag[] = []
|
|
42
|
-
const tagNames: TagName[] = ['meta', 'link', 'style', 'script', 'noscript']
|
|
43
52
|
tagNames.forEach((tag) => {
|
|
44
53
|
tags.push(...items[tag].map((item) => ({ ...item, tagName: tag })))
|
|
45
54
|
})
|
|
@@ -66,6 +75,7 @@ export function renderHead(headItems: HeadItems[]): string {
|
|
|
66
75
|
function mergeHeadItems(items: HeadItems[]): MergedHeadItems {
|
|
67
76
|
const mergedHeadItems: MergedHeadItems = {
|
|
68
77
|
title: '',
|
|
78
|
+
base: [],
|
|
69
79
|
meta: [],
|
|
70
80
|
link: [],
|
|
71
81
|
style: [],
|
|
@@ -75,6 +85,7 @@ function mergeHeadItems(items: HeadItems[]): MergedHeadItems {
|
|
|
75
85
|
|
|
76
86
|
items.forEach((item) => {
|
|
77
87
|
if (item.title && item.title.length) mergedHeadItems.title = item.title
|
|
88
|
+
if (item.base) mergedHeadItems.base.push(...item.base)
|
|
78
89
|
if (item.meta) mergedHeadItems.meta.push(...item.meta)
|
|
79
90
|
if (item.link) mergedHeadItems.link.push(...item.link)
|
|
80
91
|
if (item.style) mergedHeadItems.style.push(...item.style)
|
|
@@ -83,6 +94,7 @@ function mergeHeadItems(items: HeadItems[]): MergedHeadItems {
|
|
|
83
94
|
})
|
|
84
95
|
|
|
85
96
|
mergedHeadItems.meta = deduplicateMetaItems(mergedHeadItems.meta)
|
|
97
|
+
mergedHeadItems.base = mergedHeadItems.base.slice(-1)
|
|
86
98
|
|
|
87
99
|
return mergedHeadItems
|
|
88
100
|
}
|
|
@@ -99,9 +111,12 @@ function applyDefaultPriorities(tags: Tag[]): PrioritisedTag[] {
|
|
|
99
111
|
unprioritisedTags.forEach((tag) => {
|
|
100
112
|
let priority: number
|
|
101
113
|
switch (tag.tagName) {
|
|
114
|
+
case 'base':
|
|
115
|
+
priority = -2
|
|
116
|
+
break
|
|
102
117
|
case 'meta':
|
|
103
|
-
if (tag.charset) priority = -
|
|
104
|
-
else if (tag.name === 'viewport') priority = -
|
|
118
|
+
if (tag.charset) priority = -4
|
|
119
|
+
else if (tag.name === 'viewport') priority = -3
|
|
105
120
|
else if (tag['http-equiv']) priority = -1
|
|
106
121
|
else priority = 100
|
|
107
122
|
break
|
|
@@ -135,10 +150,10 @@ function applyDefaultPriorities(tags: Tag[]): PrioritisedTag[] {
|
|
|
135
150
|
|
|
136
151
|
function applyDefaultTags(tags: PrioritisedTag[]): PrioritisedTag[] {
|
|
137
152
|
if (!tags.some((tag) => tag.tagName === 'meta' && tag.charset))
|
|
138
|
-
tags.push({ ...DEFAULT_CHARSET, tagName: 'meta', priority: -
|
|
153
|
+
tags.push({ ...DEFAULT_CHARSET, tagName: 'meta', priority: -4 })
|
|
139
154
|
|
|
140
155
|
if (!tags.some((tag) => tag.tagName === 'meta' && tag.name === 'viewport'))
|
|
141
|
-
tags.push({ ...DEFAULT_VIEWPORT, tagName: 'meta', priority: -
|
|
156
|
+
tags.push({ ...DEFAULT_VIEWPORT, tagName: 'meta', priority: -3 })
|
|
142
157
|
|
|
143
158
|
return tags
|
|
144
159
|
}
|
|
@@ -156,8 +171,8 @@ function deduplicateMetaItems(metaItems: BaseItem[]): BaseItem[] {
|
|
|
156
171
|
|
|
157
172
|
function renderHeadTag(item: BaseItem | ContentItem): string {
|
|
158
173
|
const attrs = renderAttrs(item)
|
|
159
|
-
return ['meta', 'link'].includes(item.tagName)
|
|
160
|
-
? `<${item.tagName} ${attrs}
|
|
174
|
+
return ['meta', 'link', 'base'].includes(item.tagName)
|
|
175
|
+
? `<${item.tagName} ${attrs}>`
|
|
161
176
|
: `<${item.tagName}${attrs && ' '}${attrs}>${item.innerHTML || ''}</${
|
|
162
177
|
item.tagName
|
|
163
178
|
}>`
|
|
@@ -168,8 +183,20 @@ export function renderAttrs(item: BaseItem | ContentItem): string {
|
|
|
168
183
|
.filter(([key]) => !['innerHTML', 'priority', 'tagName'].includes(key))
|
|
169
184
|
.map(([key, value]) => {
|
|
170
185
|
if (typeof value === 'boolean') return value ? key : ''
|
|
171
|
-
else return
|
|
186
|
+
else if (value === null || value === undefined) return ''
|
|
187
|
+
else return `${key}="${escapeHtml(String(value))}"`
|
|
172
188
|
})
|
|
173
189
|
.filter((attr) => attr !== '')
|
|
174
190
|
.join(' ')
|
|
175
191
|
}
|
|
192
|
+
|
|
193
|
+
function escapeHtml(str: string): string {
|
|
194
|
+
const escapeMap: Record<string, string> = {
|
|
195
|
+
'"': '"',
|
|
196
|
+
'&': '&',
|
|
197
|
+
'<': '<',
|
|
198
|
+
'>': '>'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return str.replace(/["&<>]/g, (match) => escapeMap[match] || match)
|
|
202
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { renderAttrs } from './index'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
|
|
4
|
+
const testCases = [
|
|
5
|
+
{
|
|
6
|
+
description: 'Preload a font with standard attributes',
|
|
7
|
+
attributes: {
|
|
8
|
+
rel: 'preload',
|
|
9
|
+
as: 'font',
|
|
10
|
+
type: 'font/woff2',
|
|
11
|
+
crossorigin: 'anonymous',
|
|
12
|
+
href: '/fonts/InterVariable.woff2'
|
|
13
|
+
},
|
|
14
|
+
expected:
|
|
15
|
+
'rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href="/fonts/InterVariable.woff2"'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
description: 'Link a stylesheet with standard attributes',
|
|
19
|
+
attributes: {
|
|
20
|
+
rel: 'stylesheet',
|
|
21
|
+
href: '/css/styles.css'
|
|
22
|
+
},
|
|
23
|
+
expected: 'rel="stylesheet" href="/css/styles.css"'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
description: "Preload font with missing 'as' attribute",
|
|
27
|
+
attributes: {
|
|
28
|
+
rel: 'preload',
|
|
29
|
+
type: 'font/woff2',
|
|
30
|
+
crossorigin: 'anonymous',
|
|
31
|
+
href: '/fonts/InterVariable.woff2'
|
|
32
|
+
},
|
|
33
|
+
expected:
|
|
34
|
+
'rel="preload" type="font/woff2" crossorigin="anonymous" href="/fonts/InterVariable.woff2"'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
description: 'Use non-standard rel attribute',
|
|
38
|
+
attributes: {
|
|
39
|
+
rel: 'data',
|
|
40
|
+
href: '/data/config.json'
|
|
41
|
+
},
|
|
42
|
+
expected: 'rel="data" href="/data/config.json"'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
description: 'Link tag with invalid type attribute for CSS',
|
|
46
|
+
attributes: {
|
|
47
|
+
rel: 'stylesheet',
|
|
48
|
+
type: 'text/plain',
|
|
49
|
+
href: '/css/styles.css'
|
|
50
|
+
},
|
|
51
|
+
expected: 'rel="stylesheet" type="text/plain" href="/css/styles.css"'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
description: 'Attributes with boolean values',
|
|
55
|
+
attributes: {
|
|
56
|
+
async: true,
|
|
57
|
+
defer: false,
|
|
58
|
+
src: '/js/script.js'
|
|
59
|
+
},
|
|
60
|
+
expected: 'async src="/js/script.js"'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
description: 'Attributes with special characters',
|
|
64
|
+
attributes: {
|
|
65
|
+
'data-test': 'value with spaces',
|
|
66
|
+
'data-test2': 'value with "quotes"',
|
|
67
|
+
src: '/js/script.js'
|
|
68
|
+
},
|
|
69
|
+
expected:
|
|
70
|
+
'data-test="value with spaces" data-test2="value with "quotes"" src="/js/script.js"'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
description: 'Attributes with empty values',
|
|
74
|
+
attributes: {
|
|
75
|
+
rel: '',
|
|
76
|
+
href: '/css/styles.css'
|
|
77
|
+
},
|
|
78
|
+
expected: 'rel="" href="/css/styles.css"'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
description: 'Attributes with undefined values',
|
|
82
|
+
attributes: {
|
|
83
|
+
rel: undefined,
|
|
84
|
+
href: '/css/styles.css'
|
|
85
|
+
},
|
|
86
|
+
expected: 'href="/css/styles.css"'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
description: 'Attributes with null values',
|
|
90
|
+
attributes: {
|
|
91
|
+
rel: null,
|
|
92
|
+
href: '/css/styles.css'
|
|
93
|
+
},
|
|
94
|
+
expected: 'href="/css/styles.css"'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
description: 'Attributes with number values',
|
|
98
|
+
attributes: {
|
|
99
|
+
width: 100,
|
|
100
|
+
height: 200,
|
|
101
|
+
src: '/img/image.jpg'
|
|
102
|
+
},
|
|
103
|
+
expected: 'width="100" height="200" src="/img/image.jpg"'
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
describe('renderAttrs', () => {
|
|
108
|
+
testCases.forEach(({ description, attributes, expected }) => {
|
|
109
|
+
it(description, () => {
|
|
110
|
+
expect(renderAttrs(attributes)).toEqual(expected)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { renderHead, type HeadItems } from './index'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
|
|
4
|
+
type TestCase = {
|
|
5
|
+
description: string
|
|
6
|
+
params: HeadItems[]
|
|
7
|
+
expected: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const testCases: TestCase[] = [
|
|
11
|
+
{
|
|
12
|
+
description: 'Render head with minimal content',
|
|
13
|
+
params: [
|
|
14
|
+
{
|
|
15
|
+
title: 'My Site Title'
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
expected: `<meta charset="UTF-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
20
|
+
<title>My Site Title</title>`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
description: 'Render head with base tag',
|
|
24
|
+
params: [
|
|
25
|
+
{
|
|
26
|
+
title: 'My Site Title',
|
|
27
|
+
base: [{ href: 'https://example.com' }]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
expected: `<meta charset="UTF-8">
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
32
|
+
<base href="https://example.com">
|
|
33
|
+
<title>My Site Title</title>`
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
description: 'Render head with meta tags',
|
|
37
|
+
params: [
|
|
38
|
+
{
|
|
39
|
+
title: 'My Site Title',
|
|
40
|
+
meta: [{ name: 'description', content: 'My site description' }]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
expected: `<meta charset="UTF-8">
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
45
|
+
<title>My Site Title</title>
|
|
46
|
+
<meta name="description" content="My site description">`
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
description: 'Render head with link tags',
|
|
50
|
+
params: [
|
|
51
|
+
{
|
|
52
|
+
title: 'My Site Title',
|
|
53
|
+
link: [{ rel: 'stylesheet', href: 'styles.css' }]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
expected: `<meta charset="UTF-8">
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
58
|
+
<title>My Site Title</title>
|
|
59
|
+
<link rel="stylesheet" href="styles.css">`
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: 'Render head with style tags',
|
|
63
|
+
params: [
|
|
64
|
+
{
|
|
65
|
+
title: 'My Site Title',
|
|
66
|
+
style: [{ innerHTML: 'body { color: red; }' }]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
expected: `<meta charset="UTF-8">
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
71
|
+
<title>My Site Title</title>
|
|
72
|
+
<style>body { color: red; }</style>`
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
description: 'Render head with script tags',
|
|
76
|
+
params: [
|
|
77
|
+
{
|
|
78
|
+
title: 'My Site Title',
|
|
79
|
+
script: [{ innerHTML: 'console.log("Hello, world!")' }]
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
expected: `<meta charset="UTF-8">
|
|
83
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
84
|
+
<title>My Site Title</title>
|
|
85
|
+
<script>console.log("Hello, world!")</script>`
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
description: 'Render head with noscript tags',
|
|
89
|
+
params: [
|
|
90
|
+
{
|
|
91
|
+
title: 'My Site Title',
|
|
92
|
+
noscript: [{ innerHTML: 'Please enable JavaScript' }]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
expected: `<meta charset="UTF-8">
|
|
96
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
97
|
+
<title>My Site Title</title>
|
|
98
|
+
<noscript>Please enable JavaScript</noscript>`
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
description: 'Render head with all tags',
|
|
102
|
+
params: [
|
|
103
|
+
{
|
|
104
|
+
title: 'My Site Title',
|
|
105
|
+
base: [{ href: 'https://example.com' }],
|
|
106
|
+
meta: [{ name: 'description', content: 'My site description' }],
|
|
107
|
+
link: [{ rel: 'stylesheet', href: 'styles.css' }],
|
|
108
|
+
style: [{ innerHTML: 'body { color: red; }' }],
|
|
109
|
+
script: [{ innerHTML: 'console.log("Hello, world!")' }],
|
|
110
|
+
noscript: [{ innerHTML: 'Please enable JavaScript' }]
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
expected: `<meta charset="UTF-8">
|
|
114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
115
|
+
<base href="https://example.com">
|
|
116
|
+
<title>My Site Title</title>
|
|
117
|
+
<script>console.log("Hello, world!")</script>
|
|
118
|
+
<link rel="stylesheet" href="styles.css">
|
|
119
|
+
<style>body { color: red; }</style>
|
|
120
|
+
<meta name="description" content="My site description">
|
|
121
|
+
<noscript>Please enable JavaScript</noscript>`
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
describe('renderHead', () => {
|
|
126
|
+
testCases.forEach(({ description, params, expected }) => {
|
|
127
|
+
it(description, () => {
|
|
128
|
+
expect(renderHead(params)).toEqual(expected)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
package/src/index.test.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { renderHead, renderAttrs, HeadItems } from './index'
|
|
3
|
-
|
|
4
|
-
describe('renderHead', () => {
|
|
5
|
-
it('should render the head tags in the correct order', () => {
|
|
6
|
-
const headItems: HeadItems[] = [
|
|
7
|
-
{
|
|
8
|
-
title: 'My Page',
|
|
9
|
-
meta: [{ name: 'description', content: 'My page description' }],
|
|
10
|
-
link: [{ rel: 'stylesheet', href: '/styles.css' }],
|
|
11
|
-
script: [{ src: '/script.js' }]
|
|
12
|
-
}
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
const expectedOutput = `<meta charset="UTF-8" />
|
|
16
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
-
<title>My Page</title>
|
|
18
|
-
<script src="/script.js"></script>
|
|
19
|
-
<link rel="stylesheet" href="/styles.css" />
|
|
20
|
-
<meta name="description" content="My page description" />`
|
|
21
|
-
|
|
22
|
-
console.log(renderHead(headItems))
|
|
23
|
-
|
|
24
|
-
expect(renderHead(headItems)).toEqual(expectedOutput)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('should throw an error if no title is provided', () => {
|
|
28
|
-
const headItems: HeadItems[] = [
|
|
29
|
-
{
|
|
30
|
-
meta: [{ name: 'description', content: 'My page description' }]
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
expect(() => renderHead(headItems)).toThrowError('Missing title tag.')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('should deduplicate meta tags', () => {
|
|
38
|
-
const headItems: HeadItems[] = [
|
|
39
|
-
{
|
|
40
|
-
title: 'My Page',
|
|
41
|
-
meta: [
|
|
42
|
-
{ name: 'description', content: 'My page description' },
|
|
43
|
-
{ name: 'description', content: 'Duplicate description' }
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
const expectedOutput = `<meta charset="UTF-8" />
|
|
49
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
50
|
-
<title>My Page</title>
|
|
51
|
-
<meta name="description" content="Duplicate description" />`
|
|
52
|
-
|
|
53
|
-
expect(renderHead(headItems)).toEqual(expectedOutput)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('renderAttrs', () => {
|
|
58
|
-
it('should render boolean attributes correctly', () => {
|
|
59
|
-
const item = { async: true, src: '/script.js' }
|
|
60
|
-
expect(renderAttrs(item)).toEqual('async src="/script.js"')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('should filter out invalid attributes', () => {
|
|
64
|
-
const item = {
|
|
65
|
-
innerHTML: '<p>Hello</p>',
|
|
66
|
-
priority: 1,
|
|
67
|
-
tagName: 'script',
|
|
68
|
-
src: '/script.js'
|
|
69
|
-
}
|
|
70
|
-
expect(renderAttrs(item)).toEqual('src="/script.js"')
|
|
71
|
-
})
|
|
72
|
-
})
|