/[suikacvs]/markup/html/scripting-parser/parser.html
Suika

Contents of /markup/html/scripting-parser/parser.html

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.21 - (show annotations) (download) (as text)
Sun Jun 20 03:39:12 2010 UTC (16 years, 4 months ago) by wakaba
Branch: MAIN
CVS Tags: HEAD
Changes since 1.20: +6 -5 lines
File MIME type: text/html
was not committed

1 <!DOCTYPE HTML>
2 <html lang=en>
3 <head>
4 <title>Live Scripting HTML Parser</title>
5 <link rel=author href="http://suika.fam.cx/~wakaba/who?">
6 <link rel=license href="http://suika.fam.cx/c/gnu/gpl"
7 title="GNU GPL2 or later">
8 <style>
9 h1 {
10 margin: 0;
11 font-size: 150%;
12 }
13 h2 {
14 margin: 0;
15 font-size: 100%;
16 }
17 p {
18 margin: 0 1em;
19 }
20 textarea {
21 width: 100%;
22 -width: 99%;
23 height: 10em;
24 }
25 output {
26 display: block;
27 font-family: monospace;
28 white-space: -moz-pre-wrap;
29 white-space: pre-wrap;
30 }
31 </style>
32 <script>
33 var delayedUpdater = 0;
34
35 function update () {
36 if (delayedUpdater) {
37 clearTimeout (delayedUpdater);
38 delayedUpdater = 0;
39 }
40 delayedUpdater = setTimeout (update2, 100);
41 } // update
42
43 function update2 () {
44 var v = document.sourceElement.value;
45 if (v != document.previousSourceText) {
46 document.previousSourceText = v;
47 document.links['permalink'].href
48 = location.pathname + '?s=' + encodeURIComponent (v);
49 document.links['ldvlink'].href
50 = 'http://software.hixie.ch/utilities/js/live-dom-viewer/?'
51 + encodeURIComponent (v);
52
53 document.logElement.textContent = '';
54 var p = new Parser (new InputStream (v));
55 var doc = p.doc;
56 p.parse ();
57
58 log (dumpTree (doc, ''));
59
60 if (p.hasAsyncScript) {
61 log ('Some script codes are executed asynchronously; it means that the document might be rendered in different ways depending on the network condition and other factors');
62 }
63 }
64 } // update2
65
66 var logIndentLevel = 0;
67 function log (s) {
68 var indent = '';
69 for (var i = 0; i < logIndentLevel; i++) {
70 indent += ' ';
71 }
72 s = indent + s.replace (/\n/g, "\n" + indent);
73 document.logElement.appendChild (document.createTextNode (s + "\n"));
74 } // log
75
76 function InputStream (s) {
77 this.s = s;
78 } // InputStream
79
80 function Parser (i, doc) {
81 this.parseMode = 'pcdata';
82 if (!doc) {
83 doc = new JSDocument (this);
84 doc.manakaiIsHTML = true;
85 }
86 this.nextToken = [];
87 this.doc = doc;
88 this.openElements = [doc];
89 this.input = i;
90 this.scriptsExecutedAfterParsing = [];
91 this.scriptsExecutedSoon = [];
92 this.scriptsExecutedAsynchronously = [];
93 } // Parser
94
95 Parser.prototype.getNextToken = function () {
96 if (this.nextToken.length) {
97 return this.nextToken.shift ();
98 }
99
100 var p = this;
101 var i = this.input;
102 if (this.parseMode == 'cdata') {
103 var tagName = this.endTagName;
104 var token;
105 if (p.insertionPoint <= 0) {
106 return {type: 'abort'};
107 }
108 i.s = i.s.replace (/^([^<]+)/,
109 function (s, t) {
110 if (0 < p.insertionPoint && p.insertionPoint < t.length) {
111 token = {type: 'char', value: t.substring (0, p.insertionPoint)};
112 var ip = p.insertionPoint;
113 p.insertionPoint = 0;
114 return t.substring (ip, t.length);
115 }
116 token = {type: 'char', value: t};
117 p.insertionPoint -= t.length;
118 return '';
119 });
120 if (token) return token;
121 var pattern = new RegExp ('^</' + tagName + '>', 'i');
122 i.s = i.s.replace (pattern, function (s) {
123 if (p.insertionPoint < s.length) {
124 token = {type: 'abort'};
125 return s;
126 }
127 token = {type: 'end-tag', value: tagName};
128 p.insertionPoint -= s.length;
129 return '';
130 });
131 if (token) return token;
132 var m;
133 if ((p.insertionPoint < ('</' + tagName).length) &&
134 (m = i.s.match (/^<\/([A-Za-z]+)/))) {
135 var v = m[1].substring (0, p.insertionPoint).toLowerCase ();
136 if (v == tagName.substring (0, p.insertionPoint - '</'.length)) {
137 return {type: 'abort'};
138 }
139 }
140 i.s = i.s.replace (/^</,
141 function (s) {
142 token = {type: 'char', value: s};
143 p.insertionPoint -= s.length;
144 return '';
145 });
146 if (token) return token;
147 return {type: 'eof'};
148 }
149
150 var token;
151 i.s = i.s.replace (/^<\/([^>]+)(?:>|$)/, function (s, e) {
152 if (p.insertionPoint < s.length ||
153 (p.insertionPoint <= s.length &&
154 s.substring (s.length - 1, s.length) != '>')) {
155 token = {type: 'abort'};
156 return s;
157 }
158 token = {type: 'end-tag', value: e.toLowerCase ()};
159 p.insertionPoint -= s.length;
160 return '';
161 });
162 if (token) return token;
163 i.s = i.s.replace (/^<([^>]+)(?:>|$)/, function (s, e) {
164 if (p.insertionPoint < s.length ||
165 (p.insertionPoint <= s.length &&
166 s.substring (s.length - 1, s.length) != '>')) {
167 token = {type: 'abort'};
168 return s;
169 }
170 var tagName;
171 var attrs = {};
172 e = e.replace (/^[\S]+/, function (v) {
173 tagName = v.toLowerCase ();
174 return '';
175 });
176 while (true) {
177 var m = false;
178 e = e.replace (/^\s*([^\s=]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"'\s]*)))?/,
179 function (x, attrName, attrValue1, attrValue2, attrValue3) {
180 v = attrValue1 || attrValue2 || attrValue3;
181 v = v.replace (/&quot;/g, '"').replace (/&apos;/g, "'")
182 .replace (/&amp;/g, '&');
183 attrs[attrName.toLowerCase ()] = v;
184 m = true;
185 return '';
186 });
187 if (!m) break;
188 }
189 if (e.length) {
190 log ('Broken start tag: "' + e + '"');
191 }
192 token = {type: 'start-tag', value: tagName, attrs: attrs};
193 p.insertionPoint -= s.length;
194 return '';
195 });
196 if (token) return token;
197 if (p.insertionPoint <= 0) {
198 return {type: 'abort'};
199 }
200 i.s = i.s.replace (/^[^<]+/, function (s) {
201 if (p.insertionPoint < s.length) {
202 token = {type: 'char', value: s.substring (0, p.insertionPoint)};
203 var ip = p.insertionPoint;
204 p.insertionPoint = 0;
205 return s.substring (ip, s.length);
206 }
207 token = {type: 'char', value: s};
208 p.insertionPoint -= s.length;
209 return '';
210 });
211 if (token) return token;
212 i.s = i.s.replace (/^[\s\S]/, function (s) {
213 token = {type: 'char', value: s};
214 p.insertionPoint -= s.length;
215 return '';
216 });
217 if (token) return token;
218 return {type: 'eof'};
219 } // getNextToken
220
221 Parser.prototype.parse = function () {
222 logIndentLevel++;
223 log ('parse: start');
224
225 while (true) {
226 var token = this.getNextToken ();
227 log ('token: ' + token.type + ' "' + token.value + '"');
228
229 if (this.cdataEndTagRequired) {
230 // Generic CDATA parsing algorithm
231
232 if (token.type != 'abort') {
233 // 7.
234 if (token.type == 'end-tag' && token.value == this.endTagName) {
235 // 7.1. Ignores it.
236 //
237 } else {
238 // 7.2. Parse error.
239 log ('Parse error: no </' + this.endTagName + '>');
240 this.nextToken.unshift (token);
241 }
242 this.cdataEndTagRequired = false;
243 continue;
244 }
245 }
246
247 if (token.type == 'start-tag') {
248 if (token.value == 'script') {
249 // 1. Create an element for the token in the HTML namespace.
250 var el = new JSElement (this.doc, token.value);
251 if (token.attrs.async != null) el.async = true;
252 if (token.attrs.defer != null) el.defer = true;
253 if (token.attrs.src != null) el.src = token.attrs.src;
254
255 // 2. Mark the element as being "parser-inserted".
256 el.manakaiParserInserted = true;
257
258 // 3. Switch the tokeniser's content model flag to the CDATA state.
259 this.parseMode = 'cdata';
260 this.endTagName = 'script';
261
262 // 4.1. Collect all the character tokens.
263 while (true) {
264 var token = this.getNextToken ();
265 log ('token: ' + token.type + ' "' + token.value + '"');
266
267 if (token.type == 'char') {
268 // 5. Append a single Text node to the script element node.
269 el.manakaiAppendText (token.value);
270
271 // 4.2. Until it returns a token that is not a character token, or
272 // until it stops tokenising.
273 } else if (token.type == 'eof' ||
274 token.type == 'end-tag' ||
275 token.type == 'abort') {
276 // 6. Switched back to the PCDATA state.
277 this.parseMode = 'pcdata';
278
279 // 7.1. If the next token is not an end tag token with ...
280 if (!(token.type == 'end-tag' && token.value == 'script')) {
281 // 7.2. This is a parse error.
282 log ('Parse error: no </' + 'script>');
283 this.nextToken.unshift (token);
284
285 // 7.3. Mark the script element as "already executed".
286 el.manakaiAlreadyExecuted = true;
287 } else {
288 // 7.4. Ignore it.
289 //
290 }
291 break;
292 }
293 }
294
295 // 8.1. If the parser were originally created for the ...
296 if (this.fragmentParsingMode) {
297 // 8.2. Mark the script element as "already executed" and ...
298 el.manakaiAlreadyExecuted = true;
299 continue;
300 }
301
302 // 9.1. Let the old insertion point have the same value as the ...
303 var oldInsertionPoint = this.insertionPoint;
304 // 9.2. Let the insertion point be just before the next input ...
305 this.setInsertionPoint (0);
306
307 // 10. Append the new element to the current node.
308 this.openElements[this.openElements.length - 1].appendChild (el);
309
310 // 11. Let the insertion point have the value of the old ...
311
312 oldInsertionPoint += this.insertionPoint;
313 this.setInsertionPoint (oldInsertionPoint);
314
315 // 12. If there is a pending external script
316 while (this.pendingExternalScript) {
317 // 12.1. If the tree construction stage is being called reentrantly
318 if (this.reentrant) {
319 log ('parse: abort (reentrance)');
320 logIndentLevel--;
321 return;
322
323 // 12.2. Otherwise
324 } else {
325 // 1.
326 var script = this.pendingExternalScript;
327 this.pendingExternalScript = null;
328
329 // 2. Pause until the script has completed loading.
330 //
331
332 // 3. Let the insertion point to just before the next input char.
333 this.setInsertionPoint (0);
334
335 // 4. Execute the script.
336 executeScript (this.doc, script);
337
338 // 5. Let the insertion point be undefined again.
339 this.setInsertionPoint (undefined);
340
341 // 6. If there is once again a script that will execute ...
342 //
343 }
344 }
345 } else if (token.value == 'style' ||
346 token.value == 'noscript' ||
347 token.value == 'xmp') {
348 // 1. Create an element for the token in the HTML namespace.
349 var el = new JSElement (this.doc, token.value);
350
351 // 2. Append the new element to the current node.
352 this.openElements[this.openElements.length - 1].appendChild (el);
353
354 // 3. Switch the tokeniser's content model flag to the CDATA state.
355 this.parseMode = 'cdata';
356 this.endTagName = token.value;
357
358 // 4.1. Collect all the character tokens.
359 while (true) {
360 var token = this.getNextToken ();
361 log ('token: ' + token.type + ' "' + token.value + '"');
362
363 if (token.type == 'char') {
364 // 5. Append a single Text node to the script element node.
365 el.manakaiAppendText (token.value);
366
367 // 4.2. Until it returns a token that is not a character token, or
368 // until it stops tokenising.
369 } else if (token.type == 'eof' ||
370 token.type == 'end-tag' ||
371 token.type == 'abort') {
372 // 6. Switched back to the PCDATA state.
373 this.parseMode = 'pcdata';
374
375 if (token.type == 'abort') {
376 this.cdataEndTagRequired = true;
377 break;
378 }
379
380 // 7.1. If the next token is not an end tag token with ...
381 if (!(token.type == 'end-tag' &&
382 token.value == this.endTagName)) {
383 // 7.2. This is a parse error.
384 log ('Parse error: no </' + this.endTagName + '>');
385 this.nextToken.unshift (token);
386
387 // 7.3. Mark the script element as "already executed".
388 el.manakaiAlreadyExecuted = true;
389 } else {
390 // 7.4. Ignore it.
391 //
392 }
393 break;
394 }
395 }
396 } else {
397 var el = new JSElement (this.doc, token.value);
398 this.openElements[this.openElements.length - 1].appendChild (el);
399 this.openElements.push (el);
400 }
401 } else if (token.type == 'end-tag') {
402 if (this.openElements[this.openElements.length - 1].localName ==
403 token.value) {
404 this.openElements.pop ();
405 } else {
406 log ('parse error: unmatched end tag: ' + token.value);
407 }
408 } else if (token.type == 'char') {
409 this.openElements[this.openElements.length - 1].manakaiAppendText
410 (token.value);
411 } else if (token.type == 'eof') {
412 break;
413 } else if (token.type == 'abort') {
414 log ('parse: abort');
415 logIndentLevel--;
416 return;
417 }
418 }
419
420 log ('stop parsing');
421
422 // readyState = 'interactive'
423
424 // "When a script completes loading" rules start applying.
425
426 while (this.scriptsExecutedSoon.length > 0 ||
427 this.scriptsExecutedAsynchronously.length > 0) {
428 // Handle "list of scripts that will execute as soon as possible".
429 while (this.scriptsExecutedSoon.length > 0) {
430 var e = this.scriptsExecutedSoon.shift ();
431
432 // If it has completed loading
433 log ('Execute an external script not inserted by parser...');
434 executeScript (this.doc, e);
435
436 // NOTE: It MAY be executed before the end of the parsing, according
437 // to the spec.
438 this.hasAsyncScript = true;
439 }
440
441 // Handle "list of scripts that will execute asynchronously".
442 while (this.scriptsExecutedAsynchronously.length > 0) {
443 var e = this.scriptsExecutedAsynchronously.shift ();
444
445 // Step 1.
446 // We assume that all scripts have been loaded at this time.
447
448 // Step 2.
449 log ('Execute an asynchronous script...');
450 executeScript (this.doc, e);
451
452 // Step 3.
453 //
454
455 // Step 4.
456 //
457
458 this.hasAsyncScript = true;
459 }
460 }
461
462 // Handle "list of scripts that will execute when the document has finished
463 // parsing".
464 var list = this.scriptsExecutedAfterParsing;
465 while (list.length > 0) {
466 // TODO: break unless completed loading
467
468 // Step 1.
469 //
470
471 // Step 2. and Step 3.
472 log ('Executing a |defer|red script...');
473 executeScript (this.doc, list.shift ());
474
475 // Step 4.
476 }
477
478 log ('DOMContentLoaded event fired');
479
480 // "delays the load event" things has completed:
481 // readyState = 'complete'
482 log ('load event fired');
483
484 logIndentLevel--;
485 } // parse
486
487 Parser.prototype.setInsertionPoint = function (ip) {
488 if (ip == undefined || ip == null || isNaN (ip)) {
489 log ('insertion point: set to undefined');
490 this.insertionPoint = undefined;
491 } else if (ip == this.input.s.length) {
492 log ('insertion point: end of file');
493 this.insertionPoint = ip;
494 } else {
495 log ('insertion point: set to ' + ip +
496 ' (before "' + this.input.s.substring (0, 10) + '")');
497 this.insertionPoint = ip;
498 }
499 }; // setInsertionPoint
500
501 function JSDocument (p) {
502 this.childNodes = [];
503 this._parser = p;
504 } // JSDocument
505
506 function JSElement (doc, localName) {
507 this.localName = localName;
508 this.ownerDocument = doc;
509 this.childNodes = [];
510 } // JSElement
511
512 JSDocument.prototype.appendChild = JSElement.prototype.appendChild =
513 function (e) {
514 this.childNodes.push (e);
515 e.parentNode = this;
516
517 if (e.localName == 'script') {
518 logIndentLevel++;
519 log ('Running a script: start');
520
521 var doc = this.ownerDocument || this;
522 var p = doc._parser;
523
524 // 1.The script's type
525 //
526
527 // 2. The cript's character encoding
528 //
529
530 // 3.1. If without script
531 //
532 // 2.2. If the script element was created by an XML ... innerHTML ...
533 //
534 // 2.3. If the user agent does not support the scripting language ...
535 //
536 if (false) {
537 // 2.5. Abort these steps at this point.
538 log ('Running a script: aborted (noscript)');
539 logIndentLevel--;
540 return e;
541 }
542
543 // 4. Set the element's "already executed" flag.
544 e.manakaiAlreadyExecuted = true;
545
546 // 5. If the element has a src attribute, then a load for ...
547 // TODO: load an external resource
548
549 // 5. The first of the following options:
550
551 if (/* TODO: If the document is still being parsed && */
552 e.defer && !e.async) {
553 // 6.1.
554 p.scriptsExecutedAfterParsing.push (e);
555 log ('Running a script: aborted (defer)');
556 } else if (e.async && e.src != null) {
557 // 6.2.
558 p.scriptsExecutedAsynchronously.push (e);
559 log ('Running a script: aborted (async src)');
560 } else if (e.async && e.src == null &&
561 p.scriptsExecutedAsynchronously.length > 0) {
562 // 6.3.
563 p.scriptsExecutedAsynchronously.push (e);
564 log ('Running a script: aborted (async)');
565 } else if (e.src != null && e.manakaiParserInserted) {
566 // 6.4.
567 if (p.pendingExternalScript) {
568 log ('Error: There is a pending external script.');
569 }
570 p.pendingExternalScript = e;
571 log ('Running a script: aborted (src parser-inserted)');
572 } else if (e.src != null) {
573 // 6.5.
574 p.scriptsExecutedSoon.push (e);
575 log ('Running a script: aborted (src)');
576 } else {
577 // 6.6.
578 executeScript (doc, e); // even if other scripts are already executing.
579 }
580
581 log ('Running a script: end');
582 logIndentLevel--;
583 }
584
585 return e;
586 }; // appendChild
587
588 function executeScript (doc, e) {
589 log ('executing a script block: start');
590
591 var s;
592 if (e.src != null) {
593 s = getExternalScript (e.src);
594
595 // If the load resulted in an error, then ... firing an error event ...
596 if (s == null) {
59