├── .gitignore
├── .npmignore
├── README.md
├── bin
└── presenter-cli.js
├── examples
├── doc.xml
├── docs
│ └── images
│ │ ├── charts.png
│ │ └── map.png
├── images
│ ├── charts.png
│ └── map.png
├── rich.xml
└── sample.xml
├── lib
├── document-viewer.js
└── ter-u12n.json
├── package.json
└── server
├── package.json
├── presenter.js
├── server.js
└── test
└── test.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | p.js
2 | node_modules
3 | p.txt
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /server
2 | /test
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WOPR
2 |
3 | WOPR is a simple markup language for creating [rich terminal reports](https://github.com/yaronn/blessed-contrib), presentations and infographics.
4 |
5 | Put [a report](https://raw.githubusercontent.com/yaronn/wopr/master/examples/sample.xml) on the web (e.g. gist) and view it via curl:
6 |
7 | `````bash
8 | $> curl -N tty.zone/\[0-2\]\?auto\&cols=$((COLUMNS))
9 | `````
10 | (If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com or use a [local viewer](https://github.com/yaronn/wopr#viewing-reports))
11 |
12 | Created by Yaron Naveh ([@YaronNaveh](https://twitter.com/YaronNaveh))
13 |
14 | 
15 | 
16 |
17 | ##Writing your first terminal report##
18 |
19 | Here is a simple report with a bar chart:
20 |
21 | `````xml
22 |
23 |
24 | -
25 |
26 |
27 |
28 |
29 | `````
30 |
31 | You have 3 options to view this report:
32 |
33 | **Option 1: POST it to the wopr online viewer**
34 |
35 | `````bash
36 | $> curl --data ' ' tty.zone\?cols=$((COLUMNS))
37 | `````
38 |
39 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com.
40 |
41 | **Note:** The online viewer is a reference implementation. Do not send it secret data but rather create [your own](https://github.com/yaronn/wopr/tree/master/server).
42 |
43 | **Option 2: POST it from external url**
44 |
45 | Save the report content in some url (e.g. gist) and then:
46 |
47 | `````bash
48 | $> a=$(curl -s https://gist.githubusercontent.com/yaronn/e6eec6d0e7adac63c83f/raw/50aca544d26a32aa189e790635c8679067017948/gistfile1.xml); curl --data "$a" tty.zone\?cols=$((COLUMNS))
49 | `````
50 |
51 | (note you need the gist raw url)
52 |
53 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com.
54 |
55 | **Note:** The online viewer is a reference implementation. Do not send it secret data but rather create [your own](https://github.com/yaronn/wopr/tree/master/server).
56 |
57 |
58 | **Option 3: Via the local viewer**
59 |
60 | Save the report xml to report.xml and then:
61 |
62 | `````bash
63 | $> npm install -g wopr
64 | $> wopr report.xml
65 | `````
66 |
67 | Note the local viewer does not send anything online and does not require network.
68 |
69 | 
70 |
71 | ##Markup Basics#
72 |
73 | **Pages**
74 |
75 | A document is a set of pages:
76 |
77 | `````xml
78 |
79 |
80 | ...
81 |
82 |
83 | ...
84 |
85 |
86 | `````
87 |
88 | **Layout**
89 |
90 | A page is a 12x12 grid in which you can position different widgets:
91 |
92 | `````xml
93 |
94 |
95 | -
96 |
97 |
98 | -
99 |
100 |
101 |
102 |
103 | `````
104 |
105 | Here, the bar widget is in the first column and row (0-based indexing) and spans three columns and rows.
106 | The box element is in the same page but in a different position.
107 |
108 |
109 | **Widgets**
110 |
111 | The available widgets are the ones that exist in the [blessed](https://github.com/chjj/blessed) and [blessed-contrib](https://github.com/yaronn/blessed-contrib) projects.
112 | You can infer the xml representation of a javascript widget using a simple convention. Assume that you would instantiate some blessed widget with this javascript:
113 |
114 | `````javascript
115 | blessed.widget({ string: "5"
116 | , int: 1
117 | , intArray: [1,2,3]
118 | , stringArray: ["a", "b", "c"]
119 | , multiArray: [ [1,2,3], [4,5,6] ]
120 | , complexArray: [ {a: 1, b: [1,2] }, {a: 3, b: [3,4]} ]
121 | , object: { innerProp: 1, multiArray: [ [1,2], [3,4] ] }
122 | })
123 | `````
124 |
125 | Then here is how you would represent it in xml:
126 |
127 | `````xml
128 |
129 |
130 | 1,2,3
131 | 4,5,6
132 |
133 |
134 | 1,2
135 | 3,4
136 |
137 |
138 |
139 |
140 |
141 |
142 | `````
143 |
144 | You can also look at the [demo xml](https://raw.githubusercontent.com/yaronn/wopr/master/examples/sample.xml) to get more samples.
145 |
146 | 
147 |
148 | ##Viewing Reports##
149 |
150 |
151 | Depending on how you use a report, you have a few ways to view it. On Windows you will probably only be able to use the third option and need to [install the fonts](http://webservices20.blogspot.com/2015/04/running-terminal-dashboards-on-windows.html) for best view.
152 |
153 | **Option 1: POST it to the wopr online viewer**
154 |
155 | `````bash
156 | $> curl --data ' ' tty.zone\?cols=$((COLUMNS))
157 | `````
158 |
159 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com.
160 |
161 | **Option 2: POST it from external url**
162 |
163 | Save the report content in some url (e.g. gist) and then:
164 |
165 | `````bash
166 | $> a=$(curl -s https://gist.githubusercontent.com/yaronn/e6eec6d0e7adac63c83f/raw/50aca544d26a32aa189e790635c8679067017948/gistfile1.xml); curl --data "$a" tty.zone\?cols=$((COLUMNS))
167 | `````
168 |
169 | (note you need the gist raw url)
170 |
171 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com.
172 |
173 | Tip: If you use a url shortener (e.g. bit.ly) add the -L flag to curl to follow redirects.
174 |
175 | **Option 3: via the local viewer**
176 |
177 | Save the report xml to report.xml and then:
178 |
179 | `````bash
180 | $> npm install -g wopr
181 | $> wopr report.xml
182 | `````
183 |
184 | Note the local viewer does not send anything online and does not require network.
185 |
186 | **Tip:** Maximize the terminal before viewing the report for best viewing experience
187 | **Tip:** If you CTRL+C in the middle or rendering your cursoe might disappear. Restore it by running again and letting the render complete or with `$> echo '\033[?25h'`
188 |
189 | **View customization**
190 | When using the online reports, you might need to adjust the slides size based on your font / resolution or use non-xterm terminal. tty.zone supports the following query params:
191 |
192 | `````bash
193 | curl -N tty.zone\?\&cols=200\&rows=50\&terminal=xterm
194 | `````
195 |
196 | You can infer them automatically from your environment:
197 |
198 | `````bash
199 | curl -N tty.zone\?\&cols=$((COLUMNS))\&rows=$((LINES-5))\&terminal=${TERM}
200 | `````
201 |
202 | It is best to escape all special characters (e.g. ? &) as seen in the above samples, since some shells will require this (zsh).
203 |
204 |
205 | **Pages**
206 |
207 | When viewing a report with the local viewer you can advance slides with the Return or Space keys.
208 | When using the online viewer you have 2 options:
209 |
210 | **Option 1:** Manually advance slides with Return or Space:
211 |
212 | `````bash
213 | p=0; while true; do curl tty.zone/$((p++))\?cols=$((COLUMNS)); read; done
214 | `````
215 |
216 | **Option 2:** Slides advance automatically every 5 seconds:
217 |
218 | `````bash
219 | curl -N tty.zone/\[0-2\]\?auto\&cols=$((COLUMNS))
220 | `````
221 |
222 | Where 0 is the index of the first slide and 2 of the last slide. Keep the brackets in the url (they are not to express optional argument) and escape them as in the above sample.
223 |
224 | Tip: disable curl buffering with the -N flag
225 |
226 | You can also view a specific slide (#4 in this case):
227 |
228 | `````bash
229 | curl --data '...' tty.zone/4\?cols=$((COLUMNS))
230 | `````
231 |
232 | ##License##
233 | MIT
234 |
235 |
236 | ## More Information
237 | Created by Yaron Naveh ([twitter](http://twitter.com/YaronNaveh), [blog](http://webservices20.blogspot.com/))
238 |
--------------------------------------------------------------------------------
/bin/presenter-cli.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | var file = process.argv[2]
4 | if (file=="-h" | file=="--help") {
5 | showHelp()
6 | process.exit()
7 | }
8 |
9 | var fs = require('fs')
10 | , parse = require('xml2js').parseString
11 | , Viewer = require('../lib/document-viewer.js')
12 | , blessed = require('blessed')
13 | , screen = blessed.screen()
14 |
15 | var page = -1
16 | var viewer = null
17 |
18 | if (!file) throw "please specify the presentation to render: \r\n\r\n $> doc-viewer doc.xml \r\n\r\n "
19 |
20 | var xml = fs.readFileSync(file)
21 |
22 | parse(xml, function (err, doc) {
23 | if (err) throw err
24 |
25 | if (!doc || !doc.document) throw "invalid document was provided"
26 |
27 |
28 | viewer = new Viewer(doc.document, screen)
29 | next()
30 |
31 | screen.key(['space', 'return'], function(ch, key) {
32 | next()
33 | });
34 |
35 | screen.key(['backspace'], function(ch, key) {
36 | prev()
37 | });
38 |
39 | screen.key(['q', 'escape'], function(ch, key) {
40 | process.exit()
41 | });
42 |
43 | });
44 |
45 | function prev() {
46 | if (page==0) return
47 | page--
48 | show()
49 |
50 | }
51 |
52 | function next() {
53 | if (page>=viewer.document.page.length-1) process.exit()
54 | page++
55 | show()
56 | }
57 |
58 | function show() {
59 | var err = viewer.renderPage(page)
60 | if (err!==null) console.log(err)
61 | }
62 |
63 | function showHelp() {
64 | console.log("\r\nusage: $> wopr [file.xml] \r\n\r\nuse spacebar / return to go forward in the presentation, backspace to go back, and ESC or q or CTRL+C to exit.\r\n\r\nhttps://github.com/yaronn/wopr\r\n\r\n")
65 | }
--------------------------------------------------------------------------------
/examples/doc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 |
9 |
10 |
11 |
12 | -
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 | -
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/examples/docs/images/charts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/docs/images/charts.png
--------------------------------------------------------------------------------
/examples/docs/images/map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/docs/images/map.png
--------------------------------------------------------------------------------
/examples/images/charts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/images/charts.png
--------------------------------------------------------------------------------
/examples/images/map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/images/map.png
--------------------------------------------------------------------------------
/examples/rich.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | -
7 |
8 |
20 |
21 |
29 |
30 |
38 |
39 |
48 |
49 |
57 |
58 |
63 |
64 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/examples/sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | -
8 |
9 |
10 |
11 | *wopr* is a markup format for rich terminal `reports`, `presentations` and `infographics`.
Build the report from any language and view it locally or remotely via curl.
More information:
[https://github.com/yaronn/wopr](https://github.com/yaronn/wopr)
[https://twitter.com/YaronNaveh](https://twitter.com/YaronNaveh)
12 |
13 |
14 |
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | -
31 |
32 |
33 |
34 |
35 |
36 |
37 | -
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | -
51 |
52 |
53 |
54 | ,1,02:17,21,13,20%,{red-fg}fail{/red-fg}
55 | ,2,02:15,33,11,25%,{blue-fg}pass{/blue-fg}
56 | ,3,03:22,15,12,80%,{red-fg}fail{/red-fg}
57 | ,4,05:00,11,18,12%,{red-fg}fail{/red-fg}
58 | ,5,02:17,17,12,0%,{blue-fg}pass{/blue-fg}
59 | ,6,01:01,23,29,80%,{red-fg}fail{/red-fg}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | -
73 |
74 |
75 |
76 | *wopr* is a markup format for rich terminal `reports`, `presentations` and `inforgraphics`.
Build the report from any language and view it locally or remotely via curl.
More information:
[https://github.com/yaronn/wopr](https://github.com/yaronn/wopr)
[https://twitter.com/YaronNaveh](https://twitter.com/YaronNaveh)
77 |
78 |
79 |
80 |
81 |
82 | -
83 |
84 |
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | -
147 |
148 |
149 |
150 | *Thank you!*
More information:
[https://github.com/yaronn/wopr](https://github.com/yaronn/wopr)
[https://twitter.com/YaronNaveh](https://twitter.com/YaronNaveh)
created by @YaronNaveh
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/lib/document-viewer.js:
--------------------------------------------------------------------------------
1 | var blessed = require('blessed')
2 | , contrib = require('blessed-contrib')
3 |
4 | function DocumentViewer(document, screen) {
5 | this.document = document
6 | this.screen = screen || blessed.screen()
7 | //console.log(JSON.stringify(document, null, 2))
8 | }
9 |
10 | DocumentViewer.prototype.renderPage = function(pageId, msg) {
11 |
12 | var self = this
13 |
14 | if (typeof(msg)=="undefined") msg = "press Return to continue"
15 |
16 | this.clear()
17 |
18 | var page = this.document.page[pageId]
19 |
20 | var grid = new contrib.grid({rows: 12, cols: 12, hideBorder: true, screen: this.screen, dashboardMargin: 5})
21 |
22 | for (var i in page.item) {
23 | var item = page.item[i]
24 | var className = Object.keys(item)[1]
25 | var ctor = contrib[className] || blessed[className]
26 | if (!ctor) {
27 | return "no such widget: " + className
28 | }
29 | var opts = this.readOptions(item[className][0], ctor)
30 |
31 | //workaround
32 | if (className=="picture") {
33 | opts.onReady = function() { self.screen.render() }
34 | }
35 |
36 | //workaround
37 | if (classname="bigtext" && !opts.font) {
38 | opts.font = __dirname + "/../lib/ter-u12n.json"
39 | }
40 |
41 | //console.log(JSON.stringify(opts, null, 2))
42 | grid.set(item.$.row, item.$.col, item.$.rowSpan, item.$.colSpan, ctor, opts)
43 | }
44 |
45 | var next = blessed.box({top: "90%", content: msg})
46 | this.screen.append(next)
47 |
48 | this.screen.render()
49 | return null
50 | }
51 |
52 | DocumentViewer.prototype.clear = function(node) {
53 | var i = this.screen.children.length
54 | while (i--) this.screen.children[i].detach()
55 | }
56 |
57 | DocumentViewer.prototype.readOptions = function(node, ctor) {
58 |
59 | var optionsMethod = ctor.prototype["getOptionsPrototype"]
60 | var optionsMainProto = optionsMethod==null ? null : optionsMethod()
61 | //console.log(optionsMainProto)
62 | return this.readOptionsInner(node, optionsMainProto)
63 | }
64 |
65 | DocumentViewer.prototype.readOptionsInner = function(node, optionsMainProto) {
66 | var res = {}
67 |
68 | var items = []
69 | for (var attr in node.$) {
70 | items.push({name: attr, value: node.$[attr]})
71 | }
72 |
73 | for (var n in node) {
74 | if (n!='$') items.push( { name: n, value: node[n] } )
75 | }
76 |
77 | for (var i=0; i0) res.push(self.convertArray(s, type[0][0]))
116 | }
117 | return res
118 | }
119 | else if (typeof(type[0])=="object") {
120 | var res = []
121 | for (var i=0; i1) page=parseInt(u.pathname.substr(1)) || 0
17 | var auto = u.query["auto"]==='' || u.query["auto/"]==='' //second can happen in zsh when quoting the string
18 | var msg = auto?'next slide will appear within a few seconds':undefined
19 |
20 | if (!body || body=="") {
21 | return cba("You must upload the document to present as the POST body")
22 | }
23 |
24 |
25 | parse(body, function (err, doc) {
26 | try {
27 |
28 | if (err) {
29 | return cba("Document xml is not valid: " + err)
30 | }
31 |
32 | if (!doc || !doc.document) return cba("document not valid or has no pages")
33 | if (!doc.document.page || doc.document.page.length==0) return cba("document must have at least one page")
34 |
35 | if (page>=doc.document.page.length) {
36 | return cba('\r\n\r\nPresentation has ended (total '+doc.document.page.length+' pages). Press CTRL+C to exit.\r\n\r\n')
37 | }
38 |
39 | req.connection.on('close',function(){
40 | screen = null
41 | });
42 |
43 | var screen = contrib.createScreen(req, res)
44 | if (screen==null) return
45 |
46 | viewer = new Viewer(doc.document, screen)
47 | var err = viewer.renderPage(page, msg)
48 | if (err!==null) {
49 | clean(screen)
50 | return cba(err)
51 | }
52 |
53 | //note the setTimeout is necessary even if delay is 0
54 | setTimeout(function() {
55 | //restore cursor
56 | res.end('\033[?25h')
57 | clean(screen)
58 | return cba()
59 | }, auto?5000:0)
60 |
61 | }
62 |
63 | catch (e) {
64 | return cba(e)
65 | }
66 |
67 |
68 | })
69 | }
70 |
71 | function clean(screen) {
72 | //TODO this code is very sensitive to blessed versions, need to check right version/usage
73 | //screen.program.destroy()
74 | //screen.destroy()
75 | }
76 |
77 | module.exports = present
78 |
79 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | var blessed = require('blessed')
4 |
5 | patchBlessed()
6 |
7 | var http = require('http')
8 | , url = require('url')
9 | , fs = require('fs')
10 | , present = require('./presenter')
11 | , contrib = require('blessed-contrib')
12 |
13 | var port = process.env.PORT || 1337
14 |
15 | http.createServer(function (req, res) {
16 |
17 | if (req.method == 'POST') {
18 | var body = '';
19 | req.on('data', function (data) {
20 | body += data;
21 |
22 | // Too much POST data, kill the connection!
23 | if (body.length > 1e6)
24 | req.connection.destroy();
25 | });
26 | req.on('end', function () {
27 | present(req, res, body, function(err) {
28 | if (err) console.log(new Error().stack)
29 | if (err) return contrib.serverError(req, res, err)
30 | })
31 | });
32 | }
33 | else {
34 | if (req.headers["user-agent"] && req.headers["user-agent"].indexOf('curl')!=-1) {
35 |
36 | var content = fs.readFileSync(__dirname+'/../examples/sample.xml')
37 | present(req, res, content, function(err) {
38 | if (err) console.log(new Error().stack)
39 | if (err) return contrib.serverError(req, res, err)
40 | })
41 | return
42 | }
43 | else {
44 | res.writeHead(301, {'Location': 'https://github.com/yaronn/wopr'});
45 | res.end()
46 | return
47 | }
48 | }
49 |
50 |
51 | }).listen(port);
52 |
53 | /*in the context of web server the following code will leak:
54 |
55 | http.createServer(function (req, res) {
56 | var s = contrib.createScreen(req, res)
57 | //alternatively:
58 | //var program = new blessed.Program()
59 |
60 | setTimeout(function() {res.end(Date.now() + "")}, 0)
61 |
62 | }).listen(8080);
63 |
64 | this comes down to event registrations on program (the latest commit here points them https://github.com/yaronn/blessed-patch-temp)
65 | */
66 | function patchBlessed() {
67 | blessed.Program.prototype.listem = function() {}
68 | process.on = function() {}
69 | }
70 |
71 | console.log('Server running at http://127.0.0.1:'+port+'/');
72 |
--------------------------------------------------------------------------------
/server/test/test.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | p=0; while true; do curl -NL --data ' ' http://localhost:1337/?$((p++)); read; done
6 |
7 |
8 |
9 |
10 | a=$(curl -s http:// www.netscape.com/); echo "$a"
11 |
12 |
13 |
14 | a=$(curl -s https://gist.githubusercontent.com/yaronn/6a8f6947fedd88f783d3/raw/12a9f3216769834f356a458f0b8ee6049e59206d/gistfile1.txt); p=0; while true; do curl -NL --data "$a" http://localhost:1337/$((p++)); read; done
15 |
16 |
17 |
18 | p=0; while true; do curl tty.zone/$((p++)); read; done
19 |
20 |
21 | sudo PORT=8080 FORCE_COLOR=1 forever start server.js
22 | sudo PORT=8081 FORCE_COLOR=1 forever start server.js
23 |
24 |
25 | curl -N tty.zone/[0-2]?auto
26 |
27 | curl -N ec2-23-21-64-152.compute-1.amazonaws.com/[0-2]?auto
28 |
29 |
30 |
31 | curl -N --data ' ' tty.zone
32 |
33 |
34 | nginx:
35 |
36 | http {
37 | upstream my {
38 | server localhost:8080;
39 | server localhost:8081;
40 | }
41 | server {
42 | listen 80;
43 | server_name tty.zone www.tty.zone;
44 | location / {
45 | proxy_pass http://my;
46 | proxy_buffering off;
47 | }
48 | }
49 |
50 | ...
51 | }
52 |
53 | top + CTRL+I to get process cpu percent per all cores
54 |
55 | cat /var/log/nginx/error.log | tail -10
56 | cat /var/log/nginx/access.log | tail -10
57 |
58 |
59 |
60 |
61 | //weather parse
62 |
63 | var str="http://www.w.com/?markerCount=37&lon0=-122.33&lat0=47.61&char0=61&lon1=-119.77&lat1=36.75&char1=78&lon2=-115.14&lat2=36.17&char2=71&lon3=-116.2&lat3=43.61&char3=61&lon4=-111.89&lat4=40.76&char4=55&lon5=-112.07&lat5=33.45&char5=81&lon6=-108.5&lat6=45.78&char6=61&lon7=-104.82&lat7=41.14&char7=50&lon8=-104.98&lat8=39.74&char8=48&lon9=-106.65&lat9=35.08&char9=52&lon10=-100.78&lat10=46.81&char10=56&lon11=-103.23&lat11=44.08&char11=59&lon12=-95.94&lat12=41.26&char12=67&lon13=-94.58&lat13=39.1&char13=69&lon14=-97.52&lat14=35.47&char14=64&lon15=-96.81&lat15=32.78&char15=60&lon16=-95.36&lat16=29.76&char16=67&lon17=-93.26&lat17=44.98&char17=55&lon18=-93.61&lat18=41.6&char18=66&lon19=-92.29&lat19=34.75&char19=63&lon20=-90.23&lat20=39.73&char20=57&lon21=-90.08&lat21=29.95&char21=64&lon22=-87.91&lat22=43.04&char22=43&lon23=-87.64&lat23=41.51&char23=52&lon24=-83.05&lat24=42.33&char24=40&lon25=-86.16&lat25=39.77&char25=54&lon26=-85.76&lat26=38.25&char26=56&lon27=-86.78&lat27=36.17&char27=57&lon28=-90.05&lat28=35.15&char28=64&lon29=-84.39&lat29=33.75&char29=52&lon30=-81.38&lat30=28.54&char30=78&lon31=-80.19&lat31=25.77&char31=75&lon32=-94.7&lat32=37.41&char32=57&lon33=-84.29&lat33=37.75&char33=54&lon34=-77.04&lat34=38.9&char34=50&lon35=-75.5&lat35=43&char35=38&lon36=-71.06&lat36=42.36&char36=32"
64 |
65 |
66 | var url = require('url')
67 |
68 | var q = url.parse(str, true).query
69 |
70 |
71 | var res = ""
72 |
73 | for (var i=0; i\r\n'
81 | }
82 |
83 | console.log(res)
--------------------------------------------------------------------------------