~mmach/netext73/webkit2gtk

« back to all changes in this revision

Viewing changes to Source/WebInspectorUI/UserInterface/Workers/Formatter/HTMLParser.js

  • Committer: mmach
  • Date: 2023-06-16 17:21:37 UTC
  • Revision ID: netbit73@gmail.com-20230616172137-2rqx6yr96ga9g3kp
1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright (C) 2019 Apple Inc. All rights reserved.
 
3
 *
 
4
 * Redistribution and use in source and binary forms, with or without
 
5
 * modification, are permitted provided that the following conditions
 
6
 * are met:
 
7
 * 1. Redistributions of source code must retain the above copyright
 
8
 *    notice, this list of conditions and the following disclaimer.
 
9
 * 2. Redistributions in binary form must reproduce the above copyright
 
10
 *    notice, this list of conditions and the following disclaimer in the
 
11
 *    documentation and/or other materials provided with the distribution.
 
12
 *
 
13
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 
14
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 
15
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 
16
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 
17
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 
18
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 
19
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 
20
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 
21
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 
22
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 
23
 * THE POSSIBILITY OF SUCH DAMAGE.
 
24
 */
 
25
 
 
26
HTMLParser = class HTMLParser {
 
27
 
 
28
    // Public
 
29
 
 
30
    parseDocument(sourceText, treeBuilder, {isXML} = {})
 
31
    {
 
32
        console.assert(typeof sourceText === "string");
 
33
        console.assert(treeBuilder);
 
34
        console.assert(treeBuilder.pushParserNode);
 
35
 
 
36
        this._treeBuilder = treeBuilder;
 
37
 
 
38
        this._pos = 0;
 
39
        this._mode = HTMLParser.Mode.Data;
 
40
        this._data = sourceText;
 
41
        this._bogusCommentOpener = null;
 
42
        this._isXML = !!isXML;
 
43
 
 
44
        if (this._treeBuilder.begin)
 
45
            this._treeBuilder.begin();
 
46
 
 
47
        while (this._pos < this._data.length)
 
48
            this._parse();
 
49
 
 
50
        if (this._treeBuilder.end)
 
51
            this._treeBuilder.end();
 
52
    }
 
53
 
 
54
    // Private
 
55
 
 
56
    _isEOF()
 
57
    {
 
58
        return this._pos === this._data.length;
 
59
    }
 
60
 
 
61
    _peek(n = 1)
 
62
    {
 
63
        return this._data.substring(this._pos, this._pos + n);
 
64
    }
 
65
 
 
66
    _peekCharacterRegex(regex)
 
67
    {
 
68
        return regex.test(this._data.charAt(this._pos));
 
69
    }
 
70
 
 
71
    _peekString(str)
 
72
    {
 
73
        for (let i = 0; i < str.length; ++i) {
 
74
            let c = str[i];
 
75
            if (this._data.charAt(this._pos + i) !== c)
 
76
                return false;
 
77
        }
 
78
 
 
79
        return true;
 
80
    }
 
81
 
 
82
    _peekCaseInsensitiveString(str)
 
83
    {
 
84
        console.assert(str.toLowerCase() === str, "String should be passed in as lowercase.");
 
85
 
 
86
        for (let i = 0; i < str.length; ++i) {
 
87
            let d = this._data.charAt(this._pos + i);
 
88
            if (!d)
 
89
                return false;
 
90
            let c = str[i];
 
91
            if (d.toLowerCase() !== c)
 
92
                return false;
 
93
        }
 
94
 
 
95
        return true;
 
96
    }
 
97
 
 
98
    _consumeRegex(regex)
 
99
    {
 
100
        let startIndex = this._pos;
 
101
        while (regex.test(this._data.charAt(this._pos)))
 
102
            this._pos++;
 
103
 
 
104
        return this._data.substring(startIndex, this._pos);
 
105
    }
 
106
 
 
107
    _consumeWhitespace()
 
108
    {
 
109
        return this._consumeRegex(/\s/);
 
110
    }
 
111
 
 
112
    _consumeUntilString(str, newMode)
 
113
    {
 
114
        let index = this._data.indexOf(str, this._pos);
 
115
        if (index === -1) {
 
116
            let startIndex = this._pos;
 
117
            this._pos = this._data.length;
 
118
            if (newMode)
 
119
                this._mode = newMode;
 
120
            return this._data.substring(startIndex, this._data.length);
 
121
        }
 
122
 
 
123
        let startIndex = this._pos;
 
124
        this._pos = index + str.length;
 
125
        if (newMode)
 
126
            this._mode = newMode;
 
127
        return this._data.substring(startIndex, index);
 
128
    }
 
129
 
 
130
    _consumeDoubleQuotedString()
 
131
    {
 
132
        console.assert(this._peekString(`"`));
 
133
        this._pos++;
 
134
        let string = this._consumeUntilString(`"`);
 
135
        return string;
 
136
    }
 
137
 
 
138
    _consumeSingleQuotedString()
 
139
    {
 
140
        console.assert(this._peekString(`'`));
 
141
        this._pos++;
 
142
        let string = this._consumeUntilString(`'`);
 
143
        return string;
 
144
    }
 
145
 
 
146
    // Parser
 
147
    // This is a crude implementation of HTML tokenization:
 
148
    // https://html.spec.whatwg.org/multipage/parsing.html
 
149
 
 
150
    _parse()
 
151
    {
 
152
        switch (this._mode) {
 
153
        case HTMLParser.Mode.Data:
 
154
            return this._parseData();
 
155
        case HTMLParser.Mode.ScriptData:
 
156
            return this._parseScriptData();
 
157
        case HTMLParser.Mode.TagOpen:
 
158
            return this._parseTagOpen();
 
159
        case HTMLParser.Mode.Attr:
 
160
            return this._parseAttr();
 
161
        case HTMLParser.Mode.CData:
 
162
            return this._parseCData();
 
163
        case HTMLParser.Mode.Doctype:
 
164
            return this._parseDoctype();
 
165
        case HTMLParser.Mode.Comment:
 
166
            return this._parseComment();
 
167
        case HTMLParser.Mode.BogusComment:
 
168
            return this._parseBogusComment();
 
169
        }
 
170
 
 
171
        console.assert();
 
172
        throw "Missing parser mode";
 
173
    }
 
174
 
 
175
    _parseData()
 
176
    {
 
177
        let startPos = this._pos;
 
178
        let text = this._consumeUntilString("<", HTMLParser.Mode.TagOpen);
 
179
        if (text)
 
180
            this._push({type: HTMLParser.NodeType.Text, data: text, pos: startPos});
 
181
 
 
182
        if (this._isEOF() && this._data.endsWith("<"))
 
183
            this._handleEOF(this._pos - 1);
 
184
    }
 
185
 
 
186
    _parseScriptData()
 
187
    {
 
188
        let startPos = this._pos;
 
189
        let scriptText = "";
 
190
 
 
191
        // Parse as text until </script>.
 
192
        while (true) {
 
193
            scriptText += this._consumeUntilString("<");
 
194
            if (this._peekCaseInsensitiveString("/script>")) {
 
195
                this._pos += "/script>".length;
 
196
                this._mode = HTMLParser.Mode.Data;
 
197
                break;
 
198
            }
 
199
            if (this._handleEOF(startPos))
 
200
                return;
 
201
            scriptText += "<";
 
202
        }
 
203
 
 
204
        if (scriptText)
 
205
            this._push({type: HTMLParser.NodeType.Text, data: scriptText, pos: startPos});
 
206
        this._push({type: HTMLParser.NodeType.CloseTag, name: "script", pos: startPos + scriptText.length});
 
207
    }
 
208
 
 
209
    _parseTagOpen()
 
210
    {
 
211
        // |<tag
 
212
        this._currentTagStartPos = this._pos - 1;
 
213
 
 
214
        if (this._peekString("!")) {
 
215
            // Comment.
 
216
            if (this._peekString("!--")) {
 
217
                this._pos += "!--".length;
 
218
                this._mode = HTMLParser.Mode.Comment;
 
219
                this._handleEOF(this._currentTagStartPos);
 
220
                return;
 
221
            }
 
222
 
 
223
            // DOCTYPE.
 
224
            if (this._peekCaseInsensitiveString("!doctype")) {
 
225
                let startPos = this._pos;
 
226
                this._pos += "!DOCTYPE".length;
 
227
                this._doctypeRaw = this._data.substring(startPos, this._pos);
 
228
                this._mode = HTMLParser.Mode.Doctype;
 
229
                this._handleEOF(this._currentTagStartPos);
 
230
                return;
 
231
            }
 
232
 
 
233
            // CDATA.
 
234
            if (this._peekString("![CDATA[")) {
 
235
                this._pos += "![CDATA[".length;
 
236
                this._mode = HTMLParser.Mode.CData;
 
237
                this._handleEOF(this._currentTagStartPos);
 
238
                return;
 
239
            }
 
240
 
 
241
            // Bogus Comment.
 
242
            this._pos++;
 
243
            this._mode = HTMLParser.Mode.BogusComment;
 
244
            this._handleEOF(this._currentTagStartPos);
 
245
            return;
 
246
        }
 
247
 
 
248
        if (this._peekString("?")) {
 
249
            // Bogus Comment.
 
250
            this._pos++;
 
251
            this._mode = HTMLParser.Mode.BogusComment;
 
252
            this._bogusCommentOpener = "<?";
 
253
            this._handleEOF(this._currentTagStartPos);
 
254
            return;
 
255
        }
 
256
 
 
257
        if (this._peekString("/")) {
 
258
            // End Tag.
 
259
            this._pos++;
 
260
            let text = this._consumeUntilString(">", HTMLParser.Mode.Data);
 
261
            this._push({type: HTMLParser.NodeType.CloseTag, name: text, pos: this._currentTagStartPos});
 
262
            return;
 
263
        }
 
264
 
 
265
        // ASCII - Open Tag
 
266
        if (this._peekCharacterRegex(/[a-z]/i)) {
 
267
            let text = this._consumeRegex(/[^\s/>]+/);
 
268
            if (text) {
 
269
                if (this._peekCharacterRegex(/\s/)) {
 
270
                    this._currentTagName = text;
 
271
                    this._currentTagAttributes = [];
 
272
                    this._mode = HTMLParser.Mode.Attr;
 
273
                    return;
 
274
                }
 
275
 
 
276
                if (this._peekString("/>")) {
 
277
                    this._pos += "/>".length;
 
278
                    this._mode = HTMLParser.Mode.Data;
 
279
                    this._push({type: HTMLParser.NodeType.OpenTag, name: text, closed: true, pos: this._currentTagStartPos});
 
280
                    return;
 
281
                }
 
282
 
 
283
                if (this._peekString(">")) {
 
284
                    this._pos++;
 
285
                    this._mode = HTMLParser.Mode.Data;
 
286
                    this._push({type: HTMLParser.NodeType.OpenTag, name: text, closed: false, pos: this._currentTagStartPos});
 
287
                    return;
 
288
                }
 
289
 
 
290
                // End of document. Output any remaining data as error text.
 
291
                console.assert(this._isEOF());
 
292
                this._push({type: HTMLParser.NodeType.ErrorText, data: "<" + text, pos: this._currentTagStartPos});
 
293
                return;
 
294
            }
 
295
        }
 
296
 
 
297
        // Anything else, treat as text.
 
298
        this._push({type: HTMLParser.NodeType.Text, data: "<", pos: this._currentTagStartPos});
 
299
        this._mode = HTMLParser.Mode.Data;
 
300
    }
 
301
 
 
302
    _parseAttr()
 
303
    {
 
304
        this._consumeWhitespace();
 
305
 
 
306
        if (this._peekString("/>")) {
 
307
            this._pos += "/>".length;
 
308
            this._mode = HTMLParser.Mode.Data;
 
309
            this._push({type: HTMLParser.NodeType.OpenTag, name: this._currentTagName, closed: true, attributes: this._currentTagAttributes, pos: this._currentTagStartPos});
 
310
            return;
 
311
        }
 
312
 
 
313
        if (this._peekString(">")) {
 
314
            this._pos++;
 
315
            this._mode = HTMLParser.Mode.Data;
 
316
            this._push({type: HTMLParser.NodeType.OpenTag, name: this._currentTagName, closed: false, attributes: this._currentTagAttributes, pos: this._currentTagStartPos});
 
317
            return;
 
318
        }
 
319
 
 
320
        // <tag |attr
 
321
        let attributeNameStartPos = this._pos;
 
322
 
 
323
        let attributeName = this._consumeRegex(/[^\s=/>]+/);
 
324
        // console.assert(attributeName.length > 0, "Unexpected empty attribute name");
 
325
        if (this._peekString("/") || this._peekString(">")) {
 
326
            if (attributeName)
 
327
                this._pushAttribute({name: attributeName, value: undefined, namePos: attributeNameStartPos});
 
328
            return;
 
329
        }
 
330
 
 
331
        this._consumeWhitespace();
 
332
 
 
333
        if (this._peekString("=")) {
 
334
            this._pos++;
 
335
 
 
336
            // <tag attr=|value
 
337
            let attributeValueStartPos = this._pos;
 
338
 
 
339
            this._consumeWhitespace();
 
340
 
 
341
            if (this._peekString(`"`)) {
 
342
                let attributeValue = this._consumeDoubleQuotedString();
 
343
                this._pushAttribute({name: attributeName, value: attributeValue, quote: HTMLParser.AttrQuoteType.Double, namePos: attributeNameStartPos, valuePos: attributeValueStartPos});
 
344
                return;
 
345
            }
 
346
 
 
347
            if (this._peekString(`'`)) {
 
348
                let attributeValue = this._consumeSingleQuotedString();
 
349
                this._pushAttribute({name: attributeName, value: attributeValue, quote: HTMLParser.AttrQuoteType.Single, namePos: attributeNameStartPos, valuePos: attributeValueStartPos});
 
350
                return;
 
351
            }
 
352
 
 
353
            if (this._peekString(">")) {
 
354
                this._pos++;
 
355
                this._mode = HTMLParser.Mode.Data;
 
356
                this._push({type: HTMLParser.NodeType.OpenTag, name: this._currentTagName, closed: false, attributes: this._currentTagAttributes, pos: this._currentTagStartPos});
 
357
                return;
 
358
            }
 
359
 
 
360
            let whitespace = this._consumeWhitespace();
 
361
            if (whitespace) {
 
362
                this._pushAttribute({name: attributeName, value: undefined, quote: HTMLParser.AttrQuoteType.None, namePos: attributeNameStartPos});
 
363
                return;
 
364
            }
 
365
 
 
366
            let attributeValue = this._consumeRegex(/[^\s=>]+/);
 
367
            this._pushAttribute({name: attributeName, value: attributeValue, quote: HTMLParser.AttrQuoteType.None, namePos: attributeNameStartPos, valuePos: attributeValueStartPos});
 
368
            return;
 
369
        }
 
370
 
 
371
        if (!this._isEOF()) {
 
372
            this._pushAttribute({name: attributeName, value: undefined, quote: HTMLParser.AttrQuoteType.None, namePos: attributeNameStartPos});
 
373
            return;
 
374
        }
 
375
 
 
376
        // End of document. Treat everything up to now as error text.
 
377
        console.assert(this._isEOF());
 
378
        this._push({type: HTMLParser.NodeType.ErrorText, data: this._data.substring(this._currentTagStartPos), pos: this._currentTagStartPos});
 
379
        return;
 
380
    }
 
381
 
 
382
    _parseComment()
 
383
    {
 
384
        let text = this._consumeUntilString("-->", HTMLParser.Mode.Data);
 
385
        if (this._isEOF() && !this._data.endsWith("-->")) {
 
386
            this._push({type: HTMLParser.NodeType.ErrorText, data: this._data.substring(this._currentTagStartPos), pos: this._currentTagStartPos});
 
387
            return;
 
388
        }
 
389
 
 
390
        let closePos = this._pos - "-->".length;
 
391
        this._push({type: HTMLParser.NodeType.Comment, data: text, pos: this._currentTagStartPos, closePos});
 
392
    }
 
393
 
 
394
    _parseBogusComment()
 
395
    {
 
396
        let text = this._consumeUntilString(">", HTMLParser.Mode.Data);
 
397
        if (this._isEOF() && !this._data.endsWith(">")) {
 
398
            this._push({type: HTMLParser.NodeType.ErrorText, data: this._data.substring(this._currentTagStartPos), pos: this._currentTagStartPos});
 
399
            return;
 
400
        }
 
401
 
 
402
        let closePos = this._pos - ">".length;
 
403
        this._push({type: HTMLParser.NodeType.Comment, data: text, opener: this._bogusCommentOpener || "", pos: this._currentTagStartPos, closePos});
 
404
        this._bogusCommentOpener = null;
 
405
    }
 
406
 
 
407
    _parseDoctype()
 
408
    {
 
409
        let text = this._consumeUntilString(">", HTMLParser.Mode.Data);
 
410
        if (this._isEOF() && !this._data.endsWith(">")) {
 
411
            this._push({type: HTMLParser.NodeType.ErrorText, data: this._data.substring(this._currentTagStartPos), pos: this._currentTagStartPos});
 
412
            return;
 
413
        }
 
414
 
 
415
        let closePos = this._pos - ">".length;
 
416
        this._push({type: HTMLParser.NodeType.Doctype, data: text, raw: this._doctypeRaw, pos: this._currentTagStartPos, closePos});
 
417
        this._doctypeRaw = null;
 
418
    }
 
419
 
 
420
    _parseCData()
 
421
    {
 
422
        let text = this._consumeUntilString("]]>", HTMLParser.Mode.Data);
 
423
        if (this._isEOF() && !this._data.endsWith("]]>")) {
 
424
            this._push({type: HTMLParser.NodeType.ErrorText, data: this._data.substring(this._currentTagStartPos), pos: this._currentTagStartPos});
 
425
            return;
 
426
        }
 
427
 
 
428
        let closePos = this._pos - "]]>".length;
 
429
        this._push({type: HTMLParser.NodeType.CData, data: text, pos: this._currentTagStartPos, closePos});
 
430
    }
 
431
 
 
432
    _pushAttribute(attr)
 
433
    {
 
434
        this._currentTagAttributes.push(attr);
 
435
        this._handleEOF(this._currentTagStartPos);
 
436
    }
 
437
 
 
438
    _handleEOF(lastPosition)
 
439
    {
 
440
        if (!this._isEOF())
 
441
            return false;
 
442
 
 
443
        // End of document. Treat everything from the last position as error text.
 
444
        this._push({type: HTMLParser.NodeType.ErrorText, data: this._data.substring(lastPosition), pos: lastPosition});
 
445
        return true;
 
446
    }
 
447
 
 
448
    _push(node)
 
449
    {
 
450
        // Custom mode for some elements.
 
451
        if (node.type === HTMLParser.NodeType.OpenTag) {
 
452
            if (!this._isXML && node.name.toLowerCase() === "script")
 
453
                this._mode = HTMLParser.Mode.ScriptData;
 
454
        }
 
455
 
 
456
        this._treeBuilder.pushParserNode(node);
 
457
    }
 
458
};
 
459
 
 
460
HTMLParser.Mode = {
 
461
    Data: "data",
 
462
    TagOpen: "tag-open",
 
463
    ScriptData: "script-data",
 
464
    Attr: "attr",
 
465
    CData: "cdata",
 
466
    Doctype: "doctype",
 
467
    Comment: "comment",
 
468
    BogusComment: "bogus-comment",
 
469
};
 
470
 
 
471
HTMLParser.NodeType = {
 
472
    Text: "text",
 
473
    ErrorText: "error-text",
 
474
    OpenTag: "open-tag",
 
475
    CloseTag: "close-tag",
 
476
    Comment: "comment",
 
477
    Doctype: "doctype",
 
478
    CData: "cdata",
 
479
};
 
480
 
 
481
HTMLParser.AttrQuoteType = {
 
482
    None: "none",
 
483
    Double: "double",
 
484
    Single: "single",
 
485
};