├── js ├── README.md ├── helpers.js ├── lib │ ├── pubsub.js │ ├── info.js │ ├── sketch.js │ └── Markdown.Converter.js ├── settings.js ├── stats.hbs ├── edge.js ├── packets.js ├── main.js ├── node.js ├── packet.js ├── network.js └── stats.js ├── .gitignore ├── .eslintignore ├── img └── img.png ├── todo.md ├── css ├── reset.css ├── style.css └── info.css ├── package.json ├── .eslintrc ├── index.html └── README.md /js/README.md: -------------------------------------------------------------------------------- 1 | ./README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | js/lib 3 | node_modules 4 | -------------------------------------------------------------------------------- /img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cafe/rota/master/img/img.png -------------------------------------------------------------------------------- /js/helpers.js: -------------------------------------------------------------------------------- 1 | var Sketch = require('./lib/sketch'); 2 | let helpers = {}; 3 | Sketch.install(helpers); 4 | module.exports = helpers; 5 | -------------------------------------------------------------------------------- /js/lib/pubsub.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var _ = require('underscore'); 3 | 4 | var Pubsub = _.clone(Backbone.Events); 5 | window.Pubsub = Pubsub; 6 | 7 | modules.exports = Pubsub; 8 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | const SPACING = 50; 2 | const SPEED = 1; 3 | const LATENCY_RANGE = [20, 180].map((num) => num * SPEED); 4 | const REDZONE_TIME = 40 * (LATENCY_RANGE[0] + LATENCY_RANGE[1]) / 2; 5 | const ROW_COLUMN_COUNT = 7; 6 | 7 | const settings = { 8 | LATENCY_RANGE, 9 | REDZONE_TIME, 10 | SPACING, 11 | ROW_COLUMN_COUNT 12 | }; 13 | 14 | module.exports = settings; 15 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # To Do 2 | 3 | - Show heatmaps by clicking on each node (Or draw paths for best paths) 4 | - How do we show directionality of paths? 5 | - packet TTLs 6 | 7 | - adjust render speed 8 | 9 | - allow edges to change in latency (based on usage)? 10 | 11 | - show chart with sliding window instead of packet delivery averages 12 | - use dat.gui and have controls for settings constants 13 | - include progress bar to show how fully "trained" the grid is - maybe each node needs to have a certain number of iterations per address? This way it's easy to indicate that the grid has been reset? 14 | -------------------------------------------------------------------------------- /css/reset.css: -------------------------------------------------------------------------------- 1 | /* Eric Meyer's Reset CSS v2.0 - http://cssreset.com */ 2 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0} 3 | -------------------------------------------------------------------------------- /js/stats.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{!-- 19 | 20 | 21 | --}} 22 |
{{total}}Total
{{inFlight}}In-Flight
{{delivered}}Delivered
{{averageTime}}Average Time (ms)
Send Rate (per second){{rate}}
23 | {{!-- 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Total Paths{{pathCount}}
Average Runs/Path{{averagePathCount}}
Average Best{{averageBest}}
Average Best/Actual Ratio1 : {{averageBestActualRatio}}
--}} 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rota", 3 | "version": "0.0.2", 4 | "description": "Routing with reinforcement learning", 5 | "main": "js/main.js", 6 | "scripts": { 7 | "build": "browserify . -o build/build.js", 8 | "watch": "watchify . --full-paths -o build/build.js -dv", 9 | "serve": "open http://localhost:1442 && python -m SimpleHTTPServer 1442", 10 | "lint": "eslint js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:rolyatmax/rota.git" 15 | }, 16 | "keywords": [ 17 | "routing", 18 | "reinforcement", 19 | "learning" 20 | ], 21 | "author": "Taylor Baldwin", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/rolyatmax/rota/issues" 25 | }, 26 | "homepage": "https://github.com/rolyatmax/rota", 27 | "devDependencies": { 28 | "babel-eslint": "6.0.2", 29 | "babel-plugin-transform-object-rest-spread": "6.6.5", 30 | "babel-preset-es2015": "6.6.0", 31 | "babelify": "7.2.0", 32 | "browserify": "13.0.0", 33 | "eslint": "2.8.0", 34 | "hbsify": "1.0.1", 35 | "watchify": "3.7.0" 36 | }, 37 | "dependencies": { 38 | "babel-polyfill": "6.7.4", 39 | "underscore": "1.7.0" 40 | }, 41 | "browserify": { 42 | "transform": [ 43 | "hbsify", 44 | [ 45 | "babelify", 46 | { 47 | "presets": [ 48 | "es2015" 49 | ], 50 | "plugins": [ 51 | "transform-object-rest-spread" 52 | ] 53 | } 54 | ] 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /js/edge.js: -------------------------------------------------------------------------------- 1 | var {random} = require('underscore'); 2 | var {LATENCY_RANGE} = require('./settings'); 3 | var {map} = require('./helpers'); 4 | 5 | class Edge { 6 | constructor(node1, node2, id) { 7 | this.id = id; 8 | this.nodes = [node1, node2]; 9 | this.latency = random(...LATENCY_RANGE); 10 | this.active = true; 11 | } 12 | getOtherNode(node) { 13 | var i = this.nodes.indexOf(node); 14 | if (1 < 0) { 15 | console.warn('edge: ', this); 16 | console.warn('node: ', node); 17 | throw new Error('Node not part of Edge'); 18 | } 19 | return i === 0 ? this.nodes[1] : this.nodes[0]; 20 | } 21 | draw(ctx) { 22 | if (!this.active) { 23 | return; 24 | } 25 | var [node1, node2] = this.nodes; 26 | var [x1, y1] = node1.loc; 27 | var [x2, y2] = node2.loc; 28 | ctx.beginPath(); 29 | ctx.moveTo(x1, y1); 30 | ctx.lineTo(x2, y2); 31 | ctx.lineWidth = 2; 32 | ctx.strokeStyle = getColor(this.latency); 33 | ctx.stroke(); 34 | } 35 | toggleActive(active) { 36 | if (active === undefined) { 37 | active = !this.active; 38 | } 39 | 40 | var method = active ? 'addEdge' : 'removeEdge'; 41 | this.nodes.forEach((node) => node[method](this)); 42 | this.active = active; 43 | } 44 | } 45 | 46 | function getColor(latency) { 47 | var [minA, maxA] = LATENCY_RANGE; 48 | var opacity = (((map(-latency, -maxA, -minA, 0.2, 0.9)) * 1000) | 0) / 1000; 49 | return `rgba(0, 0, 0, ${opacity})`; 50 | } 51 | 52 | module.exports = Edge; 53 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "block-scoped-var": [0], 8 | "brace-style": [1, "1tbs", {"allowSingleLine": true}], 9 | "camelcase": [1], 10 | "comma-spacing": [1], 11 | "comma-style": [1], 12 | "computed-property-spacing": [1, "never"], 13 | "consistent-return": [1], 14 | "consistent-this": [1, "self"], 15 | "curly": [2], 16 | "dot-notation": [0], 17 | "eol-last": [1], 18 | "eqeqeq": [1], 19 | "indent": [1, 4], 20 | "key-spacing": [1], 21 | "max-len": [1, 100], 22 | "max-nested-callbacks": [2, 3], 23 | "new-cap": [1], 24 | "new-parens": [1], 25 | "no-caller": [2], 26 | "no-console": [0], 27 | "no-eval": [2], 28 | "no-extend-native": [2], 29 | "no-extra-bind": [1], 30 | "no-floating-decimal": [1], 31 | "no-iterator": [1], 32 | "no-lone-blocks": [1], 33 | "no-lonely-if": [1], 34 | "no-mixed-requires": [0], 35 | "no-mixed-spaces-and-tabs": [1], 36 | "no-multi-spaces": [1], 37 | "no-multi-str": [1], 38 | "no-multiple-empty-lines": [2, {"max": 2}], 39 | "no-native-reassign": [1], 40 | "no-new": [0], 41 | "no-redeclare": [1], 42 | "no-shadow": [1], 43 | "no-spaced-func": [1], 44 | "no-throw-literal": [1], 45 | "no-trailing-spaces": [1], 46 | "no-undef": [1], 47 | "no-underscore-dangle": [0], 48 | "no-unneeded-ternary": [1], 49 | "no-unused-vars": [1], 50 | "no-use-before-define": [1, "nofunc"], 51 | "no-with": [2], 52 | "one-var": [1, "never"], 53 | "quotes": [1, "single"], 54 | "radix": [1], 55 | "semi": [1, "always"], 56 | "semi-spacing": [1], 57 | "space-before-blocks": [1, "always"], 58 | "space-before-function-paren": [1, "never"], 59 | "space-in-parens": [1, "never"], 60 | "space-infix-ops": [1], 61 | "space-unary-ops": [1], 62 | "strict": [0], 63 | "wrap-iife": [1] 64 | }, 65 | 66 | "globals": { 67 | "module": true, 68 | "require": true, 69 | "Set": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /js/packets.js: -------------------------------------------------------------------------------- 1 | var {uniqueId, difference, sample} = require('underscore'); 2 | var Packet = require('./packet'); 3 | 4 | const RATE = 150; 5 | 6 | class Packets { 7 | constructor(network, smartOpts = {}) { 8 | this.network = network; 9 | this.inFlight = []; 10 | this.rate = RATE; 11 | this.timeout = null; 12 | this.smart = !!Object.keys(smartOpts).length; 13 | if (this.smart) { 14 | this.smartOpts = { 15 | ...smartOpts, 16 | 'version': uniqueId('policy') 17 | }; 18 | } 19 | } 20 | setStats(stats) { 21 | this.stats = stats; 22 | } 23 | start() { 24 | this.pollAddPackets(); 25 | } 26 | stop() { 27 | clearTimeout(this.timeout); 28 | } 29 | pollAddPackets() { 30 | this.timeout = setTimeout(this.pollAddPackets.bind(this), 100); 31 | this.addPackets((this.rate / 10) | 0); 32 | } 33 | update() { 34 | this.inFlight.forEach((packet) => packet.update()); 35 | var completed = this.inFlight.filter((packet) => !!packet.totalTime); 36 | completed.forEach((packet) => this.stats.logFinish(packet)); 37 | this.inFlight = difference(this.inFlight, completed); 38 | } 39 | draw(ctx) { 40 | this.inFlight.forEach((packet) => packet.draw(ctx)); 41 | } 42 | addPackets(count = 1, endNodeX, endNodeY, startNodeX, startNodeY) { 43 | var endNode; 44 | var startNode; 45 | if (endNodeX !== undefined && endNodeY !== undefined) { 46 | endNode = this.network.getNode(endNodeX, endNodeY); 47 | } 48 | if (startNodeX !== undefined && startNodeY !== undefined) { 49 | startNode = this.network.getNode(startNodeX, startNodeY); 50 | } 51 | 52 | while (count--) { 53 | this.stats.totalCount += 1; 54 | this.stats.allTimeTotal += 1; 55 | var _startNode = startNode || sample(Object.values(this.network.nodes)); 56 | var _endNode = endNode || sample(Object.values(this.network.nodes)); 57 | this.inFlight.push(new Packet(_startNode, _endNode, this.smartOpts)); 58 | } 59 | } 60 | } 61 | 62 | module.exports = Packets; 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rota 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Rota

15 |

Routing with Reinforcement Learning

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 30 |
31 |
32 | 33 | 34 |
35 |
    36 |
  • Add/remove connections by clicking them.
  • 37 |
  • Shift-click to send packets to a specific node.
  • 38 |
