From f0b6b34d09830501f849c80e11a35ac3c7be5f03 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 31 Dec 2016 09:06:13 -0800 Subject: [PATCH 01/78] This commit intentionally left blank From 1bf77e027edd18bf8f93f0a0e1e4e38f76b22fc5 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 31 Dec 2016 11:07:41 -0800 Subject: [PATCH 02/78] :goberserk::u5272: Updated at https://danielx.net/editor/ --- TODO.md | 24 +++++++++++ apps/spreadsheet.coffee | 45 ++++++++++---------- issues/{2016-12-10.coffee => 2016-12.coffee} | 1 - issues/2017-01.coffee | 4 ++ main.coffee | 2 +- os.coffee | 16 ------- 6 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 TODO.md rename issues/{2016-12-10.coffee => 2016-12.coffee} (98%) create mode 100644 issues/2017-01.coffee diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..1f94eea --- /dev/null +++ b/TODO.md @@ -0,0 +1,24 @@ +TODO +==== + +System Features +--------------- + +Folders + +Desktop Icons + +Desktop Background + +Help Documentation + +Applications +------------ + +Spreadsheet +- Save/Load Data +- Custom Cell Views +- Data Relationships +- Forms + +Tactics Game Sandbox diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index 225592d..19a9f28 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -1,42 +1,43 @@ module.exports = (os) -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = os.UI - # Observable input helper - o = (value, type) -> - attribute = Observable(value) - if type - attribute.type = type + sourceData = [0...5].map (i) -> + id: i + name: "yolo" + color: "#FF0000" - attribute.value = attribute - - return attribute - - data = Observable [0...5].map (i) -> - id: o i - name: o "yolo" - color: o "#FF0000", "color" - - {element} = Table data + {element} = Table { + data + RowElement: SampleRow + } handlers = Model().include(FileIO).extend loadFile: (blob) -> blob.readAsJSON() .then (json) -> - # TODO: Load json array to data console.log json + + unless Array.isArray json + throw new Error "Data must be an array" + + sourceData = json + # TODO: Re-render + newFile: -> # TODO saveData: -> - # TODO: Make sure to get data the right way - Promise.resolve new Blob [JSON.stringify(data())], + Promise.resolve new Blob [JSON.stringify(sourceData)], type: "application/json" about: -> Modal.alert "Spreadsheet v0.0.1 by Daniel X Moore" insertRow: -> - data.push - id: o 50 - name: o "new" - color: o "#FF00FF", "color" + # TODO: Data template + sourceData.push + id: 0 + name: "new" + color: "#FF00FF" + + # TODO: Re-render exit: -> windowView.element.remove() diff --git a/issues/2016-12-10.coffee b/issues/2016-12.coffee similarity index 98% rename from issues/2016-12-10.coffee rename to issues/2016-12.coffee index d970896..9722da5 100644 --- a/issues/2016-12-10.coffee +++ b/issues/2016-12.coffee @@ -1,5 +1,4 @@ Notepad = require "../apps/notepad" -Spreadsheet = require "../apps/spreadsheet" CommentFormTemplate = require "../social/comment-form" CommentsTemplate = require "../social/comments" diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee new file mode 100644 index 0000000..ddaa5ea --- /dev/null +++ b/issues/2017-01.coffee @@ -0,0 +1,4 @@ +Spreadsheet = require "../apps/spreadsheet" + +module.exports = -> + \ No newline at end of file diff --git a/main.coffee b/main.coffee index cf1aadb..9edfa23 100644 --- a/main.coffee +++ b/main.coffee @@ -8,4 +8,4 @@ style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style -require("./issues/2016-12-10")(os) +require("./issues/2016-12")(os) diff --git a/os.coffee b/os.coffee index 776e2a6..a5e94c4 100644 --- a/os.coffee +++ b/os.coffee @@ -37,14 +37,6 @@ DexieFS = (db) -> uniq = (array) -> Array.from new Set array -readAsText = (file) -> - new Promise (resolve, reject) -> - reader = new FileReader - reader.onload = -> - resolve reader.result - reader.onerror = reject - reader.readAsText(file) - UI = require "ui" module.exports = (dbName='zine-os') -> @@ -60,14 +52,6 @@ module.exports = (dbName='zine-os') -> .then ({blob}) -> blob - readAsText: (path) -> - self.readFile(path) - .then readAsText - - readAsJSON: (path) -> - self.readAsText(path) - .then JSON.parse - writeFile: fs.write UI: UI From 9704135c6e12d27d08e9de1419dcc03cff3db7a8 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 31 Dec 2016 11:32:41 -0800 Subject: [PATCH 03/78] This commit intentionally left blank From 1b35913a767a3b5236cc4abcfdb0834fbccb9259 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 31 Dec 2016 11:44:04 -0800 Subject: [PATCH 04/78] :womans_clothes::kissing_closed_eyes: Updated at https://danielx.net/editor/ --- apps/spreadsheet.coffee | 39 ++++++++++++++++++++++++++++---- issues/2016-12.coffee | 2 +- issues/2017-01.coffee | 35 ++++++++++++++++++++++++++++- mailchimp.coffee | 2 +- main.coffee | 2 +- os/file-io.coffee | 2 +- social/social.coffee | 49 +++++++++++++++++++++++++++++++++++++++++ templates/input.jadelet | 1 + 8 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 social/social.coffee create mode 100644 templates/input.jadelet diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index 19a9f28..f4d4b36 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -1,14 +1,45 @@ -module.exports = (os) -> - {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = os.UI +FileIO = require "../os/file-io" +Model = require "model" + +module.exports = () -> + {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI sourceData = [0...5].map (i) -> id: i name: "yolo" color: "#FF0000" + headers = ["id", "name", "color"] + + models = sourceData.map (datum) -> + Model(datum).attrObservable headers... + + InputTemplate = require "../templates/input" + RowElement = (datum) -> + tr = document.createElement "tr" + types = [ + "number" + "text" + "color" + ] + + console.log datum + + headers.forEach (key, i) -> + td = document.createElement "td" + td.appendChild InputTemplate + value: datum[key] + type: types[i] + + tr.appendChild td + + return tr + + {element} = Table { - data - RowElement: SampleRow + data: models + RowElement: RowElement + headers: headers } handlers = Model().include(FileIO).extend diff --git a/issues/2016-12.coffee b/issues/2016-12.coffee index 9722da5..b5c1944 100644 --- a/issues/2016-12.coffee +++ b/issues/2016-12.coffee @@ -13,7 +13,7 @@ module.exports = (os) -> img = document.createElement "img" img.src = "https://68.media.tumblr.com/6a141d69564a29ac7d4071df5d519808/tumblr_o0rbb4TA1k1urr1ryo1_500.gif" - handlers = + handlers = waitAroundForABit: -> initialMessage = "Waiting" progressView = Progress diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index ddaa5ea..98a23d1 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -1,4 +1,37 @@ +Model = require "model" Spreadsheet = require "../apps/spreadsheet" +Social = require "../social/social" + module.exports = -> - \ No newline at end of file + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + + img = document.createElement "img" + img.src = "https://s-media-cache-ak0.pinimg.com/originals/a3/ba/56/a3ba56cef667d14b54023cd624d4e070.jpg" + + handlers = Model().include(Social).extend + area: -> + "2017-01" + mSAccess97: -> + app = Spreadsheet(system) + document.body.appendChild app.element + + menuBar = MenuBar + items: parseMenu """ + [H]ello + [W]ait Around For A Bit + [A]pps + [M]S Access 97 + #{Social.menuText} + """ + handlers: handlers + + windowView = Window + title: "ZineOS Volume 1 | Issue 2 | January 2017" + content: img + menuBar: menuBar.element + width: 1228 + height: 936 + x: 0 + y: 0 + document.body.appendChild windowView.element diff --git a/mailchimp.coffee b/mailchimp.coffee index c988b47..da0e429 100644 --- a/mailchimp.coffee +++ b/mailchimp.coffee @@ -13,7 +13,7 @@ module.exports =
- +
diff --git a/main.coffee b/main.coffee index 9edfa23..23ae323 100644 --- a/main.coffee +++ b/main.coffee @@ -8,4 +8,4 @@ style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style -require("./issues/2016-12")(os) +require("./issues/2017-01")(os) diff --git a/os/file-io.coffee b/os/file-io.coffee index 71dfd12..82ebb67 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -2,7 +2,7 @@ # Host must provide the following methods # `loadFile` Take a blob and load it as the application state. -# `saveData` Return a promise that will be fulfilled with a blob of the +# `saveData` Return a promise that will be fulfilled with a blob of the # current application state. # `newFile` Initialize the application to an empty state. diff --git a/social/social.coffee b/social/social.coffee new file mode 100644 index 0000000..4295d8e --- /dev/null +++ b/social/social.coffee @@ -0,0 +1,49 @@ +CommentFormTemplate = require "../social/comment-form" +CommentsTemplate = require "../social/comments" + +Ajax = require "ajax" +ajax = Ajax() + +# Includer must provide self.area() method that dictates what the comments attach to +module.exports = (I, self) -> + {Modal} = system.UI + + self.extend + comment: -> + Modal.form CommentFormTemplate + area: self.area() + .then (data) -> + ajax + url: "https://whimsy-space.gomix.me/comments" + data: JSON.stringify(data) + headers: + "Content-Type": "application/json" + method: "POST" + .then -> + self.viewComments() + + viewComments: -> + ajax.getJSON "https://whimsy-space.gomix.me/comments/#{self.area()}" + .then (data) -> + data = data.reverse() + + if data.length is 0 + data = [{ + body: "no comments" + author: "mgmt" + }] + + Modal.show CommentsTemplate data + + like: -> + Modal.alert "I like you too, but we don't have a facebook or anything yet :)" + subscribe: -> + require("../mailchimp").show() + +module.exports.menuText = """ +S[o]cial Media + [V]iew Comments + [C]omment + [L]ike + [S]ubscribe +""" diff --git a/templates/input.jadelet b/templates/input.jadelet new file mode 100644 index 0000000..1c82f60 --- /dev/null +++ b/templates/input.jadelet @@ -0,0 +1 @@ +input(@value @type @min @max @checked) From fe4293d9d88097fc21dd52ade929b18592a85283 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 31 Dec 2016 11:48:57 -0800 Subject: [PATCH 05/78] :point_down::icecream: Updated at https://danielx.net/editor/ --- apps/spreadsheet.coffee | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index f4d4b36..bb44b9d 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -11,9 +11,11 @@ module.exports = () -> headers = ["id", "name", "color"] - models = sourceData.map (datum) -> + RowModel = (datum) -> Model(datum).attrObservable headers... + models = sourceData.map RowModel + InputTemplate = require "../templates/input" RowElement = (datum) -> tr = document.createElement "tr" @@ -22,8 +24,6 @@ module.exports = () -> "text" "color" ] - - console.log datum headers.forEach (key, i) -> td = document.createElement "td" @@ -36,7 +36,7 @@ module.exports = () -> return tr - {element} = Table { + {element} = tableView = Table { data: models RowElement: RowElement headers: headers @@ -52,7 +52,9 @@ module.exports = () -> throw new Error "Data must be an array" sourceData = json - # TODO: Re-render + # TODO: Update models data + # Re-render + tableView.render() newFile: -> # TODO saveData: -> @@ -63,12 +65,16 @@ module.exports = () -> Modal.alert "Spreadsheet v0.0.1 by Daniel X Moore" insertRow: -> # TODO: Data template - sourceData.push + datum = id: 0 name: "new" color: "#FF00FF" - # TODO: Re-render + sourceData.push datum + models.push RowModel(datum) + + # Re-render + tableView.render() exit: -> windowView.element.remove() From 60b5ceef5cc1122ee9ee371cb8f5ccfa48861a96 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 31 Dec 2016 11:51:05 -0800 Subject: [PATCH 06/78] :dress::eight_spoked_asterisk: Updated at https://danielx.net/editor/ From 40a455ffe64c01b1dff4d4061409863bf56d8f6c Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 1 Jan 2017 16:24:25 -0800 Subject: [PATCH 07/78] :sunny::point_right: Updated at https://danielx.net/editor/ --- apps/spreadsheet.coffee | 4 +- apps/text-editor.coffee | 82 +++++++++++++++++++++++++++++++++++++++++ issues/2017-01.coffee | 5 +++ pixie.cson | 3 ++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 apps/text-editor.coffee diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index bb44b9d..3826080 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -27,7 +27,7 @@ module.exports = () -> headers.forEach (key, i) -> td = document.createElement "td" - td.appendChild InputTemplate + td.appendChild InputTemplate value: datum[key] type: types[i] @@ -65,7 +65,7 @@ module.exports = () -> Modal.alert "Spreadsheet v0.0.1 by Daniel X Moore" insertRow: -> # TODO: Data template - datum = + datum = id: 0 name: "new" color: "#FF00FF" diff --git a/apps/text-editor.coffee b/apps/text-editor.coffee new file mode 100644 index 0000000..84a4820 --- /dev/null +++ b/apps/text-editor.coffee @@ -0,0 +1,82 @@ +Model = require "model" +FileIO = require "../os/file-io" + +ace.require("ace/ext/language_tools") + +extraModes = + jadelet: "jade" + +mode = (mode) -> + extraModes[mode] or mode + +module.exports = -> + {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI + + aceWrap = document.createElement "div" + aceWrap.style.width = aceWrap.style.height = "100%" + + aceElement = document.createElement "div" + aceElement.style.width = aceElement.style.height = "100%" + + aceWrap.appendChild aceElement + + aceEditor = ace.edit aceElement + aceEditor.$blockScrolling = Infinity + aceEditor.setOptions + fontSize: "16px" + enableBasicAutocompletion: true + enableLiveAutocompletion: true + highlightActiveLine: true + + global.aceEditor = aceEditor + + initSession: (file) -> + session = ace.createEditSession(file.content()) + + session.setMode("ace/mode/#{mode file.mode()}") + + session.setUseSoftTabs true + session.setTabSize 2 + + return session + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + blob.readAsText() + .then (text) -> + textarea.value = text + newFile: -> + textarea.value = "" + saveData: -> + data = new Blob [textarea.value], + type: "text/plain" + + return Promise.resolve data + + menuBar = MenuBar + items: parseMenu """ + [F]ile + [N]ew + [O]pen + [S]ave + Save [A]s + - + E[x]it + [H]elp + View [H]elp + - + [A]bout + """ + handlers: handlers + + windowView = Window + title: "Notepad.exe" + content: aceWrap + menuBar: menuBar.element + width: 640 + height: 480 + + windowView.on "resize", -> + aceEditor.resize() + + return windowView diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 98a23d1..d50bf41 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -1,5 +1,6 @@ Model = require "model" Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" Social = require "../social/social" @@ -15,6 +16,9 @@ module.exports = -> mSAccess97: -> app = Spreadsheet(system) document.body.appendChild app.element + textEditor: -> + app = TextEditor(system) + document.body.appendChild app.element menuBar = MenuBar items: parseMenu """ @@ -22,6 +26,7 @@ module.exports = -> [W]ait Around For A Bit [A]pps [M]S Access 97 + [T]ext Editor #{Social.menuText} """ handlers: handlers diff --git a/pixie.cson b/pixie.cson index 9bb06a5..be46b68 100644 --- a/pixie.cson +++ b/pixie.cson @@ -4,4 +4,7 @@ dependencies: ui: "STRd6/ui:master" remoteDependencies: [ "https://unpkg.com/dexie@2.0.0-beta.7/dist/dexie.js" + "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.4/ace.js" + "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.4/ext-language_tools.js" + "https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js" ] From a1bfd1e057fffa7cab8bfb16065ca048159a6ccd Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 1 Jan 2017 20:53:56 -0800 Subject: [PATCH 08/78] :nut_and_bolt::dress: Updated at https://danielx.net/editor/ --- apps/pixel.coffee | 48 +++++++++++++++++++++++++++++++++++++++++ apps/text-editor.coffee | 36 +++++++++++++++++-------------- issues/2017-01.coffee | 8 ++++++- os.coffee | 8 +++++++ os/file-io.coffee | 2 ++ pixie.cson | 5 +++-- 6 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 apps/pixel.coffee diff --git a/apps/pixel.coffee b/apps/pixel.coffee new file mode 100644 index 0000000..6abd598 --- /dev/null +++ b/apps/pixel.coffee @@ -0,0 +1,48 @@ +Model = require "model" +Postmaster = require "postmaster" +FileIO = require "../os/file-io" + +module.exports = -> + {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI + + frame = document.createElement "iframe" + frame.src = "https://danielx.net/pixel-editor/" + + postmaster = Postmaster() + postmaster.remoteTarget = -> frame.contentWindow + Object.assign postmaster, + childLoaded: -> + console.log "child loaded" + save: -> + handlers.save() + + handlers = Model().include(FileIO).extend + loadFile: -> + newFile: -> + saveData: -> + postmaster.invokeRemote "getBlob" + + menuBar = MenuBar + items: parseMenu """ + [F]ile + [N]ew + [O]pen + [S]ave + Save [A]s + - + E[x]it + [H]elp + View [H]elp + - + [A]bout + """ + handlers: handlers + + windowView = Window + title: Observable "Pixie Paint" + content: frame + menuBar: menuBar.element + width: 640 + height: 480 + + return windowView diff --git a/apps/text-editor.coffee b/apps/text-editor.coffee index 84a4820..bb435d4 100644 --- a/apps/text-editor.coffee +++ b/apps/text-editor.coffee @@ -27,28 +27,32 @@ module.exports = -> enableBasicAutocompletion: true enableLiveAutocompletion: true highlightActiveLine: true + + session = aceEditor.getSession() + session.setUseSoftTabs true + session.setTabSize 2 - global.aceEditor = aceEditor - - initSession: (file) -> - session = ace.createEditSession(file.content()) + mode = "coffee" + session.setMode("ace/mode/#{mode}") - session.setMode("ace/mode/#{mode file.mode()}") - - session.setUseSoftTabs true - session.setTabSize 2 + global.aceEditor = aceEditor - return session + initSession = (file) -> + # TODO: Update window title + file.readAsText() + .then (content) -> + session.setValue(content) + # TODO: Correct modes + mode = "coffee" + session.setMode("ace/mode/#{mode}") handlers = Model().include(FileIO).extend - loadFile: (blob) -> - blob.readAsText() - .then (text) -> - textarea.value = text + loadFile: initSession newFile: -> - textarea.value = "" + session.setValue "" saveData: -> - data = new Blob [textarea.value], + # TODO: Maintain proper mime type + data = new Blob [session.getValue()], type: "text/plain" return Promise.resolve data @@ -70,7 +74,7 @@ module.exports = -> handlers: handlers windowView = Window - title: "Notepad.exe" + title: Observable "Ace" content: aceWrap menuBar: menuBar.element width: 640 diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index d50bf41..b3b2550 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -1,4 +1,5 @@ Model = require "model" +PixiePaint = require "../apps/pixel" Spreadsheet = require "../apps/spreadsheet" TextEditor = require "../apps/text-editor" @@ -19,6 +20,9 @@ module.exports = -> textEditor: -> app = TextEditor(system) document.body.appendChild app.element + pixiePaint: -> + app = PixiePaint(system) + document.body.appendChild app.element menuBar = MenuBar items: parseMenu """ @@ -27,16 +31,18 @@ module.exports = -> [A]pps [M]S Access 97 [T]ext Editor + [P]ixie Paint #{Social.menuText} """ handlers: handlers windowView = Window - title: "ZineOS Volume 1 | Issue 2 | January 2017" + title: "ZineOS Volume 1 | Issue 2 | ENTER THE DUNGEON | January 2017" content: img menuBar: menuBar.element width: 1228 height: 936 x: 0 y: 0 + document.body.appendChild windowView.element diff --git a/os.coffee b/os.coffee index a5e94c4..24ae7e7 100644 --- a/os.coffee +++ b/os.coffee @@ -54,6 +54,14 @@ module.exports = (dbName='zine-os') -> writeFile: fs.write + # NOTE: These are experimental commands to run code + execJS: (path) -> + self.readFile(path) + .then (file) -> + file.readAsText() + .then (programText) -> + Function(programText)() + UI: UI return self diff --git a/os/file-io.coffee b/os/file-io.coffee index 82ebb67..33b8198 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -37,6 +37,8 @@ module.exports = (I, self) -> self.saveData() .then (blob) -> system.writeFile currentPath, blob + .then -> + currentPath else self.saveAs() diff --git a/pixie.cson b/pixie.cson index be46b68..c7a9401 100644 --- a/pixie.cson +++ b/pixie.cson @@ -1,10 +1,11 @@ dependencies: ajax: "distri/ajax:master" model: "distri/model:master" + postmaster: "distri/postmaster:v0.5.0" ui: "STRd6/ui:master" remoteDependencies: [ "https://unpkg.com/dexie@2.0.0-beta.7/dist/dexie.js" - "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.4/ace.js" - "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.4/ext-language_tools.js" + "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" + "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" "https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js" ] From ed41b40ccc57a92603f1f9db11b3aead7499679a Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 1 Jan 2017 21:53:01 -0800 Subject: [PATCH 09/78] :rage::link: Updated at https://danielx.net/editor/ --- apps/pixel.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pixel.coffee b/apps/pixel.coffee index 6abd598..ab00a71 100644 --- a/apps/pixel.coffee +++ b/apps/pixel.coffee @@ -11,7 +11,7 @@ module.exports = -> postmaster = Postmaster() postmaster.remoteTarget = -> frame.contentWindow Object.assign postmaster, - childLoaded: -> + childLoaded: -> console.log "child loaded" save: -> handlers.save() From 09dba2f213c4d88cbf94c1d1e2737bad6521a533 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 2 Jan 2017 13:45:02 -0800 Subject: [PATCH 10/78] :eyes::raised_hand: Updated at https://danielx.net/editor/ --- TODO.md | 6 ++++ system/module.coffee | 80 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 system/module.coffee diff --git a/TODO.md b/TODO.md index 1f94eea..071462e 100644 --- a/TODO.md +++ b/TODO.md @@ -12,6 +12,12 @@ Desktop Background Help Documentation +Compiler + +Require/Include Local Files + +Custom Models/Views + Applications ------------ diff --git a/system/module.coffee b/system/module.coffee new file mode 100644 index 0000000..8404460 --- /dev/null +++ b/system/module.coffee @@ -0,0 +1,80 @@ +module.exports = (I, self) -> + + modules = {} + + ### + Load a module from a file in the file system. + + This external context provides some variable that modules have access to. + + A `require` function is exposed to modules so they may require other modules. + + Additional properties such as a reference to the global object and some metadata + are also exposed. + ### + fileSeparator = "/" + + # TODO: This will become a promise returning function + loadModule = (content, path) -> + program = annotateSourceURL content, path + dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) + + # TODO: Need to use a defineProperty setter on + # module.exports to trigger when the module successfully exports because + # it can all be async madness. + + # May need to scan for a module.exports to see if it is the kind of + # module that exports things vs just plain side effects code + module = + path: dirname + exports: {} + + # TODO: Apply relative path wrapper for system.include + + context = + system: self + global: global + module: module + exports: module.exports + PACKAGE: pkg + __filename: path + __dirname: dirname + + args = Object.keys(context) + values = args.map (name) -> context[name] + + Function(args..., program).apply(module, values) + + return module + + Object.assign self, + # still experimenting with the API + # Async include in the vein of require.js + # it's horrible but seems necessary + include: (moduleIdentifiers...) -> + Promise.all moduleIdentifiers.map (identifier) -> + # Only allow absolute and system modules, relative modules can be handled + # by wrapping this when exposing system to scripts + + # If a file, read and execute the file + # If a system module, return the module + # TODO: Make sure we handle normalizing paths correctly + if identifier.indexOf("/") is 0 + self.readFile(identifier) + .then (file) -> + file.readAsText() + .then (sourceProgram) -> + # TODO: Apply wrapper and eval + else + module = modules[identifier] + + if module + Promise.resolve module + else + Promise.reject new Error "System module not found: #{module}" + +annotateSourceURL = (program, path) -> + """ + #{program} + //# sourceURL=#{path} + """ From fe7aa2d8948289db69f4ac3a81216d784b61f459 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 2 Jan 2017 20:36:22 -0800 Subject: [PATCH 11/78] :doughnut::card_index: Updated at https://danielx.net/editor/ --- system/module.coffee | 81 +++++++++++++++++++++------------------ test/system/module.coffee | 18 +++++++++ 2 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 test/system/module.coffee diff --git a/system/module.coffee b/system/module.coffee index 8404460..2d7cbfb 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -1,51 +1,56 @@ module.exports = (I, self) -> + # System modules table modules = {} ### Load a module from a file in the file system. - This external context provides some variable that modules have access to. - - A `require` function is exposed to modules so they may require other modules. - Additional properties such as a reference to the global object and some metadata - are also exposed. + are exposed. + + Returns a promise that is fulfilled when the module assigns its exports, or + rejected on error. ### fileSeparator = "/" - # TODO: This will become a promise returning function loadModule = (content, path) -> - program = annotateSourceURL content, path - dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) - - # TODO: Need to use a defineProperty setter on - # module.exports to trigger when the module successfully exports because - # it can all be async madness. - - # May need to scan for a module.exports to see if it is the kind of - # module that exports things vs just plain side effects code - module = - path: dirname - exports: {} - - # TODO: Apply relative path wrapper for system.include - - context = - system: self - global: global - module: module - exports: module.exports - PACKAGE: pkg - __filename: path - __dirname: dirname - - args = Object.keys(context) - values = args.map (name) -> context[name] - - Function(args..., program).apply(module, values) - - return module + new Promise (resolve, reject) -> + program = annotateSourceURL content, path + dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) + + # May need to scan for a module.exports to see if it is the kind of + # module that exports things vs just plain side effects code + module = + path: dirname + + # Use a defineProperty setter on module.exports to trigger when the module + # successfully exports because it can all be async madness. + exports = {} + Object.defineProperty module, "exports", + get: -> + exports + set: (newValue) -> + exports = newValue + # Trigger complete + resolve(module) + + # TODO: Apply relative path wrapper for system.include + context = + system: self + global: global + module: module + exports: module.exports + __filename: path + __dirname: dirname + + args = Object.keys(context) + values = args.map (name) -> context[name] + + try + Function(args..., program).apply(module, values) + catch e + reject e Object.assign self, # still experimenting with the API @@ -64,7 +69,9 @@ module.exports = (I, self) -> .then (file) -> file.readAsText() .then (sourceProgram) -> - # TODO: Apply wrapper and eval + loadModule sourceProgram, identifier + .then (module) -> + module.exports else module = modules[identifier] diff --git a/test/system/module.coffee b/test/system/module.coffee new file mode 100644 index 0000000..3cd64c0 --- /dev/null +++ b/test/system/module.coffee @@ -0,0 +1,18 @@ +require "../../extensions" +Model = require "model" +SystemModule = require "../../system/module" + +describe "System Module", -> + it "should include modules in files async", -> + model = Model() + + model.include SystemModule + + model.readFile = -> + Promise.resolve new Blob ["module.exports = 'yo';"] + + assert model.include + + model.include "/test.js" + .then (module) -> + assert.equal module, 'yo' From 8ba1b37f02a07db4c505fd491d5b032706529fac Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 3 Jan 2017 16:18:40 -0800 Subject: [PATCH 12/78] :octocat::green_apple: Updated at https://danielx.net/editor/ --- system/module.coffee | 84 +++++++++++++++++++++++++-------------- test/system/module.coffee | 31 ++++++++++++--- 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/system/module.coffee b/system/module.coffee index 2d7cbfb..e0b2f4b 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -1,8 +1,4 @@ module.exports = (I, self) -> - - # System modules table - modules = {} - ### Load a module from a file in the file system. @@ -14,10 +10,47 @@ module.exports = (I, self) -> ### fileSeparator = "/" + normalizePath = (path) -> + path.replace(/\/\/+/, fileSeparator) # /// -> / + .replace(/\/[^/]*\/\.\./g, "") # /base/something/.. -> /base + .replace(/\/\.\//g, fileSeparator) # /base/. -> /base + + # Wrap program in async include wrapper + rewriteRequires = (program) -> + id = 0 + namePrefix = "__req" + requires = {} + + # rewrite requires + rewrittenProgram = program.replace /require\(['"]([^'"]+)['"]\)/g, (match, key) -> + if requires[key] + tmpVar = requires[key] + else + tmpVar = "#{namePrefix}#{id}" + id += 1 + requires[key] = tmpVar + + return tmpVar + + tmpVars = Object.keys(requires).map (name) -> + requires[name] + + requirePaths = Object.keys(requires) + requirePaths = requirePaths + + """ + system.include(#{JSON.stringify(requirePaths)}) + .then(function(__reqResults) { + (function(#{tmpVars.join(', ')}){ + #{rewrittenProgram} + }).apply(this, __reqResults); + }); + """ + loadModule = (content, path) -> new Promise (resolve, reject) -> - program = annotateSourceURL content, path - dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) + program = annotateSourceURL(rewriteRequires(content), path) + dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) or fileSeparator # May need to scan for a module.exports to see if it is the kind of # module that exports things vs just plain side effects code @@ -35,9 +68,14 @@ module.exports = (I, self) -> # Trigger complete resolve(module) + localSystem = Object.assign {}, self, + include: (moduleIdentifiers) -> + self.include moduleIdentifiers.map (identifier) -> + normalizePath dirname + identifier + # TODO: Apply relative path wrapper for system.include context = - system: self + system: localSystem global: global module: module exports: module.exports @@ -50,35 +88,23 @@ module.exports = (I, self) -> try Function(args..., program).apply(module, values) catch e + console.error e reject e Object.assign self, # still experimenting with the API # Async include in the vein of require.js # it's horrible but seems necessary - include: (moduleIdentifiers...) -> + include: (moduleIdentifiers) -> Promise.all moduleIdentifiers.map (identifier) -> - # Only allow absolute and system modules, relative modules can be handled - # by wrapping this when exposing system to scripts - - # If a file, read and execute the file - # If a system module, return the module - # TODO: Make sure we handle normalizing paths correctly - if identifier.indexOf("/") is 0 - self.readFile(identifier) - .then (file) -> - file.readAsText() - .then (sourceProgram) -> - loadModule sourceProgram, identifier - .then (module) -> - module.exports - else - module = modules[identifier] - - if module - Promise.resolve module - else - Promise.reject new Error "System module not found: #{module}" + # Read and execute the file + self.readFile(identifier) + .then (file) -> + file.readAsText() + .then (sourceProgram) -> + loadModule sourceProgram, identifier + .then (module) -> + module.exports annotateSourceURL = (program, path) -> """ diff --git a/test/system/module.coffee b/test/system/module.coffee index 3cd64c0..e0ad846 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -8,11 +8,30 @@ describe "System Module", -> model.include SystemModule - model.readFile = -> - Promise.resolve new Blob ["module.exports = 'yo';"] + files = + "/test.js": """ + console.log('in test.js'); + module.exports = 'yo'; + """ + "/root.js": """ + console.log('in root.js', require); + var test = require('./test.js'); + var test2 = require("./folder/nested.js"); + module.exports = test + " 2 rad " + test2; + """ + "/folder/nested.js": """ + module.exports = "hella"; + """ + "/wat.js": """ + module.exports = "wat"; + """ - assert model.include + model.readFile = (path) -> + content = files[path] - model.include "/test.js" - .then (module) -> - assert.equal module, 'yo' + Promise.resolve new Blob [content] + + model.include(["/root.js", "/wat.js"]) + .then ([root, wat]) -> + console.log root, wat + assert.equal root, 'yo 2 rad hella' From 065ea713d2732105396bb6271e00889179e2c32d Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 3 Jan 2017 17:22:50 -0800 Subject: [PATCH 13/78] :fireworks::house_with_garden: Updated at https://danielx.net/editor/ --- system/module.coffee | 25 ++++++++++++++++++------- test/system/module.coffee | 10 +++++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/system/module.coffee b/system/module.coffee index e0b2f4b..64eae26 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -7,6 +7,10 @@ module.exports = (I, self) -> Returns a promise that is fulfilled when the module assigns its exports, or rejected on error. + + Caches modules so mutual includes don't get re-run + + Circular includes will never reslove ### fileSeparator = "/" @@ -47,7 +51,7 @@ module.exports = (I, self) -> }); """ - loadModule = (content, path) -> + loadModule = (content, path, state) -> new Promise (resolve, reject) -> program = annotateSourceURL(rewriteRequires(content), path) dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) or fileSeparator @@ -68,12 +72,16 @@ module.exports = (I, self) -> # Trigger complete resolve(module) + # Apply relative path wrapper for system.include localSystem = Object.assign {}, self, include: (moduleIdentifiers) -> - self.include moduleIdentifiers.map (identifier) -> + relativeIdentifiers = moduleIdentifiers.map (identifier) -> + # TODO: Allow absolute paths? normalizePath dirname + identifier - # TODO: Apply relative path wrapper for system.include + self.include relativeIdentifiers, state + # TODO: Also make working directory relative paths for readFile and writeFile + context = system: localSystem global: global @@ -95,14 +103,17 @@ module.exports = (I, self) -> # still experimenting with the API # Async include in the vein of require.js # it's horrible but seems necessary - include: (moduleIdentifiers) -> + + # This is an internal API and isn't recommended for general use + include: (moduleIdentifiers, state={}) -> + state.cache ?= {} + Promise.all moduleIdentifiers.map (identifier) -> - # Read and execute the file - self.readFile(identifier) + state.cache[identifier] ?= self.readFile(identifier) .then (file) -> file.readAsText() .then (sourceProgram) -> - loadModule sourceProgram, identifier + loadModule sourceProgram, identifier, state .then (module) -> module.exports diff --git a/test/system/module.coffee b/test/system/module.coffee index e0ad846..ae1b8ce 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -25,13 +25,17 @@ describe "System Module", -> "/wat.js": """ module.exports = "wat"; """ + "/rand.js": """ + module.exports = Math.random(); + """ model.readFile = (path) -> content = files[path] Promise.resolve new Blob [content] - model.include(["/root.js", "/wat.js"]) - .then ([root, wat]) -> - console.log root, wat + model.include(["/root.js", "/wat.js", "/rand.js", "/rand.js"]) + .then ([root, wat, r1, r2]) -> + console.log root, wat, r1, r2 + assert.equal r1, r2 assert.equal root, 'yo 2 rad hella' From 979c92a5f1c66ff9b209fe105095772744cd273a Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 3 Jan 2017 17:30:52 -0800 Subject: [PATCH 14/78] :fried_shrimp::tulip: Updated at https://danielx.net/editor/ --- system/module.coffee | 7 ++++++- test/system/module.coffee | 30 ++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/system/module.coffee b/system/module.coffee index 64eae26..12fff65 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -8,9 +8,12 @@ module.exports = (I, self) -> Returns a promise that is fulfilled when the module assigns its exports, or rejected on error. - Caches modules so mutual includes don't get re-run + Caches modules so mutual includes don't get re-run per include root. Circular includes will never reslove + # TODO: Fail early on circular includes, challenging because of async + + # TODO: Require .coffee/arbitrary files ### fileSeparator = "/" @@ -105,6 +108,8 @@ module.exports = (I, self) -> # it's horrible but seems necessary # This is an internal API and isn't recommended for general use + # The state determines an include root and should is the same for a single + # app or process include: (moduleIdentifiers, state={}) -> state.cache ?= {} diff --git a/test/system/module.coffee b/test/system/module.coffee index ae1b8ce..cef9ec8 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -10,11 +10,9 @@ describe "System Module", -> files = "/test.js": """ - console.log('in test.js'); module.exports = 'yo'; """ "/root.js": """ - console.log('in root.js', require); var test = require('./test.js'); var test2 = require("./folder/nested.js"); module.exports = test + " 2 rad " + test2; @@ -39,3 +37,31 @@ describe "System Module", -> console.log root, wat, r1, r2 assert.equal r1, r2 assert.equal root, 'yo 2 rad hella' + + it "should wait forever when resolving circular requires", (done) -> + model = Model() + + model.include SystemModule + + files = + "/a.js": """ + module.exports = require("./b.js") + """ + "/b.js": """ + module.exports = require("./a.js") + """ + + model.readFile = (path) -> + content = files[path] + + Promise.resolve new Blob [content] + + model.include(["/a.js"]) + .then ([a]) -> + # Never get here + assert false + + setTimeout -> + done() + , 100 + From 8214647286262eac92416e5528be9b79514984bf Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 3 Jan 2017 22:03:00 -0800 Subject: [PATCH 15/78] :light_rail::uk: Updated at https://danielx.net/editor/ --- TODO.md | 30 +++++++++++++++++++++++------- apps/explorer.coffee | 20 ++++++++++++++++++++ issues/2017-01.coffee | 4 ++++ os.coffee | 21 +++++++++++++++++---- style.styl | 18 ++++++++++++++++++ system/module.coffee | 8 ++------ templates/file.jadelet | 3 +++ util.coffee | 10 ++++++++++ 8 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 apps/explorer.coffee create mode 100644 templates/file.jadelet create mode 100644 util.coffee diff --git a/TODO.md b/TODO.md index 071462e..b02925f 100644 --- a/TODO.md +++ b/TODO.md @@ -3,20 +3,25 @@ TODO System Features --------------- +[ ] File Browser -Folders +[ ] App Associations -Desktop Icons +[ ] File Context Menu -Desktop Background +[ ] Folders -Help Documentation +[ ] Desktop Icons -Compiler +[ ] Desktop Background -Require/Include Local Files +[ ] Help Documentation -Custom Models/Views +[ ] Compiler + +[x] Require/Include Local Files + +[ ] Custom Models/Views Applications ------------ @@ -28,3 +33,14 @@ Spreadsheet - Forms Tactics Game Sandbox + +Network Social +-------------- + +CDN + +Comments + +Sharing + +Remote Files diff --git a/apps/explorer.coffee b/apps/explorer.coffee new file mode 100644 index 0000000..bbc6b21 --- /dev/null +++ b/apps/explorer.coffee @@ -0,0 +1,20 @@ +# Explorer File Browser +# +# Explore the file system like adventureres of old! + +FileTemplate = require "../templates/file" + +module.exports = (options={}) -> + {path} = options + path ?= '/' + + explorer = document.createElement "explorer" + + system.fs.list(path) + .then (files) -> + files.forEach (file) -> + file.dblclick = -> + console.log "dblclick", file + explorer.appendChild FileTemplate file + + return explorer diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index b3b2550..4c68476 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -5,9 +5,13 @@ TextEditor = require "../apps/text-editor" Social = require "../social/social" +Explorer = require "../apps/explorer" + module.exports = -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + document.body.appendChild Explorer() + img = document.createElement "img" img.src = "https://s-media-cache-ak0.pinimg.com/originals/a3/ba/56/a3ba56cef667d14b54023cd624d4e070.jpg" diff --git a/os.coffee b/os.coffee index 24ae7e7..85f3822 100644 --- a/os.coffee +++ b/os.coffee @@ -1,3 +1,5 @@ +{fileSeparator, normalizePath} = require "./util" + # DexieDB Containing our FS DexieFSDB = (dbName='fs') -> db = new Dexie dbName @@ -28,11 +30,15 @@ DexieFS = (db) -> delete: (path) -> Files.delete(path) + # TODO: Collapse folders + # .replace(/\/.*$/, "/") list: (dir) -> Files.where("path").startsWith(dir).toArray() - .then (results) -> - uniq results.map ({path}) -> - path = path.replace(dir, "").replace(/\/.*$/, "/") + .then (files) -> + files.forEach (file) -> + file.relativePath = file.path.replace(dir, "") + + return files uniq = (array) -> Array.from new Set array @@ -47,12 +53,19 @@ module.exports = (dbName='zine-os') -> Object.assign self, fs: fs + # TODO: Allow relative paths readFile: (path) -> + path = normalizePath "/#{path}" + fs.read(path) .then ({blob}) -> blob - writeFile: fs.write + # TODO: Allow relative paths + writeFile: (path, blob) -> + path = normalizePath "/#{path}" + + fs.write path, blob # NOTE: These are experimental commands to run code execJS: (path) -> diff --git a/style.styl b/style.styl index 177cef4..3dc0488 100644 --- a/style.styl +++ b/style.styl @@ -16,3 +16,21 @@ comment &::before content: "-" + +explorer + display: block + padding: 1em + + body > & + height: 100% + width: 100% + + > file + display: inline-block + + > icon + background-color: green + display: block + height: 32px + margin: auto + width: 32px diff --git a/system/module.coffee b/system/module.coffee index 12fff65..539bb71 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -15,12 +15,8 @@ module.exports = (I, self) -> # TODO: Require .coffee/arbitrary files ### - fileSeparator = "/" - - normalizePath = (path) -> - path.replace(/\/\/+/, fileSeparator) # /// -> / - .replace(/\/[^/]*\/\.\./g, "") # /base/something/.. -> /base - .replace(/\/\.\//g, fileSeparator) # /base/. -> /base + + {fileSeparator, normalizePath} = require "../util" # Wrap program in async include wrapper rewriteRequires = (program) -> diff --git a/templates/file.jadelet b/templates/file.jadelet new file mode 100644 index 0000000..ce48538 --- /dev/null +++ b/templates/file.jadelet @@ -0,0 +1,3 @@ +file(@dblclick) + icon + label= @relativePath diff --git a/util.coffee b/util.coffee new file mode 100644 index 0000000..125b68d --- /dev/null +++ b/util.coffee @@ -0,0 +1,10 @@ +fileSeparator = "/" + +normalizePath = (path) -> + path.replace(/\/\/+/, fileSeparator) # /// -> / + .replace(/\/[^/]*\/\.\./g, "") # /base/something/.. -> /base + .replace(/\/\.\//g, fileSeparator) # /base/. -> /base + +module.exports = + fileSeparator: fileSeparator + normalizePath: normalizePath From fa6a5df5e6e052615c879c32ca76a7290fd90e14 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 4 Jan 2017 20:13:41 -0800 Subject: [PATCH 16/78] :bear::mailbox_with_mail: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 26 +++++++++++++++++++++++++ main.coffee | 8 ++++---- os.coffee => system.coffee | 8 ++++++-- system/module.coffee | 22 ++++++++++++++++++++- templates/file.jadelet | 2 +- test/system/module.coffee | 40 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 8 deletions(-) rename os.coffee => system.coffee (92%) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index bbc6b21..e412625 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -5,16 +5,42 @@ FileTemplate = require "../templates/file" module.exports = (options={}) -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI {path} = options path ?= '/' explorer = document.createElement "explorer" + contextMenuFor = (file, e) -> + return if e.defaultPrevented + e.preventDefault() + + contextMenu = ContextMenu + items: parseMenu """ + Hello + Radical + - + Yolo + - + Cool + """ + handlers: {} + + contextMenu.display + inElement: document.body + x: e.pageX + y: e.pageY + system.fs.list(path) .then (files) -> files.forEach (file) -> file.dblclick = -> console.log "dblclick", file + system.open file + + file.contextmenu = (e) -> + contextMenuFor(file, e) + explorer.appendChild FileTemplate file return explorer diff --git a/main.coffee b/main.coffee index 23ae323..d257666 100644 --- a/main.coffee +++ b/main.coffee @@ -1,11 +1,11 @@ require "./extensions" -OS = require "../os" -global.system = os = OS() +System = require "./system" +global.system = System() -{Style} = os.UI +{Style} = system.UI style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style -require("./issues/2017-01")(os) +require("./issues/2017-01")() diff --git a/os.coffee b/system.coffee similarity index 92% rename from os.coffee rename to system.coffee index 85f3822..5f48009 100644 --- a/os.coffee +++ b/system.coffee @@ -43,14 +43,18 @@ DexieFS = (db) -> uniq = (array) -> Array.from new Set array +Model = require "model" +SystemModule = require "./system/module" UI = require "ui" module.exports = (dbName='zine-os') -> - self = {} + self = Model() fs = DexieFS(DexieFSDB(dbName)) - Object.assign self, + self.include(SystemModule) + + self.extend fs: fs # TODO: Allow relative paths diff --git a/system/module.coffee b/system/module.coffee index 539bb71..30fc1a6 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -1,3 +1,7 @@ +# Handles loading and launching files from the fs +# +# Depends on having self.readFile defined + module.exports = (I, self) -> ### Load a module from a file in the file system. @@ -13,12 +17,16 @@ module.exports = (I, self) -> Circular includes will never reslove # TODO: Fail early on circular includes, challenging because of async + # TODO: Succeed on files that don't assign module.exports + # TODO: Require .coffee/arbitrary files + # images, blobs, html, json ### - + {fileSeparator, normalizePath} = require "../util" # Wrap program in async include wrapper + # Replaces references to require('something') with local variables in an async wrapper function rewriteRequires = (program) -> id = 0 namePrefix = "__req" @@ -99,6 +107,18 @@ module.exports = (I, self) -> reject e Object.assign self, + # Open a file + # TODO: Pass arguments + # TODO: Drop files on an app to open them in that app + open: (file) -> + # Launch/Exec JS + self.include([file.path]) + .then ([moduleExports]) -> + moduleExports + + # Open JSON arrays in spreadsheet + # Open text in notepad + # still experimenting with the API # Async include in the vein of require.js # it's horrible but seems necessary diff --git a/templates/file.jadelet b/templates/file.jadelet index ce48538..cf65b19 100644 --- a/templates/file.jadelet +++ b/templates/file.jadelet @@ -1,3 +1,3 @@ -file(@dblclick) +file(@dblclick @contextmenu) icon label= @relativePath diff --git a/test/system/module.coffee b/test/system/module.coffee index cef9ec8..cf0fcaf 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -65,3 +65,43 @@ describe "System Module", -> done() , 100 + it "should return export if present", -> + model = Model() + + model.include SystemModule + + files = + "/wat.js": """ + module.exports = "wat"; + """ + + model.readFile = (path) -> + content = files[path] + + Promise.resolve new Blob [content] + + model.open + path: "/wat.js" + .then (moduleExports) -> + assert.equal moduleExports, "wat" + + it "should work even if the file doesn't assign to module.exports" + -> + model = Model() + + model.include SystemModule + + files = + "/wat.js": """ + exports.yolo = "wat"; + """ + + model.readFile = (path) -> + content = files[path] + + Promise.resolve new Blob [content] + + model.open + path: "/wat.js" + .then (moduleExports) -> + assert.equal moduleExports.yolo, "wat" From a48792b6673e5a2e2f36cb36ba3e5f4bcc5d0b77 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 4 Jan 2017 21:13:38 -0800 Subject: [PATCH 17/78] :fireworks::mailbox: Updated at https://danielx.net/editor/ --- apps/spreadsheet.coffee | 12 ++++----- apps/text-editor.coffee | 4 ++- system/module.coffee | 54 +++++++++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index 3826080..6781d04 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -4,10 +4,7 @@ Model = require "model" module.exports = () -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI - sourceData = [0...5].map (i) -> - id: i - name: "yolo" - color: "#FF0000" + sourceData = [] headers = ["id", "name", "color"] @@ -35,7 +32,6 @@ module.exports = () -> return tr - {element} = tableView = Table { data: models RowElement: RowElement @@ -52,7 +48,9 @@ module.exports = () -> throw new Error "Data must be an array" sourceData = json - # TODO: Update models data + # Update models data + models.splice(0, models.length, sourceData.map(RowModel)...) + # Re-render tableView.render() @@ -101,4 +99,6 @@ module.exports = () -> width: 640 height: 480 + windowView.loadFile = handlers.loadFile + return windowView diff --git a/apps/text-editor.coffee b/apps/text-editor.coffee index bb435d4..4dc56ae 100644 --- a/apps/text-editor.coffee +++ b/apps/text-editor.coffee @@ -27,7 +27,7 @@ module.exports = -> enableBasicAutocompletion: true enableLiveAutocompletion: true highlightActiveLine: true - + session = aceEditor.getSession() session.setUseSoftTabs true session.setTabSize 2 @@ -80,6 +80,8 @@ module.exports = -> width: 640 height: 480 + windowView.loadFile = initSession + windowView.on "resize", -> aceEditor.resize() diff --git a/system/module.coffee b/system/module.coffee index 30fc1a6..02eeb7f 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -2,6 +2,10 @@ # # Depends on having self.readFile defined +# TODO: Move handlers out +TextEditor = require "../apps/text-editor" +Spreadsheet = require "../apps/spreadsheet" + module.exports = (I, self) -> ### Load a module from a file in the file system. @@ -23,6 +27,48 @@ module.exports = (I, self) -> # images, blobs, html, json ### + # TODO: Handlers that can use combined type, extension, and contents info + # to do the right thing + # Prioritize handlers falling back to others + handlers = [{ + # JavaScript + name: "Execute" + filter: (file) -> + file.type is "application/javascript" + fn: (file) -> + self.include([file.path]) + .then ([moduleExports]) -> + moduleExports + }, { + name: "Open as Text" + filter: (file) -> + file.type.match /^text\// + fn: (file) -> + editor = TextEditor() + editor.loadFile(file.blob) + document.body.appendChild editor.element + }, { + name: "Open Spreadsheet" + filter: (file) -> + # TODO: This actually only handles JSON arrays + file.type is "application/json" + fn: (file) -> + editor = Spreadsheet() + editor.loadFile(file.blob) + document.body.appendChild editor.element + }] + + # Open JSON arrays in spreadsheet + # Open text in notepad + handle = (file) -> + handler = handlers.find ({filter}) -> + filter(file) + + if handler + handler.fn(file) + else + throw new Error "No handler for files of type #{type}" + {fileSeparator, normalizePath} = require "../util" # Wrap program in async include wrapper @@ -111,13 +157,7 @@ module.exports = (I, self) -> # TODO: Pass arguments # TODO: Drop files on an app to open them in that app open: (file) -> - # Launch/Exec JS - self.include([file.path]) - .then ([moduleExports]) -> - moduleExports - - # Open JSON arrays in spreadsheet - # Open text in notepad + handle(file) # still experimenting with the API # Async include in the vein of require.js From 17e5dbb7a242cc927e3ee9fcb1258ccd5f018bf3 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 4 Jan 2017 21:21:23 -0800 Subject: [PATCH 18/78] :smoking::part_alternation_mark: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index e412625..7a00aa2 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -31,6 +31,8 @@ module.exports = (options={}) -> x: e.pageX y: e.pageY + # TODO: Refresh files when they change + system.fs.list(path) .then (files) -> files.forEach (file) -> From d0679427272f5440c44b7e0ae66b9125aa61a87f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 4 Jan 2017 21:52:25 -0800 Subject: [PATCH 19/78] :city_sunrise::beers: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 7a00aa2..0fbe5a6 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -15,6 +15,8 @@ module.exports = (options={}) -> return if e.defaultPrevented e.preventDefault() + # TODO: Open With Options + # TODO: Set Mime Type contextMenu = ContextMenu items: parseMenu """ Hello From 572df9b56ee55c18a1ca49f9f00564262bc8421d Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 15:57:49 -0800 Subject: [PATCH 20/78] :person_frowning::collision: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 37 +++++++++++++++++---------- pixie.cson | 1 + style.styl | 3 ++- system.coffee | 61 ++++++++++++++++++++++++++------------------ util.coffee | 4 +++ 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 0fbe5a6..706f4d7 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -4,6 +4,8 @@ FileTemplate = require "../templates/file" +{emptyElement} = require "../util" + module.exports = (options={}) -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI {path} = options @@ -33,18 +35,27 @@ module.exports = (options={}) -> x: e.pageX y: e.pageY - # TODO: Refresh files when they change - - system.fs.list(path) - .then (files) -> - files.forEach (file) -> - file.dblclick = -> - console.log "dblclick", file - system.open file - - file.contextmenu = (e) -> - contextMenuFor(file, e) - - explorer.appendChild FileTemplate file + update = -> + system.fs.list(path) + .then (files) -> + emptyElement explorer + + files.forEach (file) -> + file.dblclick = -> + console.log "dblclick", file + system.open file + + file.contextmenu = (e) -> + contextMenuFor(file, e) + + explorer.appendChild FileTemplate file + + update() + + # Refresh files when they change + system.fs.on "write", (path) -> + update() + system.fs.on "delete", (path) -> + update() return explorer diff --git a/pixie.cson b/pixie.cson index c7a9401..af83104 100644 --- a/pixie.cson +++ b/pixie.cson @@ -1,5 +1,6 @@ dependencies: ajax: "distri/ajax:master" + bindable: "distri/bindable:master" model: "distri/model:master" postmaster: "distri/postmaster:v0.5.0" ui: "STRd6/ui:master" diff --git a/style.styl b/style.styl index 3dc0488..fe69b96 100644 --- a/style.styl +++ b/style.styl @@ -19,7 +19,7 @@ comment explorer display: block - padding: 1em + padding: 0.5em body > & height: 100% @@ -27,6 +27,7 @@ explorer > file display: inline-block + padding: 0.5em > icon background-color: green diff --git a/system.coffee b/system.coffee index 5f48009..ba7b94d 100644 --- a/system.coffee +++ b/system.coffee @@ -13,36 +13,47 @@ DexieFSDB = (dbName='fs') -> DexieFS = (db) -> Files = db.files - read: (path) -> - Files.get(path) + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result - write: (path, blob) -> - now = +new Date - - Files.put - path: path - blob: blob - size: blob.size - type: blob.type - createdAt: now - updatedAt: now - - delete: (path) -> - Files.delete(path) - - # TODO: Collapse folders - # .replace(/\/.*$/, "/") - list: (dir) -> - Files.where("path").startsWith(dir).toArray() - .then (files) -> - files.forEach (file) -> - file.relativePath = file.path.replace(dir, "") - - return files + self = Model() + .include(Bindable) + .extend + read: (path) -> + Files.get(path) + + write: (path, blob) -> + now = +new Date + + Files.put + path: path + blob: blob + size: blob.size + type: blob.type + createdAt: now + updatedAt: now + .then notify "write", path + + delete: (path) -> + Files.delete(path) + .then notify "delete", path + + # TODO: Collapse folders + # .replace(/\/.*$/, "/") + list: (dir) -> + Files.where("path").startsWith(dir).toArray() + .then (files) -> + files.forEach (file) -> + file.relativePath = file.path.replace(dir, "") + + return files uniq = (array) -> Array.from new Set array +Bindable = require "bindable" Model = require "model" SystemModule = require "./system/module" UI = require "ui" diff --git a/util.coffee b/util.coffee index 125b68d..45e35f6 100644 --- a/util.coffee +++ b/util.coffee @@ -6,5 +6,9 @@ normalizePath = (path) -> .replace(/\/\.\//g, fileSeparator) # /base/. -> /base module.exports = + emptyElement: -> + while element.lastChild + element.removeChild element.lastChild + fileSeparator: fileSeparator normalizePath: normalizePath From 91f69557fc3b54e0bb0034503a51345c2d98d0e1 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 16:10:36 -0800 Subject: [PATCH 21/78] :bowtie::six_pointed_star: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 15 ++++++++------- util.coffee | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 706f4d7..96c7298 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -19,14 +19,15 @@ module.exports = (options={}) -> # TODO: Open With Options # TODO: Set Mime Type + # TODO: Cut/Copy contextMenu = ContextMenu items: parseMenu """ - Hello - Radical + Open - - Yolo + Delete + Rename - - Cool + Properties """ handlers: {} @@ -39,15 +40,15 @@ module.exports = (options={}) -> system.fs.list(path) .then (files) -> emptyElement explorer - + files.forEach (file) -> file.dblclick = -> console.log "dblclick", file system.open file - + file.contextmenu = (e) -> contextMenuFor(file, e) - + explorer.appendChild FileTemplate file update() diff --git a/util.coffee b/util.coffee index 45e35f6..0f0bd72 100644 --- a/util.coffee +++ b/util.coffee @@ -6,7 +6,7 @@ normalizePath = (path) -> .replace(/\/\.\//g, fileSeparator) # /base/. -> /base module.exports = - emptyElement: -> + emptyElement: (element) -> while element.lastChild element.removeChild element.lastChild From f47a0d92cdc5753afada9e076f50c1f733e4db58 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 17:05:58 -0800 Subject: [PATCH 22/78] :free::eyes: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 11 ++++++++++- system.coffee | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 96c7298..106e3e4 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -29,7 +29,16 @@ module.exports = (options={}) -> - Properties """ - handlers: {} + handlers: + open: -> + system.open(file) + delete: -> + rename: -> + Modal.prompt "Filename", file.path + .then (newPath) -> + if newPath + system.deleteFile(file.path) + system.writeFile(newPath, file.blob) contextMenu.display inElement: document.body diff --git a/system.coffee b/system.coffee index ba7b94d..9fa685f 100644 --- a/system.coffee +++ b/system.coffee @@ -82,6 +82,12 @@ module.exports = (dbName='zine-os') -> fs.write path, blob + # TODO: Allow relative paths + deleteFile: (path) -> + path = normalizePath "/#{path}" + + fs.delete(path) + # NOTE: These are experimental commands to run code execJS: (path) -> self.readFile(path) From 4c64c5e7240a3839855ebe9f3133014be3c6026f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 17:08:26 -0800 Subject: [PATCH 23/78] :iphone::loudspeaker: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 106e3e4..7e369db 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -33,6 +33,7 @@ module.exports = (options={}) -> open: -> system.open(file) delete: -> + system.deleteFile(file.path) rename: -> Modal.prompt "Filename", file.path .then (newPath) -> From a414fc8e0fa8e108378738dcd2831a32958fe87e Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 17:34:33 -0800 Subject: [PATCH 24/78] :revolving_hearts::bus: Updated at https://danielx.net/editor/ --- style.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style.styl b/style.styl index fe69b96..1daf50b 100644 --- a/style.styl +++ b/style.styl @@ -30,7 +30,7 @@ explorer padding: 0.5em > icon - background-color: green + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3d3dwAAAAj///////////////cAAAAI///////////////3AAAACP///////3d///d39wAAAAj/zMzM//mZ//+Zn/cAAAAI////////l///+X/3AAAACP/MzMzM//l3d3l/9wAAAAj/////////mZmZf/cAAAAI/8zMzMzM//l/+X/3AAAACP//////////l/l/9wAAAAj/zMzMzMzM//l5f/cAAAAI////////////mX/3AAAACP/MzMzMzMzM//n/9wAAAAj///////////////cAAAAI/8zMzMzMzMzMzP/3AAAACP//////////////9wAAAAj/zMzMzMzMzMzM//cAAAAI///////////////3AAAACP8AAAAA/8zMzMz/9wAAAAj/iZD/8P////////cAAAAI/4AAAAD/zMzMzP/3AAAACP+P8Luw////////9wAAAAj/gAC7sP/MzMzM//cAAAAI/4/wu7D////////3AAAACP+P8Luw/////4AAAAAAAAj/j/AAAP////+P94AAAAAI/4/wzMD/////j3gAAAAACP+IiIiA/////4eAAAAAAAj///////////+IAAAAAAAI////////////gAAAAAAACIiIiIiIiIiIiIAAAAAA4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAP4AAAH+AAAD/gAAB/4AAA/+AAAf8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj8z5//n3AACP//+ZmfcAAI/MzPn59wAAj////5n3AACPzMzM+fcAAI//////9wAAjwAPzMz3AACPmY////cAAI/Pj8zM9wAAj8+P//AAAACPiI//9/gAAI/////3gAAAiIiIiIgAAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIAHAACADwAAgB8AAA==") display: block height: 32px margin: auto From f61fe8005a89756b64850734fc902c78cbe1deb9 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 20:16:35 -0800 Subject: [PATCH 25/78] :information_desk_person::credit_card: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 35 ++++++++++++++++++++++++++++++++++- style.styl | 9 +++++++-- system.coffee | 2 +- templates/folder.jadelet | 3 +++ 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 templates/folder.jadelet diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 7e369db..637255c 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -3,10 +3,11 @@ # Explore the file system like adventureres of old! FileTemplate = require "../templates/file" +FolderTemplate = require "../templates/folder" {emptyElement} = require "../util" -module.exports = (options={}) -> +module.exports = Explorer = (options={}) -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI {path} = options path ?= '/' @@ -51,7 +52,14 @@ module.exports = (options={}) -> .then (files) -> emptyElement explorer + addedFolders = {} + files.forEach (file) -> + if file.relativePath.match /\// # folder + folderPath = file.relativePath.replace /\/.*$/, "" + addedFolders[folderPath] = true + return + file.dblclick = -> console.log "dblclick", file system.open file @@ -61,6 +69,18 @@ module.exports = (options={}) -> explorer.appendChild FileTemplate file + Object.keys(addedFolders).forEach (folderName) -> + folderElement = FolderTemplate + relativePath: folderName + contextmenu: -> + dblclick: -> + # Open folder in new window + addWindow("#{path}#{folderName}/") + + explorer.insertBefore(folderElement, explorer.firstChild) + + console.log addedFolders + update() # Refresh files when they change @@ -69,4 +89,17 @@ module.exports = (options={}) -> system.fs.on "delete", (path) -> update() + addWindow = (path) -> + element = Explorer + path: path + + windowView = Window + title: path + content: element + menuBar: null + width: 640 + height: 480 + + document.body.appendChild windowView.element + return explorer diff --git a/style.styl b/style.styl index 1daf50b..d4794ec 100644 --- a/style.styl +++ b/style.styl @@ -25,13 +25,18 @@ explorer height: 100% width: 100% - > file + > file, > folder display: inline-block padding: 0.5em > icon - background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3d3dwAAAAj///////////////cAAAAI///////////////3AAAACP///////3d///d39wAAAAj/zMzM//mZ//+Zn/cAAAAI////////l///+X/3AAAACP/MzMzM//l3d3l/9wAAAAj/////////mZmZf/cAAAAI/8zMzMzM//l/+X/3AAAACP//////////l/l/9wAAAAj/zMzMzMzM//l5f/cAAAAI////////////mX/3AAAACP/MzMzMzMzM//n/9wAAAAj///////////////cAAAAI/8zMzMzMzMzMzP/3AAAACP//////////////9wAAAAj/zMzMzMzMzMzM//cAAAAI///////////////3AAAACP8AAAAA/8zMzMz/9wAAAAj/iZD/8P////////cAAAAI/4AAAAD/zMzMzP/3AAAACP+P8Luw////////9wAAAAj/gAC7sP/MzMzM//cAAAAI/4/wu7D////////3AAAACP+P8Luw/////4AAAAAAAAj/j/AAAP////+P94AAAAAI/4/wzMD/////j3gAAAAACP+IiIiA/////4eAAAAAAAj///////////+IAAAAAAAI////////////gAAAAAAACIiIiIiIiIiIiIAAAAAA4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAP4AAAH+AAAD/gAAB/4AAA/+AAAf8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj8z5//n3AACP//+ZmfcAAI/MzPn59wAAj////5n3AACPzMzM+fcAAI//////9wAAjwAPzMz3AACPmY////cAAI/Pj8zM9wAAj8+P//AAAACPiI//9/gAAI/////3gAAAiIiIiIgAAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIAHAACADwAAgB8AAA==") display: block height: 32px margin: auto width: 32px + + > file > icon + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3d3dwAAAAj///////////////cAAAAI///////////////3AAAACP///////3d///d39wAAAAj/zMzM//mZ//+Zn/cAAAAI////////l///+X/3AAAACP/MzMzM//l3d3l/9wAAAAj/////////mZmZf/cAAAAI/8zMzMzM//l/+X/3AAAACP//////////l/l/9wAAAAj/zMzMzMzM//l5f/cAAAAI////////////mX/3AAAACP/MzMzMzMzM//n/9wAAAAj///////////////cAAAAI/8zMzMzMzMzMzP/3AAAACP//////////////9wAAAAj/zMzMzMzMzMzM//cAAAAI///////////////3AAAACP8AAAAA/8zMzMz/9wAAAAj/iZD/8P////////cAAAAI/4AAAAD/zMzMzP/3AAAACP+P8Luw////////9wAAAAj/gAC7sP/MzMzM//cAAAAI/4/wu7D////////3AAAACP+P8Luw/////4AAAAAAAAj/j/AAAP////+P94AAAAAI/4/wzMD/////j3gAAAAACP+IiIiA/////4eAAAAAAAj///////////+IAAAAAAAI////////////gAAAAAAACIiIiIiIiIiIiIAAAAAA4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAP4AAAH+AAAD/gAAB/4AAA/+AAAf8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj8z5//n3AACP//+ZmfcAAI/MzPn59wAAj////5n3AACPzMzM+fcAAI//////9wAAjwAPzMz3AACPmY////cAAI/Pj8zM9wAAj8+P//AAAACPiI//9/gAAI/////3gAAAiIiIiIgAAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIAHAACADwAAgB8AAA==") + + > folder > icon + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDMzMzMzMzMzMzMzMzMzMwj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI///////////////////wCHd3d3d3d3d4iIiIiIiIgACPt7e3t7e3gAAAAAAAAAAACPt7e3t7eAAAAAAAAAAAAACP/////4AAAAAAAAAAAAAACIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAP//wAH//+AD///wB/////////////8oAAAAEAAAACAAAAABAAQAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIgAj7e3t7e3uACPe3t7e3t4AI+3t7e3t7gAj3t7e3t7eACPt7e3t7e4AI97e3t7e3gAj7e3t7e3uACP///////4AIe3t7eIiIgACHt7eAAAAAAAiIiAAAAAAAAAAAAAAAAA//8AAP//AACAAQCgAAHOAAABzgAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAID/AADB/wAA//8AAA==") diff --git a/system.coffee b/system.coffee index 9fa685f..680419c 100644 --- a/system.coffee +++ b/system.coffee @@ -47,7 +47,7 @@ DexieFS = (db) -> .then (files) -> files.forEach (file) -> file.relativePath = file.path.replace(dir, "") - + return files uniq = (array) -> diff --git a/templates/folder.jadelet b/templates/folder.jadelet new file mode 100644 index 0000000..f4b73b3 --- /dev/null +++ b/templates/folder.jadelet @@ -0,0 +1,3 @@ +folder(@dblclick @contextmenu) + icon + label= @relativePath From 67a17bc04f49adfa636af0d910d28dea92958811 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 5 Jan 2017 20:57:05 -0800 Subject: [PATCH 26/78] :circus_tent::clock130: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 53 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 637255c..5cbc581 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -1,6 +1,7 @@ # Explorer File Browser # # Explore the file system like adventureres of old! +# TODO: Drag and drop files and folders FileTemplate = require "../templates/file" FolderTemplate = require "../templates/folder" @@ -20,10 +21,13 @@ module.exports = Explorer = (options={}) -> # TODO: Open With Options # TODO: Set Mime Type - # TODO: Cut/Copy contextMenu = ContextMenu items: parseMenu """ Open + Open With + - + Cut + Copy - Delete Rename @@ -33,6 +37,9 @@ module.exports = Explorer = (options={}) -> handlers: open: -> system.open(file) + openWith: -> #TODO + cut: -> #TODO + copy: -> #TODO delete: -> system.deleteFile(file.path) rename: -> @@ -41,6 +48,35 @@ module.exports = Explorer = (options={}) -> if newPath system.deleteFile(file.path) system.writeFile(newPath, file.blob) + properties: -> #TODO + + contextMenu.display + inElement: document.body + x: e.pageX + y: e.pageY + + contextMenuForFolder = (folder, e) -> + return if e.defaultPrevented + e.preventDefault() + + # TODO: Cut/Copy + contextMenu = ContextMenu + items: parseMenu """ + Open + - + Delete + Rename + - + Properties + """ + handlers: + open: -> + addWindow(folder.path) + delete: -> # TODO: Delete all files under folder + rename: -> + ;# TODO: Rename all files under folder (!) + # May want to think about inodes or something that makes this simpler + properties: -> # TODO contextMenu.display inElement: document.body @@ -70,17 +106,18 @@ module.exports = Explorer = (options={}) -> explorer.appendChild FileTemplate file Object.keys(addedFolders).forEach (folderName) -> - folderElement = FolderTemplate + folder = + path: "#{path}#{folderName}/" relativePath: folderName - contextmenu: -> + contextmenu: (e) -> + contextMenuForFolder(folder, e) dblclick: -> # Open folder in new window - addWindow("#{path}#{folderName}/") + addWindow(folder.path) + folderElement = FolderTemplate folder explorer.insertBefore(folderElement, explorer.firstChild) - console.log addedFolders - update() # Refresh files when they change @@ -90,7 +127,9 @@ module.exports = Explorer = (options={}) -> update() addWindow = (path) -> - element = Explorer + element = document.createElement "container" + + element.appendChild Explorer path: path windowView = Window From cdce5205a506d0f07116b06e491b6e3cff75a995 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 6 Jan 2017 16:21:53 -0800 Subject: [PATCH 27/78] :yellow_heart::underage: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 16 ++++++++++++++- apps/filter.coffee | 46 ++++++++++++++++++++++++++++++++++++++++++ extensions.coffee | 18 +++++++++++++++-- lib/drop.coffee | 11 ++++++++++ style.styl | 6 ++++++ templates/file.jadelet | 2 +- 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 apps/filter.coffee create mode 100644 lib/drop.coffee diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 5cbc581..3be73e0 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -2,7 +2,11 @@ # # Explore the file system like adventureres of old! # TODO: Drag and drop files and folders +# TODO: Select multiple +# TOOD: Keyboard Input +# TODO: Style file types +Drop = require "../lib/drop" FileTemplate = require "../templates/file" FolderTemplate = require "../templates/folder" @@ -14,6 +18,13 @@ module.exports = Explorer = (options={}) -> path ?= '/' explorer = document.createElement "explorer" + + Drop explorer, (e) -> + files = e.dataTransfer.files + + if files.length + files.forEach (file) -> + console.log file contextMenuFor = (file, e) -> return if e.defaultPrevented @@ -64,6 +75,9 @@ module.exports = Explorer = (options={}) -> items: parseMenu """ Open - + Cut + Copy + - Delete Rename - @@ -106,7 +120,7 @@ module.exports = Explorer = (options={}) -> explorer.appendChild FileTemplate file Object.keys(addedFolders).forEach (folderName) -> - folder = + folder = path: "#{path}#{folderName}/" relativePath: folderName contextmenu: (e) -> diff --git a/apps/filter.coffee b/apps/filter.coffee new file mode 100644 index 0000000..2fa6b4d --- /dev/null +++ b/apps/filter.coffee @@ -0,0 +1,46 @@ +# Filter Images + +FileIO = require "../os/file-io" +Model = require "model" + +module.exports = -> + # Global system + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + + canvas = document.createElement 'canvas' + context = canvas.getContext('2d') + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + Image.fromBlob blob + .then (img) -> + canvas.width = img.width + canvas.height = img.height + context.drawImage(img, 0, 0) + + saveData: -> + new Promise (resolve) -> + canvas.toBlob resolve + + exit: -> + windowView.element.remove() + + menuBar = MenuBar + items: parseMenu """ + [F]ile + [O]pen + [S]ave + Save [A]s + - + E[x]it + """ + handlers: handlers + + windowView = Window + title: "Filter Booth" + content: canvas + menuBar: menuBar.element + width: 640 + height: 480 + + return windowView diff --git a/extensions.coffee b/extensions.coffee index 8934ac7..0439bfd 100644 --- a/extensions.coffee +++ b/extensions.coffee @@ -1,5 +1,5 @@ # Add some utility readers to the Blob API -Blob.prototype.readAsText = -> +Blob::readAsText = -> file = this new Promise (resolve, reject) -> @@ -9,6 +9,20 @@ Blob.prototype.readAsText = -> reader.onerror = reject reader.readAsText(file) -Blob.prototype.readAsJSON = -> +Blob::readAsJSON = -> @readAsText() .then JSON.parse + +# Load an image from a blob returning a promise that is fulfilled with the +# loaded image or rejected with an error +Image.fromBlob = (blob) -> + new Promise (resolve, reject) -> + img = new Image + img.onload = -> + resolve img + img.onerror = reject + + img.src = URL.createObjectURL blob + +FileList::forEach ?= (args...) -> + Array::forEach.apply(this, args) diff --git a/lib/drop.coffee b/lib/drop.coffee new file mode 100644 index 0000000..6e1fb0a --- /dev/null +++ b/lib/drop.coffee @@ -0,0 +1,11 @@ +module.exports = (element, handler) -> + cancel = (e) -> + e.preventDefault() + return false + + element.addEventListener "dragover", cancel + element.addEventListener "dragenter", cancel + element.addEventListener "drop", (e) -> + e.preventDefault() + handler(e) + return false diff --git a/style.styl b/style.styl index d4794ec..9a6752f 100644 --- a/style.styl +++ b/style.styl @@ -38,5 +38,11 @@ explorer > file > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3d3dwAAAAj///////////////cAAAAI///////////////3AAAACP///////3d///d39wAAAAj/zMzM//mZ//+Zn/cAAAAI////////l///+X/3AAAACP/MzMzM//l3d3l/9wAAAAj/////////mZmZf/cAAAAI/8zMzMzM//l/+X/3AAAACP//////////l/l/9wAAAAj/zMzMzMzM//l5f/cAAAAI////////////mX/3AAAACP/MzMzMzMzM//n/9wAAAAj///////////////cAAAAI/8zMzMzMzMzMzP/3AAAACP//////////////9wAAAAj/zMzMzMzMzMzM//cAAAAI///////////////3AAAACP8AAAAA/8zMzMz/9wAAAAj/iZD/8P////////cAAAAI/4AAAAD/zMzMzP/3AAAACP+P8Luw////////9wAAAAj/gAC7sP/MzMzM//cAAAAI/4/wu7D////////3AAAACP+P8Luw/////4AAAAAAAAj/j/AAAP////+P94AAAAAI/4/wzMD/////j3gAAAAACP+IiIiA/////4eAAAAAAAj///////////+IAAAAAAAI////////////gAAAAAAACIiIiIiIiIiIiIAAAAAA4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAP4AAAH+AAAD/gAAB/4AAA/+AAAf8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj8z5//n3AACP//+ZmfcAAI/MzPn59wAAj////5n3AACPzMzM+fcAAI//////9wAAjwAPzMz3AACPmY////cAAI/Pj8zM9wAAj8+P//AAAACPiI//9/gAAI/////3gAAAiIiIiIgAAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIAHAACADwAAgB8AAA==") + > file[type^="text/"] > icon + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3dwcAAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI/wAAAAAAAAAAAP9wcAAACP//////////////cHAAAAj/AAAAAAAAAAAA/3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD////w8AD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAjw/w/w/w/w/w/w/4BwAAAI8P8P8P8P8P8P8P+AcAAAAI+I+I+I+I+I+I+I+AAAAAAAAAAAAAAAAAAAAAAAAA8AAAH+AAAA/AAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAH4AAAD/JJJL8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAh3d3d3d3AACP//////cAAI//////9wAAj/AAAA/3AACP//////cAAI/wAAAP9wAAj//////3AACP8AAAD/cAAI//////9wAAj/AA8A/3AACP//////cAAI//////9wAAjw8PDw8PAAAA+Pj4+PAAAAAAAAAAAAAAgA8AAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAACADwAA1V8AAA==") + + > file[type="application/javascript"] > icon + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3dwcAAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA////AAD//3BwAAAI////////AwsDAP9wcAAACP8AAAD//zu7uzD/cHAAAAj///////M7MDsAD3BwAAAI///////zuwgLsw9wcAAACP8AAAAAAzswOwAPcHAAAAj///////87u7sw/3BwAAAI/wAAAAAAMwsDAP9wcAAACP////////8zM///cHAAAAj/AAAAAAAAAAAA/3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD////w8AD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAjw/w/w/w/w/w/w/4BwAAAI8P8P8P8P8P8P8P+AcAAAAI+I+I+I+I+I+I+I+AAAAAAAAAAAAAAAAAAAAAAAAA8AAAH+AAAA/AAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAH4AAAD/JJJL8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj///Aw/3AACP8Ac7MPcAAI//87Cz9wAAj/AHOzD3AACP//9zf/cAAI/wAAAP9wAAj//////3AACP8ADwD/cAAI//////9wAAj//////3AACPDw8PDw8AAAD4+Pj48AAAAAAAAAAAAAwAcAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAADABwAA6q8AAA==") + > folder > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDMzMzMzMzMzMzMzMzMzMwj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI///////////////////wCHd3d3d3d3d4iIiIiIiIgACPt7e3t7e3gAAAAAAAAAAACPt7e3t7eAAAAAAAAAAAAACP/////4AAAAAAAAAAAAAACIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAP//wAH//+AD///wB/////////////8oAAAAEAAAACAAAAABAAQAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIgAj7e3t7e3uACPe3t7e3t4AI+3t7e3t7gAj3t7e3t7eACPt7e3t7e4AI97e3t7e3gAj7e3t7e3uACP///////4AIe3t7eIiIgACHt7eAAAAAAAiIiAAAAAAAAAAAAAAAAA//8AAP//AACAAQCgAAHOAAABzgAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAID/AADB/wAA//8AAA==") diff --git a/templates/file.jadelet b/templates/file.jadelet index cf65b19..e36c5bd 100644 --- a/templates/file.jadelet +++ b/templates/file.jadelet @@ -1,3 +1,3 @@ -file(@dblclick @contextmenu) +file(@dblclick @contextmenu @type) icon label= @relativePath From e84c11b01768f7721ab9694093f799687358418d Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 6 Jan 2017 16:54:24 -0800 Subject: [PATCH 28/78] :blossom::mag_right: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 14 ++++++++++---- system/module.coffee | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 3be73e0..d46edad 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -18,13 +18,14 @@ module.exports = Explorer = (options={}) -> path ?= '/' explorer = document.createElement "explorer" - + Drop explorer, (e) -> files = e.dataTransfer.files - + if files.length files.forEach (file) -> - console.log file + newPath = path + file.name + system.writeFile(newPath, file) contextMenuFor = (file, e) -> return if e.defaultPrevented @@ -117,7 +118,12 @@ module.exports = Explorer = (options={}) -> file.contextmenu = (e) -> contextMenuFor(file, e) - explorer.appendChild FileTemplate file + fileElement = FileTemplate file + if file.type.match /^image\// + url = URL.createObjectURL file.blob + fileElement.querySelector('icon').style.backgroundImage = "url(#{url})" + + explorer.appendChild fileElement Object.keys(addedFolders).forEach (folderName) -> folder = diff --git a/system/module.coffee b/system/module.coffee index 02eeb7f..7fe19a2 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -67,7 +67,7 @@ module.exports = (I, self) -> if handler handler.fn(file) else - throw new Error "No handler for files of type #{type}" + throw new Error "No handler for files of type #{file.type}" {fileSeparator, normalizePath} = require "../util" From 54c12a720a7c1fbffd92ae699004674e18c532f8 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 7 Jan 2017 20:58:55 -0800 Subject: [PATCH 29/78] :horse_racing::tshirt: Updated at https://danielx.net/editor/ --- apps/filter.coffee | 2 ++ extensions.coffee | 2 +- system/module.coffee | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/filter.coffee b/apps/filter.coffee index 2fa6b4d..f432f8a 100644 --- a/apps/filter.coffee +++ b/apps/filter.coffee @@ -42,5 +42,7 @@ module.exports = -> menuBar: menuBar.element width: 640 height: 480 + + windowView.loadFile = handlers.loadFile return windowView diff --git a/extensions.coffee b/extensions.coffee index 0439bfd..e292975 100644 --- a/extensions.coffee +++ b/extensions.coffee @@ -22,7 +22,7 @@ Image.fromBlob = (blob) -> resolve img img.onerror = reject - img.src = URL.createObjectURL blob + img.src = URL.createObjectURL blob FileList::forEach ?= (args...) -> Array::forEach.apply(this, args) diff --git a/system/module.coffee b/system/module.coffee index 7fe19a2..b71eadc 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -3,6 +3,7 @@ # Depends on having self.readFile defined # TODO: Move handlers out +Filter = require "../apps/filter" TextEditor = require "../apps/text-editor" Spreadsheet = require "../apps/spreadsheet" @@ -56,6 +57,14 @@ module.exports = (I, self) -> editor = Spreadsheet() editor.loadFile(file.blob) document.body.appendChild editor.element + }, { + name: "View Image" + filter: (file) -> + file.type.match /^image\// + fn: (file) -> + app = Filter() + app.loadFile(file.blob) + document.body.appendChild app.element }] # Open JSON arrays in spreadsheet From 7997b89b21d008b5d0dae159f0d39c4e4eb5120e Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 8 Jan 2017 10:37:57 -0800 Subject: [PATCH 30/78] :smoking::microscope: Updated at https://danielx.net/editor/ --- system.coffee | 3 +- system/associations.coffee | 64 ++++++++++++++++++++++++++++++++++++++ system/module.coffee | 61 ------------------------------------ test/system/module.coffee | 4 ++- 4 files changed, 69 insertions(+), 63 deletions(-) create mode 100644 system/associations.coffee diff --git a/system.coffee b/system.coffee index 680419c..0c63ffe 100644 --- a/system.coffee +++ b/system.coffee @@ -55,6 +55,7 @@ uniq = (array) -> Bindable = require "bindable" Model = require "model" +Associations = require "./system/associations" SystemModule = require "./system/module" UI = require "ui" @@ -63,7 +64,7 @@ module.exports = (dbName='zine-os') -> fs = DexieFS(DexieFSDB(dbName)) - self.include(SystemModule) + self.include(Associations, SystemModule) self.extend fs: fs diff --git a/system/associations.coffee b/system/associations.coffee new file mode 100644 index 0000000..173610f --- /dev/null +++ b/system/associations.coffee @@ -0,0 +1,64 @@ +# TODO: Move handlers out +Filter = require "../apps/filter" +TextEditor = require "../apps/text-editor" +Spreadsheet = require "../apps/spreadsheet" + +module.exports = (I, self) -> + # TODO: Handlers that can use combined type, extension, and contents info + # to do the right thing + # Prioritize handlers falling back to others + handlers = [{ + # JavaScript + name: "Execute" + filter: (file) -> + file.type is "application/javascript" + fn: (file) -> + self.include([file.path]) + .then ([moduleExports]) -> + moduleExports + }, { + name: "Open as Text" + filter: (file) -> + file.type.match /^text\// + fn: (file) -> + editor = TextEditor() + editor.loadFile(file.blob) + document.body.appendChild editor.element + }, { + name: "Open Spreadsheet" + filter: (file) -> + # TODO: This actually only handles JSON arrays + file.type is "application/json" + fn: (file) -> + editor = Spreadsheet() + editor.loadFile(file.blob) + document.body.appendChild editor.element + }, { + name: "View Image" + filter: (file) -> + file.type.match /^image\// + fn: (file) -> + app = Filter() + app.loadFile(file.blob) + document.body.appendChild app.element + }] + + # Open JSON arrays in spreadsheet + # Open text in notepad + handle = (file) -> + handler = handlers.find ({filter}) -> + filter(file) + + if handler + handler.fn(file) + else + throw new Error "No handler for files of type #{file.type}" + + Object.assign self, + # Open a file + # TODO: Pass arguments + # TODO: Drop files on an app to open them in that app + open: (file) -> + handle(file) + + return self diff --git a/system/module.coffee b/system/module.coffee index b71eadc..54f3d67 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -2,11 +2,6 @@ # # Depends on having self.readFile defined -# TODO: Move handlers out -Filter = require "../apps/filter" -TextEditor = require "../apps/text-editor" -Spreadsheet = require "../apps/spreadsheet" - module.exports = (I, self) -> ### Load a module from a file in the file system. @@ -28,56 +23,6 @@ module.exports = (I, self) -> # images, blobs, html, json ### - # TODO: Handlers that can use combined type, extension, and contents info - # to do the right thing - # Prioritize handlers falling back to others - handlers = [{ - # JavaScript - name: "Execute" - filter: (file) -> - file.type is "application/javascript" - fn: (file) -> - self.include([file.path]) - .then ([moduleExports]) -> - moduleExports - }, { - name: "Open as Text" - filter: (file) -> - file.type.match /^text\// - fn: (file) -> - editor = TextEditor() - editor.loadFile(file.blob) - document.body.appendChild editor.element - }, { - name: "Open Spreadsheet" - filter: (file) -> - # TODO: This actually only handles JSON arrays - file.type is "application/json" - fn: (file) -> - editor = Spreadsheet() - editor.loadFile(file.blob) - document.body.appendChild editor.element - }, { - name: "View Image" - filter: (file) -> - file.type.match /^image\// - fn: (file) -> - app = Filter() - app.loadFile(file.blob) - document.body.appendChild app.element - }] - - # Open JSON arrays in spreadsheet - # Open text in notepad - handle = (file) -> - handler = handlers.find ({filter}) -> - filter(file) - - if handler - handler.fn(file) - else - throw new Error "No handler for files of type #{file.type}" - {fileSeparator, normalizePath} = require "../util" # Wrap program in async include wrapper @@ -162,12 +107,6 @@ module.exports = (I, self) -> reject e Object.assign self, - # Open a file - # TODO: Pass arguments - # TODO: Drop files on an app to open them in that app - open: (file) -> - handle(file) - # still experimenting with the API # Async include in the vein of require.js # it's horrible but seems necessary diff --git a/test/system/module.coffee b/test/system/module.coffee index cf0fcaf..aaa73a2 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -1,5 +1,6 @@ require "../../extensions" Model = require "model" +Associations = require "../../system/associations" SystemModule = require "../../system/module" describe "System Module", -> @@ -68,7 +69,7 @@ describe "System Module", -> it "should return export if present", -> model = Model() - model.include SystemModule + model.include Associations, SystemModule files = "/wat.js": """ @@ -82,6 +83,7 @@ describe "System Module", -> model.open path: "/wat.js" + type: "application/javascript" .then (moduleExports) -> assert.equal moduleExports, "wat" From 4211681ba59fae79c7df13477f5d4156280779a4 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 8 Jan 2017 11:28:43 -0800 Subject: [PATCH 31/78] :dolls::facepunch: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 52 ++++++++++++++++++++++++++------------ apps/pixel.coffee | 13 +++++++++- system/associations.coffee | 38 +++++++++++++++++----------- 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index d46edad..3a71f79 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -31,12 +31,45 @@ module.exports = Explorer = (options={}) -> return if e.defaultPrevented e.preventDefault() + contextMenuHandlers = + open: -> + system.open(file) + openWith: -> #TODO + cut: -> #TODO + copy: -> #TODO + delete: -> + system.deleteFile(file.path) + rename: -> + Modal.prompt "Filename", file.path + .then (newPath) -> + if newPath + system.deleteFile(file.path) + system.writeFile(newPath, file.blob) + properties: -> #TODO + + openers = system.openersFor(file) + + openerOptions = openers.map ({name, fn}, i) -> + handlerName = "opener#{i}" + contextMenuHandlers[handlerName] = -> + fn(file) + + " #{name} -> #{handlerName}" + .join("\n") + + openWithMenu = "" + if openers.length > 0 + openWithMenu = """ + Open With + #{openerOptions} + """ + # TODO: Open With Options # TODO: Set Mime Type contextMenu = ContextMenu items: parseMenu """ Open - Open With + #{openWithMenu} - Cut Copy @@ -46,21 +79,8 @@ module.exports = Explorer = (options={}) -> - Properties """ - handlers: - open: -> - system.open(file) - openWith: -> #TODO - cut: -> #TODO - copy: -> #TODO - delete: -> - system.deleteFile(file.path) - rename: -> - Modal.prompt "Filename", file.path - .then (newPath) -> - if newPath - system.deleteFile(file.path) - system.writeFile(newPath, file.blob) - properties: -> #TODO + handlers: contextMenuHandlers + contextMenu.display inElement: document.body diff --git a/apps/pixel.coffee b/apps/pixel.coffee index ab00a71..dea7b50 100644 --- a/apps/pixel.coffee +++ b/apps/pixel.coffee @@ -8,16 +8,25 @@ module.exports = -> frame = document.createElement "iframe" frame.src = "https://danielx.net/pixel-editor/" + # TODO: Gross hack to keep track of waiting for child window to load + # May want to move it into the postmaster library + resolveLoaded = null + loadedPromise = new Promise (resolve) -> + resolveLoaded = resolve + postmaster = Postmaster() postmaster.remoteTarget = -> frame.contentWindow Object.assign postmaster, childLoaded: -> console.log "child loaded" + resolveLoaded() save: -> handlers.save() handlers = Model().include(FileIO).extend - loadFile: -> + loadFile: (blob) -> + loadedPromise.then -> + postmaster.invokeRemote "loadFile", blob newFile: -> saveData: -> postmaster.invokeRemote "getBlob" @@ -45,4 +54,6 @@ module.exports = -> width: 640 height: 480 + windowView.loadFile = handlers.loadFile + return windowView diff --git a/system/associations.coffee b/system/associations.coffee index 173610f..b79fa00 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -2,6 +2,13 @@ Filter = require "../apps/filter" TextEditor = require "../apps/text-editor" Spreadsheet = require "../apps/spreadsheet" +PixelEditor = require "../apps/pixel" + +openWith = (App) -> + (file) -> + app = App() + app.loadFile(file.blob) + document.body.appendChild app.element module.exports = (I, self) -> # TODO: Handlers that can use combined type, extension, and contents info @@ -17,30 +24,27 @@ module.exports = (I, self) -> .then ([moduleExports]) -> moduleExports }, { - name: "Open as Text" + name: "Text Editor" filter: (file) -> file.type.match /^text\// - fn: (file) -> - editor = TextEditor() - editor.loadFile(file.blob) - document.body.appendChild editor.element + fn: openWith(TextEditor) }, { - name: "Open Spreadsheet" + name: "Spreadsheet" filter: (file) -> # TODO: This actually only handles JSON arrays file.type is "application/json" - fn: (file) -> - editor = Spreadsheet() - editor.loadFile(file.blob) - document.body.appendChild editor.element + fn: openWith(Spreadsheet) }, { - name: "View Image" + name: "Image Viewer" filter: (file) -> file.type.match /^image\// - fn: (file) -> - app = Filter() - app.loadFile(file.blob) - document.body.appendChild app.element + fn: openWith(Filter) + + }, { + name: "Pixel Editor" + filter: (file) -> + file.type.match /^image\// + fn: openWith(PixelEditor) }] # Open JSON arrays in spreadsheet @@ -61,4 +65,8 @@ module.exports = (I, self) -> open: (file) -> handle(file) + openersFor: (file) -> + handlers.filter (handler) -> + handler.filter(file) + return self From 50c45e3e7aae6ad81cc9e343632bc5a985b05ce9 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 8 Jan 2017 12:12:46 -0800 Subject: [PATCH 32/78] :passport_control::tongue: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 15 +++++++++++---- system.coffee | 12 +++++++++--- system/associations.coffee | 4 ++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 3a71f79..f7535ec 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -46,6 +46,13 @@ module.exports = Explorer = (options={}) -> system.deleteFile(file.path) system.writeFile(newPath, file.blob) properties: -> #TODO + editMIMEType: -> + Modal.prompt "MIME Type", file.type + .then (newType) -> + if newType + system.updateFile file.path, + type: newType + .then console.log openers = system.openersFor(file) @@ -77,6 +84,7 @@ module.exports = Explorer = (options={}) -> Delete Rename - + Edit MIME Type Properties """ handlers: contextMenuHandlers @@ -161,10 +169,9 @@ module.exports = Explorer = (options={}) -> update() # Refresh files when they change - system.fs.on "write", (path) -> - update() - system.fs.on "delete", (path) -> - update() + system.fs.on "write", (path) -> update() + system.fs.on "delete", (path) -> update() + system.fs.on "update", (path) -> update() addWindow = (path) -> element = document.createElement "container" diff --git a/system.coffee b/system.coffee index 0c63ffe..ad2ec59 100644 --- a/system.coffee +++ b/system.coffee @@ -36,6 +36,10 @@ DexieFS = (db) -> updatedAt: now .then notify "write", path + update: (path, changes) -> + Files.update path, changes + .then notify "update", path + delete: (path) -> Files.delete(path) .then notify "delete", path @@ -72,7 +76,6 @@ module.exports = (dbName='zine-os') -> # TODO: Allow relative paths readFile: (path) -> path = normalizePath "/#{path}" - fs.read(path) .then ({blob}) -> blob @@ -80,15 +83,18 @@ module.exports = (dbName='zine-os') -> # TODO: Allow relative paths writeFile: (path, blob) -> path = normalizePath "/#{path}" - fs.write path, blob # TODO: Allow relative paths deleteFile: (path) -> path = normalizePath "/#{path}" - fs.delete(path) + # TODO: Allow relative paths + updateFile: (path, changes) -> + path = normalizePath "/#{path}" + fs.update(path, changes) + # NOTE: These are experimental commands to run code execJS: (path) -> self.readFile(path) diff --git a/system/associations.coffee b/system/associations.coffee index b79fa00..0eed4f7 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -26,7 +26,8 @@ module.exports = (I, self) -> }, { name: "Text Editor" filter: (file) -> - file.type.match /^text\// + file.type.match(/^text\//) or + file.type is "application/javascript" fn: openWith(TextEditor) }, { name: "Spreadsheet" @@ -39,7 +40,6 @@ module.exports = (I, self) -> filter: (file) -> file.type.match /^image\// fn: openWith(Filter) - }, { name: "Pixel Editor" filter: (file) -> From a08b61187b5674abd764dbfb14265233f2706b50 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 8 Jan 2017 19:32:42 -0800 Subject: [PATCH 33/78] :mailbox_with_no_mail::man_with_gua_pi_mao: Updated at https://danielx.net/editor/ --- apps/filter.coffee | 2 +- os/file-io.coffee | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/filter.coffee b/apps/filter.coffee index f432f8a..e66b3e5 100644 --- a/apps/filter.coffee +++ b/apps/filter.coffee @@ -42,7 +42,7 @@ module.exports = -> menuBar: menuBar.element width: 640 height: 480 - + windowView.loadFile = handlers.loadFile return windowView diff --git a/os/file-io.coffee b/os/file-io.coffee index 33b8198..93a2471 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -29,6 +29,11 @@ module.exports = (I, self) -> # TODO: Prompt if unsaved # TODO: File browser Modal.prompt "File Path", currentPath + .then (newPath) -> + if newPath + currentPath = newPath + else + throw new Error "No path given" .then system.readFile .then self.loadFile From 608ee08cd360d377ab69a64aa5ff1917d5d0b34a Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 8 Jan 2017 20:04:44 -0800 Subject: [PATCH 34/78] :clock1::stuck_out_tongue: Updated at https://danielx.net/editor/ --- style.styl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/style.styl b/style.styl index 9a6752f..b687f91 100644 --- a/style.styl +++ b/style.styl @@ -1,3 +1,9 @@ +user-select(s) + user-select: s + -moz-user-select: s + -webkit-user-select: s + -ms-user-select: s + comments, comment display: block padding: 1em @@ -20,6 +26,7 @@ comment explorer display: block padding: 0.5em + user-select: none body > & height: 100% From 1897da016990dcb4f5e4cf0af92572d8ec1a13f6 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 9 Jan 2017 20:02:11 -0800 Subject: [PATCH 35/78] :pig2::eyes: Updated at https://danielx.net/editor/ --- apps/markdown.coffee | 43 ++++++++++++++++++++++++++++++++++++++ pixie.cson | 3 ++- system/associations.coffee | 6 ++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 apps/markdown.coffee diff --git a/apps/markdown.coffee b/apps/markdown.coffee new file mode 100644 index 0000000..22921b3 --- /dev/null +++ b/apps/markdown.coffee @@ -0,0 +1,43 @@ +# Render Markdown + +FileIO = require "../os/file-io" +Model = require "model" + +module.exports = -> + # Global system + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + + canvas = document.createElement 'div' + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + blob.readAsText() + .then (textContent) -> + canvas.innerHTML = marked(textContent) + + saveData: -> + + exit: -> + windowView.element.remove() + + menuBar = MenuBar + items: parseMenu """ + [F]ile + [O]pen + [S]ave + Save [A]s + - + E[x]it + """ + handlers: handlers + + windowView = Window + title: "Markdown" + content: canvas + menuBar: menuBar.element + width: 640 + height: 480 + + windowView.loadFile = handlers.loadFile + + return windowView diff --git a/pixie.cson b/pixie.cson index af83104..d4c7d40 100644 --- a/pixie.cson +++ b/pixie.cson @@ -5,8 +5,9 @@ dependencies: postmaster: "distri/postmaster:v0.5.0" ui: "STRd6/ui:master" remoteDependencies: [ - "https://unpkg.com/dexie@2.0.0-beta.7/dist/dexie.js" + "https://cdnjs.cloudflare.com/ajax/libs/dexie/2.0.0-beta.7/dexie.min.js" "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" "https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js" + "https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js" ] diff --git a/system/associations.coffee b/system/associations.coffee index 0eed4f7..1f7c07b 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -3,6 +3,7 @@ Filter = require "../apps/filter" TextEditor = require "../apps/text-editor" Spreadsheet = require "../apps/spreadsheet" PixelEditor = require "../apps/pixel" +Markdown = require "../apps/markdown" openWith = (App) -> (file) -> @@ -23,6 +24,11 @@ module.exports = (I, self) -> self.include([file.path]) .then ([moduleExports]) -> moduleExports + }, { + name: "Markdown" + filter: (file) -> + file.path.match /\.md$/ + fn: openWith(Markdown) }, { name: "Text Editor" filter: (file) -> From 7c5cfce043c890b7d6c635f0b8cb7b30cea59dd3 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 9 Jan 2017 20:36:33 -0800 Subject: [PATCH 36/78] :arrow_down_small::persevere: Updated at https://danielx.net/editor/ --- apps/markdown.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/markdown.coffee b/apps/markdown.coffee index 22921b3..3ac9d88 100644 --- a/apps/markdown.coffee +++ b/apps/markdown.coffee @@ -7,13 +7,14 @@ module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI - canvas = document.createElement 'div' + container = document.createElement 'container' + container.style.padding = "1em" handlers = Model().include(FileIO).extend loadFile: (blob) -> blob.readAsText() .then (textContent) -> - canvas.innerHTML = marked(textContent) + container.innerHTML = marked(textContent) saveData: -> @@ -33,7 +34,7 @@ module.exports = -> windowView = Window title: "Markdown" - content: canvas + content: container menuBar: menuBar.element width: 640 height: 480 From f7b53ec9d8886321b8a0af1ddef48257cd23c7af Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 10 Jan 2017 20:15:17 -0800 Subject: [PATCH 37/78] :smiley::four_leaf_clover: Updated at https://danielx.net/editor/ --- TODO.md | 22 +++++++++++++--------- apps/filter.coffee | 10 ++++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/TODO.md b/TODO.md index b02925f..9fc0f2f 100644 --- a/TODO.md +++ b/TODO.md @@ -3,15 +3,15 @@ TODO System Features --------------- -[ ] File Browser +[x] File Browser -[ ] App Associations +[x] App Associations -[ ] File Context Menu +[x] File Context Menu -[ ] Folders +[x] Folders -[ ] Desktop Icons +[x] Desktop Icons [ ] Desktop Background @@ -34,13 +34,17 @@ Spreadsheet Tactics Game Sandbox +Markdown / Wiki + +Image Munger + Network Social -------------- -CDN +[ ] CDN -Comments +[x] Comments -Sharing +[ ] Sharing -Remote Files +[ ] Remote Files diff --git a/apps/filter.coffee b/apps/filter.coffee index e66b3e5..71b3c8e 100644 --- a/apps/filter.coffee +++ b/apps/filter.coffee @@ -1,4 +1,4 @@ -# Filter Images +# View and Manipulate Images FileIO = require "../os/file-io" Model = require "model" @@ -25,6 +25,9 @@ module.exports = -> exit: -> windowView.element.remove() + crop: -> + Modal + menuBar = MenuBar items: parseMenu """ [F]ile @@ -33,11 +36,14 @@ module.exports = -> Save [A]s - E[x]it + [E]dit + [C]rop + [F]ilter """ handlers: handlers windowView = Window - title: "Filter Booth" + title: "Spectacle Image Viewer" content: canvas menuBar: menuBar.element width: 640 From c22b5b029671367e4f97e9c1319e6914f2c0a901 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 10 Jan 2017 20:37:00 -0800 Subject: [PATCH 38/78] :pencil::ship: Updated at https://danielx.net/editor/ --- apps/filter.coffee | 13 +- lib/hamlet.js | 2451 ++++++++++++++++++++++++++++++++++++++++ main.coffee | 2 + system.coffee | 3 +- system/template.coffee | 12 + 5 files changed, 2479 insertions(+), 2 deletions(-) create mode 100644 lib/hamlet.js create mode 100644 system/template.coffee diff --git a/apps/filter.coffee b/apps/filter.coffee index 71b3c8e..ed76ed3 100644 --- a/apps/filter.coffee +++ b/apps/filter.coffee @@ -9,6 +9,16 @@ module.exports = -> canvas = document.createElement 'canvas' context = canvas.getContext('2d') + + modalForm = system.compileTemplate """ + form + label + h2 Width + input(name="width") + label + h2 Height + input(name="height") + """ handlers = Model().include(FileIO).extend loadFile: (blob) -> @@ -26,7 +36,8 @@ module.exports = -> windowView.element.remove() crop: -> - Modal + Modal.form modalForm() + .then console.log menuBar = MenuBar items: parseMenu """ diff --git a/lib/hamlet.js b/lib/hamlet.js new file mode 100644 index 0000000..b18539c --- /dev/null +++ b/lib/hamlet.js @@ -0,0 +1,2451 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Hamlet = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o", indentText(contents.join("\n")), " return"]; + }, + buffer: function(value) { + return ["" + ROOT_NAME + ".buffer " + value]; + }, + attributes: function(node) { + var attributeLines, attributes, classes, id, ids, idsAndClasses; + id = node.id, classes = node.classes, attributes = node.attributes; + if (id) { + ids = [JSON.stringify(id)]; + } else { + ids = []; + } + classes = (classes || []).map(JSON.stringify); + if (attributes) { + attributes = attributes.filter(function(_arg) { + var name, value; + name = _arg.name, value = _arg.value; + if (name === "class") { + classes.push(value); + return false; + } else if (name === "id") { + ids.push(value); + return false; + } else { + return true; + } + }); + } else { + attributes = []; + } + idsAndClasses = []; + if (ids.length) { + idsAndClasses.push("id: [" + (ids.join(', ')) + "]"); + } + if (classes.length) { + idsAndClasses.push("class: [" + (classes.join(', ')) + "]"); + } + attributeLines = attributes.map(function(_arg) { + var name, value; + name = _arg.name, value = _arg.value; + name = JSON.stringify(name); + return "" + name + ": " + value; + }); + return idsAndClasses.concat(attributeLines); + }, + render: function(node) { + var filter, tag, text; + tag = node.tag, filter = node.filter, text = node.text; + if (tag) { + return this.tag(node); + } else if (filter) { + return this.filter(node); + } else { + return this.contents(node); + } + }, + filter: function(node) { + var filter, filterName; + filterName = node.filter; + if (filter = this.filters[filterName]) { + return [].concat.apply([], this.filters[filterName](node.content, this)); + } else { + return ["" + ROOT_NAME + ".filter(" + (JSON.stringify(filterName)) + ", " + (JSON.stringify(node.content)) + ")"]; + } + }, + contents: function(node) { + var bufferedCode, childContent, children, contents, indent, text, unbufferedCode; + children = node.children, bufferedCode = node.bufferedCode, unbufferedCode = node.unbufferedCode, text = node.text; + if (unbufferedCode) { + indent = true; + contents = [unbufferedCode]; + } else if (bufferedCode) { + contents = this.buffer(bufferedCode); + } else if (text) { + contents = this.buffer(JSON.stringify(text)); + } else if (node.tag) { + contents = []; + } else if (node.comment) { + return []; + } else { + contents = []; + console.warn("No content for node:", node); + } + if (children) { + childContent = this.renderNodes(children); + if (indent) { + childContent = this.indent(childContent.join("\n")); + } + contents = contents.concat(childContent); + } + return contents; + }, + renderNodes: function(nodes) { + return [].concat.apply([], nodes.map(this.render, this)); + }, + tag: function(node) { + var tag; + tag = node.tag; + return this.element(tag, this.attributes(node), this.contents(node)); + } + }; + + exports.compile = function(parseTree, _arg) { + var compiler, exports, items, options, program, programSource, runtime, source, _ref; + _ref = _arg != null ? _arg : {}, compiler = _ref.compiler, runtime = _ref.runtime, exports = _ref.exports; + if (runtime == null) { + runtime = "require" + "(\"hamlet-runtime\")"; + } + if (exports == null) { + exports = "module.exports"; + } + items = util.renderNodes(parseTree); + if (exports) { + exports = "" + exports + " = "; + } else { + exports = ""; + } + source = "" + exports + "(data) ->\n \"use strict\"\n (->\n " + ROOT_NAME + " = " + runtime + "(this)\n\n" + (util.indent(items.join("\n"), " ")) + "\n return " + ROOT_NAME + ".root\n ).call(data)"; + options = { + bare: true + }; + programSource = source; + program = compiler.compile(programSource, options); + return program; + }; + +}).call(this); + +},{}],5:[function(require,module,exports){ +// Generated by CoffeeScript 1.7.1 +(function() { + var compile, parser; + + compile = require("./compiler").compile; + + parser = require("hamlet-parser"); + + module.exports = { + compile: function(input, options) { + if (options == null) { + options = {}; + } + if (typeof input === "string") { + input = parser.parse(input, options.mode); + } + return compile(input, options); + } + }; + +}).call(this); + +},{"./compiler":4,"hamlet-parser":8}],6:[function(require,module,exports){ +/* generated by jison-lex 0.2.1 */ +var haml_lexer = (function(){ +var lexer = { + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input) { + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len - 1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {"moduleName":"haml_lexer"}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:return 'SEPARATOR'; +break; +case 1:this.popState(); return 'RIGHT_PARENTHESIS'; +break; +case 2:return 'ATTRIBUTE'; +break; +case 3:this.begin('value'); return 'EQUAL'; +break; +case 4:return 'AT_ATTRIBUTE'; +break; +case 5:this.popState(); return 'ATTRIBUTE_VALUE'; +break; +case 6:this.popState(); return 'ATTRIBUTE_VALUE'; +break; +case 7:this.popState(); return 'ATTRIBUTE_VALUE'; +break; +case 8:yy.indent = 0; this.popState(); return 'NEWLINE'; +break; +case 9:return 'FILTER_LINE'; +break; +case 10:yy.indent = 0; return 'NEWLINE'; +break; +case 11:yy.indent += 1; if(yy.indent > yy.filterIndent){this.begin('filter'); }; return 'INDENT'; +break; +case 12:this.begin("parentheses_attributes"); return 'LEFT_PARENTHESIS'; +break; +case 13:yy_.yytext = yy_.yytext.substring(1); return 'COMMENT'; +break; +case 14:yy.filterIndent = yy.indent; yy_.yytext = yy_.yytext.substring(1); return 'FILTER'; +break; +case 15:yy_.yytext = yy_.yytext.substring(1); return 'ID'; +break; +case 16:yy_.yytext = yy_.yytext.substring(1); return 'CLASS'; +break; +case 17:yy_.yytext = yy_.yytext.substring(1); return 'TAG'; +break; +case 18:yy_.yytext = yy_.yytext.substring(1).trim(); return 'BUFFERED_CODE'; +break; +case 19:yy_.yytext = yy_.yytext.substring(1).trim(); return 'UNBUFFERED_CODE'; +break; +case 20:yy_.yytext = yy_.yytext.trim(); return 'TEXT'; +break; +} +}, +rules: [/^(?:[ \t]+)/,/^(?:\))/,/^(?:([_a-zA-Z][-_a-zA-Z0-9]*))/,/^(?:=)/,/^(?:@([_a-zA-Z][-_a-zA-Z0-9]*))/,/^(?:"(\\.|[^\\"])*")/,/^(?:'(\\.|[^\\'])*')/,/^(?:[^ \t\)]*)/,/^(?:(\n|$))/,/^(?:[^\n]*)/,/^(?:\s*(\n|$))/,/^(?:( |\\t))/,/^(?:\()/,/^(?:\/.*)/,/^(?::([_a-zA-Z][-_a-zA-Z0-9]*))/,/^(?:#((:|[A-Z]|_|[a-z])((:|[A-Z]|_|[a-z])|-|[0-9])*(?!-)))/,/^(?:\.((:|[A-Z]|_|[a-z])((:|[A-Z]|_|[a-z])|-|[0-9])*(?!-)))/,/^(?:%((:|[A-Z]|_|[a-z])((:|[A-Z]|_|[a-z])|-|[0-9])*(?!-)))/,/^(?:=.*)/,/^(?:-.*)/,/^(?:.*)/], +conditions: {"filter":{"rules":[8,9],"inclusive":false},"value":{"rules":[5,6,7],"inclusive":false},"parentheses_attributes":{"rules":[0,1,2,3,4],"inclusive":false},"INITIAL":{"rules":[10,11,12,13,14,15,16,17,18,19,20],"inclusive":true}} +}; +return lexer; +})();module.exports = haml_lexer; + +},{}],7:[function(require,module,exports){ +/* generated by jison-lex 0.2.1 */ +var jade_lexer = (function(){ +var lexer = { + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input) { + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len - 1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {"moduleName":"jade_lexer"}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:return 'SEPARATOR'; +break; +case 1:this.popState(); return 'RIGHT_PARENTHESIS'; +break; +case 2:return 'ATTRIBUTE'; +break; +case 3:this.begin('value'); return 'EQUAL'; +break; +case 4:return 'AT_ATTRIBUTE'; +break; +case 5:this.popState(); return 'ATTRIBUTE_VALUE'; +break; +case 6:this.popState(); return 'ATTRIBUTE_VALUE'; +break; +case 7:this.popState(); return 'ATTRIBUTE_VALUE'; +break; +case 8:yy.indent = 0; this.popState(); return 'NEWLINE'; +break; +case 9:return 'FILTER_LINE'; +break; +case 10:yy.indent = 0; return 'NEWLINE'; +break; +case 11:yy.indent += 1; if(yy.indent > yy.filterIndent){this.begin('filter'); }; return 'INDENT'; +break; +case 12:this.begin("parentheses_attributes"); return 'LEFT_PARENTHESIS'; +break; +case 13:yy_.yytext = yy_.yytext.substring(2); return 'COMMENT'; +break; +case 14:yy.filterIndent = yy.indent; yy_.yytext = yy_.yytext.substring(1); return 'FILTER'; +break; +case 15:yy_.yytext = yy_.yytext.substring(1); return 'ID'; +break; +case 16:yy_.yytext = yy_.yytext.substring(1); return 'CLASS'; +break; +case 17:return 'TAG'; +break; +case 18:yy_.yytext = yy_.yytext.substring(1).trim(); return 'BUFFERED_CODE'; +break; +case 19:yy_.yytext = yy_.yytext.substring(1).trim(); return 'UNBUFFERED_CODE'; +break; +case 20:yy_.yytext = yy_.yytext.trim(); return 'TEXT'; +break; +} +}, +rules: [/^(?:[ \t]+)/,/^(?:\))/,/^(?:([_a-zA-Z][-_a-zA-Z0-9]*))/,/^(?:=)/,/^(?:@([_a-zA-Z][-_a-zA-Z0-9]*))/,/^(?:"(\\.|[^\\"])*")/,/^(?:'(\\.|[^\\'])*')/,/^(?:[^ \t\)]*)/,/^(?:(\n|$))/,/^(?:[^\n]*)/,/^(?:\s*(\n|$))/,/^(?:( |\\t))/,/^(?:\()/,/^(?:\/\/.*)/,/^(?::([_a-zA-Z][-_a-zA-Z0-9]*))/,/^(?:#((:|[A-Z]|_|[a-z])((:|[A-Z]|_|[a-z])|-|[0-9])*(?!-)))/,/^(?:\.((:|[A-Z]|_|[a-z])((:|[A-Z]|_|[a-z])|-|[0-9])*(?!-)))/,/^(?:((:|[A-Z]|_|[a-z])((:|[A-Z]|_|[a-z])|-|[0-9])*(?!-)))/,/^(?:=.*)/,/^(?:-.*)/,/^(?:.*)/], +conditions: {"filter":{"rules":[8,9],"inclusive":false},"value":{"rules":[5,6,7],"inclusive":false},"parentheses_attributes":{"rules":[0,1,2,3,4],"inclusive":false},"INITIAL":{"rules":[10,11,12,13,14,15,16,17,18,19,20],"inclusive":true}} +}; +return lexer; +})();module.exports = jade_lexer; + +},{}],8:[function(require,module,exports){ +// Generated by CoffeeScript 1.7.1 +(function() { + var extend, lexers, oldParse, parser, + __slice = [].slice; + + parser = require("./parser").parser; + + lexers = { + haml: require("./haml_lexer"), + jade: require("./jade_lexer") + }; + + extend = function() { + var name, source, sources, target, _i, _len; + target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + for (_i = 0, _len = sources.length; _i < _len; _i++) { + source = sources[_i]; + for (name in source) { + target[name] = source[name]; + } + } + return target; + }; + + oldParse = parser.parse; + + extend(parser, { + parse: function(input, mode) { + if (mode == null) { + mode = "haml"; + } + parser.lexer = lexers[mode]; + extend(parser.yy, { + indent: 0, + nodePath: [ + { + children: [] + } + ], + filterIndent: void 0 + }); + return oldParse.call(parser, input); + } + }); + + extend(parser.yy, { + extend: extend, + newline: function() { + var lastNode; + lastNode = this.nodePath[this.nodePath.length - 1]; + if (lastNode.filter) { + return this.appendFilterContent(lastNode, ""); + } + }, + lastParent: function(indentation) { + var parent; + while (!(parent = this.nodePath[indentation])) { + indentation -= 1; + } + return parent; + }, + append: function(node, indentation) { + var index, lastNode, parent; + if (indentation == null) { + indentation = 0; + } + if (node.filterLine) { + lastNode = this.nodePath[this.nodePath.length - 1]; + this.appendFilterContent(lastNode, node.filterLine); + return; + } + parent = this.lastParent(indentation); + this.appendChild(parent, node); + index = indentation + 1; + this.nodePath[index] = node; + this.nodePath.length = index + 1; + return node; + }, + appendChild: function(parent, child) { + if (!child.filter) { + this.filterIndent = void 0; + this.lexer.popState(); + } + parent.children || (parent.children = []); + return parent.children.push(child); + }, + appendFilterContent: function(filter, content) { + filter.content || (filter.content = ""); + return filter.content += "" + content + "\n"; + } + }); + + module.exports = parser; + +}).call(this); + +},{"./haml_lexer":6,"./jade_lexer":7,"./parser":9}],9:[function(require,module,exports){ +(function (process){ +/* parser generated by jison 0.4.6 */ +/* + Returns a Parser object of the following structure: + + Parser: { + yy: {} + } + + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + + + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + + + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var parser = (function(){ +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"root":3,"lines":4,"line":5,"indentation":6,"indentationLevel":7,"INDENT":8,"lineMain":9,"end":10,"tag":11,"rest":12,"COMMENT":13,"FILTER":14,"FILTER_LINE":15,"NEWLINE":16,"name":17,"tagComponents":18,"attributes":19,"idComponent":20,"classComponents":21,"ID":22,"CLASS":23,"LEFT_PARENTHESIS":24,"attributePairs":25,"RIGHT_PARENTHESIS":26,"SEPARATOR":27,"attributePair":28,"ATTRIBUTE":29,"EQUAL":30,"ATTRIBUTE_VALUE":31,"AT_ATTRIBUTE":32,"TAG":33,"BUFFERED_CODE":34,"UNBUFFERED_CODE":35,"TEXT":36,"$accept":0,"$end":1}, +terminals_: {2:"error",8:"INDENT",13:"COMMENT",14:"FILTER",15:"FILTER_LINE",16:"NEWLINE",22:"ID",23:"CLASS",24:"LEFT_PARENTHESIS",26:"RIGHT_PARENTHESIS",27:"SEPARATOR",29:"ATTRIBUTE",30:"EQUAL",31:"ATTRIBUTE_VALUE",32:"AT_ATTRIBUTE",33:"TAG",34:"BUFFERED_CODE",35:"UNBUFFERED_CODE",36:"TEXT"}, +productions_: [0,[3,1],[4,2],[4,1],[6,0],[6,1],[7,2],[7,1],[5,3],[5,1],[9,2],[9,1],[9,1],[9,1],[9,1],[9,1],[10,1],[11,2],[11,2],[11,1],[11,1],[18,3],[18,2],[18,2],[18,2],[18,1],[18,1],[20,1],[21,2],[21,1],[19,3],[25,3],[25,1],[28,3],[28,1],[17,1],[12,1],[12,1],[12,1]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1:return this.$ = yy.nodePath[0].children; +break; +case 2:this.$ = $$[$0-1]; +break; +case 3:this.$ = $$[$0]; +break; +case 4:this.$ = 0; +break; +case 5:this.$ = $$[$0]; +break; +case 6:this.$ = $$[$0-1] + 1; +break; +case 7:this.$ = 1; +break; +case 8:this.$ = yy.append($$[$0-1], $$[$0-2]); +break; +case 9:this.$ = (function () { + if ($$[$0].newline) { + return yy.newline(); + } + }()); +break; +case 10:this.$ = yy.extend($$[$0-1], $$[$0]); +break; +case 11:this.$ = $$[$0]; +break; +case 12:this.$ = $$[$0]; +break; +case 13:this.$ = { + comment: $$[$0] + }; +break; +case 14:this.$ = { + filter: $$[$0] + }; +break; +case 15:this.$ = { + filterLine: $$[$0] + }; +break; +case 16:this.$ = { + newline: true + }; +break; +case 17:this.$ = (function () { + $$[$0].tag = $$[$0-1]; + return $$[$0]; + }()); +break; +case 18:this.$ = { + tag: $$[$0-1], + attributes: $$[$0] + }; +break; +case 19:this.$ = { + tag: $$[$0] + }; +break; +case 20:this.$ = yy.extend($$[$0], { + tag: "div" + }); +break; +case 21:this.$ = { + id: $$[$0-2], + classes: $$[$0-1], + attributes: $$[$0] + }; +break; +case 22:this.$ = { + id: $$[$0-1], + attributes: $$[$0] + }; +break; +case 23:this.$ = { + classes: $$[$0-1], + attributes: $$[$0] + }; +break; +case 24:this.$ = { + id: $$[$0-1], + classes: $$[$0] + }; +break; +case 25:this.$ = { + id: $$[$0] + }; +break; +case 26:this.$ = { + classes: $$[$0] + }; +break; +case 27:this.$ = $$[$0]; +break; +case 28:this.$ = $$[$0-1].concat($$[$0]); +break; +case 29:this.$ = [$$[$0]]; +break; +case 30:this.$ = $$[$0-1]; +break; +case 31:this.$ = $$[$0-2].concat($$[$0]); +break; +case 32:this.$ = [$$[$0]]; +break; +case 33:this.$ = { + name: $$[$0-2], + value: $$[$0] + }; +break; +case 34:this.$ = { + name: $$[$0].substring(1), + value: $$[$0] + }; +break; +case 35:this.$ = $$[$0]; +break; +case 36:this.$ = { + bufferedCode: $$[$0] + }; +break; +case 37:this.$ = { + unbufferedCode: $$[$0] + }; +break; +case 38:this.$ = { + text: $$[$0] + "\n" + }; +break; +} +}, +table: [{3:1,4:2,5:3,6:4,7:6,8:[1,8],10:5,13:[2,4],14:[2,4],15:[2,4],16:[1,7],22:[2,4],23:[2,4],33:[2,4],34:[2,4],35:[2,4],36:[2,4]},{1:[3]},{1:[2,1],5:9,6:4,7:6,8:[1,8],10:5,13:[2,4],14:[2,4],15:[2,4],16:[1,7],22:[2,4],23:[2,4],33:[2,4],34:[2,4],35:[2,4],36:[2,4]},{1:[2,3],8:[2,3],13:[2,3],14:[2,3],15:[2,3],16:[2,3],22:[2,3],23:[2,3],33:[2,3],34:[2,3],35:[2,3],36:[2,3]},{9:10,11:11,12:12,13:[1,13],14:[1,14],15:[1,15],17:16,18:17,20:22,21:23,22:[1,24],23:[1,25],33:[1,21],34:[1,18],35:[1,19],36:[1,20]},{1:[2,9],8:[2,9],13:[2,9],14:[2,9],15:[2,9],16:[2,9],22:[2,9],23:[2,9],33:[2,9],34:[2,9],35:[2,9],36:[2,9]},{8:[1,26],13:[2,5],14:[2,5],15:[2,5],22:[2,5],23:[2,5],33:[2,5],34:[2,5],35:[2,5],36:[2,5]},{1:[2,16],8:[2,16],13:[2,16],14:[2,16],15:[2,16],16:[2,16],22:[2,16],23:[2,16],33:[2,16],34:[2,16],35:[2,16],36:[2,16]},{8:[2,7],13:[2,7],14:[2,7],15:[2,7],22:[2,7],23:[2,7],33:[2,7],34:[2,7],35:[2,7],36:[2,7]},{1:[2,2],8:[2,2],13:[2,2],14:[2,2],15:[2,2],16:[2,2],22:[2,2],23:[2,2],33:[2,2],34:[2,2],35:[2,2],36:[2,2]},{10:27,16:[1,7]},{12:28,16:[2,11],34:[1,18],35:[1,19],36:[1,20]},{16:[2,12]},{16:[2,13]},{16:[2,14]},{16:[2,15]},{16:[2,19],18:29,19:30,20:22,21:23,22:[1,24],23:[1,25],24:[1,31],34:[2,19],35:[2,19],36:[2,19]},{16:[2,20],34:[2,20],35:[2,20],36:[2,20]},{16:[2,36]},{16:[2,37]},{16:[2,38]},{16:[2,35],22:[2,35],23:[2,35],24:[2,35],34:[2,35],35:[2,35],36:[2,35]},{16:[2,25],19:33,21:32,23:[1,25],24:[1,31],34:[2,25],35:[2,25],36:[2,25]},{16:[2,26],19:34,23:[1,35],24:[1,31],34:[2,26],35:[2,26],36:[2,26]},{16:[2,27],23:[2,27],24:[2,27],34:[2,27],35:[2,27],36:[2,27]},{16:[2,29],23:[2,29],24:[2,29],34:[2,29],35:[2,29],36:[2,29]},{8:[2,6],13:[2,6],14:[2,6],15:[2,6],22:[2,6],23:[2,6],33:[2,6],34:[2,6],35:[2,6],36:[2,6]},{1:[2,8],8:[2,8],13:[2,8],14:[2,8],15:[2,8],16:[2,8],22:[2,8],23:[2,8],33:[2,8],34:[2,8],35:[2,8],36:[2,8]},{16:[2,10]},{16:[2,17],34:[2,17],35:[2,17],36:[2,17]},{16:[2,18],34:[2,18],35:[2,18],36:[2,18]},{25:36,28:37,29:[1,38],32:[1,39]},{16:[2,24],19:40,23:[1,35],24:[1,31],34:[2,24],35:[2,24],36:[2,24]},{16:[2,22],34:[2,22],35:[2,22],36:[2,22]},{16:[2,23],34:[2,23],35:[2,23],36:[2,23]},{16:[2,28],23:[2,28],24:[2,28],34:[2,28],35:[2,28],36:[2,28]},{26:[1,41],27:[1,42]},{26:[2,32],27:[2,32]},{30:[1,43]},{26:[2,34],27:[2,34]},{16:[2,21],34:[2,21],35:[2,21],36:[2,21]},{16:[2,30],34:[2,30],35:[2,30],36:[2,30]},{28:44,29:[1,38],32:[1,39]},{31:[1,45]},{26:[2,31],27:[2,31]},{26:[2,33],27:[2,33]}], +defaultActions: {12:[2,12],13:[2,13],14:[2,14],15:[2,15],18:[2,36],19:[2,37],20:[2,38],28:[2,10]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + throw new Error(str); + } +}, +parse: function parse(input) { + var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + this.lexer.setInput(input); + this.lexer.yy = this.yy; + this.yy.lexer = this.lexer; + this.yy.parser = this; + if (typeof this.lexer.yylloc == 'undefined') { + this.lexer.yylloc = {}; + } + var yyloc = this.lexer.yylloc; + lstack.push(yyloc); + var ranges = this.lexer.options && this.lexer.options.ranges; + if (typeof this.yy.parseError === 'function') { + this.parseError = this.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + function lex() { + var token; + token = self.lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push('\'' + this.terminals_[p] + '\''); + } + } + if (this.lexer.showPosition) { + errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + this.lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; + } else { + errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); + } + this.parseError(errStr, { + text: this.lexer.match, + token: this.terminals_[symbol] || symbol, + line: this.lexer.yylineno, + loc: yyloc, + expected: expected + }); + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(this.lexer.yytext); + lstack.push(this.lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + yyloc = this.lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }; + if (ranges) { + yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]; + } + r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); + if (typeof r !== 'undefined') { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +}}; +undefined +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); + + +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = parser; +exports.Parser = parser.Parser; +exports.parse = function () { return parser.parse.apply(parser, arguments); }; +exports.main = function commonjsMain(args) { + if (!args[1]) { + console.log('Usage: '+args[0]+' FILE'); + process.exit(1); + } + var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); + return exports.parser.parse(source); +}; +if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); +} +} +}).call(this,require('_process')) +},{"_process":12,"fs":3,"path":11}],10:[function(require,module,exports){ +(function (global){ +// Generated by CoffeeScript 1.8.0 +(function() { + var Observable, PROXY_LENGTH, computeDependencies, copy, extend, flatten, get, last, magicDependency, remove, splat, tryCallWithFinallyPop, + __slice = [].slice; + + module.exports = Observable = function(value, context) { + var changed, fn, listeners, notify, notifyReturning, self; + if (typeof (value != null ? value.observe : void 0) === "function") { + return value; + } + listeners = []; + notify = function(newValue) { + return copy(listeners).forEach(function(listener) { + return listener(newValue); + }); + }; + if (typeof value === 'function') { + fn = value; + self = function() { + magicDependency(self); + return value; + }; + changed = function() { + value = computeDependencies(self, fn, changed, context); + return notify(value); + }; + changed(); + } else { + self = function(newValue) { + if (arguments.length > 0) { + if (value !== newValue) { + value = newValue; + notify(newValue); + } + } else { + magicDependency(self); + } + return value; + }; + } + self.each = function(callback) { + magicDependency(self); + if (value != null) { + [value].forEach(function(item) { + return callback.call(item, item); + }); + } + return self; + }; + if (Array.isArray(value)) { + ["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf", "map", "reduce", "reduceRight", "slice", "some"].forEach(function(method) { + return self[method] = function() { + var args; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + magicDependency(self); + return value[method].apply(value, args); + }; + }); + ["pop", "push", "reverse", "shift", "splice", "sort", "unshift"].forEach(function(method) { + return self[method] = function() { + var args; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return notifyReturning(value[method].apply(value, args)); + }; + }); + if (PROXY_LENGTH) { + Object.defineProperty(self, 'length', { + get: function() { + magicDependency(self); + return value.length; + }, + set: function(length) { + value.length = length; + return notifyReturning(value.length); + } + }); + } + notifyReturning = function(returnValue) { + notify(value); + return returnValue; + }; + extend(self, { + each: function(callback) { + self.forEach(function(item, index) { + return callback.call(item, item, index, self); + }); + return self; + }, + remove: function(object) { + var index; + index = value.indexOf(object); + if (index >= 0) { + return notifyReturning(value.splice(index, 1)[0]); + } + }, + get: function(index) { + magicDependency(self); + return value[index]; + }, + first: function() { + magicDependency(self); + return value[0]; + }, + last: function() { + magicDependency(self); + return value[value.length - 1]; + }, + size: function() { + magicDependency(self); + return value.length; + } + }); + } + extend(self, { + listeners: listeners, + observe: function(listener) { + return listeners.push(listener); + }, + stopObserving: function(fn) { + return remove(listeners, fn); + }, + toggle: function() { + return self(!value); + }, + increment: function(n) { + return self(value + n); + }, + decrement: function(n) { + return self(value - n); + }, + toString: function() { + return "Observable(" + value + ")"; + } + }); + return self; + }; + + Observable.concat = function() { + var arg, args, collection, i, o, _i, _len; + args = new Array(arguments.length); + for (i = _i = 0, _len = arguments.length; _i < _len; i = ++_i) { + arg = arguments[i]; + args[i] = arguments[i]; + } + collection = Observable(args); + o = Observable(function() { + return flatten(collection.map(splat)); + }); + o.push = collection.push; + return o; + }; + + extend = function(target) { + var i, name, source, _i, _len; + for (i = _i = 0, _len = arguments.length; _i < _len; i = ++_i) { + source = arguments[i]; + if (i > 0) { + for (name in source) { + target[name] = source[name]; + } + } + } + return target; + }; + + global.OBSERVABLE_ROOT_HACK = []; + + magicDependency = function(self) { + var observerSet; + observerSet = last(global.OBSERVABLE_ROOT_HACK); + if (observerSet) { + return observerSet.add(self); + } + }; + + tryCallWithFinallyPop = function(fn, context) { + try { + return fn.call(context); + } finally { + global.OBSERVABLE_ROOT_HACK.pop(); + } + }; + + computeDependencies = function(self, fn, update, context) { + var deps, value, _ref; + deps = new Set; + global.OBSERVABLE_ROOT_HACK.push(deps); + value = tryCallWithFinallyPop(fn, context); + if ((_ref = self._deps) != null) { + _ref.forEach(function(observable) { + return observable.stopObserving(update); + }); + } + self._deps = deps; + deps.forEach(function(observable) { + return observable.observe(update); + }); + return value; + }; + + try { + Object.defineProperty((function() {}), 'length', { + get: function() {}, + set: function() {} + }); + PROXY_LENGTH = true; + } catch (_error) { + PROXY_LENGTH = false; + } + + remove = function(array, value) { + var index; + index = array.indexOf(value); + if (index >= 0) { + return array.splice(index, 1)[0]; + } + }; + + copy = function(array) { + return array.concat([]); + }; + + get = function(arg) { + if (typeof arg === "function") { + return arg(); + } else { + return arg; + } + }; + + splat = function(item) { + var result, results; + results = []; + if (item == null) { + return results; + } + if (typeof item.forEach === "function") { + item.forEach(function(i) { + return results.push(i); + }); + } else { + result = get(item); + if (result != null) { + results.push(result); + } + } + return results; + }; + + last = function(array) { + return array[array.length - 1]; + }; + + flatten = function(array) { + return array.reduce(function(a, b) { + return a.concat(b); + }, []); + }; + +}).call(this); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],11:[function(require,module,exports){ +(function (process){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; +var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); +}; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = substr(path, -1) === '/'; + + // Normalize the path + path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + +// posix version +exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; +}; + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); +}; + + +// path.relative(from, to) +// posix version +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +exports.sep = '/'; +exports.delimiter = ':'; + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +function filter (xs, f) { + if (xs.filter) return xs.filter(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + if (f(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// String.prototype.substr - negative index don't work in IE8 +var substr = 'ab'.substr(-1) === 'b' + ? function (str, start, len) { return str.substr(start, len) } + : function (str, start, len) { + if (start < 0) start = str.length + start; + return str.substr(start, len); + } +; + +}).call(this,require('_process')) +},{"_process":12}],12:[function(require,module,exports){ +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],13:[function(require,module,exports){ +module.exports={ + "name": "hamlet.coffee", + "version": "0.7.6", + "description": "Truly amazing templating!", + "devDependencies": { + "browserify": "^12.0.1", + "coffee-script": "~1.7.1", + "jsdom": "^7.2.0", + "mocha": "^2.3.3" + }, + "dependencies": { + "hamlet-compiler": "0.7.0", + "o_0": "0.3.8" + }, + "homepage": "hamlet.coffee", + "repository": { + "type": "git", + "url": "https://github.com/dr-coffee-labs/hamlet.git" + }, + "scripts": { + "prepublish": "script/prepublish", + "test": "script/test" + }, + "files": [ + "dist/" + ], + "main": "dist/runtime.js" +} + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/main.coffee b/main.coffee index d257666..b9dd581 100644 --- a/main.coffee +++ b/main.coffee @@ -1,5 +1,7 @@ require "./extensions" +global.Hamlet = require "./lib/hamlet" + System = require "./system" global.system = System() diff --git a/system.coffee b/system.coffee index ad2ec59..0934b99 100644 --- a/system.coffee +++ b/system.coffee @@ -61,6 +61,7 @@ Bindable = require "bindable" Model = require "model" Associations = require "./system/associations" SystemModule = require "./system/module" +Template = require "./system/template" UI = require "ui" module.exports = (dbName='zine-os') -> @@ -68,7 +69,7 @@ module.exports = (dbName='zine-os') -> fs = DexieFS(DexieFSDB(dbName)) - self.include(Associations, SystemModule) + self.include(Associations, SystemModule, Template) self.extend fs: fs diff --git a/system/template.coffee b/system/template.coffee new file mode 100644 index 0000000..d4d41e9 --- /dev/null +++ b/system/template.coffee @@ -0,0 +1,12 @@ +# Compile a template from source text + +module.exports = (I, self) -> + self.extend + compileTemplate: (source, mode="jade") -> + templateSource = Hamlet.compile source, + compiler: CoffeeScript + mode: mode + runtime: "Hamlet" + exports: false + + Function("return " + templateSource)() From 17ff620377e1b1e50bb994abcc004ea2488240fc Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 10 Jan 2017 20:52:00 -0800 Subject: [PATCH 39/78] :tram::heavy_exclamation_mark: Updated at https://danielx.net/editor/ --- system/associations.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/associations.coffee b/system/associations.coffee index 1f7c07b..d7ccc0d 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -19,7 +19,8 @@ module.exports = (I, self) -> # JavaScript name: "Execute" filter: (file) -> - file.type is "application/javascript" + file.type is "application/javascript" or + file.path.match /\.js$/ fn: (file) -> self.include([file.path]) .then ([moduleExports]) -> From edce999575d94f9d7f9ff361a2a05f7f67b69d80 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Tue, 10 Jan 2017 21:24:58 -0800 Subject: [PATCH 40/78] :clock1130::mobile_phone_off: Updated at https://danielx.net/editor/ --- system/associations.coffee | 11 +++++++++++ system/module.coffee | 3 +++ 2 files changed, 14 insertions(+) diff --git a/system/associations.coffee b/system/associations.coffee index d7ccc0d..3e000f1 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -25,6 +25,17 @@ module.exports = (I, self) -> self.include([file.path]) .then ([moduleExports]) -> moduleExports + }, { + # CoffeeScript + name: "Execute" + filter: (file) -> + file.path.match /\.coffee$/ + fn: (file) -> + file.blob.readAsText() + .then (coffeeSource) -> + sourceProgram = CoffeeScript.compile coffeeSource, bare: true + + system.loadModule sourceProgram, file.path }, { name: "Markdown" filter: (file) -> diff --git a/system/module.coffee b/system/module.coffee index 54f3d67..a418796 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -126,6 +126,9 @@ module.exports = (I, self) -> .then (module) -> module.exports + # May want to reconsider this name + loadModule: loadModule + annotateSourceURL = (program, path) -> """ #{program} From bc04c1b099b631550a8aca113c6b13f1a242f9f0 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 11 Jan 2017 15:04:37 -0800 Subject: [PATCH 41/78] :dart::broken_heart: Updated at https://danielx.net/editor/ --- apps/audio-bro.coffee | 37 +++++++++++++++++++++++++++++++++++++ system/associations.coffee | 8 +++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 apps/audio-bro.coffee diff --git a/apps/audio-bro.coffee b/apps/audio-bro.coffee new file mode 100644 index 0000000..1fe93c7 --- /dev/null +++ b/apps/audio-bro.coffee @@ -0,0 +1,37 @@ +# Play Audio + +FileIO = require "../os/file-io" +Model = require "model" + +module.exports = -> + # Global system + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + + audio = document.createElement 'audio' + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + audio.src = URL.createObjectURL blob + + exit: -> + windowView.element.remove() + + menuBar = MenuBar + items: parseMenu """ + [F]ile + [O]pen + - + E[x]it + """ + handlers: handlers + + windowView = Window + title: "Audio Bro" + content: audio + menuBar: menuBar.element + width: 640 + height: 480 + + windowView.loadFile = handlers.loadFile + + return windowView diff --git a/system/associations.coffee b/system/associations.coffee index 3e000f1..a28dbb3 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -1,4 +1,5 @@ # TODO: Move handlers out +AudioBro = require "../apps/audio-bro" Filter = require "../apps/filter" TextEditor = require "../apps/text-editor" Spreadsheet = require "../apps/spreadsheet" @@ -19,7 +20,7 @@ module.exports = (I, self) -> # JavaScript name: "Execute" filter: (file) -> - file.type is "application/javascript" or + file.type is "application/javascript" or file.path.match /\.js$/ fn: (file) -> self.include([file.path]) @@ -63,6 +64,11 @@ module.exports = (I, self) -> filter: (file) -> file.type.match /^image\// fn: openWith(PixelEditor) + }, { + name: "Audio Bro" + filter: (file) -> + file.type.match /^audio\// + fn: openWith(AudioBro) }] # Open JSON arrays in spreadsheet From 57916d95d55ed277a2935329695eaeb6d4f76e7e Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 11 Jan 2017 17:37:04 -0800 Subject: [PATCH 42/78] :penguin::fuelpump: Updated at https://danielx.net/editor/ --- apps/audio-bro.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/audio-bro.coffee b/apps/audio-bro.coffee index 1fe93c7..c41895a 100644 --- a/apps/audio-bro.coffee +++ b/apps/audio-bro.coffee @@ -8,6 +8,8 @@ module.exports = -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI audio = document.createElement 'audio' + audio.controls = true + audio.autoplay = true handlers = Model().include(FileIO).extend loadFile: (blob) -> From b7d5e11dd36710fb69f391e9c49584ba07ebebf7 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 11 Jan 2017 20:33:43 -0800 Subject: [PATCH 43/78] :tropical_drink::imp: Updated at https://danielx.net/editor/ --- apps/audio-bro.coffee | 4 ++-- issues/2017-01.coffee | 35 ++++++++++++++++++++++++++++++++--- util.coffee | 10 ++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/audio-bro.coffee b/apps/audio-bro.coffee index c41895a..03a6576 100644 --- a/apps/audio-bro.coffee +++ b/apps/audio-bro.coffee @@ -31,8 +31,8 @@ module.exports = -> title: "Audio Bro" content: audio menuBar: menuBar.element - width: 640 - height: 480 + width: 308 + height: 80 windowView.loadFile = handlers.loadFile diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 4c68476..362648f 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -7,13 +7,30 @@ Social = require "../social/social" Explorer = require "../apps/explorer" +{parentElementOfType, emptyElement} = require "../util" + module.exports = -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI document.body.appendChild Explorer() - img = document.createElement "img" - img.src = "https://s-media-cache-ak0.pinimg.com/originals/a3/ba/56/a3ba56cef667d14b54023cd624d4e070.jpg" + container = document.createElement "container" + + pages = + front: """ + a(href="#enter") + img(src="https://s-media-cache-ak0.pinimg.com/originals/a3/ba/56/a3ba56cef667d14b54023cd624d4e070.jpg") + """ + enter: """ + a(href="#yolo") + img(src="https://books.google.com/books/content?id=2cgDAAAAMBAJ&rview=1&pg=PA10&img=1&zoom=3&hl=en&sig=ACfU3U3477L46r0KxSQusJrQ6w9qxIQ70w&w=1280") + """ + + Object.keys(pages).forEach (pageName) -> + value = pages[pageName] + pages[pageName] = system.compileTemplate(value)({}) + + container.appendChild pages.front handlers = Model().include(Social).extend area: -> @@ -42,11 +59,23 @@ module.exports = -> windowView = Window title: "ZineOS Volume 1 | Issue 2 | ENTER THE DUNGEON | January 2017" - content: img + content: container menuBar: menuBar.element width: 1228 height: 936 x: 0 y: 0 + + windowView.element.addEventListener "click", (e) -> + anchor = parentElementOfType("a", e.target) + + if anchor + next = anchor.getAttribute('href') + + if next.match /^\#/ + e.preventDefault() + emptyElement(container) + page = next.substr(1) + container.appendChild(pages[page]) document.body.appendChild windowView.element diff --git a/util.coffee b/util.coffee index 0f0bd72..639305b 100644 --- a/util.coffee +++ b/util.coffee @@ -12,3 +12,13 @@ module.exports = fileSeparator: fileSeparator normalizePath: normalizePath + + parentElementOfType: (tagname, element) -> + tagname = tagname.toLowerCase() + + if element.nodeName.toLowerCase() is tagname + return element + + while element = element.parentNode + if element.nodeName.toLowerCase() is tagname + return element From 28a7263e1aaace58a058b119817ad2014ce0e6b1 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 13 Jan 2017 15:43:10 -0800 Subject: [PATCH 44/78] :straight_ruler::twisted_rightwards_arrows: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 362648f..2fe371c 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -18,13 +18,32 @@ module.exports = -> pages = front: """ - a(href="#enter") + a(href="#vista") img(src="https://s-media-cache-ak0.pinimg.com/originals/a3/ba/56/a3ba56cef667d14b54023cd624d4e070.jpg") """ - enter: """ - a(href="#yolo") + vista: """ + a(href="#table") img(src="https://books.google.com/books/content?id=2cgDAAAAMBAJ&rview=1&pg=PA10&img=1&zoom=3&hl=en&sig=ACfU3U3477L46r0KxSQusJrQ6w9qxIQ70w&w=1280") """ + table: """ + div + h1 Table of Contents + ul + li + a(href="#front") Cover + li + a(href="#vista") Excerpt from Windows Vista Magazine + li + a(href="#table") Table of Contents + li + a(href="#random") Random Thoughts + """ + random: """ + div + h1 Random Thoughts + p Don't you hate it when you're cooking something and you look at the stove clock and think it's 3:75 and you're late for your appointment but it was just the temperature and also 3:75 isn't even a real time? + p I suggest you bone up a bit on torts before the next attempt at the bar exam. + """ Object.keys(pages).forEach (pageName) -> value = pages[pageName] From e2dc9483dacee5ce911703832e63f5856bc0bb4f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 16 Jan 2017 21:13:03 -0800 Subject: [PATCH 45/78] :bomb::1234: Updated at https://danielx.net/editor/ --- apps/chateau.coffee | 268 ++++++++++++++++++++++++++++++++++++++ apps/explorer.coffee | 2 +- apps/filter.coffee | 2 +- extensions.coffee | 10 ++ issues/2017-01.coffee | 7 +- style.styl | 26 ++++ templates/chateau.jadelet | 6 + 7 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 apps/chateau.coffee create mode 100644 templates/chateau.jadelet diff --git a/apps/chateau.coffee b/apps/chateau.coffee new file mode 100644 index 0000000..b650bbf --- /dev/null +++ b/apps/chateau.coffee @@ -0,0 +1,268 @@ +# Chat Based MUD + +Drop = require "../lib/drop" +FileIO = require "../os/file-io" +Model = require "model" + +Template = require "../templates/chateau" + +sortBy = (attribute) -> + (a, b) -> + a[attribute] - b[attribute] + +rand = (n) -> + Math.floor(Math.random() * n) + +createSocket = (room, accountId) -> + new WebSocket("wss://message-relay.gomix.me/r/#{room}?accountId=#{accountId}") + +setLocal = (data) -> + localStorage.chateau = JSON.stringify(data) + +getLocal = -> + try + JSON.parse localStorage.chateau + +module.exports = -> + # Global system + {ContextMenu, MenuBar, Modal, Observable, Progress, Util:{parseMenu}, Window} = system.UI + + wordsArray = Observable [] + connected = Observable false + + avatars = {} + myAccountId = null + + localData = getLocal() + + if localData + myAccountId = localData.myAccountId + avatars[myAccountId] = localData.myAvatar + else + myAccountId = "Anon" + Math.random().toString().substr(3) + + addAvatar = (accountId, url) -> + avatars[accountId] = + img: null + color: "orange" + x: rand canvas.width + y: rand canvas.height + + updateWords = -> + wordsElements = Object.keys(avatars).map (key) -> + avatars[key] + .filter ({say}) -> + say + .map ({x, y, say}) -> + words = document.createElement "words" + words.style.top = "#{y - 50}px" + words.style.left = "#{x}px" + words.innerText = say + + return words + + wordsArray wordsElements + + setAvatar = (blob) -> + return unless myAvatar = avatars[myAccountId] + + Image.fromBlob(blob) + .then (img) -> + myAvatar.img = img + + blob.readAsDataURL() + .then (url) -> + broadcast + avatar: url + + broadcast = (data) -> + socket.send JSON.stringify + type: "broadcast" + message: data + + canvas = document.createElement 'canvas' + context = canvas.getContext('2d') + + canvas.onclick = (e) -> + {pageX, pageY, currentTarget} = e + {top, left} = currentTarget.getBoundingClientRect() + + x = pageX - left + y = pageY - top + + broadcast + move: + x: x + y: y + + content = Template + canvas: canvas + connected: connected + words: wordsArray + submit: (e) -> + e.preventDefault() + + input = content.querySelector('input') + words = input.value + if words + input.value = "" + + broadcast + say: words + + Drop content, (e) -> + files = e.dataTransfer.files + + if files.length + file = files[0] + + setAvatar(file) + + socket = createSocket("cshopmilli", myAccountId) + + setInterval -> + try + socket.send JSON.stringify + meta: "keepalive" + , 30000 + + socket.onopen = -> + + + socket.onclose = -> + connected false + + socket.onmessage = (e) -> + message = JSON.parse e.data + console.log message + + accountId = message.clientId + + switch message.type + when "meta" + if message.status is "connect" + connected true + myAccountId = accountId + when "connect" + # Add Avatar + addAvatar(accountId) + when "disconnect" + # Remove Avatar + delete avatars[accountId] + updateWords() + when "broadcast" + {message} = message + + if message.move + {x, y} = message.move + avatars[accountId].x = x + avatars[accountId].y = y + + if message.say + avatars[accountId].say = message.say + updateWords() + + if message.avatar + img = new Image() + img.src = message.avatar + + avatars[accountId].img = img + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + Image.fromBlob blob + .then (img) -> + canvas.width = img.width + canvas.height = img.height + context.drawImage(img, 0, 0) + + saveData: -> + new Promise (resolve) -> + canvas.toBlob resolve + + exit: -> + windowView.element.remove() + + crop: -> + Modal.form modalForm() + .then console.log + + menuBar = MenuBar + items: parseMenu """ + [F]ile + [O]pen + [S]ave + Save [A]s + - + E[x]it + [E]dit + [C]rop + [F]ilter + """ + handlers: handlers + + windowView = Window + title: "Chateau" + content: content + menuBar: null + width: 640 + height: 480 + + roomstate = + background: null + objects: [] + + repaint = -> + # Draw BG + context.fillStyle = 'blue' + context.fillRect(0, 0, canvas.width, canvas.height) + + {background, objects} = roomstate + if background + context.drawImage(background, 0, 0, canvas.width, canvas.height) + + # Draw Avatars/Objects + Object.keys(avatars).map (accountId) -> + avatars[accountId] + .concat(objects).sort(sortBy("z")).forEach ({color, img, x, y}) -> + if img + {width, height} = img + context.drawImage(img, x - width / 2, y - height / 2) + else + context.fillStyle = color + context.fillRect(x - 25, y - 25, 50, 50) + + # Draw connection status + if connected() + indicatorColor = "green" + else + indicatorColor = "red" + + context.beginPath() + context.arc(canvas.width - 20, 20, 10, 0, 2 * Math.PI, false) + context.fillStyle = indicatorColor + context.fill() + context.lineWidth = 2 + context.strokeStyle = '#003300' + context.stroke() + + resize = -> + rect = canvas.getBoundingClientRect() + canvas.width = rect.width + canvas.height = rect.height + + windowView.on "resize", resize + + windowView.loadFile = handlers.loadFile + + animate = -> + requestAnimationFrame animate + repaint() + + animate() + + # TODO: ViewDidLoad? or equivalent event? + setTimeout -> + resize() + + return windowView diff --git a/apps/explorer.coffee b/apps/explorer.coffee index f7535ec..7fb0209 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -88,7 +88,7 @@ module.exports = Explorer = (options={}) -> Properties """ handlers: contextMenuHandlers - + contextMenu.display inElement: document.body diff --git a/apps/filter.coffee b/apps/filter.coffee index ed76ed3..cadfa32 100644 --- a/apps/filter.coffee +++ b/apps/filter.coffee @@ -9,7 +9,7 @@ module.exports = -> canvas = document.createElement 'canvas' context = canvas.getContext('2d') - + modalForm = system.compileTemplate """ form label diff --git a/extensions.coffee b/extensions.coffee index e292975..fd4ec69 100644 --- a/extensions.coffee +++ b/extensions.coffee @@ -13,6 +13,16 @@ Blob::readAsJSON = -> @readAsText() .then JSON.parse +Blob::readAsDataURL = -> + file = this + + new Promise (resolve, reject) -> + reader = new FileReader + reader.onload = -> + resolve reader.result + reader.onerror = reject + reader.readAsDataURL(file) + # Load an image from a blob returning a promise that is fulfilled with the # loaded image or rejected with an error Image.fromBlob = (blob) -> diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 2fe371c..f059977 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -1,4 +1,5 @@ Model = require "model" +Chateau = require "../apps/chateau" PixiePaint = require "../apps/pixel" Spreadsheet = require "../apps/spreadsheet" TextEditor = require "../apps/text-editor" @@ -63,12 +64,16 @@ module.exports = -> pixiePaint: -> app = PixiePaint(system) document.body.appendChild app.element + chateau: -> + app = Chateau(system) + document.body.appendChild app.element menuBar = MenuBar items: parseMenu """ [H]ello [W]ait Around For A Bit [A]pps + [C]hateau [M]S Access 97 [T]ext Editor [P]ixie Paint @@ -84,7 +89,7 @@ module.exports = -> height: 936 x: 0 y: 0 - + windowView.element.addEventListener "click", (e) -> anchor = parentElementOfType("a", e.target) diff --git a/style.styl b/style.styl index b687f91..371be6b 100644 --- a/style.styl +++ b/style.styl @@ -53,3 +53,29 @@ explorer > folder > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDMzMzMzMzMzMzMzMzMzMwj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI///////////////////wCHd3d3d3d3d4iIiIiIiIgACPt7e3t7e3gAAAAAAAAAAACPt7e3t7eAAAAAAAAAAAAACP/////4AAAAAAAAAAAAAACIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAP//wAH//+AD///wB/////////////8oAAAAEAAAACAAAAABAAQAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIgAj7e3t7e3uACPe3t7e3t4AI+3t7e3t7gAj3t7e3t7eACPt7e3t7e4AI97e3t7e3gAj7e3t7e3uACP///////4AIe3t7eIiIgACHt7eAAAAAAAiIiAAAAAAAAAAAAAAAAA//8AAP//AACAAQCgAAHOAAABzgAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAID/AADB/wAA//8AAA==") + +chateau + width: 100% + height: 100% + position: relative + + > words + border: 1px solid black + border-radius: 4px + background-color: white + padding: 0.5em + position: absolute + display: inline-block + + > canvas + display: block + width: 100% + height: 100% + position: absolute + + > form + position: absolute + bottom: 0 + + window > viewport > & + margin: initial diff --git a/templates/chateau.jadelet b/templates/chateau.jadelet new file mode 100644 index 0000000..c96587b --- /dev/null +++ b/templates/chateau.jadelet @@ -0,0 +1,6 @@ +chateau + = @canvas + = @words + form(@submit) + input + button Speak From f52767e3445039d1bbc74c1e2e51029d59b1839c Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 16 Jan 2017 21:22:45 -0800 Subject: [PATCH 46/78] :lollipop::leftwards_arrow_with_hook: Updated at https://danielx.net/editor/ --- apps/chateau.coffee | 54 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/apps/chateau.coffee b/apps/chateau.coffee index b650bbf..ce4fb04 100644 --- a/apps/chateau.coffee +++ b/apps/chateau.coffee @@ -72,6 +72,8 @@ module.exports = -> blob.readAsDataURL() .then (url) -> + myAvatar.dataURL = url + broadcast avatar: url @@ -80,6 +82,24 @@ module.exports = -> type: "broadcast" message: data + directMessage = (data, accountId) -> + socket.send JSON.stringify + type: "dm" + recipient: accountId + message: data + + updateNewcomer = (accountId) -> + return unless myAvatar = avatars[myAccountId] + + data = + move: + x: myAvatar.x + y: myAvatar.y + avatar: myAvatar.dataURL + say: myAvatar.say + + directMessage(data, accountId) + canvas = document.createElement 'canvas' context = canvas.getContext('2d') @@ -146,27 +166,37 @@ module.exports = -> when "connect" # Add Avatar addAvatar(accountId) + updateNewcomer(accountId) when "disconnect" # Remove Avatar delete avatars[accountId] updateWords() - when "broadcast" + when "broadcast", "dm" {message} = message - if message.move - {x, y} = message.move - avatars[accountId].x = x - avatars[accountId].y = y + receiveMessage(message, accountId) + + receiveMessage = (message, accountId) -> + avatars[accountId] ?= + x: rand canvas.width + y: rand canvas.height + color: "orange" + + if message.move + {x, y} = message.move + avatars[accountId].x = x + avatars[accountId].y = y + + if message.say + avatars[accountId].say = message.say - if message.say - avatars[accountId].say = message.say - updateWords() + if message.avatar + img = new Image() + img.src = message.avatar - if message.avatar - img = new Image() - img.src = message.avatar + avatars[accountId].img = img - avatars[accountId].img = img + updateWords() handlers = Model().include(FileIO).extend loadFile: (blob) -> From f7b6f2200cbc7bd8ab2ed4f667e6e14efc73ac65 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 16 Jan 2017 21:33:15 -0800 Subject: [PATCH 47/78] :toilet::godmode: Updated at https://danielx.net/editor/ --- style.styl | 19 +++++++++++++------ templates/chateau.jadelet | 3 ++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/style.styl b/style.styl index 371be6b..c09b7df 100644 --- a/style.styl +++ b/style.styl @@ -59,13 +59,20 @@ chateau height: 100% position: relative - > words - border: 1px solid black - border-radius: 4px - background-color: white - padding: 0.5em + > word-area + display: block + pointer-events: none position: absolute - display: inline-block + width: 100% + height: 100% + + > words + border: 1px solid black + border-radius: 4px + background-color: white + padding: 0.5em + position: absolute + display: inline-block > canvas display: block diff --git a/templates/chateau.jadelet b/templates/chateau.jadelet index c96587b..daf5d7a 100644 --- a/templates/chateau.jadelet +++ b/templates/chateau.jadelet @@ -1,6 +1,7 @@ chateau = @canvas - = @words + word-area + = @words form(@submit) input button Speak From 6867ebc9470fbf0e8f201cd45f5013b3fc46867e Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 16 Jan 2017 22:07:16 -0800 Subject: [PATCH 48/78] :wind_chime::four_leaf_clover: Updated at https://danielx.net/editor/ --- apps/chateau.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/chateau.coffee b/apps/chateau.coffee index ce4fb04..8e50d2d 100644 --- a/apps/chateau.coffee +++ b/apps/chateau.coffee @@ -155,8 +155,8 @@ module.exports = -> socket.onmessage = (e) -> message = JSON.parse e.data console.log message - - accountId = message.clientId + + accountId = message.accountId switch message.type when "meta" From d5bd945ccf2d382eacba46eeba230e5c12355f63 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 18 Jan 2017 19:51:51 -0800 Subject: [PATCH 49/78] :nut_and_bolt::cow2: Updated at https://danielx.net/editor/ --- main.coffee | 3 +++ style.styl | 35 +++++++++++++++++++++++++++++++++++ templates/achievement.jadelet | 4 ++++ 3 files changed, 42 insertions(+) create mode 100644 templates/achievement.jadelet diff --git a/main.coffee b/main.coffee index b9dd581..47131ec 100644 --- a/main.coffee +++ b/main.coffee @@ -11,3 +11,6 @@ style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style require("./issues/2017-01")() + +AchievementTemplate = require "./templates/achievement" +document.body.appendChild AchievementTemplate() diff --git a/style.styl b/style.styl index c09b7df..0546669 100644 --- a/style.styl +++ b/style.styl @@ -86,3 +86,38 @@ chateau window > viewport > & margin: initial + +achievement + color: white + background-color: blue + border-radius: 32px + bottom: 2em + display: block + height: 64px + left: 0 + margin: auto + padding: 8px + position: absolute + right: 0 + width: 300px + + > h2 + font-size: 1.25em + margin: 0 + + > p + margin-top: 8px + margin-bottom: 8px + + > icon + background-color: white + border-radius: 32px + color: blue + display: inline-block + float: left + font-size: 35px + height: 48px + line-height: 48px + margin-right: 8px + text-align: center + width: 48px diff --git a/templates/achievement.jadelet b/templates/achievement.jadelet new file mode 100644 index 0000000..9b78f97 --- /dev/null +++ b/templates/achievement.jadelet @@ -0,0 +1,4 @@ +achievement + icon 🍔 + h2 Achievement Unlocked + p Displayed an Achievement From bf19f39ca2ab647e682ae8eff4fd5ed10c16221d Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 18 Jan 2017 20:18:55 -0800 Subject: [PATCH 50/78] :speedboat::arrow_down: Updated at https://danielx.net/editor/ From a40a677b0bec91ac6398b2ba2e00c3843b8f0d6b Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Wed, 18 Jan 2017 21:15:02 -0800 Subject: [PATCH 51/78] :metal::v: Updated at https://danielx.net/editor/ --- main.coffee | 4 +++- templates/achievement.jadelet | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/main.coffee b/main.coffee index 47131ec..38fca62 100644 --- a/main.coffee +++ b/main.coffee @@ -13,4 +13,6 @@ document.head.appendChild style require("./issues/2017-01")() AchievementTemplate = require "./templates/achievement" -document.body.appendChild AchievementTemplate() +document.body.appendChild AchievementTemplate + icon: "📓" + text: "Constructed a notebook" diff --git a/templates/achievement.jadelet b/templates/achievement.jadelet index 9b78f97..035cace 100644 --- a/templates/achievement.jadelet +++ b/templates/achievement.jadelet @@ -1,4 +1,4 @@ achievement - icon 🍔 + icon= @icon h2 Achievement Unlocked - p Displayed an Achievement + p= @text From dfdddfa3af9fd5817bd6976b2d67711ec8bb10d2 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 20 Jan 2017 21:07:58 -0800 Subject: [PATCH 52/78] :mega::put_litter_in_its_place: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 7 +++++++ lib/achievement.coffee | 26 ++++++++++++++++++++++++++ main.coffee | 5 ----- style.styl | 19 ++++++++++++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 lib/achievement.coffee diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index f059977..24db7c8 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -1,3 +1,4 @@ +Achievement = require "../lib/achievement" Model = require "model" Chateau = require "../apps/chateau" PixiePaint = require "../apps/pixel" @@ -17,6 +18,12 @@ module.exports = -> container = document.createElement "container" + setTimeout -> + Achievement.display + icon: "📰" + text: "Issue 2" + , 3000 + pages = front: """ a(href="#vista") diff --git a/lib/achievement.coffee b/lib/achievement.coffee new file mode 100644 index 0000000..61ebe29 --- /dev/null +++ b/lib/achievement.coffee @@ -0,0 +1,26 @@ +AchievementTemplate = require "../templates/achievement" + +pending = [] +displaying = false + +module.exports = Achievement = + display: (options={}) -> + if displaying + return pending.push(options) + + achievementElement = AchievementTemplate options + document.body.appendChild achievementElement + achievementElement.classList.add "display" + + displaying = true + + achievementElement.addEventListener "animationend", (e) -> + achievementElement.remove() + + displaying = false + if pending.length + Achievement.display(pending.shift()) + + , false + + return achievementElement diff --git a/main.coffee b/main.coffee index 38fca62..b9dd581 100644 --- a/main.coffee +++ b/main.coffee @@ -11,8 +11,3 @@ style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style require("./issues/2017-01")() - -AchievementTemplate = require "./templates/achievement" -document.body.appendChild AchievementTemplate - icon: "📓" - text: "Constructed a notebook" diff --git a/style.styl b/style.styl index 0546669..df846f7 100644 --- a/style.styl +++ b/style.styl @@ -1,3 +1,6 @@ +body + overflow: hidden + user-select(s) user-select: s -moz-user-select: s @@ -87,11 +90,21 @@ chateau window > viewport > & margin: initial +@keyframes display-achievement + 0% + bottom: -128px + 5% + bottom: 2em + 95% + bottom: 2em + 100% + bottom: -128px + achievement color: white background-color: blue border-radius: 32px - bottom: 2em + bottom: -128px display: block height: 64px left: 0 @@ -100,6 +113,10 @@ achievement position: absolute right: 0 width: 300px + z-index: 9000 + + &.display + animation: display-achievement 6s > h2 font-size: 1.25em From 3259c8c2d0347b44d4c6cd4b67c6edb0a83d5264 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 21 Jan 2017 21:29:05 -0800 Subject: [PATCH 53/78] :zap::clock3: Updated at https://danielx.net/editor/ --- main.coffee | 5 +++++ style.styl | 31 ++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/main.coffee b/main.coffee index b9dd581..f5f1381 100644 --- a/main.coffee +++ b/main.coffee @@ -11,3 +11,8 @@ style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style require("./issues/2017-01")() + +Achievement = require "./lib/achievement" +Achievement.display + icon: "📰" + text: "Issue 2" diff --git a/style.styl b/style.styl index df846f7..c1e7d10 100644 --- a/style.styl +++ b/style.styl @@ -90,6 +90,7 @@ chateau window > viewport > & margin: initial + @keyframes display-achievement 0% bottom: -128px @@ -100,6 +101,16 @@ chateau 100% bottom: -128px +@keyframes slide-left + 0% + left: 100% + 25% + left: 0 + 50% + left: -100% + 100% + left: -100% + achievement color: white background-color: blue @@ -109,14 +120,32 @@ achievement height: 64px left: 0 margin: auto + overflow: hidden padding: 8px position: absolute right: 0 width: 300px z-index: 9000 + &:after + animation: slide-left 3s linear 0.75s infinite + content: "" + background: linear-gradient( + 50deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0) 40%, + rgba(255, 255, 255, 0.95) 50%, + rgba(255, 255, 255, 0) 75%, + rgba(255, 255, 255, 0) 100% + ) + height: 100% + position: absolute + top: 0 + left: 100% + width: 100% + &.display - animation: display-achievement 6s + animation: display-achievement 6s linear > h2 font-size: 1.25em From a910c7eca9194d1e6264ba3dcdf9c21b24aa30d2 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 21 Jan 2017 21:51:11 -0800 Subject: [PATCH 54/78] :cold_sweat::on: Updated at https://danielx.net/editor/ --- style.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style.styl b/style.styl index c1e7d10..6f5797e 100644 --- a/style.styl +++ b/style.styl @@ -145,7 +145,7 @@ achievement width: 100% &.display - animation: display-achievement 6s linear + animation: display-achievement 6s > h2 font-size: 1.25em From 07fa504464345a15f1cafdc5dfb7f71ab95fc0aa Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 22 Jan 2017 16:38:16 -0800 Subject: [PATCH 55/78] :grey_exclamation::pouch: Updated at https://danielx.net/editor/ --- lib/achievement.coffee | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/achievement.coffee b/lib/achievement.coffee index 61ebe29..32e532a 100644 --- a/lib/achievement.coffee +++ b/lib/achievement.coffee @@ -3,6 +3,14 @@ AchievementTemplate = require "../templates/achievement" pending = [] displaying = false +audioPath = "https://cdn.gomix.com/294e834f-223f-4792-9323-5b1fa8d0402b/unlock2.mp3" + +playSound = -> + audio = new Audio(audioPath) + audio.autoplay = true + + audio + module.exports = Achievement = display: (options={}) -> if displaying @@ -11,6 +19,7 @@ module.exports = Achievement = achievementElement = AchievementTemplate options document.body.appendChild achievementElement achievementElement.classList.add "display" + achievementElement.appendChild playSound() displaying = true From 6eacc51c81998e249d57c3efe02283dff5c5c59f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 23 Jan 2017 18:14:52 -0800 Subject: [PATCH 56/78] :small_red_triangle_down::gun: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 2 ++ stories/around-the-world.coffee | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 stories/around-the-world.coffee diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 24db7c8..f541011 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -24,6 +24,8 @@ module.exports = -> text: "Issue 2" , 3000 + system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" + pages = front: """ a(href="#vista") diff --git a/stories/around-the-world.coffee b/stories/around-the-world.coffee new file mode 100644 index 0000000..20b863e --- /dev/null +++ b/stories/around-the-world.coffee @@ -0,0 +1,37 @@ +module.exports = """ +# Around the World in 20 Years and a Weekend + +The history of human progress is rife with us entering things. [Tombs](https://en.wikipedia.org/wiki/Raiders_of_the_Lost_Ark), [dragons](https://en.wikipedia.org/wiki/Enter_the_Dragon), [voids](https://en.wikipedia.org/wiki/Enter_the_Void), even [gungeons](http://dodgeroll.com/gungeon/). + + +But have you ever stopped to think about what fuels this endless urge to ingress? Rather fearlessly, I decided to find out. + +![sensuous nights](https://frog-feels.s3.amazonaws.com/2016-52/391848cb-afa1-4149-b2be-054d6d86b540.png) + +It's now 20 years later. I've waded through the jungles of the Amazon, skied the slopes of Norway, swam the Ganges, and braved long lines. I've lost a lot too. The years have aged me dramatically - I'm a hot mess. And I lost my phone a month ago, so my friends and family are dead to me now. + +Here's what I've learned on my journey, + +![lederhosen leroy](https://frog-feels.s3.amazonaws.com/2016-30/57b1f586-4f32-4cd5-8aea-d1a7c9400964.png) + +The Freudian says we want to go in things for obvious sex reasons. Freudian? More like ‘Fraud’-ian, right? Har har. + +![horse confessional](https://frog-feels.s3.amazonaws.com/2016-27/75889dde-ba0c-434e-b5dd-1edda7daa97e.png) + +My Peyote spirit animal is a horse. Under the desert sky he whispers in my ear that what we really want is to enter the stable of the mind. I'm not so sure though. I don't even really like normal earthly stables. + +![soup boy](https://frog-feels.s3.amazonaws.com/2016-32/821e8ec3-e55d-4388-8cdd-6da58d902924.png) + +After the peyote trip, things get a little fuzzy. I stumbled into a place. It was either a diner, a drive-in, or a dive. There I met TV food guy Guy Fieri. Between mouthfuls of cheese and enthusiasm, he proclaimed that we enter so that we may exit. + +“We’re all searching for something that’s everything we imagine it to be.” + +... + +If I'm honest, this odyssey only really took a weekend. After that I didn't really feel like going back to work, so I just hung out for a while. + +![you are great](https://frog-feels.s3.amazonaws.com/2016-50/2d44fdcd-05b9-4a92-a954-ec1235ee7b1e.png) + + +[🐸 cool art made by cool people at [Frog Feels](http://frogfeels.com)] +""" From ab12487c67de5f6a90c13be176be8c91a2b95746 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 23 Jan 2017 19:23:24 -0800 Subject: [PATCH 57/78] :mortar_board::fries: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 2 +- main.coffee | 5 ----- system.coffee | 3 ++- system/achievement.coffee | 10 ++++++++++ 4 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 system/achievement.coffee diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index f541011..5b35583 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -19,7 +19,7 @@ module.exports = -> container = document.createElement "container" setTimeout -> - Achievement.display + system.achieve icon: "📰" text: "Issue 2" , 3000 diff --git a/main.coffee b/main.coffee index f5f1381..b9dd581 100644 --- a/main.coffee +++ b/main.coffee @@ -11,8 +11,3 @@ style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style require("./issues/2017-01")() - -Achievement = require "./lib/achievement" -Achievement.display - icon: "📰" - text: "Issue 2" diff --git a/system.coffee b/system.coffee index 0934b99..9a8de89 100644 --- a/system.coffee +++ b/system.coffee @@ -59,6 +59,7 @@ uniq = (array) -> Bindable = require "bindable" Model = require "model" +Achievement = require "./system/achievement" Associations = require "./system/associations" SystemModule = require "./system/module" Template = require "./system/template" @@ -69,7 +70,7 @@ module.exports = (dbName='zine-os') -> fs = DexieFS(DexieFSDB(dbName)) - self.include(Associations, SystemModule, Template) + self.include(Achievement, Associations, SystemModule, Template) self.extend fs: fs diff --git a/system/achievement.coffee b/system/achievement.coffee new file mode 100644 index 0000000..fa3cd77 --- /dev/null +++ b/system/achievement.coffee @@ -0,0 +1,10 @@ +Achievement = require "../lib/achievement" + +# TODO: Track unlocks, save/restore achievements +# TODO: Only display once +# TODO: View achievement progress grouped by area + +module.exports = (I, self) -> + Object.assign self, + achieve: (opts) -> + Achievement.display opts From a224def35286bb76baa7485014949d1fbb133479 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 10:16:56 -0800 Subject: [PATCH 58/78] :cn::hushed: Updated at https://danielx.net/editor/ --- apps/markdown.coffee | 2 +- style.styl | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/markdown.coffee b/apps/markdown.coffee index 3ac9d88..537d933 100644 --- a/apps/markdown.coffee +++ b/apps/markdown.coffee @@ -36,7 +36,7 @@ module.exports = -> title: "Markdown" content: container menuBar: menuBar.element - width: 640 + width: 720 height: 480 windowView.loadFile = handlers.loadFile diff --git a/style.styl b/style.styl index 6f5797e..2d9ec2f 100644 --- a/style.styl +++ b/style.styl @@ -1,6 +1,9 @@ body overflow: hidden +h1 + line-height: 1.25em + user-select(s) user-select: s -moz-user-select: s From ccd37703afa3e39b6def7a7244dd2ae5a808044f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 14:39:08 -0800 Subject: [PATCH 59/78] :electric_plug::pisces: Updated at https://danielx.net/editor/ --- apps/text-editor.coffee | 2 ++ issues/2017-01.coffee | 6 ++---- lib/achievement.coffee | 2 ++ stories/around-the-world.coffee | 6 +++--- style.styl | 2 ++ system/achievement.coffee | 30 ++++++++++++++++++++++++++++-- templates/achievement.jadelet | 2 +- 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/apps/text-editor.coffee b/apps/text-editor.coffee index 4dc56ae..b957178 100644 --- a/apps/text-editor.coffee +++ b/apps/text-editor.coffee @@ -12,6 +12,8 @@ mode = (mode) -> module.exports = -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI + system.Achievement.unlock "Notepad.exe" + aceWrap = document.createElement "div" aceWrap.style.width = aceWrap.style.height = "100%" diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 5b35583..f49dd04 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -19,10 +19,8 @@ module.exports = -> container = document.createElement "container" setTimeout -> - system.achieve - icon: "📰" - text: "Issue 2" - , 3000 + system.Achievement.unlock "Issue 2" + #, 3000 system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" diff --git a/lib/achievement.coffee b/lib/achievement.coffee index 32e532a..315de5c 100644 --- a/lib/achievement.coffee +++ b/lib/achievement.coffee @@ -16,6 +16,8 @@ module.exports = Achievement = if displaying return pending.push(options) + options.title ?= "Achievement Unlocked" + achievementElement = AchievementTemplate options document.body.appendChild achievementElement achievementElement.classList.add "display" diff --git a/stories/around-the-world.coffee b/stories/around-the-world.coffee index 20b863e..32202bb 100644 --- a/stories/around-the-world.coffee +++ b/stories/around-the-world.coffee @@ -1,14 +1,14 @@ module.exports = """ # Around the World in 20 Years and a Weekend -The history of human progress is rife with us entering things. [Tombs](https://en.wikipedia.org/wiki/Raiders_of_the_Lost_Ark), [dragons](https://en.wikipedia.org/wiki/Enter_the_Dragon), [voids](https://en.wikipedia.org/wiki/Enter_the_Void), even [gungeons](http://dodgeroll.com/gungeon/). +The history of human progress is rife with us entering things. [Tombs](https://en.wikipedia.org/wiki/Raiders_of_the_Lost_Ark), [dragons](https://en.wikipedia.org/wiki/Enter_the_Dragon), [voids](https://en.wikipedia.org/wiki/Enter_the_Void), even [gungeons](http://dodgeroll.com/gungeon/). But have you ever stopped to think about what fuels this endless urge to ingress? Rather fearlessly, I decided to find out. ![sensuous nights](https://frog-feels.s3.amazonaws.com/2016-52/391848cb-afa1-4149-b2be-054d6d86b540.png) -It's now 20 years later. I've waded through the jungles of the Amazon, skied the slopes of Norway, swam the Ganges, and braved long lines. I've lost a lot too. The years have aged me dramatically - I'm a hot mess. And I lost my phone a month ago, so my friends and family are dead to me now. +It's now 20 years later. I've waded through the jungles of the Amazon, skied the slopes of Norway, swam the Ganges, and braved long lines. I've lost a lot too. The years have aged me dramatically - I'm a hot mess. And I lost my phone a month ago, so my friends and family are dead to me now. Here's what I've learned on my journey, @@ -22,7 +22,7 @@ My Peyote spirit animal is a horse. Under the desert sky he whispers in my ear t ![soup boy](https://frog-feels.s3.amazonaws.com/2016-32/821e8ec3-e55d-4388-8cdd-6da58d902924.png) -After the peyote trip, things get a little fuzzy. I stumbled into a place. It was either a diner, a drive-in, or a dive. There I met TV food guy Guy Fieri. Between mouthfuls of cheese and enthusiasm, he proclaimed that we enter so that we may exit. +After the peyote trip, things get a little fuzzy. I stumbled into a place. It was either a diner, a drive-in, or a dive. There I met TV food guy Guy Fieri. Between mouthfuls of cheese and enthusiasm, he proclaimed that we enter so that we may exit. “We’re all searching for something that’s everything we imagine it to be.” diff --git a/style.styl b/style.styl index 2d9ec2f..51d87ea 100644 --- a/style.styl +++ b/style.styl @@ -119,6 +119,7 @@ achievement background-color: blue border-radius: 32px bottom: -128px + box-shadow: 2px 2px 10px black display: block height: 64px left: 0 @@ -165,6 +166,7 @@ achievement display: inline-block float: left font-size: 35px + font-family: "Apple Color Emoji","Segoe UI Emoji","NotoColorEmoji","Segoe UI Symbol","Android Emoji","EmojiSymbols" height: 48px line-height: 48px margin-right: 8px diff --git a/system/achievement.coffee b/system/achievement.coffee index fa3cd77..2637592 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -4,7 +4,33 @@ Achievement = require "../lib/achievement" # TODO: Only display once # TODO: View achievement progress grouped by area +achievementData = [{ + text: "Issue 1" + icon: "📰" + group: "Issue" +}, { + text: "Issue 2" + icon: "📰" + group: "Issue" +}, { # Apps + text: "Feeling the frog" + icon: "🐸" + group: "App" +}, { + text: "Notepad.exe" + icon: "📝" + group: "App" +}] + module.exports = (I, self) -> Object.assign self, - achieve: (opts) -> - Achievement.display opts + Achievement: + unlock: (name) -> + opts = achievementData.find ({text}) -> + text is name + + if opts and !opts.achieved + opts.achieved = true + + # TODO: Persist + Achievement.display opts diff --git a/templates/achievement.jadelet b/templates/achievement.jadelet index 035cace..a815f32 100644 --- a/templates/achievement.jadelet +++ b/templates/achievement.jadelet @@ -1,4 +1,4 @@ achievement icon= @icon - h2 Achievement Unlocked + h2= @title p= @text From a7cc0ad5fe618a2b0f04605b3769568065a630c2 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 15:00:12 -0800 Subject: [PATCH 60/78] :heart_eyes_cat::clock430: Updated at https://danielx.net/editor/ --- apps/markdown.coffee | 2 -- os/file-io.coffee | 8 +++++--- system.coffee | 10 ++++++++-- system/achievement.coffee | 14 +++++++++++++- system/module.coffee | 4 +++- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/apps/markdown.coffee b/apps/markdown.coffee index 537d933..1a706c9 100644 --- a/apps/markdown.coffee +++ b/apps/markdown.coffee @@ -25,8 +25,6 @@ module.exports = -> items: parseMenu """ [F]ile [O]pen - [S]ave - Save [A]s - E[x]it """ diff --git a/os/file-io.coffee b/os/file-io.coffee index 93a2471..3f32d70 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -34,14 +34,16 @@ module.exports = (I, self) -> currentPath = newPath else throw new Error "No path given" - .then system.readFile - .then self.loadFile + .then (path) -> + system.readFile path, true + .then (file) -> + self.loadFile file save: -> if currentPath self.saveData() .then (blob) -> - system.writeFile currentPath, blob + system.writeFile currentPath, blob, true .then -> currentPath else diff --git a/system.coffee b/system.coffee index 9a8de89..68726a7 100644 --- a/system.coffee +++ b/system.coffee @@ -76,14 +76,20 @@ module.exports = (dbName='zine-os') -> fs: fs # TODO: Allow relative paths - readFile: (path) -> + readFile: (path, userEvent) -> + if userEvent + self.Achievement.unlock "Load a file" + path = normalizePath "/#{path}" fs.read(path) .then ({blob}) -> blob # TODO: Allow relative paths - writeFile: (path, blob) -> + writeFile: (path, blob, userEvent) -> + if userEvent + self.Achievement.unlock "Save a file" + path = normalizePath "/#{path}" fs.write path, blob diff --git a/system/achievement.coffee b/system/achievement.coffee index 2637592..0997ef4 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -13,13 +13,25 @@ achievementData = [{ icon: "📰" group: "Issue" }, { # Apps - text: "Feeling the frog" + text: "Feel the frog" icon: "🐸" group: "App" }, { text: "Notepad.exe" icon: "📝" group: "App" +}, { # OS + text: "Save a file" + icon: "💾" + group: "OS" +}, { + text: "Load a file" + icon: "💽" + group: "OS" +}, { + text: "Execute code" + icon: "🖥️" + group: "OS" }] module.exports = (I, self) -> diff --git a/system/module.coffee b/system/module.coffee index a418796..fcc39d0 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -127,7 +127,9 @@ module.exports = (I, self) -> module.exports # May want to reconsider this name - loadModule: loadModule + loadModule: (args...) -> + self.Achievement.unlock "Execute code" + loadModule(args...) annotateSourceURL = (program, path) -> """ From b970ee38bbade6c0f36e88429193085781ba38a6 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 15:38:31 -0800 Subject: [PATCH 61/78] :bear::swimmer: Updated at https://danielx.net/editor/ --- apps/filter.coffee | 2 ++ apps/spreadsheet.coffee | 2 ++ system.coffee | 10 ++++++++++ system/achievement.coffee | 22 ++++++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/apps/filter.coffee b/apps/filter.coffee index cadfa32..149f9ac 100644 --- a/apps/filter.coffee +++ b/apps/filter.coffee @@ -7,6 +7,8 @@ module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + system.Achievement.unlock "Look at that" + canvas = document.createElement 'canvas' context = canvas.getContext('2d') diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index 6781d04..06dbdc4 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -4,6 +4,8 @@ Model = require "model" module.exports = () -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI + system.Achievement.unlock "Microsoft Access 97" + sourceData = [] headers = ["id", "name", "color"] diff --git a/system.coffee b/system.coffee index 68726a7..b46a4fd 100644 --- a/system.coffee +++ b/system.coffee @@ -113,4 +113,14 @@ module.exports = (dbName='zine-os') -> UI: UI + invokeBefore UI.Modal, "hide", -> + self.Achievement.unlock "Dismiss modal" + return self + +invokeBefore = (receiver, method, fn) -> + oldFn = receiver[method] + + receiver[method] = -> + fn() + oldFn.apply(receiver, arguments) diff --git a/system/achievement.coffee b/system/achievement.coffee index 0997ef4..339700c 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -8,30 +8,52 @@ achievementData = [{ text: "Issue 1" icon: "📰" group: "Issue" + description: "View Issue 1" }, { text: "Issue 2" icon: "📰" group: "Issue" + description: "View Issue 2" }, { # Apps text: "Feel the frog" icon: "🐸" group: "App" + description: "Visit frogfeels.com" }, { text: "Notepad.exe" icon: "📝" group: "App" + description: "Launch a text editor" +}, { + text: "Microsoft Access 97" + icon: "🔞" + group: "App" + description: "Launch a spreadsheet application" +}, { + text: "Look at that" + icon: "🖼️" + group: "App" + description: "Open the image viewer" }, { # OS text: "Save a file" icon: "💾" group: "OS" + description: "Write to the file system" }, { text: "Load a file" icon: "💽" group: "OS" + description: "Read from the file system" }, { text: "Execute code" icon: "🖥️" group: "OS" + description: "Some people like to live dangerously" +}, { + text: "Dismiss modal" + icon: "💃" + group: "OS" + description: "Dismiss a modal without even reading it" }] module.exports = (I, self) -> From 27acd0a389cc98eb1bab61ef3f503e0ca5e5e041 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 15:42:31 -0800 Subject: [PATCH 62/78] :atm::mailbox_with_mail: Updated at https://danielx.net/editor/ --- apps/explorer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 7fb0209..31ab27b 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -25,7 +25,7 @@ module.exports = Explorer = (options={}) -> if files.length files.forEach (file) -> newPath = path + file.name - system.writeFile(newPath, file) + system.writeFile(newPath, file, true) contextMenuFor = (file, e) -> return if e.defaultPrevented From a520c56813f6798bf37e61a862e857c46e5ece73 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 17:22:03 -0800 Subject: [PATCH 63/78] :loop::kissing: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 5 ++++- system/achievement.coffee | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index f49dd04..ed2eba1 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -31,7 +31,7 @@ module.exports = -> """ vista: """ a(href="#table") - img(src="https://books.google.com/books/content?id=2cgDAAAAMBAJ&rview=1&pg=PA10&img=1&zoom=3&hl=en&sig=ACfU3U3477L46r0KxSQusJrQ6w9qxIQ70w&w=1280") + img(width=640 height="auto" src="https://books.google.com/books/content?id=2cgDAAAAMBAJ&rview=1&pg=PA10&img=1&zoom=3&hl=en&sig=ACfU3U3477L46r0KxSQusJrQ6w9qxIQ70w&w=1280") """ table: """ div @@ -107,6 +107,9 @@ module.exports = -> e.preventDefault() emptyElement(container) page = next.substr(1) + + if page is "vista" + system.Achievement.unlock "Lol wut" container.appendChild(pages[page]) document.body.appendChild windowView.element diff --git a/system/achievement.coffee b/system/achievement.coffee index 339700c..1a41068 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -14,6 +14,11 @@ achievementData = [{ icon: "📰" group: "Issue" description: "View Issue 2" +}, { + text: "Lol wut" + icon: "😂" + group: "Issue 2" + description: "Did you know Windows Vista had a magazine?" }, { # Apps text: "Feel the frog" icon: "🐸" From 821a5a7fa681381ae9719a58540a1866d6717353 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 19:20:04 -0800 Subject: [PATCH 64/78] :aquarius::arrow_forward: Updated at https://danielx.net/editor/ --- system/achievement.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/achievement.coffee b/system/achievement.coffee index 1a41068..42a5711 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -19,12 +19,12 @@ achievementData = [{ icon: "😂" group: "Issue 2" description: "Did you know Windows Vista had a magazine?" -}, { # Apps +}, { text: "Feel the frog" icon: "🐸" - group: "App" + group: "Issue 2" description: "Visit frogfeels.com" -}, { +}, { # Apps text: "Notepad.exe" icon: "📝" group: "App" From 4d43f9c841f43162ba7898031708334de4e727f0 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Fri, 27 Jan 2017 19:36:59 -0800 Subject: [PATCH 65/78] :dragon::paperclip: Updated at https://danielx.net/editor/ --- main.coffee | 2 ++ pixie.cson | 1 + 2 files changed, 3 insertions(+) diff --git a/main.coffee b/main.coffee index b9dd581..7def5b4 100644 --- a/main.coffee +++ b/main.coffee @@ -1,3 +1,5 @@ +require("analytics").init("UA-3464282-16") + require "./extensions" global.Hamlet = require "./lib/hamlet" diff --git a/pixie.cson b/pixie.cson index d4c7d40..3d9e471 100644 --- a/pixie.cson +++ b/pixie.cson @@ -1,5 +1,6 @@ dependencies: ajax: "distri/ajax:master" + analytics: "distri/google-analytics:master" bindable: "distri/bindable:master" model: "distri/model:master" postmaster: "distri/postmaster:v0.5.0" From a4a8eeb36e9c44e851c93f342f98e4d28bdce05c Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 28 Jan 2017 10:48:20 -0800 Subject: [PATCH 66/78] :computer::cancer: Updated at https://danielx.net/editor/ --- apps/audio-bro.coffee | 3 +++ apps/chateau.coffee | 43 +++++++++++++++++--------------------- issues/2017-01.coffee | 2 +- style.styl | 3 +++ system/achievement.coffee | 5 +++++ system/associations.coffee | 6 +++--- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/apps/audio-bro.coffee b/apps/audio-bro.coffee index 03a6576..ae3e9f8 100644 --- a/apps/audio-bro.coffee +++ b/apps/audio-bro.coffee @@ -6,6 +6,9 @@ Model = require "model" module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement} = system + + Achievement.unlock "Pump up the jam" audio = document.createElement 'audio' audio.controls = true diff --git a/apps/chateau.coffee b/apps/chateau.coffee index 8e50d2d..c913990 100644 --- a/apps/chateau.coffee +++ b/apps/chateau.coffee @@ -128,7 +128,7 @@ module.exports = -> input.value = "" broadcast - say: words + say: words Drop content, (e) -> files = e.dataTransfer.files @@ -147,7 +147,7 @@ module.exports = -> , 30000 socket.onopen = -> - + socket.onclose = -> connected false @@ -198,43 +198,38 @@ module.exports = -> updateWords() - handlers = Model().include(FileIO).extend - loadFile: (blob) -> - Image.fromBlob blob - .then (img) -> - canvas.width = img.width - canvas.height = img.height - context.drawImage(img, 0, 0) - - saveData: -> - new Promise (resolve) -> - canvas.toBlob resolve + AboutTemplate = system.compileTemplate """ + container + h1 About + p Chat with your friends in this online chateau! + + p Drag and drop an image to become your avatar. + + p Click to position yourself in the room. + + p Say what you want to talk with others! + """ + handlers = Model().include(FileIO).extend exit: -> windowView.element.remove() - crop: -> - Modal.form modalForm() - .then console.log + about: -> + Modal.show AboutTemplate() menuBar = MenuBar items: parseMenu """ [F]ile - [O]pen - [S]ave - Save [A]s - - E[x]it - [E]dit - [C]rop - [F]ilter + [H]elp + [A]bout """ handlers: handlers windowView = Window title: "Chateau" content: content - menuBar: null + menuBar: menuBar.element width: 640 height: 480 diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index ed2eba1..86eb266 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -89,7 +89,7 @@ module.exports = -> handlers: handlers windowView = Window - title: "ZineOS Volume 1 | Issue 2 | ENTER THE DUNGEON | January 2017" + title: "ZineOS Volume 1 | Issue 2 | ENTER THE DUNGEON | February 2017" content: container menuBar: menuBar.element width: 1228 diff --git a/style.styl b/style.styl index 51d87ea..9099be4 100644 --- a/style.styl +++ b/style.styl @@ -60,6 +60,9 @@ explorer > folder > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDMzMzMzMzMzMzMzMzMzMwj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI///////////////////wCHd3d3d3d3d4iIiIiIiIgACPt7e3t7e3gAAAAAAAAAAACPt7e3t7eAAAAAAAAAAAAACP/////4AAAAAAAAAAAAAACIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAP//wAH//+AD///wB/////////////8oAAAAEAAAACAAAAABAAQAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIgAj7e3t7e3uACPe3t7e3t4AI+3t7e3t7gAj3t7e3t7eACPt7e3t7e4AI97e3t7e3gAj7e3t7e3uACP///////4AIe3t7eIiIgACHt7eAAAAAAAiIiAAAAAAAAAAAAAAAAA//8AAP//AACAAQCgAAHOAAABzgAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAID/AADB/wAA//8AAA==") +#modal > container + padding: 1em + chateau width: 100% height: 100% diff --git a/system/achievement.coffee b/system/achievement.coffee index 42a5711..0680aca 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -29,6 +29,11 @@ achievementData = [{ icon: "📝" group: "App" description: "Launch a text editor" +}, { + text: "Pump up the jam" + icon: "🎶" + group: "App" + description: "Launch audio application" }, { text: "Microsoft Access 97" icon: "🔞" diff --git a/system/associations.coffee b/system/associations.coffee index a28dbb3..54458a8 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -23,9 +23,9 @@ module.exports = (I, self) -> file.type is "application/javascript" or file.path.match /\.js$/ fn: (file) -> - self.include([file.path]) - .then ([moduleExports]) -> - moduleExports + file.blob.readAsText() + .then (sourceProgram) -> + system.loadModule sourceProgram, file.path }, { # CoffeeScript name: "Execute" From 501d88c8657a08ed47a128649b83c0351e31fb9e Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 28 Jan 2017 13:10:38 -0800 Subject: [PATCH 67/78] :ok::no_mobile_phones: Updated at https://danielx.net/editor/ --- apps/chateau.coffee | 6 +++--- issues/2017-01.coffee | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/chateau.coffee b/apps/chateau.coffee index c913990..bee9520 100644 --- a/apps/chateau.coffee +++ b/apps/chateau.coffee @@ -202,11 +202,11 @@ module.exports = -> container h1 About p Chat with your friends in this online chateau! - + p Drag and drop an image to become your avatar. - + p Click to position yourself in the room. - + p Say what you want to talk with others! """ diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 86eb266..64effaa 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -105,11 +105,37 @@ module.exports = -> if next.match /^\#/ e.preventDefault() - emptyElement(container) page = next.substr(1) - - if page is "vista" - system.Achievement.unlock "Lol wut" - container.appendChild(pages[page]) + + displayPage(page) + + currentPage = "front" + + displayPage = (page) -> + return unless page + + if page is "vista" + system.Achievement.unlock "Lol wut" + + emptyElement(container) + container.appendChild(pages[page]) + + currentPage = page + + nextPage = (n=1) -> + pageKeys = Object.keys(pages) + nextIndex = pageKeys.indexOf(currentPage) + n + + return pageKeys[nextIndex] + + windowView.element.addEventListener "keydown", (e) -> + switch e.key + when "Enter", "ArrowRight", " " + displayPage nextPage() + when "ArrowLeft" + displayPage nextPage(-1) document.body.appendChild windowView.element + + windowView.element.tabIndex = 0 + windowView.element.focus() From 8297b8e7ce4c233492b0ef2e69d654aa0f1898a6 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 28 Jan 2017 19:02:26 -0800 Subject: [PATCH 68/78] :u6e80::kissing_closed_eyes: Updated at https://danielx.net/editor/ --- apps/notepad.coffee | 4 + issues/2017-01.coffee | 1 + stories/provision.coffee | 145 +++++++++++++++++++++++++++++++++++++ system/associations.coffee | 3 +- 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 stories/provision.coffee diff --git a/apps/notepad.coffee b/apps/notepad.coffee index d563067..42aa2e4 100644 --- a/apps/notepad.coffee +++ b/apps/notepad.coffee @@ -5,6 +5,8 @@ module.exports = () -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + system.Achievement.unlock "Notepad.exe" + exec = (cmd) -> -> textarea.focus() @@ -114,5 +116,7 @@ module.exports = () -> menuBar: menuBar.element width: 640 height: 480 + + windowView.loadFile = handlers.loadFile return windowView diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 64effaa..6399398 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -23,6 +23,7 @@ module.exports = -> #, 3000 system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" + system.writeFile "issue-2/provision.txt", new Blob [require "../stories/provision"], type: "text/plain" pages = front: """ diff --git a/stories/provision.coffee b/stories/provision.coffee new file mode 100644 index 0000000..d60fa19 --- /dev/null +++ b/stories/provision.coffee @@ -0,0 +1,145 @@ +module.exports = """ +One hundred or so years from now, when I +die, my descendants come across this +rich text file while going through the +drawers and the suitcases in my husband +and I's house under the pretense of +cleaning. They will try to understand +who we were and if we ever were more +than just parents. They will have +problems with technology that has yet to +be invented; they will have kids and +lovers that I will never meet. + +My husband will die twenty or so years +before me. He will fall asleep at Sam's +Club and refuse to wake up. A +badly-shaken mid-level Sam's Club +franchise manager with kind eyes and a +polo T-shirt and exactly one earring +will knock on my door and deliver the +news. I will feel weak and, 18 hours +later, I will cry. + +I want you to know how ordinary I was. I +wanted to be one person and then I ended +up being someone else. Cumulatively I +thought I had serious fantasies about +marrying three different people and, +after college, I ended up marrying the +second. I was part of a dozen thousand +who fled to Canada during the dark +years. My grocer patiently taught me how +to say 100 words in a French-Canadian +accent and gave me the courage to eat +ugly vegetables. I could have traveled +more and been happy. I could have not +traveled more and been happy. + +The worst part about traveling is all +the advertisements for foreign brands. +Walking home at night under the +spotlights of billboards with which you +cannot empathize is a piercing kind of +loneliness. The best part about +traveling is meeting all the other +expats in your city, who each is tacitly +a member of a busy and tired club. + +My parents were Armenian mathematicians. +They left their country, which gave me +the courage to leave mine. They were +constantly worried about me and, to +distract themselves, they bought and +flipped real estate. The first time you +buy a house it seems impossible that +each room will fill with the right +furniture and the each wall will have +the right artwork. The second time is +twice as easy; and the third is twice +that. They met one hundred or so +different people that way, which I think +somehow made my relationship with them +better or at least easier. Toward the +end they will play gin with their +neighbors in the morning and watch +Armenian television in the afternoon, on +a small satellite modem that only +Armenians know where to buy. They forgot +English first, then walking, then +breathing. Acting on this information, I +will try to die much faster. + +The early 2010s will be remembered +fondly as a simpler time, and +additionally I want to add they were my +favorite years. The late 2040s were good +too, as were the 2060s. It will become +difficult to separate the objective +quality of a time period from how much +of a burden my body becomes. Every year +I will buy clothes that looked like all +the clothes I owned fifteen years ago +until, one day, I throw everything out +that isn't white and blue. I will die in +a white blouse and blue shorts. In death +I will stop looking cute and start +looking serene. + +Though I know some facts about the +future I have a great deal of questions. +What are the food trends now? Are there +any new bands that sound like the old +bands I liked? Do radiators still break +during the coldest week? + +My husband and I ran into each other in +three different cities. The first two +times we recognized in each other an +understanding that the timing was off. +The third time we fucked in a friend's +closet, groping each other like we were +younger, beneath layers of woolen duvets +that we by accident pulled down. It +seemed tawdry and sticky at the time and +now it's just a sentence. + +Into our lives we carved out space for +each other. We put up load-bearing +beams. We added good thick curtains and +a little OLED display that read the +amount of humidity in the air. We agreed +to never make the other person sleep on +the couch, as we wanted ours to be +filled with the flatulence of our +friends. We threw the best parties. We +were an unstoppable hosting machine. We +figured out that a party becomes the +memory of a party. It happens one or two +days after the party, when the +conversation and the discoveries curdle +into something distant and bittersweet. + +The city will build a subway station +near us in 2031 after years of private +funding into subterranean +infrastructure. The money will come from +tech moguls looking to survive zombies +and/or climate change, but a subway +station is a subway station. One late +night, as we step off the train on our +way home, we will walk past a woman +wearing a bright striped parka. I will +turn my head to get a second look and +she will not be there. I will crook my +head to see behind the poles until my +neck hurts, and still I will see no one. +I will turn to my husband, who sees my +face, and we will walk home with his +head lying on my shoulder and his hand +lying on my chest so that my +inexplicably racing heartbeat will bang +and shudder against his hand like a +hurricane's wind against the shutters of +a house where nobody has ever lived. +""" \ No newline at end of file diff --git a/system/associations.coffee b/system/associations.coffee index 54458a8..0f627e8 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -1,6 +1,7 @@ # TODO: Move handlers out AudioBro = require "../apps/audio-bro" Filter = require "../apps/filter" +Notepad = require "../apps/notepad" TextEditor = require "../apps/text-editor" Spreadsheet = require "../apps/spreadsheet" PixelEditor = require "../apps/pixel" @@ -47,7 +48,7 @@ module.exports = (I, self) -> filter: (file) -> file.type.match(/^text\//) or file.type is "application/javascript" - fn: openWith(TextEditor) + fn: openWith(Notepad) }, { name: "Spreadsheet" filter: (file) -> From 668e1e3638d90230f58e2fa61992541ec1fbeb20 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 28 Jan 2017 22:41:05 -0800 Subject: [PATCH 69/78] :tea::gun: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 61 ++++++++++++-- lib/outbound-clicks.coffee | 16 ++++ main.coffee | 2 + presenters/achievement-group-progress.coffee | 20 +++++ style.styl | 27 +++++++ system/achievement.coffee | 83 +++++++++++++++++++- templates/achievement-badge.jadelet | 2 + templates/achievement-progress.jadelet | 5 ++ 8 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 lib/outbound-clicks.coffee create mode 100644 presenters/achievement-group-progress.coffee create mode 100644 templates/achievement-badge.jadelet create mode 100644 templates/achievement-progress.jadelet diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 6399398..3784a4f 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -46,19 +46,56 @@ module.exports = -> a(href="#table") Table of Contents li a(href="#random") Random Thoughts + li + a(href="#cheevos") Cheevos + li + a(href="#contributors") Contributors """ random: """ - div + div(style="padding: 1em;") h1 Random Thoughts p Don't you hate it when you're cooking something and you look at the stove clock and think it's 3:75 and you're late for your appointment but it was just the temperature and also 3:75 isn't even a real time? p I suggest you bone up a bit on torts before the next attempt at the bar exam. + p Does anyone remember thepalace.com avatar based chat and virtual worlds? + p Those spreadsheets you like are going back in style. + """ + cheevos: """ + div(style="padding: 1em;") + h1 Cheevos + + p No matter if you guy/girl or whatever, Cheevos impress people. It's almost like saying 'Well, I got tons of Cheevos. There are tons of people online that are interested and respect me." + + p You might be thinking 'Oh that's complete BS, I personally don't care about Cheevos when dating'. And yeah, you are probably telling the truth, but it's in your sub-concious. Sort of like how girls always like the bad guy, but never admit it. + + p Braggin about your Cheevos sometimes makes you look conceited, but that's a good thing. It like how celebraties look conceited because they're rolling VIP into clubs and you're stuck in line. + + p 'Brewer' asks 'What happens if you're cheevo talking at a bar, club or party and someone says they have more cheevos that you?' + + p Well, hopefully they are just hating and are lying. First look up their score with your cellphone, make sure you have a page bookmarked where you can check. If you catch them in a lie, you look even better. If they are telling the truth and have more Cheevos than you, leave. Nothing else you can do. Buy the person a drink and leave, unless you're willing to look 2nd best. If you brought a date, odds are she's going to be impressed with the higher gamer score and ditch you. Get out as soon as you can and go to some other party. + + p + a(href="http://cheevos.com") Learn more about cheevos from Bboy360 at cheevos.com + + """ + contributors: """ + div(style="padding: 1em;") + h1 Contributors + ul + li Daniel X + li Lan + li pketh + li Mayor + li and you! + + p + a(href="#table") Return to table of contents """ Object.keys(pages).forEach (pageName) -> value = pages[pageName] pages[pageName] = system.compileTemplate(value)({}) - container.appendChild pages.front + pages.cheevos.appendChild system.Achievement.progressView() handlers = Model().include(Social).extend area: -> @@ -75,17 +112,20 @@ module.exports = -> chateau: -> app = Chateau(system) document.body.appendChild app.element + credits: -> + displayPage "contributors" + tableofContents: -> + displayPage "table" menuBar = MenuBar items: parseMenu """ - [H]ello - [W]ait Around For A Bit [A]pps - [C]hateau - [M]S Access 97 [T]ext Editor [P]ixie Paint #{Social.menuText} + H[e]lp + [T]able of Contents + [C]redits """ handlers: handlers @@ -112,8 +152,15 @@ module.exports = -> currentPage = "front" + visited = {} + displayPage = (page) -> return unless page + + visited[page] = true + + if Object.keys(visited).length is Object.keys(pages).length + system.Achievement.unlock "Cover-2-cover 2: 2 cover 2 furious" if page is "vista" system.Achievement.unlock "Lol wut" @@ -123,6 +170,8 @@ module.exports = -> currentPage = page + displayPage currentPage + nextPage = (n=1) -> pageKeys = Object.keys(pages) nextIndex = pageKeys.indexOf(currentPage) + n diff --git a/lib/outbound-clicks.coffee b/lib/outbound-clicks.coffee new file mode 100644 index 0000000..d8feb7a --- /dev/null +++ b/lib/outbound-clicks.coffee @@ -0,0 +1,16 @@ +{parentElementOfType} = require "../util" + +# Outbound clicker +document.addEventListener "click", (e) -> + anchor = parentElementOfType("a", e.target) + + if anchor + href = anchor.getAttribute('href') + + if href.match /^http/ + e.preventDefault() + + if href.match /frogfeels\.com/ + system.Achievement.unlock "Feeling the frog" + + window.open href diff --git a/main.coffee b/main.coffee index 7def5b4..a648bb5 100644 --- a/main.coffee +++ b/main.coffee @@ -2,6 +2,8 @@ require("analytics").init("UA-3464282-16") require "./extensions" +require "./lib/outbound-clicks" + global.Hamlet = require "./lib/hamlet" System = require "./system" diff --git a/presenters/achievement-group-progress.coffee b/presenters/achievement-group-progress.coffee new file mode 100644 index 0000000..f53b645 --- /dev/null +++ b/presenters/achievement-group-progress.coffee @@ -0,0 +1,20 @@ +AchievementBadgeTemplate = require "../templates/achievement-badge" +ProgressTemplate = require "../templates/achievement-progress" + +module.exports = ({name, achievements}) -> + achieved = achievements.filter ({achieved}) -> + achieved + .length + + total = achievements.length + value = achieved / total + + ProgressTemplate + name: name + achievements: achievements + badges: achievements.map (cheevo) -> + AchievementBadgeTemplate Object.assign {}, cheevo, + class: -> + "achieved" if cheevo.achieved + fraction: "#{achieved}/#{total}" + value: value.toString() diff --git a/style.styl b/style.styl index 9099be4..79b28e2 100644 --- a/style.styl +++ b/style.styl @@ -175,3 +175,30 @@ achievement margin-right: 8px text-align: center width: 48px + +achievement-badges + display: block + +achievement-progress + display: block + + > h2 + margin-bottom: 4px + +achievement-badge + background-color: white + border: 1px solid gray + color: gray + display: inline-block + font-size: 35px + font-family: "Apple Color Emoji","Segoe UI Emoji","NotoColorEmoji","Segoe UI Symbol","Android Emoji","EmojiSymbols" + height: 48px + line-height: 48px + margin-right: 8px + margin-top: 8px + text-align: center + width: 48px + + &.achieved + border: 1px solid blue + color: blue diff --git a/system/achievement.coffee b/system/achievement.coffee index 0680aca..90b80c3 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -1,5 +1,9 @@ Achievement = require "../lib/achievement" +{Observable} = UI = require "ui" + +{emptyElement} = require "../util" + # TODO: Track unlocks, save/restore achievements # TODO: Only display once # TODO: View achievement progress grouped by area @@ -7,12 +11,17 @@ Achievement = require "../lib/achievement" achievementData = [{ text: "Issue 1" icon: "📰" - group: "Issue" + group: "Issue 1" description: "View Issue 1" +}, { + text: "Cover-2-cover" + icon: "📗" + group: "Issue 1" + description: "Read the entire issue" }, { text: "Issue 2" icon: "📰" - group: "Issue" + group: "Issue 2" description: "View Issue 2" }, { text: "Lol wut" @@ -20,7 +29,12 @@ achievementData = [{ group: "Issue 2" description: "Did you know Windows Vista had a magazine?" }, { - text: "Feel the frog" + text: "Cover-2-cover 2: 2 cover 2 furious" + icon: "📗" + group: "Issue 2" + description: "Read the entire issue" +}, { + text: "Feeling the frog" icon: "🐸" group: "Issue 2" description: "Visit frogfeels.com" @@ -64,11 +78,47 @@ achievementData = [{ icon: "💃" group: "OS" description: "Dismiss a modal without even reading it" +}, { # Social + text: "Do you 'like' like me?" + icon: "💕" + group: "Social" + description: "Have fine taste" }] +restore = -> + storedCheevos = [] + + try + storedCheevos = JSON.parse localStorage.cheevos + + storedAchieved = {} + storedCheevos.forEach ({achieved, text}) -> + storedAchieved[text] = achieved + + achievementData.forEach (cheevo) -> + {text} = cheevo + + if storedAchieved[text] + cheevo.achieved = true + +persist = -> + localStorage.cheevos = JSON.stringify(achievementData) + +AchievementProgressPresenter = require "../presenters/achievement-group-progress" + +groupBy = (xs, key) -> + xs.reduce (rv, x) -> + (rv[x[key]] ?= []).push(x) + + rv + , {} + module.exports = (I, self) -> + restore() + Object.assign self, Achievement: + groupData: Observable {} unlock: (name) -> opts = achievementData.find ({text}) -> text is name @@ -76,5 +126,30 @@ module.exports = (I, self) -> if opts and !opts.achieved opts.achieved = true - # TODO: Persist + persist() + updateStatus() Achievement.display opts + progressView: -> + content = document.createElement "content" + + Observable -> + data = self.Achievement.groupData() + + elements = Object.keys(data).map (group) -> + AchievementProgressPresenter + name: group + achievements: data[group] + + console.log "EE", elements + + emptyElement content + elements.forEach (element) -> + content.appendChild(element) + + return content + + updateStatus = -> + console.log self.Achievement.groupData groupBy(achievementData, "group") + updateStatus() + + return self diff --git a/templates/achievement-badge.jadelet b/templates/achievement-badge.jadelet new file mode 100644 index 0000000..3338c0d --- /dev/null +++ b/templates/achievement-badge.jadelet @@ -0,0 +1,2 @@ +achievement-badge(title=@description @class) + = @icon diff --git a/templates/achievement-progress.jadelet b/templates/achievement-progress.jadelet new file mode 100644 index 0000000..c5a798a --- /dev/null +++ b/templates/achievement-progress.jadelet @@ -0,0 +1,5 @@ +achievement-progress + h2= @name + progress(@value) + span= @fraction + achievement-badges= @badges From d02c0ab3aa8532f6ed649ed6ec494b22ab73da38 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sat, 28 Jan 2017 22:42:54 -0800 Subject: [PATCH 70/78] :speaker::school: Updated at https://danielx.net/editor/ --- social/social.coffee | 1 + system/achievement.coffee | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/social/social.coffee b/social/social.coffee index 4295d8e..1fa8ae8 100644 --- a/social/social.coffee +++ b/social/social.coffee @@ -36,6 +36,7 @@ module.exports = (I, self) -> Modal.show CommentsTemplate data like: -> + system.Achievement.unlock "Do you 'like' like me?" Modal.alert "I like you too, but we don't have a facebook or anything yet :)" subscribe: -> require("../mailchimp").show() diff --git a/system/achievement.coffee b/system/achievement.coffee index 90b80c3..95e7cad 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -134,22 +134,20 @@ module.exports = (I, self) -> Observable -> data = self.Achievement.groupData() - + elements = Object.keys(data).map (group) -> AchievementProgressPresenter name: group achievements: data[group] - console.log "EE", elements - emptyElement content elements.forEach (element) -> content.appendChild(element) - + return content updateStatus = -> - console.log self.Achievement.groupData groupBy(achievementData, "group") + self.Achievement.groupData groupBy(achievementData, "group") updateStatus() return self From 3b66e6010e5161f86a6f199f42cfdef807255f9f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 29 Jan 2017 08:31:18 -0800 Subject: [PATCH 71/78] :no_entry_sign::small_orange_diamond: Updated at https://danielx.net/editor/ --- lib/error-reporter.coffee | 5 +++++ main.coffee | 1 + system/achievement.coffee | 5 +++++ 3 files changed, 11 insertions(+) create mode 100644 lib/error-reporter.coffee diff --git a/lib/error-reporter.coffee b/lib/error-reporter.coffee new file mode 100644 index 0000000..0717af7 --- /dev/null +++ b/lib/error-reporter.coffee @@ -0,0 +1,5 @@ +window.addEventListener "error", (e) -> + system.Achievement.unlock "I AM ERROR" + +window.addEventListener "unhandledrejection", (e) -> + system.Achievement.unlock "I AM ERROR" diff --git a/main.coffee b/main.coffee index a648bb5..a1f56d6 100644 --- a/main.coffee +++ b/main.coffee @@ -3,6 +3,7 @@ require("analytics").init("UA-3464282-16") require "./extensions" require "./lib/outbound-clicks" +require "./lib/error-reporter" global.Hamlet = require "./lib/hamlet" diff --git a/system/achievement.coffee b/system/achievement.coffee index 95e7cad..fcad2ee 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -78,6 +78,11 @@ achievementData = [{ icon: "💃" group: "OS" description: "Dismiss a modal without even reading it" +}, { + text: "I AM ERROR" + icon: "🐛" + group: "OS" + description: "Encountered a JavaScript error" }, { # Social text: "Do you 'like' like me?" icon: "💕" From 41a71a42cebb9aeab6a2ee9de15be7dd3a220e9a Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 29 Jan 2017 08:56:25 -0800 Subject: [PATCH 72/78] :lipstick::lock_with_ink_pen: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 3784a4f..71e1c32 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -135,8 +135,8 @@ module.exports = -> menuBar: menuBar.element width: 1228 height: 936 - x: 0 - y: 0 + x: 32 + y: 32 windowView.element.addEventListener "click", (e) -> anchor = parentElementOfType("a", e.target) From 434e21cbf311946113fb7e3712d9308a07ebd3d1 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 29 Jan 2017 09:06:15 -0800 Subject: [PATCH 73/78] :crystal_ball::circus_tent: Updated at https://danielx.net/editor/ --- issues/2017-01.coffee | 1 + stories/dungeon-dog.coffee | 120 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 stories/dungeon-dog.coffee diff --git a/issues/2017-01.coffee b/issues/2017-01.coffee index 71e1c32..7ba428a 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-01.coffee @@ -24,6 +24,7 @@ module.exports = -> system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" system.writeFile "issue-2/provision.txt", new Blob [require "../stories/provision"], type: "text/plain" + system.writeFile "issue-2/dungeon-dog.txt", new Blob [require "../stories/dungeon-dog"], type: "text/plain" pages = front: """ diff --git a/stories/dungeon-dog.coffee b/stories/dungeon-dog.coffee new file mode 100644 index 0000000..6d7ec67 --- /dev/null +++ b/stories/dungeon-dog.coffee @@ -0,0 +1,120 @@ +module.exports = """ +Dungeon Crawl with Cliffford! + +One sunny day, Cliffford the big rad dog was playing at the park with his friends U-Bone and Cloe. They chased each other through the playground, around the play structures, and through the sand. Children squealed with delight to see Cliffford jump right over the swing set in his excitement. “He’s so big! He can jump over things! Heehee!” they giggled. THUMP! went Cliffford’s paws as he landed on the grassy field, and Cloe and U-Bone followed right behind. + +The dogs dashed through a soccer game, U-Bone stopping to bite at the ball, and Cleo weaving her way through at least four players’ legs. They emerged on the other side of the field at the northernmost edge of the park, where the well-kept field gave way to a little meadow, and then the woods. + +Cliffford flopped down in a shady spot, and Cloe and U-Bone stood panting next to him. Cool breezes blew in from the ocean, rippling the leaves on the trees and the long meadow-grass. + +“What do you guys want to do now?” Cliffford asked. “I could really go for a nap.” +“Yeah, a nap sounds good,” U-Bone replied, snuggling up beside him. +“We could nap, but what about going back and trying to get that Frisbee over there?” Cloe asked. +“Mmphuhuhmph,” U-Bone answered, already half-asleep. +“All right, fine,” Cloe huffed. Cliffford began to doze as well, so Cloe meandered away, sniffing around the meadow. + +“Cliffford? U-BONE! WAAAAAAAKE UP!” Cliffford and U-Bone bolted awake, jumping to their feet. +“HAHAHAHA! Did I scare you?” Cloe yipped, bouncing around in circles around her two friends as excitement practically dripped off her. +“What’s up, Cloe?” U-Bone asked. +“I found something neat. Come with me!” + +Without looking behind her to see if the others were coming, Cloe took off at a run down a path that Cliffford and U-Bone hadn’t seen until now. The path wound through the woods, the trees getting taller and closer together the further along they went. The terrain became rockier, and a few times they dodged around large boulders as they continued forward. + +Around a bend in the path, Cloe came into view, her tail wagging wildly. She stood in front of a large outcropping with a dark, conveniently Cliffford-sized opening in its face. + +“This place gives me the willies,” U-Bone said, looking around. He noticed crumbling rock around the entrance, which on closer inspection, he realized were carvings of some kind of dog-dragon looking menacingly into his eyes. U-Bone decided not to inspect it further. It was definitely a dungeon. + +“So? Come on, guys, what do you think? Want to go inside?” Cloe asked. +“You know, I’d really rather just go back and see if we can score any free snacks from the picnic blankets,” U-Bone muttered, looking uncomfortable. +“Yeah, I’m not even sure I’ll fit in there,” Cliffford said. +“Uh, Cliffford, did you see the part up there in the paragraph before last where it said the entrance is conveniently Cliffford-sized? Do you remember the last story where you fit into the double doors of a shopping mall? You can’t worm your way out of this.” Cloe looked smug. “I’m sure there are all kinds of interesting things to eat in there anyway, U-Bone.” +“Oh, all right,” Cliffford and U-Bone sighed. “Let’s enter the dungeon.” +“Fuck* yeah!” Cloe shouted, and ran inside. + +*If you’re reading this to young children for some reason, have fun substituting the word of your choice. May we suggest fizzle? Forward? Finagle? Forsooth? Just say it with all the energy of a dog hopped up on self-righteousness and you’ll be fine. + +Cliffford did, indeed, fit inside the entrance, as well as the tunnel just inside. There were plenty of smells wafting around, and his head whipped from side to side trying to take them all in. In doing so, he failed to look at the ground ahead, and he tripped. + +“Look, a bone!” Clifford said happily as he scrambled up off the floor. Things were beginning to look up. Cliffford picked it up and gnawed around on it as they trekked deeper into the dungeon. + +U-Bone noticed some orangish clingy stuff close to the bottom of the cave wall. He sniffed it. “Slime mold? Don’t mind if I do.” He nibbled a little, and then scarfed the rest down in just a few bites. Not bad. He feels invigorated! + +“Oh, wow, another one!” Cliffford shouted around the bone in his mouth as he stopped to pick up a second bone. “And another! This dungeon is turning out to be pretty great.” + +“I told you!” Cloe grinned. She bounded down the tunnel ahead of the other two. “Come on, there’s a staircase that’s leading down!” + +The three dogs descended the stairs into near-blackness. No longer lit by the daylight streaming in from the entrance, the dungeon was distinctly murkier. Something brushed past U-Bone’s ear and he snapped at it. + +“I found a torch!” Cloe said. “I wonder if there’s any way to light it. We’d better take it with us. She grabbed the torch in her mouth and crept into the gloom. + +The trio inched forward, sniffing and listening before moving along the path. Eventually, the darkness lifted a little bit, and they approached a second, lit torch set into a sconce in the wall. Even Cloe felt better once she lit their torch, which now illuminated a small circle around them. + +Something else glinted in the darkness just beyond them. Cloe moved closer and the dogs saw a beautifully crafted, lightweight sword lying on the dusty floor. “This one’s all yours, U-Bone,” Cliffford said through his mouthful of bones. U-Bone gingerly took the sword and swung it around a few times. Cliffford and Cloe saw his apprehension turn to excitement as U-Bone growled, advanced on a spiderweb, and dispatched it. + +U-Bone stumbled upon a sheath and baldric for the sword a short distance away, and strapped them on. BOSS! A few turns of the path later, and the dogs found another staircase down. Come on, it’s a dungeon crawl. Of course they found another staircase! + +The dogs explored further and deeper into the dungeon. Cliffford’s mouth was crammed as full as possible with bones, and he looked sadly at each one he had to leave behind as they pressed on. + +“Humuhlauhsunrrmmmuh,” said the big rad dog, furrowing his brows. +“Hmm?” asked Cloe, through the side of her mouth around the torch. +“Mmmmahhhhh!” U-Bone hollered as he turned a bat into a meat icon that dropped a large leather satchel and a helmet that fit no one. + +Cliffford pawed the satchel with his foot, turning it over. It flopped open. There was nothing inside, so he started to put his bones in his bag. He was delighted to find every single one fit, and then some. “Huzzzah! A bag of holding!” he shouted, and raced forward to find more. + +Instead, Cliffford found a kobold corpse. He sniffed it, hesitated, sniffed again, shrugged, and dug in. “Any good?” U-Bone asked, sheathing his sword. “Ugh, no, this corpse is definitely tainted,” Clifford said, not lifting his head from his meal. U-Bone sniffed the corpse and took a bite. “Yeah, man, ugh, that’s rank. Don’t eat those guys.” + +The dogs began to feel thirsty from all their exertions, and conveniently the next level had some water! They descended the stairs to find themselves standing in front of an expanse of shimmering deep blue-violet, a gorgeous underground lake. + +“It’s beautiful!” Cloe sighed. She bent down to drink, and U-Bone followed suit. Cliffford bent over, too, and as he drank, his stomach began to twist. “Uh, guys?” Cliffford said. “HUUUGGGGGGHHHHHHHHHH,” he retched, and vomited into the lake. He lay down in the mud on the shore. + +U-Bone noticed a sign nearby. “Lake of Eternal Cleanliness,” he read. +“Dude, not anymore. HAH!” Cloe barked. + +Just then, they heard a shout ahead of them. A small, crotchety dog-dragon-hermit thing emerged from a hovel at the edge of the lake. + +“THIS LAKE HAS BEEN PRISTINE FOR NINE HUNDRED THIRTY SEVEN YEARS!” shouted the angry lake-keeper, shaking one fist wildly and wielding a Staff of Glowing Menace in the other. Cloe backed up, and U-Bone got his sword ready, but Cliffford still lay in the mud, groaning. + +Just then, Cliffford happened to twist his head at just the right angle to notice a small vial of something pink resting in the mud by his ear. He quaffed this (come on, Cliffford, stop ingesting all these questionable things!) and instantly felt not only invigorated, but also amplified! Cliffford barked an ear-poppingly loud enchanted bark, which repelled the lake-keeper and sent him splashing into the Lake of Recent Uncleanliness, and the three friends hauled ass* (*apples?) further down into the dungeon. + +By the time the dogs felt like they could slow down, they had descended what felt like (and was) the umpteenth flight of stairs. They came out in front of a small, narrow opening. Cloe held the torch while U-Bone scouted inside, and he noticed that the path was twistier and turnier than the rest of the dungeon had been. + +“I think it’s a labyrinth,” he said, as the two smaller dogs emerged from the entrance. “But Cliffford, I don’t think you’re going to fit.” + +“I’m going to look around and see if there’s any way around,” Cloe said, and trotted off. She came back a few moments later with a rolled-up piece of paper. + +“Hey guys, what’s this?” she said, dropping it in front of the others. +“It’s some kind of scroll,” U-Bone said, nosing at it. +“Is that like some kind of newspaper?” she asked. “I love newspapers! I like to go out really early in the morning right after they’re delivered, slide them out of their plastic sleeve, and shaaaaaaaaaake them so hard that they get all shredded up! It’s so funny!” Cloe giggled. “I’m going to go for it. I think this scroll is going to be pretty satisfying too.” + +Cloe whipped her head back and forth, and the scroll unfurled, revealing the text GRRMOC UHLPROOT in old-timey writing, and she stopped. “Grrmoc uhlproot?” she asked, and instantly, Cliffford fell to the ground. Yeah, the scroll was totally cursed. + +A moment later, Cliffford was back up again, only now he was the size of a normal dog. Yup! + +This was a very useful development for our plot, because they heard shouts from the top of the stairs. The angry lake-keeper’s scratchy toenails clicked on the stone treads as he skittered down toward them. + +The dogs wasted no time, and all three dove right into the entrance to the labyrinth. They tried their best to stay together, but Clifford, unused to being tiny, lagged behind. He felt weak, and needed food badly. + +The other dogs called encouragingly behind them for Cliffford to hurry the fuck* (*fart!) up, but he stopped short when he saw some meat lying in a corner. It was another kobold corpse. His brain said to him, “Hey, wait a second, Cliffford,” but then he discovered he’d already begun to eat it. + +Oh yeah, this one’s tainted too. + +The dogs all managed to find their way to the center of the labyrinth in enough time to marvel at the treasure there: the Squeaky Toy of Canidae. A lovely, fuzzy, stuffed squirrel with a magical squeaker that never, ever breaks, and that is also enchanted with legendary but vague special powers. + +Their marvel was cut short by the angry lake-keeper, who barreled into the center of the labyrinth behind them, hollering. Cliffford turned to face the lake-keeper, and felt a familiar twist in his stomach as he did so. + +“HGGGGGRRRRRRRPH!” he grunted, spewing vomit all over the lake-keeper. It sizzled as it hit the lake-keeper, dealing quite a bit of damage. It also broke a curse on the lake-keeper, and from his crusty, smelly robes emerged Moc. + +“What is going on? I’ve been in this godforsaken cave for the last five and a half days* (*937 dog-hours!). The whole time I’ve been having repeated nightmares that I’m some sort of disgusting hermit encamped by a lake.” He shook himself, and droplets of vomit spattered the labyrinth walls. + +“And you, Cliffford, your vomit seems to be a recurring feature in my nightmares as well. Can we please get out of here?” Moc huffed, as he caught a whiff of the tainted, partially digested kobold corpse clinging to his fur. + +“Hey, guys, I think Moc’s curse isn’t the only one that’s broken!!” Cliffford shouted, as he began to grow and grow. The rapid growth broke down the walls of the labyrinth, revealing a clear path to the exit. + +Cliffford stuffed the Squeaky Toy of Canidae into his bag of holding, and all the other dogs hopped on his back and held on for dear life as Cliffford sprinted out of the dungeon and back into the sunshine of the park. + +After rolling around in the grass to get the last of the spiderwebs and vomit off themselves, the dogs went back to Cliffford’s doghouse, where they all feasted on the bones from Cliffford’s bag of holding and took turns with the Squeaky Toy of Canidae. + +The friends all agreed that it was the best dungeon crawl ever! + +THE END!! +""" From 1c815cf57747ad0ac324e7bd72951d4cbaa7b466 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 29 Jan 2017 09:16:53 -0800 Subject: [PATCH 74/78] :rage::on: Updated at https://danielx.net/editor/ --- apps/notepad.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/notepad.coffee b/apps/notepad.coffee index 42aa2e4..ab195c0 100644 --- a/apps/notepad.coffee +++ b/apps/notepad.coffee @@ -15,6 +15,7 @@ module.exports = () -> TODO = -> console.log "TODO" textarea = document.createElement "textarea" + textarea.spellcheck = false handlers = Model().include(FileIO).extend loadFile: (blob) -> From 452c159a65c2f0057a0595e1a03d96c09bce09f6 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 29 Jan 2017 10:08:53 -0800 Subject: [PATCH 75/78] :smirk::clock6: Updated at https://danielx.net/editor/ --- apps/dungeon-of-sadness.coffee | 18 ++++++++++++++++++ apps/notepad.coffee | 2 +- apps/spreadsheet.coffee | 2 +- issues/2016-12.coffee | 17 +++++++++++------ issues/{2017-01.coffee => 2017-02.coffee} | 8 ++++---- main.coffee | 7 ++++++- system/achievement.coffee | 10 ++++++++++ system/associations.coffee | 20 ++++++++++++++++++++ 8 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 apps/dungeon-of-sadness.coffee rename issues/{2017-01.coffee => 2017-02.coffee} (96%) diff --git a/apps/dungeon-of-sadness.coffee b/apps/dungeon-of-sadness.coffee new file mode 100644 index 0000000..99123ff --- /dev/null +++ b/apps/dungeon-of-sadness.coffee @@ -0,0 +1,18 @@ +Model = require "model" + +module.exports = -> + {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI + + frame = document.createElement "iframe" + frame.src = "https://danielx.net/ld33/" + + system.Achievement.unlock "The dungeon is in our heart" + + windowView = Window + title: "Dungeon of Sadness" + content: frame + menuBar: null + width: 648 + height: 507 + + return windowView diff --git a/apps/notepad.coffee b/apps/notepad.coffee index ab195c0..5302178 100644 --- a/apps/notepad.coffee +++ b/apps/notepad.coffee @@ -1,7 +1,7 @@ FileIO = require "../os/file-io" Model = require "model" -module.exports = () -> +module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index 06dbdc4..5295e73 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -1,7 +1,7 @@ FileIO = require "../os/file-io" Model = require "model" -module.exports = () -> +module.exports = -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI system.Achievement.unlock "Microsoft Access 97" diff --git a/issues/2016-12.coffee b/issues/2016-12.coffee index b5c1944..0689c54 100644 --- a/issues/2016-12.coffee +++ b/issues/2016-12.coffee @@ -7,8 +7,10 @@ ajax = Ajax() issueTag = "2016-12" -module.exports = (os) -> - {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = os.UI +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + + system.Achievement.unlock "Issue 1" img = document.createElement "img" img.src = "https://68.media.tumblr.com/6a141d69564a29ac7d4071df5d519808/tumblr_o0rbb4TA1k1urr1ryo1_500.gif" @@ -33,6 +35,7 @@ module.exports = (os) -> progressView.value(newValue) progressView.message(initialMessage + ellipses) if newValue > 2 + system.Achievement.unlock "No rush" clearInterval intervalId Modal.hide() , 15 @@ -67,12 +70,14 @@ module.exports = (os) -> subscribe: -> require("../mailchimp").show() notepadexe: -> - app = Notepad(os) + app = Notepad() document.body.appendChild app.element mSAccess97: -> - app = Spreadsheet(os) + app = Spreadsheet() document.body.appendChild app.element mysterySmell: -> + system.Achievement.unlock "Cover-2-cover" + div = document.createElement "div" div.textContent = require "../stories/mystery-smell" div.style.padding = "1em" @@ -92,7 +97,7 @@ module.exports = (os) -> [A]pps [N]otepad.exe [S]tories - Mystery Smell + [M]ystery Smell S[o]cial Media [V]iew Comments [C]omment @@ -107,5 +112,5 @@ module.exports = (os) -> menuBar: menuBar.element width: 508 height: 604 - document.body.appendChild windowView.element + document.body.appendChild windowView.element diff --git a/issues/2017-01.coffee b/issues/2017-02.coffee similarity index 96% rename from issues/2017-01.coffee rename to issues/2017-02.coffee index 7ba428a..175da12 100644 --- a/issues/2017-01.coffee +++ b/issues/2017-02.coffee @@ -7,15 +7,11 @@ TextEditor = require "../apps/text-editor" Social = require "../social/social" -Explorer = require "../apps/explorer" - {parentElementOfType, emptyElement} = require "../util" module.exports = -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI - document.body.appendChild Explorer() - container = document.createElement "container" setTimeout -> @@ -25,6 +21,10 @@ module.exports = -> system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" system.writeFile "issue-2/provision.txt", new Blob [require "../stories/provision"], type: "text/plain" system.writeFile "issue-2/dungeon-dog.txt", new Blob [require "../stories/dungeon-dog"], type: "text/plain" + system.writeFile "issue-2/dsad.exe", new Blob [""], type: "application/exe" + system.writeFile "issue-2/zine2.exe", new Blob [""], type: "application/exe" + + system.writeFile "issue-1/zine1.exe", new Blob [""], type: "application/exe" pages = front: """ diff --git a/main.coffee b/main.coffee index a1f56d6..1d4c9c8 100644 --- a/main.coffee +++ b/main.coffee @@ -15,4 +15,9 @@ style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style -require("./issues/2017-01")() +# Desktop +Explorer = require "./apps/explorer" +document.body.appendChild Explorer() + +# Launch Current Issue +require("./issues/2017-02")() diff --git a/system/achievement.coffee b/system/achievement.coffee index fcad2ee..f668963 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -18,6 +18,11 @@ achievementData = [{ icon: "📗" group: "Issue 1" description: "Read the entire issue" +}, { + text: "No rush" + icon: "⏳" + group: "Issue 1" + description: "Patience is a virtue" }, { text: "Issue 2" icon: "📰" @@ -38,6 +43,11 @@ achievementData = [{ icon: "🐸" group: "Issue 2" description: "Visit frogfeels.com" +}, { + text: "The dungeon is in our heart" + icon: "😭" + group: "Issue 2" + description: "Played dungeon of sadness" }, { # Apps text: "Notepad.exe" icon: "📝" diff --git a/system/associations.coffee b/system/associations.coffee index 0f627e8..20de981 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -6,6 +6,7 @@ TextEditor = require "../apps/text-editor" Spreadsheet = require "../apps/spreadsheet" PixelEditor = require "../apps/pixel" Markdown = require "../apps/markdown" +DSad = require "../apps/dungeon-of-sadness" openWith = (App) -> (file) -> @@ -70,6 +71,25 @@ module.exports = (I, self) -> filter: (file) -> file.type.match /^audio\// fn: openWith(AudioBro) + }, { + name: "dsad.exe" + filter: (file) -> + file.path.match /dsad\.exe$/ + fn: -> + app = DSad() + document.body.appendChild app.element + }, { + name: "zine2.exe" + filter: (file) -> + file.path.match /zine2\.exe$/ + fn: -> + require("../issues/2017-02")() + }, { + name: "zine1.exe" + filter: (file) -> + file.path.match /zine1\.exe$/ + fn: -> + require("../issues/2016-12")() }] # Open JSON arrays in spreadsheet From afd82654abc1638675425ebe18c7288309493a71 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 29 Jan 2017 10:17:41 -0800 Subject: [PATCH 76/78] :mortar_board::palm_tree: Updated at https://danielx.net/editor/ --- issues/2017-02.coffee | 2 +- style.styl | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/issues/2017-02.coffee b/issues/2017-02.coffee index 175da12..2ab0d28 100644 --- a/issues/2017-02.coffee +++ b/issues/2017-02.coffee @@ -36,7 +36,7 @@ module.exports = -> img(width=640 height="auto" src="https://books.google.com/books/content?id=2cgDAAAAMBAJ&rview=1&pg=PA10&img=1&zoom=3&hl=en&sig=ACfU3U3477L46r0KxSQusJrQ6w9qxIQ70w&w=1280") """ table: """ - div + div(style="padding: 1em;") h1 Table of Contents ul li diff --git a/style.styl b/style.styl index 79b28e2..575f538 100644 --- a/style.styl +++ b/style.styl @@ -3,6 +3,11 @@ body h1 line-height: 1.25em + margin: 0 + +ul + list-style-type: square + padding-left: 1.25em user-select(s) user-select: s From b68590dba873bf808e270b6dffb4d6abbbc4b21f Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 21 May 2017 10:59:23 -0700 Subject: [PATCH 77/78] This commit intentionally left blank From 6ad4389a63e0099c63d2143692005b4140c13891 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 21 May 2017 10:59:52 -0700 Subject: [PATCH 78/78] :hourglass_flowing_sand::door: Updated at https://danielx.net/editor/ --- README.md | 40 ++- TODO.md | 32 ++- apps/achievement-status.coffee | 15 ++ apps/audio-bro.coffee | 12 +- apps/chateau.coffee | 309 ++-------------------- apps/contrasaurus.coffee | 17 ++ apps/dungeon-of-sadness.coffee | 18 +- apps/explorer.coffee | 138 ++++++++-- apps/markdown.coffee | 79 +++++- apps/my-briefcase.coffee | 172 +++++++++++++ apps/notepad.coffee | 42 ++- apps/pixel.coffee | 43 +--- apps/story-reader.coffee | 16 ++ apps/text-editor.coffee | 67 ++++- extensions.coffee | 43 +++- feedback.coffee | 17 ++ issues/2016-12.coffee | 6 +- issues/2017-02.coffee | 18 +- issues/2017-03.coffee | 131 ++++++++++ issues/2017-04.coffee | 174 +++++++++++++ issues/2017-05.coffee | 99 +++++++ lib/app-drop.coffee | 35 +++ lib/dexie-fs.coffee | 65 +++++ lib/drop.coffee | 2 - lib/hamlet.js | 2 +- lib/iframe-app.coffee | 88 +++++++ lib/mount-fs.coffee | 75 ++++++ lib/outbound-clicks.coffee | 2 +- lib/pkg-fs.coffee | 105 ++++++++ lib/s3-fs.coffee | 171 +++++++++++++ lib/stylus.min.js | 6 + lib/system-client.coffee | 40 +++ main.coffee | 24 +- os/file-io.coffee | 68 +++-- pixie.cson | 10 +- presenters/home-button.coffee | 112 ++++++++ social/social.coffee | 6 +- stories/blue-light-special.coffee | 96 +++++++ stories/crescent.coffee | 13 + stories/dungeon-dog.coffee | 6 +- stories/izzy.coffee | 61 +++++ stories/marigold.coffee | 87 +++++++ stories/provision.coffee | 2 +- stories/residue.coffee | 79 ++++++ style.styl | 59 ++++- system.coffee | 165 ++++++++---- system/achievement.coffee | 90 +++++++ system/associations.coffee | 247 +++++++++++++++--- system/module.coffee | 412 ++++++++++++++++++++++++++++-- templates/file.jadelet | 4 +- templates/folder.jadelet | 4 +- templates/home-button.jadelet | 1 + templates/site-url.jadelet | 1 + templates/version.jadelet | 2 + test/system/module.coffee | 176 +++++++++---- util.coffee | 104 +++++++- 56 files changed, 3290 insertions(+), 618 deletions(-) create mode 100644 apps/achievement-status.coffee create mode 100644 apps/contrasaurus.coffee create mode 100644 apps/my-briefcase.coffee create mode 100644 apps/story-reader.coffee create mode 100644 feedback.coffee create mode 100644 issues/2017-03.coffee create mode 100644 issues/2017-04.coffee create mode 100644 issues/2017-05.coffee create mode 100644 lib/app-drop.coffee create mode 100644 lib/dexie-fs.coffee create mode 100644 lib/iframe-app.coffee create mode 100644 lib/mount-fs.coffee create mode 100644 lib/pkg-fs.coffee create mode 100644 lib/s3-fs.coffee create mode 100644 lib/stylus.min.js create mode 100644 lib/system-client.coffee create mode 100644 presenters/home-button.coffee create mode 100644 stories/blue-light-special.coffee create mode 100644 stories/crescent.coffee create mode 100644 stories/izzy.coffee create mode 100644 stories/marigold.coffee create mode 100644 stories/residue.coffee create mode 100644 templates/home-button.jadelet create mode 100644 templates/site-url.jadelet create mode 100644 templates/version.jadelet diff --git a/README.md b/README.md index 2686eea..767cb13 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# zine +# Zine OS + DIY E-Zine and Operating System + +Interfaces +========== + +FS Interface +------------ + +Read a blob from a path, returns a promise fulfilled with the blob object. The +blob is annotated with the path i.e.: blob.path == path + + read: (path) -> + +Write a blob to a path, returns a promise that is fulfilled when the write succeeds. + + write: (path, blob) -> + +Delete a file at a path, returns a promise that is fulfilled when the delete succeeds. + + delete: (path) -> + +Returns a promise + + list: (directoryPath) -> + + +FileEntry Interface +------------------- + + path: + size: + type: + +FolderEntry Interface +--------------------- + + folder: true + path: diff --git a/TODO.md b/TODO.md index 9fc0f2f..7573cd5 100644 --- a/TODO.md +++ b/TODO.md @@ -3,25 +3,27 @@ TODO System Features --------------- -[x] File Browser +[X] File Browser -[x] App Associations +[X] App Associations -[x] File Context Menu +[X] File Context Menu -[x] Folders +[X] Folders -[x] Desktop Icons +[X] Desktop Icons [ ] Desktop Background [ ] Help Documentation -[ ] Compiler +[X] Compilerz -[x] Require/Include Local Files +[X] Require/Include Local Files -[ ] Custom Models/Views +[ ] Drag 'n' Drop + +[X] Cloud Briefcase Applications ------------ @@ -38,13 +40,21 @@ Markdown / Wiki Image Munger +Programatic Animator + +Pixel Editor + +Music Maker + +Database? + Network Social -------------- -[ ] CDN - [x] Comments [ ] Sharing -[ ] Remote Files +[ ] Remote Files (Network Neighborhood) + +[X] Personal Homepage diff --git a/apps/achievement-status.coffee b/apps/achievement-status.coffee new file mode 100644 index 0000000..b8bd0fd --- /dev/null +++ b/apps/achievement-status.coffee @@ -0,0 +1,15 @@ +module.exports = -> + {Achievement, UI} = system + {Window} = UI + + cheevoElement = Achievement.progressView() + cheevoElement.style.width = "100%" + cheevoElement.style.padding = "1em" + + Achievement.unlock "Check yo' self" + + windowView = Window + title: "Cheevos" + content: cheevoElement + width: 640 + height: 480 diff --git a/apps/audio-bro.coffee b/apps/audio-bro.coffee index ae3e9f8..1fb6679 100644 --- a/apps/audio-bro.coffee +++ b/apps/audio-bro.coffee @@ -5,7 +5,7 @@ Model = require "model" module.exports = -> # Global system - {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {ContextMenu, MenuBar, Modal, Observable, Progress, Util:{parseMenu}, Window} = system.UI {Achievement} = system Achievement.unlock "Pump up the jam" @@ -14,8 +14,11 @@ module.exports = -> audio.controls = true audio.autoplay = true + filePath = Observable() + handlers = Model().include(FileIO).extend loadFile: (blob) -> + filePath blob.path audio.src = URL.createObjectURL blob exit: -> @@ -31,11 +34,16 @@ module.exports = -> handlers: handlers windowView = Window - title: "Audio Bro" + title: -> + if path = filePath() + "Audio Bro - #{path}" + else + "Audio Bro" content: audio menuBar: menuBar.element width: 308 height: 80 + iconEmoji: "🎶" windowView.loadFile = handlers.loadFile diff --git a/apps/chateau.coffee b/apps/chateau.coffee index bee9520..6f9ebfc 100644 --- a/apps/chateau.coffee +++ b/apps/chateau.coffee @@ -1,293 +1,24 @@ -# Chat Based MUD - -Drop = require "../lib/drop" -FileIO = require "../os/file-io" -Model = require "model" - -Template = require "../templates/chateau" - -sortBy = (attribute) -> - (a, b) -> - a[attribute] - b[attribute] - -rand = (n) -> - Math.floor(Math.random() * n) - -createSocket = (room, accountId) -> - new WebSocket("wss://message-relay.gomix.me/r/#{room}?accountId=#{accountId}") - -setLocal = (data) -> - localStorage.chateau = JSON.stringify(data) - -getLocal = -> - try - JSON.parse localStorage.chateau +IFrameApp = require "../lib/iframe-app" module.exports = -> - # Global system - {ContextMenu, MenuBar, Modal, Observable, Progress, Util:{parseMenu}, Window} = system.UI - - wordsArray = Observable [] - connected = Observable false - - avatars = {} - myAccountId = null - - localData = getLocal() - - if localData - myAccountId = localData.myAccountId - avatars[myAccountId] = localData.myAvatar - else - myAccountId = "Anon" + Math.random().toString().substr(3) - - addAvatar = (accountId, url) -> - avatars[accountId] = - img: null - color: "orange" - x: rand canvas.width - y: rand canvas.height - - updateWords = -> - wordsElements = Object.keys(avatars).map (key) -> - avatars[key] - .filter ({say}) -> - say - .map ({x, y, say}) -> - words = document.createElement "words" - words.style.top = "#{y - 50}px" - words.style.left = "#{x}px" - words.innerText = say - - return words - - wordsArray wordsElements - - setAvatar = (blob) -> - return unless myAvatar = avatars[myAccountId] - - Image.fromBlob(blob) - .then (img) -> - myAvatar.img = img - - blob.readAsDataURL() - .then (url) -> - myAvatar.dataURL = url - - broadcast - avatar: url - - broadcast = (data) -> - socket.send JSON.stringify - type: "broadcast" - message: data - - directMessage = (data, accountId) -> - socket.send JSON.stringify - type: "dm" - recipient: accountId - message: data - - updateNewcomer = (accountId) -> - return unless myAvatar = avatars[myAccountId] - - data = - move: - x: myAvatar.x - y: myAvatar.y - avatar: myAvatar.dataURL - say: myAvatar.say - - directMessage(data, accountId) - - canvas = document.createElement 'canvas' - context = canvas.getContext('2d') - - canvas.onclick = (e) -> - {pageX, pageY, currentTarget} = e - {top, left} = currentTarget.getBoundingClientRect() - - x = pageX - left - y = pageY - top - - broadcast - move: - x: x - y: y - - content = Template - canvas: canvas - connected: connected - words: wordsArray - submit: (e) -> - e.preventDefault() - - input = content.querySelector('input') - words = input.value - if words - input.value = "" - - broadcast - say: words - - Drop content, (e) -> - files = e.dataTransfer.files - - if files.length - file = files[0] - - setAvatar(file) - - socket = createSocket("cshopmilli", myAccountId) + {Achievement} = system - setInterval -> - try - socket.send JSON.stringify - meta: "keepalive" - , 30000 - - socket.onopen = -> - - - socket.onclose = -> - connected false - - socket.onmessage = (e) -> - message = JSON.parse e.data - console.log message - - accountId = message.accountId - - switch message.type - when "meta" - if message.status is "connect" - connected true - myAccountId = accountId - when "connect" - # Add Avatar - addAvatar(accountId) - updateNewcomer(accountId) - when "disconnect" - # Remove Avatar - delete avatars[accountId] - updateWords() - when "broadcast", "dm" - {message} = message - - receiveMessage(message, accountId) - - receiveMessage = (message, accountId) -> - avatars[accountId] ?= - x: rand canvas.width - y: rand canvas.height - color: "orange" - - if message.move - {x, y} = message.move - avatars[accountId].x = x - avatars[accountId].y = y - - if message.say - avatars[accountId].say = message.say - - if message.avatar - img = new Image() - img.src = message.avatar - - avatars[accountId].img = img - - updateWords() - - AboutTemplate = system.compileTemplate """ - container - h1 About - p Chat with your friends in this online chateau! - - p Drag and drop an image to become your avatar. - - p Click to position yourself in the room. - - p Say what you want to talk with others! - """ - - handlers = Model().include(FileIO).extend - exit: -> - windowView.element.remove() - - about: -> - Modal.show AboutTemplate() - - menuBar = MenuBar - items: parseMenu """ - [F]ile - E[x]it - [H]elp - [A]bout - """ - handlers: handlers - - windowView = Window + app = IFrameApp + src: "https://danielx.net/chateau/" + width: 960 + height: 540 title: "Chateau" - content: content - menuBar: menuBar.element - width: 640 - height: 480 - - roomstate = - background: null - objects: [] - - repaint = -> - # Draw BG - context.fillStyle = 'blue' - context.fillRect(0, 0, canvas.width, canvas.height) - - {background, objects} = roomstate - if background - context.drawImage(background, 0, 0, canvas.width, canvas.height) - - # Draw Avatars/Objects - Object.keys(avatars).map (accountId) -> - avatars[accountId] - .concat(objects).sort(sortBy("z")).forEach ({color, img, x, y}) -> - if img - {width, height} = img - context.drawImage(img, x - width / 2, y - height / 2) - else - context.fillStyle = color - context.fillRect(x - 25, y - 25, 50, 50) - - # Draw connection status - if connected() - indicatorColor = "green" - else - indicatorColor = "red" - - context.beginPath() - context.arc(canvas.width - 20, 20, 10, 0, 2 * Math.PI, false) - context.fillStyle = indicatorColor - context.fill() - context.lineWidth = 2 - context.strokeStyle = '#003300' - context.stroke() - - resize = -> - rect = canvas.getBoundingClientRect() - canvas.width = rect.width - canvas.height = rect.height - - windowView.on "resize", resize - - windowView.loadFile = handlers.loadFile - - animate = -> - requestAnimationFrame animate - repaint() - - animate() - - # TODO: ViewDidLoad? or equivalent event? - setTimeout -> - resize() - - return windowView + iconEmoji: "🍷" + + app.on "event", (name) -> + switch name + when "login" + Achievement.unlock "Enter the Chateau" + when "custom-avatar" + Achievement.unlock "Puttin' on the Ritz" + when "custom-background" + Achievement.unlock "Paint the town red" + when "file-upload" + Achievement.unlock "It's in the cloud" + + return app diff --git a/apps/contrasaurus.coffee b/apps/contrasaurus.coffee new file mode 100644 index 0000000..acc9a2f --- /dev/null +++ b/apps/contrasaurus.coffee @@ -0,0 +1,17 @@ +module.exports = -> + {Achievement, iframeApp} = system + + app = iframeApp + src: "https://contrasaur.us/" + width: 960 + height: 540 + title: "Contrasaurus: Defender of the American Dream" + + Achievement.unlock "Rawr" + + app.on "event", (name) -> + switch name + when "win" + Achievement.unlock "A winner is you" + + return app diff --git a/apps/dungeon-of-sadness.coffee b/apps/dungeon-of-sadness.coffee index 99123ff..ddaf317 100644 --- a/apps/dungeon-of-sadness.coffee +++ b/apps/dungeon-of-sadness.coffee @@ -1,18 +1,12 @@ -Model = require "model" - module.exports = -> - {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI - - frame = document.createElement "iframe" - frame.src = "https://danielx.net/ld33/" + {Achievement, iframeApp} = system - system.Achievement.unlock "The dungeon is in our heart" - - windowView = Window + app = iframeApp title: "Dungeon of Sadness" - content: frame - menuBar: null + src: "https://danielx.net/ld33/" width: 648 height: 507 - return windowView + Achievement.unlock "The dungeon is in our heart" + + return app diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 31ab27b..1b8b9e3 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -1,10 +1,9 @@ # Explorer File Browser # # Explore the file system like adventureres of old! -# TODO: Drag and drop files and folders +# TODO: Drop files onto applications # TODO: Select multiple # TOOD: Keyboard Input -# TODO: Style file types Drop = require "../lib/drop" FileTemplate = require "../templates/file" @@ -12,20 +11,75 @@ FolderTemplate = require "../templates/folder" {emptyElement} = require "../util" +extractPath = (element) -> + while element + path = element.getAttribute("path") + return path if path + element = element.parentElement + module.exports = Explorer = (options={}) -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI {path} = options path ?= '/' explorer = document.createElement "explorer" + explorer.setAttribute("path", path) Drop explorer, (e) -> + return if e.defaultPrevented + + targetPath = extractPath(e.target) or path + folderTarget = targetPath.match(/\/$/) + + fileSelectionData = e.dataTransfer.getData("zineos/file-selection") + + if fileSelectionData + data = JSON.parse fileSelectionData + + if folderTarget + system.moveFileSelection(data, targetPath) + else + # Attempt to open file in app + selectedFile = data.files[0] + console.log "Open in app #{targetPath} <- #{selectedFile}" + system.readFile(selectedFile.path) + .then (file) -> + system.execPathWithFile(targetPath, file) + e.preventDefault() + + return + files = e.dataTransfer.files if files.length - files.forEach (file) -> - newPath = path + file.name - system.writeFile(newPath, file, true) + e.preventDefault() + if folderTarget + files.forEach (file) -> + newPath = targetPath + file.name + system.writeFile(newPath, file, true) + else + file = files[0] + system.execPathWithFile(targetPath, file) + + explorerContextMenu = ContextMenu + items: parseMenu """ + [N]ew File + """ + handlers: + newFile: -> + Modal.prompt "Filename", "#{path}newfile.txt" + .then (newFilePath) -> + if newFilePath + system.writeFile newFilePath, new Blob [], type: "text/plain" + + explorer.oncontextmenu = (e) -> + return if e.defaultPrevented + e.preventDefault() + + explorerContextMenu.display + inElement: document.body + x: e.pageX + y: e.pageY contextMenuFor = (file, e) -> return if e.defaultPrevented @@ -34,7 +88,6 @@ module.exports = Explorer = (options={}) -> contextMenuHandlers = open: -> system.open(file) - openWith: -> #TODO cut: -> #TODO copy: -> #TODO delete: -> @@ -43,9 +96,12 @@ module.exports = Explorer = (options={}) -> Modal.prompt "Filename", file.path .then (newPath) -> if newPath - system.deleteFile(file.path) - system.writeFile(newPath, file.blob) - properties: -> #TODO + system.moveFile(file.path, newPath) + properties: -> + pre = document.createElement "pre" + pre.textContent = JSON.stringify(file, null, 2) + pre.style = "padding: 1rem" + Modal.show pre editMIMEType: -> Modal.prompt "MIME Type", file.type .then (newType) -> @@ -115,10 +171,24 @@ module.exports = Explorer = (options={}) -> handlers: open: -> addWindow(folder.path) - delete: -> # TODO: Delete all files under folder + delete: -> + system.readTree(folder.path) + .then (results) -> + Promise.all results.map (result) -> + system.deleteFile(result.path) rename: -> - ;# TODO: Rename all files under folder (!) - # May want to think about inodes or something that makes this simpler + Modal.prompt "Name", folder.path + .then (newName) -> + return unless newName + + # Ensure trailing slash + newName = newName.replace(/\/*$/, "/") + + system.readTree(folder.path) + .then (files) -> + Promise.all files.map (file) -> + newPath = file.path.replace(folder.path, newName) + system.moveFile(file.path, newPath) properties: -> # TODO contextMenu.display @@ -134,34 +204,51 @@ module.exports = Explorer = (options={}) -> addedFolders = {} files.forEach (file) -> - if file.relativePath.match /\// # folder - folderPath = file.relativePath.replace /\/.*$/, "" + if file.relativePath.match /\/$/ # folder + folderPath = file.relativePath addedFolders[folderPath] = true return - file.dblclick = -> - console.log "dblclick", file - system.open file + Object.assign file, + displayName: file.relativePath + + dblclick: -> + system.open file + + contextmenu: (e) -> + contextMenuFor(file, e) - file.contextmenu = (e) -> - contextMenuFor(file, e) + dragstart: (e) -> + # Note: Blobs don't make it through the stringify + e.dataTransfer.setData "zineos/file-selection", JSON.stringify + sourcePath: path + files: [ file ] fileElement = FileTemplate file if file.type.match /^image\// - url = URL.createObjectURL file.blob - fileElement.querySelector('icon').style.backgroundImage = "url(#{url})" + file.blob.getURL() + .then (url) -> + icon = fileElement.querySelector('icon') + icon.style.backgroundImage = "url(#{url})" + icon.style.backgroundSize = "100%" + icon.style.backgroundPosition = "50%" explorer.appendChild fileElement - Object.keys(addedFolders).forEach (folderName) -> + Object.keys(addedFolders).reverse().forEach (folderName) -> folder = - path: "#{path}#{folderName}/" + path: "#{path}#{folderName}" relativePath: folderName + displayName: folderName.replace(/\/$/, "") contextmenu: (e) -> contextMenuForFolder(folder, e) dblclick: -> # Open folder in new window addWindow(folder.path) + dragstart: (e) -> + e.dataTransfer.setData "zineos/file-selection", JSON.stringify + sourcePath: folder.path.slice(0, folder.path.length - folder.relativePath.length) + files: [ folder ] folderElement = FolderTemplate folder explorer.insertBefore(folderElement, explorer.firstChild) @@ -174,9 +261,7 @@ module.exports = Explorer = (options={}) -> system.fs.on "update", (path) -> update() addWindow = (path) -> - element = document.createElement "container" - - element.appendChild Explorer + element = Explorer path: path windowView = Window @@ -185,6 +270,7 @@ module.exports = Explorer = (options={}) -> menuBar: null width: 640 height: 480 + iconEmoji: "📂" document.body.appendChild windowView.element diff --git a/apps/markdown.coffee b/apps/markdown.coffee index 1a706c9..d109cf1 100644 --- a/apps/markdown.coffee +++ b/apps/markdown.coffee @@ -3,24 +3,85 @@ FileIO = require "../os/file-io" Model = require "model" +{absolutizePath} = require "../util" + +isRelative = (url) -> + url.match /^\.\.?\// + module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI container = document.createElement 'container' - container.style.padding = "1em" + container.style.padding = "1rem" + container.style.userSelect = "initial" + + baseDir = "" + navigationStack = [] + # TODO: Maintain a container stack to keep state + + navigateToPath = (path) -> + system.readFile(path) + .then (blob) -> + handlers.loadFile(blob, path) + + rewriteURL = (url) -> + Promise.resolve() + .then -> + if isRelative(url) + targetPath = absolutizePath baseDir, url + + system.urlForPath(targetPath) + else if url.match /^\// # Absolute paths + targetPath = absolutizePath "", url + system.urlForPath(targetPath) + else + url + + rewriteURLs = (container) -> + container.querySelectorAll("img").forEach (img) -> + url = img.getAttribute("src") + + if url + rewriteURL(url) + .then (url) -> + img.src = url handlers = Model().include(FileIO).extend - loadFile: (blob) -> + loadFile: (blob, path) -> + navigationStack.push path + baseDir = path.replace /\/[^/]*$/, "" + blob.readAsText() .then (textContent) -> - container.innerHTML = marked(textContent) + if path.match /\.html$/ + container.innerHTML = textContent + else + container.innerHTML = marked(textContent) + + rewriteURLs(container) saveData: -> exit: -> windowView.element.remove() + # Handle Links + container.addEventListener "click", (e) -> + [anchor] = e.path.filter (element) -> + element.nodeName is "A" + + if anchor + url = anchor.getAttribute("href") + + if isRelative(url) + e.preventDefault() + path = absolutizePath baseDir, url + + # Navigate to page + navigateToPath(path) + + menuBar = MenuBar items: parseMenu """ [F]ile @@ -37,6 +98,18 @@ module.exports = -> width: 720 height: 480 + windowView.element.setAttribute("tabindex", -1) + windowView.element.addEventListener "keydown", (e) -> + {key} = e + + if key is "Backspace" + e.preventDefault() + if navigationStack.length > 1 + navigationStack.pop() + + lastPath = navigationStack.pop() + navigateToPath(lastPath) + windowView.loadFile = handlers.loadFile return windowView diff --git a/apps/my-briefcase.coffee b/apps/my-briefcase.coffee new file mode 100644 index 0000000..a9a62b4 --- /dev/null +++ b/apps/my-briefcase.coffee @@ -0,0 +1,172 @@ +### +Use the madness that is Amazon Cognito to support a 'My Briefcase' functionality. + +This depends on having the AWS library available: +- https://sdk.amazonaws.com/js/aws-sdk-2.2.42.min.js + +This is where you can put the files that you want to access from the cloud. + +They'll live in the whimsy-fs bucket under the path to your aws user id. + +The subdomain -> s3 proxy will have a map from simple names to the crazy ids. + +The proxy will serve the /public folder in your 'briefcase'. You can put your +blog or apps or whatever there. The rest currently isn't 'private', but maybe +it should be. We can set the access control when uploading. + +Ideally the briefcase will be browsable like the local FS and you'll be able to +run files from it, load them in applications, save files there, and drag n drop +between them. +### + +Explorer = require "./explorer" +FileTemplate = require "../templates/file" +FolderTemplate = require "../templates/folder" + +S3FS = require "../lib/s3-fs" + +{emptyElement, pinvoke} = require "../util" + +window.onAmazonLoginReady = -> + amazon.Login.setClientId('amzn1.application-oa2-client.29b275f9076a406c90a66b025fab96bf') + +do (d=document) -> + r = d.createElement 'div' + r.id = "amazon-root" + d.body.appendChild r + a = d.createElement('script') + a.type = 'text/javascript' + a.async = true + a.id = 'amazon-login-sdk' + a.src = 'https://api-cdn.amazon.com/sdk/login1.js' + r.appendChild(a) + + +module.exports = -> + {Observable, Window} = system.UI + + system.Achievement.unlock "Oh no, my files!" + + LoginTemplate = system.compileTemplate """ + span(style="text-align: center; padding: 0 2em;") + h1 My Briefcase + p= @description + a#LoginWithAmazon(@click) + img(border="0" alt="Login with Amazon" src="https://images-na.ssl-images-amazon.com/images/G/01/lwa/btnLWA_gold_156x32.png" width="156" height="32") + """ + + LoadedTemplate = system.compileTemplate """ + section + h1 Connected! + p Loading files... + """ + + # Observable holding content element + content = Observable null + + receivedCredentials = -> + console.log AWS.config.credentials + id = AWS.config.credentials.identityId + + content LoadedTemplate() + + bucket = new AWS.S3 + params: + Bucket: "whimsy-fs" + + fs = S3FS(id, bucket) + + uuidToken = id.split(":")[1] + + system.fs.mount "/My Briefcase/", fs + + infoBlob = new Blob [""" + Welcome to Your Cloud Briefcase + =============================== + + Store your files in a magical cloud that floats between computers. + + Files stored in `My Briefcase/public` are available to anyone on the + internet. (Technically so are all the files in your cloud briefcase... + Security: Coming Soon™) + + But the ones in /public are easily accessible, like when computing was fun + again. [Check this out](https://#{uuidToken}.whimsy.space/info.md) + and see what I mean. + + You can get your own cool and non-ugly subdomain if you contact me (the + creator of this computing system). Just send me your id and the short + name you'd prefer. DM me in the friendsofjack slack or something. + """] , type: "text/markdown; charset=utf-8" + system.writeFile "/My Briefcase/public/info.md", infoBlob + system.writeFile "/My Briefcase/public/.keep", new Blob [] + + content Explorer + path: "/My Briefcase/" + + AWS.config.update + region: 'us-east-1' + + try + logins = JSON.parse localStorage.WHIMSY_FS_AWS_LOGIN + + AWS.config.credentials = new AWS.CognitoIdentityCredentials + IdentityPoolId: 'us-east-1:4fe22da5-bb5e-4a78-a260-74ae0a140bf9' + Logins: logins + + if logins + pinvoke AWS.config.credentials, "get" + .then receivedCredentials + .catch (e) -> + unless e.message is "Invalid login token." + console.warn e, e.message + + content LoginTemplate + description: -> + """ + Maintain access to your files across different machines. Publish + effortlessly to the internet. Your briefcase holds all of your hopes + and dreams in a magical cloud that is available anywhere there is an + internet connection. 💼 + """ + click: -> + options = { scope : 'profile' } + amazon.Login.authorize options, (resp) -> + if !resp.error + console.log resp + token = resp.access_token + creds = AWS.config.credentials + + logins = + 'www.amazon.com': token + localStorage.WHIMSY_FS_AWS_LOGIN = JSON.stringify(logins) + + creds.params.Logins = logins + + creds.expired = true + + queryUserInfo(token) + + pinvoke AWS.config.credentials, "get" + .then receivedCredentials + + windowView = Window + title: "My Briefcase" + width: 640 + height: 480 + content: content + iconEmoji: "💼" + + return windowView + +queryUserInfo = (token) -> + fetch "https://api.amazon.com/user/profile", + headers: + Authorization: "bearer #{token}" + Accept: "application/json" + .then (response) -> + response.json() + .then (json) -> + console.log json + .catch (e) -> + console.error e diff --git a/apps/notepad.coffee b/apps/notepad.coffee index 5302178..1d8933e 100644 --- a/apps/notepad.coffee +++ b/apps/notepad.coffee @@ -17,13 +17,21 @@ module.exports = -> textarea = document.createElement "textarea" textarea.spellcheck = false + initialValue = "" + + textarea.addEventListener "input", -> + handlers.saved textarea.value is initialValue + handlers = Model().include(FileIO).extend - loadFile: (blob) -> + loadFile: (blob, path) -> blob.readAsText() .then (text) -> - textarea.value = text + handlers.currentPath path + initialValue = text + textarea.value = initialValue newFile: -> - textarea.value = "" + initialValue = "" + textarea.value = initialValue saveData: -> data = new Blob [textarea.value], type: "text/plain" @@ -112,12 +120,36 @@ module.exports = -> handlers: handlers windowView = Window - title: "Notepad.exe" + title: -> + path = handlers.currentPath() + if handlers.saved() + savedIndicator = "" + else + savedIndicator = "*" + + if path + path = " - #{path}" + + "Notepad.exe#{path}#{savedIndicator}" + content: textarea menuBar: menuBar.element width: 640 height: 480 - + + # Key handling + windowView.element.setAttribute("tabindex", "-1") + windowView.element.addEventListener "keydown", (e) -> + {ctrlKey:ctrl, key} = e + if ctrl + switch key + when "s" + e.preventDefault() + handlers.save() + when "o" + e.preventDefault() + handlers.open() + windowView.loadFile = handlers.loadFile return windowView diff --git a/apps/pixel.coffee b/apps/pixel.coffee index dea7b50..120a2ed 100644 --- a/apps/pixel.coffee +++ b/apps/pixel.coffee @@ -1,35 +1,16 @@ -Model = require "model" -Postmaster = require "postmaster" +IFrameApp = require "../lib/iframe-app" FileIO = require "../os/file-io" +Model = require "model" module.exports = -> - {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI - - frame = document.createElement "iframe" - frame.src = "https://danielx.net/pixel-editor/" - - # TODO: Gross hack to keep track of waiting for child window to load - # May want to move it into the postmaster library - resolveLoaded = null - loadedPromise = new Promise (resolve) -> - resolveLoaded = resolve - - postmaster = Postmaster() - postmaster.remoteTarget = -> frame.contentWindow - Object.assign postmaster, - childLoaded: -> - console.log "child loaded" - resolveLoaded() - save: -> - handlers.save() + {MenuBar, Modal, Observable, Util:{parseMenu}} = system.UI handlers = Model().include(FileIO).extend loadFile: (blob) -> - loadedPromise.then -> - postmaster.invokeRemote "loadFile", blob + app.send "loadFile", blob newFile: -> saveData: -> - postmaster.invokeRemote "getBlob" + app.send "getBlob" menuBar = MenuBar items: parseMenu """ @@ -47,13 +28,17 @@ module.exports = -> """ handlers: handlers - windowView = Window + app = IFrameApp title: Observable "Pixie Paint" - content: frame - menuBar: menuBar.element + src: "https://danielx.net/pixel-editor/" + menuBar: menuBar + handlers: handlers width: 640 height: 480 - windowView.loadFile = handlers.loadFile + app.handlers = handlers + app.loadFile = handlers.loadFile + + system.Achievement.unlock "Pixel perfect" - return windowView + return app diff --git a/apps/story-reader.coffee b/apps/story-reader.coffee new file mode 100644 index 0000000..f7cbe2e --- /dev/null +++ b/apps/story-reader.coffee @@ -0,0 +1,16 @@ +module.exports = (opts) -> + {title, width, height, text} = opts + width ?= 380 + height ?= 480 + + div = document.createElement "div" + div.textContent = text + div.style.padding = "1em" + div.style.whiteSpace = "pre-wrap" + div.style.textAlign = "justify" + + system.UI.Window + title: title + content: div + width: width + height: height diff --git a/apps/text-editor.coffee b/apps/text-editor.coffee index b957178..6892a23 100644 --- a/apps/text-editor.coffee +++ b/apps/text-editor.coffee @@ -3,11 +3,7 @@ FileIO = require "../os/file-io" ace.require("ace/ext/language_tools") -extraModes = - jadelet: "jade" - -mode = (mode) -> - extraModes[mode] or mode +{extensionFor} = require "../util" module.exports = -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI @@ -39,23 +35,44 @@ module.exports = -> global.aceEditor = aceEditor - initSession = (file) -> - # TODO: Update window title + modes = + cson: "coffeescript" + jadelet: "jade" + js: "javascript" + md: "markdown" + styl: "stylus" + + mimeTypeFor = (path) -> + type = system.mimeTypeFor(path) + + "#{type}; charset=utf-8" + + setModeFor = (path) -> + extension = extensionFor(path) + mode = modes[extension] or extension + + session.setMode("ace/mode/#{mode}") + + initSession = (file, path) -> file.readAsText() .then (content) -> + if path + handlers.currentPath path + setModeFor(path) + session.setValue(content) - # TODO: Correct modes - mode = "coffee" - session.setMode("ace/mode/#{mode}") + handlers.saved true + + session.on "change", -> + handlers.saved false handlers = Model().include(FileIO).extend loadFile: initSession newFile: -> session.setValue "" saveData: -> - # TODO: Maintain proper mime type data = new Blob [session.getValue()], - type: "text/plain" + type: mimeTypeFor(handlers.currentPath()) return Promise.resolve data @@ -76,7 +93,18 @@ module.exports = -> handlers: handlers windowView = Window - title: Observable "Ace" + title: -> + path = handlers.currentPath() + if handlers.saved() + savedIndicator = "" + else + savedIndicator = "*" + + if path + path = " - #{path}" + + "Ace#{path}#{savedIndicator}" + content: aceWrap menuBar: menuBar.element width: 640 @@ -87,4 +115,17 @@ module.exports = -> windowView.on "resize", -> aceEditor.resize() + # Key handling + windowView.element.setAttribute("tabindex", "-1") + windowView.element.addEventListener "keydown", (e) -> + {ctrlKey:ctrl, key} = e + if ctrl + switch key + when "s" + e.preventDefault() + handlers.save() + when "o" + e.preventDefault() + handlers.open() + return windowView diff --git a/extensions.coffee b/extensions.coffee index fd4ec69..2523543 100644 --- a/extensions.coffee +++ b/extensions.coffee @@ -1,3 +1,9 @@ +# Pretend Hamlet Runtime is a real package +PACKAGE.dependencies["_lib_hamlet-runtime"] = + entryPoint: "main" + distribution: + main: PACKAGE.distribution["lib/hamlet-runtime"] + # Add some utility readers to the Blob API Blob::readAsText = -> file = this @@ -9,6 +15,9 @@ Blob::readAsText = -> reader.onerror = reject reader.readAsText(file) +Blob::getURL = -> + Promise.resolve URL.createObjectURL(this) + Blob::readAsJSON = -> @readAsText() .then JSON.parse @@ -23,16 +32,38 @@ Blob::readAsDataURL = -> reader.onerror = reject reader.readAsDataURL(file) +# BlobSham interface must implement getURL and readAs* methods + # Load an image from a blob returning a promise that is fulfilled with the # loaded image or rejected with an error Image.fromBlob = (blob) -> - new Promise (resolve, reject) -> - img = new Image - img.onload = -> - resolve img - img.onerror = reject + blob.getURL() + .then (url) -> + new Promise (resolve, reject) -> + img = new Image + img.onload = -> + resolve img + img.onerror = reject - img.src = URL.createObjectURL blob + img.src = url FileList::forEach ?= (args...) -> Array::forEach.apply(this, args) + +# Event#path polyfill for Firefox +unless "path" in Event.prototype + Object.defineProperty Event.prototype, "path", + get: -> + path = [] + currentElem = this.target + while currentElem + path.push currentElem + currentElem = currentElem.parentElement + + if path.indexOf(window) is -1 && path.indexOf(document) is -1 + path.push(document) + + if path.indexOf(window) is -1 + path.push(window) + + path diff --git a/feedback.coffee b/feedback.coffee new file mode 100644 index 0000000..507bc4f --- /dev/null +++ b/feedback.coffee @@ -0,0 +1,17 @@ +# Open a feedback form +module.exports = -> + iframe = document.createElement "iframe" + iframe.src = "https://docs.google.com/forms/d/e/1FAIpQLSfAK8ZYmMd4-XsDqyTK4soYGWApGD9R33nReuqwG-TxjXaGFg/viewform?embedded=true" + + Window = system.UI.Window + + windowView = Window + title: "Whimsy Space Feedback" + content: iframe + menuBar: null + width: 600 + height: 500 + + system.Achievement.unlock "We value your input" + + document.body.appendChild windowView.element diff --git a/issues/2016-12.coffee b/issues/2016-12.coffee index 0689c54..a2abea0 100644 --- a/issues/2016-12.coffee +++ b/issues/2016-12.coffee @@ -44,7 +44,7 @@ module.exports = -> area: issueTag .then (data) -> ajax - url: "https://whimsy-space.gomix.me/comments" + url: "https://whimsy-space.glitch.me/comments" data: JSON.stringify(data) headers: "Content-Type": "application/json" @@ -53,7 +53,7 @@ module.exports = -> handlers.viewComments() viewComments: -> - ajax.getJSON "https://whimsy-space.gomix.me/comments/#{issueTag}" + ajax.getJSON "https://whimsy-space.glitch.me/comments/#{issueTag}" .then (data) -> data = data.reverse() @@ -77,7 +77,7 @@ module.exports = -> document.body.appendChild app.element mysterySmell: -> system.Achievement.unlock "Cover-2-cover" - + div = document.createElement "div" div.textContent = require "../stories/mystery-smell" div.style.padding = "1em" diff --git a/issues/2017-02.coffee b/issues/2017-02.coffee index 2ab0d28..b862408 100644 --- a/issues/2017-02.coffee +++ b/issues/2017-02.coffee @@ -14,17 +14,12 @@ module.exports = -> container = document.createElement "container" - setTimeout -> - system.Achievement.unlock "Issue 2" - #, 3000 + system.Achievement.unlock "Issue 2" system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" system.writeFile "issue-2/provision.txt", new Blob [require "../stories/provision"], type: "text/plain" system.writeFile "issue-2/dungeon-dog.txt", new Blob [require "../stories/dungeon-dog"], type: "text/plain" system.writeFile "issue-2/dsad.exe", new Blob [""], type: "application/exe" - system.writeFile "issue-2/zine2.exe", new Blob [""], type: "application/exe" - - system.writeFile "issue-1/zine1.exe", new Blob [""], type: "application/exe" pages = front: """ @@ -64,7 +59,7 @@ module.exports = -> div(style="padding: 1em;") h1 Cheevos - p No matter if you guy/girl or whatever, Cheevos impress people. It's almost like saying 'Well, I got tons of Cheevos. There are tons of people online that are interested and respect me." + p No matter if you guy/girl or whatever, Cheevos impress people. It's almost like saying 'Well, I got tons of Cheevos. There are tons of people online that are interested and respect me." p You might be thinking 'Oh that's complete BS, I personally don't care about Cheevos when dating'. And yeah, you are probably telling the truth, but it's in your sub-concious. Sort of like how girls always like the bad guy, but never admit it. @@ -72,7 +67,7 @@ module.exports = -> p 'Brewer' asks 'What happens if you're cheevo talking at a bar, club or party and someone says they have more cheevos that you?' - p Well, hopefully they are just hating and are lying. First look up their score with your cellphone, make sure you have a page bookmarked where you can check. If you catch them in a lie, you look even better. If they are telling the truth and have more Cheevos than you, leave. Nothing else you can do. Buy the person a drink and leave, unless you're willing to look 2nd best. If you brought a date, odds are she's going to be impressed with the higher gamer score and ditch you. Get out as soon as you can and go to some other party. + p Well, hopefully they are just hating and are lying. First look up their score with your cellphone, make sure you have a page bookmarked where you can check. If you catch them in a lie, you look even better. If they are telling the truth and have more Cheevos than you, leave. Nothing else you can do. Buy the person a drink and leave, unless you're willing to look 2nd best. If you brought a date, odds are she's going to be impressed with the higher gamer score and ditch you. Get out as soon as you can and go to some other party. p a(href="http://cheevos.com") Learn more about cheevos from Bboy360 at cheevos.com @@ -87,7 +82,7 @@ module.exports = -> li pketh li Mayor li and you! - + p a(href="#table") Return to table of contents """ @@ -157,7 +152,7 @@ module.exports = -> displayPage = (page) -> return unless page - + visited[page] = true if Object.keys(visited).length is Object.keys(pages).length @@ -166,6 +161,9 @@ module.exports = -> if page is "vista" system.Achievement.unlock "Lol wut" + if page is "cheevos" + system.Achievement.unlock "Check yo' self" + emptyElement(container) container.appendChild(pages[page]) diff --git a/issues/2017-03.coffee b/issues/2017-03.coffee new file mode 100644 index 0000000..0bcf701 --- /dev/null +++ b/issues/2017-03.coffee @@ -0,0 +1,131 @@ +Model = require "model" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" + +Social = require "../social/social" + +{parentElementOfType, emptyElement} = require "../util" + +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement} = system + + visitedAreas = + blue: false + csaur: false + chateau: false + cheevo: false + evan: false + + visit = (area) -> + visitedAreas[area] = true + + visitedAll = Object.keys(visitedAreas).every (key) -> + visitedAreas[key] + + if visitedAll + Achievement.unlock "Cover-2-cover 3: Tokyo Drift" + + system.writeFile "issue-3/blue-light-special.txt", new Blob [require "../stories/blue-light-special"], type: "text/plain" + + system.Achievement.unlock "Issue 3" + + handlers = Model().include(Social).extend + area: -> + "2017-03" + + mSAccess97: -> + app = Spreadsheet(system) + document.body.appendChild app.element + + chateau: -> + visit "chateau" + app = Chateau(system) + document.body.appendChild app.element + + contrasaurus: -> + visit "csaur" + document.body.appendChild Contrasaurus().element + + achievementStatus: -> + visit "cheevo" + cheevoElement = system.Achievement.progressView() + cheevoElement.style.width = "100%" + cheevoElement.style.padding = "1em" + + system.Achievement.unlock "Check yo' self" + + windowView = Window + title: "Cheevos" + content: cheevoElement + width: 640 + height: 480 + + document.body.appendChild windowView.element + + evanAndMore: -> + visit "evan" + url = "https://s3.amazonaws.com/whimsyspace-databucket-1g3p6d9lcl6x1/danielx/IMG_9794.JPG" + img = document.createElement "img" + img.src = url + + {element} = system.UI.Window + title: "Evan And More" + content: img + width: 600 + height: 480 + + document.body.appendChild element + + blueLightSpecial: -> + Achievement.unlock "Blue light special" + visit "blue" + + storyWindow = StoryWindow("Blue Light Special", require("../stories/blue-light-special")) + + document.body.appendChild storyWindow.element + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [C]hateau + Contra[s]aurus + [S]tories + [B]lue Light Special + [E]van And More + #{Social.menuText} + [H]elp + [A]chievement Status + """ + handlers: handlers + + kmartGif = document.createElement "img" + kmartGif.src = "http://media.boingboing.net/wp-content/uploads/2015/07/m66DBJ.gif" + kmartGif.style = "width: 100%; height: 100%" + + windowView = Window + title: "ZineOS Volume 1 | Issue 3 | ATTN: K-Mart Shoppers | March 2017" + content: kmartGif + menuBar: menuBar.element + width: 800 + height: 600 + x: 32 + y: 32 + + document.body.appendChild windowView.element + +StoryWindow = (title, text) -> + div = document.createElement "div" + div.textContent = text + div.style.padding = "1em" + div.style.whiteSpace = "pre-wrap" + div.style.textAlign = "justify" + + system.UI.Window + title: title + content: div + width: 380 + height: 480 diff --git a/issues/2017-04.coffee b/issues/2017-04.coffee new file mode 100644 index 0000000..64abd37 --- /dev/null +++ b/issues/2017-04.coffee @@ -0,0 +1,174 @@ +Model = require "model" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" +MyBriefcase = require "../apps/my-briefcase" + +Social = require "../social/social" + +{parentElementOfType, emptyElement} = require "../util" + +writeIfNotPresent = (destination, sourceURL) -> + system.readFile destination + .then (file) -> + throw new Error "File not found" unless file + return file + .catch -> + ajax + url: sourceURL + responseType: "blob" + .then (blob) -> + system.writeFile destination, blob + +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement, ajax} = system + + visitedAreas = + bikes: false + izzy: false + residue: false + chateau: false + cheevo: false + briefcase: false + podcast: false + + visit = (area) -> + visitedAreas[area] = true + + visitedAll = Object.keys(visitedAreas).every (key) -> + visitedAreas[key] + + if visitedAll + Achievement.unlock "Cover-2-cover 4: Fast & Furious" + + system.writeFile "issue-4/izzy.txt", new Blob [require "../stories/izzy"], type: "text/plain" + system.writeFile "issue-4/residue.txt", new Blob [require "../stories/residue"], type: "text/plain" + + downloadBikes = -> + ["and-yet-they-rode-bikes.md", "infog.png", "lanes.png", "totally-a.html"].forEach (path) -> + filePath = "issue-4/bikes/#{path}" + + writeIfNotPresent filePath, "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/bikes/#{path}" + + downloadBikes() + + writeIfNotPresent "issue-4/Funkytown.mp3", "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/music/Funkytown.mp3" + .then -> + system.openPath "issue-4/Funkytown.mp3" + + writeIfNotPresent "issue-4/zinecast1.mp3", "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/podcasts/zinecast1.mp3" + + system.Achievement.unlock "Issue 4" + + handlers = Model().include(Social).extend + area: -> + "2017-04" + + zinecast1: -> + visit "podcast" + system.readFile "issue-4/zinecast1.mp3" + .then system.open + + bikes: -> + visit "bikes" + system.readFile "issue-4/bikes/and-yet-they-rode-bikes.md" + .then system.open + + chateau: -> + visit "chateau" + app = Chateau(system) + document.body.appendChild app.element + + achievementStatus: -> + visit "cheevo" + cheevoElement = system.Achievement.progressView() + cheevoElement.style.width = "100%" + cheevoElement.style.padding = "1em" + + system.Achievement.unlock "Check yo' self" + + windowView = Window + title: "Cheevos" + content: cheevoElement + width: 640 + height: 480 + + document.body.appendChild windowView.element + + myBriefcase: -> + visit "briefcase" + app = MyBriefcase() + document.body.appendChild app.element + + izzy: -> + Achievement.unlock "Izzy" + visit "izzy" + storyWindow = StoryWindow("Izzy", require("../stories/izzy")) + + document.body.appendChild storyWindow.element + + residue: -> + Achievement.unlock "Residue" + visit "residue" + + storyWindow = StoryWindow("Residue", require("../stories/residue")) + + document.body.appendChild storyWindow.element + + funkytown8bitRemix: -> + system.readFile "issue-4/Funkytown.mp3" + .then system.open + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [C]hateau + My [B]riefcase + [M]usic + [F]unkytown (8-bit Remix) + [Z]inecast 1 + [S]tories + [B]ikes + [I]zzy + [R]esidue + #{Social.menuText} + [H]elp + [A]chievement Status + """ + handlers: handlers + + content = document.createElement "content" + content.style = "width: 100%; height: 100%" + + img = document.createElement "img" + img.src = "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/images/708e9398a4b4bea08d7c61ff7a0f863f.gif" + img.style = "width: 100%; height: 100%" + + windowView = Window + title: "ZineOS Volume 1 | Issue 4 | DISCO TECH | April 2017" + content: img + menuBar: menuBar.element + width: 480 + height: 600 + x: 64 + y: 64 + + windowView.element.querySelector('viewport').style.overflow = "initial" + + document.body.appendChild windowView.element + +StoryWindow = (title, text) -> + div = document.createElement "div" + div.textContent = text + div.style.padding = "1em" + div.style.whiteSpace = "pre-wrap" + div.style.textAlign = "justify" + + system.UI.Window + title: title + content: div + width: 380 + height: 480 diff --git a/issues/2017-05.coffee b/issues/2017-05.coffee new file mode 100644 index 0000000..519dc2f --- /dev/null +++ b/issues/2017-05.coffee @@ -0,0 +1,99 @@ +Model = require "model" + +AchievementStatus = require "../apps/achievement-status" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" +MyBriefcase = require "../apps/my-briefcase" + +StoryReader = require "../apps/story-reader" + +Social = require "../social/social" + +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement, ajax} = system + + ggPath = "issue-5/gleep-glorp.m4a" + + system.readFile ggPath + .then (file) -> + throw new Error "File not found" unless file + .catch -> + ajax + url: "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/hao/gleep-glorp.m4a" + responseType: "blob" + .then (blob) -> + system.writeFile ggPath, blob + + handlers = Model().include(Social).extend + area: -> + "2017-05" + + achievementStatus: -> + system.launchApp AchievementStatus + + chateau: -> + system.launchApp Chateau + + crescent: -> + app = StoryReader + text: require "../stories/crescent" + title: "Crescent" + + document.body.appendChild app.element + + gleepGlorp: -> + system.openPath ggPath + + marigold: -> + app = StoryReader + text: require "../stories/marigold" + title: "Marigold" + + document.body.appendChild app.element + + myBriefcase: -> + system.launchApp MyBriefcase + + pixiePaint: -> + system.launchApp PixiePaint + + textEditor: -> + system.launchApp TextEditor + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [C]hateau + My [B]riefcase + [P]ixie Paint + [T]ext Editor + [C]ontent + [C]rescent + [G]leep Glorp + [M]arigold + #{Social.menuText} + [H]elp + [A]chievement Status + """ + handlers: handlers + + img = document.createElement "img" + img.src = "https://i.imgur.com/hKOGoex.jpg" + img.style = "width: 100%; height: 100%" + + windowView = Window + title: "ZineOS Volume 1 | Issue 5 | A May Zine | May 2017" + content: img + menuBar: menuBar.element + width: 640 + height: 360 + x: 64 + y: 64 + + windowView.element.querySelector('viewport').style.overflow = "initial" + + document.body.appendChild windowView.element diff --git a/lib/app-drop.coffee b/lib/app-drop.coffee new file mode 100644 index 0000000..500c1cd --- /dev/null +++ b/lib/app-drop.coffee @@ -0,0 +1,35 @@ +Drop = require "./drop" + +# General drop handling for apps +module.exports = (app) -> + {element} = app + + Drop element, (e) -> + {handlers} = app + + fileSelectionData = e.dataTransfer.getData("zineos/file-selection") + + if fileSelectionData + data = JSON.parse(fileSelectionData) + e.preventDefault() + file = data.files[0] + + # TODO: Handle multi-files + path = data.files[0].path + + system.readFile path + .then handlers.loadFile + .then -> + handlers.currentPath path + + return + + files = e.dataTransfer.files + + if files.length + e.preventDefault() + + file = files[0] + handlers.loadFile file + .then -> + handlers.currentPath null diff --git a/lib/dexie-fs.coffee b/lib/dexie-fs.coffee new file mode 100644 index 0000000..1ad5ec4 --- /dev/null +++ b/lib/dexie-fs.coffee @@ -0,0 +1,65 @@ +Bindable = require "bindable" +Model = require "model" + +FolderEntry = (path, prefix) -> + folder: true + path: prefix + path + relativePath: path + +# FS Wrapper to Dexie database +module.exports = (db) -> + Files = db.files + + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result + + self = Model() + .include(Bindable) + .extend + # Read a blob from a path + read: (path) -> + Files.get(path) + .then ({blob}) -> + blob + .then notify "read", path + + # Write a blob to a path + write: (path, blob) -> + now = +new Date + + Files.put + path: path + blob: blob + size: blob.size + type: blob.type + createdAt: now + updatedAt: now + .then notify "write", path + + # Delete a file at a path + delete: (path) -> + Files.delete(path) + .then notify "delete", path + + # List files and folders in a directory + list: (dir) -> + Files.where("path").startsWith(dir).toArray() + .then (files) -> + folderPaths = {} + + files = files.filter (file) -> + file.relativePath = file.path.replace(dir, "") + + if file.relativePath.match /\// # folder + folderPath = file.relativePath.replace /\/.*$/, "/" + folderPaths[folderPath] = true + return + else + return file + + folders = Object.keys(folderPaths).map (folderPath) -> + FolderEntry folderPath, dir + + return folders.concat(files) diff --git a/lib/drop.coffee b/lib/drop.coffee index 6e1fb0a..7993784 100644 --- a/lib/drop.coffee +++ b/lib/drop.coffee @@ -6,6 +6,4 @@ module.exports = (element, handler) -> element.addEventListener "dragover", cancel element.addEventListener "dragenter", cancel element.addEventListener "drop", (e) -> - e.preventDefault() handler(e) - return false diff --git a/lib/hamlet.js b/lib/hamlet.js index b18539c..9445320 100644 --- a/lib/hamlet.js +++ b/lib/hamlet.js @@ -2448,4 +2448,4 @@ module.exports={ } },{}]},{},[1])(1) -}); \ No newline at end of file +}); diff --git a/lib/iframe-app.coffee b/lib/iframe-app.coffee new file mode 100644 index 0000000..52847de --- /dev/null +++ b/lib/iframe-app.coffee @@ -0,0 +1,88 @@ +Model = require "model" +Postmaster = require "postmaster" +FileIO = require "../os/file-io" + +module.exports = (opts={}) -> + {Window} = system.UI + + {height, menuBar, src, handlers, title, width, sandbox, pkg, packageOptions, iconEmoji} = opts + + frame = document.createElement "iframe" + + if sandbox + frame.setAttribute("sandbox", sandbox) + + if src + frame.src = src + else if pkg + html = system.htmlForPackage(pkg, packageOptions) + blob = new Blob [html], + type: "text/html; charset=utf-8" + frame.src = URL.createObjectURL blob + + # Keep track of waiting for child window to load, all remote invocations are + # queued behind a promise until the child has loaded + # May want to move it into the postmaster library + resolveLoaded = null + loadedPromise = new Promise (resolve) -> + resolveLoaded = resolve + + loaded = false + setTimeout -> + console.warn "Child never loaded" unless loaded + , 5000 + + # Attach a postmaster to receive events from the child frame + postmaster = Postmaster() + + Object.assign postmaster, + remoteTarget: -> + frame.contentWindow + + childLoaded: -> + console.log "child loaded" + resolveLoaded() + loaded = true + + # Send events from the iframe app to the application + event: -> + application.trigger "event", arguments... + + return + + # Add application method access to client iFrame + application: (method, args...) -> + application[method](args...) + + # Add system method access to client iFrame + # TODO: Security :P + system: (method, args...) -> + system[method](args...) + + handlers ?= Model().include(FileIO).extend + loadFile: (blob) -> + loadedPromise.then -> + postmaster.invokeRemote "loadFile", blob + + application = Window + title: title + content: frame + menuBar: menuBar?.element + width: width + height: height + iconEmoji: iconEmoji + + Object.assign application, + exit: -> + # TODO: Prompt unsaved, etc. + setTimeout -> + application.element.remove() + , 0 + return + handlers: handlers + loadFile: handlers.loadFile + send: (args...) -> + loadedPromise.then -> + postmaster.invokeRemote args... + + return application diff --git a/lib/mount-fs.coffee b/lib/mount-fs.coffee new file mode 100644 index 0000000..58c54bf --- /dev/null +++ b/lib/mount-fs.coffee @@ -0,0 +1,75 @@ +{startsWith} = require "../util" + +Bindable = require "bindable" +Model = require "model" + +module.exports = (I) -> + mounts = {} + mountPaths = [] + + longestToShortest = (a, b) -> + b.length - a.length + + findMountPathFor = (path) -> + [mountPath] = mountPaths.filter (p) -> + startsWith path, p + + return mountPath + + proxyToMount = (method) -> + (path, params...) -> + mountPath = findMountPathFor path + + if mountPath + mount = mounts[mountPath] + else + throw new Error "No mounted filesystem for #{path}" + + subsystemPath = path.replace(mountPath, "/") + + if method is "list" + # Remap paths when retrieving entries + mount[method](subsystemPath, params...) + .then (entries) -> + entries.forEach (entry) -> + entry.path = entry.path.replace("/", mountPath) + + return entries + else if method is "read" + mount[method](subsystemPath, params...) + .then (blob) -> + if blob + blob.path = path + + return blob + else + mount[method](subsystemPath, params...) + + bindSubsystemEvent = (folderPath, subsystem, eventName) -> + subsystem.on eventName, (path) -> + self.trigger eventName, path.replace("/", folderPath) + + self = Model() + .include(Bindable) + .extend + read: proxyToMount "read" + + write: proxyToMount "write" + + delete: proxyToMount "delete" + + list: proxyToMount "list" + + mount: (folderPath, subsystem) -> + mounts[folderPath] = subsystem + mountPaths.push folderPath + mountPaths.sort longestToShortest + + # TODO: Want to be able to pass through all events + bindSubsystemEvent(folderPath, subsystem, "write") + bindSubsystemEvent(folderPath, subsystem, "update") + bindSubsystemEvent(folderPath, subsystem, "delete") + + return self + + return self diff --git a/lib/outbound-clicks.coffee b/lib/outbound-clicks.coffee index d8feb7a..55c1896 100644 --- a/lib/outbound-clicks.coffee +++ b/lib/outbound-clicks.coffee @@ -7,7 +7,7 @@ document.addEventListener "click", (e) -> if anchor href = anchor.getAttribute('href') - if href.match /^http/ + if href?.match /^http/ e.preventDefault() if href.match /frogfeels\.com/ diff --git a/lib/pkg-fs.coffee b/lib/pkg-fs.coffee new file mode 100644 index 0000000..b774ea4 --- /dev/null +++ b/lib/pkg-fs.coffee @@ -0,0 +1,105 @@ +Bindable = require "bindable" +Model = require "model" + +FolderEntry = (path, prefix) -> + folder: true + path: prefix + path + relativePath: path + +# Keys in the package's source object don't begin with slashes +sourcePath = (path) -> + path.replace(/^\//, "") + +# Strip out extension suffixes +distributionPath = (path) -> + path.replace(/\..*$/, "") + +# FS Wrapper to a pixie package +module.exports = (pkg, persistencePath) -> + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result + + persist = -> + # Persist entire pkg + pkgBlob = new Blob [JSON.stringify(pkg)], + type: "application/json; charset=utf8" + system.writeFile persistencePath, pkgBlob + + compileAndWrite = (path, blob) -> + writeSource = blob.readAsText() + .then (text) -> + srcPath = sourcePath(path) + pkg.source[srcPath] = + content: text + type: blob.type or "" + path: srcPath + + # Compilers expect blob to be annotated with the path + blob.path = path + + writeCompiled = system.compileFile(blob) + .then (compiledSource) -> + if typeof compiledSource is "string" + pkg.distribution[distributionPath(sourcePath(path))] = + content: compiledSource + else + console.warn "Can't package files like #{path} yet", compiledSource + + Promise.all [writeSource, writeCompiled] + .then persist + + self = Model() + .include(Bindable) + .extend + # Read a blob from a path + read: (path) -> + {content, type} = pkg.source[sourcePath(path)] + type ?= "" + + blob = new Blob [content], + type: type + + Promise.resolve blob + .then notify "read", path + + # Write a blob to a path + write: (path, blob) -> + compileAndWrite(path, blob) + .then notify "write", path + + # Delete a file at a path + delete: (path) -> + Promise.resolve() + .then -> + delete pkg.source[sourcePath(path)] + .then notify "delete", path + + # List files and folders in a directory + list: (dir) -> + sourceDir = sourcePath(dir) + + Promise.resolve() + .then -> + Object.keys(pkg.source).filter (path) -> + path.indexOf(sourceDir) is 0 + .map (path) -> + path: "/" + path + relativePath: path.replace(sourceDir, "") + type: pkg.source[path].type or "" + .then (files) -> + folderPaths = {} + + files = files.filter (file) -> + if file.relativePath.match /\// # folder + folderPath = file.relativePath.replace /\/.*$/, "/" + folderPaths[folderPath] = true + return + else + return file + + folders = Object.keys(folderPaths).map (folderPath) -> + FolderEntry folderPath, dir + + return folders.concat(files) diff --git a/lib/s3-fs.coffee b/lib/s3-fs.coffee new file mode 100644 index 0000000..3ecd874 --- /dev/null +++ b/lib/s3-fs.coffee @@ -0,0 +1,171 @@ +Bindable = require "bindable" +Model = require "model" + +{pinvoke, startsWith, endsWith} = require "../util" + +delimiter = "/" + +# NOTE: Not scoped for multi-bucket yet +localCache = {} + +status = (response) -> + if response.status >= 200 && response.status < 300 + return response + else + throw response + +json = (response) -> + response.json() + +blob = (response) -> + response.blob() + +uploadToS3 = (bucket, key, file, options={}) -> + {cacheControl} = options + + cacheControl ?= 0 + + # Optimistically Cache + localCache[key] = file + + pinvoke bucket, "putObject", + Key: key + ContentType: file.type + Body: file + CacheControl: "max-age=#{cacheControl}" + +getRemote = (bucket, key) -> + cachedItem = localCache[key] + + if cachedItem + if cachedItem instanceof Blob + return Promise.resolve(cachedItem) + else + return Promise.reject(cachedItem) + + pinvoke bucket, "getObject", + Key: key + .then (data) -> + {Body, ContentType} = data + + new Blob [Body], + type: ContentType + .then (data) -> + localCache[key] = data + .catch (e) -> + # Cache Not Founds too, since that's often what is slow + localCache[key] = e + throw e + +deleteFromS3 = (bucket, key) -> + localCache[key] = new Error "Not Found" + + pinvoke bucket, "deleteObject", + Key: key + +list = (bucket, id, dir) -> + unless startsWith dir, delimiter + dir = "#{delimiter}#{dir}" + + unless endsWith dir, delimiter + dir = "#{dir}#{delimiter}" + + prefix = "#{id}#{dir}" + + pinvoke bucket, "listObjects", + Prefix: prefix + Delimiter: delimiter + .then (result) -> + results = result.CommonPrefixes.map (p) -> + FolderEntry p.Prefix, id, prefix + .concat result.Contents.map (o) -> + FileEntry o, id, prefix, bucket + .map (entry) -> + fetchMeta(entry, bucket) + + Promise.all results + +module.exports = (id, bucket) -> + + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result + + self = Model() + .include Bindable + .extend + read: (path) -> + unless startsWith path, delimiter + path = delimiter + path + + key = "#{id}#{path}" + + getRemote(bucket, key) + .then notify "read", path + + write: (path, blob) -> + unless startsWith path, delimiter + path = delimiter + path + + key = "#{id}#{path}" + + uploadToS3 bucket, key, blob + .then notify "write", path + + delete: (path) -> + unless startsWith path, delimiter + path = delimiter + path + + key = "#{id}#{path}" + + deleteFromS3 bucket, key + .then notify "delete", path + + list: (folderPath="/") -> + list bucket, id, folderPath + +fetchFileMeta = (fileEntry, bucket) -> + pinvoke bucket, "headObject", + Key: fileEntry.remotePath + .then (result) -> + fileEntry.type = result.ContentType + + fileEntry + +fetchMeta = (entry, bucket) -> + Promise.resolve() + .then -> + return entry if entry.folder + + fetchFileMeta entry, bucket + +FolderEntry = (path, id, prefix) -> + folder: true + path: path.replace(id, "") + relativePath: path.replace(prefix, "") + remotePath: path + +FileEntry = (object, id, prefix, bucket) -> + path = object.Key + + entry = + path: path.replace(id, "") + relativePath: path.replace(prefix, "") + remotePath: path + size: object.Size + + entry.blob = BlobSham(entry, bucket) + + return entry + +BlobSham = (entry, bucket) -> + remotePath = entry.remotePath + + getURL: -> + getRemote(bucket, remotePath) + .then URL.createObjectURL + readAsText: -> + getRemote(bucket, remotePath) + .then (blob) -> + blob.readAsText() diff --git a/lib/stylus.min.js b/lib/stylus.min.js new file mode 100644 index 0000000..117adb7 --- /dev/null +++ b/lib/stylus.min.js @@ -0,0 +1,6 @@ +if(Function.prototype.name===undefined&&Object.defineProperty!==undefined){Object.defineProperty(Function.prototype,"name",{get:function(){var regex=/function\s([^(]{1,})\(/,match=regex.exec(this.toString());return match&&match.length>1?match[1].trim():""}})}if(String.prototype.trimRight===undefined){String.prototype.trimRight=function(){return String(this).replace(/\s+$/,"")}}var stylus=module.exports=function(){function require(p){var path=require.resolve(p),mod=require.modules[path];if(!mod)throw new Error('failed to require "'+p+'"');if(!mod.exports){mod.exports={};mod.call(mod.exports,mod,mod.exports,require.relative(path))}return mod.exports}var bifs="called-from = ()\n\nvendors = moz webkit o ms official\n\n// stringify the given arg\n\n-string(arg)\n type(arg) + ' ' + arg\n\n// require a color\n\nrequire-color(color)\n unless color is a 'color'\n error('RGB or HSL value expected, got a ' + -string(color))\n\n// require a unit\n\nrequire-unit(n)\n unless n is a 'unit'\n error('unit expected, got a ' + -string(n))\n\n// require a string\n\nrequire-string(str)\n unless str is a 'string' or str is a 'ident'\n error('string expected, got a ' + -string(str))\n\n// Math functions\n\nabs(n) { math(n, 'abs') }\nmin(a, b) { a < b ? a : b }\nmax(a, b) { a > b ? a : b }\n\n// Trigonometrics\nPI = -math-prop('PI')\n\nradians-to-degrees(angle)\n angle * (180 / PI)\n\ndegrees-to-radians(angle)\n unit(angle * (PI / 180),'')\n\nsin(n)\n n = degrees-to-radians(n) if unit(n) == 'deg'\n round(math(n, 'sin'), 9)\n\ncos(n)\n n = degrees-to-radians(n) if unit(n) == 'deg'\n round(math(n, 'cos'), 9)\n\n// Rounding Math functions\n\nceil(n, precision = 0)\n multiplier = 10 ** precision\n math(n * multiplier, 'ceil') / multiplier\n\nfloor(n, precision = 0)\n multiplier = 10 ** precision\n math(n * multiplier, 'floor') / multiplier\n\nround(n, precision = 0)\n multiplier = 10 ** precision\n math(n * multiplier, 'round') / multiplier\n\n// return the sum of the given numbers\n\nsum(nums)\n sum = 0\n sum += n for n in nums\n\n// return the average of the given numbers\n\navg(nums)\n sum(nums) / length(nums)\n\n// return a unitless number, or pass through\n\nremove-unit(n)\n if typeof(n) is 'unit'\n unit(n, '')\n else\n n\n\n// convert a percent to a decimal, or pass through\n\npercent-to-decimal(n)\n if unit(n) is '%'\n remove-unit(n) / 100\n else\n n\n\n// check if n is an odd number\n\nodd(n)\n 1 == n % 2\n\n// check if n is an even number\n\neven(n)\n 0 == n % 2\n\n// check if color is light\n\nlight(color)\n lightness(color) >= 50%\n\n// check if color is dark\n\ndark(color)\n lightness(color) < 50%\n\n// desaturate color by amount\n\ndesaturate(color, amount)\n adjust(color, 'saturation', - amount)\n\n// saturate color by amount\n\nsaturate(color = '', amount = 100%)\n if color is a 'color'\n adjust(color, 'saturation', amount)\n else\n unquote( 'saturate(' + color + ')' )\n\n// darken by the given amount\n\ndarken(color, amount)\n adjust(color, 'lightness', - amount)\n\n// lighten by the given amount\n\nlighten(color, amount)\n adjust(color, 'lightness', amount)\n\n// decrease opacity by amount\n\nfade-out(color, amount)\n color - rgba(black, percent-to-decimal(amount))\n\n// increase opacity by amount\n\nfade-in(color, amount)\n color + rgba(black, percent-to-decimal(amount))\n\n// spin hue by a given amount\n\nspin(color, amount)\n color + unit(amount, deg)\n\n// mix two colors by a given amount\n\nmix(color1, color2, weight = 50%)\n unless weight in 0..100\n error('Weight must be between 0% and 100%')\n\n if length(color1) == 2\n weight = color1[0]\n color1 = color1[1]\n\n else if length(color2) == 2\n weight = 100 - color2[0]\n color2 = color2[1]\n\n require-color(color1)\n require-color(color2)\n\n p = unit(weight / 100, '')\n w = p * 2 - 1\n\n a = alpha(color1) - alpha(color2)\n\n w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2\n w2 = 1 - w1\n\n channels = (red(color1) red(color2)) (green(color1) green(color2)) (blue(color1) blue(color2))\n rgb = ()\n\n for pair in channels\n push(rgb, floor(pair[0] * w1 + pair[1] * w2))\n\n a1 = alpha(color1) * p\n a2 = alpha(color2) * (1 - p)\n alpha = a1 + a2\n\n rgba(rgb[0], rgb[1], rgb[2], alpha)\n\n// invert colors, leave alpha intact\n\ninvert(color = '')\n if color is a 'color'\n rgba(#fff - color, alpha(color))\n else\n unquote( 'invert(' + color + ')' )\n\n// give complement of the given color\n\ncomplement( color )\n spin( color, 180 )\n\n// give grayscale of the given color\n\ngrayscale( color = '' )\n if color is a 'color'\n desaturate( color, 100% )\n else\n unquote( 'grayscale(' + color + ')' )\n\n// mix the given color with white\n\ntint( color, percent )\n mix( white, color, percent )\n\n// mix the given color with black\n\nshade( color, percent )\n mix( black, color, percent )\n\n// return the last value in the given expr\n\nlast(expr)\n expr[length(expr) - 1]\n\n// return keys in the given pairs or object\n\nkeys(pairs)\n ret = ()\n if type(pairs) == 'object'\n for key in pairs\n push(ret, key)\n else\n for pair in pairs\n push(ret, pair[0]);\n ret\n\n// return values in the given pairs or object\n\nvalues(pairs)\n ret = ()\n if type(pairs) == 'object'\n for key, val in pairs\n push(ret, val)\n else\n for pair in pairs\n push(ret, pair[1]);\n ret\n\n// join values with the given delimiter\n\njoin(delim, vals...)\n buf = ''\n vals = vals[0] if length(vals) == 1\n for val, i in vals\n buf += i ? delim + val : val\n\n// add a CSS rule to the containing block\n\n// - This definition allows add-property to be used as a mixin\n// - It has the same effect as interpolation but allows users\n// to opt for a functional style\n\nadd-property-function = add-property\nadd-property(name, expr)\n if mixin\n {name} expr\n else\n add-property-function(name, expr)\n\nprefix-classes(prefix)\n -prefix-classes(prefix, block)\n\n// Caching mixin, use inside your functions to enable caching by extending.\n\n$stylus_mixin_cache = {}\ncache()\n $key = (current-media() or 'no-media') + '__' + called-from[0] + '__' + arguments\n if $key in $stylus_mixin_cache\n @extend {'$cache_placeholder_for_' + $stylus_mixin_cache[$key]}\n else if 'cache' in called-from\n {block}\n else\n $id = length($stylus_mixin_cache)\n\n &,\n /$cache_placeholder_for_{$id}\n $stylus_mixin_cache[$key] = $id\n {block}\n";require.modules={};require.resolve=function(path){var orig=path,reg=path+".js",index=path+"/index.js";return require.modules[reg]&®||require.modules[index]&&index||orig};require.register=function(path,fn){require.modules[path]=fn};require.relative=function(parent){return function(p){if("."!=p[0])return require(p);var path=parent.split("/"),segs=p.split("/");path.pop();for(var i=0;i=0;i--){var last=parts[i];if(last=="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift("..")}}return parts}var splitPathRe=/^([\s\S]+\/(?!$)|\/)?((?:[\s\S]+?)?(\.[^.]*)?)$/;exports.normalize=function(path){var isAbsolute=path.charAt(0)==="/",trailingSlash=path.slice(-1)==="/";path=normalizeArray(path.split("/").filter(function(p){return!!p}),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(paths.filter(function(p,index){return p&&typeof p==="string"}).join("/"))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;isizeLimit)return literal;return new nodes.Literal('url("data:'+mime+";base64,"+buf.toString("base64")+hash+'")')}fn.raw=true;return fn};module.exports.mimes=defaultMimes});require.register("functions/add-property.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");(module.exports=function addProperty(name,expr){utils.assertType(name,"expression","name");name=utils.unwrap(name).first;utils.assertString(name,"name");utils.assertType(expr,"expression","expr");var prop=new nodes.Property([name],expr);var block=this.closestBlock;var len=block.nodes.length,head=block.nodes.slice(0,block.index),tail=block.nodes.slice(block.index++,len);head.push(prop);block.nodes=head.concat(tail);return prop}).raw=true});require.register("functions/adjust.js",function(module,exports,require){var utils=require("../utils");module.exports=function adjust(color,prop,amount){utils.assertColor(color,"color");utils.assertString(prop,"prop");utils.assertType(amount,"unit","amount");var hsl=color.hsla.clone();prop={hue:"h",saturation:"s",lightness:"l"}[prop.string];if(!prop)throw new Error("invalid adjustment property");var val=amount.val;if("%"==amount.type){val="l"==prop&&val>0?(100-hsl[prop])*val/100:hsl[prop]*(val/100)}hsl[prop]+=val;return hsl.rgba}});require.register("functions/alpha.js",function(module,exports,require){var nodes=require("../nodes"),rgba=require("./rgba");module.exports=function alpha(color,value){color=color.rgba;if(value){return rgba(new nodes.Unit(color.r),new nodes.Unit(color.g),new nodes.Unit(color.b),value)}return new nodes.Unit(color.a,"")}});require.register("functions/base-convert.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");(module.exports=function(num,base,width){utils.assertPresent(num,"number");utils.assertPresent(base,"base");num=utils.unwrap(num).nodes[0].val;base=utils.unwrap(base).nodes[0].val;width=width&&utils.unwrap(width).nodes[0].val||2;var result=Number(num).toString(base);while(result.lengthtop.a){top=blend(top,bottom)}var l1=luminosity(bottom).val+.05,l2=luminosity(top).val+.05,ratio=l1/l2;if(l2>l1){ratio=1/ratio}return Math.round(ratio*10)/10}if(1<=bottom.a){var resultRatio=new nodes.Unit(contrast(top,bottom));result.set("ratio",resultRatio);result.set("error",new nodes.Unit(0));result.set("min",resultRatio);result.set("max",resultRatio)}else{var onBlack=contrast(top,blend(bottom,new nodes.RGBA(0,0,0,1))),onWhite=contrast(top,blend(bottom,new nodes.RGBA(255,255,255,1))),max=Math.max(onBlack,onWhite);function processChannel(topChannel,bottomChannel){return Math.min(Math.max(0,(topChannel-bottomChannel*bottom.a)/(1-bottom.a)),255)}var closest=new nodes.RGBA(processChannel(top.r,bottom.r),processChannel(top.g,bottom.g),processChannel(top.b,bottom.b),1);var min=contrast(top,blend(bottom,closest));result.set("ratio",new nodes.Unit(Math.round((min+max)*50)/100));result.set("error",new nodes.Unit(Math.round((max-min)*50)/100));result.set("min",new nodes.Unit(min));result.set("max",new nodes.Unit(max))}return result}});require.register("functions/convert.js",function(module,exports,require){var utils=require("../utils");module.exports=function convert(str){utils.assertString(str,"str");return utils.parseString(str.string)}});require.register("functions/current-media.js",function(module,exports,require){var nodes=require("../nodes");module.exports=function currentMedia(){return new nodes.String(lookForMedia(this.closestBlock.node)||"");function lookForMedia(node){if("media"==node.nodeName){return node.toString()}else if(node.block.parent.node){return lookForMedia(node.block.parent.node)}}}});require.register("functions/define.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function define(name,expr){utils.assertType(name,"string","name");expr=utils.unwrap(expr);var scope=this.currentScope;var node=new nodes.Ident(name.val,expr);scope.add(node);return nodes.nil}});require.register("functions/dirname.js",function(module,exports,require){var utils=require("../utils"),path=require("../path");module.exports=function dirname(p){utils.assertString(p,"path");return path.dirname(p.val).replace(/\\/g,"/")}});require.register("functions/error.js",function(module,exports,require){var utils=require("../utils");module.exports=function error(msg){utils.assertType(msg,"string","msg");var err=new Error(msg.val);err.fromStylus=true;throw err}});require.register("functions/extname.js",function(module,exports,require){var utils=require("../utils"),path=require("../path");module.exports=function extname(p){utils.assertString(p,"path");return path.extname(p.val)}});require.register("functions/green.js",function(module,exports,require){var nodes=require("../nodes"),rgba=require("./rgba");module.exports=function green(color,value){color=color.rgba;if(value){return rgba(new nodes.Unit(color.r),value,new nodes.Unit(color.b),new nodes.Unit(color.a))}return new nodes.Unit(color.g,"")}});require.register("functions/hsl.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes"),hsla=require("./hsla");module.exports=function hsl(hue,saturation,lightness){if(1==arguments.length){utils.assertColor(hue,"color");return hue.hsla}else{return hsla(hue,saturation,lightness,new nodes.Unit(1))}}});require.register("functions/hsla.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function hsla(hue,saturation,lightness,alpha){switch(arguments.length){case 1:utils.assertColor(hue);return hue.hsla;case 2:utils.assertColor(hue);var color=hue.hsla;utils.assertType(saturation,"unit","alpha");var alpha=saturation.clone();if("%"==alpha.type)alpha.val/=100;return new nodes.HSLA(color.h,color.s,color.l,alpha.val);default:utils.assertType(hue,"unit","hue");utils.assertType(saturation,"unit","saturation");utils.assertType(lightness,"unit","lightness");utils.assertType(alpha,"unit","alpha");var alpha=alpha.clone();if(alpha&&"%"==alpha.type)alpha.val/=100;return new nodes.HSLA(hue.val,saturation.val,lightness.val,alpha.val)}}});require.register("functions/hue.js",function(module,exports,require){var nodes=require("../nodes"),hsla=require("./hsla"),component=require("./component");module.exports=function hue(color,value){if(value){var hslaColor=color.hsla;return hsla(value,new nodes.Unit(hslaColor.s),new nodes.Unit(hslaColor.l),new nodes.Unit(hslaColor.a))}return component(color,new nodes.String("hue"))}});require.register("functions/length.js",function(module,exports,require){var utils=require("../utils");(module.exports=function length(expr){if(expr){if(expr.nodes){var nodes=utils.unwrap(expr).nodes;if(1==nodes.length&&"object"==nodes[0].nodeName){return nodes[0].length}else{return nodes.length}}else{return 1}}return 0}).raw=true});require.register("functions/lightness.js",function(module,exports,require){var nodes=require("../nodes"),hsla=require("./hsla"),component=require("./component");module.exports=function lightness(color,value){if(value){var hslaColor=color.hsla;return hsla(new nodes.Unit(hslaColor.h),new nodes.Unit(hslaColor.s),value,new nodes.Unit(hslaColor.a))}return component(color,new nodes.String("lightness"))}});require.register("functions/list-separator.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");(module.exports=function listSeparator(list){list=utils.unwrap(list);return new nodes.String(list.isList?",":" ")}).raw=true});require.register("functions/lookup.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function lookup(name){utils.assertType(name,"string","name");var node=this.lookup(name.val);if(!node)return nodes.nil;return this.visit(node)}});require.register("functions/luminosity.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function luminosity(color){utils.assertColor(color);color=color.rgba;function processChannel(channel){channel=channel/255;return.03928>channel?channel/12.92:Math.pow((channel+.055)/1.055,2.4)}return new nodes.Unit(.2126*processChannel(color.r)+.7152*processChannel(color.g)+.0722*processChannel(color.b))}});require.register("functions/match.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function match(pattern,val){utils.assertType(pattern,"string","pattern");utils.assertString(val,"val");var re=new RegExp(pattern.val);return new nodes.Boolean(re.test(val.string))}});require.register("functions/math-prop.js",function(module,exports,require){var nodes=require("../nodes");module.exports=function math(prop){return new nodes.Unit(Math[prop.string])}});require.register("functions/math.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function math(n,fn){utils.assertType(n,"unit","n");utils.assertString(fn,"fn");return new nodes.Unit(Math[fn.string](n.val),n.type)}});require.register("functions/merge.js",function(module,exports,require){var utils=require("../utils");(module.exports=function merge(dest){utils.assertPresent(dest,"dest");dest=utils.unwrap(dest).first;utils.assertType(dest,"object","dest");var last=utils.unwrap(arguments[arguments.length-1]).first,deep=true===last.val;for(var i=1,len=arguments.length-deep;i1){if(expr.isList){pushToStack(expr.nodes,stack)}else{stack.push(parse(expr.nodes.map(function(node){utils.assertString(node,"selector");return node.string}).join(" ")))}}}else if(args.length>1){pushToStack(args,stack)}return stack.length?utils.compileSelectors(stack).join(","):"&"}).raw=true;function pushToStack(selectors,stack){selectors.forEach(function(sel){sel=sel.first;utils.assertString(sel,"selector");stack.push(parse(sel.string))})}function parse(selector){var Parser=new require("../parser"),parser=new Parser(selector),nodes;parser.state.push("selector-parts");nodes=parser.selector();nodes.forEach(function(node){node.val=node.segments.map(function(seg){return seg.toString()}).join("")});return nodes}});require.register("functions/selectors.js",function(module,exports,require){var nodes=require("../nodes"),Parser=require("../selector-parser");module.exports=function selectors(){var stack=this.selectorStack,expr=new nodes.Expression(true);if(stack.length){for(var i=0;i1){expr.push(new nodes.String(group.map(function(selector){nested=new Parser(selector.val).parse().nested;return(nested&&i?"& ":"")+selector.val}).join(",")))}else{var selector=group[0].val;nested=new Parser(selector).parse().nested;expr.push(new nodes.String((nested&&i?"& ":"")+selector))}}}else{expr.push(new nodes.String("&"))}return expr}});require.register("functions/shift.js",function(module,exports,require){var utils=require("../utils");(module.exports=function(expr){expr=utils.unwrap(expr);return expr.nodes.shift()}).raw=true});require.register("functions/split.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function split(delim,val){utils.assertString(delim,"delimiter");utils.assertString(val,"val");var splitted=val.string.split(delim.string);var expr=new nodes.Expression;var ItemNode=val instanceof nodes.Ident?nodes.Ident:nodes.String;for(var i=0,len=splitted.length;is.lastIndexOf("*/",offset),commentIdx=s.lastIndexOf("//",offset),i=s.lastIndexOf("\n",offset),double=0,single=0;if(~commentIdx&&commentIdx>i){while(i!=offset){if("'"==s[i])single?single--:single++;if('"'==s[i])double?double--:double++;if("/"==s[i]&&"/"==s[i+1]){inComment=!single&&!double;break}++i}}return inComment?str:val+"\r"}if(""==str.charAt(0))str=str.slice(1);this.str=str.replace(/\s+$/,"\n").replace(/\r\n?/g,"\n").replace(/\\ *\n/g,"\r").replace(/([,(:](?!\/\/[^ ])) *(?:\/\/[^\n]*)?\n\s*/g,comment).replace(/\s*\n[ \t]*([,)])/g,comment)}Lexer.prototype={inspect:function(){var tok,tmp=this.str,buf=[];while("eos"!=(tok=this.next()).type){buf.push(tok.inspect())}this.str=tmp;return buf.concat(tok.inspect()).join("\n")},lookahead:function(n){var fetch=n-this.stash.length;while(fetch-->0)this.stash.push(this.advance());return this.stash[--n]},skip:function(len){var chunk=len[0];len=chunk?chunk.length:len;this.str=this.str.substr(len);if(chunk){this.move(chunk)}else{this.column+=len}},move:function(str){var lines=str.match(/\n/g),idx=str.lastIndexOf("\n");if(lines)this.lineno+=lines.length;this.column=~idx?str.length-idx:this.column+str.length},next:function(){var tok=this.stashed()||this.advance();this.prev=tok;return tok},isPartOfSelector:function(){var tok=this.stash[this.stash.length-1]||this.prev;switch(tok&&tok.type){case"color":return 2==tok.val.raw.length;case".":case"[":return true}return false},advance:function(){var column=this.column,line=this.lineno,tok=this.eos()||this.nil()||this.sep()||this.keyword()||this.urlchars()||this.comment()||this.newline()||this.escaped()||this.important()||this.literal()||this.fun()||this.anonFunc()||this.atrule()||this.brace()||this.paren()||this.color()||this.string()||this.unit()||this.namedop()||this.boolean()||this.unicode()||this.ident()||this.op()||this.eol()||this.space()||this.selector();tok.lineno=line;tok.column=column;return tok},peek:function(){return this.lookahead(1)},stashed:function(){return this.stash.shift()},eos:function(){if(this.str.length)return;if(this.indentStack.length){this.indentStack.shift();return new Token("outdent")}else{return new Token("eos")}},urlchars:function(){var captures;if(!this.isURL)return;if(captures=/^[\/:@.;?&=*!,<>#%0-9]+/.exec(this.str)){this.skip(captures);return new Token("literal",new nodes.Literal(captures[0]))}},sep:function(){var captures;if(captures=/^;[ \t]*/.exec(this.str)){this.skip(captures);return new Token(";")}},eol:function(){if("\r"==this.str[0]){++this.lineno;this.skip(1);return this.advance()}},space:function(){var captures;if(captures=/^([ \t]+)/.exec(this.str)){this.skip(captures);return new Token("space")}},escaped:function(){var captures;if(captures=/^\\(.)[ \t]*/.exec(this.str)){var c=captures[1];this.skip(captures);return new Token("ident",new nodes.Literal(c))}},literal:function(){var captures;if(captures=/^@css[ \t]*\{/.exec(this.str)){this.skip(captures);var c,braces=1,css="",node;while(c=this.str[0]){this.str=this.str.substr(1);switch(c){case"{":++braces;break;case"}":--braces;break;case"\n":case"\r":++this.lineno;break}css+=c;if(!braces)break}css=css.replace(/\s*}$/,"");node=new nodes.Literal(css);node.css=true;return new Token("literal",node)}},important:function(){var captures;if(captures=/^!important[ \t]*/.exec(this.str)){this.skip(captures);return new Token("ident",new nodes.Literal("!important"))}},brace:function(){var captures;if(captures=/^([{}])/.exec(this.str)){this.skip(1);var brace=captures[1];return new Token(brace,brace)}},paren:function(){var captures;if(captures=/^([()])([ \t]*)/.exec(this.str)){var paren=captures[1];this.skip(captures);if(")"==paren)this.isURL=false;var tok=new Token(paren,paren);tok.space=captures[2];return tok}},nil:function(){var captures,tok;if(captures=/^(null)\b[ \t]*/.exec(this.str)){this.skip(captures);if(this.isPartOfSelector()){tok=new Token("ident",new nodes.Ident(captures[0]))}else{tok=new Token("null",nodes.nil)}return tok}},keyword:function(){var captures,tok;if(captures=/^(return|if|else|unless|for|in)\b[ \t]*/.exec(this.str)){var keyword=captures[1];this.skip(captures);if(this.isPartOfSelector()){tok=new Token("ident",new nodes.Ident(captures[0]))}else{tok=new Token(keyword,keyword)}return tok}},namedop:function(){var captures,tok;if(captures=/^(not|and|or|is a|is defined|isnt|is not|is)(?!-)\b([ \t]*)/.exec(this.str)){var op=captures[1];this.skip(captures);if(this.isPartOfSelector()){tok=new Token("ident",new nodes.Ident(captures[0]))}else{op=alias[op]||op;tok=new Token(op,op)}tok.space=captures[2];return tok}},op:function(){var captures;if(captures=/^([.]{1,3}|&&|\|\||[!<>=?:]=|\*\*|[-+*\/%]=?|[,=?:!~<>&\[\]])([ \t]*)/.exec(this.str)){var op=captures[1];this.skip(captures);op=alias[op]||op;var tok=new Token(op,op);tok.space=captures[2];this.isURL=false;return tok}},anonFunc:function(){var tok;if("@"==this.str[0]&&"("==this.str[1]){this.skip(2);tok=new Token("function",new nodes.Ident("anonymous"));tok.anonymous=true;return tok}},atrule:function(){var captures;if(captures=/^@(?:-(\w+)-)?([a-zA-Z0-9-_]+)[ \t]*/.exec(this.str)){this.skip(captures);var vendor=captures[1],type=captures[2],tok;switch(type){case"require":case"import":case"charset":case"namespace":case"media":case"scope":case"supports":return new Token(type);case"document":return new Token("-moz-document");case"block":return new Token("atblock");case"extend":case"extends":return new Token("extend");case"keyframes":return new Token(type,vendor);default:return new Token("atrule",vendor?"-"+vendor+"-"+type:type)}}},comment:function(){if("/"==this.str[0]&&"/"==this.str[1]){var end=this.str.indexOf("\n");if(-1==end)end=this.str.length;this.skip(end);return this.advance()}if("/"==this.str[0]&&"*"==this.str[1]){var end=this.str.indexOf("*/");if(-1==end)end=this.str.length;var str=this.str.substr(0,end+2),lines=str.split(/\n|\r/).length-1,suppress=true,inline=false;this.lineno+=lines;this.skip(end+2);if("!"==str[2]){str=str.replace("*!","*");suppress=false}if(this.prev&&";"==this.prev.type)inline=true;return new Token("comment",new nodes.Comment(str,suppress,inline))}},"boolean":function(){var captures;if(captures=/^(true|false)\b([ \t]*)/.exec(this.str)){var val=nodes.Boolean("true"==captures[1]);this.skip(captures);var tok=new Token("boolean",val);tok.space=captures[2];return tok}},unicode:function(){var captures;if(captures=/^u\+[0-9a-f?]{1,6}(?:-[0-9a-f]{1,6})?/i.exec(this.str)){this.skip(captures);return new Token("literal",new nodes.Literal(captures[0]))}},fun:function(){var captures;if(captures=/^(-*[_a-zA-Z$][-\w\d$]*)\(([ \t]*)/.exec(this.str)){var name=captures[1];this.skip(captures);this.isURL="url"==name;var tok=new Token("function",new nodes.Ident(name));tok.space=captures[2];return tok}},ident:function(){var captures;if(captures=/^-*[_a-zA-Z$][-\w\d$]*/.exec(this.str)){this.skip(captures);return new Token("ident",new nodes.Ident(captures[0]))}},newline:function(){var captures,re;if(this.indentRe){captures=this.indentRe.exec(this.str)}else{re=/^\n([\t]*)[ \t]*/;captures=re.exec(this.str);if(captures&&!captures[1].length){re=/^\n([ \t]*)/;captures=re.exec(this.str)}if(captures&&captures[1].length)this.indentRe=re}if(captures){var tok,indents=captures[1].length;this.skip(captures);if(this.str[0]===" "||this.str[0]===" "){throw new errors.SyntaxError("Invalid indentation. You can use tabs or spaces to indent, but not both.")}if("\n"==this.str[0])return this.advance();if(this.indentStack.length&&indentsindents){this.stash.push(new Token("outdent"));this.indentStack.shift()}tok=this.stash.pop()}else if(indents&&indents!=this.indentStack[0]){this.indentStack.unshift(indents);tok=new Token("indent")}else{tok=new Token("newline")}return tok}},unit:function(){var captures;if(captures=/^(-)?(\d+\.\d+|\d+|\.\d+)(%|[a-zA-Z]+)?[ \t]*/.exec(this.str)){this.skip(captures);var n=parseFloat(captures[2]);if("-"==captures[1])n=-n;var node=new nodes.Unit(n,captures[3]);node.raw=captures[0];return new Token("unit",node)}},string:function(){var captures;if(captures=/^("[^"]*"|'[^']*')[ \t]*/.exec(this.str)){var str=captures[1],quote=captures[0][0];this.skip(captures);str=str.slice(1,-1).replace(/\\n/g,"\n");return new Token("string",new nodes.String(str,quote))}},color:function(){return this.rrggbbaa()||this.rrggbb()||this.rgba()||this.rgb()||this.nn()||this.n()},n:function(){var captures;if(captures=/^#([a-fA-F0-9]{1})[ \t]*/.exec(this.str)){this.skip(captures);var n=parseInt(captures[1]+captures[1],16),color=new nodes.RGBA(n,n,n,1);color.raw=captures[0];return new Token("color",color)}},nn:function(){var captures;if(captures=/^#([a-fA-F0-9]{2})[ \t]*/.exec(this.str)){this.skip(captures);var n=parseInt(captures[1],16),color=new nodes.RGBA(n,n,n,1);color.raw=captures[0];return new Token("color",color)}},rgb:function(){var captures;if(captures=/^#([a-fA-F0-9]{3})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb[0]+rgb[0],16),g=parseInt(rgb[1]+rgb[1],16),b=parseInt(rgb[2]+rgb[2],16),color=new nodes.RGBA(r,g,b,1);color.raw=captures[0];return new Token("color",color)}},rgba:function(){var captures;if(captures=/^#([a-fA-F0-9]{4})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb[0]+rgb[0],16),g=parseInt(rgb[1]+rgb[1],16),b=parseInt(rgb[2]+rgb[2],16),a=parseInt(rgb[3]+rgb[3],16),color=new nodes.RGBA(r,g,b,a/255);color.raw=captures[0];return new Token("color",color)}},rrggbb:function(){var captures;if(captures=/^#([a-fA-F0-9]{6})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb.substr(0,2),16),g=parseInt(rgb.substr(2,2),16),b=parseInt(rgb.substr(4,2),16),color=new nodes.RGBA(r,g,b,1);color.raw=captures[0];return new Token("color",color)}},rrggbbaa:function(){var captures;if(captures=/^#([a-fA-F0-9]{8})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb.substr(0,2),16),g=parseInt(rgb.substr(2,2),16),b=parseInt(rgb.substr(4,2),16),a=parseInt(rgb.substr(6,2),16),color=new nodes.RGBA(r,g,b,a/255);color.raw=captures[0];return new Token("color",color)}},selector:function(){var captures;if(captures=/^\^|.*?(?=\/\/(?![^\[]*\])|[,\n{])/.exec(this.str)){var selector=captures[0];this.skip(captures);return new Token("selector",selector)}}}});require.register("nodes/arguments.js",function(module,exports,require){var Node=require("./node"),nodes=require("../nodes"),utils=require("../utils");var Arguments=module.exports=function Arguments(){nodes.Expression.call(this);this.map={}};Arguments.prototype.__proto__=nodes.Expression.prototype;Arguments.fromExpression=function(expr){var args=new Arguments,len=expr.nodes.length;args.lineno=expr.lineno;args.column=expr.column;args.isList=expr.isList;for(var i=0;ilen)self.nodes[i]=nodes.nil;self.nodes[n]=val}else if(unit.string){node=self.nodes[0];if(node&&"object"==node.nodeName)node.set(unit.string,val.clone())}});return val;case"[]":var expr=new nodes.Expression,vals=utils.unwrap(this).nodes,range=utils.unwrap(right).nodes,node;range.forEach(function(unit){if("unit"==unit.nodeName){node=vals[unit.val<0?vals.length+unit.val:unit.val]}else if("object"==vals[0].nodeName){node=vals[0].get(unit.string)}if(node)expr.push(node)});return expr.isEmpty?nodes.nil:utils.unwrap(expr);case"||":return this.toBoolean().isTrue?this:right;case"in":return Node.prototype.operate.call(this,op,right);case"!=":return this.operate("==",right,val).negate();case"==":var len=this.nodes.length,right=right.toExpression(),a,b;if(len!=right.nodes.length)return nodes.no;for(var i=0;i1)return nodes.yes;return this.first.toBoolean()};Expression.prototype.toString=function(){return"("+this.nodes.map(function(node){return node.toString()}).join(this.isList?", ":" ")+")"};Expression.prototype.toJSON=function(){return{__type:"Expression",isList:this.isList,preserve:this.preserve,lineno:this.lineno,column:this.column,filename:this.filename,nodes:this.nodes}}});require.register("nodes/function.js",function(module,exports,require){var Node=require("./node");var Function=module.exports=function Function(name,params,body){Node.call(this);this.name=name;this.params=params;this.block=body;if("function"==typeof params)this.fn=params};Function.prototype.__defineGetter__("arity",function(){return this.params.length});Function.prototype.__proto__=Node.prototype;Function.prototype.__defineGetter__("hash",function(){return"function "+this.name});Function.prototype.clone=function(parent){if(this.fn){var clone=new Function(this.name,this.fn)}else{var clone=new Function(this.name);clone.params=this.params.clone(parent,clone);clone.block=this.block.clone(parent,clone)}clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Function.prototype.toString=function(){if(this.fn){return this.name+"("+this.fn.toString().match(/^function *\w*\((.*?)\)/).slice(1).join(", ")+")"}else{return this.name+"("+this.params.nodes.join(", ")+")"}};Function.prototype.toJSON=function(){var json={__type:"Function",name:this.name,lineno:this.lineno,column:this.column,filename:this.filename};if(this.fn){json.fn=this.fn}else{json.params=this.params;json.block=this.block}return json}});require.register("nodes/group.js",function(module,exports,require){var Node=require("./node");var Group=module.exports=function Group(){Node.call(this);this.nodes=[];this.extends=[]};Group.prototype.__proto__=Node.prototype;Group.prototype.push=function(selector){this.nodes.push(selector)};Group.prototype.__defineGetter__("block",function(){return this.nodes[0].block});Group.prototype.__defineSetter__("block",function(block){for(var i=0,len=this.nodes.length;i=":case"<":case">":case"is a":case"||":case"&&":return this.rgba.operate(op,right);default:return this.rgba.operate(op,right).hsla}};exports.fromRGBA=function(rgba){var r=rgba.r/255,g=rgba.g/255,b=rgba.b/255,a=rgba.a;var min=Math.min(r,g,b),max=Math.max(r,g,b),l=(max+min)/2,d=max-min,h,s;switch(max){case min:h=0;break;case r:h=60*(g-b)/d;break;case g:h=60*(b-r)/d+120;break;case b:h=60*(r-g)/d+240;break}if(max==min){s=0}else if(l<.5){s=d/(2*l)}else{s=d/(2-2*l)}h%=360;s*=100;l*=100;return new HSLA(h,s,l,a)};HSLA.prototype.adjustLightness=function(percent){this.l=clampPercentage(this.l+this.l*(percent/100));return this};HSLA.prototype.adjustHue=function(deg){this.h=clampDegrees(this.h+deg);return this};function clampDegrees(n){n=n%360;return n>=0?n:360+n}function clampPercentage(n){return Math.max(0,Math.min(n,100))}function clampAlpha(n){return Math.max(0,Math.min(n,1))}});require.register("nodes/ident.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index");var Ident=module.exports=function Ident(name,val,mixin){Node.call(this);this.name=name;this.string=name;this.val=val||nodes.nil;this.mixin=!!mixin};Ident.prototype.__defineGetter__("isEmpty",function(){return undefined==this.val});Ident.prototype.__defineGetter__("hash",function(){return this.name});Ident.prototype.__proto__=Node.prototype;Ident.prototype.clone=function(parent){var clone=new Ident(this.name);clone.val=this.val.clone(parent,clone);clone.mixin=this.mixin;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;clone.property=this.property;clone.rest=this.rest;return clone};Ident.prototype.toJSON=function(){return{__type:"Ident",name:this.name,val:this.val,mixin:this.mixin,property:this.property,rest:this.rest,lineno:this.lineno,column:this.column,filename:this.filename}};Ident.prototype.toString=function(){return this.name};Ident.prototype.coerce=function(other){switch(other.nodeName){case"ident":case"string":case"literal":return new Ident(other.string);case"unit":return new Ident(other.toString());default:return Node.prototype.coerce.call(this,other)}};Ident.prototype.operate=function(op,right){var val=right.first;switch(op){case"-":if("unit"==val.nodeName){var expr=new nodes.Expression;val=val.clone();val.val=-val.val;expr.push(this);expr.push(val);return expr}case"+":return new nodes.Ident(this.string+this.coerce(val).string)}return Node.prototype.operate.call(this,op,right)}});require.register("nodes/if.js",function(module,exports,require){var Node=require("./node");var If=module.exports=function If(cond,negate){Node.call(this);this.cond=cond;this.elses=[];if(negate&&negate.nodeName){this.block=negate}else{this.negate=negate}};If.prototype.__proto__=Node.prototype;If.prototype.clone=function(parent){var clone=new If;clone.cond=this.cond.clone(parent,clone);clone.block=this.block.clone(parent,clone);clone.elses=this.elses.map(function(node){return node.clone(parent,clone)});clone.negate=this.negate;clone.postfix=this.postfix;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};If.prototype.toJSON=function(){return{__type:"If",cond:this.cond,block:this.block,elses:this.elses,negate:this.negate,postfix:this.postfix,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/import.js",function(module,exports,require){var Node=require("./node");var Import=module.exports=function Import(expr,once){Node.call(this);this.path=expr;this.once=once||false};Import.prototype.__proto__=Node.prototype;Import.prototype.clone=function(parent){var clone=new Import;clone.path=this.path.nodeName?this.path.clone(parent,clone):this.path;clone.once=this.once;clone.mtime=this.mtime;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Import.prototype.toJSON=function(){return{__type:"Import",path:this.path,once:this.once,mtime:this.mtime,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/extend.js",function(module,exports,require){var Node=require("./node");var Extend=module.exports=function Extend(selectors){Node.call(this);this.selectors=selectors};Extend.prototype.__proto__=Node.prototype;Extend.prototype.clone=function(){return new Extend(this.selectors)};Extend.prototype.toString=function(){return"@extend "+this.selectors.join(", ")};Extend.prototype.toJSON=function(){return{__type:"Extend",selectors:this.selectors,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/index.js",function(module,exports,require){exports.Node=require("./node");exports.Root=require("./root");exports.Null=require("./null");exports.Each=require("./each");exports.If=require("./if");exports.Call=require("./call");exports.UnaryOp=require("./unaryop");exports.BinOp=require("./binop");exports.Ternary=require("./ternary");exports.Block=require("./block");exports.Unit=require("./unit");exports.String=require("./string");exports.HSLA=require("./hsla");exports.RGBA=require("./rgba");exports.Ident=require("./ident");exports.Group=require("./group");exports.Literal=require("./literal");exports.Boolean=require("./boolean");exports.Return=require("./return");exports.Media=require("./media");exports.QueryList=require("./query-list");exports.Query=require("./query");exports.Feature=require("./feature");exports.Params=require("./params");exports.Comment=require("./comment");exports.Keyframes=require("./keyframes");exports.Member=require("./member");exports.Charset=require("./charset");exports.Namespace=require("./namespace");exports.Import=require("./import");exports.Extend=require("./extend");exports.Object=require("./object");exports.Function=require("./function");exports.Property=require("./property");exports.Selector=require("./selector");exports.Expression=require("./expression");exports.Arguments=require("./arguments");exports.Atblock=require("./atblock");exports.Atrule=require("./atrule");exports.Supports=require("./supports");exports.yes=new exports.Boolean(true);exports.no=new exports.Boolean(false);exports.nil=new exports.Null});require.register("nodes/keyframes.js",function(module,exports,require){var Atrule=require("./atrule");var Keyframes=module.exports=function Keyframes(segs,prefix){Atrule.call(this,"keyframes");this.segments=segs;this.prefix=prefix||"official"};Keyframes.prototype.__proto__=Atrule.prototype;Keyframes.prototype.clone=function(parent){var clone=new Keyframes;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;clone.segments=this.segments.map(function(node){return node.clone(parent,clone)});clone.prefix=this.prefix;clone.block=this.block.clone(parent,clone);return clone};Keyframes.prototype.toJSON=function(){return{__type:"Keyframes",segments:this.segments,prefix:this.prefix,block:this.block,lineno:this.lineno,column:this.column,filename:this.filename}};Keyframes.prototype.toString=function(){return"@keyframes "+this.segments.join("")}});require.register("nodes/literal.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index");var Literal=module.exports=function Literal(str){Node.call(this);this.val=str;this.string=str;this.prefixed=false};Literal.prototype.__proto__=Node.prototype;Literal.prototype.__defineGetter__("hash",function(){return this.val});Literal.prototype.toString=function(){return this.val};Literal.prototype.coerce=function(other){switch(other.nodeName){case"ident":case"string":case"literal":return new Literal(other.string);default:return Node.prototype.coerce.call(this,other)}};Literal.prototype.operate=function(op,right){var val=right.first;switch(op){case"+":return new nodes.Literal(this.string+this.coerce(val).string);default:return Node.prototype.operate.call(this,op,right)}};Literal.prototype.toJSON=function(){return{__type:"Literal",val:this.val,string:this.string,prefixed:this.prefixed,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/media.js",function(module,exports,require){var Atrule=require("./atrule");var Media=module.exports=function Media(val){Atrule.call(this,"media");this.val=val};Media.prototype.__proto__=Atrule.prototype;Media.prototype.clone=function(parent){var clone=new Media;clone.val=this.val.clone(parent,clone);clone.block=this.block.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Media.prototype.toJSON=function(){return{__type:"Media",val:this.val,block:this.block,lineno:this.lineno,column:this.column,filename:this.filename}};Media.prototype.toString=function(){return"@media "+this.val}});require.register("nodes/query-list.js",function(module,exports,require){var Node=require("./node");var QueryList=module.exports=function QueryList(){Node.call(this);this.nodes=[]};QueryList.prototype.__proto__=Node.prototype;QueryList.prototype.clone=function(parent){var clone=new QueryList;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;for(var i=0;i=":return nodes.Boolean(this.hash>=right.hash);case"<=":return nodes.Boolean(this.hash<=right.hash);case">":return nodes.Boolean(this.hash>right.hash);case"<":return nodes.Boolean(this.hash1)--h;if(h*6<1)return m1+(m2-m1)*h*6;if(h*2<1)return m2;if(h*3<2)return m1+(m2-m1)*(2/3-h)*6;return m1}return new RGBA(r,g,b,a)};function clamp(n){return Math.max(0,Math.min(n.toFixed(0),255))}function clampAlpha(n){return Math.max(0,Math.min(n,1))}});require.register("nodes/root.js",function(module,exports,require){var Node=require("./node");var Root=module.exports=function Root(){this.nodes=[]};Root.prototype.__proto__=Node.prototype;Root.prototype.push=function(node){this.nodes.push(node)};Root.prototype.unshift=function(node){this.nodes.unshift(node)};Root.prototype.clone=function(){var clone=new Root;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;this.nodes.forEach(function(node){clone.push(node.clone(clone,clone))});return clone};Root.prototype.toString=function(){return"[Root]"};Root.prototype.toJSON=function(){return{__type:"Root",nodes:this.nodes,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/selector.js",function(module,exports,require){var Block=require("./block"),Node=require("./node");var Selector=module.exports=function Selector(segs){Node.call(this);this.inherits=true;this.segments=segs;this.optional=false};Selector.prototype.__proto__=Node.prototype;Selector.prototype.toString=function(){return this.segments.join("")+(this.optional?" !optional":"")};Selector.prototype.__defineGetter__("isPlaceholder",function(){return this.val&&~this.val.substr(0,2).indexOf("$")});Selector.prototype.clone=function(parent){var clone=new Selector;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;clone.inherits=this.inherits;clone.val=this.val;clone.segments=this.segments.map(function(node){return node.clone(parent,clone)});clone.optional=this.optional;return clone};Selector.prototype.toJSON=function(){return{__type:"Selector",inherits:this.inherits,segments:this.segments,optional:this.optional,val:this.val,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/string.js",function(module,exports,require){var Node=require("./node"),sprintf=require("../functions").s,utils=require("../utils"),nodes=require("./index");var String=module.exports=function String(val,quote){Node.call(this);this.val=val;this.string=val;this.prefixed=false;if(typeof quote!=="string"){this.quote="'"}else{this.quote=quote}};String.prototype.__proto__=Node.prototype;String.prototype.toString=function(){return this.quote+this.val+this.quote};String.prototype.clone=function(){var clone=new String(this.val,this.quote);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};String.prototype.toJSON=function(){return{__type:"String",val:this.val,quote:this.quote,lineno:this.lineno,column:this.column,filename:this.filename}};String.prototype.toBoolean=function(){return nodes.Boolean(this.val.length)};String.prototype.coerce=function(other){switch(other.nodeName){case"string":return other;case"expression":return new String(other.nodes.map(function(node){return this.coerce(node).val},this).join(" "));default:return new String(other.toString())}};String.prototype.operate=function(op,right){switch(op){case"%":var expr=new nodes.Expression;expr.push(this);var args="expression"==right.nodeName?utils.unwrap(right).nodes:[right];return sprintf.apply(null,[expr].concat(args));case"+":var expr=new nodes.Expression;expr.push(new String(this.val+this.coerce(right).val));return expr;default:return Node.prototype.operate.call(this,op,right)}}});require.register("nodes/ternary.js",function(module,exports,require){var Node=require("./node");var Ternary=module.exports=function Ternary(cond,trueExpr,falseExpr){Node.call(this);this.cond=cond;this.trueExpr=trueExpr;this.falseExpr=falseExpr};Ternary.prototype.__proto__=Node.prototype;Ternary.prototype.clone=function(parent){var clone=new Ternary;clone.cond=this.cond.clone(parent,clone);clone.trueExpr=this.trueExpr.clone(parent,clone);clone.falseExpr=this.falseExpr.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Ternary.prototype.toJSON=function(){return{__type:"Ternary",cond:this.cond,trueExpr:this.trueExpr,falseExpr:this.falseExpr,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/unaryop.js",function(module,exports,require){var Node=require("./node");var UnaryOp=module.exports=function UnaryOp(op,expr){Node.call(this);this.op=op;this.expr=expr};UnaryOp.prototype.__proto__=Node.prototype;UnaryOp.prototype.clone=function(parent){var clone=new UnaryOp(this.op);clone.expr=this.expr.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};UnaryOp.prototype.toJSON=function(){return{__type:"UnaryOp",op:this.op,expr:this.expr,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/unit.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index");var FACTOR_TABLE={mm:{val:1,label:"mm"},cm:{val:10,label:"mm"},"in":{val:25.4,label:"mm"},pt:{val:25.4/72,label:"mm"},ms:{val:1,label:"ms"},s:{val:1e3,label:"ms"},Hz:{val:1,label:"Hz"},kHz:{val:1e3,label:"Hz"}};var Unit=module.exports=function Unit(val,type){Node.call(this);this.val=val;this.type=type};Unit.prototype.__proto__=Node.prototype;Unit.prototype.toBoolean=function(){return nodes.Boolean(this.type?true:this.val)};Unit.prototype.toString=function(){return this.val+(this.type||"")};Unit.prototype.clone=function(){var clone=new Unit(this.val,this.type);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Unit.prototype.toJSON=function(){return{__type:"Unit",val:this.val,type:this.type,lineno:this.lineno,column:this.column,filename:this.filename}};Unit.prototype.operate=function(op,right){var type=this.type||right.first.type;if("rgba"==right.nodeName||"hsla"==right.nodeName){return right.operate(op,this)}if(this.shouldCoerce(op)){right=right.first;if("%"!=this.type&&("-"==op||"+"==op)&&"%"==right.type){right=new Unit(this.val*(right.val/100),"%")}else{right=this.coerce(right)}switch(op){case"-":return new Unit(this.val-right.val,type);case"+":type=type||right.type=="%"&&right.type;return new Unit(this.val+right.val,type);case"/":return new Unit(this.val/right.val,type);case"*":return new Unit(this.val*right.val,type);case"%":return new Unit(this.val%right.val,type);case"**":return new Unit(Math.pow(this.val,right.val),type);case"..":case"...":var start=this.val,end=right.val,expr=new nodes.Expression,inclusive=".."==op;if(start=end:--start>end)}return expr}}return Node.prototype.operate.call(this,op,right)};Unit.prototype.coerce=function(other){if("unit"==other.nodeName){var a=this,b=other,factorA=FACTOR_TABLE[a.type],factorB=FACTOR_TABLE[b.type];if(factorA&&factorB&&factorA.label==factorB.label){var bVal=b.val*(factorB.val/factorA.val);return new nodes.Unit(bVal,a.type)}else{return new nodes.Unit(b.val,a.type)}}else if("string"==other.nodeName){if("%"==other.val)return new nodes.Unit(0,"%");var val=parseFloat(other.val);if(isNaN(val))Node.prototype.coerce.call(this,other);return new nodes.Unit(val)}else{return Node.prototype.coerce.call(this,other)}}});require.register("nodes/object.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index"),nativeObj={}.constructor;var Object=module.exports=function Object(){Node.call(this);this.vals={}};Object.prototype.__proto__=Node.prototype;Object.prototype.set=function(key,val){this.vals[key]=val;return this};Object.prototype.__defineGetter__("length",function(){return nativeObj.keys(this.vals).length});Object.prototype.get=function(key){return this.vals[key]||nodes.nil};Object.prototype.has=function(key){return key in this.vals};Object.prototype.operate=function(op,right){switch(op){case".":case"[]":return this.get(right.hash);case"==":var vals=this.vals,a,b;if("object"!=right.nodeName||this.length!=right.length)return nodes.no;for(var key in vals){a=vals[key];b=right.vals[key];if(a.operate(op,b).isFalse)return nodes.no}return nodes.yes;case"!=":return this.operate("==",right).negate();default:return Node.prototype.operate.call(this,op,right)}};Object.prototype.toBoolean=function(){return nodes.Boolean(this.length)};Object.prototype.toBlock=function(){var str="{",key,val;for(key in this.vals){val=this.get(key);if("object"==val.first.nodeName){str+=key+" "+val.first.toBlock()}else{switch(key){case"@charset":str+=key+" "+val.first.toString()+";";break;default:str+=key+":"+toString(val)+";"}}}str+="}";return str;function toString(node){if(node.nodes){return node.nodes.map(toString).join(node.isList?",":" ")}else if("literal"==node.nodeName&&","==node.val){return"\\,"}return node.toString()}};Object.prototype.clone=function(parent){var clone=new Object;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;for(var key in this.vals){clone.vals[key]=this.vals[key].clone(parent,clone)}return clone};Object.prototype.toJSON=function(){return{__type:"Object",vals:this.vals,lineno:this.lineno,column:this.column,filename:this.filename}};Object.prototype.toString=function(){var obj={};for(var prop in this.vals){obj[prop]=this.vals[prop].toString()}return JSON.stringify(obj)}});require.register("nodes/supports.js",function(module,exports,require){var Atrule=require("./atrule");var Supports=module.exports=function Supports(condition){Atrule.call(this,"supports");this.condition=condition};Supports.prototype.__proto__=Atrule.prototype;Supports.prototype.clone=function(parent){var clone=new Supports;clone.condition=this.condition.clone(parent,clone); +clone.block=this.block.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Supports.prototype.toJSON=function(){return{__type:"Supports",condition:this.condition,block:this.block,lineno:this.lineno,column:this.column,filename:this.filename}};Supports.prototype.toString=function(){return"@supports "+this.condition}});require.register("nodes/member.js",function(module,exports,require){var Node=require("./node");var Member=module.exports=function Member(left,right){Node.call(this);this.left=left;this.right=right};Member.prototype.__proto__=Node.prototype;Member.prototype.clone=function(parent){var clone=new Member;clone.left=this.left.clone(parent,clone);clone.right=this.right.clone(parent,clone);if(this.val)clone.val=this.val.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Member.prototype.toJSON=function(){var json={__type:"Member",left:this.left,right:this.right,lineno:this.lineno,column:this.column,filename:this.filename};if(this.val)json.val=this.val;return json};Member.prototype.toString=function(){return this.left.toString()+"."+this.right.toString()}});require.register("nodes/atblock.js",function(module,exports,require){var Node=require("./node");var Atblock=module.exports=function Atblock(){Node.call(this)};Atblock.prototype.__defineGetter__("nodes",function(){return this.block.nodes});Atblock.prototype.__proto__=Node.prototype;Atblock.prototype.clone=function(parent){var clone=new Atblock;clone.block=this.block.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Atblock.prototype.toString=function(){return"@block"};Atblock.prototype.toJSON=function(){return{__type:"Atblock",block:this.block,lineno:this.lineno,column:this.column,fileno:this.fileno}}});require.register("nodes/atrule.js",function(module,exports,require){var Node=require("./node");var Atrule=module.exports=function Atrule(type){Node.call(this);this.type=type};Atrule.prototype.__proto__=Node.prototype;Atrule.prototype.__defineGetter__("hasOnlyProperties",function(){if(!this.block)return false;var nodes=this.block.nodes;for(var i=0,len=nodes.length;i","=",":","&","&&","~","{","}",".","..","/"];var pseudoSelectors=["matches","not","dir","lang","any-link","link","visited","local-link","target","scope","hover","active","focus","drop","current","past","future","enabled","disabled","read-only","read-write","placeholder-shown","checked","indeterminate","valid","invalid","in-range","out-of-range","required","optional","user-error","root","empty","blank","nth-child","nth-last-child","first-child","last-child","only-child","nth-of-type","nth-last-of-type","first-of-type","last-of-type","only-of-type","nth-match","nth-last-match","nth-column","nth-last-column","first-line","first-letter","before","after","selection"];var Parser=module.exports=function Parser(str,options){var self=this;options=options||{};this.lexer=new Lexer(str,options);this.prefix=options.prefix||"";this.root=options.root||new nodes.Root;this.state=["root"];this.stash=[];this.parens=0;this.css=0;this.state.pop=function(){self.prevState=[].pop.call(this)}};Parser.prototype={constructor:Parser,currentState:function(){return this.state[this.state.length-1]},previousState:function(){return this.state[this.state.length-2]},parse:function(){var block=this.parent=this.root;while("eos"!=this.peek().type){this.skipWhitespace();if("eos"==this.peek().type)break;var stmt=this.statement();this.accept(";");if(!stmt)this.error("unexpected token {peek}, not allowed at the root level");block.push(stmt)}return block},error:function(msg){var type=this.peek().type,val=undefined==this.peek().val?"":" "+this.peek().toString();if(val.trim()==type.trim())val="";throw new errors.ParseError(msg.replace("{peek}",'"'+type+val+'"'))},accept:function(type){if(type==this.peek().type){return this.next()}},expect:function(type){if(type!=this.peek().type){this.error('expected "'+type+'", got {peek}')}return this.next()},next:function(){var tok=this.stash.length?this.stash.pop():this.lexer.next(),line=tok.lineno,column=tok.column||1;if(tok.val&&tok.val.nodeName){tok.val.lineno=line;tok.val.column=column}nodes.lineno=line;nodes.column=column;return tok},peek:function(){return this.lexer.peek()},lookahead:function(n){return this.lexer.lookahead(n)},isSelectorToken:function(n){var la=this.lookahead(n).type;switch(la){case"for":return this.bracketed;case"[":this.bracketed=true;return true;case"]":this.bracketed=false;return true;default:return~selectorTokens.indexOf(la)}},isPseudoSelector:function(n){var val=this.lookahead(n).val;return val&&~pseudoSelectors.indexOf(val.name)},lineContains:function(type){var i=1,la;while(la=this.lookahead(i++)){if(~["indent","outdent","newline","eos"].indexOf(la.type))return;if(type==la.type)return true}},selectorToken:function(){if(this.isSelectorToken(1)){if("{"==this.peek().type){if(!this.lineContains("}"))return;var i=0,la;while(la=this.lookahead(++i)){if("}"==la.type){if(i==2||i==3&&this.lookahead(i-1).type=="space")return;break}if(":"==la.type)return}}return this.next()}},skip:function(tokens){while(~tokens.indexOf(this.peek().type))this.next()},skipWhitespace:function(){this.skip(["space","indent","outdent","newline"])},skipNewlines:function(){while("newline"==this.peek().type)this.next()},skipSpaces:function(){while("space"==this.peek().type)this.next()},skipSpacesAndComments:function(){while("space"==this.peek().type||"comment"==this.peek().type)this.next()},looksLikeFunctionDefinition:function(i){return"indent"==this.lookahead(i).type||"{"==this.lookahead(i).type},looksLikeSelector:function(fromProperty){var i=1,brace;if(fromProperty&&":"==this.lookahead(i+1).type&&(this.lookahead(i+1).space||"indent"==this.lookahead(i+2).type))return false;while("ident"==this.lookahead(i).type&&("newline"==this.lookahead(i+1).type||","==this.lookahead(i+1).type))i+=2;while(this.isSelectorToken(i)||","==this.lookahead(i).type){if("selector"==this.lookahead(i).type)return true;if("&"==this.lookahead(i+1).type)return true;if("."==this.lookahead(i).type&&"ident"==this.lookahead(i+1).type)return true;if("*"==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true;if(":"==this.lookahead(i).type&&":"==this.lookahead(i+1).type)return true;if("color"==this.lookahead(i).type&&"newline"==this.lookahead(i-1).type)return true;if(this.looksLikeAttributeSelector(i))return true;if(("="==this.lookahead(i).type||"function"==this.lookahead(i).type)&&"{"==this.lookahead(i+1).type)return false;if(":"==this.lookahead(i).type&&!this.isPseudoSelector(i+1)&&this.lineContains("."))return false;if("{"==this.lookahead(i).type)brace=true;else if("}"==this.lookahead(i).type)brace=false;if(brace&&":"==this.lookahead(i).type)return true;if("space"==this.lookahead(i).type&&"{"==this.lookahead(i+1).type)return true;if(":"==this.lookahead(i++).type&&!this.lookahead(i-1).space&&this.isPseudoSelector(i))return true;if("space"==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type&&"{"==this.lookahead(i+2).type)return true;if(","==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true}if(","==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true;if("{"==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true;if(this.css){if(";"==this.lookahead(i).type||"}"==this.lookahead(i-1).type)return false}while(!~["indent","outdent","newline","for","if",";","}","eos"].indexOf(this.lookahead(i).type))++i;if("indent"==this.lookahead(i).type)return true},looksLikeAttributeSelector:function(n){var type=this.lookahead(n).type;if("="==type&&this.bracketed)return true;return("ident"==type||"string"==type)&&"]"==this.lookahead(n+1).type&&("newline"==this.lookahead(n+2).type||this.isSelectorToken(n+2))&&!this.lineContains(":")&&!this.lineContains("=")},looksLikeKeyframe:function(){var i=2,type;switch(this.lookahead(i).type){case"{":case"indent":case",":return true;case"newline":while("unit"==this.lookahead(++i).type||"newline"==this.lookahead(i).type);type=this.lookahead(i).type;return"indent"==type||"{"==type}},stateAllowsSelector:function(){switch(this.currentState()){case"root":case"atblock":case"selector":case"conditional":case"function":case"atrule":case"for":return true}},assignAtblock:function(expr){try{expr.push(this.atblock(expr))}catch(err){this.error("invalid right-hand side operand in assignment, got {peek}")}},statement:function(){var stmt=this.stmt(),state=this.prevState,block,op;if(this.allowPostfix){this.allowPostfix=false;state="expression"}switch(state){case"assignment":case"expression":case"function arguments":while(op=this.accept("if")||this.accept("unless")||this.accept("for")){switch(op.type){case"if":case"unless":stmt=new nodes.If(this.expression(),stmt);stmt.postfix=true;stmt.negate="unless"==op.type;this.accept(";");break;case"for":var key,val=this.id().name;if(this.accept(","))key=this.id().name;this.expect("in");var each=new nodes.Each(val,key,this.expression());block=new nodes.Block(this.parent,each);block.push(stmt);each.block=block;stmt=each}}}return stmt},stmt:function(){var type=this.peek().type;switch(type){case"keyframes":return this.keyframes();case"-moz-document":return this.mozdocument();case"comment":case"selector":case"extend":case"literal":case"charset":case"namespace":case"require":case"extend":case"media":case"atrule":case"ident":case"scope":case"supports":case"unless":return this[type]();case"function":return this.fun();case"import":return this.atimport();case"if":return this.ifstmt();case"for":return this.forin();case"return":return this.ret();case"{":return this.property();default:if(this.stateAllowsSelector()){switch(type){case"color":case"~":case">":case"<":case":":case"&":case"&&":case"[":case".":case"/":return this.selector();case"..":if("/"==this.lookahead(2).type)return this.selector();case"+":return"function"==this.lookahead(2).type?this.functionCall():this.selector();case"*":return this.property();case"unit":if(this.looksLikeKeyframe())return this.selector();case"-":if("{"==this.lookahead(2).type)return this.property()}}var expr=this.expression();if(expr.isEmpty)this.error("unexpected {peek}");return expr}},block:function(node,scope){var delim,stmt,next,block=this.parent=new nodes.Block(this.parent,node);if(false===scope)block.scope=false;this.accept("newline");if(this.accept("{")){this.css++;delim="}";this.skipWhitespace()}else{delim="outdent";this.expect("indent")}while(delim!=this.peek().type){if(this.css){if(this.accept("newline")||this.accept("indent"))continue;stmt=this.statement();this.accept(";");this.skipWhitespace()}else{if(this.accept("newline"))continue;next=this.lookahead(2).type;if("indent"==this.peek().type&&~["outdent","newline","comment"].indexOf(next)){this.skip(["indent","outdent"]);continue}if("eos"==this.peek().type)return block;stmt=this.statement();this.accept(";")}if(!stmt)this.error("unexpected token {peek} in block");block.push(stmt)}if(this.css){this.skipWhitespace();this.expect("}");this.skipSpaces();this.css--}else{this.expect("outdent")}this.parent=block.parent;return block},comment:function(){var node=this.next().val;this.skipSpaces();return node},forin:function(){this.expect("for");var key,val=this.id().name;if(this.accept(","))key=this.id().name;this.expect("in");this.state.push("for");this.cond=true;var each=new nodes.Each(val,key,this.expression());this.cond=false;each.block=this.block(each,false);this.state.pop();return each},ret:function(){this.expect("return");var expr=this.expression();return expr.isEmpty?new nodes.Return:new nodes.Return(expr)},unless:function(){this.expect("unless");this.state.push("conditional");this.cond=true;var node=new nodes.If(this.expression(),true);this.cond=false;node.block=this.block(node,false);this.state.pop();return node},ifstmt:function(){this.expect("if");this.state.push("conditional");this.cond=true;var node=new nodes.If(this.expression()),cond,block;this.cond=false;node.block=this.block(node,false);this.skip(["newline","comment"]);while(this.accept("else")){if(this.accept("if")){this.cond=true;cond=this.expression();this.cond=false;block=this.block(node,false);node.elses.push(new nodes.If(cond,block))}else{node.elses.push(this.block(node,false));break}this.skip(["newline","comment"])}this.state.pop();return node},atblock:function(node){if(!node)this.expect("atblock");node=new nodes.Atblock;this.state.push("atblock");node.block=this.block(node,false);this.state.pop();return node},atrule:function(){var type=this.expect("atrule").val,node=new nodes.Atrule(type),tok;this.skipSpacesAndComments();node.segments=this.selectorParts();this.skipSpacesAndComments();tok=this.peek().type;if("indent"==tok||"{"==tok||"newline"==tok&&"{"==this.lookahead(2).type){this.state.push("atrule");node.block=this.block(node);this.state.pop()}return node},scope:function(){this.expect("scope");var selector=this.selectorParts().map(function(selector){return selector.val}).join("");this.selectorScope=selector.trim();return nodes.nil},supports:function(){this.expect("supports");var node=new nodes.Supports(this.supportsCondition());this.state.push("atrule");node.block=this.block(node);this.state.pop();return node},supportsCondition:function(){var node=this.supportsNegation()||this.supportsOp();if(!node){this.cond=true;node=this.expression();this.cond=false}return node},supportsNegation:function(){if(this.accept("not")){var node=new nodes.Expression;node.push(new nodes.Literal("not"));node.push(this.supportsFeature());return node}},supportsOp:function(){var feature=this.supportsFeature(),op,expr;if(feature){expr=new nodes.Expression;expr.push(feature);while(op=this.accept("&&")||this.accept("||")){expr.push(new nodes.Literal("&&"==op.val?"and":"or"));expr.push(this.supportsFeature())}return expr}},supportsFeature:function(){this.skipSpacesAndComments();if("("==this.peek().type){var la=this.lookahead(2).type;if("ident"==la||"{"==la){return this.feature()}else{this.expect("(");var node=new nodes.Expression;node.push(new nodes.Literal("("));node.push(this.supportsCondition());this.expect(")");node.push(new nodes.Literal(")"));this.skipSpacesAndComments();return node}}},extend:function(){var tok=this.expect("extend"),selectors=[],sel,node,arr;do{arr=this.selectorParts();if(!arr.length)continue;sel=new nodes.Selector(arr);selectors.push(sel);if("!"!==this.peek().type)continue;tok=this.lookahead(2);if("ident"!==tok.type||"optional"!==tok.val.name)continue;this.skip(["!","ident"]);sel.optional=true}while(this.accept(","));node=new nodes.Extend(selectors);node.lineno=tok.lineno;node.column=tok.column;return node},media:function(){this.expect("media");this.state.push("atrule");var media=new nodes.Media(this.queries());media.block=this.block(media);this.state.pop();return media},queries:function(){var queries=new nodes.QueryList,skip=["comment","newline","space"];do{this.skip(skip);queries.push(this.query());this.skip(skip)}while(this.accept(","));return queries},query:function(){var query=new nodes.Query,expr,pred,id;if("ident"==this.peek().type&&("."==this.lookahead(2).type||"["==this.lookahead(2).type)){this.cond=true;expr=this.expression();this.cond=false;query.push(new nodes.Feature(expr.nodes));return query}if(pred=this.accept("ident")||this.accept("not")){pred=new nodes.Literal(pred.val.string||pred.val);this.skipSpacesAndComments();if(id=this.accept("ident")){query.type=id.val;query.predicate=pred}else{query.type=pred}this.skipSpacesAndComments();if(!this.accept("&&"))return query}do{query.push(this.feature())}while(this.accept("&&"));return query},feature:function(){this.skipSpacesAndComments();this.expect("(");this.skipSpacesAndComments();var node=new nodes.Feature(this.interpolate());this.skipSpacesAndComments();this.accept(":");this.skipSpacesAndComments();this.inProperty=true;node.expr=this.list();this.inProperty=false;this.skipSpacesAndComments();this.expect(")");this.skipSpacesAndComments();return node},mozdocument:function(){this.expect("-moz-document");var mozdocument=new nodes.Atrule("-moz-document"),calls=[];do{this.skipSpacesAndComments();calls.push(this.functionCall());this.skipSpacesAndComments()}while(this.accept(","));mozdocument.segments=[new nodes.Literal(calls.join(", "))];this.state.push("atrule");mozdocument.block=this.block(mozdocument,false);this.state.pop();return mozdocument},atimport:function(){this.expect("import");this.allowPostfix=true;return new nodes.Import(this.expression(),false)},require:function(){this.expect("require");this.allowPostfix=true;return new nodes.Import(this.expression(),true)},charset:function(){this.expect("charset");var str=this.expect("string").val;this.allowPostfix=true;return new nodes.Charset(str)},namespace:function(){var str,prefix;this.expect("namespace");this.skipSpacesAndComments();if(prefix=this.accept("ident")){prefix=prefix.val}this.skipSpacesAndComments();str=this.accept("string")||this.url();this.allowPostfix=true;return new nodes.Namespace(str,prefix)},keyframes:function(){var tok=this.expect("keyframes"),keyframes;this.skipSpacesAndComments();keyframes=new nodes.Keyframes(this.selectorParts(),tok.val);this.skipSpacesAndComments();this.state.push("atrule");keyframes.block=this.block(keyframes);this.state.pop();return keyframes},literal:function(){return this.expect("literal").val},id:function(){var tok=this.expect("ident");this.accept("space");return tok.val},ident:function(){var i=2,la=this.lookahead(i).type;while("space"==la)la=this.lookahead(++i).type;switch(la){case"=":case"?=":case"-=":case"+=":case"*=":case"/=":case"%=":return this.assignment();case".":if("space"==this.lookahead(i-1).type)return this.selector();if(this._ident==this.peek())return this.id();while("="!=this.lookahead(++i).type&&!~["[",",","newline","indent","eos"].indexOf(this.lookahead(i).type));if("="==this.lookahead(i).type){this._ident=this.peek();return this.expression()}else if(this.looksLikeSelector()&&this.stateAllowsSelector()){return this.selector()}case"[":if(this._ident==this.peek())return this.id();while("]"!=this.lookahead(i++).type&&"selector"!=this.lookahead(i).type&&"eos"!=this.lookahead(i).type);if("="==this.lookahead(i).type){this._ident=this.peek();return this.expression()}else if(this.looksLikeSelector()&&this.stateAllowsSelector()){return this.selector()}case"-":case"+":case"/":case"*":case"%":case"**":case"&&":case"||":case">":case"<":case">=":case"<=":case"!=":case"==":case"?":case"in":case"is a":case"is defined":if(this._ident==this.peek()){return this.id()}else{this._ident=this.peek();switch(this.currentState()){case"for":case"selector":return this.property();case"root":case"atblock":case"atrule":return"["==la?this.subscript():this.selector();case"function":case"conditional":return this.looksLikeSelector()?this.selector():this.expression();default:return this.operand?this.id():this.expression()}}default:switch(this.currentState()){case"root":return this.selector();case"for":case"selector":case"function":case"conditional":case"atblock":case"atrule":return this.property();default:var id=this.id();if("interpolation"==this.previousState())id.mixin=true;return id}}},interpolate:function(){var node,segs=[],star;star=this.accept("*");if(star)segs.push(new nodes.Literal("*"));while(true){if(this.accept("{")){this.state.push("interpolation");segs.push(this.expression());this.expect("}");this.state.pop()}else if(node=this.accept("-")){segs.push(new nodes.Literal("-"))}else if(node=this.accept("ident")){segs.push(node.val)}else{break}}if(!segs.length)this.expect("ident");return segs},property:function(){if(this.looksLikeSelector(true))return this.selector();var ident=this.interpolate(),prop=new nodes.Property(ident),ret=prop;this.accept("space");if(this.accept(":"))this.accept("space");this.state.push("property");this.inProperty=true;prop.expr=this.list();if(prop.expr.isEmpty)ret=ident[0];this.inProperty=false;this.allowPostfix=true;this.state.pop();this.accept(";");return ret},selector:function(){var arr,group=new nodes.Group,scope=this.selectorScope,isRoot="root"==this.currentState(),selector;do{this.accept("newline");arr=this.selectorParts();if(isRoot&&scope)arr.unshift(new nodes.Literal(scope+" "));if(arr.length){selector=new nodes.Selector(arr);selector.lineno=arr[0].lineno;selector.column=arr[0].column;group.push(selector)}}while(this.accept(",")||this.accept("newline"));if("selector-parts"==this.currentState())return group.nodes;this.state.push("selector");group.block=this.block(group);this.state.pop();return group},selectorParts:function(){var tok,arr=[];while(tok=this.selectorToken()){switch(tok.type){case"{":this.skipSpaces();var expr=this.expression();this.skipSpaces();this.expect("}");arr.push(expr);break;case this.prefix&&".":var literal=new nodes.Literal(tok.val+this.prefix);literal.prefixed=true;arr.push(literal);break;case"comment":break;case"color":case"unit":arr.push(new nodes.Literal(tok.val.raw));break;case"space":arr.push(new nodes.Literal(" "));break;case"function":arr.push(new nodes.Literal(tok.val.name+"("));break;case"ident":arr.push(new nodes.Literal(tok.val.name||tok.val.string));break;default:arr.push(new nodes.Literal(tok.val));if(tok.space)arr.push(new nodes.Literal(" "))}}return arr},assignment:function(){var op,node,name=this.id().name;if(op=this.accept("=")||this.accept("?=")||this.accept("+=")||this.accept("-=")||this.accept("*=")||this.accept("/=")||this.accept("%=")){this.state.push("assignment");var expr=this.list();if(expr.isEmpty)this.assignAtblock(expr);node=new nodes.Ident(name,expr);this.state.pop();switch(op.type){case"?=":var defined=new nodes.BinOp("is defined",node),lookup=new nodes.Ident(name);node=new nodes.Ternary(defined,lookup,node);break;case"+=":case"-=":case"*=":case"/=":case"%=":node.val=new nodes.BinOp(op.type[0],new nodes.Ident(name),expr);break}}return node},fun:function(){var parens=1,i=2,tok;out:while(tok=this.lookahead(i++)){switch(tok.type){case"function":case"(":++parens;break;case")":if(!--parens)break out;break;case"eos":this.error('failed to find closing paren ")"')}}switch(this.currentState()){case"expression":return this.functionCall();default:return this.looksLikeFunctionDefinition(i)?this.functionDefinition():this.expression()}},url:function(){this.expect("function");this.state.push("function arguments");var args=this.args();this.expect(")");this.state.pop();return new nodes.Call("url",args)},functionCall:function(){var withBlock=this.accept("+");if("url"==this.peek().val.name)return this.url();var name=this.expect("function").val.name;this.state.push("function arguments");this.parens++;var args=this.args();this.expect(")");this.parens--;this.state.pop();var call=new nodes.Call(name,args);if(withBlock){this.state.push("function");call.block=this.block(call);this.state.pop()}return call},functionDefinition:function(){var name=this.expect("function").val.name;this.state.push("function params");this.skipWhitespace();var params=this.params();this.skipWhitespace();this.expect(")");this.state.pop();this.state.push("function");var fn=new nodes.Function(name,params);fn.block=this.block(fn);this.state.pop();return new nodes.Ident(name,fn)},params:function(){var tok,node,params=new nodes.Params;while(tok=this.accept("ident")){this.accept("space");params.push(node=tok.val);if(this.accept("...")){node.rest=true}else if(this.accept("=")){node.val=this.expression()}this.skipWhitespace();this.accept(",");this.skipWhitespace()}return params},args:function(){var args=new nodes.Arguments,keyword;do{if("ident"==this.peek().type&&":"==this.lookahead(2).type){keyword=this.next().val.string;this.expect(":");args.map[keyword]=this.expression()}else{args.push(this.expression())}}while(this.accept(","));return args},list:function(){var node=this.expression();while(this.accept(",")){if(node.isList){list.push(this.expression())}else{var list=new nodes.Expression(true);list.push(node);list.push(this.expression());node=list}}return node},expression:function(){var node,expr=new nodes.Expression;this.state.push("expression");while(node=this.negation()){if(!node)this.error("unexpected token {peek} in expression");expr.push(node)}this.state.pop();if(expr.nodes.length){expr.lineno=expr.nodes[0].lineno;expr.column=expr.nodes[0].column}return expr},negation:function(){if(this.accept("not")){return new nodes.UnaryOp("!",this.negation())}return this.ternary()},ternary:function(){var node=this.logical();if(this.accept("?")){var trueExpr=this.expression();this.expect(":");var falseExpr=this.expression();node=new nodes.Ternary(node,trueExpr,falseExpr)}return node},logical:function(){var op,node=this.typecheck();while(op=this.accept("&&")||this.accept("||")){node=new nodes.BinOp(op.type,node,this.typecheck())}return node},typecheck:function(){var op,node=this.equality();while(op=this.accept("is a")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.equality());this.operand=false}return node},equality:function(){var op,node=this.inop();while(op=this.accept("==")||this.accept("!=")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.inop());this.operand=false}return node},inop:function(){var node=this.relational();while(this.accept("in")){this.operand=true;if(!node)this.error('illegal unary "in", missing left-hand operand');node=new nodes.BinOp("in",node,this.relational());this.operand=false}return node},relational:function(){var op,node=this.range();while(op=this.accept(">=")||this.accept("<=")||this.accept("<")||this.accept(">")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.range());this.operand=false}return node},range:function(){var op,node=this.additive();if(op=this.accept("...")||this.accept("..")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.val,node,this.additive());this.operand=false}return node},additive:function(){var op,node=this.multiplicative();while(op=this.accept("+")||this.accept("-")){this.operand=true;node=new nodes.BinOp(op.type,node,this.multiplicative());this.operand=false}return node},multiplicative:function(){var op,node=this.defined();while(op=this.accept("**")||this.accept("*")||this.accept("/")||this.accept("%")){this.operand=true;if("/"==op&&this.inProperty&&!this.parens){this.stash.push(new Token("literal",new nodes.Literal("/")));this.operand=false;return node}else{if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.defined());this.operand=false}}return node},defined:function(){var node=this.unary();if(this.accept("is defined")){if(!node)this.error('illegal unary "is defined", missing left-hand operand');node=new nodes.BinOp("is defined",node)}return node},unary:function(){var op,node;if(op=this.accept("!")||this.accept("~")||this.accept("+")||this.accept("-")){this.operand=true;node=this.unary();if(!node)this.error('illegal unary "'+op+'"');node=new nodes.UnaryOp(op.type,node);this.operand=false;return node}return this.subscript()},subscript:function(){var node=this.member(),id;while(this.accept("[")){node=new nodes.BinOp("[]",node,this.expression());this.expect("]")}if(this.accept("=")){node.op+="=";node.val=this.list();if(node.val.isEmpty)this.assignAtblock(node.val)}return node},member:function(){var node=this.primary();if(node){while(this.accept(".")){var id=new nodes.Ident(this.expect("ident").val.string);node=new nodes.Member(node,id)}this.skipSpaces();if(this.accept("=")){node.val=this.list();if(node.val.isEmpty)this.assignAtblock(node.val)}}return node},object:function(){var obj=new nodes.Object,id,val,comma;this.expect("{");this.skipWhitespace();while(!this.accept("}")){if(this.accept("comment")||this.accept("newline"))continue;if(!comma)this.accept(",");id=this.accept("ident")||this.accept("string");if(!id)this.error('expected "ident" or "string", got {peek}');id=id.val.hash;this.skipSpacesAndComments();this.expect(":");val=this.expression();obj.set(id,val);comma=this.accept(",");this.skipWhitespace()}return obj},primary:function(){var tok;this.skipSpaces();if(this.accept("(")){++this.parens;var expr=this.expression(),paren=this.expect(")");--this.parens;if(this.accept("%"))expr.push(new nodes.Ident("%"));tok=this.peek();if(!paren.space&&"ident"==tok.type&&~units.indexOf(tok.val.string)){expr.push(new nodes.Ident(tok.val.string));this.next()}return expr}tok=this.peek();switch(tok.type){case"null":case"unit":case"color":case"string":case"literal":case"boolean":case"comment":return this.next().val;case!this.cond&&"{":return this.object();case"atblock":return this.atblock();case"atrule":var id=new nodes.Ident(this.next().val);id.property=true;return id;case"ident":return this.ident();case"function":return tok.anonymous?this.functionDefinition():this.functionCall()}}}});require.register("renderer.js",function(module,exports,require){var Parser=require("./parser"),Evaluator=require("./visitor/evaluator"),Normalizer=require("./visitor/normalizer"),utils=require("./utils"),nodes=require("./nodes"),join=require("./path").join;module.exports=Renderer;function Renderer(str,options){options=options||{};options.globals=options.globals||{};options.functions=options.functions||{};options.use=options.use||[];options.use=Array.isArray(options.use)?options.use:[options.use];options.imports=[];options.paths=options.paths||[];options.filename=options.filename||"stylus";options.Evaluator=options.Evaluator||Evaluator;this.options=options;this.str=str}Renderer.prototype.render=function(fn){var parser=this.parser=new Parser(this.str,this.options);for(var i=0,len=this.options.use.length;i","+","~"];var SelectorParser=module.exports=function SelectorParser(str,stack,parts){this.str=str;this.stack=stack||[];this.parts=parts||[];this.pos=0;this.level=2;this.nested=true;this.ignore=false};SelectorParser.prototype.skip=function(len){this.str=this.str.substr(len);this.pos+=len};SelectorParser.prototype.skipSpaces=function(){while(" "==this.str[0])this.skip(1)};SelectorParser.prototype.advance=function(){return this.root()||this.relative()||this.escaped()||this.parent()||this.partial()||this.char()};SelectorParser.prototype.root=function(){if(!this.pos&&"/"==this.str[0]&&"deep"!=this.str.slice(1,5)){this.nested=false;this.skip(1)}};SelectorParser.prototype.relative=function(multi){if((!this.pos||multi)&&"../"==this.str.slice(0,3)){this.nested=false;this.skip(3);while(this.relative(true))this.level++;if(!this.raw){var ret=this.stack[this.stack.length-this.level];if(ret){return ret}else{this.ignore=true}}}};SelectorParser.prototype.escaped=function(){if("\\"==this.str[0]){var char=this.str[1];if("&"==char||"^"==char){this.skip(2);return char}}};SelectorParser.prototype.parent=function(){if("&"==this.str[0]){if(!this.pos&&(!this.stack.length||this.raw)){var i=0;while(" "==this.str[++i]);if(~COMBINATORS.indexOf(this.str[i])){this.skip(i+1);return}}this.nested=false;this.skip(1);if(!this.raw)return this.stack[this.stack.length-1]}};SelectorParser.prototype.partial=function(){if("^"==this.str[0]&&"["==this.str[1]){this.skip(2);this.skipSpaces();var ret=this.range();this.skipSpaces();if("]"!=this.str[0])return"^[";this.nested=false;this.skip(1);if(ret){return ret}else{this.ignore=true}}};SelectorParser.prototype.number=function(){var i=0,ret="";if("-"==this.str[i])ret+=this.str[i++];while(this.str.charCodeAt(i)>=48&&this.str.charCodeAt(i)<=57)ret+=this.str[i++];if(ret){this.skip(i);return Number(ret)}};SelectorParser.prototype.range=function(){var start=this.number(),ret;if(".."==this.str.slice(0,2)){this.skip(2);var end=this.number(),len=this.parts.length;if(start<0)start=len+start-1;if(end<0)end=len+end-1;if(start>end){var tmp=start;start=end;end=tmp}if(end-1){return n.toString().replace("0.",".")+type}}return(float?parseFloat(n.toFixed(15)):n).toString()+type};Compiler.prototype.visitGroup=function(group){var stack=this.keyframe?[]:this.stack,comma=this.compress?",":",\n";stack.push(group.nodes);if(group.block.hasProperties){var selectors=utils.compileSelectors.call(this,stack),len=selectors.length;if(len){if(this.keyframe)comma=this.compress?",":", ";for(var i=0;i200){throw new RangeError("Maximum stylus call stack size exceeded")}if("expression"==fn.nodeName)fn=fn.first;this.ret++;var args=this.visit(call.args);for(var key in args.map){args.map[key]=this.visit(args.map[key].clone())}this.ret--;if(fn.fn){ret=this.invokeBuiltin(fn.fn,args)}else if("function"==fn.nodeName){if(call.block)call.block=this.visit(call.block);ret=this.invokeFunction(fn,args,call.block)}this.calling.pop();this.ignoreColors=false;return ret};Evaluator.prototype.visitIdent=function(ident){var prop;if(ident.property){if(prop=this.lookupProperty(ident.name)){return this.visit(prop.expr.clone())}return nodes.nil}else if(ident.val.isNull){var val=this.lookup(ident.name);if(val&&ident.mixin)this.mixinNode(val);return val?this.visit(val):ident}else{this.ret++;ident.val=this.visit(ident.val);this.ret--;this.currentScope.add(ident);return ident.val}};Evaluator.prototype.visitBinOp=function(binop){if("is defined"==binop.op)return this.isDefined(binop.left);this.ret++;var op=binop.op,left=this.visit(binop.left),right="||"==op||"&&"==op?binop.right:this.visit(binop.right);var val=binop.val?this.visit(binop.val):null;this.ret--;try{return this.visit(left.operate(op,right,val))}catch(err){if("CoercionError"==err.name){switch(op){case"==":return nodes.no;case"!=":return nodes.yes}}throw err}};Evaluator.prototype.visitUnaryOp=function(unary){var op=unary.op,node=this.visit(unary.expr);if("!"!=op){node=node.first.clone();utils.assertType(node,"unit")}switch(op){case"-":node.val=-node.val;break;case"+":node.val=+node.val;break;case"~":node.val=~node.val;break;case"!":return node.toBoolean().negate()}return node};Evaluator.prototype.visitTernary=function(ternary){var ok=this.visit(ternary.cond).toBoolean();return ok.isTrue?this.visit(ternary.trueExpr):this.visit(ternary.falseExpr)};Evaluator.prototype.visitExpression=function(expr){for(var i=0,len=expr.nodes.length;i1){for(var i=0;i0&&!~part.indexOf("&")){part="/"+part}s=new nodes.Selector([new nodes.Literal(part)]);s.val=part;s.block=group.block;group.nodes[i++]=s}});stack.push(group.nodes);var selectors=utils.compileSelectors(stack,true);selectors.forEach(function(selector){map[selector]=map[selector]||[];map[selector].push(group)});this.extend(group,selectors);stack.pop();return group};Normalizer.prototype.visitFunction=function(){return nodes.nil};Normalizer.prototype.visitMedia=function(media){var medias=[],group=this.closestGroup(media.block),parent;function mergeQueries(block){block.nodes.forEach(function(node,i){switch(node.nodeName){case"media":node.val=media.val.merge(node.val);medias.push(node);block.nodes[i]=nodes.nil;break;case"block":mergeQueries(node);break;default:if(node.block&&node.block.nodes)mergeQueries(node.block)}})}mergeQueries(media.block);this.bubble(media);if(medias.length){medias.forEach(function(node){if(group){group.block.push(node)}else{this.root.nodes.splice(++this.rootIndex,0,node)}node=this.visit(node);parent=node.block.parent;if(node.bubbled&&(!group||"group"==parent.node.nodeName)){node.group.block=node.block.nodes[0].block;node.block.nodes[0]=node.group}},this)}return media};Normalizer.prototype.visitSupports=function(node){this.bubble(node);return node};Normalizer.prototype.visitAtrule=function(node){if(node.block)node.block=this.visit(node.block);return node};Normalizer.prototype.visitKeyframes=function(node){var frames=node.block.nodes.filter(function(frame){return frame.block&&frame.block.hasProperties});node.frames=frames.length;return node};Normalizer.prototype.visitImport=function(node){this.imports.push(node);return this.hoist?nodes.nil:node};Normalizer.prototype.visitCharset=function(node){this.charset=node;return this.hoist?nodes.nil:node};Normalizer.prototype.extend=function(group,selectors){var map=this.map,self=this,parent=this.closestGroup(group.block);group.extends.forEach(function(extend){var groups=map[extend.selector];if(!groups){if(extend.optional)return;var err=new Error('Failed to @extend "'+extend.selector+'"');err.lineno=extend.lineno;err.column=extend.column;throw err}selectors.forEach(function(selector){var node=new nodes.Selector;node.val=selector;node.inherits=false;groups.forEach(function(group){if(!parent||parent!=group)self.extend(group,selectors);group.push(node)})})});group.block=this.visit(group.block)}});return require("stylus")}(); diff --git a/lib/system-client.coffee b/lib/system-client.coffee new file mode 100644 index 0000000..338bcd9 --- /dev/null +++ b/lib/system-client.coffee @@ -0,0 +1,40 @@ +# system-client is what prepares the environment for user apps +# we hook up the postmaster and proxy messages to the OS +# we also provide system packages for the application to use like UI + +do -> + # NOTE: These required packages get populated from the parent package when building + # the runnable app. See util.coffee + Postmaster = require "_SYS_postmaster" + UI = require "_SYS_ui" + + style = document.createElement "style" + style.innerHTML = UI.Style.all + document.head.appendChild style + + postmaster = Postmaster() + + applicationProxy = new Proxy {}, + get: (target, property, receiver) -> + -> + postmaster.invokeRemote "application", property, arguments... + + document.addEventListener "mousedown", -> + applicationProxy.raiseToTop() + + systemProxy = new Proxy + Observable: UI.Observable + UI: UI + , + get: (target, property, receiver) -> + target[property] or + -> + postmaster.invokeRemote "system", property, arguments... + + # TODO: Also interesting would be to proxy observable arguments where we + # create the receiver on the opposite end of the membrane and pass messages + # back and forth like magic + + window.system = systemProxy + window.application = applicationProxy + window.postmaster = postmaster diff --git a/main.coffee b/main.coffee index 1d4c9c8..227c762 100644 --- a/main.coffee +++ b/main.coffee @@ -9,15 +9,37 @@ global.Hamlet = require "./lib/hamlet" System = require "./system" global.system = System() +system.PACKAGE = PACKAGE # For debugging {Style} = system.UI style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style +# Drag shenanigans +document.addEventListener "dragstart", -> + document.body.classList.add "drag-active" +document.addEventListener "mouseup", -> + document.body.classList.remove "drag-active" + # Desktop Explorer = require "./apps/explorer" document.body.appendChild Explorer() -# Launch Current Issue +VersionTemplate = require "./templates/version" +document.body.appendChild VersionTemplate + version: system.version + +SiteURLTemplate = require "./templates/site-url" +document.body.appendChild SiteURLTemplate() + +HomeButton = require "./presenters/home-button" +document.body.appendChild HomeButton() + +system.writeFile "feedback.exe", new Blob [""], type: "application/exe" +system.writeFile "My Briefcase", new Blob [""], type: "application/briefcase" + +system.autoboot() +# system.dumpModules() + require("./issues/2017-02")() diff --git a/os/file-io.coffee b/os/file-io.coffee index 3f32d70..9bf2453 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -7,53 +7,67 @@ # `newFile` Initialize the application to an empty state. module.exports = (I, self) -> + {Observable} = system {Modal} = system.UI - currentPath = "" - # TODO: Update saved to be false when model changes - saved = true + currentPath = Observable "" + saved = Observable true + + confirmUnsaved = -> + return Promise.resolve() if saved() + + new Promise (resolve, reject) -> + Modal.confirm "You will lose unsaved progress, continue?" + .then (result) -> + if result + resolve() + else + reject() self.extend + currentPath: currentPath + saved: saved new: -> - if saved - currentPath = "" + if saved() + currentPath "" self.newFile() else - Modal.confirm "You will lose unsaved progress, continue?" - .then (result) -> - if result - saved = true - self.newFile() + confirmUnsaved() + .then -> + saved true + self.newFile() open: -> - # TODO: Prompt if unsaved - # TODO: File browser - Modal.prompt "File Path", currentPath - .then (newPath) -> - if newPath - currentPath = newPath - else - throw new Error "No path given" - .then (path) -> - system.readFile path, true - .then (file) -> - self.loadFile file + confirmUnsaved() + .then -> + # TODO: File browser + Modal.prompt "File Path", currentPath() + .then (newPath) -> + if newPath + currentPath newPath + else + throw new Error "No path given" + .then (path) -> + system.readFile path, true + .then (file) -> + self.loadFile file save: -> - if currentPath + if currentPath() self.saveData() .then (blob) -> - system.writeFile currentPath, blob, true + system.writeFile currentPath(), blob, true .then -> - currentPath + saved true + currentPath() else self.saveAs() saveAs: -> - Modal.prompt "File Path", currentPath + Modal.prompt "File Path", currentPath() .then (path) -> if path - currentPath = path + currentPath path self.save() return self diff --git a/pixie.cson b/pixie.cson index 3d9e471..282d8c6 100644 --- a/pixie.cson +++ b/pixie.cson @@ -1,14 +1,20 @@ +title: "Whimsy Space - ZineOS v0.420.950a ⌊ALPHA⌉" +description: """ + When the bombs drop, and it's OS against OS, you're gonna wish you could view source +""" dependencies: ajax: "distri/ajax:master" analytics: "distri/google-analytics:master" + base64: "distri/base64:v0.9.2" bindable: "distri/bindable:master" model: "distri/model:master" - postmaster: "distri/postmaster:v0.5.0" - ui: "STRd6/ui:master" + postmaster: "distri/postmaster:v0.5.3" + ui: "STRd6/ui:v0.1.9" remoteDependencies: [ "https://cdnjs.cloudflare.com/ajax/libs/dexie/2.0.0-beta.7/dexie.min.js" "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" "https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js" "https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js" + "https://sdk.amazonaws.com/js/aws-sdk-2.7.20.min.js" ] diff --git a/presenters/home-button.coffee b/presenters/home-button.coffee new file mode 100644 index 0000000..3b59a37 --- /dev/null +++ b/presenters/home-button.coffee @@ -0,0 +1,112 @@ +AceEditor = require "../apps/text-editor" +AchievementStatus = require "../apps/achievement-status" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +DungeonOfSadness = require "../apps/dungeon-of-sadness" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +MyBriefcase = require "../apps/my-briefcase" + +HomeButtonTemplate = require "../templates/home-button" + +module.exports = -> + {Achievement} = system + {ContextMenu, Util:{parseMenu}} = system.UI + + launch = (App) -> + app = App() + system.attachApplication app + + contextMenu = ContextMenu + items: parseMenu """ + 🔨 [A]pplications + 📝 [A]ce Editor + 🍷 [C]hateau + 🎨 [P]ixie Paint + 🎮 [G]ames + 🌭 [B]ionic Hotdog + 🍖 [C]ontrasaurus + 😭 [D]ungeon Of Sadness + 📰 [I]ssues + 1️⃣ [F]irst + 🏰 [E]nter The Dungeon + 🏬 [A]TTN: K-Mart Shoppers + 💃 [D]isco Tech + 🌻 [A] May Zine + ⚙️ [S]ettings + 📱 [A]ppearance + 💯 [C]heevos + 💼 [M]y Briefcase + - + 🔌 S[h]ut Down + """ + handlers: + aceEditor: -> + launch AceEditor + + aMayZine: -> + system.launchIssue("2017-05") + + appearance: -> + system.UI.Modal.alert "TODO :)" + + aTTNKMartShoppers: -> + system.launchIssue("2017-03") + + bionicHotdog: -> + Promise.resolve + src: "https://danielx.net/grappl3r/" + width: 960 + height: 540 + iconEmoji: "🌭" + title: "Bionic Hotdog" + .then system.iframeApp + .then system.attachApplication + + chateau: -> + launch Chateau + + cheevos: -> + launch AchievementStatus + + contrasaurus: -> + launch Contrasaurus + + discoTech: -> + system.launchIssue("2017-04") + + dungeonOfSadness: -> + launch DungeonOfSadness + + enterTheDungeon: -> + system.launchIssue("2017-02") + + "1First": -> + system.launchIssue("2016-12") + + myBriefcase: -> + launch MyBriefcase + + pixiePaint: -> + launch PixiePaint + + shutDown: -> + Achievement.unlock "Shut Down" + system.UI.Modal.alert "You'll never shut us down! ZineOS 5ever!" + + updateStyle = -> + height = element.getBoundingClientRect().height + + contextMenu.element.style.fontSize = "1.5rem" + contextMenu.element.style.lineHeight = "1.5" + contextMenu.element.style.bottom = "#{height}px" + contextMenu.element.style.textAlign = "left" + + element = HomeButtonTemplate + click: -> + contextMenu.display + inElement: document.body + + updateStyle() + + return element diff --git a/social/social.coffee b/social/social.coffee index 1fa8ae8..2ea5c9c 100644 --- a/social/social.coffee +++ b/social/social.coffee @@ -14,7 +14,7 @@ module.exports = (I, self) -> area: self.area() .then (data) -> ajax - url: "https://whimsy-space.gomix.me/comments" + url: "https://whimsy-space.glitch.me/comments" data: JSON.stringify(data) headers: "Content-Type": "application/json" @@ -23,7 +23,7 @@ module.exports = (I, self) -> self.viewComments() viewComments: -> - ajax.getJSON "https://whimsy-space.gomix.me/comments/#{self.area()}" + ajax.getJSON "https://whimsy-space.glitch.me/comments/#{self.area()}" .then (data) -> data = data.reverse() @@ -37,7 +37,7 @@ module.exports = (I, self) -> like: -> system.Achievement.unlock "Do you 'like' like me?" - Modal.alert "I like you too, but we don't have a facebook or anything yet :)" + window.open "https://www.facebook.com/whimsyspace/" subscribe: -> require("../mailchimp").show() diff --git a/stories/blue-light-special.coffee b/stories/blue-light-special.coffee new file mode 100644 index 0000000..dc7a15a --- /dev/null +++ b/stories/blue-light-special.coffee @@ -0,0 +1,96 @@ +module.exports = """ +Blue Light Special + +“You know, I really want some cookies.” Elo said. She sat on a cushion on top of a tarp spread on the ground in front of their small campfire and whittled a piece of wood, periodically tossing the shavings into the fire. The warm glow bathed her deft fingers, tired face, and honey-colored hair with a soft light, and faintly illuminated the oak trees around them in the pressing darkness. + +“We can break camp tomorrow, move on, and see what we can find on the way,” Jib replied, as he munched some popcorn and scratched his dark beard. He scooted his log seat closer to the fire and buttoned his shearling jacket to keep out the fine mist rolling in among the trees. + +“I don’t want to break camp,” Elo protested. “I like it here. We should stay a while. I just want to have some cookies.” She looked around their camp: at Jib on his stump; at Poli beside her on the tarp, seated on her own cushion; at their reliable and well-used tent; at the horses draped with their blankets; at the clean dishes drying fireside. This was a good place. She rubbed her ever-growing abdomen. The tiny person ensconced within dragged a heel over her insides in response. + +“Okay, we won’t break camp, then. We can just go to town tomorrow. We need some other supplies anyway.” Jib raised his lanky body to pass the popcorn to Poli, who took the enameled bowl and placed it between her and Elo. + +Poli offered the popcorn to Elo. “Here, have some popcorn. That will help take your mind off the craving.” + +Elo knitted her brows. “No, it won’t. It’s not the same and you know it.” + +“Yeah, okay. I know. It’s not. But it’s what we have,” Poli smiled gently, hoping to diffuse the situation. + +“Why can’t you just go get some now? We didn’t camp that far from town, and it’s not that late,” Elo pleaded. + +“Because it’s going to rain soon, and because I’m tired, and because we decide things together, not based on whim,” Jib retorted grumpily. + +“Pleeeeeease! I really, really want them. I’m tired too and my back hurts from hauling this baby around and I just want to have some cookies. Is that too much to ask?” Elo burst into tears. + +“Elo, please don’t cry.” Poli’s red curls bounced as she snapped her head up to glare at Jib. She enfolded Elo in a hug, and Elo sobbed harder on the petite woman’s shoulder. + +“Poli, come on,” Jib sighed. “Get Elo settled in the tent and you and I will go tonight. Let’s get moving so we’re not caught in too much rain on the way back.” + +** + +Gabe sat on a cold concrete bench at a cold, round, concrete table, eating cold pizza under a murky and unforgiving sky in which clouds entirely obscured the stars, and in which mist threatened to give way to outright rain. He reflected miserably that of all the ways to eat pizza, this was probably the second-worst. The worst would be back in the break room with the other Kmart employees, where they would have excited yet hushed conversations that did not include him, the night manager. + +The table he was sitting at had recently had a fiberglass umbrella to provide meager protection against the elements. Last week, however, seven drunk teenagers had removed it to use as a boat in a very large puddle at the back of the parking lot and it had not been seen since. The sky began to spit at him, and he thought, “At least the sky is doing what they’re all thinking.” + +Gabe looked up the nearly deserted road and noticed two large figures with an odd gait. They resolved into a tall, bearded man and a petite, redheaded woman, each on horseback, emerging from the mist and entering the Kmart parking lot. Amused, Gabe wondered whether the pair might tie their horses up to the bike racks, or find a parking space near a lamp post and hitch them there, or tether them to a tree. + +The man and woman did none of these things. They simply rode the horses through the automatic doors and into the Kmart. Gabe took a moment to process this. He started to get up, looked at his phone, noticed ten minutes remained of his break, and sat back down. + +** + +Poli dismounted inside the doors and hitched up a shopping cart to her horse. She nimbly saddled up again, and she and Jib then began perusing the aisles of the Kmart and checking the items off their list. + +“Sir? Miss? Can I help you?” an employee demanded tersely, body language suggesting the only help would be toward an exit. + +“Yes, thank you. We are looking for cookies such as would satisfy a pregnancy craving,” Jib replied. He placed some batteries in their cart. + +“Cookies? Aisle 12A,” the employee responded automatically, then shook her head. “The horses. You can’t have them in here. You’ll have to take them outside.” The employee was joined by two others, all of them looking simultaneously stern and bewildered. + +They reached the aisle’s endcap. “Ooh, look, nut mixes are on sale,” Poli grinned. + +“Go ahead and get a few,” Jib called to her as he rode ahead to select some prenatal vitamins and toothpaste. + +“Miss, please. The two of you need to take the horses outside,” the employee wheedled. As if to respond, Poli’s horse lifted its tail and dropped a steaming load near the employee’s boot. The employee leapt back, dismayed. + +“Oh, no, I’m afraid that won’t be possible. These are our service animals,” Poli chirped sweetly. + +The employees huddled together. “Where’s Gabe?” they worried. They barked into walkie-talkies to Gabe to come over here RIGHT NOW and received no response. + +Poli caught up with Jib and together they loaded dry beans and rice into the cart. Suddenly, a voice spoke, as if from the heavens, and a flashing blue light punctuated the announcement: + +“Attention, Kmart shoppers! Blue light special on Aisle 12A. Tedwich Sandwich Cookies, buy two, get one free!” + +“We have to get those for Elo. I bet they’ll be perfect!” Poli shouted to Jib, and she urged her horse forward. Jib rode after her, followed by the three ineffectual employees, and an ever-growing crowd of onlookers. + +Aisle 12A displayed an alarming variety of cookies, snack cakes, and other treats, and Poli and Jib were, for the first time, ill at ease in the Kmart. A small girl with braids and cutoff jeans approached them. + +“Here you go,” she smiled shyly, handing up a box. + +“Thank you,” Jib smiled back. He tore into the box and passed a handful of the Tedwich cookies to Poli. They sampled the miniature bear-shaped cinnamon cookies sandwiching a vanilla cream filling. They were, indeed, exactly what Elo would love. + +“Can you help us load our cart with every box there is?” Poli asked. + +“Not every box. I want to have three for myself,” the girl quickly replied. + +“Done.” + +With all but three boxes of Tedwich cookies in their cart, the only thing left was to find some ice cream to go with them. This expedition caused no small amount of chaos in the freezer case aisle when a small, yappy dog in a handbag spooked the horses, which in turn sent the crowd scattering in all directions. Fortunately, it also resulted in Jib’s horse being perfectly positioned in front of a case containing pint after pint of ice cream. They selected one of chocolate and one of vanilla, and Jib and Poli rode to the checkout. + +Poli pulled a small wooden box from her canvas messenger bag and handed it down to the cashier. “This is a very rare and ancient turtle, carved from an unknown precious stone to bring the bearer untold riches. It is more than enough payment for these goods in our cart.” + +Poli and Jib rode out of the store, cart still in tow, as Gabe came back in. The two riders disappeared into the night. + +The employees surrounded him. Gabe said nothing. He strolled through the store, surveying the relative lack of damage, the confused shoppers, and the piles of manure, and he grinned as he reached for his walkie-talkie. + +“Cleanup on Aisles 6, 7, 10, and 12. Better get a shovel.” + +** + +Elo drifted to sleep that night, completely content, filled with cookies and ice cream. She dreamed of a girl with braids and cutoff jeans, holding boxes and boxes of Tedwich cookies stacked higher than she was tall. She flew above the girl as a deep blue light bathed them both. + +“What is your name?” Elo asked. + +“Theodosia,” the girl answered. The girl flew up to meet Elo and they soared effortlessly through the ceiling and out into the mist. + +Three months later, in the middle of another rainy night, Elo gave birth to a baby girl. The child was dusted with cinnamon freckles and looked up at Elo with the bluest eyes she had ever seen. She named the girl Theodosia. + +""" \ No newline at end of file diff --git a/stories/crescent.coffee b/stories/crescent.coffee new file mode 100644 index 0000000..799d3ef --- /dev/null +++ b/stories/crescent.coffee @@ -0,0 +1,13 @@ +module.exports = """ +“Look, Mom, I made a moon!” +She holds aloft a crescent bitten from cereal, +and I feel a tug on my memory: +not the usual familiarity +of the Earth’s twirling, ever-changing little sister +but something deeper, and more intimate, +like a great truth, +a resonance of great importance, +had been uncovered within my mind as I slept, +and was lost upon waking. +And then she ate the moon. +""" diff --git a/stories/dungeon-dog.coffee b/stories/dungeon-dog.coffee index 6d7ec67..7607400 100644 --- a/stories/dungeon-dog.coffee +++ b/stories/dungeon-dog.coffee @@ -3,7 +3,7 @@ Dungeon Crawl with Cliffford! One sunny day, Cliffford the big rad dog was playing at the park with his friends U-Bone and Cloe. They chased each other through the playground, around the play structures, and through the sand. Children squealed with delight to see Cliffford jump right over the swing set in his excitement. “He’s so big! He can jump over things! Heehee!” they giggled. THUMP! went Cliffford’s paws as he landed on the grassy field, and Cloe and U-Bone followed right behind. -The dogs dashed through a soccer game, U-Bone stopping to bite at the ball, and Cleo weaving her way through at least four players’ legs. They emerged on the other side of the field at the northernmost edge of the park, where the well-kept field gave way to a little meadow, and then the woods. +The dogs dashed through a soccer game, U-Bone stopping to bite at the ball, and Cleo weaving her way through at least four players’ legs. They emerged on the other side of the field at the northernmost edge of the park, where the well-kept field gave way to a little meadow, and then the woods. Cliffford flopped down in a shady spot, and Cloe and U-Bone stood panting next to him. Cool breezes blew in from the ocean, rippling the leaves on the trees and the long meadow-grass. @@ -53,7 +53,7 @@ Something else glinted in the darkness just beyond them. Cloe moved closer and t U-Bone stumbled upon a sheath and baldric for the sword a short distance away, and strapped them on. BOSS! A few turns of the path later, and the dogs found another staircase down. Come on, it’s a dungeon crawl. Of course they found another staircase! -The dogs explored further and deeper into the dungeon. Cliffford’s mouth was crammed as full as possible with bones, and he looked sadly at each one he had to leave behind as they pressed on. +The dogs explored further and deeper into the dungeon. Cliffford’s mouth was crammed as full as possible with bones, and he looked sadly at each one he had to leave behind as they pressed on. “Humuhlauhsunrrmmmuh,” said the big rad dog, furrowing his brows. “Hmm?” asked Cloe, through the side of her mouth around the torch. @@ -90,7 +90,7 @@ Cloe whipped her head back and forth, and the scroll unfurled, revealing the tex A moment later, Cliffford was back up again, only now he was the size of a normal dog. Yup! -This was a very useful development for our plot, because they heard shouts from the top of the stairs. The angry lake-keeper’s scratchy toenails clicked on the stone treads as he skittered down toward them. +This was a very useful development for our plot, because they heard shouts from the top of the stairs. The angry lake-keeper’s scratchy toenails clicked on the stone treads as he skittered down toward them. The dogs wasted no time, and all three dove right into the entrance to the labyrinth. They tried their best to stay together, but Clifford, unused to being tiny, lagged behind. He felt weak, and needed food badly. diff --git a/stories/izzy.coffee b/stories/izzy.coffee new file mode 100644 index 0000000..83265f1 --- /dev/null +++ b/stories/izzy.coffee @@ -0,0 +1,61 @@ +module.exports = """ +“Izzy, your grandma is here!” Izzy jumped up from where she was seated on the classroom floor chatting with her friends during after-school care, and she ran to her grandmother to give her a huge hug. + +“Have a good weekend,” her teacher said as Izzy grabbed her backpack. +“You too!” Izzy called, as she ran for the parking lot with Grandma close behind. She nearly flew through the school gate. + +“Wait for me before you cross the street!” Grandma shouted to Izzy. +“Sorry,” Izzy replied as Grandma caught up to her. + +They crossed over to where Grandpa was leaning against their 1973 El Camino. He swung Izzy up and spun her around. + +“Did you have a good day, kiddo?” +“Yeah! I finished The BFG and I got to draw my favorite part as a poster and share it with the class,” Izzy said proudly. +“Sounds great. Did you try to draw in the style of Quentin Blake, or did you use your own style?” Grandpa asked. +“Well, I tried pretty hard to draw like Quentin Blake, but it’s hard to draw like other artists. I didn’t like how it looked so I started over by just reading the description again and imagining it in my head. I’ll show you next week when I bring it home.” + +Izzy lowered her backpack into a wooden orange crate in the truck bed of the El Camino. Her navy-blue violin case, a small amplifier, and a black bag already sat in the crate. The crate nestled in between a variety of potted plants which took up the remaining space in the bed. Izzy plucked a few ripe cherry tomatoes and some snow peas from their respective vines, which twined around cages in a large planter close to the cab’s rear window. She ran her fingers through the thyme and mint spilling out of smaller planters closer to the tailgate and breathed in, relishing the pungent and spicy smells. She loved her grandparents’ mobile garden. + +She munched her snack as she climbed into the front seat of the El Camino and slid in next to Grandma, who sat in the driver’s seat. Grandpa placed three garment bags on Izzy’s lap, and then slid in beside her, sandwiching Izzy in between her grandparents. + +“Be careful not to get any tomato juice on the garment bags,” Grandpa cautioned. +“I know. I’ll be careful.” Izzy replied. +“All right. Sorry it’s such a tight fit today!” Grandpa smiled at Izzy. “Are you excited?” +“Yeah! I can’t wait until we get to the boardwalk! Everyone is going to love our show!” +“Well, then!” Grandma exclaimed. “Let’s go!” + +The El Camino’s engine roared to life and Grandma maneuvered the car onto the road. With both windows down, the breeze ruffled everyone’s hair, and as they neared their destination, the fresh scent of the ocean wafted in, filling Izzy’s nostrils with the salty, damp aroma. The plants in the back swayed gently back and forth in time with the rhythm of the car’s motion, and their planters were so tightly arranged that nothing slid around. + +Grandma pulled into a perfect parking space close to the boardwalk at the beachfront parking lot. Grandpa carried the garment bags; Izzy carried her backpack and violin, and Grandma carried the black bag and the amplifier. + +“Nice car!” a voice called to them. They turned to see a young man and woman wearing wetsuits and loading surfboards into the back of their SUV. +“Thanks!” Grandpa called back. The two surfers smiled at them. Izzy glowed. She loved the way her grandparents brought joy everywhere they went. + +The sun was beginning to set over the ocean, creating an air of magic. Izzy and her grandparents walked the short distance to their pre-chosen destination: a bench that overlooked the water, adjoining a large, wide sidewalk upon which Friday night foot traffic was beginning to accumulate. + +A nearby planter contained palm trees and succulents, as well as a small electrical outlet on one side. Grandma pulled an orange extension cord from her black bag, and plugged it in. She carefully duct-taped the cord to the ground for safety as she ran the cord up to the bench. Izzy plugged her amplifier in the extension cord and began setting up her equipment. + +“I’m going to go get changed,” said Grandpa to Grandma, taking his garment bag and walking in the direction of the restrooms. As Izzy began tuning her violin a few minutes later, Grandpa appeared wearing a leisure suit that had a complicated design made of conductive fabric circles and lines stitched together in that turned his costume into a drum pad, which connected wirelessly with the amplifier. + +The equipment setup complete, Grandpa began do a final check of his suit while Izzy and Grandma took their turn to change as well. A short time later, they emerged, resplendent in their own costumes. + +Grandma wore a long-sleeved, tight-fitting silver dress that had fiber optic filaments running vertically along her body and dangling beyond the bottom of the dress. She had not yet switched on the battery pack, and the dress appeared at first to be rather unassuming. However, the headdress she was wearing was quite the opposite. Nestled in Grandma’s big hairdo was a variety of feathers, delicately coiled wire shapes, and more fiber optics that skimmed the top of her head before shooting up like the crest of a rainforest bird. Dramatic, dark eyeliner, silver eye shadow, and silver platform shoes completed the look. + +The simple cut of Izzy’s dress accentuated the beautiful movement of the fabric. A fitted bodice gave way to a flared skirt. She twirled, and the purple and blue dip-dyed fabric swirled in response around her knees. Opting to keep her costume simple for ease of playing her violin, it had no electronics. The bodice was adorned with sequins she had painstakingly glued in an intricate pattern, which, like a disco ball, would reflect the lights from the small footlight set up next to her. In Izzy’s hair was a small feather and fiber optic fascinator, like Grandma’s but on a smaller scale. + +Izzy did a quick, final sound check. “Ready?” she asked. +“Let’s do it!” grinned Grandma and Grandpa. + +Months of effort coalesced into one moment as the sun finally dipped below the horizon, sending the boardwalk into twilight. Grandma and Grandpa switched on their battery packs, and Grandma’s dress lit up. Izzy switched on her footlight, nodded to Grandma and Grandpa, and began to play. + +Inspired by her hero Zoë Keating, Izzy began to loop snippets of her violin with her loop pedal. She built sound after sound up to a fast-paced, catchy, danceable beat, while Grandpa and Grandma began to dance. Grandma’s fiber optics began to pulse with the rhythm of Izzy’s music and Grandpa’s drum pad leisure suit. They whirled and spun, and even Izzy swayed and swirled with the music as she played. + +A crowd began to form, cheering on the pop-up discotheque. They clapped in time to the music, delighted every time Grandma’s skirt sent pinpricks of light racing around her knees, and murmured in excitement at the intricate movements Grandpa made to keep the drums thumping. As the song ended, the crowd exploded with whoops and shouts, and several people made donations into Izzy’s violin case. The family took a bow, and launched into another song, causing the crowd to cheer yet again. + +Like any moment that has been anticipated for so long, it seemed to last but the blink of an eye. When the last song ended, the crowd drifted away on the final, lingering notes from Izzy’s violin. As Grandma and Grandpa started to pack up, Izzy took a dime out of the violin case, her favorite coin because it was the smallest, and darted to the fountain down the boardwalk. She pitched the coin into the dark water that reflected the stars. + +“What did you wish for?” Grandpa asked as Izzy returned. +“Not telling,” Izzy replied. + +She slipped her small hand into Grandpa’s, and the other into Grandma’s, and they walked back to the El Camino. +""" \ No newline at end of file diff --git a/stories/marigold.coffee b/stories/marigold.coffee new file mode 100644 index 0000000..41699c4 --- /dev/null +++ b/stories/marigold.coffee @@ -0,0 +1,87 @@ +module.exports = """ +The shutters of a third-story window of the manor house flew open, and a small girl leaned out, hurling a rope to the ground below. She lugged a chair over to the window, and stood on it. She grasped the rope in both hands, and began slowly descending the pitted grey stone walls of her home. Evenly spaced knots helped her tiny hands keep hold of the thick, scratchy rope as she leaned her slender frame away for balance. + +In the garden below, the girl brushed rope fibers from her hands on her leggings and smoothed out her tunic. A gust of wind whipped her black hair about her face, and she wrapped her cloak closer about her body and pulled its hood over her head. + +The waxing crescent moon, partially obscured by clouds, gave little light, but the girl needed no lantern. She blinked her golden eyes rapidly a few times, letting her cat-like pupils widen enough to collect the scant light, and took in her surroundings in shades of grey. She marched off into the darkness of the nearby woods, hidden under the cover of darkness. + +The girl found her favorite deer trail through the forest, almost invisible to anyone else. It wound around the ancient oak trees before descending along a sandy bank to the edge of a softly flowing river. Downstream, the rapids became rough, but here, where rocks were few and the ground was mostly level, the water pooled and was perfect for swimming. + +Despite the chill of the evening, the girl removed her boots and garments and folded them neatly, placing them on a flat rock several feet from the edge of the river. She stood expectantly, letting the water lap at her toes. + +She looked up as a swirling cloud emerged from a hollow tree and collected above her head. She whistled a haunting little tune and the cloud parted, wrapping itself around her body as she stepped into the water. She waded waist-deep into the water as the nanoswarm enveloped her in scaly, flexible skin from her torso all the way down her legs, ending in a tail with long fins. Opening her mouth, she inhaled some of the nanoswarm to allow her to breathe underwater, before her body slipped entirely under the rippling surface. + +The girl splashed her tail and a peal of light, uninhibited laughter echoed off the rocks as she resurfaced. She swam off into the night. + +*** + +“Lady Marigold?” A young woman, shivering in the predawn air, gently shook the girl’s shoulder to rouse her. The girl, Marigold, blinked her eyes and looked to find her lady’s maid standing over her with a look of concern. Marigold lay on the sandy shore where she had fallen asleep hours before. + +“Hm? Oh, no. I’m so sorry, Raina! I didn’t mean to fall asleep!” she said, brushing sand from her face as she sat up. She was still wrapped in her nanoswarm, so she whistled to them again, and they lifted away from her body and back into their hiding place in the tree. Raina retrieved the neatly folded clothing and helped her lady dress quickly. + +“My lady, if you must escape the house at night, please return in a timely manner. Surely you can program your nanoswarm to remind you to come home?” +“I can, but it is awkward to swim to shore with legs if the nanoswarm leaves me when I am not yet ready. I much prefer the tail.” + +Raina huffed in exasperation and said nothing further. She tamed Marigold’s long hair in a quick series of braids, which she wound around the girl’s head and pinned into place. + +Marigold turned to leave, but Raina said, “Your eyes, Lady Marigold.” Marigold paused, closed her eyes, and opened them again. Gone were the cat’s eyes, replaced by dark brown eyes with the usual round pupils one would expect to see in a girl five years of age. + +Raina followed her lady up the bank and back into the forest. They returned to the manor house quickly and quietly. Raina, feeling that scaling the walls of the house was undignified, had already wound Marigold’s rope back up and closed the shutters. Instead, the two of them tiptoed through the kitchen. The cook, singing loudly to herself, kneaded the day’s bread and did not hear their entrance. + +Back in Marigold’s rooms, Raina started a fire. “I am not cold,” Marigold protested. “The nanoswarm helps regulate my body temperature.” +“Very well, but I have no such protection and I, too, have also been out in the cold this morning.” Raina continued building the fire until it roared in the fireplace. + +Marigold disappeared into her library, a fortress of shelves she had arranged in a corner of the room adjacent to the fireplace. Books of all shapes and sizes and subjects, borrowed from the big library downstairs, whispered to her their secrets of ancient wars and mythology, poetry and architecture. Soft cushions stacked on a rug served as her seat, and Marigold resumed reading A Brief History of Nanoculture. + +Breakfast arrived on a tray, which Raina placed next to Marigold on the floor. The girl held the book in one hand, and ate her toast and jam and drank her tea with the other, her eyes never leaving the pages. + +A knock at the door yielded a sealed note, which, though it was addressed to Raina, was delivered to Marigold. The girl looked over the note, scowled, and snapped the book shut, mentally noting the page number. + +“My uncle has summoned me,” Marigold said to Raina, who nodded in response. “I do wish that man would write to me directly.” She read the note aloud, rolling her eyes: “‘Send the Lady Marigold to me at half-past nine and ensure the child wears something befitting her station.’ He knows I can read. Why does he torment me thus?” + +“Your uncle is a good man,” Raina soothed, “He has many responsibilities, and has better things to occupy his time than chastising you for coming to an audience with him dressed for a day out in the woods.” +“Formal clothing is pointless. My appearance is arbitrary. I am five, and therefore invisible anyway. I prefer being able to move freely.” + +Marigold flounced to a small adjoining room where her clothing was stored, and sullenly pulled a long dress from a drawer. Light, soft grey fabric draped over her head as she put the garment on. Long, tight-fitting sleeves with inlaid lace covered her arms, and the hem of the dress, which brushed the tops of her toes, featured more of the same lace. Over the dress, she put on a long, intricately embroidered vest, and Raina helped her cinch her waist with a belt the width of her hand, encrusted with beads made of semiprecious stones. Marigold slipped her feet into soft, ghillie-style shoes made of fine leather that laced up her legs. + +They sat at Marigold’s dressing table and Raina brushed Marigold’s hair before smoothing it into a full, low roll at the back of her head, pinning smaller braids around it, and placing a gold hair comb into the top of the roll. Raina carefully adjusted the small curls around Marigold’s face, and turned the girl to face her. + +“You look lovely,” Raina smiled. +“I still prefer the tunic and leggings, but thank you.” + +Marigold exited her rooms, followed by Raina. They descended the staircase at the end of the hall, and proceeded to the library, where Marigold’s uncle usually received her. Raina knocked, and entered. + +“Lady Marigold Lavande, Duchess of Frisa.” Raina announced her lady as she slowly inclined her head, a sign of respect for the child’s uncle and guardian, Lord Therus Osmarinus, a duke in his own right of the neighboring county of Astera. + +Marigold swept past Raina, stopping before her uncle and inclining her head to him. He returned the gesture of respect and motioned to a chair near the table upon which his correspondence was spread before him. Raina left the room, and Marigold sat. + +“Good morning, Marigold.” Her uncle’s green eyes were kind, but she could see bags under them. He probably had not slept much the previous evening. He sat at the table opposite her. + +“Good morning, Uncle.” From her perch on the chair, she could just see the letters he had been reading. She strained her eyes to see the details. She noted the seal of the Ruling Council affixed to several of the letters, and the swooping signatures of several different people. She also noted a map of the country he had pulled up on a tablet, with a variety of location markers scattered near the coast. + +“What is the map for, Uncle?” Marigold asked. + +Her uncle ran his fingers through his short, brown hair, speckled through with grey. He sighed. “There is some unrest along the coast. We have been summoned to the Palace of the Ruling Council for a meeting.” +“We?” Marigold raised her eyebrows. “We are never summoned. You are summoned, and I remain here and you act on my behalf.” She began to feel agitated. She liked her home, and leaving it always made her feel uneasy. +“In general, yes, but the Ruling Council specifically requested your presence this time.” Her uncle gestured toward the fireplace where a man and a woman stood, watching them. + +He introduced them as Lord and Lady Sicale, and explained that he must accompany them now to the Palace, and that Marigold would follow along as soon as possible. Marigold gathered that the summons had been delivered in person, and must be quite delicate and urgent. + +“I leave with Lord and Lady Sicale within the hour. I have already sent instructions to Raina to begin packing your things. I shall see you soon.” + +He dismissed her with a nod of his head, seeming to want to say more, but refraining. Marigold went back to her rooms. + +*** + +That evening, Marigold lay awake in bed. The day’s flurry of activity, rather than tiring her out, had set her brain tingling. Raina and the other household staff had seen to the packing of her clothing, books to study, and other personal effects. She had been left to her own devices to pack a small bag full of whatever she wished to take personally, which consisted largely of the books she was reading for pleasure, a sketchbook, some colored inks, and a variety of colored and graphite pencils. She had nearly everything, but one thing remained she wanted to take. + +She rolled over and over, pondering, before finally sitting bolt upright, coming to a decision. Flickering her eyes to golden cat-eyes, she slipped out of bed and put on her tunic and leggings, boots and cloak. She took a tiny glass vial from her dressing table. It fit in the palm of her hand. The vial had a narrow mouth and neck, with a very small silicone stopper in it, and a bulbous base. Delicate filigree metalwork around the neck sported a loop through which she threaded a fine silver chain. She put the chain around her neck and hid it under her tunic. + +As Marigold had done the previous night, she flung open the window, and let herself down silently with the rope. She flew over the deer trail to her secluded riverside swimming spot, where she whistled a slightly different nanotune than the night before. The swarm came out of its hiding place in the hollow tree. Instead of swirling around her and transforming her lower body into a sleek, aquatic creature, the nanoswarm poured into the vial around her neck, which she held aloft. + +Marigold replaced the stopper in the vial and hurried back to her room. The implications of bringing the nanoswarm to the Palace could be disastrous. Aside from her own swarm, nanoswarms had not been seen in hundreds of years. There were rumors of wild swarms, but no official reports existed, much like the mythical creatures she enjoyed reading about in her books. + +Marigold’s nanoswarm had been with her for as long as she could remember. It was her secret, shared only with Raina. She could not leave her swarm for more than a few days without missing them terribly, and based on the amount Raina and the others had packed, this trip would be much longer than a few days. + +Comforted by the swarm’s nearness, but still terrified at the prospect of her visit to the Palace, Marigold fell into an uneasy sleep. +""" diff --git a/stories/provision.coffee b/stories/provision.coffee index d60fa19..968895d 100644 --- a/stories/provision.coffee +++ b/stories/provision.coffee @@ -142,4 +142,4 @@ inexplicably racing heartbeat will bang and shudder against his hand like a hurricane's wind against the shutters of a house where nobody has ever lived. -""" \ No newline at end of file +""" diff --git a/stories/residue.coffee b/stories/residue.coffee new file mode 100644 index 0000000..02a47c6 --- /dev/null +++ b/stories/residue.coffee @@ -0,0 +1,79 @@ +module.exports = """ +\u200b RESIDUE + ======= + +Jules, my friend, used to take her new +car to Viretta Park. After a workday +filled with tickets and meetings, she +sometimes drove the five miles to this +one-block wide, one-block tall park at +the northeast coast of the city where +people prospered. There, tourists via +Ubers and Lyfts came to see the Kurt +Cobain bench. But Jules would arrive at +the park and drive past. She would not +stop. She would not get out. You cannot +see the bench from the road. It was +usually dark after work. She told no one +about these trips until years later. + +Jules, our protagonist, (as far as she +could figure) had no conscious logic for +her actions. Without it the story +refused to hang together. It was a +sequence of events united by space and +time but lacking guts hearts or brains. +It was a propulsion without a +propellant. It defied narrative +structure and, without it, lacked +rhythm. Jules, being a capable +self-aware adult, knew she was acting +out of an impulse but it seemed to her +that the situation warranted impulse. +And, at that time and at the time this +was written, gas was cheap. I can safely +say also that she was not sad; she was +not one of those people who indulged in +the habit of moping about events beyond +their control. Some people are just +strong this way. + +In my presence Jules narrated the story +with the blank point of view of an +omniscient witness. There was a lull in +the conversation, this story was told, +and then we moved on. She wore a thin +jacket and a wry frown and we were high, +more high than usual. I, after giving it +some consideration, had a grocery list +of questions. Why Kurt Cobain; why +didn't you tell me earlier; and did you +do it to feel an impossible, cosmic +connection to something bigger than +yourself, a feeling for which I cannot +do justice but can tell you that, for +me, is close to the sensation of an +egg's cracking against the chest +cavity's outer wall. But from looking at +her I knew then, just as she had always +known, it was not as if there were +answers and she were ignorant of them. +There just were no answers. At the time +these events I am writing about took +place, she was stuck in a temporary city +at a temporary time of her life. In my +country, where you work for years and +years in search of permanent residence +status and where adults values +friendship but few pursue them, it seems +harder than other places to let out +these prickly stories. I imagine people +all around me have them, though – yarn +wound and knotted by fingers +absentmindedly rubbing the threads +together. + +After eighteen months, Jules packed up +all her furniture and moved away. She +drove all the way through America. +""" diff --git a/style.styl b/style.styl index 575f538..5140e65 100644 --- a/style.styl +++ b/style.styl @@ -1,4 +1,5 @@ body + color: rgba(0, 0, 0, 0.87) overflow: hidden h1 @@ -34,14 +35,41 @@ comment &::before content: "-" +menu + font-size: 1rem + +body.drag-active iframe + pointer-events: none + +body > button.home + background-color: rgba(255, 255, 255, 0.5) + bottom: 0 + color: rgba(0, 0, 0, 0.87) + font-size: 2rem + left: 0 + padding-right: 0.3em + position: absolute + transition-duration: 0.125s + transition-property: background-color, color + z-index: 1 + + &:hover + background-color: rgba(255, 255, 255, 0.75) + + &:active + color: rgba(255, 255, 255, 1.0) + explorer display: block + height: 100% padding: 0.5em user-select: none + width: 100% + z-index: 1 body > & - height: 100% - width: 100% + background-image: url(https://i.imgur.com/hKOGoex.jpg) + background-size: cover > file, > folder display: inline-block @@ -56,12 +84,20 @@ explorer > file > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3d3dwAAAAj///////////////cAAAAI///////////////3AAAACP///////3d///d39wAAAAj/zMzM//mZ//+Zn/cAAAAI////////l///+X/3AAAACP/MzMzM//l3d3l/9wAAAAj/////////mZmZf/cAAAAI/8zMzMzM//l/+X/3AAAACP//////////l/l/9wAAAAj/zMzMzMzM//l5f/cAAAAI////////////mX/3AAAACP/MzMzMzMzM//n/9wAAAAj///////////////cAAAAI/8zMzMzMzMzMzP/3AAAACP//////////////9wAAAAj/zMzMzMzMzMzM//cAAAAI///////////////3AAAACP8AAAAA/8zMzMz/9wAAAAj/iZD/8P////////cAAAAI/4AAAAD/zMzMzP/3AAAACP+P8Luw////////9wAAAAj/gAC7sP/MzMzM//cAAAAI/4/wu7D////////3AAAACP+P8Luw/////4AAAAAAAAj/j/AAAP////+P94AAAAAI/4/wzMD/////j3gAAAAACP+IiIiA/////4eAAAAAAAj///////////+IAAAAAAAI////////////gAAAAAAACIiIiIiIiIiIiIAAAAAA4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAP4AAAH+AAAD/gAAB/4AAA/+AAAf8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj8z5//n3AACP//+ZmfcAAI/MzPn59wAAj////5n3AACPzMzM+fcAAI//////9wAAjwAPzMz3AACPmY////cAAI/Pj8zM9wAAj8+P//AAAACPiI//9/gAAI/////3gAAAiIiIiIgAAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIAHAACADwAAgB8AAA==") + > file[type="application/briefcase"] > icon + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIAAj3d3d3d3d3d3d3d3d3eIAI93d39/f39/f39/f393iICPd3d4eHh4eHh4eHh4d4iAj3d3d3d3d3d3d3d3d3eIgI93d3d//////////3d3iICPd3d/8AAAAAAAAAD/d4iAj3mXeIAAAAAAAAAAiHeIgI95l3iAAAAAAAAAAIh3iICPd3d3eIiIiIiIiIh3d4iAj3d3d3d3d3d3d3d3d3eIgI//////////////////iIAId3d3d3d3d3d3d3d3d3iAAId3d3d3d3d3d3d3d3d3gAAIiIiIiIiIiIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAABwAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADAAAAA4AAAAf////////////////////////////////////////////////////////////////////8oAAAAEAAAACAAAAABAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI+IiIiIiIgAj3d3d3d3eICPd3////d4gI938AAAD3iAj5eAAAAIeICPd3d3d3d4gI////////iACHd3d3d3d4AAiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AACAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADAAQAA//8AAP//AAD//wAA//8AAA==") + > file[type^="text/"] > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3dwcAAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI/wAAAAAAAAAAAP9wcAAACP//////////////cHAAAAj/AAAAAAAAAAAA/3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD////w8AD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAjw/w/w/w/w/w/w/4BwAAAI8P8P8P8P8P8P8P+AcAAAAI+I+I+I+I+I+I+I+AAAAAAAAAAAAAAAAAAAAAAAAA8AAAH+AAAA/AAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAH4AAAD/JJJL8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAh3d3d3d3AACP//////cAAI//////9wAAj/AAAA/3AACP//////cAAI/wAAAP9wAAj//////3AACP8AAAD/cAAI//////9wAAj/AA8A/3AACP//////cAAI//////9wAAjw8PDw8PAAAA+Pj4+PAAAAAAAAAAAAAAgA8AAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAACADwAA1V8AAA==") > file[type="application/javascript"] > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3dwcAAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA////AAD//3BwAAAI////////AwsDAP9wcAAACP8AAAD//zu7uzD/cHAAAAj///////M7MDsAD3BwAAAI///////zuwgLsw9wcAAACP8AAAAAAzswOwAPcHAAAAj///////87u7sw/3BwAAAI/wAAAAAAMwsDAP9wcAAACP////////8zM///cHAAAAj/AAAAAAAAAAAA/3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD////w8AD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAjw/w/w/w/w/w/w/4BwAAAI8P8P8P8P8P8P8P+AcAAAAI+I+I+I+I+I+I+I+AAAAAAAAAAAAAAAAAAAAAAAAA8AAAH+AAAA/AAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAH4AAAD/JJJL8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj///Aw/3AACP8Ac7MPcAAI//87Cz9wAAj/AHOzD3AACP//9zf/cAAI/wAAAP9wAAj//////3AACP8ADwD/cAAI//////9wAAj//////3AACPDw8PDw8AAAD4+Pj48AAAAAAAAAAAAAwAcAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAADABwAA6q8AAA==") + > file[path$="💾"] > icon + background-image: url("http://www.myiconfinder.com/uploads/iconsets/256-256-517c80793c96fa23c342ae1a5560ddf6.png") + background-size: cover + image-rendering: pixelated + > folder > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDMzMzMzMzMzMzMzMzMzMwj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI///////////////////wCHd3d3d3d3d4iIiIiIiIgACPt7e3t7e3gAAAAAAAAAAACPt7e3t7eAAAAAAAAAAAAACP/////4AAAAAAAAAAAAAACIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAP//wAH//+AD///wB/////////////8oAAAAEAAAACAAAAABAAQAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIgAj7e3t7e3uACPe3t7e3t4AI+3t7e3t7gAj3t7e3t7eACPt7e3t7e4AI97e3t7e3gAj7e3t7e3uACP///////4AIe3t7eIiIgACHt7eAAAAAAAiIiAAAAAAAAAAAAAAAAA//8AAP//AACAAQCgAAHOAAABzgAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAID/AADB/wAA//8AAA==") @@ -101,6 +137,8 @@ chateau window > viewport > & margin: initial +window > viewport > audio + width: 100% @keyframes display-achievement 0% @@ -207,3 +245,20 @@ achievement-badge &.achieved border: 1px solid blue color: blue + +version, site-url + color: white + display: block + font-size: 20px + font-weight: bold + pointer-events: none + position: absolute + right: 1rem + text-shadow: 2px 2px 4px black + z-index: 1 + +version + bottom: 1rem + +site-url + bottom: 3rem diff --git a/system.coffee b/system.coffee index b46a4fd..3580f4d 100644 --- a/system.coffee +++ b/system.coffee @@ -9,55 +9,13 @@ DexieFSDB = (dbName='fs') -> return db -# FS Wrapper to DB -DexieFS = (db) -> - Files = db.files - - notify = (eventType, path) -> - (result) -> - self.trigger eventType, path - return result - - self = Model() - .include(Bindable) - .extend - read: (path) -> - Files.get(path) - - write: (path, blob) -> - now = +new Date - - Files.put - path: path - blob: blob - size: blob.size - type: blob.type - createdAt: now - updatedAt: now - .then notify "write", path - - update: (path, changes) -> - Files.update path, changes - .then notify "update", path - - delete: (path) -> - Files.delete(path) - .then notify "delete", path - - # TODO: Collapse folders - # .replace(/\/.*$/, "/") - list: (dir) -> - Files.where("path").startsWith(dir).toArray() - .then (files) -> - files.forEach (file) -> - file.relativePath = file.path.replace(dir, "") - - return files +DexieFS = require "./lib/dexie-fs" +MountFS = require "./lib/mount-fs" uniq = (array) -> Array.from new Set array -Bindable = require "bindable" +Ajax = require "ajax" Model = require "model" Achievement = require "./system/achievement" Associations = require "./system/associations" @@ -68,24 +26,78 @@ UI = require "ui" module.exports = (dbName='zine-os') -> self = Model() - fs = DexieFS(DexieFSDB(dbName)) + fs = MountFS() + fs.mount "/", DexieFS(DexieFSDB(dbName)) self.include(Achievement, Associations, SystemModule, Template) + {title} = require "./pixie" + [..., version] = title.split('-') + self.extend + ajax: Ajax() fs: fs - # TODO: Allow relative paths + version: -> version + + require: require + stylus: require "./lib/stylus.min" + + moveFile: (oldPath, newPath) -> + oldPath = normalizePath oldPath + newPath = normalizePath newPath + + return Promise.resolve() if oldPath is newPath + + self.copyFile(oldPath, newPath) + .then -> + self.deleteFile(oldPath) + + copyFile: (oldPath, newPath) -> + return Promise.resolve() if oldPath is newPath + + self.readFile(oldPath) + .then (blob) -> + self.writeFile(newPath, blob) + + moveFileSelection: (selectionData, destinationPath) -> + Promise.resolve() + .then -> + {sourcePath, files} = selectionData + if sourcePath is destinationPath + return + else + Promise.all files.map ({relativePath}) -> + if relativePath.match(/\/$/) + # Folder + self.readTree("#{sourcePath}#{relativePath}") + .then (files) -> + Promise.all files.map (file) -> + targetPath = file.path.replace(sourcePath, destinationPath) + self.moveFile(file.path, targetPath) + else + self.moveFile("#{sourcePath}#{relativePath}", "#{destinationPath}#{relativePath}") + readFile: (path, userEvent) -> if userEvent self.Achievement.unlock "Load a file" path = normalizePath "/#{path}" fs.read(path) - .then ({blob}) -> - blob - # TODO: Allow relative paths + readTree: (directoryPath) -> + fs.list(directoryPath) + .then (files) -> + Promise.all files.map (file) -> + if file.folder + self.readTree(file.path) + else + file + .then (filesAndFolderFiles) -> + filesAndFolderFiles.reduce (a, b) -> + a.concat(b) + , [] + writeFile: (path, blob, userEvent) -> if userEvent self.Achievement.unlock "Save a file" @@ -93,16 +105,53 @@ module.exports = (dbName='zine-os') -> path = normalizePath "/#{path}" fs.write path, blob - # TODO: Allow relative paths deleteFile: (path) -> path = normalizePath "/#{path}" fs.delete(path) - # TODO: Allow relative paths updateFile: (path, changes) -> path = normalizePath "/#{path}" fs.update(path, changes) + urlForPath: (path) -> + fs.read(path) + .then URL.createObjectURL + + launchIssue: (date) -> + require("./issues/#{date}")() + + # TODO: Move this into some kind of system utils + installModulePrompt: -> + UI.Modal.prompt("url", "https://danielx.net/editor/master.json") + .then (url) -> + throw new Error "No url given" unless url + + baseName = url.replace(/^https:\/\/(.*)/, "$1") + .replace(/(\.json)?$/, "💾") + + pathPrompt = UI.Modal.prompt "path", "/lib/#{baseName}" + .then (path) -> + throw new Error "No path given" unless path + path + + blobRequest = fetch url + .then (result) -> + result.blob() + + Promise.all([blobRequest, pathPrompt]) + .then ([path, blob]) -> + self.writeFile(path, blob) + + installModule: (url, path) -> + path ?= url.replace(/^https:\/\/(.*)/, "/lib/$1") + .replace(/(\.json)?$/, "💾") + + fetch url + .then (result) -> + result.blob() + .then (blob) -> + self.writeFile(path, blob) + # NOTE: These are experimental commands to run code execJS: (path) -> self.readFile(path) @@ -111,8 +160,20 @@ module.exports = (dbName='zine-os') -> .then (programText) -> Function(programText)() + Observable: UI.Observable UI: UI + dumpModules: -> + src = PACKAGE.source + Object.keys(src).forEach (path) -> + file = src[path] + blob = new Blob [file.content] + self.writeFile("System/#{path}", blob) + + dumpPackage: -> + blob = new Blob [JSON.stringify(PACKAGE)], type: "application/json; charset=utf-8" + self.writeFile("System 💾", blob) + invokeBefore UI.Modal, "hide", -> self.Achievement.unlock "Dismiss modal" diff --git a/system/achievement.coffee b/system/achievement.coffee index f668963..253a54c 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -48,6 +48,41 @@ achievementData = [{ icon: "😭" group: "Issue 2" description: "Played dungeon of sadness" +}, { + text: "Issue 3" + icon: "📰" + group: "Issue 3" + description: "View Issue 3" +}, { + text: "Cover-2-cover 3: Tokyo Drift" + icon: "📗" + group: "Issue 3" + description: "Read the entire issue" +}, { + text: "Blue light special" + icon: "🈹" + group: "Issue 3" + description: "Read 'Blue Light Special'" +}, { + text: "Issue 4" + icon: "📰" + group: "Issue 4" + description: "View Issue 4" +}, { + text: "Cover-2-cover 4: Fast & Furious" + icon: "📗" + group: "Issue 4" + description: "Read the entire issue" +}, { + text: "Izzy" + icon: "🈹" + group: "Issue 4" + description: "Read 'Izzy'" +}, { + text: "Residue" + icon: "🈹" + group: "Issue 4" + description: "Read 'Residue'" }, { # Apps text: "Notepad.exe" icon: "📝" @@ -68,6 +103,21 @@ achievementData = [{ icon: "🖼️" group: "App" description: "Open the image viewer" +}, { + text: "Pixel perfect" + icon: "◼️️" + group: "App" + description: "Open the pixel editor" +}, { + text: "Check yo' self" + icon: "😉" + group: "App" + description: "Check your achievement status" +}, { + text: "Oh no, my files!" + icon: "💼" + group: "App" + description: "Opened 'My Briefcase'" }, { # OS text: "Save a file" icon: "💾" @@ -93,11 +143,51 @@ achievementData = [{ icon: "🐛" group: "OS" description: "Encountered a JavaScript error" +}, { + text: "Shut Down" + icon: "🔌" + group: "OS" + description: "ZineOS cannot be stopped" }, { # Social text: "Do you 'like' like me?" icon: "💕" group: "Social" description: "Have fine taste" +}, { + text: "We value your input" + icon: "📩" + group: "Social" + description: "View feedback form" +}, { # Chateau + text: "Enter the Chateau" + icon: "🏡" + group: "Chateau" + description: "Enter the Chateau" +}, { + text: "Puttin' on the Ritz" + icon: "🐭" + group: "Chateau" + description: "Upload custom avatar" +}, { + text: "Paint the town red" + icon: "🌆" + group: "Chateau" + description: "Upload a custom background" +}, { + text: "Poutine on the Ritz" + icon: "🍘" + group: "Chateau" + description: "Put poutine on a Ritz cracker" +}, { + text: "It's in the cloud" + icon: "☁️️" + group: "Chateau" + description: "Upload a file" +}, { + text: "Rawr" + icon: "🐉" + group: "Contrasaurus" + description: "Played Contrasaurus" }] restore = -> diff --git a/system/associations.coffee b/system/associations.coffee index 20de981..4e223c3 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -1,60 +1,149 @@ +AppDrop = require "../lib/app-drop" + # TODO: Move handlers out AudioBro = require "../apps/audio-bro" Filter = require "../apps/filter" Notepad = require "../apps/notepad" -TextEditor = require "../apps/text-editor" +CodeEditor = require "../apps/text-editor" +Explorer = require "../apps/explorer" Spreadsheet = require "../apps/spreadsheet" PixelEditor = require "../apps/pixel" Markdown = require "../apps/markdown" DSad = require "../apps/dungeon-of-sadness" +MyBriefcase = require "../apps/my-briefcase" + +PkgFS = require "../lib/pkg-fs" + +{extensionFor} = require "../util" openWith = (App) -> (file) -> app = App() - app.loadFile(file.blob) - document.body.appendChild app.element + + if file + {path} = file + system.readFile path + .then (blob) -> + app.loadFile(blob, path) + + system.attachApplication(app) module.exports = (I, self) -> - # TODO: Handlers that can use combined type, extension, and contents info - # to do the right thing - # Prioritize handlers falling back to others + # Handlers use combined type, extension, and contents info to do the right thing + # The first handler that matches is the default handler, the rest are available + # from context menu handlers = [{ - # JavaScript - name: "Execute" + name: "Markdown" # TODO: This renders html now too, so may need a broader name + filter: (file) -> + file.path.match(/\.md$/) or + file.path.match(/\.html$/) + fn: openWith(Markdown) # TODO: This can be a pointer to a system package + }, { + name: "Ace Editor" + filter: (file) -> + file.path.match(/\.coffee$/) or + file.path.match(/\.cson$/) or + file.path.match(/\.html$/) or + file.path.match(/\.jadelet$/) or + file.path.match(/\.js$/) or + file.path.match(/\.json$/) or + file.path.match(/\.md$/) or + file.path.match(/\.styl$/) + fn: openWith(CodeEditor) # TODO: This can be a pointer to a system package + }, { + name: "Run" + filter: (file) -> + file.type is "application/javascript" or + file.path.match(/\.js$/) or + file.path.match(/\.coffee$/) + fn: (file) -> + self.executeInIFrame(file.path) + }, { + name: "Exec" filter: (file) -> file.type is "application/javascript" or - file.path.match /\.js$/ + file.path.match(/\.js$/) or + file.path.match(/\.coffee$/) fn: (file) -> - file.blob.readAsText() - .then (sourceProgram) -> - system.loadModule sourceProgram, file.path + self.execute(file.path) }, { - # CoffeeScript - name: "Execute" + name: "Explore" filter: (file) -> - file.path.match /\.coffee$/ + file.path.match(/💾$/) fn: (file) -> - file.blob.readAsText() - .then (coffeeSource) -> - sourceProgram = CoffeeScript.compile coffeeSource, bare: true + system.readFile(file.path) + .then (blob) -> + blob.readAsJSON() + .then (pkg) -> + mountPath = file.path + "/" + fs = PkgFS(pkg, file.path) + system.fs.mount mountPath, fs + + # TODO: Can we make the explorer less specialized here? + element = Explorer + path: mountPath + windowView = system.UI.Window + title: mountPath + content: element + menuBar: null + width: 640 + height: 480 + iconEmoji: "📂" - system.loadModule sourceProgram, file.path + document.body.appendChild windowView.element }, { - name: "Markdown" + name: "Run" filter: (file) -> - file.path.match /\.md$/ - fn: openWith(Markdown) + file.path.match(/💾$/) + fn: (file) -> + # TODO: Rename? + system.execPathWithFile file.path, null }, { - name: "Text Editor" + name: "Publish" + filter: (file) -> + file.path.match(/💾$/) + fn: (file) -> + system.readFile file.path + .then (blob) -> + blob.readAsJSON() + .then (pkg) -> + system.UI.Modal.prompt "Path", "/My Briefcase/public/somefolder" + .then (path) -> + blob = new Blob [system.htmlForPackage(pkg)], + type: "text/html; charset=utf-8" + system.writeFile(path + "/index.html", blob) + }, { + name: "Run Link" + filter: (file) -> + file.path.match(/🔗$/) + fn: (file) -> + # TODO: Rename? + system.execPathWithFile file.path, null + }, { + name: "Edit Link" + filter: (file) -> + file.path.match(/🔗$/) + fn: openWith(CodeEditor) + }, { + name: "Sys Exec" + filter: (file) -> + return false # TODO: Enable with super mode :P + file.type is "application/javascript" or + file.path.match(/\.js$/) or + file.path.match(/\.coffee$/) + fn: (file) -> + self.execute(file.path) + }, { + name: "Notepad" filter: (file) -> file.type.match(/^text\//) or - file.type is "application/javascript" + file.type.match(/^application\/javascript/) fn: openWith(Notepad) }, { name: "Spreadsheet" filter: (file) -> # TODO: This actually only handles JSON arrays - file.type is "application/json" + file.type.match(/^application\/json/) fn: openWith(Spreadsheet) }, { name: "Image Viewer" @@ -66,6 +155,17 @@ module.exports = (I, self) -> filter: (file) -> file.type.match /^image\// fn: openWith(PixelEditor) + }, { + name: "PDF Viewer" + filter: (file) -> + file.path.match /\.pdf$/ + fn: (file) -> + file.blob.getURL() + .then (url) -> + app = system.iframeApp + src: url + title: file.path + system.attachApplication app }, { name: "Audio Bro" filter: (file) -> @@ -77,7 +177,13 @@ module.exports = (I, self) -> file.path.match /dsad\.exe$/ fn: -> app = DSad() - document.body.appendChild app.element + system.attachApplication app + }, { + name: "zine1.exe" + filter: (file) -> + file.path.match /zine1\.exe$/ + fn: -> + require("../issues/2016-12")() }, { name: "zine2.exe" filter: (file) -> @@ -85,11 +191,30 @@ module.exports = (I, self) -> fn: -> require("../issues/2017-02")() }, { - name: "zine1.exe" + name: "zine3.exe" filter: (file) -> - file.path.match /zine1\.exe$/ + file.path.match /zine3\.exe$/ fn: -> - require("../issues/2016-12")() + require("../issues/2017-03")() + }, { + name: "zine4.exe" + filter: (file) -> + file.path.match /zine4\.exe$/ + fn: -> + require("../issues/2017-04")() + }, { + name: "feedback.exe" + filter: (file) -> + file.path.match /feedback\.exe$/ + fn: -> + require("../feedback")() + }, { + name: "My Briefcase" + filter: ({path}) -> + path.match /My Briefcase$/ + fn: -> + app = MyBriefcase() + system.attachApplication app }] # Open JSON arrays in spreadsheet @@ -103,15 +228,79 @@ module.exports = (I, self) -> else throw new Error "No handler for files of type #{file.type}" + mimes = + html: "text/html" + js: "application/javascript" + json: "application/json" + md: "text/markdown" + Object.assign self, + iframeApp: require "../lib/iframe-app" + # Open a file # TODO: Pass arguments # TODO: Drop files on an app to open them in that app open: (file) -> handle(file) + openPath: (path) -> + self.readFile path + .then self.open + + # Return a list of all handlers that can be used for this file openersFor: (file) -> handlers.filter (handler) -> handler.filter(file) + # Add a handler to the list of handlers, position zero is highest priority + # position -1 is lowest priority. + registerHandler: (handler, position=0) -> + handlers.splice(position, 0, handler) + + handlers: -> + handlers.slice() + + # The final step in launching an application in the OS + # This wires up event streams, drop events, adds the app to the list + # of running applications, and attaches the app's element to the DOM + attachApplication: (app, options={}) -> + # Bind Drop events + AppDrop(app) + + # TODO: Bind to app event streams + + # TODO: Add to list of apps + + document.body.appendChild app.element + + pathAsApp: (path) -> + if path.match(/💾$/) + system.readFile path + .then (blob) -> + blob.readAsJSON() + .then (pkg) -> + return -> + self.executePackageInIFrame(pkg) + else if path.match(/🔗$/) + system.readFile path + .then (blob) -> + blob.readAsText() + .then system.evalCSON + .then (data) -> + return -> + system.iframeApp data + else if path.match(/\.js$|\.coffee$/) + -> + self.executeInIFrame(path) + else + Promise.reject new Error "Could not launch #{path}" + + execPathWithFile: (path, file) -> + self.pathAsApp(path) + .then (App) -> + openWith(App)(file) + + mimeTypeFor: (path) -> + mimes[extensionFor(path)] or "text/plain" + return self diff --git a/system/module.coffee b/system/module.coffee index fcc39d0..8c8926a 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -2,6 +2,8 @@ # # Depends on having self.readFile defined +IFrameApp = require "../lib/iframe-app" + module.exports = (I, self) -> ### Load a module from a file in the file system. @@ -17,13 +19,42 @@ module.exports = (I, self) -> Circular includes will never reslove # TODO: Fail early on circular includes, challenging because of async - # TODO: Succeed on files that don't assign module.exports + # Currently can require + # js, coffee, jadelet, json, cson + + # Requiring other file types returns a Blob - # TODO: Require .coffee/arbitrary files - # images, blobs, html, json ### - {fileSeparator, normalizePath} = require "../util" + { + absolutizePath + evalCSON + fileSeparator + normalizePath + isAbsolutePath + isRelativePath + htmlForPackage + } = require "../util" + + findDependencies = (sourceProgram) -> + requireMatcher = /[^.]require\(['"]([^'"]+)['"]\)/g + results = [] + count = 0 + + loop + match = requireMatcher.exec sourceProgram + + if match + results.push match[1] + else + break + + # Circuit breaker for safety + count += 1 + if count > 256 + break + + return results # Wrap program in async include wrapper # Replaces references to require('something') with local variables in an async wrapper function @@ -32,8 +63,10 @@ module.exports = (I, self) -> namePrefix = "__req" requires = {} - # rewrite requires - rewrittenProgram = program.replace /require\(['"]([^'"]+)['"]\)/g, (match, key) -> + # rewrite requires like `require('cool-module')` or `require('./relative-path')` + # don't rewrite one that belong to another object `something.require('somepath')` + # don't rewrite dynamic ones like `require(someVar)` + rewrittenProgram = program.replace /[^.]require\(['"]([^'"]+)['"]\)/g, (match, key) -> if requires[key] tmpVar = requires[key] else @@ -50,7 +83,7 @@ module.exports = (I, self) -> requirePaths = requirePaths """ - system.include(#{JSON.stringify(requirePaths)}) + return system.vivifyPrograms(#{JSON.stringify(requirePaths)}) .then(function(__reqResults) { (function(#{tmpVars.join(', ')}){ #{rewrittenProgram} @@ -63,8 +96,6 @@ module.exports = (I, self) -> program = annotateSourceURL(rewriteRequires(content), path) dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) or fileSeparator - # May need to scan for a module.exports to see if it is the kind of - # module that exports things vs just plain side effects code module = path: dirname @@ -79,14 +110,18 @@ module.exports = (I, self) -> # Trigger complete resolve(module) - # Apply relative path wrapper for system.include + # Apply relative path wrapper for system.vivifyPrograms localSystem = Object.assign {}, self, - include: (moduleIdentifiers) -> - relativeIdentifiers = moduleIdentifiers.map (identifier) -> - # TODO: Allow absolute paths? - normalizePath dirname + identifier + vivifyPrograms: (moduleIdentifiers) -> + absoluteIdentifiers = moduleIdentifiers.map (identifier) -> + if isAbsolutePath(identifier) + absolutizePath "/", identifier + else if isRelativePath(identifier) + absolutizePath dirname, identifier + else + identifier - self.include relativeIdentifiers, state + self.vivifyPrograms absoluteIdentifiers, state # TODO: Also make working directory relative paths for readFile and writeFile context = @@ -100,37 +135,360 @@ module.exports = (I, self) -> args = Object.keys(context) values = args.map (name) -> context[name] - try + Promise.resolve() + .then -> Function(args..., program).apply(module, values) - catch e - console.error e - reject e + .catch reject + + # Scan for a module.exports to see if it is the kind of + # module that exports things vs just plain side effects code + # This can return false positives if it just matches the string and isn't + # really exporting, regex is not a parser, yolo, etc. + hasExports = program.match /module\.exports/ + + # Just resolve next tick if we're not specifically exporting + # can be fun with race conditions, but just export your biz, yo! + if !hasExports + setTimeout -> + resolve(module) + , 0 Object.assign self, + autoboot: -> + self.fs.list "/System/Boot/" + .then (files) -> + console.log files + bootablePaths = files.filter ({blob}) -> + blob? + .map ({path}) -> + path + + self.vivifyPrograms(bootablePaths) + + # A simpler, dumber, packager that reads a pixie.cson, then + # just packages every file recursively down in the directories + createPackageFromPixie: (pixiePath) -> + basePath = pixiePath.match(/^.*\//)?[0] or "" + pkg = + distribution: {} + + self.loadProgram(pixiePath).then (config) -> + pkg.config = config + .then -> + self.readTree(basePath) + .then (files) -> + Promise.all files.map ({path, blob}) -> + (if blob instanceof Blob + self.compileFile(blob) + else + self.readFile(path) + .then(self.compileFile) + ) + .then (result) -> + [path, result] + .then (results) -> + results.forEach ([path, result]) -> + pkgPath = path.replace(basePath, "").replace(/\.[^.]*$/, "") + + if typeof result is "string" + pkg.distribution[pkgPath] = + content: result + else + console.warn "Can't package files like #{path} yet" + + return pkg + + # This is kind of the opposite approach of the vivifyPrograms, here we want + # to load everything statically and put it in a package that can be run by + # `require`. + packageProgram: (absolutePath, state={}) -> + state.cache = {} + state.pkg = {} + + basePath = absolutePath.match(/^.*\//)?[0] or "" + state.basePath = basePath + + # Strip out base path and final suffix + # NOTE: .coffee.md type files won't like this + state.pkgPath = (path) -> + path.replace(state.basePath, "").replace(/\.[^.]*$/, "") + pkgPath = state.pkgPath(absolutePath) + + {pkg} = state + pkg.distribution ?= {} + + unless state.loadConfigPromise + configPath = absolutizePath basePath, "pixie.cson" + state.loadConfigPromise = self.loadProgram(configPath).then (configSource) -> + module = {} + Function("module", configSource)(module) + module.exports + .then (config) -> + entryPoint = config.entryPoint + (if entryPoint + path = absolutizePath(basePath, entryPoint) + self.loadProgramIntoPackage(path, state) + else + Promise.resolve() + ).then -> + debugger + pkg.remoteDependencies = config.remoteDependencies + pkg.config = config + .catch (e) -> + if e.message.match /File not found/i + pkg.config = {} + else + throw e + + self.loadProgramIntoPackage(absolutePath, state) + .then -> + state.loadConfigPromise + .then -> + pkg.remoteDependencies = pkg.config.remoteDependencies + if pkg.config.entryPoint + pkg.entryPoint = pkg.config.entryPoint + else + pkg.entryPoint ?= pkgPath + + return pkg + + # Internal helper to load a program and its dependencies into the pkg + # in the state + # TODO: Loading deps like this doesn't work at all if require is used + # from browserified js sources :( + loadProgramIntoPackage: (absolutePath, state) -> + {basePath, pkg} = state + pkgPath = state.pkgPath(absolutePath) + relativeRoot = absolutePath.replace(/\/[^/]*$/, "") + + state.cache[absolutePath] ?= self.loadProgram(absolutePath) + .then (sourceProgram) -> + if typeof sourceProgram is "string" + # NOTE: Things will fail if we require ../../ above our + # initial directory. + # TODO: Detect and throw if requiring relative or absolute paths above + # or outside of our base path + + # Add to package + pkg.distribution[pkgPath] = + content: sourceProgram + + # Pull in dependencies + depPaths = findDependencies(sourceProgram) + Promise.all depPaths.map (depPath) -> + Promise.resolve().then -> + if isRelativePath depPath + path = absolutizePath(relativeRoot, depPath) + self.loadProgramIntoPackage path, state + else if isAbsolutePath depPath + throw new Error "Absolute paths not supported yet" + else + # package path + depPkg = PACKAGE.dependencies[depPath] + if depPkg + pkg.dependencies ?= {} + pkg.dependencies[depPath] = depPkg + else + # TODO: Load from remote? + throw new Error "Package '#{depPath}' not found" + else + throw new Error "TODO: Can't package files like #{absolutePath} yet" + + # still experimenting with the API - # Async include in the vein of require.js + # Async 'require' in the vein of require.js # it's horrible but seems necessary # This is an internal API and isn't recommended for general use - # The state determines an include root and should is the same for a single + # The state determines an include root and should be the same for a single # app or process - include: (moduleIdentifiers, state={}) -> + vivifyPrograms: (absolutePaths, state={}) -> state.cache ?= {} - Promise.all moduleIdentifiers.map (identifier) -> - state.cache[identifier] ?= self.readFile(identifier) - .then (file) -> - file.readAsText() + Promise.all absolutePaths.map (absolutePath) -> + state.cache[absolutePath] ?= self.loadProgram(absolutePath) .then (sourceProgram) -> - loadModule sourceProgram, identifier, state + # loadProgram returns an object in the case of JSON because it has no + # dependencies and doesn't need an require re-writing + # Having this special case lets us take a short cut without having to + # Parse/unparse json extra. + # This may be handy for other binary assets like images, etc. as well + if typeof sourceProgram is "string" + loadModule sourceProgram, absolutePath, state + else + exports: sourceProgram .then (module) -> module.exports + loadProgram: (path, basePath="/") -> + self.readForRequire path, basePath + .then self.compileFile + + compileFile: (file) -> + # system modules are loaded as functions/objects right now, so just return them + unless file instanceof Blob + return file + + [compiler] = compilers.filter ({filter}) -> + filter file + + if compiler + compiler.fn(file) + else + # Return the blob itself if we didn't find any compilers + return file + # May want to reconsider this name loadModule: (args...) -> self.Achievement.unlock "Execute code" loadModule(args...) + # Execute in the context of the system itself + spawn: (args...) -> + loadModule(args...) + .then ({exports}) -> + if typeof exports is "function" and exports.length is 0 + result = exports() + + if result.element + document.body.appendChild result.element + + execute: (absolutePath) -> + self.vivifyPrograms [absolutePath] + .then ([{exports}])-> + if typeof exports is "function" and exports.length is 0 + result = exports() + + if result.element + document.body.appendChild result.element + + executeInIFrame: (absolutePath) -> + self.packageProgram(absolutePath) + .then (pkg) -> + self.executePackageInIFrame pkg + + # Execute a package in the context of an iframe + executePackageInIFrame: (pkg) -> + app = IFrameApp + pkg: pkg + title: pkg.config?.title + packageOptions: + script: """ + var ZINEOS = #{JSON.stringify system.version()}; + #{PACKAGE.distribution["lib/system-client"].content}; + """ + sandbox: "allow-scripts allow-forms" + + document.body.appendChild app.element + + return app + + # Handle requiring with or without explicit extension + # require "a" + # First check: + # a + # a.coffee + # a.coffee.md + # a.litcoffee + # a.jadelet + # a.js + readForRequire: (path, basePath) -> + # Hack to load 'system' modules + isModule = !path.match(/^.?.?\//) + if isModule + return Promise.resolve() + .then -> + require path + + absolutePath = absolutizePath(basePath, path) + + suffixes = ["", ".coffee", ".coffee.md", ".litcoffee", ".jadelet", ".js", ".styl"] + + p = suffixes.reduce (promise, suffix) -> + promise.then (file) -> + return file if file + filePath = "#{absolutePath}#{suffix}" + self.readFile(filePath) + .catch -> # If read fails try next read + , Promise.resolve() + + p.then (file) -> + unless file + tries = suffixes.map (suffix) -> + "#{absolutePath}#{suffix}" + throw new Error "File not found at path: #{absolutePath}. Tried #{tries}" + + return file + + htmlForPackage: htmlForPackage + + evalCSON: evalCSON + +# Compile files based on type to JS program source +# These compilers return a string of JS source code that assigns a +# result to module.exports +compilers = [{ + filter: ({path}) -> + path.match /\.js$/ + fn: (blob) -> + blob.readAsText() +}, { + filter: ({path}) -> + path.match(/\.coffee.md$/) or + path.match(/\.litcoffee$/) + fn: (blob) -> + blob.readAsText() + .then (coffeeSource) -> + CoffeeScript.compile coffeeSource, bare: true, literate: true +}, { + filter: ({path}) -> + path.match /\.coffee$/ + fn: (blob) -> + blob.readAsText() + .then (coffeeSource) -> + CoffeeScript.compile coffeeSource, bare: true +}, { + filter: ({path}) -> + path.match /\.jadelet$/ + fn: (blob) -> + blob.readAsText() + .then (jadeletSource) -> + Hamlet.compile jadeletSource, + compiler: CoffeeScript + mode: "jade" + runtime: "require('_lib_hamlet-runtime')" +}, { + filter: ({path}) -> + path.match /\.styl$/ + fn: (blob) -> + blob.readAsText() + .then (source) -> + system.stylus(source).render() + .then stringifyExport +}, { + filter: ({path}) -> + path.match /\.json$/ + fn: (blob) -> + blob.readAsJSON() + .then stringifyExport +}, { + filter: ({path}) -> + path.match /\.cson$/ + fn: (blob) -> + blob.readAsText() + .then evalCSON + .then stringifyExport +}, { + filter: ({path}) -> + path.match /\.te?xt$/ + fn: (blob) -> + blob.readAsText() + .then stringifyExport +}] + +stringifyExport = (data) -> + "module.exports = #{JSON.stringify(data)}" + annotateSourceURL = (program, path) -> """ #{program} diff --git a/templates/file.jadelet b/templates/file.jadelet index e36c5bd..dc88da2 100644 --- a/templates/file.jadelet +++ b/templates/file.jadelet @@ -1,3 +1,3 @@ -file(@dblclick @contextmenu @type) +file(draggable="true" @dragstart @dblclick @contextmenu @path @type) icon - label= @relativePath + label= @displayName diff --git a/templates/folder.jadelet b/templates/folder.jadelet index f4b73b3..0d00839 100644 --- a/templates/folder.jadelet +++ b/templates/folder.jadelet @@ -1,3 +1,3 @@ -folder(@dblclick @contextmenu) +folder(draggable="true" @dragstart @dblclick @contextmenu @path) icon - label= @relativePath + label= @displayName diff --git a/templates/home-button.jadelet b/templates/home-button.jadelet new file mode 100644 index 0000000..ceb6a4d --- /dev/null +++ b/templates/home-button.jadelet @@ -0,0 +1 @@ +button.home(@click) 🏡 Start diff --git a/templates/site-url.jadelet b/templates/site-url.jadelet new file mode 100644 index 0000000..1013448 --- /dev/null +++ b/templates/site-url.jadelet @@ -0,0 +1 @@ +site-url https://whimsy.space diff --git a/templates/version.jadelet b/templates/version.jadelet new file mode 100644 index 0000000..e989d72 --- /dev/null +++ b/templates/version.jadelet @@ -0,0 +1,2 @@ +version + span= @version diff --git a/test/system/module.coffee b/test/system/module.coffee index aaa73a2..68c2e74 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -3,13 +3,33 @@ Model = require "model" Associations = require "../../system/associations" SystemModule = require "../../system/module" -describe "System Module", -> - it "should include modules in files async", -> - model = Model() +global.Hamlet = require "../../lib/hamlet" + +mocha.setup + globals: ['amazon'] + +makeSystemFS = (files) -> + model = Model() + model.include SystemModule, Associations + + Object.assign model, + readFile: (path) -> + Promise.resolve() + .then -> + content = files[path] + + throw new Error "File not found: #{path}" unless content? + + blob = new Blob [content] + blob.path = path + + return blob - model.include SystemModule + return model - files = +describe "System Module", -> + it "should vivifyPrograms in files asynchronously", -> + model = makeSystemFS "/test.js": """ module.exports = 'yo'; """ @@ -28,23 +48,64 @@ describe "System Module", -> module.exports = Math.random(); """ - model.readFile = (path) -> - content = files[path] - - Promise.resolve new Blob [content] - - model.include(["/root.js", "/wat.js", "/rand.js", "/rand.js"]) + model.vivifyPrograms(["/root.js", "/wat.js", "/rand.js", "/rand.js"]) .then ([root, wat, r1, r2]) -> console.log root, wat, r1, r2 assert.equal r1, r2 assert.equal root, 'yo 2 rad hella' - it "should wait forever when resolving circular requires", (done) -> - model = Model() + it "should throw an error when requiring a file that doesn't exist", (done) -> + #@timeout 250 + + model = makeSystemFS + "/a.js": """ + module.exports = require("./b.js") + """ + + model.vivifyPrograms(["/a.js"]) + .catch (e) -> + done() - model.include SystemModule + it "should throw an error when requiring a file that throws an error", (done) -> + @timeout 250 - files = + model = makeSystemFS + "/a.js": """ + throw new Error("I am error") + """ + + model.vivifyPrograms(["/a.js"]) + .catch (e) -> + done() + + it "should require valid json", -> + @timeout 250 + + model = makeSystemFS + "/a.json": """ + { + "yolo": "wat" + } + """ + + model.vivifyPrograms(["/a.json"]) + .then ([json]) -> + assert.equal json.yolo, "wat" + + it "should throw an error when requiring invalid json", (done) -> + @timeout 250 + + model = makeSystemFS + "/a.json": """ + yolo: 'wat' + """ + + model.vivifyPrograms(["/a.json"]) + .catch (e) -> + done() + + it "should wait forever when resolving circular requires", (done) -> + model = makeSystemFS "/a.js": """ module.exports = require("./b.js") """ @@ -52,12 +113,7 @@ describe "System Module", -> module.exports = require("./a.js") """ - model.readFile = (path) -> - content = files[path] - - Promise.resolve new Blob [content] - - model.include(["/a.js"]) + model.vivifyPrograms(["/a.js"]) .then ([a]) -> # Never get here assert false @@ -66,44 +122,60 @@ describe "System Module", -> done() , 100 - it "should return export if present", -> - model = Model() - - model.include Associations, SystemModule - - files = + it "should work even if the file doesn't assign to module.exports", -> + model = makeSystemFS "/wat.js": """ - module.exports = "wat"; + exports.yolo = "wat"; """ - model.readFile = (path) -> - content = files[path] - - Promise.resolve new Blob [content] + model.vivifyPrograms ["/wat.js"] + .then ([wat]) -> + assert.equal wat.yolo, "wat" - model.open - path: "/wat.js" - type: "application/javascript" - .then (moduleExports) -> - assert.equal moduleExports, "wat" - - it "should work even if the file doesn't assign to module.exports" - -> - model = Model() + it "should work with relative paths in subfolders", -> + model = makeSystemFS + "/main.js": """ + module.exports = require("./folder/a.js"); + """ + "/folder/a.js": """ + module.exports = require("./b.js"); + """ + "/folder/b.js": """ + module.exports = "b"; + """ - model.include SystemModule + model.vivifyPrograms ["/main.js"] + .then ([main]) -> + assert.equal main, "b" - files = - "/wat.js": """ - exports.yolo = "wat"; + it "should work with absolute paths in subfolders", -> + model = makeSystemFS + "/main.js": """ + module.exports = require("./folder/a.js"); + """ + "/folder/a.js": """ + module.exports = require("/b.js"); + """ + "/b.js": """ + module.exports = "b"; """ - model.readFile = (path) -> - content = files[path] + model.vivifyPrograms ["/main.js"] + .then ([main]) -> + assert.equal main, "b" - Promise.resolve new Blob [content] + it "should require .jadelet sources", -> + model = makeSystemFS + "/main.coffee": """ + template = require "./button.jadelet" + + module.exports = + buttonTemplate: template + """ + "/button.jadelet": """ + button(@click)= @text + """ - model.open - path: "/wat.js" - .then (moduleExports) -> - assert.equal moduleExports.yolo, "wat" + model.vivifyPrograms ["/main.coffee"] + .then ([main]) -> + assert typeof main.buttonTemplate is "function" diff --git a/util.coffee b/util.coffee index 639305b..c9ccfba 100644 --- a/util.coffee +++ b/util.coffee @@ -1,17 +1,95 @@ fileSeparator = "/" normalizePath = (path) -> - path.replace(/\/\/+/, fileSeparator) # /// -> / + path.replace(/\/+/g, fileSeparator) # /// -> / .replace(/\/[^/]*\/\.\./g, "") # /base/something/.. -> /base .replace(/\/\.\//g, fileSeparator) # /base/. -> /base +# NOTE: Allows paths like '../..' to go above the base path +absolutizePath = (base, relativePath) -> + normalizePath "/#{base}/#{relativePath}" + +makeScript = (src) -> + "