Merge pull request #507 from github/markdown-and-out

Swap out Markdown render logic with module
This commit is contained in:
Garen Torikian 2013-05-03 16:59:00 -07:00
commit 4b993b99cd
11 changed files with 226 additions and 153 deletions

2
.pairs
View File

@ -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

View File

@ -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"
},

View File

@ -1,3 +1,20 @@
## File.markdown
:cool:
:cool:
```ruby
def func
x = 1
end
```
```
function f(x) {
return x++;
}
```
```kombucha
drink-that-stuff:
tastes-weird~
```

View File

@ -94,7 +94,7 @@ class EditSession
# Retrieves the filename of the open file.
#
# This is `'untitled'` if the file is new and not saved to the disk.
#
#
# Returns a {String}.
getTitle: ->
if path = @getPath()
@ -175,7 +175,7 @@ class EditSession
setSoftWrap: (@softWrap) ->
# Retrieves that character used to indicate a tab.
#
#
# If soft tabs are enabled, this is a space (`" "`) times the {.getTabLength} value.
# Otherwise, it's a tab (`\t`).
#
@ -191,22 +191,22 @@ class EditSession
#
# tabLength - A {Number} that defines the new tab length.
setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
# Given a position, this clips it to a real position.
#
# For example, if `position`'s row exceeds the row count of the buffer,
# or if its column goes beyond a line's length, this "sanitizes" the value
# or if its column goes beyond a line's length, this "sanitizes" the value
# to a real position.
#
# position - The {Point} to clip
#
# Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed.
clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition)
# Given a range, this clips it to a real range.
#
# For example, if `range`'s row exceeds the row count of the buffer,
# or if its column goes beyond a line's length, this "sanitizes" the value
# or if its column goes beyond a line's length, this "sanitizes" the value
# to a real range.
#
# range - The {Point} to clip
@ -319,13 +319,13 @@ class EditSession
# {Delegates to: DisplayBuffer.screenPositionForBufferPosition}
screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options)
# {Delegates to: DisplayBuffer.bufferPositionForScreenPosition}
bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options)
# {Delegates to: DisplayBuffer.screenRangeForBufferRange}
screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange)
# {Delegates to: DisplayBuffer.bufferRangeForScreenRange}
bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange)
@ -592,7 +592,7 @@ class EditSession
# Given a buffer row, this returns a suggested indentation level.
#
# The indentation level provided is based on the current language.
# The indentation level provided is based on the current language.
#
# bufferRow - A {Number} indicating the buffer row
#
@ -806,7 +806,7 @@ class EditSession
# {Delegates to: DisplayBuffer.getMarkerHeadScreenPosition}
getMarkerHeadScreenPosition: (args...) ->
@displayBuffer.getMarkerHeadScreenPosition(args...)
# {Delegates to: DisplayBuffer.setMarkerHeadScreenPosition}
setMarkerHeadScreenPosition: (args...) ->
@displayBuffer.setMarkerHeadScreenPosition(args...)
@ -826,7 +826,7 @@ class EditSession
# {Delegates to: DisplayBuffer.setMarkerTailScreenPosition}
setMarkerTailScreenPosition: (args...) ->
@displayBuffer.setMarkerTailScreenPosition(args...)
# {Delegates to: DisplayBuffer.getMarkerTailBufferPosition}
getMarkerTailBufferPosition: (args...) ->
@displayBuffer.getMarkerTailBufferPosition(args...)
@ -990,7 +990,7 @@ class EditSession
#
# Returns an {Array} of {Selection}s.
getSelections: -> new Array(@selections...)
# Gets the selection at the specified index.
#
# index - The id {Number} of the selection
@ -1268,7 +1268,7 @@ class EditSession
# Selects all the text from the current cursor position to the beginning of the next word.
selectToBeginningOfNextWord: ->
@expandSelectionsForward (selection) => selection.selectToBeginningOfNextWord()
# Selects the current word.
selectWord: ->
@expandSelectionsForward (selection) => selection.selectWord()

View File

@ -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(' ')
@ -1317,18 +1317,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("<span class='invisible-character'>#{invisible}</span>")
@ -1343,99 +1369,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 = ["<span class='indent-guide'>"]
for characterPosition in [0...tabLength]
if invisible = invisibles.shift()
indentLevelHtml.push("<span class='invisible-character'>#{invisible}</span>")
else
indentLevelHtml.push(' ')
indentLevelHtml.push("</span>")
indentGuideHtml.push(indentLevelHtml.join(''))
for invisible in invisibles
indentGuideHtml.push("<span class='invisible-character'>#{invisible}</span>")
return indentGuideHtml.join('')
invisibles = @buildEndOfLineInvisibles(screenLine)
if invisibles.length > 0
invisibles
else
'&nbsp;'
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("<span class=\"#{scope.replace(/\./g, ' ')}\">")
popScope = ->
scopeStack.pop()
line.push("</span>")
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("<div #{attributePairs.join(' ')}>")
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("<span class='fold-marker'/>") if fold
line.push('</div>')
line.join('')
lineElementForScreenRow: (screenRow) ->
@renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})")
@ -1460,7 +1393,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)
@ -1555,6 +1488,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("<span class=\"#{scope.replace(/\./g, ' ')}\">")
popScope = ->
scopeStack.pop()
line.push("</span>")
attributePairs = []
attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes
line.push("<div #{attributePairs.join(' ')}>")
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("<span class='fold-marker'/>") if fold
line.push('</div>')
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 = ["<span class='indent-guide'>"]
for characterPosition in [0...tabLength]
if invisible = eolInvisibles.shift()
indentLevelHtml.push("<span class='invisible-character'>#{invisible}</span>")
else
indentLevelHtml.push(' ')
indentLevelHtml.push("</span>")
indentGuideHtml.push(indentLevelHtml.join(''))
for invisible in eolInvisibles
indentGuideHtml.push("<span class='invisible-character'>#{invisible}</span>")
return indentGuideHtml.join('')
invisibles = htmlEolInvisibles
if invisibles.length > 0
invisibles
else
'&nbsp;'
bindToKeyedEvent: (key, event, callback) ->
binding = {}
binding[key] = event

View File

@ -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))
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -1,4 +1,4 @@
.editor, .editor .gutter {
.editor-colors {
background-color: #1d1f21;
color: #c5c8c6;
}