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

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

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 1.7 by wakaba, Fri Apr 25 23:03:35 2008 UTC revision 1.15 by wakaba, Tue Apr 29 03:29:41 2008 UTC
# Line 1  Line 1 
1  <!DOCTYPE HTML>  <!DOCTYPE HTML>
2  <html lang=en>  <html lang=en>
3  <head>  <head>
4  <title>Demo of HTML5 Parsing Algorithm with Scripting Enabled</title>  <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>  <style>
9    h1, h2 {    h1 {
10        margin: 0;
11        font-size: 150%;
12      }
13      h2 {
14      margin: 0;      margin: 0;
15      font-size: 100%;      font-size: 100%;
16    }    }
17    p, pre {    p {
18      margin: 0;      margin: 0 1em;
19    }    }
20    textarea {    textarea {
21      width: 100%;      width: 100%;
# Line 34  Line 41 
41    } // update    } // update
42    
43    function update2 () {    function update2 () {
     document.logElement.textContent = '';  
44      var v = document.sourceElement.value;      var v = document.sourceElement.value;
45      var p = new Parser (new InputStream (v));      if (v != document.previousSourceText) {
46      var doc = p.doc;        document.previousSourceText = v;
47      p.parse ();        document.links['permalink'].href
48      log (dumpTree (doc, ''));            = location.pathname + '?s=' + encodeURIComponent (v);
49          document.links['ldvlink'].href
50      document.links['permalink'].href            = 'http://software.hixie.ch/utilities/js/live-dom-viewer/?'
51          = location.href + '?s=' + encodeURIComponent (v);            + 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    } // update2
65    
66    var logIndentLevel = 0;    var logIndentLevel = 0;
67    function log (s) {    function log (s) {
68        var indent = '';
69      for (var i = 0; i < logIndentLevel; i++) {      for (var i = 0; i < logIndentLevel; i++) {
70        s = '  ' + s;        indent += '  ';
71      }      }
72        s = indent + s.replace (/\n/g, "\n" + indent);
73      document.logElement.appendChild (document.createTextNode (s + "\n"));      document.logElement.appendChild (document.createTextNode (s + "\n"));
74    } // log    } // log
75    
# Line 65  Line 85 
85      }      }
86      this.doc = doc;      this.doc = doc;
87      this.openElements = [doc];      this.openElements = [doc];
88      this.in = i;      this.input = i;
89      this.scriptsExecutedAfterParsing = [];      this.scriptsExecutedAfterParsing = [];
90        this.scriptsExecutedSoon = [];
91        this.scriptsExecutedAsynchronously = [];
92    } // Parser    } // Parser
93    
94    Parser.prototype.getNextToken = function () {    Parser.prototype.getNextToken = function () {
95      var p = this;      var p = this;
96      var i = this.in;      var i = this.input;
97      if (this.parseMode == 'script') {      if (this.parseMode == 'cdata') {
98          var tagName = this.endTagName;
99        var token;        var token;
100        if (p.insertionPoint <= 0) {        if (p.insertionPoint <= 0) {
101          return {type: 'abort'};          return {type: 'abort'};
# Line 90  Line 113 
113          return '';          return '';
114        });        });
115        if (token) return token;        if (token) return token;
116        i.s = i.s.replace (/^<\/[Ss][Cc][Rr][Ii][Pp][Tt]>/, function (s) {        var pattern = new RegExp ('^</' + tagName + '>', 'i');
117          i.s = i.s.replace (pattern, function (s) {
118          if (p.insertionPoint < s.length) {          if (p.insertionPoint < s.length) {
119            token = {type: 'abort'};            token = {type: 'abort'};
120            return s;            return s;
121          }          }
122          token = {type: 'end-tag', value: 'script'};          token = {type: 'end-tag', value: tagName};
123          p.insertionPoint -= s.length;          p.insertionPoint -= s.length;
124          return '';          return '';
125        });        });
126        if (token) return token;        if (token) return token;
127        var m;        var m;
128        if ((p.insertionPoint < '</script'.length) &&        if ((p.insertionPoint < ('</' + tagName).length) &&
129            (m = i.s.match (/^<\/([SCRIPTscript]+)/))) {            (m = i.s.match (/^<\/([A-Za-z]+)/))) {
130          var v = m[1].substring (0, p.insertionPoint).toLowerCase ();          var v = m[1].substring (0, p.insertionPoint).toLowerCase ();
131          if (v == 'script'.substring (0, p.insertionPoint - '</'.length)) {          if (v == tagName.substring (0, p.insertionPoint - '</'.length)) {
132            return {type: 'abort'};            return {type: 'abort'};
133          }          }
134        }        }
# Line 144  Line 168 
168          tagName = v.toLowerCase ();          tagName = v.toLowerCase ();
169          return '';          return '';
170        });        });
171        e = e.replace (/^\s*([^\s=]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"']+)))?/,        while (true) {
172        function (x, attrName, attrValue1, attrValue2, attrValue3) {          var m = false;
173          v = attrValue1 || attrValue2 || attrValue3;          e = e.replace (/^\s*([^\s=]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"'\s]*)))?/,
174          v = v.replace (/&quot;/g, '"').replace (/&apos;/g, "'")          function (x, attrName, attrValue1, attrValue2, attrValue3) {
175              .replace (/&amp;/g, '&');            v = attrValue1 || attrValue2 || attrValue3;
176          attrs[attrName.toLowerCase ()] = v;            v = v.replace (/&quot;/g, '"').replace (/&apos;/g, "'")
177          return '';                .replace (/&amp;/g, '&');
178        });            attrs[attrName.toLowerCase ()] = v;
179              m = true;
180              return '';
181            });
182            if (!m) break;
183          }
184        if (e.length) {        if (e.length) {
185          log ('Broken start tag: "' + e + '"');          log ('Broken start tag: "' + e + '"');
186        }        }
# Line 204  Line 233 
233            el.manakaiParserInserted = true;            el.manakaiParserInserted = true;
234    
235            // 3. Switch the tokeniser's content model flag to the CDATA state.            // 3. Switch the tokeniser's content model flag to the CDATA state.
236            this.parseMode = 'script';            this.parseMode = 'cdata';
237              this.endTagName = 'script';
238    
239            // 4.1. Collect all the character tokens.            // 4.1. Collect all the character tokens.
240            while (true) {            while (true) {
# Line 218  Line 248 
248              // 4.2. Until it returns a token that is not a character token, or              // 4.2. Until it returns a token that is not a character token, or
249              // until it stops tokenising.              // until it stops tokenising.
250              } else if (token.type == 'eof' ||              } else if (token.type == 'eof' ||
251                         (token.type == 'end-tag' && token.value == 'script') ||                         token.type == 'end-tag' ||
252                         token.type == 'abort') {                         token.type == 'abort') {
253                // 6. Switched back to the PCDATA state.                // 6. Switched back to the PCDATA state.
254                this.parseMode = 'pcdata';                this.parseMode = 'pcdata';
255    
256                // 7.1. If the next token is not an end tag token with ...                // 7.1. If the next token is not an end tag token with ...
257                if (token.type != 'end-tag') {                if (!(token.type == 'end-tag' && token.value == 'script')) {
258                  // 7.2. This is a parse error.                  // 7.2. This is a parse error.
259                  log ('Parse error: no </' + 'script>');                  log ('Parse error: no </' + 'script>');
260    
# Line 288  Line 318 
318                //                //
319              }              }
320            }            }
321            } else if (token.value == 'style' ||
322                       token.value == 'noscript' ||
323                       token.value == 'xmp') {
324              // 1. Create an element for the token in the HTML namespace.
325              var el = new JSElement (this.doc, token.value);
326    
327              // 2. Append the new element to the current node.
328              this.openElements[this.openElements.length - 1].appendChild (el);
329    
330              // 3. Switch the tokeniser's content model flag to the CDATA state.
331              this.parseMode = 'cdata';
332              this.endTagName = token.value;
333    
334              // 4.1. Collect all the character tokens.
335              while (true) {
336                var token = this.getNextToken ();
337                log ('token: ' + token.type + ' "' + token.value + '"');
338    
339                if (token.type == 'char') {
340                  // 5. Append a single Text node to the script element node.
341                  el.manakaiAppendText (token.value);
342    
343                // 4.2. Until it returns a token that is not a character token, or
344                // until it stops tokenising.
345                } else if (token.type == 'eof' ||
346                           token.type == 'end-tag' ||
347                           token.type == 'abort') {
348                  // 6. Switched back to the PCDATA state.
349                  this.parseMode = 'pcdata';
350    
351                  // 7.1. If the next token is not an end tag token with ...
352                  if (!(token.type == 'end-tag' &&
353                        token.value == this.endTagName)) {
354                    // 7.2. This is a parse error.
355                    log ('Parse error: no </' + this.endTagName + '>');
356    
357                    // 7.3. Mark the script element as "already executed".
358                    el.manakaiAlreadyExecuted = true;
359                  } else {
360                    // 7.4. Ignore it.
361                    //
362                  }
363                  break;
364                }
365              }
366          } else {          } else {
367            var el = new JSElement (this.doc, token.value);            var el = new JSElement (this.doc, token.value);
368            this.openElements[this.openElements.length - 1].appendChild (el);            this.openElements[this.openElements.length - 1].appendChild (el);
# Line 318  Line 393 
393    
394      // "When a script completes loading" rules start applying.      // "When a script completes loading" rules start applying.
395    
396      // TODO: Handles "list of scripts that will execute as soon as possible"      while (this.scriptsExecutedSoon.length > 0 ||
397      // and "list of scripts that will execute asynchronously"             this.scriptsExecutedAsynchronously.length > 0) {
398          // Handle "list of scripts that will execute as soon as possible".
399          while (this.scriptsExecutedSoon.length > 0) {
400            var e = this.scriptsExecutedSoon.shift ();
401      
402            // If it has completed loading
403            log ('Execute an external script not inserted by parser...');
404            executeScript (this.doc, e);
405    
406            // NOTE: It MAY be executed before the end of the parsing, according
407            // to the spec.
408            this.hasAsyncScript = true;
409          }
410    
411          // Handle "list of scripts that will execute asynchronously".
412          while (this.scriptsExecutedAsynchronously.length > 0) {
413            var e = this.scriptsExecutedAsynchronously.shift ();
414    
415            // Step 1.
416            // We assume that all scripts have been loaded at this time.
417      
418            // Step 2.
419            log ('Execute an asynchronous script...');
420            executeScript (this.doc, e);
421    
422            // Step 3.
423            //
424    
425            // Step 4.
426            //
427    
428            this.hasAsyncScript = true;
429          }
430        }
431    
432      // Handle "list of scripts that will execute when the document has finished      // Handle "list of scripts that will execute when the document has finished
433      // parsing".      // parsing".
# Line 339  Line 447 
447    
448      log ('DOMContentLoaded event fired');      log ('DOMContentLoaded event fired');
449    
450      // "delays tha load event" things has completed:      // "delays the load event" things has completed:
451      // readyState = 'complete'      // readyState = 'complete'
452      log ('load event fired');      log ('load event fired');
453    
# Line 350  Line 458 
458      if (ip == undefined || ip == null || isNaN (ip)) {      if (ip == undefined || ip == null || isNaN (ip)) {
459        log ('insertion point: set to undefined');        log ('insertion point: set to undefined');
460        this.insertionPoint = undefined;        this.insertionPoint = undefined;
461      } else if (ip == this.in.s.length) {      } else if (ip == this.input.s.length) {
462        log ('insertion point: end of file');        log ('insertion point: end of file');
463        this.insertionPoint = ip;        this.insertionPoint = ip;
464      } else {      } else {
465        log ('insertion point: set to ' + ip +        log ('insertion point: set to ' + ip +
466             ' (before "' + this.in.s.substring (0, 10) + '")');             ' (before "' + this.input.s.substring (0, 10) + '")');
467        this.insertionPoint = ip;        this.insertionPoint = ip;
468      }      }
469    }; // setInsertionPoint    }; // setInsertionPoint
# Line 395  Line 503 
503        // 2.4. If the script element has its "already executed" flag set        // 2.4. If the script element has its "already executed" flag set
504        if (e.manakaiAlreadyExecuted) {        if (e.manakaiAlreadyExecuted) {
505          // 2.5. Abort these steps at this point.          // 2.5. Abort these steps at this point.
506          log ('Running a script: aborted');          log ('Running a script: aborted (already executed)');
507          logIndentLevel--;          logIndentLevel--;
508          return e;          return e;
509        }        }
# Line 414  Line 522 
522          p.scriptsExecutedAfterParsing.push (e);          p.scriptsExecutedAfterParsing.push (e);
523          log ('Running a script: aborted (defer)');          log ('Running a script: aborted (defer)');
524        } else if (e.async && e.src != null) {        } else if (e.async && e.src != null) {
525          // TODO          p.scriptsExecutedAsynchronously.push (e);
526        } else if (e.async && e.src == null          log ('Running a script: aborted (async src)');
527                   /* && list of scripts that will execute asynchronously is not empty */) {        } else if (e.async && e.src == null &&
528          // TODO                   p.scriptsExecutedAsynchronously.length > 0) {
529            p.scriptsExecutedAsynchronously.push (e);
530            log ('Running a script: aborted (async)');
531            // ISSUE: What is the difference with the case above?
532        } else if (e.src != null && e.manakaiParserInserted) {        } else if (e.src != null && e.manakaiParserInserted) {
533          if (p.scriptExecutedWhenParserResumes) {          if (p.scriptExecutedWhenParserResumes) {
534            log ('Error: There is a script that will execute as soon as the parser resumes.');            log ('Error: There is a script that will execute as soon as the parser resumes.');
535          }          }
536          p.scriptExecutedWhenParserResumes = e;          p.scriptExecutedWhenParserResumes = e;
537          log ('Running a script: aborted (src)');          log ('Running a script: aborted (src parser-inserted)');
538        } else if (e.src != null) {        } else if (e.src != null) {
539          // TODO          p.scriptsExecutedSoon.push (e);
540            log ('Running a script: aborted (src)');
541        } else {        } else {
542          executeScript (doc, e); // even if other scripts are already executing.          executeScript (doc, e); // even if other scripts are already executing.
543        }        }
# Line 473  Line 585 
585        var m;        var m;
586        if (m = uri.match (/^javascript:\s*(?:'([^']*)'|"([^"]+)")\s*$/i)) {        if (m = uri.match (/^javascript:\s*(?:'([^']*)'|"([^"]+)")\s*$/i)) {
587          if (m[1]) {          if (m[1]) {
588            return m[1];            return unescapeJSLiteral (m[1]);
589          } else if (m[2]) {          } else if (m[2]) {
590            return m[2];            return unescapeJSLiteral (m[2]);
591          } else {          } else {
592            return null;            return null;
593          }          }
# Line 496  Line 608 
608          matched = true;          matched = true;
609          var args = [];          var args = [];
610          t.replace (/('[^']*'|"[^"]*")/g, function (s, v) {          t.replace (/('[^']*'|"[^"]*")/g, function (s, v) {
611            args.push (v.substring (1, v.length - 1));            args.push (unescapeJSLiteral (v.substring (1, v.length - 1)));
612            return '';            return '';
613          });          });
614          doc.write.apply (doc, args);          doc.write.apply (doc, args);
615          return '';          return '';
616        });        });
617          var noDocumentElement = false;
618          s = s.replace (/^\s*var\s+s\s*=\s*document\.createElement\s*\(\s*['"]script['"]\s*\)\s*;\s*s\.src\s*=\s*(?:'([^']*)'|"([^"]*)")\s*;\s*document\.documentElement\.appendChild\s*\(\s*s\s*\)\s*;\s*/,
619          function (s, t, u) {
620            matched = true;
621            var args = [unescapeJSLiteral (t ? t : u)];
622            noDocumentElement = !doc._insertExternalScript.apply (doc, args);
623            return '';
624          });
625          if (noDocumentElement) {
626            log ('Script error: documentElement is null');
627            break;
628          }
629          s = s.replace (/^\s*w\s*\(\s*document\.documentElement\.innerHTML\s*\)\s*;\s*/,
630          function (s, t) {
631            matched = true;
632            log (dumpTree (doc, ''));
633            return '';
634          });
635        if (s == '') break;        if (s == '') break;
636        if (!matched) {        if (!matched) {
637          log ('Script parse error: "' + s + '"');          log ('Script parse error: "' + s + '"');
# Line 510  Line 640 
640      }      }
641    } // parseAndRunScript    } // parseAndRunScript
642    
643      function unescapeJSLiteral (s) {
644        return s.replace (/\\u([0-9A-Fa-f]{4})/g, function (t, v) {
645          return String.fromCharCode (parseInt ('0x' + v));
646        });
647      } // unescapeJSLiteral
648    
649    function JSText (data) {    function JSText (data) {
650      this.data = data;      this.data = data;
651    } // JSText    } // JSText
# Line 537  Line 673 
673      // Step 3.      // Step 3.
674      if (this._parser &&      if (this._parser &&
675          !this._parser.scriptCreated &&          !this._parser.scriptCreated &&
676          this._parser.in.insertionPoint != undefined) {          this._parser.input.insertionPoint != undefined) {
677        log ('document.open () in parsing mode is ignored');        log ('document.open () in parsing mode is ignored');
678        return this;        return this;
679      }      }
# Line 571  Line 707 
707      }      }
708    
709      // Step 11.      // Step 11.
710      this._parser.setInsertionPoint (this._parser.in.s.length);      this._parser.setInsertionPoint (this._parser.input.s.length);
711    
712      // Step 12.      // Step 12.
713      return this;      return this;
714    }; // document.open    }; // document.open
715    
716    JSDocument.prototype.write = function () {    JSDocument.prototype.write = function () {
717        log ('document.write: start');
718      logIndentLevel++;      logIndentLevel++;
719    
720      var p = this._parser;      var p = this._parser;
# Line 591  Line 728 
728      // 2. ... inserted into the input stream just before the insertion point.      // 2. ... inserted into the input stream just before the insertion point.
729      var s = Array.join (arguments, '');      var s = Array.join (arguments, '');
730      log ('document.write: insert "' + s + '"' +      log ('document.write: insert "' + s + '"' +
731           ' before "' + p.in.s.substring (p.insertionPoint, p.insertionPoint + 10) + '"');           ' before "' +
732      p.in.s = p.in.s.substring (0, p.insertionPoint) + s           p.input.s.substring (p.insertionPoint, p.insertionPoint + 10) + '"');
733          + p.in.s.substring (p.insertionPoint, p.in.s.length);      p.input.s = p.input.s.substring (0, p.insertionPoint) + s
734            + p.input.s.substring (p.insertionPoint, p.input.s.length);
735      p.insertionPoint += s.length;      p.insertionPoint += s.length;
736