From ff50bc2e6f6179e8e7aa82f337a42ebf6427e13d Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 11:04:17 -0800 Subject: [PATCH 01/80] Add toBeInstanceOf and toHaveLength jasmine matchers --- spec/spec-helper.coffee | 14 ++++ .../spec/markdown-preview-spec.coffee | 66 +++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 4632db296..151ca8488 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -68,6 +68,8 @@ beforeEach -> spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent + addCustomMatchers(this) + afterEach -> keymap.bindingSets = bindingSetsToRestore keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore @@ -121,6 +123,18 @@ 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 + window.keyIdentifierForKey = (key) -> if key.length > 1 # named key key diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 7704768ee..8f56b2c88 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -1,14 +1,72 @@ $ = require 'jquery' RootView = require 'root-view' -MarkdownPreview = require 'markdown-preview/lib/markdown-preview-view' +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' _ = require 'underscore' -describe "MarkdownPreview", -> +describe "MarkdownPreviewView", -> beforeEach -> project.setPath(project.resolve('markdown')) window.rootView = new RootView window.loadPackage("markdown-preview") - spyOn(MarkdownPreview.prototype, 'loadHtml') + spyOn(MarkdownPreviewView.prototype, 'loadHtml') + + fdescribe "markdown-preview:show", -> + beforeEach -> + rootView.open("file.markdown") + + describe "when the active item is an edit session", -> + beforeEach -> + rootView.attachToDom() + + 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() + + rootView.getActiveView().trigger 'markdown-preview:show' + + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + expect(preview.buffer).toBe rootView.getActivePaneItem().buffer + expect(pane1).toMatchSelector(':has(:focus)') + + 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 + + rootView.getActiveView().trigger 'markdown-preview:show' + + expect(rootView.getPanes()).toHaveLength 2 + [pane1, pane2] = rootView.getPanes() + + 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 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() + + rootView.getActiveView().trigger 'markdown-preview:show' + expect(rootView.getPanes()).toHaveLength 2 + expect(pane2.getItems()).toHaveLength 2 + expect(pane2.activeItem).toBe preview + expect(pane1).toMatchSelector(':has(:focus)') + + describe "when the active item is not an edit session ", -> + it "logs a warning to the console saying that it isn't possible to preview the item", -> describe "markdown-preview:toggle event", -> it "toggles on/off a preview for a .md file", -> @@ -50,7 +108,7 @@ describe "MarkdownPreview", -> 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() + expect(MarkdownPreviewView.prototype.loadHtml).not.toHaveBeenCalled() describe "core:cancel event", -> it "removes markdown preview", -> From d84614866a35e4f463142e09fe8af53de7b3f0cd Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 12:21:20 -0800 Subject: [PATCH 02/80] Add Pane.getNextPane --- spec/app/pane-spec.coffee | 8 ++++++++ src/app/pane.coffee | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 3c62fc797..96f379acd 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/src/app/pane.coffee b/src/app/pane.coffee index ef29ce72f..b0134f552 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -72,6 +72,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...) From e26d2e5637c1911a8fe95da281f2860fda608c6a Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 14:53:42 -0800 Subject: [PATCH 03/80] WIP: Preview markdown in next pane, splitting current pane if needed --- .../keymaps/markdown-preview.cson | 5 +-- .../lib/markdown-preview-view.coffee | 35 ++++++++++++++----- src/packages/markdown-preview/package.cson | 2 +- 3 files changed, 28 insertions(+), 14 deletions(-) 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 b78827a83..4292cf072 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -1,23 +1,40 @@ -ScrollView = require 'scroll-view' fs = require 'fs' $ = require 'jquery' +ScrollView = require 'scroll-view' {$$$} = require 'space-pen' module.exports = class MarkdownPreviewView extends ScrollView @activate: -> @instance = new MarkdownPreviewView + rootView.command 'markdown-preview:show', '.editor', => @show() + + @show: -> + activePane = rootView.getActivePane() + editSession = activePane.activeItem + if nextPane = activePane.getNextPane() + if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}") + nextPane.showItem(preview) + else + nextPane.showItem(new MarkdownPreviewView(editSession.buffer)) + else + activePane.splitRight(new MarkdownPreviewView(editSession.buffer)) + activePane.focus() @content: -> @div class: 'markdown-preview', tabindex: -1, => @div class: 'markdown-body', outlet: 'markdownBody' - initialize: -> + initialize: (@buffer) -> super rootView.command 'markdown-preview:toggle', => @toggle() - @on 'blur', => @detach() unless document.activeElement is this[0] - @command 'core:cancel', => @detach() + + getTitle: -> + "Markdown Preview" + + getUri: -> + "markdown-preview:#{@buffer.getPath()}" toggle: -> if @hasParent() @@ -33,11 +50,11 @@ class MarkdownPreviewView extends ScrollView @focus() detach: -> - return if @detaching - @detaching = true - super - rootView.focus() - @detaching = false +# return if @detaching +# @detaching = true +# super +# rootView.focus() +# @detaching = false getActiveText: -> rootView.getActiveView()?.getText() diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson index deea08f07..ce5b1ff39 100644 --- a/src/packages/markdown-preview/package.cson +++ b/src/packages/markdown-preview/package.cson @@ -1,3 +1,3 @@ 'main': 'lib/markdown-preview-view' 'activationEvents': - 'markdown-preview:toggle': '.editor' + 'markdown-preview:show': '.editor' From 0f1ffdaee85afc7fb990e255a568cce928e7015b Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 15:34:28 -0800 Subject: [PATCH 04/80] Set the window title to 'untitled' when the active item has no title --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 2d3b8fa83..659b331c9 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -115,7 +115,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 From 7e33bd17e09336b48732d8b2f999c7d9a1a15b37 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 15:49:33 -0800 Subject: [PATCH 05/80] Make markdown preview views fit into panes and work w/ a single buffer --- spec/fixtures/markdown/file.markdown | 3 + .../lib/markdown-preview-view.coffee | 82 +---- .../lib/markdown-preview.coffee | 24 ++ src/packages/markdown-preview/package.cson | 2 +- .../spec/markdown-preview-spec.coffee | 85 +---- .../spec/markdown-preview-view-spec.coffee | 34 ++ .../stylesheets/markdown-preview.css | 314 +++++++++--------- 7 files changed, 233 insertions(+), 311 deletions(-) create mode 100644 src/packages/markdown-preview/lib/markdown-preview.coffee create mode 100644 src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee 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/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 4292cf072..1575d9e24 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -5,62 +5,21 @@ ScrollView = require 'scroll-view' module.exports = class MarkdownPreviewView extends ScrollView - @activate: -> - @instance = new MarkdownPreviewView - rootView.command 'markdown-preview:show', '.editor', => @show() - - @show: -> - activePane = rootView.getActivePane() - editSession = activePane.activeItem - if nextPane = activePane.getNextPane() - if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}") - nextPane.showItem(preview) - else - nextPane.showItem(new MarkdownPreviewView(editSession.buffer)) - else - activePane.splitRight(new MarkdownPreviewView(editSession.buffer)) - activePane.focus() - @content: -> - @div class: 'markdown-preview', tabindex: -1, => - @div class: 'markdown-body', outlet: 'markdownBody' + @div class: 'markdown-preview', tabindex: -1 initialize: (@buffer) -> super - - rootView.command 'markdown-preview:toggle', => @toggle() + @fetchRenderedMarkdown() getTitle: -> - "Markdown Preview" + "Markdown Preview – #{@buffer.getBaseName()}" getUri: -> "markdown-preview:#{@buffer.getPath()}" - toggle: -> - if @hasParent() - @detach() - else - @attach() - - attach: -> - return unless @isMarkdownEditor() - rootView.append(this) - @markdownBody.html(@getLoadingHtml()) - @loadHtml() - @focus() - - 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 => @@ -69,29 +28,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..588545e83 --- /dev/null +++ b/src/packages/markdown-preview/lib/markdown-preview.coffee @@ -0,0 +1,24 @@ +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) + 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 ce5b1ff39..dbd69aef5 100644 --- a/src/packages/markdown-preview/package.cson +++ b/src/packages/markdown-preview/package.cson @@ -1,3 +1,3 @@ -'main': 'lib/markdown-preview-view' +'main': 'lib/markdown-preview' 'activationEvents': 'markdown-preview:show': '.editor' diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 8f56b2c88..8337ea46f 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -1,16 +1,15 @@ -$ = require 'jquery' RootView = require 'root-view' MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' -_ = require 'underscore' +{$$} = require 'space-pen' -describe "MarkdownPreviewView", -> +describe "MarkdownPreview package", -> beforeEach -> project.setPath(project.resolve('markdown')) window.rootView = new RootView window.loadPackage("markdown-preview") - spyOn(MarkdownPreviewView.prototype, 'loadHtml') + spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown') - fdescribe "markdown-preview:show", -> + describe "markdown-preview:show", -> beforeEach -> rootView.open("file.markdown") @@ -64,79 +63,3 @@ describe "MarkdownPreviewView", -> expect(pane2.getItems()).toHaveLength 2 expect(pane2.activeItem).toBe preview expect(pane1).toMatchSelector(':has(:focus)') - - describe "when the active item is not an edit session ", -> - it "logs a warning to the console saying that it isn't possible to preview the item", -> - - 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') - - 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() - - 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() - - 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() - - 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(MarkdownPreviewView.prototype.loadHtml).not.toHaveBeenCalled() - - 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') - - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView).toExist() - markdownPreviewView.trigger('core:cancel') - expect(rootView.find('.markdown-preview')).not.toExist() - - 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') - - markdownPreviewView = rootView.find('.markdown-preview') - editor.focus() - expect(markdownPreviewView).toExist() - expect(rootView.find('.markdown-preview')).not.toExist() - - 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() 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..59d3202ae --- /dev/null +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee @@ -0,0 +1,34 @@ +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" diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css index 1138dc1b7..9eb41bd60 100644 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.css +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.css @@ -2,38 +2,28 @@ 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; + -webkit-flex: 1; } -.markdown-body { - min-width: 680px; -} - -.markdown-body pre, -.markdown-body code, -.markdown-body tt { +.markdown-preview pre, +.markdown-preview code, +.markdown-preview tt { font-size: 12px; font-family: Consolas, "Liberation Mono", Courier, monospace; } -.markdown-body a { +.markdown-preview a { color: #4183c4; } -.markdown-body ol > li { +.markdown-preview ol > li { list-style-type: decimal; } -.markdown-body ul > li { +.markdown-preview ul > li { list-style-type: disc; } @@ -50,17 +40,17 @@ /* 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 { +.markdown-preview { font-size: 14px; line-height: 1.6; overflow: hidden; } - .markdown-body > *:first-child { + .markdown-preview > *:first-child { margin-top: 0 !important; } - .markdown-body > *:last-child { + .markdown-preview > *:last-child { margin-bottom: 0 !important; } - .markdown-body a.absent { + .markdown-preview a.absent { color: #c00; } - .markdown-body a.anchor { + .markdown-preview a.anchor { display: block; padding-left: 30px; margin-left: -30px; @@ -69,130 +59,130 @@ top: 0; left: 0; bottom: 0; } - .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { + .markdown-preview h1, .markdown-preview h2, .markdown-preview h3, .markdown-preview h4, .markdown-preview h5, .markdown-preview 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 { + .markdown-preview h1 .mini-icon-link, .markdown-preview h2 .mini-icon-link, .markdown-preview h3 .mini-icon-link, .markdown-preview h4 .mini-icon-link, .markdown-preview h5 .mini-icon-link, .markdown-preview 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 { + .markdown-preview h1:hover a.anchor, .markdown-preview h2:hover a.anchor, .markdown-preview h3:hover a.anchor, .markdown-preview h4:hover a.anchor, .markdown-preview h5:hover a.anchor, .markdown-preview 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 { + .markdown-preview h1:hover a.anchor .mini-icon-link, .markdown-preview h2:hover a.anchor .mini-icon-link, .markdown-preview h3:hover a.anchor .mini-icon-link, .markdown-preview h4:hover a.anchor .mini-icon-link, .markdown-preview h5:hover a.anchor .mini-icon-link, .markdown-preview 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 { + .markdown-preview h1 tt, .markdown-preview h1 code, .markdown-preview h2 tt, .markdown-preview h2 code, .markdown-preview h3 tt, .markdown-preview h3 code, .markdown-preview h4 tt, .markdown-preview h4 code, .markdown-preview h5 tt, .markdown-preview h5 code, .markdown-preview h6 tt, .markdown-preview h6 code { font-size: inherit; } - .markdown-body h1 { + .markdown-preview h1 { font-size: 28px; color: #000; } - .markdown-body h2 { + .markdown-preview h2 { font-size: 24px; border-bottom: 1px solid #ccc; color: #000; } - .markdown-body h3 { + .markdown-preview h3 { font-size: 18px; } - .markdown-body h4 { + .markdown-preview h4 { font-size: 16px; } - .markdown-body h5 { + .markdown-preview h5 { font-size: 14px; } - .markdown-body h6 { + .markdown-preview 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 { + .markdown-preview p, + .markdown-preview blockquote, + .markdown-preview ul, .markdown-preview ol, .markdown-preview dl, + .markdown-preview table, + .markdown-preview pre { margin: 15px 0; } - .markdown-body hr { + .markdown-preview 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 { + .markdown-preview > h2:first-child, .markdown-preview > h1:first-child, .markdown-preview > h1:first-child + h2, .markdown-preview > h3:first-child, .markdown-preview > h4:first-child, .markdown-preview > h5:first-child, .markdown-preview > 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 { + .markdown-preview a:first-child h1, .markdown-preview a:first-child h2, .markdown-preview a:first-child h3, .markdown-preview a:first-child h4, .markdown-preview a:first-child h5, .markdown-preview 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 { + .markdown-preview h1 + p, + .markdown-preview h2 + p, + .markdown-preview h3 + p, + .markdown-preview h4 + p, + .markdown-preview h5 + p, + .markdown-preview h6 + p { margin-top: 0; } - .markdown-body li p.first { + .markdown-preview li p.first { display: inline-block; } - .markdown-body ul, .markdown-body ol { + .markdown-preview ul, .markdown-preview ol { padding-left: 30px; } - .markdown-body ul.no-list, .markdown-body ol.no-list { + .markdown-preview ul.no-list, .markdown-preview 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 { + .markdown-preview ul li > :first-child, + .markdown-preview ul li ul:first-of-type, .markdown-preview ol li > :first-child, + .markdown-preview 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 { + .markdown-preview ul ul, + .markdown-preview ul ol, + .markdown-preview ol ol, + .markdown-preview ol ul { margin-bottom: 0; } - .markdown-body dl { + .markdown-preview dl { padding: 0; } - .markdown-body dl dt { + .markdown-preview dl dt { font-size: 14px; font-weight: bold; font-style: italic; padding: 0; margin: 15px 0 5px; } - .markdown-body dl dt:first-child { + .markdown-preview dl dt:first-child { padding: 0; } - .markdown-body dl dt > :first-child { + .markdown-preview dl dt > :first-child { margin-top: 0px; } - .markdown-body dl dt > :last-child { + .markdown-preview dl dt > :last-child { margin-bottom: 0px; } - .markdown-body dl dd { + .markdown-preview dl dd { margin: 0 0 15px; padding: 0 15px; } - .markdown-body dl dd > :first-child { + .markdown-preview dl dd > :first-child { margin-top: 0px; } - .markdown-body dl dd > :last-child { + .markdown-preview dl dd > :last-child { margin-bottom: 0px; } - .markdown-body blockquote { + .markdown-preview blockquote { border-left: 4px solid #DDD; padding: 0 15px; color: #777; } - .markdown-body blockquote > :first-child { + .markdown-preview blockquote > :first-child { margin-top: 0px; } - .markdown-body blockquote > :last-child { + .markdown-preview blockquote > :last-child { margin-bottom: 0px; } - .markdown-body table th { + .markdown-preview table th { font-weight: bold; } - .markdown-body table th, .markdown-body table td { + .markdown-preview table th, .markdown-preview table td { border: 1px solid #ccc; padding: 6px 13px; } - .markdown-body table tr { + .markdown-preview table tr { border-top: 1px solid #ccc; background-color: #fff; } - .markdown-body table tr:nth-child(2n) { + .markdown-preview table tr:nth-child(2n) { background-color: #f8f8f8; } - .markdown-body img { + .markdown-preview img { max-width: 100%; -moz-box-sizing: border-box; box-sizing: border-box; } - .markdown-body span.frame { + .markdown-preview span.frame { display: block; overflow: hidden; } - .markdown-body span.frame > span { + .markdown-preview span.frame > span { border: 1px solid #ddd; display: block; float: left; @@ -200,70 +190,70 @@ margin: 13px 0 0; padding: 7px; width: auto; } - .markdown-body span.frame span img { + .markdown-preview span.frame span img { display: block; float: left; } - .markdown-body span.frame span span { + .markdown-preview span.frame span span { clear: both; color: #333; display: block; padding: 5px 0 0; } - .markdown-body span.align-center { + .markdown-preview span.align-center { display: block; overflow: hidden; clear: both; } - .markdown-body span.align-center > span { + .markdown-preview span.align-center > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: center; } - .markdown-body span.align-center span img { + .markdown-preview span.align-center span img { margin: 0 auto; text-align: center; } - .markdown-body span.align-right { + .markdown-preview span.align-right { display: block; overflow: hidden; clear: both; } - .markdown-body span.align-right > span { + .markdown-preview span.align-right > span { display: block; overflow: hidden; margin: 13px 0 0; text-align: right; } - .markdown-body span.align-right span img { + .markdown-preview span.align-right span img { margin: 0; text-align: right; } - .markdown-body span.float-left { + .markdown-preview span.float-left { display: block; margin-right: 13px; overflow: hidden; float: left; } - .markdown-body span.float-left span { + .markdown-preview span.float-left span { margin: 13px 0 0; } - .markdown-body span.float-right { + .markdown-preview span.float-right { display: block; margin-left: 13px; overflow: hidden; float: right; } - .markdown-body span.float-right > span { + .markdown-preview span.float-right > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: right; } - .markdown-body code, .markdown-body tt { + .markdown-preview code, .markdown-preview tt { margin: 0 2px; padding: 0px 5px; border: 1px solid #eaeaea; background-color: #f8f8f8; border-radius: 3px; } - .markdown-body code { + .markdown-preview code { white-space: nowrap; } - .markdown-body pre > code { + .markdown-preview pre > code { margin: 0; padding: 0; white-space: pre; border: none; background: transparent; } - .markdown-body .highlight pre, .markdown-body pre { + .markdown-preview .highlight pre, .markdown-preview pre { background-color: #f8f8f8; border: 1px solid #ccc; font-size: 13px; @@ -271,168 +261,168 @@ overflow: auto; padding: 6px 10px; border-radius: 3px; } - .markdown-body pre code, .markdown-body pre tt { + .markdown-preview pre code, .markdown-preview 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 { +/* the .markdown-preview class was then added to all rules */ +.markdown-preview .highlight { background: #ffffff; } - .markdown-body .highlight .c { + .markdown-preview .highlight .c { color: #999988; font-style: italic; } - .markdown-body .highlight .err { + .markdown-preview .highlight .err { color: #a61717; background-color: #e3d2d2; } - .markdown-body .highlight .k { + .markdown-preview .highlight .k { font-weight: bold; } - .markdown-body .highlight .o { + .markdown-preview .highlight .o { font-weight: bold; } - .markdown-body .highlight .cm { + .markdown-preview .highlight .cm { color: #999988; font-style: italic; } - .markdown-body .highlight .cp { + .markdown-preview .highlight .cp { color: #999999; font-weight: bold; } - .markdown-body .highlight .c1 { + .markdown-preview .highlight .c1 { color: #999988; font-style: italic; } - .markdown-body .highlight .cs { + .markdown-preview .highlight .cs { color: #999999; font-weight: bold; font-style: italic; } - .markdown-body .highlight .gd { + .markdown-preview .highlight .gd { color: #000000; background-color: #ffdddd; } - .markdown-body .highlight .gd .x { + .markdown-preview .highlight .gd .x { color: #000000; background-color: #ffaaaa; } - .markdown-body .highlight .ge { + .markdown-preview .highlight .ge { font-style: italic; } - .markdown-body .highlight .gr { + .markdown-preview .highlight .gr { color: #aa0000; } - .markdown-body .highlight .gh { + .markdown-preview .highlight .gh { color: #999999; } - .markdown-body .highlight .gi { + .markdown-preview .highlight .gi { color: #000000; background-color: #ddffdd; } - .markdown-body .highlight .gi .x { + .markdown-preview .highlight .gi .x { color: #000000; background-color: #aaffaa; } - .markdown-body .highlight .go { + .markdown-preview .highlight .go { color: #888888; } - .markdown-body .highlight .gp { + .markdown-preview .highlight .gp { color: #555555; } - .markdown-body .highlight .gs { + .markdown-preview .highlight .gs { font-weight: bold; } - .markdown-body .highlight .gu { + .markdown-preview .highlight .gu { color: #800080; font-weight: bold; } - .markdown-body .highlight .gt { + .markdown-preview .highlight .gt { color: #aa0000; } - .markdown-body .highlight .kc { + .markdown-preview .highlight .kc { font-weight: bold; } - .markdown-body .highlight .kd { + .markdown-preview .highlight .kd { font-weight: bold; } - .markdown-body .highlight .kn { + .markdown-preview .highlight .kn { font-weight: bold; } - .markdown-body .highlight .kp { + .markdown-preview .highlight .kp { font-weight: bold; } - .markdown-body .highlight .kr { + .markdown-preview .highlight .kr { font-weight: bold; } - .markdown-body .highlight .kt { + .markdown-preview .highlight .kt { color: #445588; font-weight: bold; } - .markdown-body .highlight .m { + .markdown-preview .highlight .m { color: #009999; } - .markdown-body .highlight .s { + .markdown-preview .highlight .s { color: #d14; } - .markdown-body .highlight .na { + .markdown-preview .highlight .na { color: #008080; } - .markdown-body .highlight .nb { + .markdown-preview .highlight .nb { color: #0086B3; } - .markdown-body .highlight .nc { + .markdown-preview .highlight .nc { color: #445588; font-weight: bold; } - .markdown-body .highlight .no { + .markdown-preview .highlight .no { color: #008080; } - .markdown-body .highlight .ni { + .markdown-preview .highlight .ni { color: #800080; } - .markdown-body .highlight .ne { + .markdown-preview .highlight .ne { color: #990000; font-weight: bold; } - .markdown-body .highlight .nf { + .markdown-preview .highlight .nf { color: #990000; font-weight: bold; } - .markdown-body .highlight .nn { + .markdown-preview .highlight .nn { color: #555555; } - .markdown-body .highlight .nt { + .markdown-preview .highlight .nt { color: #000080; } - .markdown-body .highlight .nv { + .markdown-preview .highlight .nv { color: #008080; } - .markdown-body .highlight .ow { + .markdown-preview .highlight .ow { font-weight: bold; } - .markdown-body .highlight .w { + .markdown-preview .highlight .w { color: #bbbbbb; } - .markdown-body .highlight .mf { + .markdown-preview .highlight .mf { color: #009999; } - .markdown-body .highlight .mh { + .markdown-preview .highlight .mh { color: #009999; } - .markdown-body .highlight .mi { + .markdown-preview .highlight .mi { color: #009999; } - .markdown-body .highlight .mo { + .markdown-preview .highlight .mo { color: #009999; } - .markdown-body .highlight .sb { + .markdown-preview .highlight .sb { color: #d14; } - .markdown-body .highlight .sc { + .markdown-preview .highlight .sc { color: #d14; } - .markdown-body .highlight .sd { + .markdown-preview .highlight .sd { color: #d14; } - .markdown-body .highlight .s2 { + .markdown-preview .highlight .s2 { color: #d14; } - .markdown-body .highlight .se { + .markdown-preview .highlight .se { color: #d14; } - .markdown-body .highlight .sh { + .markdown-preview .highlight .sh { color: #d14; } - .markdown-body .highlight .si { + .markdown-preview .highlight .si { color: #d14; } - .markdown-body .highlight .sx { + .markdown-preview .highlight .sx { color: #d14; } - .markdown-body .highlight .sr { + .markdown-preview .highlight .sr { color: #009926; } - .markdown-body .highlight .s1 { + .markdown-preview .highlight .s1 { color: #d14; } - .markdown-body .highlight .ss { + .markdown-preview .highlight .ss { color: #990073; } - .markdown-body .highlight .bp { + .markdown-preview .highlight .bp { color: #999999; } - .markdown-body .highlight .vc { + .markdown-preview .highlight .vc { color: #008080; } - .markdown-body .highlight .vg { + .markdown-preview .highlight .vg { color: #008080; } - .markdown-body .highlight .vi { + .markdown-preview .highlight .vi { color: #008080; } - .markdown-body .highlight .il { + .markdown-preview .highlight .il { color: #009999; } - .markdown-body .highlight .gc { + .markdown-preview .highlight .gc { color: #999; background-color: #EAF2F5; } -.type-csharp .markdown-body .highlight .k { +.type-csharp .markdown-preview .highlight .k { color: #0000FF; } -.type-csharp .markdown-body .highlight .kt { +.type-csharp .markdown-preview .highlight .kt { color: #0000FF; } -.type-csharp .markdown-body .highlight .nf { +.type-csharp .markdown-preview .highlight .nf { color: #000000; font-weight: normal; } -.type-csharp .markdown-body .highlight .nc { +.type-csharp .markdown-preview .highlight .nc { color: #2B91AF; } -.type-csharp .markdown-body .highlight .nn { +.type-csharp .markdown-preview .highlight .nn { color: #000000; } -.type-csharp .markdown-body .highlight .s { +.type-csharp .markdown-preview .highlight .s { color: #A31515; } -.type-csharp .markdown-body .highlight .sc { +.type-csharp .markdown-preview .highlight .sc { color: #A31515; } From f432ad350f06713e6e84dd2d131a6a4f2f72746c Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 8 Mar 2013 09:16:10 -0800 Subject: [PATCH 06/80] Exclude package-generator templates from coffee compilation Closes #359 --- script/generate-sources-gypi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/generate-sources-gypi b/script/generate-sources-gypi index 266fde180..f890ee16f 100755 --- a/script/generate-sources-gypi +++ b/script/generate-sources-gypi @@ -5,9 +5,10 @@ set -e cd "$(dirname $0)/.." DIRS="src static vendor" +EXCLUDE_DIRS="src/packages/package-generator/template" find_files() { - find ${DIRS} -type file -name ${1} + find ${DIRS} -type file -name ${1} | grep -v ${EXCLUDE_DIRS} } file_list() { From 709d9738efb24f497f857fc36625e4cfd3fa40cc Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 8 Mar 2013 09:27:44 -0800 Subject: [PATCH 07/80] Revert "Exclude package-generator templates from coffee compilation" This reverts commit f432ad350f06713e6e84dd2d131a6a4f2f72746c. --- script/generate-sources-gypi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/generate-sources-gypi b/script/generate-sources-gypi index f890ee16f..266fde180 100755 --- a/script/generate-sources-gypi +++ b/script/generate-sources-gypi @@ -5,10 +5,9 @@ set -e cd "$(dirname $0)/.." DIRS="src static vendor" -EXCLUDE_DIRS="src/packages/package-generator/template" find_files() { - find ${DIRS} -type file -name ${1} | grep -v ${EXCLUDE_DIRS} + find ${DIRS} -type file -name ${1} } file_list() { From 5cd3dfce8e6af6c5456986e6728c25a6211901ee Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 8 Mar 2013 09:28:07 -0800 Subject: [PATCH 08/80] Add .template extension to package-generator template files Acutally Closes #359 --- src/packages/package-generator/lib/package-generator-view.coffee | 1 + .../{__package-name__.cson => __package-name__.cson.template} | 0 ...-name__-view.coffee => __package-name__-view.coffee.template} | 0 ...{__package-name__.coffee => __package-name__.coffee.template} | 0 ...-name__-spec.coffee => __package-name__-spec.coffee.template} | 0 ...ew-spec.coffee => __package-name__-view-spec.coffee.template} | 0 .../{__package-name__.css => __package-name__.css.template} | 0 7 files changed, 1 insertion(+) rename src/packages/package-generator/template/keymaps/{__package-name__.cson => __package-name__.cson.template} (100%) rename src/packages/package-generator/template/lib/{__package-name__-view.coffee => __package-name__-view.coffee.template} (100%) rename src/packages/package-generator/template/lib/{__package-name__.coffee => __package-name__.coffee.template} (100%) rename src/packages/package-generator/template/spec/{__package-name__-spec.coffee => __package-name__-spec.coffee.template} (100%) rename src/packages/package-generator/template/spec/{__package-name__-view-spec.coffee => __package-name__-view-spec.coffee.template} (100%) rename src/packages/package-generator/template/stylesheets/{__package-name__.css => __package-name__.css.template} (100%) diff --git a/src/packages/package-generator/lib/package-generator-view.coffee b/src/packages/package-generator/lib/package-generator-view.coffee index d1ca900cc..a38315446 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/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 From 1561f22853f644579b3b066474fcdc27429991d0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 10:26:44 -0800 Subject: [PATCH 09/80] Clean project before tests are run --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index e26ae7a7b..5893ed201 100644 --- a/Rakefile +++ b/Rakefile @@ -83,7 +83,7 @@ task :clean do end desc "Run the specs" -task :test => ["update-cef", "clone-default-bundles", "build"] do +task :test => ["clean", "update-cef", "clone-default-bundles", "build"] do `pkill Atom` if path = application_path() cmd = "#{path}/Contents/MacOS/Atom --test --resource-path=#{ATOM_SRC_PATH} 2> /dev/null" From 22d1336aa006f2ad036a091280d9b32c097d450f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 7 Mar 2013 16:52:03 -0800 Subject: [PATCH 10/80] Apply correct flexbox styling to `.pane > .item-views > *` Markdown preview was overflowing because the min-height of flexbox items is automatically set to min-content. Setting it to 0 ensures that the item doesn't expand beyond its flex size. Moving this styling to atom.css ensures that people don't have to work too hard to fit their views into panes. --- .../markdown-preview/stylesheets/markdown-preview.css | 8 ++------ static/atom.css | 5 +++++ static/editor.css | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css index 9eb41bd60..e45e5f436 100644 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.css +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.css @@ -3,9 +3,8 @@ font-size: 14px; line-height: 1.6; background-color: #fff; - overflow: auto; + overflow: scroll; padding: 20px; - -webkit-flex: 1; } .markdown-preview pre, @@ -40,10 +39,7 @@ /* 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-preview { - font-size: 14px; - line-height: 1.6; - overflow: hidden; } +.markdown-preview {} .markdown-preview > *:first-child { margin-top: 0 !important; } .markdown-preview > *:last-child { diff --git a/static/atom.css b/static/atom.css index 6cfb79269..7013037a3 100644 --- a/static/atom.css +++ b/static/atom.css @@ -61,6 +61,11 @@ html, body { -webkit-flex-flow: column; } +#panes .pane .item-views > * { + -webkit-flex: 1; + min-height: 0; +} + @font-face { font-family: 'Octicons Regular'; src: url("octicons-regular-webfont.woff") format("woff"); diff --git a/static/editor.css b/static/editor.css index 1b6c35a74..c88b3f1f2 100644 --- a/static/editor.css +++ b/static/editor.css @@ -6,7 +6,6 @@ z-index: 0; font-family: Inconsolata, Monaco, Courier; line-height: 1.3; - -webkit-flex: 1; } .editor.mini { From cf6c46ba3ac55fed987f24ca4a8f8a7ca0219312 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:10:13 -0800 Subject: [PATCH 11/80] Tree view deselects entry when active item has no path --- src/packages/tree-view/lib/tree-view.coffee | 11 ++++++-- .../tree-view/spec/tree-view-spec.coffee | 28 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/packages/tree-view/lib/tree-view.coffee b/src/packages/tree-view/lib/tree-view.coffee index bf1df2b2e..5a6ea480f 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() @@ -290,9 +292,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 fc2f9fe81..2bd5ac3d6 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' @@ -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(require.resolve('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", -> From 8ca8841f9ed90489fb40c4064129a2356079f33f Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:14:44 -0800 Subject: [PATCH 12/80] Make command-panel's editor have -webkit-flex: 1 We moved the setting of flex on editors to the panes stylesheet previously, but here is another context where we need flex to be set. --- static/command-panel.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/command-panel.css b/static/command-panel.css index 8f2098259..a169d2759 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -122,6 +122,7 @@ .command-panel .prompt-and-editor .editor { position: relative; + -webkit-flex: 1; } .command-panel .prompt-and-editor { From 06c9a3ac8603270a3b48bb9bc206e4e7e7071a74 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:45:20 -0800 Subject: [PATCH 13/80] Remove empty panes when PaneContainer deserializes --- spec/app/pane-container-spec.coffee | 7 +++++++ src/app/pane-container.coffee | 5 +++++ src/app/pane.coffee | 7 ++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 56ea53336..c448ca3d2 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -146,3 +146,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/src/app/pane-container.coffee b/src/app/pane-container.coffee index 6fc367e56..225716d0c 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: -> @@ -93,5 +94,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 b0134f552..240540042 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() From f3910ba34e993beba65d229525cb84334f2e26be Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:59:49 -0800 Subject: [PATCH 14/80] WIP: make markdown preview view serializable --- .../markdown-preview/lib/markdown-preview-view.coffee | 9 +++++++++ .../spec/markdown-preview-view-spec.coffee | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 1575d9e24..f06842de6 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -5,6 +5,11 @@ ScrollView = require 'scroll-view' module.exports = class MarkdownPreviewView extends ScrollView + registerDeserializer(this) + + @deserialize: ({path}) -> + new MarkdownPreviewView(project.bufferForPath(path)) + @content: -> @div class: 'markdown-preview', tabindex: -1 @@ -12,6 +17,10 @@ class MarkdownPreviewView extends ScrollView super @fetchRenderedMarkdown() + serialize: -> + deserializer: 'MarkdownPreviewView' + path: @buffer.getPath() + getTitle: -> "Markdown Preview – #{@buffer.getBaseName()}" diff --git a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee index 59d3202ae..7d98a70dc 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee @@ -32,3 +32,8 @@ describe "MarkdownPreviewView", -> it "shows an error message on error", -> ajaxArgs.error() expect(preview.text()).toContain "Failed" + + describe "serialization", -> + fit "reassociates with the same buffer when deserialized", -> + newPreview = deserialize(preview.serialize()) + expect(newPreview.buffer).toBe buffer From 110d3719bb2a85e4034978bb0b03d7fc56a22530 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 8 Mar 2013 13:15:41 -0800 Subject: [PATCH 15/80] Use actual root path length Previously a one was added just to use for the length with the null byte when creating the root path passed to fts_open. Closes #391 --- native/v8_extensions/native.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 21f7e6c3a..71a975c8f 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -166,8 +166,8 @@ namespace v8_extensions { } else if (name == "traverseTree") { std::string argument = arguments[0]->GetStringValue().ToString(); - int rootPathLength = argument.size() + 1; - char rootPath[rootPathLength]; + int rootPathLength = argument.size(); + char rootPath[rootPathLength + 1]; strcpy(rootPath, argument.c_str()); char * const paths[] = {rootPath, NULL}; From 8cf32149b7e9dda31eea36125ae6259a932dfacb Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 8 Mar 2013 13:42:31 -0800 Subject: [PATCH 16/80] Return absolute paths from $native.traverseTree() Previously relative paths were generated even though things like fs.list() and fs.listTree() would just recombine them with the root path. Closes #391 --- native/v8_extensions/native.mm | 9 ++------- spec/stdlib/fs-spec.coffee | 12 +++++++----- src/app/config.coffee | 12 ++++++------ .../fuzzy-finder/lib/load-paths-handler.coffee | 2 ++ src/stdlib/fs.coffee | 6 +++--- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 71a975c8f..b34b68145 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -166,8 +166,7 @@ namespace v8_extensions { } else if (name == "traverseTree") { std::string argument = arguments[0]->GetStringValue().ToString(); - int rootPathLength = argument.size(); - char rootPath[rootPathLength + 1]; + char rootPath[argument.size() + 1]; strcpy(rootPath, argument.c_str()); char * const paths[] = {rootPath, NULL}; @@ -191,12 +190,8 @@ namespace v8_extensions { continue; } - int pathLength = entry->fts_pathlen - rootPathLength; - char relative[pathLength + 1]; - relative[pathLength] = '\0'; - strncpy(relative, entry->fts_path + rootPathLength, pathLength); args.clear(); - args.push_back(CefV8Value::CreateString(relative)); + args.push_back(CefV8Value::CreateString(entry->fts_path)); if (isFile) { onFile->ExecuteFunction(onFile, args); } diff --git a/spec/stdlib/fs-spec.coffee b/spec/stdlib/fs-spec.coffee index b0a6fae5f..94445d011 100644 --- a/spec/stdlib/fs-spec.coffee +++ b/spec/stdlib/fs-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.traverseTree 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.traverseTree(fs.join(fixturesDir, 'symlink-to-dir'), onSymlinkPath, onSymlinkPath) - fs.traverseTree(fs.join(fixturesDir, 'dir'), onPath, onPath) + fs.traverseTree(symlinkPath, onSymlinkPath, onSymlinkPath) + fs.traverseTree(regularPath, onPath, onPath) expect(symlinkPaths).toEqual(paths) diff --git a/src/app/config.coffee b/src/app/config.coffee index 2940b33f4..7c22c362e 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -37,16 +37,16 @@ class Config 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.traverseTree(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.traverseTree(bundledThemesDirPath, onThemeDirFile, (path) -> true) load: -> diff --git a/src/packages/fuzzy-finder/lib/load-paths-handler.coffee b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee index 9ef442c99..ac7c338dc 100644 --- a/src/packages/fuzzy-finder/lib/load-paths-handler.coffee +++ b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee @@ -13,8 +13,10 @@ module.exports = return true if _.contains(ignoredNames, segment) repo?.isPathIgnored(fs.join(rootPath, path)) onFile = (path) -> + path = path.substring(rootPath.length + 1) paths.push(path) unless isIgnored(path) onDirectory = (path) -> + path = path.substring(rootPath.length + 1) not isIgnored(path) fs.traverseTree(rootPath, onFile, onDirectory) diff --git a/src/stdlib/fs.coffee b/src/stdlib/fs.coffee index cac22e5fe..5d9ffe226 100644 --- a/src/stdlib/fs.coffee +++ b/src/stdlib/fs.coffee @@ -63,11 +63,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 @traverseTree(rootPath, onPath, onPath) paths @@ -75,7 +75,7 @@ module.exports = listTree: (rootPath) -> paths = [] onPath = (path) => - paths.push(@join(rootPath, path)) + paths.push(path) true @traverseTree(rootPath, onPath, onPath) paths From 6dd9d011aa446e910739e6eafed47080b662eaa3 Mon Sep 17 00:00:00 2001 From: Ben Burkert Date: Mon, 11 Mar 2013 10:58:52 -0300 Subject: [PATCH 17/80] The CSON library requires the underscore-extensions library. --- src/stdlib/cson.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stdlib/cson.coffee b/src/stdlib/cson.coffee index 382d8600a..84832f35f 100644 --- a/src/stdlib/cson.coffee +++ b/src/stdlib/cson.coffee @@ -1,3 +1,4 @@ +require 'underscore-extensions' _ = require 'underscore' module.exports = From 0b674978dbe6e52d42b1c8588dbd4e0d66b51b58 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Thu, 28 Feb 2013 14:27:56 -0800 Subject: [PATCH 18/80] Require will parse .less files into css --- spec/fixtures/sample-with-error.less | 1 + spec/fixtures/sample.less | 8 + spec/stdlib/require-spec.coffee | 19 + src/stdlib/require.coffee | 7 + vendor/less.js | 5078 ++++++++++++++++++++++++++ 5 files changed, 5113 insertions(+) create mode 100644 spec/fixtures/sample-with-error.less create mode 100644 spec/fixtures/sample.less create mode 100644 spec/stdlib/require-spec.coffee create mode 100644 vendor/less.js 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/stdlib/require-spec.coffee b/spec/stdlib/require-spec.coffee new file mode 100644 index 000000000..a4b5004a4 --- /dev/null +++ b/spec/stdlib/require-spec.coffee @@ -0,0 +1,19 @@ +{less} = require('less') + +describe "require", -> + describe "files with a `.less` extension", -> + it "parses valid files into css", -> + output = require(project.resolve("sample.less")) + expect(output).toBe """ + #header { + color: #4d926f; + } + h2 { + color: #4d926f; + } + + """ + + it "throws an error when parsing invalid file", -> + functionWithError = (-> require(project.resolve("sample-with-error.less"))) + expect(functionWithError).toThrow() \ No newline at end of file diff --git a/src/stdlib/require.coffee b/src/stdlib/require.coffee index d7e8b87dd..42f0f05b3 100644 --- a/src/stdlib/require.coffee +++ b/src/stdlib/require.coffee @@ -66,6 +66,13 @@ exts = evaluated = exts.js(file, compiled) $native.write(cacheFilePath, compiled) if writeToCache evaluated + less: (file) -> + output = "" + (new less.Parser).parse __read(file), (e, tree) -> + throw new Error(e.message, file, e.line) if e + output = tree.toCSS() + output + getPath = (path) -> path = resolve(path) 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 From b502c811cbbf3a2e0aee488aec176c88d8f1d090 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:12:20 -0800 Subject: [PATCH 19/80] move less parsing to requireStylesheet --- spec/app/window-spec.coffee | 20 ++++++++++++++++++++ src/app/window.coffee | 14 ++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 12f79248f..0ae136754 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -1,5 +1,6 @@ $ = require 'jquery' fs = require 'fs' +{less} = require 'less' describe "Window", -> projectPath = null @@ -78,6 +79,25 @@ describe "Window", -> requireStylesheet('atom.css') expect($('head style').length).toBe lengthBefore + 1 + it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", -> + $('head style[id*="markdown.less"]').remove() + lengthBefore = $('head style').length + requireStylesheet('markdown.less') + expect($('head style').length).toBe lengthBefore + 1 + + styleElt = $('head style[id*="markdown.less"]') + + fullPath = require.resolve('markdown.less') + expect(styleElt.attr('id')).toBe fullPath + + (new less.Parser).parse __read(fullPath), (e, tree) -> + throw new Error(e.message, file, e.line) if e + expect(styleElt.text()).toBe tree.toCSS() + + # doesn't append twice + requireStylesheet('markdown.less') + expect($('head style').length).toBe lengthBefore + 1 + describe ".disableStyleSheet(path)", -> it "removes styling applied by given stylesheet path", -> cssPath = require.resolve(fs.join("fixtures", "css.css")) diff --git a/src/app/window.coffee b/src/app/window.coffee index 321a60f56..3fafdd953 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -1,6 +1,7 @@ fs = require 'fs' $ = require 'jquery' ChildProcess = require 'child-process' +{less} = require 'less' require 'jquery-extensions' require 'underscore-extensions' require 'space-pen-extensions' @@ -33,7 +34,7 @@ window.setUpEnvironment = -> requireStylesheet 'overlay.css' requireStylesheet 'popover-list.css' requireStylesheet 'notification.css' - requireStylesheet 'markdown.css' + requireStylesheet 'markdown.less' if nativeStylesheetPath = require.resolve("#{platform}.css") requireStylesheet(nativeStylesheetPath) @@ -114,8 +115,17 @@ window.stylesheetElementForId = (id) -> $("head style[id='#{id}']") window.requireStylesheet = (path) -> + console.log path if fullPath = require.resolve(path) - window.applyStylesheet(fullPath, fs.read(fullPath)) + content = "" + if fs.extension(fullPath) == '.less' + (new less.Parser).parse __read(fullPath), (e, tree) -> + throw new Error(e.message, file, e.line) if e + content = tree.toCSS() + else + content = fs.read(fullPath) + + window.applyStylesheet(fullPath, content) unless fullPath throw new Error("Could not find a file at path '#{path}'") From 0e2ada4a916099e6a78eb30e7ee1eeda98b90097 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:12:33 -0800 Subject: [PATCH 20/80] markdown.css -> markdown.less --- static/markdown.css | 23 ----------------------- static/markdown.less | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 23 deletions(-) delete mode 100644 static/markdown.css create mode 100644 static/markdown.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; + } + } +} From b33494eada55452ed4fac4daf5d0198f1cbed737 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:14:10 -0800 Subject: [PATCH 21/80] -console.log --- src/app/window.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/window.coffee b/src/app/window.coffee index 3fafdd953..14bb504f0 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -115,7 +115,6 @@ window.stylesheetElementForId = (id) -> $("head style[id='#{id}']") window.requireStylesheet = (path) -> - console.log path if fullPath = require.resolve(path) content = "" if fs.extension(fullPath) == '.less' From 3d3947722f66585ee1e50fc8ac187cb0fd30b586 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:58:40 -0800 Subject: [PATCH 22/80] autocomplete.css -> autocomplete.less --- .../stylesheets/{autocomplete.css => autocomplete.less} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/packages/autocomplete/stylesheets/{autocomplete.css => autocomplete.less} (100%) 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 From bd0751e17ee846447e2e34df8430488ddcec5061 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:58:55 -0800 Subject: [PATCH 23/80] braket-matcher.css -> bracket-matcher.less --- .../stylesheets/{bracket-matcher.css => bracket-matcher.less} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/packages/bracket-matcher/stylesheets/{bracket-matcher.css => bracket-matcher.less} (100%) 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 From 3821a492fc0693d7b6ddbeb6d83238cb0d1308cb Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:59:13 -0800 Subject: [PATCH 24/80] command-logger.css -> command-logger.less --- .../stylesheets/{command-logger.css => command-logger.less} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/packages/command-logger/stylesheets/{command-logger.css => command-logger.less} (100%) 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 From 03642a292303a992dcee99c6bc14f566602028ee Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:59:26 -0800 Subject: [PATCH 25/80] editor-stats.css -> editor-stats.less --- .../editor-stats/stylesheets/editor-stats.css | 45 ------------------- .../stylesheets/editor-stats.less | 45 +++++++++++++++++++ 2 files changed, 45 insertions(+), 45 deletions(-) delete mode 100644 src/packages/editor-stats/stylesheets/editor-stats.css create mode 100644 src/packages/editor-stats/stylesheets/editor-stats.less 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; + } +} From dcfee2d9d93722094a7571a4db34fee2b598d620 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 12:00:08 -0800 Subject: [PATCH 26/80] markdown preview to less. also seperate out pygments colors --- .../stylesheets/markdown-preview.css | 438 ------------------ .../stylesheets/markdown-preview.less | 385 +++++++++++++++ .../stylesheets/pygments.less | 201 ++++++++ 3 files changed, 586 insertions(+), 438 deletions(-) delete mode 100644 src/packages/markdown-preview/stylesheets/markdown-preview.css create mode 100644 src/packages/markdown-preview/stylesheets/markdown-preview.less create mode 100644 src/packages/markdown-preview/stylesheets/pygments.less 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..db4a68195 --- /dev/null +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.less @@ -0,0 +1,385 @@ +.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; +} + +// This is styling for generic markdownized text. Anything you put in a +// container with .markdown-body on it should render generally well. It also +// includes some GitHub Flavored Markdown specific styling (like @mentions) +.markdown-body { + + font-size: 14px; + line-height: 1.6; + overflow: hidden; + + & > *: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 }
    +}
    
    From 758c9f116f847e7274a52d63eed18bafe4cd64c0 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:06 -0800
    Subject: [PATCH 27/80] package-generator.css -> package-generator.less
    
    ---
     .../stylesheets/{package-generator.css => package-generator.less} | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename src/packages/package-generator/stylesheets/{package-generator.css => package-generator.less} (100%)
    
    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
    
    From 7ad67e50a66282ff6635efef41a3abe8709a3e23 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:24 -0800
    Subject: [PATCH 28/80] spell-check.css -> spell-check.less
    
    ---
     .../spell-check/stylesheets/{spell-check.css => spell-check.less} | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename src/packages/spell-check/stylesheets/{spell-check.css => spell-check.less} (100%)
    
    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
    
    From 6fd0118d2c92adc6400005cf8e1d07e06209effe Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:40 -0800
    Subject: [PATCH 29/80] tree-view.css -> tree-view.less
    
    ---
     .../tree-view/stylesheets/tree-view.css       | 57 -------------------
     .../tree-view/stylesheets/tree-view.less      | 57 +++++++++++++++++++
     2 files changed, 57 insertions(+), 57 deletions(-)
     delete mode 100644 src/packages/tree-view/stylesheets/tree-view.css
     create mode 100644 src/packages/tree-view/stylesheets/tree-view.less
    
    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;
    +}
    
    From e1a9362448c871d29633e502c2256535be474f1f Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:57 -0800
    Subject: [PATCH 30/80] wrap-guide.css -> wrap-guide.less
    
    ---
     .../wrap-guide/stylesheets/{wrap-guide.css => wrap-guide.less}    | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename src/packages/wrap-guide/stylesheets/{wrap-guide.css => wrap-guide.less} (100%)
    
    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
    
    From b9604d1baa2794697a52d09ffa0befc0dcd5adb5 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:24:22 -0800
    Subject: [PATCH 31/80] reset.css -> reset.less
    
    ---
     src/app/window.coffee            | 2 +-
     static/{reset.css => reset.less} | 0
     2 files changed, 1 insertion(+), 1 deletion(-)
     rename static/{reset.css => reset.less} (100%)
    
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 14bb504f0..3cab24751 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -24,7 +24,7 @@ window.setUpEnvironment = ->
       $(document).on 'keydown', keymap.handleKeyEvent
       keymap.bindDefaultKeys()
     
    -  requireStylesheet 'reset.css'
    +  requireStylesheet 'reset.less'
       requireStylesheet 'atom.css'
       requireStylesheet 'tabs.css'
       requireStylesheet 'tree-view.css'
    diff --git a/static/reset.css b/static/reset.less
    similarity index 100%
    rename from static/reset.css
    rename to static/reset.less
    
    From 2b66b033e03871888665fef540a1a8d0462cd447 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:37:41 -0800
    Subject: [PATCH 32/80] add less parsing helper to spec helper
    
    ---
     spec/spec-helper.coffee | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index 4632db296..c03a9b6ed 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -4,6 +4,7 @@ window.setUpEnvironment()
     nakedLoad 'jasmine-jquery'
     $ = require 'jquery'
     _ = require 'underscore'
    +{less} = require 'less'
     Keymap = require 'keymap'
     Config = require 'config'
     Point = require 'point'
    @@ -213,6 +214,13 @@ window.setEditorHeightInLines = (editor, heightInChars, charHeight=editor.lineHe
       editor.height(charHeight * heightInChars + editor.renderedLines.position().top)
       $(window).trigger 'resize' # update editor's on-screen lines
     
    +window.parseLessFile = (path) ->
    +  content = ""
    +  (new less.Parser).parse __read(path), (e, tree) ->
    +    throw new Error(e.message, file, e.line) if e
    +    content = tree.toCSS()
    +  content
    +
     $.fn.resultOfTrigger = (type) ->
       event = $.Event(type)
       this.trigger(event)
    
    From a448a79ae67ca732496b400c1329e04e39bd4677 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:37:56 -0800
    Subject: [PATCH 33/80] atom.css -> atom.less
    
    ---
     spec/app/window-spec.coffee | 17 ++++----
     src/app/window.coffee       |  2 +-
     static/atom.css             | 81 -------------------------------------
     static/atom.less            | 75 ++++++++++++++++++++++++++++++++++
     4 files changed, 83 insertions(+), 92 deletions(-)
     delete mode 100644 static/atom.css
     create mode 100644 static/atom.less
    
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index 0ae136754..dac3771aa 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -64,19 +64,19 @@ describe "Window", ->
     
       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()
    +      $('head style[id*="atom.less"]').remove()
           lengthBefore = $('head style').length
    -      requireStylesheet('atom.css')
    +      requireStylesheet('atom.less')
           expect($('head style').length).toBe lengthBefore + 1
     
    -      styleElt = $('head style[id*="atom.css"]')
    +      styleElt = $('head style[id*="atom.less"]')
     
    -      fullPath = require.resolve('atom.css')
    +      fullPath = require.resolve('atom.less')
           expect(styleElt.attr('id')).toBe fullPath
    -      expect(styleElt.text()).toBe fs.read(fullPath)
    +      expect(styleElt.text()).toBe parseLessFile(fullPath)
     
           # doesn't append twice
    -      requireStylesheet('atom.css')
    +      requireStylesheet('atom.less')
           expect($('head style').length).toBe lengthBefore + 1
     
         it  "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
    @@ -89,10 +89,7 @@ describe "Window", ->
     
           fullPath = require.resolve('markdown.less')
           expect(styleElt.attr('id')).toBe fullPath
    -
    -      (new less.Parser).parse __read(fullPath), (e, tree) ->
    -        throw new Error(e.message, file, e.line) if e
    -        expect(styleElt.text()).toBe tree.toCSS()
    +      expect(styleElt.text()).toBe parseLessFile(fullPath)
     
           # doesn't append twice
           requireStylesheet('markdown.less')
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 3cab24751..4a548b5bd 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -25,7 +25,7 @@ window.setUpEnvironment = ->
       keymap.bindDefaultKeys()
     
       requireStylesheet 'reset.less'
    -  requireStylesheet 'atom.css'
    +  requireStylesheet 'atom.less'
       requireStylesheet 'tabs.css'
       requireStylesheet 'tree-view.css'
       requireStylesheet 'status-bar.css'
    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..c02f90d90
    --- /dev/null
    +++ b/static/atom.less
    @@ -0,0 +1,75 @@
    +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;
    +    }
    +  }
    +}
    +
    +@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;
    +}
    
    From 392b9cfeabc1689c52a986127d8738b5be983f8a Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:47:35 -0800
    Subject: [PATCH 34/80] command-panel.css -> command-panel.less
    
    ---
     src/app/window.coffee     |   2 +-
     static/command-panel.css  | 134 --------------------------------------
     static/command-panel.less | 131 +++++++++++++++++++++++++++++++++++++
     3 files changed, 132 insertions(+), 135 deletions(-)
     delete mode 100644 static/command-panel.css
     create mode 100644 static/command-panel.less
    
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 4a548b5bd..6ab360ad0 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -29,7 +29,7 @@ window.setUpEnvironment = ->
       requireStylesheet 'tabs.css'
       requireStylesheet 'tree-view.css'
       requireStylesheet 'status-bar.css'
    -  requireStylesheet 'command-panel.css'
    +  requireStylesheet 'command-panel.less'
       requireStylesheet 'fuzzy-finder.css'
       requireStylesheet 'overlay.css'
       requireStylesheet 'popover-list.css'
    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..18fa88fab
    --- /dev/null
    +++ b/static/command-panel.less
    @@ -0,0 +1,131 @@
    +.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;
    +    }
    +  }
    +}
    +
    +.error-messages {
    +  padding: 5px 1em;
    +  color: white;
    +}
    
    From 0624ba6d3fc3929c03ed07846d45af3fc72626c6 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 15:10:52 -0800
    Subject: [PATCH 35/80] rename remaining static css files to less
    
    ---
     benchmark/benchmark-helper.coffee              |  3 +--
     spec/spec-helper.coffee                        |  2 +-
     src/app/editor.coffee                          |  2 +-
     src/app/select-list.coffee                     |  2 +-
     src/app/window.coffee                          | 14 +++++++-------
     static/{editor.css => editor.less}             |  0
     static/{fuzzy-finder.css => fuzzy-finder.less} |  0
     static/{jasmine.css => jasmine.less}           |  0
     static/{notification.css => notification.less} |  0
     static/{overlay.css => overlay.less}           |  0
     static/{popover-list.css => popover-list.less} |  0
     static/{select-list.css => select-list.less}   |  0
     static/{status-bar.css => status-bar.less}     |  0
     static/{tabs.css => tabs.less}                 |  0
     static/{tree-view.css => tree-view.less}       |  0
     15 files changed, 11 insertions(+), 12 deletions(-)
     rename static/{editor.css => editor.less} (100%)
     rename static/{fuzzy-finder.css => fuzzy-finder.less} (100%)
     rename static/{jasmine.css => jasmine.less} (100%)
     rename static/{notification.css => notification.less} (100%)
     rename static/{overlay.css => overlay.less} (100%)
     rename static/{popover-list.css => popover-list.less} (100%)
     rename static/{select-list.css => select-list.less} (100%)
     rename static/{status-bar.css => status-bar.less} (100%)
     rename static/{tabs.css => tabs.less} (100%)
     rename static/{tree-view.css => tree-view.less} (100%)
    
    diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee
    index dff4cee35..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()
    @@ -127,4 +127,3 @@ $.fn.textInput = (data) ->
       event = document.createEvent 'TextEvent'
       event.initTextEvent('textInput', true, true, window, data)
       this.each -> this.dispatchEvent(event)
    -
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index c03a9b6ed..c5c846112 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -16,7 +16,7 @@ TokenizedBuffer = require 'tokenized-buffer'
     fs = require 'fs'
     RootView = require 'root-view'
     Git = require 'git'
    -requireStylesheet "jasmine.css"
    +requireStylesheet "jasmine.less"
     fixturePackagesPath = require.resolve('fixtures/packages')
     require.paths.unshift(fixturePackagesPath)
     keymap.loadBundledKeymaps()
    diff --git a/src/app/editor.coffee b/src/app/editor.coffee
    index 1e2c0b464..a8380d7af 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/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/window.coffee b/src/app/window.coffee
    index 6ab360ad0..5b1614085 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -26,14 +26,14 @@ window.setUpEnvironment = ->
     
       requireStylesheet 'reset.less'
       requireStylesheet 'atom.less'
    -  requireStylesheet 'tabs.css'
    -  requireStylesheet 'tree-view.css'
    -  requireStylesheet 'status-bar.css'
    +  requireStylesheet 'tabs.less'
    +  requireStylesheet 'tree-view.less'
    +  requireStylesheet 'status-bar.less'
       requireStylesheet 'command-panel.less'
    -  requireStylesheet 'fuzzy-finder.css'
    -  requireStylesheet 'overlay.css'
    -  requireStylesheet 'popover-list.css'
    -  requireStylesheet 'notification.css'
    +  requireStylesheet 'fuzzy-finder.less'
    +  requireStylesheet 'overlay.less'
    +  requireStylesheet 'popover-list.less'
    +  requireStylesheet 'notification.less'
       requireStylesheet 'markdown.less'
     
       if nativeStylesheetPath = require.resolve("#{platform}.css")
    diff --git a/static/editor.css b/static/editor.less
    similarity index 100%
    rename from static/editor.css
    rename to static/editor.less
    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/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/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 100%
    rename from static/status-bar.css
    rename to static/status-bar.less
    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
    
    From 050c376e87c45d80635e5c27783b3da3d33e3fc4 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 15:23:21 -0800
    Subject: [PATCH 36/80] remove old less require spec
    
    ---
     spec/stdlib/require-spec.coffee | 19 -------------------
     1 file changed, 19 deletions(-)
     delete mode 100644 spec/stdlib/require-spec.coffee
    
    diff --git a/spec/stdlib/require-spec.coffee b/spec/stdlib/require-spec.coffee
    deleted file mode 100644
    index a4b5004a4..000000000
    --- a/spec/stdlib/require-spec.coffee
    +++ /dev/null
    @@ -1,19 +0,0 @@
    -{less} = require('less')
    -
    -describe "require", ->
    -  describe "files with a `.less` extension", ->
    -    it "parses valid files into css", ->
    -      output = require(project.resolve("sample.less"))
    -      expect(output).toBe """
    -        #header {
    -          color: #4d926f;
    -        }
    -        h2 {
    -          color: #4d926f;
    -        }
    -
    -      """
    -
    -    it "throws an error when parsing invalid file", ->
    -      functionWithError = (-> require(project.resolve("sample-with-error.less")))
    -      expect(functionWithError).toThrow()
    \ No newline at end of file
    
    From 9acd401b9ec4a2975f959870973e4fd4b7b8556c Mon Sep 17 00:00:00 2001
    From: probablycorey 
    Date: Tue, 5 Mar 2013 17:03:28 -0800
    Subject: [PATCH 37/80] Use fixture files to requireStylesheet
    
    ---
     spec/app/window-spec.coffee | 40 ++++++++++++++++++++++---------------
     spec/spec-helper.coffee     |  8 --------
     2 files changed, 24 insertions(+), 24 deletions(-)
    
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index dac3771aa..5687cb3d4 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -63,37 +63,45 @@ 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.less"]').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.less')
    +
    +      requireStylesheet(cssPath)
           expect($('head style').length).toBe lengthBefore + 1
     
    -      styleElt = $('head style[id*="atom.less"]')
    -
    -      fullPath = require.resolve('atom.less')
    -      expect(styleElt.attr('id')).toBe fullPath
    -      expect(styleElt.text()).toBe parseLessFile(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.less')
    +      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", ->
    -      $('head style[id*="markdown.less"]').remove()
    +      lessPath = project.resolve('sample.less')
           lengthBefore = $('head style').length
    -      requireStylesheet('markdown.less')
    +      requireStylesheet(lessPath)
           expect($('head style').length).toBe lengthBefore + 1
     
    -      styleElt = $('head style[id*="markdown.less"]')
    +      element = $('head style[id*="sample.less"]')
    +      expect(element.attr('id')).toBe lessPath
    +      expect(element.text()).toBe """
    +      #header {
    +        color: #4d926f;
    +      }
    +      h2 {
    +        color: #4d926f;
    +      }
     
    -      fullPath = require.resolve('markdown.less')
    -      expect(styleElt.attr('id')).toBe fullPath
    -      expect(styleElt.text()).toBe parseLessFile(fullPath)
    +      """
     
           # doesn't append twice
    -      requireStylesheet('markdown.less')
    +      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", ->
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index c5c846112..e220facdc 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -4,7 +4,6 @@ window.setUpEnvironment()
     nakedLoad 'jasmine-jquery'
     $ = require 'jquery'
     _ = require 'underscore'
    -{less} = require 'less'
     Keymap = require 'keymap'
     Config = require 'config'
     Point = require 'point'
    @@ -214,13 +213,6 @@ window.setEditorHeightInLines = (editor, heightInChars, charHeight=editor.lineHe
       editor.height(charHeight * heightInChars + editor.renderedLines.position().top)
       $(window).trigger 'resize' # update editor's on-screen lines
     
    -window.parseLessFile = (path) ->
    -  content = ""
    -  (new less.Parser).parse __read(path), (e, tree) ->
    -    throw new Error(e.message, file, e.line) if e
    -    content = tree.toCSS()
    -  content
    -
     $.fn.resultOfTrigger = (type) ->
       event = $.Event(type)
       this.trigger(event)
    
    From 478a376c97b7a5d3b1bc300ac0d972d5a8d446a7 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Fri, 8 Mar 2013 16:27:14 -0800
    Subject: [PATCH 38/80] Less files work in atom themes
    
    ---
     spec/app/theme-spec.coffee                    | 25 +++++++++++--------
     spec/fixtures/themes/theme-stylesheet.less    |  5 ++++
     .../theme-with-package-file/package.json      |  2 +-
     .../themes/theme-with-package-file/second.css |  5 ----
     .../theme-with-package-file/second.less       |  7 ++++++
     .../themes/theme-without-package-file/c.css   |  3 ---
     .../themes/theme-without-package-file/c.less  |  5 ++++
     src/app/atom-theme.coffee                     |  6 ++---
     src/app/theme.coffee                          |  2 +-
     src/app/window.coffee                         | 21 +++++++++-------
     10 files changed, 49 insertions(+), 32 deletions(-)
     create mode 100644 spec/fixtures/themes/theme-stylesheet.less
     delete mode 100644 spec/fixtures/themes/theme-with-package-file/second.css
     create mode 100644 spec/fixtures/themes/theme-with-package-file/second.less
     delete mode 100644 spec/fixtures/themes/theme-without-package-file/c.css
     create mode 100644 spec/fixtures/themes/theme-without-package-file/c.less
    
    diff --git a/spec/app/theme-spec.coffee b/spec/app/theme-spec.coffee
    index 518482292..53349c8ba 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/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/src/app/atom-theme.coffee b/src/app/atom-theme.coffee
    index 6a3e1e379..04563b3c7 100644
    --- a/src/app/atom-theme.coffee
    +++ b/src/app/atom-theme.coffee
    @@ -5,10 +5,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'])
    @@ -17,6 +17,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/theme.coffee b/src/app/theme.coffee
    index b16a7ef1c..ba35e23e1 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 5b1614085..7d1558bd6 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -116,18 +116,21 @@ window.stylesheetElementForId = (id) ->
     
     window.requireStylesheet = (path) ->
       if fullPath = require.resolve(path)
    -    content = ""
    -    if fs.extension(fullPath) == '.less'
    -      (new less.Parser).parse __read(fullPath), (e, tree) ->
    -        throw new Error(e.message, file, e.line) if e
    -        content = tree.toCSS()
    -    else
    -      content = fs.read(fullPath)
    -
    +    content = window.loadStylesheet(fullPath)
         window.applyStylesheet(fullPath, content)
    -  unless fullPath
    +  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}'")
    
    From 7c47b7f8d41d219fff2f869f14765305c78ab6df Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 09:47:34 -0700
    Subject: [PATCH 39/80] Pane styling isn't a child of the .root-view class
    
    Panes aren't attached to a root view in spes.
    ---
     static/atom.less | 68 ++++++++++++++++++++++++++----------------------
     1 file changed, 37 insertions(+), 31 deletions(-)
    
    diff --git a/static/atom.less b/static/atom.less
    index c02f90d90..0ab8cae9a 100644
    --- a/static/atom.less
    +++ b/static/atom.less
    @@ -19,39 +19,45 @@ html, body {
         -webkit-flex: 1;
         -webkit-flex-flow: column;
       }
    +}
     
    -  #panes {
    -    position: relative;
    +#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;
    -
    -    .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;
    -    }
    +    display: -webkit-flex;
    +    -webkit-flex-flow: column;
       }
     }
     
    
    From 634702005dcdd78f94c1379ff4867586d2db11c6 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 10:24:00 -0700
    Subject: [PATCH 40/80] :lipstick:
    
    ---
     native/atom_main_mac.mm | 16 ++++++----------
     1 file changed, 6 insertions(+), 10 deletions(-)
    
    diff --git a/native/atom_main_mac.mm b/native/atom_main_mac.mm
    index 6c148fc84..1f418f9f0 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);
     
    
    From 8fec1e82eebcad2bd330c7cb2ea419f7b9f47e91 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 10:26:46 -0700
    Subject: [PATCH 41/80] Use instantiateWithOwner:topLevelObjects
    
    Removes deprecation warnings
    ---
     native/atom_main_mac.mm | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/native/atom_main_mac.mm b/native/atom_main_mac.mm
    index 1f418f9f0..ed1f3c3ca 100644
    --- a/native/atom_main_mac.mm
    +++ b/native/atom_main_mac.mm
    @@ -29,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();
       }
    
    From 8247e56bef0cdf9c86b41ba842aa4b5c18adaf83 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 10:49:01 -0700
    Subject: [PATCH 42/80] Fix objective-c compiler warning
    
    ---
     native/atom_application.h | 1 +
     1 file changed, 1 insertion(+)
    
    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;
    
    From a39217416421c90676fa9f3a70840abd769dd641 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 09:08:29 -0700
    Subject: [PATCH 43/80] Un-f
    
    ---
     .../markdown-preview/spec/markdown-preview-view-spec.coffee     | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee
    index 7d98a70dc..42cac9b84 100644
    --- a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee
    +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee
    @@ -34,6 +34,6 @@ describe "MarkdownPreviewView", ->
           expect(preview.text()).toContain "Failed"
     
       describe "serialization", ->
    -    fit "reassociates with the same buffer when deserialized", ->
    +    it "reassociates with the same buffer when deserialized", ->
           newPreview = deserialize(preview.serialize())
           expect(newPreview.buffer).toBe buffer
    
    From 10d0fdf2d7ca1328335a0523e8e6546d95912e2b Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 15:08:19 -0600
    Subject: [PATCH 44/80] Require a deferred package early if needed when
     deserializing panes
    
    The requiring of a package's main module is now decoupled from package
    activation. Non-deferred packages will always be required before the
    panes are deserialized. This allows the package to register any
    deserializers for objects displayed in the panes.
    
    Deferred packages can contain a 'deferredDeserializers' array in their
    package.cson. If we attempt to deserialize an object with a deserializer
    in the list, the package's main module will be required first so it has
    a chance to register the deserializer. But the package still won't be
    activated until an activation event occurs.
    
    We may want to add an additional optional hook called 'load' which is
    called at require time. We would not guarantee that the rootView
    global would exist, but we could give the package a chance to register
    deserializers etc. For now, registering deserializers is a side-effect
    of requiring the package.
    ---
     spec/app/atom-package-spec.coffee             |   4 +-
     spec/app/atom-spec.coffee                     |   9 +-
     spec/spec-helper.coffee                       |   4 +-
     src/app/atom-package.coffee                   | 117 ++++++++++--------
     src/app/atom.coffee                           |   3 +
     src/app/text-mate-package.coffee              |   2 +
     src/app/window.coffee                         |  13 +-
     src/packages/markdown-preview/package.cson    |   1 +
     .../spec/markdown-preview-spec.coffee         |   2 +-
     .../spell-check/spec/spell-check-spec.coffee  |   2 +-
     10 files changed, 97 insertions(+), 60 deletions(-)
    
    diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee
    index 7b75b8ede..8154b08b4 100644
    --- a/spec/app/atom-package-spec.coffee
    +++ b/spec/app/atom-package-spec.coffee
    @@ -3,7 +3,7 @@ AtomPackage = require 'atom-package'
     fs = require 'fs'
     
     describe "AtomPackage", ->
    -  describe ".load()", ->
    +  describe ".activate()", ->
         beforeEach ->
           window.rootView = new RootView
     
    @@ -15,6 +15,7 @@ describe "AtomPackage", ->
             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,6 +45,7 @@ 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", ->
    diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee
    index e614a2ff7..717a75426 100644
    --- a/spec/app/atom-spec.coffee
    +++ b/spec/app/atom-spec.coffee
    @@ -103,7 +103,7 @@ describe "the `atom` global", ->
             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')
    @@ -115,7 +115,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
    @@ -124,8 +125,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/spec-helper.coffee b/spec/spec-helper.coffee
    index 151ca8488..b56f9ca78 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -88,12 +88,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)
    diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee
    index 4c3195b0e..1fd4d9dbf 100644
    --- a/src/app/atom-package.coffee
    +++ b/src/app/atom-package.coffee
    @@ -7,66 +7,21 @@ module.exports =
     class AtomPackage extends Package
       metadata: null
       packageMain: null
    +  deferActivation: false
     
    -  load: ({activateImmediately}={}) ->
    +  load: ->
         try
           @loadMetadata()
           @loadKeymaps()
           @loadStylesheets()
    -      if @metadata.activationEvents and not activateImmediately
    -        @subscribeToActivationEvents()
    +      if @deferActivation = @metadata.activationEvents?
    +        @registerDeferredDeserializers()
           else
    -        @activatePackageMain()
    +        @requirePackageMain()
         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
    -    mainPath = require.resolve(mainPath)
    -    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 = fs.readObject(metadataPath)
    @@ -86,3 +41,65 @@ class AtomPackage extends Package
         stylesheetDirPath = fs.join(@path, 'stylesheets')
         for stylesheetPath in fs.list(stylesheetDirPath)
           requireStylesheet(stylesheetPath)
    +
    +  activate: ->
    +    if @deferActivation
    +      @subscribeToActivationEvents()
    +    else
    +      try
    +        if @requirePackageMain()
    +          config.setDefaults(@name, @packageMain.configDefaults)
    +          atom.activateAtomPackage(this)
    +      catch e
    +        console.warn "Failed to activate package named '#{@name}'", e.stack
    +
    +  requirePackageMain: ->
    +    return @packageMain if @packageMain
    +    mainPath = @path
    +    mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
    +    mainPath = require.resolve(mainPath)
    +    @packageMain = require(mainPath) if fs.isFile(mainPath)
    +
    +  registerDeferredDeserializers: ->
    +    for deserializerName in @metadata.deferredDeserializers ? []
    +      registerDeferredDeserializer deserializerName, => @requirePackageMain()
    +
    +  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.coffee b/src/app/atom.coffee
    index 2e4635c18..472a6dcb4 100644
    --- a/src/app/atom.coffee
    +++ b/src/app/atom.coffee
    @@ -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/text-mate-package.coffee b/src/app/text-mate-package.coffee
    index c4901cc02..2929e1883 100644
    --- a/src/app/text-mate-package.coffee
    +++ b/src/app/text-mate-package.coffee
    @@ -31,6 +31,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/window.coffee b/src/app/window.coffee
    index 321a60f56..1a58bce09 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -6,6 +6,7 @@ require 'underscore-extensions'
     require 'space-pen-extensions'
     
     deserializers = {}
    +deferredDeserializers = {}
     
     # This method is called in any window needing a general environment, including specs
     window.setUpEnvironment = ->
    @@ -52,10 +53,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
    @@ -151,6 +153,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 +165,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()
    diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson
    index dbd69aef5..29925172d 100644
    --- a/src/packages/markdown-preview/package.cson
    +++ b/src/packages/markdown-preview/package.cson
    @@ -1,3 +1,4 @@
     'main': 'lib/markdown-preview'
     'activationEvents':
       '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 8337ea46f..4bb3dbdf8 100644
    --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    @@ -6,7 +6,7 @@ describe "MarkdownPreview package", ->
       beforeEach ->
         project.setPath(project.resolve('markdown'))
         window.rootView = new RootView
    -    window.loadPackage("markdown-preview")
    +    window.loadPackage("markdown-preview", activateImmediately: true)
         spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown')
     
       describe "markdown-preview:show", ->
    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()
     
    
    From 50b61f3a00537fe05d1771e106acdd7e4cb28544 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 15:20:51 -0600
    Subject: [PATCH 45/80] Test requiring of package main module via deferred
     deserializer
    
    ---
     spec/app/atom-package-spec.coffee             | 24 ++++++++++++++-----
     .../main.coffee                               |  7 ++++++
     .../package.cson                              |  1 +
     3 files changed, 26 insertions(+), 6 deletions(-)
    
    diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee
    index 8154b08b4..6a0b898bc 100644
    --- a/spec/app/atom-package-spec.coffee
    +++ b/spec/app/atom-package-spec.coffee
    @@ -3,18 +3,30 @@ AtomPackage = require 'atom-package'
     fs = require 'fs'
     
     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.packageMain).toBeNull()
    +        object = deserialize(deserializer: 'Foo', data: "Hello")
    +        expect(object.constructor.name).toBe 'Foo'
    +        expect(object.data).toBe 'Hello'
    +        expect(pack.packageMain).toBeDefined()
    +        expect(pack.packageMain.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", ->
    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'
    
    From bf7fc39434ec6b2c3798b8d9e92f60907c8861d2 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 15:28:28 -0600
    Subject: [PATCH 46/80] Rename AtomPackage.packageMain to .mainModule
    
    ---
     spec/app/atom-package-spec.coffee                         | 6 +++---
     spec/app/atom-spec.coffee                                 | 8 ++++----
     src/app/atom-package.coffee                               | 8 ++++----
     src/app/atom.coffee                                       | 6 +++---
     .../command-logger/spec/command-logger-spec.coffee        | 2 +-
     src/packages/command-panel/spec/command-panel-spec.coffee | 2 +-
     src/packages/editor-stats/spec/editor-stats-spec.coffee   | 2 +-
     src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee   | 2 +-
     src/packages/tree-view/spec/tree-view-spec.coffee         | 6 +++---
     9 files changed, 21 insertions(+), 21 deletions(-)
    
    diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee
    index 6a0b898bc..469eb59e4 100644
    --- a/spec/app/atom-package-spec.coffee
    +++ b/spec/app/atom-package-spec.coffee
    @@ -12,12 +12,12 @@ describe "AtomPackage", ->
       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.packageMain).toBeNull()
    +        expect(pack.mainModule).toBeNull()
             object = deserialize(deserializer: 'Foo', data: "Hello")
             expect(object.constructor.name).toBe 'Foo'
             expect(object.data).toBe 'Hello'
    -        expect(pack.packageMain).toBeDefined()
    -        expect(pack.packageMain.activateCallCount).toBe 0
    +        expect(pack.mainModule).toBeDefined()
    +        expect(pack.mainModule.activateCallCount).toBe 0
     
       describe ".activate()", ->
         beforeEach ->
    diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee
    index 717a75426..3b007f125 100644
    --- a/spec/app/atom-spec.coffee
    +++ b/spec/app/atom-spec.coffee
    @@ -84,22 +84,22 @@ 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", ->
    diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee
    index 1fd4d9dbf..b671dc045 100644
    --- a/src/app/atom-package.coffee
    +++ b/src/app/atom-package.coffee
    @@ -6,7 +6,7 @@ $ = require 'jquery'
     module.exports =
     class AtomPackage extends Package
       metadata: null
    -  packageMain: null
    +  mainModule: null
       deferActivation: false
     
       load: ->
    @@ -48,17 +48,17 @@ class AtomPackage extends Package
         else
           try
             if @requirePackageMain()
    -          config.setDefaults(@name, @packageMain.configDefaults)
    +          config.setDefaults(@name, @mainModule.configDefaults)
               atom.activateAtomPackage(this)
           catch e
             console.warn "Failed to activate package named '#{@name}'", e.stack
     
       requirePackageMain: ->
    -    return @packageMain if @packageMain
    +    return @mainModule if @mainModule
         mainPath = @path
         mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
         mainPath = require.resolve(mainPath)
    -    @packageMain = require(mainPath) if fs.isFile(mainPath)
    +    @mainModule = require(mainPath) if fs.isFile(mainPath)
     
       registerDeferredDeserializers: ->
         for deserializerName in @metadata.deferredDeserializers ? []
    diff --git a/src/app/atom.coffee b/src/app/atom.coffee
    index 472a6dcb4..315f8314e 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
    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-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/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/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee
    index ffaae8cb0..bde11cca9 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/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee
    index 2bd5ac3d6..d48f5577c 100644
    --- a/src/packages/tree-view/spec/tree-view-spec.coffee
    +++ b/src/packages/tree-view/spec/tree-view-spec.coffee
    @@ -50,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()
    @@ -76,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()
     
    
    From 194ac13f4342e8e36744356afc721ae9dcab1072 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 16:05:20 -0600
    Subject: [PATCH 47/80] :lipstick:
    
    ---
     src/app/atom-package.coffee | 8 ++++----
     1 file changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee
    index b671dc045..8a0e312c9 100644
    --- a/src/app/atom-package.coffee
    +++ b/src/app/atom-package.coffee
    @@ -17,7 +17,7 @@ class AtomPackage extends Package
           if @deferActivation = @metadata.activationEvents?
             @registerDeferredDeserializers()
           else
    -        @requirePackageMain()
    +        @requireMainModule()
         catch e
           console.warn "Failed to load package named '#{@name}'", e.stack
         this
    @@ -47,13 +47,13 @@ class AtomPackage extends Package
           @subscribeToActivationEvents()
         else
           try
    -        if @requirePackageMain()
    +        if @requireMainModule()
               config.setDefaults(@name, @mainModule.configDefaults)
               atom.activateAtomPackage(this)
           catch e
             console.warn "Failed to activate package named '#{@name}'", e.stack
     
    -  requirePackageMain: ->
    +  requireMainModule: ->
         return @mainModule if @mainModule
         mainPath = @path
         mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
    @@ -62,7 +62,7 @@ class AtomPackage extends Package
     
       registerDeferredDeserializers: ->
         for deserializerName in @metadata.deferredDeserializers ? []
    -      registerDeferredDeserializer deserializerName, => @requirePackageMain()
    +      registerDeferredDeserializer deserializerName, => @requireMainModule()
     
       subscribeToActivationEvents: () ->
         return unless @metadata.activationEvents?
    
    From 2aefd8ca46ba21e4e0df75598e3a15bcbdb60c88 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 16:52:15 -0600
    Subject: [PATCH 48/80] Set overflow hidden on status bar
    
    The octicon was causing overflow, which was making the entire view
    scroll because the status bar was bigger than the height used by
    flexbox.
    ---
     static/status-bar.css | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/static/status-bar.css b/static/status-bar.css
    index d7822f50c..f30dd48c8 100644
    --- a/static/status-bar.css
    +++ b/static/status-bar.css
    @@ -5,6 +5,7 @@
       position: relative;
       -webkit-user-select: none;
       cursor: default;
    +  overflow: hidden;
     }
     
     .status-bar .git-branch {
    
    From 140b22737ea3a750baef21fd83605669acf07889 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 16:56:44 -0600
    Subject: [PATCH 49/80] Refetch rendered markdown when triggering preview a
     subsequent time
    
    ---
     src/packages/markdown-preview/lib/markdown-preview.coffee       | 1 +
     src/packages/markdown-preview/spec/markdown-preview-spec.coffee | 2 ++
     2 files changed, 3 insertions(+)
    
    diff --git a/src/packages/markdown-preview/lib/markdown-preview.coffee b/src/packages/markdown-preview/lib/markdown-preview.coffee
    index 588545e83..b30bbb621 100644
    --- a/src/packages/markdown-preview/lib/markdown-preview.coffee
    +++ b/src/packages/markdown-preview/lib/markdown-preview.coffee
    @@ -17,6 +17,7 @@ module.exports =
         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
    diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    index 4bb3dbdf8..9a7ed6e33 100644
    --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    @@ -58,7 +58,9 @@ describe "MarkdownPreview package", ->
               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
    
    From 7e03880bd09b86eb139d66dd83323b3ac5fa0fd7 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 17:31:17 -0600
    Subject: [PATCH 50/80] Add $.fn.scrollUp and .scrollDown
    
    These scroll the element by a small amount up or down.
    ---
     spec/stdlib/jquery-extensions-spec.coffee | 30 +++++++++++++++++++++++
     src/stdlib/jquery-extensions.coffee       |  6 +++++
     2 files changed, 36 insertions(+)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index 6b781370b..4bdd8fd11 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', ->
    @@ -76,6 +77,35 @@ describe 'jQuery extensions', ->
             'a1': "A1: Waste perfectly-good steak"
             'a2': null
     
    +  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/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index 089363577..fe403c2a9 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)
     
    
    From b01a4aa041c6aaecef80cadb5277e86770b0906e Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 17:34:59 -0600
    Subject: [PATCH 51/80] Allow markdown preview view to be scrolled with
     core:move-up/move-down
    
    ---
     src/packages/markdown-preview/lib/markdown-preview-view.coffee | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee
    index f06842de6..a65c09eba 100644
    --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee
    +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee
    @@ -16,6 +16,8 @@ class MarkdownPreviewView extends ScrollView
       initialize: (@buffer) ->
         super
         @fetchRenderedMarkdown()
    +    @on 'core:move-up', => @scrollUp()
    +    @on 'core:move-down', => @scrollDown()
     
       serialize: ->
         deserializer: 'MarkdownPreviewView'
    
    From 214209d2da82591f0bbb1ac81b2e6a413fe4819d Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 18:35:13 -0600
    Subject: [PATCH 52/80] Add docs about serialization
    
    ---
     docs/internals/serialization.md | 98 +++++++++++++++++++++++++++++++++
     1 file changed, 98 insertions(+)
     create mode 100644 docs/internals/serialization.md
    
    diff --git a/docs/internals/serialization.md b/docs/internals/serialization.md
    new file mode 100644
    index 000000000..ec6a2aba3
    --- /dev/null
    +++ b/docs/internals/serialization.md
    @@ -0,0 +1,98 @@
    +## 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 version 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']
    +...
    +```
    
    From 35419e8d80a657a904b69ffdff90ecb437cc5c44 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 19:59:18 -0600
    Subject: [PATCH 53/80] :lipstick:
    
    ---
     docs/internals/serialization.md | 13 ++++++-------
     1 file changed, 6 insertions(+), 7 deletions(-)
    
    diff --git a/docs/internals/serialization.md b/docs/internals/serialization.md
    index ec6a2aba3..ce1d7a78e 100644
    --- a/docs/internals/serialization.md
    +++ b/docs/internals/serialization.md
    @@ -65,13 +65,12 @@ class MyObject
       serialize: -> { version: MyObject.version, ... }
     ```
     
    -Your serializable class can optionally have a version 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.
    +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
     
    
    From 66467b35712e24599ab0c0a5a769663a34857263 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 20:17:24 -0600
    Subject: [PATCH 54/80] Fix markdown preview height. Some styles crept back in
     during merge.
    
    ---
     .../markdown-preview/stylesheets/markdown-preview.less      | 6 ------
     1 file changed, 6 deletions(-)
    
    diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.less b/src/packages/markdown-preview/stylesheets/markdown-preview.less
    index b79aedb3e..608389a88 100644
    --- a/src/packages/markdown-preview/stylesheets/markdown-preview.less
    +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.less
    @@ -2,14 +2,8 @@
       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: scroll;
    -  z-index: 3;
       box-sizing: border-box;
       padding: 20px;
     }
    
    From 7c04aaf536b131876fc40df6837c642dd2ed1ea1 Mon Sep 17 00:00:00 2001
    From: Kevin Sawicki 
    Date: Mon, 11 Mar 2013 19:45:01 -0700
    Subject: [PATCH 55/80] Only set project paths if non-empty
    
    ---
     src/packages/fuzzy-finder/lib/fuzzy-finder.coffee | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    index c6fbe92aa..fbb180020 100644
    --- a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    @@ -37,7 +37,7 @@ module.exports =
         unless @fuzzyFinderView
           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
    
    From 9330276ae97c923f8fe8fa8b2472644787b1ab08 Mon Sep 17 00:00:00 2001
    From: Kevin Sawicki 
    Date: Mon, 11 Mar 2013 19:45:38 -0700
    Subject: [PATCH 56/80] Abort task when view is created
    
    ---
     src/packages/fuzzy-finder/lib/fuzzy-finder.coffee | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    index fbb180020..d90ff497d 100644
    --- a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    @@ -35,6 +35,7 @@ module.exports =
     
       createView:  ->
         unless @fuzzyFinderView
    +      @loadPathsTask?.abort()
           FuzzyFinderView  = require 'fuzzy-finder/lib/fuzzy-finder-view'
           @fuzzyFinderView = new FuzzyFinderView()
           if @projectPaths?.length > 0 and not @fuzzyFinderView.projectPaths?
    
    From 21cdde11889d3d0be458c9c7aeb9f11f506730bf Mon Sep 17 00:00:00 2001
    From: "Kevin R. Barnes" 
    Date: Tue, 12 Mar 2013 09:40:05 -0700
    Subject: [PATCH 57/80] Update relative link for key bindings
    
    ---
     docs/getting-started.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    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)
     
    
    From 9860f32d4cc5d185998569aa3ddd62b3c53aa019 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:41:52 -0700
    Subject: [PATCH 58/80] Add cancel callback to promptToSaveItem
    
    ---
     src/app/pane.coffee | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/src/app/pane.coffee b/src/app/pane.coffee
    index 240540042..c1397b89e 100644
    --- a/src/app/pane.coffee
    +++ b/src/app/pane.coffee
    @@ -159,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?"
           "Your changes will be lost if close this item without saving."
           "Save", => @saveItem(item, nextAction)
    -      "Cancel", null
    +      "Cancel", cancelAction
           "Don't Save", nextAction
         )
     
    
    From 44d4dc7e60255857d1c1d3695a1de43992a8288f Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:42:25 -0700
    Subject: [PATCH 59/80] Use URI in prompt message if pane item doesn't have a
     title
    
    ---
     src/app/pane.coffee | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/app/pane.coffee b/src/app/pane.coffee
    index c1397b89e..3bd9c9f4c 100644
    --- a/src/app/pane.coffee
    +++ b/src/app/pane.coffee
    @@ -162,7 +162,7 @@ class Pane extends View
       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", cancelAction
    
    From d916962a80a5d0c2544f9b3fc5816557ab7730d5 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:43:37 -0700
    Subject: [PATCH 60/80] Defer window close events until all modified pane items
     are handled
    
    If the user presses cancel, the close event is canceled
    ---
     spec/app/pane-container-spec.coffee | 37 +++++++++++++++++++++++++++++
     spec/app/window-spec.coffee         | 21 +++++++++++++++-
     src/app/pane-container.coffee       | 17 +++++++++++++
     src/app/root-view.coffee            |  3 +++
     src/app/window.coffee               |  6 ++++-
     5 files changed, 82 insertions(+), 2 deletions(-)
    
    diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee
    index c448ca3d2..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())
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index 5687cb3d4..20a93ca55 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -35,7 +35,7 @@ describe "Window", ->
             $(window).trigger 'focus'
             expect($("body")).not.toHaveClass("is-blurred")
     
    -  describe ".close()", ->
    +  describe "window close events", ->
         it "is triggered by the 'core:close' event", ->
           spyOn window, 'close'
           $(window).trigger 'core:close'
    @@ -46,6 +46,25 @@ describe "Window", ->
           $(window).trigger 'window:close'
           expect(window.close).toHaveBeenCalled()
     
    +    describe "when modified buffers exist", ->
    +      it "prompts user to save and aborts if prompt 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()
    +
    +      it "prompts user to save and closes", ->
    +        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()
    +
       describe ".reload()", ->
         beforeEach ->
           spyOn($native, "reload")
    diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee
    index 225716d0c..2118af2b0 100644
    --- a/src/app/pane-container.coffee
    +++ b/src/app/pane-container.coffee
    @@ -62,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()
     
    diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee
    index 659b331c9..9c2eaae40 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()
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 562dbe288..ded8aaefb 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -88,10 +88,10 @@ window.installAtomCommand = (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'
    @@ -189,3 +189,7 @@ window.measure = (description, fn) ->
       result = new Date().getTime() - start
       console.log description, result
       value
    +
    +
    +confirmClose = ->
    +  rootView.confirmClose().done -> window.close()
    \ No newline at end of file
    
    From 4755233f92ddb01115c88efe31c10aae58065086 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:54:07 -0700
    Subject: [PATCH 61/80] :lipstick:
    
    ---
     spec/app/window-spec.coffee | 36 ++++++++++++++++--------------------
     1 file changed, 16 insertions(+), 20 deletions(-)
    
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index 20a93ca55..4abcf6d5b 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -35,28 +35,15 @@ describe "Window", ->
             $(window).trigger 'focus'
             expect($("body")).not.toHaveClass("is-blurred")
     
    -  describe "window close events", ->
    -    it "is triggered by the 'core:close' event", ->
    -      spyOn window, 'close'
    -      $(window).trigger 'core: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 modified buffers exist", ->
    -      it "prompts user to save and aborts if prompt 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.")
    +  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).not.toHaveBeenCalled()
    -        expect(atom.confirm).toHaveBeenCalled()
    +        expect(window.close).toHaveBeenCalled()
     
    -      it "prompts user to save and closes", ->
    +    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")
    @@ -65,6 +52,15 @@ describe "Window", ->
             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 ->
           spyOn($native, "reload")
    
    From f7f034ad2ac52fbd1ebc2a7389645fc802d0a4bf Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:54:22 -0700
    Subject: [PATCH 62/80] Remove core:close event from window
    
    ---
     src/app/window.coffee | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index ded8aaefb..bba97a751 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -87,7 +87,6 @@ window.installAtomCommand = (commandPath) ->
         ChildProcess.exec("chmod u+x '#{commandPath}'")
     
     window.handleWindowEvents = ->
    -  $(window).on 'core: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')
    
    From c236325c1a5f2753cbc08be834d734174eda0029 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 16:47:26 -0700
    Subject: [PATCH 63/80] Log errors (instead of crashing) when the config file
     cannot be parsed
    
    Also, config won't overwrite changes to config.cson when the file can not be parsed. Closes #401
    ---
     spec/app/config-spec.coffee | 19 +++++++++++++++++++
     src/app/config.coffee       | 11 +++++++++--
     2 files changed, 28 insertions(+), 2 deletions(-)
    
    diff --git a/spec/app/config-spec.coffee b/spec/app/config-spec.coffee
    index 7062c4042..39aae22fe 100644
    --- a/spec/app/config-spec.coffee
    +++ b/spec/app/config-spec.coffee
    @@ -1,3 +1,4 @@
    +Config = require 'config'
     fs = require 'fs'
     
     describe "Config", ->
    @@ -133,3 +134,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()
    \ No newline at end of file
    diff --git a/src/app/config.coffee b/src/app/config.coffee
    index 7c22c362e..dc79df9ee 100644
    --- a/src/app/config.coffee
    +++ b/src/app/config.coffee
    @@ -20,6 +20,7 @@ class Config
       userPackagesDirPath: userPackagesDirPath
       defaultSettings: null
       settings: null
    +  configFileHasErrors: null
     
       constructor: ->
         @defaultSettings =
    @@ -55,8 +56,13 @@ class Config
     
       loadUserConfig: ->
         if fs.exists(@configFilePath)
    -      userConfig = fs.readObject(@configFilePath)
    -      _.extend(@settings, userConfig)
    +      try
    +        userConfig = fs.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) ?
    @@ -92,6 +98,7 @@ class Config
         subscription
     
       update: ->
    +    return if @configFileHasErrors
         @save()
         @trigger 'updated'
     
    
    From 3ec74f32113d2d30a805f8de0bd2e5215222dc03 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 16:55:28 -0700
    Subject: [PATCH 64/80] Move `toExistOnDisk` matcher to spec helper
    
    ---
     spec/spec-helper.coffee                                     | 5 +++++
     .../package-generator/spec/package-generator-spec.coffee    | 6 ------
     2 files changed, 5 insertions(+), 6 deletions(-)
    
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index 1dd2c4a18..8518fa6c3 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -137,6 +137,11 @@ addCustomMatchers = (spec) ->
           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/src/packages/package-generator/spec/package-generator-spec.coffee b/src/packages/package-generator/spec/package-generator-spec.coffee
    index cae5d5d54..c9dd0c727 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)
     
    
    From 2b35eaa41404da6e19c9efdcd7c8360506bfb91a Mon Sep 17 00:00:00 2001
    From: Kevin Sawicki 
    Date: Wed, 13 Mar 2013 08:39:52 -0700
    Subject: [PATCH 65/80] Update python bundle to 70dd4be
    
    Adds .gypi to file types
    ---
     vendor/packages/python.tmbundle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/vendor/packages/python.tmbundle b/vendor/packages/python.tmbundle
    index df88cd66d..70dd4be1f 160000
    --- a/vendor/packages/python.tmbundle
    +++ b/vendor/packages/python.tmbundle
    @@ -1 +1 @@
    -Subproject commit df88cd66d00ed44b1d1a212a347334bb8308299c
    +Subproject commit 70dd4be1f12d6e5b2f9238f04e38567f7cebfe4c
    
    From 460577d9ae0a71bd65eb7ad327ea6b2d12c0bc4f Mon Sep 17 00:00:00 2001
    From: John Barnette 
    Date: Wed, 13 Mar 2013 19:40:35 -0700
    Subject: [PATCH 66/80] Spike optional doc: key for command
    
    This isn't working yet.
    ---
     src/stdlib/jquery-extensions.coffee     | 33 +++++++++++++++++++------
     src/stdlib/underscore-extensions.coffee |  8 ++++--
     2 files changed, 32 insertions(+), 9 deletions(-)
    
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index fe403c2a9..95c882471 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -59,7 +59,12 @@ $.fn.trueHeight = ->
     $.fn.trueWidth = ->
       this[0].getBoundingClientRect().width
     
    -$.fn.document = (eventDescriptions) ->
    +$.fn.document = (eventDescriptions, optionalDoc) ->
    +  if optionalDoc
    +    eventName = eventDescriptions
    +    eventDescriptions = {}
    +    eventDescriptions[eventName] = optionalDoc
    +
       @data('documentation', {}) unless @data('documentation')
       _.extend(@data('documentation'), eventDescriptions)
     
    @@ -75,12 +80,26 @@ $.fn.events = ->
       else
         events
     
    -$.fn.command = (args...) ->
    -  eventName = args[0]
    -  documentation = {}
    -  documentation[eventName] = _.humanizeEventName(eventName)
    -  @document(documentation)
    -  @on(args...)
    +# Valid calling styles:
    +# command(eventName, handler)
    +# command(eventName, selector, handler)
    +# command(eventName, options, handler)
    +# command(eventName, selector, options, handler)
    +$.fn.command = (eventName, selector, options, handler) ->
    +  if not options? and not handler?
    +    handler  = selector
    +    selector = null
    +  else if not handler?
    +    handler = options
    +    options = null
    +
    +  if selector? and typeof(selector) is 'object'
    +    handler  = options
    +    options  = selector
    +    selector = null
    +
    +  @document(eventName, _.humanizeEventName(eventName, options?["xxx"]))
    +  @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 df5865b24..824424099 100644
    --- a/src/stdlib/underscore-extensions.coffee
    +++ b/src/stdlib/underscore-extensions.coffee
    @@ -52,10 +52,14 @@ _.mixin
         regex = RegExp('[' + specials.join('\\') + ']', 'g')
         string.replace(regex, "\\$&");
     
    -  humanizeEventName: (eventName) ->
    +  humanizeEventName: (eventName, optionalDocString) ->
    +    return "GitHub" if eventName.toLowerCase() is "github"
    +
         if /:/.test(eventName)
           [namespace, name] = eventName.split(':')
    -      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name)}"
    +      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name, optionalDocString)}"
    +
    +    return optionalDocString if not _.isEmpty(optionalDocString)
     
         words = eventName.split('-')
         words.map(_.capitalize).join(' ')
    
    From eb5d0fe3f5bbb774aaee1c9fc5d6528b90adb466 Mon Sep 17 00:00:00 2001
    From: John Barnette 
    Date: Wed, 13 Mar 2013 21:30:00 -0700
    Subject: [PATCH 67/80] Actually make doc: work
    
    ---
     src/stdlib/jquery-extensions.coffee     | 10 ++--------
     src/stdlib/underscore-extensions.coffee | 16 +++++++++-------
     2 files changed, 11 insertions(+), 15 deletions(-)
    
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index 95c882471..80397f975 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -80,13 +80,8 @@ $.fn.events = ->
       else
         events
     
    -# Valid calling styles:
    -# command(eventName, handler)
    -# command(eventName, selector, handler)
    -# command(eventName, options, handler)
    -# command(eventName, selector, options, handler)
     $.fn.command = (eventName, selector, options, handler) ->
    -  if not options? and not handler?
    +  if not options?
         handler  = selector
         selector = null
       else if not handler?
    @@ -94,11 +89,10 @@ $.fn.command = (eventName, selector, options, handler) ->
         options = null
     
       if selector? and typeof(selector) is 'object'
    -    handler  = options
         options  = selector
         selector = null
     
    -  @document(eventName, _.humanizeEventName(eventName, options?["xxx"]))
    +  @document(eventName, _.humanizeEventName(eventName, options?["doc"]))
       @on(eventName, selector, options?['data'], handler)
     
     $.fn.iconSize = (size) ->
    diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
    index 824424099..f490b7715 100644
    --- a/src/stdlib/underscore-extensions.coffee
    +++ b/src/stdlib/underscore-extensions.coffee
    @@ -52,17 +52,16 @@ _.mixin
         regex = RegExp('[' + specials.join('\\') + ']', 'g')
         string.replace(regex, "\\$&");
     
    -  humanizeEventName: (eventName, optionalDocString) ->
    +  humanizeEventName: (eventName, eventDoc) ->
         return "GitHub" if eventName.toLowerCase() is "github"
     
    -    if /:/.test(eventName)
    -      [namespace, name] = eventName.split(':')
    -      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name, optionalDocString)}"
    +    [namespace, event]  = eventName.split(':')
    +    return _.capitalize(namespace) unless event?
     
    -    return optionalDocString if not _.isEmpty(optionalDocString)
    +    namespaceDoc   = _.undasherize(namespace)
    +    eventDoc     ||= _.undasherize(event)
     
    -    words = eventName.split('-')
    -    words.map(_.capitalize).join(' ')
    +    "#{namespaceDoc}: #{eventDoc}"
     
       capitalize: (word) ->
         word[0].toUpperCase() + word[1..]
    @@ -84,6 +83,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) ->
    
    From 25839c5cf52bf3092dad99441c249441f23c8e5a Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Thu, 14 Mar 2013 11:15:55 -0600
    Subject: [PATCH 68/80] Add spec coverage for `$.fn.command`
    
    ---
     spec/stdlib/jquery-extensions-spec.coffee | 41 +++++++++++++++++++++++
     1 file changed, 41 insertions(+)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index 4bdd8fd11..f0962a200 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -77,6 +77,47 @@ 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!'
    +
       describe "$.fn.scrollUp/Down/ToTop/ToBottom", ->
         it "scrolls the element in the specified way if possible", ->
           view = $$ -> @div => _.times 20, => @div('A')
    
    From 634117ed66edd0be5ab4001c58732b17cd36dc34 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Thu, 14 Mar 2013 11:34:28 -0600
    Subject: [PATCH 69/80] Make `$.fn.document` always take event name / doc
     string args
    
    It's simpler and we don't use the other syntax right now.
    ---
     spec/stdlib/jquery-extensions-spec.coffee | 14 ++++++--------
     src/stdlib/jquery-extensions.coffee       |  9 +++------
     2 files changed, 9 insertions(+), 14 deletions(-)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index f0962a200..3b7ce522c 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -42,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', =>
    @@ -50,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', ->
     
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index 80397f975..70d24fc3a 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -59,12 +59,9 @@ $.fn.trueHeight = ->
     $.fn.trueWidth = ->
       this[0].getBoundingClientRect().width
     
    -$.fn.document = (eventDescriptions, optionalDoc) ->
    -  if optionalDoc
    -    eventName = eventDescriptions
    -    eventDescriptions = {}
    -    eventDescriptions[eventName] = optionalDoc
    -
    +$.fn.document = (eventName, docString) ->
    +  eventDescriptions = {}
    +  eventDescriptions[eventName] = docString
       @data('documentation', {}) unless @data('documentation')
       _.extend(@data('documentation'), eventDescriptions)
     
    
    From a1882ffd1f1cc2b1cfb66cb5d7f03c898a1264bf Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Thu, 14 Mar 2013 11:35:06 -0600
    Subject: [PATCH 70/80] Move "GitHub" special-case to `_.capitalize`. Add
     specs.
    
    ---
     spec/stdlib/jquery-extensions-spec.coffee |  4 ++++
     src/stdlib/underscore-extensions.coffee   | 11 ++++++-----
     2 files changed, 10 insertions(+), 5 deletions(-)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index 3b7ce522c..b803de4f1 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -116,6 +116,10 @@ describe 'jQuery extensions', ->
           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')
    diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
    index f490b7715..09c79c4f5 100644
    --- a/src/stdlib/underscore-extensions.coffee
    +++ b/src/stdlib/underscore-extensions.coffee
    @@ -53,18 +53,19 @@ _.mixin
         string.replace(regex, "\\$&");
     
       humanizeEventName: (eventName, eventDoc) ->
    -    return "GitHub" if eventName.toLowerCase() is "github"
    -
         [namespace, event]  = eventName.split(':')
         return _.capitalize(namespace) unless event?
     
    -    namespaceDoc   = _.undasherize(namespace)
    -    eventDoc     ||= _.undasherize(event)
    +    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
    
    From 09259a579d4bb8d68a4a58851c3904aaa3742f7b Mon Sep 17 00:00:00 2001
    From: John Barnette 
    Date: Thu, 14 Mar 2013 18:06:55 -0700
    Subject: [PATCH 71/80] Let's not accidentally publish this :heart:
    
    ---
     package.json | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/package.json b/package.json
    index 1893b5e86..6633d19bd 100644
    --- a/package.json
    +++ b/package.json
    @@ -6,6 +6,8 @@
         "coffee-script": "1.5"
       },
     
    +  "private": true,
    +
       "scripts": {
         "preinstall": "true"
       }
    
    From 919cee3e4a108f677d453d5f1f9fd88d3d55861b Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Wed, 13 Mar 2013 15:43:28 -0700
    Subject: [PATCH 72/80] Allow operations to be added to a PathView
    
    ---
     src/packages/command-panel/lib/path-view.coffee | 14 +++++++++-----
     1 file changed, 9 insertions(+), 5 deletions(-)
    
    diff --git a/src/packages/command-panel/lib/path-view.coffee b/src/packages/command-panel/lib/path-view.coffee
    index dbd93751d..cdefe073e 100644
    --- a/src/packages/command-panel/lib/path-view.coffee
    +++ b/src/packages/command-panel/lib/path-view.coffee
    @@ -5,18 +5,16 @@ $ = 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}) ->
    +  initialize: ({operations, @previewList}) ->
         @pathDetails.on 'mousedown', => @toggle(true)
         @subscribe @previewList, 'command-panel:collapse-result', =>
           @collapse(true) if @isSelected()
    @@ -27,6 +25,12 @@ class PathView extends View
             @toggle(true)
             false
     
    +    @addOperation(operation) for operation in operations
    +
    +  addOperation: (operation) ->
    +    @matches.append new OperationView({operation, @previewList})
    +    @description.text("(#{@matches.find('li').length})")
    +
       isSelected: ->
         @hasClass('selected') or @find('.selected').length
     
    
    From 7798f04cc44f2e7b63a7e0d74a4abf3d91f1127f Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Wed, 13 Mar 2013 15:44:42 -0700
    Subject: [PATCH 73/80] Index variable from operations is not longer used
    
    ---
     src/packages/command-panel/lib/operation-view.coffee | 2 +-
     src/packages/command-panel/lib/preview-list.coffee   | 1 -
     2 files changed, 1 insertion(+), 2 deletions(-)
    
    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/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee
    index 5430d7ae9..f53723a5f 100644
    --- a/src/packages/command-panel/lib/preview-list.coffee
    +++ b/src/packages/command-panel/lib/preview-list.coffee
    @@ -37,7 +37,6 @@ class PreviewList extends ScrollView
         @operations = operations
         @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})
    
    From 014d5e7bb39d6d048f2c226b594009ea905f8258 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Wed, 13 Mar 2013 16:18:41 -0700
    Subject: [PATCH 74/80] Remove setLineNumberWidth
    
    ---
     src/packages/command-panel/lib/preview-list.coffee | 8 --------
     1 file changed, 8 deletions(-)
    
    diff --git a/src/packages/command-panel/lib/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee
    index f53723a5f..6e51b4e44 100644
    --- a/src/packages/command-panel/lib/preview-list.coffee
    +++ b/src/packages/command-panel/lib/preview-list.coffee
    @@ -43,14 +43,6 @@ class PreviewList extends ScrollView
     
         @show()
         @find('.operation:first').addClass('selected')
    -    @setLineNumberWidth()
    -
    -  setLineNumberWidth: ->
    -    lineNumbers = @find('.line-number')
    -    maxWidth = 0
    -    lineNumbers.each (index, element) ->
    -      maxWidth = Math.max($(element).outerWidth(), maxWidth)
    -    lineNumbers.width(maxWidth)
     
       selectNextOperation: ->
         selectedView = @find('.selected').view()
    
    From 6b3d527ddaad4a88e9499c9eeee4957ed99a68ca Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Wed, 13 Mar 2013 16:19:46 -0700
    Subject: [PATCH 75/80] Add one operations at a time to PathViews
    
    ---
     src/packages/command-panel/lib/path-view.coffee  |  4 +---
     .../command-panel/lib/preview-list.coffee        | 16 +++++++++++++---
     2 files changed, 14 insertions(+), 6 deletions(-)
    
    diff --git a/src/packages/command-panel/lib/path-view.coffee b/src/packages/command-panel/lib/path-view.coffee
    index cdefe073e..3cd82414d 100644
    --- a/src/packages/command-panel/lib/path-view.coffee
    +++ b/src/packages/command-panel/lib/path-view.coffee
    @@ -14,7 +14,7 @@ class PathView extends View
             @span outlet: 'description', class: 'path-match-number'
           @ul outlet: 'matches', class: 'matches', =>
     
    -  initialize: ({operations, @previewList}) ->
    +  initialize: ({@previewList}) ->
         @pathDetails.on 'mousedown', => @toggle(true)
         @subscribe @previewList, 'command-panel:collapse-result', =>
           @collapse(true) if @isSelected()
    @@ -25,8 +25,6 @@ class PathView extends View
             @toggle(true)
             false
     
    -    @addOperation(operation) for operation in operations
    -
       addOperation: (operation) ->
         @matches.append new OperationView({operation, @previewList})
         @description.text("(#{@matches.find('li').length})")
    diff --git a/src/packages/command-panel/lib/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee
    index 6e51b4e44..d27623cb9 100644
    --- a/src/packages/command-panel/lib/preview-list.coffee
    +++ b/src/packages/command-panel/lib/preview-list.coffee
    @@ -11,6 +11,7 @@ class PreviewList extends ScrollView
         @ol class: 'preview-list', tabindex: -1
     
       operations: null
    +  viewsForPath: null
     
       initialize: ->
         super
    @@ -36,14 +37,23 @@ class PreviewList extends ScrollView
         @destroyOperations() if @operations
         @operations = operations
         @empty()
    +    @viewsForPath = {}
     
    -    operationsByPath = _.groupBy(operations, (operation) -> operation.getPath())
    -    for path, operations of operationsByPath
    -      @append new PathView({path, operations, previewList: this})
    +    for operation in operations
    +      pathView = @pathViewForPath(operation.getPath())
    +      pathView.addOperation(operation)
     
         @show()
         @find('.operation:first').addClass('selected')
     
    +  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()
         nextView = selectedView.next().view()
    
    From 10f405f61fdde6ea652250ffc9086e492fe47d88 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 18 Mar 2013 11:45:07 -0700
    Subject: [PATCH 76/80] Create preview list Dom elements only when needed
    
    ---
     spec/app/config-spec.coffee                   |  2 +-
     .../command-panel/lib/preview-list.coffee     | 18 +++++++---
     .../spec/preview-list-spec.coffee             | 34 +++++++++++++++++++
     3 files changed, 49 insertions(+), 5 deletions(-)
     create mode 100644 src/packages/command-panel/spec/preview-list-spec.coffee
    
    diff --git a/spec/app/config-spec.coffee b/spec/app/config-spec.coffee
    index 39aae22fe..915e92ce8 100644
    --- a/spec/app/config-spec.coffee
    +++ b/spec/app/config-spec.coffee
    @@ -151,4 +151,4 @@ describe "Config", ->
           config.loadUserConfig()
           config.set("hair", "blonde") # trigger a save
           expect(console.error).toHaveBeenCalled()
    -      expect(config.save).not.toHaveBeenCalled()
    \ No newline at end of file
    +      expect(config.save).not.toHaveBeenCalled()
    diff --git a/src/packages/command-panel/lib/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee
    index d27623cb9..1a3a62b13 100644
    --- a/src/packages/command-panel/lib/preview-list.coffee
    +++ b/src/packages/command-panel/lib/preview-list.coffee
    @@ -12,12 +12,16 @@ class PreviewList extends ScrollView
     
       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()
    @@ -36,15 +40,21 @@ class PreviewList extends ScrollView
       populate: (operations) ->
         @destroyOperations() if @operations
         @operations = operations
    +    @lastRenderedOperationIndex = 0
         @empty()
         @viewsForPath = {}
     
    -    for operation in operations
    +    @show()
    +    @renderOperations()
    +    @find('.operation:first').addClass('selected')
    +
    +  renderOperations: ->
    +    startingScrollHeight = @prop('scrollHeight')
    +    for operation in @operations[@lastRenderedOperationIndex..]
           pathView = @pathViewForPath(operation.getPath())
           pathView.addOperation(operation)
    -
    -    @show()
    -    @find('.operation:first').addClass('selected')
    +      @lastRenderedOperationIndex++
    +      break if @prop('scrollHeight') >= startingScrollHeight + @pixelOverdraw and @prop('scrollHeight') > @height() + @pixelOverdraw
     
       pathViewForPath: (path) ->
         pathView = @viewsForPath[path]
    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..c4530678b
    --- /dev/null
    +++ b/src/packages/command-panel/spec/preview-list-spec.coffee
    @@ -0,0 +1,34 @@
    +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
    
    From 4702f3775d40ede77979fa6529c56046461e501c Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 18 Mar 2013 11:55:33 -0700
    Subject: [PATCH 77/80] Calling collapseAllPaths creates elements for every Dom
     node
    
    ---
     src/packages/command-panel/lib/preview-list.coffee    |  7 +++++--
     .../command-panel/spec/preview-list-spec.coffee       | 11 +++++++++++
     2 files changed, 16 insertions(+), 2 deletions(-)
    
    diff --git a/src/packages/command-panel/lib/preview-list.coffee b/src/packages/command-panel/lib/preview-list.coffee
    index 1a3a62b13..6e35f3586 100644
    --- a/src/packages/command-panel/lib/preview-list.coffee
    +++ b/src/packages/command-panel/lib/preview-list.coffee
    @@ -30,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: ->
    @@ -46,15 +47,17 @@ class PreviewList extends ScrollView
     
         @show()
         @renderOperations()
    +
         @find('.operation:first').addClass('selected')
     
    -  renderOperations: ->
    +  renderOperations: ({renderAll}={}) ->
    +    renderAll ?= false
         startingScrollHeight = @prop('scrollHeight')
         for operation in @operations[@lastRenderedOperationIndex..]
           pathView = @pathViewForPath(operation.getPath())
           pathView.addOperation(operation)
           @lastRenderedOperationIndex++
    -      break if @prop('scrollHeight') >= startingScrollHeight + @pixelOverdraw and @prop('scrollHeight') > @height() + @pixelOverdraw
    +      break if not renderAll and @prop('scrollHeight') >= startingScrollHeight + @pixelOverdraw and @prop('scrollHeight') > @height() + @pixelOverdraw
     
       pathViewForPath: (path) ->
         pathView = @viewsForPath[path]
    diff --git a/src/packages/command-panel/spec/preview-list-spec.coffee b/src/packages/command-panel/spec/preview-list-spec.coffee
    index c4530678b..4fecb0f2d 100644
    --- a/src/packages/command-panel/spec/preview-list-spec.coffee
    +++ b/src/packages/command-panel/spec/preview-list-spec.coffee
    @@ -32,3 +32,14 @@ describe "Preview List", ->
             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
    
    From 1f23fa8cfc6d7166cfb8c0a0cccb03a1a73e303a Mon Sep 17 00:00:00 2001
    From: Corey 
    Date: Tue, 19 Mar 2013 10:33:53 -0700
    Subject: [PATCH 78/80] :lipstick:
    
    ---
     src/packages/command-panel/lib/command-panel-view.coffee | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    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()
     
    
    From 35ab49eff08241d391672be156721bae1a214612 Mon Sep 17 00:00:00 2001
    From: probablycorey 
    Date: Tue, 19 Mar 2013 10:48:46 -0700
    Subject: [PATCH 79/80] SelectAllMatchesInProject Operations create buffers on
     demand
    
    ---
     .../select-all-matches-in-project.coffee      |  2 +-
     .../command-panel/lib/operation.coffee        | 26 ++++++++++++-------
     2 files changed, 17 insertions(+), 11 deletions(-)
    
    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.coffee b/src/packages/command-panel/lib/operation.coffee
    index 52e45d544..2261acf41 100644
    --- a/src/packages/command-panel/lib/operation.coffee
    +++ b/src/packages/command-panel/lib/operation.coffee
    @@ -1,22 +1,28 @@
     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}) ->
    +    @buffer?.retain()
    +
    +  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 +30,5 @@ class Operation
         {prefix, suffix, match, range}
     
       destroy: ->
    -    @buffer.destroyMarker(@marker)
    -    @buffer.release()
    +    @buffer?.destroyMarker(@marker) if @marker?
    +    @buffer?.release()
    
    From 9c6978e913da2f9cb5bfc567ec07d13702938e9d Mon Sep 17 00:00:00 2001
    From: probablycorey 
    Date: Tue, 19 Mar 2013 14:32:25 -0700
    Subject: [PATCH 80/80] Operation creates a marker immediately when given a
     buffer
    
    ---
     src/packages/command-panel/lib/operation.coffee | 4 +++-
     1 file changed, 3 insertions(+), 1 deletion(-)
    
    diff --git a/src/packages/command-panel/lib/operation.coffee b/src/packages/command-panel/lib/operation.coffee
    index 2261acf41..9be9ad7f6 100644
    --- a/src/packages/command-panel/lib/operation.coffee
    +++ b/src/packages/command-panel/lib/operation.coffee
    @@ -1,7 +1,9 @@
     module.exports =
     class Operation
       constructor: ({@project, @path, @buffer, @bufferRange, @newText, @preserveSelection, @errorMessage}) ->
    -    @buffer?.retain()
    +    if @buffer?
    +      @buffer.retain()
    +      @getMarker()
     
       getMarker: ->
         @marker ?= @getBuffer().markRange(@bufferRange)