From 81c345d76abe3b8483328afccaf4bd3f23c7ac6d Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 23 Jan 2017 18:02:01 -0800 Subject: [PATCH 1/4] This commit intentionally left blank From c2bf45e9efc240c82768ab45d2a908c4df649432 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Mon, 30 Jan 2017 15:06:01 -0800 Subject: [PATCH 2/4] :spaghetti::hibiscus: Updated at https://danielx.net/editor/ --- main.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.coffee b/main.coffee index 1d4c9c8..ce35592 100644 --- a/main.coffee +++ b/main.coffee @@ -20,4 +20,4 @@ Explorer = require "./apps/explorer" document.body.appendChild Explorer() # Launch Current Issue -require("./issues/2017-02")() +require("./issues/2016-12")() From 731222d879a9883d99a2437a1ca114f7794e57a9 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 21 May 2017 10:57:09 -0700 Subject: [PATCH 3/4] This commit intentionally left blank From acaf83c367d10cf8f6764a52f41ec1f03bc7dace Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Sun, 21 May 2017 10:58:21 -0700 Subject: [PATCH 4/4] :high_brightness::anguished: Updated at https://danielx.net/editor/ --- README.md | 40 ++- TODO.md | 32 ++- apps/achievement-status.coffee | 15 ++ apps/audio-bro.coffee | 12 +- apps/chateau.coffee | 309 ++-------------------- apps/contrasaurus.coffee | 17 ++ apps/dungeon-of-sadness.coffee | 18 +- apps/explorer.coffee | 138 ++++++++-- apps/markdown.coffee | 79 +++++- apps/my-briefcase.coffee | 172 +++++++++++++ apps/notepad.coffee | 42 ++- apps/pixel.coffee | 43 +--- apps/story-reader.coffee | 16 ++ apps/text-editor.coffee | 67 ++++- extensions.coffee | 43 +++- feedback.coffee | 17 ++ issues/2016-12.coffee | 6 +- issues/2017-02.coffee | 18 +- issues/2017-03.coffee | 131 ++++++++++ issues/2017-04.coffee | 174 +++++++++++++ issues/2017-05.coffee | 99 +++++++ lib/app-drop.coffee | 35 +++ lib/dexie-fs.coffee | 65 +++++ lib/drop.coffee | 2 - lib/hamlet.js | 2 +- lib/iframe-app.coffee | 88 +++++++ lib/mount-fs.coffee | 75 ++++++ lib/outbound-clicks.coffee | 2 +- lib/pkg-fs.coffee | 105 ++++++++ lib/s3-fs.coffee | 171 +++++++++++++ lib/stylus.min.js | 6 + lib/system-client.coffee | 40 +++ main.coffee | 24 +- os/file-io.coffee | 68 +++-- pixie.cson | 10 +- presenters/home-button.coffee | 112 ++++++++ social/social.coffee | 6 +- stories/blue-light-special.coffee | 96 +++++++ stories/crescent.coffee | 13 + stories/dungeon-dog.coffee | 6 +- stories/izzy.coffee | 61 +++++ stories/marigold.coffee | 87 +++++++ stories/provision.coffee | 2 +- stories/residue.coffee | 79 ++++++ style.styl | 59 ++++- system.coffee | 165 ++++++++---- system/achievement.coffee | 90 +++++++ system/associations.coffee | 247 +++++++++++++++--- system/module.coffee | 412 ++++++++++++++++++++++++++++-- templates/file.jadelet | 4 +- templates/folder.jadelet | 4 +- templates/home-button.jadelet | 1 + templates/site-url.jadelet | 1 + templates/version.jadelet | 2 + test/system/module.coffee | 176 +++++++++---- util.coffee | 104 +++++++- 56 files changed, 3290 insertions(+), 618 deletions(-) create mode 100644 apps/achievement-status.coffee create mode 100644 apps/contrasaurus.coffee create mode 100644 apps/my-briefcase.coffee create mode 100644 apps/story-reader.coffee create mode 100644 feedback.coffee create mode 100644 issues/2017-03.coffee create mode 100644 issues/2017-04.coffee create mode 100644 issues/2017-05.coffee create mode 100644 lib/app-drop.coffee create mode 100644 lib/dexie-fs.coffee create mode 100644 lib/iframe-app.coffee create mode 100644 lib/mount-fs.coffee create mode 100644 lib/pkg-fs.coffee create mode 100644 lib/s3-fs.coffee create mode 100644 lib/stylus.min.js create mode 100644 lib/system-client.coffee create mode 100644 presenters/home-button.coffee create mode 100644 stories/blue-light-special.coffee create mode 100644 stories/crescent.coffee create mode 100644 stories/izzy.coffee create mode 100644 stories/marigold.coffee create mode 100644 stories/residue.coffee create mode 100644 templates/home-button.jadelet create mode 100644 templates/site-url.jadelet create mode 100644 templates/version.jadelet diff --git a/README.md b/README.md index 2686eea..767cb13 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# zine +# Zine OS + DIY E-Zine and Operating System + +Interfaces +========== + +FS Interface +------------ + +Read a blob from a path, returns a promise fulfilled with the blob object. The +blob is annotated with the path i.e.: blob.path == path + + read: (path) -> + +Write a blob to a path, returns a promise that is fulfilled when the write succeeds. + + write: (path, blob) -> + +Delete a file at a path, returns a promise that is fulfilled when the delete succeeds. + + delete: (path) -> + +Returns a promise + + list: (directoryPath) -> + + +FileEntry Interface +------------------- + + path: + size: + type: + +FolderEntry Interface +--------------------- + + folder: true + path: diff --git a/TODO.md b/TODO.md index 9fc0f2f..7573cd5 100644 --- a/TODO.md +++ b/TODO.md @@ -3,25 +3,27 @@ TODO System Features --------------- -[x] File Browser +[X] File Browser -[x] App Associations +[X] App Associations -[x] File Context Menu +[X] File Context Menu -[x] Folders +[X] Folders -[x] Desktop Icons +[X] Desktop Icons [ ] Desktop Background [ ] Help Documentation -[ ] Compiler +[X] Compilerz -[x] Require/Include Local Files +[X] Require/Include Local Files -[ ] Custom Models/Views +[ ] Drag 'n' Drop + +[X] Cloud Briefcase Applications ------------ @@ -38,13 +40,21 @@ Markdown / Wiki Image Munger +Programatic Animator + +Pixel Editor + +Music Maker + +Database? + Network Social -------------- -[ ] CDN - [x] Comments [ ] Sharing -[ ] Remote Files +[ ] Remote Files (Network Neighborhood) + +[X] Personal Homepage diff --git a/apps/achievement-status.coffee b/apps/achievement-status.coffee new file mode 100644 index 0000000..b8bd0fd --- /dev/null +++ b/apps/achievement-status.coffee @@ -0,0 +1,15 @@ +module.exports = -> + {Achievement, UI} = system + {Window} = UI + + cheevoElement = Achievement.progressView() + cheevoElement.style.width = "100%" + cheevoElement.style.padding = "1em" + + Achievement.unlock "Check yo' self" + + windowView = Window + title: "Cheevos" + content: cheevoElement + width: 640 + height: 480 diff --git a/apps/audio-bro.coffee b/apps/audio-bro.coffee index ae3e9f8..1fb6679 100644 --- a/apps/audio-bro.coffee +++ b/apps/audio-bro.coffee @@ -5,7 +5,7 @@ Model = require "model" module.exports = -> # Global system - {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {ContextMenu, MenuBar, Modal, Observable, Progress, Util:{parseMenu}, Window} = system.UI {Achievement} = system Achievement.unlock "Pump up the jam" @@ -14,8 +14,11 @@ module.exports = -> audio.controls = true audio.autoplay = true + filePath = Observable() + handlers = Model().include(FileIO).extend loadFile: (blob) -> + filePath blob.path audio.src = URL.createObjectURL blob exit: -> @@ -31,11 +34,16 @@ module.exports = -> handlers: handlers windowView = Window - title: "Audio Bro" + title: -> + if path = filePath() + "Audio Bro - #{path}" + else + "Audio Bro" content: audio menuBar: menuBar.element width: 308 height: 80 + iconEmoji: "🎶" windowView.loadFile = handlers.loadFile diff --git a/apps/chateau.coffee b/apps/chateau.coffee index bee9520..6f9ebfc 100644 --- a/apps/chateau.coffee +++ b/apps/chateau.coffee @@ -1,293 +1,24 @@ -# Chat Based MUD - -Drop = require "../lib/drop" -FileIO = require "../os/file-io" -Model = require "model" - -Template = require "../templates/chateau" - -sortBy = (attribute) -> - (a, b) -> - a[attribute] - b[attribute] - -rand = (n) -> - Math.floor(Math.random() * n) - -createSocket = (room, accountId) -> - new WebSocket("wss://message-relay.gomix.me/r/#{room}?accountId=#{accountId}") - -setLocal = (data) -> - localStorage.chateau = JSON.stringify(data) - -getLocal = -> - try - JSON.parse localStorage.chateau +IFrameApp = require "../lib/iframe-app" module.exports = -> - # Global system - {ContextMenu, MenuBar, Modal, Observable, Progress, Util:{parseMenu}, Window} = system.UI - - wordsArray = Observable [] - connected = Observable false - - avatars = {} - myAccountId = null - - localData = getLocal() - - if localData - myAccountId = localData.myAccountId - avatars[myAccountId] = localData.myAvatar - else - myAccountId = "Anon" + Math.random().toString().substr(3) - - addAvatar = (accountId, url) -> - avatars[accountId] = - img: null - color: "orange" - x: rand canvas.width - y: rand canvas.height - - updateWords = -> - wordsElements = Object.keys(avatars).map (key) -> - avatars[key] - .filter ({say}) -> - say - .map ({x, y, say}) -> - words = document.createElement "words" - words.style.top = "#{y - 50}px" - words.style.left = "#{x}px" - words.innerText = say - - return words - - wordsArray wordsElements - - setAvatar = (blob) -> - return unless myAvatar = avatars[myAccountId] - - Image.fromBlob(blob) - .then (img) -> - myAvatar.img = img - - blob.readAsDataURL() - .then (url) -> - myAvatar.dataURL = url - - broadcast - avatar: url - - broadcast = (data) -> - socket.send JSON.stringify - type: "broadcast" - message: data - - directMessage = (data, accountId) -> - socket.send JSON.stringify - type: "dm" - recipient: accountId - message: data - - updateNewcomer = (accountId) -> - return unless myAvatar = avatars[myAccountId] - - data = - move: - x: myAvatar.x - y: myAvatar.y - avatar: myAvatar.dataURL - say: myAvatar.say - - directMessage(data, accountId) - - canvas = document.createElement 'canvas' - context = canvas.getContext('2d') - - canvas.onclick = (e) -> - {pageX, pageY, currentTarget} = e - {top, left} = currentTarget.getBoundingClientRect() - - x = pageX - left - y = pageY - top - - broadcast - move: - x: x - y: y - - content = Template - canvas: canvas - connected: connected - words: wordsArray - submit: (e) -> - e.preventDefault() - - input = content.querySelector('input') - words = input.value - if words - input.value = "" - - broadcast - say: words - - Drop content, (e) -> - files = e.dataTransfer.files - - if files.length - file = files[0] - - setAvatar(file) - - socket = createSocket("cshopmilli", myAccountId) + {Achievement} = system - setInterval -> - try - socket.send JSON.stringify - meta: "keepalive" - , 30000 - - socket.onopen = -> - - - socket.onclose = -> - connected false - - socket.onmessage = (e) -> - message = JSON.parse e.data - console.log message - - accountId = message.accountId - - switch message.type - when "meta" - if message.status is "connect" - connected true - myAccountId = accountId - when "connect" - # Add Avatar - addAvatar(accountId) - updateNewcomer(accountId) - when "disconnect" - # Remove Avatar - delete avatars[accountId] - updateWords() - when "broadcast", "dm" - {message} = message - - receiveMessage(message, accountId) - - receiveMessage = (message, accountId) -> - avatars[accountId] ?= - x: rand canvas.width - y: rand canvas.height - color: "orange" - - if message.move - {x, y} = message.move - avatars[accountId].x = x - avatars[accountId].y = y - - if message.say - avatars[accountId].say = message.say - - if message.avatar - img = new Image() - img.src = message.avatar - - avatars[accountId].img = img - - updateWords() - - AboutTemplate = system.compileTemplate """ - container - h1 About - p Chat with your friends in this online chateau! - - p Drag and drop an image to become your avatar. - - p Click to position yourself in the room. - - p Say what you want to talk with others! - """ - - handlers = Model().include(FileIO).extend - exit: -> - windowView.element.remove() - - about: -> - Modal.show AboutTemplate() - - menuBar = MenuBar - items: parseMenu """ - [F]ile - E[x]it - [H]elp - [A]bout - """ - handlers: handlers - - windowView = Window + app = IFrameApp + src: "https://danielx.net/chateau/" + width: 960 + height: 540 title: "Chateau" - content: content - menuBar: menuBar.element - width: 640 - height: 480 - - roomstate = - background: null - objects: [] - - repaint = -> - # Draw BG - context.fillStyle = 'blue' - context.fillRect(0, 0, canvas.width, canvas.height) - - {background, objects} = roomstate - if background - context.drawImage(background, 0, 0, canvas.width, canvas.height) - - # Draw Avatars/Objects - Object.keys(avatars).map (accountId) -> - avatars[accountId] - .concat(objects).sort(sortBy("z")).forEach ({color, img, x, y}) -> - if img - {width, height} = img - context.drawImage(img, x - width / 2, y - height / 2) - else - context.fillStyle = color - context.fillRect(x - 25, y - 25, 50, 50) - - # Draw connection status - if connected() - indicatorColor = "green" - else - indicatorColor = "red" - - context.beginPath() - context.arc(canvas.width - 20, 20, 10, 0, 2 * Math.PI, false) - context.fillStyle = indicatorColor - context.fill() - context.lineWidth = 2 - context.strokeStyle = '#003300' - context.stroke() - - resize = -> - rect = canvas.getBoundingClientRect() - canvas.width = rect.width - canvas.height = rect.height - - windowView.on "resize", resize - - windowView.loadFile = handlers.loadFile - - animate = -> - requestAnimationFrame animate - repaint() - - animate() - - # TODO: ViewDidLoad? or equivalent event? - setTimeout -> - resize() - - return windowView + iconEmoji: "🍷" + + app.on "event", (name) -> + switch name + when "login" + Achievement.unlock "Enter the Chateau" + when "custom-avatar" + Achievement.unlock "Puttin' on the Ritz" + when "custom-background" + Achievement.unlock "Paint the town red" + when "file-upload" + Achievement.unlock "It's in the cloud" + + return app diff --git a/apps/contrasaurus.coffee b/apps/contrasaurus.coffee new file mode 100644 index 0000000..acc9a2f --- /dev/null +++ b/apps/contrasaurus.coffee @@ -0,0 +1,17 @@ +module.exports = -> + {Achievement, iframeApp} = system + + app = iframeApp + src: "https://contrasaur.us/" + width: 960 + height: 540 + title: "Contrasaurus: Defender of the American Dream" + + Achievement.unlock "Rawr" + + app.on "event", (name) -> + switch name + when "win" + Achievement.unlock "A winner is you" + + return app diff --git a/apps/dungeon-of-sadness.coffee b/apps/dungeon-of-sadness.coffee index 99123ff..ddaf317 100644 --- a/apps/dungeon-of-sadness.coffee +++ b/apps/dungeon-of-sadness.coffee @@ -1,18 +1,12 @@ -Model = require "model" - module.exports = -> - {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI - - frame = document.createElement "iframe" - frame.src = "https://danielx.net/ld33/" + {Achievement, iframeApp} = system - system.Achievement.unlock "The dungeon is in our heart" - - windowView = Window + app = iframeApp title: "Dungeon of Sadness" - content: frame - menuBar: null + src: "https://danielx.net/ld33/" width: 648 height: 507 - return windowView + Achievement.unlock "The dungeon is in our heart" + + return app diff --git a/apps/explorer.coffee b/apps/explorer.coffee index 31ab27b..1b8b9e3 100644 --- a/apps/explorer.coffee +++ b/apps/explorer.coffee @@ -1,10 +1,9 @@ # Explorer File Browser # # Explore the file system like adventureres of old! -# TODO: Drag and drop files and folders +# TODO: Drop files onto applications # TODO: Select multiple # TOOD: Keyboard Input -# TODO: Style file types Drop = require "../lib/drop" FileTemplate = require "../templates/file" @@ -12,20 +11,75 @@ FolderTemplate = require "../templates/folder" {emptyElement} = require "../util" +extractPath = (element) -> + while element + path = element.getAttribute("path") + return path if path + element = element.parentElement + module.exports = Explorer = (options={}) -> {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI {path} = options path ?= '/' explorer = document.createElement "explorer" + explorer.setAttribute("path", path) Drop explorer, (e) -> + return if e.defaultPrevented + + targetPath = extractPath(e.target) or path + folderTarget = targetPath.match(/\/$/) + + fileSelectionData = e.dataTransfer.getData("zineos/file-selection") + + if fileSelectionData + data = JSON.parse fileSelectionData + + if folderTarget + system.moveFileSelection(data, targetPath) + else + # Attempt to open file in app + selectedFile = data.files[0] + console.log "Open in app #{targetPath} <- #{selectedFile}" + system.readFile(selectedFile.path) + .then (file) -> + system.execPathWithFile(targetPath, file) + e.preventDefault() + + return + files = e.dataTransfer.files if files.length - files.forEach (file) -> - newPath = path + file.name - system.writeFile(newPath, file, true) + e.preventDefault() + if folderTarget + files.forEach (file) -> + newPath = targetPath + file.name + system.writeFile(newPath, file, true) + else + file = files[0] + system.execPathWithFile(targetPath, file) + + explorerContextMenu = ContextMenu + items: parseMenu """ + [N]ew File + """ + handlers: + newFile: -> + Modal.prompt "Filename", "#{path}newfile.txt" + .then (newFilePath) -> + if newFilePath + system.writeFile newFilePath, new Blob [], type: "text/plain" + + explorer.oncontextmenu = (e) -> + return if e.defaultPrevented + e.preventDefault() + + explorerContextMenu.display + inElement: document.body + x: e.pageX + y: e.pageY contextMenuFor = (file, e) -> return if e.defaultPrevented @@ -34,7 +88,6 @@ module.exports = Explorer = (options={}) -> contextMenuHandlers = open: -> system.open(file) - openWith: -> #TODO cut: -> #TODO copy: -> #TODO delete: -> @@ -43,9 +96,12 @@ module.exports = Explorer = (options={}) -> Modal.prompt "Filename", file.path .then (newPath) -> if newPath - system.deleteFile(file.path) - system.writeFile(newPath, file.blob) - properties: -> #TODO + system.moveFile(file.path, newPath) + properties: -> + pre = document.createElement "pre" + pre.textContent = JSON.stringify(file, null, 2) + pre.style = "padding: 1rem" + Modal.show pre editMIMEType: -> Modal.prompt "MIME Type", file.type .then (newType) -> @@ -115,10 +171,24 @@ module.exports = Explorer = (options={}) -> handlers: open: -> addWindow(folder.path) - delete: -> # TODO: Delete all files under folder + delete: -> + system.readTree(folder.path) + .then (results) -> + Promise.all results.map (result) -> + system.deleteFile(result.path) rename: -> - ;# TODO: Rename all files under folder (!) - # May want to think about inodes or something that makes this simpler + Modal.prompt "Name", folder.path + .then (newName) -> + return unless newName + + # Ensure trailing slash + newName = newName.replace(/\/*$/, "/") + + system.readTree(folder.path) + .then (files) -> + Promise.all files.map (file) -> + newPath = file.path.replace(folder.path, newName) + system.moveFile(file.path, newPath) properties: -> # TODO contextMenu.display @@ -134,34 +204,51 @@ module.exports = Explorer = (options={}) -> addedFolders = {} files.forEach (file) -> - if file.relativePath.match /\// # folder - folderPath = file.relativePath.replace /\/.*$/, "" + if file.relativePath.match /\/$/ # folder + folderPath = file.relativePath addedFolders[folderPath] = true return - file.dblclick = -> - console.log "dblclick", file - system.open file + Object.assign file, + displayName: file.relativePath + + dblclick: -> + system.open file + + contextmenu: (e) -> + contextMenuFor(file, e) - file.contextmenu = (e) -> - contextMenuFor(file, e) + dragstart: (e) -> + # Note: Blobs don't make it through the stringify + e.dataTransfer.setData "zineos/file-selection", JSON.stringify + sourcePath: path + files: [ file ] fileElement = FileTemplate file if file.type.match /^image\// - url = URL.createObjectURL file.blob - fileElement.querySelector('icon').style.backgroundImage = "url(#{url})" + file.blob.getURL() + .then (url) -> + icon = fileElement.querySelector('icon') + icon.style.backgroundImage = "url(#{url})" + icon.style.backgroundSize = "100%" + icon.style.backgroundPosition = "50%" explorer.appendChild fileElement - Object.keys(addedFolders).forEach (folderName) -> + Object.keys(addedFolders).reverse().forEach (folderName) -> folder = - path: "#{path}#{folderName}/" + path: "#{path}#{folderName}" relativePath: folderName + displayName: folderName.replace(/\/$/, "") contextmenu: (e) -> contextMenuForFolder(folder, e) dblclick: -> # Open folder in new window addWindow(folder.path) + dragstart: (e) -> + e.dataTransfer.setData "zineos/file-selection", JSON.stringify + sourcePath: folder.path.slice(0, folder.path.length - folder.relativePath.length) + files: [ folder ] folderElement = FolderTemplate folder explorer.insertBefore(folderElement, explorer.firstChild) @@ -174,9 +261,7 @@ module.exports = Explorer = (options={}) -> system.fs.on "update", (path) -> update() addWindow = (path) -> - element = document.createElement "container" - - element.appendChild Explorer + element = Explorer path: path windowView = Window @@ -185,6 +270,7 @@ module.exports = Explorer = (options={}) -> menuBar: null width: 640 height: 480 + iconEmoji: "📂" document.body.appendChild windowView.element diff --git a/apps/markdown.coffee b/apps/markdown.coffee index 1a706c9..d109cf1 100644 --- a/apps/markdown.coffee +++ b/apps/markdown.coffee @@ -3,24 +3,85 @@ FileIO = require "../os/file-io" Model = require "model" +{absolutizePath} = require "../util" + +isRelative = (url) -> + url.match /^\.\.?\// + module.exports = -> # Global system {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI container = document.createElement 'container' - container.style.padding = "1em" + container.style.padding = "1rem" + container.style.userSelect = "initial" + + baseDir = "" + navigationStack = [] + # TODO: Maintain a container stack to keep state + + navigateToPath = (path) -> + system.readFile(path) + .then (blob) -> + handlers.loadFile(blob, path) + + rewriteURL = (url) -> + Promise.resolve() + .then -> + if isRelative(url) + targetPath = absolutizePath baseDir, url + + system.urlForPath(targetPath) + else if url.match /^\// # Absolute paths + targetPath = absolutizePath "", url + system.urlForPath(targetPath) + else + url + + rewriteURLs = (container) -> + container.querySelectorAll("img").forEach (img) -> + url = img.getAttribute("src") + + if url + rewriteURL(url) + .then (url) -> + img.src = url handlers = Model().include(FileIO).extend - loadFile: (blob) -> + loadFile: (blob, path) -> + navigationStack.push path + baseDir = path.replace /\/[^/]*$/, "" + blob.readAsText() .then (textContent) -> - container.innerHTML = marked(textContent) + if path.match /\.html$/ + container.innerHTML = textContent + else + container.innerHTML = marked(textContent) + + rewriteURLs(container) saveData: -> exit: -> windowView.element.remove() + # Handle Links + container.addEventListener "click", (e) -> + [anchor] = e.path.filter (element) -> + element.nodeName is "A" + + if anchor + url = anchor.getAttribute("href") + + if isRelative(url) + e.preventDefault() + path = absolutizePath baseDir, url + + # Navigate to page + navigateToPath(path) + + menuBar = MenuBar items: parseMenu """ [F]ile @@ -37,6 +98,18 @@ module.exports = -> width: 720 height: 480 + windowView.element.setAttribute("tabindex", -1) + windowView.element.addEventListener "keydown", (e) -> + {key} = e + + if key is "Backspace" + e.preventDefault() + if navigationStack.length > 1 + navigationStack.pop() + + lastPath = navigationStack.pop() + navigateToPath(lastPath) + windowView.loadFile = handlers.loadFile return windowView diff --git a/apps/my-briefcase.coffee b/apps/my-briefcase.coffee new file mode 100644 index 0000000..a9a62b4 --- /dev/null +++ b/apps/my-briefcase.coffee @@ -0,0 +1,172 @@ +### +Use the madness that is Amazon Cognito to support a 'My Briefcase' functionality. + +This depends on having the AWS library available: +- https://sdk.amazonaws.com/js/aws-sdk-2.2.42.min.js + +This is where you can put the files that you want to access from the cloud. + +They'll live in the whimsy-fs bucket under the path to your aws user id. + +The subdomain -> s3 proxy will have a map from simple names to the crazy ids. + +The proxy will serve the /public folder in your 'briefcase'. You can put your +blog or apps or whatever there. The rest currently isn't 'private', but maybe +it should be. We can set the access control when uploading. + +Ideally the briefcase will be browsable like the local FS and you'll be able to +run files from it, load them in applications, save files there, and drag n drop +between them. +### + +Explorer = require "./explorer" +FileTemplate = require "../templates/file" +FolderTemplate = require "../templates/folder" + +S3FS = require "../lib/s3-fs" + +{emptyElement, pinvoke} = require "../util" + +window.onAmazonLoginReady = -> + amazon.Login.setClientId('amzn1.application-oa2-client.29b275f9076a406c90a66b025fab96bf') + +do (d=document) -> + r = d.createElement 'div' + r.id = "amazon-root" + d.body.appendChild r + a = d.createElement('script') + a.type = 'text/javascript' + a.async = true + a.id = 'amazon-login-sdk' + a.src = 'https://api-cdn.amazon.com/sdk/login1.js' + r.appendChild(a) + + +module.exports = -> + {Observable, Window} = system.UI + + system.Achievement.unlock "Oh no, my files!" + + LoginTemplate = system.compileTemplate """ + span(style="text-align: center; padding: 0 2em;") + h1 My Briefcase + p= @description + a#LoginWithAmazon(@click) + img(border="0" alt="Login with Amazon" src="https://images-na.ssl-images-amazon.com/images/G/01/lwa/btnLWA_gold_156x32.png" width="156" height="32") + """ + + LoadedTemplate = system.compileTemplate """ + section + h1 Connected! + p Loading files... + """ + + # Observable holding content element + content = Observable null + + receivedCredentials = -> + console.log AWS.config.credentials + id = AWS.config.credentials.identityId + + content LoadedTemplate() + + bucket = new AWS.S3 + params: + Bucket: "whimsy-fs" + + fs = S3FS(id, bucket) + + uuidToken = id.split(":")[1] + + system.fs.mount "/My Briefcase/", fs + + infoBlob = new Blob [""" + Welcome to Your Cloud Briefcase + =============================== + + Store your files in a magical cloud that floats between computers. + + Files stored in `My Briefcase/public` are available to anyone on the + internet. (Technically so are all the files in your cloud briefcase... + Security: Coming Soon™) + + But the ones in /public are easily accessible, like when computing was fun + again. [Check this out](https://#{uuidToken}.whimsy.space/info.md) + and see what I mean. + + You can get your own cool and non-ugly subdomain if you contact me (the + creator of this computing system). Just send me your id and the short + name you'd prefer. DM me in the friendsofjack slack or something. + """] , type: "text/markdown; charset=utf-8" + system.writeFile "/My Briefcase/public/info.md", infoBlob + system.writeFile "/My Briefcase/public/.keep", new Blob [] + + content Explorer + path: "/My Briefcase/" + + AWS.config.update + region: 'us-east-1' + + try + logins = JSON.parse localStorage.WHIMSY_FS_AWS_LOGIN + + AWS.config.credentials = new AWS.CognitoIdentityCredentials + IdentityPoolId: 'us-east-1:4fe22da5-bb5e-4a78-a260-74ae0a140bf9' + Logins: logins + + if logins + pinvoke AWS.config.credentials, "get" + .then receivedCredentials + .catch (e) -> + unless e.message is "Invalid login token." + console.warn e, e.message + + content LoginTemplate + description: -> + """ + Maintain access to your files across different machines. Publish + effortlessly to the internet. Your briefcase holds all of your hopes + and dreams in a magical cloud that is available anywhere there is an + internet connection. 💼 + """ + click: -> + options = { scope : 'profile' } + amazon.Login.authorize options, (resp) -> + if !resp.error + console.log resp + token = resp.access_token + creds = AWS.config.credentials + + logins = + 'www.amazon.com': token + localStorage.WHIMSY_FS_AWS_LOGIN = JSON.stringify(logins) + + creds.params.Logins = logins + + creds.expired = true + + queryUserInfo(token) + + pinvoke AWS.config.credentials, "get" + .then receivedCredentials + + windowView = Window + title: "My Briefcase" + width: 640 + height: 480 + content: content + iconEmoji: "💼" + + return windowView + +queryUserInfo = (token) -> + fetch "https://api.amazon.com/user/profile", + headers: + Authorization: "bearer #{token}" + Accept: "application/json" + .then (response) -> + response.json() + .then (json) -> + console.log json + .catch (e) -> + console.error e diff --git a/apps/notepad.coffee b/apps/notepad.coffee index 5302178..1d8933e 100644 --- a/apps/notepad.coffee +++ b/apps/notepad.coffee @@ -17,13 +17,21 @@ module.exports = -> textarea = document.createElement "textarea" textarea.spellcheck = false + initialValue = "" + + textarea.addEventListener "input", -> + handlers.saved textarea.value is initialValue + handlers = Model().include(FileIO).extend - loadFile: (blob) -> + loadFile: (blob, path) -> blob.readAsText() .then (text) -> - textarea.value = text + handlers.currentPath path + initialValue = text + textarea.value = initialValue newFile: -> - textarea.value = "" + initialValue = "" + textarea.value = initialValue saveData: -> data = new Blob [textarea.value], type: "text/plain" @@ -112,12 +120,36 @@ module.exports = -> handlers: handlers windowView = Window - title: "Notepad.exe" + title: -> + path = handlers.currentPath() + if handlers.saved() + savedIndicator = "" + else + savedIndicator = "*" + + if path + path = " - #{path}" + + "Notepad.exe#{path}#{savedIndicator}" + content: textarea menuBar: menuBar.element width: 640 height: 480 - + + # Key handling + windowView.element.setAttribute("tabindex", "-1") + windowView.element.addEventListener "keydown", (e) -> + {ctrlKey:ctrl, key} = e + if ctrl + switch key + when "s" + e.preventDefault() + handlers.save() + when "o" + e.preventDefault() + handlers.open() + windowView.loadFile = handlers.loadFile return windowView diff --git a/apps/pixel.coffee b/apps/pixel.coffee index dea7b50..120a2ed 100644 --- a/apps/pixel.coffee +++ b/apps/pixel.coffee @@ -1,35 +1,16 @@ -Model = require "model" -Postmaster = require "postmaster" +IFrameApp = require "../lib/iframe-app" FileIO = require "../os/file-io" +Model = require "model" module.exports = -> - {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI - - frame = document.createElement "iframe" - frame.src = "https://danielx.net/pixel-editor/" - - # TODO: Gross hack to keep track of waiting for child window to load - # May want to move it into the postmaster library - resolveLoaded = null - loadedPromise = new Promise (resolve) -> - resolveLoaded = resolve - - postmaster = Postmaster() - postmaster.remoteTarget = -> frame.contentWindow - Object.assign postmaster, - childLoaded: -> - console.log "child loaded" - resolveLoaded() - save: -> - handlers.save() + {MenuBar, Modal, Observable, Util:{parseMenu}} = system.UI handlers = Model().include(FileIO).extend loadFile: (blob) -> - loadedPromise.then -> - postmaster.invokeRemote "loadFile", blob + app.send "loadFile", blob newFile: -> saveData: -> - postmaster.invokeRemote "getBlob" + app.send "getBlob" menuBar = MenuBar items: parseMenu """ @@ -47,13 +28,17 @@ module.exports = -> """ handlers: handlers - windowView = Window + app = IFrameApp title: Observable "Pixie Paint" - content: frame - menuBar: menuBar.element + src: "https://danielx.net/pixel-editor/" + menuBar: menuBar + handlers: handlers width: 640 height: 480 - windowView.loadFile = handlers.loadFile + app.handlers = handlers + app.loadFile = handlers.loadFile + + system.Achievement.unlock "Pixel perfect" - return windowView + return app diff --git a/apps/story-reader.coffee b/apps/story-reader.coffee new file mode 100644 index 0000000..f7cbe2e --- /dev/null +++ b/apps/story-reader.coffee @@ -0,0 +1,16 @@ +module.exports = (opts) -> + {title, width, height, text} = opts + width ?= 380 + height ?= 480 + + div = document.createElement "div" + div.textContent = text + div.style.padding = "1em" + div.style.whiteSpace = "pre-wrap" + div.style.textAlign = "justify" + + system.UI.Window + title: title + content: div + width: width + height: height diff --git a/apps/text-editor.coffee b/apps/text-editor.coffee index b957178..6892a23 100644 --- a/apps/text-editor.coffee +++ b/apps/text-editor.coffee @@ -3,11 +3,7 @@ FileIO = require "../os/file-io" ace.require("ace/ext/language_tools") -extraModes = - jadelet: "jade" - -mode = (mode) -> - extraModes[mode] or mode +{extensionFor} = require "../util" module.exports = -> {ContextMenu, MenuBar, Modal, Observable, Progress, Table, Util:{parseMenu}, Window} = system.UI @@ -39,23 +35,44 @@ module.exports = -> global.aceEditor = aceEditor - initSession = (file) -> - # TODO: Update window title + modes = + cson: "coffeescript" + jadelet: "jade" + js: "javascript" + md: "markdown" + styl: "stylus" + + mimeTypeFor = (path) -> + type = system.mimeTypeFor(path) + + "#{type}; charset=utf-8" + + setModeFor = (path) -> + extension = extensionFor(path) + mode = modes[extension] or extension + + session.setMode("ace/mode/#{mode}") + + initSession = (file, path) -> file.readAsText() .then (content) -> + if path + handlers.currentPath path + setModeFor(path) + session.setValue(content) - # TODO: Correct modes - mode = "coffee" - session.setMode("ace/mode/#{mode}") + handlers.saved true + + session.on "change", -> + handlers.saved false handlers = Model().include(FileIO).extend loadFile: initSession newFile: -> session.setValue "" saveData: -> - # TODO: Maintain proper mime type data = new Blob [session.getValue()], - type: "text/plain" + type: mimeTypeFor(handlers.currentPath()) return Promise.resolve data @@ -76,7 +93,18 @@ module.exports = -> handlers: handlers windowView = Window - title: Observable "Ace" + title: -> + path = handlers.currentPath() + if handlers.saved() + savedIndicator = "" + else + savedIndicator = "*" + + if path + path = " - #{path}" + + "Ace#{path}#{savedIndicator}" + content: aceWrap menuBar: menuBar.element width: 640 @@ -87,4 +115,17 @@ module.exports = -> windowView.on "resize", -> aceEditor.resize() + # Key handling + windowView.element.setAttribute("tabindex", "-1") + windowView.element.addEventListener "keydown", (e) -> + {ctrlKey:ctrl, key} = e + if ctrl + switch key + when "s" + e.preventDefault() + handlers.save() + when "o" + e.preventDefault() + handlers.open() + return windowView diff --git a/extensions.coffee b/extensions.coffee index fd4ec69..2523543 100644 --- a/extensions.coffee +++ b/extensions.coffee @@ -1,3 +1,9 @@ +# Pretend Hamlet Runtime is a real package +PACKAGE.dependencies["_lib_hamlet-runtime"] = + entryPoint: "main" + distribution: + main: PACKAGE.distribution["lib/hamlet-runtime"] + # Add some utility readers to the Blob API Blob::readAsText = -> file = this @@ -9,6 +15,9 @@ Blob::readAsText = -> reader.onerror = reject reader.readAsText(file) +Blob::getURL = -> + Promise.resolve URL.createObjectURL(this) + Blob::readAsJSON = -> @readAsText() .then JSON.parse @@ -23,16 +32,38 @@ Blob::readAsDataURL = -> reader.onerror = reject reader.readAsDataURL(file) +# BlobSham interface must implement getURL and readAs* methods + # Load an image from a blob returning a promise that is fulfilled with the # loaded image or rejected with an error Image.fromBlob = (blob) -> - new Promise (resolve, reject) -> - img = new Image - img.onload = -> - resolve img - img.onerror = reject + blob.getURL() + .then (url) -> + new Promise (resolve, reject) -> + img = new Image + img.onload = -> + resolve img + img.onerror = reject - img.src = URL.createObjectURL blob + img.src = url FileList::forEach ?= (args...) -> Array::forEach.apply(this, args) + +# Event#path polyfill for Firefox +unless "path" in Event.prototype + Object.defineProperty Event.prototype, "path", + get: -> + path = [] + currentElem = this.target + while currentElem + path.push currentElem + currentElem = currentElem.parentElement + + if path.indexOf(window) is -1 && path.indexOf(document) is -1 + path.push(document) + + if path.indexOf(window) is -1 + path.push(window) + + path diff --git a/feedback.coffee b/feedback.coffee new file mode 100644 index 0000000..507bc4f --- /dev/null +++ b/feedback.coffee @@ -0,0 +1,17 @@ +# Open a feedback form +module.exports = -> + iframe = document.createElement "iframe" + iframe.src = "https://docs.google.com/forms/d/e/1FAIpQLSfAK8ZYmMd4-XsDqyTK4soYGWApGD9R33nReuqwG-TxjXaGFg/viewform?embedded=true" + + Window = system.UI.Window + + windowView = Window + title: "Whimsy Space Feedback" + content: iframe + menuBar: null + width: 600 + height: 500 + + system.Achievement.unlock "We value your input" + + document.body.appendChild windowView.element diff --git a/issues/2016-12.coffee b/issues/2016-12.coffee index 0689c54..a2abea0 100644 --- a/issues/2016-12.coffee +++ b/issues/2016-12.coffee @@ -44,7 +44,7 @@ module.exports = -> area: issueTag .then (data) -> ajax - url: "https://whimsy-space.gomix.me/comments" + url: "https://whimsy-space.glitch.me/comments" data: JSON.stringify(data) headers: "Content-Type": "application/json" @@ -53,7 +53,7 @@ module.exports = -> handlers.viewComments() viewComments: -> - ajax.getJSON "https://whimsy-space.gomix.me/comments/#{issueTag}" + ajax.getJSON "https://whimsy-space.glitch.me/comments/#{issueTag}" .then (data) -> data = data.reverse() @@ -77,7 +77,7 @@ module.exports = -> document.body.appendChild app.element mysterySmell: -> system.Achievement.unlock "Cover-2-cover" - + div = document.createElement "div" div.textContent = require "../stories/mystery-smell" div.style.padding = "1em" diff --git a/issues/2017-02.coffee b/issues/2017-02.coffee index 2ab0d28..b862408 100644 --- a/issues/2017-02.coffee +++ b/issues/2017-02.coffee @@ -14,17 +14,12 @@ module.exports = -> container = document.createElement "container" - setTimeout -> - system.Achievement.unlock "Issue 2" - #, 3000 + system.Achievement.unlock "Issue 2" system.writeFile "issue-2/around.md", new Blob [require "../stories/around-the-world"], type: "text/markdown" system.writeFile "issue-2/provision.txt", new Blob [require "../stories/provision"], type: "text/plain" system.writeFile "issue-2/dungeon-dog.txt", new Blob [require "../stories/dungeon-dog"], type: "text/plain" system.writeFile "issue-2/dsad.exe", new Blob [""], type: "application/exe" - system.writeFile "issue-2/zine2.exe", new Blob [""], type: "application/exe" - - system.writeFile "issue-1/zine1.exe", new Blob [""], type: "application/exe" pages = front: """ @@ -64,7 +59,7 @@ module.exports = -> div(style="padding: 1em;") h1 Cheevos - p No matter if you guy/girl or whatever, Cheevos impress people. It's almost like saying 'Well, I got tons of Cheevos. There are tons of people online that are interested and respect me." + p No matter if you guy/girl or whatever, Cheevos impress people. It's almost like saying 'Well, I got tons of Cheevos. There are tons of people online that are interested and respect me." p You might be thinking 'Oh that's complete BS, I personally don't care about Cheevos when dating'. And yeah, you are probably telling the truth, but it's in your sub-concious. Sort of like how girls always like the bad guy, but never admit it. @@ -72,7 +67,7 @@ module.exports = -> p 'Brewer' asks 'What happens if you're cheevo talking at a bar, club or party and someone says they have more cheevos that you?' - p Well, hopefully they are just hating and are lying. First look up their score with your cellphone, make sure you have a page bookmarked where you can check. If you catch them in a lie, you look even better. If they are telling the truth and have more Cheevos than you, leave. Nothing else you can do. Buy the person a drink and leave, unless you're willing to look 2nd best. If you brought a date, odds are she's going to be impressed with the higher gamer score and ditch you. Get out as soon as you can and go to some other party. + p Well, hopefully they are just hating and are lying. First look up their score with your cellphone, make sure you have a page bookmarked where you can check. If you catch them in a lie, you look even better. If they are telling the truth and have more Cheevos than you, leave. Nothing else you can do. Buy the person a drink and leave, unless you're willing to look 2nd best. If you brought a date, odds are she's going to be impressed with the higher gamer score and ditch you. Get out as soon as you can and go to some other party. p a(href="http://cheevos.com") Learn more about cheevos from Bboy360 at cheevos.com @@ -87,7 +82,7 @@ module.exports = -> li pketh li Mayor li and you! - + p a(href="#table") Return to table of contents """ @@ -157,7 +152,7 @@ module.exports = -> displayPage = (page) -> return unless page - + visited[page] = true if Object.keys(visited).length is Object.keys(pages).length @@ -166,6 +161,9 @@ module.exports = -> if page is "vista" system.Achievement.unlock "Lol wut" + if page is "cheevos" + system.Achievement.unlock "Check yo' self" + emptyElement(container) container.appendChild(pages[page]) diff --git a/issues/2017-03.coffee b/issues/2017-03.coffee new file mode 100644 index 0000000..0bcf701 --- /dev/null +++ b/issues/2017-03.coffee @@ -0,0 +1,131 @@ +Model = require "model" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" + +Social = require "../social/social" + +{parentElementOfType, emptyElement} = require "../util" + +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement} = system + + visitedAreas = + blue: false + csaur: false + chateau: false + cheevo: false + evan: false + + visit = (area) -> + visitedAreas[area] = true + + visitedAll = Object.keys(visitedAreas).every (key) -> + visitedAreas[key] + + if visitedAll + Achievement.unlock "Cover-2-cover 3: Tokyo Drift" + + system.writeFile "issue-3/blue-light-special.txt", new Blob [require "../stories/blue-light-special"], type: "text/plain" + + system.Achievement.unlock "Issue 3" + + handlers = Model().include(Social).extend + area: -> + "2017-03" + + mSAccess97: -> + app = Spreadsheet(system) + document.body.appendChild app.element + + chateau: -> + visit "chateau" + app = Chateau(system) + document.body.appendChild app.element + + contrasaurus: -> + visit "csaur" + document.body.appendChild Contrasaurus().element + + achievementStatus: -> + visit "cheevo" + cheevoElement = system.Achievement.progressView() + cheevoElement.style.width = "100%" + cheevoElement.style.padding = "1em" + + system.Achievement.unlock "Check yo' self" + + windowView = Window + title: "Cheevos" + content: cheevoElement + width: 640 + height: 480 + + document.body.appendChild windowView.element + + evanAndMore: -> + visit "evan" + url = "https://s3.amazonaws.com/whimsyspace-databucket-1g3p6d9lcl6x1/danielx/IMG_9794.JPG" + img = document.createElement "img" + img.src = url + + {element} = system.UI.Window + title: "Evan And More" + content: img + width: 600 + height: 480 + + document.body.appendChild element + + blueLightSpecial: -> + Achievement.unlock "Blue light special" + visit "blue" + + storyWindow = StoryWindow("Blue Light Special", require("../stories/blue-light-special")) + + document.body.appendChild storyWindow.element + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [C]hateau + Contra[s]aurus + [S]tories + [B]lue Light Special + [E]van And More + #{Social.menuText} + [H]elp + [A]chievement Status + """ + handlers: handlers + + kmartGif = document.createElement "img" + kmartGif.src = "http://media.boingboing.net/wp-content/uploads/2015/07/m66DBJ.gif" + kmartGif.style = "width: 100%; height: 100%" + + windowView = Window + title: "ZineOS Volume 1 | Issue 3 | ATTN: K-Mart Shoppers | March 2017" + content: kmartGif + menuBar: menuBar.element + width: 800 + height: 600 + x: 32 + y: 32 + + document.body.appendChild windowView.element + +StoryWindow = (title, text) -> + div = document.createElement "div" + div.textContent = text + div.style.padding = "1em" + div.style.whiteSpace = "pre-wrap" + div.style.textAlign = "justify" + + system.UI.Window + title: title + content: div + width: 380 + height: 480 diff --git a/issues/2017-04.coffee b/issues/2017-04.coffee new file mode 100644 index 0000000..64abd37 --- /dev/null +++ b/issues/2017-04.coffee @@ -0,0 +1,174 @@ +Model = require "model" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" +MyBriefcase = require "../apps/my-briefcase" + +Social = require "../social/social" + +{parentElementOfType, emptyElement} = require "../util" + +writeIfNotPresent = (destination, sourceURL) -> + system.readFile destination + .then (file) -> + throw new Error "File not found" unless file + return file + .catch -> + ajax + url: sourceURL + responseType: "blob" + .then (blob) -> + system.writeFile destination, blob + +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement, ajax} = system + + visitedAreas = + bikes: false + izzy: false + residue: false + chateau: false + cheevo: false + briefcase: false + podcast: false + + visit = (area) -> + visitedAreas[area] = true + + visitedAll = Object.keys(visitedAreas).every (key) -> + visitedAreas[key] + + if visitedAll + Achievement.unlock "Cover-2-cover 4: Fast & Furious" + + system.writeFile "issue-4/izzy.txt", new Blob [require "../stories/izzy"], type: "text/plain" + system.writeFile "issue-4/residue.txt", new Blob [require "../stories/residue"], type: "text/plain" + + downloadBikes = -> + ["and-yet-they-rode-bikes.md", "infog.png", "lanes.png", "totally-a.html"].forEach (path) -> + filePath = "issue-4/bikes/#{path}" + + writeIfNotPresent filePath, "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/bikes/#{path}" + + downloadBikes() + + writeIfNotPresent "issue-4/Funkytown.mp3", "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/music/Funkytown.mp3" + .then -> + system.openPath "issue-4/Funkytown.mp3" + + writeIfNotPresent "issue-4/zinecast1.mp3", "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/podcasts/zinecast1.mp3" + + system.Achievement.unlock "Issue 4" + + handlers = Model().include(Social).extend + area: -> + "2017-04" + + zinecast1: -> + visit "podcast" + system.readFile "issue-4/zinecast1.mp3" + .then system.open + + bikes: -> + visit "bikes" + system.readFile "issue-4/bikes/and-yet-they-rode-bikes.md" + .then system.open + + chateau: -> + visit "chateau" + app = Chateau(system) + document.body.appendChild app.element + + achievementStatus: -> + visit "cheevo" + cheevoElement = system.Achievement.progressView() + cheevoElement.style.width = "100%" + cheevoElement.style.padding = "1em" + + system.Achievement.unlock "Check yo' self" + + windowView = Window + title: "Cheevos" + content: cheevoElement + width: 640 + height: 480 + + document.body.appendChild windowView.element + + myBriefcase: -> + visit "briefcase" + app = MyBriefcase() + document.body.appendChild app.element + + izzy: -> + Achievement.unlock "Izzy" + visit "izzy" + storyWindow = StoryWindow("Izzy", require("../stories/izzy")) + + document.body.appendChild storyWindow.element + + residue: -> + Achievement.unlock "Residue" + visit "residue" + + storyWindow = StoryWindow("Residue", require("../stories/residue")) + + document.body.appendChild storyWindow.element + + funkytown8bitRemix: -> + system.readFile "issue-4/Funkytown.mp3" + .then system.open + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [C]hateau + My [B]riefcase + [M]usic + [F]unkytown (8-bit Remix) + [Z]inecast 1 + [S]tories + [B]ikes + [I]zzy + [R]esidue + #{Social.menuText} + [H]elp + [A]chievement Status + """ + handlers: handlers + + content = document.createElement "content" + content.style = "width: 100%; height: 100%" + + img = document.createElement "img" + img.src = "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/images/708e9398a4b4bea08d7c61ff7a0f863f.gif" + img.style = "width: 100%; height: 100%" + + windowView = Window + title: "ZineOS Volume 1 | Issue 4 | DISCO TECH | April 2017" + content: img + menuBar: menuBar.element + width: 480 + height: 600 + x: 64 + y: 64 + + windowView.element.querySelector('viewport').style.overflow = "initial" + + document.body.appendChild windowView.element + +StoryWindow = (title, text) -> + div = document.createElement "div" + div.textContent = text + div.style.padding = "1em" + div.style.whiteSpace = "pre-wrap" + div.style.textAlign = "justify" + + system.UI.Window + title: title + content: div + width: 380 + height: 480 diff --git a/issues/2017-05.coffee b/issues/2017-05.coffee new file mode 100644 index 0000000..519dc2f --- /dev/null +++ b/issues/2017-05.coffee @@ -0,0 +1,99 @@ +Model = require "model" + +AchievementStatus = require "../apps/achievement-status" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +TextEditor = require "../apps/text-editor" +MyBriefcase = require "../apps/my-briefcase" + +StoryReader = require "../apps/story-reader" + +Social = require "../social/social" + +module.exports = -> + {ContextMenu, MenuBar, Modal, Progress, Util:{parseMenu}, Window} = system.UI + {Achievement, ajax} = system + + ggPath = "issue-5/gleep-glorp.m4a" + + system.readFile ggPath + .then (file) -> + throw new Error "File not found" unless file + .catch -> + ajax + url: "https://fs.whimsy.space/us-east-1:90fe8dfb-e9d2-45c7-a347-cf840a3e757f/public/hao/gleep-glorp.m4a" + responseType: "blob" + .then (blob) -> + system.writeFile ggPath, blob + + handlers = Model().include(Social).extend + area: -> + "2017-05" + + achievementStatus: -> + system.launchApp AchievementStatus + + chateau: -> + system.launchApp Chateau + + crescent: -> + app = StoryReader + text: require "../stories/crescent" + title: "Crescent" + + document.body.appendChild app.element + + gleepGlorp: -> + system.openPath ggPath + + marigold: -> + app = StoryReader + text: require "../stories/marigold" + title: "Marigold" + + document.body.appendChild app.element + + myBriefcase: -> + system.launchApp MyBriefcase + + pixiePaint: -> + system.launchApp PixiePaint + + textEditor: -> + system.launchApp TextEditor + + menuBar = MenuBar + items: parseMenu """ + [A]pps + [C]hateau + My [B]riefcase + [P]ixie Paint + [T]ext Editor + [C]ontent + [C]rescent + [G]leep Glorp + [M]arigold + #{Social.menuText} + [H]elp + [A]chievement Status + """ + handlers: handlers + + img = document.createElement "img" + img.src = "https://i.imgur.com/hKOGoex.jpg" + img.style = "width: 100%; height: 100%" + + windowView = Window + title: "ZineOS Volume 1 | Issue 5 | A May Zine | May 2017" + content: img + menuBar: menuBar.element + width: 640 + height: 360 + x: 64 + y: 64 + + windowView.element.querySelector('viewport').style.overflow = "initial" + + document.body.appendChild windowView.element diff --git a/lib/app-drop.coffee b/lib/app-drop.coffee new file mode 100644 index 0000000..500c1cd --- /dev/null +++ b/lib/app-drop.coffee @@ -0,0 +1,35 @@ +Drop = require "./drop" + +# General drop handling for apps +module.exports = (app) -> + {element} = app + + Drop element, (e) -> + {handlers} = app + + fileSelectionData = e.dataTransfer.getData("zineos/file-selection") + + if fileSelectionData + data = JSON.parse(fileSelectionData) + e.preventDefault() + file = data.files[0] + + # TODO: Handle multi-files + path = data.files[0].path + + system.readFile path + .then handlers.loadFile + .then -> + handlers.currentPath path + + return + + files = e.dataTransfer.files + + if files.length + e.preventDefault() + + file = files[0] + handlers.loadFile file + .then -> + handlers.currentPath null diff --git a/lib/dexie-fs.coffee b/lib/dexie-fs.coffee new file mode 100644 index 0000000..1ad5ec4 --- /dev/null +++ b/lib/dexie-fs.coffee @@ -0,0 +1,65 @@ +Bindable = require "bindable" +Model = require "model" + +FolderEntry = (path, prefix) -> + folder: true + path: prefix + path + relativePath: path + +# FS Wrapper to Dexie database +module.exports = (db) -> + Files = db.files + + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result + + self = Model() + .include(Bindable) + .extend + # Read a blob from a path + read: (path) -> + Files.get(path) + .then ({blob}) -> + blob + .then notify "read", path + + # Write a blob to a path + write: (path, blob) -> + now = +new Date + + Files.put + path: path + blob: blob + size: blob.size + type: blob.type + createdAt: now + updatedAt: now + .then notify "write", path + + # Delete a file at a path + delete: (path) -> + Files.delete(path) + .then notify "delete", path + + # List files and folders in a directory + list: (dir) -> + Files.where("path").startsWith(dir).toArray() + .then (files) -> + folderPaths = {} + + files = files.filter (file) -> + file.relativePath = file.path.replace(dir, "") + + if file.relativePath.match /\// # folder + folderPath = file.relativePath.replace /\/.*$/, "/" + folderPaths[folderPath] = true + return + else + return file + + folders = Object.keys(folderPaths).map (folderPath) -> + FolderEntry folderPath, dir + + return folders.concat(files) diff --git a/lib/drop.coffee b/lib/drop.coffee index 6e1fb0a..7993784 100644 --- a/lib/drop.coffee +++ b/lib/drop.coffee @@ -6,6 +6,4 @@ module.exports = (element, handler) -> element.addEventListener "dragover", cancel element.addEventListener "dragenter", cancel element.addEventListener "drop", (e) -> - e.preventDefault() handler(e) - return false diff --git a/lib/hamlet.js b/lib/hamlet.js index b18539c..9445320 100644 --- a/lib/hamlet.js +++ b/lib/hamlet.js @@ -2448,4 +2448,4 @@ module.exports={ } },{}]},{},[1])(1) -}); \ No newline at end of file +}); diff --git a/lib/iframe-app.coffee b/lib/iframe-app.coffee new file mode 100644 index 0000000..52847de --- /dev/null +++ b/lib/iframe-app.coffee @@ -0,0 +1,88 @@ +Model = require "model" +Postmaster = require "postmaster" +FileIO = require "../os/file-io" + +module.exports = (opts={}) -> + {Window} = system.UI + + {height, menuBar, src, handlers, title, width, sandbox, pkg, packageOptions, iconEmoji} = opts + + frame = document.createElement "iframe" + + if sandbox + frame.setAttribute("sandbox", sandbox) + + if src + frame.src = src + else if pkg + html = system.htmlForPackage(pkg, packageOptions) + blob = new Blob [html], + type: "text/html; charset=utf-8" + frame.src = URL.createObjectURL blob + + # Keep track of waiting for child window to load, all remote invocations are + # queued behind a promise until the child has loaded + # May want to move it into the postmaster library + resolveLoaded = null + loadedPromise = new Promise (resolve) -> + resolveLoaded = resolve + + loaded = false + setTimeout -> + console.warn "Child never loaded" unless loaded + , 5000 + + # Attach a postmaster to receive events from the child frame + postmaster = Postmaster() + + Object.assign postmaster, + remoteTarget: -> + frame.contentWindow + + childLoaded: -> + console.log "child loaded" + resolveLoaded() + loaded = true + + # Send events from the iframe app to the application + event: -> + application.trigger "event", arguments... + + return + + # Add application method access to client iFrame + application: (method, args...) -> + application[method](args...) + + # Add system method access to client iFrame + # TODO: Security :P + system: (method, args...) -> + system[method](args...) + + handlers ?= Model().include(FileIO).extend + loadFile: (blob) -> + loadedPromise.then -> + postmaster.invokeRemote "loadFile", blob + + application = Window + title: title + content: frame + menuBar: menuBar?.element + width: width + height: height + iconEmoji: iconEmoji + + Object.assign application, + exit: -> + # TODO: Prompt unsaved, etc. + setTimeout -> + application.element.remove() + , 0 + return + handlers: handlers + loadFile: handlers.loadFile + send: (args...) -> + loadedPromise.then -> + postmaster.invokeRemote args... + + return application diff --git a/lib/mount-fs.coffee b/lib/mount-fs.coffee new file mode 100644 index 0000000..58c54bf --- /dev/null +++ b/lib/mount-fs.coffee @@ -0,0 +1,75 @@ +{startsWith} = require "../util" + +Bindable = require "bindable" +Model = require "model" + +module.exports = (I) -> + mounts = {} + mountPaths = [] + + longestToShortest = (a, b) -> + b.length - a.length + + findMountPathFor = (path) -> + [mountPath] = mountPaths.filter (p) -> + startsWith path, p + + return mountPath + + proxyToMount = (method) -> + (path, params...) -> + mountPath = findMountPathFor path + + if mountPath + mount = mounts[mountPath] + else + throw new Error "No mounted filesystem for #{path}" + + subsystemPath = path.replace(mountPath, "/") + + if method is "list" + # Remap paths when retrieving entries + mount[method](subsystemPath, params...) + .then (entries) -> + entries.forEach (entry) -> + entry.path = entry.path.replace("/", mountPath) + + return entries + else if method is "read" + mount[method](subsystemPath, params...) + .then (blob) -> + if blob + blob.path = path + + return blob + else + mount[method](subsystemPath, params...) + + bindSubsystemEvent = (folderPath, subsystem, eventName) -> + subsystem.on eventName, (path) -> + self.trigger eventName, path.replace("/", folderPath) + + self = Model() + .include(Bindable) + .extend + read: proxyToMount "read" + + write: proxyToMount "write" + + delete: proxyToMount "delete" + + list: proxyToMount "list" + + mount: (folderPath, subsystem) -> + mounts[folderPath] = subsystem + mountPaths.push folderPath + mountPaths.sort longestToShortest + + # TODO: Want to be able to pass through all events + bindSubsystemEvent(folderPath, subsystem, "write") + bindSubsystemEvent(folderPath, subsystem, "update") + bindSubsystemEvent(folderPath, subsystem, "delete") + + return self + + return self diff --git a/lib/outbound-clicks.coffee b/lib/outbound-clicks.coffee index d8feb7a..55c1896 100644 --- a/lib/outbound-clicks.coffee +++ b/lib/outbound-clicks.coffee @@ -7,7 +7,7 @@ document.addEventListener "click", (e) -> if anchor href = anchor.getAttribute('href') - if href.match /^http/ + if href?.match /^http/ e.preventDefault() if href.match /frogfeels\.com/ diff --git a/lib/pkg-fs.coffee b/lib/pkg-fs.coffee new file mode 100644 index 0000000..b774ea4 --- /dev/null +++ b/lib/pkg-fs.coffee @@ -0,0 +1,105 @@ +Bindable = require "bindable" +Model = require "model" + +FolderEntry = (path, prefix) -> + folder: true + path: prefix + path + relativePath: path + +# Keys in the package's source object don't begin with slashes +sourcePath = (path) -> + path.replace(/^\//, "") + +# Strip out extension suffixes +distributionPath = (path) -> + path.replace(/\..*$/, "") + +# FS Wrapper to a pixie package +module.exports = (pkg, persistencePath) -> + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result + + persist = -> + # Persist entire pkg + pkgBlob = new Blob [JSON.stringify(pkg)], + type: "application/json; charset=utf8" + system.writeFile persistencePath, pkgBlob + + compileAndWrite = (path, blob) -> + writeSource = blob.readAsText() + .then (text) -> + srcPath = sourcePath(path) + pkg.source[srcPath] = + content: text + type: blob.type or "" + path: srcPath + + # Compilers expect blob to be annotated with the path + blob.path = path + + writeCompiled = system.compileFile(blob) + .then (compiledSource) -> + if typeof compiledSource is "string" + pkg.distribution[distributionPath(sourcePath(path))] = + content: compiledSource + else + console.warn "Can't package files like #{path} yet", compiledSource + + Promise.all [writeSource, writeCompiled] + .then persist + + self = Model() + .include(Bindable) + .extend + # Read a blob from a path + read: (path) -> + {content, type} = pkg.source[sourcePath(path)] + type ?= "" + + blob = new Blob [content], + type: type + + Promise.resolve blob + .then notify "read", path + + # Write a blob to a path + write: (path, blob) -> + compileAndWrite(path, blob) + .then notify "write", path + + # Delete a file at a path + delete: (path) -> + Promise.resolve() + .then -> + delete pkg.source[sourcePath(path)] + .then notify "delete", path + + # List files and folders in a directory + list: (dir) -> + sourceDir = sourcePath(dir) + + Promise.resolve() + .then -> + Object.keys(pkg.source).filter (path) -> + path.indexOf(sourceDir) is 0 + .map (path) -> + path: "/" + path + relativePath: path.replace(sourceDir, "") + type: pkg.source[path].type or "" + .then (files) -> + folderPaths = {} + + files = files.filter (file) -> + if file.relativePath.match /\// # folder + folderPath = file.relativePath.replace /\/.*$/, "/" + folderPaths[folderPath] = true + return + else + return file + + folders = Object.keys(folderPaths).map (folderPath) -> + FolderEntry folderPath, dir + + return folders.concat(files) diff --git a/lib/s3-fs.coffee b/lib/s3-fs.coffee new file mode 100644 index 0000000..3ecd874 --- /dev/null +++ b/lib/s3-fs.coffee @@ -0,0 +1,171 @@ +Bindable = require "bindable" +Model = require "model" + +{pinvoke, startsWith, endsWith} = require "../util" + +delimiter = "/" + +# NOTE: Not scoped for multi-bucket yet +localCache = {} + +status = (response) -> + if response.status >= 200 && response.status < 300 + return response + else + throw response + +json = (response) -> + response.json() + +blob = (response) -> + response.blob() + +uploadToS3 = (bucket, key, file, options={}) -> + {cacheControl} = options + + cacheControl ?= 0 + + # Optimistically Cache + localCache[key] = file + + pinvoke bucket, "putObject", + Key: key + ContentType: file.type + Body: file + CacheControl: "max-age=#{cacheControl}" + +getRemote = (bucket, key) -> + cachedItem = localCache[key] + + if cachedItem + if cachedItem instanceof Blob + return Promise.resolve(cachedItem) + else + return Promise.reject(cachedItem) + + pinvoke bucket, "getObject", + Key: key + .then (data) -> + {Body, ContentType} = data + + new Blob [Body], + type: ContentType + .then (data) -> + localCache[key] = data + .catch (e) -> + # Cache Not Founds too, since that's often what is slow + localCache[key] = e + throw e + +deleteFromS3 = (bucket, key) -> + localCache[key] = new Error "Not Found" + + pinvoke bucket, "deleteObject", + Key: key + +list = (bucket, id, dir) -> + unless startsWith dir, delimiter + dir = "#{delimiter}#{dir}" + + unless endsWith dir, delimiter + dir = "#{dir}#{delimiter}" + + prefix = "#{id}#{dir}" + + pinvoke bucket, "listObjects", + Prefix: prefix + Delimiter: delimiter + .then (result) -> + results = result.CommonPrefixes.map (p) -> + FolderEntry p.Prefix, id, prefix + .concat result.Contents.map (o) -> + FileEntry o, id, prefix, bucket + .map (entry) -> + fetchMeta(entry, bucket) + + Promise.all results + +module.exports = (id, bucket) -> + + notify = (eventType, path) -> + (result) -> + self.trigger eventType, path + return result + + self = Model() + .include Bindable + .extend + read: (path) -> + unless startsWith path, delimiter + path = delimiter + path + + key = "#{id}#{path}" + + getRemote(bucket, key) + .then notify "read", path + + write: (path, blob) -> + unless startsWith path, delimiter + path = delimiter + path + + key = "#{id}#{path}" + + uploadToS3 bucket, key, blob + .then notify "write", path + + delete: (path) -> + unless startsWith path, delimiter + path = delimiter + path + + key = "#{id}#{path}" + + deleteFromS3 bucket, key + .then notify "delete", path + + list: (folderPath="/") -> + list bucket, id, folderPath + +fetchFileMeta = (fileEntry, bucket) -> + pinvoke bucket, "headObject", + Key: fileEntry.remotePath + .then (result) -> + fileEntry.type = result.ContentType + + fileEntry + +fetchMeta = (entry, bucket) -> + Promise.resolve() + .then -> + return entry if entry.folder + + fetchFileMeta entry, bucket + +FolderEntry = (path, id, prefix) -> + folder: true + path: path.replace(id, "") + relativePath: path.replace(prefix, "") + remotePath: path + +FileEntry = (object, id, prefix, bucket) -> + path = object.Key + + entry = + path: path.replace(id, "") + relativePath: path.replace(prefix, "") + remotePath: path + size: object.Size + + entry.blob = BlobSham(entry, bucket) + + return entry + +BlobSham = (entry, bucket) -> + remotePath = entry.remotePath + + getURL: -> + getRemote(bucket, remotePath) + .then URL.createObjectURL + readAsText: -> + getRemote(bucket, remotePath) + .then (blob) -> + blob.readAsText() diff --git a/lib/stylus.min.js b/lib/stylus.min.js new file mode 100644 index 0000000..117adb7 --- /dev/null +++ b/lib/stylus.min.js @@ -0,0 +1,6 @@ +if(Function.prototype.name===undefined&&Object.defineProperty!==undefined){Object.defineProperty(Function.prototype,"name",{get:function(){var regex=/function\s([^(]{1,})\(/,match=regex.exec(this.toString());return match&&match.length>1?match[1].trim():""}})}if(String.prototype.trimRight===undefined){String.prototype.trimRight=function(){return String(this).replace(/\s+$/,"")}}var stylus=module.exports=function(){function require(p){var path=require.resolve(p),mod=require.modules[path];if(!mod)throw new Error('failed to require "'+p+'"');if(!mod.exports){mod.exports={};mod.call(mod.exports,mod,mod.exports,require.relative(path))}return mod.exports}var bifs="called-from = ()\n\nvendors = moz webkit o ms official\n\n// stringify the given arg\n\n-string(arg)\n type(arg) + ' ' + arg\n\n// require a color\n\nrequire-color(color)\n unless color is a 'color'\n error('RGB or HSL value expected, got a ' + -string(color))\n\n// require a unit\n\nrequire-unit(n)\n unless n is a 'unit'\n error('unit expected, got a ' + -string(n))\n\n// require a string\n\nrequire-string(str)\n unless str is a 'string' or str is a 'ident'\n error('string expected, got a ' + -string(str))\n\n// Math functions\n\nabs(n) { math(n, 'abs') }\nmin(a, b) { a < b ? a : b }\nmax(a, b) { a > b ? a : b }\n\n// Trigonometrics\nPI = -math-prop('PI')\n\nradians-to-degrees(angle)\n angle * (180 / PI)\n\ndegrees-to-radians(angle)\n unit(angle * (PI / 180),'')\n\nsin(n)\n n = degrees-to-radians(n) if unit(n) == 'deg'\n round(math(n, 'sin'), 9)\n\ncos(n)\n n = degrees-to-radians(n) if unit(n) == 'deg'\n round(math(n, 'cos'), 9)\n\n// Rounding Math functions\n\nceil(n, precision = 0)\n multiplier = 10 ** precision\n math(n * multiplier, 'ceil') / multiplier\n\nfloor(n, precision = 0)\n multiplier = 10 ** precision\n math(n * multiplier, 'floor') / multiplier\n\nround(n, precision = 0)\n multiplier = 10 ** precision\n math(n * multiplier, 'round') / multiplier\n\n// return the sum of the given numbers\n\nsum(nums)\n sum = 0\n sum += n for n in nums\n\n// return the average of the given numbers\n\navg(nums)\n sum(nums) / length(nums)\n\n// return a unitless number, or pass through\n\nremove-unit(n)\n if typeof(n) is 'unit'\n unit(n, '')\n else\n n\n\n// convert a percent to a decimal, or pass through\n\npercent-to-decimal(n)\n if unit(n) is '%'\n remove-unit(n) / 100\n else\n n\n\n// check if n is an odd number\n\nodd(n)\n 1 == n % 2\n\n// check if n is an even number\n\neven(n)\n 0 == n % 2\n\n// check if color is light\n\nlight(color)\n lightness(color) >= 50%\n\n// check if color is dark\n\ndark(color)\n lightness(color) < 50%\n\n// desaturate color by amount\n\ndesaturate(color, amount)\n adjust(color, 'saturation', - amount)\n\n// saturate color by amount\n\nsaturate(color = '', amount = 100%)\n if color is a 'color'\n adjust(color, 'saturation', amount)\n else\n unquote( 'saturate(' + color + ')' )\n\n// darken by the given amount\n\ndarken(color, amount)\n adjust(color, 'lightness', - amount)\n\n// lighten by the given amount\n\nlighten(color, amount)\n adjust(color, 'lightness', amount)\n\n// decrease opacity by amount\n\nfade-out(color, amount)\n color - rgba(black, percent-to-decimal(amount))\n\n// increase opacity by amount\n\nfade-in(color, amount)\n color + rgba(black, percent-to-decimal(amount))\n\n// spin hue by a given amount\n\nspin(color, amount)\n color + unit(amount, deg)\n\n// mix two colors by a given amount\n\nmix(color1, color2, weight = 50%)\n unless weight in 0..100\n error('Weight must be between 0% and 100%')\n\n if length(color1) == 2\n weight = color1[0]\n color1 = color1[1]\n\n else if length(color2) == 2\n weight = 100 - color2[0]\n color2 = color2[1]\n\n require-color(color1)\n require-color(color2)\n\n p = unit(weight / 100, '')\n w = p * 2 - 1\n\n a = alpha(color1) - alpha(color2)\n\n w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2\n w2 = 1 - w1\n\n channels = (red(color1) red(color2)) (green(color1) green(color2)) (blue(color1) blue(color2))\n rgb = ()\n\n for pair in channels\n push(rgb, floor(pair[0] * w1 + pair[1] * w2))\n\n a1 = alpha(color1) * p\n a2 = alpha(color2) * (1 - p)\n alpha = a1 + a2\n\n rgba(rgb[0], rgb[1], rgb[2], alpha)\n\n// invert colors, leave alpha intact\n\ninvert(color = '')\n if color is a 'color'\n rgba(#fff - color, alpha(color))\n else\n unquote( 'invert(' + color + ')' )\n\n// give complement of the given color\n\ncomplement( color )\n spin( color, 180 )\n\n// give grayscale of the given color\n\ngrayscale( color = '' )\n if color is a 'color'\n desaturate( color, 100% )\n else\n unquote( 'grayscale(' + color + ')' )\n\n// mix the given color with white\n\ntint( color, percent )\n mix( white, color, percent )\n\n// mix the given color with black\n\nshade( color, percent )\n mix( black, color, percent )\n\n// return the last value in the given expr\n\nlast(expr)\n expr[length(expr) - 1]\n\n// return keys in the given pairs or object\n\nkeys(pairs)\n ret = ()\n if type(pairs) == 'object'\n for key in pairs\n push(ret, key)\n else\n for pair in pairs\n push(ret, pair[0]);\n ret\n\n// return values in the given pairs or object\n\nvalues(pairs)\n ret = ()\n if type(pairs) == 'object'\n for key, val in pairs\n push(ret, val)\n else\n for pair in pairs\n push(ret, pair[1]);\n ret\n\n// join values with the given delimiter\n\njoin(delim, vals...)\n buf = ''\n vals = vals[0] if length(vals) == 1\n for val, i in vals\n buf += i ? delim + val : val\n\n// add a CSS rule to the containing block\n\n// - This definition allows add-property to be used as a mixin\n// - It has the same effect as interpolation but allows users\n// to opt for a functional style\n\nadd-property-function = add-property\nadd-property(name, expr)\n if mixin\n {name} expr\n else\n add-property-function(name, expr)\n\nprefix-classes(prefix)\n -prefix-classes(prefix, block)\n\n// Caching mixin, use inside your functions to enable caching by extending.\n\n$stylus_mixin_cache = {}\ncache()\n $key = (current-media() or 'no-media') + '__' + called-from[0] + '__' + arguments\n if $key in $stylus_mixin_cache\n @extend {'$cache_placeholder_for_' + $stylus_mixin_cache[$key]}\n else if 'cache' in called-from\n {block}\n else\n $id = length($stylus_mixin_cache)\n\n &,\n /$cache_placeholder_for_{$id}\n $stylus_mixin_cache[$key] = $id\n {block}\n";require.modules={};require.resolve=function(path){var orig=path,reg=path+".js",index=path+"/index.js";return require.modules[reg]&®||require.modules[index]&&index||orig};require.register=function(path,fn){require.modules[path]=fn};require.relative=function(parent){return function(p){if("."!=p[0])return require(p);var path=parent.split("/"),segs=p.split("/");path.pop();for(var i=0;i=0;i--){var last=parts[i];if(last=="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift("..")}}return parts}var splitPathRe=/^([\s\S]+\/(?!$)|\/)?((?:[\s\S]+?)?(\.[^.]*)?)$/;exports.normalize=function(path){var isAbsolute=path.charAt(0)==="/",trailingSlash=path.slice(-1)==="/";path=normalizeArray(path.split("/").filter(function(p){return!!p}),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(paths.filter(function(p,index){return p&&typeof p==="string"}).join("/"))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;isizeLimit)return literal;return new nodes.Literal('url("data:'+mime+";base64,"+buf.toString("base64")+hash+'")')}fn.raw=true;return fn};module.exports.mimes=defaultMimes});require.register("functions/add-property.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");(module.exports=function addProperty(name,expr){utils.assertType(name,"expression","name");name=utils.unwrap(name).first;utils.assertString(name,"name");utils.assertType(expr,"expression","expr");var prop=new nodes.Property([name],expr);var block=this.closestBlock;var len=block.nodes.length,head=block.nodes.slice(0,block.index),tail=block.nodes.slice(block.index++,len);head.push(prop);block.nodes=head.concat(tail);return prop}).raw=true});require.register("functions/adjust.js",function(module,exports,require){var utils=require("../utils");module.exports=function adjust(color,prop,amount){utils.assertColor(color,"color");utils.assertString(prop,"prop");utils.assertType(amount,"unit","amount");var hsl=color.hsla.clone();prop={hue:"h",saturation:"s",lightness:"l"}[prop.string];if(!prop)throw new Error("invalid adjustment property");var val=amount.val;if("%"==amount.type){val="l"==prop&&val>0?(100-hsl[prop])*val/100:hsl[prop]*(val/100)}hsl[prop]+=val;return hsl.rgba}});require.register("functions/alpha.js",function(module,exports,require){var nodes=require("../nodes"),rgba=require("./rgba");module.exports=function alpha(color,value){color=color.rgba;if(value){return rgba(new nodes.Unit(color.r),new nodes.Unit(color.g),new nodes.Unit(color.b),value)}return new nodes.Unit(color.a,"")}});require.register("functions/base-convert.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");(module.exports=function(num,base,width){utils.assertPresent(num,"number");utils.assertPresent(base,"base");num=utils.unwrap(num).nodes[0].val;base=utils.unwrap(base).nodes[0].val;width=width&&utils.unwrap(width).nodes[0].val||2;var result=Number(num).toString(base);while(result.lengthtop.a){top=blend(top,bottom)}var l1=luminosity(bottom).val+.05,l2=luminosity(top).val+.05,ratio=l1/l2;if(l2>l1){ratio=1/ratio}return Math.round(ratio*10)/10}if(1<=bottom.a){var resultRatio=new nodes.Unit(contrast(top,bottom));result.set("ratio",resultRatio);result.set("error",new nodes.Unit(0));result.set("min",resultRatio);result.set("max",resultRatio)}else{var onBlack=contrast(top,blend(bottom,new nodes.RGBA(0,0,0,1))),onWhite=contrast(top,blend(bottom,new nodes.RGBA(255,255,255,1))),max=Math.max(onBlack,onWhite);function processChannel(topChannel,bottomChannel){return Math.min(Math.max(0,(topChannel-bottomChannel*bottom.a)/(1-bottom.a)),255)}var closest=new nodes.RGBA(processChannel(top.r,bottom.r),processChannel(top.g,bottom.g),processChannel(top.b,bottom.b),1);var min=contrast(top,blend(bottom,closest));result.set("ratio",new nodes.Unit(Math.round((min+max)*50)/100));result.set("error",new nodes.Unit(Math.round((max-min)*50)/100));result.set("min",new nodes.Unit(min));result.set("max",new nodes.Unit(max))}return result}});require.register("functions/convert.js",function(module,exports,require){var utils=require("../utils");module.exports=function convert(str){utils.assertString(str,"str");return utils.parseString(str.string)}});require.register("functions/current-media.js",function(module,exports,require){var nodes=require("../nodes");module.exports=function currentMedia(){return new nodes.String(lookForMedia(this.closestBlock.node)||"");function lookForMedia(node){if("media"==node.nodeName){return node.toString()}else if(node.block.parent.node){return lookForMedia(node.block.parent.node)}}}});require.register("functions/define.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function define(name,expr){utils.assertType(name,"string","name");expr=utils.unwrap(expr);var scope=this.currentScope;var node=new nodes.Ident(name.val,expr);scope.add(node);return nodes.nil}});require.register("functions/dirname.js",function(module,exports,require){var utils=require("../utils"),path=require("../path");module.exports=function dirname(p){utils.assertString(p,"path");return path.dirname(p.val).replace(/\\/g,"/")}});require.register("functions/error.js",function(module,exports,require){var utils=require("../utils");module.exports=function error(msg){utils.assertType(msg,"string","msg");var err=new Error(msg.val);err.fromStylus=true;throw err}});require.register("functions/extname.js",function(module,exports,require){var utils=require("../utils"),path=require("../path");module.exports=function extname(p){utils.assertString(p,"path");return path.extname(p.val)}});require.register("functions/green.js",function(module,exports,require){var nodes=require("../nodes"),rgba=require("./rgba");module.exports=function green(color,value){color=color.rgba;if(value){return rgba(new nodes.Unit(color.r),value,new nodes.Unit(color.b),new nodes.Unit(color.a))}return new nodes.Unit(color.g,"")}});require.register("functions/hsl.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes"),hsla=require("./hsla");module.exports=function hsl(hue,saturation,lightness){if(1==arguments.length){utils.assertColor(hue,"color");return hue.hsla}else{return hsla(hue,saturation,lightness,new nodes.Unit(1))}}});require.register("functions/hsla.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function hsla(hue,saturation,lightness,alpha){switch(arguments.length){case 1:utils.assertColor(hue);return hue.hsla;case 2:utils.assertColor(hue);var color=hue.hsla;utils.assertType(saturation,"unit","alpha");var alpha=saturation.clone();if("%"==alpha.type)alpha.val/=100;return new nodes.HSLA(color.h,color.s,color.l,alpha.val);default:utils.assertType(hue,"unit","hue");utils.assertType(saturation,"unit","saturation");utils.assertType(lightness,"unit","lightness");utils.assertType(alpha,"unit","alpha");var alpha=alpha.clone();if(alpha&&"%"==alpha.type)alpha.val/=100;return new nodes.HSLA(hue.val,saturation.val,lightness.val,alpha.val)}}});require.register("functions/hue.js",function(module,exports,require){var nodes=require("../nodes"),hsla=require("./hsla"),component=require("./component");module.exports=function hue(color,value){if(value){var hslaColor=color.hsla;return hsla(value,new nodes.Unit(hslaColor.s),new nodes.Unit(hslaColor.l),new nodes.Unit(hslaColor.a))}return component(color,new nodes.String("hue"))}});require.register("functions/length.js",function(module,exports,require){var utils=require("../utils");(module.exports=function length(expr){if(expr){if(expr.nodes){var nodes=utils.unwrap(expr).nodes;if(1==nodes.length&&"object"==nodes[0].nodeName){return nodes[0].length}else{return nodes.length}}else{return 1}}return 0}).raw=true});require.register("functions/lightness.js",function(module,exports,require){var nodes=require("../nodes"),hsla=require("./hsla"),component=require("./component");module.exports=function lightness(color,value){if(value){var hslaColor=color.hsla;return hsla(new nodes.Unit(hslaColor.h),new nodes.Unit(hslaColor.s),value,new nodes.Unit(hslaColor.a))}return component(color,new nodes.String("lightness"))}});require.register("functions/list-separator.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");(module.exports=function listSeparator(list){list=utils.unwrap(list);return new nodes.String(list.isList?",":" ")}).raw=true});require.register("functions/lookup.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function lookup(name){utils.assertType(name,"string","name");var node=this.lookup(name.val);if(!node)return nodes.nil;return this.visit(node)}});require.register("functions/luminosity.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function luminosity(color){utils.assertColor(color);color=color.rgba;function processChannel(channel){channel=channel/255;return.03928>channel?channel/12.92:Math.pow((channel+.055)/1.055,2.4)}return new nodes.Unit(.2126*processChannel(color.r)+.7152*processChannel(color.g)+.0722*processChannel(color.b))}});require.register("functions/match.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function match(pattern,val){utils.assertType(pattern,"string","pattern");utils.assertString(val,"val");var re=new RegExp(pattern.val);return new nodes.Boolean(re.test(val.string))}});require.register("functions/math-prop.js",function(module,exports,require){var nodes=require("../nodes");module.exports=function math(prop){return new nodes.Unit(Math[prop.string])}});require.register("functions/math.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function math(n,fn){utils.assertType(n,"unit","n");utils.assertString(fn,"fn");return new nodes.Unit(Math[fn.string](n.val),n.type)}});require.register("functions/merge.js",function(module,exports,require){var utils=require("../utils");(module.exports=function merge(dest){utils.assertPresent(dest,"dest");dest=utils.unwrap(dest).first;utils.assertType(dest,"object","dest");var last=utils.unwrap(arguments[arguments.length-1]).first,deep=true===last.val;for(var i=1,len=arguments.length-deep;i1){if(expr.isList){pushToStack(expr.nodes,stack)}else{stack.push(parse(expr.nodes.map(function(node){utils.assertString(node,"selector");return node.string}).join(" ")))}}}else if(args.length>1){pushToStack(args,stack)}return stack.length?utils.compileSelectors(stack).join(","):"&"}).raw=true;function pushToStack(selectors,stack){selectors.forEach(function(sel){sel=sel.first;utils.assertString(sel,"selector");stack.push(parse(sel.string))})}function parse(selector){var Parser=new require("../parser"),parser=new Parser(selector),nodes;parser.state.push("selector-parts");nodes=parser.selector();nodes.forEach(function(node){node.val=node.segments.map(function(seg){return seg.toString()}).join("")});return nodes}});require.register("functions/selectors.js",function(module,exports,require){var nodes=require("../nodes"),Parser=require("../selector-parser");module.exports=function selectors(){var stack=this.selectorStack,expr=new nodes.Expression(true);if(stack.length){for(var i=0;i1){expr.push(new nodes.String(group.map(function(selector){nested=new Parser(selector.val).parse().nested;return(nested&&i?"& ":"")+selector.val}).join(",")))}else{var selector=group[0].val;nested=new Parser(selector).parse().nested;expr.push(new nodes.String((nested&&i?"& ":"")+selector))}}}else{expr.push(new nodes.String("&"))}return expr}});require.register("functions/shift.js",function(module,exports,require){var utils=require("../utils");(module.exports=function(expr){expr=utils.unwrap(expr);return expr.nodes.shift()}).raw=true});require.register("functions/split.js",function(module,exports,require){var utils=require("../utils"),nodes=require("../nodes");module.exports=function split(delim,val){utils.assertString(delim,"delimiter");utils.assertString(val,"val");var splitted=val.string.split(delim.string);var expr=new nodes.Expression;var ItemNode=val instanceof nodes.Ident?nodes.Ident:nodes.String;for(var i=0,len=splitted.length;is.lastIndexOf("*/",offset),commentIdx=s.lastIndexOf("//",offset),i=s.lastIndexOf("\n",offset),double=0,single=0;if(~commentIdx&&commentIdx>i){while(i!=offset){if("'"==s[i])single?single--:single++;if('"'==s[i])double?double--:double++;if("/"==s[i]&&"/"==s[i+1]){inComment=!single&&!double;break}++i}}return inComment?str:val+"\r"}if(""==str.charAt(0))str=str.slice(1);this.str=str.replace(/\s+$/,"\n").replace(/\r\n?/g,"\n").replace(/\\ *\n/g,"\r").replace(/([,(:](?!\/\/[^ ])) *(?:\/\/[^\n]*)?\n\s*/g,comment).replace(/\s*\n[ \t]*([,)])/g,comment)}Lexer.prototype={inspect:function(){var tok,tmp=this.str,buf=[];while("eos"!=(tok=this.next()).type){buf.push(tok.inspect())}this.str=tmp;return buf.concat(tok.inspect()).join("\n")},lookahead:function(n){var fetch=n-this.stash.length;while(fetch-->0)this.stash.push(this.advance());return this.stash[--n]},skip:function(len){var chunk=len[0];len=chunk?chunk.length:len;this.str=this.str.substr(len);if(chunk){this.move(chunk)}else{this.column+=len}},move:function(str){var lines=str.match(/\n/g),idx=str.lastIndexOf("\n");if(lines)this.lineno+=lines.length;this.column=~idx?str.length-idx:this.column+str.length},next:function(){var tok=this.stashed()||this.advance();this.prev=tok;return tok},isPartOfSelector:function(){var tok=this.stash[this.stash.length-1]||this.prev;switch(tok&&tok.type){case"color":return 2==tok.val.raw.length;case".":case"[":return true}return false},advance:function(){var column=this.column,line=this.lineno,tok=this.eos()||this.nil()||this.sep()||this.keyword()||this.urlchars()||this.comment()||this.newline()||this.escaped()||this.important()||this.literal()||this.fun()||this.anonFunc()||this.atrule()||this.brace()||this.paren()||this.color()||this.string()||this.unit()||this.namedop()||this.boolean()||this.unicode()||this.ident()||this.op()||this.eol()||this.space()||this.selector();tok.lineno=line;tok.column=column;return tok},peek:function(){return this.lookahead(1)},stashed:function(){return this.stash.shift()},eos:function(){if(this.str.length)return;if(this.indentStack.length){this.indentStack.shift();return new Token("outdent")}else{return new Token("eos")}},urlchars:function(){var captures;if(!this.isURL)return;if(captures=/^[\/:@.;?&=*!,<>#%0-9]+/.exec(this.str)){this.skip(captures);return new Token("literal",new nodes.Literal(captures[0]))}},sep:function(){var captures;if(captures=/^;[ \t]*/.exec(this.str)){this.skip(captures);return new Token(";")}},eol:function(){if("\r"==this.str[0]){++this.lineno;this.skip(1);return this.advance()}},space:function(){var captures;if(captures=/^([ \t]+)/.exec(this.str)){this.skip(captures);return new Token("space")}},escaped:function(){var captures;if(captures=/^\\(.)[ \t]*/.exec(this.str)){var c=captures[1];this.skip(captures);return new Token("ident",new nodes.Literal(c))}},literal:function(){var captures;if(captures=/^@css[ \t]*\{/.exec(this.str)){this.skip(captures);var c,braces=1,css="",node;while(c=this.str[0]){this.str=this.str.substr(1);switch(c){case"{":++braces;break;case"}":--braces;break;case"\n":case"\r":++this.lineno;break}css+=c;if(!braces)break}css=css.replace(/\s*}$/,"");node=new nodes.Literal(css);node.css=true;return new Token("literal",node)}},important:function(){var captures;if(captures=/^!important[ \t]*/.exec(this.str)){this.skip(captures);return new Token("ident",new nodes.Literal("!important"))}},brace:function(){var captures;if(captures=/^([{}])/.exec(this.str)){this.skip(1);var brace=captures[1];return new Token(brace,brace)}},paren:function(){var captures;if(captures=/^([()])([ \t]*)/.exec(this.str)){var paren=captures[1];this.skip(captures);if(")"==paren)this.isURL=false;var tok=new Token(paren,paren);tok.space=captures[2];return tok}},nil:function(){var captures,tok;if(captures=/^(null)\b[ \t]*/.exec(this.str)){this.skip(captures);if(this.isPartOfSelector()){tok=new Token("ident",new nodes.Ident(captures[0]))}else{tok=new Token("null",nodes.nil)}return tok}},keyword:function(){var captures,tok;if(captures=/^(return|if|else|unless|for|in)\b[ \t]*/.exec(this.str)){var keyword=captures[1];this.skip(captures);if(this.isPartOfSelector()){tok=new Token("ident",new nodes.Ident(captures[0]))}else{tok=new Token(keyword,keyword)}return tok}},namedop:function(){var captures,tok;if(captures=/^(not|and|or|is a|is defined|isnt|is not|is)(?!-)\b([ \t]*)/.exec(this.str)){var op=captures[1];this.skip(captures);if(this.isPartOfSelector()){tok=new Token("ident",new nodes.Ident(captures[0]))}else{op=alias[op]||op;tok=new Token(op,op)}tok.space=captures[2];return tok}},op:function(){var captures;if(captures=/^([.]{1,3}|&&|\|\||[!<>=?:]=|\*\*|[-+*\/%]=?|[,=?:!~<>&\[\]])([ \t]*)/.exec(this.str)){var op=captures[1];this.skip(captures);op=alias[op]||op;var tok=new Token(op,op);tok.space=captures[2];this.isURL=false;return tok}},anonFunc:function(){var tok;if("@"==this.str[0]&&"("==this.str[1]){this.skip(2);tok=new Token("function",new nodes.Ident("anonymous"));tok.anonymous=true;return tok}},atrule:function(){var captures;if(captures=/^@(?:-(\w+)-)?([a-zA-Z0-9-_]+)[ \t]*/.exec(this.str)){this.skip(captures);var vendor=captures[1],type=captures[2],tok;switch(type){case"require":case"import":case"charset":case"namespace":case"media":case"scope":case"supports":return new Token(type);case"document":return new Token("-moz-document");case"block":return new Token("atblock");case"extend":case"extends":return new Token("extend");case"keyframes":return new Token(type,vendor);default:return new Token("atrule",vendor?"-"+vendor+"-"+type:type)}}},comment:function(){if("/"==this.str[0]&&"/"==this.str[1]){var end=this.str.indexOf("\n");if(-1==end)end=this.str.length;this.skip(end);return this.advance()}if("/"==this.str[0]&&"*"==this.str[1]){var end=this.str.indexOf("*/");if(-1==end)end=this.str.length;var str=this.str.substr(0,end+2),lines=str.split(/\n|\r/).length-1,suppress=true,inline=false;this.lineno+=lines;this.skip(end+2);if("!"==str[2]){str=str.replace("*!","*");suppress=false}if(this.prev&&";"==this.prev.type)inline=true;return new Token("comment",new nodes.Comment(str,suppress,inline))}},"boolean":function(){var captures;if(captures=/^(true|false)\b([ \t]*)/.exec(this.str)){var val=nodes.Boolean("true"==captures[1]);this.skip(captures);var tok=new Token("boolean",val);tok.space=captures[2];return tok}},unicode:function(){var captures;if(captures=/^u\+[0-9a-f?]{1,6}(?:-[0-9a-f]{1,6})?/i.exec(this.str)){this.skip(captures);return new Token("literal",new nodes.Literal(captures[0]))}},fun:function(){var captures;if(captures=/^(-*[_a-zA-Z$][-\w\d$]*)\(([ \t]*)/.exec(this.str)){var name=captures[1];this.skip(captures);this.isURL="url"==name;var tok=new Token("function",new nodes.Ident(name));tok.space=captures[2];return tok}},ident:function(){var captures;if(captures=/^-*[_a-zA-Z$][-\w\d$]*/.exec(this.str)){this.skip(captures);return new Token("ident",new nodes.Ident(captures[0]))}},newline:function(){var captures,re;if(this.indentRe){captures=this.indentRe.exec(this.str)}else{re=/^\n([\t]*)[ \t]*/;captures=re.exec(this.str);if(captures&&!captures[1].length){re=/^\n([ \t]*)/;captures=re.exec(this.str)}if(captures&&captures[1].length)this.indentRe=re}if(captures){var tok,indents=captures[1].length;this.skip(captures);if(this.str[0]===" "||this.str[0]===" "){throw new errors.SyntaxError("Invalid indentation. You can use tabs or spaces to indent, but not both.")}if("\n"==this.str[0])return this.advance();if(this.indentStack.length&&indentsindents){this.stash.push(new Token("outdent"));this.indentStack.shift()}tok=this.stash.pop()}else if(indents&&indents!=this.indentStack[0]){this.indentStack.unshift(indents);tok=new Token("indent")}else{tok=new Token("newline")}return tok}},unit:function(){var captures;if(captures=/^(-)?(\d+\.\d+|\d+|\.\d+)(%|[a-zA-Z]+)?[ \t]*/.exec(this.str)){this.skip(captures);var n=parseFloat(captures[2]);if("-"==captures[1])n=-n;var node=new nodes.Unit(n,captures[3]);node.raw=captures[0];return new Token("unit",node)}},string:function(){var captures;if(captures=/^("[^"]*"|'[^']*')[ \t]*/.exec(this.str)){var str=captures[1],quote=captures[0][0];this.skip(captures);str=str.slice(1,-1).replace(/\\n/g,"\n");return new Token("string",new nodes.String(str,quote))}},color:function(){return this.rrggbbaa()||this.rrggbb()||this.rgba()||this.rgb()||this.nn()||this.n()},n:function(){var captures;if(captures=/^#([a-fA-F0-9]{1})[ \t]*/.exec(this.str)){this.skip(captures);var n=parseInt(captures[1]+captures[1],16),color=new nodes.RGBA(n,n,n,1);color.raw=captures[0];return new Token("color",color)}},nn:function(){var captures;if(captures=/^#([a-fA-F0-9]{2})[ \t]*/.exec(this.str)){this.skip(captures);var n=parseInt(captures[1],16),color=new nodes.RGBA(n,n,n,1);color.raw=captures[0];return new Token("color",color)}},rgb:function(){var captures;if(captures=/^#([a-fA-F0-9]{3})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb[0]+rgb[0],16),g=parseInt(rgb[1]+rgb[1],16),b=parseInt(rgb[2]+rgb[2],16),color=new nodes.RGBA(r,g,b,1);color.raw=captures[0];return new Token("color",color)}},rgba:function(){var captures;if(captures=/^#([a-fA-F0-9]{4})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb[0]+rgb[0],16),g=parseInt(rgb[1]+rgb[1],16),b=parseInt(rgb[2]+rgb[2],16),a=parseInt(rgb[3]+rgb[3],16),color=new nodes.RGBA(r,g,b,a/255);color.raw=captures[0];return new Token("color",color)}},rrggbb:function(){var captures;if(captures=/^#([a-fA-F0-9]{6})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb.substr(0,2),16),g=parseInt(rgb.substr(2,2),16),b=parseInt(rgb.substr(4,2),16),color=new nodes.RGBA(r,g,b,1);color.raw=captures[0];return new Token("color",color)}},rrggbbaa:function(){var captures;if(captures=/^#([a-fA-F0-9]{8})[ \t]*/.exec(this.str)){this.skip(captures);var rgb=captures[1],r=parseInt(rgb.substr(0,2),16),g=parseInt(rgb.substr(2,2),16),b=parseInt(rgb.substr(4,2),16),a=parseInt(rgb.substr(6,2),16),color=new nodes.RGBA(r,g,b,a/255);color.raw=captures[0];return new Token("color",color)}},selector:function(){var captures;if(captures=/^\^|.*?(?=\/\/(?![^\[]*\])|[,\n{])/.exec(this.str)){var selector=captures[0];this.skip(captures);return new Token("selector",selector)}}}});require.register("nodes/arguments.js",function(module,exports,require){var Node=require("./node"),nodes=require("../nodes"),utils=require("../utils");var Arguments=module.exports=function Arguments(){nodes.Expression.call(this);this.map={}};Arguments.prototype.__proto__=nodes.Expression.prototype;Arguments.fromExpression=function(expr){var args=new Arguments,len=expr.nodes.length;args.lineno=expr.lineno;args.column=expr.column;args.isList=expr.isList;for(var i=0;ilen)self.nodes[i]=nodes.nil;self.nodes[n]=val}else if(unit.string){node=self.nodes[0];if(node&&"object"==node.nodeName)node.set(unit.string,val.clone())}});return val;case"[]":var expr=new nodes.Expression,vals=utils.unwrap(this).nodes,range=utils.unwrap(right).nodes,node;range.forEach(function(unit){if("unit"==unit.nodeName){node=vals[unit.val<0?vals.length+unit.val:unit.val]}else if("object"==vals[0].nodeName){node=vals[0].get(unit.string)}if(node)expr.push(node)});return expr.isEmpty?nodes.nil:utils.unwrap(expr);case"||":return this.toBoolean().isTrue?this:right;case"in":return Node.prototype.operate.call(this,op,right);case"!=":return this.operate("==",right,val).negate();case"==":var len=this.nodes.length,right=right.toExpression(),a,b;if(len!=right.nodes.length)return nodes.no;for(var i=0;i1)return nodes.yes;return this.first.toBoolean()};Expression.prototype.toString=function(){return"("+this.nodes.map(function(node){return node.toString()}).join(this.isList?", ":" ")+")"};Expression.prototype.toJSON=function(){return{__type:"Expression",isList:this.isList,preserve:this.preserve,lineno:this.lineno,column:this.column,filename:this.filename,nodes:this.nodes}}});require.register("nodes/function.js",function(module,exports,require){var Node=require("./node");var Function=module.exports=function Function(name,params,body){Node.call(this);this.name=name;this.params=params;this.block=body;if("function"==typeof params)this.fn=params};Function.prototype.__defineGetter__("arity",function(){return this.params.length});Function.prototype.__proto__=Node.prototype;Function.prototype.__defineGetter__("hash",function(){return"function "+this.name});Function.prototype.clone=function(parent){if(this.fn){var clone=new Function(this.name,this.fn)}else{var clone=new Function(this.name);clone.params=this.params.clone(parent,clone);clone.block=this.block.clone(parent,clone)}clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Function.prototype.toString=function(){if(this.fn){return this.name+"("+this.fn.toString().match(/^function *\w*\((.*?)\)/).slice(1).join(", ")+")"}else{return this.name+"("+this.params.nodes.join(", ")+")"}};Function.prototype.toJSON=function(){var json={__type:"Function",name:this.name,lineno:this.lineno,column:this.column,filename:this.filename};if(this.fn){json.fn=this.fn}else{json.params=this.params;json.block=this.block}return json}});require.register("nodes/group.js",function(module,exports,require){var Node=require("./node");var Group=module.exports=function Group(){Node.call(this);this.nodes=[];this.extends=[]};Group.prototype.__proto__=Node.prototype;Group.prototype.push=function(selector){this.nodes.push(selector)};Group.prototype.__defineGetter__("block",function(){return this.nodes[0].block});Group.prototype.__defineSetter__("block",function(block){for(var i=0,len=this.nodes.length;i=":case"<":case">":case"is a":case"||":case"&&":return this.rgba.operate(op,right);default:return this.rgba.operate(op,right).hsla}};exports.fromRGBA=function(rgba){var r=rgba.r/255,g=rgba.g/255,b=rgba.b/255,a=rgba.a;var min=Math.min(r,g,b),max=Math.max(r,g,b),l=(max+min)/2,d=max-min,h,s;switch(max){case min:h=0;break;case r:h=60*(g-b)/d;break;case g:h=60*(b-r)/d+120;break;case b:h=60*(r-g)/d+240;break}if(max==min){s=0}else if(l<.5){s=d/(2*l)}else{s=d/(2-2*l)}h%=360;s*=100;l*=100;return new HSLA(h,s,l,a)};HSLA.prototype.adjustLightness=function(percent){this.l=clampPercentage(this.l+this.l*(percent/100));return this};HSLA.prototype.adjustHue=function(deg){this.h=clampDegrees(this.h+deg);return this};function clampDegrees(n){n=n%360;return n>=0?n:360+n}function clampPercentage(n){return Math.max(0,Math.min(n,100))}function clampAlpha(n){return Math.max(0,Math.min(n,1))}});require.register("nodes/ident.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index");var Ident=module.exports=function Ident(name,val,mixin){Node.call(this);this.name=name;this.string=name;this.val=val||nodes.nil;this.mixin=!!mixin};Ident.prototype.__defineGetter__("isEmpty",function(){return undefined==this.val});Ident.prototype.__defineGetter__("hash",function(){return this.name});Ident.prototype.__proto__=Node.prototype;Ident.prototype.clone=function(parent){var clone=new Ident(this.name);clone.val=this.val.clone(parent,clone);clone.mixin=this.mixin;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;clone.property=this.property;clone.rest=this.rest;return clone};Ident.prototype.toJSON=function(){return{__type:"Ident",name:this.name,val:this.val,mixin:this.mixin,property:this.property,rest:this.rest,lineno:this.lineno,column:this.column,filename:this.filename}};Ident.prototype.toString=function(){return this.name};Ident.prototype.coerce=function(other){switch(other.nodeName){case"ident":case"string":case"literal":return new Ident(other.string);case"unit":return new Ident(other.toString());default:return Node.prototype.coerce.call(this,other)}};Ident.prototype.operate=function(op,right){var val=right.first;switch(op){case"-":if("unit"==val.nodeName){var expr=new nodes.Expression;val=val.clone();val.val=-val.val;expr.push(this);expr.push(val);return expr}case"+":return new nodes.Ident(this.string+this.coerce(val).string)}return Node.prototype.operate.call(this,op,right)}});require.register("nodes/if.js",function(module,exports,require){var Node=require("./node");var If=module.exports=function If(cond,negate){Node.call(this);this.cond=cond;this.elses=[];if(negate&&negate.nodeName){this.block=negate}else{this.negate=negate}};If.prototype.__proto__=Node.prototype;If.prototype.clone=function(parent){var clone=new If;clone.cond=this.cond.clone(parent,clone);clone.block=this.block.clone(parent,clone);clone.elses=this.elses.map(function(node){return node.clone(parent,clone)});clone.negate=this.negate;clone.postfix=this.postfix;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};If.prototype.toJSON=function(){return{__type:"If",cond:this.cond,block:this.block,elses:this.elses,negate:this.negate,postfix:this.postfix,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/import.js",function(module,exports,require){var Node=require("./node");var Import=module.exports=function Import(expr,once){Node.call(this);this.path=expr;this.once=once||false};Import.prototype.__proto__=Node.prototype;Import.prototype.clone=function(parent){var clone=new Import;clone.path=this.path.nodeName?this.path.clone(parent,clone):this.path;clone.once=this.once;clone.mtime=this.mtime;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Import.prototype.toJSON=function(){return{__type:"Import",path:this.path,once:this.once,mtime:this.mtime,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/extend.js",function(module,exports,require){var Node=require("./node");var Extend=module.exports=function Extend(selectors){Node.call(this);this.selectors=selectors};Extend.prototype.__proto__=Node.prototype;Extend.prototype.clone=function(){return new Extend(this.selectors)};Extend.prototype.toString=function(){return"@extend "+this.selectors.join(", ")};Extend.prototype.toJSON=function(){return{__type:"Extend",selectors:this.selectors,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/index.js",function(module,exports,require){exports.Node=require("./node");exports.Root=require("./root");exports.Null=require("./null");exports.Each=require("./each");exports.If=require("./if");exports.Call=require("./call");exports.UnaryOp=require("./unaryop");exports.BinOp=require("./binop");exports.Ternary=require("./ternary");exports.Block=require("./block");exports.Unit=require("./unit");exports.String=require("./string");exports.HSLA=require("./hsla");exports.RGBA=require("./rgba");exports.Ident=require("./ident");exports.Group=require("./group");exports.Literal=require("./literal");exports.Boolean=require("./boolean");exports.Return=require("./return");exports.Media=require("./media");exports.QueryList=require("./query-list");exports.Query=require("./query");exports.Feature=require("./feature");exports.Params=require("./params");exports.Comment=require("./comment");exports.Keyframes=require("./keyframes");exports.Member=require("./member");exports.Charset=require("./charset");exports.Namespace=require("./namespace");exports.Import=require("./import");exports.Extend=require("./extend");exports.Object=require("./object");exports.Function=require("./function");exports.Property=require("./property");exports.Selector=require("./selector");exports.Expression=require("./expression");exports.Arguments=require("./arguments");exports.Atblock=require("./atblock");exports.Atrule=require("./atrule");exports.Supports=require("./supports");exports.yes=new exports.Boolean(true);exports.no=new exports.Boolean(false);exports.nil=new exports.Null});require.register("nodes/keyframes.js",function(module,exports,require){var Atrule=require("./atrule");var Keyframes=module.exports=function Keyframes(segs,prefix){Atrule.call(this,"keyframes");this.segments=segs;this.prefix=prefix||"official"};Keyframes.prototype.__proto__=Atrule.prototype;Keyframes.prototype.clone=function(parent){var clone=new Keyframes;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;clone.segments=this.segments.map(function(node){return node.clone(parent,clone)});clone.prefix=this.prefix;clone.block=this.block.clone(parent,clone);return clone};Keyframes.prototype.toJSON=function(){return{__type:"Keyframes",segments:this.segments,prefix:this.prefix,block:this.block,lineno:this.lineno,column:this.column,filename:this.filename}};Keyframes.prototype.toString=function(){return"@keyframes "+this.segments.join("")}});require.register("nodes/literal.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index");var Literal=module.exports=function Literal(str){Node.call(this);this.val=str;this.string=str;this.prefixed=false};Literal.prototype.__proto__=Node.prototype;Literal.prototype.__defineGetter__("hash",function(){return this.val});Literal.prototype.toString=function(){return this.val};Literal.prototype.coerce=function(other){switch(other.nodeName){case"ident":case"string":case"literal":return new Literal(other.string);default:return Node.prototype.coerce.call(this,other)}};Literal.prototype.operate=function(op,right){var val=right.first;switch(op){case"+":return new nodes.Literal(this.string+this.coerce(val).string);default:return Node.prototype.operate.call(this,op,right)}};Literal.prototype.toJSON=function(){return{__type:"Literal",val:this.val,string:this.string,prefixed:this.prefixed,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/media.js",function(module,exports,require){var Atrule=require("./atrule");var Media=module.exports=function Media(val){Atrule.call(this,"media");this.val=val};Media.prototype.__proto__=Atrule.prototype;Media.prototype.clone=function(parent){var clone=new Media;clone.val=this.val.clone(parent,clone);clone.block=this.block.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Media.prototype.toJSON=function(){return{__type:"Media",val:this.val,block:this.block,lineno:this.lineno,column:this.column,filename:this.filename}};Media.prototype.toString=function(){return"@media "+this.val}});require.register("nodes/query-list.js",function(module,exports,require){var Node=require("./node");var QueryList=module.exports=function QueryList(){Node.call(this);this.nodes=[]};QueryList.prototype.__proto__=Node.prototype;QueryList.prototype.clone=function(parent){var clone=new QueryList;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;for(var i=0;i=":return nodes.Boolean(this.hash>=right.hash);case"<=":return nodes.Boolean(this.hash<=right.hash);case">":return nodes.Boolean(this.hash>right.hash);case"<":return nodes.Boolean(this.hash1)--h;if(h*6<1)return m1+(m2-m1)*h*6;if(h*2<1)return m2;if(h*3<2)return m1+(m2-m1)*(2/3-h)*6;return m1}return new RGBA(r,g,b,a)};function clamp(n){return Math.max(0,Math.min(n.toFixed(0),255))}function clampAlpha(n){return Math.max(0,Math.min(n,1))}});require.register("nodes/root.js",function(module,exports,require){var Node=require("./node");var Root=module.exports=function Root(){this.nodes=[]};Root.prototype.__proto__=Node.prototype;Root.prototype.push=function(node){this.nodes.push(node)};Root.prototype.unshift=function(node){this.nodes.unshift(node)};Root.prototype.clone=function(){var clone=new Root;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;this.nodes.forEach(function(node){clone.push(node.clone(clone,clone))});return clone};Root.prototype.toString=function(){return"[Root]"};Root.prototype.toJSON=function(){return{__type:"Root",nodes:this.nodes,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/selector.js",function(module,exports,require){var Block=require("./block"),Node=require("./node");var Selector=module.exports=function Selector(segs){Node.call(this);this.inherits=true;this.segments=segs;this.optional=false};Selector.prototype.__proto__=Node.prototype;Selector.prototype.toString=function(){return this.segments.join("")+(this.optional?" !optional":"")};Selector.prototype.__defineGetter__("isPlaceholder",function(){return this.val&&~this.val.substr(0,2).indexOf("$")});Selector.prototype.clone=function(parent){var clone=new Selector;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;clone.inherits=this.inherits;clone.val=this.val;clone.segments=this.segments.map(function(node){return node.clone(parent,clone)});clone.optional=this.optional;return clone};Selector.prototype.toJSON=function(){return{__type:"Selector",inherits:this.inherits,segments:this.segments,optional:this.optional,val:this.val,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/string.js",function(module,exports,require){var Node=require("./node"),sprintf=require("../functions").s,utils=require("../utils"),nodes=require("./index");var String=module.exports=function String(val,quote){Node.call(this);this.val=val;this.string=val;this.prefixed=false;if(typeof quote!=="string"){this.quote="'"}else{this.quote=quote}};String.prototype.__proto__=Node.prototype;String.prototype.toString=function(){return this.quote+this.val+this.quote};String.prototype.clone=function(){var clone=new String(this.val,this.quote);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};String.prototype.toJSON=function(){return{__type:"String",val:this.val,quote:this.quote,lineno:this.lineno,column:this.column,filename:this.filename}};String.prototype.toBoolean=function(){return nodes.Boolean(this.val.length)};String.prototype.coerce=function(other){switch(other.nodeName){case"string":return other;case"expression":return new String(other.nodes.map(function(node){return this.coerce(node).val},this).join(" "));default:return new String(other.toString())}};String.prototype.operate=function(op,right){switch(op){case"%":var expr=new nodes.Expression;expr.push(this);var args="expression"==right.nodeName?utils.unwrap(right).nodes:[right];return sprintf.apply(null,[expr].concat(args));case"+":var expr=new nodes.Expression;expr.push(new String(this.val+this.coerce(right).val));return expr;default:return Node.prototype.operate.call(this,op,right)}}});require.register("nodes/ternary.js",function(module,exports,require){var Node=require("./node");var Ternary=module.exports=function Ternary(cond,trueExpr,falseExpr){Node.call(this);this.cond=cond;this.trueExpr=trueExpr;this.falseExpr=falseExpr};Ternary.prototype.__proto__=Node.prototype;Ternary.prototype.clone=function(parent){var clone=new Ternary;clone.cond=this.cond.clone(parent,clone);clone.trueExpr=this.trueExpr.clone(parent,clone);clone.falseExpr=this.falseExpr.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Ternary.prototype.toJSON=function(){return{__type:"Ternary",cond:this.cond,trueExpr:this.trueExpr,falseExpr:this.falseExpr,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/unaryop.js",function(module,exports,require){var Node=require("./node");var UnaryOp=module.exports=function UnaryOp(op,expr){Node.call(this);this.op=op;this.expr=expr};UnaryOp.prototype.__proto__=Node.prototype;UnaryOp.prototype.clone=function(parent){var clone=new UnaryOp(this.op);clone.expr=this.expr.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};UnaryOp.prototype.toJSON=function(){return{__type:"UnaryOp",op:this.op,expr:this.expr,lineno:this.lineno,column:this.column,filename:this.filename}}});require.register("nodes/unit.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index");var FACTOR_TABLE={mm:{val:1,label:"mm"},cm:{val:10,label:"mm"},"in":{val:25.4,label:"mm"},pt:{val:25.4/72,label:"mm"},ms:{val:1,label:"ms"},s:{val:1e3,label:"ms"},Hz:{val:1,label:"Hz"},kHz:{val:1e3,label:"Hz"}};var Unit=module.exports=function Unit(val,type){Node.call(this);this.val=val;this.type=type};Unit.prototype.__proto__=Node.prototype;Unit.prototype.toBoolean=function(){return nodes.Boolean(this.type?true:this.val)};Unit.prototype.toString=function(){return this.val+(this.type||"")};Unit.prototype.clone=function(){var clone=new Unit(this.val,this.type);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Unit.prototype.toJSON=function(){return{__type:"Unit",val:this.val,type:this.type,lineno:this.lineno,column:this.column,filename:this.filename}};Unit.prototype.operate=function(op,right){var type=this.type||right.first.type;if("rgba"==right.nodeName||"hsla"==right.nodeName){return right.operate(op,this)}if(this.shouldCoerce(op)){right=right.first;if("%"!=this.type&&("-"==op||"+"==op)&&"%"==right.type){right=new Unit(this.val*(right.val/100),"%")}else{right=this.coerce(right)}switch(op){case"-":return new Unit(this.val-right.val,type);case"+":type=type||right.type=="%"&&right.type;return new Unit(this.val+right.val,type);case"/":return new Unit(this.val/right.val,type);case"*":return new Unit(this.val*right.val,type);case"%":return new Unit(this.val%right.val,type);case"**":return new Unit(Math.pow(this.val,right.val),type);case"..":case"...":var start=this.val,end=right.val,expr=new nodes.Expression,inclusive=".."==op;if(start=end:--start>end)}return expr}}return Node.prototype.operate.call(this,op,right)};Unit.prototype.coerce=function(other){if("unit"==other.nodeName){var a=this,b=other,factorA=FACTOR_TABLE[a.type],factorB=FACTOR_TABLE[b.type];if(factorA&&factorB&&factorA.label==factorB.label){var bVal=b.val*(factorB.val/factorA.val);return new nodes.Unit(bVal,a.type)}else{return new nodes.Unit(b.val,a.type)}}else if("string"==other.nodeName){if("%"==other.val)return new nodes.Unit(0,"%");var val=parseFloat(other.val);if(isNaN(val))Node.prototype.coerce.call(this,other);return new nodes.Unit(val)}else{return Node.prototype.coerce.call(this,other)}}});require.register("nodes/object.js",function(module,exports,require){var Node=require("./node"),nodes=require("./index"),nativeObj={}.constructor;var Object=module.exports=function Object(){Node.call(this);this.vals={}};Object.prototype.__proto__=Node.prototype;Object.prototype.set=function(key,val){this.vals[key]=val;return this};Object.prototype.__defineGetter__("length",function(){return nativeObj.keys(this.vals).length});Object.prototype.get=function(key){return this.vals[key]||nodes.nil};Object.prototype.has=function(key){return key in this.vals};Object.prototype.operate=function(op,right){switch(op){case".":case"[]":return this.get(right.hash);case"==":var vals=this.vals,a,b;if("object"!=right.nodeName||this.length!=right.length)return nodes.no;for(var key in vals){a=vals[key];b=right.vals[key];if(a.operate(op,b).isFalse)return nodes.no}return nodes.yes;case"!=":return this.operate("==",right).negate();default:return Node.prototype.operate.call(this,op,right)}};Object.prototype.toBoolean=function(){return nodes.Boolean(this.length)};Object.prototype.toBlock=function(){var str="{",key,val;for(key in this.vals){val=this.get(key);if("object"==val.first.nodeName){str+=key+" "+val.first.toBlock()}else{switch(key){case"@charset":str+=key+" "+val.first.toString()+";";break;default:str+=key+":"+toString(val)+";"}}}str+="}";return str;function toString(node){if(node.nodes){return node.nodes.map(toString).join(node.isList?",":" ")}else if("literal"==node.nodeName&&","==node.val){return"\\,"}return node.toString()}};Object.prototype.clone=function(parent){var clone=new Object;clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;for(var key in this.vals){clone.vals[key]=this.vals[key].clone(parent,clone)}return clone};Object.prototype.toJSON=function(){return{__type:"Object",vals:this.vals,lineno:this.lineno,column:this.column,filename:this.filename}};Object.prototype.toString=function(){var obj={};for(var prop in this.vals){obj[prop]=this.vals[prop].toString()}return JSON.stringify(obj)}});require.register("nodes/supports.js",function(module,exports,require){var Atrule=require("./atrule");var Supports=module.exports=function Supports(condition){Atrule.call(this,"supports");this.condition=condition};Supports.prototype.__proto__=Atrule.prototype;Supports.prototype.clone=function(parent){var clone=new Supports;clone.condition=this.condition.clone(parent,clone); +clone.block=this.block.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Supports.prototype.toJSON=function(){return{__type:"Supports",condition:this.condition,block:this.block,lineno:this.lineno,column:this.column,filename:this.filename}};Supports.prototype.toString=function(){return"@supports "+this.condition}});require.register("nodes/member.js",function(module,exports,require){var Node=require("./node");var Member=module.exports=function Member(left,right){Node.call(this);this.left=left;this.right=right};Member.prototype.__proto__=Node.prototype;Member.prototype.clone=function(parent){var clone=new Member;clone.left=this.left.clone(parent,clone);clone.right=this.right.clone(parent,clone);if(this.val)clone.val=this.val.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Member.prototype.toJSON=function(){var json={__type:"Member",left:this.left,right:this.right,lineno:this.lineno,column:this.column,filename:this.filename};if(this.val)json.val=this.val;return json};Member.prototype.toString=function(){return this.left.toString()+"."+this.right.toString()}});require.register("nodes/atblock.js",function(module,exports,require){var Node=require("./node");var Atblock=module.exports=function Atblock(){Node.call(this)};Atblock.prototype.__defineGetter__("nodes",function(){return this.block.nodes});Atblock.prototype.__proto__=Node.prototype;Atblock.prototype.clone=function(parent){var clone=new Atblock;clone.block=this.block.clone(parent,clone);clone.lineno=this.lineno;clone.column=this.column;clone.filename=this.filename;return clone};Atblock.prototype.toString=function(){return"@block"};Atblock.prototype.toJSON=function(){return{__type:"Atblock",block:this.block,lineno:this.lineno,column:this.column,fileno:this.fileno}}});require.register("nodes/atrule.js",function(module,exports,require){var Node=require("./node");var Atrule=module.exports=function Atrule(type){Node.call(this);this.type=type};Atrule.prototype.__proto__=Node.prototype;Atrule.prototype.__defineGetter__("hasOnlyProperties",function(){if(!this.block)return false;var nodes=this.block.nodes;for(var i=0,len=nodes.length;i","=",":","&","&&","~","{","}",".","..","/"];var pseudoSelectors=["matches","not","dir","lang","any-link","link","visited","local-link","target","scope","hover","active","focus","drop","current","past","future","enabled","disabled","read-only","read-write","placeholder-shown","checked","indeterminate","valid","invalid","in-range","out-of-range","required","optional","user-error","root","empty","blank","nth-child","nth-last-child","first-child","last-child","only-child","nth-of-type","nth-last-of-type","first-of-type","last-of-type","only-of-type","nth-match","nth-last-match","nth-column","nth-last-column","first-line","first-letter","before","after","selection"];var Parser=module.exports=function Parser(str,options){var self=this;options=options||{};this.lexer=new Lexer(str,options);this.prefix=options.prefix||"";this.root=options.root||new nodes.Root;this.state=["root"];this.stash=[];this.parens=0;this.css=0;this.state.pop=function(){self.prevState=[].pop.call(this)}};Parser.prototype={constructor:Parser,currentState:function(){return this.state[this.state.length-1]},previousState:function(){return this.state[this.state.length-2]},parse:function(){var block=this.parent=this.root;while("eos"!=this.peek().type){this.skipWhitespace();if("eos"==this.peek().type)break;var stmt=this.statement();this.accept(";");if(!stmt)this.error("unexpected token {peek}, not allowed at the root level");block.push(stmt)}return block},error:function(msg){var type=this.peek().type,val=undefined==this.peek().val?"":" "+this.peek().toString();if(val.trim()==type.trim())val="";throw new errors.ParseError(msg.replace("{peek}",'"'+type+val+'"'))},accept:function(type){if(type==this.peek().type){return this.next()}},expect:function(type){if(type!=this.peek().type){this.error('expected "'+type+'", got {peek}')}return this.next()},next:function(){var tok=this.stash.length?this.stash.pop():this.lexer.next(),line=tok.lineno,column=tok.column||1;if(tok.val&&tok.val.nodeName){tok.val.lineno=line;tok.val.column=column}nodes.lineno=line;nodes.column=column;return tok},peek:function(){return this.lexer.peek()},lookahead:function(n){return this.lexer.lookahead(n)},isSelectorToken:function(n){var la=this.lookahead(n).type;switch(la){case"for":return this.bracketed;case"[":this.bracketed=true;return true;case"]":this.bracketed=false;return true;default:return~selectorTokens.indexOf(la)}},isPseudoSelector:function(n){var val=this.lookahead(n).val;return val&&~pseudoSelectors.indexOf(val.name)},lineContains:function(type){var i=1,la;while(la=this.lookahead(i++)){if(~["indent","outdent","newline","eos"].indexOf(la.type))return;if(type==la.type)return true}},selectorToken:function(){if(this.isSelectorToken(1)){if("{"==this.peek().type){if(!this.lineContains("}"))return;var i=0,la;while(la=this.lookahead(++i)){if("}"==la.type){if(i==2||i==3&&this.lookahead(i-1).type=="space")return;break}if(":"==la.type)return}}return this.next()}},skip:function(tokens){while(~tokens.indexOf(this.peek().type))this.next()},skipWhitespace:function(){this.skip(["space","indent","outdent","newline"])},skipNewlines:function(){while("newline"==this.peek().type)this.next()},skipSpaces:function(){while("space"==this.peek().type)this.next()},skipSpacesAndComments:function(){while("space"==this.peek().type||"comment"==this.peek().type)this.next()},looksLikeFunctionDefinition:function(i){return"indent"==this.lookahead(i).type||"{"==this.lookahead(i).type},looksLikeSelector:function(fromProperty){var i=1,brace;if(fromProperty&&":"==this.lookahead(i+1).type&&(this.lookahead(i+1).space||"indent"==this.lookahead(i+2).type))return false;while("ident"==this.lookahead(i).type&&("newline"==this.lookahead(i+1).type||","==this.lookahead(i+1).type))i+=2;while(this.isSelectorToken(i)||","==this.lookahead(i).type){if("selector"==this.lookahead(i).type)return true;if("&"==this.lookahead(i+1).type)return true;if("."==this.lookahead(i).type&&"ident"==this.lookahead(i+1).type)return true;if("*"==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true;if(":"==this.lookahead(i).type&&":"==this.lookahead(i+1).type)return true;if("color"==this.lookahead(i).type&&"newline"==this.lookahead(i-1).type)return true;if(this.looksLikeAttributeSelector(i))return true;if(("="==this.lookahead(i).type||"function"==this.lookahead(i).type)&&"{"==this.lookahead(i+1).type)return false;if(":"==this.lookahead(i).type&&!this.isPseudoSelector(i+1)&&this.lineContains("."))return false;if("{"==this.lookahead(i).type)brace=true;else if("}"==this.lookahead(i).type)brace=false;if(brace&&":"==this.lookahead(i).type)return true;if("space"==this.lookahead(i).type&&"{"==this.lookahead(i+1).type)return true;if(":"==this.lookahead(i++).type&&!this.lookahead(i-1).space&&this.isPseudoSelector(i))return true;if("space"==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type&&"{"==this.lookahead(i+2).type)return true;if(","==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true}if(","==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true;if("{"==this.lookahead(i).type&&"newline"==this.lookahead(i+1).type)return true;if(this.css){if(";"==this.lookahead(i).type||"}"==this.lookahead(i-1).type)return false}while(!~["indent","outdent","newline","for","if",";","}","eos"].indexOf(this.lookahead(i).type))++i;if("indent"==this.lookahead(i).type)return true},looksLikeAttributeSelector:function(n){var type=this.lookahead(n).type;if("="==type&&this.bracketed)return true;return("ident"==type||"string"==type)&&"]"==this.lookahead(n+1).type&&("newline"==this.lookahead(n+2).type||this.isSelectorToken(n+2))&&!this.lineContains(":")&&!this.lineContains("=")},looksLikeKeyframe:function(){var i=2,type;switch(this.lookahead(i).type){case"{":case"indent":case",":return true;case"newline":while("unit"==this.lookahead(++i).type||"newline"==this.lookahead(i).type);type=this.lookahead(i).type;return"indent"==type||"{"==type}},stateAllowsSelector:function(){switch(this.currentState()){case"root":case"atblock":case"selector":case"conditional":case"function":case"atrule":case"for":return true}},assignAtblock:function(expr){try{expr.push(this.atblock(expr))}catch(err){this.error("invalid right-hand side operand in assignment, got {peek}")}},statement:function(){var stmt=this.stmt(),state=this.prevState,block,op;if(this.allowPostfix){this.allowPostfix=false;state="expression"}switch(state){case"assignment":case"expression":case"function arguments":while(op=this.accept("if")||this.accept("unless")||this.accept("for")){switch(op.type){case"if":case"unless":stmt=new nodes.If(this.expression(),stmt);stmt.postfix=true;stmt.negate="unless"==op.type;this.accept(";");break;case"for":var key,val=this.id().name;if(this.accept(","))key=this.id().name;this.expect("in");var each=new nodes.Each(val,key,this.expression());block=new nodes.Block(this.parent,each);block.push(stmt);each.block=block;stmt=each}}}return stmt},stmt:function(){var type=this.peek().type;switch(type){case"keyframes":return this.keyframes();case"-moz-document":return this.mozdocument();case"comment":case"selector":case"extend":case"literal":case"charset":case"namespace":case"require":case"extend":case"media":case"atrule":case"ident":case"scope":case"supports":case"unless":return this[type]();case"function":return this.fun();case"import":return this.atimport();case"if":return this.ifstmt();case"for":return this.forin();case"return":return this.ret();case"{":return this.property();default:if(this.stateAllowsSelector()){switch(type){case"color":case"~":case">":case"<":case":":case"&":case"&&":case"[":case".":case"/":return this.selector();case"..":if("/"==this.lookahead(2).type)return this.selector();case"+":return"function"==this.lookahead(2).type?this.functionCall():this.selector();case"*":return this.property();case"unit":if(this.looksLikeKeyframe())return this.selector();case"-":if("{"==this.lookahead(2).type)return this.property()}}var expr=this.expression();if(expr.isEmpty)this.error("unexpected {peek}");return expr}},block:function(node,scope){var delim,stmt,next,block=this.parent=new nodes.Block(this.parent,node);if(false===scope)block.scope=false;this.accept("newline");if(this.accept("{")){this.css++;delim="}";this.skipWhitespace()}else{delim="outdent";this.expect("indent")}while(delim!=this.peek().type){if(this.css){if(this.accept("newline")||this.accept("indent"))continue;stmt=this.statement();this.accept(";");this.skipWhitespace()}else{if(this.accept("newline"))continue;next=this.lookahead(2).type;if("indent"==this.peek().type&&~["outdent","newline","comment"].indexOf(next)){this.skip(["indent","outdent"]);continue}if("eos"==this.peek().type)return block;stmt=this.statement();this.accept(";")}if(!stmt)this.error("unexpected token {peek} in block");block.push(stmt)}if(this.css){this.skipWhitespace();this.expect("}");this.skipSpaces();this.css--}else{this.expect("outdent")}this.parent=block.parent;return block},comment:function(){var node=this.next().val;this.skipSpaces();return node},forin:function(){this.expect("for");var key,val=this.id().name;if(this.accept(","))key=this.id().name;this.expect("in");this.state.push("for");this.cond=true;var each=new nodes.Each(val,key,this.expression());this.cond=false;each.block=this.block(each,false);this.state.pop();return each},ret:function(){this.expect("return");var expr=this.expression();return expr.isEmpty?new nodes.Return:new nodes.Return(expr)},unless:function(){this.expect("unless");this.state.push("conditional");this.cond=true;var node=new nodes.If(this.expression(),true);this.cond=false;node.block=this.block(node,false);this.state.pop();return node},ifstmt:function(){this.expect("if");this.state.push("conditional");this.cond=true;var node=new nodes.If(this.expression()),cond,block;this.cond=false;node.block=this.block(node,false);this.skip(["newline","comment"]);while(this.accept("else")){if(this.accept("if")){this.cond=true;cond=this.expression();this.cond=false;block=this.block(node,false);node.elses.push(new nodes.If(cond,block))}else{node.elses.push(this.block(node,false));break}this.skip(["newline","comment"])}this.state.pop();return node},atblock:function(node){if(!node)this.expect("atblock");node=new nodes.Atblock;this.state.push("atblock");node.block=this.block(node,false);this.state.pop();return node},atrule:function(){var type=this.expect("atrule").val,node=new nodes.Atrule(type),tok;this.skipSpacesAndComments();node.segments=this.selectorParts();this.skipSpacesAndComments();tok=this.peek().type;if("indent"==tok||"{"==tok||"newline"==tok&&"{"==this.lookahead(2).type){this.state.push("atrule");node.block=this.block(node);this.state.pop()}return node},scope:function(){this.expect("scope");var selector=this.selectorParts().map(function(selector){return selector.val}).join("");this.selectorScope=selector.trim();return nodes.nil},supports:function(){this.expect("supports");var node=new nodes.Supports(this.supportsCondition());this.state.push("atrule");node.block=this.block(node);this.state.pop();return node},supportsCondition:function(){var node=this.supportsNegation()||this.supportsOp();if(!node){this.cond=true;node=this.expression();this.cond=false}return node},supportsNegation:function(){if(this.accept("not")){var node=new nodes.Expression;node.push(new nodes.Literal("not"));node.push(this.supportsFeature());return node}},supportsOp:function(){var feature=this.supportsFeature(),op,expr;if(feature){expr=new nodes.Expression;expr.push(feature);while(op=this.accept("&&")||this.accept("||")){expr.push(new nodes.Literal("&&"==op.val?"and":"or"));expr.push(this.supportsFeature())}return expr}},supportsFeature:function(){this.skipSpacesAndComments();if("("==this.peek().type){var la=this.lookahead(2).type;if("ident"==la||"{"==la){return this.feature()}else{this.expect("(");var node=new nodes.Expression;node.push(new nodes.Literal("("));node.push(this.supportsCondition());this.expect(")");node.push(new nodes.Literal(")"));this.skipSpacesAndComments();return node}}},extend:function(){var tok=this.expect("extend"),selectors=[],sel,node,arr;do{arr=this.selectorParts();if(!arr.length)continue;sel=new nodes.Selector(arr);selectors.push(sel);if("!"!==this.peek().type)continue;tok=this.lookahead(2);if("ident"!==tok.type||"optional"!==tok.val.name)continue;this.skip(["!","ident"]);sel.optional=true}while(this.accept(","));node=new nodes.Extend(selectors);node.lineno=tok.lineno;node.column=tok.column;return node},media:function(){this.expect("media");this.state.push("atrule");var media=new nodes.Media(this.queries());media.block=this.block(media);this.state.pop();return media},queries:function(){var queries=new nodes.QueryList,skip=["comment","newline","space"];do{this.skip(skip);queries.push(this.query());this.skip(skip)}while(this.accept(","));return queries},query:function(){var query=new nodes.Query,expr,pred,id;if("ident"==this.peek().type&&("."==this.lookahead(2).type||"["==this.lookahead(2).type)){this.cond=true;expr=this.expression();this.cond=false;query.push(new nodes.Feature(expr.nodes));return query}if(pred=this.accept("ident")||this.accept("not")){pred=new nodes.Literal(pred.val.string||pred.val);this.skipSpacesAndComments();if(id=this.accept("ident")){query.type=id.val;query.predicate=pred}else{query.type=pred}this.skipSpacesAndComments();if(!this.accept("&&"))return query}do{query.push(this.feature())}while(this.accept("&&"));return query},feature:function(){this.skipSpacesAndComments();this.expect("(");this.skipSpacesAndComments();var node=new nodes.Feature(this.interpolate());this.skipSpacesAndComments();this.accept(":");this.skipSpacesAndComments();this.inProperty=true;node.expr=this.list();this.inProperty=false;this.skipSpacesAndComments();this.expect(")");this.skipSpacesAndComments();return node},mozdocument:function(){this.expect("-moz-document");var mozdocument=new nodes.Atrule("-moz-document"),calls=[];do{this.skipSpacesAndComments();calls.push(this.functionCall());this.skipSpacesAndComments()}while(this.accept(","));mozdocument.segments=[new nodes.Literal(calls.join(", "))];this.state.push("atrule");mozdocument.block=this.block(mozdocument,false);this.state.pop();return mozdocument},atimport:function(){this.expect("import");this.allowPostfix=true;return new nodes.Import(this.expression(),false)},require:function(){this.expect("require");this.allowPostfix=true;return new nodes.Import(this.expression(),true)},charset:function(){this.expect("charset");var str=this.expect("string").val;this.allowPostfix=true;return new nodes.Charset(str)},namespace:function(){var str,prefix;this.expect("namespace");this.skipSpacesAndComments();if(prefix=this.accept("ident")){prefix=prefix.val}this.skipSpacesAndComments();str=this.accept("string")||this.url();this.allowPostfix=true;return new nodes.Namespace(str,prefix)},keyframes:function(){var tok=this.expect("keyframes"),keyframes;this.skipSpacesAndComments();keyframes=new nodes.Keyframes(this.selectorParts(),tok.val);this.skipSpacesAndComments();this.state.push("atrule");keyframes.block=this.block(keyframes);this.state.pop();return keyframes},literal:function(){return this.expect("literal").val},id:function(){var tok=this.expect("ident");this.accept("space");return tok.val},ident:function(){var i=2,la=this.lookahead(i).type;while("space"==la)la=this.lookahead(++i).type;switch(la){case"=":case"?=":case"-=":case"+=":case"*=":case"/=":case"%=":return this.assignment();case".":if("space"==this.lookahead(i-1).type)return this.selector();if(this._ident==this.peek())return this.id();while("="!=this.lookahead(++i).type&&!~["[",",","newline","indent","eos"].indexOf(this.lookahead(i).type));if("="==this.lookahead(i).type){this._ident=this.peek();return this.expression()}else if(this.looksLikeSelector()&&this.stateAllowsSelector()){return this.selector()}case"[":if(this._ident==this.peek())return this.id();while("]"!=this.lookahead(i++).type&&"selector"!=this.lookahead(i).type&&"eos"!=this.lookahead(i).type);if("="==this.lookahead(i).type){this._ident=this.peek();return this.expression()}else if(this.looksLikeSelector()&&this.stateAllowsSelector()){return this.selector()}case"-":case"+":case"/":case"*":case"%":case"**":case"&&":case"||":case">":case"<":case">=":case"<=":case"!=":case"==":case"?":case"in":case"is a":case"is defined":if(this._ident==this.peek()){return this.id()}else{this._ident=this.peek();switch(this.currentState()){case"for":case"selector":return this.property();case"root":case"atblock":case"atrule":return"["==la?this.subscript():this.selector();case"function":case"conditional":return this.looksLikeSelector()?this.selector():this.expression();default:return this.operand?this.id():this.expression()}}default:switch(this.currentState()){case"root":return this.selector();case"for":case"selector":case"function":case"conditional":case"atblock":case"atrule":return this.property();default:var id=this.id();if("interpolation"==this.previousState())id.mixin=true;return id}}},interpolate:function(){var node,segs=[],star;star=this.accept("*");if(star)segs.push(new nodes.Literal("*"));while(true){if(this.accept("{")){this.state.push("interpolation");segs.push(this.expression());this.expect("}");this.state.pop()}else if(node=this.accept("-")){segs.push(new nodes.Literal("-"))}else if(node=this.accept("ident")){segs.push(node.val)}else{break}}if(!segs.length)this.expect("ident");return segs},property:function(){if(this.looksLikeSelector(true))return this.selector();var ident=this.interpolate(),prop=new nodes.Property(ident),ret=prop;this.accept("space");if(this.accept(":"))this.accept("space");this.state.push("property");this.inProperty=true;prop.expr=this.list();if(prop.expr.isEmpty)ret=ident[0];this.inProperty=false;this.allowPostfix=true;this.state.pop();this.accept(";");return ret},selector:function(){var arr,group=new nodes.Group,scope=this.selectorScope,isRoot="root"==this.currentState(),selector;do{this.accept("newline");arr=this.selectorParts();if(isRoot&&scope)arr.unshift(new nodes.Literal(scope+" "));if(arr.length){selector=new nodes.Selector(arr);selector.lineno=arr[0].lineno;selector.column=arr[0].column;group.push(selector)}}while(this.accept(",")||this.accept("newline"));if("selector-parts"==this.currentState())return group.nodes;this.state.push("selector");group.block=this.block(group);this.state.pop();return group},selectorParts:function(){var tok,arr=[];while(tok=this.selectorToken()){switch(tok.type){case"{":this.skipSpaces();var expr=this.expression();this.skipSpaces();this.expect("}");arr.push(expr);break;case this.prefix&&".":var literal=new nodes.Literal(tok.val+this.prefix);literal.prefixed=true;arr.push(literal);break;case"comment":break;case"color":case"unit":arr.push(new nodes.Literal(tok.val.raw));break;case"space":arr.push(new nodes.Literal(" "));break;case"function":arr.push(new nodes.Literal(tok.val.name+"("));break;case"ident":arr.push(new nodes.Literal(tok.val.name||tok.val.string));break;default:arr.push(new nodes.Literal(tok.val));if(tok.space)arr.push(new nodes.Literal(" "))}}return arr},assignment:function(){var op,node,name=this.id().name;if(op=this.accept("=")||this.accept("?=")||this.accept("+=")||this.accept("-=")||this.accept("*=")||this.accept("/=")||this.accept("%=")){this.state.push("assignment");var expr=this.list();if(expr.isEmpty)this.assignAtblock(expr);node=new nodes.Ident(name,expr);this.state.pop();switch(op.type){case"?=":var defined=new nodes.BinOp("is defined",node),lookup=new nodes.Ident(name);node=new nodes.Ternary(defined,lookup,node);break;case"+=":case"-=":case"*=":case"/=":case"%=":node.val=new nodes.BinOp(op.type[0],new nodes.Ident(name),expr);break}}return node},fun:function(){var parens=1,i=2,tok;out:while(tok=this.lookahead(i++)){switch(tok.type){case"function":case"(":++parens;break;case")":if(!--parens)break out;break;case"eos":this.error('failed to find closing paren ")"')}}switch(this.currentState()){case"expression":return this.functionCall();default:return this.looksLikeFunctionDefinition(i)?this.functionDefinition():this.expression()}},url:function(){this.expect("function");this.state.push("function arguments");var args=this.args();this.expect(")");this.state.pop();return new nodes.Call("url",args)},functionCall:function(){var withBlock=this.accept("+");if("url"==this.peek().val.name)return this.url();var name=this.expect("function").val.name;this.state.push("function arguments");this.parens++;var args=this.args();this.expect(")");this.parens--;this.state.pop();var call=new nodes.Call(name,args);if(withBlock){this.state.push("function");call.block=this.block(call);this.state.pop()}return call},functionDefinition:function(){var name=this.expect("function").val.name;this.state.push("function params");this.skipWhitespace();var params=this.params();this.skipWhitespace();this.expect(")");this.state.pop();this.state.push("function");var fn=new nodes.Function(name,params);fn.block=this.block(fn);this.state.pop();return new nodes.Ident(name,fn)},params:function(){var tok,node,params=new nodes.Params;while(tok=this.accept("ident")){this.accept("space");params.push(node=tok.val);if(this.accept("...")){node.rest=true}else if(this.accept("=")){node.val=this.expression()}this.skipWhitespace();this.accept(",");this.skipWhitespace()}return params},args:function(){var args=new nodes.Arguments,keyword;do{if("ident"==this.peek().type&&":"==this.lookahead(2).type){keyword=this.next().val.string;this.expect(":");args.map[keyword]=this.expression()}else{args.push(this.expression())}}while(this.accept(","));return args},list:function(){var node=this.expression();while(this.accept(",")){if(node.isList){list.push(this.expression())}else{var list=new nodes.Expression(true);list.push(node);list.push(this.expression());node=list}}return node},expression:function(){var node,expr=new nodes.Expression;this.state.push("expression");while(node=this.negation()){if(!node)this.error("unexpected token {peek} in expression");expr.push(node)}this.state.pop();if(expr.nodes.length){expr.lineno=expr.nodes[0].lineno;expr.column=expr.nodes[0].column}return expr},negation:function(){if(this.accept("not")){return new nodes.UnaryOp("!",this.negation())}return this.ternary()},ternary:function(){var node=this.logical();if(this.accept("?")){var trueExpr=this.expression();this.expect(":");var falseExpr=this.expression();node=new nodes.Ternary(node,trueExpr,falseExpr)}return node},logical:function(){var op,node=this.typecheck();while(op=this.accept("&&")||this.accept("||")){node=new nodes.BinOp(op.type,node,this.typecheck())}return node},typecheck:function(){var op,node=this.equality();while(op=this.accept("is a")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.equality());this.operand=false}return node},equality:function(){var op,node=this.inop();while(op=this.accept("==")||this.accept("!=")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.inop());this.operand=false}return node},inop:function(){var node=this.relational();while(this.accept("in")){this.operand=true;if(!node)this.error('illegal unary "in", missing left-hand operand');node=new nodes.BinOp("in",node,this.relational());this.operand=false}return node},relational:function(){var op,node=this.range();while(op=this.accept(">=")||this.accept("<=")||this.accept("<")||this.accept(">")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.range());this.operand=false}return node},range:function(){var op,node=this.additive();if(op=this.accept("...")||this.accept("..")){this.operand=true;if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.val,node,this.additive());this.operand=false}return node},additive:function(){var op,node=this.multiplicative();while(op=this.accept("+")||this.accept("-")){this.operand=true;node=new nodes.BinOp(op.type,node,this.multiplicative());this.operand=false}return node},multiplicative:function(){var op,node=this.defined();while(op=this.accept("**")||this.accept("*")||this.accept("/")||this.accept("%")){this.operand=true;if("/"==op&&this.inProperty&&!this.parens){this.stash.push(new Token("literal",new nodes.Literal("/")));this.operand=false;return node}else{if(!node)this.error('illegal unary "'+op+'", missing left-hand operand');node=new nodes.BinOp(op.type,node,this.defined());this.operand=false}}return node},defined:function(){var node=this.unary();if(this.accept("is defined")){if(!node)this.error('illegal unary "is defined", missing left-hand operand');node=new nodes.BinOp("is defined",node)}return node},unary:function(){var op,node;if(op=this.accept("!")||this.accept("~")||this.accept("+")||this.accept("-")){this.operand=true;node=this.unary();if(!node)this.error('illegal unary "'+op+'"');node=new nodes.UnaryOp(op.type,node);this.operand=false;return node}return this.subscript()},subscript:function(){var node=this.member(),id;while(this.accept("[")){node=new nodes.BinOp("[]",node,this.expression());this.expect("]")}if(this.accept("=")){node.op+="=";node.val=this.list();if(node.val.isEmpty)this.assignAtblock(node.val)}return node},member:function(){var node=this.primary();if(node){while(this.accept(".")){var id=new nodes.Ident(this.expect("ident").val.string);node=new nodes.Member(node,id)}this.skipSpaces();if(this.accept("=")){node.val=this.list();if(node.val.isEmpty)this.assignAtblock(node.val)}}return node},object:function(){var obj=new nodes.Object,id,val,comma;this.expect("{");this.skipWhitespace();while(!this.accept("}")){if(this.accept("comment")||this.accept("newline"))continue;if(!comma)this.accept(",");id=this.accept("ident")||this.accept("string");if(!id)this.error('expected "ident" or "string", got {peek}');id=id.val.hash;this.skipSpacesAndComments();this.expect(":");val=this.expression();obj.set(id,val);comma=this.accept(",");this.skipWhitespace()}return obj},primary:function(){var tok;this.skipSpaces();if(this.accept("(")){++this.parens;var expr=this.expression(),paren=this.expect(")");--this.parens;if(this.accept("%"))expr.push(new nodes.Ident("%"));tok=this.peek();if(!paren.space&&"ident"==tok.type&&~units.indexOf(tok.val.string)){expr.push(new nodes.Ident(tok.val.string));this.next()}return expr}tok=this.peek();switch(tok.type){case"null":case"unit":case"color":case"string":case"literal":case"boolean":case"comment":return this.next().val;case!this.cond&&"{":return this.object();case"atblock":return this.atblock();case"atrule":var id=new nodes.Ident(this.next().val);id.property=true;return id;case"ident":return this.ident();case"function":return tok.anonymous?this.functionDefinition():this.functionCall()}}}});require.register("renderer.js",function(module,exports,require){var Parser=require("./parser"),Evaluator=require("./visitor/evaluator"),Normalizer=require("./visitor/normalizer"),utils=require("./utils"),nodes=require("./nodes"),join=require("./path").join;module.exports=Renderer;function Renderer(str,options){options=options||{};options.globals=options.globals||{};options.functions=options.functions||{};options.use=options.use||[];options.use=Array.isArray(options.use)?options.use:[options.use];options.imports=[];options.paths=options.paths||[];options.filename=options.filename||"stylus";options.Evaluator=options.Evaluator||Evaluator;this.options=options;this.str=str}Renderer.prototype.render=function(fn){var parser=this.parser=new Parser(this.str,this.options);for(var i=0,len=this.options.use.length;i","+","~"];var SelectorParser=module.exports=function SelectorParser(str,stack,parts){this.str=str;this.stack=stack||[];this.parts=parts||[];this.pos=0;this.level=2;this.nested=true;this.ignore=false};SelectorParser.prototype.skip=function(len){this.str=this.str.substr(len);this.pos+=len};SelectorParser.prototype.skipSpaces=function(){while(" "==this.str[0])this.skip(1)};SelectorParser.prototype.advance=function(){return this.root()||this.relative()||this.escaped()||this.parent()||this.partial()||this.char()};SelectorParser.prototype.root=function(){if(!this.pos&&"/"==this.str[0]&&"deep"!=this.str.slice(1,5)){this.nested=false;this.skip(1)}};SelectorParser.prototype.relative=function(multi){if((!this.pos||multi)&&"../"==this.str.slice(0,3)){this.nested=false;this.skip(3);while(this.relative(true))this.level++;if(!this.raw){var ret=this.stack[this.stack.length-this.level];if(ret){return ret}else{this.ignore=true}}}};SelectorParser.prototype.escaped=function(){if("\\"==this.str[0]){var char=this.str[1];if("&"==char||"^"==char){this.skip(2);return char}}};SelectorParser.prototype.parent=function(){if("&"==this.str[0]){if(!this.pos&&(!this.stack.length||this.raw)){var i=0;while(" "==this.str[++i]);if(~COMBINATORS.indexOf(this.str[i])){this.skip(i+1);return}}this.nested=false;this.skip(1);if(!this.raw)return this.stack[this.stack.length-1]}};SelectorParser.prototype.partial=function(){if("^"==this.str[0]&&"["==this.str[1]){this.skip(2);this.skipSpaces();var ret=this.range();this.skipSpaces();if("]"!=this.str[0])return"^[";this.nested=false;this.skip(1);if(ret){return ret}else{this.ignore=true}}};SelectorParser.prototype.number=function(){var i=0,ret="";if("-"==this.str[i])ret+=this.str[i++];while(this.str.charCodeAt(i)>=48&&this.str.charCodeAt(i)<=57)ret+=this.str[i++];if(ret){this.skip(i);return Number(ret)}};SelectorParser.prototype.range=function(){var start=this.number(),ret;if(".."==this.str.slice(0,2)){this.skip(2);var end=this.number(),len=this.parts.length;if(start<0)start=len+start-1;if(end<0)end=len+end-1;if(start>end){var tmp=start;start=end;end=tmp}if(end-1){return n.toString().replace("0.",".")+type}}return(float?parseFloat(n.toFixed(15)):n).toString()+type};Compiler.prototype.visitGroup=function(group){var stack=this.keyframe?[]:this.stack,comma=this.compress?",":",\n";stack.push(group.nodes);if(group.block.hasProperties){var selectors=utils.compileSelectors.call(this,stack),len=selectors.length;if(len){if(this.keyframe)comma=this.compress?",":", ";for(var i=0;i200){throw new RangeError("Maximum stylus call stack size exceeded")}if("expression"==fn.nodeName)fn=fn.first;this.ret++;var args=this.visit(call.args);for(var key in args.map){args.map[key]=this.visit(args.map[key].clone())}this.ret--;if(fn.fn){ret=this.invokeBuiltin(fn.fn,args)}else if("function"==fn.nodeName){if(call.block)call.block=this.visit(call.block);ret=this.invokeFunction(fn,args,call.block)}this.calling.pop();this.ignoreColors=false;return ret};Evaluator.prototype.visitIdent=function(ident){var prop;if(ident.property){if(prop=this.lookupProperty(ident.name)){return this.visit(prop.expr.clone())}return nodes.nil}else if(ident.val.isNull){var val=this.lookup(ident.name);if(val&&ident.mixin)this.mixinNode(val);return val?this.visit(val):ident}else{this.ret++;ident.val=this.visit(ident.val);this.ret--;this.currentScope.add(ident);return ident.val}};Evaluator.prototype.visitBinOp=function(binop){if("is defined"==binop.op)return this.isDefined(binop.left);this.ret++;var op=binop.op,left=this.visit(binop.left),right="||"==op||"&&"==op?binop.right:this.visit(binop.right);var val=binop.val?this.visit(binop.val):null;this.ret--;try{return this.visit(left.operate(op,right,val))}catch(err){if("CoercionError"==err.name){switch(op){case"==":return nodes.no;case"!=":return nodes.yes}}throw err}};Evaluator.prototype.visitUnaryOp=function(unary){var op=unary.op,node=this.visit(unary.expr);if("!"!=op){node=node.first.clone();utils.assertType(node,"unit")}switch(op){case"-":node.val=-node.val;break;case"+":node.val=+node.val;break;case"~":node.val=~node.val;break;case"!":return node.toBoolean().negate()}return node};Evaluator.prototype.visitTernary=function(ternary){var ok=this.visit(ternary.cond).toBoolean();return ok.isTrue?this.visit(ternary.trueExpr):this.visit(ternary.falseExpr)};Evaluator.prototype.visitExpression=function(expr){for(var i=0,len=expr.nodes.length;i1){for(var i=0;i0&&!~part.indexOf("&")){part="/"+part}s=new nodes.Selector([new nodes.Literal(part)]);s.val=part;s.block=group.block;group.nodes[i++]=s}});stack.push(group.nodes);var selectors=utils.compileSelectors(stack,true);selectors.forEach(function(selector){map[selector]=map[selector]||[];map[selector].push(group)});this.extend(group,selectors);stack.pop();return group};Normalizer.prototype.visitFunction=function(){return nodes.nil};Normalizer.prototype.visitMedia=function(media){var medias=[],group=this.closestGroup(media.block),parent;function mergeQueries(block){block.nodes.forEach(function(node,i){switch(node.nodeName){case"media":node.val=media.val.merge(node.val);medias.push(node);block.nodes[i]=nodes.nil;break;case"block":mergeQueries(node);break;default:if(node.block&&node.block.nodes)mergeQueries(node.block)}})}mergeQueries(media.block);this.bubble(media);if(medias.length){medias.forEach(function(node){if(group){group.block.push(node)}else{this.root.nodes.splice(++this.rootIndex,0,node)}node=this.visit(node);parent=node.block.parent;if(node.bubbled&&(!group||"group"==parent.node.nodeName)){node.group.block=node.block.nodes[0].block;node.block.nodes[0]=node.group}},this)}return media};Normalizer.prototype.visitSupports=function(node){this.bubble(node);return node};Normalizer.prototype.visitAtrule=function(node){if(node.block)node.block=this.visit(node.block);return node};Normalizer.prototype.visitKeyframes=function(node){var frames=node.block.nodes.filter(function(frame){return frame.block&&frame.block.hasProperties});node.frames=frames.length;return node};Normalizer.prototype.visitImport=function(node){this.imports.push(node);return this.hoist?nodes.nil:node};Normalizer.prototype.visitCharset=function(node){this.charset=node;return this.hoist?nodes.nil:node};Normalizer.prototype.extend=function(group,selectors){var map=this.map,self=this,parent=this.closestGroup(group.block);group.extends.forEach(function(extend){var groups=map[extend.selector];if(!groups){if(extend.optional)return;var err=new Error('Failed to @extend "'+extend.selector+'"');err.lineno=extend.lineno;err.column=extend.column;throw err}selectors.forEach(function(selector){var node=new nodes.Selector;node.val=selector;node.inherits=false;groups.forEach(function(group){if(!parent||parent!=group)self.extend(group,selectors);group.push(node)})})});group.block=this.visit(group.block)}});return require("stylus")}(); diff --git a/lib/system-client.coffee b/lib/system-client.coffee new file mode 100644 index 0000000..338bcd9 --- /dev/null +++ b/lib/system-client.coffee @@ -0,0 +1,40 @@ +# system-client is what prepares the environment for user apps +# we hook up the postmaster and proxy messages to the OS +# we also provide system packages for the application to use like UI + +do -> + # NOTE: These required packages get populated from the parent package when building + # the runnable app. See util.coffee + Postmaster = require "_SYS_postmaster" + UI = require "_SYS_ui" + + style = document.createElement "style" + style.innerHTML = UI.Style.all + document.head.appendChild style + + postmaster = Postmaster() + + applicationProxy = new Proxy {}, + get: (target, property, receiver) -> + -> + postmaster.invokeRemote "application", property, arguments... + + document.addEventListener "mousedown", -> + applicationProxy.raiseToTop() + + systemProxy = new Proxy + Observable: UI.Observable + UI: UI + , + get: (target, property, receiver) -> + target[property] or + -> + postmaster.invokeRemote "system", property, arguments... + + # TODO: Also interesting would be to proxy observable arguments where we + # create the receiver on the opposite end of the membrane and pass messages + # back and forth like magic + + window.system = systemProxy + window.application = applicationProxy + window.postmaster = postmaster diff --git a/main.coffee b/main.coffee index ce35592..f10f949 100644 --- a/main.coffee +++ b/main.coffee @@ -9,15 +9,37 @@ global.Hamlet = require "./lib/hamlet" System = require "./system" global.system = System() +system.PACKAGE = PACKAGE # For debugging {Style} = system.UI style = document.createElement "style" style.innerHTML = Style.all + "\n" + require("./style") document.head.appendChild style +# Drag shenanigans +document.addEventListener "dragstart", -> + document.body.classList.add "drag-active" +document.addEventListener "mouseup", -> + document.body.classList.remove "drag-active" + # Desktop Explorer = require "./apps/explorer" document.body.appendChild Explorer() -# Launch Current Issue +VersionTemplate = require "./templates/version" +document.body.appendChild VersionTemplate + version: system.version + +SiteURLTemplate = require "./templates/site-url" +document.body.appendChild SiteURLTemplate() + +HomeButton = require "./presenters/home-button" +document.body.appendChild HomeButton() + +system.writeFile "feedback.exe", new Blob [""], type: "application/exe" +system.writeFile "My Briefcase", new Blob [""], type: "application/briefcase" + +system.autoboot() +# system.dumpModules() + require("./issues/2016-12")() diff --git a/os/file-io.coffee b/os/file-io.coffee index 3f32d70..9bf2453 100644 --- a/os/file-io.coffee +++ b/os/file-io.coffee @@ -7,53 +7,67 @@ # `newFile` Initialize the application to an empty state. module.exports = (I, self) -> + {Observable} = system {Modal} = system.UI - currentPath = "" - # TODO: Update saved to be false when model changes - saved = true + currentPath = Observable "" + saved = Observable true + + confirmUnsaved = -> + return Promise.resolve() if saved() + + new Promise (resolve, reject) -> + Modal.confirm "You will lose unsaved progress, continue?" + .then (result) -> + if result + resolve() + else + reject() self.extend + currentPath: currentPath + saved: saved new: -> - if saved - currentPath = "" + if saved() + currentPath "" self.newFile() else - Modal.confirm "You will lose unsaved progress, continue?" - .then (result) -> - if result - saved = true - self.newFile() + confirmUnsaved() + .then -> + saved true + self.newFile() open: -> - # TODO: Prompt if unsaved - # TODO: File browser - Modal.prompt "File Path", currentPath - .then (newPath) -> - if newPath - currentPath = newPath - else - throw new Error "No path given" - .then (path) -> - system.readFile path, true - .then (file) -> - self.loadFile file + confirmUnsaved() + .then -> + # TODO: File browser + Modal.prompt "File Path", currentPath() + .then (newPath) -> + if newPath + currentPath newPath + else + throw new Error "No path given" + .then (path) -> + system.readFile path, true + .then (file) -> + self.loadFile file save: -> - if currentPath + if currentPath() self.saveData() .then (blob) -> - system.writeFile currentPath, blob, true + system.writeFile currentPath(), blob, true .then -> - currentPath + saved true + currentPath() else self.saveAs() saveAs: -> - Modal.prompt "File Path", currentPath + Modal.prompt "File Path", currentPath() .then (path) -> if path - currentPath = path + currentPath path self.save() return self diff --git a/pixie.cson b/pixie.cson index 3d9e471..282d8c6 100644 --- a/pixie.cson +++ b/pixie.cson @@ -1,14 +1,20 @@ +title: "Whimsy Space - ZineOS v0.420.950a ⌊ALPHA⌉" +description: """ + When the bombs drop, and it's OS against OS, you're gonna wish you could view source +""" dependencies: ajax: "distri/ajax:master" analytics: "distri/google-analytics:master" + base64: "distri/base64:v0.9.2" bindable: "distri/bindable:master" model: "distri/model:master" - postmaster: "distri/postmaster:v0.5.0" - ui: "STRd6/ui:master" + postmaster: "distri/postmaster:v0.5.3" + ui: "STRd6/ui:v0.1.9" remoteDependencies: [ "https://cdnjs.cloudflare.com/ajax/libs/dexie/2.0.0-beta.7/dexie.min.js" "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" "https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js" "https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js" + "https://sdk.amazonaws.com/js/aws-sdk-2.7.20.min.js" ] diff --git a/presenters/home-button.coffee b/presenters/home-button.coffee new file mode 100644 index 0000000..3b59a37 --- /dev/null +++ b/presenters/home-button.coffee @@ -0,0 +1,112 @@ +AceEditor = require "../apps/text-editor" +AchievementStatus = require "../apps/achievement-status" +Chateau = require "../apps/chateau" +Contrasaurus = require "../apps/contrasaurus" +DungeonOfSadness = require "../apps/dungeon-of-sadness" +PixiePaint = require "../apps/pixel" +Spreadsheet = require "../apps/spreadsheet" +MyBriefcase = require "../apps/my-briefcase" + +HomeButtonTemplate = require "../templates/home-button" + +module.exports = -> + {Achievement} = system + {ContextMenu, Util:{parseMenu}} = system.UI + + launch = (App) -> + app = App() + system.attachApplication app + + contextMenu = ContextMenu + items: parseMenu """ + 🔨 [A]pplications + 📝 [A]ce Editor + 🍷 [C]hateau + 🎨 [P]ixie Paint + 🎮 [G]ames + 🌭 [B]ionic Hotdog + 🍖 [C]ontrasaurus + 😭 [D]ungeon Of Sadness + 📰 [I]ssues + 1️⃣ [F]irst + 🏰 [E]nter The Dungeon + 🏬 [A]TTN: K-Mart Shoppers + 💃 [D]isco Tech + 🌻 [A] May Zine + ⚙️ [S]ettings + 📱 [A]ppearance + 💯 [C]heevos + 💼 [M]y Briefcase + - + 🔌 S[h]ut Down + """ + handlers: + aceEditor: -> + launch AceEditor + + aMayZine: -> + system.launchIssue("2017-05") + + appearance: -> + system.UI.Modal.alert "TODO :)" + + aTTNKMartShoppers: -> + system.launchIssue("2017-03") + + bionicHotdog: -> + Promise.resolve + src: "https://danielx.net/grappl3r/" + width: 960 + height: 540 + iconEmoji: "🌭" + title: "Bionic Hotdog" + .then system.iframeApp + .then system.attachApplication + + chateau: -> + launch Chateau + + cheevos: -> + launch AchievementStatus + + contrasaurus: -> + launch Contrasaurus + + discoTech: -> + system.launchIssue("2017-04") + + dungeonOfSadness: -> + launch DungeonOfSadness + + enterTheDungeon: -> + system.launchIssue("2017-02") + + "1First": -> + system.launchIssue("2016-12") + + myBriefcase: -> + launch MyBriefcase + + pixiePaint: -> + launch PixiePaint + + shutDown: -> + Achievement.unlock "Shut Down" + system.UI.Modal.alert "You'll never shut us down! ZineOS 5ever!" + + updateStyle = -> + height = element.getBoundingClientRect().height + + contextMenu.element.style.fontSize = "1.5rem" + contextMenu.element.style.lineHeight = "1.5" + contextMenu.element.style.bottom = "#{height}px" + contextMenu.element.style.textAlign = "left" + + element = HomeButtonTemplate + click: -> + contextMenu.display + inElement: document.body + + updateStyle() + + return element diff --git a/social/social.coffee b/social/social.coffee index 1fa8ae8..2ea5c9c 100644 --- a/social/social.coffee +++ b/social/social.coffee @@ -14,7 +14,7 @@ module.exports = (I, self) -> area: self.area() .then (data) -> ajax - url: "https://whimsy-space.gomix.me/comments" + url: "https://whimsy-space.glitch.me/comments" data: JSON.stringify(data) headers: "Content-Type": "application/json" @@ -23,7 +23,7 @@ module.exports = (I, self) -> self.viewComments() viewComments: -> - ajax.getJSON "https://whimsy-space.gomix.me/comments/#{self.area()}" + ajax.getJSON "https://whimsy-space.glitch.me/comments/#{self.area()}" .then (data) -> data = data.reverse() @@ -37,7 +37,7 @@ module.exports = (I, self) -> like: -> system.Achievement.unlock "Do you 'like' like me?" - Modal.alert "I like you too, but we don't have a facebook or anything yet :)" + window.open "https://www.facebook.com/whimsyspace/" subscribe: -> require("../mailchimp").show() diff --git a/stories/blue-light-special.coffee b/stories/blue-light-special.coffee new file mode 100644 index 0000000..dc7a15a --- /dev/null +++ b/stories/blue-light-special.coffee @@ -0,0 +1,96 @@ +module.exports = """ +Blue Light Special + +“You know, I really want some cookies.” Elo said. She sat on a cushion on top of a tarp spread on the ground in front of their small campfire and whittled a piece of wood, periodically tossing the shavings into the fire. The warm glow bathed her deft fingers, tired face, and honey-colored hair with a soft light, and faintly illuminated the oak trees around them in the pressing darkness. + +“We can break camp tomorrow, move on, and see what we can find on the way,” Jib replied, as he munched some popcorn and scratched his dark beard. He scooted his log seat closer to the fire and buttoned his shearling jacket to keep out the fine mist rolling in among the trees. + +“I don’t want to break camp,” Elo protested. “I like it here. We should stay a while. I just want to have some cookies.” She looked around their camp: at Jib on his stump; at Poli beside her on the tarp, seated on her own cushion; at their reliable and well-used tent; at the horses draped with their blankets; at the clean dishes drying fireside. This was a good place. She rubbed her ever-growing abdomen. The tiny person ensconced within dragged a heel over her insides in response. + +“Okay, we won’t break camp, then. We can just go to town tomorrow. We need some other supplies anyway.” Jib raised his lanky body to pass the popcorn to Poli, who took the enameled bowl and placed it between her and Elo. + +Poli offered the popcorn to Elo. “Here, have some popcorn. That will help take your mind off the craving.” + +Elo knitted her brows. “No, it won’t. It’s not the same and you know it.” + +“Yeah, okay. I know. It’s not. But it’s what we have,” Poli smiled gently, hoping to diffuse the situation. + +“Why can’t you just go get some now? We didn’t camp that far from town, and it’s not that late,” Elo pleaded. + +“Because it’s going to rain soon, and because I’m tired, and because we decide things together, not based on whim,” Jib retorted grumpily. + +“Pleeeeeease! I really, really want them. I’m tired too and my back hurts from hauling this baby around and I just want to have some cookies. Is that too much to ask?” Elo burst into tears. + +“Elo, please don’t cry.” Poli’s red curls bounced as she snapped her head up to glare at Jib. She enfolded Elo in a hug, and Elo sobbed harder on the petite woman’s shoulder. + +“Poli, come on,” Jib sighed. “Get Elo settled in the tent and you and I will go tonight. Let’s get moving so we’re not caught in too much rain on the way back.” + +** + +Gabe sat on a cold concrete bench at a cold, round, concrete table, eating cold pizza under a murky and unforgiving sky in which clouds entirely obscured the stars, and in which mist threatened to give way to outright rain. He reflected miserably that of all the ways to eat pizza, this was probably the second-worst. The worst would be back in the break room with the other Kmart employees, where they would have excited yet hushed conversations that did not include him, the night manager. + +The table he was sitting at had recently had a fiberglass umbrella to provide meager protection against the elements. Last week, however, seven drunk teenagers had removed it to use as a boat in a very large puddle at the back of the parking lot and it had not been seen since. The sky began to spit at him, and he thought, “At least the sky is doing what they’re all thinking.” + +Gabe looked up the nearly deserted road and noticed two large figures with an odd gait. They resolved into a tall, bearded man and a petite, redheaded woman, each on horseback, emerging from the mist and entering the Kmart parking lot. Amused, Gabe wondered whether the pair might tie their horses up to the bike racks, or find a parking space near a lamp post and hitch them there, or tether them to a tree. + +The man and woman did none of these things. They simply rode the horses through the automatic doors and into the Kmart. Gabe took a moment to process this. He started to get up, looked at his phone, noticed ten minutes remained of his break, and sat back down. + +** + +Poli dismounted inside the doors and hitched up a shopping cart to her horse. She nimbly saddled up again, and she and Jib then began perusing the aisles of the Kmart and checking the items off their list. + +“Sir? Miss? Can I help you?” an employee demanded tersely, body language suggesting the only help would be toward an exit. + +“Yes, thank you. We are looking for cookies such as would satisfy a pregnancy craving,” Jib replied. He placed some batteries in their cart. + +“Cookies? Aisle 12A,” the employee responded automatically, then shook her head. “The horses. You can’t have them in here. You’ll have to take them outside.” The employee was joined by two others, all of them looking simultaneously stern and bewildered. + +They reached the aisle’s endcap. “Ooh, look, nut mixes are on sale,” Poli grinned. + +“Go ahead and get a few,” Jib called to her as he rode ahead to select some prenatal vitamins and toothpaste. + +“Miss, please. The two of you need to take the horses outside,” the employee wheedled. As if to respond, Poli’s horse lifted its tail and dropped a steaming load near the employee’s boot. The employee leapt back, dismayed. + +“Oh, no, I’m afraid that won’t be possible. These are our service animals,” Poli chirped sweetly. + +The employees huddled together. “Where’s Gabe?” they worried. They barked into walkie-talkies to Gabe to come over here RIGHT NOW and received no response. + +Poli caught up with Jib and together they loaded dry beans and rice into the cart. Suddenly, a voice spoke, as if from the heavens, and a flashing blue light punctuated the announcement: + +“Attention, Kmart shoppers! Blue light special on Aisle 12A. Tedwich Sandwich Cookies, buy two, get one free!” + +“We have to get those for Elo. I bet they’ll be perfect!” Poli shouted to Jib, and she urged her horse forward. Jib rode after her, followed by the three ineffectual employees, and an ever-growing crowd of onlookers. + +Aisle 12A displayed an alarming variety of cookies, snack cakes, and other treats, and Poli and Jib were, for the first time, ill at ease in the Kmart. A small girl with braids and cutoff jeans approached them. + +“Here you go,” she smiled shyly, handing up a box. + +“Thank you,” Jib smiled back. He tore into the box and passed a handful of the Tedwich cookies to Poli. They sampled the miniature bear-shaped cinnamon cookies sandwiching a vanilla cream filling. They were, indeed, exactly what Elo would love. + +“Can you help us load our cart with every box there is?” Poli asked. + +“Not every box. I want to have three for myself,” the girl quickly replied. + +“Done.” + +With all but three boxes of Tedwich cookies in their cart, the only thing left was to find some ice cream to go with them. This expedition caused no small amount of chaos in the freezer case aisle when a small, yappy dog in a handbag spooked the horses, which in turn sent the crowd scattering in all directions. Fortunately, it also resulted in Jib’s horse being perfectly positioned in front of a case containing pint after pint of ice cream. They selected one of chocolate and one of vanilla, and Jib and Poli rode to the checkout. + +Poli pulled a small wooden box from her canvas messenger bag and handed it down to the cashier. “This is a very rare and ancient turtle, carved from an unknown precious stone to bring the bearer untold riches. It is more than enough payment for these goods in our cart.” + +Poli and Jib rode out of the store, cart still in tow, as Gabe came back in. The two riders disappeared into the night. + +The employees surrounded him. Gabe said nothing. He strolled through the store, surveying the relative lack of damage, the confused shoppers, and the piles of manure, and he grinned as he reached for his walkie-talkie. + +“Cleanup on Aisles 6, 7, 10, and 12. Better get a shovel.” + +** + +Elo drifted to sleep that night, completely content, filled with cookies and ice cream. She dreamed of a girl with braids and cutoff jeans, holding boxes and boxes of Tedwich cookies stacked higher than she was tall. She flew above the girl as a deep blue light bathed them both. + +“What is your name?” Elo asked. + +“Theodosia,” the girl answered. The girl flew up to meet Elo and they soared effortlessly through the ceiling and out into the mist. + +Three months later, in the middle of another rainy night, Elo gave birth to a baby girl. The child was dusted with cinnamon freckles and looked up at Elo with the bluest eyes she had ever seen. She named the girl Theodosia. + +""" \ No newline at end of file diff --git a/stories/crescent.coffee b/stories/crescent.coffee new file mode 100644 index 0000000..799d3ef --- /dev/null +++ b/stories/crescent.coffee @@ -0,0 +1,13 @@ +module.exports = """ +“Look, Mom, I made a moon!” +She holds aloft a crescent bitten from cereal, +and I feel a tug on my memory: +not the usual familiarity +of the Earth’s twirling, ever-changing little sister +but something deeper, and more intimate, +like a great truth, +a resonance of great importance, +had been uncovered within my mind as I slept, +and was lost upon waking. +And then she ate the moon. +""" diff --git a/stories/dungeon-dog.coffee b/stories/dungeon-dog.coffee index 6d7ec67..7607400 100644 --- a/stories/dungeon-dog.coffee +++ b/stories/dungeon-dog.coffee @@ -3,7 +3,7 @@ Dungeon Crawl with Cliffford! One sunny day, Cliffford the big rad dog was playing at the park with his friends U-Bone and Cloe. They chased each other through the playground, around the play structures, and through the sand. Children squealed with delight to see Cliffford jump right over the swing set in his excitement. “He’s so big! He can jump over things! Heehee!” they giggled. THUMP! went Cliffford’s paws as he landed on the grassy field, and Cloe and U-Bone followed right behind. -The dogs dashed through a soccer game, U-Bone stopping to bite at the ball, and Cleo weaving her way through at least four players’ legs. They emerged on the other side of the field at the northernmost edge of the park, where the well-kept field gave way to a little meadow, and then the woods. +The dogs dashed through a soccer game, U-Bone stopping to bite at the ball, and Cleo weaving her way through at least four players’ legs. They emerged on the other side of the field at the northernmost edge of the park, where the well-kept field gave way to a little meadow, and then the woods. Cliffford flopped down in a shady spot, and Cloe and U-Bone stood panting next to him. Cool breezes blew in from the ocean, rippling the leaves on the trees and the long meadow-grass. @@ -53,7 +53,7 @@ Something else glinted in the darkness just beyond them. Cloe moved closer and t U-Bone stumbled upon a sheath and baldric for the sword a short distance away, and strapped them on. BOSS! A few turns of the path later, and the dogs found another staircase down. Come on, it’s a dungeon crawl. Of course they found another staircase! -The dogs explored further and deeper into the dungeon. Cliffford’s mouth was crammed as full as possible with bones, and he looked sadly at each one he had to leave behind as they pressed on. +The dogs explored further and deeper into the dungeon. Cliffford’s mouth was crammed as full as possible with bones, and he looked sadly at each one he had to leave behind as they pressed on. “Humuhlauhsunrrmmmuh,” said the big rad dog, furrowing his brows. “Hmm?” asked Cloe, through the side of her mouth around the torch. @@ -90,7 +90,7 @@ Cloe whipped her head back and forth, and the scroll unfurled, revealing the tex A moment later, Cliffford was back up again, only now he was the size of a normal dog. Yup! -This was a very useful development for our plot, because they heard shouts from the top of the stairs. The angry lake-keeper’s scratchy toenails clicked on the stone treads as he skittered down toward them. +This was a very useful development for our plot, because they heard shouts from the top of the stairs. The angry lake-keeper’s scratchy toenails clicked on the stone treads as he skittered down toward them. The dogs wasted no time, and all three dove right into the entrance to the labyrinth. They tried their best to stay together, but Clifford, unused to being tiny, lagged behind. He felt weak, and needed food badly. diff --git a/stories/izzy.coffee b/stories/izzy.coffee new file mode 100644 index 0000000..83265f1 --- /dev/null +++ b/stories/izzy.coffee @@ -0,0 +1,61 @@ +module.exports = """ +“Izzy, your grandma is here!” Izzy jumped up from where she was seated on the classroom floor chatting with her friends during after-school care, and she ran to her grandmother to give her a huge hug. + +“Have a good weekend,” her teacher said as Izzy grabbed her backpack. +“You too!” Izzy called, as she ran for the parking lot with Grandma close behind. She nearly flew through the school gate. + +“Wait for me before you cross the street!” Grandma shouted to Izzy. +“Sorry,” Izzy replied as Grandma caught up to her. + +They crossed over to where Grandpa was leaning against their 1973 El Camino. He swung Izzy up and spun her around. + +“Did you have a good day, kiddo?” +“Yeah! I finished The BFG and I got to draw my favorite part as a poster and share it with the class,” Izzy said proudly. +“Sounds great. Did you try to draw in the style of Quentin Blake, or did you use your own style?” Grandpa asked. +“Well, I tried pretty hard to draw like Quentin Blake, but it’s hard to draw like other artists. I didn’t like how it looked so I started over by just reading the description again and imagining it in my head. I’ll show you next week when I bring it home.” + +Izzy lowered her backpack into a wooden orange crate in the truck bed of the El Camino. Her navy-blue violin case, a small amplifier, and a black bag already sat in the crate. The crate nestled in between a variety of potted plants which took up the remaining space in the bed. Izzy plucked a few ripe cherry tomatoes and some snow peas from their respective vines, which twined around cages in a large planter close to the cab’s rear window. She ran her fingers through the thyme and mint spilling out of smaller planters closer to the tailgate and breathed in, relishing the pungent and spicy smells. She loved her grandparents’ mobile garden. + +She munched her snack as she climbed into the front seat of the El Camino and slid in next to Grandma, who sat in the driver’s seat. Grandpa placed three garment bags on Izzy’s lap, and then slid in beside her, sandwiching Izzy in between her grandparents. + +“Be careful not to get any tomato juice on the garment bags,” Grandpa cautioned. +“I know. I’ll be careful.” Izzy replied. +“All right. Sorry it’s such a tight fit today!” Grandpa smiled at Izzy. “Are you excited?” +“Yeah! I can’t wait until we get to the boardwalk! Everyone is going to love our show!” +“Well, then!” Grandma exclaimed. “Let’s go!” + +The El Camino’s engine roared to life and Grandma maneuvered the car onto the road. With both windows down, the breeze ruffled everyone’s hair, and as they neared their destination, the fresh scent of the ocean wafted in, filling Izzy’s nostrils with the salty, damp aroma. The plants in the back swayed gently back and forth in time with the rhythm of the car’s motion, and their planters were so tightly arranged that nothing slid around. + +Grandma pulled into a perfect parking space close to the boardwalk at the beachfront parking lot. Grandpa carried the garment bags; Izzy carried her backpack and violin, and Grandma carried the black bag and the amplifier. + +“Nice car!” a voice called to them. They turned to see a young man and woman wearing wetsuits and loading surfboards into the back of their SUV. +“Thanks!” Grandpa called back. The two surfers smiled at them. Izzy glowed. She loved the way her grandparents brought joy everywhere they went. + +The sun was beginning to set over the ocean, creating an air of magic. Izzy and her grandparents walked the short distance to their pre-chosen destination: a bench that overlooked the water, adjoining a large, wide sidewalk upon which Friday night foot traffic was beginning to accumulate. + +A nearby planter contained palm trees and succulents, as well as a small electrical outlet on one side. Grandma pulled an orange extension cord from her black bag, and plugged it in. She carefully duct-taped the cord to the ground for safety as she ran the cord up to the bench. Izzy plugged her amplifier in the extension cord and began setting up her equipment. + +“I’m going to go get changed,” said Grandpa to Grandma, taking his garment bag and walking in the direction of the restrooms. As Izzy began tuning her violin a few minutes later, Grandpa appeared wearing a leisure suit that had a complicated design made of conductive fabric circles and lines stitched together in that turned his costume into a drum pad, which connected wirelessly with the amplifier. + +The equipment setup complete, Grandpa began do a final check of his suit while Izzy and Grandma took their turn to change as well. A short time later, they emerged, resplendent in their own costumes. + +Grandma wore a long-sleeved, tight-fitting silver dress that had fiber optic filaments running vertically along her body and dangling beyond the bottom of the dress. She had not yet switched on the battery pack, and the dress appeared at first to be rather unassuming. However, the headdress she was wearing was quite the opposite. Nestled in Grandma’s big hairdo was a variety of feathers, delicately coiled wire shapes, and more fiber optics that skimmed the top of her head before shooting up like the crest of a rainforest bird. Dramatic, dark eyeliner, silver eye shadow, and silver platform shoes completed the look. + +The simple cut of Izzy’s dress accentuated the beautiful movement of the fabric. A fitted bodice gave way to a flared skirt. She twirled, and the purple and blue dip-dyed fabric swirled in response around her knees. Opting to keep her costume simple for ease of playing her violin, it had no electronics. The bodice was adorned with sequins she had painstakingly glued in an intricate pattern, which, like a disco ball, would reflect the lights from the small footlight set up next to her. In Izzy’s hair was a small feather and fiber optic fascinator, like Grandma’s but on a smaller scale. + +Izzy did a quick, final sound check. “Ready?” she asked. +“Let’s do it!” grinned Grandma and Grandpa. + +Months of effort coalesced into one moment as the sun finally dipped below the horizon, sending the boardwalk into twilight. Grandma and Grandpa switched on their battery packs, and Grandma’s dress lit up. Izzy switched on her footlight, nodded to Grandma and Grandpa, and began to play. + +Inspired by her hero Zoë Keating, Izzy began to loop snippets of her violin with her loop pedal. She built sound after sound up to a fast-paced, catchy, danceable beat, while Grandpa and Grandma began to dance. Grandma’s fiber optics began to pulse with the rhythm of Izzy’s music and Grandpa’s drum pad leisure suit. They whirled and spun, and even Izzy swayed and swirled with the music as she played. + +A crowd began to form, cheering on the pop-up discotheque. They clapped in time to the music, delighted every time Grandma’s skirt sent pinpricks of light racing around her knees, and murmured in excitement at the intricate movements Grandpa made to keep the drums thumping. As the song ended, the crowd exploded with whoops and shouts, and several people made donations into Izzy’s violin case. The family took a bow, and launched into another song, causing the crowd to cheer yet again. + +Like any moment that has been anticipated for so long, it seemed to last but the blink of an eye. When the last song ended, the crowd drifted away on the final, lingering notes from Izzy’s violin. As Grandma and Grandpa started to pack up, Izzy took a dime out of the violin case, her favorite coin because it was the smallest, and darted to the fountain down the boardwalk. She pitched the coin into the dark water that reflected the stars. + +“What did you wish for?” Grandpa asked as Izzy returned. +“Not telling,” Izzy replied. + +She slipped her small hand into Grandpa’s, and the other into Grandma’s, and they walked back to the El Camino. +""" \ No newline at end of file diff --git a/stories/marigold.coffee b/stories/marigold.coffee new file mode 100644 index 0000000..41699c4 --- /dev/null +++ b/stories/marigold.coffee @@ -0,0 +1,87 @@ +module.exports = """ +The shutters of a third-story window of the manor house flew open, and a small girl leaned out, hurling a rope to the ground below. She lugged a chair over to the window, and stood on it. She grasped the rope in both hands, and began slowly descending the pitted grey stone walls of her home. Evenly spaced knots helped her tiny hands keep hold of the thick, scratchy rope as she leaned her slender frame away for balance. + +In the garden below, the girl brushed rope fibers from her hands on her leggings and smoothed out her tunic. A gust of wind whipped her black hair about her face, and she wrapped her cloak closer about her body and pulled its hood over her head. + +The waxing crescent moon, partially obscured by clouds, gave little light, but the girl needed no lantern. She blinked her golden eyes rapidly a few times, letting her cat-like pupils widen enough to collect the scant light, and took in her surroundings in shades of grey. She marched off into the darkness of the nearby woods, hidden under the cover of darkness. + +The girl found her favorite deer trail through the forest, almost invisible to anyone else. It wound around the ancient oak trees before descending along a sandy bank to the edge of a softly flowing river. Downstream, the rapids became rough, but here, where rocks were few and the ground was mostly level, the water pooled and was perfect for swimming. + +Despite the chill of the evening, the girl removed her boots and garments and folded them neatly, placing them on a flat rock several feet from the edge of the river. She stood expectantly, letting the water lap at her toes. + +She looked up as a swirling cloud emerged from a hollow tree and collected above her head. She whistled a haunting little tune and the cloud parted, wrapping itself around her body as she stepped into the water. She waded waist-deep into the water as the nanoswarm enveloped her in scaly, flexible skin from her torso all the way down her legs, ending in a tail with long fins. Opening her mouth, she inhaled some of the nanoswarm to allow her to breathe underwater, before her body slipped entirely under the rippling surface. + +The girl splashed her tail and a peal of light, uninhibited laughter echoed off the rocks as she resurfaced. She swam off into the night. + +*** + +“Lady Marigold?” A young woman, shivering in the predawn air, gently shook the girl’s shoulder to rouse her. The girl, Marigold, blinked her eyes and looked to find her lady’s maid standing over her with a look of concern. Marigold lay on the sandy shore where she had fallen asleep hours before. + +“Hm? Oh, no. I’m so sorry, Raina! I didn’t mean to fall asleep!” she said, brushing sand from her face as she sat up. She was still wrapped in her nanoswarm, so she whistled to them again, and they lifted away from her body and back into their hiding place in the tree. Raina retrieved the neatly folded clothing and helped her lady dress quickly. + +“My lady, if you must escape the house at night, please return in a timely manner. Surely you can program your nanoswarm to remind you to come home?” +“I can, but it is awkward to swim to shore with legs if the nanoswarm leaves me when I am not yet ready. I much prefer the tail.” + +Raina huffed in exasperation and said nothing further. She tamed Marigold’s long hair in a quick series of braids, which she wound around the girl’s head and pinned into place. + +Marigold turned to leave, but Raina said, “Your eyes, Lady Marigold.” Marigold paused, closed her eyes, and opened them again. Gone were the cat’s eyes, replaced by dark brown eyes with the usual round pupils one would expect to see in a girl five years of age. + +Raina followed her lady up the bank and back into the forest. They returned to the manor house quickly and quietly. Raina, feeling that scaling the walls of the house was undignified, had already wound Marigold’s rope back up and closed the shutters. Instead, the two of them tiptoed through the kitchen. The cook, singing loudly to herself, kneaded the day’s bread and did not hear their entrance. + +Back in Marigold’s rooms, Raina started a fire. “I am not cold,” Marigold protested. “The nanoswarm helps regulate my body temperature.” +“Very well, but I have no such protection and I, too, have also been out in the cold this morning.” Raina continued building the fire until it roared in the fireplace. + +Marigold disappeared into her library, a fortress of shelves she had arranged in a corner of the room adjacent to the fireplace. Books of all shapes and sizes and subjects, borrowed from the big library downstairs, whispered to her their secrets of ancient wars and mythology, poetry and architecture. Soft cushions stacked on a rug served as her seat, and Marigold resumed reading A Brief History of Nanoculture. + +Breakfast arrived on a tray, which Raina placed next to Marigold on the floor. The girl held the book in one hand, and ate her toast and jam and drank her tea with the other, her eyes never leaving the pages. + +A knock at the door yielded a sealed note, which, though it was addressed to Raina, was delivered to Marigold. The girl looked over the note, scowled, and snapped the book shut, mentally noting the page number. + +“My uncle has summoned me,” Marigold said to Raina, who nodded in response. “I do wish that man would write to me directly.” She read the note aloud, rolling her eyes: “‘Send the Lady Marigold to me at half-past nine and ensure the child wears something befitting her station.’ He knows I can read. Why does he torment me thus?” + +“Your uncle is a good man,” Raina soothed, “He has many responsibilities, and has better things to occupy his time than chastising you for coming to an audience with him dressed for a day out in the woods.” +“Formal clothing is pointless. My appearance is arbitrary. I am five, and therefore invisible anyway. I prefer being able to move freely.” + +Marigold flounced to a small adjoining room where her clothing was stored, and sullenly pulled a long dress from a drawer. Light, soft grey fabric draped over her head as she put the garment on. Long, tight-fitting sleeves with inlaid lace covered her arms, and the hem of the dress, which brushed the tops of her toes, featured more of the same lace. Over the dress, she put on a long, intricately embroidered vest, and Raina helped her cinch her waist with a belt the width of her hand, encrusted with beads made of semiprecious stones. Marigold slipped her feet into soft, ghillie-style shoes made of fine leather that laced up her legs. + +They sat at Marigold’s dressing table and Raina brushed Marigold’s hair before smoothing it into a full, low roll at the back of her head, pinning smaller braids around it, and placing a gold hair comb into the top of the roll. Raina carefully adjusted the small curls around Marigold’s face, and turned the girl to face her. + +“You look lovely,” Raina smiled. +“I still prefer the tunic and leggings, but thank you.” + +Marigold exited her rooms, followed by Raina. They descended the staircase at the end of the hall, and proceeded to the library, where Marigold’s uncle usually received her. Raina knocked, and entered. + +“Lady Marigold Lavande, Duchess of Frisa.” Raina announced her lady as she slowly inclined her head, a sign of respect for the child’s uncle and guardian, Lord Therus Osmarinus, a duke in his own right of the neighboring county of Astera. + +Marigold swept past Raina, stopping before her uncle and inclining her head to him. He returned the gesture of respect and motioned to a chair near the table upon which his correspondence was spread before him. Raina left the room, and Marigold sat. + +“Good morning, Marigold.” Her uncle’s green eyes were kind, but she could see bags under them. He probably had not slept much the previous evening. He sat at the table opposite her. + +“Good morning, Uncle.” From her perch on the chair, she could just see the letters he had been reading. She strained her eyes to see the details. She noted the seal of the Ruling Council affixed to several of the letters, and the swooping signatures of several different people. She also noted a map of the country he had pulled up on a tablet, with a variety of location markers scattered near the coast. + +“What is the map for, Uncle?” Marigold asked. + +Her uncle ran his fingers through his short, brown hair, speckled through with grey. He sighed. “There is some unrest along the coast. We have been summoned to the Palace of the Ruling Council for a meeting.” +“We?” Marigold raised her eyebrows. “We are never summoned. You are summoned, and I remain here and you act on my behalf.” She began to feel agitated. She liked her home, and leaving it always made her feel uneasy. +“In general, yes, but the Ruling Council specifically requested your presence this time.” Her uncle gestured toward the fireplace where a man and a woman stood, watching them. + +He introduced them as Lord and Lady Sicale, and explained that he must accompany them now to the Palace, and that Marigold would follow along as soon as possible. Marigold gathered that the summons had been delivered in person, and must be quite delicate and urgent. + +“I leave with Lord and Lady Sicale within the hour. I have already sent instructions to Raina to begin packing your things. I shall see you soon.” + +He dismissed her with a nod of his head, seeming to want to say more, but refraining. Marigold went back to her rooms. + +*** + +That evening, Marigold lay awake in bed. The day’s flurry of activity, rather than tiring her out, had set her brain tingling. Raina and the other household staff had seen to the packing of her clothing, books to study, and other personal effects. She had been left to her own devices to pack a small bag full of whatever she wished to take personally, which consisted largely of the books she was reading for pleasure, a sketchbook, some colored inks, and a variety of colored and graphite pencils. She had nearly everything, but one thing remained she wanted to take. + +She rolled over and over, pondering, before finally sitting bolt upright, coming to a decision. Flickering her eyes to golden cat-eyes, she slipped out of bed and put on her tunic and leggings, boots and cloak. She took a tiny glass vial from her dressing table. It fit in the palm of her hand. The vial had a narrow mouth and neck, with a very small silicone stopper in it, and a bulbous base. Delicate filigree metalwork around the neck sported a loop through which she threaded a fine silver chain. She put the chain around her neck and hid it under her tunic. + +As Marigold had done the previous night, she flung open the window, and let herself down silently with the rope. She flew over the deer trail to her secluded riverside swimming spot, where she whistled a slightly different nanotune than the night before. The swarm came out of its hiding place in the hollow tree. Instead of swirling around her and transforming her lower body into a sleek, aquatic creature, the nanoswarm poured into the vial around her neck, which she held aloft. + +Marigold replaced the stopper in the vial and hurried back to her room. The implications of bringing the nanoswarm to the Palace could be disastrous. Aside from her own swarm, nanoswarms had not been seen in hundreds of years. There were rumors of wild swarms, but no official reports existed, much like the mythical creatures she enjoyed reading about in her books. + +Marigold’s nanoswarm had been with her for as long as she could remember. It was her secret, shared only with Raina. She could not leave her swarm for more than a few days without missing them terribly, and based on the amount Raina and the others had packed, this trip would be much longer than a few days. + +Comforted by the swarm’s nearness, but still terrified at the prospect of her visit to the Palace, Marigold fell into an uneasy sleep. +""" diff --git a/stories/provision.coffee b/stories/provision.coffee index d60fa19..968895d 100644 --- a/stories/provision.coffee +++ b/stories/provision.coffee @@ -142,4 +142,4 @@ inexplicably racing heartbeat will bang and shudder against his hand like a hurricane's wind against the shutters of a house where nobody has ever lived. -""" \ No newline at end of file +""" diff --git a/stories/residue.coffee b/stories/residue.coffee new file mode 100644 index 0000000..02a47c6 --- /dev/null +++ b/stories/residue.coffee @@ -0,0 +1,79 @@ +module.exports = """ +\u200b RESIDUE + ======= + +Jules, my friend, used to take her new +car to Viretta Park. After a workday +filled with tickets and meetings, she +sometimes drove the five miles to this +one-block wide, one-block tall park at +the northeast coast of the city where +people prospered. There, tourists via +Ubers and Lyfts came to see the Kurt +Cobain bench. But Jules would arrive at +the park and drive past. She would not +stop. She would not get out. You cannot +see the bench from the road. It was +usually dark after work. She told no one +about these trips until years later. + +Jules, our protagonist, (as far as she +could figure) had no conscious logic for +her actions. Without it the story +refused to hang together. It was a +sequence of events united by space and +time but lacking guts hearts or brains. +It was a propulsion without a +propellant. It defied narrative +structure and, without it, lacked +rhythm. Jules, being a capable +self-aware adult, knew she was acting +out of an impulse but it seemed to her +that the situation warranted impulse. +And, at that time and at the time this +was written, gas was cheap. I can safely +say also that she was not sad; she was +not one of those people who indulged in +the habit of moping about events beyond +their control. Some people are just +strong this way. + +In my presence Jules narrated the story +with the blank point of view of an +omniscient witness. There was a lull in +the conversation, this story was told, +and then we moved on. She wore a thin +jacket and a wry frown and we were high, +more high than usual. I, after giving it +some consideration, had a grocery list +of questions. Why Kurt Cobain; why +didn't you tell me earlier; and did you +do it to feel an impossible, cosmic +connection to something bigger than +yourself, a feeling for which I cannot +do justice but can tell you that, for +me, is close to the sensation of an +egg's cracking against the chest +cavity's outer wall. But from looking at +her I knew then, just as she had always +known, it was not as if there were +answers and she were ignorant of them. +There just were no answers. At the time +these events I am writing about took +place, she was stuck in a temporary city +at a temporary time of her life. In my +country, where you work for years and +years in search of permanent residence +status and where adults values +friendship but few pursue them, it seems +harder than other places to let out +these prickly stories. I imagine people +all around me have them, though – yarn +wound and knotted by fingers +absentmindedly rubbing the threads +together. + +After eighteen months, Jules packed up +all her furniture and moved away. She +drove all the way through America. +""" diff --git a/style.styl b/style.styl index 575f538..5140e65 100644 --- a/style.styl +++ b/style.styl @@ -1,4 +1,5 @@ body + color: rgba(0, 0, 0, 0.87) overflow: hidden h1 @@ -34,14 +35,41 @@ comment &::before content: "-" +menu + font-size: 1rem + +body.drag-active iframe + pointer-events: none + +body > button.home + background-color: rgba(255, 255, 255, 0.5) + bottom: 0 + color: rgba(0, 0, 0, 0.87) + font-size: 2rem + left: 0 + padding-right: 0.3em + position: absolute + transition-duration: 0.125s + transition-property: background-color, color + z-index: 1 + + &:hover + background-color: rgba(255, 255, 255, 0.75) + + &:active + color: rgba(255, 255, 255, 1.0) + explorer display: block + height: 100% padding: 0.5em user-select: none + width: 100% + z-index: 1 body > & - height: 100% - width: 100% + background-image: url(https://i.imgur.com/hKOGoex.jpg) + background-size: cover > file, > folder display: inline-block @@ -56,12 +84,20 @@ explorer > file > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3d3dwAAAAj///////////////cAAAAI///////////////3AAAACP///////3d///d39wAAAAj/zMzM//mZ//+Zn/cAAAAI////////l///+X/3AAAACP/MzMzM//l3d3l/9wAAAAj/////////mZmZf/cAAAAI/8zMzMzM//l/+X/3AAAACP//////////l/l/9wAAAAj/zMzMzMzM//l5f/cAAAAI////////////mX/3AAAACP/MzMzMzMzM//n/9wAAAAj///////////////cAAAAI/8zMzMzMzMzMzP/3AAAACP//////////////9wAAAAj/zMzMzMzMzMzM//cAAAAI///////////////3AAAACP8AAAAA/8zMzMz/9wAAAAj/iZD/8P////////cAAAAI/4AAAAD/zMzMzP/3AAAACP+P8Luw////////9wAAAAj/gAC7sP/MzMzM//cAAAAI/4/wu7D////////3AAAACP+P8Luw/////4AAAAAAAAj/j/AAAP////+P94AAAAAI/4/wzMD/////j3gAAAAACP+IiIiA/////4eAAAAAAAj///////////+IAAAAAAAI////////////gAAAAAAACIiIiIiIiIiIiIAAAAAA4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAP4AAAH+AAAD/gAAB/4AAA/+AAAf8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj8z5//n3AACP//+ZmfcAAI/MzPn59wAAj////5n3AACPzMzM+fcAAI//////9wAAjwAPzMz3AACPmY////cAAI/Pj8zM9wAAj8+P//AAAACPiI//9/gAAI/////3gAAAiIiIiIgAAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIAHAACADwAAgB8AAA==") + > file[type="application/briefcase"] > icon + background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIAAj3d3d3d3d3d3d3d3d3eIAI93d39/f39/f39/f393iICPd3d4eHh4eHh4eHh4d4iAj3d3d3d3d3d3d3d3d3eIgI93d3d//////////3d3iICPd3d/8AAAAAAAAAD/d4iAj3mXeIAAAAAAAAAAiHeIgI95l3iAAAAAAAAAAIh3iICPd3d3eIiIiIiIiIh3d4iAj3d3d3d3d3d3d3d3d3eIgI//////////////////iIAId3d3d3d3d3d3d3d3d3iAAId3d3d3d3d3d3d3d3d3gAAIiIiIiIiIiIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAABwAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADAAAAA4AAAAf////////////////////////////////////////////////////////////////////8oAAAAEAAAACAAAAABAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI+IiIiIiIgAj3d3d3d3eICPd3////d4gI938AAAD3iAj5eAAAAIeICPd3d3d3d4gI////////iACHd3d3d3d4AAiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AACAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADAAQAA//8AAP//AAD//wAA//8AAA==") + > file[type^="text/"] > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3dwcAAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI/wAAAAAAAAAAAP9wcAAACP//////////////cHAAAAj/AAAAAAAAAAAA/3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD////w8AD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAjw/w/w/w/w/w/w/4BwAAAI8P8P8P8P8P8P8P+AcAAAAI+I+I+I+I+I+I+I+AAAAAAAAAAAAAAAAAAAAAAAAA8AAAH+AAAA/AAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAH4AAAD/JJJL8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAh3d3d3d3AACP//////cAAI//////9wAAj/AAAA/3AACP//////cAAI/wAAAP9wAAj//////3AACP8AAAD/cAAI//////9wAAj/AA8A/3AACP//////cAAI//////9wAAjw8PDw8PAAAA+Pj4+PAAAAAAAAAAAAAAgA8AAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAACADwAA1V8AAA==") > file[type="application/javascript"] > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAACHd3d3d3d3d3d3dwcAAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA////AAD//3BwAAAI////////AwsDAP9wcAAACP8AAAD//zu7uzD/cHAAAAj///////M7MDsAD3BwAAAI///////zuwgLsw9wcAAACP8AAAAAAzswOwAPcHAAAAj///////87u7sw/3BwAAAI/wAAAAAAMwsDAP9wcAAACP////////8zM///cHAAAAj/AAAAAAAAAAAA/3BwAAAI//////////////9wcAAACP8AAAAAAAAAAAD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP8AAAD/////////cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAj/AAAA/////////3BwAAAI//////////////9wcAAACP8AAAD////w8AD/cHAAAAj//////////////3BwAAAI//////////////9wcAAACP//////////////cHAAAAjw/w/w/w/w/w/w/4BwAAAI8P8P8P8P8P8P8P+AcAAAAI+I+I+I+I+I+I+I+AAAAAAAAAAAAAAAAAAAAAAAAA8AAAH+AAAA/AAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAH4AAAD/JJJL8oAAAAEAAAACAAAAABAAQAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvwAAvwAAAL+/AL8AAAC/AL8Av78AAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAACHd3d3d3cAAI//////9wAAj///Aw/3AACP8Ac7MPcAAI//87Cz9wAAj/AHOzD3AACP//9zf/cAAI/wAAAP9wAAj//////3AACP8ADwD/cAAI//////9wAAj//////3AACPDw8PDw8AAAD4+Pj48AAAAAAAAAAAAAwAcAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAACAAwAAgAMAAIADAADABwAA6q8AAA==") + > file[path$="💾"] > icon + background-image: url("http://www.myiconfinder.com/uploads/iconsets/256-256-517c80793c96fa23c342ae1a5560ddf6.png") + background-size: cover + image-rendering: pixelated + > folder > icon background-image: url("data:image/x-icon;base64,AAABAAIAICAQAAEABADoAgAAJgAAABAQEAABAAQAKAEAAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDMzMzMzMzMzMzMzMzMzMwj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI97e3t7e3t7e3t7e3t7ezCPt7e3t7e3t7e3t7e3t7cwj3t7e3t7e3t7e3t7e3t7MI+3t7e3t7e3t7e3t7e3tzCPe3t7e3t7e3t7e3t7e3swj7e3t7e3t7e3t7e3t7e3MI///////////////////wCHd3d3d3d3d4iIiIiIiIgACPt7e3t7e3gAAAAAAAAAAACPt7e3t7eAAAAAAAAAAAAACP/////4AAAAAAAAAAAAAACIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////gAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAP//wAH//+AD///wB/////////////8oAAAAEAAAACAAAAABAAQAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIgAj7e3t7e3uACPe3t7e3t4AI+3t7e3t7gAj3t7e3t7eACPt7e3t7e4AI97e3t7e3gAj7e3t7e3uACP///////4AIe3t7eIiIgACHt7eAAAAAAAiIiAAAAAAAAAAAAAAAAA//8AAP//AACAAQCgAAHOAAABzgAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAMAAID/AADB/wAA//8AAA==") @@ -101,6 +137,8 @@ chateau window > viewport > & margin: initial +window > viewport > audio + width: 100% @keyframes display-achievement 0% @@ -207,3 +245,20 @@ achievement-badge &.achieved border: 1px solid blue color: blue + +version, site-url + color: white + display: block + font-size: 20px + font-weight: bold + pointer-events: none + position: absolute + right: 1rem + text-shadow: 2px 2px 4px black + z-index: 1 + +version + bottom: 1rem + +site-url + bottom: 3rem diff --git a/system.coffee b/system.coffee index b46a4fd..3580f4d 100644 --- a/system.coffee +++ b/system.coffee @@ -9,55 +9,13 @@ DexieFSDB = (dbName='fs') -> return db -# FS Wrapper to DB -DexieFS = (db) -> - Files = db.files - - notify = (eventType, path) -> - (result) -> - self.trigger eventType, path - return result - - self = Model() - .include(Bindable) - .extend - read: (path) -> - Files.get(path) - - write: (path, blob) -> - now = +new Date - - Files.put - path: path - blob: blob - size: blob.size - type: blob.type - createdAt: now - updatedAt: now - .then notify "write", path - - update: (path, changes) -> - Files.update path, changes - .then notify "update", path - - delete: (path) -> - Files.delete(path) - .then notify "delete", path - - # TODO: Collapse folders - # .replace(/\/.*$/, "/") - list: (dir) -> - Files.where("path").startsWith(dir).toArray() - .then (files) -> - files.forEach (file) -> - file.relativePath = file.path.replace(dir, "") - - return files +DexieFS = require "./lib/dexie-fs" +MountFS = require "./lib/mount-fs" uniq = (array) -> Array.from new Set array -Bindable = require "bindable" +Ajax = require "ajax" Model = require "model" Achievement = require "./system/achievement" Associations = require "./system/associations" @@ -68,24 +26,78 @@ UI = require "ui" module.exports = (dbName='zine-os') -> self = Model() - fs = DexieFS(DexieFSDB(dbName)) + fs = MountFS() + fs.mount "/", DexieFS(DexieFSDB(dbName)) self.include(Achievement, Associations, SystemModule, Template) + {title} = require "./pixie" + [..., version] = title.split('-') + self.extend + ajax: Ajax() fs: fs - # TODO: Allow relative paths + version: -> version + + require: require + stylus: require "./lib/stylus.min" + + moveFile: (oldPath, newPath) -> + oldPath = normalizePath oldPath + newPath = normalizePath newPath + + return Promise.resolve() if oldPath is newPath + + self.copyFile(oldPath, newPath) + .then -> + self.deleteFile(oldPath) + + copyFile: (oldPath, newPath) -> + return Promise.resolve() if oldPath is newPath + + self.readFile(oldPath) + .then (blob) -> + self.writeFile(newPath, blob) + + moveFileSelection: (selectionData, destinationPath) -> + Promise.resolve() + .then -> + {sourcePath, files} = selectionData + if sourcePath is destinationPath + return + else + Promise.all files.map ({relativePath}) -> + if relativePath.match(/\/$/) + # Folder + self.readTree("#{sourcePath}#{relativePath}") + .then (files) -> + Promise.all files.map (file) -> + targetPath = file.path.replace(sourcePath, destinationPath) + self.moveFile(file.path, targetPath) + else + self.moveFile("#{sourcePath}#{relativePath}", "#{destinationPath}#{relativePath}") + readFile: (path, userEvent) -> if userEvent self.Achievement.unlock "Load a file" path = normalizePath "/#{path}" fs.read(path) - .then ({blob}) -> - blob - # TODO: Allow relative paths + readTree: (directoryPath) -> + fs.list(directoryPath) + .then (files) -> + Promise.all files.map (file) -> + if file.folder + self.readTree(file.path) + else + file + .then (filesAndFolderFiles) -> + filesAndFolderFiles.reduce (a, b) -> + a.concat(b) + , [] + writeFile: (path, blob, userEvent) -> if userEvent self.Achievement.unlock "Save a file" @@ -93,16 +105,53 @@ module.exports = (dbName='zine-os') -> path = normalizePath "/#{path}" fs.write path, blob - # TODO: Allow relative paths deleteFile: (path) -> path = normalizePath "/#{path}" fs.delete(path) - # TODO: Allow relative paths updateFile: (path, changes) -> path = normalizePath "/#{path}" fs.update(path, changes) + urlForPath: (path) -> + fs.read(path) + .then URL.createObjectURL + + launchIssue: (date) -> + require("./issues/#{date}")() + + # TODO: Move this into some kind of system utils + installModulePrompt: -> + UI.Modal.prompt("url", "https://danielx.net/editor/master.json") + .then (url) -> + throw new Error "No url given" unless url + + baseName = url.replace(/^https:\/\/(.*)/, "$1") + .replace(/(\.json)?$/, "💾") + + pathPrompt = UI.Modal.prompt "path", "/lib/#{baseName}" + .then (path) -> + throw new Error "No path given" unless path + path + + blobRequest = fetch url + .then (result) -> + result.blob() + + Promise.all([blobRequest, pathPrompt]) + .then ([path, blob]) -> + self.writeFile(path, blob) + + installModule: (url, path) -> + path ?= url.replace(/^https:\/\/(.*)/, "/lib/$1") + .replace(/(\.json)?$/, "💾") + + fetch url + .then (result) -> + result.blob() + .then (blob) -> + self.writeFile(path, blob) + # NOTE: These are experimental commands to run code execJS: (path) -> self.readFile(path) @@ -111,8 +160,20 @@ module.exports = (dbName='zine-os') -> .then (programText) -> Function(programText)() + Observable: UI.Observable UI: UI + dumpModules: -> + src = PACKAGE.source + Object.keys(src).forEach (path) -> + file = src[path] + blob = new Blob [file.content] + self.writeFile("System/#{path}", blob) + + dumpPackage: -> + blob = new Blob [JSON.stringify(PACKAGE)], type: "application/json; charset=utf-8" + self.writeFile("System 💾", blob) + invokeBefore UI.Modal, "hide", -> self.Achievement.unlock "Dismiss modal" diff --git a/system/achievement.coffee b/system/achievement.coffee index f668963..253a54c 100644 --- a/system/achievement.coffee +++ b/system/achievement.coffee @@ -48,6 +48,41 @@ achievementData = [{ icon: "😭" group: "Issue 2" description: "Played dungeon of sadness" +}, { + text: "Issue 3" + icon: "📰" + group: "Issue 3" + description: "View Issue 3" +}, { + text: "Cover-2-cover 3: Tokyo Drift" + icon: "📗" + group: "Issue 3" + description: "Read the entire issue" +}, { + text: "Blue light special" + icon: "🈹" + group: "Issue 3" + description: "Read 'Blue Light Special'" +}, { + text: "Issue 4" + icon: "📰" + group: "Issue 4" + description: "View Issue 4" +}, { + text: "Cover-2-cover 4: Fast & Furious" + icon: "📗" + group: "Issue 4" + description: "Read the entire issue" +}, { + text: "Izzy" + icon: "🈹" + group: "Issue 4" + description: "Read 'Izzy'" +}, { + text: "Residue" + icon: "🈹" + group: "Issue 4" + description: "Read 'Residue'" }, { # Apps text: "Notepad.exe" icon: "📝" @@ -68,6 +103,21 @@ achievementData = [{ icon: "🖼️" group: "App" description: "Open the image viewer" +}, { + text: "Pixel perfect" + icon: "◼️️" + group: "App" + description: "Open the pixel editor" +}, { + text: "Check yo' self" + icon: "😉" + group: "App" + description: "Check your achievement status" +}, { + text: "Oh no, my files!" + icon: "💼" + group: "App" + description: "Opened 'My Briefcase'" }, { # OS text: "Save a file" icon: "💾" @@ -93,11 +143,51 @@ achievementData = [{ icon: "🐛" group: "OS" description: "Encountered a JavaScript error" +}, { + text: "Shut Down" + icon: "🔌" + group: "OS" + description: "ZineOS cannot be stopped" }, { # Social text: "Do you 'like' like me?" icon: "💕" group: "Social" description: "Have fine taste" +}, { + text: "We value your input" + icon: "📩" + group: "Social" + description: "View feedback form" +}, { # Chateau + text: "Enter the Chateau" + icon: "🏡" + group: "Chateau" + description: "Enter the Chateau" +}, { + text: "Puttin' on the Ritz" + icon: "🐭" + group: "Chateau" + description: "Upload custom avatar" +}, { + text: "Paint the town red" + icon: "🌆" + group: "Chateau" + description: "Upload a custom background" +}, { + text: "Poutine on the Ritz" + icon: "🍘" + group: "Chateau" + description: "Put poutine on a Ritz cracker" +}, { + text: "It's in the cloud" + icon: "☁️️" + group: "Chateau" + description: "Upload a file" +}, { + text: "Rawr" + icon: "🐉" + group: "Contrasaurus" + description: "Played Contrasaurus" }] restore = -> diff --git a/system/associations.coffee b/system/associations.coffee index 20de981..4e223c3 100644 --- a/system/associations.coffee +++ b/system/associations.coffee @@ -1,60 +1,149 @@ +AppDrop = require "../lib/app-drop" + # TODO: Move handlers out AudioBro = require "../apps/audio-bro" Filter = require "../apps/filter" Notepad = require "../apps/notepad" -TextEditor = require "../apps/text-editor" +CodeEditor = require "../apps/text-editor" +Explorer = require "../apps/explorer" Spreadsheet = require "../apps/spreadsheet" PixelEditor = require "../apps/pixel" Markdown = require "../apps/markdown" DSad = require "../apps/dungeon-of-sadness" +MyBriefcase = require "../apps/my-briefcase" + +PkgFS = require "../lib/pkg-fs" + +{extensionFor} = require "../util" openWith = (App) -> (file) -> app = App() - app.loadFile(file.blob) - document.body.appendChild app.element + + if file + {path} = file + system.readFile path + .then (blob) -> + app.loadFile(blob, path) + + system.attachApplication(app) module.exports = (I, self) -> - # TODO: Handlers that can use combined type, extension, and contents info - # to do the right thing - # Prioritize handlers falling back to others + # Handlers use combined type, extension, and contents info to do the right thing + # The first handler that matches is the default handler, the rest are available + # from context menu handlers = [{ - # JavaScript - name: "Execute" + name: "Markdown" # TODO: This renders html now too, so may need a broader name + filter: (file) -> + file.path.match(/\.md$/) or + file.path.match(/\.html$/) + fn: openWith(Markdown) # TODO: This can be a pointer to a system package + }, { + name: "Ace Editor" + filter: (file) -> + file.path.match(/\.coffee$/) or + file.path.match(/\.cson$/) or + file.path.match(/\.html$/) or + file.path.match(/\.jadelet$/) or + file.path.match(/\.js$/) or + file.path.match(/\.json$/) or + file.path.match(/\.md$/) or + file.path.match(/\.styl$/) + fn: openWith(CodeEditor) # TODO: This can be a pointer to a system package + }, { + name: "Run" + filter: (file) -> + file.type is "application/javascript" or + file.path.match(/\.js$/) or + file.path.match(/\.coffee$/) + fn: (file) -> + self.executeInIFrame(file.path) + }, { + name: "Exec" filter: (file) -> file.type is "application/javascript" or - file.path.match /\.js$/ + file.path.match(/\.js$/) or + file.path.match(/\.coffee$/) fn: (file) -> - file.blob.readAsText() - .then (sourceProgram) -> - system.loadModule sourceProgram, file.path + self.execute(file.path) }, { - # CoffeeScript - name: "Execute" + name: "Explore" filter: (file) -> - file.path.match /\.coffee$/ + file.path.match(/💾$/) fn: (file) -> - file.blob.readAsText() - .then (coffeeSource) -> - sourceProgram = CoffeeScript.compile coffeeSource, bare: true + system.readFile(file.path) + .then (blob) -> + blob.readAsJSON() + .then (pkg) -> + mountPath = file.path + "/" + fs = PkgFS(pkg, file.path) + system.fs.mount mountPath, fs + + # TODO: Can we make the explorer less specialized here? + element = Explorer + path: mountPath + windowView = system.UI.Window + title: mountPath + content: element + menuBar: null + width: 640 + height: 480 + iconEmoji: "📂" - system.loadModule sourceProgram, file.path + document.body.appendChild windowView.element }, { - name: "Markdown" + name: "Run" filter: (file) -> - file.path.match /\.md$/ - fn: openWith(Markdown) + file.path.match(/💾$/) + fn: (file) -> + # TODO: Rename? + system.execPathWithFile file.path, null }, { - name: "Text Editor" + name: "Publish" + filter: (file) -> + file.path.match(/💾$/) + fn: (file) -> + system.readFile file.path + .then (blob) -> + blob.readAsJSON() + .then (pkg) -> + system.UI.Modal.prompt "Path", "/My Briefcase/public/somefolder" + .then (path) -> + blob = new Blob [system.htmlForPackage(pkg)], + type: "text/html; charset=utf-8" + system.writeFile(path + "/index.html", blob) + }, { + name: "Run Link" + filter: (file) -> + file.path.match(/🔗$/) + fn: (file) -> + # TODO: Rename? + system.execPathWithFile file.path, null + }, { + name: "Edit Link" + filter: (file) -> + file.path.match(/🔗$/) + fn: openWith(CodeEditor) + }, { + name: "Sys Exec" + filter: (file) -> + return false # TODO: Enable with super mode :P + file.type is "application/javascript" or + file.path.match(/\.js$/) or + file.path.match(/\.coffee$/) + fn: (file) -> + self.execute(file.path) + }, { + name: "Notepad" filter: (file) -> file.type.match(/^text\//) or - file.type is "application/javascript" + file.type.match(/^application\/javascript/) fn: openWith(Notepad) }, { name: "Spreadsheet" filter: (file) -> # TODO: This actually only handles JSON arrays - file.type is "application/json" + file.type.match(/^application\/json/) fn: openWith(Spreadsheet) }, { name: "Image Viewer" @@ -66,6 +155,17 @@ module.exports = (I, self) -> filter: (file) -> file.type.match /^image\// fn: openWith(PixelEditor) + }, { + name: "PDF Viewer" + filter: (file) -> + file.path.match /\.pdf$/ + fn: (file) -> + file.blob.getURL() + .then (url) -> + app = system.iframeApp + src: url + title: file.path + system.attachApplication app }, { name: "Audio Bro" filter: (file) -> @@ -77,7 +177,13 @@ module.exports = (I, self) -> file.path.match /dsad\.exe$/ fn: -> app = DSad() - document.body.appendChild app.element + system.attachApplication app + }, { + name: "zine1.exe" + filter: (file) -> + file.path.match /zine1\.exe$/ + fn: -> + require("../issues/2016-12")() }, { name: "zine2.exe" filter: (file) -> @@ -85,11 +191,30 @@ module.exports = (I, self) -> fn: -> require("../issues/2017-02")() }, { - name: "zine1.exe" + name: "zine3.exe" filter: (file) -> - file.path.match /zine1\.exe$/ + file.path.match /zine3\.exe$/ fn: -> - require("../issues/2016-12")() + require("../issues/2017-03")() + }, { + name: "zine4.exe" + filter: (file) -> + file.path.match /zine4\.exe$/ + fn: -> + require("../issues/2017-04")() + }, { + name: "feedback.exe" + filter: (file) -> + file.path.match /feedback\.exe$/ + fn: -> + require("../feedback")() + }, { + name: "My Briefcase" + filter: ({path}) -> + path.match /My Briefcase$/ + fn: -> + app = MyBriefcase() + system.attachApplication app }] # Open JSON arrays in spreadsheet @@ -103,15 +228,79 @@ module.exports = (I, self) -> else throw new Error "No handler for files of type #{file.type}" + mimes = + html: "text/html" + js: "application/javascript" + json: "application/json" + md: "text/markdown" + Object.assign self, + iframeApp: require "../lib/iframe-app" + # Open a file # TODO: Pass arguments # TODO: Drop files on an app to open them in that app open: (file) -> handle(file) + openPath: (path) -> + self.readFile path + .then self.open + + # Return a list of all handlers that can be used for this file openersFor: (file) -> handlers.filter (handler) -> handler.filter(file) + # Add a handler to the list of handlers, position zero is highest priority + # position -1 is lowest priority. + registerHandler: (handler, position=0) -> + handlers.splice(position, 0, handler) + + handlers: -> + handlers.slice() + + # The final step in launching an application in the OS + # This wires up event streams, drop events, adds the app to the list + # of running applications, and attaches the app's element to the DOM + attachApplication: (app, options={}) -> + # Bind Drop events + AppDrop(app) + + # TODO: Bind to app event streams + + # TODO: Add to list of apps + + document.body.appendChild app.element + + pathAsApp: (path) -> + if path.match(/💾$/) + system.readFile path + .then (blob) -> + blob.readAsJSON() + .then (pkg) -> + return -> + self.executePackageInIFrame(pkg) + else if path.match(/🔗$/) + system.readFile path + .then (blob) -> + blob.readAsText() + .then system.evalCSON + .then (data) -> + return -> + system.iframeApp data + else if path.match(/\.js$|\.coffee$/) + -> + self.executeInIFrame(path) + else + Promise.reject new Error "Could not launch #{path}" + + execPathWithFile: (path, file) -> + self.pathAsApp(path) + .then (App) -> + openWith(App)(file) + + mimeTypeFor: (path) -> + mimes[extensionFor(path)] or "text/plain" + return self diff --git a/system/module.coffee b/system/module.coffee index fcc39d0..8c8926a 100644 --- a/system/module.coffee +++ b/system/module.coffee @@ -2,6 +2,8 @@ # # Depends on having self.readFile defined +IFrameApp = require "../lib/iframe-app" + module.exports = (I, self) -> ### Load a module from a file in the file system. @@ -17,13 +19,42 @@ module.exports = (I, self) -> Circular includes will never reslove # TODO: Fail early on circular includes, challenging because of async - # TODO: Succeed on files that don't assign module.exports + # Currently can require + # js, coffee, jadelet, json, cson + + # Requiring other file types returns a Blob - # TODO: Require .coffee/arbitrary files - # images, blobs, html, json ### - {fileSeparator, normalizePath} = require "../util" + { + absolutizePath + evalCSON + fileSeparator + normalizePath + isAbsolutePath + isRelativePath + htmlForPackage + } = require "../util" + + findDependencies = (sourceProgram) -> + requireMatcher = /[^.]require\(['"]([^'"]+)['"]\)/g + results = [] + count = 0 + + loop + match = requireMatcher.exec sourceProgram + + if match + results.push match[1] + else + break + + # Circuit breaker for safety + count += 1 + if count > 256 + break + + return results # Wrap program in async include wrapper # Replaces references to require('something') with local variables in an async wrapper function @@ -32,8 +63,10 @@ module.exports = (I, self) -> namePrefix = "__req" requires = {} - # rewrite requires - rewrittenProgram = program.replace /require\(['"]([^'"]+)['"]\)/g, (match, key) -> + # rewrite requires like `require('cool-module')` or `require('./relative-path')` + # don't rewrite one that belong to another object `something.require('somepath')` + # don't rewrite dynamic ones like `require(someVar)` + rewrittenProgram = program.replace /[^.]require\(['"]([^'"]+)['"]\)/g, (match, key) -> if requires[key] tmpVar = requires[key] else @@ -50,7 +83,7 @@ module.exports = (I, self) -> requirePaths = requirePaths """ - system.include(#{JSON.stringify(requirePaths)}) + return system.vivifyPrograms(#{JSON.stringify(requirePaths)}) .then(function(__reqResults) { (function(#{tmpVars.join(', ')}){ #{rewrittenProgram} @@ -63,8 +96,6 @@ module.exports = (I, self) -> program = annotateSourceURL(rewriteRequires(content), path) dirname = path.split(fileSeparator)[0...-1].join(fileSeparator) or fileSeparator - # May need to scan for a module.exports to see if it is the kind of - # module that exports things vs just plain side effects code module = path: dirname @@ -79,14 +110,18 @@ module.exports = (I, self) -> # Trigger complete resolve(module) - # Apply relative path wrapper for system.include + # Apply relative path wrapper for system.vivifyPrograms localSystem = Object.assign {}, self, - include: (moduleIdentifiers) -> - relativeIdentifiers = moduleIdentifiers.map (identifier) -> - # TODO: Allow absolute paths? - normalizePath dirname + identifier + vivifyPrograms: (moduleIdentifiers) -> + absoluteIdentifiers = moduleIdentifiers.map (identifier) -> + if isAbsolutePath(identifier) + absolutizePath "/", identifier + else if isRelativePath(identifier) + absolutizePath dirname, identifier + else + identifier - self.include relativeIdentifiers, state + self.vivifyPrograms absoluteIdentifiers, state # TODO: Also make working directory relative paths for readFile and writeFile context = @@ -100,37 +135,360 @@ module.exports = (I, self) -> args = Object.keys(context) values = args.map (name) -> context[name] - try + Promise.resolve() + .then -> Function(args..., program).apply(module, values) - catch e - console.error e - reject e + .catch reject + + # Scan for a module.exports to see if it is the kind of + # module that exports things vs just plain side effects code + # This can return false positives if it just matches the string and isn't + # really exporting, regex is not a parser, yolo, etc. + hasExports = program.match /module\.exports/ + + # Just resolve next tick if we're not specifically exporting + # can be fun with race conditions, but just export your biz, yo! + if !hasExports + setTimeout -> + resolve(module) + , 0 Object.assign self, + autoboot: -> + self.fs.list "/System/Boot/" + .then (files) -> + console.log files + bootablePaths = files.filter ({blob}) -> + blob? + .map ({path}) -> + path + + self.vivifyPrograms(bootablePaths) + + # A simpler, dumber, packager that reads a pixie.cson, then + # just packages every file recursively down in the directories + createPackageFromPixie: (pixiePath) -> + basePath = pixiePath.match(/^.*\//)?[0] or "" + pkg = + distribution: {} + + self.loadProgram(pixiePath).then (config) -> + pkg.config = config + .then -> + self.readTree(basePath) + .then (files) -> + Promise.all files.map ({path, blob}) -> + (if blob instanceof Blob + self.compileFile(blob) + else + self.readFile(path) + .then(self.compileFile) + ) + .then (result) -> + [path, result] + .then (results) -> + results.forEach ([path, result]) -> + pkgPath = path.replace(basePath, "").replace(/\.[^.]*$/, "") + + if typeof result is "string" + pkg.distribution[pkgPath] = + content: result + else + console.warn "Can't package files like #{path} yet" + + return pkg + + # This is kind of the opposite approach of the vivifyPrograms, here we want + # to load everything statically and put it in a package that can be run by + # `require`. + packageProgram: (absolutePath, state={}) -> + state.cache = {} + state.pkg = {} + + basePath = absolutePath.match(/^.*\//)?[0] or "" + state.basePath = basePath + + # Strip out base path and final suffix + # NOTE: .coffee.md type files won't like this + state.pkgPath = (path) -> + path.replace(state.basePath, "").replace(/\.[^.]*$/, "") + pkgPath = state.pkgPath(absolutePath) + + {pkg} = state + pkg.distribution ?= {} + + unless state.loadConfigPromise + configPath = absolutizePath basePath, "pixie.cson" + state.loadConfigPromise = self.loadProgram(configPath).then (configSource) -> + module = {} + Function("module", configSource)(module) + module.exports + .then (config) -> + entryPoint = config.entryPoint + (if entryPoint + path = absolutizePath(basePath, entryPoint) + self.loadProgramIntoPackage(path, state) + else + Promise.resolve() + ).then -> + debugger + pkg.remoteDependencies = config.remoteDependencies + pkg.config = config + .catch (e) -> + if e.message.match /File not found/i + pkg.config = {} + else + throw e + + self.loadProgramIntoPackage(absolutePath, state) + .then -> + state.loadConfigPromise + .then -> + pkg.remoteDependencies = pkg.config.remoteDependencies + if pkg.config.entryPoint + pkg.entryPoint = pkg.config.entryPoint + else + pkg.entryPoint ?= pkgPath + + return pkg + + # Internal helper to load a program and its dependencies into the pkg + # in the state + # TODO: Loading deps like this doesn't work at all if require is used + # from browserified js sources :( + loadProgramIntoPackage: (absolutePath, state) -> + {basePath, pkg} = state + pkgPath = state.pkgPath(absolutePath) + relativeRoot = absolutePath.replace(/\/[^/]*$/, "") + + state.cache[absolutePath] ?= self.loadProgram(absolutePath) + .then (sourceProgram) -> + if typeof sourceProgram is "string" + # NOTE: Things will fail if we require ../../ above our + # initial directory. + # TODO: Detect and throw if requiring relative or absolute paths above + # or outside of our base path + + # Add to package + pkg.distribution[pkgPath] = + content: sourceProgram + + # Pull in dependencies + depPaths = findDependencies(sourceProgram) + Promise.all depPaths.map (depPath) -> + Promise.resolve().then -> + if isRelativePath depPath + path = absolutizePath(relativeRoot, depPath) + self.loadProgramIntoPackage path, state + else if isAbsolutePath depPath + throw new Error "Absolute paths not supported yet" + else + # package path + depPkg = PACKAGE.dependencies[depPath] + if depPkg + pkg.dependencies ?= {} + pkg.dependencies[depPath] = depPkg + else + # TODO: Load from remote? + throw new Error "Package '#{depPath}' not found" + else + throw new Error "TODO: Can't package files like #{absolutePath} yet" + + # still experimenting with the API - # Async include in the vein of require.js + # Async 'require' in the vein of require.js # it's horrible but seems necessary # This is an internal API and isn't recommended for general use - # The state determines an include root and should is the same for a single + # The state determines an include root and should be the same for a single # app or process - include: (moduleIdentifiers, state={}) -> + vivifyPrograms: (absolutePaths, state={}) -> state.cache ?= {} - Promise.all moduleIdentifiers.map (identifier) -> - state.cache[identifier] ?= self.readFile(identifier) - .then (file) -> - file.readAsText() + Promise.all absolutePaths.map (absolutePath) -> + state.cache[absolutePath] ?= self.loadProgram(absolutePath) .then (sourceProgram) -> - loadModule sourceProgram, identifier, state + # loadProgram returns an object in the case of JSON because it has no + # dependencies and doesn't need an require re-writing + # Having this special case lets us take a short cut without having to + # Parse/unparse json extra. + # This may be handy for other binary assets like images, etc. as well + if typeof sourceProgram is "string" + loadModule sourceProgram, absolutePath, state + else + exports: sourceProgram .then (module) -> module.exports + loadProgram: (path, basePath="/") -> + self.readForRequire path, basePath + .then self.compileFile + + compileFile: (file) -> + # system modules are loaded as functions/objects right now, so just return them + unless file instanceof Blob + return file + + [compiler] = compilers.filter ({filter}) -> + filter file + + if compiler + compiler.fn(file) + else + # Return the blob itself if we didn't find any compilers + return file + # May want to reconsider this name loadModule: (args...) -> self.Achievement.unlock "Execute code" loadModule(args...) + # Execute in the context of the system itself + spawn: (args...) -> + loadModule(args...) + .then ({exports}) -> + if typeof exports is "function" and exports.length is 0 + result = exports() + + if result.element + document.body.appendChild result.element + + execute: (absolutePath) -> + self.vivifyPrograms [absolutePath] + .then ([{exports}])-> + if typeof exports is "function" and exports.length is 0 + result = exports() + + if result.element + document.body.appendChild result.element + + executeInIFrame: (absolutePath) -> + self.packageProgram(absolutePath) + .then (pkg) -> + self.executePackageInIFrame pkg + + # Execute a package in the context of an iframe + executePackageInIFrame: (pkg) -> + app = IFrameApp + pkg: pkg + title: pkg.config?.title + packageOptions: + script: """ + var ZINEOS = #{JSON.stringify system.version()}; + #{PACKAGE.distribution["lib/system-client"].content}; + """ + sandbox: "allow-scripts allow-forms" + + document.body.appendChild app.element + + return app + + # Handle requiring with or without explicit extension + # require "a" + # First check: + # a + # a.coffee + # a.coffee.md + # a.litcoffee + # a.jadelet + # a.js + readForRequire: (path, basePath) -> + # Hack to load 'system' modules + isModule = !path.match(/^.?.?\//) + if isModule + return Promise.resolve() + .then -> + require path + + absolutePath = absolutizePath(basePath, path) + + suffixes = ["", ".coffee", ".coffee.md", ".litcoffee", ".jadelet", ".js", ".styl"] + + p = suffixes.reduce (promise, suffix) -> + promise.then (file) -> + return file if file + filePath = "#{absolutePath}#{suffix}" + self.readFile(filePath) + .catch -> # If read fails try next read + , Promise.resolve() + + p.then (file) -> + unless file + tries = suffixes.map (suffix) -> + "#{absolutePath}#{suffix}" + throw new Error "File not found at path: #{absolutePath}. Tried #{tries}" + + return file + + htmlForPackage: htmlForPackage + + evalCSON: evalCSON + +# Compile files based on type to JS program source +# These compilers return a string of JS source code that assigns a +# result to module.exports +compilers = [{ + filter: ({path}) -> + path.match /\.js$/ + fn: (blob) -> + blob.readAsText() +}, { + filter: ({path}) -> + path.match(/\.coffee.md$/) or + path.match(/\.litcoffee$/) + fn: (blob) -> + blob.readAsText() + .then (coffeeSource) -> + CoffeeScript.compile coffeeSource, bare: true, literate: true +}, { + filter: ({path}) -> + path.match /\.coffee$/ + fn: (blob) -> + blob.readAsText() + .then (coffeeSource) -> + CoffeeScript.compile coffeeSource, bare: true +}, { + filter: ({path}) -> + path.match /\.jadelet$/ + fn: (blob) -> + blob.readAsText() + .then (jadeletSource) -> + Hamlet.compile jadeletSource, + compiler: CoffeeScript + mode: "jade" + runtime: "require('_lib_hamlet-runtime')" +}, { + filter: ({path}) -> + path.match /\.styl$/ + fn: (blob) -> + blob.readAsText() + .then (source) -> + system.stylus(source).render() + .then stringifyExport +}, { + filter: ({path}) -> + path.match /\.json$/ + fn: (blob) -> + blob.readAsJSON() + .then stringifyExport +}, { + filter: ({path}) -> + path.match /\.cson$/ + fn: (blob) -> + blob.readAsText() + .then evalCSON + .then stringifyExport +}, { + filter: ({path}) -> + path.match /\.te?xt$/ + fn: (blob) -> + blob.readAsText() + .then stringifyExport +}] + +stringifyExport = (data) -> + "module.exports = #{JSON.stringify(data)}" + annotateSourceURL = (program, path) -> """ #{program} diff --git a/templates/file.jadelet b/templates/file.jadelet index e36c5bd..dc88da2 100644 --- a/templates/file.jadelet +++ b/templates/file.jadelet @@ -1,3 +1,3 @@ -file(@dblclick @contextmenu @type) +file(draggable="true" @dragstart @dblclick @contextmenu @path @type) icon - label= @relativePath + label= @displayName diff --git a/templates/folder.jadelet b/templates/folder.jadelet index f4b73b3..0d00839 100644 --- a/templates/folder.jadelet +++ b/templates/folder.jadelet @@ -1,3 +1,3 @@ -folder(@dblclick @contextmenu) +folder(draggable="true" @dragstart @dblclick @contextmenu @path) icon - label= @relativePath + label= @displayName diff --git a/templates/home-button.jadelet b/templates/home-button.jadelet new file mode 100644 index 0000000..ceb6a4d --- /dev/null +++ b/templates/home-button.jadelet @@ -0,0 +1 @@ +button.home(@click) 🏡 Start diff --git a/templates/site-url.jadelet b/templates/site-url.jadelet new file mode 100644 index 0000000..1013448 --- /dev/null +++ b/templates/site-url.jadelet @@ -0,0 +1 @@ +site-url https://whimsy.space diff --git a/templates/version.jadelet b/templates/version.jadelet new file mode 100644 index 0000000..e989d72 --- /dev/null +++ b/templates/version.jadelet @@ -0,0 +1,2 @@ +version + span= @version diff --git a/test/system/module.coffee b/test/system/module.coffee index aaa73a2..68c2e74 100644 --- a/test/system/module.coffee +++ b/test/system/module.coffee @@ -3,13 +3,33 @@ Model = require "model" Associations = require "../../system/associations" SystemModule = require "../../system/module" -describe "System Module", -> - it "should include modules in files async", -> - model = Model() +global.Hamlet = require "../../lib/hamlet" + +mocha.setup + globals: ['amazon'] + +makeSystemFS = (files) -> + model = Model() + model.include SystemModule, Associations + + Object.assign model, + readFile: (path) -> + Promise.resolve() + .then -> + content = files[path] + + throw new Error "File not found: #{path}" unless content? + + blob = new Blob [content] + blob.path = path + + return blob - model.include SystemModule + return model - files = +describe "System Module", -> + it "should vivifyPrograms in files asynchronously", -> + model = makeSystemFS "/test.js": """ module.exports = 'yo'; """ @@ -28,23 +48,64 @@ describe "System Module", -> module.exports = Math.random(); """ - model.readFile = (path) -> - content = files[path] - - Promise.resolve new Blob [content] - - model.include(["/root.js", "/wat.js", "/rand.js", "/rand.js"]) + model.vivifyPrograms(["/root.js", "/wat.js", "/rand.js", "/rand.js"]) .then ([root, wat, r1, r2]) -> console.log root, wat, r1, r2 assert.equal r1, r2 assert.equal root, 'yo 2 rad hella' - it "should wait forever when resolving circular requires", (done) -> - model = Model() + it "should throw an error when requiring a file that doesn't exist", (done) -> + #@timeout 250 + + model = makeSystemFS + "/a.js": """ + module.exports = require("./b.js") + """ + + model.vivifyPrograms(["/a.js"]) + .catch (e) -> + done() - model.include SystemModule + it "should throw an error when requiring a file that throws an error", (done) -> + @timeout 250 - files = + model = makeSystemFS + "/a.js": """ + throw new Error("I am error") + """ + + model.vivifyPrograms(["/a.js"]) + .catch (e) -> + done() + + it "should require valid json", -> + @timeout 250 + + model = makeSystemFS + "/a.json": """ + { + "yolo": "wat" + } + """ + + model.vivifyPrograms(["/a.json"]) + .then ([json]) -> + assert.equal json.yolo, "wat" + + it "should throw an error when requiring invalid json", (done) -> + @timeout 250 + + model = makeSystemFS + "/a.json": """ + yolo: 'wat' + """ + + model.vivifyPrograms(["/a.json"]) + .catch (e) -> + done() + + it "should wait forever when resolving circular requires", (done) -> + model = makeSystemFS "/a.js": """ module.exports = require("./b.js") """ @@ -52,12 +113,7 @@ describe "System Module", -> module.exports = require("./a.js") """ - model.readFile = (path) -> - content = files[path] - - Promise.resolve new Blob [content] - - model.include(["/a.js"]) + model.vivifyPrograms(["/a.js"]) .then ([a]) -> # Never get here assert false @@ -66,44 +122,60 @@ describe "System Module", -> done() , 100 - it "should return export if present", -> - model = Model() - - model.include Associations, SystemModule - - files = + it "should work even if the file doesn't assign to module.exports", -> + model = makeSystemFS "/wat.js": """ - module.exports = "wat"; + exports.yolo = "wat"; """ - model.readFile = (path) -> - content = files[path] - - Promise.resolve new Blob [content] + model.vivifyPrograms ["/wat.js"] + .then ([wat]) -> + assert.equal wat.yolo, "wat" - model.open - path: "/wat.js" - type: "application/javascript" - .then (moduleExports) -> - assert.equal moduleExports, "wat" - - it "should work even if the file doesn't assign to module.exports" - -> - model = Model() + it "should work with relative paths in subfolders", -> + model = makeSystemFS + "/main.js": """ + module.exports = require("./folder/a.js"); + """ + "/folder/a.js": """ + module.exports = require("./b.js"); + """ + "/folder/b.js": """ + module.exports = "b"; + """ - model.include SystemModule + model.vivifyPrograms ["/main.js"] + .then ([main]) -> + assert.equal main, "b" - files = - "/wat.js": """ - exports.yolo = "wat"; + it "should work with absolute paths in subfolders", -> + model = makeSystemFS + "/main.js": """ + module.exports = require("./folder/a.js"); + """ + "/folder/a.js": """ + module.exports = require("/b.js"); + """ + "/b.js": """ + module.exports = "b"; """ - model.readFile = (path) -> - content = files[path] + model.vivifyPrograms ["/main.js"] + .then ([main]) -> + assert.equal main, "b" - Promise.resolve new Blob [content] + it "should require .jadelet sources", -> + model = makeSystemFS + "/main.coffee": """ + template = require "./button.jadelet" + + module.exports = + buttonTemplate: template + """ + "/button.jadelet": """ + button(@click)= @text + """ - model.open - path: "/wat.js" - .then (moduleExports) -> - assert.equal moduleExports.yolo, "wat" + model.vivifyPrograms ["/main.coffee"] + .then ([main]) -> + assert typeof main.buttonTemplate is "function" diff --git a/util.coffee b/util.coffee index 639305b..c9ccfba 100644 --- a/util.coffee +++ b/util.coffee @@ -1,17 +1,95 @@ fileSeparator = "/" normalizePath = (path) -> - path.replace(/\/\/+/, fileSeparator) # /// -> / + path.replace(/\/+/g, fileSeparator) # /// -> / .replace(/\/[^/]*\/\.\./g, "") # /base/something/.. -> /base .replace(/\/\.\//g, fileSeparator) # /base/. -> /base +# NOTE: Allows paths like '../..' to go above the base path +absolutizePath = (base, relativePath) -> + normalizePath "/#{base}/#{relativePath}" + +makeScript = (src) -> + "