Parent Directory
|
Revision Log
|
Patch
| revision 1.5 by wakaba, Fri Apr 25 11:40:56 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; |
| # | Line 18 | Line 23 |
| 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 (new InputStream (document.sourceElement.value)); | clearTimeout (delayedUpdater); |
| 31 | var doc = p.doc; | delayedUpdater = 0; |
| 32 | p.parse (); | } |
| 33 | log (dumpTree (doc, '')); | 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 42 | Line 76 |
| 76 | } | } |
| 77 | this.doc = doc; | this.doc = doc; |
| 78 | this.openElements = [doc]; | this.openElements = [doc]; |
| 79 | this.in = i; | this.input = i; |
| 80 | this.scriptsExecutedAfterParsing = []; | this.scriptsExecutedAfterParsing = []; |
| 81 | this.scriptsExecutedSoon = []; | |
| 82 | this.scriptsExecutedAsynchronously = []; | |
| 83 | } // Parser | } // Parser |
| 84 | ||
| 85 | Parser.prototype.getNextToken = function () { | Parser.prototype.getNextToken = function () { |
| 86 | var p = this; | var p = this; |
| 87 | var i = this.in; | var i = this.input; |
| 88 | if (this.parseMode == 'script') { | if (this.parseMode == 'script') { |
| 89 | var token; | var token; |
| 90 | if (p.insertionPoint <= 0) { | if (p.insertionPoint <= 0) { |
| # | Line 121 | Line 157 |
| 157 | tagName = v.toLowerCase (); | tagName = v.toLowerCase (); |
| 158 | return ''; | return ''; |
| 159 | }); | }); |
| 160 | e = e.replace (/^\s*(\S+)\s*(?:=\s*"([^"]*)"|'([^']*)'|([^"']+))?/, | while (true) { |
| 161 | function (x, attrName, attrValue1, attrValue2, attrValue3) { | var m = false; |
| 162 | attrs[attrName] = attrValue1 || attrValue2 || attrValue3; | e = e.replace (/^\s*([^\s=]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"'\s]*)))?/, |
| 163 | return ''; | function (x, attrName, attrValue1, attrValue2, attrValue3) { |
| 164 | }); | v = attrValue1 || attrValue2 || attrValue3; |
| 165 | v = v.replace (/"/g, '"').replace (/'/g, "'") | |
| 166 | .replace (/&/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}; | token = {type: 'start-tag', value: tagName, attrs: attrs}; |
| 177 | p.insertionPoint -= s.length; | p.insertionPoint -= s.length; |
| 178 | return ''; | return ''; |
| # | Line 156 | Line 203 |
| 203 | } // getNextToken | } // getNextToken |
| 204 | ||
| 205 | Parser.prototype.parse = function () { | Parser.prototype.parse = function () { |
| 206 | log ('start parsing'); | logIndentLevel++; |
| 207 | log ('parse: start'); | |
| 208 | ||
| 209 | while (true) { | while (true) { |
| 210 | var token = this.getNextToken (); | var token = this.getNextToken (); |
| # | Line 224 | Line 272 |
| 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 ... | // 11. Let the insertion point have the value of the old ... |
| 275 | ||
| 276 | oldInsertionPoint += this.insertionPoint; | oldInsertionPoint += this.insertionPoint; |
| 277 | this.setInsertionPoint (oldInsertionPoint); | this.setInsertionPoint (oldInsertionPoint); |
| 278 | ||
| 279 | // 12. If there is a script that will execute as soon as ... | // 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 | // | |
| 307 | } | |
| 308 | } | |
| 309 | } else { | } else { |
| 310 | var el = new JSElement (this.doc, token.value); | var el = new JSElement (this.doc, token.value); |
| 311 | this.openElements[this.openElements.length - 1].appendChild (el); | this.openElements[this.openElements.length - 1].appendChild (el); |
| # | Line 249 | Line 325 |
| 325 | break; | break; |
| 326 | } else if (token.type == 'abort') { | } else if (token.type == 'abort') { |
| 327 | log ('parse: abort'); | log ('parse: abort'); |
| 328 | logIndentLevel--; | |
| 329 | return; | return; |
| 330 | } | } |
| 331 | } | } |
| # | Line 259 | Line 336 |
| 336 | ||
| 337 | // "When a script completes loading" rules start applying. | // "When a script completes loading" rules start applying. |
| 338 | ||
| 339 | // TODO: Handles "list of scripts that will execute as soon as possible" | while (this.scriptsExecutedSoon.length > 0 || |
| 340 | // and "list of scripts that will execute asynchronously" | this.scriptsExecutedAsynchronously.length > 0) { |
| 341 | // Handle "list of scripts that will execute as soon as possible". | |
| 342 | while (this.scriptsExecutedSoon.length > 0) { | |
| 343 | var e = this.scriptsExecutedSoon.shift (); | |
| 344 | ||
| 345 | // If it has completed loading | |
| 346 | log ('Execute an external script not inserted by parser...'); | |
| 347 | executeScript (this.doc, e); | |
| 348 | ||
| 349 | // NOTE: It MAY be executed before the end of the parsing, according | |
| 350 | // to the spec. | |
| 351 | this.hasAsyncScript = true; | |
| 352 | } | |
| 353 | ||
| 354 | // Handle "list of scripts that will execute asynchronously". | |
| 355 | while (this.scriptsExecutedAsynchronously.length > 0) { | |
| 356 | var e = this.scriptsExecutedAsynchronously.shift (); | |
| 357 | ||
| 358 | // Step 1. | |
| 359 | // We assume that all scripts have been loaded at this time. | |
| 360 | ||
| 361 | // Step 2. | |
| 362 | log ('Execute an asynchronous script...'); | |
| 363 | executeScript (this.doc, e); | |
| 364 | ||
| 365 | // Step 3. | |
| 366 | // | |
| 367 | ||
| 368 | // Step 4. | |
| 369 | // | |
| 370 | ||
| 371 | this.hasAsyncScript = true; | |
| 372 | } | |
| 373 | } | |
| 374 | ||
| 375 | // Handle "list of scripts that will execute when the document has finished | // Handle "list of scripts that will execute when the document has finished |
| 376 | // parsing". | // parsing". |
| # | Line 283 | Line 393 |
| 393 | // "delays tha load event" things has completed: | // "delays tha load event" things has completed: |
| 394 | // readyState = 'complete' | // readyState = 'complete' |
| 395 | log ('load event fired'); | log ('load event fired'); |
| 396 | ||
| 397 | logIndentLevel--; | |
| 398 | } // parse | } // parse |
| 399 | ||
| 400 | Parser.prototype.setInsertionPoint = function (ip) { | Parser.prototype.setInsertionPoint = function (ip) { |
| 401 | if (ip == undefined || ip == null || isNaN (ip)) { | if (ip == undefined || ip == null || isNaN (ip)) { |
| 402 | log ('insertion point: set to undefined'); | log ('insertion point: set to undefined'); |
| 403 | this.insertionPoint = undefined; | this.insertionPoint = undefined; |
| 404 | } else if (ip == this.in.s.length) { | } else if (ip == this.input.s.length) { |
| 405 | log ('insertion point: end of file'); | log ('insertion point: end of file'); |
| 406 | this.insertionPoint = ip; | this.insertionPoint = ip; |
| 407 | } else { | } else { |
| 408 | log ('insertion point: set to ' + ip + | log ('insertion point: set to ' + ip + |
| 409 | ' (before "' + this.in.s.substring (0, 10) + '")'); | ' (before "' + this.input.s.substring (0, 10) + '")'); |
| 410 | this.insertionPoint = ip; | this.insertionPoint = ip; |
| 411 | } | } |
| 412 | }; // setInsertionPoint | }; // setInsertionPoint |
| # | Line 316 | Line 428 |
| 428 | e.parentNode = this; | e.parentNode = this; |
| 429 | ||
| 430 | if (e.localName == 'script') { | if (e.localName == 'script') { |
| 431 | logIndentLevel++; | |
| 432 | log ('Running a script: start'); | log ('Running a script: start'); |
| 433 | ||
| 434 | var doc = this.ownerDocument || this; | var doc = this.ownerDocument || this; |
| # | Line 334 | Line 447 |
| 447 | if (e.manakaiAlreadyExecuted) { | if (e.manakaiAlreadyExecuted) { |
| 448 | // 2.5. Abort these steps at this point. | // 2.5. Abort these steps at this point. |
| 449 | log ('Running a script: aborted'); | log ('Running a script: aborted'); |
| 450 | logIndentLevel--; | |
| 451 | return e; | return e; |
| 452 | } | } |
| 453 | ||
| # | Line 351 | Line 465 |
| 465 | p.scriptsExecutedAfterParsing.push (e); | p.scriptsExecutedAfterParsing.push (e); |
| 466 | log ('Running a script: aborted (defer)'); | log ('Running a script: aborted (defer)'); |
| 467 | } else if (e.async && e.src != null) { | } else if (e.async && e.src != null) { |
| 468 | // TODO | p.scriptsExecutedAsynchronously.push (e); |
| 469 | } else if (e.async && e.src == null | log ('Running a script: aborted (async src)'); |
| 470 | /* && list of scripts that will execute asynchronously is not empty */) { | } else if (e.async && e.src == null && |
| 471 | // TODO | p.scriptsExecutedAsynchronously.length > 0) { |
| 472 | p.scriptsExecutedAsynchronously.push (e); | |
| 473 | log ('Running a script: aborted (async)'); | |
| 474 | // ISSUE: What is the difference with the case above? | |
| 475 | } else if (e.src != null && e.manakaiParserInserted) { | } else if (e.src != null && e.manakaiParserInserted) { |
| 476 | // TODO | if (p.scriptExecutedWhenParserResumes) { |
| 477 | log ('Error: There is a script that will execute as soon as the parser resumes.'); | |
| 478 | } | |
| 479 | p.scriptExecutedWhenParserResumes = e; | |
| 480 | log ('Running a script: aborted (src parser-inserted)'); | |
| 481 | } else if (e.src != null) { | } else if (e.src != null) { |
| 482 | // TODO | p.scriptsExecutedSoon.push (e); |
| 483 | log ('Running a script: aborted (src)'); | |
| 484 | } else { | } else { |
| 485 | executeScript (doc, e); // even if other scripts are already executing. | executeScript (doc, e); // even if other scripts are already executing. |
| 486 | } | } |
| 487 | ||
| 488 | log ('Running a script: end'); | log ('Running a script: end'); |
| 489 | logIndentLevel--; | |
| 490 | } | } |
| 491 | ||
| 492 | return e; | return e; |
| # | Line 372 | Line 495 |
| 495 | function executeScript (doc, e) { | function executeScript (doc, e) { |
| 496 | log ('executing a script block: start'); | log ('executing a script block: start'); |
| 497 | ||
| 498 | // If the load resulted in an error, then ... firing an error event ... | var s; |
| 499 | if (e.src != null) { | |
| 500 | s = getExternalScript (e.src); | |
| 501 | ||
| 502 | // If the load resulted in an error, then ... firing an error event ... | |
| 503 | if (s == null) { | |
| 504 | log ('error event fired at the script element'); | |
| 505 | return; | |
| 506 | } | |
| 507 | ||
| 508 | log ('External script loaded: "' + s + '"'); | |
| 509 | } else { | |
| 510 | s = e.text; | |
| 511 | } | |
| 512 | ||
| 513 | // If the load was successful | // If the load was successful |
| 514 | log ('load event fired at the script element'); | log ('load event fired at the script element'); |
| # | Line 381 | Line 517 |
| 517 | // Scripting is enabled, Document.designMode is disabled, | // Scripting is enabled, Document.designMode is disabled, |
| 518 | // Document is the active document in its browsing context | // Document is the active document in its browsing context |
| 519 | ||
| var s; | ||
| if (e.src != null) { | ||
| // TODO: from external file | ||
| } else { | ||
| s = e.text; | ||
| } | ||
| 520 | parseAndRunScript (doc, s); | parseAndRunScript (doc, s); |
| 521 | } | } |
| 522 | ||
| 523 | log ('executing a script block: end'); | log ('executing a script block: end'); |
| 524 | } // executeScript | } // executeScript |
| 525 | ||
| 526 | function getExternalScript (uri) { | |
| 527 | if (uri.match (/^javascript:/i)) { | |
| 528 | var m; | |
| 529 | if (m = uri.match (/^javascript:\s*(?:'([^']*)'|"([^"]+)")\s*$/i)) { | |
| 530 | if (m[1]) { | |
| 531 | return unescapeJSLiteral (m[1]); | |
| 532 | } else if (m[2]) { | |
| 533 | return unescapeJSLiteral (m[2]); | |
| 534 | } else { | |
| 535 | return null; | |
| 536 | } | |
| 537 | } else { | |
| 538 | log ('Complex javascript: URI is not supported: <' + uri + '>'); | |
| 539 | return null; | |
| 540 | } | |
| 541 | } else { | |
| 542 | log ('URI scheme not supported: <' + uri + '>'); | |
| 543 | return null; | |
| 544 | } | |
| 545 | } // getExternalScript | |
| 546 | ||
| 547 | function parseAndRunScript (doc, s) { | function parseAndRunScript (doc, s) { |
| 548 | while (true) { | while (true) { |
| 549 | var matched = false; | var matched = false; |