diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee index 16272283b..b9ec9e3aa 100644 --- a/benchmark/benchmark-helper.coffee +++ b/benchmark/benchmark-helper.coffee @@ -7,7 +7,7 @@ Config = require 'config' Project = require 'project' require 'window' -requireStylesheet "jasmine.css" +requireStylesheet "jasmine.less" # Load TextMate bundles, which specs rely on (but not other packages) atom.loadTextMatePackages() diff --git a/docs/getting-started.md b/docs/getting-started.md index 507b6c653..b5abb2d63 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,7 +11,7 @@ always hit `meta-p` to bring up a list of commands that are relevant to the currently focused UI element. If there is a key binding for a given command, it is also displayed. This is a great way to explore the system and get to know the key commands interactively. If you'd like to add or change a binding for a -command, refer to the [keymaps](#keymaps) section to learn how. +command, refer to the [key bindings](#customizing-key-bindings) section to learn how. ![Command Palette](http://f.cl.ly/items/32041o3w471F3C0F0V2O/Screen%20Shot%202013-02-13%20at%207.27.41%20PM.png) diff --git a/docs/internals/serialization.md b/docs/internals/serialization.md new file mode 100644 index 000000000..ce1d7a78e --- /dev/null +++ b/docs/internals/serialization.md @@ -0,0 +1,97 @@ +## Serialization in Atom + +When a window is refreshed or restored from a previous session, the view and its +associated objects are *deserialized* from a JSON representation that was stored +during the window's previous shutdown. For your own views and objects to be +compatible with refreshing, you'll need to make them play nicely with the +serializing and deserializing. + +### Package Serialization Hook + +Your package's main module can optionally include a `serialize` method, which +will be called before your package is deactivated. You should return JSON, which +will be handed back to you as an argument to `activate` next time it is called. +In the following example, the package keeps an instance of `MyObject` in the +same state across refreshes. + +```coffee-script +module.exports = + activate: (state) -> + @myObject = + if state + deserialize(state) + else + new MyObject("Hello") + + serialize: -> + @myObject.serialize() +``` + +### Serialization Methods + +```coffee-script +class MyObject + registerDeserializer(this) + @deserialize: ({data}) -> new MyObject(data) + constructor: (@data) -> + serialize: -> { deserializer: 'MyObject', data: @data } +``` + +#### .serialize() +Objects that you want to serialize should implement `.serialize()`. This method +should return a serializable object, and it must contain a key named +`deserializer` whose value is the name of a registered deserializer that can +convert the rest of the data to an object. It's usually just the name of the +class itself. + +#### @deserialize(data) +The other side of the coin is the `deserialize` method, which is usually a +class-level method on the same class that implements `serialize`. This method's +job is to convert a state object returned from a previous call `serialize` back +into a genuine object. + +#### registerDeserializer(klass) +You need to call the global `registerDeserializer` method with your class in +order to make it available to the deserialization system. Now you can call the +global `deserialize` method with state returned from `serialize`, and your +class's `deserialize` method will be selected automatically. + +### Versioning + +```coffee-script +class MyObject + @version: 2 + @deserialize: (state) -> ... + serialize: -> { version: MyObject.version, ... } +``` + +Your serializable class can optionally have a class-level `@version` property +and include a `version` key in its serialized state. When deserializing, Atom +will only attempt to call deserialize if the two versions match, and otherwise +return undefined. We plan on implementing a migration system in the future, but +this at least protects you from improperly deserializing old state. If you find +yourself in dire need of the migration system, let us know. + +### Deferred Package Deserializers + +If your package defers loading on startup with an `activationEvents` property in +its `package.cson`, your deserializers won't be loaded until your package is +activated. If you want to deserialize an object from your package on startup, +this could be a problem. + +The solution is to also supply a `deferredDeserializers` array in your +`package.cson` with the names of all your deserializers. When Atom attempts to +deserialize some state whose `deserializer` matches one of these names, it will +load your package first so it can register any necessary deserializers before +proceeding. + +For example, the markdown preview package doesn't fully load until a preview is +triggered. But if you refresh a window with a preview pane, it loads the +markdown package early so Atom can deserialize the view correctly. + +```coffee-script +# markdown-preview/package.cson +'activationEvents': 'markdown-preview:toggle': '.editor' +'deferredDeserializers': ['MarkdownPreviewView'] +... +``` diff --git a/native/atom_application.h b/native/atom_application.h index cef8719b1..d2f79686b 100644 --- a/native/atom_application.h +++ b/native/atom_application.h @@ -18,6 +18,7 @@ class AtomCefClient; + (CefSettings)createCefSettings; + (NSDictionary *)parseArguments:(char **)argv count:(int)argc; - (void)open:(NSString *)path; +- (void)openDev:(NSString *)path; - (void)open:(NSString *)path pidToKillWhenWindowCloses:(NSNumber *)pid; - (IBAction)runSpecs:(id)sender; - (IBAction)runBenchmarks:(id)sender; diff --git a/native/atom_main_mac.mm b/native/atom_main_mac.mm index 6c148fc84..ed1f3c3ca 100644 --- a/native/atom_main_mac.mm +++ b/native/atom_main_mac.mm @@ -13,18 +13,14 @@ void activateOpenApp(); BOOL isAppAlreadyOpen(); int AtomMain(int argc, char* argv[]) { - { - // See if we're being run as a secondary process. - - CefMainArgs main_args(argc, argv); - CefRefPtr app(new AtomCefApp); - int exitCode = CefExecuteProcess(main_args, app); - if (exitCode >= 0) - return exitCode; - } + // Check if we're being run as a secondary process. + CefMainArgs main_args(argc, argv); + CefRefPtr app(new AtomCefApp); + int exitCode = CefExecuteProcess(main_args, app); + if (exitCode >= 0) + return exitCode; // We're the main process. - @autoreleasepool { handleBeingOpenedAgain(argc, argv); @@ -33,7 +29,7 @@ int AtomMain(int argc, char* argv[]) { NSString *mainNibName = [infoDictionary objectForKey:@"NSMainNibFile"]; NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle bundleWithIdentifier:@"com.github.atom.framework"]]; - [mainNib instantiateNibWithOwner:application topLevelObjects:nil]; + [mainNib instantiateWithOwner:application topLevelObjects:nil]; CefRunMessageLoop(); } diff --git a/package.json b/package.json index 9b1fee82b..142a4455d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "plist": "git://github.com/nathansobo/node-plist.git" }, + "private": true, + "scripts": { "preinstall": "true" } diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee index 9bbf6095f..795141e58 100644 --- a/spec/app/atom-package-spec.coffee +++ b/spec/app/atom-package-spec.coffee @@ -3,18 +3,31 @@ AtomPackage = require 'atom-package' fs = require 'fs-utils' describe "AtomPackage", -> + [packageMainModule, pack] = [] + + beforeEach -> + pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events')) + pack.load() + describe ".load()", -> + describe "if the package's metadata has a `deferredDeserializers` array", -> + it "requires the package's main module attempting to use deserializers named in the array", -> + expect(pack.mainModule).toBeNull() + object = deserialize(deserializer: 'Foo', data: "Hello") + expect(object.constructor.name).toBe 'Foo' + expect(object.data).toBe 'Hello' + expect(pack.mainModule).toBeDefined() + expect(pack.mainModule.activateCallCount).toBe 0 + + describe ".activate()", -> beforeEach -> window.rootView = new RootView + packageMainModule = require 'fixtures/packages/package-with-activation-events/main' + spyOn(packageMainModule, 'activate').andCallThrough() describe "when the package metadata includes activation events", -> - [packageMainModule, pack] = [] - beforeEach -> - pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events')) - packageMainModule = require 'fixtures/packages/package-with-activation-events/main' - spyOn(packageMainModule, 'activate').andCallThrough() - pack.load() + pack.activate() it "defers activating the package until an activation event bubbles to the root view", -> expect(packageMainModule.activate).not.toHaveBeenCalled() @@ -44,12 +57,13 @@ describe "AtomPackage", -> expect(packageMainModule.activate).not.toHaveBeenCalled() pack.load() + pack.activate() expect(packageMainModule.activate).toHaveBeenCalled() describe "when the package doesn't have an index.coffee", -> it "does not throw an exception or log an error", -> spyOn(console, "error") - spyOn(console, "warn") + spyOn(console, "warn").andCallThrough() pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-keymaps-manifest')) expect(-> pack.load()).not.toThrow() diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 29f5dcc4d..e89ae779e 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -85,26 +85,26 @@ describe "the `atom` global", -> describe "activation", -> it "calls activate on the package main with its previous state", -> pack = window.loadPackage('package-with-module') - spyOn(pack.packageMain, 'activate') + spyOn(pack.mainModule, 'activate') serializedState = rootView.serialize() rootView.deactivate() RootView.deserialize(serializedState) window.loadPackage('package-with-module') - expect(pack.packageMain.activate).toHaveBeenCalledWith(someNumber: 1) + expect(pack.mainModule.activate).toHaveBeenCalledWith(someNumber: 1) describe "deactivation", -> it "deactivates and removes the package module from the package module map", -> pack = window.loadPackage('package-with-module') expect(atom.activatedAtomPackages.length).toBe 1 - spyOn(pack.packageMain, "deactivate").andCallThrough() + spyOn(pack.mainModule, "deactivate").andCallThrough() atom.deactivateAtomPackages() - expect(pack.packageMain.deactivate).toHaveBeenCalled() + expect(pack.mainModule.deactivate).toHaveBeenCalled() expect(atom.activatedAtomPackages.length).toBe 0 describe "serialization", -> - it "uses previous serialization state on unactivated packages", -> + it "uses previous serialization state on packages whose activation has been deferred", -> atom.atomPackageStates['package-with-activation-events'] = {previousData: 'exists'} unactivatedPackage = window.loadPackage('package-with-activation-events') activatedPackage = window.loadPackage('package-with-module') @@ -116,7 +116,8 @@ describe "the `atom` global", -> 'previousData': 'exists' # ensure serialization occurs when the packageis activated - unactivatedPackage.activatePackageMain() + unactivatedPackage.deferActivation = false + unactivatedPackage.activate() expect(atom.serializeAtomPackages()).toEqual 'package-with-module': 'someNumber': 1 @@ -125,8 +126,8 @@ describe "the `atom` global", -> it "absorbs exceptions that are thrown by the package module's serialize methods", -> spyOn(console, 'error') - window.loadPackage('package-with-module') - window.loadPackage('package-with-serialize-error', activateImmediately: true) + window.loadPackage('package-with-module', activateImmediately: true) + window.loadPackage('package-with-serialize-error', activateImmediately: true) packageStates = atom.serializeAtomPackages() expect(packageStates['package-with-module']).toEqual someNumber: 1 diff --git a/spec/app/config-spec.coffee b/spec/app/config-spec.coffee index 7602d1118..2daa360cd 100644 --- a/spec/app/config-spec.coffee +++ b/spec/app/config-spec.coffee @@ -133,3 +133,21 @@ describe "Config", -> expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-ui/package.cson'))).toBeTruthy() expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-dark-syntax.css'))).toBeTruthy() expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-syntax.css'))).toBeTruthy() + + describe "when the config file is not parseable", -> + beforeEach -> + config.configDirPath = '/tmp/dot-atom-dir' + config.configFilePath = fs.join(config.configDirPath, "config.cson") + expect(fs.exists(config.configDirPath)).toBeFalsy() + + afterEach -> + fs.remove('/tmp/dot-atom-dir') if fs.exists('/tmp/dot-atom-dir') + + it "logs an error to the console and does not overwrite the config file", -> + config.save.reset() + spyOn(console, 'error') + fs.write(config.configFilePath, "{{{{{") + config.loadUserConfig() + config.set("hair", "blonde") # trigger a save + expect(console.error).toHaveBeenCalled() + expect(config.save).not.toHaveBeenCalled() diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 56ea53336..708beb081 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -136,6 +136,43 @@ describe "PaneContainer", -> for item in pane.getItems() expect(item.saved).toBeTruthy() + describe ".confirmClose()", -> + it "resolves the returned promise after modified files are saved", -> + pane1.itemAtIndex(0).isModified = -> true + pane2.itemAtIndex(0).isModified = -> true + spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSaveFn) -> noSaveFn() + + promiseHandler = jasmine.createSpy("promiseHandler") + failedPromiseHandler = jasmine.createSpy("failedPromiseHandler") + promise = container.confirmClose() + promise.done promiseHandler + promise.fail failedPromiseHandler + + waitsFor -> + promiseHandler.wasCalled + + runs -> + expect(failedPromiseHandler).not.toHaveBeenCalled() + expect(atom.confirm).toHaveBeenCalled() + + it "rejects the returned promise if the user cancels saving", -> + pane1.itemAtIndex(0).isModified = -> true + pane2.itemAtIndex(0).isModified = -> true + spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancelFn, f, g) -> cancelFn() + + promiseHandler = jasmine.createSpy("promiseHandler") + failedPromiseHandler = jasmine.createSpy("failedPromiseHandler") + promise = container.confirmClose() + promise.done promiseHandler + promise.fail failedPromiseHandler + + waitsFor -> + failedPromiseHandler.wasCalled + + runs -> + expect(promiseHandler).not.toHaveBeenCalled() + expect(atom.confirm).toHaveBeenCalled() + describe "serialization", -> it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> newContainer = deserialize(container.serialize()) @@ -146,3 +183,10 @@ describe "PaneContainer", -> newContainer.height(200).width(300).attachToDom() expect(newContainer.find('.row > :contains(1)').width()).toBe 150 expect(newContainer.find('.row > .column > :contains(2)').height()).toBe 100 + + it "removes empty panes on deserialization", -> + # only deserialize pane 1's view successfully + TestView.deserialize = ({name}) -> new TestView(name) if name is '1' + newContainer = deserialize(container.serialize()) + expect(newContainer.find('.row, .column')).not.toExist() + expect(newContainer.find('> :contains(1)')).toExist() diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index bbe85e207..b67b41449 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -450,6 +450,14 @@ describe "Pane", -> pane.remove() expect(rootView.focus).not.toHaveBeenCalled() + describe ".getNextPane()", -> + it "returns the next pane if one exists, wrapping around from the last pane to the first", -> + pane.showItem(editSession1) + expect(pane.getNextPane()).toBeUndefined + pane2 = pane.splitRight() + expect(pane.getNextPane()).toBe pane2 + expect(pane2.getNextPane()).toBe pane + describe "when the pane is focused", -> it "focuses the active item view", -> focusHandler = jasmine.createSpy("focusHandler") diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 78ca0db59..205b5b188 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -134,8 +134,8 @@ describe "Project", -> project.getFilePaths().done (foundPaths) -> paths = foundPaths runs -> - expect(paths).not.toContain('a') - expect(paths).toContain('b') + expect(paths).not.toContain(project.resolve('a')) + expect(paths).toContain(project.resolve('b')) describe "when config.core.hideGitIgnoredFiles is true", -> it "ignores files that are present in .gitignore if the project is a git repo", -> diff --git a/spec/app/theme-spec.coffee b/spec/app/theme-spec.coffee index abaaf977f..86d703b9d 100644 --- a/spec/app/theme-spec.coffee +++ b/spec/app/theme-spec.coffee @@ -20,8 +20,21 @@ describe "@load(name)", -> expect($(".editor").css("background-color")).toBe("rgb(20, 20, 20)") describe "AtomTheme", -> + describe "when the theme is a file", -> + it "loads and applies css", -> + expect($(".editor").css("padding-bottom")).not.toBe "1234px" + themePath = project.resolve('themes/theme-stylesheet.css') + theme = Theme.load(themePath) + expect($(".editor").css("padding-top")).toBe "1234px" + + it "parses, loads and applies less", -> + expect($(".editor").css("padding-bottom")).not.toBe "1234px" + themePath = project.resolve('themes/theme-stylesheet.less') + theme = Theme.load(themePath) + expect($(".editor").css("padding-top")).toBe "4321px" + describe "when the theme contains a package.json file", -> - it "loads and applies css from package.json in the correct order", -> + it "loads and applies stylesheets from package.json in the correct order", -> expect($(".editor").css("padding-top")).not.toBe("101px") expect($(".editor").css("padding-right")).not.toBe("102px") expect($(".editor").css("padding-bottom")).not.toBe("103px") @@ -32,16 +45,8 @@ describe "@load(name)", -> expect($(".editor").css("padding-right")).toBe("102px") expect($(".editor").css("padding-bottom")).toBe("103px") - describe "when the theme is a CSS file", -> - it "loads and applies the stylesheet", -> - expect($(".editor").css("padding-bottom")).not.toBe "1234px" - - themePath = project.resolve('themes/theme-stylesheet.css') - theme = Theme.load(themePath) - expect($(".editor").css("padding-top")).toBe "1234px" - describe "when the theme does not contain a package.json file and is a directory", -> - it "loads all CSS files in the directory", -> + it "loads all stylesheet files in the directory", -> expect($(".editor").css("padding-top")).not.toBe "10px" expect($(".editor").css("padding-right")).not.toBe "20px" expect($(".editor").css("padding-bottom")).not.toBe "30px" diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 1f6e77d91..5ec40eade 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -1,5 +1,6 @@ $ = require 'jquery' fs = require 'fs-utils' +{less} = require 'less' describe "Window", -> projectPath = null @@ -34,16 +35,31 @@ describe "Window", -> $(window).trigger 'focus' expect($("body")).not.toHaveClass("is-blurred") - describe ".close()", -> - it "is triggered by the 'core:close' event", -> - spyOn window, 'close' - $(window).trigger 'core:close' - expect(window.close).toHaveBeenCalled() + describe "window:close event", -> + describe "when no pane items are modified", -> + it "calls window.close", -> + spyOn window, 'close' + $(window).trigger 'window:close' + expect(window.close).toHaveBeenCalled() - it "is triggered by the 'window:close event'", -> - spyOn window, 'close' - $(window).trigger 'window:close' - expect(window.close).toHaveBeenCalled() + describe "when pane items are are modified", -> + it "prompts user to save and and calls window.close", -> + spyOn(window, 'close') + spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSave) -> noSave() + editSession = rootView.open("sample.js") + editSession.insertText("I look different, I feel different.") + $(window).trigger 'window:close' + expect(window.close).toHaveBeenCalled() + expect(atom.confirm).toHaveBeenCalled() + + it "prompts user to save and aborts if dialog is canceled", -> + spyOn(window, 'close') + spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancel) -> cancel() + editSession = rootView.open("sample.js") + editSession.insertText("I look different, I feel different.") + $(window).trigger 'window:close' + expect(window.close).not.toHaveBeenCalled() + expect(atom.confirm).toHaveBeenCalled() describe ".reload()", -> beforeEach -> @@ -62,22 +78,46 @@ describe "Window", -> expect(atom.confirm).toHaveBeenCalled() describe "requireStylesheet(path)", -> - it "synchronously loads the stylesheet at the given path and installs a style tag for it in the head", -> - $('head style[id*="atom.css"]').remove() + it "synchronously loads css at the given path and installs a style tag for it in the head", -> + cssPath = project.resolve('css.css') lengthBefore = $('head style').length - requireStylesheet('atom.css') + + requireStylesheet(cssPath) expect($('head style').length).toBe lengthBefore + 1 - styleElt = $('head style[id*="atom.css"]') - - fullPath = require.resolve('atom.css') - expect(styleElt.attr('id')).toBe fullPath - expect(styleElt.text()).toBe fs.read(fullPath) + element = $('head style[id*="css.css"]') + expect(element.attr('id')).toBe cssPath + expect(element.text()).toBe fs.read(cssPath) # doesn't append twice - requireStylesheet('atom.css') + requireStylesheet(cssPath) expect($('head style').length).toBe lengthBefore + 1 + $('head style[id*="css.css"]').remove() + + it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", -> + lessPath = project.resolve('sample.less') + lengthBefore = $('head style').length + requireStylesheet(lessPath) + expect($('head style').length).toBe lengthBefore + 1 + + element = $('head style[id*="sample.less"]') + expect(element.attr('id')).toBe lessPath + expect(element.text()).toBe """ + #header { + color: #4d926f; + } + h2 { + color: #4d926f; + } + + """ + + # doesn't append twice + requireStylesheet(lessPath) + expect($('head style').length).toBe lengthBefore + 1 + $('head style[id*="sample.less"]').remove() + describe ".disableStyleSheet(path)", -> it "removes styling applied by given stylesheet path", -> cssPath = require.resolve(fs.join("fixtures", "css.css")) diff --git a/spec/fixtures/markdown/file.markdown b/spec/fixtures/markdown/file.markdown index e69de29bb..0eec6a120 100644 --- a/spec/fixtures/markdown/file.markdown +++ b/spec/fixtures/markdown/file.markdown @@ -0,0 +1,3 @@ +## File.markdown + +:cool: \ No newline at end of file diff --git a/spec/fixtures/packages/package-with-activation-events/main.coffee b/spec/fixtures/packages/package-with-activation-events/main.coffee index a860be2bb..d57ca7c24 100644 --- a/spec/fixtures/packages/package-with-activation-events/main.coffee +++ b/spec/fixtures/packages/package-with-activation-events/main.coffee @@ -1,7 +1,14 @@ +class Foo + registerDeserializer(this) + @deserialize: ({data}) -> new Foo(data) + constructor: (@data) -> + module.exports = + activateCallCount: 0 activationEventCallCount: 0 activate: -> + @activateCallCount++ rootView.getActiveView()?.command 'activation-event', => @activationEventCallCount++ diff --git a/spec/fixtures/packages/package-with-activation-events/package.cson b/spec/fixtures/packages/package-with-activation-events/package.cson index 80903d6f4..42d3eb78d 100644 --- a/spec/fixtures/packages/package-with-activation-events/package.cson +++ b/spec/fixtures/packages/package-with-activation-events/package.cson @@ -1,2 +1,3 @@ 'activationEvents': ['activation-event'] +'deferredDeserializers': ['Foo'] 'main': 'main' diff --git a/spec/fixtures/sample-with-error.less b/spec/fixtures/sample-with-error.less new file mode 100644 index 000000000..4396e25cf --- /dev/null +++ b/spec/fixtures/sample-with-error.less @@ -0,0 +1 @@ +#header { \ No newline at end of file diff --git a/spec/fixtures/sample.less b/spec/fixtures/sample.less new file mode 100644 index 000000000..a076a9d01 --- /dev/null +++ b/spec/fixtures/sample.less @@ -0,0 +1,8 @@ +@color: #4D926F; + +#header { + color: @color; +} +h2 { + color: @color; +} \ No newline at end of file diff --git a/spec/fixtures/themes/theme-stylesheet.less b/spec/fixtures/themes/theme-stylesheet.less new file mode 100644 index 000000000..29e0d80c6 --- /dev/null +++ b/spec/fixtures/themes/theme-stylesheet.less @@ -0,0 +1,5 @@ +@padding: 4321px; + +.editor { + padding-top: @padding; +} diff --git a/spec/fixtures/themes/theme-with-package-file/package.json b/spec/fixtures/themes/theme-with-package-file/package.json index 9add36774..9dc6565c6 100644 --- a/spec/fixtures/themes/theme-with-package-file/package.json +++ b/spec/fixtures/themes/theme-with-package-file/package.json @@ -1,3 +1,3 @@ { - "stylesheets": ["first.css", "second.css", "last.css"] + "stylesheets": ["first.css", "second.less", "last.css"] } \ No newline at end of file diff --git a/spec/fixtures/themes/theme-with-package-file/second.css b/spec/fixtures/themes/theme-with-package-file/second.css deleted file mode 100644 index 3ddf03add..000000000 --- a/spec/fixtures/themes/theme-with-package-file/second.css +++ /dev/null @@ -1,5 +0,0 @@ -.editor { -/* padding-top: 102px;*/ - padding-right: 102px; - padding-bottom: 102px; -} \ No newline at end of file diff --git a/spec/fixtures/themes/theme-with-package-file/second.less b/spec/fixtures/themes/theme-with-package-file/second.less new file mode 100644 index 000000000..71fad0d44 --- /dev/null +++ b/spec/fixtures/themes/theme-with-package-file/second.less @@ -0,0 +1,7 @@ +@number: 102px; + +.editor { +/* padding-top: 102px;*/ + padding-right: @number; + padding-bottom: @number; +} \ No newline at end of file diff --git a/spec/fixtures/themes/theme-without-package-file/c.css b/spec/fixtures/themes/theme-without-package-file/c.css deleted file mode 100644 index 017dea2af..000000000 --- a/spec/fixtures/themes/theme-without-package-file/c.css +++ /dev/null @@ -1,3 +0,0 @@ -.editor { - padding-bottom: 30px; -} diff --git a/spec/fixtures/themes/theme-without-package-file/c.less b/spec/fixtures/themes/theme-without-package-file/c.less new file mode 100644 index 000000000..91b80c92f --- /dev/null +++ b/spec/fixtures/themes/theme-without-package-file/c.less @@ -0,0 +1,5 @@ +@number: 30px; + +.editor { + padding-bottom: @number; +} diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c4dbe999f..ad3c816fa 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -15,7 +15,7 @@ TokenizedBuffer = require 'tokenized-buffer' fs = require 'fs-utils' RootView = require 'root-view' Git = require 'git' -requireStylesheet "jasmine.css" +requireStylesheet "jasmine.less" fixturePackagesPath = fs.resolveOnLoadPath('fixtures/packages') keymap.loadBundledKeymaps() [bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = [] @@ -67,6 +67,8 @@ beforeEach -> spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent + addCustomMatchers(this) + afterEach -> keymap.bindingSets = bindingSetsToRestore keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore @@ -85,12 +87,14 @@ afterEach -> atom.presentingModal = false waits(0) # yield to ui thread to make screen update more frequently -window.loadPackage = (name, options) -> +window.loadPackage = (name, options={}) -> Package = require 'package' packagePath = _.find atom.getPackagePaths(), (packagePath) -> fs.base(packagePath) == name if pack = Package.build(packagePath) pack.load(options) atom.loadedPackages.push(pack) + pack.deferActivation = false if options.activateImmediately + pack.activate() pack # Specs rely on TextMate bundles (but not atom packages) @@ -120,6 +124,23 @@ jasmine.unspy = (object, methodName) -> throw new Error("Not a spy") unless object[methodName].originalValue? object[methodName] = object[methodName].originalValue +addCustomMatchers = (spec) -> + spec.addMatchers + toBeInstanceOf: (expected) -> + notText = if @isNot then " not" else "" + this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class" + @actual instanceof expected + + toHaveLength: (expected) -> + notText = if @isNot then " not" else "" + this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}" + @actual.length == expected + + toExistOnDisk: (expected) -> + notText = this.isNot and " not" or "" + @message = -> return "Expected path '" + @actual + "'" + notText + " to exist." + fs.exists(@actual) + window.keyIdentifierForKey = (key) -> if key.length > 1 # named key key diff --git a/spec/stdlib/fs-utils-spec.coffee b/spec/stdlib/fs-utils-spec.coffee index ddd1f2e63..cc82a3e66 100644 --- a/spec/stdlib/fs-utils-spec.coffee +++ b/spec/stdlib/fs-utils-spec.coffee @@ -86,7 +86,7 @@ describe "fs", -> it "calls fn for every path in the tree at the given path", -> paths = [] onPath = (path) -> - paths.push(fs.join(fixturesDir, path)) + paths.push(path) true fs.traverseTreeSync fixturesDir, onPath, onPath expect(paths).toEqual fs.listTree(fixturesDir) @@ -106,14 +106,16 @@ describe "fs", -> expect(path).not.toMatch /\/dir\// it "returns entries if path is a symlink", -> + symlinkPath = fs.join(fixturesDir, 'symlink-to-dir') symlinkPaths = [] - onSymlinkPath = (path) -> symlinkPaths.push(path) + onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1)) + regularPath = fs.join(fixturesDir, 'dir') paths = [] - onPath = (path) -> paths.push(path) + onPath = (path) -> paths.push(path.substring(regularPath.length + 1)) - fs.traverseTreeSync(fs.join(fixturesDir, 'symlink-to-dir'), onSymlinkPath, onSymlinkPath) - fs.traverseTreeSync(fs.join(fixturesDir, 'dir'), onPath, onPath) + fs.traverseTreeSync(symlinkPath, onSymlinkPath, onSymlinkPath) + fs.traverseTreeSync(regularPath, onPath, onPath) expect(symlinkPaths).toEqual(paths) diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee index 6b781370b..b803de4f1 100644 --- a/spec/stdlib/jquery-extensions-spec.coffee +++ b/spec/stdlib/jquery-extensions-spec.coffee @@ -1,4 +1,5 @@ $ = require 'jquery' +_ = require 'underscore' {View, $$} = require 'space-pen' describe 'jQuery extensions', -> @@ -41,7 +42,7 @@ describe 'jQuery extensions', -> element.trigger 'foo' expect(events).toEqual [2,1,3] - describe "$.fn.events() and $.fn.document", -> + describe "$.fn.events() and $.fn.document(...)", -> it "returns a list of all events being listened for on the target node or its ancestors, along with their documentation string", -> view = $$ -> @div id: 'a', => @@ -49,20 +50,18 @@ describe 'jQuery extensions', -> @div id: 'c' @div id: 'd' - view.document - 'a1': "This is event A2" - 'b2': "This is event b2" + view.document 'a1', "This is event A2" + view.document 'b2', "This is event b2" - view.document 'a1': "A1: Waste perfectly-good steak" + view.document 'a1', "A1: Waste perfectly-good steak" view.on 'a1', -> view.on 'a2', -> view.on 'b1', -> # should not appear as a duplicate divB = view.find('#b') - divB.document - 'b1': "B1: Super-sonic bomber" - 'b2': "B2: Looks evil. Kinda is." + divB.document 'b1', "B1: Super-sonic bomber" + divB.document 'b2', "B2: Looks evil. Kinda is." divB.on 'b1', -> divB.on 'b2', -> @@ -76,6 +75,80 @@ describe 'jQuery extensions', -> 'a1': "A1: Waste perfectly-good steak" 'a2': null + describe "$.fn.command(eventName, [selector, options,] handler)", -> + [view, handler] = [] + + beforeEach -> + view = $$ -> + @div class: 'a', => + @div class: 'b' + @div class: 'c' + handler = jasmine.createSpy("commandHandler") + + it "binds the handler to the given event / selector for all argument combinations", -> + view.command 'test:foo', handler + view.trigger 'test:foo' + expect(handler).toHaveBeenCalled() + handler.reset() + + view.command 'test:bar', '.b', handler + view.find('.b').trigger 'test:bar' + view.find('.c').trigger 'test:bar' + expect(handler.callCount).toBe 1 + handler.reset() + + view.command 'test:baz', doc: 'Spaz', handler + view.trigger 'test:baz' + expect(handler).toHaveBeenCalled() + handler.reset() + + view.command 'test:quux', '.c', doc: 'Lorem', handler + view.find('.b').trigger 'test:quux' + view.find('.c').trigger 'test:quux' + expect(handler.callCount).toBe 1 + + it "passes the 'data' option through when binding the event handler", -> + view.command 'test:foo', data: "bar", handler + view.trigger 'test:foo' + expect(handler.argsForCall[0][0].data).toBe 'bar' + + it "sets a custom docstring if the 'doc' option is specified", -> + view.command 'test:foo', doc: "Foo!", handler + expect(view.events()).toEqual 'test:foo': 'Test: Foo!' + + it "capitalizes the 'github' prefix how we like it", -> + view.command 'github:spelling', handler + expect(view.events()).toEqual 'github:spelling': 'GitHub: Spelling' + + describe "$.fn.scrollUp/Down/ToTop/ToBottom", -> + it "scrolls the element in the specified way if possible", -> + view = $$ -> @div => _.times 20, => @div('A') + view.css(height: 100, width: 100, overflow: 'scroll') + view.attachToDom() + + view.scrollUp() + expect(view.scrollTop()).toBe 0 + + view.scrollDown() + expect(view.scrollTop()).toBeGreaterThan 0 + previousScrollTop = view.scrollTop() + view.scrollDown() + expect(view.scrollTop()).toBeGreaterThan previousScrollTop + + view.scrollToBottom() + expect(view.scrollTop()).toBe view.prop('scrollHeight') - 100 + previousScrollTop = view.scrollTop() + view.scrollDown() + expect(view.scrollTop()).toBe previousScrollTop + view.scrollUp() + expect(view.scrollTop()).toBeLessThan previousScrollTop + previousScrollTop = view.scrollTop() + view.scrollUp() + expect(view.scrollTop()).toBeLessThan previousScrollTop + + view.scrollToTop() + expect(view.scrollTop()).toBe 0 + describe "Event.prototype", -> class GrandchildView extends View @content: -> @div class: 'grandchild' diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee index d528acde1..162f4d7e5 100644 --- a/src/app/atom-package.coffee +++ b/src/app/atom-package.coffee @@ -7,70 +7,22 @@ CSON = require 'cson' module.exports = class AtomPackage extends Package metadata: null - packageMain: null + mainModule: null + deferActivation: false - load: ({activateImmediately}={}) -> + load: -> try @loadMetadata() @loadKeymaps() @loadStylesheets() - if @metadata.activationEvents and not activateImmediately - @subscribeToActivationEvents() + if @deferActivation = @metadata.activationEvents? + @registerDeferredDeserializers() else - @activatePackageMain() + @requireMainModule() catch e console.warn "Failed to load package named '#{@name}'", e.stack this - disableEventHandlersOnBubblePath: (event) -> - bubblePathEventHandlers = [] - disabledHandler = -> - element = $(event.target) - while element.length - if eventHandlers = element.data('events')?[event.type] - for eventHandler in eventHandlers - eventHandler.disabledHandler = eventHandler.handler - eventHandler.handler = disabledHandler - bubblePathEventHandlers.push(eventHandler) - element = element.parent() - bubblePathEventHandlers - - restoreEventHandlersOnBubblePath: (eventHandlers) -> - for eventHandler in eventHandlers - eventHandler.handler = eventHandler.disabledHandler - delete eventHandler.disabledHandler - - unsubscribeFromActivationEvents: (activateHandler) -> - if _.isArray(@metadata.activationEvents) - rootView.off(event, activateHandler) for event in @metadata.activationEvents - else - rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents - - subscribeToActivationEvents: () -> - activateHandler = (event) => - bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event) - @activatePackageMain() - $(event.target).trigger(event) - @restoreEventHandlersOnBubblePath(bubblePathEventHandlers) - @unsubscribeFromActivationEvents(activateHandler) - - if _.isArray(@metadata.activationEvents) - rootView.command(event, activateHandler) for event in @metadata.activationEvents - else - rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents - - activatePackageMain: -> - mainPath = @path - mainPath = fs.join(mainPath, @metadata.main) if @metadata.main - try - mainPath = require.resolve(mainPath) - catch e - return - if fs.isFile(mainPath) - @packageMain = require(mainPath) - config.setDefaults(@name, @packageMain.configDefaults) - atom.activateAtomPackage(this) - loadMetadata: -> if metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json']) @metadata = CSON.readObject(metadataPath) @@ -90,3 +42,68 @@ class AtomPackage extends Package stylesheetDirPath = fs.join(@path, 'stylesheets') for stylesheetPath in fs.list(stylesheetDirPath) requireStylesheet(stylesheetPath) + + activate: -> + if @deferActivation + @subscribeToActivationEvents() + else + try + if @requireMainModule() + config.setDefaults(@name, @mainModule.configDefaults) + atom.activateAtomPackage(this) + catch e + console.warn "Failed to activate package named '#{@name}'", e.stack + + requireMainModule: -> + return @mainModule if @mainModule + mainPath = + if @metadata.main + fs.join(@path, @metadata.main) + else + fs.join(@path, 'index') + mainPath = fs.resolveExtension(mainPath, ["", _.keys(require.extensions)...]) + @mainModule = require(mainPath) if fs.isFile(mainPath) + + registerDeferredDeserializers: -> + for deserializerName in @metadata.deferredDeserializers ? [] + registerDeferredDeserializer deserializerName, => @requireMainModule() + + subscribeToActivationEvents: () -> + return unless @metadata.activationEvents? + + activateHandler = (event) => + bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event) + @deferActivation = false + @activate() + $(event.target).trigger(event) + @restoreEventHandlersOnBubblePath(bubblePathEventHandlers) + @unsubscribeFromActivationEvents(activateHandler) + + if _.isArray(@metadata.activationEvents) + rootView.command(event, activateHandler) for event in @metadata.activationEvents + else + rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents + + unsubscribeFromActivationEvents: (activateHandler) -> + if _.isArray(@metadata.activationEvents) + rootView.off(event, activateHandler) for event in @metadata.activationEvents + else + rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents + + disableEventHandlersOnBubblePath: (event) -> + bubblePathEventHandlers = [] + disabledHandler = -> + element = $(event.target) + while element.length + if eventHandlers = element.data('events')?[event.type] + for eventHandler in eventHandlers + eventHandler.disabledHandler = eventHandler.handler + eventHandler.handler = disabledHandler + bubblePathEventHandlers.push(eventHandler) + element = element.parent() + bubblePathEventHandlers + + restoreEventHandlersOnBubblePath: (eventHandlers) -> + for eventHandler in eventHandlers + eventHandler.handler = eventHandler.disabledHandler + delete eventHandler.disabledHandler diff --git a/src/app/atom-theme.coffee b/src/app/atom-theme.coffee index 9c524eac7..06f765118 100644 --- a/src/app/atom-theme.coffee +++ b/src/app/atom-theme.coffee @@ -6,10 +6,10 @@ module.exports = class AtomTheme extends Theme loadStylesheet: (stylesheetPath)-> - @stylesheets[stylesheetPath] = fs.read(stylesheetPath) + @stylesheets[stylesheetPath] = window.loadStylesheet(stylesheetPath) load: -> - if fs.extension(@path) is '.css' + if fs.extension(@path) in ['.css', '.less'] @loadStylesheet(@path) else metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json']) @@ -18,6 +18,6 @@ class AtomTheme extends Theme if stylesheetNames @loadStylesheet(fs.join(@path, name)) for name in stylesheetNames else - @loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css']) + @loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css', '.less']) super diff --git a/src/app/atom.coffee b/src/app/atom.coffee index 4eaec8f73..d30762b55 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -24,10 +24,10 @@ _.extend atom, activateAtomPackage: (pack) -> @activatedAtomPackages.push(pack) - pack.packageMain.activate(@atomPackageStates[pack.name] ? {}) + pack.mainModule.activate(@atomPackageStates[pack.name] ? {}) deactivateAtomPackages: -> - pack.packageMain.deactivate?() for pack in @activatedAtomPackages + pack.mainModule.deactivate?() for pack in @activatedAtomPackages @activatedAtomPackages = [] serializeAtomPackages: -> @@ -35,7 +35,7 @@ _.extend atom, for pack in @loadedPackages if pack in @activatedAtomPackages try - packageStates[pack.name] = pack.packageMain.serialize?() + packageStates[pack.name] = pack.mainModule.serialize?() catch e console?.error("Exception serializing '#{pack.name}' package's module\n", e.stack) else @@ -61,6 +61,9 @@ _.extend atom, new LoadTextMatePackagesTask(textMatePackages).start() if textMatePackages.length > 0 + activatePackages: -> + pack.activate() for pack in @loadedPackages + getLoadedPackages: -> _.clone(@loadedPackages) diff --git a/src/app/config.coffee b/src/app/config.coffee index e816dc6de..c8d36db6e 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -19,6 +19,7 @@ class Config userPackagesDirPath: userPackagesDirPath defaultSettings: null settings: null + configFileHasErrors: null constructor: -> @defaultSettings = @@ -34,18 +35,17 @@ class Config fs.makeDirectory(@configDirPath) templateConfigDirPath = fs.resolve(window.resourcePath, 'dot-atom') - onConfigDirFile = (path) => - templatePath = fs.join(templateConfigDirPath, path) - configPath = fs.join(@configDirPath, path) - fs.write(configPath, fs.read(templatePath)) + relativePath = path.substring(templateConfigDirPath.length + 1) + configPath = fs.join(@configDirPath, relativePath) + fs.write(configPath, fs.read(path)) fs.traverseTreeSync(templateConfigDirPath, onConfigDirFile, (path) -> true) configThemeDirPath = fs.join(@configDirPath, 'themes') onThemeDirFile = (path) -> - templatePath = fs.join(bundledThemesDirPath, path) - configPath = fs.join(configThemeDirPath, path) - fs.write(configPath, fs.read(templatePath)) + relativePath = path.substring(bundledThemesDirPath.length + 1) + configPath = fs.join(configThemeDirPath, relativePath) + fs.write(configPath, fs.read(path)) fs.traverseTreeSync(bundledThemesDirPath, onThemeDirFile, (path) -> true) load: -> @@ -54,8 +54,13 @@ class Config loadUserConfig: -> if fs.exists(@configFilePath) - userConfig = CSON.readObject(@configFilePath) - _.extend(@settings, userConfig) + try + userConfig = CSON.readObject(@configFilePath) + _.extend(@settings, userConfig) + catch e + @configFileHasErrors = true + console.error "Failed to load user config '#{@configFilePath}'", e.message + console.error e.stack get: (keyPath) -> _.valueForKeyPath(@settings, keyPath) ? @@ -91,6 +96,7 @@ class Config subscription update: -> + return if @configFileHasErrors @save() @trigger 'updated' diff --git a/src/app/editor.coffee b/src/app/editor.coffee index c82b7fb79..88f66b6e4 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -61,7 +61,7 @@ class Editor extends View else {editSession, @mini} = (editSessionOrOptions ? {}) - requireStylesheet 'editor.css' + requireStylesheet 'editor.less' @id = Editor.nextEditorId++ @lineCache = [] diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 6fc367e56..2118af2b0 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -9,6 +9,7 @@ class PaneContainer extends View @deserialize: ({root}) -> container = new PaneContainer container.append(deserialize(root)) if root + container.removeEmptyPanes() container @content: -> @@ -61,6 +62,23 @@ class PaneContainer extends View saveAll: -> pane.saveItems() for pane in @getPanes() + confirmClose: -> + deferred = $.Deferred() + modifiedItems = [] + for pane in @getPanes() + modifiedItems.push(item) for item in pane.getItems() when item.isModified?() + + cancel = => deferred.reject() + saveNextModifiedItem = => + if modifiedItems.length == 0 + deferred.resolve() + else + item = modifiedItems.pop() + @paneAtIndex(0).promptToSaveItem item, saveNextModifiedItem, cancel + + saveNextModifiedItem() + deferred.promise() + getPanes: -> @find('.pane').views() @@ -93,5 +111,9 @@ class PaneContainer extends View root.css(width: '100%', height: '100%', top: 0, left: 0) root.adjustDimensions() + removeEmptyPanes: -> + for pane in @getPanes() when pane.getItems().length == 0 + pane.remove() + afterAttach: -> @adjustPaneDimensions() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index ef29ce72f..3bd9c9f4c 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -11,7 +11,8 @@ class Pane extends View @div class: 'item-views', outlet: 'itemViews' @deserialize: ({items, focused, activeItemUri}) -> - pane = new Pane(items.map((item) -> deserialize(item))...) + deserializedItems = _.compact(items.map((item) -> deserialize(item))) + pane = new Pane(deserializedItems...) pane.showItemForUri(activeItemUri) if activeItemUri pane.focusOnAttach = true if focused pane @@ -21,7 +22,7 @@ class Pane extends View initialize: (@items...) -> @viewsByClassName = {} - @showItem(@items[0]) + @showItem(@items[0]) if @items.length > 0 @command 'core:close', @destroyActiveItem @command 'core:save', @saveActiveItem @@ -46,7 +47,7 @@ class Pane extends View @command 'pane:split-down', => @splitDown() @command 'pane:close', => @destroyItems() @command 'pane:close-other-items', => @destroyInactiveItems() - @on 'focus', => @activeView.focus(); false + @on 'focus', => @activeView?.focus(); false @on 'focusin', => @makeActive() @on 'focusout', => @autosaveActiveItem() @@ -72,6 +73,12 @@ class Pane extends View isActive: -> @hasClass('active') + getNextPane: -> + panes = @getContainer()?.getPanes() + return unless panes.length > 1 + nextIndex = (panes.indexOf(this) + 1) % panes.length + panes[nextIndex] + getItems: -> new Array(@items...) @@ -152,13 +159,13 @@ class Pane extends View destroyInactiveItems: -> @destroyItem(item) for item in @getItems() when item isnt @activeItem - promptToSaveItem: (item, nextAction) -> + promptToSaveItem: (item, nextAction, cancelAction) -> uri = item.getUri() atom.confirm( - "'#{item.getTitle()}' has changes, do you want to save them?" + "'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?" "Your changes will be lost if close this item without saving." "Save", => @saveItem(item, nextAction) - "Cancel", null + "Cancel", cancelAction "Don't Save", nextAction ) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index f25e156ad..5a9f35a3f 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -75,6 +75,9 @@ class RootView extends View panes: @panes.serialize() packages: atom.serializeAtomPackages() + confirmClose: -> + @panes.confirmClose() + handleFocus: (e) -> if @getActivePane() @getActivePane().focus() @@ -115,7 +118,7 @@ class RootView extends View updateTitle: -> if projectPath = project.getPath() if item = @getActivePaneItem() - @setTitle("#{item.getTitle()} - #{projectPath}") + @setTitle("#{item.getTitle?() ? 'untitled'} - #{projectPath}") else @setTitle(projectPath) else diff --git a/src/app/select-list.coffee b/src/app/select-list.coffee index 297c3fa35..e2441deff 100644 --- a/src/app/select-list.coffee +++ b/src/app/select-list.coffee @@ -20,7 +20,7 @@ class SelectList extends View cancelling: false initialize: -> - requireStylesheet 'select-list.css' + requireStylesheet 'select-list.less' @miniEditor.getBuffer().on 'changed', => @schedulePopulateList() @miniEditor.on 'focusout', => @cancel() unless @cancelling diff --git a/src/app/text-mate-package.coffee b/src/app/text-mate-package.coffee index 107637723..cfad66e04 100644 --- a/src/app/text-mate-package.coffee +++ b/src/app/text-mate-package.coffee @@ -32,6 +32,8 @@ class TextMatePackage extends Package console.warn "Failed to load package at '#{@path}'", e.stack this + activate: -> # no-op + getGrammars: -> @grammars readGrammars: -> diff --git a/src/app/theme.coffee b/src/app/theme.coffee index 7b20df793..6ca385d66 100644 --- a/src/app/theme.coffee +++ b/src/app/theme.coffee @@ -11,7 +11,7 @@ class Theme if fs.exists(name) path = name else - path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css']) + path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css', 'less']) throw new Error("No theme exists named '#{name}'") unless path diff --git a/src/app/window.coffee b/src/app/window.coffee index e13007169..f2ad099c8 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -1,11 +1,13 @@ fs = require 'fs-utils' $ = require 'jquery' -ChildProcess = require 'child_process' +{less} = require 'less' +{spawn} = require 'child_process' require 'jquery-extensions' require 'underscore-extensions' require 'space-pen-extensions' deserializers = {} +deferredDeserializers = {} # This method is called in any window needing a general environment, including specs window.setUpEnvironment = -> @@ -23,17 +25,17 @@ window.setUpEnvironment = -> $(document).on 'keydown', keymap.handleKeyEvent keymap.bindDefaultKeys() - requireStylesheet 'reset.css' - requireStylesheet 'atom.css' - requireStylesheet 'tabs.css' - requireStylesheet 'tree-view.css' - requireStylesheet 'status-bar.css' - requireStylesheet 'command-panel.css' - requireStylesheet 'fuzzy-finder.css' - requireStylesheet 'overlay.css' - requireStylesheet 'popover-list.css' - requireStylesheet 'notification.css' - requireStylesheet 'markdown.css' + requireStylesheet 'reset.less' + requireStylesheet 'atom.less' + requireStylesheet 'tabs.less' + requireStylesheet 'tree-view.less' + requireStylesheet 'status-bar.less' + requireStylesheet 'command-panel.less' + requireStylesheet 'fuzzy-finder.less' + requireStylesheet 'overlay.less' + requireStylesheet 'popover-list.less' + requireStylesheet 'notification.less' + requireStylesheet 'markdown.less' if nativeStylesheetPath = require.resolve("#{platform}.css") requireStylesheet(nativeStylesheetPath) @@ -52,10 +54,11 @@ window.startup = -> handleWindowEvents() config.load() atom.loadTextPackage() - buildProjectAndRootView() keymap.loadBundledKeymaps() atom.loadThemes() atom.loadPackages() + buildProjectAndRootView() + atom.activatePackages() keymap.loadUserKeymaps() atom.requireUserInitScript() $(window).on 'beforeunload', -> shutdown(); false @@ -81,14 +84,13 @@ window.installAtomCommand = (commandPath) -> bundledCommandPath = fs.resolve(window.resourcePath, 'atom.sh') if bundledCommandPath? fs.write(commandPath, fs.read(bundledCommandPath)) - ChildProcess.exec("chmod u+x '#{commandPath}'") + spawn("chmod u+x '#{commandPath}'") window.handleWindowEvents = -> - $(window).on 'core:close', => window.close() - $(window).command 'window:close', => window.close() $(window).command 'window:toggle-full-screen', => atom.toggleFullScreen() $(window).on 'focus', -> $("body").removeClass('is-blurred') $(window).on 'blur', -> $("body").addClass('is-blurred') + $(window).command 'window:close', => confirmClose() window.buildProjectAndRootView = -> RootView = require 'root-view' @@ -115,10 +117,21 @@ window.stylesheetElementForId = (id) -> window.requireStylesheet = (path) -> if fullPath = require.resolve(path) - window.applyStylesheet(fullPath, fs.read(fullPath)) - unless fullPath + content = window.loadStylesheet(fullPath) + window.applyStylesheet(fullPath, content) + else + console.log "bad", path throw new Error("Could not find a file at path '#{path}'") +window.loadStylesheet = (path) -> + content = fs.read(path) + if fs.extension(path) == '.less' + (new less.Parser).parse content, (e, tree) -> + throw new Error(e.message, file, e.line) if e + content = tree.toCSS() + + content + window.removeStylesheet = (path) -> unless fullPath = require.resolve(path) throw new Error("Could not find a file at path '#{path}'") @@ -151,6 +164,9 @@ window.registerDeserializers = (args...) -> window.registerDeserializer = (klass) -> deserializers[klass.name] = klass +window.registerDeferredDeserializer = (name, fn) -> + deferredDeserializers[name] = fn + window.unregisterDeserializer = (klass) -> delete deserializers[klass.name] @@ -160,7 +176,11 @@ window.deserialize = (state) -> deserializer.deserialize(state) window.getDeserializer = (state) -> - deserializers[state?.deserializer] + name = state?.deserializer + if deferredDeserializers[name] + deferredDeserializers[name]() + delete deferredDeserializers[name] + deserializers[name] window.measure = (description, fn) -> start = new Date().getTime() @@ -175,3 +195,6 @@ window.profile = (description, fn) -> value = fn() console.profileEnd(description) value + +confirmClose = -> + rootView.confirmClose().done -> window.close() diff --git a/src/packages/autocomplete/stylesheets/autocomplete.css b/src/packages/autocomplete/stylesheets/autocomplete.less similarity index 100% rename from src/packages/autocomplete/stylesheets/autocomplete.css rename to src/packages/autocomplete/stylesheets/autocomplete.less diff --git a/src/packages/bracket-matcher/stylesheets/bracket-matcher.css b/src/packages/bracket-matcher/stylesheets/bracket-matcher.less similarity index 100% rename from src/packages/bracket-matcher/stylesheets/bracket-matcher.css rename to src/packages/bracket-matcher/stylesheets/bracket-matcher.less diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee index 4305f9ada..8feec78bd 100644 --- a/src/packages/command-logger/spec/command-logger-spec.coffee +++ b/src/packages/command-logger/spec/command-logger-spec.coffee @@ -7,7 +7,7 @@ describe "CommandLogger", -> beforeEach -> window.rootView = new RootView rootView.open('sample.js') - commandLogger = window.loadPackage('command-logger').packageMain + commandLogger = window.loadPackage('command-logger').mainModule commandLogger.eventLog = {} editor = rootView.getActiveView() diff --git a/src/packages/command-logger/stylesheets/command-logger.css b/src/packages/command-logger/stylesheets/command-logger.less similarity index 100% rename from src/packages/command-logger/stylesheets/command-logger.css rename to src/packages/command-logger/stylesheets/command-logger.less diff --git a/src/packages/command-panel/lib/command-panel-view.coffee b/src/packages/command-panel/lib/command-panel-view.coffee index 2430c7ae0..ff574caa6 100644 --- a/src/packages/command-panel/lib/command-panel-view.coffee +++ b/src/packages/command-panel/lib/command-panel-view.coffee @@ -115,7 +115,7 @@ class CommandPanelView extends View escapedCommand: -> @miniEditor.getText() - execute: (command=@escapedCommand())-> + execute: (command=@escapedCommand()) -> @loadingMessage.show() @errorMessages.empty() diff --git a/src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee b/src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee index 25acc5fbb..619378ed3 100644 --- a/src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee +++ b/src/packages/command-panel/lib/commands/select-all-matches-in-project.coffee @@ -16,7 +16,7 @@ class SelectAllMatchesInProject extends Command promise = project.scan @regex, ({path, range}) -> operations.push(new Operation( project: project - buffer: project.bufferForPath(path) + path: path bufferRange: range )) diff --git a/src/packages/command-panel/lib/operation-view.coffee b/src/packages/command-panel/lib/operation-view.coffee index 297ef93b6..666aa8836 100644 --- a/src/packages/command-panel/lib/operation-view.coffee +++ b/src/packages/command-panel/lib/operation-view.coffee @@ -4,7 +4,7 @@ module.exports = class OperationView extends View @content: ({operation} = {}) -> {prefix, suffix, match, range} = operation.preview() - @li 'data-index': operation.index, class: 'operation', => + @li class: 'operation', => @span range.start.row + 1, class: 'line-number' @span class: 'preview', => @span prefix diff --git a/src/packages/command-panel/lib/operation.coffee b/src/packages/command-panel/lib/operation.coffee index 52e45d544..9be9ad7f6 100644 --- a/src/packages/command-panel/lib/operation.coffee +++ b/src/packages/command-panel/lib/operation.coffee @@ -1,22 +1,30 @@ module.exports = class Operation - constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection, @errorMessage}) -> - @buffer.retain() - @marker = @buffer.markRange(bufferRange) + constructor: ({@project, @path, @buffer, @bufferRange, @newText, @preserveSelection, @errorMessage}) -> + if @buffer? + @buffer.retain() + @getMarker() + + getMarker: -> + @marker ?= @getBuffer().markRange(@bufferRange) + + getBuffer: -> + @buffer ?= @project.bufferForPath(@path).retain() getPath: -> - @project.relativize(@buffer.getPath()) + path = @path ? @getBuffer().getPath() + @project.relativize(path) getBufferRange: -> - @buffer.getMarkerRange(@marker) + @getBuffer().getMarkerRange(@getMarker()) execute: (editSession) -> - @buffer.change(@getBufferRange(), @newText) if @newText + @getBuffer().change(@getBufferRange(), @newText) if @newText @getBufferRange() unless @preserveSelection preview: -> - range = @buffer.getMarkerRange(@marker) - line = @buffer.lineForRow(range.start.row) + range = @getBuffer().getMarkerRange(@getMarker()) + line = @getBuffer().lineForRow(range.start.row) prefix = line[0...range.start.column] match = line[range.start.column...range.end.column] suffix = line[range.end.column..] @@ -24,5 +32,5 @@ class Operation {prefix, suffix, match, range} destroy: -> - @buffer.destroyMarker(@marker) - @buffer.release() + @buffer?.destroyMarker(@marker) if @marker? + @buffer?.release() diff --git a/src/packages/command-panel/lib/path-view.coffee b/src/packages/command-panel/lib/path-view.coffee index 21042bfb9..9561d019b 100644 --- a/src/packages/command-panel/lib/path-view.coffee +++ b/src/packages/command-panel/lib/path-view.coffee @@ -5,16 +5,14 @@ $ = require 'jquery' module.exports = class PathView extends View - @content: ({path, operations, previewList} = {}) -> + @content: ({path, previewList} = {}) -> classes = ['path'] classes.push('readme') if fs.isReadmePath(path) @li class: classes.join(' '), => @div outlet: 'pathDetails', class: 'path-details', => @span class: 'path-name', path - @span "(#{operations.length})", class: 'path-match-number' + @span outlet: 'description', class: 'path-match-number' @ul outlet: 'matches', class: 'matches', => - for operation in operations - @subview "operation#{operation.index}", new OperationView({operation, previewList}) initialize: ({@previewList}) -> @pathDetails.on 'mousedown', => @toggle(true) @@ -27,6 +25,10 @@ class PathView extends View @toggle(true) false + addOperation: (operation) -> + @matches.append new OperationView({operation, @previewList}) + @description.text("(#{@matches.find('li').length})") + isSelected: -> @hasClass('selected') or @find('.selected').length diff --git a/src/packages/command-panel/lib/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee index 06abb868e..7f70db89d 100644 --- a/src/packages/command-panel/lib/preview-list.coffee +++ b/src/packages/command-panel/lib/preview-list.coffee @@ -11,12 +11,17 @@ class PreviewList extends ScrollView @ol class: 'preview-list', tabindex: -1 operations: null + viewsForPath: null + pixelOverdraw: 100 + lastRenderedOperationIndex: null initialize: -> super @on 'core:move-down', => @selectNextOperation(); false @on 'core:move-up', => @selectPreviousOperation(); false + @on 'scroll', => + @renderOperations() if @scrollBottom() >= (@prop('scrollHeight')) @command 'command-panel:collapse-all', => @collapseAllPaths() @command 'command-panel:expand-all', => @expandAllPaths() @@ -25,6 +30,7 @@ class PreviewList extends ScrollView @children().each (index, element) -> $(element).view().expand() collapseAllPaths: -> + @renderOperations(renderAll: true) @children().each (index, element) -> $(element).view().collapse() destroy: -> @@ -35,23 +41,31 @@ class PreviewList extends ScrollView populate: (operations) -> @destroyOperations() if @operations @operations = operations + @lastRenderedOperationIndex = 0 @empty() - - operation.index = index for operation, index in operations - operationsByPath = _.groupBy(operations, (operation) -> operation.getPath()) - for path, operations of operationsByPath - @append new PathView({path, operations, previewList: this}) + @viewsForPath = {} @show() - @find('.operation:first').addClass('selected') - @setLineNumberWidth() + @renderOperations() - setLineNumberWidth: -> - lineNumbers = @find('.line-number') - maxWidth = 0 - lineNumbers.each (index, element) -> - maxWidth = Math.max($(element).outerWidth(), maxWidth) - lineNumbers.width(maxWidth) + @find('.operation:first').addClass('selected') + + renderOperations: ({renderAll}={}) -> + renderAll ?= false + startingScrollHeight = @prop('scrollHeight') + for operation in @operations[@lastRenderedOperationIndex..] + pathView = @pathViewForPath(operation.getPath()) + pathView.addOperation(operation) + @lastRenderedOperationIndex++ + break if not renderAll and @prop('scrollHeight') >= startingScrollHeight + @pixelOverdraw and @prop('scrollHeight') > @height() + @pixelOverdraw + + pathViewForPath: (path) -> + pathView = @viewsForPath[path] + if not pathView + pathView = new PathView({path: path, previewList: this}) + @viewsForPath[path] = pathView + @append(pathView) + pathView selectNextOperation: -> selectedView = @find('.selected').view() diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index 213d14191..c6d9f7997 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -11,7 +11,7 @@ describe "CommandPanel", -> rootView.enableKeymap() editSession = rootView.getActivePaneItem() buffer = editSession.buffer - commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).packageMain + commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).mainModule commandPanel = commandPanelMain.commandPanelView commandPanel.history = [] commandPanel.historyIndex = 0 diff --git a/src/packages/command-panel/spec/preview-list-spec.coffee b/src/packages/command-panel/spec/preview-list-spec.coffee new file mode 100644 index 000000000..4fecb0f2d --- /dev/null +++ b/src/packages/command-panel/spec/preview-list-spec.coffee @@ -0,0 +1,45 @@ +RootView = require 'root-view' +CommandPanelView = require 'command-panel/lib/command-panel-view' +_ = require 'underscore' + +describe "Preview List", -> + [previewList, commandPanelMain, commandPanelView] = [] + + beforeEach -> + window.rootView = new RootView() + rootView.attachToDom() + commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).mainModule + commandPanelView = commandPanelMain.commandPanelView + previewList = commandPanelView.previewList + rootView.trigger 'command-panel:toggle' + + describe "when the list is scrollable", -> + it "adds more operations to the DOM when `scrollBottom` nears the `pixelOverdraw`", -> + waitsForPromise -> + commandPanelView.execute('X x/so/') + + runs -> + expect(previewList.prop('scrollHeight')).toBeGreaterThan previewList.height() + previousScrollHeight = previewList.prop('scrollHeight') + previousOperationCount = previewList.find("li").length + + previewList.scrollTop(previewList.pixelOverdraw / 2) + previewList.trigger('scroll') # Not sure why scroll event isn't being triggered on it's own + expect(previewList.prop('scrollHeight')).toBe previousScrollHeight + expect(previewList.find("li").length).toBe previousOperationCount + + previewList.scrollToBottom() + previewList.trigger('scroll') # Not sure why scroll event isn't being triggered on it's own + expect(previewList.prop('scrollHeight')).toBeGreaterThan previousScrollHeight + expect(previewList.find("li").length).toBeGreaterThan previousOperationCount + + it "renders all operations if the preview items are collapsed", -> + waitsForPromise -> + commandPanelView.execute('X x/so/') + + runs -> + expect(previewList.prop('scrollHeight')).toBeGreaterThan previewList.height() + previousScrollHeight = previewList.prop('scrollHeight') + previousOperationCount = previewList.find("li").length + previewList.collapseAllPaths() + expect(previewList.find("li").length).toBeGreaterThan previousOperationCount diff --git a/src/packages/editor-stats/spec/editor-stats-spec.coffee b/src/packages/editor-stats/spec/editor-stats-spec.coffee index 5d8708e42..9ce2360a2 100644 --- a/src/packages/editor-stats/spec/editor-stats-spec.coffee +++ b/src/packages/editor-stats/spec/editor-stats-spec.coffee @@ -17,7 +17,7 @@ describe "EditorStats", -> beforeEach -> window.rootView = new RootView rootView.open('sample.js') - editorStats = window.loadPackage('editor-stats').packageMain.stats + editorStats = window.loadPackage('editor-stats').mainModule.stats describe "when a keyup event is triggered", -> beforeEach -> diff --git a/src/packages/editor-stats/stylesheets/editor-stats.css b/src/packages/editor-stats/stylesheets/editor-stats.css deleted file mode 100644 index a7d5e3ff2..000000000 --- a/src/packages/editor-stats/stylesheets/editor-stats.css +++ /dev/null @@ -1,45 +0,0 @@ -.editor-stats-wrapper { - padding: 5px; - box-sizing: border-box; - border-top: 1px solid rgba(255, 255, 255, 0.05); - z-index: 9999; -} - -.editor-stats { - height: 50px; - width: 100%; - background: #1d1f21; - border: 1px solid rgba(0, 0, 0, 0.3); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - border-right: 1px solid rgba(255, 255, 255, 0.1); -} - -.editor-stats rect.bar { - fill: rgba(255, 255, 255, 0.2); - shape-rendering: crispedges; -} - -.editor-stats rect.bar.max { - fill: rgba(0, 163, 255, 1); -} - -.editor-stats text { - font-size: 10px; - fill: rgba(255, 255, 255, 0.2); - font-family: Courier; -} - -.editor-stats .minor text { - display: none; -} - -.editor-stats line { - stroke: #ccc; - stroke-opacity: 0.05; - stroke-width: 1px; - shape-rendering: crispedges; -} - -.editor-stats path.domain { - fill: none; -} diff --git a/src/packages/editor-stats/stylesheets/editor-stats.less b/src/packages/editor-stats/stylesheets/editor-stats.less new file mode 100644 index 000000000..26efaafde --- /dev/null +++ b/src/packages/editor-stats/stylesheets/editor-stats.less @@ -0,0 +1,45 @@ +.editor-stats-wrapper { + padding: 5px; + box-sizing: border-box; + border-top: 1px solid rgba(255, 255, 255, 0.05); + z-index: 9999; +} + +.editor-stats { + height: 50px; + width: 100%; + background: #1d1f21; + border: 1px solid rgba(0, 0, 0, 0.3); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-right: 1px solid rgba(255, 255, 255, 0.1); + + .bar { + fill: rgba(255, 255, 255, 0.2); + shape-rendering: crispedges; + + &.max { + fill: rgba(0, 163, 255, 1); + } + } + + text { + font-size: 10px; + fill: rgba(255, 255, 255, 0.2); + font-family: Courier; + } + + .minor text { + display: none; + } + + line { + stroke: #ccc; + stroke-opacity: 0.05; + stroke-width: 1px; + shape-rendering: crispedges; + } + + path.domain { + display: none; + } +} diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee index c6fbe92aa..d90ff497d 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee @@ -35,9 +35,10 @@ module.exports = createView: -> unless @fuzzyFinderView + @loadPathsTask?.abort() FuzzyFinderView = require 'fuzzy-finder/lib/fuzzy-finder-view' @fuzzyFinderView = new FuzzyFinderView() - if @projectPaths? and not @fuzzyFinderView.projectPaths? + if @projectPaths?.length > 0 and not @fuzzyFinderView.projectPaths? @fuzzyFinderView.projectPaths = @projectPaths @fuzzyFinderView.reloadProjectPaths = false @fuzzyFinderView diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index d512d0b0e..444164441 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -13,7 +13,7 @@ describe 'FuzzyFinder', -> window.rootView = new RootView rootView.open('sample.js') rootView.enableKeymap() - finderView = window.loadPackage("fuzzy-finder").packageMain.createView() + finderView = window.loadPackage("fuzzy-finder").mainModule.createView() describe "file-finder behavior", -> describe "toggling", -> diff --git a/src/packages/markdown-preview/keymaps/markdown-preview.cson b/src/packages/markdown-preview/keymaps/markdown-preview.cson index d98f00093..af52d2f0c 100644 --- a/src/packages/markdown-preview/keymaps/markdown-preview.cson +++ b/src/packages/markdown-preview/keymaps/markdown-preview.cson @@ -1,5 +1,2 @@ '.editor': - 'ctrl-m': 'markdown-preview:toggle' - -'.markdown-preview': - 'ctrl-m': 'markdown-preview:toggle' + 'ctrl-m': 'markdown-preview:show' diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 580704007..3d0d3859d 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -1,49 +1,36 @@ -ScrollView = require 'scroll-view' fs = require 'fs-utils' $ = require 'jquery' +ScrollView = require 'scroll-view' {$$$} = require 'space-pen' module.exports = class MarkdownPreviewView extends ScrollView - @activate: -> - @instance = new MarkdownPreviewView + registerDeserializer(this) + + @deserialize: ({path}) -> + new MarkdownPreviewView(project.bufferForPath(path)) @content: -> - @div class: 'markdown-preview', tabindex: -1, => - @div class: 'markdown-body', outlet: 'markdownBody' + @div class: 'markdown-preview', tabindex: -1 - initialize: -> + initialize: (@buffer) -> super + @fetchRenderedMarkdown() + @on 'core:move-up', => @scrollUp() + @on 'core:move-down', => @scrollDown() - rootView.command 'markdown-preview:toggle', => @toggle() - @on 'blur', => @detach() unless document.activeElement is this[0] - @command 'core:cancel', => @detach() + serialize: -> + deserializer: 'MarkdownPreviewView' + path: @buffer.getPath() - toggle: -> - if @hasParent() - @detach() - else - @attach() + getTitle: -> + "Markdown Preview – #{@buffer.getBaseName()}" - attach: -> - return unless @isMarkdownEditor() - rootView.append(this) - @markdownBody.html(@getLoadingHtml()) - @loadHtml() - @focus() + getUri: -> + "markdown-preview:#{@buffer.getPath()}" - detach: -> - return if @detaching - @detaching = true - super - rootView.focus() - @detaching = false - - getActiveText: -> - rootView.getActiveView()?.getText() - - getErrorHtml: (error) -> - $$$ -> + setErrorHtml: -> + @html $$$ -> @h2 'Previewing Markdown Failed' @h3 'Possible Reasons' @ul => @@ -52,29 +39,18 @@ class MarkdownPreviewView extends ScrollView @a 'github.com', href: 'https://github.com' @span '.' - getLoadingHtml: -> - $$$ -> - @div class: 'markdown-spinner', 'Loading Markdown...' + setLoading: -> + @html($$$ -> @div class: 'markdown-spinner', 'Loading Markdown...') - loadHtml: (text) -> - payload = - mode: 'markdown' - text: @getActiveText() - request = + fetchRenderedMarkdown: (text) -> + @setLoading() + $.ajax url: 'https://api.github.com/markdown' type: 'POST' dataType: 'html' contentType: 'application/json; charset=UTF-8' - data: JSON.stringify(payload) - success: (html) => @setHtml(html) - error: (jqXhr, error) => @setHtml(@getErrorHtml(error)) - $.ajax(request) - - setHtml: (html) -> - @markdownBody.html(html) if @hasParent() - - isMarkdownEditor: (path) -> - editor = rootView.getActiveView() - return unless editor? - return true if editor.getGrammar().scopeName is 'source.gfm' - path and fs.isMarkdownExtension(fs.extension(path)) + data: JSON.stringify + mode: 'markdown' + text: @buffer.getText() + success: (html) => @html(html) + error: => @setErrorHtml() diff --git a/src/packages/markdown-preview/lib/markdown-preview.coffee b/src/packages/markdown-preview/lib/markdown-preview.coffee new file mode 100644 index 000000000..b30bbb621 --- /dev/null +++ b/src/packages/markdown-preview/lib/markdown-preview.coffee @@ -0,0 +1,25 @@ +EditSession = require 'edit-session' +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' + +module.exports = + activate: -> + rootView.command 'markdown-preview:show', '.editor', => @show() + + show: -> + activePane = rootView.getActivePane() + item = activePane.activeItem + + if not item instanceof EditSession + console.warn("Can not render markdown for #{item.getUri()}") + return + + editSession = item + if nextPane = activePane.getNextPane() + if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}") + nextPane.showItem(preview) + preview.fetchRenderedMarkdown() + else + nextPane.showItem(new MarkdownPreviewView(editSession.buffer)) + else + activePane.splitRight(new MarkdownPreviewView(editSession.buffer)) + activePane.focus() \ No newline at end of file diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson index deea08f07..29925172d 100644 --- a/src/packages/markdown-preview/package.cson +++ b/src/packages/markdown-preview/package.cson @@ -1,3 +1,4 @@ -'main': 'lib/markdown-preview-view' +'main': 'lib/markdown-preview' 'activationEvents': - 'markdown-preview:toggle': '.editor' + 'markdown-preview:show': '.editor' +'deferredDeserializers': ['MarkdownPreviewView'] diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 7704768ee..9a7ed6e33 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -1,84 +1,67 @@ -$ = require 'jquery' RootView = require 'root-view' -MarkdownPreview = require 'markdown-preview/lib/markdown-preview-view' -_ = require 'underscore' +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' +{$$} = require 'space-pen' -describe "MarkdownPreview", -> +describe "MarkdownPreview package", -> beforeEach -> project.setPath(project.resolve('markdown')) window.rootView = new RootView - window.loadPackage("markdown-preview") - spyOn(MarkdownPreview.prototype, 'loadHtml') + window.loadPackage("markdown-preview", activateImmediately: true) + spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown') - describe "markdown-preview:toggle event", -> - it "toggles on/off a preview for a .md file", -> - rootView.open('file.md') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') + describe "markdown-preview:show", -> + beforeEach -> + rootView.open("file.markdown") - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(rootView.find('.markdown-preview')).toExist() - expect(markdownPreviewView.loadHtml).toHaveBeenCalled() - markdownPreviewView.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).not.toExist() + describe "when the active item is an edit session", -> + beforeEach -> + rootView.attachToDom() - it "displays a preview for a .markdown file", -> - rootView.open('file.markdown') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).toExist() - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView.loadHtml).toHaveBeenCalled() + describe "when a preview item has not been created for the edit session's uri", -> + describe "when there is more than one pane", -> + it "shows a markdown preview for the current buffer on the next pane", -> + rootView.getActivePane().splitRight() + [pane1, pane2] = rootView.getPanes() + pane1.focus() - it "displays a preview for a file with the source.gfm grammar scope", -> - gfmGrammar = _.find syntax.grammars, (grammar) -> grammar.scopeName is 'source.gfm' - rootView.open('file.js') - editor = rootView.getActiveView() - project.addGrammarOverrideForPath(editor.getPath(), gfmGrammar) - editor.reloadGrammar() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).toExist() - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView.loadHtml).toHaveBeenCalled() + rootView.getActiveView().trigger 'markdown-preview:show' - it "does not display a preview for non-markdown file", -> - rootView.open('file.js') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).not.toExist() - expect(MarkdownPreview.prototype.loadHtml).not.toHaveBeenCalled() + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + expect(preview.buffer).toBe rootView.getActivePaneItem().buffer + expect(pane1).toMatchSelector(':has(:focus)') - describe "core:cancel event", -> - it "removes markdown preview", -> - rootView.open('file.md') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') + describe "when there is only one pane", -> + it "splits the current pane to the right with a markdown preview for the current buffer", -> + expect(rootView.getPanes()).toHaveLength 1 - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView).toExist() - markdownPreviewView.trigger('core:cancel') - expect(rootView.find('.markdown-preview')).not.toExist() + rootView.getActiveView().trigger 'markdown-preview:show' - describe "when the editor receives focus", -> - it "removes the markdown preview view", -> - rootView.attachToDom() - rootView.open('file.md') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') + expect(rootView.getPanes()).toHaveLength 2 + [pane1, pane2] = rootView.getPanes() - markdownPreviewView = rootView.find('.markdown-preview') - editor.focus() - expect(markdownPreviewView).toExist() - expect(rootView.find('.markdown-preview')).not.toExist() + expect(pane2.items).toHaveLength 1 + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + expect(preview.buffer).toBe rootView.getActivePaneItem().buffer + expect(pane1).toMatchSelector(':has(:focus)') - describe "when no editor is open", -> - it "does not attach", -> - expect(rootView.getActiveView()).toBeFalsy() - rootView.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).not.toExist() + describe "when a preview item has already been created for the edit session's uri", -> + it "updates and shows the existing preview item if it isn't displayed", -> + rootView.getActiveView().trigger 'markdown-preview:show' + [pane1, pane2] = rootView.getPanes() + pane2.focus() + expect(rootView.getActivePane()).toBe pane2 + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + rootView.open() + expect(pane2.activeItem).not.toBe preview + pane1.focus() + + preview.fetchRenderedMarkdown.reset() + rootView.getActiveView().trigger 'markdown-preview:show' + expect(preview.fetchRenderedMarkdown).toHaveBeenCalled() + expect(rootView.getPanes()).toHaveLength 2 + expect(pane2.getItems()).toHaveLength 2 + expect(pane2.activeItem).toBe preview + expect(pane1).toMatchSelector(':has(:focus)') diff --git a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee new file mode 100644 index 000000000..42cac9b84 --- /dev/null +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee @@ -0,0 +1,39 @@ +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' +$ = require 'jquery' +{$$$} = require 'space-pen' + +describe "MarkdownPreviewView", -> + [buffer, preview] = [] + + beforeEach -> + spyOn($, 'ajax') + project.setPath(project.resolve('markdown')) + buffer = project.bufferForPath('file.markdown') + preview = new MarkdownPreviewView(buffer) + + afterEach -> + buffer.release() + + describe "on construction", -> + ajaxArgs = null + + beforeEach -> + ajaxArgs = $.ajax.argsForCall[0][0] + + it "shows a loading spinner and fetches the rendered markdown", -> + expect(preview.find('.markdown-spinner')).toExist() + expect($.ajax).toHaveBeenCalled() + + expect(JSON.parse(ajaxArgs.data).text).toBe buffer.getText() + + ajaxArgs.success($$$ -> @div "WWII", class: 'private-ryan') + expect(preview.find(".private-ryan")).toExist() + + it "shows an error message on error", -> + ajaxArgs.error() + expect(preview.text()).toContain "Failed" + + describe "serialization", -> + it "reassociates with the same buffer when deserialized", -> + newPreview = deserialize(preview.serialize()) + expect(newPreview.buffer).toBe buffer diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css deleted file mode 100644 index 1138dc1b7..000000000 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.css +++ /dev/null @@ -1,438 +0,0 @@ -.markdown-preview { - font-family: "Helvetica Neue", Helvetica, sans-serif; - font-size: 14px; - line-height: 1.6; - position: absolute; - width: 100%; - height: 100%; - top: 0px; - left: 0px; - background-color: #fff; - overflow: auto; - z-index: 3; - box-sizing: border-box; - padding: 20px; -} - -.markdown-body { - min-width: 680px; -} - -.markdown-body pre, -.markdown-body code, -.markdown-body tt { - font-size: 12px; - font-family: Consolas, "Liberation Mono", Courier, monospace; -} - -.markdown-body a { - color: #4183c4; -} - -.markdown-body ol > li { - list-style-type: decimal; -} - -.markdown-body ul > li { - list-style-type: disc; -} - -.markdown-spinner { - margin: auto; - background-image: url(images/octocat-spinner-128.gif); - background-repeat: no-repeat; - background-size: 64px; - background-position: top center; - padding-top: 70px; - text-align: center; -} - - -/* this code below was copied from https://github.com/assets/stylesheets/primer/components/markdown.css */ -/* we really need to get primer in here somehow. */ -.markdown-body { - font-size: 14px; - line-height: 1.6; - overflow: hidden; } - .markdown-body > *:first-child { - margin-top: 0 !important; } - .markdown-body > *:last-child { - margin-bottom: 0 !important; } - .markdown-body a.absent { - color: #c00; } - .markdown-body a.anchor { - display: block; - padding-left: 30px; - margin-left: -30px; - cursor: pointer; - position: absolute; - top: 0; - left: 0; - bottom: 0; } - .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { - margin: 20px 0 10px; - padding: 0; - font-weight: bold; - -webkit-font-smoothing: antialiased; - cursor: text; - position: relative; } - .markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { - display: none; - color: #000; } - .markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { - text-decoration: none; - line-height: 1; - padding-left: 0; - margin-left: -22px; - top: 15%; } - .markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link { - display: inline-block; } - .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { - font-size: inherit; } - .markdown-body h1 { - font-size: 28px; - color: #000; } - .markdown-body h2 { - font-size: 24px; - border-bottom: 1px solid #ccc; - color: #000; } - .markdown-body h3 { - font-size: 18px; } - .markdown-body h4 { - font-size: 16px; } - .markdown-body h5 { - font-size: 14px; } - .markdown-body h6 { - color: #777; - font-size: 14px; } - .markdown-body p, - .markdown-body blockquote, - .markdown-body ul, .markdown-body ol, .markdown-body dl, - .markdown-body table, - .markdown-body pre { - margin: 15px 0; } - .markdown-body hr { - background: transparent url("https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-0e7d81b119cc9beae17b0c98093d121fa0050a74.png") repeat-x 0 0; - border: 0 none; - color: #ccc; - height: 4px; - padding: 0; } - .markdown-body > h2:first-child, .markdown-body > h1:first-child, .markdown-body > h1:first-child + h2, .markdown-body > h3:first-child, .markdown-body > h4:first-child, .markdown-body > h5:first-child, .markdown-body > h6:first-child { - margin-top: 0; - padding-top: 0; } - .markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 { - margin-top: 0; - padding-top: 0; } - .markdown-body h1 + p, - .markdown-body h2 + p, - .markdown-body h3 + p, - .markdown-body h4 + p, - .markdown-body h5 + p, - .markdown-body h6 + p { - margin-top: 0; } - .markdown-body li p.first { - display: inline-block; } - .markdown-body ul, .markdown-body ol { - padding-left: 30px; } - .markdown-body ul.no-list, .markdown-body ol.no-list { - list-style-type: none; - padding: 0; } - .markdown-body ul li > :first-child, - .markdown-body ul li ul:first-of-type, .markdown-body ol li > :first-child, - .markdown-body ol li ul:first-of-type { - margin-top: 0px; } - .markdown-body ul ul, - .markdown-body ul ol, - .markdown-body ol ol, - .markdown-body ol ul { - margin-bottom: 0; } - .markdown-body dl { - padding: 0; } - .markdown-body dl dt { - font-size: 14px; - font-weight: bold; - font-style: italic; - padding: 0; - margin: 15px 0 5px; } - .markdown-body dl dt:first-child { - padding: 0; } - .markdown-body dl dt > :first-child { - margin-top: 0px; } - .markdown-body dl dt > :last-child { - margin-bottom: 0px; } - .markdown-body dl dd { - margin: 0 0 15px; - padding: 0 15px; } - .markdown-body dl dd > :first-child { - margin-top: 0px; } - .markdown-body dl dd > :last-child { - margin-bottom: 0px; } - .markdown-body blockquote { - border-left: 4px solid #DDD; - padding: 0 15px; - color: #777; } - .markdown-body blockquote > :first-child { - margin-top: 0px; } - .markdown-body blockquote > :last-child { - margin-bottom: 0px; } - .markdown-body table th { - font-weight: bold; } - .markdown-body table th, .markdown-body table td { - border: 1px solid #ccc; - padding: 6px 13px; } - .markdown-body table tr { - border-top: 1px solid #ccc; - background-color: #fff; } - .markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; } - .markdown-body img { - max-width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; } - .markdown-body span.frame { - display: block; - overflow: hidden; } - .markdown-body span.frame > span { - border: 1px solid #ddd; - display: block; - float: left; - overflow: hidden; - margin: 13px 0 0; - padding: 7px; - width: auto; } - .markdown-body span.frame span img { - display: block; - float: left; } - .markdown-body span.frame span span { - clear: both; - color: #333; - display: block; - padding: 5px 0 0; } - .markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; } - .markdown-body span.align-center > span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: center; } - .markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; } - .markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; } - .markdown-body span.align-right > span { - display: block; - overflow: hidden; - margin: 13px 0 0; - text-align: right; } - .markdown-body span.align-right span img { - margin: 0; - text-align: right; } - .markdown-body span.float-left { - display: block; - margin-right: 13px; - overflow: hidden; - float: left; } - .markdown-body span.float-left span { - margin: 13px 0 0; } - .markdown-body span.float-right { - display: block; - margin-left: 13px; - overflow: hidden; - float: right; } - .markdown-body span.float-right > span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: right; } - .markdown-body code, .markdown-body tt { - margin: 0 2px; - padding: 0px 5px; - border: 1px solid #eaeaea; - background-color: #f8f8f8; - border-radius: 3px; } - .markdown-body code { - white-space: nowrap; } - .markdown-body pre > code { - margin: 0; - padding: 0; - white-space: pre; - border: none; - background: transparent; } - .markdown-body .highlight pre, .markdown-body pre { - background-color: #f8f8f8; - border: 1px solid #ccc; - font-size: 13px; - line-height: 19px; - overflow: auto; - padding: 6px 10px; - border-radius: 3px; } - .markdown-body pre code, .markdown-body pre tt { - margin: 0; - padding: 0; - background-color: transparent; - border: none; } - -/* this code was copied from https://github.com/assets/stylesheets/primer/components/pygments.css */ -/* the .markdown-body class was then added to all rules */ -.markdown-body .highlight { - background: #ffffff; } - .markdown-body .highlight .c { - color: #999988; - font-style: italic; } - .markdown-body .highlight .err { - color: #a61717; - background-color: #e3d2d2; } - .markdown-body .highlight .k { - font-weight: bold; } - .markdown-body .highlight .o { - font-weight: bold; } - .markdown-body .highlight .cm { - color: #999988; - font-style: italic; } - .markdown-body .highlight .cp { - color: #999999; - font-weight: bold; } - .markdown-body .highlight .c1 { - color: #999988; - font-style: italic; } - .markdown-body .highlight .cs { - color: #999999; - font-weight: bold; - font-style: italic; } - .markdown-body .highlight .gd { - color: #000000; - background-color: #ffdddd; } - .markdown-body .highlight .gd .x { - color: #000000; - background-color: #ffaaaa; } - .markdown-body .highlight .ge { - font-style: italic; } - .markdown-body .highlight .gr { - color: #aa0000; } - .markdown-body .highlight .gh { - color: #999999; } - .markdown-body .highlight .gi { - color: #000000; - background-color: #ddffdd; } - .markdown-body .highlight .gi .x { - color: #000000; - background-color: #aaffaa; } - .markdown-body .highlight .go { - color: #888888; } - .markdown-body .highlight .gp { - color: #555555; } - .markdown-body .highlight .gs { - font-weight: bold; } - .markdown-body .highlight .gu { - color: #800080; - font-weight: bold; } - .markdown-body .highlight .gt { - color: #aa0000; } - .markdown-body .highlight .kc { - font-weight: bold; } - .markdown-body .highlight .kd { - font-weight: bold; } - .markdown-body .highlight .kn { - font-weight: bold; } - .markdown-body .highlight .kp { - font-weight: bold; } - .markdown-body .highlight .kr { - font-weight: bold; } - .markdown-body .highlight .kt { - color: #445588; - font-weight: bold; } - .markdown-body .highlight .m { - color: #009999; } - .markdown-body .highlight .s { - color: #d14; } - .markdown-body .highlight .na { - color: #008080; } - .markdown-body .highlight .nb { - color: #0086B3; } - .markdown-body .highlight .nc { - color: #445588; - font-weight: bold; } - .markdown-body .highlight .no { - color: #008080; } - .markdown-body .highlight .ni { - color: #800080; } - .markdown-body .highlight .ne { - color: #990000; - font-weight: bold; } - .markdown-body .highlight .nf { - color: #990000; - font-weight: bold; } - .markdown-body .highlight .nn { - color: #555555; } - .markdown-body .highlight .nt { - color: #000080; } - .markdown-body .highlight .nv { - color: #008080; } - .markdown-body .highlight .ow { - font-weight: bold; } - .markdown-body .highlight .w { - color: #bbbbbb; } - .markdown-body .highlight .mf { - color: #009999; } - .markdown-body .highlight .mh { - color: #009999; } - .markdown-body .highlight .mi { - color: #009999; } - .markdown-body .highlight .mo { - color: #009999; } - .markdown-body .highlight .sb { - color: #d14; } - .markdown-body .highlight .sc { - color: #d14; } - .markdown-body .highlight .sd { - color: #d14; } - .markdown-body .highlight .s2 { - color: #d14; } - .markdown-body .highlight .se { - color: #d14; } - .markdown-body .highlight .sh { - color: #d14; } - .markdown-body .highlight .si { - color: #d14; } - .markdown-body .highlight .sx { - color: #d14; } - .markdown-body .highlight .sr { - color: #009926; } - .markdown-body .highlight .s1 { - color: #d14; } - .markdown-body .highlight .ss { - color: #990073; } - .markdown-body .highlight .bp { - color: #999999; } - .markdown-body .highlight .vc { - color: #008080; } - .markdown-body .highlight .vg { - color: #008080; } - .markdown-body .highlight .vi { - color: #008080; } - .markdown-body .highlight .il { - color: #009999; } - .markdown-body .highlight .gc { - color: #999; - background-color: #EAF2F5; } - -.type-csharp .markdown-body .highlight .k { - color: #0000FF; } -.type-csharp .markdown-body .highlight .kt { - color: #0000FF; } -.type-csharp .markdown-body .highlight .nf { - color: #000000; - font-weight: normal; } -.type-csharp .markdown-body .highlight .nc { - color: #2B91AF; } -.type-csharp .markdown-body .highlight .nn { - color: #000000; } -.type-csharp .markdown-body .highlight .s { - color: #A31515; } -.type-csharp .markdown-body .highlight .sc { - color: #A31515; } diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.less b/src/packages/markdown-preview/stylesheets/markdown-preview.less new file mode 100644 index 000000000..608389a88 --- /dev/null +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.less @@ -0,0 +1,403 @@ +.markdown-preview { + font-family: "Helvetica Neue", Helvetica, sans-serif; + font-size: 14px; + line-height: 1.6; + background-color: #fff; + overflow: scroll; + box-sizing: border-box; + padding: 20px; +} + +.markdown-spinner { + margin: auto; + background-image: url(images/octocat-spinner-128.gif); + background-repeat: no-repeat; + background-size: 64px; + background-position: top center; + padding-top: 70px; + text-align: center; +} + +// This is styling for generic markdownized text. Anything you put in a +// container with .markdown-preview on it should render generally well. It also +// includes some GitHub Flavored Markdown specific styling (like @mentions) +.markdown-preview { + pre, + code, + tt { + font-size: 12px; + font-family: Consolas, "Liberation Mono", Courier, monospace; + } + + a { + color: #4183c4; + } + + ol > li { + list-style-type: decimal; + } + + ul > li { + list-style-type: disc; + } + + & > *:first-child { + margin-top: 0 !important; + } + + & > *:last-child { + margin-bottom: 0 !important; + } + + // Link Colors + a.absent { + color: #c00; + } + + a.anchor { + display: block; + padding-left: 30px; + margin-left: -30px; + cursor: pointer; + position: absolute; + top: 0; + left: 0; + bottom: 0; + } + + // Headings + h1, h2, h3, h4, h5, h6 { + margin: 20px 0 10px; + padding: 0; + font-weight: bold; + -webkit-font-smoothing: antialiased; + cursor: text; + position: relative; + + .mini-icon-link { + display: none; + color: #000; + } + + &:hover a.anchor { + text-decoration: none; + line-height: 1; + padding-left: 0; + margin-left: -22px; + top: 15%; + + .mini-icon-link { + display: inline-block; + } + } + tt, code { + font-size: inherit; + } + } + + h1 { + font-size: 28px; + color: #000; + } + + h2 { + font-size: 24px; + border-bottom: 1px solid #ccc; + color: #000; + } + + h3 { + font-size: 18px; + } + + h4 { + font-size: 16px; + } + + h5 { + font-size: 14px; + } + + h6 { + color: #777; + font-size: 14px; + } + + p, + blockquote, + ul, ol, dl, + table, + pre { + margin: 15px 0; + } + + hr { + background: transparent; + border: 0 none; + color: #ccc; + height: 4px; + padding: 0; + } + + & > h2:first-child, + & > h1:first-child, + & > h1:first-child + h2, + & > h3:first-child, + & > h4:first-child, + & > h5:first-child, + & > h6:first-child { + margin-top: 0; + padding-top: 0; + } + + // fixes margin on shit like: + // + //

