├── .gitignore ├── README.md ├── img ├── install.gif └── screenshot.png ├── npm.coffee ├── package.json ├── sheet.coffee └── sheetExample.framer ├── .gitignore ├── app.coffee ├── framer ├── .bookmark ├── coffee-script.js ├── config.json ├── framer.generated.js ├── framer.init.js ├── framer.js ├── framer.js.map ├── framer.modules.js ├── images │ ├── cursor-active.png │ ├── cursor-active@2x.png │ ├── cursor.png │ ├── cursor@2x.png │ ├── icon-120.png │ ├── icon-152.png │ ├── icon-180.png │ ├── icon-192.png │ └── icon-76.png ├── style.css └── version ├── images └── .gitkeep ├── index.html └── modules ├── npm.coffee └── sheet.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Framer-Sheet 2 | 3 | Import information from Google Sheets into Framer. 4 | 5 | ![Sheets](img/screenshot.png) 6 | 7 | ## Add it to your Framer Studio project 8 | 9 | * Create and save a new Framer project (if you don't have one started already) 10 | * [Download](https://github.com/andrewliebchen/framer-sheet/archive/master.zip) or clone this repository 11 | * Copy `sheet.coffee` and `npm.coffee` to your `/modules` directory 12 | * Open your Terminal, drag your framer project into the Terminal window and press enter. The path in the Terminal will update to your Framer project 13 | * Type `npm install tabletop` to get the dependency from NPM 14 | 15 | ![Install](img/install.gif) 16 | 17 | * Import the module into your project by adding `{ Sheet } = require 'sheet'` to the top of your project's code 18 | 19 | ## How to use it 20 | 21 | Getting started is pretty easy. Follow the instructions above to install the module. You'll also need a [Google Sheets](http://drive.google.com/) document to import. Here's how to set up your sheet: 22 | 23 | Go up to the `File` menu and pick `Publish to the web`. Fiddle with the options, then click `Start` publishing. A URL will appear, something like `https://docs.google.com/spreadsheets/d/1sbyMINQHPsJctjAtMW0lCfLrcpMqoGMOJj6AN-sNQrc/pubhtml` (of course, it might look a little different). 24 | 25 | In the example URL above, you're interested in the key, which is between the `/d/` and `/pubhtml`, so `1sbyMINQHPsJctjAtMW0lCfLrcpMqoGMOJj6AN-sNQrc`. Copy this value. 26 | 27 | In your Framer project, instantiate a new instance in your project: 28 | 29 | ```coffeescript 30 | { Sheet } = require 'sheet' 31 | 32 | sheet = new Sheet 33 | key: '1sbyMINQHPsJctjAtMW0lCfLrcpMqoGMOJj6AN-sNQrc' 34 | ``` 35 | 36 | Now, to actually GET the data from your sheet, call the `get` method: 37 | 38 | ```coffeescript 39 | sheet.get((data, sheet) -> 40 | print data 41 | ) 42 | ``` 43 | 44 | ...which should print an array of objects (a collection) like... 45 | 46 | ``` 47 | » [{Name:"Carrot", Category:"Vegetable", Healthiness:"Adequate"}, 48 | {Name:"Pork Shoulder", Category:"Meat", Healthiness:"Questionable"}, 49 | {Name:"Bubblegum", Category:"Candy", Healthiness:"Super High"}] 50 | ``` 51 | 52 | Pretty cool, huh? You'll see in the example project, I've used Lodash's `map` to split the collection into rows which and cells for a table. 53 | 54 | ```coffeescript 55 | sheet.get((data, sheet) -> 56 | _.map data, (row, i) -> 57 | @row = new Layer 58 | name: 'row' 59 | parent: table 60 | # ... 61 | 62 | @name = new Layer 63 | html: row.Name 64 | name: "cell:#{row.Name}" 65 | parent: @row 66 | # ... 67 | ``` 68 | 69 | This is only the beginning...what else can you think to do? 70 | 71 | ## More information 72 | 73 | This module makes use of [Tabletop.js](https://github.com/jsoma/tabletop) for the heavy lifting. For more information, check out their documentation, including a section on how to format your Google Sheet. 74 | -------------------------------------------------------------------------------- /img/install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/img/install.gif -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/img/screenshot.png -------------------------------------------------------------------------------- /npm.coffee: -------------------------------------------------------------------------------- 1 | exports.Tabletop = require 'tabletop' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "framer-sheet", 3 | "version": "1.0.0", 4 | "description": "Import information from Google Sheets into Framer.", 5 | "main": "app.coffee", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/andrewliebchen/framer-sheet.git" 12 | }, 13 | "keywords": [ 14 | "Framer", 15 | "Google", 16 | "Sheets", 17 | "Framer", 18 | "module", 19 | "FramerJS", 20 | "Framer", 21 | "Studio" 22 | ], 23 | "author": "Andrew Liebchen ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/andrewliebchen/framer-sheet/issues" 27 | }, 28 | "homepage": "https://github.com/andrewliebchen/framer-sheet#readme", 29 | "devDependencies": { 30 | "tabletop": "^1.5.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sheet.coffee: -------------------------------------------------------------------------------- 1 | { Tabletop } = require 'npm' 2 | 3 | 4 | class exports.Sheet 5 | constructor: (options) -> 6 | @_key = options.key 7 | 8 | @get = (callback) => 9 | Tabletop.init { 10 | key: @_key 11 | simpleSheet: true 12 | callback: (data, sheet) => 13 | return callback(data, sheet) 14 | } 15 | -------------------------------------------------------------------------------- /sheetExample.framer/.gitignore: -------------------------------------------------------------------------------- 1 | # Framer Git Ignore 2 | 3 | # General OSX 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | # Framer Specific 31 | .*.html 32 | .app.js 33 | framer/*.old* 34 | framer/.*.hash 35 | framer/backup.coffee 36 | framer/backups/* 37 | framer/manifest.txt 38 | framer/metadata.json 39 | framer/preview.png 40 | framer/social-880x460.png 41 | framer/social-1200x630.png 42 | -------------------------------------------------------------------------------- /sheetExample.framer/app.coffee: -------------------------------------------------------------------------------- 1 | { Sheet } = require 'sheet' 2 | 3 | Framer.Defaults.Layer = 4 | height: 40 5 | backgroundColor: null 6 | color: 'black' 7 | style: 8 | 'font-size': '16px' 9 | 'line-height': '40px' 10 | 'padding': '0 0.5em' 11 | 12 | 13 | sheet = new Sheet 14 | key: '0AmYzu_s7QHsmdDNZUzRlYldnWTZCLXdrMXlYQzVxSFE' 15 | 16 | table = new Layer 17 | 18 | sheet.get((data, sheet) -> 19 | _.map data, (row, i) -> 20 | @row = new Layer 21 | name: 'row' 22 | parent: table 23 | backgroundColor: null 24 | width: 600 25 | backgroundColor: if i % 2 then '#f0f0f0' 26 | x: 10 27 | y: 40 * i 28 | 29 | 30 | @name = new Layer 31 | html: row.Name 32 | name: "cell:#{row.Name}" 33 | parent: @row 34 | 35 | @category = new Layer 36 | html: row.Category 37 | name: "cell:#{row.Category}" 38 | parent: @row 39 | x: 200 40 | 41 | @healthiness = new Layer 42 | html: row.Healthiness 43 | name: "cell:#{row.Healthiness}" 44 | parent: @row 45 | height: 40 46 | x: 400 47 | ) 48 | -------------------------------------------------------------------------------- /sheetExample.framer/framer/.bookmark: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/.bookmark -------------------------------------------------------------------------------- /sheetExample.framer/framer/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "propertyPanelToggleStates" : { 3 | 4 | }, 5 | "deviceOrientation" : 0, 6 | "sharedPrototype" : 0, 7 | "contentScale" : 1, 8 | "deviceType" : "fullscreen", 9 | "selectedHand" : "", 10 | "updateDelay" : 0.3, 11 | "deviceScale" : 1, 12 | "foldedCodeRanges" : [ 13 | 14 | ], 15 | "orientation" : 0, 16 | "projectId" : "C33ADB9E-0989-4525-B853-CFA5081510A5" 17 | } -------------------------------------------------------------------------------- /sheetExample.framer/framer/framer.generated.js: -------------------------------------------------------------------------------- 1 | // This is autogenerated by Framer 2 | 3 | 4 | if (!window.Framer && window._bridge) {window._bridge('runtime.error', {message:'[framer.js] Framer library missing or corrupt. Select File → Update Framer Library.'})} 5 | if (DeviceComponent) {DeviceComponent.Devices["iphone-6-silver"].deviceImageJP2 = false}; 6 | if (window.Framer) {window.Framer.Defaults.DeviceView = {"deviceScale":1,"selectedHand":"","deviceType":"fullscreen","contentScale":1,"orientation":0}; 7 | } 8 | if (window.Framer) {window.Framer.Defaults.DeviceComponent = {"deviceScale":1,"selectedHand":"","deviceType":"fullscreen","contentScale":1,"orientation":0}; 9 | } 10 | window.FramerStudioInfo = {"deviceImagesUrl":"\/_server\/resources\/DeviceImages","documentTitle":"sheetExample.framer"}; 11 | 12 | Framer.Device = new Framer.DeviceView(); 13 | Framer.Device.setupContext(); -------------------------------------------------------------------------------- /sheetExample.framer/framer/framer.init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function isFileLoadingAllowed() { 4 | return (window.location.protocol.indexOf("file") == -1) 5 | } 6 | 7 | function isHomeScreened() { 8 | return ("standalone" in window.navigator) && window.navigator.standalone == true 9 | } 10 | 11 | function isCompatibleBrowser() { 12 | return Utils.isWebKit() 13 | } 14 | 15 | var alertNode; 16 | 17 | function dismissAlert() { 18 | alertNode.parentElement.removeChild(alertNode) 19 | loadProject() 20 | } 21 | 22 | function showAlert(html) { 23 | 24 | alertNode = document.createElement("div") 25 | 26 | alertNode.classList.add("framerAlertBackground") 27 | alertNode.innerHTML = html 28 | 29 | document.addEventListener("DOMContentLoaded", function(event) { 30 | document.body.appendChild(alertNode) 31 | }) 32 | 33 | window.dismissAlert = dismissAlert; 34 | } 35 | 36 | function showBrowserAlert() { 37 | var html = "" 38 | html += "
" 39 | html += "Error: Not A WebKit Browser" 40 | html += "Your browser is not supported.
Please use Safari or Chrome.
" 41 | html += "Try anyway" 42 | html += "
" 43 | 44 | showAlert(html) 45 | } 46 | 47 | function showFileLoadingAlert() { 48 | var html = "" 49 | html += "
" 50 | html += "Error: Local File Restrictions" 51 | html += "Preview this prototype with Framer Mirror or learn more about " 52 | html += "file restrictions.
" 53 | html += "Try anyway" 54 | html += "
" 55 | 56 | showAlert(html) 57 | } 58 | 59 | function loadProject() { 60 | CoffeeScript.load("app.coffee") 61 | } 62 | 63 | function setDefaultPageTitle() { 64 | // If no title was set we set it to the project folder name so 65 | // you get a nice name on iOS if you bookmark to desktop. 66 | document.addEventListener("DOMContentLoaded", function() { 67 | if (document.title == "") { 68 | if (window.FramerStudioInfo && window.FramerStudioInfo.documentTitle) { 69 | document.title = window.FramerStudioInfo.documentTitle 70 | } else { 71 | document.title = window.location.pathname.replace(/\//g, "") 72 | } 73 | } 74 | }) 75 | } 76 | 77 | function init() { 78 | 79 | if (Utils.isFramerStudio()) { 80 | return 81 | } 82 | 83 | setDefaultPageTitle() 84 | 85 | if (!isCompatibleBrowser()) { 86 | return showBrowserAlert() 87 | } 88 | 89 | if (!isFileLoadingAllowed()) { 90 | return showFileLoadingAlert() 91 | } 92 | 93 | loadProject() 94 | 95 | } 96 | 97 | init() 98 | 99 | })() 100 | -------------------------------------------------------------------------------- /sheetExample.framer/framer/framer.modules.js: -------------------------------------------------------------------------------- 1 | require=(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 1) { 138 | for (var i = 1; i < arguments.length; i++) { 139 | args[i - 1] = arguments[i]; 140 | } 141 | } 142 | queue.push(new Item(fun, args)); 143 | if (queue.length === 1 && !draining) { 144 | runTimeout(drainQueue); 145 | } 146 | }; 147 | 148 | // v8 likes predictible objects 149 | function Item(fun, array) { 150 | this.fun = fun; 151 | this.array = array; 152 | } 153 | Item.prototype.run = function () { 154 | this.fun.apply(null, this.array); 155 | }; 156 | process.title = 'browser'; 157 | process.browser = true; 158 | process.env = {}; 159 | process.argv = []; 160 | process.version = ''; // empty string to avoid regexp issues 161 | process.versions = {}; 162 | 163 | function noop() {} 164 | 165 | process.on = noop; 166 | process.addListener = noop; 167 | process.once = noop; 168 | process.off = noop; 169 | process.removeListener = noop; 170 | process.removeAllListeners = noop; 171 | process.emit = noop; 172 | 173 | process.binding = function (name) { 174 | throw new Error('process.binding is not supported'); 175 | }; 176 | 177 | process.cwd = function () { return '/' }; 178 | process.chdir = function (dir) { 179 | throw new Error('process.chdir is not supported'); 180 | }; 181 | process.umask = function() { return 0; }; 182 | 183 | },{}],2:[function(require,module,exports){ 184 | (function (process){ 185 | (function() { 186 | 'use strict'; 187 | 188 | var inNodeJS = false; 189 | if (typeof process !== 'undefined' && !process.browser) { 190 | inNodeJS = true; 191 | var request = require('request'.trim()); //prevents browserify from bundling the module 192 | } 193 | 194 | var supportsCORS = false; 195 | var inLegacyIE = false; 196 | try { 197 | var testXHR = new XMLHttpRequest(); 198 | if (typeof testXHR.withCredentials !== 'undefined') { 199 | supportsCORS = true; 200 | } else { 201 | if ('XDomainRequest' in window) { 202 | supportsCORS = true; 203 | inLegacyIE = true; 204 | } 205 | } 206 | } catch (e) { } 207 | 208 | // Create a simple indexOf function for support 209 | // of older browsers. Uses native indexOf if 210 | // available. Code similar to underscores. 211 | // By making a separate function, instead of adding 212 | // to the prototype, we will not break bad for loops 213 | // in older browsers 214 | var indexOfProto = Array.prototype.indexOf; 215 | var ttIndexOf = function(array, item) { 216 | var i = 0, l = array.length; 217 | 218 | if (indexOfProto && array.indexOf === indexOfProto) { 219 | return array.indexOf(item); 220 | } 221 | 222 | for (; i < l; i++) { 223 | if (array[i] === item) { 224 | return i; 225 | } 226 | } 227 | return -1; 228 | }; 229 | 230 | /* 231 | Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) 232 | OR! 233 | Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } ) 234 | OR! 235 | Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc') 236 | */ 237 | 238 | var Tabletop = function(options) { 239 | // Make sure Tabletop is being used as a constructor no matter what. 240 | if(!this || !(this instanceof Tabletop)) { 241 | return new Tabletop(options); 242 | } 243 | 244 | if(typeof(options) === 'string') { 245 | options = { key : options }; 246 | } 247 | 248 | this.callback = options.callback; 249 | this.wanted = options.wanted || []; 250 | this.key = options.key; 251 | this.simpleSheet = !!options.simpleSheet; 252 | this.parseNumbers = !!options.parseNumbers; 253 | this.wait = !!options.wait; 254 | this.reverse = !!options.reverse; 255 | this.postProcess = options.postProcess; 256 | this.debug = !!options.debug; 257 | this.query = options.query || ''; 258 | this.orderby = options.orderby; 259 | this.endpoint = options.endpoint || 'https://spreadsheets.google.com'; 260 | this.singleton = !!options.singleton; 261 | this.simpleUrl = !!(options.simpleUrl || options.simple_url); //jshint ignore:line 262 | this.callbackContext = options.callbackContext; 263 | // Default to on, unless there's a proxy, in which case it's default off 264 | this.prettyColumnNames = typeof(options.prettyColumnNames) === 'undefined' ? !options.proxy : options.prettyColumnNames; 265 | 266 | if(typeof(options.proxy) !== 'undefined') { 267 | // Remove trailing slash, it will break the app 268 | this.endpoint = options.proxy.replace(/\/$/,''); 269 | this.simpleUrl = true; 270 | this.singleton = true; 271 | // Let's only use CORS (straight JSON request) when 272 | // fetching straight from Google 273 | supportsCORS = false; 274 | } 275 | 276 | this.parameterize = options.parameterize || false; 277 | 278 | if (this.singleton) { 279 | if (typeof(Tabletop.singleton) !== 'undefined') { 280 | this.log('WARNING! Tabletop singleton already defined'); 281 | } 282 | Tabletop.singleton = this; 283 | } 284 | 285 | /* Be friendly about what you accept */ 286 | if (/key=/.test(this.key)) { 287 | this.log('You passed an old Google Docs url as the key! Attempting to parse.'); 288 | this.key = this.key.match('key=(.*?)(&|#|$)')[1]; 289 | } 290 | 291 | if (/pubhtml/.test(this.key)) { 292 | this.log('You passed a new Google Spreadsheets url as the key! Attempting to parse.'); 293 | this.key = this.key.match('d\\/(.*?)\\/pubhtml')[1]; 294 | } 295 | 296 | if(/spreadsheets\/d/.test(this.key)) { 297 | this.log('You passed the most recent version of Google Spreadsheets url as the key! Attempting to parse.'); 298 | this.key = this.key.match('d\\/(.*?)\/')[1]; 299 | } 300 | 301 | if (!this.key) { 302 | this.log('You need to pass Tabletop a key!'); 303 | return; 304 | } 305 | 306 | this.log('Initializing with key ' + this.key); 307 | 308 | this.models = {}; 309 | this.modelNames = []; 310 | this.model_names = this.modelNames; //jshint ignore:line 311 | 312 | this.baseJsonPath = '/feeds/worksheets/' + this.key + '/public/basic?alt='; 313 | 314 | if (inNodeJS || supportsCORS) { 315 | this.baseJsonPath += 'json'; 316 | } else { 317 | this.baseJsonPath += 'json-in-script'; 318 | } 319 | 320 | if(!this.wait) { 321 | this.fetch(); 322 | } 323 | }; 324 | 325 | // A global storage for callbacks. 326 | Tabletop.callbacks = {}; 327 | 328 | // Backwards compatibility. 329 | Tabletop.init = function(options) { 330 | return new Tabletop(options); 331 | }; 332 | 333 | Tabletop.sheets = function() { 334 | this.log('Times have changed! You\'ll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)'); 335 | }; 336 | 337 | Tabletop.prototype = { 338 | 339 | fetch: function(callback) { 340 | if (typeof(callback) !== 'undefined') { 341 | this.callback = callback; 342 | } 343 | this.requestData(this.baseJsonPath, this.loadSheets); 344 | }, 345 | 346 | /* 347 | This will call the environment appropriate request method. 348 | 349 | In browser it will use JSON-P, in node it will use request() 350 | */ 351 | requestData: function(path, callback) { 352 | this.log('Requesting', path); 353 | 354 | if (inNodeJS) { 355 | this.serverSideFetch(path, callback); 356 | } else { 357 | //CORS only works in IE8/9 across the same protocol 358 | //You must have your server on HTTPS to talk to Google, or it'll fall back on injection 359 | var protocol = this.endpoint.split('//').shift() || 'http'; 360 | if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { 361 | this.xhrFetch(path, callback); 362 | } else { 363 | this.injectScript(path, callback); 364 | } 365 | } 366 | }, 367 | 368 | /* 369 | Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. 370 | */ 371 | xhrFetch: function(path, callback) { 372 | //support IE8's separate cross-domain object 373 | var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); 374 | xhr.open('GET', this.endpoint + path); 375 | var self = this; 376 | xhr.onload = function() { 377 | var json; 378 | try { 379 | json = JSON.parse(xhr.responseText); 380 | } catch (e) { 381 | console.error(e); 382 | } 383 | callback.call(self, json); 384 | }; 385 | xhr.send(); 386 | }, 387 | 388 | /* 389 | Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data 390 | it triggers the callback. This helps you avoid cross-domain errors 391 | http://code.google.com/apis/gdata/samples/spreadsheet_sample.html 392 | 393 | Let's be plain-Jane and not use jQuery or anything. 394 | */ 395 | injectScript: function(path, callback) { 396 | var script = document.createElement('script'); 397 | var callbackName; 398 | 399 | if (this.singleton) { 400 | if (callback === this.loadSheets) { 401 | callbackName = 'Tabletop.singleton.loadSheets'; 402 | } else if (callback === this.loadSheet) { 403 | callbackName = 'Tabletop.singleton.loadSheet'; 404 | } 405 | } else { 406 | var self = this; 407 | callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); 408 | // Create a temp callback which will get removed once it has executed, 409 | // this allows multiple instances of Tabletop to coexist. 410 | Tabletop.callbacks[ callbackName ] = function () { 411 | var args = Array.prototype.slice.call( arguments, 0 ); 412 | callback.apply(self, args); 413 | script.parentNode.removeChild(script); 414 | delete Tabletop.callbacks[callbackName]; 415 | }; 416 | callbackName = 'Tabletop.callbacks.' + callbackName; 417 | } 418 | 419 | var url = path + '&callback=' + callbackName; 420 | 421 | if (this.simpleUrl) { 422 | // We've gone down a rabbit hole of passing injectScript the path, so let's 423 | // just pull the sheet_id out of the path like the least efficient worker bees 424 | if(path.indexOf('/list/') !== -1) { 425 | script.src = this.endpoint + '/' + this.key + '-' + path.split('/')[4]; 426 | } else { 427 | script.src = this.endpoint + '/' + this.key; 428 | } 429 | } else { 430 | script.src = this.endpoint + url; 431 | } 432 | 433 | if (this.parameterize) { 434 | script.src = this.parameterize + encodeURIComponent(script.src); 435 | } 436 | 437 | this.log('Injecting', script.src); 438 | 439 | document.getElementsByTagName('script')[0].parentNode.appendChild(script); 440 | }, 441 | 442 | /* 443 | This will only run if tabletop is being run in node.js 444 | */ 445 | serverSideFetch: function(path, callback) { 446 | var self = this; 447 | 448 | this.log('Fetching', this.endpoint + path); 449 | request({url: this.endpoint + path, json: true}, function(err, resp, body) { 450 | if (err) { 451 | return console.error(err); 452 | } 453 | callback.call(self, body); 454 | }); 455 | }, 456 | 457 | /* 458 | Is this a sheet you want to pull? 459 | If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported 460 | Pulls all sheets if none are specified 461 | */ 462 | isWanted: function(sheetName) { 463 | if (this.wanted.length === 0) { 464 | return true; 465 | } else { 466 | return (ttIndexOf(this.wanted, sheetName) !== -1); 467 | } 468 | }, 469 | 470 | /* 471 | What gets send to the callback 472 | if simpleSheet === true, then don't return an array of Tabletop.this.models, 473 | only return the first one's elements 474 | */ 475 | data: function() { 476 | // If the instance is being queried before the data's been fetched 477 | // then return undefined. 478 | if (this.modelNames.length === 0) { 479 | return undefined; 480 | } 481 | if (this.simpleSheet) { 482 | if (this.modelNames.length > 1 && this.debug) { 483 | this.log('WARNING You have more than one sheet but are using simple sheet mode! Don\'t blame me when something goes wrong.'); 484 | } 485 | return this.models[this.modelNames[0]].all(); 486 | } else { 487 | return this.models; 488 | } 489 | }, 490 | 491 | /* 492 | Add another sheet to the wanted list 493 | */ 494 | addWanted: function(sheet) { 495 | if(ttIndexOf(this.wanted, sheet) === -1) { 496 | this.wanted.push(sheet); 497 | } 498 | }, 499 | 500 | /* 501 | Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 502 | Need to use injectScript because the worksheet view that you're working from 503 | doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 504 | Calls back to loadSheet in order to get the real work done. 505 | 506 | Used as a callback for the worksheet-based JSON 507 | */ 508 | loadSheets: function(data) { 509 | var i, ilen; 510 | var toLoad = []; 511 | this.googleSheetName = data.feed.title.$t; 512 | this.foundSheetNames = []; 513 | 514 | for (i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 515 | this.foundSheetNames.push(data.feed.entry[i].title.$t); 516 | // Only pull in desired sheets to reduce loading 517 | if (this.isWanted(data.feed.entry[i].content.$t)) { 518 | var linkIdx = data.feed.entry[i].link.length-1; 519 | var sheetId = data.feed.entry[i].link[linkIdx].href.split('/').pop(); 520 | var jsonPath = '/feeds/list/' + this.key + '/' + sheetId + '/public/values?alt='; 521 | if (inNodeJS || supportsCORS) { 522 | jsonPath += 'json'; 523 | } else { 524 | jsonPath += 'json-in-script'; 525 | } 526 | if (this.query) { 527 | // Query Language Reference (0.7) 528 | jsonPath += '&tq=' + this.query; 529 | } 530 | if (this.orderby) { 531 | jsonPath += '&orderby=column:' + this.orderby.toLowerCase(); 532 | } 533 | if (this.reverse) { 534 | jsonPath += '&reverse=true'; 535 | } 536 | toLoad.push(jsonPath); 537 | } 538 | } 539 | 540 | this.sheetsToLoad = toLoad.length; 541 | for(i = 0, ilen = toLoad.length; i < ilen; i++) { 542 | this.requestData(toLoad[i], this.loadSheet); 543 | } 544 | }, 545 | 546 | /* 547 | Access layer for the this.models 548 | .sheets() gets you all of the sheets 549 | .sheets('Sheet1') gets you the sheet named Sheet1 550 | */ 551 | sheets: function(sheetName) { 552 | if (typeof sheetName === 'undefined') { 553 | return this.models; 554 | } else { 555 | if (typeof(this.models[sheetName]) === 'undefined') { 556 | // alert( "Can't find " + sheetName ); 557 | return; 558 | } else { 559 | return this.models[sheetName]; 560 | } 561 | } 562 | }, 563 | 564 | sheetReady: function(model) { 565 | this.models[model.name] = model; 566 | if (ttIndexOf(this.modelNames, model.name) === -1) { 567 | this.modelNames.push(model.name); 568 | } 569 | 570 | this.sheetsToLoad--; 571 | if (this.sheetsToLoad === 0) { 572 | this.doCallback(); 573 | } 574 | }, 575 | 576 | /* 577 | Parse a single list-based worksheet, turning it into a Tabletop Model 578 | 579 | Used as a callback for the list-based JSON 580 | */ 581 | loadSheet: function(data) { 582 | var that = this; 583 | new Tabletop.Model({ 584 | data: data, 585 | parseNumbers: this.parseNumbers, 586 | postProcess: this.postProcess, 587 | tabletop: this, 588 | prettyColumnNames: this.prettyColumnNames, 589 | onReady: function() { 590 | that.sheetReady(this); 591 | } 592 | }); 593 | }, 594 | 595 | /* 596 | Execute the callback upon loading! Rely on this.data() because you might 597 | only request certain pieces of data (i.e. simpleSheet mode) 598 | Tests this.sheetsToLoad just in case a race condition happens to show up 599 | */ 600 | doCallback: function() { 601 | if(this.sheetsToLoad === 0) { 602 | this.callback.apply(this.callbackContext || this, [this.data(), this]); 603 | } 604 | }, 605 | 606 | log: function() { 607 | if(this.debug) { 608 | if(typeof console !== 'undefined' && typeof console.log !== 'undefined') { 609 | Function.prototype.apply.apply(console.log, [console, arguments]); 610 | } 611 | } 612 | } 613 | 614 | }; 615 | 616 | /* 617 | Tabletop.Model stores the attribute names and parses the worksheet data 618 | to turn it into something worthwhile 619 | 620 | Options should be in the format { data: XXX }, with XXX being the list-based worksheet 621 | */ 622 | Tabletop.Model = function(options) { 623 | var i, j, ilen, jlen; 624 | this.columnNames = []; 625 | this.column_names = this.columnNames; // jshint ignore:line 626 | this.name = options.data.feed.title.$t; 627 | this.tabletop = options.tabletop; 628 | this.elements = []; 629 | this.onReady = options.onReady; 630 | this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 631 | 632 | if (typeof(options.data.feed.entry) === 'undefined') { 633 | options.tabletop.log('Missing data for ' + this.name + ', make sure you didn\'t forget column headers'); 634 | this.originalColumns = []; 635 | this.elements = []; 636 | this.onReady.call(this); 637 | return; 638 | } 639 | 640 | for (var key in options.data.feed.entry[0]){ 641 | if (/^gsx/.test(key)) { 642 | this.columnNames.push(key.replace('gsx$','')); 643 | } 644 | } 645 | 646 | this.originalColumns = this.columnNames; 647 | this.original_columns = this.originalColumns; // jshint ignore:line 648 | 649 | for (i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 650 | var source = options.data.feed.entry[i]; 651 | var element = {}; 652 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 653 | var cell = source['gsx$' + this.columnNames[j]]; 654 | if (typeof(cell) !== 'undefined') { 655 | if (options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) { 656 | element[this.columnNames[j]] = +cell.$t; 657 | } else { 658 | element[this.columnNames[j]] = cell.$t; 659 | } 660 | } else { 661 | element[this.columnNames[j]] = ''; 662 | } 663 | } 664 | if (element.rowNumber === undefined) { 665 | element.rowNumber = i + 1; 666 | } 667 | 668 | if (options.postProcess) { 669 | options.postProcess(element); 670 | } 671 | 672 | this.elements.push(element); 673 | } 674 | 675 | if (options.prettyColumnNames) { 676 | this.fetchPrettyColumns(); 677 | } else { 678 | this.onReady.call(this); 679 | } 680 | }; 681 | 682 | Tabletop.Model.prototype = { 683 | /* 684 | Returns all of the elements (rows) of the worksheet as objects 685 | */ 686 | all: function() { 687 | return this.elements; 688 | }, 689 | 690 | fetchPrettyColumns: function() { 691 | if (!this.raw.feed.link[3]) { 692 | return this.ready(); 693 | } 694 | 695 | var cellurl = this.raw.feed.link[3].href.replace('/feeds/list/', '/feeds/cells/').replace('https://spreadsheets.google.com', ''); 696 | var that = this; 697 | this.tabletop.requestData(cellurl, function(data) { 698 | that.loadPrettyColumns(data); 699 | }); 700 | }, 701 | 702 | ready: function() { 703 | this.onReady.call(this); 704 | }, 705 | 706 | /* 707 | * Store column names as an object 708 | * with keys of Google-formatted "columnName" 709 | * and values of human-readable "Column name" 710 | */ 711 | loadPrettyColumns: function(data) { 712 | var prettyColumns = {}; 713 | 714 | var columnNames = this.columnNames; 715 | 716 | var i = 0; 717 | var l = columnNames.length; 718 | 719 | for (; i < l; i++) { 720 | if (typeof data.feed.entry[i].content.$t !== 'undefined') { 721 | prettyColumns[columnNames[i]] = data.feed.entry[i].content.$t; 722 | } else { 723 | prettyColumns[columnNames[i]] = columnNames[i]; 724 | } 725 | } 726 | 727 | this.prettyColumns = prettyColumns; 728 | this.pretty_columns = this.prettyColumns; // jshint ignore:line 729 | this.prettifyElements(); 730 | this.ready(); 731 | }, 732 | 733 | /* 734 | * Go through each row, substitutiting 735 | * Google-formatted "columnName" 736 | * with human-readable "Column name" 737 | */ 738 | prettifyElements: function() { 739 | var prettyElements = [], 740 | orderedPrettyNames = [], 741 | i, j, ilen, jlen; 742 | 743 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 744 | orderedPrettyNames.push(this.prettyColumns[this.columnNames[j]]); 745 | } 746 | 747 | for (i = 0, ilen = this.elements.length; i < ilen; i++) { 748 | var newElement = {}; 749 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 750 | var newColumnName = this.prettyColumns[this.columnNames[j]]; 751 | newElement[newColumnName] = this.elements[i][this.columnNames[j]]; 752 | } 753 | prettyElements.push(newElement); 754 | } 755 | this.elements = prettyElements; 756 | this.columnNames = orderedPrettyNames; 757 | }, 758 | 759 | /* 760 | Return the elements as an array of arrays, instead of an array of objects 761 | */ 762 | toArray: function() { 763 | var array = [], 764 | i, j, ilen, jlen; 765 | for (i = 0, ilen = this.elements.length; i < ilen; i++) { 766 | var row = []; 767 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 768 | row.push(this.elements[i][ this.columnNames[j]]); 769 | } 770 | array.push(row); 771 | } 772 | 773 | return array; 774 | } 775 | }; 776 | 777 | if(typeof module !== 'undefined' && module.exports) { //don't just use inNodeJS, we may be in Browserify 778 | module.exports = Tabletop; 779 | } else if (typeof define === 'function' && define.amd) { 780 | define(function () { 781 | return Tabletop; 782 | }); 783 | } else { 784 | window.Tabletop = Tabletop; 785 | } 786 | 787 | })(); 788 | }).call(this,require('_process')) 789 | 790 | },{"_process":1}],"npm":[function(require,module,exports){ 791 | exports.Tabletop = require('tabletop'); 792 | 793 | 794 | },{"tabletop":2}],"sheet":[function(require,module,exports){ 795 | var Tabletop; 796 | 797 | Tabletop = require('npm').Tabletop; 798 | 799 | exports.Sheet = (function() { 800 | function Sheet(options) { 801 | this._key = options.key; 802 | this.get = (function(_this) { 803 | return function(callback) { 804 | return Tabletop.init({ 805 | key: _this._key, 806 | simpleSheet: true, 807 | callback: function(data, sheet) { 808 | return callback(data, sheet); 809 | } 810 | }); 811 | }; 812 | })(this); 813 | } 814 | 815 | return Sheet; 816 | 817 | })(); 818 | 819 | 820 | },{"npm":"npm"}]},{},[]) 821 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnJhbWVyLm1vZHVsZXMuanMiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL1VzZXJzL2xpZWJjaGVuL0Ryb3Bib3ggKEZhY2Vib29rKS9QZXJzb25hbC9GcmFtZXIvU2hlZXQvc2hlZXRFeGFtcGxlLmZyYW1lci9tb2R1bGVzL3NoZWV0LmNvZmZlZSIsIi4uLy4uLy4uLy4uLy4uL1VzZXJzL2xpZWJjaGVuL0Ryb3Bib3ggKEZhY2Vib29rKS9QZXJzb25hbC9GcmFtZXIvU2hlZXQvc2hlZXRFeGFtcGxlLmZyYW1lci9tb2R1bGVzL25wbS5jb2ZmZWUiLCIuLi8uLi8uLi8uLi8uLi9Vc2Vycy9saWViY2hlbi9Ecm9wYm94IChGYWNlYm9vaykvUGVyc29uYWwvRnJhbWVyL1NoZWV0L3NoZWV0RXhhbXBsZS5mcmFtZXIvbm9kZV9tb2R1bGVzL3RhYmxldG9wL3NyYy90YWJsZXRvcC5qcyIsIm5vZGVfbW9kdWxlcy9wcm9jZXNzL2Jyb3dzZXIuanMiLCJub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbInsgVGFibGV0b3AgfSA9IHJlcXVpcmUgJ25wbSdcblxuXG5jbGFzcyBleHBvcnRzLlNoZWV0XG4gIGNvbnN0cnVjdG9yOiAob3B0aW9ucykgLT5cbiAgICBAX2tleSA9IG9wdGlvbnMua2V5XG5cbiAgICBAZ2V0ID0gKGNhbGxiYWNrKSA9PlxuICAgICAgVGFibGV0b3AuaW5pdCB7XG4gICAgICAgIGtleTogQF9rZXlcbiAgICAgICAgc2ltcGxlU2hlZXQ6IHRydWVcbiAgICAgICAgY2FsbGJhY2s6IChkYXRhLCBzaGVldCkgPT5cbiAgICAgICAgICByZXR1cm4gY2FsbGJhY2soZGF0YSwgc2hlZXQpXG4gICAgICB9XG4iLCJleHBvcnRzLlRhYmxldG9wID0gcmVxdWlyZSAndGFibGV0b3AnXG4iLCIoZnVuY3Rpb24oKSB7XG4gICd1c2Ugc3RyaWN0JztcblxuICB2YXIgaW5Ob2RlSlMgPSBmYWxzZTtcbiAgaWYgKHR5cGVvZiBwcm9jZXNzICE9PSAndW5kZWZpbmVkJyAmJiAhcHJvY2Vzcy5icm93c2VyKSB7XG4gICAgaW5Ob2RlSlMgPSB0cnVlO1xuICAgIHZhciByZXF1ZXN0ID0gcmVxdWlyZSgncmVxdWVzdCcudHJpbSgpKTsgLy9wcmV2ZW50cyBicm93c2VyaWZ5IGZyb20gYnVuZGxpbmcgdGhlIG1vZHVsZVxuICB9XG5cbiAgdmFyIHN1cHBvcnRzQ09SUyA9IGZhbHNlO1xuICB2YXIgaW5MZWdhY3lJRSA9IGZhbHNlO1xuICB0cnkge1xuICAgIHZhciB0ZXN0WEhSID0gbmV3IFhNTEh0dHBSZXF1ZXN0KCk7XG4gICAgaWYgKHR5cGVvZiB0ZXN0WEhSLndpdGhDcmVkZW50aWFscyAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgIHN1cHBvcnRzQ09SUyA9IHRydWU7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmICgnWERvbWFpblJlcXVlc3QnIGluIHdpbmRvdykge1xuICAgICAgICBzdXBwb3J0c0NPUlMgPSB0cnVlO1xuICAgICAgICBpbkxlZ2FjeUlFID0gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHsgfVxuXG4gIC8vIENyZWF0ZSBhIHNpbXBsZSBpbmRleE9mIGZ1bmN0aW9uIGZvciBzdXBwb3J0XG4gIC8vIG9mIG9sZGVyIGJyb3dzZXJzLiAgVXNlcyBuYXRpdmUgaW5kZXhPZiBpZiBcbiAgLy8gYXZhaWxhYmxlLiAgQ29kZSBzaW1pbGFyIHRvIHVuZGVyc2NvcmVzLlxuICAvLyBCeSBtYWtpbmcgYSBzZXBhcmF0ZSBmdW5jdGlvbiwgaW5zdGVhZCBvZiBhZGRpbmdcbiAgLy8gdG8gdGhlIHByb3RvdHlwZSwgd2Ugd2lsbCBub3QgYnJlYWsgYmFkIGZvciBsb29wc1xuICAvLyBpbiBvbGRlciBicm93c2Vyc1xuICB2YXIgaW5kZXhPZlByb3RvID0gQXJyYXkucHJvdG90eXBlLmluZGV4T2Y7XG4gIHZhciB0dEluZGV4T2YgPSBmdW5jdGlvbihhcnJheSwgaXRlbSkge1xuICAgIHZhciBpID0gMCwgbCA9IGFycmF5Lmxlbmd0aDtcbiAgICBcbiAgICBpZiAoaW5kZXhPZlByb3RvICYmIGFycmF5LmluZGV4T2YgPT09IGluZGV4T2ZQcm90bykge1xuICAgICAgcmV0dXJuIGFycmF5LmluZGV4T2YoaXRlbSk7XG4gICAgfVxuICAgIFxuICAgIGZvciAoOyBpIDwgbDsgaSsrKSB7XG4gICAgICBpZiAoYXJyYXlbaV0gPT09IGl0ZW0pIHtcbiAgICAgICAgcmV0dXJuIGk7XG4gICAgICB9XG4gICAgfVxuICAgIHJldHVybiAtMTtcbiAgfTtcbiAgXG4gIC8qXG4gICAgSW5pdGlhbGl6ZSB3aXRoIFRhYmxldG9wLmluaXQoIHsga2V5OiAnMEFqQVBhQVU5TWVMRmRIVXhUbEppVlZSWU5HUkpRblJtU25Rd1RscG9VWGMnIH0gKVxuICAgICAgT1IhXG4gICAgSW5pdGlhbGl6ZSB3aXRoIFRhYmxldG9wLmluaXQoIHsga2V5OiAnaHR0cHM6Ly9kb2NzLmdvb2dsZS5jb20vc3ByZWFkc2hlZXQvcHViP2hsPWVuX1VTJmhsPWVuX1VTJmtleT0wQWpBUGFBVTlNZUxGZEhVeFRsSmlWVlJZTkdSSlFuUm1TblF3VGxwb1VYYyZvdXRwdXQ9aHRtbCZ3aWRnZXQ9dHJ1ZScgfSApXG4gICAgICBPUiFcbiAgICBJbml0aWFsaXplIHdpdGggVGFibGV0b3AuaW5pdCgnMEFqQVBhQVU5TWVMRmRIVXhUbEppVlZSWU5HUkpRblJtU25Rd1RscG9VWGMnKVxuICAqL1xuXG4gIHZhciBUYWJsZXRvcCA9IGZ1bmN0aW9uKG9wdGlvbnMpIHtcbiAgICAvLyBNYWtlIHN1cmUgVGFibGV0b3AgaXMgYmVpbmcgdXNlZCBhcyBhIGNvbnN0cnVjdG9yIG5vIG1hdHRlciB3aGF0LlxuICAgIGlmKCF0aGlzIHx8ICEodGhpcyBpbnN0YW5jZW9mIFRhYmxldG9wKSkge1xuICAgICAgcmV0dXJuIG5ldyBUYWJsZXRvcChvcHRpb25zKTtcbiAgICB9XG4gICAgXG4gICAgaWYodHlwZW9mKG9wdGlvbnMpID09PSAnc3RyaW5nJykge1xuICAgICAgb3B0aW9ucyA9IHsga2V5IDogb3B0aW9ucyB9O1xuICAgIH1cblxuICAgIHRoaXMuY2FsbGJhY2sgPSBvcHRpb25zLmNhbGxiYWNrO1xuICAgIHRoaXMud2FudGVkID0gb3B0aW9ucy53YW50ZWQgfHwgW107XG4gICAgdGhpcy5rZXkgPSBvcHRpb25zLmtleTtcbiAgICB0aGlzLnNpbXBsZVNoZWV0ID0gISFvcHRpb25zLnNpbXBsZVNoZWV0O1xuICAgIHRoaXMucGFyc2VOdW1iZXJzID0gISFvcHRpb25zLnBhcnNlTnVtYmVycztcbiAgICB0aGlzLndhaXQgPSAhIW9wdGlvbnMud2FpdDtcbiAgICB0aGlzLnJldmVyc2UgPSAhIW9wdGlvbnMucmV2ZXJzZTtcbiAgICB0aGlzLnBvc3RQcm9jZXNzID0gb3B0aW9ucy5wb3N0UHJvY2VzcztcbiAgICB0aGlzLmRlYnVnID0gISFvcHRpb25zLmRlYnVnO1xuICAgIHRoaXMucXVlcnkgPSBvcHRpb25zLnF1ZXJ5IHx8ICcnO1xuICAgIHRoaXMub3JkZXJieSA9IG9wdGlvbnMub3JkZXJieTtcbiAgICB0aGlzLmVuZHBvaW50ID0gb3B0aW9ucy5lbmRwb2ludCB8fCAnaHR0cHM6Ly9zcHJlYWRzaGVldHMuZ29vZ2xlLmNvbSc7XG4gICAgdGhpcy5zaW5nbGV0b24gPSAhIW9wdGlvbnMuc2luZ2xldG9uO1xuICAgIHRoaXMuc2ltcGxlVXJsID0gISEob3B0aW9ucy5zaW1wbGVVcmwgfHwgb3B0aW9ucy5zaW1wbGVfdXJsKTsgLy9qc2hpbnQgaWdub3JlOmxpbmVcbiAgICB0aGlzLmNhbGxiYWNrQ29udGV4dCA9IG9wdGlvbnMuY2FsbGJhY2tDb250ZXh0O1xuICAgIC8vIERlZmF1bHQgdG8gb24sIHVubGVzcyB0aGVyZSdzIGEgcHJveHksIGluIHdoaWNoIGNhc2UgaXQncyBkZWZhdWx0IG9mZlxuICAgIHRoaXMucHJldHR5Q29sdW1uTmFtZXMgPSB0eXBlb2Yob3B0aW9ucy5wcmV0dHlDb2x1bW5OYW1lcykgPT09ICd1bmRlZmluZWQnID8gIW9wdGlvbnMucHJveHkgOiBvcHRpb25zLnByZXR0eUNvbHVtbk5hbWVzO1xuICAgIFxuICAgIGlmKHR5cGVvZihvcHRpb25zLnByb3h5KSAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgIC8vIFJlbW92ZSB0cmFpbGluZyBzbGFzaCwgaXQgd2lsbCBicmVhayB0aGUgYXBwXG4gICAgICB0aGlzLmVuZHBvaW50ID0gb3B0aW9ucy5wcm94eS5yZXBsYWNlKC9cXC8kLywnJyk7XG4gICAgICB0aGlzLnNpbXBsZVVybCA9IHRydWU7XG4gICAgICB0aGlzLnNpbmdsZXRvbiA9IHRydWU7XG4gICAgICAvLyBMZXQncyBvbmx5IHVzZSBDT1JTIChzdHJhaWdodCBKU09OIHJlcXVlc3QpIHdoZW5cbiAgICAgIC8vIGZldGNoaW5nIHN0cmFpZ2h0IGZyb20gR29vZ2xlXG4gICAgICBzdXBwb3J0c0NPUlMgPSBmYWxzZTtcbiAgICB9XG4gICAgXG4gICAgdGhpcy5wYXJhbWV0ZXJpemUgPSBvcHRpb25zLnBhcmFtZXRlcml6ZSB8fCBmYWxzZTtcbiAgICBcbiAgICBpZiAodGhpcy5zaW5nbGV0b24pIHtcbiAgICAgIGlmICh0eXBlb2YoVGFibGV0b3Auc2luZ2xldG9uKSAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgdGhpcy5sb2coJ1dBUk5JTkchIFRhYmxldG9wIHNpbmdsZXRvbiBhbHJlYWR5IGRlZmluZWQnKTtcbiAgICAgIH1cbiAgICAgIFRhYmxldG9wLnNpbmdsZXRvbiA9IHRoaXM7XG4gICAgfVxuICAgIFxuICAgIC8qIEJlIGZyaWVuZGx5IGFib3V0IHdoYXQgeW91IGFjY2VwdCAqL1xuICAgIGlmICgva2V5PS8udGVzdCh0aGlzLmtleSkpIHtcbiAgICAgIHRoaXMubG9nKCdZb3UgcGFzc2VkIGFuIG9sZCBHb29nbGUgRG9jcyB1cmwgYXMgdGhlIGtleSEgQXR0ZW1wdGluZyB0byBwYXJzZS4nKTtcbiAgICAgIHRoaXMua2V5ID0gdGhpcy5rZXkubWF0Y2goJ2tleT0oLio/KSgmfCN8JCknKVsxXTtcbiAgICB9XG5cbiAgICBpZiAoL3B1Ymh0bWwvLnRlc3QodGhpcy5rZXkpKSB7XG4gICAgICB0aGlzLmxvZygnWW91IHBhc3NlZCBhIG5ldyBHb29nbGUgU3ByZWFkc2hlZXRzIHVybCBhcyB0aGUga2V5ISBBdHRlbXB0aW5nIHRvIHBhcnNlLicpO1xuICAgICAgdGhpcy5rZXkgPSB0aGlzLmtleS5tYXRjaCgnZFxcXFwvKC4qPylcXFxcL3B1Ymh0bWwnKVsxXTtcbiAgICB9XG4gICAgXG4gICAgaWYoL3NwcmVhZHNoZWV0c1xcL2QvLnRlc3QodGhpcy5rZXkpKSB7XG4gICAgICB0aGlzLmxvZygnWW91IHBhc3NlZCB0aGUgbW9zdCByZWNlbnQgdmVyc2lvbiBvZiBHb29nbGUgU3ByZWFkc2hlZXRzIHVybCBhcyB0aGUga2V5ISBBdHRlbXB0aW5nIHRvIHBhcnNlLicpO1xuICAgICAgdGhpcy5rZXkgPSB0aGlzLmtleS5tYXRjaCgnZFxcXFwvKC4qPylcXC8nKVsxXTtcbiAgICB9XG5cbiAgICBpZiAoIXRoaXMua2V5KSB7XG4gICAgICB0aGlzLmxvZygnWW91IG5lZWQgdG8gcGFzcyBUYWJsZXRvcCBhIGtleSEnKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICB0aGlzLmxvZygnSW5pdGlhbGl6aW5nIHdpdGgga2V5ICcgKyB0aGlzLmtleSk7XG5cbiAgICB0aGlzLm1vZGVscyA9IHt9O1xuICAgIHRoaXMubW9kZWxOYW1lcyA9IFtdO1xuICAgIHRoaXMubW9kZWxfbmFtZXMgPSB0aGlzLm1vZGVsTmFtZXM7IC8vanNoaW50IGlnbm9yZTpsaW5lXG5cbiAgICB0aGlzLmJhc2VKc29uUGF0aCA9ICcvZmVlZHMvd29ya3NoZWV0cy8nICsgdGhpcy5rZXkgKyAnL3B1YmxpYy9iYXNpYz9hbHQ9JztcblxuICAgIGlmIChpbk5vZGVKUyB8fCBzdXBwb3J0c0NPUlMpIHtcbiAgICAgIHRoaXMuYmFzZUpzb25QYXRoICs9ICdqc29uJztcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5iYXNlSnNvblBhdGggKz0gJ2pzb24taW4tc2NyaXB0JztcbiAgICB9XG4gICAgXG4gICAgaWYoIXRoaXMud2FpdCkge1xuICAgICAgdGhpcy5mZXRjaCgpO1xuICAgIH1cbiAgfTtcblxuICAvLyBBIGdsb2JhbCBzdG9yYWdlIGZvciBjYWxsYmFja3MuXG4gIFRhYmxldG9wLmNhbGxiYWNrcyA9IHt9O1xuXG4gIC8vIEJhY2t3YXJkcyBjb21wYXRpYmlsaXR5LlxuICBUYWJsZXRvcC5pbml0ID0gZnVuY3Rpb24ob3B0aW9ucykge1xuICAgIHJldHVybiBuZXcgVGFibGV0b3Aob3B0aW9ucyk7XG4gIH07XG5cbiAgVGFibGV0b3Auc2hlZXRzID0gZnVuY3Rpb24oKSB7XG4gICAgdGhpcy5sb2coJ1RpbWVzIGhhdmUgY2hhbmdlZCEgWW91XFwnbGwgd2FudCB0byB1c2UgdmFyIHRhYmxldG9wID0gVGFibGV0b3AuaW5pdCguLi4pOyB0YWJsZXRvcC5zaGVldHMoLi4uKTsgaW5zdGVhZCBvZiBUYWJsZXRvcC5zaGVldHMoLi4uKScpO1xuICB9O1xuXG4gIFRhYmxldG9wLnByb3RvdHlwZSA9IHtcblxuICAgIGZldGNoOiBmdW5jdGlvbihjYWxsYmFjaykge1xuICAgICAgaWYgKHR5cGVvZihjYWxsYmFjaykgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgIHRoaXMuY2FsbGJhY2sgPSBjYWxsYmFjaztcbiAgICAgIH1cbiAgICAgIHRoaXMucmVxdWVzdERhdGEodGhpcy5iYXNlSnNvblBhdGgsIHRoaXMubG9hZFNoZWV0cyk7XG4gICAgfSxcbiAgICBcbiAgICAvKlxuICAgICAgVGhpcyB3aWxsIGNhbGwgdGhlIGVudmlyb25tZW50IGFwcHJvcHJpYXRlIHJlcXVlc3QgbWV0aG9kLlxuICAgICAgXG4gICAgICBJbiBicm93c2VyIGl0IHdpbGwgdXNlIEpTT04tUCwgaW4gbm9kZSBpdCB3aWxsIHVzZSByZXF1ZXN0KClcbiAgICAqL1xuICAgIHJlcXVlc3REYXRhOiBmdW5jdGlvbihwYXRoLCBjYWxsYmFjaykge1xuICAgICAgdGhpcy5sb2coJ1JlcXVlc3RpbmcnLCBwYXRoKTtcblxuICAgICAgaWYgKGluTm9kZUpTKSB7XG4gICAgICAgIHRoaXMuc2VydmVyU2lkZUZldGNoKHBhdGgsIGNhbGxiYWNrKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIC8vQ09SUyBvbmx5IHdvcmtzIGluIElFOC85IGFjcm9zcyB0aGUgc2FtZSBwcm90b2NvbFxuICAgICAgICAvL1lvdSBtdXN0IGhhdmUgeW91ciBzZXJ2ZXIgb24gSFRUUFMgdG8gdGFsayB0byBHb29nbGUsIG9yIGl0J2xsIGZhbGwgYmFjayBvbiBpbmplY3Rpb25cbiAgICAgICAgdmFyIHByb3RvY29sID0gdGhpcy5lbmRwb2ludC5zcGxpdCgnLy8nKS5zaGlmdCgpIHx8ICdodHRwJztcbiAgICAgICAgaWYgKHN1cHBvcnRzQ09SUyAmJiAoIWluTGVnYWN5SUUgfHwgcHJvdG9jb2wgPT09IGxvY2F0aW9uLnByb3RvY29sKSkge1xuICAgICAgICAgIHRoaXMueGhyRmV0Y2gocGF0aCwgY2FsbGJhY2spO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHRoaXMuaW5qZWN0U2NyaXB0KHBhdGgsIGNhbGxiYWNrKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0sXG5cbiAgICAvKlxuICAgICAgVXNlIENyb3NzLU9yaWdpbiBYTUxIdHRwUmVxdWVzdCB0byBnZXQgdGhlIGRhdGEgaW4gYnJvd3NlcnMgdGhhdCBzdXBwb3J0IGl0LlxuICAgICovXG4gICAgeGhyRmV0Y2g6IGZ1bmN0aW9uKHBhdGgsIGNhbGxiYWNrKSB7XG4gICAgICAvL3N1cHBvcnQgSUU4J3Mgc2VwYXJhdGUgY3Jvc3MtZG9tYWluIG9iamVjdFxuICAgICAgdmFyIHhociA9IGluTGVnYWN5SUUgPyBuZXcgWERvbWFpblJlcXVlc3QoKSA6IG5ldyBYTUxIdHRwUmVxdWVzdCgpO1xuICAgICAgeGhyLm9wZW4oJ0dFVCcsIHRoaXMuZW5kcG9pbnQgKyBwYXRoKTtcbiAgICAgIHZhciBzZWxmID0gdGhpcztcbiAgICAgIHhoci5vbmxvYWQgPSBmdW5jdGlvbigpIHtcbiAgICAgICAgdmFyIGpzb247XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAganNvbiA9IEpTT04ucGFyc2UoeGhyLnJlc3BvbnNlVGV4dCk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBjb25zb2xlLmVycm9yKGUpO1xuICAgICAgICB9XG4gICAgICAgIGNhbGxiYWNrLmNhbGwoc2VsZiwganNvbik7XG4gICAgICB9O1xuICAgICAgeGhyLnNlbmQoKTtcbiAgICB9LFxuICAgIFxuICAgIC8qXG4gICAgICBJbnNlcnQgdGhlIFVSTCBpbnRvIHRoZSBwYWdlIGFzIGEgc2NyaXB0IHRhZy4gT25jZSBpdCdzIGxvYWRlZCB0aGUgc3ByZWFkc2hlZXQgZGF0YVxuICAgICAgaXQgdHJpZ2dlcnMgdGhlIGNhbGxiYWNrLiBUaGlzIGhlbHBzIHlvdSBhdm9pZCBjcm9zcy1kb21haW4gZXJyb3JzXG4gICAgICBodHRwOi8vY29kZS5nb29nbGUuY29tL2FwaXMvZ2RhdGEvc2FtcGxlcy9zcHJlYWRzaGVldF9zYW1wbGUuaHRtbFxuXG4gICAgICBMZXQncyBiZSBwbGFpbi1KYW5lIGFuZCBub3QgdXNlIGpRdWVyeSBvciBhbnl0aGluZy5cbiAgICAqL1xuICAgIGluamVjdFNjcmlwdDogZnVuY3Rpb24ocGF0aCwgY2FsbGJhY2spIHtcbiAgICAgIHZhciBzY3JpcHQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTtcbiAgICAgIHZhciBjYWxsYmFja05hbWU7XG4gICAgICBcbiAgICAgIGlmICh0aGlzLnNpbmdsZXRvbikge1xuICAgICAgICBpZiAoY2FsbGJhY2sgPT09IHRoaXMubG9hZFNoZWV0cykge1xuICAgICAgICAgIGNhbGxiYWNrTmFtZSA9ICdUYWJsZXRvcC5zaW5nbGV0b24ubG9hZFNoZWV0cyc7XG4gICAgICAgIH0gZWxzZSBpZiAoY2FsbGJhY2sgPT09IHRoaXMubG9hZFNoZWV0KSB7XG4gICAgICAgICAgY2FsbGJhY2tOYW1lID0gJ1RhYmxldG9wLnNpbmdsZXRvbi5sb2FkU2hlZXQnO1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB2YXIgc2VsZiA9IHRoaXM7XG4gICAgICAgIGNhbGxiYWNrTmFtZSA9ICd0dCcgKyAoK25ldyBEYXRlKCkpICsgKE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSoxMDAwMDApKTtcbiAgICAgICAgLy8gQ3JlYXRlIGEgdGVtcCBjYWxsYmFjayB3aGljaCB3aWxsIGdldCByZW1vdmVkIG9uY2UgaXQgaGFzIGV4ZWN1dGVkLFxuICAgICAgICAvLyB0aGlzIGFsbG93cyBtdWx0aXBsZSBpbnN0YW5jZXMgb2YgVGFibGV0b3AgdG8gY29leGlzdC5cbiAgICAgICAgVGFibGV0b3AuY2FsbGJhY2tzWyBjYWxsYmFja05hbWUgXSA9IGZ1bmN0aW9uICgpIHtcbiAgICAgICAgICB2YXIgYXJncyA9IEFycmF5LnByb3RvdHlwZS5zbGljZS5jYWxsKCBhcmd1bWVudHMsIDAgKTtcbiAgICAgICAgICBjYWxsYmFjay5hcHBseShzZWxmLCBhcmdzKTtcbiAgICAgICAgICBzY3JpcHQucGFyZW50Tm9kZS5yZW1vdmVDaGlsZChzY3JpcHQpO1xuICAgICAgICAgIGRlbGV0ZSBUYWJsZXRvcC5jYWxsYmFja3NbY2FsbGJhY2tOYW1lXTtcbiAgICAgICAgfTtcbiAgICAgICAgY2FsbGJhY2tOYW1lID0gJ1RhYmxldG9wLmNhbGxiYWNrcy4nICsgY2FsbGJhY2tOYW1lO1xuICAgICAgfVxuICAgICAgXG4gICAgICB2YXIgdXJsID0gcGF0aCArICcmY2FsbGJhY2s9JyArIGNhbGxiYWNrTmFtZTtcbiAgICAgIFxuICAgICAgaWYgKHRoaXMuc2ltcGxlVXJsKSB7XG4gICAgICAgIC8vIFdlJ3ZlIGdvbmUgZG93biBhIHJhYmJpdCBob2xlIG9mIHBhc3NpbmcgaW5qZWN0U2NyaXB0IHRoZSBwYXRoLCBzbyBsZXQnc1xuICAgICAgICAvLyBqdXN0IHB1bGwgdGhlIHNoZWV0X2lkIG91dCBvZiB0aGUgcGF0aCBsaWtlIHRoZSBsZWFzdCBlZmZpY2llbnQgd29ya2VyIGJlZXNcbiAgICAgICAgaWYocGF0aC5pbmRleE9mKCcvbGlzdC8nKSAhPT0gLTEpIHtcbiAgICAgICAgICBzY3JpcHQuc3JjID0gdGhpcy5lbmRwb2ludCArICcvJyArIHRoaXMua2V5ICsgJy0nICsgcGF0aC5zcGxpdCgnLycpWzRdO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHNjcmlwdC5zcmMgPSB0aGlzLmVuZHBvaW50ICsgJy8nICsgdGhpcy5rZXk7XG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHNjcmlwdC5zcmMgPSB0aGlzLmVuZHBvaW50ICsgdXJsO1xuICAgICAgfVxuICAgICAgXG4gICAgICBpZiAodGhpcy5wYXJhbWV0ZXJpemUpIHtcbiAgICAgICAgc2NyaXB0LnNyYyA9IHRoaXMucGFyYW1ldGVyaXplICsgZW5jb2RlVVJJQ29tcG9uZW50KHNjcmlwdC5zcmMpO1xuICAgICAgfVxuXG4gICAgICB0aGlzLmxvZygnSW5qZWN0aW5nJywgc2NyaXB0LnNyYyk7XG5cbiAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRzQnlUYWdOYW1lKCdzY3JpcHQnKVswXS5wYXJlbnROb2RlLmFwcGVuZENoaWxkKHNjcmlwdCk7XG4gICAgfSxcbiAgICBcbiAgICAvKiBcbiAgICAgIFRoaXMgd2lsbCBvbmx5IHJ1biBpZiB0YWJsZXRvcCBpcyBiZWluZyBydW4gaW4gbm9kZS5qc1xuICAgICovXG4gICAgc2VydmVyU2lkZUZldGNoOiBmdW5jdGlvbihwYXRoLCBjYWxsYmFjaykge1xuICAgICAgdmFyIHNlbGYgPSB0aGlzO1xuXG4gICAgICB0aGlzLmxvZygnRmV0Y2hpbmcnLCB0aGlzLmVuZHBvaW50ICsgcGF0aCk7XG4gICAgICByZXF1ZXN0KHt1cmw6IHRoaXMuZW5kcG9pbnQgKyBwYXRoLCBqc29uOiB0cnVlfSwgZnVuY3Rpb24oZXJyLCByZXNwLCBib2R5KSB7XG4gICAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgICByZXR1cm4gY29uc29sZS5lcnJvcihlcnIpO1xuICAgICAgICB9XG4gICAgICAgIGNhbGxiYWNrLmNhbGwoc2VsZiwgYm9keSk7XG4gICAgICB9KTtcbiAgICB9LFxuXG4gICAgLyogXG4gICAgICBJcyB0aGlzIGEgc2hlZXQgeW91IHdhbnQgdG8gcHVsbD9cbiAgICAgIElmIHsgd2FudGVkOiBbXCJTaGVldDFcIl0gfSBoYXMgYmVlbiBzcGVjaWZpZWQsIG9ubHkgU2hlZXQxIGlzIGltcG9ydGVkXG4gICAgICBQdWxscyBhbGwgc2hlZXRzIGlmIG5vbmUgYXJlIHNwZWNpZmllZFxuICAgICovXG4gICAgaXNXYW50ZWQ6IGZ1bmN0aW9uKHNoZWV0TmFtZSkge1xuICAgICAgaWYgKHRoaXMud2FudGVkLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiAodHRJbmRleE9mKHRoaXMud2FudGVkLCBzaGVldE5hbWUpICE9PSAtMSk7XG4gICAgICB9XG4gICAgfSxcbiAgICBcbiAgICAvKlxuICAgICAgV2hhdCBnZXRzIHNlbmQgdG8gdGhlIGNhbGxiYWNrXG4gICAgICBpZiBzaW1wbGVTaGVldCA9PT0gdHJ1ZSwgdGhlbiBkb24ndCByZXR1cm4gYW4gYXJyYXkgb2YgVGFibGV0b3AudGhpcy5tb2RlbHMsXG4gICAgICBvbmx5IHJldHVybiB0aGUgZmlyc3Qgb25lJ3MgZWxlbWVudHNcbiAgICAqL1xuICAgIGRhdGE6IGZ1bmN0aW9uKCkge1xuICAgICAgLy8gSWYgdGhlIGluc3RhbmNlIGlzIGJlaW5nIHF1ZXJpZWQgYmVmb3JlIHRoZSBkYXRhJ3MgYmVlbiBmZXRjaGVkXG4gICAgICAvLyB0aGVuIHJldHVybiB1bmRlZmluZWQuXG4gICAgICBpZiAodGhpcy5tb2RlbE5hbWVzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgICAgaWYgKHRoaXMuc2ltcGxlU2hlZXQpIHtcbiAgICAgICAgaWYgKHRoaXMubW9kZWxOYW1lcy5sZW5ndGggPiAxICYmIHRoaXMuZGVidWcpIHtcbiAgICAgICAgICB0aGlzLmxvZygnV0FSTklORyBZb3UgaGF2ZSBtb3JlIHRoYW4gb25lIHNoZWV0IGJ1dCBhcmUgdXNpbmcgc2ltcGxlIHNoZWV0IG1vZGUhIERvblxcJ3QgYmxhbWUgbWUgd2hlbiBzb21ldGhpbmcgZ29lcyB3cm9uZy4nKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGhpcy5tb2RlbHNbdGhpcy5tb2RlbE5hbWVzWzBdXS5hbGwoKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiB0aGlzLm1vZGVscztcbiAgICAgIH1cbiAgICB9LFxuXG4gICAgLypcbiAgICAgIEFkZCBhbm90aGVyIHNoZWV0IHRvIHRoZSB3YW50ZWQgbGlzdFxuICAgICovXG4gICAgYWRkV2FudGVkOiBmdW5jdGlvbihzaGVldCkge1xuICAgICAgaWYodHRJbmRleE9mKHRoaXMud2FudGVkLCBzaGVldCkgPT09IC0xKSB7XG4gICAgICAgIHRoaXMud2FudGVkLnB1c2goc2hlZXQpO1xuICAgICAgfVxuICAgIH0sXG4gICAgXG4gICAgLypcbiAgICAgIExvYWQgYWxsIHdvcmtzaGVldHMgb2YgdGhlIHNwcmVhZHNoZWV0LCB0dXJuaW5nIGVhY2ggaW50byBhIFRhYmxldG9wIE1vZGVsLlxuICAgICAgTmVlZCB0byB1c2UgaW5qZWN0U2NyaXB0IGJlY2F1c2UgdGhlIHdvcmtzaGVldCB2aWV3IHRoYXQgeW91J3JlIHdvcmtpbmcgZnJvbVxuICAgICAgZG9lc24ndCBhY3R1YWxseSBpbmNsdWRlIHRoZSBkYXRhLiBUaGUgbGlzdC1iYXNlZCBmZWVkICgvZmVlZHMvbGlzdC9rZXkuLikgZG9lcywgdGhvdWdoLlxuICAgICAgQ2FsbHMgYmFjayB0byBsb2FkU2hlZXQgaW4gb3JkZXIgdG8gZ2V0IHRoZSByZWFsIHdvcmsgZG9uZS5cblxuICAgICAgVXNlZCBhcyBhIGNhbGxiYWNrIGZvciB0aGUgd29ya3NoZWV0LWJhc2VkIEpTT05cbiAgICAqL1xuICAgIGxvYWRTaGVldHM6IGZ1bmN0aW9uKGRhdGEpIHtcbiAgICAgIHZhciBpLCBpbGVuO1xuICAgICAgdmFyIHRvTG9hZCA9IFtdO1xuICAgICAgdGhpcy5nb29nbGVTaGVldE5hbWUgPSBkYXRhLmZlZWQudGl0bGUuJHQ7XG4gICAgICB0aGlzLmZvdW5kU2hlZXROYW1lcyA9IFtdO1xuXG4gICAgICBmb3IgKGkgPSAwLCBpbGVuID0gZGF0YS5mZWVkLmVudHJ5Lmxlbmd0aDsgaSA8IGlsZW4gOyBpKyspIHtcbiAgICAgICAgdGhpcy5mb3VuZFNoZWV0TmFtZXMucHVzaChkYXRhLmZlZWQuZW50cnlbaV0udGl0bGUuJHQpO1xuICAgICAgICAvLyBPbmx5IHB1bGwgaW4gZGVzaXJlZCBzaGVldHMgdG8gcmVkdWNlIGxvYWRpbmdcbiAgICAgICAgaWYgKHRoaXMuaXNXYW50ZWQoZGF0YS5mZWVkLmVudHJ5W2ldLmNvbnRlbnQuJHQpKSB7XG4gICAgICAgICAgdmFyIGxpbmtJZHggPSBkYXRhLmZlZWQuZW50cnlbaV0ubGluay5sZW5ndGgtMTtcbiAgICAgICAgICB2YXIgc2hlZXRJZCA9IGRhdGEuZmVlZC5lbnRyeVtpXS5saW5rW2xpbmtJZHhdLmhyZWYuc3BsaXQoJy8nKS5wb3AoKTtcbiAgICAgICAgICB2YXIganNvblBhdGggPSAnL2ZlZWRzL2xpc3QvJyArIHRoaXMua2V5ICsgJy8nICsgc2hlZXRJZCArICcvcHVibGljL3ZhbHVlcz9hbHQ9JztcbiAgICAgICAgICBpZiAoaW5Ob2RlSlMgfHwgc3VwcG9ydHNDT1JTKSB7XG4gICAgICAgICAgICBqc29uUGF0aCArPSAnanNvbic7XG4gICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGpzb25QYXRoICs9ICdqc29uLWluLXNjcmlwdCc7XG4gICAgICAgICAgfVxuICAgICAgICAgIGlmICh0aGlzLnF1ZXJ5KSB7XG4gICAgICAgICAgICAvLyBRdWVyeSBMYW5ndWFnZSBSZWZlcmVuY2UgKDAuNylcbiAgICAgICAgICAgIGpzb25QYXRoICs9ICcmdHE9JyArIHRoaXMucXVlcnk7XG4gICAgICAgICAgfVxuICAgICAgICAgIGlmICh0aGlzLm9yZGVyYnkpIHtcbiAgICAgICAgICAgIGpzb25QYXRoICs9ICcmb3JkZXJieT1jb2x1bW46JyArIHRoaXMub3JkZXJieS50b0xvd2VyQ2FzZSgpO1xuICAgICAgICAgIH1cbiAgICAgICAgICBpZiAodGhpcy5yZXZlcnNlKSB7XG4gICAgICAgICAgICBqc29uUGF0aCArPSAnJnJldmVyc2U9dHJ1ZSc7XG4gICAgICAgICAgfVxuICAgICAgICAgIHRvTG9hZC5wdXNoKGpzb25QYXRoKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICB0aGlzLnNoZWV0c1RvTG9hZCA9IHRvTG9hZC5sZW5ndGg7XG4gICAgICBmb3IoaSA9IDAsIGlsZW4gPSB0b0xvYWQubGVuZ3RoOyBpIDwgaWxlbjsgaSsrKSB7XG4gICAgICAgIHRoaXMucmVxdWVzdERhdGEodG9Mb2FkW2ldLCB0aGlzLmxvYWRTaGVldCk7XG4gICAgICB9XG4gICAgfSxcblxuICAgIC8qXG4gICAgICBBY2Nlc3MgbGF5ZXIgZm9yIHRoZSB0aGlzLm1vZGVsc1xuICAgICAgLnNoZWV0cygpIGdldHMgeW91IGFsbCBvZiB0aGUgc2hlZXRzXG4gICAgICAuc2hlZXRzKCdTaGVldDEnKSBnZXRzIHlvdSB0aGUgc2hlZXQgbmFtZWQgU2hlZXQxXG4gICAgKi9cbiAgICBzaGVldHM6IGZ1bmN0aW9uKHNoZWV0TmFtZSkge1xuICAgICAgaWYgKHR5cGVvZiBzaGVldE5hbWUgPT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgIHJldHVybiB0aGlzLm1vZGVscztcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGlmICh0eXBlb2YodGhpcy5tb2RlbHNbc2hlZXROYW1lXSkgPT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgICAgLy8gYWxlcnQoIFwiQ2FuJ3QgZmluZCBcIiArIHNoZWV0TmFtZSApO1xuICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICByZXR1cm4gdGhpcy5tb2RlbHNbc2hlZXROYW1lXTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0sXG5cbiAgICBzaGVldFJlYWR5OiBmdW5jdGlvbihtb2RlbCkge1xuICAgICAgdGhpcy5tb2RlbHNbbW9kZWwubmFtZV0gPSBtb2RlbDtcbiAgICAgIGlmICh0dEluZGV4T2YodGhpcy5tb2RlbE5hbWVzLCBtb2RlbC5uYW1lKSA9PT0gLTEpIHtcbiAgICAgICAgdGhpcy5tb2RlbE5hbWVzLnB1c2gobW9kZWwubmFtZSk7XG4gICAgICB9XG4gICAgICBcbiAgICAgIHRoaXMuc2hlZXRzVG9Mb2FkLS07XG4gICAgICBpZiAodGhpcy5zaGVldHNUb0xvYWQgPT09IDApIHtcbiAgICAgICAgdGhpcy5kb0NhbGxiYWNrKCk7XG4gICAgICB9XG4gICAgfSxcbiAgICBcbiAgICAvKlxuICAgICAgUGFyc2UgYSBzaW5nbGUgbGlzdC1iYXNlZCB3b3Jrc2hlZXQsIHR1cm5pbmcgaXQgaW50byBhIFRhYmxldG9wIE1vZGVsXG5cbiAgICAgIFVzZWQgYXMgYSBjYWxsYmFjayBmb3IgdGhlIGxpc3QtYmFzZWQgSlNPTlxuICAgICovXG4gICAgbG9hZFNoZWV0OiBmdW5jdGlvbihkYXRhKSB7XG4gICAgICB2YXIgdGhhdCA9IHRoaXM7XG4gICAgICBuZXcgVGFibGV0b3AuTW9kZWwoeyBcbiAgICAgICAgZGF0YTogZGF0YSwgXG4gICAgICAgIHBhcnNlTnVtYmVyczogdGhpcy5wYXJzZU51bWJlcnMsXG4gICAgICAgIHBvc3RQcm9jZXNzOiB0aGlzLnBvc3RQcm9jZXNzLFxuICAgICAgICB0YWJsZXRvcDogdGhpcyxcbiAgICAgICAgcHJldHR5Q29sdW1uTmFtZXM6IHRoaXMucHJldHR5Q29sdW1uTmFtZXMsXG4gICAgICAgIG9uUmVhZHk6IGZ1bmN0aW9uKCkge1xuICAgICAgICAgIHRoYXQuc2hlZXRSZWFkeSh0aGlzKTtcbiAgICAgICAgfSBcbiAgICAgIH0pO1xuICAgIH0sXG5cbiAgICAvKlxuICAgICAgRXhlY3V0ZSB0aGUgY2FsbGJhY2sgdXBvbiBsb2FkaW5nISBSZWx5IG9uIHRoaXMuZGF0YSgpIGJlY2F1c2UgeW91IG1pZ2h0XG4gICAgICAgIG9ubHkgcmVxdWVzdCBjZXJ0YWluIHBpZWNlcyBvZiBkYXRhIChpLmUuIHNpbXBsZVNoZWV0IG1vZGUpXG4gICAgICBUZXN0cyB0aGlzLnNoZWV0c1RvTG9hZCBqdXN0IGluIGNhc2UgYSByYWNlIGNvbmRpdGlvbiBoYXBwZW5zIHRvIHNob3cgdXBcbiAgICAqL1xuICAgIGRvQ2FsbGJhY2s6IGZ1bmN0aW9uKCkge1xuICAgICAgaWYodGhpcy5zaGVldHNUb0xvYWQgPT09IDApIHtcbiAgICAgICAgdGhpcy5jYWxsYmFjay5hcHBseSh0aGlzLmNhbGxiYWNrQ29udGV4dCB8fCB0aGlzLCBbdGhpcy5kYXRhKCksIHRoaXNdKTtcbiAgICAgIH1cbiAgICB9LFxuXG4gICAgbG9nOiBmdW5jdGlvbigpIHtcbiAgICAgIGlmKHRoaXMuZGVidWcpIHtcbiAgICAgICAgaWYodHlwZW9mIGNvbnNvbGUgIT09ICd1bmRlZmluZWQnICYmIHR5cGVvZiBjb25zb2xlLmxvZyAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgICBGdW5jdGlvbi5wcm90b3R5cGUuYXBwbHkuYXBwbHkoY29uc29sZS5sb2csIFtjb25zb2xlLCBhcmd1bWVudHNdKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cblxuICB9O1xuXG4gIC8qXG4gICAgVGFibGV0b3AuTW9kZWwgc3RvcmVzIHRoZSBhdHRyaWJ1dGUgbmFtZXMgYW5kIHBhcnNlcyB0aGUgd29ya3NoZWV0IGRhdGFcbiAgICAgIHRvIHR1cm4gaXQgaW50byBzb21ldGhpbmcgd29ydGh3aGlsZVxuXG4gICAgT3B0aW9ucyBzaG91bGQgYmUgaW4gdGhlIGZvcm1hdCB7IGRhdGE6IFhYWCB9LCB3aXRoIFhYWCBiZWluZyB0aGUgbGlzdC1iYXNlZCB3b3Jrc2hlZXRcbiAgKi9cbiAgVGFibGV0b3AuTW9kZWwgPSBmdW5jdGlvbihvcHRpb25zKSB7XG4gICAgdmFyIGksIGosIGlsZW4sIGpsZW47XG4gICAgdGhpcy5jb2x1bW5OYW1lcyA9IFtdO1xuICAgIHRoaXMuY29sdW1uX25hbWVzID0gdGhpcy5jb2x1bW5OYW1lczsgLy8ganNoaW50IGlnbm9yZTpsaW5lXG4gICAgdGhpcy5uYW1lID0gb3B0aW9ucy5kYXRhLmZlZWQudGl0bGUuJHQ7XG4gICAgdGhpcy50YWJsZXRvcCA9IG9wdGlvbnMudGFibGV0b3A7XG4gICAgdGhpcy5lbGVtZW50cyA9IFtdO1xuICAgIHRoaXMub25SZWFkeSA9IG9wdGlvbnMub25SZWFkeTtcbiAgICB0aGlzLnJhdyA9IG9wdGlvbnMuZGF0YTsgLy8gQSBjb3B5IG9mIHRoZSBzaGVldCdzIHJhdyBkYXRhLCBmb3IgYWNjZXNzaW5nIG1pbnV0aWFlXG5cbiAgICBpZiAodHlwZW9mKG9wdGlvbnMuZGF0YS5mZWVkLmVudHJ5KSA9PT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgIG9wdGlvbnMudGFibGV0b3AubG9nKCdNaXNzaW5nIGRhdGEgZm9yICcgKyB0aGlzLm5hbWUgKyAnLCBtYWtlIHN1cmUgeW91IGRpZG5cXCd0IGZvcmdldCBjb2x1bW4gaGVhZGVycycpO1xuICAgICAgdGhpcy5vcmlnaW5hbENvbHVtbnMgPSBbXTtcbiAgICAgIHRoaXMuZWxlbWVudHMgPSBbXTtcbiAgICAgIHRoaXMub25SZWFkeS5jYWxsKHRoaXMpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBcbiAgICBmb3IgKHZhciBrZXkgaW4gb3B0aW9ucy5kYXRhLmZlZWQuZW50cnlbMF0pe1xuICAgICAgaWYgKC9eZ3N4Ly50ZXN0KGtleSkpIHtcbiAgICAgICAgdGhpcy5jb2x1bW5OYW1lcy5wdXNoKGtleS5yZXBsYWNlKCdnc3gkJywnJykpO1xuICAgICAgfVxuICAgIH1cblxuICAgIHRoaXMub3JpZ2luYWxDb2x1bW5zID0gdGhpcy5jb2x1bW5OYW1lcztcbiAgICB0aGlzLm9yaWdpbmFsX2NvbHVtbnMgPSB0aGlzLm9yaWdpbmFsQ29sdW1uczsgLy8ganNoaW50IGlnbm9yZTpsaW5lXG4gICAgXG4gICAgZm9yIChpID0gMCwgaWxlbiA9ICBvcHRpb25zLmRhdGEuZmVlZC5lbnRyeS5sZW5ndGggOyBpIDwgaWxlbjsgaSsrKSB7XG4gICAgICB2YXIgc291cmNlID0gb3B0aW9ucy5kYXRhLmZlZWQuZW50cnlbaV07XG4gICAgICB2YXIgZWxlbWVudCA9IHt9O1xuICAgICAgZm9yIChqID0gMCwgamxlbiA9IHRoaXMuY29sdW1uTmFtZXMubGVuZ3RoOyBqIDwgamxlbiA7IGorKykge1xuICAgICAgICB2YXIgY2VsbCA9IHNvdXJjZVsnZ3N4JCcgKyB0aGlzLmNvbHVtbk5hbWVzW2pdXTtcbiAgICAgICAgaWYgKHR5cGVvZihjZWxsKSAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgICBpZiAob3B0aW9ucy5wYXJzZU51bWJlcnMgJiYgY2VsbC4kdCAhPT0gJycgJiYgIWlzTmFOKGNlbGwuJHQpKSB7XG4gICAgICAgICAgICBlbGVtZW50W3RoaXMuY29sdW1uTmFtZXNbal1dID0gK2NlbGwuJHQ7XG4gICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGVsZW1lbnRbdGhpcy5jb2x1bW5OYW1lc1tqXV0gPSBjZWxsLiR0O1xuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBlbGVtZW50W3RoaXMuY29sdW1uTmFtZXNbal1dID0gJyc7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIChlbGVtZW50LnJvd051bWJlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGVsZW1lbnQucm93TnVtYmVyID0gaSArIDE7XG4gICAgICB9XG4gICAgICAgIFxuICAgICAgaWYgKG9wdGlvbnMucG9zdFByb2Nlc3MpIHtcbiAgICAgICAgb3B0aW9ucy5wb3N0UHJvY2VzcyhlbGVtZW50KTtcbiAgICAgIH1cbiAgICAgICAgXG4gICAgICB0aGlzLmVsZW1lbnRzLnB1c2goZWxlbWVudCk7XG4gICAgfVxuICAgIFxuICAgIGlmIChvcHRpb25zLnByZXR0eUNvbHVtbk5hbWVzKSB7XG4gICAgICB0aGlzLmZldGNoUHJldHR5Q29sdW1ucygpO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLm9uUmVhZHkuY2FsbCh0aGlzKTtcbiAgICB9XG4gIH07XG5cbiAgVGFibGV0b3AuTW9kZWwucHJvdG90eXBlID0ge1xuICAgIC8qXG4gICAgICBSZXR1cm5zIGFsbCBvZiB0aGUgZWxlbWVudHMgKHJvd3MpIG9mIHRoZSB3b3Jrc2hlZXQgYXMgb2JqZWN0c1xuICAgICovXG4gICAgYWxsOiBmdW5jdGlvbigpIHtcbiAgICAgIHJldHVybiB0aGlzLmVsZW1lbnRzO1xuICAgIH0sXG4gICAgXG4gICAgZmV0Y2hQcmV0dHlDb2x1bW5zOiBmdW5jdGlvbigpIHtcbiAgICAgIGlmICghdGhpcy5yYXcuZmVlZC5saW5rWzNdKSB7XG4gICAgICAgIHJldHVybiB0aGlzLnJlYWR5KCk7XG4gICAgICB9XG4gICAgICAgIFxuICAgICAgdmFyIGNlbGx1cmwgPSB0aGlzLnJhdy5mZWVkLmxpbmtbM10uaHJlZi5yZXBsYWNlKCcvZmVlZHMvbGlzdC8nLCAnL2ZlZWRzL2NlbGxzLycpLnJlcGxhY2UoJ2h0dHBzOi8vc3ByZWFkc2hlZXRzLmdvb2dsZS5jb20nLCAnJyk7XG4gICAgICB2YXIgdGhhdCA9IHRoaXM7XG4gICAgICB0aGlzLnRhYmxldG9wLnJlcXVlc3REYXRhKGNlbGx1cmwsIGZ1bmN0aW9uKGRhdGEpIHtcbiAgICAgICAgdGhhdC5sb2FkUHJldHR5Q29sdW1ucyhkYXRhKTtcbiAgICAgIH0pO1xuICAgIH0sXG4gICAgXG4gICAgcmVhZHk6IGZ1bmN0aW9uKCkge1xuICAgICAgdGhpcy5vblJlYWR5LmNhbGwodGhpcyk7XG4gICAgfSxcbiAgICBcbiAgICAvKlxuICAgICAqIFN0b3JlIGNvbHVtbiBuYW1lcyBhcyBhbiBvYmplY3RcbiAgICAgKiB3aXRoIGtleXMgb2YgR29vZ2xlLWZvcm1hdHRlZCBcImNvbHVtbk5hbWVcIlxuICAgICAqIGFuZCB2YWx1ZXMgb2YgaHVtYW4tcmVhZGFibGUgXCJDb2x1bW4gbmFtZVwiXG4gICAgICovXG4gICAgbG9hZFByZXR0eUNvbHVtbnM6IGZ1bmN0aW9uKGRhdGEpIHtcbiAgICAgIHZhciBwcmV0dHlDb2x1bW5zID0ge307XG5cbiAgICAgIHZhciBjb2x1bW5OYW1lcyA9IHRoaXMuY29sdW1uTmFtZXM7XG5cbiAgICAgIHZhciBpID0gMDtcbiAgICAgIHZhciBsID0gY29sdW1uTmFtZXMubGVuZ3RoO1xuXG4gICAgICBmb3IgKDsgaSA8IGw7IGkrKykge1xuICAgICAgICBpZiAodHlwZW9mIGRhdGEuZmVlZC5lbnRyeVtpXS5jb250ZW50LiR0ICE9PSAndW5kZWZpbmVkJykge1xuICAgICAgICAgIHByZXR0eUNvbHVtbnNbY29sdW1uTmFtZXNbaV1dID0gZGF0YS5mZWVkLmVudHJ5W2ldLmNvbnRlbnQuJHQ7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgcHJldHR5Q29sdW1uc1tjb2x1bW5OYW1lc1tpXV0gPSBjb2x1bW5OYW1lc1tpXTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICB0aGlzLnByZXR0eUNvbHVtbnMgPSBwcmV0dHlDb2x1bW5zO1xuICAgICAgdGhpcy5wcmV0dHlfY29sdW1ucyA9IHRoaXMucHJldHR5Q29sdW1uczsgLy8ganNoaW50IGlnbm9yZTpsaW5lXG4gICAgICB0aGlzLnByZXR0aWZ5RWxlbWVudHMoKTtcbiAgICAgIHRoaXMucmVhZHkoKTtcbiAgICB9LFxuICAgIFxuICAgIC8qXG4gICAgICogR28gdGhyb3VnaCBlYWNoIHJvdywgc3Vic3RpdHV0aXRpbmdcbiAgICAgKiBHb29nbGUtZm9ybWF0dGVkIFwiY29sdW1uTmFtZVwiXG4gICAgICogd2l0aCBodW1hbi1yZWFkYWJsZSBcIkNvbHVtbiBuYW1lXCJcbiAgICAgKi9cbiAgICBwcmV0dGlmeUVsZW1lbnRzOiBmdW5jdGlvbigpIHtcbiAgICAgIHZhciBwcmV0dHlFbGVtZW50cyA9IFtdLFxuICAgICAgICAgIG9yZGVyZWRQcmV0dHlOYW1lcyA9IFtdLFxuICAgICAgICAgIGksIGosIGlsZW4sIGpsZW47XG5cbiAgICAgIGZvciAoaiA9IDAsIGpsZW4gPSB0aGlzLmNvbHVtbk5hbWVzLmxlbmd0aDsgaiA8IGpsZW4gOyBqKyspIHtcbiAgICAgICAgb3JkZXJlZFByZXR0eU5hbWVzLnB1c2godGhpcy5wcmV0dHlDb2x1bW5zW3RoaXMuY29sdW1uTmFtZXNbal1dKTtcbiAgICAgIH1cblxuICAgICAgZm9yIChpID0gMCwgaWxlbiA9IHRoaXMuZWxlbWVudHMubGVuZ3RoOyBpIDwgaWxlbjsgaSsrKSB7XG4gICAgICAgIHZhciBuZXdFbGVtZW50ID0ge307XG4gICAgICAgIGZvciAoaiA9IDAsIGpsZW4gPSB0aGlzLmNvbHVtbk5hbWVzLmxlbmd0aDsgaiA8IGpsZW4gOyBqKyspIHtcbiAgICAgICAgICB2YXIgbmV3Q29sdW1uTmFtZSA9IHRoaXMucHJldHR5Q29sdW1uc1t0aGlzLmNvbHVtbk5hbWVzW2pdXTtcbiAgICAgICAgICBuZXdFbGVtZW50W25ld0NvbHVtbk5hbWVdID0gdGhpcy5lbGVtZW50c1tpXVt0aGlzLmNvbHVtbk5hbWVzW2pdXTtcbiAgICAgICAgfVxuICAgICAgICBwcmV0dHlFbGVtZW50cy5wdXNoKG5ld0VsZW1lbnQpO1xuICAgICAgfVxuICAgICAgdGhpcy5lbGVtZW50cyA9IHByZXR0eUVsZW1lbnRzO1xuICAgICAgdGhpcy5jb2x1bW5OYW1lcyA9IG9yZGVyZWRQcmV0dHlOYW1lcztcbiAgICB9LFxuXG4gICAgLypcbiAgICAgIFJldHVybiB0aGUgZWxlbWVudHMgYXMgYW4gYXJyYXkgb2YgYXJyYXlzLCBpbnN0ZWFkIG9mIGFuIGFycmF5IG9mIG9iamVjdHNcbiAgICAqL1xuICAgIHRvQXJyYXk6IGZ1bmN0aW9uKCkge1xuICAgICAgdmFyIGFycmF5ID0gW10sXG4gICAgICAgICAgaSwgaiwgaWxlbiwgamxlbjtcbiAgICAgIGZvciAoaSA9IDAsIGlsZW4gPSB0aGlzLmVsZW1lbnRzLmxlbmd0aDsgaSA8IGlsZW47IGkrKykge1xuICAgICAgICB2YXIgcm93ID0gW107XG4gICAgICAgIGZvciAoaiA9IDAsIGpsZW4gPSB0aGlzLmNvbHVtbk5hbWVzLmxlbmd0aDsgaiA8IGpsZW4gOyBqKyspIHtcbiAgICAgICAgICByb3cucHVzaCh0aGlzLmVsZW1lbnRzW2ldWyB0aGlzLmNvbHVtbk5hbWVzW2pdXSk7XG4gICAgICAgIH1cbiAgICAgICAgYXJyYXkucHVzaChyb3cpO1xuICAgICAgfVxuICAgICAgXG4gICAgICByZXR1cm4gYXJyYXk7XG4gICAgfVxuICB9O1xuXG4gIGlmKHR5cGVvZiBtb2R1bGUgIT09ICd1bmRlZmluZWQnICYmIG1vZHVsZS5leHBvcnRzKSB7IC8vZG9uJ3QganVzdCB1c2UgaW5Ob2RlSlMsIHdlIG1heSBiZSBpbiBCcm93c2VyaWZ5XG4gICAgbW9kdWxlLmV4cG9ydHMgPSBUYWJsZXRvcDtcbiAgfSBlbHNlIGlmICh0eXBlb2YgZGVmaW5lID09PSAnZnVuY3Rpb24nICYmIGRlZmluZS5hbWQpIHtcbiAgICBkZWZpbmUoZnVuY3Rpb24gKCkge1xuICAgICAgcmV0dXJuIFRhYmxldG9wO1xuICAgIH0pO1xuICB9IGVsc2Uge1xuICAgIHdpbmRvdy5UYWJsZXRvcCA9IFRhYmxldG9wO1xuICB9XG5cbn0pKCk7IiwiLy8gc2hpbSBmb3IgdXNpbmcgcHJvY2VzcyBpbiBicm93c2VyXG52YXIgcHJvY2VzcyA9IG1vZHVsZS5leHBvcnRzID0ge307XG5cbi8vIGNhY2hlZCBmcm9tIHdoYXRldmVyIGdsb2JhbCBpcyBwcmVzZW50IHNvIHRoYXQgdGVzdCBydW5uZXJzIHRoYXQgc3R1YiBpdFxuLy8gZG9uJ3QgYnJlYWsgdGhpbmdzLiAgQnV0IHdlIG5lZWQgdG8gd3JhcCBpdCBpbiBhIHRyeSBjYXRjaCBpbiBjYXNlIGl0IGlzXG4vLyB3cmFwcGVkIGluIHN0cmljdCBtb2RlIGNvZGUgd2hpY2ggZG9lc24ndCBkZWZpbmUgYW55IGdsb2JhbHMuICBJdCdzIGluc2lkZSBhXG4vLyBmdW5jdGlvbiBiZWNhdXNlIHRyeS9jYXRjaGVzIGRlb3B0aW1pemUgaW4gY2VydGFpbiBlbmdpbmVzLlxuXG52YXIgY2FjaGVkU2V0VGltZW91dDtcbnZhciBjYWNoZWRDbGVhclRpbWVvdXQ7XG5cbmZ1bmN0aW9uIGRlZmF1bHRTZXRUaW1vdXQoKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdzZXRUaW1lb3V0IGhhcyBub3QgYmVlbiBkZWZpbmVkJyk7XG59XG5mdW5jdGlvbiBkZWZhdWx0Q2xlYXJUaW1lb3V0ICgpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ2NsZWFyVGltZW91dCBoYXMgbm90IGJlZW4gZGVmaW5lZCcpO1xufVxuKGZ1bmN0aW9uICgpIHtcbiAgICB0cnkge1xuICAgICAgICBpZiAodHlwZW9mIHNldFRpbWVvdXQgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIGNhY2hlZFNldFRpbWVvdXQgPSBzZXRUaW1lb3V0O1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgY2FjaGVkU2V0VGltZW91dCA9IGRlZmF1bHRTZXRUaW1vdXQ7XG4gICAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNhY2hlZFNldFRpbWVvdXQgPSBkZWZhdWx0U2V0VGltb3V0O1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICBpZiAodHlwZW9mIGNsZWFyVGltZW91dCA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICAgICAgY2FjaGVkQ2xlYXJUaW1lb3V0ID0gY2xlYXJUaW1lb3V0O1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgY2FjaGVkQ2xlYXJUaW1lb3V0ID0gZGVmYXVsdENsZWFyVGltZW91dDtcbiAgICAgICAgfVxuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgY2FjaGVkQ2xlYXJUaW1lb3V0ID0gZGVmYXVsdENsZWFyVGltZW91dDtcbiAgICB9XG59ICgpKVxuZnVuY3Rpb24gcnVuVGltZW91dChmdW4pIHtcbiAgICBpZiAoY2FjaGVkU2V0VGltZW91dCA9PT0gc2V0VGltZW91dCkge1xuICAgICAgICAvL25vcm1hbCBlbnZpcm9tZW50cyBpbiBzYW5lIHNpdHVhdGlvbnNcbiAgICAgICAgcmV0dXJuIHNldFRpbWVvdXQoZnVuLCAwKTtcbiAgICB9XG4gICAgLy8gaWYgc2V0VGltZW91dCB3YXNuJ3QgYXZhaWxhYmxlIGJ1dCB3YXMgbGF0dGVyIGRlZmluZWRcbiAgICBpZiAoKGNhY2hlZFNldFRpbWVvdXQgPT09IGRlZmF1bHRTZXRUaW1vdXQgfHwgIWNhY2hlZFNldFRpbWVvdXQpICYmIHNldFRpbWVvdXQpIHtcbiAgICAgICAgY2FjaGVkU2V0VGltZW91dCA9IHNldFRpbWVvdXQ7XG4gICAgICAgIHJldHVybiBzZXRUaW1lb3V0KGZ1biwgMCk7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIC8vIHdoZW4gd2hlbiBzb21lYm9keSBoYXMgc2NyZXdlZCB3aXRoIHNldFRpbWVvdXQgYnV0IG5vIEkuRS4gbWFkZG5lc3NcbiAgICAgICAgcmV0dXJuIGNhY2hlZFNldFRpbWVvdXQoZnVuLCAwKTtcbiAgICB9IGNhdGNoKGUpe1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgLy8gV2hlbiB3ZSBhcmUgaW4gSS5FLiBidXQgdGhlIHNjcmlwdCBoYXMgYmVlbiBldmFsZWQgc28gSS5FLiBkb2Vzbid0IHRydXN0IHRoZSBnbG9iYWwgb2JqZWN0IHdoZW4gY2FsbGVkIG5vcm1hbGx5XG4gICAgICAgICAgICByZXR1cm4gY2FjaGVkU2V0VGltZW91dC5jYWxsKG51bGwsIGZ1biwgMCk7XG4gICAgICAgIH0gY2F0Y2goZSl7XG4gICAgICAgICAgICAvLyBzYW1lIGFzIGFib3ZlIGJ1dCB3aGVuIGl0J3MgYSB2ZXJzaW9uIG9mIEkuRS4gdGhhdCBtdXN0IGhhdmUgdGhlIGdsb2JhbCBvYmplY3QgZm9yICd0aGlzJywgaG9wZnVsbHkgb3VyIGNvbnRleHQgY29ycmVjdCBvdGhlcndpc2UgaXQgd2lsbCB0aHJvdyBhIGdsb2JhbCBlcnJvclxuICAgICAgICAgICAgcmV0dXJuIGNhY2hlZFNldFRpbWVvdXQuY2FsbCh0aGlzLCBmdW4sIDApO1xuICAgICAgICB9XG4gICAgfVxuXG5cbn1cbmZ1bmN0aW9uIHJ1bkNsZWFyVGltZW91dChtYXJrZXIpIHtcbiAgICBpZiAoY2FjaGVkQ2xlYXJUaW1lb3V0ID09PSBjbGVhclRpbWVvdXQpIHtcbiAgICAgICAgLy9ub3JtYWwgZW52aXJvbWVudHMgaW4gc2FuZSBzaXR1YXRpb25zXG4gICAgICAgIHJldHVybiBjbGVhclRpbWVvdXQobWFya2VyKTtcbiAgICB9XG4gICAgLy8gaWYgY2xlYXJUaW1lb3V0IHdhc24ndCBhdmFpbGFibGUgYnV0IHdhcyBsYXR0ZXIgZGVmaW5lZFxuICAgIGlmICgoY2FjaGVkQ2xlYXJUaW1lb3V0ID09PSBkZWZhdWx0Q2xlYXJUaW1lb3V0IHx8ICFjYWNoZWRDbGVhclRpbWVvdXQpICYmIGNsZWFyVGltZW91dCkge1xuICAgICAgICBjYWNoZWRDbGVhclRpbWVvdXQgPSBjbGVhclRpbWVvdXQ7XG4gICAgICAgIHJldHVybiBjbGVhclRpbWVvdXQobWFya2VyKTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgLy8gd2hlbiB3aGVuIHNvbWVib2R5IGhhcyBzY3Jld2VkIHdpdGggc2V0VGltZW91dCBidXQgbm8gSS5FLiBtYWRkbmVzc1xuICAgICAgICByZXR1cm4gY2FjaGVkQ2xlYXJUaW1lb3V0KG1hcmtlcik7XG4gICAgfSBjYXRjaCAoZSl7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICAvLyBXaGVuIHdlIGFyZSBpbiBJLkUuIGJ1dCB0aGUgc2NyaXB0IGhhcyBiZWVuIGV2YWxlZCBzbyBJLkUuIGRvZXNuJ3QgIHRydXN0IHRoZSBnbG9iYWwgb2JqZWN0IHdoZW4gY2FsbGVkIG5vcm1hbGx5XG4gICAgICAgICAgICByZXR1cm4gY2FjaGVkQ2xlYXJUaW1lb3V0LmNhbGwobnVsbCwgbWFya2VyKTtcbiAgICAgICAgfSBjYXRjaCAoZSl7XG4gICAgICAgICAgICAvLyBzYW1lIGFzIGFib3ZlIGJ1dCB3aGVuIGl0J3MgYSB2ZXJzaW9uIG9mIEkuRS4gdGhhdCBtdXN0IGhhdmUgdGhlIGdsb2JhbCBvYmplY3QgZm9yICd0aGlzJywgaG9wZnVsbHkgb3VyIGNvbnRleHQgY29ycmVjdCBvdGhlcndpc2UgaXQgd2lsbCB0aHJvdyBhIGdsb2JhbCBlcnJvci5cbiAgICAgICAgICAgIC8vIFNvbWUgdmVyc2lvbnMgb2YgSS5FLiBoYXZlIGRpZmZlcmVudCBydWxlcyBmb3IgY2xlYXJUaW1lb3V0IHZzIHNldFRpbWVvdXRcbiAgICAgICAgICAgIHJldHVybiBjYWNoZWRDbGVhclRpbWVvdXQuY2FsbCh0aGlzLCBtYXJrZXIpO1xuICAgICAgICB9XG4gICAgfVxuXG5cblxufVxudmFyIHF1ZXVlID0gW107XG52YXIgZHJhaW5pbmcgPSBmYWxzZTtcbnZhciBjdXJyZW50UXVldWU7XG52YXIgcXVldWVJbmRleCA9IC0xO1xuXG5mdW5jdGlvbiBjbGVhblVwTmV4dFRpY2soKSB7XG4gICAgaWYgKCFkcmFpbmluZyB8fCAhY3VycmVudFF1ZXVlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgZHJhaW5pbmcgPSBmYWxzZTtcbiAgICBpZiAoY3VycmVudFF1ZXVlLmxlbmd0aCkge1xuICAgICAgICBxdWV1ZSA9IGN1cnJlbnRRdWV1ZS5jb25jYXQocXVldWUpO1xuICAgIH0gZWxzZSB7XG4gICAgICAgIHF1ZXVlSW5kZXggPSAtMTtcbiAgICB9XG4gICAgaWYgKHF1ZXVlLmxlbmd0aCkge1xuICAgICAgICBkcmFpblF1ZXVlKCk7XG4gICAgfVxufVxuXG5mdW5jdGlvbiBkcmFpblF1ZXVlKCkge1xuICAgIGlmIChkcmFpbmluZykge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIHZhciB0aW1lb3V0ID0gcnVuVGltZW91dChjbGVhblVwTmV4dFRpY2spO1xuICAgIGRyYWluaW5nID0gdHJ1ZTtcblxuICAgIHZhciBsZW4gPSBxdWV1ZS5sZW5ndGg7XG4gICAgd2hpbGUobGVuKSB7XG4gICAgICAgIGN1cnJlbnRRdWV1ZSA9IHF1ZXVlO1xuICAgICAgICBxdWV1ZSA9IFtdO1xuICAgICAgICB3aGlsZSAoKytxdWV1ZUluZGV4IDwgbGVuKSB7XG4gICAgICAgICAgICBpZiAoY3VycmVudFF1ZXVlKSB7XG4gICAgICAgICAgICAgICAgY3VycmVudFF1ZXVlW3F1ZXVlSW5kZXhdLnJ1bigpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIHF1ZXVlSW5kZXggPSAtMTtcbiAgICAgICAgbGVuID0gcXVldWUubGVuZ3RoO1xuICAgIH1cbiAgICBjdXJyZW50UXVldWUgPSBudWxsO1xuICAgIGRyYWluaW5nID0gZmFsc2U7XG4gICAgcnVuQ2xlYXJUaW1lb3V0KHRpbWVvdXQpO1xufVxuXG5wcm9jZXNzLm5leHRUaWNrID0gZnVuY3Rpb24gKGZ1bikge1xuICAgIHZhciBhcmdzID0gbmV3IEFycmF5KGFyZ3VtZW50cy5sZW5ndGggLSAxKTtcbiAgICBpZiAoYXJndW1lbnRzLmxlbmd0aCA+IDEpIHtcbiAgICAgICAgZm9yICh2YXIgaSA9IDE7IGkgPCBhcmd1bWVudHMubGVuZ3RoOyBpKyspIHtcbiAgICAgICAgICAgIGFyZ3NbaSAtIDFdID0gYXJndW1lbnRzW2ldO1xuICAgICAgICB9XG4gICAgfVxuICAgIHF1ZXVlLnB1c2gobmV3IEl0ZW0oZnVuLCBhcmdzKSk7XG4gICAgaWYgKHF1ZXVlLmxlbmd0aCA9PT0gMSAmJiAhZHJhaW5pbmcpIHtcbiAgICAgICAgcnVuVGltZW91dChkcmFpblF1ZXVlKTtcbiAgICB9XG59O1xuXG4vLyB2OCBsaWtlcyBwcmVkaWN0aWJsZSBvYmplY3RzXG5mdW5jdGlvbiBJdGVtKGZ1biwgYXJyYXkpIHtcbiAgICB0aGlzLmZ1biA9IGZ1bjtcbiAgICB0aGlzLmFycmF5ID0gYXJyYXk7XG59XG5JdGVtLnByb3RvdHlwZS5ydW4gPSBmdW5jdGlvbiAoKSB7XG4gICAgdGhpcy5mdW4uYXBwbHkobnVsbCwgdGhpcy5hcnJheSk7XG59O1xucHJvY2Vzcy50aXRsZSA9ICdicm93c2VyJztcbnByb2Nlc3MuYnJvd3NlciA9IHRydWU7XG5wcm9jZXNzLmVudiA9IHt9O1xucHJvY2Vzcy5hcmd2ID0gW107XG5wcm9jZXNzLnZlcnNpb24gPSAnJzsgLy8gZW1wdHkgc3RyaW5nIHRvIGF2b2lkIHJlZ2V4cCBpc3N1ZXNcbnByb2Nlc3MudmVyc2lvbnMgPSB7fTtcblxuZnVuY3Rpb24gbm9vcCgpIHt9XG5cbnByb2Nlc3Mub24gPSBub29wO1xucHJvY2Vzcy5hZGRMaXN0ZW5lciA9IG5vb3A7XG5wcm9jZXNzLm9uY2UgPSBub29wO1xucHJvY2Vzcy5vZmYgPSBub29wO1xucHJvY2Vzcy5yZW1vdmVMaXN0ZW5lciA9IG5vb3A7XG5wcm9jZXNzLnJlbW92ZUFsbExpc3RlbmVycyA9IG5vb3A7XG5wcm9jZXNzLmVtaXQgPSBub29wO1xuXG5wcm9jZXNzLmJpbmRpbmcgPSBmdW5jdGlvbiAobmFtZSkge1xuICAgIHRocm93IG5ldyBFcnJvcigncHJvY2Vzcy5iaW5kaW5nIGlzIG5vdCBzdXBwb3J0ZWQnKTtcbn07XG5cbnByb2Nlc3MuY3dkID0gZnVuY3Rpb24gKCkgeyByZXR1cm4gJy8nIH07XG5wcm9jZXNzLmNoZGlyID0gZnVuY3Rpb24gKGRpcikge1xuICAgIHRocm93IG5ldyBFcnJvcigncHJvY2Vzcy5jaGRpciBpcyBub3Qgc3VwcG9ydGVkJyk7XG59O1xucHJvY2Vzcy51bWFzayA9IGZ1bmN0aW9uKCkgeyByZXR1cm4gMDsgfTtcbiIsIihmdW5jdGlvbiBlKHQsbixyKXtmdW5jdGlvbiBzKG8sdSl7aWYoIW5bb10pe2lmKCF0W29dKXt2YXIgYT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2lmKCF1JiZhKXJldHVybiBhKG8sITApO2lmKGkpcmV0dXJuIGkobywhMCk7dmFyIGY9bmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgbW9kdWxlICdcIitvK1wiJ1wiKTt0aHJvdyBmLmNvZGU9XCJNT0RVTEVfTk9UX0ZPVU5EXCIsZn12YXIgbD1uW29dPXtleHBvcnRzOnt9fTt0W29dWzBdLmNhbGwobC5leHBvcnRzLGZ1bmN0aW9uKGUpe3ZhciBuPXRbb11bMV1bZV07cmV0dXJuIHMobj9uOmUpfSxsLGwuZXhwb3J0cyxlLHQsbixyKX1yZXR1cm4gbltvXS5leHBvcnRzfXZhciBpPXR5cGVvZiByZXF1aXJlPT1cImZ1bmN0aW9uXCImJnJlcXVpcmU7Zm9yKHZhciBvPTA7bzxyLmxlbmd0aDtvKyspcyhyW29dKTtyZXR1cm4gc30pIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBSUFBO0FEQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7OztBRHBMQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Ozs7QUQxbEJBLE9BQU8sQ0FBQyxRQUFSLEdBQW1CLE9BQUEsQ0FBUSxVQUFSOzs7O0FEQW5CLElBQUE7O0FBQUUsV0FBYSxPQUFBLENBQVEsS0FBUjs7QUFHVCxPQUFPLENBQUM7RUFDQyxlQUFDLE9BQUQ7SUFDWCxJQUFDLENBQUEsSUFBRCxHQUFRLE9BQU8sQ0FBQztJQUVoQixJQUFDLENBQUEsR0FBRCxHQUFPLENBQUEsU0FBQSxLQUFBO2FBQUEsU0FBQyxRQUFEO2VBQ0wsUUFBUSxDQUFDLElBQVQsQ0FBYztVQUNaLEdBQUEsRUFBSyxLQUFDLENBQUEsSUFETTtVQUVaLFdBQUEsRUFBYSxJQUZEO1VBR1osUUFBQSxFQUFVLFNBQUMsSUFBRCxFQUFPLEtBQVA7QUFDUixtQkFBTyxRQUFBLENBQVMsSUFBVCxFQUFlLEtBQWY7VUFEQyxDQUhFO1NBQWQ7TUFESztJQUFBLENBQUEsQ0FBQSxDQUFBLElBQUE7RUFISSJ9 822 | -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/cursor-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/cursor-active.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/cursor-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/cursor-active@2x.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/cursor.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/cursor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/cursor@2x.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/icon-120.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/icon-152.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/icon-180.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/icon-192.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/images/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/framer/images/icon-76.png -------------------------------------------------------------------------------- /sheetExample.framer/framer/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: none; 5 | -webkit-user-select: none; 6 | -webkit-tap-highlight-color: rgba(0,0,0,0); 7 | } 8 | 9 | body { 10 | background-color: #fff; 11 | font: 28px/1em "Helvetica"; 12 | color: gray; 13 | overflow: hidden; 14 | } 15 | 16 | a { 17 | color: gray; 18 | } 19 | 20 | body { 21 | cursor: url('images/cursor.png') 32 32, auto; 22 | cursor: -webkit-image-set( 23 | url('images/cursor.png') 1x, 24 | url('images/cursor@2x.png') 2x 25 | ) 32 32, auto; 26 | } 27 | 28 | body:active { 29 | cursor: url('images/cursor-active.png') 32 32, auto; 30 | cursor: -webkit-image-set( 31 | url('images/cursor-active.png') 1x, 32 | url('images/cursor-active@2x.png') 2x 33 | ) 32 32, auto; 34 | } 35 | 36 | .framerAlertBackground { 37 | position: absolute; top:0px; left:0px; right:0px; bottom:0px; 38 | z-index: 1000; 39 | background-color: #fff; 40 | } 41 | 42 | .framerAlert { 43 | font:400 14px/1.4 "Helvetica Neue", Helvetica, Arial, sans-serif; 44 | -webkit-font-smoothing:antialiased; 45 | color:#616367; text-align:center; 46 | position: absolute; top:40%; left:50%; width:260px; margin-left:-130px; 47 | } 48 | .framerAlert strong { font-weight:500; color:#000; margin-bottom:8px; display:block; } 49 | .framerAlert a { color:#28AFFA; } 50 | .framerAlert .btn { 51 | font-weight:500; text-decoration:none; line-height:1; 52 | display:inline-block; padding:6px 12px 7px 12px; 53 | border-radius:3px; margin-top:12px; 54 | background:#28AFFA; color:#fff; 55 | } 56 | 57 | ::-webkit-scrollbar { 58 | display: none; 59 | } -------------------------------------------------------------------------------- /sheetExample.framer/framer/version: -------------------------------------------------------------------------------- 1 | 5 -------------------------------------------------------------------------------- /sheetExample.framer/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewliebchen/framer-sheet/973d519c5c6c74c143bf1175fee469c79c06777d/sheetExample.framer/images/.gitkeep -------------------------------------------------------------------------------- /sheetExample.framer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sheetExample.framer/modules/npm.coffee: -------------------------------------------------------------------------------- 1 | exports.Tabletop = require 'tabletop' 2 | -------------------------------------------------------------------------------- /sheetExample.framer/modules/sheet.coffee: -------------------------------------------------------------------------------- 1 | { Tabletop } = require 'npm' 2 | 3 | 4 | class exports.Sheet 5 | constructor: (options) -> 6 | @_key = options.key 7 | 8 | @get = (callback) => 9 | Tabletop.init { 10 | key: @_key 11 | simpleSheet: true 12 | callback: (data, sheet) => 13 | return callback(data, sheet) 14 | } 15 | --------------------------------------------------------------------------------