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 new file mode 100644 index 0000000..7573cd5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,60 @@ +TODO +==== + +System Features +--------------- +[X] File Browser + +[X] App Associations + +[X] File Context Menu + +[X] Folders + +[X] Desktop Icons + +[ ] Desktop Background + +[ ] Help Documentation + +[X] Compilerz + +[X] Require/Include Local Files + +[ ] Drag 'n' Drop + +[X] Cloud Briefcase + +Applications +------------ + +Spreadsheet +- Save/Load Data +- Custom Cell Views +- Data Relationships +- Forms + +Tactics Game Sandbox + +Markdown / Wiki + +Image Munger + +Programatic Animator + +Pixel Editor + +Music Maker + +Database? + +Network Social +-------------- + +[x] Comments + +[ ] Sharing + +[ ] 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 new file mode 100644 index 0000000..1fb6679 --- /dev/null +++ b/apps/audio-bro.coffee @@ -0,0 +1,50 @@ +# Play Audio + +FileIO = require "../os/file-io" +Model = require "model" + +module.exports = -> + # Global system + {ContextMenu, MenuBar, Modal, Observable, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement} = system + + Achievement.unlock "Pump up the jam" + + audio = document.createElement 'audio' + audio.controls = true + audio.autoplay = true + + filePath = Observable() + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + filePath blob.path + 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: -> + if path = filePath() + "Audio Bro - #{path}" + else + "Audio Bro" + content: audio + menuBar: menuBar.element + width: 308 + height: 80 + iconEmoji: "🎶" + + windowView.loadFile = handlers.loadFile + + return windowView diff --git a/apps/chateau.coffee b/apps/chateau.coffee new file mode 100644 index 0000000..6f9ebfc --- /dev/null +++ b/apps/chateau.coffee @@ -0,0 +1,24 @@ +IFrameApp = require "../lib/iframe-app" + +module.exports = -> + {Achievement} = system + + app = IFrameApp + src: "https://danielx.net/chateau/" + width: 960 + height: 540 + title: "Chateau" + 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 new file mode 100644 index 0000000..ddaf317 --- /dev/null +++ b/apps/dungeon-of-sadness.coffee @@ -0,0 +1,12 @@ +module.exports = -> + {Achievement, iframeApp} = system + + app = iframeApp + title: "Dungeon of Sadness" + src: "https://danielx.net/ld33/" + width: 648 + height: 507 + + Achievement.unlock "The dungeon is in our heart" + + return app diff --git a/apps/explorer.coffee b/apps/explorer.coffee new file mode 100644 index 0000000..1b8b9e3 --- /dev/null +++ b/apps/explorer.coffee @@ -0,0 +1,277 @@ +# Explorer File Browser +# +# Explore the file system like adventureres of old! +# TODO: Drop files onto applications +# TODO: Select multiple +# TOOD: Keyboard Input + +Drop = require "../lib/drop" +FileTemplate = require "../templates/file" +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 + 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 + e.preventDefault() + + contextMenuHandlers = + open: -> + system.open(file) + cut: -> #TODO + copy: -> #TODO + delete: -> + system.deleteFile(file.path) + rename: -> + Modal.prompt "Filename", file.path + .then (newPath) -> + if newPath + 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) -> + if newType + system.updateFile file.path, + type: newType + .then console.log + + 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 + #{openWithMenu} + - + Cut + Copy + - + Delete + Rename + - + Edit MIME Type + Properties + """ + handlers: contextMenuHandlers + + + 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 + - + Cut + Copy + - + Delete + Rename + - + Properties + """ + handlers: + open: -> + addWindow(folder.path) + delete: -> + system.readTree(folder.path) + .then (results) -> + Promise.all results.map (result) -> + system.deleteFile(result.path) + rename: -> + 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 + inElement: document.body + x: e.pageX + y: e.pageY + + update = -> + system.fs.list(path) + .then (files) -> + emptyElement explorer + + addedFolders = {} + + files.forEach (file) -> + if file.relativePath.match /\/$/ # folder + folderPath = file.relativePath + addedFolders[folderPath] = true + return + + Object.assign file, + displayName: file.relativePath + + dblclick: -> + system.open 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\// + 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).reverse().forEach (folderName) -> + folder = + 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) + + update() + + # Refresh files when they change + system.fs.on "write", (path) -> update() + system.fs.on "delete", (path) -> update() + system.fs.on "update", (path) -> update() + + addWindow = (path) -> + element = Explorer + path: path + + windowView = Window + title: path + content: element + menuBar: null + width: 640 + height: 480 + iconEmoji: "📂" + + document.body.appendChild windowView.element + + return explorer diff --git a/apps/filter.coffee b/apps/filter.coffee new file mode 100644 index 0000000..149f9ac --- /dev/null +++ b/apps/filter.coffee @@ -0,0 +1,67 @@ +# View and Manipulate Images + +FileIO = require "../os/file-io" +Model = require "model" + +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') + + modalForm = system.compileTemplate """ + form + label + h2 Width + input(name="width") + label + h2 Height + input(name="height") + """ + + 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: "Spectacle Image Viewer" + content: canvas + menuBar: menuBar.element + width: 640 + height: 480 + + windowView.loadFile = handlers.loadFile + + return windowView diff --git a/apps/markdown.coffee b/apps/markdown.coffee new file mode 100644 index 0000000..d109cf1 --- /dev/null +++ b/apps/markdown.coffee @@ -0,0 +1,115 @@ +# Render Markdown + +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 = "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, path) -> + navigationStack.push path + baseDir = path.replace /\/[^/]*$/, "" + + blob.readAsText() + .then (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 + [O]pen + - + E[x]it + """ + handlers: handlers + + windowView = Window + title: "Markdown" + content: container + menuBar: menuBar.element + 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 d563067..1d8933e 100644 --- a/apps/notepad.coffee +++ b/apps/notepad.coffee @@ -1,10 +1,12 @@ FileIO = require "../os/file-io" Model = require "model" -module.exports = () -> +module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + system.Achievement.unlock "Notepad.exe" + exec = (cmd) -> -> textarea.focus() @@ -13,14 +15,23 @@ module.exports = () -> TODO = -> console.log "TODO" 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" @@ -109,10 +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 new file mode 100644 index 0000000..120a2ed --- /dev/null +++ b/apps/pixel.coffee @@ -0,0 +1,44 @@ +IFrameApp = require "../lib/iframe-app" +FileIO = require "../os/file-io" +Model = require "model" + +module.exports = -> + {MenuBar, Modal, Observable, Util:{parseMenu}} = system.UI + + handlers = Model().include(FileIO).extend + loadFile: (blob) -> + app.send "loadFile", blob + newFile: -> + saveData: -> + app.send "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 + + app = IFrameApp + title: Observable "Pixie Paint" + src: "https://danielx.net/pixel-editor/" + menuBar: menuBar + handlers: handlers + width: 640 + height: 480 + + app.handlers = handlers + app.loadFile = handlers.loadFile + + system.Achievement.unlock "Pixel perfect" + + return app diff --git a/apps/spreadsheet.coffee b/apps/spreadsheet.coffee index 225592d..5295e73 100644 --- a/apps/spreadsheet.coffee +++ b/apps/spreadsheet.coffee @@ -1,42 +1,80 @@ -module.exports = (os) -> - {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = os.UI +FileIO = require "../os/file-io" +Model = require "model" - # Observable input helper - o = (value, type) -> - attribute = Observable(value) - if type - attribute.type = type +module.exports = -> + {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI - attribute.value = attribute + system.Achievement.unlock "Microsoft Access 97" - return attribute + sourceData = [] - data = Observable [0...5].map (i) -> - id: o i - name: o "yolo" - color: o "#FF0000", "color" + headers = ["id", "name", "color"] - {element} = Table data + RowModel = (datum) -> + Model(datum).attrObservable headers... + + models = sourceData.map RowModel + + InputTemplate = require "../templates/input" + RowElement = (datum) -> + tr = document.createElement "tr" + types = [ + "number" + "text" + "color" + ] + + headers.forEach (key, i) -> + td = document.createElement "td" + td.appendChild InputTemplate + value: datum[key] + type: types[i] + + tr.appendChild td + + return tr + + {element} = tableView = Table { + data: models + RowElement: RowElement + headers: headers + } 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 + # Update models data + models.splice(0, models.length, sourceData.map(RowModel)...) + + # Re-render + tableView.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 + datum = + id: 0 + name: "new" + color: "#FF00FF" + + sourceData.push datum + models.push RowModel(datum) + + # Re-render + tableView.render() exit: -> windowView.element.remove() @@ -63,4 +101,6 @@ module.exports = (os) -> width: 640 height: 480 + windowView.loadFile = handlers.loadFile + return windowView 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 new file mode 100644 index 0000000..6892a23 --- /dev/null +++ b/apps/text-editor.coffee @@ -0,0 +1,131 @@ +Model = require "model" +FileIO = require "../os/file-io" + +ace.require("ace/ext/language_tools") + +{extensionFor} = require "../util" + +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%" + + 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 + + session = aceEditor.getSession() + session.setUseSoftTabs true + session.setTabSize 2 + + mode = "coffee" + session.setMode("ace/mode/#{mode}") + + global.aceEditor = aceEditor + + 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) + handlers.saved true + + session.on "change", -> + handlers.saved false + + handlers = Model().include(FileIO).extend + loadFile: initSession + newFile: -> + session.setValue "" + saveData: -> + data = new Blob [session.getValue()], + type: mimeTypeFor(handlers.currentPath()) + + 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: -> + path = handlers.currentPath() + if handlers.saved() + savedIndicator = "" + else + savedIndicator = "*" + + if path + path = " - #{path}" + + "Ace#{path}#{savedIndicator}" + + content: aceWrap + menuBar: menuBar.element + width: 640 + height: 480 + + windowView.loadFile = initSession + + 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 8934ac7..2523543 100644 --- a/extensions.coffee +++ b/extensions.coffee @@ -1,5 +1,11 @@ +# 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.prototype.readAsText = -> +Blob::readAsText = -> file = this new Promise (resolve, reject) -> @@ -9,6 +15,55 @@ Blob.prototype.readAsText = -> reader.onerror = reject reader.readAsText(file) -Blob.prototype.readAsJSON = -> +Blob::getURL = -> + Promise.resolve URL.createObjectURL(this) + +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) + +# 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) -> + blob.getURL() + .then (url) -> + new Promise (resolve, reject) -> + img = new Image + img.onload = -> + resolve img + img.onerror = reject + + 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-10.coffee b/issues/2016-12.coffee similarity index 87% rename from issues/2016-12-10.coffee rename to issues/2016-12.coffee index d970896..a2abea0 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" @@ -8,13 +7,15 @@ 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" - handlers = + handlers = waitAroundForABit: -> initialMessage = "Waiting" progressView = Progress @@ -34,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 @@ -42,7 +44,7 @@ module.exports = (os) -> 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" @@ -51,7 +53,7 @@ module.exports = (os) -> 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() @@ -68,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" @@ -93,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 @@ -108,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-02.coffee b/issues/2017-02.coffee new file mode 100644 index 0000000..b862408 --- /dev/null +++ b/issues/2017-02.coffee @@ -0,0 +1,190 @@ +Achievement = require "../lib/achievement" +Model = require "model" +Chateau = require "../apps/chateau" +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 + + container = document.createElement "container" + + 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" + + pages = + front: """ + a(href="#vista") + img(src="https://s-media-cache-ak0.pinimg.com/originals/a3/ba/56/a3ba56cef667d14b54023cd624d4e070.jpg") + """ + vista: """ + a(href="#table") + 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(style="padding: 1em;") + 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 + li + a(href="#cheevos") Cheevos + li + a(href="#contributors") Contributors + """ + random: """ + 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)({}) + + pages.cheevos.appendChild system.Achievement.progressView() + + handlers = Model().include(Social).extend + area: -> + "2017-01" + mSAccess97: -> + app = Spreadsheet(system) + document.body.appendChild app.element + textEditor: -> + app = TextEditor(system) + document.body.appendChild app.element + pixiePaint: -> + app = PixiePaint(system) + document.body.appendChild app.element + chateau: -> + app = Chateau(system) + document.body.appendChild app.element + credits: -> + displayPage "contributors" + tableofContents: -> + displayPage "table" + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [T]ext Editor + [P]ixie Paint + #{Social.menuText} + H[e]lp + [T]able of Contents + [C]redits + """ + handlers: handlers + + windowView = Window + title: "ZineOS Volume 1 | Issue 2 | ENTER THE DUNGEON | February 2017" + content: container + menuBar: menuBar.element + width: 1228 + height: 936 + x: 32 + y: 32 + + windowView.element.addEventListener "click", (e) -> + anchor = parentElementOfType("a", e.target) + + if anchor + next = anchor.getAttribute('href') + + if next.match /^\#/ + e.preventDefault() + page = next.substr(1) + + displayPage(page) + + 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" + + if page is "cheevos" + system.Achievement.unlock "Check yo' self" + + emptyElement(container) + container.appendChild(pages[page]) + + currentPage = page + + displayPage currentPage + + 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() 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/achievement.coffee b/lib/achievement.coffee new file mode 100644 index 0000000..315de5c --- /dev/null +++ b/lib/achievement.coffee @@ -0,0 +1,37 @@ +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 + return pending.push(options) + + options.title ?= "Achievement Unlocked" + + achievementElement = AchievementTemplate options + document.body.appendChild achievementElement + achievementElement.classList.add "display" + achievementElement.appendChild playSound() + + displaying = true + + achievementElement.addEventListener "animationend", (e) -> + achievementElement.remove() + + displaying = false + if pending.length + Achievement.display(pending.shift()) + + , false + + return achievementElement 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 new file mode 100644 index 0000000..7993784 --- /dev/null +++ b/lib/drop.coffee @@ -0,0 +1,9 @@ +module.exports = (element, handler) -> + cancel = (e) -> + e.preventDefault() + return false + + element.addEventListener "dragover", cancel + element.addEventListener "dragenter", cancel + element.addEventListener "drop", (e) -> + handler(e) 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/lib/hamlet.js b/lib/hamlet.js new file mode 100644 index 0000000..9445320 --- /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) +}); 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 new file mode 100644 index 0000000..55c1896 --- /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/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/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 cf1aadb..663483c 100644 --- a/main.coffee +++ b/main.coffee @@ -1,11 +1,45 @@ +require("analytics").init("UA-3464282-16") + require "./extensions" -OS = require "../os" -global.system = os = OS() +require "./lib/outbound-clicks" +require "./lib/error-reporter" + +global.Hamlet = require "./lib/hamlet" + +System = require "./system" +global.system = System() +system.PACKAGE = PACKAGE # For debugging -{Style} = os.UI +{Style} = system.UI style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style -require("./issues/2016-12-10")(os) +# 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() + +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-03")() diff --git a/os.coffee b/os.coffee deleted file mode 100644 index 776e2a6..0000000 --- a/os.coffee +++ /dev/null @@ -1,75 +0,0 @@ -# DexieDB Containing our FS -DexieFSDB = (dbName='fs') -> - db = new Dexie dbName - - db.version(1).stores - files: 'path, blob, size, type, createdAt, updatedAt' - - return db - -# FS Wrapper to DB -DexieFS = (db) -> - Files = db.files - - 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 - - delete: (path) -> - Files.delete(path) - - list: (dir) -> - Files.where("path").startsWith(dir).toArray() - .then (results) -> - uniq results.map ({path}) -> - path = path.replace(dir, "").replace(/\/.*$/, "/") - -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') -> - self = {} - - fs = DexieFS(DexieFSDB(dbName)) - - Object.assign self, - fs: fs - - readFile: (path) -> - fs.read(path) - .then ({blob}) -> - blob - - readAsText: (path) -> - self.readFile(path) - .then readAsText - - readAsJSON: (path) -> - self.readAsText(path) - .then JSON.parse - - writeFile: fs.write - - UI: UI - - return self diff --git a/os/file-io.coffee b/os/file-io.coffee index 71dfd12..9bf2453 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -2,49 +2,72 @@ # 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. 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 system.readFile - .then self.loadFile + 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 + system.writeFile currentPath(), blob, true + .then -> + 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 9bb06a5..282d8c6 100644 --- a/pixie.cson +++ b/pixie.cson @@ -1,7 +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" - ui: "STRd6/ui:master" + postmaster: "distri/postmaster:v0.5.3" + ui: "STRd6/ui:v0.1.9" 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" + "https://sdk.amazonaws.com/js/aws-sdk-2.7.20.min.js" ] 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/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 new file mode 100644 index 0000000..2ea5c9c --- /dev/null +++ b/social/social.coffee @@ -0,0 +1,50 @@ +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.glitch.me/comments" + data: JSON.stringify(data) + headers: + "Content-Type": "application/json" + method: "POST" + .then -> + self.viewComments() + + viewComments: -> + ajax.getJSON "https://whimsy-space.glitch.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: -> + system.Achievement.unlock "Do you 'like' like me?" + window.open "https://www.facebook.com/whimsyspace/" + subscribe: -> + require("../mailchimp").show() + +module.exports.menuText = """ +S[o]cial Media + [V]iew Comments + [C]omment + [L]ike + [S]ubscribe +""" diff --git a/stories/around-the-world.coffee b/stories/around-the-world.coffee new file mode 100644 index 0000000..32202bb --- /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)] +""" 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 new file mode 100644 index 0000000..7607400 --- /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!! +""" 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 new file mode 100644 index 0000000..968895d --- /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. +""" 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 177cef4..5140e65 100644 --- a/style.styl +++ b/style.styl @@ -1,3 +1,21 @@ +body + color: rgba(0, 0, 0, 0.87) + overflow: hidden + +h1 + line-height: 1.25em + margin: 0 + +ul + list-style-type: square + padding-left: 1.25em + +user-select(s) + user-select: s + -moz-user-select: s + -webkit-user-select: s + -ms-user-select: s + comments, comment display: block padding: 1em @@ -16,3 +34,231 @@ 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 > & + background-image: url(https://i.imgur.com/hKOGoex.jpg) + background-size: cover + + > file, > folder + display: inline-block + padding: 0.5em + + > icon + 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==") + + > 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==") + +#modal > container + padding: 1em + +chateau + width: 100% + height: 100% + position: relative + + > word-area + display: block + pointer-events: none + position: absolute + 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 + width: 100% + height: 100% + position: absolute + + > form + position: absolute + bottom: 0 + + window > viewport > & + margin: initial + +window > viewport > audio + width: 100% + +@keyframes display-achievement + 0% + bottom: -128px + 5% + bottom: 2em + 95% + bottom: 2em + 100% + bottom: -128px + +@keyframes slide-left + 0% + left: 100% + 25% + left: 0 + 50% + left: -100% + 100% + left: -100% + +achievement + color: white + background-color: blue + border-radius: 32px + bottom: -128px + box-shadow: 2px 2px 10px black + display: block + 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 + + > 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 + font-family: "Apple Color Emoji","Segoe UI Emoji","NotoColorEmoji","Segoe UI Symbol","Android Emoji","EmojiSymbols" + height: 48px + line-height: 48px + 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 + +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 new file mode 100644 index 0000000..3580f4d --- /dev/null +++ b/system.coffee @@ -0,0 +1,187 @@ +{fileSeparator, normalizePath} = require "./util" + +# DexieDB Containing our FS +DexieFSDB = (dbName='fs') -> + db = new Dexie dbName + + db.version(1).stores + files: 'path, blob, size, type, createdAt, updatedAt' + + return db + +DexieFS = require "./lib/dexie-fs" +MountFS = require "./lib/mount-fs" + +uniq = (array) -> + Array.from new Set array + +Ajax = require "ajax" +Model = require "model" +Achievement = require "./system/achievement" +Associations = require "./system/associations" +SystemModule = require "./system/module" +Template = require "./system/template" +UI = require "ui" + +module.exports = (dbName='zine-os') -> + self = Model() + + 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 + + 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) + + 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" + + path = normalizePath "/#{path}" + fs.write path, blob + + deleteFile: (path) -> + path = normalizePath "/#{path}" + fs.delete(path) + + 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) + .then (file) -> + file.readAsText() + .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" + + 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 new file mode 100644 index 0000000..253a54c --- /dev/null +++ b/system/achievement.coffee @@ -0,0 +1,258 @@ +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 + +achievementData = [{ + text: "Issue 1" + icon: "📰" + group: "Issue 1" + description: "View Issue 1" +}, { + text: "Cover-2-cover" + 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: "📰" + group: "Issue 2" + description: "View Issue 2" +}, { + text: "Lol wut" + icon: "😂" + group: "Issue 2" + description: "Did you know Windows Vista had a magazine?" +}, { + 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" +}, { + text: "The dungeon is in our heart" + 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: "📝" + group: "App" + description: "Launch a text editor" +}, { + text: "Pump up the jam" + icon: "🎶" + group: "App" + description: "Launch audio application" +}, { + text: "Microsoft Access 97" + icon: "🔞" + group: "App" + description: "Launch a spreadsheet application" +}, { + text: "Look at that" + 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: "💾" + 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" +}, { + text: "I AM ERROR" + 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 = -> + 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 + + if opts and !opts.achieved + opts.achieved = true + + 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] + + emptyElement content + elements.forEach (element) -> + content.appendChild(element) + + return content + + updateStatus = -> + self.Achievement.groupData groupBy(achievementData, "group") + updateStatus() + + return self diff --git a/system/associations.coffee b/system/associations.coffee new file mode 100644 index 0000000..4e223c3 --- /dev/null +++ b/system/associations.coffee @@ -0,0 +1,306 @@ +AppDrop = require "../lib/app-drop" + +# TODO: Move handlers out +AudioBro = require "../apps/audio-bro" +Filter = require "../apps/filter" +Notepad = require "../apps/notepad" +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() + + if file + {path} = file + system.readFile path + .then (blob) -> + app.loadFile(blob, path) + + system.attachApplication(app) + +module.exports = (I, self) -> + # 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 = [{ + 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$/) or + file.path.match(/\.coffee$/) + fn: (file) -> + self.execute(file.path) + }, { + name: "Explore" + filter: (file) -> + file.path.match(/💾$/) + fn: (file) -> + 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: "📂" + + document.body.appendChild windowView.element + }, { + name: "Run" + filter: (file) -> + file.path.match(/💾$/) + fn: (file) -> + # TODO: Rename? + system.execPathWithFile file.path, null + }, { + 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.match(/^application\/javascript/) + fn: openWith(Notepad) + }, { + name: "Spreadsheet" + filter: (file) -> + # TODO: This actually only handles JSON arrays + file.type.match(/^application\/json/) + fn: openWith(Spreadsheet) + }, { + name: "Image Viewer" + filter: (file) -> + file.type.match /^image\// + fn: openWith(Filter) + }, { + name: "Pixel Editor" + 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) -> + file.type.match /^audio\// + fn: openWith(AudioBro) + }, { + name: "dsad.exe" + filter: (file) -> + file.path.match /dsad\.exe$/ + fn: -> + app = DSad() + system.attachApplication app + }, { + name: "zine1.exe" + filter: (file) -> + file.path.match /zine1\.exe$/ + fn: -> + require("../issues/2016-12")() + }, { + name: "zine2.exe" + filter: (file) -> + file.path.match /zine2\.exe$/ + fn: -> + require("../issues/2017-02")() + }, { + name: "zine3.exe" + filter: (file) -> + file.path.match /zine3\.exe$/ + fn: -> + 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 + # 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}" + + 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 new file mode 100644 index 0000000..8c8926a --- /dev/null +++ b/system/module.coffee @@ -0,0 +1,496 @@ +# Handles loading and launching files from the fs +# +# 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. + + Additional properties such as a reference to the global object and some metadata + are exposed. + + 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 per include root. + + Circular includes will never reslove + # TODO: Fail early on circular includes, challenging because of async + + # Currently can require + # js, coffee, jadelet, json, cson + + # Requiring other file types returns a Blob + + ### + + { + 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 + rewriteRequires = (program) -> + id = 0 + namePrefix = "__req" + requires = {} + + # 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 + tmpVar = "#{namePrefix}#{id}" + id += 1 + requires[key] = tmpVar + + return tmpVar + + tmpVars = Object.keys(requires).map (name) -> + requires[name] + + requirePaths = Object.keys(requires) + requirePaths = requirePaths + + """ + return system.vivifyPrograms(#{JSON.stringify(requirePaths)}) + .then(function(__reqResults) { + (function(#{tmpVars.join(', ')}){ + #{rewrittenProgram} + }).apply(this, __reqResults); + }); + """ + + loadModule = (content, path, state) -> + new Promise (resolve, reject) -> + program = annotateSourceURL(rewriteRequires(content), path) + dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) or fileSeparator + + 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) + + # Apply relative path wrapper for system.vivifyPrograms + localSystem = Object.assign {}, self, + vivifyPrograms: (moduleIdentifiers) -> + absoluteIdentifiers = moduleIdentifiers.map (identifier) -> + if isAbsolutePath(identifier) + absolutizePath "/", identifier + else if isRelativePath(identifier) + absolutizePath dirname, identifier + else + identifier + + self.vivifyPrograms absoluteIdentifiers, state + # TODO: Also make working directory relative paths for readFile and writeFile + + context = + system: localSystem + global: global + module: module + exports: module.exports + __filename: path + __dirname: dirname + + args = Object.keys(context) + values = args.map (name) -> context[name] + + Promise.resolve() + .then -> + Function(args..., program).apply(module, values) + .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 '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 be the same for a single + # app or process + vivifyPrograms: (absolutePaths, state={}) -> + state.cache ?= {} + + Promise.all absolutePaths.map (absolutePath) -> + state.cache[absolutePath] ?= self.loadProgram(absolutePath) + .then (sourceProgram) -> + # 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} + //# sourceURL=#{path} + """ 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)() 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 diff --git a/templates/achievement.jadelet b/templates/achievement.jadelet new file mode 100644 index 0000000..a815f32 --- /dev/null +++ b/templates/achievement.jadelet @@ -0,0 +1,4 @@ +achievement + icon= @icon + h2= @title + p= @text diff --git a/templates/chateau.jadelet b/templates/chateau.jadelet new file mode 100644 index 0000000..daf5d7a --- /dev/null +++ b/templates/chateau.jadelet @@ -0,0 +1,7 @@ +chateau + = @canvas + word-area + = @words + form(@submit) + input + button Speak diff --git a/templates/file.jadelet b/templates/file.jadelet new file mode 100644 index 0000000..dc88da2 --- /dev/null +++ b/templates/file.jadelet @@ -0,0 +1,3 @@ +file(draggable="true" @dragstart @dblclick @contextmenu @path @type) + icon + label= @displayName diff --git a/templates/folder.jadelet b/templates/folder.jadelet new file mode 100644 index 0000000..0d00839 --- /dev/null +++ b/templates/folder.jadelet @@ -0,0 +1,3 @@ +folder(draggable="true" @dragstart @dblclick @contextmenu @path) + icon + 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/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) 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 new file mode 100644 index 0000000..68c2e74 --- /dev/null +++ b/test/system/module.coffee @@ -0,0 +1,181 @@ +require "../../extensions" +Model = require "model" +Associations = require "../../system/associations" +SystemModule = require "../../system/module" + +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 + + return model + +describe "System Module", -> + it "should vivifyPrograms in files asynchronously", -> + model = makeSystemFS + "/test.js": """ + module.exports = 'yo'; + """ + "/root.js": """ + 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"; + """ + "/rand.js": """ + module.exports = Math.random(); + """ + + 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 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() + + it "should throw an error when requiring a file that throws an error", (done) -> + @timeout 250 + + 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") + """ + "/b.js": """ + module.exports = require("./a.js") + """ + + model.vivifyPrograms(["/a.js"]) + .then ([a]) -> + # Never get here + assert false + + setTimeout -> + done() + , 100 + + it "should work even if the file doesn't assign to module.exports", -> + model = makeSystemFS + "/wat.js": """ + exports.yolo = "wat"; + """ + + model.vivifyPrograms ["/wat.js"] + .then ([wat]) -> + assert.equal wat.yolo, "wat" + + 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.vivifyPrograms ["/main.js"] + .then ([main]) -> + assert.equal main, "b" + + 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.vivifyPrograms ["/main.js"] + .then ([main]) -> + assert.equal main, "b" + + it "should require .jadelet sources", -> + model = makeSystemFS + "/main.coffee": """ + template = require "./button.jadelet" + + module.exports = + buttonTemplate: template + """ + "/button.jadelet": """ + button(@click)= @text + """ + + model.vivifyPrograms ["/main.coffee"] + .then ([main]) -> + assert typeof main.buttonTemplate is "function" diff --git a/util.coffee b/util.coffee new file mode 100644 index 0000000..c9ccfba --- /dev/null +++ b/util.coffee @@ -0,0 +1,126 @@ +fileSeparator = "/" + +normalizePath = (path) -> + 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) -> + "