39 |
40 |
41 |
42 | 43 | 44 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | require('babel/polyfill'); 2 | 3 | var Sketch = require('./lib/sketch'); 4 | var Network = require('./network'); 5 | var Packets = require('./packets'); 6 | var Stats = require('./stats'); 7 | var Info = require('./lib/info'); 8 | var {ROW_COLUMN_COUNT} = require('./settings'); 9 | 10 | new Info({ 11 | url: 'README.md', 12 | keyTrigger: true, 13 | container: 'wrapper' 14 | }); 15 | 16 | var network = new Network(ROW_COLUMN_COUNT); 17 | 18 | var sketch = Sketch.create({ 19 | 'fullscreen': false, 20 | 'autopause': false, 21 | 'width': 800, 22 | 'height': 400, 23 | 'container': document.querySelector('.canvas-container'), 24 | 'globals': false 25 | }); 26 | 27 | var packets = []; 28 | var stats = []; 29 | 30 | var smart1 = new Packets(network, { 31 | 'explore': 0.15, 32 | 'alpha': 0.9, 33 | 'discount': 0.8, 34 | 'initial': 200, 35 | 'completionReward': 50000 36 | }); 37 | var stats1 = new Stats('.algo-0', smart1, sketch); 38 | smart1.setStats(stats1); 39 | packets.push(smart1); 40 | stats.push(stats1); 41 | // smart1.start(); 42 | 43 | var showOverlay = true; 44 | 45 | sketch.update = () => { 46 | packets.forEach((packet) => packet.update()); 47 | }; 48 | 49 | sketch.draw = () => { 50 | network.draw(sketch); 51 | packets.forEach((packet) => packet.draw(sketch)); 52 | if (showOverlay) { 53 | stats.forEach((stat) => stat.showAddressOverlay()); 54 | } 55 | }; 56 | 57 | sketch.touchstart = () => { 58 | var {x, y} = sketch.touches[0]; 59 | if (sketch.keys['SHIFT']) { 60 | let node = network.findClosestNodes(x, y, 1)[0]; 61 | if (node) { 62 | smart1.addPackets(getCount('.algo-0'), node.x, node.y); 63 | } 64 | return; 65 | } 66 | var edge = network.findClosestEdge(x, y); 67 | if (edge) { 68 | edge.toggleActive(); 69 | } 70 | }; 71 | 72 | window.sketch = sketch; 73 | window.network = network; 74 | window.smart1 = smart1; 75 | 76 | packets.forEach((collection, i) => { 77 | (() => { 78 | var selector = `.algo-${i}`; 79 | var coll = collection; 80 | document.querySelector(`${selector} .add`).addEventListener('click', () => { 81 | coll.addPackets(getCount(selector)); 82 | }); 83 | })(); 84 | }); 85 | 86 | document.querySelector('.reset-stats').addEventListener('click', () => { 87 | stats.forEach((stat) => stat.resetStats()); 88 | }); 89 | 90 | var $popularAddressesBtn = document.querySelector('.get-popular-addresses'); 91 | $popularAddressesBtn.addEventListener('click', () => { 92 | showOverlay = !showOverlay; 93 | var method = showOverlay ? 'add' : 'remove'; 94 | $popularAddressesBtn.classList[method]('on'); 95 | }); 96 | 97 | function getCount(selector) { 98 | return parseInt(document.querySelector(`${selector} input.count`).value, 10); 99 | } 100 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Raleway", sans-serif; 3 | color: #333; 4 | } 5 | 6 | .container { 7 | width: 100%; 8 | max-width: 800px; 9 | margin: auto; 10 | padding-top: 20px; 11 | } 12 | 13 | .info_container h1 { 14 | text-align: left; 15 | } 16 | 17 | .info_container p { 18 | margin: 1em 0; 19 | line-height: 1.8em; 20 | } 21 | 22 | .info_container pre { 23 | background-color: #333; 24 | padding: 10px 15px; 25 | border-radius: 5px; 26 | overflow-y: scroll; 27 | } 28 | 29 | .info_container code { 30 | font-family: monospace; 31 | } 32 | 33 | h1, h2, h3, h4 { 34 | line-height: 1.5em; 35 | } 36 | 37 | h1 { 38 | font-size: 32px; 39 | font-weight: 900; 40 | border-bottom: 1px solid #333; 41 | } 42 | 43 | h2 { 44 | font-size: 26px; 45 | font-weight: 600; 46 | } 47 | 48 | h3 { 49 | font-size: 22px; 50 | font-weight: 300; 51 | } 52 | 53 | h4 { 54 | margin: 10px 0; 55 | border-bottom: 1px solid #333; 56 | border-top: 1px solid #333; 57 | padding: 10px 0; 58 | } 59 | 60 | .content { 61 | position: relative; 62 | } 63 | 64 | .stats-container, .controls { 65 | width: 320px; 66 | position: absolute; 67 | top: 55px; 68 | right: 0; 69 | border-top: 1px solid #333; 70 | padding-top: 16px; 71 | } 72 | 73 | .controls { 74 | top: -34px; 75 | right: 0; 76 | } 77 | 78 | button { 79 | outline: none; 80 | padding: 10px; 81 | margin: 5px 0; 82 | background-color: #f5f5f5; 83 | border: 1px solid #ccc; 84 | border-radius: 3px; 85 | font-weight: 500; 86 | color: #333; 87 | font-size: 12px; 88 | cursor: pointer; 89 | -webkit-transition: background-color 200ms linear; 90 | } 91 | 92 | button:hover { 93 | background-color: #eaeaea; 94 | } 95 | 96 | button.on { 97 | background-color: #aaa; 98 | color: white; 99 | border-color: #888; 100 | } 101 | 102 | .controls button { 103 | float: left; 104 | margin-right: 5px; 105 | } 106 | 107 | .instructions { 108 | width: 320px; 109 | position: absolute; 110 | top: 252px; 111 | right: 0; 112 | line-height: 1.5em; 113 | border-top: 1px solid #333; 114 | padding-top: 20px; 115 | } 116 | 117 | .instructions li { 118 | list-style: initial; 119 | } 120 | 121 | input { 122 | outline: none; 123 | padding: 9px 5px; 124 | margin: 0 2px; 125 | font-size: 12px; 126 | color: #333; 127 | border-radius: 3px; 128 | border: 1px solid #ccc; 129 | } 130 | 131 | input.count { 132 | width: 40px; 133 | } 134 | 135 | input.x, input.y { 136 | width: 30px; 137 | } 138 | 139 | td { 140 | padding: 6px 10px; 141 | min-width: 55px; 142 | } 143 | 144 | td:first-child { 145 | text-align: right; 146 | } 147 | 148 | .popular-addresses { 149 | text-align: right; 150 | margin: 5px; 151 | } 152 | -------------------------------------------------------------------------------- /css/info.css: -------------------------------------------------------------------------------- 1 | 2 | .info_container h1 { 3 | text-align: center; 4 | margin-top: 0; 5 | } 6 | 7 | .info_container { 8 | text-align: left; 9 | padding: 30px 50px; 10 | min-height: 100%; 11 | position: absolute; 12 | top: 0; 13 | left: -500px; 14 | background-color: rgb(27,34,32); 15 | -webkit-transition: left .5s cubic-bezier(0.23, 1, 0.32, 1); 16 | -moz-transition: left .5s cubic-bezier(0.23, 1, 0.32, 1); 17 | transition: left .5s cubic-bezier(0.23, 1, 0.32, 1); 18 | color: #eee; 19 | font-weight: 300; 20 | line-height: 1.5em; 21 | letter-spacing: 0; 22 | font-size: 0.95em; 23 | width: 500px; 24 | height: 100vh; 25 | overflow: scroll; 26 | box-sizing: border-box; 27 | } 28 | 29 | .info_container.open { 30 | left: 0; 31 | } 32 | 33 | .info_container a { 34 | color: #eee; 35 | } 36 | 37 | .content_wrapper { 38 | position: relative; 39 | left: 0; 40 | -webkit-transition: left .5s cubic-bezier(0.23, 1, 0.32, 1); 41 | -moz-transition: left .5s cubic-bezier(0.23, 1, 0.32, 1); 42 | transition: left .5s cubic-bezier(0.23, 1, 0.32, 1); 43 | } 44 | 45 | .content_wrapper.inactive { 46 | left: 500px; 47 | } 48 | 49 | @-webkit-keyframes info-anim { 50 | 0% { -webkit-transform: rotateY(120deg); } 51 | 100% { -webkit-transform: rotateY(0deg); } 52 | } 53 | /*@-moz-keyframes info-anim { 54 | 0% { -moz-transform: rotateY(120deg); } 55 | 100% { -moz-transform: rotateY(0deg); } 56 | }*/ 57 | @keyframes info-anim { 58 | 0% { transform: rotateY(120deg); } 59 | 100% { transform: rotateY(0deg); } 60 | } 61 | 62 | .info_btn { 63 | 64 | -webkit-transition: all .5s cubic-bezier(0.23, 1, 0.32, 1); 65 | -moz-transition: all .5s cubic-bezier(0.23, 1, 0.32, 1); 66 | transition: all .5s cubic-bezier(0.23, 1, 0.32, 1); 67 | 68 | -webkit-transform-style: preserve-3d; 69 | /*-moz-transform-style: preserve-3d;*/ 70 | transform-style: preserve-3d; 71 | 72 | -webkit-transform-origin: 0% 50%; 73 | /*-moz-transform-origin: 0% 50%;*/ 74 | transform-origin: 0% 50%; 75 | 76 | -webkit-transform: rotateY(120deg); 77 | /*-moz-transform: rotateY(120deg);*/ 78 | transform: rotateY(120deg); 79 | 80 | -webkit-animation: info-anim 1s cubic-bezier(0.23, 1, 0.32, 1) 0.5s 1 normal forwards; 81 | /*-moz-animation: info-anim 1s cubic-bezier(0.23, 1, 0.32, 1) 0.5s 1 normal forwards;*/ 82 | animation: info-anim 1s cubic-bezier(0.23, 1, 0.32, 1) 0.5s 1 normal forwards; 83 | 84 | position: absolute; 85 | top: 0; 86 | left: 0; 87 | z-index: 100; 88 | display: inline-block; 89 | margin-right: 10px; 90 | background-color: rgb(27,34,32); 91 | color: #ddd; 92 | cursor: pointer; 93 | padding: 18px 24px; 94 | letter-spacing: 2em; 95 | width: 0.3em; 96 | overflow: hidden; 97 | text-align: left; 98 | box-sizing: content-box; 99 | } 100 | 101 | .info_btn:hover { 102 | color: #fff; 103 | box-shadow: none; 104 | letter-spacing: 0; 105 | width: 1.6em; 106 | } 107 | -------------------------------------------------------------------------------- /js/node.js: -------------------------------------------------------------------------------- 1 | var {sample, where, max} = require('underscore'); 2 | var {SPACING} = require('./settings'); 3 | var {TWO_PI} = require('./helpers'); 4 | 5 | const RADIUS = 4; 6 | const COLOR = 'rgba(0, 0, 0, 0.5)'; 7 | 8 | class Node { 9 | constructor(x, y) { 10 | this.x = x; 11 | this.y = y; 12 | this.id = key(x, y); 13 | this.loc = [(x + 1) * SPACING, (y + 1) * SPACING]; 14 | this.edges = []; 15 | this.policy = {}; 16 | } 17 | addEdge(edge) { 18 | this.edges.push(edge); 19 | } 20 | removeEdge(edge) { 21 | var i = this.edges.indexOf(edge); 22 | if (i < 0) { 23 | return; 24 | } 25 | this.edges.splice(i, 1); 26 | } 27 | getNeighbors() { 28 | return this.edges.map((edge) => edge.getOtherNode(this)); 29 | } 30 | selectEdge(smartOpts, smart, endNode) { 31 | if (!smart) { 32 | return sample(this.edges); 33 | } 34 | var {version, initial, explore} = smartOpts; 35 | _ensureDefaults(this.policy, version, endNode, this.edges, initial); 36 | var validActions = this.getValidActions(version, endNode); 37 | if (Math.random() > explore) { 38 | let maxValue = this.getMaxActionValue(smartOpts, endNode); 39 | validActions = where(validActions, {'value': maxValue}); 40 | } 41 | var action = sample(validActions); 42 | return action && action['edge']; 43 | } 44 | getMaxActionValue(smartOpts, endNode) { 45 | var {version, initial} = smartOpts; 46 | _ensureDefaults(this.policy, version, endNode, this.edges, initial); 47 | var validActions = this.getValidActions(version, endNode); 48 | var maxAction = max(validActions, (action) => action.value); 49 | return maxAction['value']; 50 | } 51 | getValidActions(version, endNode) { 52 | var actions = Object.values(this.policy[version][endNode.id]); 53 | return actions.filter((action) => this.edges.includes(action.edge)); 54 | } 55 | getEvaluationCb(smartOpts, endNode, edge) { 56 | var {version, alpha, discount} = smartOpts; 57 | return (reward, curMaxValue) => { 58 | var prevVal = this.policy[version][endNode.id][edge.id]['value']; 59 | var newVal = (1 - discount) * prevVal + alpha * (reward + discount * curMaxValue); 60 | this.policy[version][endNode.id][edge.id]['value'] = newVal; 61 | }; 62 | } 63 | draw(ctx) { 64 | var [x, y] = this.loc; 65 | ctx.beginPath(); 66 | ctx.arc(x, y, RADIUS, 0, TWO_PI); 67 | ctx.fillStyle = COLOR; 68 | ctx.fill(); 69 | } 70 | } 71 | 72 | function key(x, y) { 73 | return x + '|' + y; 74 | } 75 | 76 | function _ensureDefaults(policy, policyVersion, endNode, edges, initial) { 77 | policy[policyVersion] = policy[policyVersion] || {}; 78 | var curPolicy = policy[policyVersion]; 79 | curPolicy[endNode.id] = curPolicy[endNode.id] || {}; 80 | edges.forEach((edge) => { 81 | curPolicy[endNode.id][edge.id] = curPolicy[endNode.id][edge.id] || { 82 | 'value': initial, 83 | 'edge': edge 84 | }; 85 | }); 86 | } 87 | 88 | module.exports = Node; 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rota 2 | ==== 3 | 4 | ![rota](/img/img.png?raw=true "rota") 5 | 6 | Another reinforcement learning adventure! I wrote this on the heels of my [Maze project](https://tbaldw.in/maze) upon realizing that a similar method could be used to route packets in a network. In a maze, there is one start, one end, and usually just one path. In Rota, there are as many starts and ends as there are nodes in the network and an infinite number of paths. 7 | 8 | I figured that one of the interesting properties of the Maze project was that the knowledge and the learning was distributed among the "forks" in the maze. This seemed like an advantage when applied to network routing where a decentralized routing system could hold up in the face of network partitions without losing significant routing knowledge or functionality. 9 | 10 | In Rota, the grid represents a network of nodes and their connections. When you drop packets into the network, they are randomly assigned an address (or destination node) and a node to start from. As a packet is sent from node to node (at first by randomly selecting a neighboring node), the nodes learn which neighbor offers the best probability for getting the packet to its destination in the least amount of time. (Packets turn red as the time they've been in the network increases.) 11 | 12 | Interestingly, the routing knowledge (and the learning itself) is distributed among all the nodes in the system. This means that none of the nodes has a knowledge of the network topology or _even if any of their neighbors is the addressee_. Indeed, the addresses themselves could be obscurred with a hashing function without affecting the routing functionality. 13 | 14 | You can click on the connections between the nodes to add/remove them. The lighter the line, the larger the latency - darker lines are faster connections - and the routing algorithm will optimize for fastest delivery, not necessarily the fewest number of steps. You can click around on the connections, changing the topology of the network, and then watch the nodes adapt to the new network as more and more packets are added. 15 | 16 | Reinforcement learning is essentially based on a trial-and-error methodology, so the more packets a topology has routed, the better it will be at routing future packets. In some topologies, certain nodes will be difficult to reach. By dropping in packets addressed to a specific node (by shift-clicking a node), the nodes in the network will become better and better and routing packets to that node. 17 | 18 | There are a number of improvements I hope to make which haven't made their way into this version of rota. I'd like to make available better metrics, heatmaps for each address, TTLs for packets, and have each connection's traffic affect its latency. This last change would allow the algorithm to be tested for its ability to evenly distribute traffic in the network. 19 | 20 | ------------------------ 21 | 22 | # Requirements 23 | 24 | To simplify deploys, we check in built JS assets. So to run this project yourself, just clone the repo and run from the root: 25 | 26 | npm run serve 27 | 28 | ------------------------ 29 | 30 | You can see this project live at [tbaldw.in/rota](https://tbaldw.in/rota) or check out the code at [github.com/rolyatmax/rota](https://github.com/rolyatmax/rota). 31 | -------------------------------------------------------------------------------- /js/packet.js: -------------------------------------------------------------------------------- 1 | var {uniqueId} = require('underscore'); 2 | var {REDZONE_TIME, LATENCY_RANGE} = require('./settings'); 3 | var {map, TWO_PI, lerp} = require('./helpers'); 4 | 5 | const GREEN = [39, 179, 171]; 6 | const RED = [167, 29, 36]; 7 | const OPACITY = 0.7; 8 | const RADIUS = 6; 9 | const SCORE_RANGE = [20, 180]; 10 | 11 | class Packet { 12 | constructor(startNode, endNode, smartOpts = {}) { 13 | this.smart = !!smartOpts['version']; 14 | this.smartOpts = smartOpts; 15 | this.id = uniqueId('packet-'); 16 | this.startNode = startNode; 17 | this.endNode = endNode; 18 | this.startTime = Date.now(); 19 | this.totalTime = 0; 20 | this.curEdge = null; 21 | this.curNode = startNode; 22 | this.curEdgeStart = 0; 23 | this.evaluationCb = null; 24 | } 25 | // TODO: Refactor meeeeeeee 26 | update() { 27 | var now = Date.now(); 28 | if (!this.curEdgeStart) { 29 | this.curEdgeStart = now; 30 | this.curEdge = this.curNode.selectEdge(this.smartOpts, this.smart, this.endNode); 31 | if (this.smart && this.curEdge) { 32 | this.evaluationCb = this.curNode.getEvaluationCb( 33 | this.smartOpts, 34 | this.endNode, 35 | this.curEdge 36 | ); 37 | } 38 | return; 39 | } 40 | if (!this.curEdge) { 41 | this.curEdge = this.curNode.selectEdge(this.smartOpts, this.smart, this.endNode); 42 | return; 43 | } 44 | if (now < (this.curEdge.latency + this.curEdgeStart)) { 45 | return; 46 | } 47 | this.curNode = this.curEdge.getOtherNode(this.curNode); 48 | if (this.curNode === this.endNode) { 49 | if (this.evaluationCb) { 50 | let completionReward = this.smartOpts['completionReward']; 51 | let maxActionValue = this.curNode.getMaxActionValue(this.smartOpts, this.endNode); 52 | this.evaluationCb(completionReward, maxActionValue); 53 | } 54 | this.totalTime = now - this.startTime; 55 | return; 56 | } 57 | if (this.evaluationCb) { 58 | let score = -1 * (map(this.curEdge.latency, ...LATENCY_RANGE, ...SCORE_RANGE)); 59 | this.evaluationCb(score, this.curNode.getMaxActionValue(this.smartOpts, this.endNode)); 60 | } 61 | this.curEdgeStart = now; 62 | this.curEdge = this.curNode.selectEdge(this.smartOpts, this.smart, this.endNode); 63 | if (this.smart && this.curEdge) { 64 | this.evaluationCb = this.curNode.getEvaluationCb( 65 | this.smartOpts, 66 | this.endNode, 67 | this.curEdge 68 | ); 69 | } 70 | } 71 | draw(ctx) { 72 | var nextNode = this.curEdge ? this.curEdge.getOtherNode(this.curNode) : this.curNode; 73 | var [curX, curY] = this.curNode.loc; 74 | var [nextX, nextY] = nextNode.loc; 75 | var latency = this.curEdge ? this.curEdge.latency : 1; 76 | var now = Date.now(); 77 | var curTime = now - this.curEdgeStart; 78 | var x = easing(curX, nextX, latency, curTime); 79 | var y = easing(curY, nextY, latency, curTime); 80 | 81 | ctx.beginPath(); 82 | ctx.arc(x, y, RADIUS, 0, TWO_PI); 83 | ctx.fillStyle = getColor(now - this.startTime); 84 | ctx.fill(); 85 | } 86 | } 87 | 88 | function getColor(time) { 89 | var perc = time / REDZONE_TIME; 90 | var [r, g, b] = [0, 1, 2].map((i) => lerp(GREEN[i], RED[i], perc) | 0); 91 | return `rgba(${r}, ${g}, ${b}, ${OPACITY})`; 92 | } 93 | 94 | // In/Out Cubic 95 | function easing(start, end, duration, curTime) { 96 | var change = end - start; 97 | curTime /= duration / 2; 98 | if (curTime < 1) { 99 | return change / 2 * curTime * curTime * curTime + start; 100 | } 101 | curTime -= 2; 102 | return change / 2 * (curTime * curTime * curTime + 2) + start; 103 | } 104 | 105 | module.exports = Packet; 106 | -------------------------------------------------------------------------------- /js/network.js: -------------------------------------------------------------------------------- 1 | var {range, min} = require('underscore'); 2 | var Node = require('./node'); 3 | var Edge = require('./edge'); 4 | var {SPACING} = require('./settings'); 5 | var {sqrt, pow, TWO_PI} = require('./helpers'); 6 | 7 | const RADIUS = 12; 8 | const COLOR = 'rgba(96, 57, 193, 0.5)'; // purple 9 | 10 | class Network { 11 | constructor(n) { 12 | this.width = this.height = n; 13 | this.nodes = {}; 14 | this.edges = {}; 15 | var x = this.width; 16 | while (x--) { 17 | var y = this.height; 18 | while (y--) { 19 | var node = new Node(x, y); 20 | this.nodes[node.id] = node; 21 | } 22 | } 23 | this.connectAll(); 24 | } 25 | getNode(x, y) { 26 | return this.nodes[key(x, y)]; 27 | } 28 | getEdge(node1, node2) { 29 | var edge = this.edges[generatePathID(node1, node2)]; 30 | if (!edge) { 31 | throw new Error('Edge not found!'); 32 | } 33 | return edge; 34 | } 35 | findClosestNodes(touchX, touchY, nodeCount) { 36 | var normalized = [touchX, touchY].map((num) => (num / SPACING) - 1); 37 | var [minMaxX, minMaxY] = normalized.map((num) => { 38 | return [Math.floor, Math.ceil].map((fn) => fn(num)); 39 | }); 40 | 41 | var possibleVectors = [ 42 | [minMaxX[0], minMaxY[0]], 43 | [minMaxX[0], minMaxY[1]], 44 | [minMaxX[1], minMaxY[0]], 45 | [minMaxX[1], minMaxY[1]] 46 | ]; 47 | possibleVectors = possibleVectors.filter((vector) => vector[0] >= 0 && vector[1] >= 0); 48 | var [normalX, normalY] = normalized; 49 | var closest = range(nodeCount).map(() => { 50 | var vector = min(possibleVectors, ([x, y]) => { 51 | return sqrt(pow(x - normalX, 2) + pow(y - normalY, 2)); 52 | }); 53 | var i = possibleVectors.indexOf(vector); 54 | possibleVectors.splice(i, 1); 55 | return this.getNode(...vector); 56 | }); 57 | 58 | if (!closest.every(node => node)) { 59 | return console.log('Missing nodes', closest); 60 | } 61 | 62 | return closest; 63 | } 64 | findClosestEdge(touchX, touchY) { 65 | var closest = this.findClosestNodes(touchX, touchY, 2); 66 | return closest ? this.getEdge(...closest) : null; 67 | } 68 | connectAll() { 69 | Object.values(this.nodes).forEach((node) => { 70 | let {x, y} = node; 71 | var edges = node.getNeighbors(); 72 | [ 73 | this.getNode(x + 1, y), 74 | this.getNode(x - 1, y), 75 | this.getNode(x, y + 1), 76 | this.getNode(x, y - 1) 77 | ].map((neighbor) => { 78 | if (!neighbor || edges.indexOf(neighbor) >= 0) { 79 | return; 80 | } 81 | var id = generatePathID(node, neighbor); 82 | var edge = new Edge(node, neighbor, id); 83 | node.addEdge(edge); 84 | neighbor.addEdge(edge); 85 | this.edges[edge.id] = edge; 86 | }); 87 | }); 88 | } 89 | draw(ctx) { 90 | Object.values(this.nodes).forEach((node) => node.draw(ctx)); 91 | Object.values(this.edges).forEach((edge) => edge.draw(ctx)); 92 | if (ctx.keys['SHIFT']) { 93 | let nodes = this.findClosestNodes(ctx.mouse.x, ctx.mouse.y, 1); 94 | if (nodes && nodes.length) { 95 | let [x, y] = nodes[0].loc; 96 | ctx.beginPath(); 97 | ctx.arc(x, y, RADIUS, 0, TWO_PI); 98 | ctx.fillStyle = COLOR; 99 | ctx.fill(); 100 | } 101 | } 102 | } 103 | } 104 | 105 | function key(x, y) { 106 | return `${x}|${y}`; 107 | } 108 | 109 | // deterministic way to generate ids based on a 2-node combo 110 | function generatePathID(node1, node2) { 111 | var id1 = node1.id; 112 | var id2 = node2.id; 113 | return id1 < id2 ? `${id1}-${id2}` : `${id2}-${id1}`; 114 | } 115 | 116 | module.exports = Network; 117 | -------------------------------------------------------------------------------- /js/lib/info.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | /* 3 | Author: Taylor Baldwin (http://tbaldw.in) 4 | 5 | This project is meant to make it easy to add in an info panel 6 | on projects. If using Markdown, the Markdown.Converter.js file 7 | is required as a dependency (https://code.google.com/p/pagedown/wiki/PageDown). 8 | 9 | In the options object, you must include a container property which may be either 10 | a string or an HTML element. If you specify an 'el' property (which is to be the 11 | info element), it should reside outside of the container element. 12 | */ 13 | 14 | 15 | (function() { 16 | var Info, 17 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 18 | 19 | Info = (function() { 20 | function Info(opts) { 21 | this.toggleInfo = __bind(this.toggleInfo, this); 22 | this.closeInfo = __bind(this.closeInfo, this); 23 | this.openInfo = __bind(this.openInfo, this); 24 | var MDConverter, arry, request, url, 25 | _this = this; 26 | 27 | this.opts = opts; 28 | this.el = opts.el || "info"; 29 | this.btn = opts.btn || "info_btn"; 30 | this.container = opts.container || "container"; 31 | this.text = opts.text || null; 32 | this.isMarkdown = opts.isMarkdown || false; 33 | this.html = opts.html || null; 34 | this.keyTrigger = opts.keyTrigger || false; 35 | this.isOpen = false; 36 | if (typeof this.el === "string") { 37 | this.el = document.getElementById(this.el); 38 | } 39 | if (typeof this.container === "string") { 40 | this.container = document.getElementById(this.container); 41 | } 42 | if (typeof this.btn === "string") { 43 | this.btn = document.getElementById(this.btn); 44 | } 45 | if (this.el == null) { 46 | this.createDiv(); 47 | } 48 | if (this.btn == null) { 49 | this.createButton(); 50 | } 51 | this.container.className += " content_wrapper"; 52 | this.el.className += " info_container"; 53 | this.btn.className += " info_btn"; 54 | if ((opts.text == null) && (this.html == null) && (opts.url != null)) { 55 | url = opts.url; 56 | arry = url.split("."); 57 | if (arry[arry.length - 1] === "md") { 58 | this.isMarkdown = true; 59 | } 60 | request = new XMLHttpRequest(); 61 | request.open("GET", url, true); 62 | request.responseType = "text"; 63 | request.onload = function() { 64 | _this.text = request.response; 65 | return _this.setup(); 66 | }; 67 | request.send(); 68 | this.loadingFromFile = true; 69 | } 70 | if (this.isMarkdown) { 71 | MDConverter = window.Markdown.Converter || window.pagedown.Converter; 72 | this.converter = new MDConverter(); 73 | } 74 | if (this.loadingFromFile !== true) { 75 | this.setup(); 76 | } 77 | } 78 | 79 | Info.prototype.setup = function() { 80 | if (this.html == null) { 81 | this.html = this.text; 82 | } 83 | if (this.isMarkdown) { 84 | this.html = this.converter.makeHtml(this.text); 85 | } 86 | if (this.html != null) { 87 | this.el.innerHTML = this.html; 88 | } 89 | return this.attachEvents(); 90 | }; 91 | 92 | Info.prototype.createDiv = function() { 93 | this.el = document.createElement('div'); 94 | this.el.id = "info"; 95 | return document.body.appendChild(this.el); 96 | }; 97 | 98 | Info.prototype.createButton = function() { 99 | this.btn = document.createElement('div'); 100 | this.btn.id = "info_btn"; 101 | this.btn.innerHTML = "info"; 102 | return this.container.appendChild(this.btn); 103 | }; 104 | 105 | Info.prototype.attachEvents = function() { 106 | var _this = this; 107 | 108 | this.btn.addEventListener("click", this.toggleInfo); 109 | if (this.keyTrigger) { 110 | return document.addEventListener("keyup", function(e) { 111 | if (e.which === 73) { 112 | return _this.toggleInfo(); 113 | } 114 | }); 115 | } 116 | }; 117 | 118 | Info.prototype.openInfo = function() { 119 | this.el.className += " open"; 120 | this.container.className += " inactive"; 121 | return this.isOpen = true; 122 | }; 123 | 124 | Info.prototype.closeInfo = function() { 125 | this.el.className = this.el.className.replace("open", ""); 126 | this.container.className = this.container.className.replace("inactive", ""); 127 | return this.isOpen = false; 128 | }; 129 | 130 | Info.prototype.toggleInfo = function() { 131 | if (!this.isOpen) { 132 | return this.openInfo(); 133 | } else { 134 | return this.closeInfo(); 135 | } 136 | }; 137 | 138 | return Info; 139 | 140 | })(); 141 | 142 | if (module && module.exports) { 143 | module.exports = Info; 144 | } else { 145 | window.Info = Info; 146 | } 147 | 148 | })(); 149 | -------------------------------------------------------------------------------- /js/stats.js: -------------------------------------------------------------------------------- 1 | var {groupBy} = require('underscore'); 2 | var template = require('./stats.hbs'); 3 | var {max, min} = Math; 4 | var {TWO_PI} = require('./helpers'); 5 | 6 | const RENDER_RATE = 500; 7 | const RADIUS = 14; 8 | const MAX_OVERLAY_OPACITY = 0.6; 9 | const OVERLAY_COLOR = [253, 110, 0]; // orange 10 | 11 | class Stats { 12 | constructor(el, packets, ctx) { 13 | this.el = document.querySelector(el); 14 | this.statsContainer = this.el.querySelector('.stats'); 15 | this.packets = packets; 16 | this.ctx = ctx; 17 | this.timeout = null; 18 | this.showOverlay = false; 19 | this.resetStats(); 20 | this.allTimeTotal = 0; 21 | this.allTimeCompleted = 0; 22 | this.poll(); 23 | } 24 | poll() { 25 | this.timeout = setTimeout(() => this.poll(), RENDER_RATE); 26 | this.render(); 27 | } 28 | render() { 29 | var now = Date.now(); 30 | var inFlightAggregateTimes = this.packets.inFlight.reduce((total, packet) => { 31 | return total + (now - packet.startTime); 32 | }, 0); 33 | 34 | var aggregateTimes = inFlightAggregateTimes + this.aggregateTimes; 35 | var ctx = { 36 | 'total': this.totalCount, 37 | 'inFlight': this.allTimeTotal - this.allTimeCompleted, 38 | 'delivered': this.completedCount, 39 | 'averageTime': (aggregateTimes / this.totalCount) | 0, 40 | 'rate': this.packets.rate, 41 | ...this.comparePaths() 42 | }; 43 | this.statsContainer.innerHTML = template(ctx); 44 | } 45 | resetStats() { 46 | this.totalCount = 0; 47 | this.completedCount = 0; 48 | this.finished = new Set(); 49 | this.aggregateTimes = 0; 50 | this.pathStats = {}; 51 | this.render(); 52 | } 53 | logFinish(packet) { 54 | this.completedCount += 1; 55 | this.allTimeCompleted += 1; 56 | this.aggregateTimes += packet.totalTime; 57 | this.finished.add(packet); 58 | 59 | var {startNode, endNode} = packet; 60 | 61 | var pathKey = `${startNode}-${endNode}`; 62 | _ensureDefaults(this.pathStats, pathKey); 63 | 64 | var pathStat = this.pathStats[pathKey]; 65 | pathStat['totalCount'] += 1; 66 | pathStat['aggregateTimes'] += packet.totalTime; 67 | pathStat['averageTime'] = (pathStat['aggregateTimes'] / pathStat['totalCount']) | 0; 68 | if (!pathStat['bestTime'] || packet.totalTime < pathStat['bestTime']) { 69 | pathStat['bestTime'] = packet.totalTime; 70 | } 71 | if (!pathStat['worstTime'] || packet.totalTime > pathStat['worstTime']) { 72 | pathStat['worstTime'] = packet.totalTime; 73 | } 74 | } 75 | comparePaths() { 76 | // average count of each path run 77 | var paths = Object.values(this.pathStats); 78 | 79 | var pathCount = paths.length; 80 | var totalCount = paths.reduce((total, pathStat) => total + pathStat.totalCount, 0); 81 | var averagePathCount = Math.round(totalCount / pathCount); 82 | 83 | // average best 84 | var totalBest = paths.reduce((total, pathStat) => total + pathStat.bestTime, 0); 85 | var averageBest = Math.round(totalBest / pathCount); 86 | 87 | // average worst 88 | var totalWorst = paths.reduce((total, pathStat) => total + pathStat.worstTime, 0); 89 | var averageWorst = Math.round(totalWorst / pathCount); 90 | 91 | // average difference between best and actual 92 | var totalBestActualDiff = paths.reduce((total, pathStat) => { 93 | return total + pathStat.averageTime - pathStat.bestTime; 94 | }, 0); 95 | var averageBestActualDifference = Math.round(totalBestActualDiff / pathCount); 96 | 97 | // average difference between best and worst 98 | var totalBestWorstDiff = paths.reduce((memo, pathStat) => { 99 | return memo + pathStat.worstTime - pathStat.bestTime; 100 | }, 0); 101 | var averageBestWorstDifference = Math.round(totalBestWorstDiff / pathCount); 102 | 103 | // average difference between best and average 104 | var totalBestActualDiffRatio = paths.reduce((memo, pathStat) => { 105 | return memo + pathStat.averageTime / pathStat.bestTime; 106 | }, 0); 107 | var averageBestActualRatio = (((totalBestActualDiffRatio / pathCount) * 100) | 0) / 100; 108 | return { 109 | pathCount, 110 | averagePathCount, 111 | averageBest, 112 | averageWorst, 113 | averageBestActualDifference, 114 | averageBestActualRatio, 115 | averageBestWorstDifference 116 | }; 117 | } 118 | showAddressOverlay() { 119 | if (!this.packets.inFlight.length) { 120 | return; 121 | } 122 | var endNodes = this.packets.inFlight.map((packet) => packet.endNode); 123 | var groups = groupBy(endNodes, 'id'); 124 | var addresses = Object.values(groups).map((group) => ({ 125 | 'node': group[0], 126 | 'count': group.length 127 | })); 128 | this.draw(this.ctx, addresses); 129 | } 130 | draw(ctx, addresses) { 131 | addresses.forEach((address) => { 132 | var [x, y] = address.node.loc; 133 | ctx.beginPath(); 134 | ctx.arc(x, y, RADIUS, 0, TWO_PI); 135 | ctx.fillStyle = getColor(address.count); 136 | ctx.fill(); 137 | }); 138 | } 139 | } 140 | 141 | function getColor(count) { 142 | var opacity = max(0.3, min(count / 20, 1)) * MAX_OVERLAY_OPACITY; 143 | var [r, g, b] = OVERLAY_COLOR; 144 | return `rgba(${r}, ${g}, ${b}, ${opacity})`; 145 | } 146 | 147 | function _ensureDefaults(pathStats, pathKey) { 148 | pathStats[pathKey] = pathStats[pathKey] || { 149 | 'totalCount': 0, 150 | 'aggregateTimes': 0, 151 | 'bestTime': null, 152 | 'worstTime': null, 153 | 'averageTime': null 154 | }; 155 | } 156 | 157 | module.exports = Stats; 158 | -------------------------------------------------------------------------------- /js/lib/sketch.js: -------------------------------------------------------------------------------- 1 | 2 | /* Copyright (C) 2013 Justin Windle, http://soulwire.co.uk */ 3 | 4 | (function ( root, factory ) { 5 | 6 | if ( typeof exports === 'object' ) { 7 | 8 | // CommonJS like 9 | module.exports = factory(root, root.document); 10 | 11 | } else if ( typeof define === 'function' && define.amd ) { 12 | 13 | // AMD 14 | define( function() { return factory( root, root.document ); }); 15 | 16 | } else { 17 | 18 | // Browser global 19 | root.Sketch = factory( root, root.document ); 20 | } 21 | 22 | }( window, function ( window, document ) { 23 | 24 | "use strict"; 25 | 26 | /* 27 | ---------------------------------------------------------------------- 28 | 29 | Config 30 | 31 | ---------------------------------------------------------------------- 32 | */ 33 | 34 | var MATH_PROPS = 'E LN10 LN2 LOG2E LOG10E PI SQRT1_2 SQRT2 abs acos asin atan ceil cos exp floor log round sin sqrt tan atan2 pow max min'.split( ' ' ); 35 | var HAS_SKETCH = '__hasSketch'; 36 | var M = Math; 37 | 38 | var CANVAS = 'canvas'; 39 | var WEBGL = 'webgl'; 40 | var DOM = 'dom'; 41 | 42 | var doc = document; 43 | var win = window; 44 | 45 | var instances = []; 46 | 47 | var defaults = { 48 | 49 | fullscreen: true, 50 | autostart: true, 51 | autoclear: true, 52 | autopause: true, 53 | container: doc.body, 54 | interval: 1, 55 | globals: true, 56 | retina: false, 57 | type: CANVAS 58 | }; 59 | 60 | var keyMap = { 61 | 62 | 8: 'BACKSPACE', 63 | 9: 'TAB', 64 | 13: 'ENTER', 65 | 16: 'SHIFT', 66 | 27: 'ESCAPE', 67 | 32: 'SPACE', 68 | 37: 'LEFT', 69 | 38: 'UP', 70 | 39: 'RIGHT', 71 | 40: 'DOWN' 72 | }; 73 | 74 | /* 75 | ---------------------------------------------------------------------- 76 | 77 | Utilities 78 | 79 | ---------------------------------------------------------------------- 80 | */ 81 | 82 | function isArray( object ) { 83 | 84 | return Object.prototype.toString.call( object ) == '[object Array]'; 85 | } 86 | 87 | function isFunction( object ) { 88 | 89 | return typeof object == 'function'; 90 | } 91 | 92 | function isNumber( object ) { 93 | 94 | return typeof object == 'number'; 95 | } 96 | 97 | function isString( object ) { 98 | 99 | return typeof object == 'string'; 100 | } 101 | 102 | function keyName( code ) { 103 | 104 | return keyMap[ code ] || String.fromCharCode( code ); 105 | } 106 | 107 | function extend( target, source, overwrite ) { 108 | 109 | for ( var key in source ) 110 | 111 | if ( overwrite || !( key in target ) ) 112 | 113 | target[ key ] = source[ key ]; 114 | 115 | return target; 116 | } 117 | 118 | function proxy( method, context ) { 119 | 120 | return function() { 121 | 122 | method.apply( context, arguments ); 123 | }; 124 | } 125 | 126 | function clone( target ) { 127 | 128 | var object = {}; 129 | 130 | for ( var key in target ) { 131 | 132 | if ( isFunction( target[ key ] ) ) 133 | 134 | object[ key ] = proxy( target[ key ], target ); 135 | 136 | else 137 | 138 | object[ key ] = target[ key ]; 139 | } 140 | 141 | return object; 142 | } 143 | 144 | /* 145 | ---------------------------------------------------------------------- 146 | 147 | Constructor 148 | 149 | ---------------------------------------------------------------------- 150 | */ 151 | 152 | function constructor( context ) { 153 | 154 | var request, handler, target, parent, bounds, index, suffix, clock, node, copy, type, key, val, min, max, w, h; 155 | 156 | var counter = 0; 157 | var touches = []; 158 | var resized = false; 159 | var setup = false; 160 | var ratio = win.devicePixelRatio || 1; 161 | var isDiv = context.type == DOM; 162 | var is2D = context.type == CANVAS; 163 | 164 | var mouse = { 165 | x: 0.0, y: 0.0, 166 | ox: 0.0, oy: 0.0, 167 | dx: 0.0, dy: 0.0 168 | }; 169 | 170 | var eventMap = [ 171 | 172 | context.element, 173 | 174 | pointer, 'mousedown', 'touchstart', 175 | pointer, 'mousemove', 'touchmove', 176 | pointer, 'mouseup', 'touchend', 177 | pointer, 'click', 178 | pointer, 'mouseout', 179 | pointer, 'mouseover', 180 | 181 | doc, 182 | 183 | keypress, 'keydown', 'keyup', 184 | 185 | win, 186 | 187 | active, 'focus', 'blur', 188 | resize, 'resize' 189 | ]; 190 | 191 | var keys = {}; for ( key in keyMap ) keys[ keyMap[ key ] ] = false; 192 | 193 | function trigger( method ) { 194 | 195 | if ( isFunction( method ) ) 196 | 197 | method.apply( context, [].splice.call( arguments, 1 ) ); 198 | } 199 | 200 | function bind( on ) { 201 | 202 | for ( index = 0; index < eventMap.length; index++ ) { 203 | 204 | node = eventMap[ index ]; 205 | 206 | if ( isString( node ) ) 207 | 208 | target[ ( on ? 'add' : 'remove' ) + 'EventListener' ].call( target, node, handler, false ); 209 | 210 | else if ( isFunction( node ) ) 211 | 212 | handler = node; 213 | 214 | else target = node; 215 | } 216 | } 217 | 218 | function update() { 219 | 220 | cAF( request ); 221 | request = rAF( update ); 222 | 223 | if ( !setup ) { 224 | 225 | trigger( context.setup ); 226 | setup = isFunction( context.setup ); 227 | } 228 | 229 | if ( !resized ) { 230 | trigger( context.resize ); 231 | resized = isFunction( context.resize ); 232 | } 233 | 234 | if ( context.running && !counter ) { 235 | 236 | context.dt = ( clock = +new Date() ) - context.now; 237 | context.millis += context.dt; 238 | context.now = clock; 239 | 240 | trigger( context.update ); 241 | 242 | // Pre draw 243 | 244 | if ( is2D ) { 245 | 246 | if ( context.retina ) { 247 | 248 | context.save(); 249 | context.scale( ratio, ratio ); 250 | } 251 | 252 | if ( context.autoclear ) 253 | 254 | context.clear(); 255 | } 256 | 257 | // Draw 258 | 259 | trigger( context.draw ); 260 | 261 | // Post draw 262 | 263 | if ( is2D && context.retina ) 264 | 265 | context.restore(); 266 | } 267 | 268 | counter = ++counter % context.interval; 269 | } 270 | 271 | function resize() { 272 | 273 | target = isDiv ? context.style : context.canvas; 274 | suffix = isDiv ? 'px' : ''; 275 | 276 | w = context.width; 277 | h = context.height; 278 | 279 | if ( context.fullscreen ) { 280 | 281 | h = context.height = win.innerHeight; 282 | w = context.width = win.innerWidth; 283 | } 284 | 285 | if ( context.retina && is2D && ratio ) { 286 | 287 | target.style.height = h + 'px'; 288 | target.style.width = w + 'px'; 289 | 290 | w *= ratio; 291 | h *= ratio; 292 | } 293 | 294 | if ( target.height !== h ) 295 | 296 | target.height = h + suffix; 297 | 298 | if ( target.width !== w ) 299 | 300 | target.width = w + suffix; 301 | 302 | if ( setup ) trigger( context.resize ); 303 | } 304 | 305 | function align( touch, target ) { 306 | 307 | bounds = target.getBoundingClientRect(); 308 | 309 | touch.x = touch.pageX - bounds.left - (win.scrollX || win.pageXOffset); 310 | touch.y = touch.pageY - bounds.top - (win.scrollY || win.pageYOffset); 311 | 312 | return touch; 313 | } 314 | 315 | function augment( touch, target ) { 316 | 317 | align( touch, context.element ); 318 | 319 | target = target || {}; 320 | 321 | target.ox = target.x || touch.x; 322 | target.oy = target.y || touch.y; 323 | 324 | target.x = touch.x; 325 | target.y = touch.y; 326 | 327 | target.dx = target.x - target.ox; 328 | target.dy = target.y - target.oy; 329 | 330 | return target; 331 | } 332 | 333 | function process( event ) { 334 | 335 | event.preventDefault(); 336 | 337 | copy = clone( event ); 338 | copy.originalEvent = event; 339 | 340 | if ( copy.touches ) { 341 | 342 | touches.length = copy.touches.length; 343 | 344 | for ( index = 0; index < copy.touches.length; index++ ) 345 | 346 | touches[ index ] = augment( copy.touches[ index ], touches[ index ] ); 347 | 348 | } else { 349 | 350 | touches.length = 0; 351 | touches[0] = augment( copy, mouse ); 352 | } 353 | 354 | extend( mouse, touches[0], true ); 355 | 356 | return copy; 357 | } 358 | 359 | function pointer( event ) { 360 | 361 | event = process( event ); 362 | 363 | min = ( max = eventMap.indexOf( type = event.type ) ) - 1; 364 | 365 | context.dragging = 366 | 367 | /down|start/.test( type ) ? true : 368 | 369 | /up|end/.test( type ) ? false : 370 | 371 | context.dragging; 372 | 373 | while( min ) 374 | 375 | isString( eventMap[ min ] ) ? 376 | 377 | trigger( context[ eventMap[ min-- ] ], event ) : 378 | 379 | isString( eventMap[ max ] ) ? 380 | 381 | trigger( context[ eventMap[ max++ ] ], event ) : 382 | 383 | min = 0; 384 | } 385 | 386 | function keypress( event ) { 387 | 388 | key = event.keyCode; 389 | val = event.type == 'keyup'; 390 | keys[ key ] = keys[ keyName( key ) ] = !val; 391 | 392 | trigger( context[ event.type ], event ); 393 | } 394 | 395 | function active( event ) { 396 | 397 | if ( context.autopause ) 398 | 399 | ( event.type == 'blur' ? stop : start )(); 400 | 401 | trigger( context[ event.type ], event ); 402 | } 403 | 404 | // Public API 405 | 406 | function start() { 407 | 408 | context.now = +new Date(); 409 | context.running = true; 410 | } 411 | 412 | function stop() { 413 | 414 | context.running = false; 415 | } 416 | 417 | function toggle() { 418 | 419 | ( context.running ? stop : start )(); 420 | } 421 | 422 | function clear() { 423 | 424 | if ( is2D ) 425 | 426 | context.clearRect( 0, 0, context.width, context.height ); 427 | } 428 | 429 | function destroy() { 430 | 431 | parent = context.element.parentNode; 432 | index = instances.indexOf( context ); 433 | 434 | if ( parent ) parent.removeChild( context.element ); 435 | if ( ~index ) instances.splice( index, 1 ); 436 | 437 | bind( false ); 438 | stop(); 439 | } 440 | 441 | extend( context, { 442 | 443 | touches: touches, 444 | mouse: mouse, 445 | keys: keys, 446 | 447 | dragging: false, 448 | running: false, 449 | millis: 0, 450 | now: NaN, 451 | dt: NaN, 452 | 453 | destroy: destroy, 454 | toggle: toggle, 455 | clear: clear, 456 | start: start, 457 | stop: stop 458 | }); 459 | 460 | instances.push( context ); 461 | 462 | return ( context.autostart && start(), bind( true ), resize(), update(), context ); 463 | } 464 | 465 | /* 466 | ---------------------------------------------------------------------- 467 | 468 | Global API 469 | 470 | ---------------------------------------------------------------------- 471 | */ 472 | 473 | var element, context, Sketch = { 474 | 475 | CANVAS: CANVAS, 476 | WEB_GL: WEBGL, 477 | WEBGL: WEBGL, 478 | DOM: DOM, 479 | 480 | instances: instances, 481 | 482 | install: function( context ) { 483 | 484 | if ( !context[ HAS_SKETCH ] ) { 485 | 486 | for ( var i = 0; i < MATH_PROPS.length; i++ ) 487 | 488 | context[ MATH_PROPS[i] ] = M[ MATH_PROPS[i] ]; 489 | 490 | extend( context, { 491 | 492 | TWO_PI: M.PI * 2, 493 | HALF_PI: M.PI / 2, 494 | QUATER_PI: M.PI / 4, 495 | 496 | random: function( min, max ) { 497 | 498 | if ( isArray( min ) ) 499 | 500 | return min[ ~~( M.random() * min.length ) ]; 501 | 502 | if ( !isNumber( max ) ) 503 | 504 | max = min || 1, min = 0; 505 | 506 | return min + M.random() * ( max - min ); 507 | }, 508 | 509 | lerp: function( min, max, amount ) { 510 | 511 | return min + amount * ( max - min ); 512 | }, 513 | 514 | map: function( num, minA, maxA, minB, maxB ) { 515 | 516 | return ( num - minA ) / ( maxA - minA ) * ( maxB - minB ) + minB; 517 | } 518 | }); 519 | 520 | context[ HAS_SKETCH ] = true; 521 | } 522 | }, 523 | 524 | create: function( options ) { 525 | 526 | options = extend( options || {}, defaults ); 527 | 528 | if ( options.globals ) Sketch.install( self ); 529 | 530 | element = options.element = options.element || doc.createElement( options.type === DOM ? 'div' : 'canvas' ); 531 | 532 | context = options.context = options.context || (function() { 533 | 534 | switch( options.type ) { 535 | 536 | case CANVAS: 537 | 538 | return element.getContext( '2d', options ); 539 | 540 | case WEBGL: 541 | 542 | return element.getContext( 'webgl', options ) || element.getContext( 'experimental-webgl', options ); 543 | 544 | case DOM: 545 | 546 | return element.canvas = element; 547 | } 548 | 549 | })(); 550 | 551 | ( options.container || doc.body ).appendChild( element ); 552 | 553 | return Sketch.augment( context, options ); 554 | }, 555 | 556 | augment: function( context, options ) { 557 | 558 | options = extend( options || {}, defaults ); 559 | 560 | options.element = context.canvas || context; 561 | options.element.className += ' sketch'; 562 | 563 | extend( context, options, true ); 564 | 565 | return constructor( context ); 566 | } 567 | }; 568 | 569 | /* 570 | ---------------------------------------------------------------------- 571 | 572 | Shims 573 | 574 | ---------------------------------------------------------------------- 575 | */ 576 | 577 | var vendors = [ 'ms', 'moz', 'webkit', 'o' ]; 578 | var scope = self; 579 | var then = 0; 580 | 581 | var a = 'AnimationFrame'; 582 | var b = 'request' + a; 583 | var c = 'cancel' + a; 584 | 585 | var rAF = scope[ b ]; 586 | var cAF = scope[ c ]; 587 | 588 | for ( var i = 0; i < vendors.length && !rAF; i++ ) { 589 | 590 | rAF = scope[ vendors[ i ] + 'Request' + a ]; 591 | cAF = scope[ vendors[ i ] + 'Cancel' + a ]; 592 | } 593 | 594 | scope[ b ] = rAF = rAF || function( callback ) { 595 | 596 | var now = +new Date(); 597 | var dt = M.max( 0, 16 - ( now - then ) ); 598 | var id = setTimeout( function() { 599 | callback( now + dt ); 600 | }, dt ); 601 | 602 | then = now + dt; 603 | return id; 604 | }; 605 | 606 | scope[ c ] = cAF = cAF || function( id ) { 607 | clearTimeout( id ); 608 | }; 609 | 610 | /* 611 | ---------------------------------------------------------------------- 612 | 613 | Output 614 | 615 | ---------------------------------------------------------------------- 616 | */ 617 | 618 | return Sketch; 619 | 620 | })); 621 | -------------------------------------------------------------------------------- /js/lib/Markdown.Converter.js: -------------------------------------------------------------------------------- 1 | var Markdown = window.Markdown = {}; 2 | 3 | // The following text is included for historical reasons, but should 4 | // be taken with a pinch of salt; it's not all true anymore. 5 | 6 | // 7 | // Wherever possible, Showdown is a straight, line-by-line port 8 | // of the Perl version of Markdown. 9 | // 10 | // This is not a normal parser design; it's basically just a 11 | // series of string substitutions. It's hard to read and 12 | // maintain this way, but keeping Showdown close to the original 13 | // design makes it easier to port new features. 14 | // 15 | // More importantly, Showdown behaves like markdown.pl in most 16 | // edge cases. So web applications can do client-side preview 17 | // in Javascript, and then build identical HTML on the server. 18 | // 19 | // This port needs the new RegExp functionality of ECMA 262, 20 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 21 | // should do fine. Even with the new regular expression features, 22 | // We do a lot of work to emulate Perl's regex functionality. 23 | // The tricky changes in this file mostly have the "attacklab:" 24 | // label. Major or self-explanatory changes don't. 25 | // 26 | // Smart diff tools like Araxis Merge will be able to match up 27 | // this file with markdown.pl in a useful way. A little tweaking 28 | // helps: in a copy of markdown.pl, replace "#" with "//" and 29 | // replace "$text" with "text". Be sure to ignore whitespace 30 | // and line endings. 31 | // 32 | 33 | 34 | // 35 | // Usage: 36 | // 37 | // var text = "Markdown *rocks*."; 38 | // 39 | // var converter = new Markdown.Converter(); 40 | // var html = converter.makeHtml(text); 41 | // 42 | // alert(html); 43 | // 44 | // Note: move the sample code to the bottom of this 45 | // file before uncommenting it. 46 | // 47 | 48 | (function () { 49 | 50 | function identity(x) { return x; } 51 | function returnFalse(x) { return false; } 52 | 53 | function HookCollection() { } 54 | 55 | HookCollection.prototype = { 56 | 57 | chain: function (hookname, func) { 58 | var original = this[hookname]; 59 | if (!original) 60 | throw new Error("unknown hook " + hookname); 61 | 62 | if (original === identity) 63 | this[hookname] = func; 64 | else 65 | this[hookname] = function (text) { 66 | var args = Array.prototype.slice.call(arguments, 0); 67 | args[0] = original.apply(null, args); 68 | return func.apply(null, args); 69 | }; 70 | }, 71 | set: function (hookname, func) { 72 | if (!this[hookname]) 73 | throw new Error("unknown hook " + hookname); 74 | this[hookname] = func; 75 | }, 76 | addNoop: function (hookname) { 77 | this[hookname] = identity; 78 | }, 79 | addFalse: function (hookname) { 80 | this[hookname] = returnFalse; 81 | } 82 | }; 83 | 84 | Markdown.HookCollection = HookCollection; 85 | 86 | // g_urls and g_titles allow arbitrary user-entered strings as keys. This 87 | // caused an exception (and hence stopped the rendering) when the user entered 88 | // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this 89 | // (since no builtin property starts with "s_"). See 90 | // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug 91 | // (granted, switching from Array() to Object() alone would have left only __proto__ 92 | // to be a problem) 93 | function SaveHash() { } 94 | SaveHash.prototype = { 95 | set: function (key, value) { 96 | this["s_" + key] = value; 97 | }, 98 | get: function (key) { 99 | return this["s_" + key]; 100 | } 101 | }; 102 | 103 | Markdown.Converter = function () { 104 | var pluginHooks = this.hooks = new HookCollection(); 105 | 106 | // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link 107 | pluginHooks.addNoop("plainLinkText"); 108 | 109 | // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked 110 | pluginHooks.addNoop("preConversion"); 111 | 112 | // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have 113 | pluginHooks.addNoop("postNormalization"); 114 | 115 | // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively 116 | // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner 117 | // call will receive outdented text. 118 | pluginHooks.addNoop("preBlockGamut"); 119 | pluginHooks.addNoop("postBlockGamut"); 120 | 121 | // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made 122 | pluginHooks.addNoop("preSpanGamut"); 123 | pluginHooks.addNoop("postSpanGamut"); 124 | 125 | // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml 126 | pluginHooks.addNoop("postConversion"); 127 | 128 | // 129 | // Private state of the converter instance: 130 | // 131 | 132 | // Global hashes, used by various utility routines 133 | var g_urls; 134 | var g_titles; 135 | var g_html_blocks; 136 | 137 | // Used to track when we're inside an ordered or unordered list 138 | // (see _ProcessListItems() for details): 139 | var g_list_level; 140 | 141 | this.makeHtml = function (text) { 142 | 143 | // 144 | // Main function. The order in which other subs are called here is 145 | // essential. Link and image substitutions need to happen before 146 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 147 | // and tags get encoded. 148 | // 149 | 150 | // This will only happen if makeHtml on the same converter instance is called from a plugin hook. 151 | // Don't do that. 152 | if (g_urls) 153 | throw new Error("Recursive call to converter.makeHtml"); 154 | 155 | // Create the private state objects. 156 | g_urls = new SaveHash(); 157 | g_titles = new SaveHash(); 158 | g_html_blocks = []; 159 | g_list_level = 0; 160 | 161 | text = pluginHooks.preConversion(text); 162 | 163 | // attacklab: Replace ~ with ~T 164 | // This lets us use tilde as an escape char to avoid md5 hashes 165 | // The choice of character is arbitray; anything that isn't 166 | // magic in Markdown will work. 167 | text = text.replace(/~/g, "~T"); 168 | 169 | // attacklab: Replace $ with ~D 170 | // RegExp interprets $ as a special character 171 | // when it's in a replacement string 172 | text = text.replace(/\$/g, "~D"); 173 | 174 | // Standardize line endings 175 | text = text.replace(/\r\n/g, "\n"); // DOS to Unix 176 | text = text.replace(/\r/g, "\n"); // Mac to Unix 177 | 178 | // Make sure text begins and ends with a couple of newlines: 179 | text = "\n\n" + text + "\n\n"; 180 | 181 | // Convert all tabs to spaces. 182 | text = _Detab(text); 183 | 184 | // Strip any lines consisting only of spaces and tabs. 185 | // This makes subsequent regexen easier to write, because we can 186 | // match consecutive blank lines with /\n+/ instead of something 187 | // contorted like /[ \t]*\n+/ . 188 | text = text.replace(/^[ \t]+$/mg, ""); 189 | 190 | text = pluginHooks.postNormalization(text); 191 | 192 | // Turn block-level HTML blocks into hash entries 193 | text = _HashHTMLBlocks(text); 194 | 195 | // Strip link definitions, store in hashes. 196 | text = _StripLinkDefinitions(text); 197 | 198 | text = _RunBlockGamut(text); 199 | 200 | text = _UnescapeSpecialChars(text); 201 | 202 | // attacklab: Restore dollar signs 203 | text = text.replace(/~D/g, "$$"); 204 | 205 | // attacklab: Restore tildes 206 | text = text.replace(/~T/g, "~"); 207 | 208 | text = pluginHooks.postConversion(text); 209 | 210 | g_html_blocks = g_titles = g_urls = null; 211 | 212 | return text; 213 | }; 214 | 215 | function _StripLinkDefinitions(text) { 216 | // 217 | // Strips link definitions from text, stores the URLs and titles in 218 | // hash references. 219 | // 220 | 221 | // Link defs are in the form: ^[id]: url "optional title" 222 | 223 | /* 224 | text = text.replace(/ 225 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 226 | [ \t]* 227 | \n? // maybe *one* newline 228 | [ \t]* 229 | ? // url = $2 230 | (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below 231 | [ \t]* 232 | \n? // maybe one newline 233 | [ \t]* 234 | ( // (potential) title = $3 235 | (\n*) // any lines skipped = $4 attacklab: lookbehind removed 236 | [ \t]+ 237 | ["(] 238 | (.+?) // title = $5 239 | [")] 240 | [ \t]* 241 | )? // title is optional 242 | (?:\n+|$) 243 | /gm, function(){...}); 244 | */ 245 | 246 | text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, 247 | function (wholeMatch, m1, m2, m3, m4, m5) { 248 | m1 = m1.toLowerCase(); 249 | g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive 250 | if (m4) { 251 | // Oops, found blank lines, so it's not a title. 252 | // Put back the parenthetical statement we stole. 253 | return m3; 254 | } else if (m5) { 255 | g_titles.set(m1, m5.replace(/"/g, """)); 256 | } 257 | 258 | // Completely remove the definition from the text 259 | return ""; 260 | } 261 | ); 262 | 263 | return text; 264 | } 265 | 266 | function _HashHTMLBlocks(text) { 267 | 268 | // Hashify HTML blocks: 269 | // We only want to do this for block-level HTML tags, such as headers, 270 | // lists, and tables. That's because we still want to wrap

