astro-helmet 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "astro-helmet",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
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
@@ -168,8 +168,20 @@ export function renderAttrs(item: BaseItem | ContentItem): string {
168
168
  .filter(([key]) => !['innerHTML', 'priority', 'tagName'].includes(key))
169
169
  .map(([key, value]) => {
170
170
  if (typeof value === 'boolean') return value ? key : ''
171
- else return `${key}="${value}"`
171
+ else if (value === null || value === undefined) return ''
172
+ else return `${key}="${escapeHtml(String(value))}"`
172
173
  })
173
174
  .filter((attr) => attr !== '')
174
175
  .join(' ')
175
176
  }
177
+
178
+ function escapeHtml(str: string): string {
179
+ const escapeMap: Record<string, string> = {
180
+ '"': '&quot;',
181
+ '&': '&amp;',
182
+ '<': '&lt;',
183
+ '>': '&gt;'
184
+ }
185
+
186
+ return str.replace(/["&<>]/g, (match) => escapeMap[match] || match)
187
+ }
@@ -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 &quot;quotes&quot;" 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
+ })
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { renderHead, renderAttrs, HeadItems } from './index'
2
+ import { renderHead, HeadItems } from './index'
3
3
 
4
4
  describe('renderHead', () => {
5
5
  it('should render the head tags in the correct order', () => {
@@ -19,8 +19,6 @@ describe('renderHead', () => {
19
19
  <link rel="stylesheet" href="/styles.css" />
20
20
  <meta name="description" content="My page description" />`
21
21
 
22
- console.log(renderHead(headItems))
23
-
24
22
  expect(renderHead(headItems)).toEqual(expectedOutput)
25
23
  })
26
24
 
@@ -53,20 +51,3 @@ describe('renderHead', () => {
53
51
  expect(renderHead(headItems)).toEqual(expectedOutput)
54
52
  })
55
53
  })
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
- })