Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
titaniumnetwork-dev
GitHub Repository: titaniumnetwork-dev/Ultraviolet
Path: blob/main/src/rewrite/html.js
304 views
1
import EventEmitter from "events";
2
import { parse, parseFragment, serialize } from "parse5";
3
4
/**
5
* @typedef {import('./index').default} Ultraviolet
6
*/
7
8
class HTML extends EventEmitter {
9
/**
10
*
11
* @param {Ultraviolet} ctx
12
*/
13
constructor(ctx) {
14
super();
15
this.ctx = ctx;
16
this.rewriteUrl = ctx.rewriteUrl;
17
this.sourceUrl = ctx.sourceUrl;
18
}
19
rewrite(str, options = {}) {
20
if (!str) return str;
21
return this.recast(
22
str,
23
(node) => {
24
if (node.tagName) this.emit("element", node, "rewrite");
25
if (node.attr) this.emit("attr", node, "rewrite");
26
if (node.nodeName === "#text") this.emit("text", node, "rewrite");
27
},
28
options
29
);
30
}
31
source(str, options = {}) {
32
if (!str) return str;
33
return this.recast(
34
str,
35
(node) => {
36
if (node.tagName) this.emit("element", node, "source");
37
if (node.attr) this.emit("attr", node, "source");
38
if (node.nodeName === "#text") this.emit("text", node, "source");
39
},
40
options
41
);
42
}
43
recast(str, fn, options = {}) {
44
try {
45
const ast = (options.document ? parse : parseFragment)(
46
new String(str).toString()
47
);
48
this.iterate(ast, fn, options);
49
return serialize(ast);
50
} catch (e) {
51
return str;
52
}
53
}
54
iterate(ast, fn, fnOptions) {
55
if (!ast) return ast;
56
57
if (ast.tagName) {
58
const element = new P5Element(ast, false, fnOptions);
59
fn(element);
60
if (ast.attrs) {
61
for (const attr of ast.attrs) {
62
if (!attr.skip) fn(new AttributeEvent(element, attr, fnOptions));
63
}
64
}
65
}
66
67
if (ast.childNodes) {
68
for (const child of ast.childNodes) {
69
if (!child.skip) this.iterate(child, fn, fnOptions);
70
}
71
}
72
73
if (ast.nodeName === "#text") {
74
fn(new TextEvent(ast, new P5Element(ast.parentNode), false, fnOptions));
75
}
76
77
return ast;
78
}
79
wrapSrcset(str, meta = this.ctx.meta) {
80
return str
81
.split(",")
82
.map((src) => {
83
const parts = src.trimStart().split(" ");
84
if (parts[0]) parts[0] = this.ctx.rewriteUrl(parts[0], meta);
85
return parts.join(" ");
86
})
87
.join(", ");
88
}
89
unwrapSrcset(str, meta = this.ctx.meta) {
90
return str
91
.split(",")
92
.map((src) => {
93
const parts = src.trimStart().split(" ");
94
if (parts[0]) parts[0] = this.ctx.sourceUrl(parts[0], meta);
95
return parts.join(" ");
96
})
97
.join(", ");
98
}
99
static parse = parse;
100
static parseFragment = parseFragment;
101
static serialize = serialize;
102
}
103
104
class P5Element extends EventEmitter {
105
constructor(node, stream = false, options = {}) {
106
super();
107
this.stream = stream;
108
this.node = node;
109
this.options = options;
110
}
111
setAttribute(name, value) {
112
for (const attr of this.attrs) {
113
if (attr.name === name) {
114
attr.value = value;
115
return true;
116
}
117
}
118
119
this.attrs.push({
120
name,
121
value,
122
});
123
}
124
getAttribute(name) {
125
const attr = this.attrs.find((attr) => attr.name === name) || {};
126
return attr.value;
127
}
128
hasAttribute(name) {
129
return !!this.attrs.find((attr) => attr.name === name);
130
}
131
removeAttribute(name) {
132
const i = this.attrs.findIndex((attr) => attr.name === name);
133
if (typeof i !== "undefined") this.attrs.splice(i, 1);
134
}
135
get tagName() {
136
return this.node.tagName;
137
}
138
set tagName(val) {
139
this.node.tagName = val;
140
}
141
get childNodes() {
142
return !this.stream ? this.node.childNodes : null;
143
}
144
get innerHTML() {
145
return !this.stream
146
? serialize({
147
nodeName: "#document-fragment",
148
childNodes: this.childNodes,
149
})
150
: null;
151
}
152
set innerHTML(val) {
153
if (!this.stream) this.node.childNodes = parseFragment(val).childNodes;
154
}
155
get outerHTML() {
156
return !this.stream
157
? serialize({
158
nodeName: "#document-fragment",
159
childNodes: [this],
160
})
161
: null;
162
}
163
set outerHTML(val) {
164
if (!this.stream)
165
this.parentNode.childNodes.splice(
166
this.parentNode.childNodes.findIndex((node) => node === this.node),
167
1,
168
...parseFragment(val).childNodes
169
);
170
}
171
get textContent() {
172
if (this.stream) return null;
173
174
let str = "";
175
this.iterate(this.node, (node) => {
176
if (node.nodeName === "#text") str += node.value;
177
});
178
179
return str;
180
}
181
set textContent(val) {
182
if (!this.stream)
183
this.node.childNodes = [
184
{
185
nodeName: "#text",
186
value: val,
187
parentNode: this.node,
188
},
189
];
190
}
191
get nodeName() {
192
return this.node.nodeName;
193
}
194
get parentNode() {
195
return this.node.parentNode ? new P5Element(this.node.parentNode) : null;
196
}
197
get attrs() {
198
return this.node.attrs;
199
}
200
get namespaceURI() {
201
return this.node.namespaceURI;
202
}
203
}
204
205
class AttributeEvent {
206
constructor(node, attr, options = {}) {
207
this.attr = attr;
208
this.attrs = node.attrs;
209
this.node = node;
210
this.options = options;
211
}
212
delete() {
213
const i = this.attrs.findIndex((attr) => attr === this.attr);
214
215
this.attrs.splice(i, 1);
216
217
Object.defineProperty(this, "deleted", {
218
get: () => true,
219
});
220
221
return true;
222
}
223
get name() {
224
return this.attr.name;
225
}
226
227
set name(val) {
228
this.attr.name = val;
229
}
230
get value() {
231
return this.attr.value;
232
}
233
234
set value(val) {
235
this.attr.value = val;
236
}
237
get deleted() {
238
return false;
239
}
240
}
241
242
class TextEvent {
243
constructor(node, element, stream = false, options = {}) {
244
this.stream = stream;
245
this.node = node;
246
this.element = element;
247
this.options = options;
248
}
249
get nodeName() {
250
return this.node.nodeName;
251
}
252
get parentNode() {
253
return this.element;
254
}
255
get value() {
256
return this.stream ? this.node.text : this.node.value;
257
}
258
set value(val) {
259
if (this.stream) this.node.text = val;
260
else this.node.value = val;
261
}
262
}
263
264
export default HTML;
265
266