├── README.md ├── main.py ├── scripts └── jott ├── static ├── MathJax.js ├── edit_animation.js ├── favicon.ico ├── gif.js ├── gif.js.map ├── gif.worker.js ├── gif.worker.js.map ├── jott-icon.png ├── jott.png └── texdown.js └── templates ├── code.html ├── edit.html ├── edit_animation.html ├── edit_texdown.html ├── index.html ├── render.html └── texdown.html /README.md: -------------------------------------------------------------------------------- 1 | # jott 2 | A minimal tool for quickly writing and sharing notes. Check out https://jott.live for a demo. 3 | ![jott](https://jott.live/static/jott.png?) 4 | 5 | ### Command line 6 | 7 | Use `curl` directly for quick uploading. 8 | 9 | ``` 10 | $ echo "test" | curl -F 'note=<-' https://jott.live/save/raw// 11 | ``` 12 | 13 | Alternatively, the `jott` script in `jott/scripts` makes it easier to upload and read notes. 14 | 15 | To install the script without downloading the repo: 16 | ``` 17 | $ curl https://jott.live/raw/note/note_script > jott.sh && chmod +x jott.sh && alias jott='./jott.sh' 18 | ``` 19 | 20 | Upload a note by piping through `stdin`, `jott [note name] [password]` 21 | ``` 22 | $ echo "this is a test" | jott my_test_note secret_password 23 | Success! Note "my_test_note" saved 24 | ``` 25 | Download a note with `jott [note name]` 26 | ``` 27 | $ jott my_test_note 28 | this is a test 29 | ``` 30 | Delete a note with `jott -d [note name] [password]` 31 | ``` 32 | $ jott -d my_test_note secret_password 33 | Success! Note "my_test_note" deleted 34 | ``` 35 | 36 | ### Website 37 | 38 | - `/edit/note/` will create a blank note. 39 | - `/edit/note/` will edit an existing note. 40 | - `/note/` will return the default HTML rendering of the note. 41 | - `/raw/` will return the note as raw text. (Useful with wget/curl.) 42 | - `/texdown/` will return a minimal [TeXDown](https://github.com/tex-ninja/texdown#texdown) rendering of the note. [Example](https://jott.live/texdown/note/test) 43 | - `/code/` will syntax highlight the note. 44 | - `/edit/note/` will open a basic editor for the note. 45 | 46 | 47 | ## Installation 48 | Although you can use https://jott.live to test out this project, do not rely on it for anything important. 49 | 50 | If you find this useful, I'd recommend hosting your own instance. It is quite lightweight. 51 | 52 | Requirements: 53 | - flask (`pip install flask`) 54 | 55 | Run the server with 56 | ``` 57 | FLASK_ENV=prod python3 main.py 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, g, abort, redirect, url_for 2 | import sqlite3 3 | import time 4 | from textwrap import dedent 5 | 6 | app = Flask(__name__) 7 | 8 | # Singleton pattern (because threading) 9 | def get_db(name): 10 | if app.config["DEBUG"]: 11 | name = "{}_debug".format(name) 12 | db = getattr(g, "_database_{}".format(name), None) 13 | db_list = getattr(g, "_db_list", []) 14 | if db is None: 15 | db = g._database = sqlite3.connect("{}.db".format(name)) 16 | db_list.append(name) 17 | return db 18 | 19 | 20 | def get_ip(): 21 | ip_forward = request.headers.getlist("X-Forwarded-For") 22 | if ip_forward: 23 | return ip_forward[0] 24 | else: 25 | return request.remote_addr 26 | 27 | 28 | @app.route("/") 29 | def index(): 30 | conn = get_db("notes") 31 | c = conn.cursor() 32 | c.execute("SELECT COUNT(*) FROM notes") 33 | count = c.fetchone()[0] 34 | return render_template("index.html", count=count) 35 | 36 | 37 | def edit_impl(name, template): 38 | note = "" 39 | if name: 40 | conn = get_db("notes") 41 | c = conn.cursor() 42 | c.execute("SELECT note FROM notes WHERE name=?", (name,)) 43 | result = c.fetchone() 44 | note = result[0] if result else "" 45 | return render_template(template, name=name, note=note) 46 | 47 | 48 | @app.route("/edit/note/") 49 | @app.route("/edit/note/") 50 | def edit_note(name=""): 51 | return edit_impl(name, "edit.html") 52 | 53 | 54 | @app.route("/edit") 55 | @app.route("/edit/") 56 | @app.route("/note") 57 | @app.route("/note/") 58 | def redirect_note(): 59 | return redirect(url_for("edit_note", _external=True)) 60 | 61 | 62 | @app.route("/edit/texdown/") 63 | @app.route("/edit/texdown/") 64 | def edit_texdown(name=""): 65 | return edit_impl(name, "edit_texdown.html") 66 | 67 | @app.route("/edit/animation/") 68 | @app.route("/edit/animation/") 69 | def edit_animation(name=""): 70 | return edit_impl(name, "edit_animation.html") 71 | 72 | 73 | @app.route("/texdown") 74 | @app.route("/texdown/") 75 | def redirect_texdown(): 76 | return redirect(url_for("edit_texdown", _external=True)) 77 | 78 | 79 | # Used by both save_note and save_raw 80 | def save_note_impl(name, note, key): 81 | if len(name) > 99 or len(name) < 1: 82 | return "Note name length must be between 1 and 100 characters\n", 403 83 | 84 | if len(key) > 99: 85 | return "Note key length must be under 100 characters\n", 403 86 | 87 | if len(note) > 10000: 88 | return "Note length must be under 10K characters\n", 403 89 | 90 | conn = get_db("notes") 91 | c = conn.cursor() 92 | c.execute("SELECT key FROM notes WHERE name=?", (name,)) 93 | 94 | saved_key = c.fetchone() 95 | if saved_key and saved_key[0] != key: 96 | return "Note already saved with different key\n", 403 97 | 98 | ip = get_ip() 99 | date = int(time.time()) 100 | 101 | c.execute( 102 | """ 103 | SELECT count, date FROM ips WHERE ip=? 104 | """, 105 | (ip,), 106 | ) 107 | count_lookup = c.fetchone() 108 | 109 | if count_lookup: 110 | time_since = date - count_lookup[1] 111 | count = count_lookup[0] + 1 112 | if time_since < 5: 113 | return "Rate limit reached. Please try again later.\n", 403 114 | else: 115 | count = 1 116 | c.execute( 117 | """ 118 | INSERT INTO ips VALUES (?, ?, ?) 119 | """, 120 | (ip, count, date), 121 | ) 122 | 123 | c.execute( 124 | """ 125 | UPDATE ips SET count=?, date=? WHERE ip=? 126 | """, 127 | (count, date, ip), 128 | ) 129 | 130 | c.execute( 131 | """ 132 | DELETE FROM notes WHERE name=? 133 | """, 134 | (name,), 135 | ) 136 | 137 | c.execute( 138 | """ 139 | INSERT INTO notes VALUES (?, ?, ?, ?, ?) 140 | """, 141 | (name, note, key, ip, date), 142 | ) 143 | 144 | conn.commit() 145 | return "https://jott.live/note/{}\n".format(name) 146 | 147 | 148 | @app.route("/save/", methods=["POST"]) 149 | @app.route("/save//", methods=["POST"]) 150 | @app.route("/save//", methods=["POST"]) 151 | @app.route("/save/raw//", methods=["POST"]) 152 | @app.route("/save/raw//", methods=["POST"]) 153 | @app.route("/save/note//", methods=["POST"]) 154 | @app.route("/save/note//", methods=["POST"]) 155 | def save(name, key=""): 156 | if request.json: 157 | note = request.json["note"] 158 | elif request.form: 159 | note = request.form["note"] 160 | else: 161 | return "Invalid payload\n", 403 162 | return save_note_impl(name, note, key) 163 | 164 | 165 | @app.route("/delete/note//", methods=["GET"]) 166 | @app.route("/delete/note//", methods=["GET"]) 167 | def delete_note(name, key=""): 168 | conn = get_db("notes") 169 | c = conn.cursor() 170 | c.execute("SELECT key FROM notes WHERE name=?", (name,)) 171 | saved_key = c.fetchone() 172 | if saved_key and saved_key[0] != key: 173 | return "Incorrect key\n", 403 174 | c.execute( 175 | """ 176 | DELETE FROM notes WHERE name=? 177 | """, 178 | (name,), 179 | ) 180 | conn.commit() 181 | return 'Success! Note "{}" deleted\n'.format(name) 182 | 183 | 184 | @app.route("/note/") 185 | def note(name): 186 | conn = get_db("notes") 187 | c = conn.cursor() 188 | c.execute("SELECT note FROM notes WHERE name=?", (name,)) 189 | result = c.fetchone() 190 | note = result[0] if result else "" 191 | return render_template("render.html", name=name, note=note) 192 | 193 | 194 | @app.route("/texdown/") 195 | @app.route("/texdown/note/") 196 | def texdown_note(name): 197 | conn = get_db("notes") 198 | c = conn.cursor() 199 | c.execute("SELECT note FROM notes WHERE name=?", (name,)) 200 | result = c.fetchone() 201 | note = result[0] if result else "" 202 | return render_template("texdown.html", name=name, note=note) 203 | 204 | 205 | @app.route("/code/") 206 | @app.route("/code/note/") 207 | def code_note(name): 208 | conn = get_db("notes") 209 | c = conn.cursor() 210 | c.execute("SELECT note FROM notes WHERE name=?", (name,)) 211 | result = c.fetchone() 212 | note = result[0] if result else "" 213 | return render_template("code.html", name=name, note=note) 214 | 215 | 216 | @app.route("/raw/") 217 | @app.route("/raw/note/") 218 | def raw_note(name): 219 | conn = get_db("notes") 220 | c = conn.cursor() 221 | c.execute("SELECT note FROM notes WHERE name=?", (name,)) 222 | result = c.fetchone() 223 | note = result[0] if result else "" 224 | return note 225 | 226 | 227 | @app.before_request 228 | def track(): 229 | ip = get_ip() 230 | path = request.full_path 231 | 232 | if path[-1] == "?": 233 | path = path[:-1] 234 | 235 | referrer = request.referrer if request.referrer else "None" 236 | 237 | conn = get_db("notes") 238 | c = conn.cursor() 239 | 240 | # Update ip -> path stats 241 | c.execute("SELECT count FROM visits WHERE ip=? AND path=?", (ip, path)) 242 | result = c.fetchone() 243 | if result: 244 | count = result[0] + 1 245 | else: 246 | count = 1 247 | c.execute( 248 | """ 249 | INSERT INTO visits VALUES (?, ?, ?) 250 | """, 251 | (ip, path, count), 252 | ) 253 | c.execute( 254 | """ 255 | UPDATE visits SET count=? WHERE ip=? AND path=? 256 | """, 257 | (count, ip, path), 258 | ) 259 | 260 | # Update referrer -> path stats 261 | c.execute( 262 | """ 263 | SELECT COUNT(*) FROM referrals 264 | WHERE referrer=? AND ip=? AND path=? 265 | """, 266 | (referrer, ip, path), 267 | ) 268 | result = c.fetchone() 269 | if result[0] == 0: 270 | c.execute( 271 | """ 272 | INSERT INTO referrals VALUES (?, ?, ?) 273 | """, 274 | (referrer, ip, path), 275 | ) 276 | conn.commit() 277 | 278 | 279 | @app.route("/stats") 280 | def stats(): 281 | conn = get_db("notes") 282 | c = conn.cursor() 283 | c.execute("SELECT COUNT(*) FROM ips") 284 | result = c.fetchone() 285 | author_count = result[0] if result else 0 286 | 287 | c.execute("SELECT COUNT(*) FROM notes") 288 | result = c.fetchone() 289 | note_count = result[0] if result else 0 290 | 291 | c.execute("SELECT COUNT(DISTINCT ip) FROM visits") 292 | result = c.fetchone() 293 | visit_count = result[0] if result else 0 294 | 295 | # Top most visited paths 296 | c.execute( 297 | """ 298 | SELECT path, COUNT(DISTINCT ip) as sum 299 | FROM visits 300 | WHERE 301 | path NOT LIKE '/save/%' AND 302 | path NOT LIKE '/static/%' 303 | GROUP BY path 304 | ORDER BY sum DESC 305 | """ 306 | ) 307 | top_paths = [] 308 | for i in range(10): 309 | result = c.fetchone() 310 | if result: 311 | top_paths.append(result) 312 | top_paths_str = "\n".join( 313 | [" {}: {}".format(path, count) for path, count in top_paths] 314 | ) 315 | 316 | # Top referrers 317 | c.execute( 318 | """ 319 | SELECT referrer, COUNT(DISTINCT ip) as sum 320 | FROM referrals 321 | GROUP BY referrer 322 | ORDER BY sum DESC 323 | """ 324 | ) 325 | top_referrers = [] 326 | for i in range(10): 327 | result = c.fetchone() 328 | if result: 329 | if str(result[0]) == 'None': 330 | continue 331 | top_referrers.append(result) 332 | top_referrers_str = "\n".join( 333 | [" {}: {}".format(referrer, count) for referrer, count in top_referrers] 334 | ) 335 | 336 | return dedent( 337 | """\ 338 | visitors: {} 339 | notes: {} 340 | authors: {} 341 | top paths: 342 | {} 343 | top referrers: 344 | {} 345 | """ 346 | ).format(visit_count, note_count, author_count, top_paths_str, top_referrers_str) 347 | 348 | 349 | @app.before_first_request 350 | def initialize_dbs(): 351 | db = get_db("notes") 352 | db.execute( 353 | """ 354 | CREATE TABLE IF NOT EXISTS notes (name text, note text, key text, author_ip text, date integer) 355 | """ 356 | ) 357 | db.execute( 358 | """ 359 | CREATE TABLE IF NOT EXISTS ips (ip text, count integer, date integer) 360 | """ 361 | ) 362 | db.execute( 363 | """ 364 | CREATE TABLE IF NOT EXISTS visits (ip text, path text, count integer) 365 | """ 366 | ) 367 | db.execute( 368 | """ 369 | CREATE TABLE IF NOT EXISTS referrals (referrer text, ip text, path text) 370 | """ 371 | ) 372 | db.commit() 373 | 374 | 375 | @app.teardown_appcontext 376 | def close_connection(exception): 377 | db_list = getattr(g, "_db_list", []) 378 | for db_name in db_list: 379 | db = get_db(db_name) 380 | db.close() 381 | 382 | 383 | app.run("0.0.0.0", port=8000 if app.config["DEBUG"] else 5000) 384 | -------------------------------------------------------------------------------- /scripts/jott: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ] 3 | then 4 | echo "Error, please provide a note name" 5 | exit 1 6 | fi 7 | 8 | if ! [ ! -t 0 ]; then 9 | if [ "$1" = "-d" ] 10 | then 11 | if [ -z "$2" ] 12 | then 13 | echo "Error, please provide a note name" 14 | exit 1 15 | fi 16 | curl https://jott.live/delete/note/$2/$3 17 | exit 18 | fi 19 | 20 | curl https://jott.live/raw/note/$1 21 | exit 22 | fi 23 | 24 | python -c 'import json,sys; print(json.dumps({"note":sys.stdin.read()}))' | curl -H "Content-Type: application/json" -d @- -X POST https://jott.live/save/note/$1/$2 25 | -------------------------------------------------------------------------------- /static/edit_animation.js: -------------------------------------------------------------------------------- 1 | let code_mirror = null; 2 | function save_note(button, textarea, input) { 3 | let note = textarea.value; 4 | if (code_mirror) { 5 | note = code_mirror.doc.getValue(); 6 | } 7 | let name_pass = input.value.split('#'); 8 | let name = encodeURIComponent(name_pass[0]); 9 | if (name == '') { 10 | alert("Note needs a name"); 11 | return; 12 | } 13 | let pass = ''; 14 | if (name_pass.length > 1) { 15 | pass = encodeURIComponent(name_pass[1]); 16 | } 17 | let httpRequest = new XMLHttpRequest(); 18 | httpRequest.onreadystatechange = function(){ 19 | if (httpRequest.readyState == 4) { 20 | if (httpRequest.status != 200) { 21 | alert("Error: " + httpRequest.response); 22 | } else { 23 | var newurl = window.location.origin + 24 | '/edit/animation/' + 25 | name; 26 | window.history.pushState( 27 | { path: newurl }, 28 | '', 29 | newurl); 30 | button.classList.toggle('dimmed', true); 31 | button.onclick = function() {}; 32 | setTimeout(function(){ 33 | button.classList.toggle('dimmed', false); 34 | button.onclick = function() {save_note( 35 | this, 36 | document.getElementById('note'), 37 | document.getElementById('name') 38 | )}; 39 | 40 | }, 5000); 41 | } 42 | } 43 | }; 44 | httpRequest.open('POST', 45 | window.location.origin + 46 | '/save/note/' + 47 | name + '/' + pass, 48 | true); 49 | httpRequest.setRequestHeader('Content-Type', 'application/json'); 50 | httpRequest.send(JSON.stringify({ 51 | note: note}) 52 | ); 53 | } 54 | 55 | let canvas = null; 56 | let __interval = null; 57 | let __num_frames = 100; 58 | let __framerate = 20; 59 | let __width = 300; 60 | let __height = 200; 61 | let __background_color = 'white'; 62 | 63 | function updateFramerate(target) { 64 | __framerate = Number.parseInt(target.value); 65 | render(); 66 | } 67 | function updateHeight(target) { 68 | __height = Number.parseInt(target.value); 69 | render(); 70 | } 71 | function updateWidth(target) { 72 | __width = Number.parseInt(target.value); 73 | render(); 74 | } 75 | function updateNumFrames(target) { 76 | __num_frames = Number.parseInt(target.value); 77 | render(); 78 | } 79 | function render_error(err, target) { 80 | canvas.width = 0; 81 | canvas.height = 0; 82 | canvas.style.width = 0 + 'px'; 83 | canvas.style.height = 0 + 'px'; 84 | let errDiv = document.getElementById('error'); 85 | errDiv.textContent = err + '\n\n' + err.stack; 86 | } 87 | 88 | function render_impl(contents, target, capturer = null) { 89 | document.getElementById('error').textContent = ''; 90 | document.getElementById('framerate').value = __framerate; 91 | document.getElementById('frames').value = __num_frames; 92 | document.getElementById('width').value = __width; 93 | document.getElementById('height').value = __height; 94 | 95 | if (canvas == null) { 96 | canvas = document.getElementById('canvas'); 97 | } 98 | canvas.width = __width; 99 | canvas.height = __height; 100 | canvas.style.width = __width + 'px'; 101 | canvas.style.height = __height + 'px'; 102 | try { 103 | eval(contents); 104 | } catch(e) { 105 | render_error(e); 106 | } 107 | // Weird names to prevent collision with user code 108 | let __ctx = canvas.getContext("2d"); 109 | let __iters = __num_frames; 110 | let __i = 0; 111 | if (__interval) { 112 | clearInterval(__interval); 113 | } 114 | let capture = false; 115 | if (capturer) { 116 | capture = true; 117 | } 118 | __interval = setInterval(function() { 119 | try { 120 | __ctx.clearRect(0, 0, canvas.width, canvas.height); 121 | __ctx.fillStyle = __background_color; 122 | __ctx.fillRect(0, 0, canvas.width, canvas.height); 123 | loop(__ctx, __i++); 124 | if (capture) { 125 | capturer.addFrame(__ctx, {copy:true, delay: 1000/__framerate}); 126 | let info = document.getElementById('info'); 127 | info.textContent = 'Recording... ' + Math.floor(100 * __i/__iters) + '%'; 128 | } 129 | } catch(e) { 130 | clearInterval(__interval); 131 | render_error(e); 132 | } 133 | if (__i >= __iters) { 134 | if (capture) { 135 | capturer.render(); 136 | capture = false; 137 | } 138 | __i = 0; 139 | } 140 | }, 1000 / __framerate); 141 | } 142 | 143 | function render() { 144 | render_impl(code_mirror.doc.getValue(), 145 | document.getElementById('note-output')); 146 | } 147 | 148 | function capture(button) { 149 | let capturer = new GIF({ 150 | workerScript: '/static/gif.worker.js', 151 | workers: 4, 152 | quality: 1, 153 | height: __height, 154 | width: __width, 155 | }); 156 | 157 | let info = document.getElementById('info'); 158 | capturer.on('progress', function(p) { 159 | info.textContent = 'Rendering... ' + Math.floor(100 * p) + '%'; 160 | }); 161 | capturer.on('finished', function(blob) { 162 | let img = document.getElementById('output'); 163 | img.src = URL.createObjectURL(blob); 164 | info.textContent = 'Right click to download' 165 | }); 166 | 167 | render_impl(code_mirror.doc.getValue(), 168 | document.getElementById('note-output'), 169 | capturer); 170 | } 171 | 172 | function drawPoint(ctx, x, y, options={}) { 173 | let radius = options.radius || 2; 174 | ctx.beginPath(); 175 | ctx.arc(x, y, radius, 0, 2 * Math.PI); 176 | ctx.stroke(); 177 | } 178 | 179 | function drawFunction(ctx, f, x_min, x_max, options={}) { 180 | // Flip for clarity 181 | let func = (function(x) { return - f(x); }).bind({}); 182 | ctx.beginPath(); 183 | 184 | let y_offset = canvas.height - options.y_offset || canvas.height / 2; 185 | let x_offset = options.x_offset || canvas.width / 2; 186 | 187 | let y_scale = options.y_scale || 1; 188 | let x_scale = options.x_scale || 1; 189 | 190 | ctx.moveTo((x_offset + x_min) * x_scale, (y_offset + func(x_min) * y_scale)); 191 | for (let x = x_min; x < x_max; ++x) { 192 | ctx.lineTo((x_offset + x * x_scale), (y_offset + func(x) * y_scale)); 193 | ctx.moveTo((x_offset + x * x_scale), (y_offset + func(x) * y_scale)); 194 | } 195 | ctx.stroke(); 196 | 197 | return function(_x, _y) { 198 | return [x_offset + _x * x_scale, y_offset - _y * y_scale]; 199 | } 200 | } 201 | 202 | window.addEventListener('load', function() { 203 | code_mirror = CodeMirror.fromTextArea(document.getElementById('note'), 204 | { 205 | mode: "javascript", 206 | theme: "zenburn", 207 | }); 208 | 209 | let savedContents = localStorage.getItem("saved"); 210 | if (savedContents) { 211 | code_mirror.doc.setValue(savedContents); 212 | } 213 | render(); 214 | 215 | let timeout = null; 216 | code_mirror.on("change", function(){ 217 | if (timeout) { 218 | clearTimeout(timeout); 219 | } 220 | timeout = setTimeout(function() { 221 | let contents = code_mirror.doc.getValue(); 222 | localStorage.setItem("saved", contents); 223 | render(); 224 | timeout = null; 225 | }, 500); 226 | }); 227 | }); 228 | 229 | 230 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwasti/jott/57a7f5e2a90ccf1e2c5760e7af769a9dfa21dc8e/static/favicon.ico -------------------------------------------------------------------------------- /static/gif.js: -------------------------------------------------------------------------------- 1 | // gif.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;iref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?jref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?jnumWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?jref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.setFill=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)}); 3 | //# sourceMappingURL=gif.js.map 4 | -------------------------------------------------------------------------------- /static/gif.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["node_modules/browser-pack/_prelude.js","node_modules/events/events.js","src/browser.coffee","src/gif.coffee"],"names":["f","exports","module","define","amd","g","window","global","self","this","GIF","e","t","n","r","s","o","u","a","require","i","Error","code","l","call","length","1","EventEmitter","_events","_maxListeners","undefined","prototype","defaultMaxListeners","setMaxListeners","isNumber","isNaN","TypeError","emit","type","er","handler","len","args","listeners","error","isObject","arguments","err","context","isUndefined","isFunction","Array","slice","apply","addListener","listener","m","newListener","push","warned","console","trace","on","once","fired","removeListener","list","position","splice","removeAllListeners","key","ret","listenerCount","evlistener","emitter","arg","UA","browser","mode","platform","ua","navigator","userAgent","toLowerCase","match","document","documentMode","name","version","parseFloat","parseInt","extend","child","parent","hasProp","ctor","constructor","__super__","superClass","defaults","frameDefaults","workerScript","workers","repeat","background","quality","width","height","transparent","debug","dither","delay","copy","options","base","value","running","frames","freeWorkers","activeWorkers","setOptions","setOption","_canvas","results","addFrame","image","frame","ImageData","data","CanvasRenderingContext2D","WebGLRenderingContext","getContextData","childNodes","getImageData","render","j","numWorkers","ref","nextFrame","finishedFrames","imageParts","spawnWorkers","globalPalette","renderNextFrame","abort","worker","shift","log","terminate","Math","min","forEach","_this","Worker","onmessage","event","indexOf","frameFinished","index","finishRendering","k","len1","len2","len3","offset","page","ref1","ref2","pageSize","cursor","round","Uint8Array","set","Blob","task","getTask","postMessage","ctx","createElement","getContext","setFill","fillRect","drawImage","last","canTransfer"],"mappings":"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","sourceRoot":"","sourcesContent":["(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o 0 && this._events[type].length > m) {\n this._events[type].warned = true;\n console.error('(node) warning: possible EventEmitter memory ' +\n 'leak detected. %d listeners added. ' +\n 'Use emitter.setMaxListeners() to increase limit.',\n this._events[type].length);\n if (typeof console.trace === 'function') {\n // not supported in IE 10\n console.trace();\n }\n }\n }\n\n return this;\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.once = function(type, listener) {\n if (!isFunction(listener))\n throw TypeError('listener must be a function');\n\n var fired = false;\n\n function g() {\n this.removeListener(type, g);\n\n if (!fired) {\n fired = true;\n listener.apply(this, arguments);\n }\n }\n\n g.listener = listener;\n this.on(type, g);\n\n return this;\n};\n\n// emits a 'removeListener' event iff the listener was removed\nEventEmitter.prototype.removeListener = function(type, listener) {\n var list, position, length, i;\n\n if (!isFunction(listener))\n throw TypeError('listener must be a function');\n\n if (!this._events || !this._events[type])\n return this;\n\n list = this._events[type];\n length = list.length;\n position = -1;\n\n if (list === listener ||\n (isFunction(list.listener) && list.listener === listener)) {\n delete this._events[type];\n if (this._events.removeListener)\n this.emit('removeListener', type, listener);\n\n } else if (isObject(list)) {\n for (i = length; i-- > 0;) {\n if (list[i] === listener ||\n (list[i].listener && list[i].listener === listener)) {\n position = i;\n break;\n }\n }\n\n if (position < 0)\n return this;\n\n if (list.length === 1) {\n list.length = 0;\n delete this._events[type];\n } else {\n list.splice(position, 1);\n }\n\n if (this._events.removeListener)\n this.emit('removeListener', type, listener);\n }\n\n return this;\n};\n\nEventEmitter.prototype.removeAllListeners = function(type) {\n var key, listeners;\n\n if (!this._events)\n return this;\n\n // not listening for removeListener, no need to emit\n if (!this._events.removeListener) {\n if (arguments.length === 0)\n this._events = {};\n else if (this._events[type])\n delete this._events[type];\n return this;\n }\n\n // emit removeListener for all listeners on all events\n if (arguments.length === 0) {\n for (key in this._events) {\n if (key === 'removeListener') continue;\n this.removeAllListeners(key);\n }\n this.removeAllListeners('removeListener');\n this._events = {};\n return this;\n }\n\n listeners = this._events[type];\n\n if (isFunction(listeners)) {\n this.removeListener(type, listeners);\n } else if (listeners) {\n // LIFO order\n while (listeners.length)\n this.removeListener(type, listeners[listeners.length - 1]);\n }\n delete this._events[type];\n\n return this;\n};\n\nEventEmitter.prototype.listeners = function(type) {\n var ret;\n if (!this._events || !this._events[type])\n ret = [];\n else if (isFunction(this._events[type]))\n ret = [this._events[type]];\n else\n ret = this._events[type].slice();\n return ret;\n};\n\nEventEmitter.prototype.listenerCount = function(type) {\n if (this._events) {\n var evlistener = this._events[type];\n\n if (isFunction(evlistener))\n return 1;\n else if (evlistener)\n return evlistener.length;\n }\n return 0;\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n return emitter.listenerCount(type);\n};\n\nfunction isFunction(arg) {\n return typeof arg === 'function';\n}\n\nfunction isNumber(arg) {\n return typeof arg === 'number';\n}\n\nfunction isObject(arg) {\n return typeof arg === 'object' && arg !== null;\n}\n\nfunction isUndefined(arg) {\n return arg === void 0;\n}\n","### CoffeeScript version of the browser detection from MooTools ###\n\nua = navigator.userAgent.toLowerCase()\nplatform = navigator.platform.toLowerCase()\nUA = ua.match(/(opera|ie|firefox|chrome|version)[\\s\\/:]([\\w\\d\\.]+)?.*?(safari|version[\\s\\/:]([\\w\\d\\.]+)|$)/) or [null, 'unknown', 0]\nmode = UA[1] == 'ie' && document.documentMode\n\nbrowser =\n name: if UA[1] is 'version' then UA[3] else UA[1]\n version: mode or parseFloat(if UA[1] is 'opera' && UA[4] then UA[4] else UA[2])\n\n platform:\n name: if ua.match(/ip(?:ad|od|hone)/) then 'ios' else (ua.match(/(?:webos|android)/) or platform.match(/mac|win|linux/) or ['other'])[0]\n\nbrowser[browser.name] = true\nbrowser[browser.name + parseInt(browser.version, 10)] = true\nbrowser.platform[browser.platform.name] = true\n\nmodule.exports = browser\n","{EventEmitter} = require 'events'\nbrowser = require './browser.coffee'\n\nclass GIF extends EventEmitter\n\n defaults =\n workerScript: 'gif.worker.js'\n workers: 2\n repeat: 0 # repeat forever, -1 = repeat once\n background: '#fff'\n quality: 10 # pixel sample interval, lower is better\n width: null # size derermined from first frame if possible\n height: null\n transparent: null\n debug: false\n dither: false # see GIFEncoder.js for dithering options\n\n frameDefaults =\n delay: 500 # ms\n copy: false\n\n constructor: (options) ->\n @running = false\n\n @options = {}\n @frames = []\n\n @freeWorkers = []\n @activeWorkers = []\n\n @setOptions options\n for key, value of defaults\n @options[key] ?= value\n\n setOption: (key, value) ->\n @options[key] = value\n if @_canvas? and key in ['width', 'height']\n @_canvas[key] = value\n\n setOptions: (options) ->\n @setOption key, value for own key, value of options\n\n addFrame: (image, options={}) ->\n frame = {}\n frame.transparent = @options.transparent\n for key of frameDefaults\n frame[key] = options[key] or frameDefaults[key]\n\n # use the images width and height for options unless already set\n @setOption 'width', image.width unless @options.width?\n @setOption 'height', image.height unless @options.height?\n\n if ImageData? and image instanceof ImageData\n frame.data = image.data\n else if (CanvasRenderingContext2D? and image instanceof CanvasRenderingContext2D) or (WebGLRenderingContext? and image instanceof WebGLRenderingContext)\n if options.copy\n frame.data = @getContextData image\n else\n frame.context = image\n else if image.childNodes?\n if options.copy\n frame.data = @getImageData image\n else\n frame.image = image\n else\n throw new Error 'Invalid image'\n\n @frames.push frame\n\n render: ->\n throw new Error 'Already running' if @running\n\n if not @options.width? or not @options.height?\n throw new Error 'Width and height must be set prior to rendering'\n\n @running = true\n @nextFrame = 0\n @finishedFrames = 0\n\n @imageParts = (null for i in [0...@frames.length])\n numWorkers = @spawnWorkers()\n # we need to wait for the palette\n if @options.globalPalette == true\n @renderNextFrame()\n else\n @renderNextFrame() for i in [0...numWorkers]\n\n @emit 'start'\n @emit 'progress', 0\n\n abort: ->\n loop\n worker = @activeWorkers.shift()\n break unless worker?\n @log 'killing active worker'\n worker.terminate()\n @running = false\n @emit 'abort'\n\n # private\n\n spawnWorkers: ->\n numWorkers = Math.min(@options.workers, @frames.length)\n [@freeWorkers.length...numWorkers].forEach (i) =>\n @log \"spawning worker #{ i }\"\n worker = new Worker @options.workerScript\n worker.onmessage = (event) =>\n @activeWorkers.splice @activeWorkers.indexOf(worker), 1\n @freeWorkers.push worker\n @frameFinished event.data\n @freeWorkers.push worker\n return numWorkers\n\n frameFinished: (frame) ->\n @log \"frame #{ frame.index } finished - #{ @activeWorkers.length } active\"\n @finishedFrames++\n @emit 'progress', @finishedFrames / @frames.length\n @imageParts[frame.index] = frame\n # remember calculated palette, spawn the rest of the workers\n if @options.globalPalette == true\n @options.globalPalette = frame.globalPalette\n @log 'global palette analyzed'\n @renderNextFrame() for i in [1...@freeWorkers.length] if @frames.length > 2\n if null in @imageParts\n @renderNextFrame()\n else\n @finishRendering()\n\n finishRendering: ->\n len = 0\n for frame in @imageParts\n len += (frame.data.length - 1) * frame.pageSize + frame.cursor\n len += frame.pageSize - frame.cursor\n @log \"rendering finished - filesize #{ Math.round(len / 1000) }kb\"\n data = new Uint8Array len\n offset = 0\n for frame in @imageParts\n for page, i in frame.data\n data.set page, offset\n if i is frame.data.length - 1\n offset += frame.cursor\n else\n offset += frame.pageSize\n\n image = new Blob [data],\n type: 'image/gif'\n\n @emit 'finished', image, data\n\n renderNextFrame: ->\n throw new Error 'No free workers' if @freeWorkers.length is 0\n return if @nextFrame >= @frames.length # no new frame to render\n\n frame = @frames[@nextFrame++]\n worker = @freeWorkers.shift()\n task = @getTask frame\n\n @log \"starting frame #{ task.index + 1 } of #{ @frames.length }\"\n @activeWorkers.push worker\n worker.postMessage task#, [task.data.buffer]\n\n getContextData: (ctx) ->\n return ctx.getImageData(0, 0, @options.width, @options.height).data\n\n getImageData: (image) ->\n if not @_canvas?\n @_canvas = document.createElement 'canvas'\n @_canvas.width = @options.width\n @_canvas.height = @options.height\n\n ctx = @_canvas.getContext '2d'\n ctx.setFill = @options.background\n ctx.fillRect 0, 0, @options.width, @options.height\n ctx.drawImage image, 0, 0\n\n return @getContextData ctx\n\n getTask: (frame) ->\n index = @frames.indexOf frame\n task =\n index: index\n last: index is (@frames.length - 1)\n delay: frame.delay\n transparent: frame.transparent\n width: @options.width\n height: @options.height\n quality: @options.quality\n dither: @options.dither\n globalPalette: @options.globalPalette\n repeat: @options.repeat\n canTransfer: (browser.name is 'chrome')\n\n if frame.data?\n task.data = frame.data\n else if frame.context?\n task.data = @getContextData frame.context\n else if frame.image?\n task.data = @getImageData frame.image\n else\n throw new Error 'Invalid frame'\n\n return task\n\n log: (args...) ->\n return unless @options.debug\n console.log args...\n\n\nmodule.exports = GIF\n"]} -------------------------------------------------------------------------------- /static/gif.worker.js: -------------------------------------------------------------------------------- 1 | // gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j= ByteArray.pageSize) this.newPage();\n this.pages[this.page][this.cursor++] = val;\n};\n\nByteArray.prototype.writeUTFBytes = function(string) {\n for (var l = string.length, i = 0; i < l; i++)\n this.writeByte(string.charCodeAt(i));\n};\n\nByteArray.prototype.writeBytes = function(array, offset, length) {\n for (var l = length || array.length, i = offset || 0; i < l; i++)\n this.writeByte(array[i]);\n};\n\nfunction GIFEncoder(width, height) {\n // image size\n this.width = ~~width;\n this.height = ~~height;\n\n // transparent color if given\n this.transparent = null;\n\n // transparent index in color table\n this.transIndex = 0;\n\n // -1 = no repeat, 0 = forever. anything else is repeat count\n this.repeat = -1;\n\n // frame delay (hundredths)\n this.delay = 0;\n\n this.image = null; // current frame\n this.pixels = null; // BGR byte array from frame\n this.indexedPixels = null; // converted frame indexed to palette\n this.colorDepth = null; // number of bit planes\n this.colorTab = null; // RGB palette\n this.neuQuant = null; // NeuQuant instance that was used to generate this.colorTab.\n this.usedEntry = new Array(); // active palette entries\n this.palSize = 7; // color table size (bits-1)\n this.dispose = -1; // disposal code (-1 = use default)\n this.firstFrame = true;\n this.sample = 10; // default sample interval for quantizer\n this.dither = false; // default dithering\n this.globalPalette = false;\n\n this.out = new ByteArray();\n}\n\n/*\n Sets the delay time between each frame, or changes it for subsequent frames\n (applies to last frame added)\n*/\nGIFEncoder.prototype.setDelay = function(milliseconds) {\n this.delay = Math.round(milliseconds / 10);\n};\n\n/*\n Sets frame rate in frames per second.\n*/\nGIFEncoder.prototype.setFrameRate = function(fps) {\n this.delay = Math.round(100 / fps);\n};\n\n/*\n Sets the GIF frame disposal code for the last added frame and any\n subsequent frames.\n\n Default is 0 if no transparent color has been set, otherwise 2.\n*/\nGIFEncoder.prototype.setDispose = function(disposalCode) {\n if (disposalCode >= 0) this.dispose = disposalCode;\n};\n\n/*\n Sets the number of times the set of GIF frames should be played.\n\n -1 = play once\n 0 = repeat indefinitely\n\n Default is -1\n\n Must be invoked before the first image is added\n*/\n\nGIFEncoder.prototype.setRepeat = function(repeat) {\n this.repeat = repeat;\n};\n\n/*\n Sets the transparent color for the last added frame and any subsequent\n frames. Since all colors are subject to modification in the quantization\n process, the color in the final palette for each frame closest to the given\n color becomes the transparent color for that frame. May be set to null to\n indicate no transparent color.\n*/\nGIFEncoder.prototype.setTransparent = function(color) {\n this.transparent = color;\n};\n\n/*\n Adds next GIF frame. The frame is not written immediately, but is\n actually deferred until the next frame is received so that timing\n data can be inserted. Invoking finish() flushes all frames.\n*/\nGIFEncoder.prototype.addFrame = function(imageData) {\n this.image = imageData;\n\n this.colorTab = this.globalPalette && this.globalPalette.slice ? this.globalPalette : null;\n\n this.getImagePixels(); // convert to correct format if necessary\n this.analyzePixels(); // build color table & map pixels\n\n if (this.globalPalette === true) this.globalPalette = this.colorTab;\n\n if (this.firstFrame) {\n this.writeLSD(); // logical screen descriptior\n this.writePalette(); // global color table\n if (this.repeat >= 0) {\n // use NS app extension to indicate reps\n this.writeNetscapeExt();\n }\n }\n\n this.writeGraphicCtrlExt(); // write graphic control extension\n this.writeImageDesc(); // image descriptor\n if (!this.firstFrame && !this.globalPalette) this.writePalette(); // local color table\n this.writePixels(); // encode and write pixel data\n\n this.firstFrame = false;\n};\n\n/*\n Adds final trailer to the GIF stream, if you don't call the finish method\n the GIF stream will not be valid.\n*/\nGIFEncoder.prototype.finish = function() {\n this.out.writeByte(0x3b); // gif trailer\n};\n\n/*\n Sets quality of color quantization (conversion of images to the maximum 256\n colors allowed by the GIF specification). Lower values (minimum = 1)\n produce better colors, but slow processing significantly. 10 is the\n default, and produces good color mapping at reasonable speeds. Values\n greater than 20 do not yield significant improvements in speed.\n*/\nGIFEncoder.prototype.setQuality = function(quality) {\n if (quality < 1) quality = 1;\n this.sample = quality;\n};\n\n/*\n Sets dithering method. Available are:\n - FALSE no dithering\n - TRUE or FloydSteinberg\n - FalseFloydSteinberg\n - Stucki\n - Atkinson\n You can add '-serpentine' to use serpentine scanning\n*/\nGIFEncoder.prototype.setDither = function(dither) {\n if (dither === true) dither = 'FloydSteinberg';\n this.dither = dither;\n};\n\n/*\n Sets global palette for all frames.\n You can provide TRUE to create global palette from first picture.\n Or an array of r,g,b,r,g,b,...\n*/\nGIFEncoder.prototype.setGlobalPalette = function(palette) {\n this.globalPalette = palette;\n};\n\n/*\n Returns global palette used for all frames.\n If setGlobalPalette(true) was used, then this function will return\n calculated palette after the first frame is added.\n*/\nGIFEncoder.prototype.getGlobalPalette = function() {\n return (this.globalPalette && this.globalPalette.slice && this.globalPalette.slice(0)) || this.globalPalette;\n};\n\n/*\n Writes GIF file header\n*/\nGIFEncoder.prototype.writeHeader = function() {\n this.out.writeUTFBytes(\"GIF89a\");\n};\n\n/*\n Analyzes current frame colors and creates color map.\n*/\nGIFEncoder.prototype.analyzePixels = function() {\n if (!this.colorTab) {\n this.neuQuant = new NeuQuant(this.pixels, this.sample);\n this.neuQuant.buildColormap(); // create reduced palette\n this.colorTab = this.neuQuant.getColormap();\n }\n\n // map image pixels to new palette\n if (this.dither) {\n this.ditherPixels(this.dither.replace('-serpentine', ''), this.dither.match(/-serpentine/) !== null);\n } else {\n this.indexPixels();\n }\n\n this.pixels = null;\n this.colorDepth = 8;\n this.palSize = 7;\n\n // get closest match to transparent color if specified\n if (this.transparent !== null) {\n this.transIndex = this.findClosest(this.transparent, true);\n }\n};\n\n/*\n Index pixels, without dithering\n*/\nGIFEncoder.prototype.indexPixels = function(imgq) {\n var nPix = this.pixels.length / 3;\n this.indexedPixels = new Uint8Array(nPix);\n var k = 0;\n for (var j = 0; j < nPix; j++) {\n var index = this.findClosestRGB(\n this.pixels[k++] & 0xff,\n this.pixels[k++] & 0xff,\n this.pixels[k++] & 0xff\n );\n this.usedEntry[index] = true;\n this.indexedPixels[j] = index;\n }\n};\n\n/*\n Taken from http://jsbin.com/iXofIji/2/edit by PAEz\n*/\nGIFEncoder.prototype.ditherPixels = function(kernel, serpentine) {\n var kernels = {\n FalseFloydSteinberg: [\n [3 / 8, 1, 0],\n [3 / 8, 0, 1],\n [2 / 8, 1, 1]\n ],\n FloydSteinberg: [\n [7 / 16, 1, 0],\n [3 / 16, -1, 1],\n [5 / 16, 0, 1],\n [1 / 16, 1, 1]\n ],\n Stucki: [\n [8 / 42, 1, 0],\n [4 / 42, 2, 0],\n [2 / 42, -2, 1],\n [4 / 42, -1, 1],\n [8 / 42, 0, 1],\n [4 / 42, 1, 1],\n [2 / 42, 2, 1],\n [1 / 42, -2, 2],\n [2 / 42, -1, 2],\n [4 / 42, 0, 2],\n [2 / 42, 1, 2],\n [1 / 42, 2, 2]\n ],\n Atkinson: [\n [1 / 8, 1, 0],\n [1 / 8, 2, 0],\n [1 / 8, -1, 1],\n [1 / 8, 0, 1],\n [1 / 8, 1, 1],\n [1 / 8, 0, 2]\n ]\n };\n\n if (!kernel || !kernels[kernel]) {\n throw 'Unknown dithering kernel: ' + kernel;\n }\n\n var ds = kernels[kernel];\n var index = 0,\n height = this.height,\n width = this.width,\n data = this.pixels;\n var direction = serpentine ? -1 : 1;\n\n this.indexedPixels = new Uint8Array(this.pixels.length / 3);\n\n for (var y = 0; y < height; y++) {\n\n if (serpentine) direction = direction * -1;\n\n for (var x = (direction == 1 ? 0 : width - 1), xend = (direction == 1 ? width : 0); x !== xend; x += direction) {\n\n index = (y * width) + x;\n // Get original colour\n var idx = index * 3;\n var r1 = data[idx];\n var g1 = data[idx + 1];\n var b1 = data[idx + 2];\n\n // Get converted colour\n idx = this.findClosestRGB(r1, g1, b1);\n this.usedEntry[idx] = true;\n this.indexedPixels[index] = idx;\n idx *= 3;\n var r2 = this.colorTab[idx];\n var g2 = this.colorTab[idx + 1];\n var b2 = this.colorTab[idx + 2];\n\n var er = r1 - r2;\n var eg = g1 - g2;\n var eb = b1 - b2;\n\n for (var i = (direction == 1 ? 0: ds.length - 1), end = (direction == 1 ? ds.length : 0); i !== end; i += direction) {\n var x1 = ds[i][1]; // *direction; // Should this by timesd by direction?..to make the kernel go in the opposite direction....got no idea....\n var y1 = ds[i][2];\n if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) {\n var d = ds[i][0];\n idx = index + x1 + (y1 * width);\n idx *= 3;\n\n data[idx] = Math.max(0, Math.min(255, data[idx] + er * d));\n data[idx + 1] = Math.max(0, Math.min(255, data[idx + 1] + eg * d));\n data[idx + 2] = Math.max(0, Math.min(255, data[idx + 2] + eb * d));\n }\n }\n }\n }\n};\n\n/*\n Returns index of palette color closest to c\n*/\nGIFEncoder.prototype.findClosest = function(c, used) {\n return this.findClosestRGB((c & 0xFF0000) >> 16, (c & 0x00FF00) >> 8, (c & 0x0000FF), used);\n};\n\nGIFEncoder.prototype.findClosestRGB = function(r, g, b, used) {\n if (this.colorTab === null) return -1;\n\n if (this.neuQuant && !used) {\n return this.neuQuant.lookupRGB(r, g, b);\n }\n \n var c = b | (g << 8) | (r << 16);\n\n var minpos = 0;\n var dmin = 256 * 256 * 256;\n var len = this.colorTab.length;\n\n for (var i = 0, index = 0; i < len; index++) {\n var dr = r - (this.colorTab[i++] & 0xff);\n var dg = g - (this.colorTab[i++] & 0xff);\n var db = b - (this.colorTab[i++] & 0xff);\n var d = dr * dr + dg * dg + db * db;\n if ((!used || this.usedEntry[index]) && (d < dmin)) {\n dmin = d;\n minpos = index;\n }\n }\n\n return minpos;\n};\n\n/*\n Extracts image pixels into byte array pixels\n (removes alphachannel from canvas imagedata)\n*/\nGIFEncoder.prototype.getImagePixels = function() {\n var w = this.width;\n var h = this.height;\n this.pixels = new Uint8Array(w * h * 3);\n\n var data = this.image;\n var srcPos = 0;\n var count = 0;\n\n for (var i = 0; i < h; i++) {\n for (var j = 0; j < w; j++) {\n this.pixels[count++] = data[srcPos++];\n this.pixels[count++] = data[srcPos++];\n this.pixels[count++] = data[srcPos++];\n srcPos++;\n }\n }\n};\n\n/*\n Writes Graphic Control Extension\n*/\nGIFEncoder.prototype.writeGraphicCtrlExt = function() {\n this.out.writeByte(0x21); // extension introducer\n this.out.writeByte(0xf9); // GCE label\n this.out.writeByte(4); // data block size\n\n var transp, disp;\n if (this.transparent === null) {\n transp = 0;\n disp = 0; // dispose = no action\n } else {\n transp = 1;\n disp = 2; // force clear if using transparent color\n }\n\n if (this.dispose >= 0) {\n disp = dispose & 7; // user override\n }\n disp <<= 2;\n\n // packed fields\n this.out.writeByte(\n 0 | // 1:3 reserved\n disp | // 4:6 disposal\n 0 | // 7 user input - 0 = none\n transp // 8 transparency flag\n );\n\n this.writeShort(this.delay); // delay x 1/100 sec\n this.out.writeByte(this.transIndex); // transparent color index\n this.out.writeByte(0); // block terminator\n};\n\n/*\n Writes Image Descriptor\n*/\nGIFEncoder.prototype.writeImageDesc = function() {\n this.out.writeByte(0x2c); // image separator\n this.writeShort(0); // image position x,y = 0,0\n this.writeShort(0);\n this.writeShort(this.width); // image size\n this.writeShort(this.height);\n\n // packed fields\n if (this.firstFrame || this.globalPalette) {\n // no LCT - GCT is used for first (or only) frame\n this.out.writeByte(0);\n } else {\n // specify normal LCT\n this.out.writeByte(\n 0x80 | // 1 local color table 1=yes\n 0 | // 2 interlace - 0=no\n 0 | // 3 sorted - 0=no\n 0 | // 4-5 reserved\n this.palSize // 6-8 size of color table\n );\n }\n};\n\n/*\n Writes Logical Screen Descriptor\n*/\nGIFEncoder.prototype.writeLSD = function() {\n // logical screen size\n this.writeShort(this.width);\n this.writeShort(this.height);\n\n // packed fields\n this.out.writeByte(\n 0x80 | // 1 : global color table flag = 1 (gct used)\n 0x70 | // 2-4 : color resolution = 7\n 0x00 | // 5 : gct sort flag = 0\n this.palSize // 6-8 : gct size\n );\n\n this.out.writeByte(0); // background color index\n this.out.writeByte(0); // pixel aspect ratio - assume 1:1\n};\n\n/*\n Writes Netscape application extension to define repeat count.\n*/\nGIFEncoder.prototype.writeNetscapeExt = function() {\n this.out.writeByte(0x21); // extension introducer\n this.out.writeByte(0xff); // app extension label\n this.out.writeByte(11); // block size\n this.out.writeUTFBytes('NETSCAPE2.0'); // app id + auth code\n this.out.writeByte(3); // sub-block size\n this.out.writeByte(1); // loop sub-block id\n this.writeShort(this.repeat); // loop count (extra iterations, 0=repeat forever)\n this.out.writeByte(0); // block terminator\n};\n\n/*\n Writes color table\n*/\nGIFEncoder.prototype.writePalette = function() {\n this.out.writeBytes(this.colorTab);\n var n = (3 * 256) - this.colorTab.length;\n for (var i = 0; i < n; i++)\n this.out.writeByte(0);\n};\n\nGIFEncoder.prototype.writeShort = function(pValue) {\n this.out.writeByte(pValue & 0xFF);\n this.out.writeByte((pValue >> 8) & 0xFF);\n};\n\n/*\n Encodes and writes pixel data\n*/\nGIFEncoder.prototype.writePixels = function() {\n var enc = new LZWEncoder(this.width, this.height, this.indexedPixels, this.colorDepth);\n enc.encode(this.out);\n};\n\n/*\n Retrieves the GIF stream\n*/\nGIFEncoder.prototype.stream = function() {\n return this.out;\n};\n\nmodule.exports = GIFEncoder;\n","/*\n LZWEncoder.js\n\n Authors\n Kevin Weiner (original Java version - kweiner@fmsware.com)\n Thibault Imbert (AS3 version - bytearray.org)\n Johan Nordberg (JS version - code@johan-nordberg.com)\n\n Acknowledgements\n GIFCOMPR.C - GIF Image compression routines\n Lempel-Ziv compression based on 'compress'. GIF modifications by\n David Rowley (mgardi@watdcsu.waterloo.edu)\n GIF Image compression - modified 'compress'\n Based on: compress.c - File compression ala IEEE Computer, June 1984.\n By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)\n Jim McKie (decvax!mcvax!jim)\n Steve Davies (decvax!vax135!petsd!peora!srd)\n Ken Turkowski (decvax!decwrl!turtlevax!ken)\n James A. Woods (decvax!ihnp4!ames!jaw)\n Joe Orost (decvax!vax135!petsd!joe)\n*/\n\nvar EOF = -1;\nvar BITS = 12;\nvar HSIZE = 5003; // 80% occupancy\nvar masks = [0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,\n 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF,\n 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF];\n\nfunction LZWEncoder(width, height, pixels, colorDepth) {\n var initCodeSize = Math.max(2, colorDepth);\n\n var accum = new Uint8Array(256);\n var htab = new Int32Array(HSIZE);\n var codetab = new Int32Array(HSIZE);\n\n var cur_accum, cur_bits = 0;\n var a_count;\n var free_ent = 0; // first unused entry\n var maxcode;\n\n // block compression parameters -- after all codes are used up,\n // and compression rate changes, start over.\n var clear_flg = false;\n\n // Algorithm: use open addressing double hashing (no chaining) on the\n // prefix code / next character combination. We do a variant of Knuth's\n // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime\n // secondary probe. Here, the modular division first probe is gives way\n // to a faster exclusive-or manipulation. Also do block compression with\n // an adaptive reset, whereby the code table is cleared when the compression\n // ratio decreases, but after the table fills. The variable-length output\n // codes are re-sized at this point, and a special CLEAR code is generated\n // for the decompressor. Late addition: construct the table according to\n // file size for noticeable speed improvement on small files. Please direct\n // questions about this implementation to ames!jaw.\n var g_init_bits, ClearCode, EOFCode;\n\n // Add a character to the end of the current packet, and if it is 254\n // characters, flush the packet to disk.\n function char_out(c, outs) {\n accum[a_count++] = c;\n if (a_count >= 254) flush_char(outs);\n }\n\n // Clear out the hash table\n // table clear for block compress\n function cl_block(outs) {\n cl_hash(HSIZE);\n free_ent = ClearCode + 2;\n clear_flg = true;\n output(ClearCode, outs);\n }\n\n // Reset code table\n function cl_hash(hsize) {\n for (var i = 0; i < hsize; ++i) htab[i] = -1;\n }\n\n function compress(init_bits, outs) {\n var fcode, c, i, ent, disp, hsize_reg, hshift;\n\n // Set up the globals: g_init_bits - initial number of bits\n g_init_bits = init_bits;\n\n // Set up the necessary values\n clear_flg = false;\n n_bits = g_init_bits;\n maxcode = MAXCODE(n_bits);\n\n ClearCode = 1 << (init_bits - 1);\n EOFCode = ClearCode + 1;\n free_ent = ClearCode + 2;\n\n a_count = 0; // clear packet\n\n ent = nextPixel();\n\n hshift = 0;\n for (fcode = HSIZE; fcode < 65536; fcode *= 2) ++hshift;\n hshift = 8 - hshift; // set hash code range bound\n hsize_reg = HSIZE;\n cl_hash(hsize_reg); // clear hash table\n\n output(ClearCode, outs);\n\n outer_loop: while ((c = nextPixel()) != EOF) {\n fcode = (c << BITS) + ent;\n i = (c << hshift) ^ ent; // xor hashing\n if (htab[i] === fcode) {\n ent = codetab[i];\n continue;\n } else if (htab[i] >= 0) { // non-empty slot\n disp = hsize_reg - i; // secondary hash (after G. Knott)\n if (i === 0) disp = 1;\n do {\n if ((i -= disp) < 0) i += hsize_reg;\n if (htab[i] === fcode) {\n ent = codetab[i];\n continue outer_loop;\n }\n } while (htab[i] >= 0);\n }\n output(ent, outs);\n ent = c;\n if (free_ent < 1 << BITS) {\n codetab[i] = free_ent++; // code -> hashtable\n htab[i] = fcode;\n } else {\n cl_block(outs);\n }\n }\n\n // Put out the final code.\n output(ent, outs);\n output(EOFCode, outs);\n }\n\n function encode(outs) {\n outs.writeByte(initCodeSize); // write \"initial code size\" byte\n remaining = width * height; // reset navigation variables\n curPixel = 0;\n compress(initCodeSize + 1, outs); // compress and write the pixel data\n outs.writeByte(0); // write block terminator\n }\n\n // Flush the packet to disk, and reset the accumulator\n function flush_char(outs) {\n if (a_count > 0) {\n outs.writeByte(a_count);\n outs.writeBytes(accum, 0, a_count);\n a_count = 0;\n }\n }\n\n function MAXCODE(n_bits) {\n return (1 << n_bits) - 1;\n }\n\n // Return the next pixel from the image\n function nextPixel() {\n if (remaining === 0) return EOF;\n --remaining;\n var pix = pixels[curPixel++];\n return pix & 0xff;\n }\n\n function output(code, outs) {\n cur_accum &= masks[cur_bits];\n\n if (cur_bits > 0) cur_accum |= (code << cur_bits);\n else cur_accum = code;\n\n cur_bits += n_bits;\n\n while (cur_bits >= 8) {\n char_out((cur_accum & 0xff), outs);\n cur_accum >>= 8;\n cur_bits -= 8;\n }\n\n // If the next entry is going to be too big for the code size,\n // then increase it, if possible.\n if (free_ent > maxcode || clear_flg) {\n if (clear_flg) {\n maxcode = MAXCODE(n_bits = g_init_bits);\n clear_flg = false;\n } else {\n ++n_bits;\n if (n_bits == BITS) maxcode = 1 << BITS;\n else maxcode = MAXCODE(n_bits);\n }\n }\n\n if (code == EOFCode) {\n // At EOF, write the rest of the buffer.\n while (cur_bits > 0) {\n char_out((cur_accum & 0xff), outs);\n cur_accum >>= 8;\n cur_bits -= 8;\n }\n flush_char(outs);\n }\n }\n\n this.encode = encode;\n}\n\nmodule.exports = LZWEncoder;\n","/* NeuQuant Neural-Net Quantization Algorithm\n * ------------------------------------------\n *\n * Copyright (c) 1994 Anthony Dekker\n *\n * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.\n * See \"Kohonen neural networks for optimal colour quantization\"\n * in \"Network: Computation in Neural Systems\" Vol. 5 (1994) pp 351-367.\n * for a discussion of the algorithm.\n * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML\n *\n * Any party obtaining a copy of these files from the author, directly or\n * indirectly, is granted, free of charge, a full and unrestricted irrevocable,\n * world-wide, paid up, royalty-free, nonexclusive right and license to deal\n * in this software and documentation files (the \"Software\"), including without\n * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,\n * and/or sell copies of the Software, and to permit persons who receive\n * copies from any such party to do so, with the only requirement being\n * that this copyright notice remain intact.\n *\n * (JavaScript port 2012 by Johan Nordberg)\n */\n\nvar ncycles = 100; // number of learning cycles\nvar netsize = 256; // number of colors used\nvar maxnetpos = netsize - 1;\n\n// defs for freq and bias\nvar netbiasshift = 4; // bias for colour values\nvar intbiasshift = 16; // bias for fractions\nvar intbias = (1 << intbiasshift);\nvar gammashift = 10;\nvar gamma = (1 << gammashift);\nvar betashift = 10;\nvar beta = (intbias >> betashift); /* beta = 1/1024 */\nvar betagamma = (intbias << (gammashift - betashift));\n\n// defs for decreasing radius factor\nvar initrad = (netsize >> 3); // for 256 cols, radius starts\nvar radiusbiasshift = 6; // at 32.0 biased by 6 bits\nvar radiusbias = (1 << radiusbiasshift);\nvar initradius = (initrad * radiusbias); //and decreases by a\nvar radiusdec = 30; // factor of 1/30 each cycle\n\n// defs for decreasing alpha factor\nvar alphabiasshift = 10; // alpha starts at 1.0\nvar initalpha = (1 << alphabiasshift);\nvar alphadec; // biased by 10 bits\n\n/* radbias and alpharadbias used for radpower calculation */\nvar radbiasshift = 8;\nvar radbias = (1 << radbiasshift);\nvar alpharadbshift = (alphabiasshift + radbiasshift);\nvar alpharadbias = (1 << alpharadbshift);\n\n// four primes near 500 - assume no image has a length so large that it is\n// divisible by all four primes\nvar prime1 = 499;\nvar prime2 = 491;\nvar prime3 = 487;\nvar prime4 = 503;\nvar minpicturebytes = (3 * prime4);\n\n/*\n Constructor: NeuQuant\n\n Arguments:\n\n pixels - array of pixels in RGB format\n samplefac - sampling factor 1 to 30 where lower is better quality\n\n >\n > pixels = [r, g, b, r, g, b, r, g, b, ..]\n >\n*/\nfunction NeuQuant(pixels, samplefac) {\n var network; // int[netsize][4]\n var netindex; // for network lookup - really 256\n\n // bias and freq arrays for learning\n var bias;\n var freq;\n var radpower;\n\n /*\n Private Method: init\n\n sets up arrays\n */\n function init() {\n network = [];\n netindex = new Int32Array(256);\n bias = new Int32Array(netsize);\n freq = new Int32Array(netsize);\n radpower = new Int32Array(netsize >> 3);\n\n var i, v;\n for (i = 0; i < netsize; i++) {\n v = (i << (netbiasshift + 8)) / netsize;\n network[i] = new Float64Array([v, v, v, 0]);\n //network[i] = [v, v, v, 0]\n freq[i] = intbias / netsize;\n bias[i] = 0;\n }\n }\n\n /*\n Private Method: unbiasnet\n\n unbiases network to give byte values 0..255 and record position i to prepare for sort\n */\n function unbiasnet() {\n for (var i = 0; i < netsize; i++) {\n network[i][0] >>= netbiasshift;\n network[i][1] >>= netbiasshift;\n network[i][2] >>= netbiasshift;\n network[i][3] = i; // record color number\n }\n }\n\n /*\n Private Method: altersingle\n\n moves neuron *i* towards biased (b,g,r) by factor *alpha*\n */\n function altersingle(alpha, i, b, g, r) {\n network[i][0] -= (alpha * (network[i][0] - b)) / initalpha;\n network[i][1] -= (alpha * (network[i][1] - g)) / initalpha;\n network[i][2] -= (alpha * (network[i][2] - r)) / initalpha;\n }\n\n /*\n Private Method: alterneigh\n\n moves neurons in *radius* around index *i* towards biased (b,g,r) by factor *alpha*\n */\n function alterneigh(radius, i, b, g, r) {\n var lo = Math.abs(i - radius);\n var hi = Math.min(i + radius, netsize);\n\n var j = i + 1;\n var k = i - 1;\n var m = 1;\n\n var p, a;\n while ((j < hi) || (k > lo)) {\n a = radpower[m++];\n\n if (j < hi) {\n p = network[j++];\n p[0] -= (a * (p[0] - b)) / alpharadbias;\n p[1] -= (a * (p[1] - g)) / alpharadbias;\n p[2] -= (a * (p[2] - r)) / alpharadbias;\n }\n\n if (k > lo) {\n p = network[k--];\n p[0] -= (a * (p[0] - b)) / alpharadbias;\n p[1] -= (a * (p[1] - g)) / alpharadbias;\n p[2] -= (a * (p[2] - r)) / alpharadbias;\n }\n }\n }\n\n /*\n Private Method: contest\n\n searches for biased BGR values\n */\n function contest(b, g, r) {\n /*\n finds closest neuron (min dist) and updates freq\n finds best neuron (min dist-bias) and returns position\n for frequently chosen neurons, freq[i] is high and bias[i] is negative\n bias[i] = gamma * ((1 / netsize) - freq[i])\n */\n\n var bestd = ~(1 << 31);\n var bestbiasd = bestd;\n var bestpos = -1;\n var bestbiaspos = bestpos;\n\n var i, n, dist, biasdist, betafreq;\n for (i = 0; i < netsize; i++) {\n n = network[i];\n\n dist = Math.abs(n[0] - b) + Math.abs(n[1] - g) + Math.abs(n[2] - r);\n if (dist < bestd) {\n bestd = dist;\n bestpos = i;\n }\n\n biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift));\n if (biasdist < bestbiasd) {\n bestbiasd = biasdist;\n bestbiaspos = i;\n }\n\n betafreq = (freq[i] >> betashift);\n freq[i] -= betafreq;\n bias[i] += (betafreq << gammashift);\n }\n\n freq[bestpos] += beta;\n bias[bestpos] -= betagamma;\n\n return bestbiaspos;\n }\n\n /*\n Private Method: inxbuild\n\n sorts network and builds netindex[0..255]\n */\n function inxbuild() {\n var i, j, p, q, smallpos, smallval, previouscol = 0, startpos = 0;\n for (i = 0; i < netsize; i++) {\n p = network[i];\n smallpos = i;\n smallval = p[1]; // index on g\n // find smallest in i..netsize-1\n for (j = i + 1; j < netsize; j++) {\n q = network[j];\n if (q[1] < smallval) { // index on g\n smallpos = j;\n smallval = q[1]; // index on g\n }\n }\n q = network[smallpos];\n // swap p (i) and q (smallpos) entries\n if (i != smallpos) {\n j = q[0]; q[0] = p[0]; p[0] = j;\n j = q[1]; q[1] = p[1]; p[1] = j;\n j = q[2]; q[2] = p[2]; p[2] = j;\n j = q[3]; q[3] = p[3]; p[3] = j;\n }\n // smallval entry is now in position i\n\n if (smallval != previouscol) {\n netindex[previouscol] = (startpos + i) >> 1;\n for (j = previouscol + 1; j < smallval; j++)\n netindex[j] = i;\n previouscol = smallval;\n startpos = i;\n }\n }\n netindex[previouscol] = (startpos + maxnetpos) >> 1;\n for (j = previouscol + 1; j < 256; j++)\n netindex[j] = maxnetpos; // really 256\n }\n\n /*\n Private Method: inxsearch\n\n searches for BGR values 0..255 and returns a color index\n */\n function inxsearch(b, g, r) {\n var a, p, dist;\n\n var bestd = 1000; // biggest possible dist is 256*3\n var best = -1;\n\n var i = netindex[g]; // index on g\n var j = i - 1; // start at netindex[g] and work outwards\n\n while ((i < netsize) || (j >= 0)) {\n if (i < netsize) {\n p = network[i];\n dist = p[1] - g; // inx key\n if (dist >= bestd) i = netsize; // stop iter\n else {\n i++;\n if (dist < 0) dist = -dist;\n a = p[0] - b; if (a < 0) a = -a;\n dist += a;\n if (dist < bestd) {\n a = p[2] - r; if (a < 0) a = -a;\n dist += a;\n if (dist < bestd) {\n bestd = dist;\n best = p[3];\n }\n }\n }\n }\n if (j >= 0) {\n p = network[j];\n dist = g - p[1]; // inx key - reverse dif\n if (dist >= bestd) j = -1; // stop iter\n else {\n j--;\n if (dist < 0) dist = -dist;\n a = p[0] - b; if (a < 0) a = -a;\n dist += a;\n if (dist < bestd) {\n a = p[2] - r; if (a < 0) a = -a;\n dist += a;\n if (dist < bestd) {\n bestd = dist;\n best = p[3];\n }\n }\n }\n }\n }\n\n return best;\n }\n\n /*\n Private Method: learn\n\n \"Main Learning Loop\"\n */\n function learn() {\n var i;\n\n var lengthcount = pixels.length;\n var alphadec = 30 + ((samplefac - 1) / 3);\n var samplepixels = lengthcount / (3 * samplefac);\n var delta = ~~(samplepixels / ncycles);\n var alpha = initalpha;\n var radius = initradius;\n\n var rad = radius >> radiusbiasshift;\n\n if (rad <= 1) rad = 0;\n for (i = 0; i < rad; i++)\n radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad));\n\n var step;\n if (lengthcount < minpicturebytes) {\n samplefac = 1;\n step = 3;\n } else if ((lengthcount % prime1) !== 0) {\n step = 3 * prime1;\n } else if ((lengthcount % prime2) !== 0) {\n step = 3 * prime2;\n } else if ((lengthcount % prime3) !== 0) {\n step = 3 * prime3;\n } else {\n step = 3 * prime4;\n }\n\n var b, g, r, j;\n var pix = 0; // current pixel\n\n i = 0;\n while (i < samplepixels) {\n b = (pixels[pix] & 0xff) << netbiasshift;\n g = (pixels[pix + 1] & 0xff) << netbiasshift;\n r = (pixels[pix + 2] & 0xff) << netbiasshift;\n\n j = contest(b, g, r);\n\n altersingle(alpha, j, b, g, r);\n if (rad !== 0) alterneigh(rad, j, b, g, r); // alter neighbours\n\n pix += step;\n if (pix >= lengthcount) pix -= lengthcount;\n\n i++;\n\n if (delta === 0) delta = 1;\n if (i % delta === 0) {\n alpha -= alpha / alphadec;\n radius -= radius / radiusdec;\n rad = radius >> radiusbiasshift;\n\n if (rad <= 1) rad = 0;\n for (j = 0; j < rad; j++)\n radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad));\n }\n }\n }\n\n /*\n Method: buildColormap\n\n 1. initializes network\n 2. trains it\n 3. removes misconceptions\n 4. builds colorindex\n */\n function buildColormap() {\n init();\n learn();\n unbiasnet();\n inxbuild();\n }\n this.buildColormap = buildColormap;\n\n /*\n Method: getColormap\n\n builds colormap from the index\n\n returns array in the format:\n\n >\n > [r, g, b, r, g, b, r, g, b, ..]\n >\n */\n function getColormap() {\n var map = [];\n var index = [];\n\n for (var i = 0; i < netsize; i++)\n index[network[i][3]] = i;\n\n var k = 0;\n for (var l = 0; l < netsize; l++) {\n var j = index[l];\n map[k++] = (network[j][0]);\n map[k++] = (network[j][1]);\n map[k++] = (network[j][2]);\n }\n return map;\n }\n this.getColormap = getColormap;\n\n /*\n Method: lookupRGB\n\n looks for the closest *r*, *g*, *b* color in the map and\n returns its index\n */\n this.lookupRGB = inxsearch;\n}\n\nmodule.exports = NeuQuant;\n","GIFEncoder = require './GIFEncoder.js'\n\nrenderFrame = (frame) ->\n encoder = new GIFEncoder frame.width, frame.height\n\n if frame.index is 0\n encoder.writeHeader()\n else\n encoder.firstFrame = false\n\n encoder.setTransparent frame.transparent\n encoder.setRepeat frame.repeat\n encoder.setDelay frame.delay\n encoder.setQuality frame.quality\n encoder.setDither frame.dither\n encoder.setGlobalPalette frame.globalPalette\n encoder.addFrame frame.data\n encoder.finish() if frame.last\n if frame.globalPalette == true\n frame.globalPalette = encoder.getGlobalPalette()\n\n stream = encoder.stream()\n frame.data = stream.pages\n frame.cursor = stream.cursor\n frame.pageSize = stream.constructor.pageSize\n\n if frame.canTransfer\n transfer = (page.buffer for page in frame.data)\n self.postMessage frame, transfer\n else\n self.postMessage frame\n\nself.onmessage = (event) -> renderFrame event.data\n"]} -------------------------------------------------------------------------------- /static/jott-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwasti/jott/57a7f5e2a90ccf1e2c5760e7af769a9dfa21dc8e/static/jott-icon.png -------------------------------------------------------------------------------- /static/jott.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwasti/jott/57a7f5e2a90ccf1e2c5760e7af769a9dfa21dc8e/static/jott.png -------------------------------------------------------------------------------- /static/texdown.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function moo_factory() { 3 | 'use strict'; 4 | 5 | var hasOwnProperty = Object.prototype.hasOwnProperty 6 | var toString = Object.prototype.toString 7 | var hasSticky = typeof new RegExp().sticky === 'boolean' 8 | 9 | /***************************************************************************/ 10 | 11 | function isRegExp(o) { return o && toString.call(o) === '[object RegExp]' } 12 | function isObject(o) { return o && typeof o === 'object' && !isRegExp(o) && !Array.isArray(o) } 13 | 14 | function reEscape(s) { 15 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') 16 | } 17 | function reGroups(s) { 18 | var re = new RegExp('|' + s) 19 | return re.exec('').length - 1 20 | } 21 | function reCapture(s) { 22 | return '(' + s + ')' 23 | } 24 | function reUnion(regexps) { 25 | if (!regexps.length) return '(?!)' 26 | var source = regexps.map(function(s) { 27 | return "(?:" + s + ")" 28 | }).join('|') 29 | return "(?:" + source + ")" 30 | } 31 | 32 | function regexpOrLiteral(obj) { 33 | if (typeof obj === 'string') { 34 | return '(?:' + reEscape(obj) + ')' 35 | 36 | } else if (isRegExp(obj)) { 37 | // TODO: consider /u support 38 | if (obj.ignoreCase) throw new Error('RegExp /i flag not allowed') 39 | if (obj.global) throw new Error('RegExp /g flag is implied') 40 | if (obj.sticky) throw new Error('RegExp /y flag is implied') 41 | if (obj.multiline) throw new Error('RegExp /m flag is implied') 42 | if (obj.unicode) throw new Error('RegExp /u flag is not allowed') 43 | return obj.source 44 | 45 | } else { 46 | throw new Error('Not a pattern: ' + obj) 47 | } 48 | } 49 | 50 | function objectToRules(object) { 51 | var keys = Object.getOwnPropertyNames(object) 52 | var result = [] 53 | for (var i = 0; i < keys.length; i++) { 54 | var key = keys[i] 55 | var thing = object[key] 56 | var rules = Array.isArray(thing) ? thing : [thing] 57 | var match = [] 58 | rules.forEach(function(rule) { 59 | if (isObject(rule)) { 60 | if (match.length) result.push(ruleOptions(key, match)) 61 | result.push(ruleOptions(key, rule)) 62 | match = [] 63 | } else { 64 | match.push(rule) 65 | } 66 | }) 67 | if (match.length) result.push(ruleOptions(key, match)) 68 | } 69 | return result 70 | } 71 | 72 | function arrayToRules(array) { 73 | var result = [] 74 | for (var i = 0; i < array.length; i++) { 75 | var obj = array[i] 76 | if (!obj.name) { 77 | throw new Error('Rule has no name: ' + JSON.stringify(obj)) 78 | } 79 | result.push(ruleOptions(obj.name, obj)) 80 | } 81 | return result 82 | } 83 | 84 | function ruleOptions(name, obj) { 85 | if (!isObject(obj)) { 86 | obj = { match: obj } 87 | } 88 | 89 | // nb. error and fallback imply lineBreaks 90 | var options = { 91 | tokenType: name, 92 | lineBreaks: !!obj.error || !!obj.fallback, 93 | pop: false, 94 | next: null, 95 | push: null, 96 | error: false, 97 | fallback: false, 98 | value: null, 99 | getType: null, 100 | shouldThrow: false, 101 | } 102 | 103 | // Avoid Object.assign(), so we support IE9+ 104 | for (var key in obj) { 105 | if (hasOwnProperty.call(obj, key)) { 106 | options[key] = obj[key] 107 | } 108 | } 109 | 110 | // convert to array 111 | var match = options.match 112 | options.match = Array.isArray(match) ? match : match ? [match] : [] 113 | options.match.sort(function(a, b) { 114 | return isRegExp(a) && isRegExp(b) ? 0 115 | : isRegExp(b) ? -1 : isRegExp(a) ? +1 : b.length - a.length 116 | }) 117 | if (options.keywords) { 118 | options.getType = keywordTransform(options.keywords) 119 | } 120 | return options 121 | } 122 | 123 | var defaultErrorRule = ruleOptions('error', {lineBreaks: true, shouldThrow: true}) 124 | function compileRules(rules, hasStates) { 125 | rules = Array.isArray(rules) ? arrayToRules(rules) : objectToRules(rules) 126 | 127 | var errorRule = null 128 | var fast = Object.create(null) 129 | var fastAllowed = true 130 | var groups = [] 131 | var parts = [] 132 | for (var i = 0; i < rules.length; i++) { 133 | var options = rules[i] 134 | 135 | if (options.error || options.fallback) { 136 | // errorRule can only be set once 137 | if (errorRule) { 138 | if (!options.fallback === !errorRule.fallback) { 139 | throw new Error("Multiple " + (options.fallback ? "fallback" : "error") + " rules not allowed (for token '" + options.tokenType + "')") 140 | } else { 141 | throw new Error("fallback and error are mutually exclusive (for token '" + options.tokenType + "')") 142 | } 143 | } 144 | errorRule = options 145 | } 146 | 147 | var match = options.match 148 | if (fastAllowed) { 149 | while (match.length && typeof match[0] === 'string' && match[0].length === 1) { 150 | var word = match.shift() 151 | fast[word.charCodeAt(0)] = options 152 | } 153 | } 154 | 155 | // Warn about inappropriate state-switching options 156 | if (options.pop || options.push || options.next) { 157 | if (!hasStates) { 158 | throw new Error("State-switching options are not allowed in stateless lexers (for token '" + options.tokenType + "')") 159 | } 160 | if (options.fallback) { 161 | throw new Error("State-switching options are not allowed on fallback tokens (for token '" + options.tokenType + "')") 162 | } 163 | } 164 | 165 | // Only rules with a .match are included in the RegExp 166 | if (match.length === 0) { 167 | continue 168 | } 169 | fastAllowed = false 170 | 171 | groups.push(options) 172 | 173 | // convert to RegExp 174 | var pat = reUnion(match.map(regexpOrLiteral)) 175 | 176 | // validate 177 | var regexp = new RegExp(pat) 178 | if (regexp.test("")) { 179 | throw new Error("RegExp matches empty string: " + regexp) 180 | } 181 | var groupCount = reGroups(pat) 182 | if (groupCount > 0) { 183 | throw new Error("RegExp has capture groups: " + regexp + "\nUse (?: … ) instead") 184 | } 185 | 186 | // try and detect rules matching newlines 187 | if (!options.lineBreaks && regexp.test('\n')) { 188 | throw new Error('Rule should declare lineBreaks: ' + regexp) 189 | } 190 | 191 | // store regex 192 | parts.push(reCapture(pat)) 193 | } 194 | 195 | 196 | // If there's no fallback rule, use the sticky flag so we only look for 197 | // matches at the current index. 198 | // 199 | // If we don't support the sticky flag, then fake it using an irrefutable 200 | // match (i.e. an empty pattern). 201 | var fallbackRule = errorRule && errorRule.fallback 202 | var flags = hasSticky && !fallbackRule ? 'ym' : 'gm' 203 | var suffix = hasSticky || fallbackRule ? '' : '|' 204 | var combined = new RegExp(reUnion(parts) + suffix, flags) 205 | 206 | return {regexp: combined, groups: groups, fast: fast, error: errorRule || defaultErrorRule} 207 | } 208 | 209 | function compile(rules) { 210 | var result = compileRules(rules) 211 | return new Lexer({start: result}, 'start') 212 | } 213 | 214 | function checkStateGroup(g, name, map) { 215 | var state = g && (g.push || g.next) 216 | if (state && !map[state]) { 217 | throw new Error("Missing state '" + state + "' (in token '" + g.tokenType + "' of state '" + name + "')") 218 | } 219 | if (g && g.pop && +g.pop !== 1) { 220 | throw new Error("pop must be 1 (in token '" + g.tokenType + "' of state '" + name + "')") 221 | } 222 | } 223 | function compileStates(states, start) { 224 | var keys = Object.getOwnPropertyNames(states) 225 | if (!start) start = keys[0] 226 | 227 | var map = Object.create(null) 228 | for (var i = 0; i < keys.length; i++) { 229 | var key = keys[i] 230 | map[key] = compileRules(states[key], true) 231 | } 232 | 233 | for (var i = 0; i < keys.length; i++) { 234 | var name = keys[i] 235 | var state = map[name] 236 | var groups = state.groups 237 | for (var j = 0; j < groups.length; j++) { 238 | checkStateGroup(groups[j], name, map) 239 | } 240 | var keys = Object.getOwnPropertyNames(state.fast) 241 | for (var j = 0; j < keys.length; j++) { 242 | checkStateGroup(state.fast[keys[j]], name, map) 243 | } 244 | } 245 | 246 | return new Lexer(map, start) 247 | } 248 | 249 | function keywordTransform(map) { 250 | var reverseMap = Object.create(null) 251 | var byLength = Object.create(null) 252 | var types = Object.getOwnPropertyNames(map) 253 | for (var i = 0; i < types.length; i++) { 254 | var tokenType = types[i] 255 | var item = map[tokenType] 256 | var keywordList = Array.isArray(item) ? item : [item] 257 | keywordList.forEach(function(keyword) { 258 | (byLength[keyword.length] = byLength[keyword.length] || []).push(keyword) 259 | if (typeof keyword !== 'string') { 260 | throw new Error("keyword must be string (in keyword '" + tokenType + "')") 261 | } 262 | reverseMap[keyword] = tokenType 263 | }) 264 | } 265 | 266 | // fast string lookup 267 | // https://jsperf.com/string-lookups 268 | function str(x) { return JSON.stringify(x) } 269 | var source = '' 270 | source += 'switch (value.length) {\n' 271 | for (var length in byLength) { 272 | var keywords = byLength[length] 273 | source += 'case ' + length + ':\n' 274 | source += 'switch (value) {\n' 275 | keywords.forEach(function(keyword) { 276 | var tokenType = reverseMap[keyword] 277 | source += 'case ' + str(keyword) + ': return ' + str(tokenType) + '\n' 278 | }) 279 | source += '}\n' 280 | } 281 | source += '}\n' 282 | return Function('value', source) // getType 283 | } 284 | 285 | /***************************************************************************/ 286 | 287 | var Lexer = function(states, state) { 288 | this.startState = state 289 | this.states = states 290 | this.buffer = '' 291 | this.stack = [] 292 | this.reset() 293 | } 294 | 295 | Lexer.prototype.reset = function(data, info) { 296 | this.buffer = data || '' 297 | this.index = 0 298 | this.line = info ? info.line : 1 299 | this.col = info ? info.col : 1 300 | this.queuedToken = info ? info.queuedToken : null 301 | this.queuedThrow = info ? info.queuedThrow : null 302 | this.setState(info ? info.state : this.startState) 303 | return this 304 | } 305 | 306 | Lexer.prototype.save = function() { 307 | return { 308 | line: this.line, 309 | col: this.col, 310 | state: this.state, 311 | queuedToken: this.queuedToken, 312 | queuedThrow: this.queuedThrow, 313 | } 314 | } 315 | 316 | Lexer.prototype.setState = function(state) { 317 | if (!state || this.state === state) return 318 | this.state = state 319 | var info = this.states[state] 320 | this.groups = info.groups 321 | this.error = info.error 322 | this.re = info.regexp 323 | this.fast = info.fast 324 | } 325 | 326 | Lexer.prototype.popState = function() { 327 | this.setState(this.stack.pop()) 328 | } 329 | 330 | Lexer.prototype.pushState = function(state) { 331 | this.stack.push(this.state) 332 | this.setState(state) 333 | } 334 | 335 | Lexer.prototype._eat = hasSticky ? function(re) { // assume re is /y 336 | return re.exec(this.buffer) 337 | } : function(re) { // assume re is /g 338 | var match = re.exec(this.buffer) 339 | // will always match, since we used the |(?:) trick 340 | if (match[0].length === 0) { 341 | return null 342 | } 343 | return match 344 | } 345 | 346 | Lexer.prototype._getGroup = function(match) { 347 | if (match === null) { 348 | return -1 349 | } 350 | 351 | var groupCount = this.groups.length 352 | for (var i = 0; i < groupCount; i++) { 353 | if (match[i + 1] !== undefined) { 354 | return i 355 | } 356 | } 357 | throw new Error('Cannot find token type for matched text') 358 | } 359 | 360 | function tokenToString() { 361 | return this.value 362 | } 363 | 364 | Lexer.prototype.next = function() { 365 | if (this.queuedToken) { 366 | var queuedToken = this.queuedToken, queuedThrow = this.queuedThrow 367 | this.queuedToken = null 368 | this.queuedThrow = false 369 | if (queuedThrow) { 370 | throw new Error(this.formatError(queuedToken, "invalid syntax")) 371 | } 372 | return queuedToken 373 | } 374 | var re = this.re 375 | var buffer = this.buffer 376 | 377 | var index = re.lastIndex = this.index 378 | if (index === buffer.length) { 379 | return // EOF 380 | } 381 | 382 | var group, text, matchIndex 383 | group = this.fast[buffer.charCodeAt(index)] 384 | if (group) { 385 | text = buffer.charAt(index) 386 | matchIndex = index 387 | 388 | } else { 389 | var match = this._eat(re) 390 | matchIndex = match ? match.index : this.buffer.length 391 | var i = this._getGroup(match) 392 | 393 | if ((this.error.fallback && matchIndex !== index) || i === -1) { 394 | var fallbackToken = this._hadToken(this.error, buffer.slice(index, matchIndex), index) 395 | 396 | if (i === -1) { 397 | if (this.error.shouldThrow) { 398 | throw new Error(this.formatError(fallbackToken, "invalid syntax")) 399 | } 400 | return fallbackToken 401 | } 402 | } 403 | 404 | group = this.groups[i] 405 | text = match[0] 406 | } 407 | var token = this._hadToken(group, text, matchIndex) 408 | 409 | // throw, if no rule with {error: true} 410 | if (fallbackToken) { 411 | this.queuedToken = token 412 | this.queuedThrow = group.shouldThrow 413 | } else if (group.shouldThrow) { 414 | throw new Error(this.formatError(token, "invalid syntax")) 415 | } 416 | 417 | if (group.pop) this.popState() 418 | else if (group.push) this.pushState(group.push) 419 | else if (group.next) this.setState(group.next) 420 | 421 | return fallbackToken || token 422 | } 423 | 424 | Lexer.prototype._hadToken = function(group, text, offset) { 425 | // count line breaks 426 | var lineBreaks = 0 427 | if (group.lineBreaks) { 428 | var matchNL = /\n/g 429 | var nl = 1 430 | if (text === '\n') { 431 | lineBreaks = 1 432 | } else { 433 | while (matchNL.exec(text)) { lineBreaks++; nl = matchNL.lastIndex } 434 | } 435 | } 436 | 437 | var token = { 438 | type: (group.getType && group.getType(text)) || group.tokenType, 439 | value: group.value ? group.value(text) : text, 440 | text: text, 441 | toString: tokenToString, 442 | offset: offset, 443 | lineBreaks: lineBreaks, 444 | line: this.line, 445 | col: this.col, 446 | } 447 | // nb. adding more props to token object will make V8 sad! 448 | 449 | var size = text.length 450 | this.index += size 451 | this.line += lineBreaks 452 | if (lineBreaks !== 0) { 453 | this.col = size - nl + 1 454 | } else { 455 | this.col += size 456 | } 457 | return token 458 | } 459 | 460 | if (typeof Symbol !== 'undefined' && Symbol.iterator) { 461 | var LexerIterator = function(lexer) { 462 | this.lexer = lexer 463 | } 464 | 465 | LexerIterator.prototype.next = function() { 466 | var token = this.lexer.next() 467 | return {value: token, done: !token} 468 | } 469 | 470 | LexerIterator.prototype[Symbol.iterator] = function() { 471 | return this 472 | } 473 | 474 | Lexer.prototype[Symbol.iterator] = function() { 475 | return new LexerIterator(this) 476 | } 477 | } 478 | 479 | Lexer.prototype.formatError = function(token, message) { 480 | var value = token.value 481 | var index = token.offset 482 | var eol = token.lineBreaks ? value.indexOf('\n') : value.length 483 | var start = Math.max(0, index - token.col + 1) 484 | var firstLine = this.buffer.substring(start, index + eol) 485 | message += " at line " + token.line + " col " + token.col + ":\n\n" 486 | message += " " + firstLine + "\n" 487 | message += " " + Array(token.col).join(" ") + "^" 488 | return message 489 | } 490 | 491 | Lexer.prototype.clone = function() { 492 | return new Lexer(this.states, this.state) 493 | } 494 | 495 | Lexer.prototype.has = function(tokenType) { 496 | for (var s in this.states) { 497 | var state = this.states[s] 498 | if (state.error && state.error.tokenType === tokenType) return true 499 | var groups = state.groups 500 | for (var i = 0; i < groups.length; i++) { 501 | var group = groups[i] 502 | if (group.tokenType === tokenType) return true 503 | if (group.keywords && hasOwnProperty.call(group.keywords, tokenType)) { 504 | return true 505 | } 506 | } 507 | } 508 | return false 509 | } 510 | 511 | 512 | return { 513 | compile: compile, 514 | states: compileStates, 515 | error: Object.freeze({error: true}), 516 | fallback: Object.freeze({fallback: true}), 517 | } 518 | 519 | } 520 | const moo = moo_factory(); 521 | const tokens = { 522 | h6: /^###### /, 523 | h5: /^##### /, 524 | h4: /^#### /, 525 | h3: /^### /, 526 | h2: /^## /, 527 | h1: /^# /, 528 | esc: /\*\*|\/\/|__/, 529 | b: '*', 530 | i: '/', 531 | u: '_', 532 | uli: /^[ ]*\- /, 533 | oli: /^[ ]*\d+\. /, 534 | a: /\[[^\]\n]*\]\([^)\n]*\)/, 535 | img: /!\[[^\]\n]*\]\([^)\n]*\)/, 536 | $$: /^\$\$$(?:\\\$|[^$])+^\$\$$/, 537 | $: /\$(?:\\\$|[^\n$])+\$/, 538 | tikz: /^\\begin\{tikzpicture\}[^]*?^\\end\{tikzpicture\}/, 539 | cmd: /^\\\w+\{[^}]*\}$/, 540 | env: /^\\\w+$/, 541 | hr: /^--$/, 542 | txt: /[^/!\n*_$\\\[\]]+|[!*_$\\/\[\]]/, 543 | blank: { match: /^\n/, lineBreaks: true }, 544 | eol: { match: /\n/, lineBreaks: true } 545 | }; 546 | function texDown(src, ...renderers) { 547 | const lexer = moo.compile(tokens); 548 | lexer.reset(src.replace(/\r/g, '')); 549 | let id = 0; 550 | let currentToken; 551 | const stack = []; 552 | const env = {}; 553 | const topElement = () => stack[stack.length - 1]; 554 | const popElement = () => { 555 | const el = stack.pop(); 556 | renderers.forEach(r => r.endElement(el)); 557 | }; 558 | const endEnv = (name) => { 559 | env[name] = false; 560 | renderers.forEach(r => r.endEnv(name)); 561 | }; 562 | const clearElements = () => { 563 | while (stack.length) 564 | popElement(); 565 | }; 566 | const clearEnvs = () => { 567 | renderers.forEach(r => { 568 | Object.entries(env).forEach(([name, b]) => { 569 | if (b) 570 | r.endEnv(name); 571 | }); 572 | }); 573 | }; 574 | const pushElement = (el) => { 575 | stack.push(el); 576 | renderers.forEach(r => r.startElement(el, id)); 577 | return el; 578 | }; 579 | const startEnv = (e) => { 580 | env[e] = true; 581 | renderers.forEach(r => r.startEnv(e)); 582 | }; 583 | const h = (type) => { 584 | clearElements(); 585 | pushElement({ 586 | type: type, 587 | token: currentToken.text 588 | }); 589 | }; 590 | const format = (type) => { 591 | if (stack.length && topElement().type === type) { 592 | popElement(); 593 | return; 594 | } 595 | if (!stack.length) 596 | pushElement({ 597 | type: 'p', 598 | token: '' 599 | }); 600 | pushElement({ 601 | type: type, 602 | token: currentToken.text 603 | }); 604 | }; 605 | const li = (type) => { 606 | const nestLevel = currentToken.text.replace(/\d+/, '').length; 607 | const matchingList = () => { 608 | const te = topElement(); 609 | return te 610 | && ['ul', 'ol'].includes(te.type) 611 | && te.data <= nestLevel; 612 | }; 613 | while (stack.length && !matchingList()) { 614 | popElement(); 615 | } 616 | const te = topElement(); 617 | if (!te || te.type !== type || te.data < nestLevel) { 618 | pushElement({ 619 | type: type, 620 | token: '', 621 | data: nestLevel 622 | }); 623 | } 624 | pushElement({ 625 | type: 'li', 626 | token: currentToken.text 627 | }); 628 | }; 629 | const reLink = /!?\[([^\]]*)\]\(([^)]*)\)/; 630 | const extractLink = (link) => { 631 | const res = reLink.exec(link); 632 | return [res[1], res[2]]; 633 | }; 634 | const reCmd = /\\(\w+)\{([^}]*)\}/; 635 | const extractCmd = (cmd) => { 636 | const res = reCmd.exec(cmd); 637 | return [res[1], res[2]]; 638 | }; 639 | const pushParIfEmpty = () => { 640 | if (!stack.length) 641 | pushElement({ 642 | type: 'p', 643 | token: '' 644 | }); 645 | }; 646 | const actions = { 647 | h6: () => h('h6'), 648 | h5: () => h('h5'), 649 | h4: () => h('h4'), 650 | h3: () => h('h3'), 651 | h2: () => h('h2'), 652 | h1: () => h('h1'), 653 | b: () => format('b'), 654 | i: () => format('i'), 655 | u: () => format('u'), 656 | uli: () => li('ul'), 657 | oli: () => li('ol'), 658 | a: () => { 659 | pushParIfEmpty(); 660 | const [title, href] = extractLink(currentToken.text); 661 | renderers.forEach(r => r.a(title, href, id)); 662 | }, 663 | img: () => { 664 | pushParIfEmpty(); 665 | const [title, href] = extractLink(currentToken.text); 666 | renderers.forEach(r => r.img(title, href, id)); 667 | }, 668 | $$: () => { 669 | clearElements(); 670 | const txt = currentToken.text; 671 | const tex = txt.substring(2, txt.length - 2); 672 | renderers.forEach(r => r.$$(tex, id)); 673 | }, 674 | $: () => { 675 | pushParIfEmpty(); 676 | const txt = currentToken.text; 677 | const tex = txt.substring(1, txt.length - 1); 678 | renderers.forEach(r => r.$(tex, id)); 679 | }, 680 | env: () => { 681 | clearElements(); 682 | const e = currentToken.text.substr(1); 683 | if (env[e]) 684 | endEnv(e); 685 | else 686 | startEnv(e); 687 | }, 688 | cmd: () => { 689 | clearElements(); 690 | const [name, arg] = extractCmd(currentToken.text); 691 | renderers.forEach(r => { 692 | r.cmd(name, arg); 693 | }); 694 | }, 695 | tikz: () => { 696 | clearElements(); 697 | renderers.forEach(r => r.tikz(currentToken.text, id)); 698 | }, 699 | hr: () => { 700 | clearElements(); 701 | renderers.forEach(r => r.hr()); 702 | }, 703 | esc: () => { 704 | renderers.forEach(r => r.esc(currentToken.text)); 705 | }, 706 | txt: () => { 707 | pushParIfEmpty(); 708 | renderers.forEach(r => r.txt(currentToken.text)); 709 | }, 710 | blank: () => { 711 | clearElements(); 712 | renderers.forEach(r => r.blank()); 713 | }, 714 | eol: () => { 715 | const multiline = ['p', 'li']; 716 | while (stack.length 717 | && !multiline.includes(topElement().type)) 718 | popElement(); 719 | const te = topElement(); 720 | if (te && multiline.includes(te.type)) { 721 | renderers.forEach(r => r.eol()); 722 | } 723 | } 724 | }; 725 | while (true) { 726 | id++; 727 | currentToken = lexer.next(); 728 | if (currentToken === undefined) 729 | break; 730 | actions[currentToken.type](); 731 | } 732 | clearElements(); 733 | clearEnvs(); 734 | renderers.forEach(r => r.done()); 735 | } 736 | 737 | class Renderer { 738 | constructor() { 739 | this.res = ''; 740 | this.startEnv = (name) => { 741 | this.res += `
`; 742 | }; 743 | this.endEnv = (name) => { 744 | this.res += `
`; 745 | }; 746 | this.cmd = (name, arg) => { 747 | this.res += `\\${name}\{${arg}\}`; 748 | }; 749 | this.hr = () => this.res += '
'; 750 | this.startElement = (el) => { 751 | this.res += `<${el.type}>`; 752 | }; 753 | this.endElement = (el) => { 754 | this.res += ``; 755 | }; 756 | this.$$ = (tex) => this.res += `$$ ${tex} $$`; 757 | this.$ = (tex) => this.res += `$ ${tex} $`; 758 | this.a = (title, href) => this.res += `${title || href}`; 759 | this.img = (title, src) => this.res += ``; 760 | this.esc = (val) => this.res += val[0]; 761 | this.txt = (val) => this.res += val; 762 | this.tikz = (tikz) => this.res += `${tikz}`; 763 | this.eol = () => { }; 764 | this.blank = () => { }; 765 | this.done = () => { }; 766 | } 767 | } 768 | 769 | -------------------------------------------------------------------------------- /templates/code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott - {{ name }} 10 | 30 | 31 | 32 | 33 | 34 | 46 | 47 | 48 | 49 | 50 |

{{ name }}

51 |
{{ note }}
52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /templates/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott - {{ name }} [edit] 10 | 75 | 76 | 77 | 83 | 144 | 145 | 146 | 147 | 148 | 149 | 154 |
155 |
156 | 157 |
158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /templates/edit_animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott - {{ name }} [edit] 10 | 95 | 96 | 97 | 98 | 99 | 100 | 106 | 107 | {% if note %} 108 | 109 | {% endif %} 110 | 111 | 112 | 113 | 114 | 115 | 116 | 121 |
122 |
123 | 128 |
129 |
130 |
131 |
132 |
133 | frame rate 134 |
135 | number of frames 136 |
137 | height 138 |
139 | width 140 |
141 |
142 | 143 |
144 | 145 |
146 |
147 |
148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /templates/edit_texdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott - {{ name }} [edit] 10 | 93 | 94 | 95 | 96 | 97 | 103 | 106 | 107 | 108 | 194 | 195 | 196 | 197 | 198 | 199 | 204 |
205 |
206 | 207 |
208 |
209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott 10 | 24 | 25 | 26 | 27 |

Welcome to jott.live

28 |
29 | Use this site to make simple notes 30 | (or TeXdown notes). 31 | {{count}} notes made so far. 32 |
33 |
34 |

Command line usage

35 | 36 | $ curl https://jott.live/raw/note/note_script > jott && chmod +x jott 37 |
38 | $ echo "note contents" | jott <name> <password> 39 |
40 | $ jott <name> 41 |
42 | note contents 43 |
44 |
45 |
46 |

Web

47 |
    48 |
  • 49 | Find a saved note at 50 | https://jott.live/note/<name> 51 |
  • 52 |
  • 53 | For code syntax highlighting 54 | https://jott.live/code/<name> 55 |
  • 56 |
  • 57 | For TeXdown rendering 58 | https://jott.live/texdown/<name> 59 |
  • 60 |
61 |
62 | Source and documentation can be found here. 63 |
64 |
65 |
66 |
67 |
68 |
69 | jott 70 | 71 | 72 | -------------------------------------------------------------------------------- /templates/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott - {{ name }} 10 | 28 | 29 | 66 | 67 | 68 | 69 | 70 |

{{ name }}

71 |
{{ note }}
72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /templates/texdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jott - {{ name }} 10 | 31 | 32 | 35 | 36 | 37 | 55 | 56 | 57 | 58 | 59 |
{{ note }}
60 | 61 | 62 | 63 | 64 | 65 | --------------------------------------------------------------------------------