├── .gitignore ├── Gruntfile.js ├── README.md ├── console-log-div.js ├── index.html ├── marked.js ├── package.json ├── step-0 ├── console-log-div.js ├── index.html ├── micro-angular.js └── primes.js ├── step-1 ├── console-log-div.js ├── index.html ├── micro-angular-worker.js ├── micro-angular.js ├── mock-scopes.js ├── primes.js └── worker.js └── step-2 ├── console-log-div.js ├── index.html ├── micro-angular-worker.js ├── micro-angular.js ├── mock-scopes.js ├── primes.js └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .grunt/ 3 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function (grunt) { 3 | var files = ['console-log-div.js']; 4 | 5 | grunt.initConfig({ 6 | 7 | 'gh-pages': { 8 | options: { 9 | base: '.' 10 | }, 11 | src: [ 12 | 'README.md', 13 | 'marked.js', 14 | 'index.html', 15 | 'step-0/*', 16 | 'step-1/*', 17 | 'step-2/*' 18 | ] 19 | } 20 | }); 21 | 22 | var plugins = module.require('matchdep').filterDev('grunt-*'); 23 | plugins.forEach(grunt.loadNpmTasks); 24 | 25 | grunt.registerTask('deploy', ['gh-pages']); 26 | }; 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-worker-digest-demo 2 | 3 | See blog post [Run Angular digest cycle in web worker][post]. 4 | 5 | * [step-0][step-0] - initial sync implementation 6 | * [step-1][step-1] - micro-angular in web worker implementation 7 | * [step-2][step-2] - restored plain property syntax using `Object.observe` 8 | 9 | Related: code repo with more steps used to write the blog post [here][digest cycle] 10 | 11 | [post]: (http://glebbahmutov.com/blog/run-angular-digest-cycle-in-web-worker/) 12 | [step-0]: step-0 13 | [step-1]: step-1 14 | [step-2]: step-2 15 | [digest cycle]: https://github.com/bahmutov/digest-cycle-in-web-worker 16 | -------------------------------------------------------------------------------- /console-log-div.js: -------------------------------------------------------------------------------- 1 | (function initConsoleLogDiv() { 2 | 3 | if (console.log.toDiv) { 4 | return; 5 | } 6 | 7 | function toString(x) { 8 | return typeof x === 'string' ? x : JSON.stringify(x); 9 | } 10 | 11 | var log = console.log.bind(console); 12 | var error = console.error.bind(console); 13 | var warn = console.warn.bind(console); 14 | 15 | var id = 'console-log-div'; 16 | function createOuterElement() { 17 | var outer = document.getElementById(id); 18 | if (!outer) { 19 | outer = document.createElement('fieldset'); 20 | outer.id = id; 21 | document.body.appendChild(outer); 22 | } 23 | outer.classList.add('id'); 24 | 25 | var style = outer.style; 26 | style.width = '100%'; 27 | // style.minHeight = '200px'; 28 | style.fontFamily = 'monospace'; 29 | style.marginTop = '20px'; 30 | style.whiteSpace = 'pre'; 31 | style.border = '1px solid black'; 32 | style.borderRadius = '5px'; 33 | style.padding = '5px 10px'; 34 | return outer; 35 | } 36 | 37 | var logTo = (function createLogDiv() { 38 | 39 | var outer = createOuterElement(); 40 | 41 | var caption = document.createTextNode('console output'); 42 | var legend = document.createElement('legend'); 43 | legend.appendChild(caption); 44 | outer.appendChild(legend); 45 | 46 | var div = document.createElement('div'); 47 | div.id = 'console-log-text'; 48 | outer.appendChild(div); 49 | 50 | return div; 51 | }()); 52 | 53 | function printToDiv() { 54 | var msg = Array.prototype.slice.call(arguments, 0) 55 | .map(toString) 56 | .join(' '); 57 | var text = logTo.textContent; 58 | logTo.textContent = text + msg + '\n'; 59 | } 60 | 61 | function logWithCopy() { 62 | log.apply(null, arguments); 63 | printToDiv.apply(null, arguments); 64 | } 65 | 66 | console.log = logWithCopy; 67 | console.log.toDiv = true; 68 | 69 | console.error = function errorWithCopy() { 70 | error.apply(null, arguments); 71 | var args = Array.prototype.slice.call(arguments, 0); 72 | args.unshift('ERROR:'); 73 | printToDiv.apply(null, args); 74 | }; 75 | 76 | console.warn = function logWarning() { 77 | warn.apply(null, arguments); 78 | var args = Array.prototype.slice.call(arguments, 0); 79 | args.unshift('WARNING:'); 80 | printToDiv.apply(null, args); 81 | }; 82 | 83 | window.addEventListener('error', function (err) { 84 | printToDiv('EXCEPTION:', err.message + '\n ' + err.filename, err.lineno + ':' + err.colno); 85 | }); 86 | 87 | }()); 88 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Run digest cycle in web worker 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | Fork me on GitHub 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /marked.js: -------------------------------------------------------------------------------- 1 | /** 2 | * marked - a markdown parser 3 | * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed) 4 | * https://github.com/chjj/marked 5 | */ 6 | 7 | ; 8 | (function() { 9 | 10 | /** 11 | * Block-Level Grammar 12 | */ 13 | 14 | var block = { 15 | newline: /^\n+/, 16 | code: /^( {4}[^\n]+\n*)+/, 17 | fences: noop, 18 | hr: /^( *[-*_]){3,} *(?:\n+|$)/, 19 | heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, 20 | nptable: noop, 21 | lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, 22 | blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/, 23 | list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 24 | html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, 25 | def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 26 | table: noop, 27 | paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, 28 | text: /^[^\n]+/ 29 | }; 30 | 31 | block.bullet = /(?:[*+-]|\d+\.)/; 32 | block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; 33 | block.item = replace(block.item, 'gm') 34 | (/bull/g, block.bullet) 35 | (); 36 | 37 | block.list = replace(block.list) 38 | (/bull/g, block.bullet) 39 | ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/) 40 | (); 41 | 42 | block._tag = '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b'; 43 | 44 | block.html = replace(block.html) 45 | ('comment', //) 46 | ('closed', /<(tag)[\s\S]+?<\/\1>/) 47 | ('closing', /])*?>/) 48 | (/tag/g, block._tag) 49 | (); 50 | 51 | block.paragraph = replace(block.paragraph) 52 | ('hr', block.hr) 53 | ('heading', block.heading) 54 | ('lheading', block.lheading) 55 | ('blockquote', block.blockquote) 56 | ('tag', '<' + block._tag) 57 | ('def', block.def) 58 | (); 59 | 60 | /** 61 | * Normal Block Grammar 62 | */ 63 | 64 | block.normal = merge({}, block); 65 | 66 | /** 67 | * GFM Block Grammar 68 | */ 69 | 70 | block.gfm = merge({}, block.normal, { 71 | fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, 72 | paragraph: /^/ 73 | }); 74 | 75 | block.gfm.paragraph = replace(block.paragraph) 76 | ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|' + block.list.source.replace('\\1', '\\3') + '|') 77 | (); 78 | 79 | /** 80 | * GFM + Tables Block Grammar 81 | */ 82 | 83 | block.tables = merge({}, block.gfm, { 84 | nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, 85 | table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ 86 | }); 87 | 88 | /** 89 | * Block Lexer 90 | */ 91 | 92 | function Lexer(options) { 93 | this.tokens = []; 94 | this.tokens.links = {}; 95 | this.options = options || marked.defaults; 96 | this.rules = block.normal; 97 | 98 | if (this.options.gfm) { 99 | if (this.options.tables) { 100 | this.rules = block.tables; 101 | } else { 102 | this.rules = block.gfm; 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Expose Block Rules 109 | */ 110 | 111 | Lexer.rules = block; 112 | 113 | /** 114 | * Static Lex Method 115 | */ 116 | 117 | Lexer.lex = function(src, options) { 118 | var lexer = new Lexer(options); 119 | return lexer.lex(src); 120 | }; 121 | 122 | /** 123 | * Preprocessing 124 | */ 125 | 126 | Lexer.prototype.lex = function(src) { 127 | src = src 128 | .replace(/\r\n|\r/g, '\n') 129 | .replace(/\t/g, ' ') 130 | .replace(/\u00a0/g, ' ') 131 | .replace(/\u2424/g, '\n'); 132 | 133 | return this.token(src, true); 134 | }; 135 | 136 | /** 137 | * Lexing 138 | */ 139 | 140 | Lexer.prototype.token = function(src, top) { 141 | var src = src.replace(/^ +$/gm, ''), 142 | next, loose, cap, bull, b, item, space, i, l; 143 | 144 | while (src) { 145 | // newline 146 | if (cap = this.rules.newline.exec(src)) { 147 | src = src.substring(cap[0].length); 148 | if (cap[0].length > 1) { 149 | this.tokens.push({ 150 | type: 'space' 151 | }); 152 | } 153 | } 154 | 155 | // code 156 | if (cap = this.rules.code.exec(src)) { 157 | src = src.substring(cap[0].length); 158 | cap = cap[0].replace(/^ {4}/gm, ''); 159 | this.tokens.push({ 160 | type: 'code', 161 | text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap 162 | }); 163 | continue; 164 | } 165 | 166 | // fences (gfm) 167 | if (cap = this.rules.fences.exec(src)) { 168 | src = src.substring(cap[0].length); 169 | this.tokens.push({ 170 | type: 'code', 171 | lang: cap[2], 172 | text: cap[3] 173 | }); 174 | continue; 175 | } 176 | 177 | // heading 178 | if (cap = this.rules.heading.exec(src)) { 179 | src = src.substring(cap[0].length); 180 | this.tokens.push({ 181 | type: 'heading', 182 | depth: cap[1].length, 183 | text: cap[2] 184 | }); 185 | continue; 186 | } 187 | 188 | // table no leading pipe (gfm) 189 | if (top && (cap = this.rules.nptable.exec(src))) { 190 | src = src.substring(cap[0].length); 191 | 192 | item = { 193 | type: 'table', 194 | header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), 195 | align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), 196 | cells: cap[3].replace(/\n$/, '').split('\n') 197 | }; 198 | 199 | for (i = 0; i < item.align.length; i++) { 200 | if (/^ *-+: *$/.test(item.align[i])) { 201 | item.align[i] = 'right'; 202 | } else if (/^ *:-+: *$/.test(item.align[i])) { 203 | item.align[i] = 'center'; 204 | } else if (/^ *:-+ *$/.test(item.align[i])) { 205 | item.align[i] = 'left'; 206 | } else { 207 | item.align[i] = null; 208 | } 209 | } 210 | 211 | for (i = 0; i < item.cells.length; i++) { 212 | item.cells[i] = item.cells[i].split(/ *\| */); 213 | } 214 | 215 | this.tokens.push(item); 216 | 217 | continue; 218 | } 219 | 220 | // lheading 221 | if (cap = this.rules.lheading.exec(src)) { 222 | src = src.substring(cap[0].length); 223 | this.tokens.push({ 224 | type: 'heading', 225 | depth: cap[2] === '=' ? 1 : 2, 226 | text: cap[1] 227 | }); 228 | continue; 229 | } 230 | 231 | // hr 232 | if (cap = this.rules.hr.exec(src)) { 233 | src = src.substring(cap[0].length); 234 | this.tokens.push({ 235 | type: 'hr' 236 | }); 237 | continue; 238 | } 239 | 240 | // blockquote 241 | if (cap = this.rules.blockquote.exec(src)) { 242 | src = src.substring(cap[0].length); 243 | 244 | this.tokens.push({ 245 | type: 'blockquote_start' 246 | }); 247 | 248 | cap = cap[0].replace(/^ *> ?/gm, ''); 249 | 250 | // Pass `top` to keep the current 251 | // "toplevel" state. This is exactly 252 | // how markdown.pl works. 253 | this.token(cap, top); 254 | 255 | this.tokens.push({ 256 | type: 'blockquote_end' 257 | }); 258 | 259 | continue; 260 | } 261 | 262 | // list 263 | if (cap = this.rules.list.exec(src)) { 264 | src = src.substring(cap[0].length); 265 | bull = cap[2]; 266 | 267 | this.tokens.push({ 268 | type: 'list_start', 269 | ordered: bull.length > 1 270 | }); 271 | 272 | // Get each top-level item. 273 | cap = cap[0].match(this.rules.item); 274 | 275 | next = false; 276 | l = cap.length; 277 | i = 0; 278 | 279 | for (; i < l; i++) { 280 | item = cap[i]; 281 | 282 | // Remove the list item's bullet 283 | // so it is seen as the next token. 284 | space = item.length; 285 | item = item.replace(/^ *([*+-]|\d+\.) +/, ''); 286 | 287 | // Outdent whatever the 288 | // list item contains. Hacky. 289 | if (~item.indexOf('\n ')) { 290 | space -= item.length; 291 | item = !this.options.pedantic ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') : item.replace(/^ {1,4}/gm, ''); 292 | } 293 | 294 | // Determine whether the next list item belongs here. 295 | // Backpedal if it does not belong in this list. 296 | if (this.options.smartLists && i !== l - 1) { 297 | b = block.bullet.exec(cap[i + 1])[0]; 298 | if (bull !== b && !(bull.length > 1 && b.length > 1)) { 299 | src = cap.slice(i + 1).join('\n') + src; 300 | i = l - 1; 301 | } 302 | } 303 | 304 | // Determine whether item is loose or not. 305 | // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ 306 | // for discount behavior. 307 | loose = next || /\n\n(?!\s*$)/.test(item); 308 | if (i !== l - 1) { 309 | next = item.charAt(item.length - 1) === '\n'; 310 | if (!loose) loose = next; 311 | } 312 | 313 | this.tokens.push({ 314 | type: loose ? 'loose_item_start' : 'list_item_start' 315 | }); 316 | 317 | // Recurse. 318 | this.token(item, false); 319 | 320 | this.tokens.push({ 321 | type: 'list_item_end' 322 | }); 323 | } 324 | 325 | this.tokens.push({ 326 | type: 'list_end' 327 | }); 328 | 329 | continue; 330 | } 331 | 332 | // html 333 | if (cap = this.rules.html.exec(src)) { 334 | src = src.substring(cap[0].length); 335 | this.tokens.push({ 336 | type: this.options.sanitize ? 'paragraph' : 'html', 337 | pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', 338 | text: cap[0] 339 | }); 340 | continue; 341 | } 342 | 343 | // def 344 | if (top && (cap = this.rules.def.exec(src))) { 345 | src = src.substring(cap[0].length); 346 | this.tokens.links[cap[1].toLowerCase()] = { 347 | href: cap[2], 348 | title: cap[3] 349 | }; 350 | continue; 351 | } 352 | 353 | // table (gfm) 354 | if (top && (cap = this.rules.table.exec(src))) { 355 | src = src.substring(cap[0].length); 356 | 357 | item = { 358 | type: 'table', 359 | header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), 360 | align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), 361 | cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') 362 | }; 363 | 364 | for (i = 0; i < item.align.length; i++) { 365 | if (/^ *-+: *$/.test(item.align[i])) { 366 | item.align[i] = 'right'; 367 | } else if (/^ *:-+: *$/.test(item.align[i])) { 368 | item.align[i] = 'center'; 369 | } else if (/^ *:-+ *$/.test(item.align[i])) { 370 | item.align[i] = 'left'; 371 | } else { 372 | item.align[i] = null; 373 | } 374 | } 375 | 376 | for (i = 0; i < item.cells.length; i++) { 377 | item.cells[i] = item.cells[i] 378 | .replace(/^ *\| *| *\| *$/g, '') 379 | .split(/ *\| */); 380 | } 381 | 382 | this.tokens.push(item); 383 | 384 | continue; 385 | } 386 | 387 | // top-level paragraph 388 | if (top && (cap = this.rules.paragraph.exec(src))) { 389 | src = src.substring(cap[0].length); 390 | this.tokens.push({ 391 | type: 'paragraph', 392 | text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1] 393 | }); 394 | continue; 395 | } 396 | 397 | // text 398 | if (cap = this.rules.text.exec(src)) { 399 | // Top-level should never reach here. 400 | src = src.substring(cap[0].length); 401 | this.tokens.push({ 402 | type: 'text', 403 | text: cap[0] 404 | }); 405 | continue; 406 | } 407 | 408 | if (src) { 409 | throw new 410 | Error('Infinite loop on byte: ' + src.charCodeAt(0)); 411 | } 412 | } 413 | 414 | return this.tokens; 415 | }; 416 | 417 | /** 418 | * Inline-Level Grammar 419 | */ 420 | 421 | var inline = { 422 | escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, 423 | autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, 424 | url: noop, 425 | tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, 426 | link: /^!?\[(inside)\]\(href\)/, 427 | reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, 428 | nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, 429 | strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, 430 | em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 431 | code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, 432 | br: /^ {2,}\n(?!\s*$)/, 433 | del: noop, 434 | text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; 439 | 440 | inline.link = replace(inline.link) 441 | ('inside', inline._inside) 442 | ('href', inline._href) 443 | (); 444 | 445 | inline.reflink = replace(inline.reflink) 446 | ('inside', inline._inside) 447 | (); 448 | 449 | /** 450 | * Normal Inline Grammar 451 | */ 452 | 453 | inline.normal = merge({}, inline); 454 | 455 | /** 456 | * Pedantic Inline Grammar 457 | */ 458 | 459 | inline.pedantic = merge({}, inline.normal, { 460 | strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, 461 | em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ 462 | }); 463 | 464 | /** 465 | * GFM Inline Grammar 466 | */ 467 | 468 | inline.gfm = merge({}, inline.normal, { 469 | escape: replace(inline.escape)('])', '~|])')(), 470 | url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, 471 | del: /^~~(?=\S)([\s\S]*?\S)~~/, 472 | text: replace(inline.text) 473 | (']|', '~]|') 474 | ('|', '|https?://|') 475 | () 476 | }); 477 | 478 | /** 479 | * GFM + Line Breaks Inline Grammar 480 | */ 481 | 482 | inline.breaks = merge({}, inline.gfm, { 483 | br: replace(inline.br)('{2,}', '*')(), 484 | text: replace(inline.gfm.text)('{2,}', '*')() 485 | }); 486 | 487 | /** 488 | * Inline Lexer & Compiler 489 | */ 490 | 491 | function InlineLexer(links, options) { 492 | this.options = options || marked.defaults; 493 | this.links = links; 494 | this.rules = inline.normal; 495 | 496 | if (!this.links) { 497 | throw new 498 | Error('Tokens array requires a `links` property.'); 499 | } 500 | 501 | if (this.options.gfm) { 502 | if (this.options.breaks) { 503 | this.rules = inline.breaks; 504 | } else { 505 | this.rules = inline.gfm; 506 | } 507 | } else if (this.options.pedantic) { 508 | this.rules = inline.pedantic; 509 | } 510 | } 511 | 512 | /** 513 | * Expose Inline Rules 514 | */ 515 | 516 | InlineLexer.rules = inline; 517 | 518 | /** 519 | * Static Lexing/Compiling Method 520 | */ 521 | 522 | InlineLexer.output = function(src, links, options) { 523 | var inline = new InlineLexer(links, options); 524 | return inline.output(src); 525 | }; 526 | 527 | /** 528 | * Lexing/Compiling 529 | */ 530 | 531 | InlineLexer.prototype.output = function(src) { 532 | var out = '', 533 | link, text, href, cap; 534 | 535 | while (src) { 536 | // escape 537 | if (cap = this.rules.escape.exec(src)) { 538 | src = src.substring(cap[0].length); 539 | out += cap[1]; 540 | continue; 541 | } 542 | 543 | // autolink 544 | if (cap = this.rules.autolink.exec(src)) { 545 | src = src.substring(cap[0].length); 546 | if (cap[2] === '@') { 547 | text = cap[1].charAt(6) === ':' ? this.mangle(cap[1].substring(7)) : this.mangle(cap[1]); 548 | href = this.mangle('mailto:') + text; 549 | } else { 550 | text = escape(cap[1]); 551 | href = text; 552 | } 553 | out += '' + text + ''; 554 | continue; 555 | } 556 | 557 | // url (gfm) 558 | if (cap = this.rules.url.exec(src)) { 559 | src = src.substring(cap[0].length); 560 | text = escape(cap[1]); 561 | href = text; 562 | out += '' + text + ''; 563 | continue; 564 | } 565 | 566 | // tag 567 | if (cap = this.rules.tag.exec(src)) { 568 | src = src.substring(cap[0].length); 569 | out += this.options.sanitize ? escape(cap[0]) : cap[0]; 570 | continue; 571 | } 572 | 573 | // link 574 | if (cap = this.rules.link.exec(src)) { 575 | src = src.substring(cap[0].length); 576 | out += this.outputLink(cap, { 577 | href: cap[2], 578 | title: cap[3] 579 | }); 580 | continue; 581 | } 582 | 583 | // reflink, nolink 584 | if ((cap = this.rules.reflink.exec(src)) || (cap = this.rules.nolink.exec(src))) { 585 | src = src.substring(cap[0].length); 586 | link = (cap[2] || cap[1]).replace(/\s+/g, ' '); 587 | link = this.links[link.toLowerCase()]; 588 | if (!link || !link.href) { 589 | out += cap[0].charAt(0); 590 | src = cap[0].substring(1) + src; 591 | continue; 592 | } 593 | out += this.outputLink(cap, link); 594 | continue; 595 | } 596 | 597 | // strong 598 | if (cap = this.rules.strong.exec(src)) { 599 | src = src.substring(cap[0].length); 600 | out += '' + this.output(cap[2] || cap[1]) + ''; 601 | continue; 602 | } 603 | 604 | // em 605 | if (cap = this.rules.em.exec(src)) { 606 | src = src.substring(cap[0].length); 607 | out += '' + this.output(cap[2] || cap[1]) + ''; 608 | continue; 609 | } 610 | 611 | // code 612 | if (cap = this.rules.code.exec(src)) { 613 | src = src.substring(cap[0].length); 614 | out += '' + escape(cap[2], true) + ''; 615 | continue; 616 | } 617 | 618 | // br 619 | if (cap = this.rules.br.exec(src)) { 620 | src = src.substring(cap[0].length); 621 | out += '
'; 622 | continue; 623 | } 624 | 625 | // del (gfm) 626 | if (cap = this.rules.del.exec(src)) { 627 | src = src.substring(cap[0].length); 628 | out += '' + this.output(cap[1]) + ''; 629 | continue; 630 | } 631 | 632 | // text 633 | if (cap = this.rules.text.exec(src)) { 634 | src = src.substring(cap[0].length); 635 | out += escape(this.smartypants(cap[0])); 636 | continue; 637 | } 638 | 639 | if (src) { 640 | throw new 641 | Error('Infinite loop on byte: ' + src.charCodeAt(0)); 642 | } 643 | } 644 | 645 | return out; 646 | }; 647 | 648 | /** 649 | * Compile Link 650 | */ 651 | 652 | InlineLexer.prototype.outputLink = function(cap, link) { 653 | if (cap[0].charAt(0) !== '!') { 654 | return '' + this.output(cap[1]) + ''; 655 | } else { 656 | return '' + escape(cap[1]) + ''; 657 | } 658 | }; 659 | 660 | /** 661 | * Smartypants Transformations 662 | */ 663 | 664 | InlineLexer.prototype.smartypants = function(text) { 665 | if (!this.options.smartypants) return text; 666 | return text 667 | // em-dashes 668 | .replace(/--/g, '\u2014') 669 | // opening singles 670 | .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') 671 | // closing singles & apostrophes 672 | .replace(/'/g, '\u2019') 673 | // opening doubles 674 | .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') 675 | // closing doubles 676 | .replace(/"/g, '\u201d') 677 | // ellipses 678 | .replace(/\.{3}/g, '\u2026'); 679 | }; 680 | 681 | /** 682 | * Mangle Links 683 | */ 684 | 685 | InlineLexer.prototype.mangle = function(text) { 686 | var out = '', 687 | l = text.length, 688 | i = 0, 689 | ch; 690 | 691 | for (; i < l; i++) { 692 | ch = text.charCodeAt(i); 693 | if (Math.random() > 0.5) { 694 | ch = 'x' + ch.toString(16); 695 | } 696 | out += '&#' + ch + ';'; 697 | } 698 | 699 | return out; 700 | }; 701 | 702 | /** 703 | * Parsing & Compiling 704 | */ 705 | 706 | function Parser(options) { 707 | this.tokens = []; 708 | this.token = null; 709 | this.options = options || marked.defaults; 710 | } 711 | 712 | /** 713 | * Static Parse Method 714 | */ 715 | 716 | Parser.parse = function(src, options) { 717 | var parser = new Parser(options); 718 | return parser.parse(src); 719 | }; 720 | 721 | /** 722 | * Parse Loop 723 | */ 724 | 725 | Parser.prototype.parse = function(src) { 726 | this.inline = new InlineLexer(src.links, this.options); 727 | this.tokens = src.reverse(); 728 | 729 | var out = ''; 730 | while (this.next()) { 731 | out += this.tok(); 732 | } 733 | 734 | return out; 735 | }; 736 | 737 | /** 738 | * Next Token 739 | */ 740 | 741 | Parser.prototype.next = function() { 742 | return this.token = this.tokens.pop(); 743 | }; 744 | 745 | /** 746 | * Preview Next Token 747 | */ 748 | 749 | Parser.prototype.peek = function() { 750 | return this.tokens[this.tokens.length - 1] || 0; 751 | }; 752 | 753 | /** 754 | * Parse Text Tokens 755 | */ 756 | 757 | Parser.prototype.parseText = function() { 758 | var body = this.token.text; 759 | 760 | while (this.peek().type === 'text') { 761 | body += '\n' + this.next().text; 762 | } 763 | 764 | return this.inline.output(body); 765 | }; 766 | 767 | /** 768 | * Parse Current Token 769 | */ 770 | 771 | Parser.prototype.tok = function() { 772 | switch (this.token.type) { 773 | case 'space': 774 | { 775 | return ''; 776 | } 777 | case 'hr': 778 | { 779 | return '
\n'; 780 | } 781 | case 'heading': 782 | { 783 | return '' + this.inline.output(this.token.text) + '\n'; 784 | } 785 | case 'code': 786 | { 787 | if (this.options.highlight) { 788 | var code = this.options.highlight(this.token.text, this.token.lang); 789 | if (code != null && code !== this.token.text) { 790 | this.token.escaped = true; 791 | this.token.text = code; 792 | } 793 | } 794 | 795 | if (!this.token.escaped) { 796 | this.token.text = escape(this.token.text, true); 797 | } 798 | 799 | return '
' + this.token.text + '
\n'; 800 | } 801 | case 'table': 802 | { 803 | var body = '', 804 | heading, i, row, cell, j; 805 | 806 | // header 807 | body += '\n\n'; 808 | for (i = 0; i < this.token.header.length; i++) { 809 | heading = this.inline.output(this.token.header[i]); 810 | body += '\n'; 815 | } 816 | body += '\n\n'; 817 | 818 | // body 819 | body += '\n' 820 | for (i = 0; i < this.token.cells.length; i++) { 821 | row = this.token.cells[i]; 822 | body += '\n'; 823 | for (j = 0; j < row.length; j++) { 824 | cell = this.inline.output(row[j]); 825 | body += '\n'; 830 | } 831 | body += '\n'; 832 | } 833 | body += '\n'; 834 | 835 | return '\n' + body + '
\n'; 836 | } 837 | case 'blockquote_start': 838 | { 839 | var body = ''; 840 | 841 | while (this.next().type !== 'blockquote_end') { 842 | body += this.tok(); 843 | } 844 | 845 | return '
\n' + body + '
\n'; 846 | } 847 | case 'list_start': 848 | { 849 | var type = this.token.ordered ? 'ol' : 'ul', 850 | body = ''; 851 | 852 | while (this.next().type !== 'list_end') { 853 | body += this.tok(); 854 | } 855 | 856 | return '<' + type + '>\n' + body + '\n'; 857 | } 858 | case 'list_item_start': 859 | { 860 | var body = ''; 861 | 862 | while (this.next().type !== 'list_item_end') { 863 | body += this.token.type === 'text' ? this.parseText() : this.tok(); 864 | } 865 | 866 | return '
  • ' + body + '
  • \n'; 867 | } 868 | case 'loose_item_start': 869 | { 870 | var body = ''; 871 | 872 | while (this.next().type !== 'list_item_end') { 873 | body += this.tok(); 874 | } 875 | 876 | return '
  • ' + body + '
  • \n'; 877 | } 878 | case 'html': 879 | { 880 | return !this.token.pre && !this.options.pedantic ? this.inline.output(this.token.text) : this.token.text; 881 | } 882 | case 'paragraph': 883 | { 884 | return '

    ' + this.inline.output(this.token.text) + '

    \n'; 885 | } 886 | case 'text': 887 | { 888 | return '

    ' + this.parseText() + '

    \n'; 889 | } 890 | } 891 | }; 892 | 893 | /** 894 | * Helpers 895 | */ 896 | 897 | function escape(html, encode) { 898 | return html 899 | .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') 900 | .replace(//g, '>') 902 | .replace(/"/g, '"') 903 | .replace(/'/g, '''); 904 | } 905 | 906 | function replace(regex, opt) { 907 | regex = regex.source; 908 | opt = opt || ''; 909 | return function self(name, val) { 910 | if (!name) return new RegExp(regex, opt); 911 | val = val.source || val; 912 | val = val.replace(/(^|[^\[])\^/g, '$1'); 913 | regex = regex.replace(name, val); 914 | return self; 915 | }; 916 | } 917 | 918 | function noop() {} 919 | noop.exec = noop; 920 | 921 | function merge(obj) { 922 | var i = 1, 923 | target, key; 924 | 925 | for (; i < arguments.length; i++) { 926 | target = arguments[i]; 927 | for (key in target) { 928 | if (Object.prototype.hasOwnProperty.call(target, key)) { 929 | obj[key] = target[key]; 930 | } 931 | } 932 | } 933 | 934 | return obj; 935 | } 936 | 937 | /** 938 | * Marked 939 | */ 940 | 941 | function marked(src, opt, callback) { 942 | if (callback || typeof opt === 'function') { 943 | if (!callback) { 944 | callback = opt; 945 | opt = null; 946 | } 947 | 948 | opt = merge({}, marked.defaults, opt || {}); 949 | 950 | var highlight = opt.highlight, 951 | tokens, pending, i = 0; 952 | 953 | try { 954 | tokens = Lexer.lex(src, opt) 955 | } catch (e) { 956 | return callback(e); 957 | } 958 | 959 | pending = tokens.length; 960 | 961 | var done = function() { 962 | var out, err; 963 | 964 | try { 965 | out = Parser.parse(tokens, opt); 966 | } catch (e) { 967 | err = e; 968 | } 969 | 970 | opt.highlight = highlight; 971 | 972 | return err ? callback(err) : callback(null, out); 973 | }; 974 | 975 | if (!highlight || highlight.length < 3) { 976 | return done(); 977 | } 978 | 979 | delete opt.highlight; 980 | 981 | if (!pending) return done(); 982 | 983 | for (; i < tokens.length; i++) { 984 | (function(token) { 985 | if (token.type !== 'code') { 986 | return --pending || done(); 987 | } 988 | return highlight(token.text, token.lang, function(err, code) { 989 | if (code == null || code === token.text) { 990 | return --pending || done(); 991 | } 992 | token.text = code; 993 | token.escaped = true; 994 | --pending || done(); 995 | }); 996 | })(tokens[i]); 997 | } 998 | 999 | return; 1000 | } 1001 | try { 1002 | if (opt) opt = merge({}, marked.defaults, opt); 1003 | return Parser.parse(Lexer.lex(src, opt), opt); 1004 | } catch (e) { 1005 | e.message += '\nPlease report this to https://github.com/chjj/marked.'; 1006 | if ((opt || marked.defaults).silent) { 1007 | return '

    An error occured:

    ' + escape(e.message + '', true) + '
    '; 1008 | } 1009 | throw e; 1010 | } 1011 | } 1012 | 1013 | /** 1014 | * Options 1015 | */ 1016 | 1017 | marked.options = 1018 | marked.setOptions = function(opt) { 1019 | merge(marked.defaults, opt); 1020 | return marked; 1021 | }; 1022 | 1023 | marked.defaults = { 1024 | gfm: true, 1025 | tables: true, 1026 | breaks: false, 1027 | pedantic: false, 1028 | sanitize: false, 1029 | smartLists: false, 1030 | silent: false, 1031 | highlight: null, 1032 | langPrefix: 'lang-', 1033 | smartypants: false 1034 | }; 1035 | 1036 | /** 1037 | * Expose 1038 | */ 1039 | 1040 | marked.Parser = Parser; 1041 | marked.parser = Parser.parse; 1042 | 1043 | marked.Lexer = Lexer; 1044 | marked.lexer = Lexer.lex; 1045 | 1046 | marked.InlineLexer = InlineLexer; 1047 | marked.inlineLexer = InlineLexer.output; 1048 | 1049 | marked.parse = marked; 1050 | 1051 | if (typeof exports === 'object') { 1052 | module.exports = marked; 1053 | } else if (typeof define === 'function' && define.amd) { 1054 | define(function() { 1055 | return marked; 1056 | }); 1057 | } else { 1058 | this.marked = marked; 1059 | } 1060 | 1061 | }).call(function() { 1062 | return this || (typeof window !== 'undefined' ? window : global); 1063 | }()); 1064 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-worker-digest-demo", 3 | "version": "0.0.1", 4 | "description": "Running Angular digest cycle in the web worker demo site", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/bahmutov/web-worker-digest-demo.git" 12 | }, 13 | "keywords": [ 14 | "angular", 15 | "demom", 16 | "digest", 17 | "web", 18 | "worker" 19 | ], 20 | "author": "Gleb Bahmutov ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/bahmutov/web-worker-digest-demo/issues" 24 | }, 25 | "homepage": "https://github.com/bahmutov/web-worker-digest-demo", 26 | "devDependencies": { 27 | "grunt": "0.4.5", 28 | "grunt-gh-pages": "0.10.0", 29 | "matchdep": "0.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /step-0/console-log-div.js: -------------------------------------------------------------------------------- 1 | (function initConsoleLogDiv() { 2 | 3 | if (console.log.toDiv) { 4 | return; 5 | } 6 | 7 | function toString(x) { 8 | return typeof x === 'string' ? x : JSON.stringify(x); 9 | } 10 | 11 | var log = console.log.bind(console); 12 | var error = console.error.bind(console); 13 | var warn = console.warn.bind(console); 14 | 15 | var id = 'console-log-div'; 16 | function createOuterElement() { 17 | var outer = document.getElementById(id); 18 | if (!outer) { 19 | outer = document.createElement('fieldset'); 20 | outer.id = id; 21 | document.body.appendChild(outer); 22 | } 23 | outer.classList.add('id'); 24 | 25 | var style = outer.style; 26 | style.width = '100%'; 27 | // style.minHeight = '200px'; 28 | style.fontFamily = 'monospace'; 29 | style.marginTop = '20px'; 30 | style.whiteSpace = 'pre'; 31 | style.border = '1px solid black'; 32 | style.borderRadius = '5px'; 33 | style.padding = '5px 10px'; 34 | return outer; 35 | } 36 | 37 | var logTo = (function createLogDiv() { 38 | 39 | var outer = createOuterElement(); 40 | 41 | var caption = document.createTextNode('console output'); 42 | var legend = document.createElement('legend'); 43 | legend.appendChild(caption); 44 | outer.appendChild(legend); 45 | 46 | var div = document.createElement('div'); 47 | div.id = 'console-log-text'; 48 | outer.appendChild(div); 49 | 50 | return div; 51 | }()); 52 | 53 | function printToDiv() { 54 | var msg = Array.prototype.slice.call(arguments, 0) 55 | .map(toString) 56 | .join(' '); 57 | var text = logTo.textContent; 58 | logTo.textContent = text + msg + '\n'; 59 | } 60 | 61 | function logWithCopy() { 62 | log.apply(null, arguments); 63 | printToDiv.apply(null, arguments); 64 | } 65 | 66 | console.log = logWithCopy; 67 | console.log.toDiv = true; 68 | 69 | console.error = function errorWithCopy() { 70 | error.apply(null, arguments); 71 | var args = Array.prototype.slice.call(arguments, 0); 72 | args.unshift('ERROR:'); 73 | printToDiv.apply(null, args); 74 | }; 75 | 76 | console.warn = function logWarning() { 77 | warn.apply(null, arguments); 78 | var args = Array.prototype.slice.call(arguments, 0); 79 | args.unshift('WARNING:'); 80 | printToDiv.apply(null, args); 81 | }; 82 | 83 | window.addEventListener('error', function (err) { 84 | printToDiv('EXCEPTION:', err.message + '\n ' + err.filename, err.lineno + ':' + err.colno); 85 | }); 86 | 87 | }()); 88 | -------------------------------------------------------------------------------- /step-0/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Run digest cycle in web worker 6 | 26 | 27 | 28 | 29 |

    Run digest cycle in web worker

    30 |
    31 | 32 | 33 | 34 |
    35 | 36 | 37 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /step-0/micro-angular.js: -------------------------------------------------------------------------------- 1 | function Scope() { 2 | this.$$watchers = []; 3 | } 4 | Scope.prototype.$watch = function(watchFn, listenerFn) { 5 | var watcher = { 6 | watchFn: watchFn, 7 | listenerFn: listenerFn || function() { } 8 | }; 9 | this.$$watchers.push(watcher); 10 | }; 11 | 12 | Scope.prototype.$digest = function(cb) { 13 | var self = this; 14 | var dirty = this.$$watchers.some(function (watch) { 15 | var newValue = watch.watchFn(self); 16 | var oldValue = watch.last; 17 | if (newValue !== oldValue) { 18 | watch.listenerFn(newValue, oldValue, self); 19 | watch.last = newValue; 20 | return true; 21 | } 22 | }); 23 | dirty && cb && cb(this); 24 | }; 25 | -------------------------------------------------------------------------------- /step-0/primes.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 3 | 4 | function isPrime(n) { 5 | var k; 6 | var limit = Math.sqrt(n); 7 | for (k = 2; k <= limit; k += 1) { 8 | if (n % k === 0) { 9 | return false; 10 | } 11 | } 12 | return true; 13 | } 14 | console.assert(isPrime(1)); 15 | console.assert(isPrime(2)); 16 | console.assert(isPrime(3)); 17 | console.assert(!isPrime(4)); 18 | console.assert(isPrime(5)); 19 | console.assert(!isPrime(6)); 20 | console.assert(isPrime(7)); 21 | console.assert(isPrime(37)); 22 | console.assert(!isPrime(38)); 23 | 24 | // finds Nth prime 25 | function findPrime(n) { 26 | var foundPrimes = []; 27 | var k; 28 | if (foundPrimes.length) { 29 | k = foundPrimes[foundPrimes.length - 1] + 1; 30 | } else { 31 | k = 1; 32 | } 33 | while (foundPrimes.length < n) { 34 | if (isPrime(k)) { 35 | foundPrimes.push(k); 36 | } 37 | k += 1; 38 | }; 39 | return foundPrimes[n - 1]; 40 | } 41 | console.assert(findPrime(1) === 1); 42 | console.assert(findPrime(2) === 2); 43 | console.assert(findPrime(3) === 3); 44 | console.assert(findPrime(4) === 5); 45 | console.assert(findPrime(5) === 7); 46 | 47 | function findPrimes(n) { 48 | var k, primes = []; 49 | for (k = 0; k < n; k += 1) { 50 | var prime = findPrime(k + 2); 51 | primes.push(prime); 52 | } 53 | return primes; 54 | } 55 | 56 | root.findPrimes = findPrimes; 57 | }(this)); 58 | -------------------------------------------------------------------------------- /step-1/console-log-div.js: -------------------------------------------------------------------------------- 1 | (function initConsoleLogDiv() { 2 | 3 | if (console.log.toDiv) { 4 | return; 5 | } 6 | 7 | function toString(x) { 8 | return typeof x === 'string' ? x : JSON.stringify(x); 9 | } 10 | 11 | var log = console.log.bind(console); 12 | var error = console.error.bind(console); 13 | var warn = console.warn.bind(console); 14 | 15 | var id = 'console-log-div'; 16 | function createOuterElement() { 17 | var outer = document.getElementById(id); 18 | if (!outer) { 19 | outer = document.createElement('fieldset'); 20 | outer.id = id; 21 | document.body.appendChild(outer); 22 | } 23 | outer.classList.add('id'); 24 | 25 | var style = outer.style; 26 | style.width = '100%'; 27 | // style.minHeight = '200px'; 28 | style.fontFamily = 'monospace'; 29 | style.marginTop = '20px'; 30 | style.whiteSpace = 'pre'; 31 | style.border = '1px solid black'; 32 | style.borderRadius = '5px'; 33 | style.padding = '5px 10px'; 34 | return outer; 35 | } 36 | 37 | var logTo = (function createLogDiv() { 38 | 39 | var outer = createOuterElement(); 40 | 41 | var caption = document.createTextNode('console output'); 42 | var legend = document.createElement('legend'); 43 | legend.appendChild(caption); 44 | outer.appendChild(legend); 45 | 46 | var div = document.createElement('div'); 47 | div.id = 'console-log-text'; 48 | outer.appendChild(div); 49 | 50 | return div; 51 | }()); 52 | 53 | function printToDiv() { 54 | var msg = Array.prototype.slice.call(arguments, 0) 55 | .map(toString) 56 | .join(' '); 57 | var text = logTo.textContent; 58 | logTo.textContent = text + msg + '\n'; 59 | } 60 | 61 | function logWithCopy() { 62 | log.apply(null, arguments); 63 | printToDiv.apply(null, arguments); 64 | } 65 | 66 | console.log = logWithCopy; 67 | console.log.toDiv = true; 68 | 69 | console.error = function errorWithCopy() { 70 | error.apply(null, arguments); 71 | var args = Array.prototype.slice.call(arguments, 0); 72 | args.unshift('ERROR:'); 73 | printToDiv.apply(null, args); 74 | }; 75 | 76 | console.warn = function logWarning() { 77 | warn.apply(null, arguments); 78 | var args = Array.prototype.slice.call(arguments, 0); 79 | args.unshift('WARNING:'); 80 | printToDiv.apply(null, args); 81 | }; 82 | 83 | window.addEventListener('error', function (err) { 84 | printToDiv('EXCEPTION:', err.message + '\n ' + err.filename, err.lineno + ':' + err.colno); 85 | }); 86 | 87 | }()); 88 | -------------------------------------------------------------------------------- /step-1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Run digest cycle in web worker 6 | 26 | 27 | 28 | 29 |

    Run digest cycle in web worker

    30 |
    31 | 32 | 33 | 34 |
    35 | 36 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /step-1/micro-angular-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('micro-angular.js', 'primes.js'); 2 | 3 | var scopes = {}; 4 | 5 | onmessage = function digestOnMessage(e) { 6 | console.log('micro-angular-worker received:', e.data); 7 | switch (e.data.cmd) { 8 | case 'Scope': 9 | scopes[e.data.id] = new Scope(e.data.id); 10 | break; 11 | case 'set': 12 | scopes[e.data.id][e.data.name] = e.data.value; 13 | break; 14 | case '$watch': 15 | scopes[e.data.id].$watch( 16 | eval('(' + e.data.watchFn + ')'), 17 | e.data.listenerFn && eval('(' + e.data.listenerFn + ')') 18 | ); 19 | break; 20 | case '$digest': 21 | scopes[e.data.id].$digest(function digestFinished() { 22 | var $compile, scope, html; 23 | if (e.data.$compile) { 24 | $compile = eval('(' + e.data.$compile + ')'); 25 | scope = scopes[e.data.id]; 26 | html = $compile(scope); 27 | } 28 | 29 | postMessage({ 30 | cmd: 'digestFinished', 31 | html: html 32 | }); 33 | }); 34 | break; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /step-1/micro-angular.js: -------------------------------------------------------------------------------- 1 | function Scope() { 2 | this.$$watchers = []; 3 | } 4 | Scope.prototype.$watch = function(watchFn, listenerFn) { 5 | var watcher = { 6 | watchFn: watchFn, 7 | listenerFn: listenerFn || function() { } 8 | }; 9 | this.$$watchers.push(watcher); 10 | }; 11 | 12 | Scope.prototype.$digest = function(cb) { 13 | var self = this; 14 | var dirty = this.$$watchers.some(function (watch) { 15 | var newValue = watch.watchFn(self); 16 | var oldValue = watch.last; 17 | if (newValue !== oldValue) { 18 | watch.listenerFn(newValue, oldValue, self); 19 | watch.last = newValue; 20 | return true; 21 | } 22 | }); 23 | dirty && cb && cb(this); 24 | }; 25 | 26 | 27 | -------------------------------------------------------------------------------- /step-1/mock-scopes.js: -------------------------------------------------------------------------------- 1 | (function initMockScopes(root) { 2 | 3 | var digestWorker = new Worker('./micro-angular-worker.js'); 4 | 5 | digestWorker.onmessage = function (e) { 6 | root.render && root.render(e.data.html); 7 | }; 8 | 9 | var scopes = 0; 10 | function Scope() { 11 | this.id = '$' + scopes; 12 | scopes += 1; 13 | digestWorker.postMessage({ 14 | cmd: 'Scope', 15 | id: this.id 16 | }); 17 | console.log('created mock scope', this.id); 18 | } 19 | 20 | Scope.prototype.set = function (name, value) { 21 | digestWorker.postMessage({ 22 | cmd: 'set', 23 | id: this.id, 24 | name: name, 25 | value: value 26 | }); 27 | console.log('set mock scope', this.id, 'property', name, '=', value); 28 | }; 29 | 30 | Scope.prototype.$watch = function (watchFn, listenerFn) { 31 | digestWorker.postMessage({ 32 | cmd: '$watch', 33 | id: this.id, 34 | watchFn: watchFn.toString(), 35 | listenerFn: listenerFn && listenerFn.toString() 36 | }); 37 | }; 38 | 39 | Scope.prototype.$digest = function ($compile) { 40 | digestWorker.postMessage({ 41 | cmd: '$digest', 42 | id: this.id, 43 | $compile: $compile && $compile.toString() 44 | }); 45 | }; 46 | 47 | root.Scope = Scope; 48 | }(this)); 49 | -------------------------------------------------------------------------------- /step-1/primes.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 3 | 4 | function isPrime(n) { 5 | var k; 6 | var limit = Math.sqrt(n); 7 | for (k = 2; k <= limit; k += 1) { 8 | if (n % k === 0) { 9 | return false; 10 | } 11 | } 12 | return true; 13 | } 14 | console.assert(isPrime(1)); 15 | console.assert(isPrime(2)); 16 | console.assert(isPrime(3)); 17 | console.assert(!isPrime(4)); 18 | console.assert(isPrime(5)); 19 | console.assert(!isPrime(6)); 20 | console.assert(isPrime(7)); 21 | console.assert(isPrime(37)); 22 | console.assert(!isPrime(38)); 23 | 24 | // finds Nth prime 25 | function findPrime(n) { 26 | var foundPrimes = []; 27 | var k; 28 | if (foundPrimes.length) { 29 | k = foundPrimes[foundPrimes.length - 1] + 1; 30 | } else { 31 | k = 1; 32 | } 33 | while (foundPrimes.length < n) { 34 | if (isPrime(k)) { 35 | foundPrimes.push(k); 36 | } 37 | k += 1; 38 | }; 39 | return foundPrimes[n - 1]; 40 | } 41 | console.assert(findPrime(1) === 1); 42 | console.assert(findPrime(2) === 2); 43 | console.assert(findPrime(3) === 3); 44 | console.assert(findPrime(4) === 5); 45 | console.assert(findPrime(5) === 7); 46 | 47 | function findPrimes(n) { 48 | var k, primes = []; 49 | for (k = 0; k < n; k += 1) { 50 | var prime = findPrime(k + 2); 51 | primes.push(prime); 52 | } 53 | return primes; 54 | } 55 | 56 | root.findPrimes = findPrimes; 57 | }(this)); 58 | -------------------------------------------------------------------------------- /step-1/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('micro-angular.js', 'primes.js'); 2 | 3 | onmessage = function (e) { 4 | console.log('worker received message:', e.data); 5 | switch (e.data.cmd) { 6 | case 'primes': 7 | var scope = new Scope(); 8 | scope.$watch(function watcherFn(scope) { 9 | return scope.n; 10 | }, function listenerFn(newValue, oldValue, scope) { 11 | if (newValue) { 12 | console.log('finding', newValue, 'primes'); 13 | scope.primes = findPrimes(newValue); 14 | } 15 | }); 16 | scope.n = e.data.n; 17 | 18 | scope.$digest(function afterDigest(scope) { 19 | var n = scope.n; 20 | var str = '
      '; 21 | for (k = 0; k < n; k += 1) { 22 | str += '
    • ' + (k + 1) + ' prime ' + scope.primes[k] + '
    • '; 23 | } 24 | str += '
    '; 25 | postMessage({ 26 | html: str 27 | }); 28 | }); 29 | break; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /step-2/console-log-div.js: -------------------------------------------------------------------------------- 1 | (function initConsoleLogDiv() { 2 | 3 | if (console.log.toDiv) { 4 | return; 5 | } 6 | 7 | function toString(x) { 8 | return typeof x === 'string' ? x : JSON.stringify(x); 9 | } 10 | 11 | var log = console.log.bind(console); 12 | var error = console.error.bind(console); 13 | var warn = console.warn.bind(console); 14 | 15 | var id = 'console-log-div'; 16 | function createOuterElement() { 17 | var outer = document.getElementById(id); 18 | if (!outer) { 19 | outer = document.createElement('fieldset'); 20 | outer.id = id; 21 | document.body.appendChild(outer); 22 | } 23 | outer.classList.add('id'); 24 | 25 | var style = outer.style; 26 | style.width = '100%'; 27 | // style.minHeight = '200px'; 28 | style.fontFamily = 'monospace'; 29 | style.marginTop = '20px'; 30 | style.whiteSpace = 'pre'; 31 | style.border = '1px solid black'; 32 | style.borderRadius = '5px'; 33 | style.padding = '5px 10px'; 34 | return outer; 35 | } 36 | 37 | var logTo = (function createLogDiv() { 38 | 39 | var outer = createOuterElement(); 40 | 41 | var caption = document.createTextNode('console output'); 42 | var legend = document.createElement('legend'); 43 | legend.appendChild(caption); 44 | outer.appendChild(legend); 45 | 46 | var div = document.createElement('div'); 47 | div.id = 'console-log-text'; 48 | outer.appendChild(div); 49 | 50 | return div; 51 | }()); 52 | 53 | function printToDiv() { 54 | var msg = Array.prototype.slice.call(arguments, 0) 55 | .map(toString) 56 | .join(' '); 57 | var text = logTo.textContent; 58 | logTo.textContent = text + msg + '\n'; 59 | } 60 | 61 | function logWithCopy() { 62 | log.apply(null, arguments); 63 | printToDiv.apply(null, arguments); 64 | } 65 | 66 | console.log = logWithCopy; 67 | console.log.toDiv = true; 68 | 69 | console.error = function errorWithCopy() { 70 | error.apply(null, arguments); 71 | var args = Array.prototype.slice.call(arguments, 0); 72 | args.unshift('ERROR:'); 73 | printToDiv.apply(null, args); 74 | }; 75 | 76 | console.warn = function logWarning() { 77 | warn.apply(null, arguments); 78 | var args = Array.prototype.slice.call(arguments, 0); 79 | args.unshift('WARNING:'); 80 | printToDiv.apply(null, args); 81 | }; 82 | 83 | window.addEventListener('error', function (err) { 84 | printToDiv('EXCEPTION:', err.message + '\n ' + err.filename, err.lineno + ':' + err.colno); 85 | }); 86 | 87 | }()); 88 | -------------------------------------------------------------------------------- /step-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Run digest cycle in web worker 6 | 26 | 27 | 28 | 29 |

    Run digest cycle in web worker

    30 |
    31 | 32 | 33 | 34 |
    35 | 36 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /step-2/micro-angular-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('micro-angular.js', 'primes.js'); 2 | 3 | var scopes = {}; 4 | 5 | onmessage = function digestOnMessage(e) { 6 | console.log('micro-angular-worker received:', e.data); 7 | switch (e.data.cmd) { 8 | case 'Scope': 9 | scopes[e.data.id] = new Scope(e.data.id); 10 | break; 11 | case 'set': 12 | scopes[e.data.id][e.data.name] = e.data.value; 13 | break; 14 | case '$watch': 15 | scopes[e.data.id].$watch( 16 | eval('(' + e.data.watchFn + ')'), 17 | e.data.listenerFn && eval('(' + e.data.listenerFn + ')') 18 | ); 19 | break; 20 | case '$digest': 21 | scopes[e.data.id].$digest(function digestFinished() { 22 | var $compile, scope, html; 23 | if (e.data.$compile) { 24 | $compile = eval('(' + e.data.$compile + ')'); 25 | scope = scopes[e.data.id]; 26 | html = $compile(scope); 27 | } 28 | 29 | postMessage({ 30 | cmd: 'digestFinished', 31 | html: html 32 | }); 33 | }); 34 | break; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /step-2/micro-angular.js: -------------------------------------------------------------------------------- 1 | function Scope() { 2 | this.$$watchers = []; 3 | } 4 | Scope.prototype.$watch = function(watchFn, listenerFn) { 5 | var watcher = { 6 | watchFn: watchFn, 7 | listenerFn: listenerFn || function() { } 8 | }; 9 | this.$$watchers.push(watcher); 10 | }; 11 | 12 | Scope.prototype.$digest = function(cb) { 13 | var self = this; 14 | var dirty = this.$$watchers.some(function (watch) { 15 | var newValue = watch.watchFn(self); 16 | var oldValue = watch.last; 17 | if (newValue !== oldValue) { 18 | watch.listenerFn(newValue, oldValue, self); 19 | watch.last = newValue; 20 | return true; 21 | } 22 | }); 23 | dirty && cb && cb(this); 24 | }; 25 | 26 | 27 | -------------------------------------------------------------------------------- /step-2/mock-scopes.js: -------------------------------------------------------------------------------- 1 | (function initMockScopes(root) { 2 | 3 | var digestWorker = new Worker('./micro-angular-worker.js'); 4 | 5 | digestWorker.onmessage = function (e) { 6 | root.render && root.render(e.data.html); 7 | }; 8 | 9 | var scopes = 0; 10 | function Scope() { 11 | this.id = '$' + scopes; 12 | scopes += 1; 13 | digestWorker.postMessage({ 14 | cmd: 'Scope', 15 | id: this.id 16 | }); 17 | 18 | var self = this; 19 | Object.observe(this, function (changes) { 20 | console.log('changed object', self.id, changes); 21 | changes.forEach(function (change) { 22 | switch (change.type) { 23 | case 'add': 24 | case 'update': 25 | console.log('change', change.name, 'to', change.object[change.name]); 26 | self.set(change.name, change.object[change.name]); 27 | break; 28 | } 29 | }); 30 | }); 31 | console.log('created mock scope', this.id); 32 | } 33 | 34 | Scope.prototype.set = function (name, value) { 35 | digestWorker.postMessage({ 36 | cmd: 'set', 37 | id: this.id, 38 | name: name, 39 | value: value 40 | }); 41 | console.log('set mock scope', this.id, 'property', name, '=', value); 42 | }; 43 | 44 | Scope.prototype.$watch = function (watchFn, listenerFn) { 45 | digestWorker.postMessage({ 46 | cmd: '$watch', 47 | id: this.id, 48 | watchFn: watchFn.toString(), 49 | listenerFn: listenerFn && listenerFn.toString() 50 | }); 51 | }; 52 | 53 | Scope.prototype.$digest = function ($compile) { 54 | var self = this; 55 | setTimeout(function () { 56 | digestWorker.postMessage({ 57 | cmd: '$digest', 58 | id: self.id, 59 | $compile: $compile && $compile.toString() 60 | }); 61 | }, 0); 62 | }; 63 | 64 | root.Scope = Scope; 65 | }(this)); 66 | -------------------------------------------------------------------------------- /step-2/primes.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 3 | 4 | function isPrime(n) { 5 | var k; 6 | var limit = Math.sqrt(n); 7 | for (k = 2; k <= limit; k += 1) { 8 | if (n % k === 0) { 9 | return false; 10 | } 11 | } 12 | return true; 13 | } 14 | console.assert(isPrime(1)); 15 | console.assert(isPrime(2)); 16 | console.assert(isPrime(3)); 17 | console.assert(!isPrime(4)); 18 | console.assert(isPrime(5)); 19 | console.assert(!isPrime(6)); 20 | console.assert(isPrime(7)); 21 | console.assert(isPrime(37)); 22 | console.assert(!isPrime(38)); 23 | 24 | // finds Nth prime 25 | function findPrime(n) { 26 | var foundPrimes = []; 27 | var k; 28 | if (foundPrimes.length) { 29 | k = foundPrimes[foundPrimes.length - 1] + 1; 30 | } else { 31 | k = 1; 32 | } 33 | while (foundPrimes.length < n) { 34 | if (isPrime(k)) { 35 | foundPrimes.push(k); 36 | } 37 | k += 1; 38 | }; 39 | return foundPrimes[n - 1]; 40 | } 41 | console.assert(findPrime(1) === 1); 42 | console.assert(findPrime(2) === 2); 43 | console.assert(findPrime(3) === 3); 44 | console.assert(findPrime(4) === 5); 45 | console.assert(findPrime(5) === 7); 46 | 47 | function findPrimes(n) { 48 | var k, primes = []; 49 | for (k = 0; k < n; k += 1) { 50 | var prime = findPrime(k + 2); 51 | primes.push(prime); 52 | } 53 | return primes; 54 | } 55 | 56 | root.findPrimes = findPrimes; 57 | }(this)); 58 | -------------------------------------------------------------------------------- /step-2/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('micro-angular.js', 'primes.js'); 2 | 3 | onmessage = function (e) { 4 | console.log('worker received message:', e.data); 5 | switch (e.data.cmd) { 6 | case 'primes': 7 | var scope = new Scope(); 8 | scope.$watch(function watcherFn(scope) { 9 | return scope.n; 10 | }, function listenerFn(newValue, oldValue, scope) { 11 | if (newValue) { 12 | console.log('finding', newValue, 'primes'); 13 | scope.primes = findPrimes(newValue); 14 | } 15 | }); 16 | scope.n = e.data.n; 17 | 18 | scope.$digest(function afterDigest(scope) { 19 | var n = scope.n; 20 | var str = '
      '; 21 | for (k = 0; k < n; k += 1) { 22 | str += '
    • ' + (k + 1) + ' prime ' + scope.primes[k] + '
    • '; 23 | } 24 | str += '
    '; 25 | postMessage({ 26 | html: str 27 | }); 28 | }); 29 | break; 30 | } 31 | }; 32 | --------------------------------------------------------------------------------