Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
FogNetwork
GitHub Repository: FogNetwork/Tsunami
Path: blob/main/lib/html.js
1036 views
1
// -------------------------------------------------------------
2
// WARNING: this file is used by both the client and the server.
3
// Do not use any browser or node-specific API!
4
// -------------------------------------------------------------
5
const parse5 = require('parse5');
6
7
class HTMLRewriter {
8
constructor(ctx) {
9
this.ctx = ctx;
10
this.attrs = [
11
{
12
tags: ['form', 'object', 'a', 'link', 'area', 'base', 'script', 'img', 'audio', 'video', 'input', 'embed', 'iframe', 'track', 'source', 'html', 'table', 'head'],
13
attrs: ['src', 'href', 'ping', 'data', 'movie', 'action', 'poster', 'profile', 'background', 'target'],
14
handler: 'url',
15
},
16
{
17
tags: ['iframe'],
18
attrs: ['srcdoc'],
19
handler: 'html',
20
},
21
{
22
tags: ['img', 'link', 'source'],
23
attrs: ['srcset', 'imagesrcset'],
24
handler: 'srcset',
25
},
26
{
27
tags: '*',
28
attrs: ['style'],
29
handler: 'css',
30
},
31
{
32
tags: '*',
33
attrs: ['http-equiv', 'integrity', 'nonce', 'crossorigin'],
34
handler: 'delete',
35
},
36
];
37
};
38
process(source, config = {}) {
39
const ast = parse5[config.document ? 'parse' : 'parseFragment'](source);
40
const meta = {
41
origin: config.origin,
42
base: new URL(config.base),
43
};
44
iterate(ast, node => {
45
if (!node.tagName) return;
46
switch(node.tagName) {
47
case 'STYLE':
48
if (node.textContent) node.textContent = this.ctx.css.process(node.textContent, meta);
49
break;
50
case 'TITLE':
51
if (node.textContent && this.ctx.config.title) node.textContent = this.ctx.config.title;
52
break;
53
case 'SCRIPT':
54
if (node.getAttribute('type') != 'application/json' && node.textContent) node.textContent = this.ctx.js.process(node.textContent);
55
break;
56
case 'BASE':
57
if (node.hasAttribute('href')) meta.base = new URL(node.getAttribute('href'), config.base);
58
break;
59
};
60
node.attrs.forEach(attr => {
61
const handler = this.attributeRoute({
62
...attr,
63
node,
64
});
65
let flags = [];
66
if (node.tagName == 'SCRIPT' && attr.name == 'src') flags.push('js');
67
if (node.tagName == 'LINK' && node.getAttribute('rel') == 'stylesheet') flags.push('css');
68
switch(handler) {
69
case 'url':
70
node.setAttribute(`corrosion-${attr.name}`, attr.value);
71
attr.value = this.ctx.url.wrap(attr.value, { ...meta, flags });
72
break;
73
case 'srcset':
74
node.setAttribute(`corrosion-${attr.name}`, attr.value);
75
attr.value = this.srcset(attr.value, meta);
76
break;
77
case 'css':
78
attr.value = this.ctx.css.process(attr.value, { ...meta, context: 'declarationList' });
79
break;
80
case 'html':
81
node.setAttribute(`corrosion-${attr.name}`, attr.value);
82
attr.value = this.process(attr.value, { ...config, ...meta });
83
break;
84
case 'delete':
85
node.removeAttribute(attr.name);
86
break;
87
};
88
});
89
});
90
if (config.document) {
91
for (let i in ast.childNodes) if (ast.childNodes[i].tagName == 'html') ast.childNodes[i].childNodes.forEach(node => {
92
if (node.tagName == 'head') {
93
node.childNodes.unshift(...this.injectHead(config.base));
94
};
95
});
96
};
97
return parse5.serialize(ast);
98
};
99
source(processed, config = {}) {
100
const ast = parse5[config.document ? 'parse' : 'parseFragment'](processed);
101
iterate(ast, node => {
102
if (!node.tagName) return;
103
node.attrs.forEach(attr => {
104
if (node.hasAttribute(`corrosion-${attr.name}`)) {
105
attr.value = node.getAttribute(`corrosion-${attr.name}`);
106
node.removeAttribute(`corrosion-${attr.name}`)
107
};
108
});
109
});
110
return parse5.serialize(ast);
111
};
112
injectHead() {
113
return [
114
{
115
nodeName: 'title',
116
tagName: 'title',
117
childNodes: [
118
{
119
nodeName: '#text',
120
value: this.ctx.config.title || '',
121
}
122
],
123
attrs: [],
124
},
125
{
126
nodeName: 'script',
127
tagName: 'script',
128
childNodes: [],
129
attrs: [
130
{
131
name: 'src',
132
value: this.ctx.prefix + 'index.js',
133
},
134
],
135
},
136
{
137
nodeName: 'script',
138
tagName: 'script',
139
childNodes: [
140
{
141
nodeName: '#text',
142
value: `window.$corrosion = new Corrosion({ window, codec: '${this.ctx.config.codec || 'plain'}', prefix: '${this.ctx.prefix}', ws: ${this.ctx.config.ws}, cookie: ${this.ctx.config.cookie}, title: ${typeof this.ctx.config.title == 'boolean' ? this.ctx.config.title : '\'' + this.ctx.config.title + '\''}, }); $corrosion.init(); document.currentScript.remove();`
143
},
144
],
145
attrs: [],
146
}
147
];
148
}
149
attributeRoute(data) {
150
const match = this.attrs.find(entry => entry.tags == '*' && entry.attrs.includes(data.name) || entry.tags.includes(data.node.tagName.toLowerCase()) && entry.attrs.includes(data.name));
151
return match ? match.handler : false;
152
};
153
srcset(val, config = {}) {
154
return val.split(',').map(src => {
155
const parts = src.trimStart().split(' ');
156
if (parts[0]) parts[0] = this.ctx.url.wrap(parts[0], config);
157
return parts.join(' ');
158
}).join(', ');
159
};
160
unsrcset(val, config = {}) {
161
return val.split(',').map(src => {
162
const parts = src.trimStart().split(' ');
163
if (parts[0]) parts[0] = this.ctx.url.unwrap(parts[0], config);
164
return parts.join(' ');
165
}).join(', ');
166
};
167
};
168
169
class Parse5Wrapper {
170
constructor(node){
171
this.raw = node || {
172
attrs: [],
173
childNodes: [],
174
namespaceURI: '',
175
nodeName: '',
176
parentNode: {},
177
tagName: '',
178
};
179
};
180
hasAttribute(name){
181
return this.raw.attrs.some(attr => attr.name == name);
182
};
183
removeAttribute(name){
184
if (!this.hasAttribute(name)) return;
185
this.raw.attrs.splice(this.raw.attrs.findIndex(attr => attr.name == name), 1);
186
};
187
setAttribute(name, val = ''){
188
if (!name) return;
189
this.removeAttribute(name);
190
this.raw.attrs.push({
191
name: name,
192
value: val,
193
});
194
};
195
getAttribute(name){
196
return (this.raw.attrs.find(attr => attr.name == name) || { value: null }).value;
197
};
198
get textContent(){
199
return (this.raw.childNodes.find(node => node.nodeName == '#text') || { value: '', }).value
200
};
201
set textContent(val){
202
if (this.raw.childNodes.some(node => node.nodeName == '#text')) return this.raw.childNodes[this.raw.childNodes.findIndex(node => node.nodeName == '#text')].value = val;
203
this.raw.childNodes.push({
204
nodeName: '#text',
205
value: val,
206
});
207
};
208
get tagName(){
209
return (this.raw.tagName || '').toUpperCase();
210
};
211
get nodeName(){
212
return this.raw.nodeName;
213
};
214
get parentNode(){
215
return this.raw.parentNode;
216
};
217
get childNodes(){
218
return this.raw.childNodes || [];
219
};
220
get attrs() {
221
return this.raw.attrs || [];
222
};
223
};
224
225
function iterate(ast, fn = (node = Parse5Wrapper.prototype) => null) {
226
fn(new Parse5Wrapper(ast));
227
if (ast.childNodes) for (let i in ast.childNodes) iterate(ast.childNodes[i], fn);
228
};
229
230
module.exports = HTMLRewriter;
231
232