Skip to content

Commit 41210c5

Browse files
committedFeb 2, 2025·
fix: handle boolean props
1 parent a472006 commit 41210c5

File tree

2 files changed

+86
-25
lines changed

2 files changed

+86
-25
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,57 @@
1+
import type { UseHeadInput } from 'unhead'
2+
3+
const Attrs = /(\w+)(?:=["']([^"']*)["'])?/g
4+
const HtmlTag = /<html[^>]*>/
5+
const BodyTag = /<body[^>]*>/
6+
const HeadContent = /<head[^>]*>(.*?)<\/head>/s
7+
const SelfClosingTags = /<(meta|link|base)[^>]*>/g
8+
const ClosingTags = /<(title|script|style)[^>]*>[\s\S]*?<\/\1>/g
9+
// eslint-disable-next-line regexp/no-misleading-capturing-group
10+
const NewLines = /(\n\s*)+/g
11+
112
function extractAttributes(tag: string) {
2-
const attrs = tag.match(/([a-z-]+)="([^"]*)"/g)
13+
// inner should be between the < and > (non greedy), split on ' ' and after index 0
14+
const inner = tag.match(/<([^>]*)>/)?.[1].split(' ').slice(1).join(' ')
15+
if (!inner)
16+
return {}
17+
const attrs = inner.match(Attrs)
318
return attrs?.reduce((acc, attr) => {
4-
const [key, ...valueParts] = attr.split('=')
5-
// join value parts to support '=' within the quoted values
6-
const val = valueParts.join('=').slice(1, -1)
19+
const sep = attr.indexOf('=')
20+
const key = sep > 0 ? attr.slice(0, sep) : attr
21+
const val = sep > 0 ? attr.slice(sep + 1).slice(1, -1) : true
722
return { ...acc, [key]: val, tagPriority: 'low' }
8-
}, {})
23+
}, {}) || {}
924
}
1025

