diff --git a/.pairs b/.pairs index ec4f2417c..2aafc939d 100644 --- a/.pairs +++ b/.pairs @@ -6,7 +6,7 @@ pairs: jc: Jerry Cheung; jerry bl: Brian Lopez; brian jp: Justin Palmer; justin + gt: Garen Torikian; garen email: domain: github.com #global: true - diff --git a/package.json b/package.json index 85dec3096..b59587873 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "plist": "git://github.com/nathansobo/node-plist.git", "space-pen": "git://github.com/nathansobo/space-pen.git", "less": "git://github.com/nathansobo/less.js.git", + "roaster": "0.0.3", "jqueryui-browser": "1.10.2-1" }, diff --git a/spec/fixtures/markdown/file.markdown b/spec/fixtures/markdown/file.markdown index 0eec6a120..964679b09 100644 --- a/spec/fixtures/markdown/file.markdown +++ b/spec/fixtures/markdown/file.markdown @@ -1,3 +1,20 @@ ## File.markdown -:cool: \ No newline at end of file +:cool: + +```ruby +def func + x = 1 +end +``` + +``` +function f(x) { + return x++; +} +``` + +```kombucha +drink-that-stuff: + tastes-weird~ +``` diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 6f81a5d4e..210f10adc 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -43,7 +43,7 @@ class Editor extends View @div outlet: 'verticalScrollbarContent' @classes: ({mini} = {}) -> - classes = ['editor'] + classes = ['editor', 'editor-colors'] classes.push 'mini' if mini classes.join(' ') @@ -1314,18 +1314,44 @@ class Editor extends View buildLineElementsForScreenRows: (startRow, endRow) -> div = document.createElement('div') - div.innerHTML = @buildLinesHtml(startRow, endRow) + div.innerHTML = @htmlForScreenRows(startRow, endRow) new Array(div.children...) - buildLinesHtml: (startRow, endRow) -> + htmlForScreenRows: (startRow, endRow) -> lines = @activeEditSession.linesForScreenRows(startRow, endRow) htmlLines = [] screenRow = startRow for line in @activeEditSession.linesForScreenRows(startRow, endRow) - htmlLines.push(@buildLineHtml(line, screenRow++)) + htmlLines.push(@htmlForScreenLine(line, screenRow++)) htmlLines.join('\n\n') - buildEndOfLineInvisibles: (screenLine) -> + htmlForScreenLine: (screenLine, screenRow) -> + { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine + if fold + attributes = { class: 'fold line', 'fold-id': fold.id } + else + attributes = { class: 'line' } + + invisibles = @invisibles if @showInvisibles + eolInvisibles = @getEndOfLineInvisibles(screenLine) + htmlEolInvisibles = @buildHtmlEndOfLineInvisibles(screenLine) + + indentation = Editor.buildIndentation(screenRow, @activeEditSession) + + Editor.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @activeEditSession, @mini}) + + @buildIndentation: (screenRow, activeEditSession) -> + indentation = 0 + while --screenRow >= 0 + bufferRow = activeEditSession.bufferPositionForScreenPosition([screenRow]).row + bufferLine = activeEditSession.lineForBufferRow(bufferRow) + unless bufferLine is '' + indentation = Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) + break + + indentation + + buildHtmlEndOfLineInvisibles: (screenLine) -> invisibles = [] for invisible in @getEndOfLineInvisibles(screenLine) invisibles.push("#{invisible}") @@ -1340,99 +1366,6 @@ class Editor extends View invisibles.push(@invisibles.eol) if @invisibles.eol invisibles - buildEmptyLineHtml: (screenLine, screenRow) -> - if not @mini and @showIndentGuide - indentation = 0 - while --screenRow >= 0 - bufferRow = @activeEditSession.bufferPositionForScreenPosition([screenRow]).row - bufferLine = @activeEditSession.lineForBufferRow(bufferRow) - unless bufferLine is '' - indentation = Math.ceil(@activeEditSession.indentLevelForLine(bufferLine)) - break - - if indentation > 0 - tabLength = @activeEditSession.getTabLength() - invisibles = @getEndOfLineInvisibles(screenLine) - indentGuideHtml = [] - for level in [0...indentation] - indentLevelHtml = [""] - for characterPosition in [0...tabLength] - if invisible = invisibles.shift() - indentLevelHtml.push("#{invisible}") - else - indentLevelHtml.push(' ') - indentLevelHtml.push("") - indentGuideHtml.push(indentLevelHtml.join('')) - - for invisible in invisibles - indentGuideHtml.push("#{invisible}") - return indentGuideHtml.join('') - - invisibles = @buildEndOfLineInvisibles(screenLine) - if invisibles.length > 0 - invisibles - else - ' ' - - buildLineHtml: (screenLine, screenRow) -> - scopeStack = [] - line = [] - - updateScopeStack = (desiredScopes) -> - excessScopes = scopeStack.length - desiredScopes.length - _.times(excessScopes, popScope) if excessScopes > 0 - - # pop until common prefix - for i in [scopeStack.length..0] - break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) - popScope() - - # push on top of common prefix until scopeStack == desiredScopes - for j in [i...desiredScopes.length] - pushScope(desiredScopes[j]) - - pushScope = (scope) -> - scopeStack.push(scope) - line.push("") - - popScope = -> - scopeStack.pop() - line.push("") - - if fold = screenLine.fold - lineAttributes = { class: 'fold line', 'fold-id': fold.id } - else - lineAttributes = { class: 'line' } - - attributePairs = [] - attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of lineAttributes - line.push("
") - - invisibles = @invisibles if @showInvisibles - - if screenLine.text == '' - html = @buildEmptyLineHtml(screenLine, screenRow) - line.push(html) if html - else - firstNonWhitespacePosition = screenLine.text.search(/\S/) - firstTrailingWhitespacePosition = screenLine.text.search(/\s*$/) - lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 - position = 0 - for token in screenLine.tokens - updateScopeStack(token.scopes) - hasLeadingWhitespace = position < firstNonWhitespacePosition - hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition - hasIndentGuide = not @mini and @showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly) - line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})) - position += token.value.length - - popScope() while scopeStack.length > 0 - line.push(@buildEndOfLineInvisibles(screenLine)) unless screenLine.text == '' - line.push("") if fold - - line.push('
') - line.join('') - lineElementForScreenRow: (screenRow) -> @renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})") @@ -1457,7 +1390,7 @@ class Editor extends View # # Returns an object with two values: `top` and `left`, representing the pixel positions. pixelPositionForScreenPosition: (position) -> - return { top: 0, left: 0 } unless @isOnDom() and @isVisible() + return { top: 0, left: 0 } unless @isOnDom() and @isVisible() {row, column} = Point.fromObject(position) actualRow = Math.floor(row) @@ -1552,6 +1485,84 @@ class Editor extends View ### Internal ### + @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, activeEditSession, mini}) -> + scopeStack = [] + line = [] + + updateScopeStack = (desiredScopes) -> + excessScopes = scopeStack.length - desiredScopes.length + _.times(excessScopes, popScope) if excessScopes > 0 + + # pop until common prefix + for i in [scopeStack.length..0] + break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) + popScope() + + # push on top of common prefix until scopeStack == desiredScopes + for j in [i...desiredScopes.length] + pushScope(desiredScopes[j]) + + pushScope = (scope) -> + scopeStack.push(scope) + line.push("") + + popScope = -> + scopeStack.pop() + line.push("") + + attributePairs = [] + attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes + line.push("
") + + if text == '' + html = Editor.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) + line.push(html) if html + else + firstNonWhitespacePosition = text.search(/\S/) + firstTrailingWhitespacePosition = text.search(/\s*$/) + lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 + position = 0 + for token in tokens + updateScopeStack(token.scopes) + hasLeadingWhitespace = position < firstNonWhitespacePosition + hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition + hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly) + line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})) + position += token.value.length + + popScope() while scopeStack.length > 0 + line.push(htmlEolInvisibles) unless text == '' + line.push("") if fold + + line.push('
') + line.join('') + + @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) -> + if not mini and showIndentGuide + if indentation > 0 + tabLength = activeEditSession.getTabLength() + indentGuideHtml = [] + for level in [0...indentation] + indentLevelHtml = [""] + for characterPosition in [0...tabLength] + if invisible = eolInvisibles.shift() + indentLevelHtml.push("#{invisible}") + else + indentLevelHtml.push(' ') + indentLevelHtml.push("") + indentGuideHtml.push(indentLevelHtml.join('')) + + for invisible in eolInvisibles + indentGuideHtml.push("#{invisible}") + + return indentGuideHtml.join('') + + invisibles = htmlEolInvisibles + if invisibles.length > 0 + invisibles + else + ' ' + bindToKeyedEvent: (key, event, callback) -> binding = {} binding[key] = event diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 9b336cc2e..6f0c231e1 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -1,6 +1,17 @@ $ = require 'jquery' +_ = require 'underscore' ScrollView = require 'scroll-view' {$$$} = require 'space-pen' +roaster = require 'roaster' +Editor = require 'editor' + +fenceNameToExtension = + "coffeescript": "coffee" + "toml": "toml" + "ruby": "rb" + "go": "go" + "mustache": "mustache" + "java": "java" module.exports = class MarkdownPreviewView extends ScrollView @@ -15,13 +26,13 @@ class MarkdownPreviewView extends ScrollView initialize: (@buffer) -> super - @fetchRenderedMarkdown() + @renderMarkdown() @on 'core:move-up', => @scrollUp() @on 'core:move-down', => @scrollDown() afterAttach: (onDom) -> @subscribe @buffer, 'saved', => - @fetchRenderedMarkdown() + @renderMarkdown() pane = @getPane() pane.showItem(this) if pane? and pane isnt rootView.getActivePane() @@ -42,7 +53,7 @@ class MarkdownPreviewView extends ScrollView @buffer.getPath() setErrorHtml: (result)-> - try failureMessage = JSON.parse(result.responseText).message + try failureMessage = JSON.parse(result).message @html $$$ -> @h2 'Previewing Markdown Failed' @@ -59,15 +70,38 @@ class MarkdownPreviewView extends ScrollView setLoading: -> @html($$$ -> @div class: 'markdown-spinner', 'Loading Markdown...') - fetchRenderedMarkdown: (text) -> + + tokenizeCodeBlocks: (html) => + html = $(html) + preList = $(html.filter("pre")) + + for preElement in preList.toArray() + $(preElement).addClass("editor-colors") + codeBlock = $(preElement.firstChild) + + # go to next block unless this one has a class + continue unless className = codeBlock.attr('class') + + fenceName = className.replace(/^lang-/, '') + # go to next block unless the class name is matches `lang` + continue unless extension = fenceNameToExtension[fenceName] + text = codeBlock.text() + + # go to next block if this grammar is not mapped + continue unless grammar = syntax.selectGrammar("foo.#{extension}", text) + continue if grammar is syntax.nullGrammar + + codeBlock.empty() + for tokens in grammar.tokenizeLines(text) + codeBlock.append(Editor.buildLineHtml({ tokens, text })) + + html + + renderMarkdown: -> @setLoading() - $.ajax - url: 'https://api.github.com/markdown' - type: 'POST' - dataType: 'html' - contentType: 'application/json; charset=UTF-8' - data: JSON.stringify - mode: 'markdown' - text: @buffer.getText() - success: (html) => @html(html) - error: (result) => @setErrorHtml(result) + roaster(@buffer.getText(), {}, (err, html) => + if err + @setErrorHtml(err) + else + @html(@tokenizeCodeBlocks(html)) + ) diff --git a/src/packages/markdown-preview/lib/markdown-preview.coffee b/src/packages/markdown-preview/lib/markdown-preview.coffee index 8011339b9..90bb96f86 100644 --- a/src/packages/markdown-preview/lib/markdown-preview.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview.coffee @@ -18,7 +18,7 @@ module.exports = {previewPane, previewItem} = @getExistingPreview(editSession) if previewItem? previewPane.showItem(previewItem) - previewItem.fetchRenderedMarkdown() + previewItem.renderMarkdown() else if nextPane = activePane.getNextPane() 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 8e590a229..5c816d4fe 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -8,7 +8,7 @@ describe "MarkdownPreview package", -> project.setPath(project.resolve('markdown')) window.rootView = new RootView atom.activatePackage("markdown-preview", immediate: true) - spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown') + spyOn(MarkdownPreviewView.prototype, 'renderMarkdown') describe "markdown-preview:show", -> beforeEach -> @@ -61,9 +61,9 @@ describe "MarkdownPreview package", -> [pane] = rootView.getPanes() pane.focus() - MarkdownPreviewView.prototype.fetchRenderedMarkdown.reset() + MarkdownPreviewView.prototype.renderMarkdown.reset() pane.activeItem.buffer.trigger 'saved' - expect(MarkdownPreviewView.prototype.fetchRenderedMarkdown).not.toHaveBeenCalled() + expect(MarkdownPreviewView.prototype.renderMarkdown).not.toHaveBeenCalled() 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", -> @@ -77,9 +77,9 @@ describe "MarkdownPreview package", -> expect(pane2.activeItem).not.toBe preview pane1.focus() - preview.fetchRenderedMarkdown.reset() + preview.renderMarkdown.reset() rootView.getActiveView().trigger 'markdown-preview:show' - expect(preview.fetchRenderedMarkdown).toHaveBeenCalled() + expect(preview.renderMarkdown).toHaveBeenCalled() expect(rootView.getPanes()).toHaveLength 2 expect(pane2.getItems()).toHaveLength 2 expect(pane2.activeItem).toBe preview @@ -95,9 +95,9 @@ describe "MarkdownPreview package", -> pane1.showItemAtIndex(0) preview = pane1.itemAtIndex(1) - preview.fetchRenderedMarkdown.reset() + preview.renderMarkdown.reset() pane1.activeItem.buffer.trigger 'saved' - expect(preview.fetchRenderedMarkdown).toHaveBeenCalled() + expect(preview.renderMarkdown).toHaveBeenCalled() expect(pane1.activeItem).not.toBe preview describe "when the preview is not in the same pane", -> @@ -109,7 +109,7 @@ describe "MarkdownPreview package", -> expect(pane2.activeItem).not.toBe preview pane1.focus() - preview.fetchRenderedMarkdown.reset() + preview.renderMarkdown.reset() pane1.activeItem.buffer.trigger 'saved' - expect(preview.fetchRenderedMarkdown).toHaveBeenCalled() + expect(preview.renderMarkdown).toHaveBeenCalled() expect(pane2.activeItem).toBe preview 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 42cac9b84..b3ae8b63c 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee @@ -6,34 +6,39 @@ describe "MarkdownPreviewView", -> [buffer, preview] = [] beforeEach -> - spyOn($, 'ajax') project.setPath(project.resolve('markdown')) buffer = project.bufferForPath('file.markdown') + atom.activatePackage('ruby.tmbundle', sync: true) 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", -> + it "shows a loading spinner and renders the markdown", -> + preview.setLoading() expect(preview.find('.markdown-spinner')).toExist() - expect($.ajax).toHaveBeenCalled() + expect(preview.buffer.getText()).toBe buffer.getText() - expect(JSON.parse(ajaxArgs.data).text).toBe buffer.getText() - - ajaxArgs.success($$$ -> @div "WWII", class: 'private-ryan') - expect(preview.find(".private-ryan")).toExist() + preview.renderMarkdown() + expect(preview.find(".emoji")).toExist() it "shows an error message on error", -> - ajaxArgs.error() + preview.setErrorHtml("Not a real file") expect(preview.text()).toContain "Failed" describe "serialization", -> it "reassociates with the same buffer when deserialized", -> newPreview = deserialize(preview.serialize()) expect(newPreview.buffer).toBe buffer + + describe "code block tokenization", -> + describe "when the code block's fence name has a matching grammar", -> + it "tokenizes the code block with the grammar", -> + expect(preview.find("pre span.entity.name.function.ruby")).toExist() + + describe "when the code block's fence name doesn't have a matching grammar", -> + it "does not tokenize the code block", -> + expect(preview.find("pre code:not([class])").children().length).toBe 0 + expect(preview.find("pre code.lang-kombucha").children().length).toBe 0 diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.less b/src/packages/markdown-preview/stylesheets/markdown-preview.less index 608389a88..4265e8cac 100644 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.less +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.less @@ -23,6 +23,7 @@ // includes some GitHub Flavored Markdown specific styling (like @mentions) .markdown-preview { pre, + pre div.editor, code, tt { font-size: 12px; @@ -385,7 +386,6 @@ } .highlight pre, pre { - background-color: #f8f8f8; border: 1px solid #ccc; font-size: 13px; line-height: 19px; @@ -400,4 +400,9 @@ background-color: transparent; border: none; } + + .emoji { + height: 20px; + width: 20px; + } } diff --git a/themes/atom-dark-syntax.css b/themes/atom-dark-syntax.css index a095d3018..357f0886e 100644 --- a/themes/atom-dark-syntax.css +++ b/themes/atom-dark-syntax.css @@ -1,4 +1,4 @@ -.editor, .editor .gutter { +.editor-colors { background-color: #1d1f21; color: #c5c8c6; }