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;
}