1126
export function extractTagsFromHtml(html: string) {
12-
const input = {}
13-
// i should be able to give it a string of html and it should convert it to input for useHead()
14-
// parse htmlAttrs, bodyAttrs
15-
input.htmlAttrs = extractAttributes(html.match(/<html[^>]*>/)?.[0] || '')
27+
const input: UseHeadInput<any> = {}
28+
input.htmlAttrs = extractAttributes(html.match(HtmlTag)?.[0] || '')
29+
html = html.replace(HtmlTag, '<html>')
30+
31+
input.bodyAttrs = extractAttributes(html.match(BodyTag)?.[0] || '')
32+
html = html.replace(BodyTag, '<body>')
1633

17-
html = html.replace(/<html[^>]*>/, '<html>')
18-
input.bodyAttrs = extractAttributes(html.match(/<body[^>]*>/)?.[0] || '')
19-
html = html.replace(/<body[^>]*>/, '<body>')
20-
// parse headTags, need to split on /> and seperate each tag
21-
const innerHead = html.match(/<head[^>]*>([\s\S]*)<\/head>/)?.[1]
22-
// replace ['meta', 'link', 'base'] tags first because they're unique in that they don't have a closing tag
23-
innerHead?.match(/<meta[^>]*>|<link[^>]*>|<base[^>]*>/g).forEach((s) => {
34+
const innerHead = html.match(HeadContent)?.[1] || ''
35+
innerHead.match(SelfClosingTags)?.forEach((s) => {
2436
html = html.replace(s, '')
25-
const tag = s.split(' ')[0].slice(1)
37+
const tag = s.split(' ')[0].slice(1) as 'meta'
2638
input[tag] = input[tag] || []
27-
input[tag].push(extractAttributes(s))
39+
input[tag].push(extractAttributes(s) as any)
2840
})
29-
innerHead?.match(/<title[^>]*>[\s\S]*?<\/title>|<script[^>]*>[\s\S]*?<\/script>|<style[^>]*>[\s\S]*?<\/style>/g)
30-
.map(tag => tag.trim())
41+
42+
innerHead.match(ClosingTags)
43+
?.map(tag => tag.trim())
3144
.filter(Boolean)
3245
.forEach((tag) => {
3346
html = html.replace(tag, '')
34-
const type = tag.match(/<([a-z-]+)/)?.[1]
47+
const type = tag.match(/<([a-z-]+)/)?.[1] as 'script' | 'title'
3548
const res = {
3649
tagPriority: 'low',
37-
[type !== 'script' ? 'textContent' : 'innerHTML']: tag.match(/>([\s\S]*)</)?.[1],
3850
...extractAttributes(tag),
51+
} as any
52+
const innerContent = tag.match(/>([\s\S]*)</)?.[1]
53+
if (innerContent) {
54+
res[type !== 'script' ? 'textContent' : 'innerHTML'] = innerContent
3955
}
4056
if (type === 'title') {
4157
input.title = res
@@ -45,8 +61,7 @@ export function extractTagsFromHtml(html: string) {
4561
input[type].push(res)
4662
}
4763
})
48-
// remove duplicate new lines from html, could be 2, 5 or 20 in a row
49-
html = html.replace(/(\n\s*)+/g, '\n')
50-
// we leave any body tags as the order is out of our control
64+
65+
html = html.replace(NewLines, '\n')
5166
return { html, input }
5267
}

‎packages/unhead/test/unit/server/ssr.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,50 @@ describe('ssr', () => {
303303
"
304304
`)
305305
})
306+
it('random template #2', async () => {
307+
const html = `
308+
<!DOCTYPE html>
309+
<html lang="en">
310+
<head>
311+
<meta charset="UTF-8">
312+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
313+
<meta name="description" content="Test description">
314+
<link rel="stylesheet" href="styles.css">
315+
<link rel="icon" href="favicon.ico">
316+
<base href="/">
317+
<title>Test Document</title>
318+
<style>body { font-family: Arial, sans-serif; }</style>
319+
<script src="script.js" async></script>
320+
<script>console.log('Inline script');</script>
321+
<!-- Resource Hints -->
322+
<link rel="preload" href="styles.css" as="style">
323+
<link rel="preload" href="script.js" as="script">
324+
<link rel="dns-prefetch" href="//example.com">
325+
<link rel="preconnect" href="//example.com">
326+
<link rel="prefetch" href="another-script.js">
327+
</head>
328+
<body style="background-color: #f0f0f0;">
329+
<div id="content">Hello, world!</div>
330+
<script src="another-script.js"></script>
331+
</body>
332+
</html>`
333+
const head = createServerHeadWithContext()
334+
const processedHtml = await transformHtmlTemplate(head, html)
335+
expect(processedHtml).toContain('<meta charset="UTF-8">')
336+
expect(processedHtml).toContain('<meta name="viewport" content="width=device-width, initial-scale=1.0">')
337+
expect(processedHtml).toContain('<meta name="description" content="Test description">')
338+
expect(processedHtml).toContain('<link rel="stylesheet" href="styles.css">')
339+
expect(processedHtml).toContain('<link rel="icon" href="favicon.ico">')
340+
expect(processedHtml).toContain('<base href="/">')
341+
expect(processedHtml).toContain('<title>Test Document</title>')
342+
expect(processedHtml).toContain('<style>body { font-family: Arial, sans-serif; }</style>')
343+
expect(processedHtml).toContain('<script src="script.js" async></script>')
344+
expect(processedHtml).toContain('<script>console.log(\'Inline script\');</script>')
345+
expect(processedHtml).toContain('<link rel="preload" href="styles.css" as="style">')
346+
expect(processedHtml).toContain('<link rel="preload" href="script.js" as="script">')
347+
expect(processedHtml).toContain('<link rel="dns-prefetch" href="//example.com">')
348+
expect(processedHtml).toContain('<link rel="preconnect" href="//example.com">')
349+
expect(processedHtml).toContain('<link rel="prefetch" href="another-script.js">')
350+
expect(processedHtml).toContain('<script src="another-script.js"></script>')
351+
})
306352
})

0 commit comments

Comments
 (0)
Please sign in to comment.