\r\n\r\n\"\"\"\r\n\r\nsrcUrl = ->\r\n URL.createObjectURL new Blob [htmlContent()],\r\n type: \"text/html; charset=utf-8\"\r\n\r\ndataUrl = -> \"data:text/html;base64,#{btoa(htmlContent())}\"\r\n\r\ntestFrame = (fn) ->\r\n iframe = document.createElement('iframe')\r\n iframe.name = \"iframe-#{randId()}\"\r\n iframe.src = srcUrl()\r\n document.body.appendChild(iframe)\r\n\r\n postmaster = Postmaster\r\n remoteTarget: ->\r\n iframe.contentWindow\r\n\r\n iframe.addEventListener \"load\", ->\r\n fn(postmaster)\r\n .finally ->\r\n iframe.remove()\r\n postmaster.dispose()\r\n\r\n return\r\n\r\ndescribe \"Postmaster\", ->\r\n # Can't open child windows from within sandboxed iframes?\r\n it.skip \"should work with openened windows\", (done) ->\r\n childWindow = window.open(srcUrl(), \"child-#{randId()}\", \"width=200,height=200\")\r\n\r\n postmaster = Postmaster\r\n remoteTarget: -> childWindow\r\n\r\n childWindow.addEventListener \"load\", ->\r\n postmaster.send \"echo\", 5\r\n .then (result) ->\r\n assert.equal result, 5\r\n .then ->\r\n done()\r\n , (error) ->\r\n done(error)\r\n .then ->\r\n childWindow.close()\r\n postmaster.dispose()\r\n\r\n return\r\n\r\n it \"should work with iframes\", (done) ->\r\n testFrame (postmaster) ->\r\n postmaster.send \"echo\", 17\r\n .then (result) ->\r\n assert.equal result, 17\r\n .then done, done\r\n\r\n return\r\n\r\n it \"should handle the remote call throwing errors\", (done) ->\r\n testFrame (postmaster) ->\r\n postmaster.send \"throws\"\r\n .then ->\r\n done new Error \"Expected an error\"\r\n , (error) ->\r\n done()\r\n\r\n return\r\n\r\n it \"should throwing a useful error when the remote doesn't define the function\", (done) ->\r\n testFrame (postmaster) ->\r\n postmaster.send \"undefinedFn\"\r\n .then ->\r\n done new Error \"Expected an error\"\r\n , (error) ->\r\n done()\r\n\r\n return\r\n\r\n it \"should handle the remote call returning failed promises\", (done) ->\r\n testFrame (postmaster) ->\r\n postmaster.send \"promiseFail\"\r\n .then ->\r\n done new Error \"Expected an error\"\r\n , (error) ->\r\n done()\r\n\r\n return\r\n\r\n it \"should be able to go around the world\", (done) ->\r\n testFrame (postmaster) ->\r\n postmaster.yolo = (txt) ->\r\n \"heyy #{txt}\"\r\n postmaster.send \"send\", \"yolo\", \"cool\"\r\n .then (result) ->\r\n assert.equal result, \"heyy cool\"\r\n .then ->\r\n done()\r\n , (error) ->\r\n done(error)\r\n\r\n return\r\n\r\n it.skip \"should work with web workers\", (done) ->\r\n blob = new Blob [scriptContent()], type: \"application/javascript\"\r\n jsUrl = URL.createObjectURL(blob)\r\n\r\n worker = new Worker(jsUrl)\r\n\r\n postmaster = Postmaster\r\n remoteTarget: -> worker\r\n receiver: -> worker\r\n\r\n setTimeout ->\r\n postmaster.send \"echo\", 17\r\n .then (result) ->\r\n assert.equal result, 17\r\n .then ->\r\n done()\r\n , (error) ->\r\n done(error)\r\n .finally ->\r\n worker.terminate()\r\n , 100\r\n\r\n return\r\n\r\n it \"should fail quickly when contacting a window that doesn't support Postmaster\", (done) ->\r\n iframe = document.createElement('iframe')\r\n document.body.appendChild(iframe)\r\n\r\n childWindow = iframe.contentWindow\r\n postmaster = Postmaster\r\n remoteTarget: -> childWindow\r\n ackTimeout: -> 30\r\n\r\n postmaster.send \"echo\", 5\r\n .catch (e) ->\r\n if e.message.match /no ack/i\r\n done()\r\n else\r\n done(1)\r\n .finally ->\r\n iframe.remove()\r\n postmaster.dispose()\r\n\r\n return\r\n\r\n it \"should return a rejected promise when unable to send to the target\", (done) ->\r\n postmaster = Postmaster\r\n remoteTarget: -> null\r\n \r\n postmaster.send \"yo\"\r\n .then ->\r\n done throw new Error \"Expected an error\"\r\n , (e) ->\r\n assert.equal e.message, \"No remote target\"\r\n done()\r\n .catch done\r\n .finally ->\r\n postmaster.dispose()\r\n\r\n return\r\n\r\n it \"should log\", ->\r\n called = false\r\n\r\n postmaster = Postmaster\r\n logger:\r\n info: ->\r\n called = true\r\n\r\n assert called\r\n postmaster.dispose()\r\n" }, "lib/test/ui.coffee": { "content": "{AceEditor} = ui = require \"../ui/index\"\n\ndescribe \"ui\", ->\n it \"should provide an ace editor view\", ->\n {initSession, modeFor} = AceEditor\n assert initSession\n assert modeFor\n\n assert.equal modeFor(\"file.js\"), \"javascript\"\n" }, "templates/reader-input.coffee": { "content": "module.exports = (options={}) ->\n input = document.createElement('input')\n input.type = \"file\"\n input.setAttribute \"accept\", options.accept\n\n input.onchange = (e) ->\n options.select? input.files[0]\n\n return input\n" }, "lib/mousetrap.js": { "content": "/* mousetrap v1.6.3 craig.is/killing/mice */\r\n(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent(\"on\"+b,g)}function z(a){if(\"keypress\"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push(\"shift\");a.altKey&&b.push(\"alt\");a.ctrlKey&&b.push(\"ctrl\");a.metaKey&&b.push(\"meta\");return b}function w(a){return\"shift\"==a||\"ctrl\"==a||\"alt\"==a||\r\n\"meta\"==a}function A(a,b){var g,d=[];var e=a;\"+\"===e?e=[\"+\"]:(e=e.replace(/\\+{2}/g,\"+plus\"),e=e.split(\"+\"));for(g=0;gc||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?\"keydown\":\"keypress\"}\"keypress\"==g&&d.length&&(g=\"keydown\");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=\r\na||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];\"keyup\"==h&&w(a)&&(b=[a]);for(l=0;l\":\".\",\"?\":\"/\",\"|\":\"\\\\\"},B={option:\"alt\",command:\"meta\",\"return\":\"enter\",\r\nescape:\"esc\",plus:\"+\",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?\"meta\":\"ctrl\"},p;for(c=1;20>c;++c)n[111+c]=\"f\"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+\":\"+b])this._directMap[a+\":\"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};\r\nthis._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(\" \"+b.className+\" \").indexOf(\" mousetrap \")||D(b,this.target))return!1;if(\"composedPath\"in a&&\"function\"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return\"INPUT\"==b.tagName||\"SELECT\"==b.tagName||\"TEXTAREA\"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};\r\nd.init=function(){var a=d(u),b;for(b in a)\"_\"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();\"undefined\"!==typeof module&&module.exports&&(module.exports=d);\"function\"===typeof define&&define.amd&&define(function(){return d})}})(\"undefined\"!==typeof window?window:null,\"undefined\"!==typeof window?document:null);\r\n" }, "lib/test/mousetrap.js": { "content": "" }, "docs/app.md": { "content": "App\n===\n\nThe system runtime provides an app base with many useful methods:\n\n- `confirmUnsaved`\n- `currentPath` Observable string\n- `drop` handler that receives an array of Files on a drop event.\n- `exit`\n- `extend`\n- `hotkey`\n- `new`\n- `open`\n- `save`\n- `saved` Observable bool representing the state of the app, saved or not.\n- `saveAs`\n- `paste` handler that receives an array of Files on a paste event.\n\nYour app must provide these to make use of the paste/drop/fileIO interfaces.\n\n- `loadFile`\n- `newFile`\n- `saveData`\n\nOne can optionally provide a `menu` property:\n\n```\nmenu: \"\"\"\n File\n New\n Open\n Save\n Save As\n ---\n Exit\n Edit\n Undo\n Redo\n Resize -> doResize\n Example\n Item One\n Item Two\n Submenu\n Sub Item One\n Sub Item Two\n\"\"\"\n```\n\nThis menu micro-format defines a menubar for your app. The menu items have the\nname given delegating to the method of the same name except non-alphanumeric\ncharacters are removed and the initial character is downcased, i.e. `Save As` ->\n`saveAs`.\n\nOther app methods that you can optionally define:\n\n- `title`\n\nTitle will be observed and piped to the \"host application\" if it is a function with\nobservable dependencies.\n\nThe main idea is to combine all the common behaviors into one comprehensive\nfocal point.\n\nGlossary\n--------\n\n### Observables\n\nObservable properties are functions that store a value when called\nwith one argument and return that value when called with zero arguments. They\nhave an `observe` method.\n\nObservable functions will automatically re-execute if they depend on observable\nproperties. They may be composed of any number of observable properties or\nfunctions.\n\n### Host\n\nCommunication channel to the host environment.\n\n```\nhost.writeFile(path, blob)\n```\n\nThe host environment is ZineOS or none if standalone. There may be other possible\nhost environments, it could be the host OS if running in Electron.\n\n### Host Application\n\nThe ZineOS application object running inside the ZineOS frame. This holds info\nabout saved status, the title, a handle to the iframe or app element, and the\nactual window element in ZineOS.\n\nIn standalone the host application is a thin wrapper over the browser chrome.\n\nIn summary the host application is the host environment's representation of this\napplication.\n" }, "lib/runtime.coffee": { "content": "# runtime is what prepares the environment for user apps\n# we hook up the postmaster and proxy messages to the OS\n\n{version} = require \"../pixie\"\n\nPostmaster = require \"./postmaster\"\n{applyStyle, Observable, Style} = require \"../lib/ui/index\"\n\nRuntime = (system, opts={}) ->\n if opts.applyStyle\n applyStyle(Style.all, 'system')\n\n opts.logger ?=\n info: ->\n debug: ->\n\n externalObservables = {}\n\n # Queue up messages until a delegate is assigned\n heldApplicationMessages = []\n\n postmaster = Postmaster\n logger: opts.logger\n # For receiving messages from the system\n delegate:\n application: (method, args...) ->\n if applicationTarget.delegate\n applicationTarget.delegate[method](args...)\n else\n # This promise should keep the channel unresolved until the future\n new Promise (resolve, reject) ->\n heldApplicationMessages.push (delegate) ->\n try\n resolve delegate[method](args...)\n catch e\n reject e\n \n updateSignal: (name, newValue) ->\n externalObservables[name](newValue)\n \n fn: (handlerId, args) ->\n # TODO: `this` is null but should be `system` here for bound events.\n eventListeners[handlerId].apply(null, args)\n\n remoteExists = postmaster.remoteTarget()\n\n applicationTarget =\n observeSignal: (name, handler) ->\n observable = Observable()\n externalObservables[name] = observable\n\n observable.observe handler\n\n # Invoke the handler with the initial value\n postmaster.send \"application\", \"observeSignal\", name\n .then handler\n\n # For sending messages to ZineOS application side\n applicationProxy = new Proxy applicationTarget,\n get: (target, property, receiver) ->\n target[property] or\n ->\n return unless remoteExists\n postmaster.send \"application\", property, arguments...\n set: (target, property, value, receiver) ->\n if property is \"delegate\"\n heldApplicationMessages.forEach (fn)->\n fn(value)\n\n heldApplicationMessages = []\n\n target[property] = value\n\n return target[property]\n\n lastEventListenerId = 0\n eventListeners = {}\n readyPromise = null\n hostTarget =\n ready: ->\n return readyPromise if readyPromise\n\n if remoteExists\n readyPromise = postmaster.send \"ready\",\n ZineOSClient: version\n token: postmaster.token\n .then (hostConfig) ->\n appData = hostConfig?.ZineOS\n\n if appData\n initializeOnZineOS(appData)\n\n return hostConfig\n else \n # Quick resolve when there is no parent window to connect to\n polyfillForStandalone()\n\n readyPromise = Promise.resolve\n standalone: true\n\n # Bind listeners to system events, sending an id in place of a local function\n # reference\n on: (eventName, handler) ->\n lastEventListenerId += 1\n\n eventListeners[lastEventListenerId] = handler\n postmaster.send \"system\", \"on\", eventName, lastEventListenerId\n\n off: (eventName, handler) ->\n [handlerId] = Object.keys(eventListeners).filter (id) ->\n eventListeners[id] is handler\n\n delete eventListeners[handlerId]\n postmaster.send \"system\", \"off\", eventName, handlerId\n\n hostTarget.target = hostTarget\n\n # Unattached, standalone page. Use a systemTarget for that environment\n # Currently mapping system.readFile to fetch\n polyfillForStandalone = ->\n Object.assign hostTarget,\n readFile: (path) ->\n fetch(path)\n .then (response) ->\n if 200 <= response.status < 300\n response.blob()\n else\n throw new Error(response.statusText)\n writeFile: (path, blob) ->\n blob.download(path)\n\n # Proxy to the host environment\n # Host methods can be overridden by writing to the host target\n # this allows us to polyfill for standalone environments (with no host)\n # and provides bindings for event channels and others things (experimental).\n host = new Proxy hostTarget,\n get: (target, property, receiver) ->\n if Object::hasOwnProperty.call(target, property)\n target[property]\n else\n ->\n postmaster.send \"system\", property, arguments...\n\n # TODO: Also interesting would be to proxy observable arguments where we\n # create the receiver on the opposite end of the membrane and pass messages\n # back and forth like magic\n\n initializeOnZineOS = ({id}) ->\n applicationTarget.id = id\n\n document.addEventListener \"mousedown\", ->\n applicationProxy.raiseToTop()\n .catch console.warn\n\n BaseApp = require(\"./app/index\")(host, applicationProxy)\n\n client =\n # `postmaster` makes sense here since it is the client's postmaster instance\n postmaster: postmaster\n\n Object.assign system,\n # Launch stuff\n app:\n Base: BaseApp\n client: client\n config: {} # Host config gets merged into here\n host: host\n\n # Backwards compatible host proxy methods\n # TODO: deprecate?\n readFile: ->\n host.readFile arguments...\n readTree: ->\n host.readTree arguments...\n writeFile: ->\n host.writeFile arguments...\n\n # Only return {system, application}\n # Client utilities can be found in system.client\n # TODO: Remove global `application`\n application: applicationProxy\n system: system\n\nmodule.exports = Runtime\n" }, "lib/test/runtime.coffee": { "content": "mocha.globals(['OBSERVABLE_ROOT_HACK', \"application\"])\n\nsystemG = require \"/lib/exports\"\nSystemClient = require \"../runtime\"\n\nnullLogger =\n info: ->\n debug: ->\n\ndescribe \"Runtime\", ->\n it \"should return system and application proxies\", ->\n {system, application} = SystemClient(systemG)\n\n assert system\n assert application\n \n # Actual API\n app = system.app.Base()\n assert.equal app.currentPath(), \"\"\n assert.equal app.saved(), true\n\n # Cleanup\n system.client.postmaster.dispose()\n\n it \"should queue up messages until a delegate is assigned\", ->\n new Promise (resolve, reject) ->\n {system, application} = SystemClient(systemG)\n\n {postmaster} = system.client\n\n postmaster.delegate.application \"test1\", \"yo\"\n .then (c) ->\n assert.equal c, \"wat\"\n\n postmaster.delegate.application \"test2\", \"yo2\"\n .then (d) ->\n assert.equal d, \"heyy\"\n resolve()\n\n application.delegate =\n test1: (a) ->\n assert.equal a, \"yo\"\n\n return \"wat\"\n\n test2: (b) ->\n assert.equal b, \"yo2\"\n return \"heyy\"\n\n # Cleanup\n system.client.postmaster.dispose()\n\n it \"should connect when ready is called\", (done) ->\n {system, application} = SystemClient(systemG, {\n # logger: console\n })\n\n system.host.ready()\n .then ->\n done()\n system.client.postmaster.dispose()\n\n return\n\n # This was madness to test, the earlier clients had their own postmasters \n # listening!! Make sure to dispose shared resources!\n it \"should launch with config\", ->\n systemG.launch {\n # logger: console\n } , (config) ->\n # Cleanup\n system.client.postmaster.dispose()\n" }, "lib/app/index.coffee": { "content": "# Handle basic file saving/loading/picking, displaying modals/ui. This maps\n# common UI patterns to the `host`'s `readFile` and `writeFile` methods.\n\n# Caller must provide `self` object with the following methods:\n# `loadFile` Take a blob and path and load it as the application state.\n# `saveData` Return a promise that will be fulfilled with a blob of the\n# current application state.\n# `newFile` Initialize the application to an empty state.\n\n# This extends the `self` object with:\n# `currentPath`\n# `drop`\n# `exit`\n# `new`\n# `open`\n# `save`\n# `saved`\n# `saveAs`\n\n# It is expected that there is only one FileIO per page,\n# additional apps spawn in iframes or separate windows for isolation.\n# We add a global drop listener here.\n\n## Events\n#\n# `boot`\n# `dispose`\n#\n# Thought: Would it be better to call these `start` and `finish`?\n\n{applyStyle, Drop, Jadelet, MenuBar, Modal, Observable, Style} = require \"../ui/index\"\n\n# TODO: This is expanding a bit beyond FileIO and into general IO of the\n# app in its environment. We should pass the application proxy in here too and\n# wire it up. The application proxy should also handle the interface to\n# standalone mode things like window.title onbeforeunload behavior, etc.\n\n# Some more thoughts on `application` here... it's not really the right place\n# it needs to be bound to the app after boot when it can listen to the app's\n# observable properties and bind them to window.title, etc.\n\nBindable = require \"../bindable\"\n\n{crudeRequire} = require \"../pkg/index\"\n\nTemplateLoader = require \"./template-loader\"\nHotkeys = require \"./hotkeys\"\n\n# host is used for readFile and writeFile\n# application is used for syncing with the OS App state:\n# - exit\n# - icon\n# - saved\n# - title\n#\n# and setting the delegate on boot to receive messages sent from the host.\n\nmodule.exports = (host, application) ->\n (app={}) ->\n app.saved ?= Observable true\n app.currentPath ?= Observable \"\"\n app.config ?= {}\n\n # Includes\n Bindable null, app\n Hotkeys app\n\n Object.assign app,\n confirmUnsaved: ->\n return Promise.resolve() if app.saved()\n \n new Promise (resolve, reject) ->\n Modal.confirm \"You will lose unsaved progress, continue?\"\n .then (result) ->\n if result\n resolve()\n else\n reject()\n\n exit: ->\n application.exit()\n\n extend: Object.assign.bind(null, app)\n\n # Accepts an array of dropped files\n # returns true if we handled the event\n # apps can override this to customize their behavior\n drop: (files) ->\n file = files[0]\n app.loadFile file, file.name\n return true\n\n # Accepts an array of pasted files\n # returns true if we handled the event\n # apps can override this to customize their behavior\n paste: (files) ->\n file = files[0]\n app.loadFile file, file.name\n return true\n\n new: ->\n if app.saved()\n app.currentPath \"\"\n app.newFile()\n else\n app.confirmUnsaved()\n .then ->\n app.saved true\n app.newFile()\n\n open: ->\n app.confirmUnsaved()\n .then ->\n # TODO: File browser\n # TODO: Delegate to specific strategies\n Modal.prompt \"File Path\", app.currentPath()\n .then (newPath) ->\n if newPath\n app.currentPath newPath\n else\n throw new Error \"No path given\"\n .then (path) ->\n host.readFile path, true\n .then (file) ->\n app.loadFile file, path\n .catch (e) ->\n throw e if e\n\n reloadStyle: (cssText) ->\n applyStyle cssText, \"app\"\n\n save: ->\n path = app.currentPath()\n if path\n Promise.resolve()\n .then ->\n app.saveData()\n .then (blob) ->\n # TODO: Delegate to specific save strategy\n # zineOS, standalone, electron, ...\n # maybe application.writeFile?\n host.writeFile path, blob, true\n .then ->\n app.saved true\n return path\n else\n app.saveAs()\n\n saveAs: ->\n Modal.prompt \"File Path\", app.currentPath()\n .then (path) ->\n if path\n app.currentPath path\n app.save()\n\n # Detecting standalone config flag and provide alternative open and save\n # methods\n if system.config?.standalone\n ReaderInput = require \"../../templates/reader-input\"\n\n # Override chooser to use local PC\n app.open = ->\n Modal.show ReaderInput\n accept: app.accept?()\n select: (file) ->\n Modal.hide()\n app.loadFile file\n \n # Override save to present download\n app.save = ->\n Modal.prompt \"File name\", \"newfile.txt\"\n .then (name) ->\n app.saveData()\n .then (blob) ->\n blob.download()\n\n # Provide drop event\n # TODO: Remove drop handlers on dispose\n Drop document, (e) ->\n return if e.defaultPrevented\n\n files = e.dataTransfer.files\n\n if files.length\n e.preventDefault() if app.drop files\n\n # Provide paste event\n # TODO: Remove paste handlers on dispose\n document.addEventListener \"paste\", (e) ->\n return if e.defaultPrevented\n \n {clipboardData} = e\n \n files = clipboardData.files\n\n if files.length\n if app.paste files\n return e.preventDefault()\n\n files = Array::map.call e.clipboardData.items, (item) ->\n item.getAsFile()\n .filter (file) -> file\n\n if files.length\n e.preventDefault() if app.paste files\n\n try\n app.T ?= {}\n TemplateLoader app.pkg, app.T\n\n try\n app.version = crudeRequire(app.pkg.distribution.pixie.content).version\n\n # `boot` triggers\n app.on \"boot\", ->\n # Auto-apply base and app styles\n unless app.config.baseStyle is false\n applyStyle Style.all, \"base\"\n if @style\n applyStyle @style, \"app\"\n else\n try\n applyStyle crudeRequire(app.pkg.distribution.style.content), \"app\"\n\n # Auto-menu from menu string\n if @menu\n menuBar = MenuBar\n items: @menu\n handlers: @\n\n document.body.appendChild menuBar.element\n app.on \"dispose\", ->\n menuBar.element.remove()\n Jadelet.dispose menuBar.element\n\n if @element\n document.body.appendChild @element\n else if @template\n @element = Jadelet.exec(@template)(this)\n document.body.appendChild @element\n else if @T.App\n @element = @T.App this\n document.body.appendChild @element\n\n # Bind host application pieces\n application.delegate = @\n # auto-bind application title\n # Pipes title changes to os application window, etc.\n if @title?\n Observable -> application.title getProp app, \"title\"\n\n if @icon?\n Observable ->\n application.icon getProp app, \"icon\"\n\n # Pipe saved state to os app state\n if @saved?\n Observable -> application.saved getProp app, \"saved\"\n\n # TODO: onbeforeunload?\n\n app.on \"dispose\", ->\n if @element\n @element.remove()\n Jadelet.dispose @element\n\n return app\n\ngetProp = (context, prop) ->\n if typeof context[prop] is 'function'\n context[prop]()\n else\n context[prop]\n" }, "lib/test/app/index.coffee": { "content": "{Observable} = require \"/lib/ui/index\"\n\nAppGen = require \"/lib/app/index\"\n\nappProxyMock = {}\n\nBaseApp = AppGen({}, appProxyMock)\n\ndescribe \"App\", ->\n it \"should provide a base app constructor\", ->\n assert BaseApp()\n\n it \"should work standalone\", ->\n do (oldSystem=system) ->\n global.system =\n config:\n standalone: true\n\n assert BaseApp()\n\n global.system = oldSystem\n\n it \"should add a hotkey\", ->\n app = BaseApp()\n\n called = 0\n app.hotkey \"a\", (e) ->\n called++\n\n e = new KeyboardEvent('keypress', {keyCode: 97})\n document.dispatchEvent e\n\n assert.equal called, 1\n\n it \"should extend\", ->\n app = BaseApp()\n\n app.extend\n cool: \"duder\"\n\n assert.equal app.cool, \"duder\"\n\n it \"should include bindable\", ->\n app = BaseApp()\n\n assert app.on\n assert app.off\n assert app.trigger\n\n it \"should set title and icon\", ->\n saved = icon = title = null\n\n appProxyMock.title = (_title) ->\n title = _title\n appProxyMock.icon = (_icon) ->\n icon = _icon\n appProxyMock.saved = (_saved) ->\n saved = _saved\n\n app = BaseApp\n title: \"yolo\"\n icon: \"R\"\n saved: false\n\n app.trigger 'boot'\n \n assert.equal icon, \"R\"\n assert.equal title, \"yolo\"\n assert.equal saved, false\n\n it \"should pass on observable title changes\", ->\n title = null\n\n appProxyMock.title = (_title) ->\n title = _title\n\n app = BaseApp\n title: Observable \"wat\"\n\n app.trigger 'boot'\n assert.equal title, \"wat\"\n\n app.title \"cool\"\n assert.equal title, \"cool\"\n\n it \"should apply app template by default on boot and remove on dispose\", ->\n app = BaseApp\n T:\n App: system.ui.Jadelet.exec \"app Hello\"\n menu: \"\"\"\n Hello\n Wat\n \"\"\"\n\n app.trigger 'boot'\n assert document.querySelector 'app'\n app.trigger 'dispose'\n assert !document.querySelector('app')\n" }, "lib/app/template-loader.coffee": { "content": "{crudeRequire} = require \"../pkg/index\"\n\nmodule.exports = (pkg, templates={}) ->\n Object.keys(pkg.distribution).forEach (key) ->\n if key.startsWith 'templates/'\n templateName = key\n .replace(/^templates\\//, \"\")\n .replace(/^([a-z])|[_-]([a-z])/g, (m, a, b) ->\n (a or b).toUpperCase()\n )\n\n try\n templates[templateName] = crudeRequire pkg.distribution[key].content\n catch e\n console.warn e\n\n return templates\n" }, "lib/test/app/template-loader.coffee": { "content": "require \"/setup\"\nTemplateLoader = require \"/lib/app/template-loader\"\n\ndescribe \"template loader\", ->\n it \"should load templates\", ->\n tl = TemplateLoader(PACKAGE)\n\n assert tl.Progress\n" }, "lib/aws/index.coffee": { "content": "###\nInterface to all our AWS madness.\n\n###\n\n{urlSafeSHA256} = require \"../util/index\"\n\nLL = require \"./_lazy\"\n\nmodule.exports =\n Cognito: require \"./cognito\"\n\n # requires that the user has been authorized with Cognito\n # TODO: catch and refresh credentials\n api: LL (path, params={}) ->\n url = new URL \"https://api.whimsy.space/#{path}\"\n url.searchParams.append \"idpjwt\", Object.values(AWS.config.credentials.params.Logins)[0]\n\n if params.body?\n params.body = JSON.stringify params.body\n\n fetch url, params\n\n # Requires that the user has been authorized with Cognito\n cdn: LL (blob) ->\n S3 = new AWS.S3\n params:\n Bucket: \"whimsy-fs\"\n\n S3.config.credentials = AWS.config.credentials\n id = AWS.config.credentials.identityId\n\n queryExisting = (sha) ->\n fetch \"https://whimsy.space/cdn/#{sha}\",\n method: 'HEAD'\n .then (response) ->\n response.status is 200\n\n # Compute urlsafe sha256\n urlSafeSHA256(blob)\n .then (sha) ->\n queryExisting(sha)\n .then (found) ->\n return sha if found\n\n # Post to whimsy-fs/incoming/user-id/sha\n S3.putObject\n Key: \"incoming/#{id}/#{sha}\"\n ContentType: blob.type\n Body: blob\n .promise()\n .then ->\n # Gently poll whimsy.space/cdn/sha\n # reslove when available\n new Promise (resolve, reject) ->\n timeout = 1000\n n = 0\n\n check = ->\n n += 1\n\n if n <= 10\n queryExisting(sha)\n .then (found) ->\n if found\n resolve(sha)\n else\n setTimeout ->\n check()\n , timeout\n else\n reject()\n\n check()\n\n # Open an authenticated websocket connection to the whimsy.space server\n ws: ->\n url = new URL \"wss://ws.whimsy.space/\"\n url.searchParams.append \"idpjwt\", Object.values(AWS.config.credentials.params.Logins)[0]\n\n new WebSocket url\n\n # Resolve with true if lazy loading succeeded\n ready: LL ->\n AWS\n" }, "lib/test/aws/index.coffee": { "content": "require \"/lib/extensions\"\n\n{api, cdn, ready} = require \"/lib/aws/index\"\n\nCognito = require \"/lib/aws/cognito\"\n\n{cognito:config} = PACKAGE.config\ncognito = Cognito(config)\n\nmocha.setup\n globals: ['AWSCognito', 'AmazonCognitoIdentity', 'AWS']\n\n# skipped for test performance, dependence on remote resources\ndescribe.skip \"AWS\", ->\n it \"cdn\", ->\n @timeout 30000\n blob = new Blob [\"heyy234\"], type: \"text/plain\"\n\n cognito.authenticate(\"daniel+test@danielx.net\", \"yo yo yo\")\n .then ->\n cdn blob\n\n it \"api\", ->\n @timeout 5000\n\n cognito.authenticate(\"daniel+test@danielx.net\", \"yo yo yo\")\n .then ->\n api(\"\")\n\n it \"ready\", ->\n ready()\n" }, "lib/util/index.coffee": { "content": "# Load scripts sequentially, prevents failures if there is a dependency\n# order\nloadScripts = (urls) ->\n urls.reduce (p, url) ->\n p.then ->\n # Resolve if present\n if document.querySelector \"script[src=#{JSON.stringify(url)}]\"\n return Promise.resolve()\n\n script = document.createElement \"script\"\n script.src = url\n document.body.appendChild script\n\n return new Promise (resolve, reject) ->\n script.onload = resolve\n script.onerror = reject\n , Promise.resolve()\n\n\n###\nCopy a string to user's OS (win,mac,linux) clipboard.\n###\ncopyToClipboard = (str) ->\n el = document.createElement 'textarea'\n el.value = str\n el.setAttribute 'readonly', ''\n el.style.position = 'absolute' \n el.style.left = '-9999px'\n document.body.appendChild el\n\n el.select()\n document.execCommand('copy')\n\n document.body.removeChild(el)\n\nbufferToBase64 = (buffer) ->\n window.btoa String.fromCharCode.apply null, new Uint8Array buffer\n\nbase64URLEncode = (base64String) ->\n base64String.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/\\=/g, \"\")\n\ndigest = (data) ->\n crypto.subtle.digest(\"SHA-256\", data)\n\nescapeRegex = (string) ->\n string.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&')\n\n# Match is an array of regex match results\n# unmatched runs of characters followed by single character matches, alternating\n# [\"\", \"a\", \" cool \", \"d\", \"og\"]\n# returns a score, higher is better\nscoreMatch = (match) ->\n return unless match\n\n # -1 so that if first char is matched it gets consecutive bonus\n lastMatch = -1\n pos = 0\n match.slice(1).reduce (score, s, i) ->\n if s.length is 0\n value = 0\n else\n # Unmatched run\n if i % 2 is 0\n value = -s.length\n else # Matched char\n # Consecutive\n if pos is 1 + lastMatch\n lastMatch = pos\n value = 8\n else\n value = 2\n\n # console.log \"Score: #{score}, s: '#{s}', v: #{value}\"\n pos += s.length\n return score + value\n , 0\n\nmodule.exports =\n copyToClipboard: copyToClipboard\n deprecationWarning: (msg, fn) ->\n if typeof msg is \"function\"\n fn = msg\n msg = \"DEPRECATED\"\n\n ->\n console.warn msg\n fn.apply(this, arguments)\n\n escapeRegex: escapeRegex\n\n fuzzyMatch: (term, items, asString=String) ->\n re = RegExp(\"^\" +term.split(\"\").map (c) ->\n \"([^#{escapeRegex c}]*)(#{escapeRegex c})\"\n .join('') + '(.*)$', \"i\")\n\n items.map (item) ->\n if match = asString(item).match re\n [item, scoreMatch match]\n .filter (result) -> result?\n .sort (a, b) ->\n b[1] - a[1]\n .map (result) ->\n result[0]\n\n groupBy: (array, fn) ->\n array.reduce (result, item) ->\n (result[fn(item)] ?= []).push item\n\n return result\n , {}\n\n loadScripts: loadScripts\n\n # Takes an array of urls, returns a decorator that checks the deps have resolved\n # before invoking the given function\n lazyLoader: (urls) ->\n # Load the dependencies keeping a promise to limit to only one request\n # clearing the limit on failure\n # caching on success\n loadingDeps = null\n _load = ->\n if loadingDeps\n return loadingDeps\n\n loadingDeps = loadScripts(urls).catch (e) ->\n console.error e\n loadingDeps = null\n throw e\n\n # Decorator to ensure initialized\n return (fn) ->\n (args...) ->\n context = this\n _load().then ->\n fn.apply context, args\n\n Postmaster: require \"../postmaster\"\n\n # Limit promise requests with the same key to only one in flight\n promiseChoke: (fn) ->\n cache = {}\n \n (key) ->\n cached = cache[key]\n if cached\n return cached\n \n cache[key] = fn(key).finally ->\n delete cache[key]\n\n throttle: (wait, func) ->\n context = args = result = undefined\n timeout = null\n previous = 0\n\n later = ->\n previous = Date.now()\n timeout = null\n result = func.apply(context, args)\n if !timeout\n context = args = null\n\n return ->\n now = Date.now()\n remaining = wait - (now - previous)\n context = this\n args = arguments\n if remaining <= 0 || remaining > wait\n if timeout\n clearTimeout(timeout)\n timeout = null\n\n previous = now\n result = func.apply(context, args)\n if (!timeout)\n context = args = null\n else if !timeout\n timeout = setTimeout(later, remaining)\n\n return result\n\n urlSafeSHA256: (blob) ->\n blob.arrayBuffer()\n .then digest\n .then bufferToBase64\n .then base64URLEncode\n" }, "lib/test/util/index.coffee": { "content": "require \"/lib/extensions\"\n{\n copyToClipboard\n deprecationWarning\n fuzzyMatch\n groupBy\n lazyLoader\n promiseChoke\n throttle\n urlSafeSHA256\n} = require \"/lib/util/index\"\n\ndescribe \"util\", ->\n describe \"copyToClipboard\", ->\n it \"should copy a string to system clipboard\", ->\n copyToClipboard \"yolo\"\n\n describe \"deprecationWarning\", ->\n it \"should display an error when calling a deprecated function\", ->\n yolo = (x, y) ->\n x + y\n\n yolo = deprecationWarning \"util.yolo is deprecated use based.swag instead\", yolo\n\n # Mock console\n do (warn=console.warn) ->\n called = false\n console.warn = (msg) ->\n called = true\n assert.equal msg, \"util.yolo is deprecated use based.swag instead\"\n assert.equal yolo(5, 3), 8\n console.warn = warn\n assert called\n\n describe \"fuzzyMatch\", ->\n it \"should match fuzzily\", ->\n assert fuzzyMatch \"\", [\"\"]\n assert fuzzyMatch \"\", [\"a\"]\n assert fuzzyMatch \"a\", [\"a\"]\n\n result = fuzzyMatch(\"acd\", [\n \"a gcac\"\n \"a cool dog\"\n \"achieved\"\n \"yoro\"\n \"what act duder\"\n ])\n\n assert.equal result.length, 3\n\n\n describe \"groupBy\", ->\n it \"should group arrays by fn\", ->\n a = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n result = groupBy a, (n) ->\n n % 3\n\n assert.deepEqual result[0], [3, 6, 9]\n assert.deepEqual result[1], [1, 4, 7]\n assert.deepEqual result[2], [2, 5, 8]\n\n describe \"lazyLoader\", ->\n it \"should lazy load\", ->\n LL = lazyLoader([])\n \n a = (x) -> x\n b = LL a\n \n b(0).then (x) ->\n assert.equal x, 0\n\n describe \"promiseChoke\", ->\n it \"should limit promise returning function execution to one at a time\", ->\n called = 0\n fn = promiseChoke ->\n called++\n new Promise (resolve) ->\n setTimeout resolve\n\n fn()\n fn()\n fn().then ->\n assert.equal called, 1\n\n it \"should reset on error\", ->\n called = 0\n fn = promiseChoke ->\n called++\n new Promise (resolve, reject) ->\n setTimeout reject\n\n fn()\n fn()\n fn().catch ->\n assert.equal called, 1\n fn().catch ->\n assert.equal called, 2\n\n it \"should key off of first argument\", ->\n called = 0\n fn = promiseChoke (x) ->\n called++\n new Promise (resolve) ->\n setTimeout ->\n resolve x\n\n fn(1)\n fn(2)\n fn(5).then (v) ->\n assert.equal v, 5\n assert.equal called, 3\n\n describe \"throttle\", ->\n it \"should be called no more than once per time block\", (done) ->\n\n called = 0\n\n f = throttle 15, -> called += 1\n\n f()\n f()\n f()\n\n setTimeout ->\n f()\n\n setTimeout ->\n f()\n , 5\n\n setTimeout ->\n f()\n , 10\n\n setTimeout ->\n if called != 2\n done new Error \"Should have been called twice\"\n else\n done()\n , 20\n\n describe \"urlSafeSHA256\", ->\n it \"should create a URL safe SHA256 for the blob\", ->\n urlSafeSHA256(new Blob [\"yolo\"])\n .then (str) ->\n assert.equal str, \"MR_j_u0Wuc2N8PixUXvly4YEhwffSIm6jcN9TWiGbQI\"\n" }, "lib/pkg/index.coffee": { "content": "###\n`pkg` holds utilities for bundling and launching packages as standalone blobs or\nin iframes.\n\n###\n\nhtmlToBlob = (htmlString) ->\n new Blob [htmlString], type: \"text/html; charset=utf-8\"\n\nmetaTag = (name, content) ->\n \"\"\n\nlinkTag = (rel, href) ->\n \"\"\n\n{lazyLoader} = require \"../util/index\"\n\nuglifyLoaded = lazyLoader [\"https://danielx.net/cdn/uglify/3.0.0.min.js\"]\n\n###\nConstruct an HTML file for the package.\n\nThe default behavior is to load the package's entry point but\nthat can be modified enter from any file in the package.\n\nIt also adds remote dependencies to the HTML head and wraps with\nthe system launch if present.\n\nThis is designed to be simple and general, any magic binding should happen in\nthe `!system` layer. Here we are only concerned with html tags and setting up\nthe package with `require`.\n\n`opts`\nadditionalDependencies: array of additional dependency scripts to include in\nthe html source. Useful for things like testing libraries, doc formatting, etc.\ncode: The code snippet to run, defaults to requiring the default package\nentry point.\nstylesheets: array of urls to add as stylesheet link tags\nsystemConfig: configuration parameters for the system runtime.\n###\nhtmlForPackage = (pkg, opts={}) ->\n metas = [\n ''\n ''\n ]\n\n {config, progenitor} = pkg\n config ?= {}\n\n {code, systemConfig} = opts\n\n # by default launch from the packages entry point or main file.\n code ?= \"\"\"\n require('./#{pkg.entryPoint or \"main\"}');\n \"\"\"\n code = systemWrap(pkg, code, systemConfig)\n\n {title, description, lang, iconURL, manifest} = config\n\n if lang\n langFragment = \" lang=#{JSON.stringify(lang)}\"\n else\n langFragment = \"\"\n\n if title\n metas.push \"#{title}\"\n\n if description\n metas.push metaTag \"description\", description.replace(\"\\n\", \" \")\n\n if iconURL\n metas.push linkTag \"shortcut icon\", iconURL\n\n if manifest\n metas.push linkTag \"manifest\", \"./manifest.webmanifest\"\n\n # Progenitor link can be used to for a built-in \"Edit this!\" feature\n # TODO: Should url be href?\n url = pkg.progenitor?.url\n if url\n metas.push linkTag \"progenitor\", url\n\n (pkg.stylesheets || []).concat(opts.stylesheets || []).forEach (href) ->\n metas.push linkTag \"stylesheet\", href\n\n htmlToBlob \"\"\"\n \n \n \n #{metas.join(\"\\n \")}\n #{dependencyScripts(opts.additionalDependencies, pkg.remoteDependencies)}\n \n \n