/[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.1 by wakaba, Sun Apr 20 06:07:24 2008 UTC revision 1.12 by wakaba, Sun Apr 27 11:21:09 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  <style>  <style>
6      h1, h2 {
7        margin: 0;
8        font-size: 100%;
9      }
10      p, pre {
11        margin: 0;
12      }
13    textarea {    textarea {
14       display: block;      width: 100%;
15       width: 80%;      -width: 99%;
16       margin-left: auto;      height: 10em;
      margin-right: auto;  
      min-height: 20em;  
17    }    }
18    output {    output {
19      display: block;      display: block;
20      font-family: monospace;      font-family: monospace;
21      white-space: pre;      white-space: -moz-pre-wrap;
22        white-space: pre-wrap;
23    }    }
24  </style>  </style>
25  <script>  <script>
26      var delayedUpdater = 0;
27    
28    function update () {    function update () {
29      document.logElement.textContent = '';      if (delayedUpdater) {
30      var p = new Parser ();        clearTimeout (delayedUpdater);
31      p.parse (new InputStream (document.sourceElement.value));        delayedUpdater = 0;
32      log (dumpTree (p.doc, ''));      }
33        delayedUpdater = setTimeout (update2, 100);
34    } // update    } // update
35    
36      function update2 () {
37        var v = document.sourceElement.value;
38        if (v != document.previousSourceText) {
39          document.previousSourceText = v;
40          document.links['permalink'].href
41              = location.pathname + '?s=' + encodeURIComponent (v);
42          document.links['ldvlink'].href
43              = 'http://software.hixie.ch/utilities/js/live-dom-viewer/?'
44              + encodeURIComponent (v);
45    
46          document.logElement.textContent = '';
47          var p = new Parser (new InputStream (v));
48          var doc = p.doc;
49          p.parse ();
50          
51          log (dumpTree (doc, ''));
52          
53          if (p.hasAsyncScript) {
54            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');
55          }
56        }
57      } // update2
58    
59      var logIndentLevel = 0;
60    function log (s) {    function log (s) {
61        for (var i = 0; i < logIndentLevel; i++) {
62          s = '  ' + s;
63        }
64      document.logElement.appendChild (document.createTextNode (s + "\n"));      document.logElement.appendChild (document.createTextNode (s + "\n"));
65    } // log    } // log
66    
# Line 32  Line 68 
68      this.s = s;      this.s = s;
69    } // InputStream    } // InputStream
70    
71    function Parser () {    function Parser (i, doc) {
72      this.parseMode = 'pcdata';      this.parseMode = 'pcdata';
73      this.doc = new JSDocument ();      if (!doc) {
74      this.openElements = [this.doc];        doc = new JSDocument (this);
75          doc.manakaiIsHTML = true;
76        }
77        this.doc = doc;
78        this.openElements = [doc];
79        this.input = i;
80        this.scriptsExecutedAfterParsing = [];
81        this.scriptsExecutedSoon = [];
82        this.scriptsExecutedAsynchronously = [];
83    } // Parser    } // Parser
84    
85    Parser.prototype.getNextToken = function (i) {    Parser.prototype.getNextToken = function () {
86        var p = this;
87        var i = this.input;
88      if (this.parseMode == 'script') {      if (this.parseMode == 'script') {
89        var token;        var token;
90        i.s = i.s.replace (/^([\s\S]+?)<\/[Ss][Cc][Rr][Ii][Pp][Tt]>/,        if (p.insertionPoint <= 0) {
91            return {type: 'abort'};
92          }
93          i.s = i.s.replace (/^([^<]+)/,
94        function (s, t) {        function (s, t) {
95            if (0 < p.insertionPoint && p.insertionPoint < t.length) {
96              token = {type: 'char', value: t.substring (0, p.insertionPoint)};
97              var ip = p.insertionPoint;
98              p.insertionPoint = 0;
99              return t.substring (ip, t.length);
100            }
101          token = {type: 'char', value: t};          token = {type: 'char', value: t};
102          return '<' + '/script>';          p.insertionPoint -= t.length;
103            return '';
104        });        });
105        if (token) return token;        if (token) return token;
106        i.s = i.s.replace (/^<\/[Ss][Cc][Rr][Ii][Pp][Tt]>/, function () {        i.s = i.s.replace (/^<\/[Ss][Cc][Rr][Ii][Pp][Tt]>/, function (s) {
107            if (p.insertionPoint < s.length) {
108              token = {type: 'abort'};
109              return s;
110            }
111          token = {type: 'end-tag', value: 'script'};          token = {type: 'end-tag', value: 'script'};
112            p.insertionPoint -= s.length;
113            return '';
114          });
115          if (token) return token;
116          var m;
117          if ((p.insertionPoint < '</script'.length) &&
118              (m = i.s.match (/^<\/([SCRIPTscript]+)/))) {
119            var v = m[1].substring (0, p.insertionPoint).toLowerCase ();
120            if (v == 'script'.substring (0, p.insertionPoint - '</'.length)) {
121              return {type: 'abort'};
122            }
123          }
124          i.s = i.s.replace (/^</,
125          function (s) {
126            token = {type: 'char', value: s};
127            p.insertionPoint -= s.length;
128          return '';          return '';
129        });        });
130        if (token) return token;        if (token) return token;
# Line 56  Line 132 
132      }      }
133    
134      var token;      var token;
135      i.s = i.s.replace (/^<\/([^>]+)>/, function (s, e) {      i.s = i.s.replace (/^<\/([^>]+)(?:>|$)/, function (s, e) {
136          if (p.insertionPoint < s.length ||
137              (p.insertionPoint <= s.length &&
138               s.substring (s.length - 1, 1) != '>')) {
139            token = {type: 'abort'};
140            return s;
141          }
142        token = {type: 'end-tag', value: e.toLowerCase ()};        token = {type: 'end-tag', value: e.toLowerCase ()};
143          p.insertionPoint -= s.length;
144        return '';        return '';
145      });      });
146      if (token) return token;      if (token) return token;
147      i.s = i.s.replace (/^<([^>]+)>/, function (s, e) {      i.s = i.s.replace (/^<([^>]+)(?:>|$)/, function (s, e) {
148        token = {type: 'start-tag', value: e.toLowerCase ()};        if (p.insertionPoint < s.length ||
149              (p.insertionPoint <= s.length &&
150               s.substring (s.length - 1, 1) != '>')) {
151            token = {type: 'abort'};
152            return s;
153          }
154          var tagName;
155          var attrs = {};
156          e = e.replace (/^[\S]+/, function (v) {
157            tagName = v.toLowerCase ();
158            return '';
159          });
160          while (true) {
161            var m = false;
162            e = e.replace (/^\s*([^\s=]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"'\s]*)))?/,
163            function (x, attrName, attrValue1, attrValue2, attrValue3) {
164              v = attrValue1 || attrValue2 || attrValue3;
165              v = v.replace (/&quot;/g, '"').replace (/&apos;/g, "'")
166                  .replace (/&amp;/g, '&');
167              attrs[attrName.toLowerCase ()] = v;
168              m = true;
169              return '';
170            });
171            if (!m) break;
172          }
173          if (e.length) {
174            log ('Broken start tag: "' + e + '"');
175          }
176          token = {type: 'start-tag', value: tagName, attrs: attrs};
177          p.insertionPoint -= s.length;
178        return '';        return '';
179      });      });
180      if (token) return token;      if (token) return token;
181        if (p.insertionPoint <= 0) {
182          return {type: 'abort'};
183        }
184      i.s = i.s.replace (/^[^<]+/, function (s) {      i.s = i.s.replace (/^[^<]+/, function (s) {
185          if (p.insertionPoint < s.length) {
186            token = {type: 'char', value: s.substring (0, p.insertionPoint)};
187            var ip = p.insertionPoint;
188            p.insertionPoint = 0;
189            return s.substring (ip, s.length);
190          }
191        token = {type: 'char', value: s};        token = {type: 'char', value: s};
192          p.insertionPoint -= s.length;
193        return '';        return '';
194      });      });
195      if (token) return token;      if (token) return token;
196      i.s = i.s.replace (/^[\s\S]/, function (s) {      i.s = i.s.replace (/^[\s\S]/, function (s) {
197        token = {type: 'char', value: s};        token = {type: 'char', value: s};
198          p.insertionPoint -= s.length;
199        return '';        return '';
200      });      });
201      if (token) return token;      if (token) return token;
202      return {type: 'eof'};      return {type: 'eof'};
203    } // getNextToken    } // getNextToken
204    
205    Parser.prototype.parse = function (i) {    Parser.prototype.parse = function () {
206      log ('start parsing');      logIndentLevel++;
207        log ('parse: start');
208    
209      while (true) {      while (true) {
210        var token = this.getNextToken (i);        var token = this.getNextToken ();
211        log ('token: ' + token.type + ' "' + token.value + '"');        log ('token: ' + token.type + ' "' + token.value + '"');
212    
213        if (token.type == 'start-tag') {        if (token.type == 'start-tag') {
         var el = new JSElement (token.value);  
214          if (token.value == 'script') {          if (token.value == 'script') {
215              // 1. Create an element for the token in the HTML namespace.
216              var el = new JSElement (this.doc, token.value);
217              if (token.attrs.async != null) el.async = true;
218              if (token.attrs.defer != null) el.defer = true;
219              if (token.attrs.src != null) el.src = token.attrs.src;
220    
221              // 2. Mark the element as being "parser-inserted".
222              el.manakaiParserInserted = true;
223    
224              // 3. Switch the tokeniser's content model flag to the CDATA state.
225            this.parseMode = 'script';            this.parseMode = 'script';
226    
227              // 4.1. Collect all the character tokens.
228            while (true) {            while (true) {
229              var token = this.getNextToken (i);              var token = this.getNextToken ();
230              log ('token: ' + token.type + ' "' + token.value + '"');              log ('token: ' + token.type + ' "' + token.value + '"');
231    
232              if (token.type == 'char') {              if (token.type == 'char') {
233                  // 5. Append a single Text node to the script element node.
234                el.manakaiAppendText (token.value);                el.manakaiAppendText (token.value);
235    
236                // 4.2. Until it returns a token that is not a character token, or
237                // until it stops tokenising.
238              } else if (token.type == 'eof' ||              } else if (token.type == 'eof' ||
239                         (token.type == 'end-tag' && token.value == 'script')) {                         (token.type == 'end-tag' && token.value == 'script') ||
240                           token.type == 'abort') {
241                  // 6. Switched back to the PCDATA state.
242                this.parseMode = 'pcdata';                this.parseMode = 'pcdata';
243    
244                  // 7.1. If the next token is not an end tag token with ...
245                  if (token.type != 'end-tag') {
246                    // 7.2. This is a parse error.
247                    log ('Parse error: no </' + 'script>');
248    
249                    // 7.3. Mark the script element as "already executed".
250                    el.manakaiAlreadyExecuted = true;
251                  } else {
252                    // 7.4. Ignore it.
253                    //
254                  }
255                break;                break;
256              }              }
257            }            }
258    
259              // 8.1. If the parser were originally created for the ...
260              if (this.fragmentParsingMode) {
261                // 8.2. Mark the script element as "already executed" and ...
262                el.alreadyExecuted = true;
263                continue;
264              }
265    
266              // 9.1. Let the old insertion point have the same value as the ...
267              var oldInsertionPoint = this.insertionPoint;
268              // 9.2. Let the insertion point be just before the next input ...
269              this.setInsertionPoint (0);
270    
271              // 10. Append the new element to the current node.
272            this.openElements[this.openElements.length - 1].appendChild (el);            this.openElements[this.openElements.length - 1].appendChild (el);
273    
274              // 11. Let the insertion point have the value of the old ...
275    
276              oldInsertionPoint += this.insertionPoint;
277              this.setInsertionPoint (oldInsertionPoint);
278    
279              // 12. If there is a script that will execute as soon as ...
280              while (this.scriptExecutedWhenParserResumes) {
281                // 12.1. If the tree construction stage is being called reentrantly
282                if (this.reentrant) {
283                  log ('parse: abort (reentrance)');
284                  logIndentLevel--;
285                  return;
286    
287                // 12.2. Otherwise
288                } else {
289                  // 1.
290                  var script = this.scriptExecutedWhenParserResumes;
291                  this.scriptExecutedWhenParserResumes = null;
292    
293                  // 2. Pause until the script has completed loading.
294                  //
295    
296                  // 3. Let the insertion point to just before the next input char.
297                  this.setInsertionPoint (0);
298    
299                  // 4. Execute the script.
300                  executeScript (this.doc, script);
301    
302                  // 5. Let the insertion point be undefined again.
303                  this.setInsertionPoint (undefined);
304    
305                  // 6. If there is once again a script that will execute ...
306