s around 271 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 272 | // phrase emphasis, and spans. The list of tags we're looking for is 273 | // hard-coded: 274 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" 275 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" 276 | 277 | // First, look for nested blocks, e.g.: 278 | //

279 | //
280 | // tags for inner block must be indented. 281 | //
282 | //
283 | // 284 | // The outermost tags must start at the left margin for this to match, and 285 | // the inner nested divs must be indented. 286 | // We need to do this before the next, more liberal match, because the next 287 | // match will start at the first `
` and stop at the first `
`. 288 | 289 | // attacklab: This regex can be expensive when it fails. 290 | 291 | /* 292 | text = text.replace(/ 293 | ( // save in $1 294 | ^ // start of line (with /m) 295 | <($block_tags_a) // start tag = $2 296 | \b // word break 297 | // attacklab: hack around khtml/pcre bug... 298 | [^\r]*?\n // any number of lines, minimally matching 299 | // the matching end tag 300 | [ \t]* // trailing spaces/tabs 301 | (?=\n+) // followed by a newline 302 | ) // attacklab: there are sentinel newlines at end of document 303 | /gm,function(){...}}; 304 | */ 305 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement); 306 | 307 | // 308 | // Now match more liberally, simply from `\n` to `\n` 309 | // 310 | 311 | /* 312 | text = text.replace(/ 313 | ( // save in $1 314 | ^ // start of line (with /m) 315 | <($block_tags_b) // start tag = $2 316 | \b // word break 317 | // attacklab: hack around khtml/pcre bug... 318 | [^\r]*? // any number of lines, minimally matching 319 | .* // the matching end tag 320 | [ \t]* // trailing spaces/tabs 321 | (?=\n+) // followed by a newline 322 | ) // attacklab: there are sentinel newlines at end of document 323 | /gm,function(){...}}; 324 | */ 325 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement); 326 | 327 | // Special case just for
. It was easier to make a special case than 328 | // to make the other regex more complicated. 329 | 330 | /* 331 | text = text.replace(/ 332 | \n // Starting after a blank line 333 | [ ]{0,3} 334 | ( // save in $1 335 | (<(hr) // start tag = $2 336 | \b // word break 337 | ([^<>])*? 338 | \/?>) // the matching end tag 339 | [ \t]* 340 | (?=\n{2,}) // followed by a blank line 341 | ) 342 | /g,hashElement); 343 | */ 344 | text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement); 345 | 346 | // Special case for standalone HTML comments: 347 | 348 | /* 349 | text = text.replace(/ 350 | \n\n // Starting after a blank line 351 | [ ]{0,3} // attacklab: g_tab_width - 1 352 | ( // save in $1 353 | -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256 355 | > 356 | [ \t]* 357 | (?=\n{2,}) // followed by a blank line 358 | ) 359 | /g,hashElement); 360 | */ 361 | text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement); 362 | 363 | // PHP and ASP-style processor instructions ( and <%...%>) 364 | 365 | /* 366 | text = text.replace(/ 367 | (?: 368 | \n\n // Starting after a blank line 369 | ) 370 | ( // save in $1 371 | [ ]{0,3} // attacklab: g_tab_width - 1 372 | (?: 373 | <([?%]) // $2 374 | [^\r]*? 375 | \2> 376 | ) 377 | [ \t]* 378 | (?=\n{2,}) // followed by a blank line 379 | ) 380 | /g,hashElement); 381 | */ 382 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement); 383 | 384 | return text; 385 | } 386 | 387 | function hashElement(wholeMatch, m1) { 388 | var blockText = m1; 389 | 390 | // Undo double lines 391 | blockText = blockText.replace(/^\n+/, ""); 392 | 393 | // strip trailing blank lines 394 | blockText = blockText.replace(/\n+$/g, ""); 395 | 396 | // Replace the element text with a marker ("~KxK" where x is its key) 397 | blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n"; 398 | 399 | return blockText; 400 | } 401 | 402 | var blockGamutHookCallback = function (t) { return _RunBlockGamut(t); } 403 | 404 | function _RunBlockGamut(text, doNotUnhash) { 405 | // 406 | // These are all the transformations that form block-level 407 | // tags like paragraphs, headers, and list items. 408 | // 409 | 410 | text = pluginHooks.preBlockGamut(text, blockGamutHookCallback); 411 | 412 | text = _DoHeaders(text); 413 | 414 | // Do Horizontal Rules: 415 | var replacement = "
\n"; 416 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); 417 | text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); 418 | text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); 419 | 420 | text = _DoLists(text); 421 | text = _DoCodeBlocks(text); 422 | text = _DoBlockQuotes(text); 423 | 424 | text = pluginHooks.postBlockGamut(text, blockGamutHookCallback); 425 | 426 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 427 | // was to escape raw HTML in the original Markdown source. This time, 428 | // we're escaping the markup we've just created, so that we don't wrap 429 | //

tags around block-level tags. 430 | text = _HashHTMLBlocks(text); 431 | text = _FormParagraphs(text, doNotUnhash); 432 | 433 | return text; 434 | } 435 | 436 | function _RunSpanGamut(text) { 437 | // 438 | // These are all the transformations that occur *within* block-level 439 | // tags like paragraphs, headers, and list items. 440 | // 441 | 442 | text = pluginHooks.preSpanGamut(text); 443 | 444 | text = _DoCodeSpans(text); 445 | text = _EscapeSpecialCharsWithinTagAttributes(text); 446 | text = _EncodeBackslashEscapes(text); 447 | 448 | // Process anchor and image tags. Images must come first, 449 | // because ![foo][f] looks like an anchor. 450 | text = _DoImages(text); 451 | text = _DoAnchors(text); 452 | 453 | // Make links out of things like `` 454 | // Must come after _DoAnchors(), because you can use < and > 455 | // delimiters in inline links like [this](). 456 | text = _DoAutoLinks(text); 457 | 458 | text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now 459 | 460 | text = _EncodeAmpsAndAngles(text); 461 | text = _DoItalicsAndBold(text); 462 | 463 | // Do hard breaks: 464 | text = text.replace(/ +\n/g, "
\n"); 465 | 466 | text = pluginHooks.postSpanGamut(text); 467 | 468 | return text; 469 | } 470 | 471 | function _EscapeSpecialCharsWithinTagAttributes(text) { 472 | // 473 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 474 | // don't conflict with their use in Markdown for code, italics and strong. 475 | // 476 | 477 | // Build a regex to find HTML tags and comments. See Friedl's 478 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 479 | 480 | // SE: changed the comment part of the regex 481 | 482 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; 483 | 484 | text = text.replace(regex, function (wholeMatch) { 485 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); 486 | tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987 487 | return tag; 488 | }); 489 | 490 | return text; 491 | } 492 | 493 | function _DoAnchors(text) { 494 | // 495 | // Turn Markdown link shortcuts into XHTML
tags. 496 | // 497 | // 498 | // First, handle reference-style links: [link text] [id] 499 | // 500 | 501 | /* 502 | text = text.replace(/ 503 | ( // wrap whole match in $1 504 | \[ 505 | ( 506 | (?: 507 | \[[^\]]*\] // allow brackets nested one level 508 | | 509 | [^\[] // or anything else 510 | )* 511 | ) 512 | \] 513 | 514 | [ ]? // one optional space 515 | (?:\n[ ]*)? // one optional newline followed by spaces 516 | 517 | \[ 518 | (.*?) // id = $3 519 | \] 520 | ) 521 | ()()()() // pad remaining backreferences 522 | /g, writeAnchorTag); 523 | */ 524 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); 525 | 526 | // 527 | // Next, inline-style links: [link text](url "optional title") 528 | // 529 | 530 | /* 531 | text = text.replace(/ 532 | ( // wrap whole match in $1 533 | \[ 534 | ( 535 | (?: 536 | \[[^\]]*\] // allow brackets nested one level 537 | | 538 | [^\[\]] // or anything else 539 | )* 540 | ) 541 | \] 542 | \( // literal paren 543 | [ \t]* 544 | () // no id, so leave $3 empty 545 | ? 552 | [ \t]* 553 | ( // $5 554 | (['"]) // quote char = $6 555 | (.*?) // Title = $7 556 | \6 // matching quote 557 | [ \t]* // ignore any spaces/tabs between closing quote and ) 558 | )? // title is optional 559 | \) 560 | ) 561 | /g, writeAnchorTag); 562 | */ 563 | 564 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); 565 | 566 | // 567 | // Last, handle reference-style shortcuts: [link text] 568 | // These must come last in case you've also got [link test][1] 569 | // or [link test](/foo) 570 | // 571 | 572 | /* 573 | text = text.replace(/ 574 | ( // wrap whole match in $1 575 | \[ 576 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 577 | \] 578 | ) 579 | ()()()()() // pad rest of backreferences 580 | /g, writeAnchorTag); 581 | */ 582 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 583 | 584 | return text; 585 | } 586 | 587 | function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { 588 | if (m7 == undefined) m7 = ""; 589 | var whole_match = m1; 590 | var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs 591 | var link_id = m3.toLowerCase(); 592 | var url = m4; 593 | var title = m7; 594 | 595 | if (url == "") { 596 | if (link_id == "") { 597 | // lower-case and turn embedded newlines into spaces 598 | link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); 599 | } 600 | url = "#" + link_id; 601 | 602 | if (g_urls.get(link_id) != undefined) { 603 | url = g_urls.get(link_id); 604 | if (g_titles.get(link_id) != undefined) { 605 | title = g_titles.get(link_id); 606 | } 607 | } 608 | else { 609 | if (whole_match.search(/\(\s*\)$/m) > -1) { 610 | // Special case for explicit empty url 611 | url = ""; 612 | } else { 613 | return whole_match; 614 | } 615 | } 616 | } 617 | url = encodeProblemUrlChars(url); 618 | url = escapeCharacters(url, "*_"); 619 | var result = ""; 628 | 629 | return result; 630 | } 631 | 632 | function _DoImages(text) { 633 | // 634 | // Turn Markdown image shortcuts into tags. 635 | // 636 | 637 | // 638 | // First, handle reference-style labeled images: ![alt text][id] 639 | // 640 | 641 | /* 642 | text = text.replace(/ 643 | ( // wrap whole match in $1 644 | !\[ 645 | (.*?) // alt text = $2 646 | \] 647 | 648 | [ ]? // one optional space 649 | (?:\n[ ]*)? // one optional newline followed by spaces 650 | 651 | \[ 652 | (.*?) // id = $3 653 | \] 654 | ) 655 | ()()()() // pad rest of backreferences 656 | /g, writeImageTag); 657 | */ 658 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); 659 | 660 | // 661 | // Next, handle inline images: ![alt text](url "optional title") 662 | // Don't forget: encode * and _ 663 | 664 | /* 665 | text = text.replace(/ 666 | ( // wrap whole match in $1 667 | !\[ 668 | (.*?) // alt text = $2 669 | \] 670 | \s? // One optional whitespace character 671 | \( // literal paren 672 | [ \t]* 673 | () // no id, so leave $3 empty 674 | ? // src url = $4 675 | [ \t]* 676 | ( // $5 677 | (['"]) // quote char = $6 678 | (.*?) // title = $7 679 | \6 // matching quote 680 | [ \t]* 681 | )? // title is optional 682 | \) 683 | ) 684 | /g, writeImageTag); 685 | */ 686 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); 687 | 688 | return text; 689 | } 690 | 691 | function attributeEncode(text) { 692 | // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) 693 | // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) 694 | return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } 753 | ); 754 | 755 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 756 | function (matchFound, m1) { return "

" + _RunSpanGamut(m1) + "

\n\n"; } 757 | ); 758 | 759 | // atx-style headers: 760 | // # Header 1 761 | // ## Header 2 762 | // ## Header 2 with closing hashes ## 763 | // ... 764 | // ###### Header 6 765 | // 766 | 767 | /* 768 | text = text.replace(/ 769 | ^(\#{1,6}) // $1 = string of #'s 770 | [ \t]* 771 | (.+?) // $2 = Header text 772 | [ \t]* 773 | \#* // optional closing #'s (not counted) 774 | \n+ 775 | /gm, function() {...}); 776 | */ 777 | 778 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, 779 | function (wholeMatch, m1, m2) { 780 | var h_level = m1.length; 781 | return "" + _RunSpanGamut(m2) + "\n\n"; 782 | } 783 | ); 784 | 785 | return text; 786 | } 787 | 788 | function _DoLists(text) { 789 | // 790 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 791 | // 792 | 793 | // attacklab: add sentinel to hack around khtml/safari bug: 794 | // http://bugs.webkit.org/show_bug.cgi?id=11231 795 | text += "~0"; 796 | 797 | // Re-usable pattern to match any entirel ul or ol list: 798 | 799 | /* 800 | var whole_list = / 801 | ( // $1 = whole list 802 | ( // $2 803 | [ ]{0,3} // attacklab: g_tab_width - 1 804 | ([*+-]|\d+[.]) // $3 = first list item marker 805 | [ \t]+ 806 | ) 807 | [^\r]+? 808 | ( // $4 809 | ~0 // sentinel for workaround; should be $ 810 | | 811 | \n{2,} 812 | (?=\S) 813 | (?! // Negative lookahead for another list item marker 814 | [ \t]* 815 | (?:[*+-]|\d+[.])[ \t]+ 816 | ) 817 | ) 818 | ) 819 | /g 820 | */ 821 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 822 | 823 | if (g_list_level) { 824 | text = text.replace(whole_list, function (wholeMatch, m1, m2) { 825 | var list = m1; 826 | var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; 827 | 828 | var result = _ProcessListItems(list, list_type); 829 | 830 | // Trim any trailing whitespace, to put the closing `` 831 | // up on the preceding line, to get it past the current stupid 832 | // HTML block parser. This is a hack to work around the terrible 833 | // hack that is the HTML block parser. 834 | result = result.replace(/\s+$/, ""); 835 | result = "<" + list_type + ">" + result + "\n"; 836 | return result; 837 | }); 838 | } else { 839 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 840 | text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { 841 | var runup = m1; 842 | var list = m2; 843 | 844 | var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; 845 | var result = _ProcessListItems(list, list_type); 846 | result = runup + "<" + list_type + ">\n" + result + "\n"; 847 | return result; 848 | }); 849 | } 850 | 851 | // attacklab: strip sentinel 852 | text = text.replace(/~0/, ""); 853 | 854 | return text; 855 | } 856 | 857 | var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; 858 | 859 | function _ProcessListItems(list_str, list_type) { 860 | // 861 | // Process the contents of a single ordered or unordered list, splitting it 862 | // into individual list items. 863 | // 864 | // list_type is either "ul" or "ol". 865 | 866 | // The $g_list_level global keeps track of when we're inside a list. 867 | // Each time we enter a list, we increment it; when we leave a list, 868 | // we decrement. If it's zero, we're not in a list anymore. 869 | // 870 | // We do this because when we're not inside a list, we want to treat 871 | // something like this: 872 | // 873 | // I recommend upgrading to version 874 | // 8. Oops, now this line is treated 875 | // as a sub-list. 876 | // 877 | // As a single paragraph, despite the fact that the second line starts 878 | // with a digit-period-space sequence. 879 | // 880 | // Whereas when we're inside a list (or sub-list), that line will be 881 | // treated as the start of a sub-list. What a kludge, huh? This is 882 | // an aspect of Markdown's syntax that's hard to parse perfectly 883 | // without resorting to mind-reading. Perhaps the solution is to 884 | // change the syntax rules such that sub-lists must start with a 885 | // starting cardinal number; e.g. "1." or "a.". 886 | 887 | g_list_level++; 888 | 889 | // trim trailing blank lines: 890 | list_str = list_str.replace(/\n{2,}$/, "\n"); 891 | 892 | // attacklab: add sentinel to emulate \z 893 | list_str += "~0"; 894 | 895 | // In the original attacklab showdown, list_type was not given to this function, and anything 896 | // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: 897 | // 898 | // Markdown rendered by WMD rendered by MarkdownSharp 899 | // ------------------------------------------------------------------ 900 | // 1. first 1. first 1. first 901 | // 2. second 2. second 2. second 902 | // - third 3. third * third 903 | // 904 | // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, 905 | // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: 906 | 907 | /* 908 | list_str = list_str.replace(/ 909 | (^[ \t]*) // leading whitespace = $1 910 | ({MARKER}) [ \t]+ // list marker = $2 911 | ([^\r]+? // list item text = $3 912 | (\n+) 913 | ) 914 | (?= 915 | (~0 | \2 ({MARKER}) [ \t]+) 916 | ) 917 | /gm, function(){...}); 918 | */ 919 | 920 | var marker = _listItemMarkers[list_type]; 921 | var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); 922 | var last_item_had_a_double_newline = false; 923 | list_str = list_str.replace(re, 924 | function (wholeMatch, m1, m2, m3) { 925 | var item = m3; 926 | var leading_space = m1; 927 | var ends_with_double_newline = /\n\n$/.test(item); 928 | var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; 929 | 930 | if (contains_double_newline || last_item_had_a_double_newline) { 931 | item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true); 932 | } 933 | else { 934 | // Recursion for sub-lists: 935 | item = _DoLists(_Outdent(item)); 936 | item = item.replace(/\n$/, ""); // chomp(item) 937 | item = _RunSpanGamut(item); 938 | } 939 | last_item_had_a_double_newline = ends_with_double_newline; 940 | return "
  • " + item + "
  • \n"; 941 | } 942 | ); 943 | 944 | // attacklab: strip sentinel 945 | list_str = list_str.replace(/~0/g, ""); 946 | 947 | g_list_level--; 948 | return list_str; 949 | } 950 | 951 | function _DoCodeBlocks(text) { 952 | // 953 | // Process Markdown `
    ` blocks.
     954 |             //
     955 | 
     956 |             /*
     957 |             text = text.replace(/
     958 |                 (?:\n\n|^)
     959 |                 (                               // $1 = the code block -- one or more lines, starting with a space/tab
     960 |                     (?:
     961 |                         (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
     962 |                         .*\n+
     963 |                     )+
     964 |                 )
     965 |                 (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
     966 |             /g ,function(){...});
     967 |             */
     968 | 
     969 |             // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
     970 |             text += "~0";
     971 | 
     972 |             text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
     973 |                 function (wholeMatch, m1, m2) {
     974 |                     var codeblock = m1;
     975 |                     var nextChar = m2;
     976 | 
     977 |                     codeblock = _EncodeCode(_Outdent(codeblock));
     978 |                     codeblock = _Detab(codeblock);
     979 |                     codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
     980 |                     codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
     981 | 
     982 |                     codeblock = "
    " + codeblock + "\n
    "; 983 | 984 | return "\n\n" + codeblock + "\n\n" + nextChar; 985 | } 986 | ); 987 | 988 | // attacklab: strip sentinel 989 | text = text.replace(/~0/, ""); 990 | 991 | return text; 992 | } 993 | 994 | function hashBlock(text) { 995 | text = text.replace(/(^\n+|\n+$)/g, ""); 996 | return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; 997 | } 998 | 999 | function _DoCodeSpans(text) { 1000 | // 1001 | // * Backtick quotes are used for spans. 1002 | // 1003 | // * You can use multiple backticks as the delimiters if you want to 1004 | // include literal backticks in the code span. So, this input: 1005 | // 1006 | // Just type ``foo `bar` baz`` at the prompt. 1007 | // 1008 | // Will translate to: 1009 | // 1010 | //

    Just type foo `bar` baz at the prompt.

    1011 | // 1012 | // There's no arbitrary limit to the number of backticks you 1013 | // can use as delimters. If you need three consecutive backticks 1014 | // in your code, use four for delimiters, etc. 1015 | // 1016 | // * You can use spaces to get literal backticks at the edges: 1017 | // 1018 | // ... type `` `bar` `` ... 1019 | // 1020 | // Turns to: 1021 | // 1022 | // ... type `bar` ... 1023 | // 1024 | 1025 | /* 1026 | text = text.replace(/ 1027 | (^|[^\\]) // Character before opening ` can't be a backslash 1028 | (`+) // $2 = Opening run of ` 1029 | ( // $3 = The code block 1030 | [^\r]*? 1031 | [^`] // attacklab: work around lack of lookbehind 1032 | ) 1033 | \2 // Matching closer 1034 | (?!`) 1035 | /gm, function(){...}); 1036 | */ 1037 | 1038 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 1039 | function (wholeMatch, m1, m2, m3, m4) { 1040 | var c = m3; 1041 | c = c.replace(/^([ \t]*)/g, ""); // leading whitespace 1042 | c = c.replace(/[ \t]*$/g, ""); // trailing whitespace 1043 | c = _EncodeCode(c); 1044 | c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. 1045 | return m1 + "" + c + ""; 1046 | } 1047 | ); 1048 | 1049 | return text; 1050 | } 1051 | 1052 | function _EncodeCode(text) { 1053 | // 1054 | // Encode/escape certain characters inside Markdown code runs. 1055 | // The point is that in code, these characters are literals, 1056 | // and lose their special Markdown meanings. 1057 | // 1058 | // Encode all ampersands; HTML entities are not 1059 | // entities within a Markdown code span. 1060 | text = text.replace(/&/g, "&"); 1061 | 1062 | // Do the angle bracket song and dance: 1063 | text = text.replace(//g, ">"); 1065 | 1066 | // Now, escape characters that are magic in Markdown: 1067 | text = escapeCharacters(text, "\*_{}[]\\", false); 1068 | 1069 | // jj the line above breaks this: 1070 | //--- 1071 | 1072 | //* Item 1073 | 1074 | // 1. Subitem 1075 | 1076 | // special char: * 1077 | //--- 1078 | 1079 | return text; 1080 | } 1081 | 1082 | function _DoItalicsAndBold(text) { 1083 | 1084 | // must go first: 1085 | text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g, 1086 | "$1$3$4"); 1087 | 1088 | text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g, 1089 | "$1$3$4"); 1090 | 1091 | return text; 1092 | } 1093 | 1094 | function _DoBlockQuotes(text) { 1095 | 1096 | /* 1097 | text = text.replace(/ 1098 | ( // Wrap whole match in $1 1099 | ( 1100 | ^[ \t]*>[ \t]? // '>' at the start of a line 1101 | .+\n // rest of the first line 1102 | (.+\n)* // subsequent consecutive lines 1103 | \n* // blanks 1104 | )+ 1105 | ) 1106 | /gm, function(){...}); 1107 | */ 1108 | 1109 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 1110 | function (wholeMatch, m1) { 1111 | var bq = m1; 1112 | 1113 | // attacklab: hack around Konqueror 3.5.4 bug: 1114 | // "----------bug".replace(/^-/g,"") == "bug" 1115 | 1116 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting 1117 | 1118 | // attacklab: clean up hack 1119 | bq = bq.replace(/~0/g, ""); 1120 | 1121 | bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines 1122 | bq = _RunBlockGamut(bq); // recurse 1123 | 1124 | bq = bq.replace(/(^|\n)/g, "$1 "); 1125 | // These leading spaces screw with
     content, so we need to fix that:
    1126 |                     bq = bq.replace(
    1127 |                             /(\s*
    [^\r]+?<\/pre>)/gm,
    1128 |                         function (wholeMatch, m1) {
    1129 |                             var pre = m1;
    1130 |                             // attacklab: hack around Konqueror 3.5.4 bug:
    1131 |                             pre = pre.replace(/^  /mg, "~0");
    1132 |                             pre = pre.replace(/~0/g, "");
    1133 |                             return pre;
    1134 |                         });
    1135 | 
    1136 |                     return hashBlock("
    \n" + bq + "\n
    "); 1137 | } 1138 | ); 1139 | return text; 1140 | } 1141 | 1142 | function _FormParagraphs(text, doNotUnhash) { 1143 | // 1144 | // Params: 1145 | // $text - string to process with html

    tags 1146 | // 1147 | 1148 | // Strip leading and trailing lines: 1149 | text = text.replace(/^\n+/g, ""); 1150 | text = text.replace(/\n+$/g, ""); 1151 | 1152 | var grafs = text.split(/\n{2,}/g); 1153 | var grafsOut = []; 1154 | 1155 | var markerRe = /~K(\d+)K/; 1156 | 1157 | // 1158 | // Wrap

    tags. 1159 | // 1160 | var end = grafs.length; 1161 | for (var i = 0; i < end; i++) { 1162 | var str = grafs[i]; 1163 | 1164 | // if this is an HTML marker, copy it 1165 | if (markerRe.test(str)) { 1166 | grafsOut.push(str); 1167 | } 1168 | else if (/\S/.test(str)) { 1169 | str = _RunSpanGamut(str); 1170 | str = str.replace(/^([ \t]*)/g, "

    "); 1171 | str += "

    " 1172 | grafsOut.push(str); 1173 | } 1174 | 1175 | } 1176 | // 1177 | // Unhashify HTML blocks 1178 | // 1179 | if (!doNotUnhash) { 1180 | end = grafsOut.length; 1181 | for (var i = 0; i < end; i++) { 1182 | var foundAny = true; 1183 | while (foundAny) { // we may need several runs, since the data may be nested 1184 | foundAny = false; 1185 | grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { 1186 | foundAny = true; 1187 | return g_html_blocks[id]; 1188 | }); 1189 | } 1190 | } 1191 | } 1192 | return grafsOut.join("\n\n"); 1193 | } 1194 | 1195 | function _EncodeAmpsAndAngles(text) { 1196 | // Smart processing for ampersands and angle brackets that need to be encoded. 1197 | 1198 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1199 | // http://bumppo.net/projects/amputator/ 1200 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); 1201 | 1202 | // Encode naked <'s 1203 | text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<"); 1204 | 1205 | return text; 1206 | } 1207 | 1208 | function _EncodeBackslashEscapes(text) { 1209 | // 1210 | // Parameter: String. 1211 | // Returns: The string, with after processing the following backslash 1212 | // escape sequences. 1213 | // 1214 | 1215 | // attacklab: The polite way to do this is with the new 1216 | // escapeCharacters() function: 1217 | // 1218 | // text = escapeCharacters(text,"\\",true); 1219 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1220 | // 1221 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1222 | // as an optimization for Firefox. This function gets called a LOT. 1223 | 1224 | text = text.replace(/\\(\\)/g, escapeCharacters_callback); 1225 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); 1226 | return text; 1227 | } 1228 | 1229 | function handleTrailingParens(wholeMatch, lookbehind, protocol, link) { 1230 | if (lookbehind) 1231 | return wholeMatch; 1232 | if (link.charAt(link.length - 1) !== ")") 1233 | return "<" + protocol + link + ">"; 1234 | var parens = link.match(/[()]/g); 1235 | var level = 0; 1236 | for (var i = 0; i < parens.length; i++) { 1237 | if (parens[i] === "(") { 1238 | if (level <= 0) 1239 | level = 1; 1240 | else 1241 | level++; 1242 | } 1243 | else { 1244 | level--; 1245 | } 1246 | } 1247 | var tail = ""; 1248 | if (level < 0) { 1249 | var re = new RegExp("\\){1," + (-level) + "}$"); 1250 | link = link.replace(re, function (trailingParens) { 1251 | tail = trailingParens; 1252 | return ""; 1253 | }); 1254 | } 1255 | 1256 | return "<" + protocol + link + ">" + tail; 1257 | } 1258 | 1259 | function _DoAutoLinks(text) { 1260 | 1261 | // note that at this point, all other URL in the text are already hyperlinked as
    1262 | // *except* for the case 1263 | 1264 | // automatically add < and > around unadorned raw hyperlinks 1265 | // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character 1266 | // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor 1267 | // with a <, so there is no risk of overlapping matches. 1268 | text = text.replace(/(="|<)?\b(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\])])(?=$|\W)/gi, handleTrailingParens); 1269 | 1270 | // autolink anything like 1271 | 1272 | var replacer = function (wholematch, m1) { return "" + pluginHooks.plainLinkText(m1) + ""; } 1273 | text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); 1274 | 1275 | // Email addresses: 1276 | /* 1277 | text = text.replace(/ 1278 | < 1279 | (?:mailto:)? 1280 | ( 1281 | [-.\w]+ 1282 | \@ 1283 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1284 | ) 1285 | > 1286 | /gi, _DoAutoLinks_callback()); 1287 | */ 1288 | 1289 | /* disabling email autolinking, since we don't do that on the server, either 1290 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1291 | function(wholeMatch,m1) { 1292 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1293 | } 1294 | ); 1295 | */ 1296 | return text; 1297 | } 1298 | 1299 | function _UnescapeSpecialChars(text) { 1300 | // 1301 | // Swap back in all the special characters we've hidden. 1302 | // 1303 | text = text.replace(/~E(\d+)E/g, 1304 | function (wholeMatch, m1) { 1305 | var charCodeToReplace = parseInt(m1); 1306 | return String.fromCharCode(charCodeToReplace); 1307 | } 1308 | ); 1309 | return text; 1310 | } 1311 | 1312 | function _Outdent(text) { 1313 | // 1314 | // Remove one level of line-leading tabs or spaces 1315 | // 1316 | 1317 | // attacklab: hack around Konqueror 3.5.4 bug: 1318 | // "----------bug".replace(/^-/g,"") == "bug" 1319 | 1320 | text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width 1321 | 1322 | // attacklab: clean up hack 1323 | text = text.replace(/~0/g, "") 1324 | 1325 | return text; 1326 | } 1327 | 1328 | function _Detab(text) { 1329 | if (!/\t/.test(text)) 1330 | return text; 1331 | 1332 | var spaces = [" ", " ", " ", " "], 1333 | skew = 0, 1334 | v; 1335 | 1336 | return text.replace(/[\n\t]/g, function (match, offset) { 1337 | if (match === "\n") { 1338 | skew = offset + 1; 1339 | return match; 1340 | } 1341 | v = (offset - skew) % 4; 1342 | skew = offset + 1; 1343 | return spaces[v]; 1344 | }); 1345 | } 1346 | 1347 | // 1348 | // attacklab: Utility functions 1349 | // 1350 | 1351 | var _problemUrlChars = /(?:["'*()[\]:]|~D)/g; 1352 | 1353 | // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems 1354 | function encodeProblemUrlChars(url) { 1355 | if (!url) 1356 | return ""; 1357 | 1358 | var len = url.length; 1359 | 1360 | return url.replace(_problemUrlChars, function (match, offset) { 1361 | if (match == "~D") // escape for dollar 1362 | return "%24"; 1363 | if (match == ":") { 1364 | if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1))) 1365 | return ":" 1366 | } 1367 | return "%" + match.charCodeAt(0).toString(16); 1368 | }); 1369 | } 1370 | 1371 | 1372 | function escapeCharacters(text, charsToEscape, afterBackslash) { 1373 | // First we have to escape the escape characters so that 1374 | // we can build a character class out of them 1375 | var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; 1376 | 1377 | if (afterBackslash) { 1378 | regexString = "\\\\" + regexString; 1379 | } 1380 | 1381 | var regex = new RegExp(regexString, "g"); 1382 | text = text.replace(regex, escapeCharacters_callback); 1383 | 1384 | return text; 1385 | } 1386 | 1387 | 1388 | function escapeCharacters_callback(wholeMatch, m1) { 1389 | var charCodeToEscape = m1.charCodeAt(0); 1390 | return "~E" + charCodeToEscape + "E"; 1391 | } 1392 | 1393 | }; // end of the Markdown.Converter constructor 1394 | 1395 | })(); 1396 | --------------------------------------------------------------------------------