/[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.3 by wakaba, Sun Apr 20 10:02:43 2008 UTC revision 1.14 by wakaba, Tue Apr 29 02:50:00 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 {
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 {    textarea {
21       display: block;      width: 100%;
22       width: 80%;      -width: 99%;
23       margin-left: auto;      height: 10em;
      margin-right: auto;  
      min-height: 20em;  
24    }    }
25    output {    output {
26      display: block;      display: block;
27      font-family: monospace;      font-family: monospace;
28      white-space: pre;      white-space: -moz-pre-wrap;
29        white-space: pre-wrap;
30    }    }
31  </style>  </style>
32  <script>  <script>
33      var delayedUpdater = 0;
34    
35    function update () {    function update () {
36      document.logElement.textContent = '';      if (delayedUpdater) {
37      var p = new Parser (new InputStream (document.sourceElement.value));        clearTimeout (delayedUpdater);
38      p.parse ();        delayedUpdater = 0;
39      log (dumpTree (p.doc, ''));      }
40        delayedUpdater = setTimeout (update2, 100);
41    } // update    } // 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) {    function log (s) {
68        for (var i = 0; i < logIndentLevel; i++) {
69          s = '  ' + s;
70        }
71      document.logElement.appendChild (document.createTextNode (s + "\n"));      document.logElement.appendChild (document.createTextNode (s + "\n"));
72    } // log    } // log
73    
# Line 32  Line 75 
75      this.s = s;      this.s = s;
76    } // InputStream    } // InputStream
77    
78    function Parser (i) {    function Parser (i, doc) {
79      this.parseMode = 'pcdata';      this.parseMode = 'pcdata';
80      this.doc = new JSDocument (this);      if (!doc) {
81      this.openElements = [this.doc];        doc = new JSDocument (this);
82      this.in = i;        doc.manakaiIsHTML = true;
83        }
84        this.doc = doc;
85        this.openElements = [doc];
86        this.input = i;
87        this.scriptsExecutedAfterParsing = [];
88        this.scriptsExecutedSoon = [];
89        this.scriptsExecutedAsynchronously = [];
90    } // Parser    } // Parser
91    
92    Parser.prototype.getNextToken = function () {    Parser.prototype.getNextToken = function () {
93      var p = this;      var p = this;
94      var i = this.in;      var i = this.input;
95      if (this.parseMode == 'script') {      if (this.parseMode == 'cdata') {
96          var tagName = this.endTagName;
97        var token;        var token;
98        if (p.insertionPoint <= 0) {        if (p.insertionPoint <= 0) {
99          return {type: 'abort'};          return {type: 'abort'};
100        }        }
101        i.s = i.s.replace (/^([\s\S]+?)<\/[Ss][Cc][Rr][Ii][Pp][Tt]>/,        i.s = i.s.replace (/^([^<]+)/,
102        function (s, t) {        function (s, t) {
103          if (0 < p.insertionPoint && p.insertionPoint < t.length) {          if (0 < p.insertionPoint && p.insertionPoint < t.length) {
104            token = {type: 'char', value: t.substring (0, p.insertionPoint)};            token = {type: 'char', value: t.substring (0, p.insertionPoint)};
105            var ip = p.insertionPoint;            var ip = p.insertionPoint;
106            p.insertionPoint = 0;            p.insertionPoint = 0;
107            return t.substring (ip, t.length) +            return t.substring (ip, t.length);
               s.substring (s.length - 9, s.length);  
108          }          }
109          token = {type: 'char', value: t};          token = {type: 'char', value: t};
110          p.insertionPoint -= s.length;          p.insertionPoint -= t.length;
111          return '<' + '/script>';          return '';
112        });        });
113        if (token) return token;        if (token) return token;
114        i.s = i.s.replace (/^<\/[Ss][Cc][Rr][Ii][Pp][Tt]>/, function (s) {        var pattern = new RegExp ('^</' + tagName + '>', 'i');
115          if (s.length < p.insertionPoint) {        i.s = i.s.replace (pattern, function (s) {
116            if (p.insertionPoint < s.length) {
117            token = {type: 'abort'};            token = {type: 'abort'};
118            return s;            return s;
119          }          }
120          token = {type: 'end-tag', value: 'script'};          token = {type: 'end-tag', value: tagName};
121            p.insertionPoint -= s.length;
122            return '';
123          });
124          if (token) return token;
125          var m;
126          if ((p.insertionPoint < ('</' + tagName).length) &&
127              (m = i.s.match (/^<\/([A-Za-z]+)/))) {
128            var v = m[1].substring (0, p.insertionPoint).toLowerCase ();
129            if (v == tagName.substring (0, p.insertionPoint - '</'.length)) {
130              return {type: 'abort'};
131            }
132          }
133          i.s = i.s.replace (/^</,
134          function (s) {
135            token = {type: 'char', value: s};
136          p.insertionPoint -= s.length;          p.insertionPoint -= s.length;
137          return '';          return '';
138        });        });
# Line 75  Line 141 
141      }      }
142    
143      var token;      var token;
144      i.s = i.s.replace (/^<\/([^>]+)>/, function (s, e) {      i.s = i.s.replace (/^<\/([^>]+)(?:>|$)/, function (s, e) {
145        if (p.insertionPoint < s.length) {        if (p.insertionPoint < s.length ||
146              (p.insertionPoint <= s.length &&
147               s.substring (s.length - 1, 1) != '>')) {
148          token = {type: 'abort'};          token = {type: 'abort'};
149          return s;          return s;
150        }        }
# Line 85  Line 153 
153        return '';        return '';
154      });      });
155      if (token) return token;      if (token) return token;
156      i.s = i.s.replace (/^<([^>]+)>/, function (s, e) {      i.s = i.s.replace (/^<([^>]+)(?:>|$)/, function (s, e) {
157        if (p.insertionPoint < s.length) {        if (p.insertionPoint < s.length ||
158              (p.insertionPoint <= s.length &&
159               s.substring (s.length - 1, 1) != '>')) {
160          token = {type: 'abort'};          token = {type: 'abort'};
161          return s;          return s;
162        }        }
163        token = {type: 'start-tag', value: e.toLowerCase ()};        var tagName;
164          var attrs = {};
165          e = e.replace (/^[\S]+/, function (v) {
166            tagName = v.toLowerCase ();
167            return '';
168          });
169          while (true) {
170            var m = false;
171            e = e.replace (/^\s*([^\s=]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"'\s]*)))?/,
172            function (x, attrName, attrValue1, attrValue2, attrValue3) {
173              v = attrValue1 || attrValue2 || attrValue3;
174              v = v.replace (/&quot;/g, '"').replace (/&apos;/g, "'")
175                  .replace (/&amp;/g, '&');
176              attrs[attrName.toLowerCase ()] = v;
177              m = true;
178              return '';
179            });
180            if (!m) break;
181          }
182          if (e.length) {
183            log ('Broken start tag: "' + e + '"');
184          }
185          token = {type: 'start-tag', value: tagName, attrs: attrs};
186        p.insertionPoint -= s.length;        p.insertionPoint -= s.length;
187        return '';        return '';
188      });      });
# Line 120  Line 212 
212    } // getNextToken    } // getNextToken
213    
214    Parser.prototype.parse = function () {    Parser.prototype.parse = function () {
215      log ('start parsing');      logIndentLevel++;
216        log ('parse: start');
217    
218      while (true) {      while (true) {
219        var token = this.getNextToken ();        var token = this.getNextToken ();
# Line 130  Line 223 
223          if (token.value == 'script') {          if (token.value == 'script') {
224            // 1. Create an element for the token in the HTML namespace.            // 1. Create an element for the token in the HTML namespace.
225            var el = new JSElement (this.doc, token.value);            var el = new JSElement (this.doc, token.value);
226              if (token.attrs.async != null) el.async = true;
227              if (token.attrs.defer != null) el.defer = true;
228              if (token.attrs.src != null) el.src = token.attrs.src;
229    
230            // 2. Mark the element as being "parser-inserted".            // 2. Mark the element as being "parser-inserted".
231            el.manakaiParserInserted = true;            el.manakaiParserInserted = true;
232    
233            // 3. Switch the tokeniser's content model flag to the CDATA state.            // 3. Switch the tokeniser's content model flag to the CDATA state.
234            this.parseMode = 'script';            this.parseMode = 'cdata';
235              this.endTagName = 'script';
236    
237            // 4.1. Collect all the character tokens.            // 4.1. Collect all the character tokens.
238            while (true) {            while (true) {
# Line 149  Line 246 
246              // 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
247              // until it stops tokenising.              // until it stops tokenising.
248              } else if (token.type == 'eof' ||              } else if (token.type == 'eof' ||
249                         (token.type == 'end-tag' && token.value == 'script') ||                         token.type == 'end-tag' ||
250                         token.type == 'abort') {                         token.type == 'abort') {
251                // 6. Switched back to the PCDATA state.                // 6. Switched back to the PCDATA state.
252                this.parseMode = 'pcdata';                this.parseMode = 'pcdata';
253    
254                // 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 ...
255                if (token.type != 'end-tag') {                if (!(token.type == 'end-tag' && token.value == 'script')) {
256                  // 7.2. This is a parse error.                  // 7.2. This is a parse error.
257                  log ('Parse error: no </' + 'script>');                  log ('Parse error: no </' + 'script>');
258    
# Line 185  Line 282 
282            this.openElements[this.openElements.length - 1].appendChild (el);            this.openElements[this.openElements.length - 1].appendChild (el);
283    
284            // 11. Let the insertion point have the value of the old ...            // 11. Let the insertion point have the value of the old ...
285    
286              oldInsertionPoint += this.insertionPoint;
287            this.setInsertionPoint (oldInsertionPoint);            this.setInsertionPoint (oldInsertionPoint);
288    
289            // 12. If there is a script that will execute as soon as ...            // 12. If there is a script that will execute as soon as ...
290                        while (this.scriptExecutedWhenParserResumes) {
291                // 12.1. If the tree construction stage is being called reentrantly
292                if (this.reentrant) {
293                  log ('parse: abort (reentrance)');
294                  logIndentLevel--;
295                  return;
296    
297                // 12.2. Otherwise
298                } else {
299                  // 1.
300                  var script = this.scriptExecutedWhenParserResumes;
301                  this.scriptExecutedWhenParserResumes = null;
302    
303                  // 2. Pause until the script has completed loading.
304                  //
305    
306                  // 3. Let the insertion point to just before the next input char.
307                  this.setInsertionPoint (0);
308    
309                  // 4. Execute the script.
310                  executeScript (this.doc, script);
311    
312                  // 5. Let the insertion point be undefined again.
313                  this.setInsertionPoint (undefined);
314    
315                  // 6. If there is once again a script that will execute ...
316                  //
317                }
318              }
319            } else if (token.value == 'style' ||
320                       token.value == 'noscript' ||
321                       token.value == 'xmp') {
322              // 1. Create an element for the token in the HTML namespace.
323              var el = new JSElement (this.doc, token.value);
324    
325              // 2. Append the new element to the current node.
326              this.openElements[this.openElements.length - 1].appendChild (el);
327    
328              // 3. Switch the tokeniser's content model flag to the CDATA state.
329              this.parseMode = 'cdata';
330              this.endTagName = token.value;
331    
332              // 4.1. Collect all the character tokens.
333              while (true) {
334                var token = this.getNextToken ();
335                log ('token: ' + token.type + ' "' + token.value + '"');
336    
337                if (token.type == 'char') {
338                  // 5. Append a single Text node to the script element node.
339                  el.manakaiAppendText (token.value);
340    
341                // 4.2. Until it returns a token that is not a character token, or
342                // until it stops tokenising.
343                } else if (token.type == 'eof' ||
344                           token.type == 'end-tag' ||
345                           token.type == 'abort') {
346                  // 6. Switched back to the PCDATA state.
347                  this.parseMode = 'pcdata';
348<