The Heading

+ a:first-child { + h1, h2, h3, h4, h5, h6 { + margin-top: 0; + padding-top: 0; + } + } + + h1 + p, + h2 + p, + h3 + p, + h4 + p, + h5 + p, + h6 + p { + margin-top: 0; + } + + // ReST first graf in nested list + li p.first { + display: inline-block; + } + + // Lists, Blockquotes & Such + ul, ol { + padding-left: 30px; + + &.no-list { + list-style-type: none; + padding: 0; + } + + li > :first-child, + li ul:first-of-type { + margin-top: 0px; + } + } + + ul ul, + ul ol, + ol ol, + ol ul { + margin-bottom: 0; + } + + dl { + padding: 0; + } + + dl dt { + font-size: 14px; + font-weight: bold; + font-style: italic; + padding: 0; + margin: 15px 0 5px; + + &:first-child { + padding: 0; + } + & > :first-child { + margin-top: 0px; + } + + & > :last-child { + margin-bottom: 0px; + } + } + + dl dd { + margin: 0 0 15px; + padding: 0 15px; + & > :first-child { + margin-top: 0px; + } + + & > :last-child { + margin-bottom: 0px; + } + } + + blockquote { + border-left: 4px solid #DDD; + padding: 0 15px; + color: #777; + + & > :first-child { + margin-top: 0px; + } + + & > :last-child { + margin-bottom: 0px; + } + } + + // Tables + table { + + th { + font-weight: bold; + } + + th, td { + border: 1px solid #ccc; + padding: 6px 13px; + } + + tr { + border-top: 1px solid #ccc; + background-color: #fff; + + &:nth-child(2n) { + background-color: #f8f8f8; + } + } + } + + // Images & Stuff + img { + max-width: 100%; + @include box-sizing(); + } + + // Gollum Image Tags + + // Framed + span.frame { + display: block; + overflow: hidden; + + & > span { + border: 1px solid #ddd; + display: block; + float: left; + overflow: hidden; + margin: 13px 0 0; + padding: 7px; + width: auto; + } + + span img { + display: block; + float: left; + } + + span span { + clear: both; + color: #333; + display: block; + padding: 5px 0 0; + } + } + + span.align-center { + display: block; + overflow: hidden; + clear: both; + + & > span { + display: block; + overflow: hidden; + margin: 13px auto 0; + text-align: center; + } + + span img { + margin: 0 auto; + text-align: center; + } + } + + span.align-right { + display: block; + overflow: hidden; + clear: both; + + & > span { + display: block; + overflow: hidden; + margin: 13px 0 0; + text-align: right; + } + + span img { + margin: 0; + text-align: right; + } + } + + span.float-left { + display: block; + margin-right: 13px; + overflow: hidden; + float: left; + + span { + margin: 13px 0 0; + } + } + + span.float-right { + display: block; + margin-left: 13px; + overflow: hidden; + float: right; + + & > span { + display: block; + overflow: hidden; + margin: 13px auto 0; + text-align: right; + } + } + + // Inline code snippets + code, tt { + margin: 0 2px; + padding: 0px 5px; + border: 1px solid #eaeaea; + background-color: #f8f8f8; + border-radius:3px; + } + + code { white-space: nowrap; } + + // Code tags within code blocks (
s)
+  pre > code {
+    margin: 0;
+    padding: 0;
+    white-space: pre;
+    border: none;
+    background: transparent;
+  }
+
+  .highlight pre, pre {
+    background-color: #f8f8f8;
+    border: 1px solid #ccc;
+    font-size: 13px;
+    line-height: 19px;
+    overflow: auto;
+    padding: 6px 10px;
+    border-radius:3px;
+  }
+
+  pre code, pre tt {
+    margin: 0;
+    padding: 0;
+    background-color: transparent;
+    border: none;
+  }
+}
diff --git a/src/packages/markdown-preview/stylesheets/pygments.less b/src/packages/markdown-preview/stylesheets/pygments.less
new file mode 100644
index 000000000..f3faab07a
--- /dev/null
+++ b/src/packages/markdown-preview/stylesheets/pygments.less
@@ -0,0 +1,201 @@
+.highlight  {
+  background: #ffffff;
+
+  // Comment
+  .c { color: #999988; font-style: italic }
+
+  // Error
+  .err { color: #a61717; background-color: #e3d2d2 }
+
+  // Keyword
+  .k { font-weight: bold }
+
+  // Operator
+  .o { font-weight: bold }
+
+  // Comment.Multiline
+  .cm { color: #999988; font-style: italic }
+
+  // Comment.Preproc
+  .cp { color: #999999; font-weight: bold }
+
+  // Comment.Single
+  .c1 { color: #999988; font-style: italic }
+
+  // Comment.Special
+  .cs { color: #999999; font-weight: bold; font-style: italic }
+
+  // Generic.Deleted
+  .gd { color: #000000; background-color: #ffdddd }
+
+  // Generic.Deleted.Specific
+  .gd .x { color: #000000; background-color: #ffaaaa }
+
+  // Generic.Emph
+  .ge { font-style: italic }
+
+  // Generic.Error
+  .gr { color: #aa0000 }
+
+  // Generic.Heading
+  .gh { color: #999999 }
+
+  // Generic.Inserted
+  .gi { color: #000000; background-color: #ddffdd }
+
+  // Generic.Inserted.Specific
+  .gi .x { color: #000000; background-color: #aaffaa }
+
+  // Generic.Output
+  .go { color: #888888 }
+
+  // Generic.Prompt
+  .gp { color: #555555 }
+
+  // Generic.Strong
+  .gs { font-weight: bold }
+
+  // Generic.Subheading
+  .gu { color: #800080; font-weight: bold; }
+
+  // Generic.Traceback
+  .gt { color: #aa0000 }
+
+  // Keyword.Constant
+  .kc { font-weight: bold }
+
+  // Keyword.Declaration
+  .kd { font-weight: bold }
+
+  // Keyword.Namespace
+  .kn { font-weight: bold }
+
+  // Keyword.Pseudo
+  .kp { font-weight: bold }
+
+  // Keyword.Reserved
+  .kr { font-weight: bold }
+
+  // Keyword.Type
+  .kt { color: #445588; font-weight: bold }
+
+  // Literal.Number
+  .m { color: #009999 }
+
+  // Literal.String
+  .s { color: #d14 }
+
+  // Name
+  .n { color: #333333 }
+
+  // Name.Attribute
+  .na { color: #008080 }
+
+  // Name.Builtin
+  .nb { color: #0086B3 }
+
+  // Name.Class
+  .nc { color: #445588; font-weight: bold }
+
+  // Name.Constant
+  .no { color: #008080 }
+
+  // Name.Entity
+  .ni { color: #800080 }
+
+  // Name.Exception
+  .ne { color: #990000; font-weight: bold }
+
+  // Name.Function
+  .nf { color: #990000; font-weight: bold }
+
+  // Name.Namespace
+  .nn { color: #555555 }
+
+  // Name.Tag
+  .nt { color: #000080 }
+
+  // Name.Variable
+  .nv { color: #008080 }
+
+  // Operator.Word
+  .ow { font-weight: bold }
+
+  // Text.Whitespace
+  .w { color: #bbbbbb }
+
+  // Literal.Number.Float
+  .mf { color: #009999 }
+
+  // Literal.Number.Hex
+  .mh { color: #009999 }
+
+  // Literal.Number.Integer
+  .mi { color: #009999 }
+
+  // Literal.Number.Oct
+  .mo { color: #009999 }
+
+  // Literal.String.Backtick
+  .sb { color: #d14 }
+
+  // Literal.String.Char
+  .sc { color: #d14 }
+
+  // Literal.String.Doc
+  .sd { color: #d14 }
+
+  // Literal.String.Double
+  .s2 { color: #d14 }
+
+  // Literal.String.Escape
+  .se { color: #d14 }
+
+  // Literal.String.Heredoc
+  .sh { color: #d14 }
+
+  // Literal.String.Interpol
+  .si { color: #d14 }
+
+  // Literal.String.Other
+  .sx { color: #d14 }
+
+  // Literal.String.Regex
+  .sr { color: #009926 }
+
+  // Literal.String.Single
+  .s1 { color: #d14 }
+
+  // Literal.String.Symbol
+  .ss { color: #990073 }
+
+  // Name.Builtin.Pseudo
+  .bp { color: #999999 }
+
+  // Name.Variable.Class
+  .vc { color: #008080 }
+
+  // Name.Variable.Global
+  .vg { color: #008080 }
+
+  // Name.Variable.Instance
+  .vi { color: #008080 }
+
+  // Literal.Number.Integer.Long
+  .il { color: #009999 }
+
+  .gc {
+    color: #999;
+    background-color: #EAF2F5;
+  }
+}
+
+.type-csharp .highlight {
+  .k { color: #0000FF }
+  .kt { color: #0000FF }
+  .nf { color: #000000; font-weight: normal }
+  .nc { color: #2B91AF }
+  .nn { color: #000000 }
+  .s { color: #A31515 }
+  .sc { color: #A31515 }
+}
diff --git a/src/packages/package-generator/lib/package-generator-view.coffee b/src/packages/package-generator/lib/package-generator-view.coffee
index 1e6f957e9..63f07890f 100644
--- a/src/packages/package-generator/lib/package-generator-view.coffee
+++ b/src/packages/package-generator/lib/package-generator-view.coffee
@@ -62,6 +62,7 @@ class PackageGeneratorView extends View
     for path in fs.listTree(templatePath)
       relativePath = path.replace(templatePath, "")
       relativePath = relativePath.replace(/^\//, '')
+      relativePath = relativePath.replace(/\.template$/, '')
       relativePath = @replacePackageNamePlaceholders(relativePath, packageName)
 
       sourcePath = fs.join(@getPackagePath(), relativePath)
diff --git a/src/packages/package-generator/spec/package-generator-spec.coffee b/src/packages/package-generator/spec/package-generator-spec.coffee
index c1b57b255..5df9d71c6 100644
--- a/src/packages/package-generator/spec/package-generator-spec.coffee
+++ b/src/packages/package-generator/spec/package-generator-spec.coffee
@@ -37,12 +37,6 @@ describe 'Package Generator', ->
       packagePath = "/tmp/atom-packages/#{packageName}"
       fs.remove(packagePath) if fs.exists(packagePath)
 
-      @addMatchers
-        toExistOnDisk: (expected) ->
-          notText = this.isNot and " not" or ""
-          @message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
-          fs.exists(@actual)
-
     afterEach ->
       fs.remove(packagePath) if fs.exists(packagePath)
 
diff --git a/src/packages/package-generator/stylesheets/package-generator.css b/src/packages/package-generator/stylesheets/package-generator.less
similarity index 100%
rename from src/packages/package-generator/stylesheets/package-generator.css
rename to src/packages/package-generator/stylesheets/package-generator.less
diff --git a/src/packages/package-generator/template/keymaps/__package-name__.cson b/src/packages/package-generator/template/keymaps/__package-name__.cson.template
similarity index 100%
rename from src/packages/package-generator/template/keymaps/__package-name__.cson
rename to src/packages/package-generator/template/keymaps/__package-name__.cson.template
diff --git a/src/packages/package-generator/template/lib/__package-name__-view.coffee b/src/packages/package-generator/template/lib/__package-name__-view.coffee.template
similarity index 100%
rename from src/packages/package-generator/template/lib/__package-name__-view.coffee
rename to src/packages/package-generator/template/lib/__package-name__-view.coffee.template
diff --git a/src/packages/package-generator/template/lib/__package-name__.coffee b/src/packages/package-generator/template/lib/__package-name__.coffee.template
similarity index 100%
rename from src/packages/package-generator/template/lib/__package-name__.coffee
rename to src/packages/package-generator/template/lib/__package-name__.coffee.template
diff --git a/src/packages/package-generator/template/spec/__package-name__-spec.coffee b/src/packages/package-generator/template/spec/__package-name__-spec.coffee.template
similarity index 100%
rename from src/packages/package-generator/template/spec/__package-name__-spec.coffee
rename to src/packages/package-generator/template/spec/__package-name__-spec.coffee.template
diff --git a/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee b/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee.template
similarity index 100%
rename from src/packages/package-generator/template/spec/__package-name__-view-spec.coffee
rename to src/packages/package-generator/template/spec/__package-name__-view-spec.coffee.template
diff --git a/src/packages/package-generator/template/stylesheets/__package-name__.css b/src/packages/package-generator/template/stylesheets/__package-name__.css.template
similarity index 100%
rename from src/packages/package-generator/template/stylesheets/__package-name__.css
rename to src/packages/package-generator/template/stylesheets/__package-name__.css.template
diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee
index f2b4f4f3a..0756dbdeb 100644
--- a/src/packages/spell-check/spec/spell-check-spec.coffee
+++ b/src/packages/spell-check/spec/spell-check-spec.coffee
@@ -7,7 +7,7 @@ describe "Spell check", ->
     window.rootView = new RootView
     rootView.open('sample.js')
     config.set('spell-check.grammars', [])
-    window.loadPackage('spell-check')
+    window.loadPackage('spell-check', activateImmediately: true)
     rootView.attachToDom()
     editor = rootView.getActiveView()
 
diff --git a/src/packages/spell-check/stylesheets/spell-check.css b/src/packages/spell-check/stylesheets/spell-check.less
similarity index 100%
rename from src/packages/spell-check/stylesheets/spell-check.css
rename to src/packages/spell-check/stylesheets/spell-check.less
diff --git a/src/packages/tree-view/lib/tree-view.coffee b/src/packages/tree-view/lib/tree-view.coffee
index 6b531aa4e..e6421679f 100644
--- a/src/packages/tree-view/lib/tree-view.coffee
+++ b/src/packages/tree-view/lib/tree-view.coffee
@@ -127,8 +127,10 @@ class TreeView extends ScrollView
       @root = null
 
   selectActiveFile: ->
-    activeFilePath = rootView.getActiveView()?.getPath()
-    @selectEntryForPath(activeFilePath) if activeFilePath
+    if activeFilePath = rootView.getActiveView()?.getPath?()
+      @selectEntryForPath(activeFilePath)
+    else
+      @deselect()
 
   revealActiveFile: ->
     @attach()
@@ -298,9 +300,12 @@ class TreeView extends ScrollView
     return false unless entry.get(0)
     entry = entry.view() unless entry instanceof View
     @selectedPath = entry.getPath()
-    @treeViewList.find('.selected').removeClass('selected')
+    @deselect()
     entry.addClass('selected')
 
+  deselect: ->
+    @treeViewList.find('.selected').removeClass('selected')
+
   scrollTop: (top) ->
     if top
       @treeViewList.scrollTop(top)
diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee
index 2b91c9630..a4e7bc604 100644
--- a/src/packages/tree-view/spec/tree-view-spec.coffee
+++ b/src/packages/tree-view/spec/tree-view-spec.coffee
@@ -1,4 +1,5 @@
 $ = require 'jquery'
+{$$} = require 'space-pen'
 _ = require 'underscore'
 TreeView = require 'tree-view/lib/tree-view'
 RootView = require 'root-view'
@@ -49,7 +50,7 @@ describe "TreeView", ->
         rootView.deactivate()
         window.rootView = new RootView()
         rootView.open()
-        treeView = window.loadPackage("tree-view").packageMain.createView()
+        treeView = window.loadPackage("tree-view").mainModule.createView()
 
       it "does not attach to the root view or create a root node when initialized", ->
         expect(treeView.hasParent()).toBeFalsy()
@@ -75,13 +76,13 @@ describe "TreeView", ->
         rootView.deactivate()
         window.rootView = new RootView
         rootView.open('tree-view.js')
-        treeView = window.loadPackage("tree-view").packageMain.createView()
+        treeView = window.loadPackage("tree-view").mainModule.createView()
         expect(treeView.hasParent()).toBeFalsy()
         expect(treeView.root).toExist()
 
     describe "when the root view is opened to a directory", ->
       it "attaches to the root view", ->
-        treeView = window.loadPackage("tree-view").packageMain.createView()
+        treeView = window.loadPackage("tree-view").mainModule.createView()
         expect(treeView.hasParent()).toBeTruthy()
         expect(treeView.root).toExist()
 
@@ -301,18 +302,25 @@ describe "TreeView", ->
       expect(subdir).toHaveClass 'expanded'
       expect(rootView.getActiveView().isFocused).toBeFalsy()
 
-  describe "when a new file is opened in the active editor", ->
-    it "selects the file in the tree view if the file's entry visible", ->
-      sampleJs.click()
-      rootView.open(fs.resolveOnLoadPath('fixtures/tree-view/tree-view.txt'))
+  describe "when the active item changes on the active pane", ->
+    describe "when the item has a path", ->
+      it "selects the entry with that path in the tree view if it is visible", ->
+        sampleJs.click()
+        rootView.open(require.resolve('fixtures/tree-view/tree-view.txt'))
 
-      expect(sampleTxt).toHaveClass 'selected'
-      expect(treeView.find('.selected').length).toBe 1
+        expect(sampleTxt).toHaveClass 'selected'
+        expect(treeView.find('.selected').length).toBe 1
 
-    it "selects the file's parent dir if the file's entry is not visible", ->
-      rootView.open('dir1/sub-dir1/sub-file1')
-      dirView = treeView.root.find('.directory:contains(dir1)').view()
-      expect(dirView).toHaveClass 'selected'
+      it "selects the path's parent dir if its entry is not visible", ->
+        rootView.open('dir1/sub-dir1/sub-file1')
+        dirView = treeView.root.find('.directory:contains(dir1)').view()
+        expect(dirView).toHaveClass 'selected'
+
+    describe "when the item has no path", ->
+      it "deselects the previously selected entry", ->
+        sampleJs.click()
+        rootView.getActivePane().showItem($$ -> @div('hello'))
+        expect(rootView.find('.selected')).not.toExist()
 
   describe "when a different editor becomes active", ->
     it "selects the file in that is open in that editor", ->
diff --git a/src/packages/tree-view/stylesheets/tree-view.css b/src/packages/tree-view/stylesheets/tree-view.css
deleted file mode 100644
index 723b85f2e..000000000
--- a/src/packages/tree-view/stylesheets/tree-view.css
+++ /dev/null
@@ -1,57 +0,0 @@
-.tree-view-wrapper {
-  position: relative;
-  height: 100%;
-  cursor: default;
-  -webkit-user-select: none;
-  min-width: 50px;
-  z-index: 2;
-}
-
-.tree-view {
-  position: relative;
-  cursor: default;
-  -webkit-user-select: none;
-  overflow: auto;
-  height: 100%;
-}
-
-.tree-view-wrapper .tree-view-resizer {
-  position: absolute;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  width: 10px;
-  cursor: col-resize;
-  z-index: 3;
-}
-
-.tree-view .entry {
-  text-wrap: none;
-  white-space: nowrap;
-}
-
-.tree-view .entry > .header,
-.tree-view .entry > .name {
-  z-index: 1;
-  position: relative;
-  display: inline-block;
-}
-
-.tree-view .selected > .highlight {
-  position: absolute;
-  left: 0;
-  right: 0;
-  height: 24px;
-}
-
-.tree-view .disclosure-arrow {
-  display: inline-block;
-}
-
-.tree-view-dialog {
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  z-index: 99;
-}
diff --git a/src/packages/tree-view/stylesheets/tree-view.less b/src/packages/tree-view/stylesheets/tree-view.less
new file mode 100644
index 000000000..33c9db860
--- /dev/null
+++ b/src/packages/tree-view/stylesheets/tree-view.less
@@ -0,0 +1,57 @@
+.tree-view-wrapper {
+  position: relative;
+  height: 100%;
+  cursor: default;
+  -webkit-user-select: none;
+  min-width: 50px;
+  z-index: 2;
+
+  .tree-view-resizer {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    width: 10px;
+    cursor: col-resize;
+    z-index: 3;
+  }
+}
+
+.tree-view {
+  position: relative;
+  cursor: default;
+  -webkit-user-select: none;
+  overflow: auto;
+  height: 100%;
+
+  .entry {
+    text-wrap: none;
+    white-space: nowrap;
+
+    & > .header,
+    > .name {
+      z-index: 1;
+      position: relative;
+      display: inline-block;
+    }
+  }
+
+  .selected > .highlight {
+    position: absolute;
+    left: 0;
+    right: 0;
+    height: 24px;
+  }
+
+  .disclosure-arrow {
+    display: inline-block;
+  }
+}
+
+.tree-view-dialog {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 99;
+}
diff --git a/src/packages/wrap-guide/stylesheets/wrap-guide.css b/src/packages/wrap-guide/stylesheets/wrap-guide.less
similarity index 100%
rename from src/packages/wrap-guide/stylesheets/wrap-guide.css
rename to src/packages/wrap-guide/stylesheets/wrap-guide.less
diff --git a/src/stdlib/cson.coffee b/src/stdlib/cson.coffee
index 1d105dc6a..6d331483d 100644
--- a/src/stdlib/cson.coffee
+++ b/src/stdlib/cson.coffee
@@ -1,3 +1,4 @@
+require 'underscore-extensions'
 _ = require 'underscore'
 fs = require 'fs'
 fsUtils = require 'fs-utils'
diff --git a/src/stdlib/fs-utils.coffee b/src/stdlib/fs-utils.coffee
index 4aefd02dc..f1ae8678f 100644
--- a/src/stdlib/fs-utils.coffee
+++ b/src/stdlib/fs-utils.coffee
@@ -86,11 +86,11 @@ module.exports =
     paths = []
     if extensions
       onPath = (path) =>
-        paths.push(@join(rootPath, path)) if _.contains(extensions, @extension(path))
+        paths.push(path) if _.contains(extensions, @extension(path))
         false
     else
       onPath = (path) =>
-        paths.push(@join(rootPath, path))
+        paths.push(path)
         false
     @traverseTreeSync(rootPath, onPath, onPath)
     paths
@@ -98,7 +98,7 @@ module.exports =
   listTree: (rootPath) ->
     paths = []
     onPath = (path) =>
-      paths.push(@join(rootPath, path))
+      paths.push(path)
       true
     @traverseTreeSync(rootPath, onPath, onPath)
     paths
@@ -162,9 +162,9 @@ module.exports =
         absolutePath = @join(rootPath, file)
         stats = fs.statSync(absolutePath)
         if stats.isDirectory()
-          traverse(absolutePath, relativePath, onFile, onDirectory) if onDirectory(relativePath)
+          traverse(absolutePath, relativePath, onFile, onDirectory) if onDirectory(absolutePath)
         else if stats.isFile()
-          onFile(relativePath)
+          onFile(absolutePath)
 
     traverse(rootPath, '', onFile, onDirectory)
 
diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
index 089363577..70d24fc3a 100644
--- a/src/stdlib/jquery-extensions.coffee
+++ b/src/stdlib/jquery-extensions.coffee
@@ -7,6 +7,12 @@ $.fn.scrollBottom = (newValue) ->
   else
     @scrollTop() + @height()
 
+$.fn.scrollDown = ->
+  @scrollTop(@scrollTop() + $(window).height() / 20)
+
+$.fn.scrollUp = ->
+  @scrollTop(@scrollTop() - $(window).height() / 20)
+
 $.fn.scrollToTop = ->
   @scrollTop(0)
 
@@ -53,7 +59,9 @@ $.fn.trueHeight = ->
 $.fn.trueWidth = ->
   this[0].getBoundingClientRect().width
 
-$.fn.document = (eventDescriptions) ->
+$.fn.document = (eventName, docString) ->
+  eventDescriptions = {}
+  eventDescriptions[eventName] = docString
   @data('documentation', {}) unless @data('documentation')
   _.extend(@data('documentation'), eventDescriptions)
 
@@ -69,12 +77,20 @@ $.fn.events = ->
   else
     events
 
-$.fn.command = (args...) ->
-  eventName = args[0]
-  documentation = {}
-  documentation[eventName] = _.humanizeEventName(eventName)
-  @document(documentation)
-  @on(args...)
+$.fn.command = (eventName, selector, options, handler) ->
+  if not options?
+    handler  = selector
+    selector = null
+  else if not handler?
+    handler = options
+    options = null
+
+  if selector? and typeof(selector) is 'object'
+    options  = selector
+    selector = null
+
+  @document(eventName, _.humanizeEventName(eventName, options?["doc"]))
+  @on(eventName, selector, options?['data'], handler)
 
 $.fn.iconSize = (size) ->
   @width(size).height(size).css('font-size', size)
diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
index 0a47fdfc1..33dde9f48 100644
--- a/src/stdlib/underscore-extensions.coffee
+++ b/src/stdlib/underscore-extensions.coffee
@@ -52,16 +52,20 @@ _.mixin
     regex = RegExp('[' + specials.join('\\') + ']', 'g')
     string.replace(regex, "\\$&");
 
-  humanizeEventName: (eventName) ->
-    if /:/.test(eventName)
-      [namespace, name] = eventName.split(':')
-      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name)}"
+  humanizeEventName: (eventName, eventDoc) ->
+    [namespace, event]  = eventName.split(':')
+    return _.capitalize(namespace) unless event?
 
-    words = eventName.split('-')
-    words.map(_.capitalize).join(' ')
+    namespaceDoc = _.undasherize(namespace)
+    eventDoc ?= _.undasherize(event)
+
+    "#{namespaceDoc}: #{eventDoc}"
 
   capitalize: (word) ->
-    word[0].toUpperCase() + word[1..]
+    if word.toLowerCase() is 'github'
+      'GitHub'
+    else
+      word[0].toUpperCase() + word[1..]
 
   pluralize: (count=0, singular, plural=singular+'s') ->
     if count is 1
@@ -80,6 +84,9 @@ _.mixin
       else
         "-"
 
+  undasherize: (string) ->
+    string.split('-').map(_.capitalize).join(' ')
+
   underscore: (string) ->
     string = string[0].toLowerCase() + string[1..]
     string.replace /([A-Z])|(-)/g, (m, letter, dash) ->
diff --git a/static/atom.css b/static/atom.css
deleted file mode 100644
index 6cfb79269..000000000
--- a/static/atom.css
+++ /dev/null
@@ -1,81 +0,0 @@
-html, body {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-}
-
-#root-view {
-  height: 100%;
-  overflow: hidden;
-  position: relative;
-}
-
-#root-view #horizontal {
-  display: -webkit-flex;
-  height: 100%;
-}
-
-#root-view #vertical {
-  display: -webkit-flex;
-  -webkit-flex: 1;
-  -webkit-flex-flow: column;
-}
-
-#panes {
-  position: relative;
-  -webkit-flex: 1;
-}
-
-#panes .column {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  overflow-y: hidden;
-}
-
-#panes .row {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  overflow-x: hidden;
-}
-
-#panes .pane {
-  position: absolute;
-  display: -webkit-flex;
-  -webkit-flex-flow: column;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  box-sizing: border-box;
-}
-
-#panes .pane .item-views {
-  -webkit-flex: 1;
-  display: -webkit-flex;
-  -webkit-flex-flow: column;
-}
-
-@font-face {
-  font-family: 'Octicons Regular';
-  src: url("octicons-regular-webfont.woff") format("woff");
-  font-weight: normal;
-  font-style: normal;
-}
-
-.is-loading {
-  background-image: url(images/spinner.svg);
-  background-repeat: no-repeat;
-  width: 14px;
-  height: 14px;
-  opacity: 0.5;
-  background-size: contain;
-  position: relative;
-  display: inline-block;
-  padding-left: 19px;
-}
diff --git a/static/atom.less b/static/atom.less
new file mode 100644
index 000000000..f58c62d76
--- /dev/null
+++ b/static/atom.less
@@ -0,0 +1,86 @@
+html, body {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+#root-view {
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+
+  #horizontal {
+    display: -webkit-flex;
+    height: 100%;
+  }
+
+  #vertical {
+    display: -webkit-flex;
+    -webkit-flex: 1;
+    -webkit-flex-flow: column;
+  }
+}
+
+#panes {
+  position: relative;
+  -webkit-flex: 1;
+
+  .column {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    overflow-y: hidden;
+  }
+
+  .row {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    overflow-x: hidden;
+  }
+
+  .pane {
+    position: absolute;
+    display: -webkit-flex;
+    -webkit-flex-flow: column;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    box-sizing: border-box;
+  }
+
+  .pane .item-views {
+    -webkit-flex: 1;
+    display: -webkit-flex;
+    -webkit-flex-flow: column;
+  }
+
+  .pane .item-views > * {
+    -webkit-flex: 1;
+    min-height: 0;
+  }
+}
+
+@font-face {
+  font-family: 'Octicons Regular';
+  src: url("octicons-regular-webfont.woff") format("woff");
+  font-weight: normal;
+  font-style: normal;
+}
+
+.is-loading {
+  background-image: url(images/spinner.svg);
+  background-repeat: no-repeat;
+  width: 14px;
+  height: 14px;
+  opacity: 0.5;
+  background-size: contain;
+  position: relative;
+  display: inline-block;
+  padding-left: 19px;
+}
diff --git a/static/command-panel.css b/static/command-panel.css
deleted file mode 100644
index 8f2098259..000000000
--- a/static/command-panel.css
+++ /dev/null
@@ -1,134 +0,0 @@
-.command-panel {
-  position: relative;
-  padding: 0;
-}
-
-.command-panel .is-loading {
-  display: block;
-  margin: 0 auto 10px auto;
-  width: 100px;
-  background-color: #111;
-  background-size: auto;
-  background-position: 5px 5px;
-  padding: 5px 5px 10px 30px;
-  border-radius: 3px;
-  border: 1px solid rgba(255, 255, 255,  0.1);
-  border-top: 1px solid rgba(0, 0, 0, 1);
-  border-left: 1px solid rgba(0, 0, 0, 1);
-}
-
-.command-panel .preview-count {
-  display: inline-block;
-  margin-top: 4px;
-  font-size: 11px;
-}
-
-.command-panel .preview-list {
-  max-height: 300px;
-  overflow: auto;
-  margin: 0 0 10px 0;
-  position: relative;
-  cursor: default;
-}
-
-.command-panel .header:after {
-  content: ".";
-  display: block;
-  visibility: hidden;
-  clear: both;
-  height: 0;
-}
-
-.command-panel .expand-collapse {
-  float: right;
-}
-
-.command-panel .expand-collapse li {
-  display: inline-block;
-  cursor: pointer;
-  font-size: 11px;
-  margin-left: 5px;
-  padding: 5px 10px;
-  border-radius: 3px;
-}
-
-.command-panel .preview-count,
-.command-panel .expand-collapse {
-  -webkit-user-select: none;
-}
-
-.command-panel .preview-list .path {
-  position: relative;
-  -webkit-user-select: none;
-}
-
-.command-panel .preview-list .path-details:before {
-  font-family: 'Octicons Regular';
-  font-size: 12px;
-  width: 12px;
-  height: 12px;
-  margin-right: 5px;
-  margin-left: 5px;
-  -webkit-font-smoothing: antialiased;
-  content: "\f05b";
-  position: relative;
-  top: 0;
-}
-
-.command-panel .preview-list .is-collapsed .path-details:before {
-  content: "\f05a";
-}
-
-.command-panel .preview-list .path-name:before {
-  font-family: 'Octicons Regular';
-  font-size: 16px;
-  width: 16px;
-  height: 16px;
-  margin-right: 5px;
-  -webkit-font-smoothing: antialiased;
-  content: "\f011";
-  position: relative;
-  top: 1px;
-}
-
-.command-panel .preview-list .path.readme .path-name:before {
-  content: "\f007";
-}
-
-.command-panel .preview-list .operation {
-  padding-top: 2px;
-  padding-bottom: 2px;
-  padding-left: 10px;
-}
-
-.command-panel .preview-list .line-number {
-  margin-right: 1ex;
-  text-align: right;
-  display: inline-block;
-}
-
-.command-panel .preview-list .path-match-number {
-  padding-left: 8px;
-}
-
-.command-panel .preview-list .preview {
-  word-break: break-all;
-}
-
-.command-panel .preview-list .preview .match {
-  -webkit-border-radius: 2px;
-  padding: 1px;
-}
-
-.command-panel .prompt-and-editor .editor {
-  position: relative;
-}
-
-.command-panel .prompt-and-editor {
-  display: -webkit-flex;
-}
-
-.error-messages {
-  padding: 5px 1em;
-  color: white;
-}
diff --git a/static/command-panel.less b/static/command-panel.less
new file mode 100644
index 000000000..15a133ec7
--- /dev/null
+++ b/static/command-panel.less
@@ -0,0 +1,132 @@
+.command-panel {
+  position: relative;
+  padding: 0;
+
+  .is-loading {
+    display: block;
+    margin: 0 auto 10px auto;
+    width: 100px;
+    background-color: #111111;
+    background-size: auto;
+    background-position: 5px 5px;
+    padding: 5px 5px 10px 30px;
+    border-radius: 3px;
+    border: 1px solid rgba(255,255,255,0.1);
+    border-top: 1px solid rgba(0,0,0,1);
+    border-left: 1px solid rgba(0,0,0,1);
+  }
+
+  .preview-count {
+    display: inline-block;
+    margin-top: 4px;
+    font-size: 11px;
+    -webkit-user-select: none;
+  }
+
+  .preview-list {
+    max-height: 300px;
+    overflow: auto;
+    margin: 0 0 10px 0;
+    position: relative;
+    cursor: default;
+
+    .path {
+      position: relative;
+      -webkit-user-select: none;
+    }
+
+    .path-details:before {
+      font-family: 'Octicons Regular';
+      font-size: 12px;
+      width: 12px;
+      height: 12px;
+      margin-right: 5px;
+      margin-left: 5px;
+      -webkit-font-smoothing: antialiased;
+      content: "\f05b";
+      position: relative;
+      top: 0;
+    }
+
+    .is-collapsed .path-details:before {
+      content: "\f05a";
+    }
+
+    .path-name:before {
+      font-family: 'Octicons Regular';
+      font-size: 16px;
+      width: 16px;
+      height: 16px;
+      margin-right: 5px;
+      -webkit-font-smoothing: antialiased;
+      content: "\f011";
+      position: relative;
+      top: 1px;
+    }
+
+    .path.readme .path-name:before {
+      content: "\f007";
+    }
+
+    .operation {
+      padding-top: 2px;
+      padding-bottom: 2px;
+      padding-left: 10px;
+    }
+
+    .line-number {
+      margin-right: 1ex;
+      text-align: right;
+      display: inline-block;
+    }
+
+    .path-match-number {
+      padding-left: 8px;
+    }
+
+    .preview {
+      word-break: break-all;
+
+      .match {
+        -webkit-border-radius: 2px;
+        padding: 1px;
+      }
+    }
+  }
+
+  .header:after {
+    content: ".";
+    display: block;
+    visibility: hidden;
+    clear: both;
+    height: 0;
+  }
+
+  .expand-collapse {
+    float: right;
+    -webkit-user-select: none;
+
+    li {
+      display: inline-block;
+      cursor: pointer;
+      font-size: 11px;
+      margin-left: 5px;
+      padding: 5px 10px;
+      border-radius: 3px;
+    }
+  }
+
+  .prompt-and-editor {
+    display: -webkit-flex;
+
+    .editor {
+      position: relative;
+      -webkit-flex: 1;
+    }
+  }
+}
+
+.error-messages {
+  padding: 5px 1em;
+  color: white;
+}
diff --git a/static/editor.css b/static/editor.less
similarity index 99%
rename from static/editor.css
rename to static/editor.less
index 1b6c35a74..c88b3f1f2 100644
--- a/static/editor.css
+++ b/static/editor.less
@@ -6,7 +6,6 @@
   z-index: 0;
   font-family: Inconsolata, Monaco, Courier;
   line-height: 1.3;
-  -webkit-flex: 1;
 }
 
 .editor.mini {
diff --git a/static/fuzzy-finder.css b/static/fuzzy-finder.less
similarity index 100%
rename from static/fuzzy-finder.css
rename to static/fuzzy-finder.less
diff --git a/static/jasmine.css b/static/jasmine.less
similarity index 100%
rename from static/jasmine.css
rename to static/jasmine.less
diff --git a/static/markdown.css b/static/markdown.css
deleted file mode 100644
index c41f233ed..000000000
--- a/static/markdown.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.source.gfm {
-  -webkit-font-smoothing: antialiased;
-}
-
-.gfm .markup.heading {
-  font-weight: bold;
-}
-
-.gfm .bold {
-  font-weight: bold;
-}
-
-.gfm .italic {
-  font-style: italic;
-}
-
-.gfm .comment.quote {
-  font-style: italic;
-}
-
-.gfm .raw {
-  -webkit-font-smoothing: subpixel-antialiased;
-}
\ No newline at end of file
diff --git a/static/markdown.less b/static/markdown.less
new file mode 100644
index 000000000..e97b8ce25
--- /dev/null
+++ b/static/markdown.less
@@ -0,0 +1,25 @@
+.source {
+  .gfm {
+    -webkit-font-smoothing: antialiased;
+
+    .markup.heading {
+      font-weight: bold;
+    }
+
+    .bold {
+      font-weight: bold;
+    }
+
+    .italic {
+      font-style: italic;
+    }
+
+    .comment.quote {
+      font-style: italic;
+    }
+
+    .raw {
+      -webkit-font-smoothing: subpixel-antialiased;
+    }
+  }
+}
diff --git a/static/notification.css b/static/notification.less
similarity index 100%
rename from static/notification.css
rename to static/notification.less
diff --git a/static/overlay.css b/static/overlay.less
similarity index 100%
rename from static/overlay.css
rename to static/overlay.less
diff --git a/static/popover-list.css b/static/popover-list.less
similarity index 100%
rename from static/popover-list.css
rename to static/popover-list.less
diff --git a/static/reset.css b/static/reset.less
similarity index 100%
rename from static/reset.css
rename to static/reset.less
diff --git a/static/select-list.css b/static/select-list.less
similarity index 100%
rename from static/select-list.css
rename to static/select-list.less
diff --git a/static/status-bar.css b/static/status-bar.less
similarity index 98%
rename from static/status-bar.css
rename to static/status-bar.less
index d7822f50c..f30dd48c8 100644
--- a/static/status-bar.css
+++ b/static/status-bar.less
@@ -5,6 +5,7 @@
   position: relative;
   -webkit-user-select: none;
   cursor: default;
+  overflow: hidden;
 }
 
 .status-bar .git-branch {
diff --git a/static/tabs.css b/static/tabs.less
similarity index 100%
rename from static/tabs.css
rename to static/tabs.less
diff --git a/static/tree-view.css b/static/tree-view.less
similarity index 100%
rename from static/tree-view.css
rename to static/tree-view.less
diff --git a/vendor/less.js b/vendor/less.js
new file mode 100644
index 000000000..59629068d
--- /dev/null
+++ b/vendor/less.js
@@ -0,0 +1,5078 @@
+// Modified
+//
+// Added
+//     module.exports.less = window.less = less = {}
+//     less.tree = tree = {}
+//     less.mode = 'browser'
+//
+// LESS - Leaner CSS v1.4.0
+// http://lesscss.org
+//
+// Copyright (c) 2009-2013, Alexis Sellier
+// Licensed under the Apache 2.0 License.
+//
+(function (window, undefined) {
+//
+// Stub out `require` in the browser
+//
+function require(arg) {
+    return window.less[arg.split('/')[1]];
+};
+
+// ecma-5.js
+//
+// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License
+// -- tlrobinson Tom Robinson
+// dantman Daniel Friesen
+
+//
+// Array
+//
+if (!Array.isArray) {
+    Array.isArray = function(obj) {
+        return Object.prototype.toString.call(obj) === "[object Array]" ||
+               (obj instanceof Array);
+    };
+}
+if (!Array.prototype.forEach) {
+    Array.prototype.forEach =  function(block, thisObject) {
+        var len = this.length >>> 0;
+        for (var i = 0; i < len; i++) {
+            if (i in this) {
+                block.call(thisObject, this[i], i, this);
+            }
+        }
+    };
+}
+if (!Array.prototype.map) {
+    Array.prototype.map = function(fun /*, thisp*/) {
+        var len = this.length >>> 0;
+        var res = new Array(len);
+        var thisp = arguments[1];
+
+        for (var i = 0; i < len; i++) {
+            if (i in this) {
+                res[i] = fun.call(thisp, this[i], i, this);
+            }
+        }
+        return res;
+    };
+}
+if (!Array.prototype.filter) {
+    Array.prototype.filter = function (block /*, thisp */) {
+        var values = [];
+        var thisp = arguments[1];
+        for (var i = 0; i < this.length; i++) {
+            if (block.call(thisp, this[i])) {
+                values.push(this[i]);
+            }
+        }
+        return values;
+    };
+}
+if (!Array.prototype.reduce) {
+    Array.prototype.reduce = function(fun /*, initial*/) {
+        var len = this.length >>> 0;
+        var i = 0;
+
+        // no value to return if no initial value and an empty array
+        if (len === 0 && arguments.length === 1) throw new TypeError();
+
+        if (arguments.length >= 2) {
+            var rv = arguments[1];
+        } else {
+            do {
+                if (i in this) {
+                    rv = this[i++];
+                    break;
+                }
+                // if array contains no values, no initial value to return
+                if (++i >= len) throw new TypeError();
+            } while (true);
+        }
+        for (; i < len; i++) {
+            if (i in this) {
+                rv = fun.call(null, rv, this[i], i, this);
+            }
+        }
+        return rv;
+    };
+}
+if (!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function (value /*, fromIndex */ ) {
+        var length = this.length;
+        var i = arguments[1] || 0;
+
+        if (!length)     return -1;
+        if (i >= length) return -1;
+        if (i < 0)       i += length;
+
+        for (; i < length; i++) {
+            if (!Object.prototype.hasOwnProperty.call(this, i)) { continue }
+            if (value === this[i]) return i;
+        }
+        return -1;
+    };
+}
+
+//
+// Object
+//
+if (!Object.keys) {
+    Object.keys = function (object) {
+        var keys = [];
+        for (var name in object) {
+            if (Object.prototype.hasOwnProperty.call(object, name)) {
+                keys.push(name);
+            }
+        }
+        return keys;
+    };
+}
+
+//
+// String
+//
+if (!String.prototype.trim) {
+    String.prototype.trim = function () {
+        return String(this).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+    };
+}
+var less, tree, charset;
+
+module.exports.less = window.less = less = {}
+less.tree = tree = {}
+less.mode = 'browser'
+//
+// less.js - parser
+//
+//    A relatively straight-forward predictive parser.
+//    There is no tokenization/lexing stage, the input is parsed
+//    in one sweep.
+//
+//    To make the parser fast enough to run in the browser, several
+//    optimization had to be made:
+//
+//    - Matching and slicing on a huge input is often cause of slowdowns.
+//      The solution is to chunkify the input into smaller strings.
+//      The chunks are stored in the `chunks` var,
+//      `j` holds the current chunk index, and `current` holds
+//      the index of the current chunk in relation to `input`.
+//      This gives us an almost 4x speed-up.
+//
+//    - In many cases, we don't need to match individual tokens;
+//      for example, if a value doesn't hold any variables, operations
+//      or dynamic references, the parser can effectively 'skip' it,
+//      treating it as a literal.
+//      An example would be '1px solid #000' - which evaluates to itself,
+//      we don't need to know what the individual components are.
+//      The drawback, of course is that you don't get the benefits of
+//      syntax-checking on the CSS. This gives us a 50% speed-up in the parser,
+//      and a smaller speed-up in the code-gen.
+//
+//
+//    Token matching is done with the `$` function, which either takes
+//    a terminal string or regexp, or a non-terminal function to call.
+//    It also takes care of moving all the indices forwards.
+//
+//
+less.Parser = function Parser(env) {
+    var input,       // LeSS input string
+        i,           // current index in `input`
+        j,           // current chunk
+        temp,        // temporarily holds a chunk's state, for backtracking
+        memo,        // temporarily holds `i`, when backtracking
+        furthest,    // furthest index the parser has gone to
+        chunks,      // chunkified input
+        current,     // index of current chunk, in `input`
+        parser;
+
+    var that = this;
+
+    // Top parser on an import tree must be sure there is one "env"
+    // which will then be passed around by reference.
+    if (!(env instanceof tree.parseEnv)) {
+        env = new tree.parseEnv(env);
+    }
+
+    if (!env.currentDirectory && env.filename) {
+        // only works for node, only used for node
+        env.currentDirectory = env.filename.replace(/[^\/\\]*$/, "");
+    }
+
+    // This function is called after all files
+    // have been imported through `@import`.
+    var finish = function () {};
+
+    var imports = this.imports = {
+        paths: env.paths || [],  // Search paths, when importing
+        queue: [],               // Files which haven't been imported yet
+        files: env.files,        // Holds the imported parse trees
+        contents: env.contents,  // Holds the imported file contents
+        mime:  env.mime,         // MIME type of .less files
+        error: null,             // Error in parsing/evaluating an import
+        push: function (path, callback) {
+            var that = this;
+            this.queue.push(path);
+
+            //
+            // Import a file asynchronously
+            //
+            less.Parser.importer(path, this.paths, function (e, root, fullPath) {
+                that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue
+
+                var imported = fullPath in that.files;
+
+                that.files[fullPath] = root;                        // Store the root
+
+                if (e && !that.error) { that.error = e; }
+
+                callback(e, root, imported);
+
+                if (that.queue.length === 0) { finish(that.error); }       // Call `finish` if we're done importing
+            }, env);
+        }
+    };
+
+    function save()    { temp = chunks[j], memo = i, current = i; }
+    function restore() { chunks[j] = temp, i = memo, current = i; }
+
+    function sync() {
+        if (i > current) {
+            chunks[j] = chunks[j].slice(i - current);
+            current = i;
+        }
+    }
+    function isWhitespace(c) {
+        // Could change to \s?
+        var code = c.charCodeAt(0);
+        return code === 32 || code === 10 || code === 9;
+    }
+    //
+    // Parse from a token, regexp or string, and move forward if match
+    //
+    function $(tok) {
+        var match, args, length, index, k;
+
+        //
+        // Non-terminal
+        //
+        if (tok instanceof Function) {
+            return tok.call(parser.parsers);
+        //
+        // Terminal
+        //
+        //     Either match a single character in the input,
+        //     or match a regexp in the current chunk (chunk[j]).
+        //
+        } else if (typeof(tok) === 'string') {
+            match = input.charAt(i) === tok ? tok : null;
+            length = 1;
+            sync ();
+        } else {
+            sync ();
+
+            if (match = tok.exec(chunks[j])) {
+                length = match[0].length;
+            } else {
+                return null;
+            }
+        }
+
+        // The match is confirmed, add the match length to `i`,
+        // and consume any extra white-space characters (' ' || '\n')
+        // which come after that. The reason for this is that LeSS's
+        // grammar is mostly white-space insensitive.
+        //
+        if (match) {
+            skipWhitespace(length);
+
+            if(typeof(match) === 'string') {
+                return match;
+            } else {
+                return match.length === 1 ? match[0] : match;
+            }
+        }
+    }
+
+    function skipWhitespace(length) {
+        var oldi = i, oldj = j,
+            endIndex = i + chunks[j].length,
+            mem = i += length;
+
+        while (i < endIndex) {
+            if (! isWhitespace(input.charAt(i))) { break }
+            i++;
+        }
+        chunks[j] = chunks[j].slice(length + (i - mem));
+        current = i;
+
+        if (chunks[j].length === 0 && j < chunks.length - 1) { j++ }
+
+        return oldi !== i || oldj !== j;
+    }
+
+    function expect(arg, msg) {
+        var result = $(arg);
+        if (! result) {
+            error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'"
+                                                   : "unexpected token"));
+        } else {
+            return result;
+        }
+    }
+
+    function error(msg, type) {
+        var e = new Error(msg);
+        e.index = i;
+        e.type = type || 'Syntax';
+        throw e;
+    }
+
+    // Same as $(), but don't change the state of the parser,
+    // just return the match.
+    function peek(tok) {
+        if (typeof(tok) === 'string') {
+            return input.charAt(i) === tok;
+        } else {
+            if (tok.test(chunks[j])) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    function getInput(e, env) {
+        if (e.filename && env.filename && (e.filename !== env.filename)) {
+            return parser.imports.contents[e.filename];
+        } else {
+            return input;
+        }
+    }
+
+    function getLocation(index, input) {
+        for (var n = index, column = -1;
+                 n >= 0 && input.charAt(n) !== '\n';
+                 n--) { column++ }
+
+        return { line:   typeof(index) === 'number' ? (input.slice(0, index).match(/\n/g) || "").length : null,
+                 column: column };
+    }
+
+    function getFileName(e) {
+        if(less.mode === 'browser' || less.mode === 'rhino')
+            return e.filename;
+        else
+            return require('path').resolve(e.filename);
+    }
+
+    function getDebugInfo(index, inputStream, e) {
+        return {
+            lineNumber: getLocation(index, inputStream).line + 1,
+            fileName: getFileName(e)
+        };
+    }
+
+    function LessError(e, env) {
+        var input = getInput(e, env),
+            loc = getLocation(e.index, input),
+            line = loc.line,
+            col  = loc.column,
+            lines = input.split('\n');
+
+        this.type = e.type || 'Syntax';
+        this.message = e.message;
+        this.filename = e.filename || env.filename;
+        this.index = e.index;
+        this.line = typeof(line) === 'number' ? line + 1 : null;
+        this.callLine = e.call && (getLocation(e.call, input).line + 1);
+        this.callExtract = lines[getLocation(e.call, input).line];
+        this.stack = e.stack;
+        this.column = col;
+        this.extract = [
+            lines[line - 1],
+            lines[line],
+            lines[line + 1]
+        ];
+    }
+
+    this.env = env = env || {};
+
+    // The optimization level dictates the thoroughness of the parser,
+    // the lower the number, the less nodes it will create in the tree.
+    // This could matter for debugging, or if you want to access
+    // the individual nodes in the tree.
+    this.optimization = ('optimization' in this.env) ? this.env.optimization : 1;
+
+    this.env.filename = this.env.filename || null;
+
+    //
+    // The Parser
+    //
+    return parser = {
+
+        imports: imports,
+        //
+        // Parse an input string into an abstract syntax tree,
+        // call `callback` when done.
+        //
+        parse: function (str, callback) {
+            var root, start, end, zone, line, lines, buff = [], c, error = null;
+
+            i = j = current = furthest = 0;
+            input = str.replace(/\r\n/g, '\n');
+
+            // Remove potential UTF Byte Order Mark
+            input = input.replace(/^\uFEFF/, '');
+
+            // Split the input into chunks.
+            chunks = (function (chunks) {
+                var j = 0,
+                    skip = /(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g,
+                    comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,
+                    string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g,
+                    level = 0,
+                    match,
+                    chunk = chunks[0],
+                    inParam;
+
+                for (var i = 0, c, cc; i < input.length;) {
+                    skip.lastIndex = i;
+                    if (match = skip.exec(input)) {
+                        if (match.index === i) {
+                            i += match[0].length;
+                            chunk.push(match[0]);
+                        }
+                    }
+                    c = input.charAt(i);
+                    comment.lastIndex = string.lastIndex = i;
+
+                    if (match = string.exec(input)) {
+                        if (match.index === i) {
+                            i += match[0].length;
+                            chunk.push(match[0]);
+                            continue;
+                        }
+                    }
+
+                    if (!inParam && c === '/') {
+                        cc = input.charAt(i + 1);
+                        if (cc === '/' || cc === '*') {
+                            if (match = comment.exec(input)) {
+                                if (match.index === i) {
+                                    i += match[0].length;
+                                    chunk.push(match[0]);
+                                    continue;
+                                }
+                            }
+                        }
+                    }
+
+                    switch (c) {
+                        case '{': if (! inParam) { level ++;        chunk.push(c);                           break }
+                        case '}': if (! inParam) { level --;        chunk.push(c); chunks[++j] = chunk = []; break }
+                        case '(': if (! inParam) { inParam = true;  chunk.push(c);                           break }
+                        case ')': if (  inParam) { inParam = false; chunk.push(c);                           break }
+                        default:                                    chunk.push(c);
+                    }
+
+                    i++;
+                }
+                if (level != 0) {
+                    error = new(LessError)({
+                        index: i-1,
+                        type: 'Parse',
+                        message: (level > 0) ? "missing closing `}`" : "missing opening `{`",
+                        filename: env.filename
+                    }, env);
+                }
+
+                return chunks.map(function (c) { return c.join('') });;
+            })([[]]);
+
+            if (error) {
+                return callback(new(LessError)(error, env));
+            }
+
+            // Start with the primary rule.
+            // The whole syntax tree is held under a Ruleset node,
+            // with the `root` property set to true, so no `{}` are
+            // output. The callback is called when the input is parsed.
+            try {
+                root = new(tree.Ruleset)([], $(this.parsers.primary));
+                root.root = true;
+            } catch (e) {
+                return callback(new(LessError)(e, env));
+            }
+
+            root.toCSS = (function (evaluate) {
+                var line, lines, column;
+
+                return function (options, variables) {
+                    options = options || {};
+                    var importError,
+                        evalEnv = new tree.evalEnv(options);
+
+                    //
+                    // Allows setting variables with a hash, so:
+                    //
+                    //   `{ color: new(tree.Color)('#f01') }` will become:
+                    //
+                    //   new(tree.Rule)('@color',
+                    //     new(tree.Value)([
+                    //       new(tree.Expression)([
+                    //         new(tree.Color)('#f01')
+                    //       ])
+                    //     ])
+                    //   )
+                    //
+                    if (typeof(variables) === 'object' && !Array.isArray(variables)) {
+                        variables = Object.keys(variables).map(function (k) {
+                            var value = variables[k];
+
+                            if (! (value instanceof tree.Value)) {
+                                if (! (value instanceof tree.Expression)) {
+                                    value = new(tree.Expression)([value]);
+                                }
+                                value = new(tree.Value)([value]);
+                            }
+                            return new(tree.Rule)('@' + k, value, false, 0);
+                        });
+                        evalEnv.frames = [new(tree.Ruleset)(null, variables)];
+                    }
+
+                    try {
+                        var css = evaluate.call(this, evalEnv)
+                                          .toCSS([], {
+                                compress: options.compress || false,
+                                dumpLineNumbers: env.dumpLineNumbers,
+                                strictUnits: options.strictUnits === false ? false : true});
+                    } catch (e) {
+                        throw new(LessError)(e, env);
+                    }
+
+                    if (options.yuicompress && less.mode === 'node') {
+                        return require('ycssmin').cssmin(css);
+                    } else if (options.compress) {
+                        return css.replace(/(\s)+/g, "$1");
+                    } else {
+                        return css;
+                    }
+                };
+            })(root.eval);
+
+            // If `i` is smaller than the `input.length - 1`,
+            // it means the parser wasn't able to parse the whole
+            // string, so we've got a parsing error.
+            //
+            // We try to extract a \n delimited string,
+            // showing the line where the parse error occured.
+            // We split it up into two parts (the part which parsed,
+            // and the part which didn't), so we can color them differently.
+            if (i < input.length - 1) {
+                i = furthest;
+                lines = input.split('\n');
+                line = (input.slice(0, i).match(/\n/g) || "").length + 1;
+
+                for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ }
+
+                error = {
+                    type: "Parse",
+                    message: "Unrecognised input",
+                    index: i,
+                    filename: env.filename,
+                    line: line,
+                    column: column,
+                    extract: [
+                        lines[line - 2],
+                        lines[line - 1],
+                        lines[line]
+                    ]
+                };
+            }
+
+            finish = function (e) {
+                e = error || e || parser.imports.error;
+
+                if (e) {
+                    if (!(e instanceof LessError)) {
+                        e = new(LessError)(e, env);
+                    }
+
+                    callback(e);
+                }
+                else {
+                    callback(null, root);
+                }
+            };
+
+            if (this.imports.queue.length === 0) {
+                finish();
+            }
+        },
+
+        //
+        // Here in, the parsing rules/functions
+        //
+        // The basic structure of the syntax tree generated is as follows:
+        //
+        //   Ruleset ->  Rule -> Value -> Expression -> Entity
+        //
+        // Here's some LESS code:
+        //
+        //    .class {
+        //      color: #fff;
+        //      border: 1px solid #000;
+        //      width: @w + 4px;
+        //      > .child {...}
+        //    }
+        //
+        // And here's what the parse tree might look like:
+        //
+        //     Ruleset (Selector '.class', [
+        //         Rule ("color",  Value ([Expression [Color #fff]]))
+        //         Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
+        //         Rule ("width",  Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
+        //         Ruleset (Selector [Element '>', '.child'], [...])
+        //     ])
+        //
+        //  In general, most rules will try to parse a token with the `$()` function, and if the return
+        //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check
+        //  first, before parsing, that's when we use `peek()`.
+        //
+        parsers: {
+            //
+            // The `primary` rule is the *entry* and *exit* point of the parser.
+            // The rules here can appear at any level of the parse tree.
+            //
+            // The recursive nature of the grammar is an interplay between the `block`
+            // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
+            // as represented by this simplified grammar:
+            //
+            //     primary  →  (ruleset | rule)+
+            //     ruleset  →  selector+ block
+            //     block    →  '{' primary '}'
+            //
+            // Only at one point is the primary rule not called from the
+            // block rule: at the root level.
+            //
+            primary: function () {
+                var node, root = [];
+
+                while ((node = $(this.extendRule) || $(this.mixin.definition) || $(this.rule)    ||  $(this.ruleset) ||
+                               $(this.mixin.call)       || $(this.comment) ||  $(this.directive))
+                               || $(/^[\s\n]+/) || $(/^;+/)) {
+                    node && root.push(node);
+                }
+                return root;
+            },
+
+            // We create a Comment node for CSS comments `/* */`,
+            // but keep the LeSS comments `//` silent, by just skipping
+            // over them.
+            comment: function () {
+                var comment;
+
+                if (input.charAt(i) !== '/') return;
+
+                if (input.charAt(i + 1) === '/') {
+                    return new(tree.Comment)($(/^\/\/.*/), true);
+                } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) {
+                    return new(tree.Comment)(comment);
+                }
+            },
+
+            //
+            // Entities are tokens which can be found inside an Expression
+            //
+            entities: {
+                //
+                // A string, which supports escaping " and '
+                //
+                //     "milky way" 'he\'s the one!'
+                //
+                quoted: function () {
+                    var str, j = i, e;
+
+                    if (input.charAt(j) === '~') { j++, e = true } // Escaped strings
+                    if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return;
+
+                    e && $('~');
+
+                    if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) {
+                        return new(tree.Quoted)(str[0], str[1] || str[2], e);
+                    }
+                },
+
+                //
+                // A catch-all word, such as:
+                //
+                //     black border-collapse
+                //
+                keyword: function () {
+                    var k;
+
+                    if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) {
+                        if (tree.colors.hasOwnProperty(k)) {
+                            // detect named color
+                            return new(tree.Color)(tree.colors[k].slice(1));
+                        } else {
+                            return new(tree.Keyword)(k);
+                        }
+                    }
+                },
+
+                //
+                // A function call
+                //
+                //     rgb(255, 0, 255)
+                //
+                // We also try to catch IE's `alpha()`, but let the `alpha` parser
+                // deal with the details.
+                //
+                // The arguments are parsed with the `entities.arguments` parser.
+                //
+                call: function () {
+                    var name, nameLC, args, alpha_ret, index = i;
+
+                    if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) return;
+
+                    name = name[1];
+                    nameLC = name.toLowerCase();
+
+                    if (nameLC === 'url') { return null }
+                    else                  { i += name.length }
+
+                    if (nameLC === 'alpha') {
+                        alpha_ret = $(this.alpha);
+                        if(typeof alpha_ret !== 'undefined') {
+                            return alpha_ret;
+                        }
+                    }
+
+                    $('('); // Parse the '(' and consume whitespace.
+
+                    args = $(this.entities.arguments);
+
+                    if (! $(')')) {
+                        return;
+                    }
+
+                    if (name) { return new(tree.Call)(name, args, index, env.filename, env.rootpath, env.currentDirectory); }
+                },
+                arguments: function () {
+                    var args = [], arg;
+
+                    while (arg = $(this.entities.assignment) || $(this.expression)) {
+                        args.push(arg);
+                        if (! $(',')) { break }
+                    }
+                    return args;
+                },
+                literal: function () {
+                    return $(this.entities.dimension) ||
+                           $(this.entities.color) ||
+                           $(this.entities.quoted) ||
+                           $(this.entities.unicodeDescriptor);
+                },
+
+                // Assignments are argument entities for calls.
+                // They are present in ie filter properties as shown below.
+                //
+                //     filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
+                //
+
+                assignment: function () {
+                    var key, value;
+                    if ((key = $(/^\w+(?=\s?=)/i)) && $('=') && (value = $(this.entity))) {
+                        return new(tree.Assignment)(key, value);
+                    }
+                },
+
+                //
+                // Parse url() tokens
+                //
+                // We use a specific rule for urls, because they don't really behave like
+                // standard function calls. The difference is that the argument doesn't have
+                // to be enclosed within a string, so it can't be parsed as an Expression.
+                //
+                url: function () {
+                    var value;
+
+                    if (input.charAt(i) !== 'u' || !$(/^url\(/)) return;
+                    value = $(this.entities.quoted)  || $(this.entities.variable) ||
+                            $(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || "";
+
+                    expect(')');
+
+                    return new(tree.URL)((value.value != null || value instanceof tree.Variable)
+                                        ? value : new(tree.Anonymous)(value), env.rootpath);
+                },
+
+                //
+                // A Variable entity, such as `@fink`, in
+                //
+                //     width: @fink + 2px
+                //
+                // We use a different parser for variable definitions,
+                // see `parsers.variable`.
+                //
+                variable: function () {
+                    var name, index = i;
+
+                    if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) {
+                        return new(tree.Variable)(name, index, env.filename);
+                    }
+                },
+
+                // A variable entity useing the protective {} e.g. @{var}
+                variableCurly: function () {
+                    var name, curly, index = i;
+
+                    if (input.charAt(i) === '@' && (curly = $(/^@\{([\w-]+)\}/))) {
+                        return new(tree.Variable)("@" + curly[1], index, env.filename);
+                    }
+                },
+
+                //
+                // A Hexadecimal color
+                //
+                //     #4F3C2F
+                //
+                // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
+                //
+                color: function () {
+                    var rgb;
+
+                    if (input.charAt(i) === '#' && (rgb = $(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {
+                        return new(tree.Color)(rgb[1]);
+                    }
+                },
+
+                //
+                // A Dimension, that is, a number and a unit
+                //
+                //     0.5em 95%
+                //
+                dimension: function () {
+                    var value, c = input.charCodeAt(i);
+                    //Is the first char of the dimension 0-9, '.', '+' or '-'
+                    if ((c > 57 || c < 43) || c === 47 || c == 44) return;
+
+                    if (value = $(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/)) {
+                        return new(tree.Dimension)(value[1], value[2]);
+                    }
+                },
+
+                //
+                // A unicode descriptor, as is used in unicode-range
+                //
+                // U+0??  or U+00A1-00A9
+                //
+                unicodeDescriptor: function () {
+                    var ud;
+
+                    if (ud = $(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/)) {
+                        return new(tree.UnicodeDescriptor)(ud[0]);
+                    }
+                },
+
+                //
+                // JavaScript code to be evaluated
+                //
+                //     `window.location.href`
+                //
+                javascript: function () {
+                    var str, j = i, e;
+
+                    if (input.charAt(j) === '~') { j++, e = true } // Escaped strings
+                    if (input.charAt(j) !== '`') { return }
+
+                    e && $('~');
+
+                    if (str = $(/^`([^`]*)`/)) {
+                        return new(tree.JavaScript)(str[1], i, e);
+                    }
+                }
+            },
+
+            //
+            // The variable part of a variable definition. Used in the `rule` parser
+            //
+            //     @fink:
+            //
+            variable: function () {
+                var name;
+
+                if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] }
+            },
+
+            //
+            // extend syntax - used to extend selectors
+            //
+            extend: function(isRule) {
+                var elements = [], e, args, index = i;
+
+                if (!$(isRule ? /^&:extend\(/ : /^:extend\(/)) { return; }
+
+                while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) {
+                    elements.push(new(tree.Element)(null, e, i));
+                }
+
+                expect(/^\)/);
+
+                if (isRule) {
+                    expect(/^;/);
+                }
+
+                return new(tree.Extend)(elements, index);
+            },
+
+            //
+            // extendRule - used in a rule to extend all the parent selectors
+            //
+            extendRule: function() {
+                return this.extend(true);
+            },
+
+            //
+            // Mixins
+            //
+            mixin: {
+                //
+                // A Mixin call, with an optional argument list
+                //
+                //     #mixins > .square(#fff);
+                //     .rounded(4px, black);
+                //     .button;
+                //
+                // The `while` loop is there because mixins can be
+                // namespaced, but we only support the child and descendant
+                // selector for now.
+                //
+                call: function () {
+                    var elements = [], e, c, argsSemiColon = [], argsComma = [], args, delim, arg, nameLoop, expressions, isSemiColonSeperated, expressionContainsNamed, index = i, s = input.charAt(i), name, value, important = false;
+
+                    if (s !== '.' && s !== '#') { return }
+
+                    save(); // stop us absorbing part of an invalid selector
+
+                    while (e = $(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)) {
+                        elements.push(new(tree.Element)(c, e, i));
+                        c = $('>');
+                    }
+                    if ($('(')) {
+                        expressions = [];
+                        while (arg = $(this.expression)) {
+                            nameLoop = null;
+                            arg.throwAwayComments();
+                            value = arg;
+
+                            // Variable
+                            if (arg.value.length == 1) {
+                                var val = arg.value[0];
+                                if (val instanceof tree.Variable) {
+                                    if ($(':')) {
+                                        if (expressions.length > 0) {
+                                            if (isSemiColonSeperated) {
+                                                error("Cannot mix ; and , as delimiter types");
+                                            }
+                                            expressionContainsNamed = true;
+                                        }
+                                        value = expect(this.expression);
+                                        nameLoop = (name = val.name);
+                                    }
+                                }
+                            }
+
+                            expressions.push(value);
+
+                            argsComma.push({ name: nameLoop, value: value });
+
+                            if ($(',')) {
+                                continue;
+                            }
+
+                            if ($(';') || isSemiColonSeperated) {
+
+                                if (expressionContainsNamed) {
+                                    error("Cannot mix ; and , as delimiter types");
+                                }
+
+                                isSemiColonSeperated = true;
+
+                                if (expressions.length > 1) {
+                                    value = new(tree.Value)(expressions);
+                                }
+                                argsSemiColon.push({ name: name, value: value });
+
+                                name = null;
+                                expressions = [];
+                                expressionContainsNamed = false;
+                            }
+                        }
+
+                        expect(')');
+                    }
+
+                    args = isSemiColonSeperated ? argsSemiColon : argsComma;
+
+                    if ($(this.important)) {
+                        important = true;
+                    }
+
+                    if (elements.length > 0 && ($(';') || peek('}'))) {
+                        return new(tree.mixin.Call)(elements, args, index, env.filename, important);
+                    }
+
+                    restore();
+                },
+
+                //
+                // A Mixin definition, with a list of parameters
+                //
+                //     .rounded (@radius: 2px, @color) {
+                //        ...
+                //     }
+                //
+                // Until we have a finer grained state-machine, we have to
+                // do a look-ahead, to make sure we don't have a mixin call.
+                // See the `rule` function for more information.
+                //
+                // We start by matching `.rounded (`, and then proceed on to
+                // the argument list, which has optional default values.
+                // We store the parameters in `params`, with a `value` key,
+                // if there is a value, such as in the case of `@radius`.
+                //
+                // Once we've got our params list, and a closing `)`, we parse
+                // the `{...}` block.
+                //
+                definition: function () {
+                    var name, params = [], match, ruleset, param, value, cond, variadic = false;
+                    if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||
+                        peek(/^[^{]*\}/)) return;
+
+                    save();
+
+                    if (match = $(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)) {
+                        name = match[1];
+
+                        do {
+                            $(this.comment);
+                            if (input.charAt(i) === '.' && $(/^\.{3}/)) {
+                                variadic = true;
+                                params.push({ variadic: true });
+                                break;
+                            } else if (param = $(this.entities.variable) || $(this.entities.literal)
+                                                                         || $(this.entities.keyword)) {
+                                // Variable
+                                if (param instanceof tree.Variable) {
+                                    if ($(':')) {
+                                        value = expect(this.expression, 'expected expression');
+                                        params.push({ name: param.name, value: value });
+                                    } else if ($(/^\.{3}/)) {
+                                        params.push({ name: param.name, variadic: true });
+                                        variadic = true;
+                                        break;
+                                    } else {
+                                        params.push({ name: param.name });
+                                    }
+                                } else {
+                                    params.push({ value: param });
+                                }
+                            } else {
+                                break;
+                            }
+                        } while ($(',') || $(';'))
+
+                        // .mixincall("@{a}");
+                        // looks a bit like a mixin definition.. so we have to be nice and restore
+                        if (!$(')')) {
+                            furthest = i;
+                            restore();
+                        }
+
+                        $(this.comment);
+
+                        if ($(/^when/)) { // Guard
+                            cond = expect(this.conditions, 'expected condition');
+                        }
+
+                        ruleset = $(this.block);
+
+                        if (ruleset) {
+                            return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
+                        } else {
+                            restore();
+                        }
+                    }
+                }
+            },
+
+            //
+            // Entities are the smallest recognized token,
+            // and can be found inside a rule's value.
+            //
+            entity: function () {
+                return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) ||
+                       $(this.entities.call)    || $(this.entities.keyword)  ||$(this.entities.javascript) ||
+                       $(this.comment);
+            },
+
+            //
+            // A Rule terminator. Note that we use `peek()` to check for '}',
+            // because the `block` rule will be expecting it, but we still need to make sure
+            // it's there, if ';' was ommitted.
+            //
+            end: function () {
+                return $(';') || peek('}');
+            },
+
+            //
+            // IE's alpha function
+            //
+            //     alpha(opacity=88)
+            //
+            alpha: function () {
+                var value;
+
+                if (! $(/^\(opacity=/i)) return;
+                if (value = $(/^\d+/) || $(this.entities.variable)) {
+                    expect(')');
+                    return new(tree.Alpha)(value);
+                }
+            },
+
+            //
+            // A Selector Element
+            //
+            //     div
+            //     + h1
+            //     #socks
+            //     input[type="text"]
+            //
+            // Elements are the building blocks for Selectors,
+            // they are made out of a `Combinator` (see combinator rule),
+            // and an element name, such as a tag a class, or `*`.
+            //
+            element: function () {
+                var e, t, c, v;
+
+                c = $(this.combinator);
+
+                e = $(/^(?:\d+\.\d+|\d+)%/) || $(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||
+                    $('*') || $('&') || $(this.attribute) || $(/^\([^()@]+\)/) || $(/^[\.#](?=@)/) || $(this.entities.variableCurly);
+
+                if (! e) {
+                    if ($('(')) {
+                        if ((v = (//$(this.entities.variableCurly) ||
+                                $(this.selector))) &&
+                                $(')')) {
+                            e = new(tree.Paren)(v);
+                        }
+                    }
+                }
+
+                if (e) { return new(tree.Element)(c, e, i) }
+            },
+
+            //
+            // Combinators combine elements together, in a Selector.
+            //
+            // Because our parser isn't white-space sensitive, special care
+            // has to be taken, when parsing the descendant combinator, ` `,
+            // as it's an empty space. We have to check the previous character
+            // in the input, to see if it's a ` ` character. More info on how
+            // we deal with this in *combinator.js*.
+            //
+            combinator: function () {
+                var match, c = input.charAt(i);
+
+                if (c === '>' || c === '+' || c === '~' || c === '|') {
+                    i++;
+                    while (input.charAt(i).match(/\s/)) { i++ }
+                    return new(tree.Combinator)(c);
+                } else if (input.charAt(i - 1).match(/\s/)) {
+                    return new(tree.Combinator)(" ");
+                } else {
+                    return new(tree.Combinator)(null);
+                }
+            },
+
+            //
+            // A CSS Selector
+            //
+            //     .class > div + h1
+            //     li a:hover
+            //
+            // Selectors are made out of one or more Elements, see above.
+            //
+            selector: function () {
+                var sel, e, elements = [], c, match, extend;
+
+                while ((extend = $(this.extend)) || (e = $(this.element))) {
+                    if (!e) {
+                        break;
+                    }
+                    c = input.charAt(i);
+                    elements.push(e)
+                    e = null;
+                    if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') { break }
+                }
+
+                if (elements.length > 0) { return new(tree.Selector)(elements, extend) }
+                if (extend) { error("Extend must be used to extend a selector"); }
+            },
+            attribute: function () {
+                var attr = '', key, val, op;
+
+                if (! $('[')) return;
+
+                if (key = $(/^(?:[_A-Za-z0-9-]|\\.)+/) || $(this.entities.quoted)) {
+                    if ((op = $(/^[|~*$^]?=/)) &&
+                        (val = $(this.entities.quoted) || $(/^[\w-]+/))) {
+                        attr = [key, op, val.toCSS ? val.toCSS() : val].join('');
+                    } else { attr = key }
+                }
+
+                if (! $(']')) return;
+
+                if (attr) { return "[" + attr + "]" }
+            },
+
+            //
+            // The `block` rule is used by `ruleset` and `mixin.definition`.
+            // It's a wrapper around the `primary` rule, with added `{}`.
+            //
+            block: function () {
+                var content;
+                if ($('{') && (content = $(this.primary)) && $('}')) {
+                    return content;
+                }
+            },
+
+            //
+            // div, .class, body > p {...}
+            //
+            ruleset: function () {
+                var selectors = [], s, rules, match, debugInfo;
+
+                save();
+
+                if (env.dumpLineNumbers)
+                    debugInfo = getDebugInfo(i, input, env);
+
+                while (s = $(this.selector)) {
+                    selectors.push(s);
+                    $(this.comment);
+                    if (! $(',')) { break }
+                    $(this.comment);
+                }
+
+                if (selectors.length > 0 && (rules = $(this.block))) {
+                    var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);
+                    if (env.dumpLineNumbers)
+                        ruleset.debugInfo = debugInfo;
+                    return ruleset;
+                } else {
+                    // Backtrack
+                    furthest = i;
+                    restore();
+                }
+            },
+            rule: function () {
+                var name, value, c = input.charAt(i), important, match;
+                save();
+
+                if (c === '.' || c === '#' || c === '&') { return }
+
+                if (name = $(this.variable) || $(this.property)) {
+                    if (!env.compress && (name.charAt(0) != '@') && (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j]))) {
+                        i += match[0].length - 1;
+                        value = new(tree.Anonymous)(match[1]);
+                    } else {
+                        value = $(this.value);
+                    }
+                    important = $(this.important);
+
+                    if (value && $(this.end)) {
+                        return new(tree.Rule)(name, value, important, memo);
+                    } else {
+                        furthest = i;
+                        restore();
+                    }
+                }
+            },
+
+            //
+            // An @import directive
+            //
+            //     @import "lib";
+            //
+            // Depending on our environemnt, importing is done differently:
+            // In the browser, it's an XHR request, in Node, it would be a
+            // file-system operation. The function used for importing is
+            // stored in `import`, which we pass to the Import constructor.
+            //
+            "import": function () {
+                var path, features, index = i;
+
+                save();
+
+                var dir = $(/^@import(?:-(once|multiple))?\s+/);
+
+                if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) {
+                    features = $(this.mediaFeatures);
+                    if ($(';')) {
+                        features = features && new(tree.Value)(features);
+                        var importOnce = dir[1] !== 'multiple';
+                        return new(tree.Import)(path, imports, features, importOnce, index, env.rootpath);
+                    }
+                }
+
+                restore();
+            },
+
+            mediaFeature: function () {
+                var e, p, nodes = [];
+
+                do {
+                    if (e = $(this.entities.keyword)) {
+                        nodes.push(e);
+                    } else if ($('(')) {
+                        p = $(this.property);
+                        e = $(this.value);
+                        if ($(')')) {
+                            if (p && e) {
+                                nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, true)));
+                            } else if (e) {
+                                nodes.push(new(tree.Paren)(e));
+                            } else {
+                                return null;
+                            }
+                        } else { return null }
+                    }
+                } while (e);
+
+                if (nodes.length > 0) {
+                    return new(tree.Expression)(nodes);
+                }
+            },
+
+            mediaFeatures: function () {
+                var e, features = [];
+
+                do {
+                  if (e = $(this.mediaFeature)) {
+                      features.push(e);
+                      if (! $(',')) { break }
+                  } else if (e = $(this.entities.variable)) {
+                      features.push(e);
+                      if (! $(',')) { break }
+                  }
+                } while (e);
+
+                return features.length > 0 ? features : null;
+            },
+
+            media: function () {
+                var features, rules, media, debugInfo;
+
+                if (env.dumpLineNumbers)
+                    debugInfo = getDebugInfo(i, input, env);
+
+                if ($(/^@media/)) {
+                    features = $(this.mediaFeatures);
+
+                    if (rules = $(this.block)) {
+                        media = new(tree.Media)(rules, features);
+                        if(env.dumpLineNumbers)
+                            media.debugInfo = debugInfo;
+                        return media;
+                    }
+                }
+            },
+
+            //
+            // A CSS Directive
+            //
+            //     @charset "utf-8";
+            //
+            directive: function () {
+                var name, value, rules, identifier, e, nodes, nonVendorSpecificName,
+                    hasBlock, hasIdentifier, hasExpression;
+
+                if (input.charAt(i) !== '@') return;
+
+                if (value = $(this['import']) || $(this.media)) {
+                    return value;
+                }
+
+                save();
+
+                name = $(/^@[a-z-]+/);
+
+                if (!name) return;
+
+                nonVendorSpecificName = name;
+                if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {
+                    nonVendorSpecificName = "@" + name.slice(name.indexOf('-', 2) + 1);
+                }
+
+                switch(nonVendorSpecificName) {
+                    case "@font-face":
+                        hasBlock = true;
+                        break;
+                    case "@viewport":
+                    case "@top-left":
+                    case "@top-left-corner":
+                    case "@top-center":
+                    case "@top-right":
+                    case "@top-right-corner":
+                    case "@bottom-left":
+                    case "@bottom-left-corner":
+                    case "@bottom-center":
+                    case "@bottom-right":
+                    case "@bottom-right-corner":
+                    case "@left-top":
+                    case "@left-middle":
+                    case "@left-bottom":
+                    case "@right-top":
+                    case "@right-middle":
+                    case "@right-bottom":
+                        hasBlock = true;
+                        break;
+                    case "@page":
+                    case "@document":
+                    case "@supports":
+                    case "@keyframes":
+                        hasBlock = true;
+                        hasIdentifier = true;
+                        break;
+                    case "@namespace":
+                        hasExpression = true;
+                        break;
+                }
+
+                if (hasIdentifier) {
+                    name += " " + ($(/^[^{]+/) || '').trim();
+                }
+
+                if (hasBlock)
+                {
+                    if (rules = $(this.block)) {
+                        return new(tree.Directive)(name, rules);
+                    }
+                } else {
+                    if ((value = hasExpression ? $(this.expression) : $(this.entity)) && $(';')) {
+                        var directive = new(tree.Directive)(name, value);
+                        if (env.dumpLineNumbers) {
+                            directive.debugInfo = getDebugInfo(i, input, env);
+                        }
+                        return directive;
+                    }
+                }
+
+                restore();
+            },
+
+            //
+            // A Value is a comma-delimited list of Expressions
+            //
+            //     font-family: Baskerville, Georgia, serif;
+            //
+            // In a Rule, a Value represents everything after the `:`,
+            // and before the `;`.
+            //
+            value: function () {
+                var e, expressions = [], important;
+
+                while (e = $(this.expression)) {
+                    expressions.push(e);
+                    if (! $(',')) { break }
+                }
+
+                if (expressions.length > 0) {
+                    return new(tree.Value)(expressions);
+                }
+            },
+            important: function () {
+                if (input.charAt(i) === '!') {
+                    return $(/^! *important/);
+                }
+            },
+            sub: function () {
+                var a, e;
+
+                if ($('(')) {
+                    if (a = $(this.addition)) {
+                        e = new(tree.Expression)([a]);
+                        expect(')');
+                        e.parens = true;
+                        return e;
+                    }
+                }
+            },
+            multiplication: function () {
+                var m, a, op, operation, isSpaced, expression = [];
+                if (m = $(this.operand)) {
+                    isSpaced = isWhitespace(input.charAt(i - 1));
+                    while (!peek(/^\/[*\/]/) && (op = ($('/') || $('*')))) {
+                        if (a = $(this.operand)) {
+                            m.parensInOp = true;
+                            a.parensInOp = true;
+                            operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
+                            isSpaced = isWhitespace(input.charAt(i - 1));
+                        } else {
+                            break;
+                        }
+                    }
+                    return operation || m;
+                }
+            },
+            addition: function () {
+                var m, a, op, operation, isSpaced;
+                if (m = $(this.multiplication)) {
+                    isSpaced = isWhitespace(input.charAt(i - 1));
+                    while ((op = $(/^[-+]\s+/) || (!isSpaced && ($('+') || $('-')))) &&
+                           (a = $(this.multiplication))) {
+                        m.parensInOp = true;
+                        a.parensInOp = true;
+                        operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
+                        isSpaced = isWhitespace(input.charAt(i - 1));
+                    }
+                    return operation || m;
+                }
+            },
+            conditions: function () {
+                var a, b, index = i, condition;
+
+                if (a = $(this.condition)) {
+                    while ($(',') && (b = $(this.condition))) {
+                        condition = new(tree.Condition)('or', condition || a, b, index);
+                    }
+                    return condition || a;
+                }
+            },
+            condition: function () {
+                var a, b, c, op, index = i, negate = false;
+
+                if ($(/^not/)) { negate = true }
+                expect('(');
+                if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) {
+                    if (op = $(/^(?:>=|=<|[<=>])/)) {
+                        if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) {
+                            c = new(tree.Condition)(op, a, b, index, negate);
+                        } else {
+                            error('expected expression');
+                        }
+                    } else {
+                        c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate);
+                    }
+                    expect(')');
+                    return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c;
+                }
+            },
+
+            //
+            // An operand is anything that can be part of an operation,
+            // such as a Color, or a Variable
+            //
+            operand: function () {
+                var negate, p = input.charAt(i + 1);
+
+                if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') }
+                var o = $(this.sub) || $(this.entities.dimension) ||
+                        $(this.entities.color) || $(this.entities.variable) ||
+                        $(this.entities.call);
+
+                if (negate) {
+                    o.parensInOp = true;
+                    o = new(tree.Negative)(o);
+                }
+
+                return o;
+            },
+
+            //
+            // Expressions either represent mathematical operations,
+            // or white-space delimited Entities.
+            //
+            //     1px solid black
+            //     @var * 2
+            //
+            expression: function () {
+                var e, delim, entities = [], d;
+
+                while (e = $(this.addition) || $(this.entity)) {
+                    entities.push(e);
+                    // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
+                    if (!peek(/^\/[\/*]/) && (delim = $('/'))) {
+                        entities.push(new(tree.Anonymous)(delim));
+                    }
+                }
+                if (entities.length > 0) {
+                    return new(tree.Expression)(entities);
+                }
+            },
+            property: function () {
+                var name;
+
+                if (name = $(/^(\*?-?[_a-z0-9-]+)\s*:/)) {
+                    return name[1];
+                }
+            }
+        }
+    };
+};
+
+if (less.mode === 'browser' || less.mode === 'rhino') {
+    //
+    // Used by `@import` directives
+    //
+    less.Parser.importer = function (path, paths, callback, env) {
+        if (!/^([a-z-]+:)?\//.test(path) && paths.length > 0) {
+            path = paths[0] + path;
+        }
+        // We pass `true` as 3rd argument, to force the reload of the import.
+        // This is so we can get the syntax tree as opposed to just the CSS output,
+        // as we need this to evaluate the current stylesheet.
+        loadStyleSheet(env.toSheet(path),
+            function (e, root, data, sheet, _, path) {
+                callback.call(null, e, root, path);
+            }, true);
+    };
+}
+
+(function (tree) {
+
+tree.functions = {
+    rgb: function (r, g, b) {
+        return this.rgba(r, g, b, 1.0);
+    },
+    rgba: function (r, g, b, a) {
+        var rgb = [r, g, b].map(function (c) { return scaled(c, 256); });
+        a = number(a);
+        return new(tree.Color)(rgb, a);
+    },
+    hsl: function (h, s, l) {
+        return this.hsla(h, s, l, 1.0);
+    },
+    hsla: function (h, s, l, a) {
+        h = (number(h) % 360) / 360;
+        s = number(s); l = number(l); a = number(a);
+
+        var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
+        var m1 = l * 2 - m2;
+
+        return this.rgba(hue(h + 1/3) * 255,
+                         hue(h)       * 255,
+                         hue(h - 1/3) * 255,
+                         a);
+
+        function hue(h) {
+            h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);
+            if      (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+            else if (h * 2 < 1) return m2;
+            else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6;
+            else                return m1;
+        }
+    },
+
+    hsv: function(h, s, v) {
+        return this.hsva(h, s, v, 1.0);
+    },
+
+    hsva: function(h, s, v, a) {
+        h = ((number(h) % 360) / 360) * 360;
+        s = number(s); v = number(v); a = number(a);
+
+        var i, f;
+        i = Math.floor((h / 60) % 6);
+        f = (h / 60) - i;
+
+        var vs = [v,
+                  v * (1 - s),
+                  v * (1 - f * s),
+                  v * (1 - (1 - f) * s)];
+        var perm = [[0, 3, 1],
+                    [2, 0, 1],
+                    [1, 0, 3],
+                    [1, 2, 0],
+                    [3, 1, 0],
+                    [0, 1, 2]];
+
+        return this.rgba(vs[perm[i][0]] * 255,
+                         vs[perm[i][1]] * 255,
+                         vs[perm[i][2]] * 255,
+                         a);
+    },
+
+    hue: function (color) {
+        return new(tree.Dimension)(Math.round(color.toHSL().h));
+    },
+    saturation: function (color) {
+        return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%');
+    },
+    lightness: function (color) {
+        return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%');
+    },
+    hsvhue: function(color) {
+        return new(tree.Dimension)(Math.round(color.toHSV().h));
+    },
+    hsvsaturation: function (color) {
+        return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%');
+    },
+    hsvvalue: function (color) {
+        return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%');
+    },
+    red: function (color) {
+        return new(tree.Dimension)(color.rgb[0]);
+    },
+    green: function (color) {
+        return new(tree.Dimension)(color.rgb[1]);
+    },
+    blue: function (color) {
+        return new(tree.Dimension)(color.rgb[2]);
+    },
+    alpha: function (color) {
+        return new(tree.Dimension)(color.toHSL().a);
+    },
+    luma: function (color) {
+        return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%');
+    },
+    saturate: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.s += amount.value / 100;
+        hsl.s = clamp(hsl.s);
+        return hsla(hsl);
+    },
+    desaturate: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.s -= amount.value / 100;
+        hsl.s = clamp(hsl.s);
+        return hsla(hsl);
+    },
+    lighten: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.l += amount.value / 100;
+        hsl.l = clamp(hsl.l);
+        return hsla(hsl);
+    },
+    darken: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.l -= amount.value / 100;
+        hsl.l = clamp(hsl.l);
+        return hsla(hsl);
+    },
+    fadein: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.a += amount.value / 100;
+        hsl.a = clamp(hsl.a);
+        return hsla(hsl);
+    },
+    fadeout: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.a -= amount.value / 100;
+        hsl.a = clamp(hsl.a);
+        return hsla(hsl);
+    },
+    fade: function (color, amount) {
+        var hsl = color.toHSL();
+
+        hsl.a = amount.value / 100;
+        hsl.a = clamp(hsl.a);
+        return hsla(hsl);
+    },
+    spin: function (color, amount) {
+        var hsl = color.toHSL();
+        var hue = (hsl.h + amount.value) % 360;
+
+        hsl.h = hue < 0 ? 360 + hue : hue;
+
+        return hsla(hsl);
+    },
+    //
+    // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein
+    // http://sass-lang.com
+    //
+    mix: function (color1, color2, weight) {
+        if (!weight) {
+            weight = new(tree.Dimension)(50);
+        }
+        var p = weight.value / 100.0;
+        var w = p * 2 - 1;
+        var a = color1.toHSL().a - color2.toHSL().a;
+
+        var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
+        var w2 = 1 - w1;
+
+        var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,
+                   color1.rgb[1] * w1 + color2.rgb[1] * w2,
+                   color1.rgb[2] * w1 + color2.rgb[2] * w2];
+
+        var alpha = color1.alpha * p + color2.alpha * (1 - p);
+
+        return new(tree.Color)(rgb, alpha);
+    },
+    greyscale: function (color) {
+        return this.desaturate(color, new(tree.Dimension)(100));
+    },
+    contrast: function (color, dark, light, threshold) {
+        // filter: contrast(3.2);
+        // should be kept as is, so check for color
+        if (!color.rgb) {
+            return null;
+        }
+        if (typeof light === 'undefined') {
+            light = this.rgba(255, 255, 255, 1.0);
+        }
+        if (typeof dark === 'undefined') {
+            dark = this.rgba(0, 0, 0, 1.0);
+        }
+        //Figure out which is actually light and dark!
+        if (dark.luma() > light.luma()) {
+            var t = light;
+            light = dark;
+            dark = t;
+        }
+        if (typeof threshold === 'undefined') {
+            threshold = 0.43;
+        } else {
+            threshold = number(threshold);
+        }
+        if ((color.luma() * color.alpha) < threshold) {
+            return light;
+        } else {
+            return dark;
+        }
+    },
+    e: function (str) {
+        return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);
+    },
+    escape: function (str) {
+        return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29"));
+    },
+    '%': function (quoted /* arg, arg, ...*/) {
+        var args = Array.prototype.slice.call(arguments, 1),
+            str = quoted.value;
+
+        for (var i = 0; i < args.length; i++) {
+            str = str.replace(/%[sda]/i, function(token) {
+                var value = token.match(/s/i) ? args[i].value : args[i].toCSS();
+                return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value;
+            });
+        }
+        str = str.replace(/%%/g, '%');
+        return new(tree.Quoted)('"' + str + '"', str);
+    },
+    unit: function (val, unit) {
+        return new(tree.Dimension)(val.value, unit ? unit.toCSS() : "");
+    },
+    convert: function (val, unit) {
+        return val.convertTo(unit.value);
+    },
+    round: function (n, f) {
+        var fraction = typeof(f) === "undefined" ? 0 : f.value;
+        return this._math(function(num) { return num.toFixed(fraction); }, null, n);
+    },
+    pi: function () {
+        return new(tree.Dimension)(Math.PI);
+    },
+    mod: function(a, b) {
+        return new(tree.Dimension)(a.value % b.value, a.unit);
+    },
+    pow: function(x, y) {
+        if (typeof x === "number" && typeof y === "number") {
+            x = new(tree.Dimension)(x);
+            y = new(tree.Dimension)(y);
+        } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) {
+            throw { type: "Argument", message: "arguments must be numbers" };
+        }
+
+        return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit);
+    },
+    _math: function (fn, unit, n) {
+        if (n instanceof tree.Dimension) {
+            return new(tree.Dimension)(fn(parseFloat(n.value)), unit == null ? n.unit : unit);
+        } else if (typeof(n) === 'number') {
+            return fn(n);
+        } else {
+            throw { type: "Argument", message: "argument must be a number" };
+        }
+    },
+    argb: function (color) {
+        return new(tree.Anonymous)(color.toARGB());
+
+    },
+    percentage: function (n) {
+        return new(tree.Dimension)(n.value * 100, '%');
+    },
+    color: function (n) {
+        if (n instanceof tree.Quoted) {
+            return new(tree.Color)(n.value.slice(1));
+        } else {
+            throw { type: "Argument", message: "argument must be a string" };
+        }
+    },
+    iscolor: function (n) {
+        return this._isa(n, tree.Color);
+    },
+    isnumber: function (n) {
+        return this._isa(n, tree.Dimension);
+    },
+    isstring: function (n) {
+        return this._isa(n, tree.Quoted);
+    },
+    iskeyword: function (n) {
+        return this._isa(n, tree.Keyword);
+    },
+    isurl: function (n) {
+        return this._isa(n, tree.URL);
+    },
+    ispixel: function (n) {
+        return (n instanceof tree.Dimension) && n.unit.is('px') ? tree.True : tree.False;
+    },
+    ispercentage: function (n) {
+        return (n instanceof tree.Dimension) && n.unit.is('%') ? tree.True : tree.False;
+    },
+    isem: function (n) {
+        return (n instanceof tree.Dimension) && n.unit.is('em') ? tree.True : tree.False;
+    },
+    _isa: function (n, Type) {
+        return (n instanceof Type) ? tree.True : tree.False;
+    },
+
+    /* Blending modes */
+
+    multiply: function(color1, color2) {
+        var r = color1.rgb[0] * color2.rgb[0] / 255;
+        var g = color1.rgb[1] * color2.rgb[1] / 255;
+        var b = color1.rgb[2] * color2.rgb[2] / 255;
+        return this.rgb(r, g, b);
+    },
+    screen: function(color1, color2) {
+        var r = 255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255;
+        var g = 255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255;
+        var b = 255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255;
+        return this.rgb(r, g, b);
+    },
+    overlay: function(color1, color2) {
+        var r = color1.rgb[0] < 128 ? 2 * color1.rgb[0] * color2.rgb[0] / 255 : 255 - 2 * (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255;
+        var g = color1.rgb[1] < 128 ? 2 * color1.rgb[1] * color2.rgb[1] / 255 : 255 - 2 * (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255;
+        var b = color1.rgb[2] < 128 ? 2 * color1.rgb[2] * color2.rgb[2] / 255 : 255 - 2 * (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255;
+        return this.rgb(r, g, b);
+    },
+    softlight: function(color1, color2) {
+        var t = color2.rgb[0] * color1.rgb[0] / 255;
+        var r = t + color1.rgb[0] * (255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255 - t) / 255;
+        t = color2.rgb[1] * color1.rgb[1] / 255;
+        var g = t + color1.rgb[1] * (255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255 - t) / 255;
+        t = color2.rgb[2] * color1.rgb[2] / 255;
+        var b = t + color1.rgb[2] * (255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255 - t) / 255;
+        return this.rgb(r, g, b);
+    },
+    hardlight: function(color1, color2) {
+        var r = color2.rgb[0] < 128 ? 2 * color2.rgb[0] * color1.rgb[0] / 255 : 255 - 2 * (255 - color2.rgb[0]) * (255 - color1.rgb[0]) / 255;
+        var g = color2.rgb[1] < 128 ? 2 * color2.rgb[1] * color1.rgb[1] / 255 : 255 - 2 * (255 - color2.rgb[1]) * (255 - color1.rgb[1]) / 255;
+        var b = color2.rgb[2] < 128 ? 2 * color2.rgb[2] * color1.rgb[2] / 255 : 255 - 2 * (255 - color2.rgb[2]) * (255 - color1.rgb[2]) / 255;
+        return this.rgb(r, g, b);
+    },
+    difference: function(color1, color2) {
+        var r = Math.abs(color1.rgb[0] - color2.rgb[0]);
+        var g = Math.abs(color1.rgb[1] - color2.rgb[1]);
+        var b = Math.abs(color1.rgb[2] - color2.rgb[2]);
+        return this.rgb(r, g, b);
+    },
+    exclusion: function(color1, color2) {
+        var r = color1.rgb[0] + color2.rgb[0] * (255 - color1.rgb[0] - color1.rgb[0]) / 255;
+        var g = color1.rgb[1] + color2.rgb[1] * (255 - color1.rgb[1] - color1.rgb[1]) / 255;
+        var b = color1.rgb[2] + color2.rgb[2] * (255 - color1.rgb[2] - color1.rgb[2]) / 255;
+        return this.rgb(r, g, b);
+    },
+    average: function(color1, color2) {
+        var r = (color1.rgb[0] + color2.rgb[0]) / 2;
+        var g = (color1.rgb[1] + color2.rgb[1]) / 2;
+        var b = (color1.rgb[2] + color2.rgb[2]) / 2;
+        return this.rgb(r, g, b);
+    },
+    negation: function(color1, color2) {
+        var r = 255 - Math.abs(255 - color2.rgb[0] - color1.rgb[0]);
+        var g = 255 - Math.abs(255 - color2.rgb[1] - color1.rgb[1]);
+        var b = 255 - Math.abs(255 - color2.rgb[2] - color1.rgb[2]);
+        return this.rgb(r, g, b);
+    },
+    tint: function(color, amount) {
+        return this.mix(this.rgb(255,255,255), color, amount);
+    },
+    shade: function(color, amount) {
+        return this.mix(this.rgb(0, 0, 0), color, amount);
+    },
+    extract: function(values, index) {
+        index = index.value - 1; // (1-based index)
+        return values.value[index];
+    },
+
+    "data-uri": function(mimetypeNode, filePathNode) {
+
+        if (typeof window !== 'undefined') {
+            return new tree.URL(filePathNode || mimetypeNode, this.rootpath).eval(this.env);
+        }
+
+        var mimetype = mimetypeNode.value;
+        var filePath = (filePathNode && filePathNode.value);
+
+        var fs = require("fs"),
+            path = require("path"),
+            useBase64 = false;
+
+        if (arguments.length < 2) {
+            filePath = mimetype;
+        }
+
+        if (this.currentDirectory && this.env.isPathRelative(filePath)) {
+            filePath = path.join(this.currentDirectory, filePath);
+        }
+
+        // detect the mimetype if not given
+        if (arguments.length < 2) {
+            var mime;
+            try {
+                mime = require('mime');
+            } catch (ex) {
+                mime = tree._mime;
+            }
+
+            mimetype = mime.lookup(filePath);
+
+            // use base 64 unless it's an ASCII or UTF-8 format
+            var charset = mime.charsets.lookup(mimetype);
+            useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0;
+            if (useBase64) mimetype += ';base64';
+        }
+        else {
+            useBase64 = /;base64$/.test(mimetype)
+        }
+
+        var buf = fs.readFileSync(filePath);
+
+        // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded
+        // and the --ieCompat flag is enabled, return a normal url() instead.
+        var DATA_URI_MAX_KB = 32,
+            fileSizeInKB = parseInt((buf.length / 1024), 10);
+        if (fileSizeInKB >= DATA_URI_MAX_KB) {
+            // the url() must be relative, not an absolute file path
+            filePath = path.relative(this.currentDirectory, filePath);
+
+            if (this.env.ieCompat !== false) {
+                if (!this.env.silent) {
+                    console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB);
+                }
+
+                return new tree.URL(filePathNode || mimetypeNode, this.rootpath).eval(this.env);
+            } else if (!this.env.silent) {
+                // if explicitly disabled (via --no-ie-compat on CLI, or env.ieCompat === false), merely warn
+                console.warn("WARNING: Embedding %s (%dKB) exceeds IE8's data-uri size limit of %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB);
+            }
+        }
+
+        buf = useBase64 ? buf.toString('base64')
+                        : encodeURIComponent(buf);
+
+        var uri = "'data:" + mimetype + ',' + buf + "'";
+        return new(tree.URL)(new(tree.Anonymous)(uri));
+    }
+};
+
+// these static methods are used as a fallback when the optional 'mime' dependency is missing
+tree._mime = {
+    // this map is intentionally incomplete
+    // if you want more, install 'mime' dep
+    _types: {
+        '.htm' : 'text/html',
+        '.html': 'text/html',
+        '.gif' : 'image/gif',
+        '.jpg' : 'image/jpeg',
+        '.jpeg': 'image/jpeg',
+        '.png' : 'image/png'
+    },
+    lookup: function (filepath) {
+        var ext = require('path').extname(filepath),
+            type = tree._mime._types[ext];
+        if (type === undefined) {
+            throw new Error('Optional dependency "mime" is required for ' + ext);
+        }
+        return type;
+    },
+    charsets: {
+        lookup: function (type) {
+            // assumes all text types are UTF-8
+            return type && (/^text\//).test(type) ? 'UTF-8' : '';
+        }
+    }
+};
+
+var mathFunctions = [{name:"ceil"}, {name:"floor"}, {name: "sqrt"}, {name:"abs"},
+        {name:"tan", unit: ""}, {name:"sin", unit: ""}, {name:"cos", unit: ""},
+        {name:"atan", unit: "rad"}, {name:"asin", unit: "rad"}, {name:"acos", unit: "rad"}],
+    createMathFunction = function(name, unit) {
+        return function(n) {
+            if (unit != null) {
+                n = n.unify();
+            }
+            return this._math(Math[name], unit, n);
+        };
+    };
+
+for(var i = 0; i < mathFunctions.length; i++) {
+    tree.functions[mathFunctions[i].name] = createMathFunction(mathFunctions[i].name, mathFunctions[i].unit);
+}
+
+function hsla(color) {
+    return tree.functions.hsla(color.h, color.s, color.l, color.a);
+}
+
+function scaled(n, size) {
+    if (n instanceof tree.Dimension && n.unit.is('%')) {
+        return parseFloat(n.value * size / 100);
+    } else {
+        return number(n);
+    }
+}
+
+function number(n) {
+    if (n instanceof tree.Dimension) {
+        return parseFloat(n.unit.is('%') ? n.value / 100 : n.value);
+    } else if (typeof(n) === 'number') {
+        return n;
+    } else {
+        throw {
+            error: "RuntimeError",
+            message: "color functions take numbers as parameters"
+        };
+    }
+}
+
+function clamp(val) {
+    return Math.min(1, Math.max(0, val));
+}
+
+tree.functionCall = function(env, rootpath, currentDirectory) {
+    this.env = env;
+    this.rootpath = rootpath;
+    this.currentDirectory = currentDirectory;
+};
+
+tree.functionCall.prototype = tree.functions;
+
+})(require('./tree'));
+(function (tree) {
+    tree.colors = {
+        'aliceblue':'#f0f8ff',
+        'antiquewhite':'#faebd7',
+        'aqua':'#00ffff',
+        'aquamarine':'#7fffd4',
+        'azure':'#f0ffff',
+        'beige':'#f5f5dc',
+        'bisque':'#ffe4c4',
+        'black':'#000000',
+        'blanchedalmond':'#ffebcd',
+        'blue':'#0000ff',
+        'blueviolet':'#8a2be2',
+        'brown':'#a52a2a',
+        'burlywood':'#deb887',
+        'cadetblue':'#5f9ea0',
+        'chartreuse':'#7fff00',
+        'chocolate':'#d2691e',
+        'coral':'#ff7f50',
+        'cornflowerblue':'#6495ed',
+        'cornsilk':'#fff8dc',
+        'crimson':'#dc143c',
+        'cyan':'#00ffff',
+        'darkblue':'#00008b',
+        'darkcyan':'#008b8b',
+        'darkgoldenrod':'#b8860b',
+        'darkgray':'#a9a9a9',
+        'darkgrey':'#a9a9a9',
+        'darkgreen':'#006400',
+        'darkkhaki':'#bdb76b',
+        'darkmagenta':'#8b008b',
+        'darkolivegreen':'#556b2f',
+        'darkorange':'#ff8c00',
+        'darkorchid':'#9932cc',
+        'darkred':'#8b0000',
+        'darksalmon':'#e9967a',
+        'darkseagreen':'#8fbc8f',
+        'darkslateblue':'#483d8b',
+        'darkslategray':'#2f4f4f',
+        'darkslategrey':'#2f4f4f',
+        'darkturquoise':'#00ced1',
+        'darkviolet':'#9400d3',
+        'deeppink':'#ff1493',
+        'deepskyblue':'#00bfff',
+        'dimgray':'#696969',
+        'dimgrey':'#696969',
+        'dodgerblue':'#1e90ff',
+        'firebrick':'#b22222',
+        'floralwhite':'#fffaf0',
+        'forestgreen':'#228b22',
+        'fuchsia':'#ff00ff',
+        'gainsboro':'#dcdcdc',
+        'ghostwhite':'#f8f8ff',
+        'gold':'#ffd700',
+        'goldenrod':'#daa520',
+        'gray':'#808080',
+        'grey':'#808080',
+        'green':'#008000',
+        'greenyellow':'#adff2f',
+        'honeydew':'#f0fff0',
+        'hotpink':'#ff69b4',
+        'indianred':'#cd5c5c',
+        'indigo':'#4b0082',
+        'ivory':'#fffff0',
+        'khaki':'#f0e68c',
+        'lavender':'#e6e6fa',
+        'lavenderblush':'#fff0f5',
+        'lawngreen':'#7cfc00',
+        'lemonchiffon':'#fffacd',
+        'lightblue':'#add8e6',
+        'lightcoral':'#f08080',
+        'lightcyan':'#e0ffff',
+        'lightgoldenrodyellow':'#fafad2',
+        'lightgray':'#d3d3d3',
+        'lightgrey':'#d3d3d3',
+        'lightgreen':'#90ee90',
+        'lightpink':'#ffb6c1',
+        'lightsalmon':'#ffa07a',
+        'lightseagreen':'#20b2aa',
+        'lightskyblue':'#87cefa',
+        'lightslategray':'#778899',
+        'lightslategrey':'#778899',
+        'lightsteelblue':'#b0c4de',
+        'lightyellow':'#ffffe0',
+        'lime':'#00ff00',
+        'limegreen':'#32cd32',
+        'linen':'#faf0e6',
+        'magenta':'#ff00ff',
+        'maroon':'#800000',
+        'mediumaquamarine':'#66cdaa',
+        'mediumblue':'#0000cd',
+        'mediumorchid':'#ba55d3',
+        'mediumpurple':'#9370d8',
+        'mediumseagreen':'#3cb371',
+        'mediumslateblue':'#7b68ee',
+        'mediumspringgreen':'#00fa9a',
+        'mediumturquoise':'#48d1cc',
+        'mediumvioletred':'#c71585',
+        'midnightblue':'#191970',
+        'mintcream':'#f5fffa',
+        'mistyrose':'#ffe4e1',
+        'moccasin':'#ffe4b5',
+        'navajowhite':'#ffdead',
+        'navy':'#000080',
+        'oldlace':'#fdf5e6',
+        'olive':'#808000',
+        'olivedrab':'#6b8e23',
+        'orange':'#ffa500',
+        'orangered':'#ff4500',
+        'orchid':'#da70d6',
+        'palegoldenrod':'#eee8aa',
+        'palegreen':'#98fb98',
+        'paleturquoise':'#afeeee',
+        'palevioletred':'#d87093',
+        'papayawhip':'#ffefd5',
+        'peachpuff':'#ffdab9',
+        'peru':'#cd853f',
+        'pink':'#ffc0cb',
+        'plum':'#dda0dd',
+        'powderblue':'#b0e0e6',
+        'purple':'#800080',
+        'red':'#ff0000',
+        'rosybrown':'#bc8f8f',
+        'royalblue':'#4169e1',
+        'saddlebrown':'#8b4513',
+        'salmon':'#fa8072',
+        'sandybrown':'#f4a460',
+        'seagreen':'#2e8b57',
+        'seashell':'#fff5ee',
+        'sienna':'#a0522d',
+        'silver':'#c0c0c0',
+        'skyblue':'#87ceeb',
+        'slateblue':'#6a5acd',
+        'slategray':'#708090',
+        'slategrey':'#708090',
+        'snow':'#fffafa',
+        'springgreen':'#00ff7f',
+        'steelblue':'#4682b4',
+        'tan':'#d2b48c',
+        'teal':'#008080',
+        'thistle':'#d8bfd8',
+        'tomato':'#ff6347',
+        // 'transparent':'rgba(0,0,0,0)',
+        'turquoise':'#40e0d0',
+        'violet':'#ee82ee',
+        'wheat':'#f5deb3',
+        'white':'#ffffff',
+        'whitesmoke':'#f5f5f5',
+        'yellow':'#ffff00',
+        'yellowgreen':'#9acd32'
+    };
+})(require('./tree'));
+(function (tree) {
+
+tree.Alpha = function (val) {
+    this.value = val;
+};
+tree.Alpha.prototype = {
+    toCSS: function () {
+        return "alpha(opacity=" +
+               (this.value.toCSS ? this.value.toCSS() : this.value) + ")";
+    },
+    eval: function (env) {
+        if (this.value.eval) { this.value = this.value.eval(env) }
+        return this;
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Anonymous = function (string) {
+    this.value = string.value || string;
+};
+tree.Anonymous.prototype = {
+    toCSS: function () {
+        return this.value;
+    },
+    eval: function () { return this },
+    compare: function (x) {
+        if (!x.toCSS) {
+            return -1;
+        }
+
+        var left = this.toCSS(),
+            right = x.toCSS();
+
+        if (left === right) {
+            return 0;
+        }
+
+        return left < right ? -1 : 1;
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Assignment = function (key, val) {
+    this.key = key;
+    this.value = val;
+};
+tree.Assignment.prototype = {
+    toCSS: function () {
+        return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value);
+    },
+    eval: function (env) {
+        if (this.value.eval) {
+            return new(tree.Assignment)(this.key, this.value.eval(env));
+        }
+        return this;
+    }
+};
+
+})(require('../tree'));(function (tree) {
+
+//
+// A function call node.
+//
+tree.Call = function (name, args, index, filename, rootpath, currentDirectory) {
+    this.name = name;
+    this.args = args;
+    this.index = index;
+    this.filename = filename;
+    this.rootpath = rootpath;
+    this.currentDirectory = currentDirectory;
+};
+tree.Call.prototype = {
+    //
+    // When evaluating a function call,
+    // we either find the function in `tree.functions` [1],
+    // in which case we call it, passing the  evaluated arguments,
+    // if this returns null or we cannot find the function, we
+    // simply print it out as it appeared originally [2].
+    //
+    // The *functions.js* file contains the built-in functions.
+    //
+    // The reason why we evaluate the arguments, is in the case where
+    // we try to pass a variable to a function, like: `saturate(@color)`.
+    // The function should receive the value, not the variable.
+    //
+    eval: function (env) {
+        var args = this.args.map(function (a) { return a.eval(env); }),
+            nameLC = this.name.toLowerCase(),
+            result, func;
+
+        if (nameLC in tree.functions) { // 1.
+            try {
+                func = new tree.functionCall(env, this.rootpath, this.currentDirectory);
+                result = func[nameLC].apply(func, args);
+                if (result != null) {
+                    return result;
+                }
+            } catch (e) {
+                throw { type: e.type || "Runtime",
+                        message: "error evaluating function `" + this.name + "`" +
+                                 (e.message ? ': ' + e.message : ''),
+                        index: this.index, filename: this.filename };
+            }
+        }
+
+        // 2.
+        return new(tree.Anonymous)(this.name +
+            "(" + args.map(function (a) { return a.toCSS(env); }).join(', ') + ")");
+    },
+
+    toCSS: function (env) {
+        return this.eval(env).toCSS();
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+//
+// RGB Colors - #ff0014, #eee
+//
+tree.Color = function (rgb, a) {
+    //
+    // The end goal here, is to parse the arguments
+    // into an integer triplet, such as `128, 255, 0`
+    //
+    // This facilitates operations and conversions.
+    //
+    if (Array.isArray(rgb)) {
+        this.rgb = rgb;
+    } else if (rgb.length == 6) {
+        this.rgb = rgb.match(/.{2}/g).map(function (c) {
+            return parseInt(c, 16);
+        });
+    } else {
+        this.rgb = rgb.split('').map(function (c) {
+            return parseInt(c + c, 16);
+        });
+    }
+    this.alpha = typeof(a) === 'number' ? a : 1;
+};
+tree.Color.prototype = {
+    eval: function () { return this },
+    luma: function () { return (0.2126 * this.rgb[0] / 255) + (0.7152 * this.rgb[1] / 255) + (0.0722 * this.rgb[2] / 255); },
+
+    //
+    // If we have some transparency, the only way to represent it
+    // is via `rgba`. Otherwise, we use the hex representation,
+    // which has better compatibility with older browsers.
+    // Values are capped between `0` and `255`, rounded and zero-padded.
+    //
+    toCSS: function (env, doNotCompress) {
+        var compress = env && env.compress && !doNotCompress;
+        if (this.alpha < 1.0) {
+            return "rgba(" + this.rgb.map(function (c) {
+                return Math.round(c);
+            }).concat(this.alpha).join(',' + (compress ? '' : ' ')) + ")";
+        } else {
+            var color = this.rgb.map(function (i) {
+                i = Math.round(i);
+                i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);
+                return i.length === 1 ? '0' + i : i;
+            }).join('');
+
+            if (compress) {
+                color = color.split('');
+
+                // Convert color to short format
+                if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) {
+                    color = color[0] + color[2] + color[4];
+                } else {
+                    color = color.join('');
+                }
+            }
+
+            return '#' + color;
+        }
+    },
+
+    //
+    // Operations have to be done per-channel, if not,
+    // channels will spill onto each other. Once we have
+    // our result, in the form of an integer triplet,
+    // we create a new Color node to hold the result.
+    //
+    operate: function (env, op, other) {
+        var result = [];
+
+        if (! (other instanceof tree.Color)) {
+            other = other.toColor();
+        }
+
+        for (var c = 0; c < 3; c++) {
+            result[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]);
+        }
+        return new(tree.Color)(result, this.alpha + other.alpha);
+    },
+
+    toHSL: function () {
+        var r = this.rgb[0] / 255,
+            g = this.rgb[1] / 255,
+            b = this.rgb[2] / 255,
+            a = this.alpha;
+
+        var max = Math.max(r, g, b), min = Math.min(r, g, b);
+        var h, s, l = (max + min) / 2, d = max - min;
+
+        if (max === min) {
+            h = s = 0;
+        } else {
+            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+
+            switch (max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2;               break;
+                case b: h = (r - g) / d + 4;               break;
+            }
+            h /= 6;
+        }
+        return { h: h * 360, s: s, l: l, a: a };
+    },
+    //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
+    toHSV: function () {
+        var r = this.rgb[0] / 255,
+            g = this.rgb[1] / 255,
+            b = this.rgb[2] / 255,
+            a = this.alpha;
+
+        var max = Math.max(r, g, b), min = Math.min(r, g, b);
+        var h, s, v = max;
+
+        var d = max - min;
+        if (max === 0) {
+            s = 0;
+        } else {
+            s = d / max;
+        }
+
+        if (max === min) {
+            h = 0;
+        } else {
+            switch(max){
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+            h /= 6;
+        }
+        return { h: h * 360, s: s, v: v, a: a };
+    },
+    toARGB: function () {
+        var argb = [Math.round(this.alpha * 255)].concat(this.rgb);
+        return '#' + argb.map(function (i) {
+            i = Math.round(i);
+            i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);
+            return i.length === 1 ? '0' + i : i;
+        }).join('');
+    },
+    compare: function (x) {
+        if (!x.rgb) {
+            return -1;
+        }
+
+        return (x.rgb[0] === this.rgb[0] &&
+            x.rgb[1] === this.rgb[1] &&
+            x.rgb[2] === this.rgb[2] &&
+            x.alpha === this.alpha) ? 0 : -1;
+    }
+};
+
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Comment = function (value, silent) {
+    this.value = value;
+    this.silent = !!silent;
+};
+tree.Comment.prototype = {
+    toCSS: function (env) {
+        return env.compress ? '' : this.value;
+    },
+    eval: function () { return this }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Condition = function (op, l, r, i, negate) {
+    this.op = op.trim();
+    this.lvalue = l;
+    this.rvalue = r;
+    this.index = i;
+    this.negate = negate;
+};
+tree.Condition.prototype.eval = function (env) {
+    var a = this.lvalue.eval(env),
+        b = this.rvalue.eval(env);
+
+    var i = this.index, result;
+
+    var result = (function (op) {
+        switch (op) {
+            case 'and':
+                return a && b;
+            case 'or':
+                return a || b;
+            default:
+                if (a.compare) {
+                    result = a.compare(b);
+                } else if (b.compare) {
+                    result = b.compare(a);
+                } else {
+                    throw { type: "Type",
+                            message: "Unable to perform comparison",
+                            index: i };
+                }
+                switch (result) {
+                    case -1: return op === '<' || op === '=<';
+                    case  0: return op === '=' || op === '>=' || op === '=<';
+                    case  1: return op === '>' || op === '>=';
+                }
+        }
+    })(this.op);
+    return this.negate ? !result : result;
+};
+
+})(require('../tree'));
+(function (tree) {
+
+//
+// A number with a unit
+//
+tree.Dimension = function (value, unit) {
+    this.value = parseFloat(value);
+    this.unit = (unit && unit instanceof tree.Unit) ? unit :
+      new(tree.Unit)(unit ? [unit] : undefined);
+};
+
+tree.Dimension.prototype = {
+    eval: function (env) {
+        return this;
+    },
+    toColor: function () {
+        return new(tree.Color)([this.value, this.value, this.value]);
+    },
+    toCSS: function (env) {
+        if ((!env || env.strictUnits !== false) && !this.unit.isSingular()) {
+            throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString());
+        }
+
+        var value = this.value,
+            strValue = String(value);
+
+        if (value !== 0 && value < 0.000001 && value > -0.000001) {
+            // would be output 1e-6 etc.
+            strValue = value.toFixed(20).replace(/0+$/, "");
+        }
+
+        if (env && env.compress) {
+            // Zero values doesn't need a unit
+            if (value === 0 && !this.unit.isAngle()) {
+                return strValue;
+            }
+
+            // Float values doesn't need a leading zero
+            if (value > 0 && value < 1) {
+                strValue = (strValue).substr(1);
+            }
+        }
+
+        return this.unit.isEmpty() ? strValue : (strValue + this.unit.toCSS());
+    },
+
+    // In an operation between two Dimensions,
+    // we default to the first Dimension's unit,
+    // so `1px + 2` will yield `3px`.
+    operate: function (env, op, other) {
+        var value = tree.operate(env, op, this.value, other.value),
+            unit = this.unit.clone();
+
+        if (op === '+' || op === '-') {
+            if (unit.numerator.length === 0 && unit.denominator.length === 0) {
+                unit.numerator = other.unit.numerator.slice(0);
+                unit.denominator = other.unit.denominator.slice(0);
+            } else if (other.unit.numerator.length == 0 && unit.denominator.length == 0) {
+                // do nothing
+            } else {
+                other = other.convertTo(this.unit.usedUnits());
+
+                if(env.strictUnits !== false && other.unit.toString() !== unit.toString()) {
+                  throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '" + unit.toString() +
+                    "' and '" + other.unit.toString() + "'.");
+                }
+
+                value = tree.operate(env, op, this.value, other.value);
+            }
+        } else if (op === '*') {
+            unit.numerator = unit.numerator.concat(other.unit.numerator).sort();
+            unit.denominator = unit.denominator.concat(other.unit.denominator).sort();
+            unit.cancel();
+        } else if (op === '/') {
+            unit.numerator = unit.numerator.concat(other.unit.denominator).sort();
+            unit.denominator = unit.denominator.concat(other.unit.numerator).sort();
+            unit.cancel();
+        }
+        return new(tree.Dimension)(value, unit);
+    },
+
+    compare: function (other) {
+        if (other instanceof tree.Dimension) {
+            var a = this.unify(), b = other.unify(),
+                aValue = a.value, bValue = b.value;
+
+            if (bValue > aValue) {
+                return -1;
+            } else if (bValue < aValue) {
+                return 1;
+            } else {
+                if (!b.unit.isEmpty() && a.unit.compare(b) !== 0) {
+                    return -1;
+                }
+                return 0;
+            }
+        } else {
+            return -1;
+        }
+    },
+
+    unify: function () {
+      return this.convertTo({ length: 'm', duration: 's', angle: 'rad' });
+    },
+
+    convertTo: function (conversions) {
+      var value = this.value, unit = this.unit.clone(),
+          i, groupName, group, conversion, targetUnit, derivedConversions = {};
+
+      if (typeof conversions === 'string') {
+          for(i in tree.UnitConversions) {
+              if (tree.UnitConversions[i].hasOwnProperty(conversions)) {
+                  derivedConversions = {};
+                  derivedConversions[i] = conversions;
+              }
+          }
+          conversions = derivedConversions;
+      }
+
+      for (groupName in conversions) {
+        if (conversions.hasOwnProperty(groupName)) {
+          targetUnit = conversions[groupName];
+          group = tree.UnitConversions[groupName];
+
+          unit.map(function (atomicUnit, denominator) {
+            if (group.hasOwnProperty(atomicUnit)) {
+              if (denominator) {
+                value = value / (group[atomicUnit] / group[targetUnit]);
+              } else {
+                value = value * (group[atomicUnit] / group[targetUnit]);
+              }
+
+              return targetUnit;
+            }
+
+            return atomicUnit;
+          });
+        }
+      }
+
+      unit.cancel();
+
+      return new(tree.Dimension)(value, unit);
+    }
+};
+
+// http://www.w3.org/TR/css3-values/#absolute-lengths
+tree.UnitConversions = {
+  length: {
+     'm': 1,
+    'cm': 0.01,
+    'mm': 0.001,
+    'in': 0.0254,
+    'pt': 0.0254 / 72,
+    'pc': 0.0254 / 72 * 12
+  },
+  duration: {
+    's': 1,
+    'ms': 0.001
+  },
+  angle: {
+    'rad': 1/(2*Math.PI),
+    'deg': 1/360,
+    'grad': 1/400,
+    'turn': 1
+  }
+};
+
+tree.Unit = function (numerator, denominator) {
+  this.numerator = numerator ? numerator.slice(0).sort() : [];
+  this.denominator = denominator ? denominator.slice(0).sort() : [];
+};
+
+tree.Unit.prototype = {
+  clone: function () {
+    return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0));
+  },
+
+  toCSS: function () {
+    if (this.numerator.length >= 1) {
+        return this.numerator[0];
+    }
+    if (this.denominator.length >= 1) {
+        return this.denominator[0];
+    }
+    return "";
+  },
+
+  toString: function () {
+      var i, returnStr = this.numerator.join("*");
+      for (i = 0; i < this.denominator.length; i++) {
+          returnStr += "/" + this.denominator[i];
+      }
+      return returnStr;
+  },
+
+  compare: function (other) {
+    return this.is(other.toCSS()) ? 0 : -1;
+  },
+
+  is: function (unitString) {
+    return this.toCSS() === unitString;
+  },
+
+  isAngle: function () {
+    return tree.UnitConversions.angle.hasOwnProperty(this.toCSS());
+  },
+
+  isEmpty: function () {
+    return this.numerator.length == 0 && this.denominator.length == 0;
+  },
+
+  isSingular: function() {
+      return this.numerator.length <= 1 && this.denominator.length == 0;
+  },
+
+  map: function(callback) {
+    var i;
+
+    for (i = 0; i < this.numerator.length; i++) {
+      this.numerator[i] = callback(this.numerator[i], false);
+    }
+
+    for (i = 0; i < this.denominator.length; i++) {
+      this.denominator[i] = callback(this.denominator[i], true);
+    }
+  },
+
+  usedUnits: function() {
+    var group, groupName, result = {};
+
+    for (groupName in tree.UnitConversions) {
+      if (tree.UnitConversions.hasOwnProperty(groupName)) {
+        group = tree.UnitConversions[groupName];
+
+        this.map(function (atomicUnit) {
+          if (group.hasOwnProperty(atomicUnit) && !result[groupName]) {
+            result[groupName] = atomicUnit;
+          }
+
+          return atomicUnit;
+        });
+      }
+    }
+
+    return result;
+  },
+
+  cancel: function () {
+    var counter = {}, atomicUnit, i;
+
+    for (i = 0; i < this.numerator.length; i++) {
+      atomicUnit = this.numerator[i];
+      counter[atomicUnit] = (counter[atomicUnit] || 0) + 1;
+    }
+
+    for (i = 0; i < this.denominator.length; i++) {
+      atomicUnit = this.denominator[i];
+      counter[atomicUnit] = (counter[atomicUnit] || 0) - 1;
+    }
+
+    this.numerator = [];
+    this.denominator = [];
+
+    for (atomicUnit in counter) {
+      if (counter.hasOwnProperty(atomicUnit)) {
+        var count = counter[atomicUnit];
+
+        if (count > 0) {
+          for (i = 0; i < count; i++) {
+            this.numerator.push(atomicUnit);
+          }
+        } else if (count < 0) {
+          for (i = 0; i < -count; i++) {
+            this.denominator.push(atomicUnit);
+          }
+        }
+      }
+    }
+
+    this.numerator.sort();
+    this.denominator.sort();
+  }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Directive = function (name, value) {
+    this.name = name;
+
+    if (Array.isArray(value)) {
+        this.ruleset = new(tree.Ruleset)([], value);
+        this.ruleset.allowImports = true;
+    } else {
+        this.value = value;
+    }
+};
+tree.Directive.prototype = {
+    toCSS: function (ctx, env) {
+        if (this.ruleset) {
+            this.ruleset.root = true;
+            return this.name + (env.compress ? '{' : ' {\n  ') +
+                   this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n  ') +
+                               (env.compress ? '}': '\n}\n');
+        } else {
+            return this.name + ' ' + this.value.toCSS() + ';\n';
+        }
+    },
+    eval: function (env) {
+        var evaldDirective = this;
+        if (this.ruleset) {
+            env.frames.unshift(this);
+            evaldDirective = new(tree.Directive)(this.name);
+            evaldDirective.ruleset = this.ruleset.eval(env);
+            env.frames.shift();
+        }
+        return evaldDirective;
+    },
+    variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },
+    find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },
+    rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Element = function (combinator, value, index) {
+    this.combinator = combinator instanceof tree.Combinator ?
+                      combinator : new(tree.Combinator)(combinator);
+
+    if (typeof(value) === 'string') {
+        this.value = value.trim();
+    } else if (value) {
+        this.value = value;
+    } else {
+        this.value = "";
+    }
+    this.index = index;
+};
+tree.Element.prototype.eval = function (env) {
+    return new(tree.Element)(this.combinator,
+                             this.value.eval ? this.value.eval(env) : this.value,
+                             this.index);
+};
+tree.Element.prototype.toCSS = function (env) {
+  var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);
+  if (value == '' && this.combinator.value.charAt(0) == '&') {
+    return '';
+  } else {
+    return this.combinator.toCSS(env || {}) + value;
+  }
+};
+
+tree.Combinator = function (value) {
+    if (value === ' ') {
+        this.value = ' ';
+    } else {
+        this.value = value ? value.trim() : "";
+    }
+};
+tree.Combinator.prototype.toCSS = function (env) {
+    return {
+        ''  : '',
+        ' ' : ' ',
+        ':' : ' :',
+        '+' : env.compress ? '+' : ' + ',
+        '~' : env.compress ? '~' : ' ~ ',
+        '>' : env.compress ? '>' : ' > ',
+        '|' : env.compress ? '|' : ' | '
+    }[this.value];
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Expression = function (value) { this.value = value; };
+tree.Expression.prototype = {
+    eval: function (env) {
+        var returnValue,
+            inParenthesis = this.parens && !this.parensInOp,
+            doubleParen = false;
+        if (inParenthesis) {
+            env.inParenthesis();
+        }
+        if (this.value.length > 1) {
+            returnValue = new(tree.Expression)(this.value.map(function (e) {
+                return e.eval(env);
+            }));
+        } else if (this.value.length === 1) {
+            if (this.value[0].parens && !this.value[0].parensInOp) {
+                doubleParen = true;
+            }
+            returnValue = this.value[0].eval(env);
+        } else {
+            returnValue = this;
+        }
+        if (inParenthesis) {
+            env.outOfParenthesis();
+        }
+        if (this.parens && this.parensInOp && !(env.isMathsOn()) && !doubleParen) {
+            returnValue = new(tree.Paren)(returnValue);
+        }
+        return returnValue;
+    },
+    toCSS: function (env) {
+        return this.value.map(function (e) {
+            return e.toCSS ? e.toCSS(env) : '';
+        }).join(' ');
+    },
+    throwAwayComments: function () {
+        this.value = this.value.filter(function(v) {
+            return !(v instanceof tree.Comment);
+        });
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Extend = function Extend(elements, index) {
+    this.selector = new(tree.Selector)(elements);
+    this.index = index;
+};
+
+tree.Extend.prototype.eval = function Extend_eval(env, selectors) {
+    var selfSelectors = findSelfSelectors(selectors || env.selectors),
+        targetValue = this.selector.elements[0].value;
+
+    env.frames.forEach(function(frame) {
+        frame.rulesets().forEach(function(rule) {
+            rule.selectors.forEach(function(selector) {
+                selector.elements.forEach(function(element, idx) {
+                    if (element.value === targetValue) {
+                        selfSelectors.forEach(function(_selector) {
+                            _selector.elements[0] = new tree.Element(
+                                element.combinator,
+                                _selector.elements[0].value,
+                                _selector.elements[0].index
+                            );
+                            rule.selectors.push(new tree.Selector(
+                                selector.elements
+                                    .slice(0, idx)
+                                    .concat(_selector.elements)
+                                    .concat(selector.elements.slice(idx + 1))
+                            ));
+                        });
+                    }
+                });
+            });
+        });
+    });
+    return this;
+};
+
+function findSelfSelectors(selectors) {
+    var ret = [];
+
+    (function loop(elem, i) {
+        if (selectors[i] && selectors[i].length) {
+            selectors[i].forEach(function(s) {
+                loop(s.elements.concat(elem), i + 1);
+            });
+        }
+        else {
+            ret.push({ elements: elem });
+        }
+    })([], 0);
+
+    return ret;
+}
+
+
+})(require('../tree'));
+(function (tree) {
+//
+// CSS @import node
+//
+// The general strategy here is that we don't want to wait
+// for the parsing to be completed, before we start importing
+// the file. That's because in the context of a browser,
+// most of the time will be spent waiting for the server to respond.
+//
+// On creation, we push the import path to our import queue, though
+// `import,push`, we also pass it a callback, which it'll call once
+// the file has been fetched, and parsed.
+//
+tree.Import = function (path, imports, features, once, index, rootpath) {
+    var that = this;
+
+    this.once = once;
+    this.index = index;
+    this._path = path;
+    this.features = features;
+    this.rootpath = rootpath;
+
+    // The '.less' extension is optional
+    if (path instanceof tree.Quoted) {
+        this.path = /(\.[a-z]*$)|([\?;].*)$/.test(path.value) ? path.value : path.value + '.less';
+    } else {
+        this.path = path.value.value || path.value;
+    }
+
+    this.css = /css([\?;].*)?$/.test(this.path);
+
+    // Only pre-compile .less files
+    if (! this.css) {
+        imports.push(this.path, function (e, root, imported) {
+            if (e) { e.index = index; }
+            if (imported && that.once) { that.skip = imported; }
+            that.root = root || new(tree.Ruleset)([], []);
+        });
+    }
+};
+
+//
+// The actual import node doesn't return anything, when converted to CSS.
+// The reason is that it's used at the evaluation stage, so that the rules
+// it imports can be treated like any other rules.
+//
+// In `eval`, we make sure all Import nodes get evaluated, recursively, so
+// we end up with a flat structure, which can easily be imported in the parent
+// ruleset.
+//
+tree.Import.prototype = {
+    toCSS: function (env) {
+        var features = this.features ? ' ' + this.features.toCSS(env) : '';
+
+        if (this.css) {
+            // Add the base path if the import is relative
+            if (typeof this._path.value === "string" && !/^(?:[a-z-]+:|\/)/.test(this._path.value)) {
+                this._path.value = this.rootpath + this._path.value;
+            }
+            return "@import " + this._path.toCSS() + features + ';\n';
+        } else {
+            return "";
+        }
+    },
+    eval: function (env) {
+        var ruleset, features = this.features && this.features.eval(env);
+
+        if (this.skip) { return []; }
+
+        if (this.css) {
+            return new(tree.Import)(this._path, null, features, this.once, this.index, this.rootpath);
+        } else {
+            ruleset = new(tree.Ruleset)([], this.root.rules.slice(0));
+
+            ruleset.evalImports(env);
+
+            return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules;
+        }
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.JavaScript = function (string, index, escaped) {
+    this.escaped = escaped;
+    this.expression = string;
+    this.index = index;
+};
+tree.JavaScript.prototype = {
+    eval: function (env) {
+        var result,
+            that = this,
+            context = {};
+
+        var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) {
+            return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env));
+        });
+
+        try {
+            expression = new(Function)('return (' + expression + ')');
+        } catch (e) {
+            throw { message: "JavaScript evaluation error: `" + expression + "`" ,
+                    index: this.index };
+        }
+
+        for (var k in env.frames[0].variables()) {
+            context[k.slice(1)] = {
+                value: env.frames[0].variables()[k].value,
+                toJS: function () {
+                    return this.value.eval(env).toCSS();
+                }
+            };
+        }
+
+        try {
+            result = expression.call(context);
+        } catch (e) {
+            throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" ,
+                    index: this.index };
+        }
+        if (typeof(result) === 'string') {
+            return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index);
+        } else if (Array.isArray(result)) {
+            return new(tree.Anonymous)(result.join(', '));
+        } else {
+            return new(tree.Anonymous)(result);
+        }
+    }
+};
+
+})(require('../tree'));
+
+(function (tree) {
+
+tree.Keyword = function (value) { this.value = value };
+tree.Keyword.prototype = {
+    eval: function () { return this },
+    toCSS: function () { return this.value },
+    compare: function (other) {
+        if (other instanceof tree.Keyword) {
+            return other.value === this.value ? 0 : 1;
+        } else {
+            return -1;
+        }
+    }
+};
+
+tree.True = new(tree.Keyword)('true');
+tree.False = new(tree.Keyword)('false');
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Media = function (value, features) {
+    var selectors = this.emptySelectors();
+
+    this.features = new(tree.Value)(features);
+    this.ruleset = new(tree.Ruleset)(selectors, value);
+    this.ruleset.allowImports = true;
+};
+tree.Media.prototype = {
+    toCSS: function (ctx, env) {
+        var features = this.features.toCSS(env);
+
+        this.ruleset.root = (ctx.length === 0 || ctx[0].multiMedia);
+        return '@media ' + features + (env.compress ? '{' : ' {\n  ') +
+               this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n  ') +
+                           (env.compress ? '}': '\n}\n');
+    },
+    eval: function (env) {
+        if (!env.mediaBlocks) {
+            env.mediaBlocks = [];
+            env.mediaPath = [];
+        }
+
+        var media = new(tree.Media)([], []);
+        if(this.debugInfo) {
+            this.ruleset.debugInfo = this.debugInfo;
+            media.debugInfo = this.debugInfo;
+        }
+        var strictMathsBypass = false;
+        if (env.strictMaths === false) {
+            strictMathsBypass = true;
+            env.strictMaths = true;
+        }
+        try {
+            media.features = this.features.eval(env);
+        }
+        finally {
+            if (strictMathsBypass) {
+                env.strictMaths = false;
+            }
+        }
+
+        env.mediaPath.push(media);
+        env.mediaBlocks.push(media);
+
+        env.frames.unshift(this.ruleset);
+        media.ruleset = this.ruleset.eval(env);
+        env.frames.shift();
+
+        env.mediaPath.pop();
+
+        return env.mediaPath.length === 0 ? media.evalTop(env) :
+                    media.evalNested(env)
+    },
+    variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },
+    find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },
+    rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) },
+    emptySelectors: function() {
+        var el = new(tree.Element)('', '&', 0);
+        return [new(tree.Selector)([el])];
+    },
+
+    evalTop: function (env) {
+        var result = this;
+
+        // Render all dependent Media blocks.
+        if (env.mediaBlocks.length > 1) {
+            var selectors = this.emptySelectors();
+            result = new(tree.Ruleset)(selectors, env.mediaBlocks);
+            result.multiMedia = true;
+        }
+
+        delete env.mediaBlocks;
+        delete env.mediaPath;
+
+        return result;
+    },
+    evalNested: function (env) {
+        var i, value,
+            path = env.mediaPath.concat([this]);
+
+        // Extract the media-query conditions separated with `,` (OR).
+        for (i = 0; i < path.length; i++) {
+            value = path[i].features instanceof tree.Value ?
+                        path[i].features.value : path[i].features;
+            path[i] = Array.isArray(value) ? value : [value];
+        }
+
+        // Trace all permutations to generate the resulting media-query.
+        //
+        // (a, b and c) with nested (d, e) ->
+        //    a and d
+        //    a and e
+        //    b and c and d
+        //    b and c and e
+        this.features = new(tree.Value)(this.permute(path).map(function (path) {
+            path = path.map(function (fragment) {
+                return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment);
+            });
+
+            for(i = path.length - 1; i > 0; i--) {
+                path.splice(i, 0, new(tree.Anonymous)("and"));
+            }
+
+            return new(tree.Expression)(path);
+        }));
+
+        // Fake a tree-node that doesn't output anything.
+        return new(tree.Ruleset)([], []);
+    },
+    permute: function (arr) {
+      if (arr.length === 0) {
+          return [];
+      } else if (arr.length === 1) {
+          return arr[0];
+      } else {
+          var result = [];
+          var rest = this.permute(arr.slice(1));
+          for (var i = 0; i < rest.length; i++) {
+              for (var j = 0; j < arr[0].length; j++) {
+                  result.push([arr[0][j]].concat(rest[i]));
+              }
+          }
+          return result;
+      }
+    },
+    bubbleSelectors: function (selectors) {
+      this.ruleset = new(tree.Ruleset)(selectors.slice(0), [this.ruleset]);
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.mixin = {};
+tree.mixin.Call = function (elements, args, index, filename, important) {
+    this.selector = new(tree.Selector)(elements);
+    this.arguments = args;
+    this.index = index;
+    this.filename = filename;
+    this.important = important;
+};
+tree.mixin.Call.prototype = {
+    eval: function (env) {
+        var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound;
+
+        args = this.arguments && this.arguments.map(function (a) {
+            return { name: a.name, value: a.value.eval(env) };
+        });
+
+        for (i = 0; i < env.frames.length; i++) {
+            if ((mixins = env.frames[i].find(this.selector)).length > 0) {
+                isOneFound = true;
+                for (m = 0; m < mixins.length; m++) {
+                    mixin = mixins[m];
+                    isRecursive = false;
+                    for(f = 0; f < env.frames.length; f++) {
+                        if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) {
+                            isRecursive = true;
+                            break;
+                        }
+                    }
+                    if (isRecursive) {
+                        continue;
+                    }
+                    if (mixin.matchArgs(args, env)) {
+                        if (!mixin.matchCondition || mixin.matchCondition(args, env)) {
+                            try {
+                                Array.prototype.push.apply(
+                                      rules, mixin.eval(env, args, this.important).rules);
+                            } catch (e) {
+                                throw { message: e.message, index: this.index, filename: this.filename, stack: e.stack };
+                            }
+                        }
+                        match = true;
+                    }
+                }
+                if (match) {
+                    return rules;
+                }
+            }
+        }
+        if (isOneFound) {
+            throw { type:    'Runtime',
+                    message: 'No matching definition was found for `' +
+                              this.selector.toCSS().trim() + '('      +
+                              (args ? args.map(function (a) {
+                                  var argValue = "";
+                                  if (a.name) {
+                                      argValue += a.name + ":";
+                                  }
+                                  if (a.value.toCSS) {
+                                      argValue += a.value.toCSS();
+                                  } else {
+                                      argValue += "???";
+                                  }
+                                  return argValue;
+                              }).join(', ') : "") + ")`",
+                    index:   this.index, filename: this.filename };
+        } else {
+            throw { type: 'Name',
+                message: this.selector.toCSS().trim() + " is undefined",
+                index: this.index, filename: this.filename };
+        }
+    }
+};
+
+tree.mixin.Definition = function (name, params, rules, condition, variadic) {
+    this.name = name;
+    this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])];
+    this.params = params;
+    this.condition = condition;
+    this.variadic = variadic;
+    this.arity = params.length;
+    this.rules = rules;
+    this._lookups = {};
+    this.required = params.reduce(function (count, p) {
+        if (!p.name || (p.name && !p.value)) { return count + 1 }
+        else                                 { return count }
+    }, 0);
+    this.parent = tree.Ruleset.prototype;
+    this.frames = [];
+};
+tree.mixin.Definition.prototype = {
+    toCSS:     function ()     { return "" },
+    variable:  function (name) { return this.parent.variable.call(this, name) },
+    variables: function ()     { return this.parent.variables.call(this) },
+    find:      function ()     { return this.parent.find.apply(this, arguments) },
+    rulesets:  function ()     { return this.parent.rulesets.apply(this) },
+
+    evalParams: function (env, mixinEnv, args, evaldArguments) {
+        var frame = new(tree.Ruleset)(null, []),
+            varargs, arg,
+            params = this.params.slice(0),
+            i, j, val, name, isNamedFound, argIndex;
+
+        mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames));
+
+        if (args) {
+            args = args.slice(0);
+
+            for(i = 0; i < args.length; i++) {
+                arg = args[i];
+                if (name = (arg && arg.name)) {
+                    isNamedFound = false;
+                    for(j = 0; j < params.length; j++) {
+                        if (!evaldArguments[j] && name === params[j].name) {
+                            evaldArguments[j] = arg.value.eval(env);
+                            frame.rules.unshift(new(tree.Rule)(name, arg.value.eval(env)));
+                            isNamedFound = true;
+                            break;
+                        }
+                    }
+                    if (isNamedFound) {
+                        args.splice(i, 1);
+                        i--;
+                        continue;
+                    } else {
+                        throw { type: 'Runtime', message: "Named argument for " + this.name +
+                            ' ' + args[i].name + ' not found' };
+                    }
+                }
+            }
+        }
+        argIndex = 0;
+        for (i = 0; i < params.length; i++) {
+            if (evaldArguments[i]) continue;
+
+            arg = args && args[argIndex];
+
+            if (name = params[i].name) {
+                if (params[i].variadic && args) {
+                    varargs = [];
+                    for (j = argIndex; j < args.length; j++) {
+                        varargs.push(args[j].value.eval(env));
+                    }
+                    frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env)));
+                } else {
+                    val = arg && arg.value;
+                    if (val) {
+                        val = val.eval(env);
+                    } else if (params[i].value) {
+                        val = params[i].value.eval(mixinEnv);
+                        frame.resetCache();
+                    } else {
+                        throw { type: 'Runtime', message: "wrong number of arguments for " + this.name +
+                            ' (' + args.length + ' for ' + this.arity + ')' };
+                    }
+
+                    frame.rules.unshift(new(tree.Rule)(name, val));
+                    evaldArguments[i] = val;
+                }
+            }
+
+            if (params[i].variadic && args) {
+                for (j = argIndex; j < args.length; j++) {
+                    evaldArguments[j] = args[j].value.eval(env);
+                }
+            }
+            argIndex++;
+        }
+
+        return frame;
+    },
+    eval: function (env, args, important) {
+        var _arguments = [],
+            mixinFrames = this.frames.concat(env.frames),
+            frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments),
+            context, rules, start, ruleset;
+
+        frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env)));
+
+        rules = important ?
+            this.parent.makeImportant.apply(this).rules : this.rules.slice(0);
+
+        ruleset = new(tree.Ruleset)(null, rules).eval(new(tree.evalEnv)(env,
+                                                    [this, frame].concat(mixinFrames)));
+        ruleset.originalRuleset = this;
+        return ruleset;
+    },
+    matchCondition: function (args, env) {
+
+        if (this.condition && !this.condition.eval(
+            new(tree.evalEnv)(env,
+                [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])]
+                    .concat(env.frames)))) {
+            return false;
+        }
+        return true;
+    },
+    matchArgs: function (args, env) {
+        var argsLength = (args && args.length) || 0, len, frame;
+
+        if (! this.variadic) {
+            if (argsLength < this.required)                               { return false }
+            if (argsLength > this.params.length)                          { return false }
+            if ((this.required > 0) && (argsLength > this.params.length)) { return false }
+        }
+
+        len = Math.min(argsLength, this.arity);
+
+        for (var i = 0; i < len; i++) {
+            if (!this.params[i].name && !this.params[i].variadic) {
+                if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Negative = function (node) {
+    this.value = node;
+};
+tree.Negative.prototype = {
+    toCSS: function (env) {
+        return '-' + this.value.toCSS(env);
+    },
+    eval: function (env) {
+        if (env.isMathsOn()) {
+            return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env);
+        }
+        return new(tree.Negative)(this.value.eval(env));
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Operation = function (op, operands, isSpaced) {
+    this.op = op.trim();
+    this.operands = operands;
+    this.isSpaced = isSpaced;
+};
+tree.Operation.prototype.eval = function (env) {
+    var a = this.operands[0].eval(env),
+        b = this.operands[1].eval(env),
+        temp;
+
+    if (env.isMathsOn()) {
+        if (a instanceof tree.Dimension && b instanceof tree.Color) {
+            if (this.op === '*' || this.op === '+') {
+                temp = b, b = a, a = temp;
+            } else {
+                throw { type: "Operation",
+                        message: "Can't substract or divide a color from a number" };
+            }
+        }
+        if (!a.operate) {
+            throw { type: "Operation",
+                    message: "Operation on an invalid type" };
+        }
+
+        return a.operate(env, this.op, b);
+    } else {
+        return new(tree.Operation)(this.op, [a, b], this.isSpaced);
+    }
+};
+tree.Operation.prototype.toCSS = function (env) {
+    var separator = this.isSpaced ? " " : "";
+    return this.operands[0].toCSS() + separator + this.op + separator + this.operands[1].toCSS();
+};
+
+tree.operate = function (env, op, a, b) {
+    switch (op) {
+        case '+': return a + b;
+        case '-': return a - b;
+        case '*': return a * b;
+        case '/': return a / b;
+    }
+};
+
+})(require('../tree'));
+
+(function (tree) {
+
+tree.Paren = function (node) {
+    this.value = node;
+};
+tree.Paren.prototype = {
+    toCSS: function (env) {
+        return '(' + this.value.toCSS(env).trim() + ')';
+    },
+    eval: function (env) {
+        return new(tree.Paren)(this.value.eval(env));
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Quoted = function (str, content, escaped, i) {
+    this.escaped = escaped;
+    this.value = content || '';
+    this.quote = str.charAt(0);
+    this.index = i;
+};
+tree.Quoted.prototype = {
+    toCSS: function () {
+        if (this.escaped) {
+            return this.value;
+        } else {
+            return this.quote + this.value + this.quote;
+        }
+    },
+    eval: function (env) {
+        var that = this;
+        var value = this.value.replace(/`([^`]+)`/g, function (_, exp) {
+            return new(tree.JavaScript)(exp, that.index, true).eval(env).value;
+        }).replace(/@\{([\w-]+)\}/g, function (_, name) {
+            var v = new(tree.Variable)('@' + name, that.index).eval(env, true);
+            return (v instanceof tree.Quoted) ? v.value : v.toCSS();
+        });
+        return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index);
+    },
+    compare: function (x) {
+        if (!x.toCSS) {
+            return -1;
+        }
+
+        var left = this.toCSS(),
+            right = x.toCSS();
+
+        if (left === right) {
+            return 0;
+        }
+
+        return left < right ? -1 : 1;
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Rule = function (name, value, important, index, inline) {
+    this.name = name;
+    this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]);
+    this.important = important ? ' ' + important.trim() : '';
+    this.index = index;
+    this.inline = inline || false;
+
+    if (name.charAt(0) === '@') {
+        this.variable = true;
+    } else { this.variable = false }
+};
+tree.Rule.prototype.toCSS = function (env) {
+    if (this.variable) { return "" }
+    else {
+        return this.name + (env.compress ? ':' : ': ') +
+               this.value.toCSS(env) +
+               this.important + (this.inline ? "" : ";");
+    }
+};
+
+tree.Rule.prototype.eval = function (env) {
+    var strictMathsBypass = false;
+    if (this.name === "font" && env.strictMaths === false) {
+        strictMathsBypass = true;
+        env.strictMaths = true;
+    }
+    try {
+        return new(tree.Rule)(this.name,
+                          this.value.eval(env),
+                          this.important,
+                          this.index, this.inline);
+    }
+    finally {
+        if (strictMathsBypass) {
+            env.strictMaths = false;
+        }
+    }
+};
+
+tree.Rule.prototype.makeImportant = function () {
+    return new(tree.Rule)(this.name,
+                          this.value,
+                          "!important",
+                          this.index, this.inline);
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Ruleset = function (selectors, rules, strictImports) {
+    this.selectors = selectors;
+    this.rules = rules;
+    this._lookups = {};
+    this.strictImports = strictImports;
+};
+tree.Ruleset.prototype = {
+    eval: function (env) {
+        var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) });
+        var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports);
+        var rules;
+
+        ruleset.originalRuleset = this;
+        ruleset.root = this.root;
+        ruleset.allowImports = this.allowImports;
+
+        if(this.debugInfo) {
+            ruleset.debugInfo = this.debugInfo;
+        }
+
+        // push the current ruleset to the frames stack
+        env.frames.unshift(ruleset);
+
+        // currrent selectors
+        if (!env.selectors) {
+            env.selectors = [];
+        }
+        env.selectors.unshift(this.selectors);
+
+        // Evaluate imports
+        if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {
+            ruleset.evalImports(env);
+        }
+
+        // Store the frames around mixin definitions,
+        // so they can be evaluated like closures when the time comes.
+        for (var i = 0; i < ruleset.rules.length; i++) {
+            if (ruleset.rules[i] instanceof tree.mixin.Definition) {
+                ruleset.rules[i].frames = env.frames.slice(0);
+            }
+        }
+
+        var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0;
+
+        // Evaluate mixin calls.
+        for (var i = 0; i < ruleset.rules.length; i++) {
+            if (ruleset.rules[i] instanceof tree.mixin.Call) {
+                rules = ruleset.rules[i].eval(env).filter(function(r) {
+                    if ((r instanceof tree.Rule) && r.variable) {
+                        // do not pollute the scope if the variable is
+                        // already there. consider returning false here
+                        // but we need a way to "return" variable from mixins
+                        return !(ruleset.variable(r.name));
+                    }
+                    return true;
+                });
+                ruleset.rules.splice.apply(ruleset.rules, [i, 1].concat(rules));
+                i += rules.length-1;
+                ruleset.resetCache();
+            }
+        }
+
+        if (this.selectors) {
+            for (var i = 0; i < this.selectors.length; i++) {
+                if (this.selectors[i].extend) {
+                    this.selectors[i].extend.eval(env, [[this.selectors[i]]].concat(env.selectors.slice(1)));
+                }
+            }
+        }
+
+        // Evaluate everything else
+        for (var i = 0, rule; i < ruleset.rules.length; i++) {
+            rule = ruleset.rules[i];
+
+            if (! (rule instanceof tree.mixin.Definition)) {
+                ruleset.rules[i] = rule.eval ? rule.eval(env) : rule;
+            }
+        }
+
+        // Pop the stack
+        env.frames.shift();
+        env.selectors.shift();
+
+        if (env.mediaBlocks) {
+            for(var i = mediaBlockCount; i < env.mediaBlocks.length; i++) {
+                env.mediaBlocks[i].bubbleSelectors(selectors);
+            }
+        }
+
+        return ruleset;
+    },
+    evalImports: function(env) {
+        var i, rules;
+        for (i = 0; i < this.rules.length; i++) {
+            if (this.rules[i] instanceof tree.Import) {
+                rules = this.rules[i].eval(env);
+                if (typeof rules.length === "number") {
+                    this.rules.splice.apply(this.rules, [i, 1].concat(rules));
+                    i+= rules.length-1;
+                } else {
+                    this.rules.splice(i, 1, rules);
+                }
+                this.resetCache();
+            }
+        }
+    },
+    makeImportant: function() {
+        return new tree.Ruleset(this.selectors, this.rules.map(function (r) {
+                    if (r.makeImportant) {
+                        return r.makeImportant();
+                    } else {
+                        return r;
+                    }
+                }), this.strictImports);
+    },
+    matchArgs: function (args) {
+        return !args || args.length === 0;
+    },
+    resetCache: function () {
+        this._rulesets = null;
+        this._variables = null;
+        this._lookups = {};
+    },
+    variables: function () {
+        if (this._variables) { return this._variables }
+        else {
+            return this._variables = this.rules.reduce(function (hash, r) {
+                if (r instanceof tree.Rule && r.variable === true) {
+                    hash[r.name] = r;
+                }
+                return hash;
+            }, {});
+        }
+    },
+    variable: function (name) {
+        return this.variables()[name];
+    },
+    rulesets: function () {
+        return this.rules.filter(function (r) {
+            return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition);
+        });
+    },
+    find: function (selector, self) {
+        self = self || this;
+        var rules = [], rule, match,
+            key = selector.toCSS();
+
+        if (key in this._lookups) { return this._lookups[key] }
+
+        this.rulesets().forEach(function (rule) {
+            if (rule !== self) {
+                for (var j = 0; j < rule.selectors.length; j++) {
+                    if (match = selector.match(rule.selectors[j])) {
+                        if (selector.elements.length > rule.selectors[j].elements.length) {
+                            Array.prototype.push.apply(rules, rule.find(
+                                new(tree.Selector)(selector.elements.slice(1)), self));
+                        } else {
+                            rules.push(rule);
+                        }
+                        break;
+                    }
+                }
+            }
+        });
+        return this._lookups[key] = rules;
+    },
+    //
+    // Entry point for code generation
+    //
+    //     `context` holds an array of arrays.
+    //
+    toCSS: function (context, env) {
+        var css = [],      // The CSS output
+            rules = [],    // node.Rule instances
+           _rules = [],    //
+            rulesets = [], // node.Ruleset instances
+            paths = [],    // Current selectors
+            selector,      // The fully rendered selector
+            debugInfo,     // Line number debugging
+            rule;
+
+        if (! this.root) {
+            this.joinSelectors(paths, context, this.selectors);
+        }
+
+        // Compile rules and rulesets
+        for (var i = 0; i < this.rules.length; i++) {
+            rule = this.rules[i];
+
+            if (rule.rules || (rule instanceof tree.Media)) {
+                rulesets.push(rule.toCSS(paths, env));
+            } else if (rule instanceof tree.Directive) {
+                var cssValue = rule.toCSS(paths, env);
+                // Output only the first @charset definition as such - convert the others
+                // to comments in case debug is enabled
+                if (rule.name === "@charset") {
+                    // Only output the debug info together with subsequent @charset definitions
+                    // a comment (or @media statement) before the actual @charset directive would
+                    // be considered illegal css as it has to be on the first line
+                    if (env.charset) {
+                        if (rule.debugInfo) {
+                            rulesets.push(tree.debugInfo(env, rule));
+                            rulesets.push(new tree.Comment("/* "+cssValue.replace(/\n/g, "")+" */\n").toCSS(env));
+                        }
+                        continue;
+                    }
+                    env.charset = true;
+                }
+                rulesets.push(cssValue);
+            } else if (rule instanceof tree.Comment) {
+                if (!rule.silent) {
+                    if (this.root) {
+                        rulesets.push(rule.toCSS(env));
+                    } else {
+                        rules.push(rule.toCSS(env));
+                    }
+                }
+            } else {
+                if (rule.toCSS && !rule.variable) {
+                    rules.push(rule.toCSS(env));
+                } else if (rule.value && !rule.variable) {
+                    rules.push(rule.value.toString());
+                }
+            }
+        }
+
+        // Remove last semicolon
+        if (env.compress && rules.length) {
+            rule = rules[rules.length - 1];
+            if (rule.charAt(rule.length - 1) === ';') {
+                rules[rules.length - 1] = rule.substring(0, rule.length - 1);
+            }
+        }
+
+        rulesets = rulesets.join('');
+
+        // If this is the root node, we don't render
+        // a selector, or {}.
+        // Otherwise, only output if this ruleset has rules.
+        if (this.root) {
+            css.push(rules.join(env.compress ? '' : '\n'));
+        } else {
+            if (rules.length > 0) {
+                debugInfo = tree.debugInfo(env, this);
+                selector = paths.map(function (p) {
+                    return p.map(function (s) {
+                        return s.toCSS(env);
+                    }).join('').trim();
+                }).join(env.compress ? ',' : ',\n');
+
+                // Remove duplicates
+                for (var i = rules.length - 1; i >= 0; i--) {
+                    if (_rules.indexOf(rules[i]) === -1) {
+                        _rules.unshift(rules[i]);
+                    }
+                }
+                rules = _rules;
+
+                css.push(debugInfo + selector +
+                        (env.compress ? '{' : ' {\n  ') +
+                        rules.join(env.compress ? '' : '\n  ') +
+                        (env.compress ? '}' : '\n}\n'));
+            }
+        }
+        css.push(rulesets);
+
+        return css.join('')  + (env.compress ? '\n' : '');
+    },
+
+    joinSelectors: function (paths, context, selectors) {
+        for (var s = 0; s < selectors.length; s++) {
+            this.joinSelector(paths, context, selectors[s]);
+        }
+    },
+
+    joinSelector: function (paths, context, selector) {
+
+        var i, j, k,
+            hasParentSelector, newSelectors, el, sel, parentSel,
+            newSelectorPath, afterParentJoin, newJoinedSelector,
+            newJoinedSelectorEmpty, lastSelector, currentElements,
+            selectorsMultiplied;
+
+        for (i = 0; i < selector.elements.length; i++) {
+            el = selector.elements[i];
+            if (el.value === '&') {
+                hasParentSelector = true;
+            }
+        }
+
+        if (!hasParentSelector) {
+            if (context.length > 0) {
+                for(i = 0; i < context.length; i++) {
+                    paths.push(context[i].concat(selector));
+                }
+            }
+            else {
+                paths.push([selector]);
+            }
+            return;
+        }
+
+        // The paths are [[Selector]]
+        // The first list is a list of comma seperated selectors
+        // The inner list is a list of inheritance seperated selectors
+        // e.g.
+        // .a, .b {
+        //   .c {
+        //   }
+        // }
+        // == [[.a] [.c]] [[.b] [.c]]
+        //
+
+        // the elements from the current selector so far
+        currentElements = [];
+        // the current list of new selectors to add to the path.
+        // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
+        // by the parents
+        newSelectors = [[]];
+
+        for (i = 0; i < selector.elements.length; i++) {
+            el = selector.elements[i];
+            // non parent reference elements just get added
+            if (el.value !== "&") {
+                currentElements.push(el);
+            } else {
+                // the new list of selectors to add
+                selectorsMultiplied = [];
+
+                // merge the current list of non parent selector elements
+                // on to the current list of selectors to add
+                if (currentElements.length > 0) {
+                    this.mergeElementsOnToSelectors(currentElements, newSelectors);
+                }
+
+                // loop through our current selectors
+                for(j = 0; j < newSelectors.length; j++) {
+                    sel = newSelectors[j];
+                    // if we don't have any parent paths, the & might be in a mixin so that it can be used
+                    // whether there are parents or not
+                    if (context.length == 0) {
+                        // the combinator used on el should now be applied to the next element instead so that
+                        // it is not lost
+                        if (sel.length > 0) {
+                            sel[0].elements = sel[0].elements.slice(0);
+                            sel[0].elements.push(new(tree.Element)(el.combinator, '', 0)); //new Element(el.Combinator,  ""));
+                        }
+                        selectorsMultiplied.push(sel);
+                    }
+                    else {
+                        // and the parent selectors
+                        for(k = 0; k < context.length; k++) {
+                            parentSel = context[k];
+                            // We need to put the current selectors
+                            // then join the last selector's elements on to the parents selectors
+
+                            // our new selector path
+                            newSelectorPath = [];
+                            // selectors from the parent after the join
+                            afterParentJoin = [];
+                            newJoinedSelectorEmpty = true;
+
+                            //construct the joined selector - if & is the first thing this will be empty,
+                            // if not newJoinedSelector will be the last set of elements in the selector
+                            if (sel.length > 0) {
+                                newSelectorPath = sel.slice(0);
+                                lastSelector = newSelectorPath.pop();
+                                newJoinedSelector = new(tree.Selector)(lastSelector.elements.slice(0));
+                                newJoinedSelectorEmpty = false;
+                            }
+                            else {
+                                newJoinedSelector = new(tree.Selector)([]);
+                            }
+
+                            //put together the parent selectors after the join
+                            if (parentSel.length > 1) {
+                                afterParentJoin = afterParentJoin.concat(parentSel.slice(1));
+                            }
+
+                            if (parentSel.length > 0) {
+                                newJoinedSelectorEmpty = false;
+
+                                // join the elements so far with the first part of the parent
+                                newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, 0));
+                                newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1));
+                            }
+
+                            if (!newJoinedSelectorEmpty) {
+                                // now add the joined selector
+                                newSelectorPath.push(newJoinedSelector);
+                            }
+
+                            // and the rest of the parent
+                            newSelectorPath = newSelectorPath.concat(afterParentJoin);
+
+                            // add that to our new set of selectors
+                            selectorsMultiplied.push(newSelectorPath);
+                        }
+                    }
+                }
+
+                // our new selectors has been multiplied, so reset the state
+                newSelectors = selectorsMultiplied;
+                currentElements = [];
+            }
+        }
+
+        // if we have any elements left over (e.g. .a& .b == .b)
+        // add them on to all the current selectors
+        if (currentElements.length > 0) {
+            this.mergeElementsOnToSelectors(currentElements, newSelectors);
+        }
+
+        for(i = 0; i < newSelectors.length; i++) {
+            if (newSelectors[i].length > 0) {
+                paths.push(newSelectors[i]);
+            }
+        }
+    },
+
+    mergeElementsOnToSelectors: function(elements, selectors) {
+        var i, sel;
+
+        if (selectors.length == 0) {
+            selectors.push([ new(tree.Selector)(elements) ]);
+            return;
+        }
+
+        for(i = 0; i < selectors.length; i++) {
+            sel = selectors[i];
+
+            // if the previous thing in sel is a parent this needs to join on to it
+            if (sel.length > 0) {
+                sel[sel.length - 1] = new(tree.Selector)(sel[sel.length - 1].elements.concat(elements));
+            }
+            else {
+                sel.push(new(tree.Selector)(elements));
+            }
+        }
+    }
+};
+})(require('../tree'));
+(function (tree) {
+
+tree.Selector = function (elements, extend) {
+    this.elements = elements;
+    this.extend = extend;
+};
+tree.Selector.prototype.match = function (other) {
+    var elements = this.elements,
+        len = elements.length,
+        oelements, olen, max, i;
+
+    oelements = other.elements.slice(
+        (other.elements.length && other.elements[0].value === "&") ? 1 : 0);
+    olen = oelements.length;
+    max = Math.min(len, olen)
+
+    if (olen === 0 || len < olen) {
+        return false;
+    } else {
+        for (i = 0; i < max; i++) {
+            if (elements[i].value !== oelements[i].value) {
+                return false;
+            }
+        }
+    }
+    return true;
+};
+tree.Selector.prototype.eval = function (env) {
+    return new(tree.Selector)(this.elements.map(function (e) {
+        return e.eval(env);
+    }), this.extend);
+};
+tree.Selector.prototype.toCSS = function (env) {
+    if (this._css) { return this._css }
+
+    if (this.elements[0].combinator.value === "") {
+        this._css = ' ';
+    } else {
+        this._css = '';
+    }
+
+    this._css += this.elements.map(function (e) {
+        if (typeof(e) === 'string') {
+            return ' ' + e.trim();
+        } else {
+            return e.toCSS(env);
+        }
+    }).join('');
+
+    return this._css;
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.UnicodeDescriptor = function (value) {
+    this.value = value;
+};
+tree.UnicodeDescriptor.prototype = {
+    toCSS: function (env) {
+        return this.value;
+    },
+    eval: function () { return this }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.URL = function (val, rootpath) {
+    this.value = val;
+    this.rootpath = rootpath;
+};
+tree.URL.prototype = {
+    toCSS: function () {
+        return "url(" + this.value.toCSS() + ")";
+    },
+    eval: function (ctx) {
+        var val = this.value.eval(ctx), rootpath;
+
+        // Add the base path if the URL is relative
+        if (this.rootpath && typeof val.value === "string" && ctx.isPathRelative(val.value)) {
+            rootpath = this.rootpath;
+            if (!val.quote) {
+                rootpath = rootpath.replace(/[\(\)'"\s]/g, function(match) { return "\\"+match; });
+            }
+            val.value = rootpath + val.value;
+        }
+
+        return new(tree.URL)(val, null);
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Value = function (value) {
+    this.value = value;
+    this.is = 'value';
+};
+tree.Value.prototype = {
+    eval: function (env) {
+        if (this.value.length === 1) {
+            return this.value[0].eval(env);
+        } else {
+            return new(tree.Value)(this.value.map(function (v) {
+                return v.eval(env);
+            }));
+        }
+    },
+    toCSS: function (env) {
+        return this.value.map(function (e) {
+            return e.toCSS(env);
+        }).join(env.compress ? ',' : ', ');
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file };
+tree.Variable.prototype = {
+    eval: function (env) {
+        var variable, v, name = this.name;
+
+        if (name.indexOf('@@') == 0) {
+            name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value;
+        }
+
+        if (this.evaluating) {
+            throw { type: 'Name',
+                    message: "Recursive variable definition for " + name,
+                    filename: this.file,
+                    index: this.index };
+        }
+
+        this.evaluating = true;
+
+        if (variable = tree.find(env.frames, function (frame) {
+            if (v = frame.variable(name)) {
+                return v.value.eval(env);
+            }
+        })) {
+            this.evaluating = false;
+            return variable;
+        }
+        else {
+            throw { type: 'Name',
+                    message: "variable " + name + " is undefined",
+                    filename: this.file,
+                    index: this.index };
+        }
+    }
+};
+
+})(require('../tree'));
+(function (tree) {
+
+tree.debugInfo = function(env, ctx) {
+    var result="";
+    if (env.dumpLineNumbers && !env.compress) {
+        switch(env.dumpLineNumbers) {
+            case 'comments':
+                result = tree.debugInfo.asComment(ctx);
+                break;
+            case 'mediaquery':
+                result = tree.debugInfo.asMediaQuery(ctx);
+                break;
+            case 'all':
+                result = tree.debugInfo.asComment(ctx)+tree.debugInfo.asMediaQuery(ctx);
+                break;
+        }
+    }
+    return result;
+};
+
+tree.debugInfo.asComment = function(ctx) {
+    return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\n';
+};
+
+tree.debugInfo.asMediaQuery = function(ctx) {
+    return '@media -sass-debug-info{filename{font-family:' +
+        ('file://' + ctx.debugInfo.fileName).replace(/[\/:.]/g, '\\$&') +
+        '}line{font-family:\\00003' + ctx.debugInfo.lineNumber + '}}\n';
+};
+
+tree.find = function (obj, fun) {
+    for (var i = 0, r; i < obj.length; i++) {
+        if (r = fun.call(obj, obj[i])) { return r }
+    }
+    return null;
+};
+tree.jsify = function (obj) {
+    if (Array.isArray(obj.value) && (obj.value.length > 1)) {
+        return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']';
+    } else {
+        return obj.toCSS(false);
+    }
+};
+
+})(require('./tree'));
+(function (tree) {
+
+    var parseCopyProperties = [
+        'paths',            // paths to search for imports on
+        'optimization',     // option - optimization level (for the chunker)
+        'filename',         // current filename, used for error reporting
+        'files',            // list of files that have been imported, used for import-once
+        'contents',         // browser-only, contents of all the files
+        'rootpath',         // current rootpath to append to all url's
+        'relativeUrls',     // option - whether to adjust URL's to be relative
+        'strictImports',    // option -
+        'dumpLineNumbers',  // option - whether to dump line numbers
+        'compress',         // option - whether to compress
+        'mime',             // browser only - mime type for sheet import
+        'entryPath',        // browser only - path of entry less file
+        'rootFilename',     // browser only - href of the entry less file
+        'currentDirectory'  // node only - the current directory
+    ];
+
+    tree.parseEnv = function(options) {
+        copyFromOriginal(options, this, parseCopyProperties);
+
+        if (!this.contents) { this.contents = {}; }
+        if (!this.rootpath) { this.rootpath = ''; }
+        if (!this.files) { this.files = {}; }
+    };
+
+    tree.parseEnv.prototype.toSheet = function (path) {
+        var env = new tree.parseEnv(this);
+        env.href = path;
+        //env.title = path;
+        env.type = this.mime;
+        return env;
+    };
+
+    var evalCopyProperties = [
+        'silent',      // whether to swallow errors and warnings
+        'verbose',     // whether to log more activity
+        'compress',    // whether to compress
+        'ieCompat',    // whether to enforce IE compatibility (IE8 data-uri)
+        'strictMaths', // whether maths has to be within parenthesis
+        'strictUnits'  // whether units need to evaluate correctly
+        ];
+
+    tree.evalEnv = function(options, frames) {
+        copyFromOriginal(options, this, evalCopyProperties);
+
+        this.frames = frames || [];
+    };
+
+    tree.evalEnv.prototype.inParenthesis = function () {
+        if (!this.parensStack) {
+            this.parensStack = [];
+        }
+        this.parensStack.push(true);
+    };
+
+    tree.evalEnv.prototype.outOfParenthesis = function () {
+        this.parensStack.pop();
+    };
+
+    tree.evalEnv.prototype.isMathsOn = function () {
+        return this.strictMaths === false ? true : (this.parensStack && this.parensStack.length);
+    };
+
+    tree.evalEnv.prototype.isPathRelative = function (path) {
+        return !/^(?:[a-z-]+:|\/)/.test(path);
+    };
+
+    //todo - do the same for the toCSS env
+    //tree.toCSSEnv = function (options) {
+    //};
+
+    var copyFromOriginal = function(original, destination, propertiesToCopy) {
+        if (!original) { return; }
+
+        for(var i = 0; i < propertiesToCopy.length; i++) {
+            if (original.hasOwnProperty(propertiesToCopy[i])) {
+                destination[propertiesToCopy[i]] = original[propertiesToCopy[i]];
+            }
+        }
+    }
+})(require('./tree'));//
+// browser.js - client-side engine
+//
+
+var isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);
+
+less.env = less.env || (location.hostname == '127.0.0.1' ||
+                        location.hostname == '0.0.0.0'   ||
+                        location.hostname == 'localhost' ||
+                        location.port.length > 0         ||
+                        isFileProtocol                   ? 'development'
+                                                         : 'production');
+
+// Load styles asynchronously (default: false)
+//
+// This is set to `false` by default, so that the body
+// doesn't start loading before the stylesheets are parsed.
+// Setting this to `true` can result in flickering.
+//
+less.async = less.async || false;
+less.fileAsync = less.fileAsync || false;
+
+// Interval between watch polls
+less.poll = less.poll || (isFileProtocol ? 1000 : 1500);
+
+//Setup user functions
+if (less.functions) {
+    for(var func in less.functions) {
+        less.tree.functions[func] = less.functions[func];
+   }
+}
+
+var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);
+if (dumpLineNumbers) {
+    less.dumpLineNumbers = dumpLineNumbers[1];
+}
+
+//
+// Watch mode
+//
+less.watch   = function () {
+    if (!less.watchMode ){
+        less.env = 'development';
+         initRunningMode();
+    }
+    return this.watchMode = true
+};
+
+less.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; };
+
+function initRunningMode(){
+    if (less.env === 'development') {
+        less.optimization = 0;
+        less.watchTimer = setInterval(function () {
+            if (less.watchMode) {
+                loadStyleSheets(function (e, root, _, sheet, env) {
+                    if (e) {
+                        error(e, sheet.href);
+                    } else if (root) {
+                        createCSS(root.toCSS(less), sheet, env.lastModified);
+                    }
+                });
+            }
+        }, less.poll);
+    } else {
+        less.optimization = 3;
+    }
+}
+
+if (/!watch/.test(location.hash)) {
+    less.watch();
+}
+
+var cache = null;
+
+if (less.env != 'development') {
+    try {
+        cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage;
+    } catch (_) {}
+}
+
+//
+// Get all  tags with the 'rel' attribute set to "stylesheet/less"
+//
+var links = document.getElementsByTagName('link');
+var typePattern = /^text\/(x-)?less$/;
+
+less.sheets = [];
+
+for (var i = 0; i < links.length; i++) {
+    if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) &&
+       (links[i].type.match(typePattern)))) {
+        less.sheets.push(links[i]);
+    }
+}
+
+//
+// With this function, it's possible to alter variables and re-render
+// CSS without reloading less-files
+//
+var session_cache = '';
+less.modifyVars = function(record) {
+    var str = session_cache;
+    for (name in record) {
+        str += ((name.slice(0,1) === '@')? '' : '@') + name +': '+
+                ((record[name].slice(-1) === ';')? record[name] : record[name] +';');
+    }
+    new(less.Parser)(new less.tree.parseEnv(less)).parse(str, function (e, root) {
+        if (e) {
+            error(e, "session_cache");
+        } else {
+            createCSS(root.toCSS(less), less.sheets[less.sheets.length - 1]);
+        }
+    });
+};
+
+less.refresh = function (reload) {
+    var startTime, endTime;
+    startTime = endTime = new(Date);
+
+    loadStyleSheets(function (e, root, _, sheet, env) {
+        if (e) {
+            return error(e, sheet.href);
+        }
+        if (env.local) {
+            log("loading " + sheet.href + " from cache.");
+        } else {
+            log("parsed " + sheet.href + " successfully.");
+            createCSS(root.toCSS(less), sheet, env.lastModified);
+        }
+        log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms');
+        (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms');
+        endTime = new(Date);
+    }, reload);
+
+    loadStyles();
+};
+less.refreshStyles = loadStyles;
+
+less.refresh(less.env === 'development');
+
+function loadStyles() {
+    var styles = document.getElementsByTagName('style');
+    for (var i = 0; i < styles.length; i++) {
+        if (styles[i].type.match(typePattern)) {
+            var env = new less.tree.parseEnv(less);
+            env.filename = document.location.href.replace(/#.*$/, '');
+
+            new(less.Parser)(env).parse(styles[i].innerHTML || '', function (e, cssAST) {
+                if (e) {
+                    return error(e, "inline");
+                }
+                var css = cssAST.toCSS(less);
+                var style = styles[i];
+                style.type = 'text/css';
+                if (style.styleSheet) {
+                    style.styleSheet.cssText = css;
+                } else {
+                    style.innerHTML = css;
+                }
+            });
+        }
+    }
+}
+
+function loadStyleSheets(callback, reload) {
+    for (var i = 0; i < less.sheets.length; i++) {
+        loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1));
+    }
+}
+
+function pathDiff(url, baseUrl) {
+    // diff between two paths to create a relative path
+
+    var urlParts = extractUrlParts(url),
+        baseUrlParts = extractUrlParts(baseUrl),
+        i, max, urlDirectories, baseUrlDirectories, diff = "";
+    if (urlParts.hostPart !== baseUrlParts.hostPart) {
+        return "";
+    }
+    max = Math.max(baseUrlParts.directories.length, urlParts.directories.length);
+    for(i = 0; i < max; i++) {
+        if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; }
+    }
+    baseUrlDirectories = baseUrlParts.directories.slice(i);
+    urlDirectories = urlParts.directories.slice(i);
+    for(i = 0; i < baseUrlDirectories.length-1; i++) {
+        diff += "../";
+    }
+    for(i = 0; i < urlDirectories.length-1; i++) {
+        diff += urlDirectories[i] + "/";
+    }
+    return diff;
+}
+
+function extractUrlParts(url, baseUrl) {
+    // urlParts[1] = protocol&hostname || /
+    // urlParts[2] = / if path relative to host base
+    // urlParts[3] = directories
+    // urlParts[4] = filename
+    // urlParts[5] = parameters
+
+    var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/,
+        urlParts = url.match(urlPartsRegex),
+        returner = {}, directories = [], i, baseUrlParts;
+
+    if (!urlParts) {
+        throw new Error("Could not parse sheet href - '"+url+"'");
+    }
+
+    // Stylesheets in IE don't always return the full path
+    if (!urlParts[1] || urlParts[2]) {
+        baseUrlParts = baseUrl.match(urlPartsRegex);
+        if (!baseUrlParts) {
+            throw new Error("Could not parse page url - '"+baseUrl+"'");
+        }
+        urlParts[1] = baseUrlParts[1];
+        if (!urlParts[2]) {
+            urlParts[3] = baseUrlParts[3] + urlParts[3];
+        }
+    }
+
+    if (urlParts[3]) {
+        directories = urlParts[3].replace("\\", "/").split("/");
+
+        for(i = 0; i < directories.length; i++) {
+            if (directories[i] === ".." && i > 0) {
+                directories.splice(i-1, 2);
+                i -= 2;
+            }
+        }
+    }
+
+    returner.hostPart = urlParts[1];
+    returner.directories = directories;
+    returner.path = urlParts[1] + directories.join("/");
+    returner.fileUrl = returner.path + (urlParts[4] || "");
+    returner.url = returner.fileUrl + (urlParts[5] || "");
+    return returner;
+}
+
+function loadStyleSheet(sheet, callback, reload, remaining) {
+
+    // sheet may be set to the stylesheet for the initial load or a collection of properties including
+    // some env variables for imports
+    var hrefParts = extractUrlParts(sheet.href, window.location.href);
+    var href      = hrefParts.url;
+    var css       = cache && cache.getItem(href);
+    var timestamp = cache && cache.getItem(href + ':timestamp');
+    var styles    = { css: css, timestamp: timestamp };
+    var env;
+
+    if (sheet instanceof less.tree.parseEnv) {
+        env = new less.tree.parseEnv(sheet);
+    } else {
+        env = new less.tree.parseEnv(less);
+        env.entryPath = hrefParts.path;
+        env.mime = sheet.type;
+    }
+
+    if (env.relativeUrls) {
+        //todo - this relies on option being set on less object rather than being passed in as an option
+        //     - need an originalRootpath
+        if (less.rootpath) {
+            env.rootpath = extractUrlParts(less.rootpath + pathDiff(hrefParts.path, env.entryPath)).path;
+        } else {
+            env.rootpath = hrefParts.path;
+        }
+    } else  {
+        if (!less.rootpath) {
+            env.rootpath = env.entryPath;
+        }
+    }
+
+    xhr(href, sheet.type, function (data, lastModified) {
+        // Store data this session
+        session_cache += data.replace(/@import .+?;/ig, '');
+
+        if (!reload && styles && lastModified &&
+           (new(Date)(lastModified).valueOf() ===
+            new(Date)(styles.timestamp).valueOf())) {
+            // Use local copy
+            createCSS(styles.css, sheet);
+            callback(null, null, data, sheet, { local: true, remaining: remaining }, href);
+        } else {
+            // Use remote copy (re-parse)
+            try {
+                env.contents[href] = data;  // Updating content cache
+                env.paths = [hrefParts.path];
+                env.filename = href;
+                env.rootFilename = env.rootFilename || href;
+                new(less.Parser)(env).parse(data, function (e, root) {
+                    if (e) { return callback(e, null, null, sheet); }
+                    try {
+                        callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }, href);
+                        //TODO - there must be a better way? A generic less-to-css function that can both call error
+                        //and removeNode where appropriate
+                        //should also add tests
+                        if (env.rootFilename === href) {
+                            removeNode(document.getElementById('less-error-message:' + extractId(href)));
+                        }
+                    } catch (e) {
+                        callback(e, null, null, sheet);
+                    }
+                });
+            } catch (e) {
+                callback(e, null, null, sheet);
+            }
+        }
+    }, function (status, url) {
+        callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, null, sheet);
+    });
+}
+
+function extractId(href) {
+    return href.replace(/^[a-z-]+:\/+?[^\/]+/, '' )  // Remove protocol & domain
+               .replace(/^\//,                 '' )  // Remove root /
+               .replace(/\.[a-zA-Z]+$/,        '' )  // Remove simple extension
+               .replace(/[^\.\w-]+/g,          '-')  // Replace illegal characters
+               .replace(/\./g,                 ':'); // Replace dots with colons(for valid id)
+}
+
+function createCSS(styles, sheet, lastModified) {
+    // Strip the query-string
+    var href = sheet.href || '';
+
+    // If there is no title set, use the filename, minus the extension
+    var id = 'less:' + (sheet.title || extractId(href));
+
+    // If this has already been inserted into the DOM, we may need to replace it
+    var oldCss = document.getElementById(id);
+    var keepOldCss = false;
+
+    // Create a new stylesheet node for insertion or (if necessary) replacement
+    var css = document.createElement('style');
+    css.setAttribute('type', 'text/css');
+    if (sheet.media) {
+        css.setAttribute('media', sheet.media);
+    }
+    css.id = id;
+
+    if (css.styleSheet) { // IE
+        try {
+            css.styleSheet.cssText = styles;
+        } catch (e) {
+            throw new(Error)("Couldn't reassign styleSheet.cssText.");
+        }
+    } else {
+        css.appendChild(document.createTextNode(styles));
+
+        // If new contents match contents of oldCss, don't replace oldCss
+        keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 &&
+            oldCss.firstChild.nodeValue === css.firstChild.nodeValue);
+    }
+
+    var head = document.getElementsByTagName('head')[0];
+
+    // If there is no oldCss, just append; otherwise, only append if we need
+    // to replace oldCss with an updated stylesheet
+    if (oldCss == null || keepOldCss === false) {
+        var nextEl = sheet && sheet.nextSibling || null;
+        (nextEl || document.getElementsByTagName('head')[0]).parentNode.insertBefore(css, nextEl);
+    }
+    if (oldCss && keepOldCss === false) {
+        head.removeChild(oldCss);
+    }
+
+    // Don't update the local store if the file wasn't modified
+    if (lastModified && cache) {
+        log('saving ' + href + ' to cache.');
+        try {
+            cache.setItem(href, styles);
+            cache.setItem(href + ':timestamp', lastModified);
+        } catch(e) {
+            //TODO - could do with adding more robust error handling
+            log('failed to save');
+        }
+    }
+}
+
+function xhr(url, type, callback, errback) {
+    var xhr = getXMLHttpRequest();
+    var async = isFileProtocol ? less.fileAsync : less.async;
+
+    if (typeof(xhr.overrideMimeType) === 'function') {
+        xhr.overrideMimeType('text/css');
+    }
+    xhr.open('GET', url, async);
+    xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');
+    xhr.send(null);
+
+    if (isFileProtocol && !less.fileAsync) {
+        if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {
+            callback(xhr.responseText);
+        } else {
+            errback(xhr.status, url);
+        }
+    } else if (async) {
+        xhr.onreadystatechange = function () {
+            if (xhr.readyState == 4) {
+                handleResponse(xhr, callback, errback);
+            }
+        };
+    } else {
+        handleResponse(xhr, callback, errback);
+    }
+
+    function handleResponse(xhr, callback, errback) {
+        if (xhr.status >= 200 && xhr.status < 300) {
+            callback(xhr.responseText,
+                     xhr.getResponseHeader("Last-Modified"));
+        } else if (typeof(errback) === 'function') {
+            errback(xhr.status, url);
+        }
+    }
+}
+
+function getXMLHttpRequest() {
+    if (window.XMLHttpRequest) {
+        return new(XMLHttpRequest);
+    } else {
+        try {
+            return new(ActiveXObject)("MSXML2.XMLHTTP.3.0");
+        } catch (e) {
+            log("browser doesn't support AJAX.");
+            return null;
+        }
+    }
+}
+
+function removeNode(node) {
+    return node && node.parentNode.removeChild(node);
+}
+
+function log(str) {
+    if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) }
+}
+
+function error(e, rootHref) {
+    var id = 'less-error-message:' + extractId(rootHref || "");
+    var template = '
  • {content}
  • '; + var elem = document.createElement('div'), timer, content, error = []; + var filename = e.filename || rootHref; + var filenameNoPath = filename.match(/([^\/]+(\?.*)?)$/)[1]; + + elem.id = id; + elem.className = "less-error-message"; + + content = '

    ' + (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + + '

    ' + '

    in ' + filenameNoPath + " "; + + var errorline = function (e, i, classname) { + if (e.extract[i] != undefined) { + error.push(template.replace(/\{line\}/, (parseInt(e.line) || 0) + (i - 1)) + .replace(/\{class\}/, classname) + .replace(/\{content\}/, e.extract[i])); + } + }; + + if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + + '
      ' + error.join('') + '
    '; + } else if (e.stack) { + content += '
    ' + e.stack.split('\n').slice(1).join('
    '); + } + elem.innerHTML = content; + + // CSS for error messages + createCSS([ + '.less-error-message ul, .less-error-message li {', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', + '}', + '.less-error-message label {', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', + '}', + '.less-error-message pre {', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', + '}', + '.less-error-message pre.line {', + 'color: #ff0000;', + '}', + '.less-error-message h3 {', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', + '}', + '.less-error-message a {', + 'color: #10a', + '}', + '.less-error-message .error {', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', + '}' + ].join('\n'), { title: 'error-message' }); + + elem.style.cssText = [ + "font-family: Arial, sans-serif", + "border: 1px solid #e00", + "background-color: #eee", + "border-radius: 5px", + "-webkit-border-radius: 5px", + "-moz-border-radius: 5px", + "color: #e00", + "padding: 15px", + "margin-bottom: 15px" + ].join(';'); + + if (less.env == 'development') { + timer = setInterval(function () { + if (document.body) { + if (document.getElementById(id)) { + document.body.replaceChild(elem, document.getElementById(id)); + } else { + document.body.insertBefore(elem, document.body.firstChild); + } + clearInterval(timer); + } + }, 10); + } +} +// amd.js +// +// Define Less as an AMD module. +if (typeof define === "function" && define.amd) { + define(function () { return less; } ); +} +})(window); \ No newline at end of file