-
Notifications
You must be signed in to change notification settings - Fork 256
Expand file tree
/
Copy pathparse-head-html.ts
More file actions
132 lines (120 loc) · 4.75 KB
/
Copy pathparse-head-html.ts
File metadata and controls
132 lines (120 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import React from 'react';
/**
* Maps lowercase HTML attribute names to their React/JSX camelCase equivalents.
* Shared by head-HTML parsing and the layer renderers so imported markup never
* leaks invalid DOM props (e.g. `fetchpriority`) onto React elements.
*/
export const HTML_TO_REACT_ATTRS: Record<string, string> = {
'class': 'className',
'for': 'htmlFor',
'autofocus': 'autoFocus',
'crossorigin': 'crossOrigin',
'charset': 'charSet',
'http-equiv': 'httpEquiv',
'tabindex': 'tabIndex',
'nomodule': 'noModule',
'referrerpolicy': 'referrerPolicy',
'fetchpriority': 'fetchPriority',
};
const TAG_REGEX =
/<(meta|link|base)(\s(?:[^>"']|"[^"]*"|'[^']*')*)?\s*\/?>|<(style|script|title|noscript)(\s[^>]*)?>[\s\S]*?<\/\3\s*>/gi;
function parseAttributes(attrString: string): Record<string, string> {
const attrs: Record<string, string> = {};
const regex = /([\w-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
let match;
while ((match = regex.exec(attrString)) !== null) {
attrs[match[1]] = match[2] ?? match[3] ?? match[4] ?? '';
}
return attrs;
}
function toReactAttrs(attrs: Record<string, string>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(attrs)) {
result[HTML_TO_REACT_ATTRS[key.toLowerCase()] || key] = value;
}
return result;
}
function extractInnerHtml(full: string, tag: string): string {
const m = full.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*)<\\/${tag}\\s*>`, 'i'));
return m ? m[1] : '';
}
const STYLE_BLOCK_REGEX = /<style[^>]*>([\s\S]*?)<\/style\s*>/gi;
/**
* Concatenates the inner CSS of every `<style>` block in an HTML string.
* Used by the builder canvas to live-preview user-defined CSS variables
* declared in custom head code, without executing any `<script>` tags.
*/
export function extractStyleBlockContents(html: string | null | undefined): string {
if (!html) return '';
const parts: string[] = [];
STYLE_BLOCK_REGEX.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = STYLE_BLOCK_REGEX.exec(html)) !== null) {
const inner = match[1].trim();
if (inner) parts.push(inner);
}
return parts.join('\n');
}
/**
* Renders global head HTML as React elements for direct placement inside
* the root layout's <head>. Bypasses next/script to avoid self.__next_s
* serialization — the browser executes scripts during head parsing.
*/
export function renderRootLayoutHeadCode(html: string, prefix = 'global-head'): React.ReactNode[] {
const elements: React.ReactNode[] = [];
TAG_REGEX.lastIndex = 0;
let match;
let idx = 0;
while ((match = TAG_REGEX.exec(html)) !== null) {
const voidTag = match[1]?.toLowerCase();
const voidAttrStr = match[2] || '';
const pairedTag = match[3]?.toLowerCase();
const pairedAttrStr = match[4] || '';
// Third-party scripts (AdSense, GTM, etc.) mutate their own head tags at
// runtime (e.g. adding `data-checked-head`), so the live DOM diverges from
// the SSR markup. suppressHydrationWarning silences these expected diffs.
if (voidTag) {
const attrs = toReactAttrs(parseAttributes(voidAttrStr.trim()));
elements.push(React.createElement(voidTag, { key: `${prefix}-${idx++}`, suppressHydrationWarning: true, ...attrs }));
} else if (pairedTag === 'script') {
const attrs = parseAttributes(pairedAttrStr.trim());
const inner = extractInnerHtml(match[0], 'script');
const reactAttrs = toReactAttrs(attrs);
const props: Record<string, unknown> = {
key: `${prefix}-${idx++}`,
suppressHydrationWarning: true,
...reactAttrs,
};
if (inner) {
props.dangerouslySetInnerHTML = { __html: inner };
}
elements.push(React.createElement('script', props));
} else if (pairedTag === 'style') {
const attrs = toReactAttrs(parseAttributes(pairedAttrStr.trim()));
const inner = extractInnerHtml(match[0], 'style');
elements.push(
React.createElement('style', {
key: `${prefix}-${idx++}`,
suppressHydrationWarning: true,
...attrs,
dangerouslySetInnerHTML: { __html: inner },
}),
);
} else if (pairedTag === 'title') {
const inner = extractInnerHtml(match[0], 'title');
elements.push(React.createElement('title', { key: `${prefix}-${idx++}`, suppressHydrationWarning: true }, inner));
} else if (pairedTag) {
const attrs = toReactAttrs(parseAttributes(pairedAttrStr.trim()));
const inner = extractInnerHtml(match[0], pairedTag);
elements.push(
React.createElement(pairedTag, {
key: `${prefix}-${idx++}`,
suppressHydrationWarning: true,
...attrs,
dangerouslySetInnerHTML: { __html: inner },
}),
);
}
}
return elements;
}