diff --git a/benchmark/fixtures/medium.coffee b/benchmark/fixtures/medium.coffee index 69e2d8f8e..403353b76 100644 --- a/benchmark/fixtures/medium.coffee +++ b/benchmark/fixtures/medium.coffee @@ -1,368 +1,240 @@ -{View, $$} = require 'space-pen' -AceOutdentAdaptor = require 'ace-outdent-adaptor' -Buffer = require 'buffer' -Cursor = require 'cursor' -Gutter = require 'gutter' -Renderer = require 'renderer' -Point = require 'point' -Range = require 'range' -Selection = require 'selection' -UndoManager = require 'undo-manager' +# **Docco** is a quick-and-dirty, hundred-line-long, literate-programming-style +# documentation generator. It produces HTML +# that displays your comments alongside your code. Comments are passed through +# [Markdown](http://daringfireball.net/projects/markdown/syntax), and code is +# passed through [Pygments](http://pygments.org/) syntax highlighting. +# This page is the result of running Docco against its own source file. +# +# If you install Docco, you can run it from the command-line: +# +# docco src/*.coffee +# +# ...will generate an HTML documentation page for each of the named source files, +# with a menu linking to the other pages, saving it into a `docs` folder. +# +# The [source for Docco](http://github.com/jashkenas/docco) is available on GitHub, +# and released under the MIT license. +# +# To install Docco, first make sure you have [Node.js](http://nodejs.org/), +# [Pygments](http://pygments.org/) (install the latest dev version of Pygments +# from [its Mercurial repo](http://dev.pocoo.org/hg/pygments-main)), and +# [CoffeeScript](http://coffeescript.org/). Then, with NPM: +# +# sudo npm install -g docco +# +# Docco can be used to process CoffeeScript, JavaScript, Ruby, Python, or TeX files. +# Only single-line comments are processed -- block comments are ignored. +# +#### Partners in Crime: +# +# * If **Node.js** doesn't run on your platform, or you'd prefer a more +# convenient package, get [Ryan Tomayko](http://github.com/rtomayko)'s +# [Rocco](http://rtomayko.github.com/rocco/rocco.html), the Ruby port that's +# available as a gem. +# +# * If you're writing shell scripts, try +# [Shocco](http://rtomayko.github.com/shocco/), a port for the **POSIX shell**, +# also by Mr. Tomayko. +# +# * If Python's more your speed, take a look at +# [Nick Fitzgerald](http://github.com/fitzgen)'s [Pycco](http://fitzgen.github.com/pycco/). +# +# * For **Clojure** fans, [Fogus](http://blog.fogus.me/)'s +# [Marginalia](http://fogus.me/fun/marginalia/) is a bit of a departure from +# "quick-and-dirty", but it'll get the job done. +# +# * **Lua** enthusiasts can get their fix with +# [Robert Gieseke](https://github.com/rgieseke)'s [Locco](http://rgieseke.github.com/locco/). +# +# * And if you happen to be a **.NET** +# aficionado, check out [Don Wilson](https://github.com/dontangg)'s +# [Nocco](http://dontangg.github.com/nocco/). -$ = require 'jquery' -_ = require 'underscore' +#### Main Documentation Generation Functions -module.exports = -class Editor extends View - @content: -> - @div class: 'editor', tabindex: -1, => - @div class: 'content', outlet: 'content', => - @subview 'gutter', new Gutter - @div class: 'horizontal-scroller', outlet: 'horizontalScroller', => - @div class: 'lines', outlet: 'lines', => - @input class: 'hidden-input', outlet: 'hiddenInput' +# Generate the documentation for a source file by reading it in, splitting it +# up into comment/code sections, highlighting them for the appropriate language, +# and merging them into an HTML template. +generate_documentation = (source, callback) -> + fs.readFile source, "utf-8", (error, code) -> + throw error if error + sections = parse source, code + highlight source, sections, -> + generate_html source, sections + callback() - vScrollMargin: 2 - hScrollMargin: 10 - softWrap: false - lineHeight: null - charWidth: null - charHeight: null - cursor: null - selection: null - buffer: null - highlighter: null - renderer: null - undoManager: null - autoIndent: null +# Given a string of source code, parse out each comment and the code that +# follows it, and create an individual **section** for it. +# Sections take the form: +# +# { +# docs_text: ... +# docs_html: ... +# code_text: ... +# code_html: ... +# } +# +parse = (source, code) -> + lines = code.split '\n' + sections = [] + language = get_language source + has_code = docs_text = code_text = '' - initialize: () -> - requireStylesheet 'editor.css' - requireStylesheet 'theme/twilight.css' - @bindKeys() - @buildCursorAndSelection() - @handleEvents() - @setBuffer(new Buffer) - @autoIndent = true + save = (docs, code) -> + sections.push docs_text: docs, code_text: code - bindKeys: -> - window.keymap.bindKeys '*:not(.editor *)', - 'meta-s': 'save' - right: 'move-right' - left: 'move-left' - down: 'move-down' - up: 'move-up' - 'shift-right': 'select-right' - 'shift-left': 'select-left' - 'shift-up': 'select-up' - 'shift-down': 'select-down' - enter: 'newline' - backspace: 'backspace' - 'delete': 'delete' - 'meta-x': 'cut' - 'meta-c': 'copy' - 'meta-v': 'paste' - 'meta-z': 'undo' - 'meta-Z': 'redo' - 'alt-meta-w': 'toggle-soft-wrap' - 'alt-meta-f': 'fold-selection' - - @on 'save', => @save() - @on 'move-right', => @moveCursorRight() - @on 'move-left', => @moveCursorLeft() - @on 'move-down', => @moveCursorDown() - @on 'move-up', => @moveCursorUp() - @on 'select-right', => @selectRight() - @on 'select-left', => @selectLeft() - @on 'select-up', => @selectUp() - @on 'select-down', => @selectDown() - @on 'newline', => @insertText("\n") - @on 'backspace', => @backspace() - @on 'delete', => @delete() - @on 'cut', => @cutSelection() - @on 'copy', => @copySelection() - @on 'paste', => @paste() - @on 'undo', => @undo() - @on 'redo', => @redo() - @on 'toggle-soft-wrap', => @toggleSoftWrap() - @on 'fold-selection', => @foldSelection() - - buildCursorAndSelection: -> - @cursor = new Cursor(this) - @lines.append(@cursor) - - @selection = new Selection(this) - @lines.append(@selection) - - handleEvents: -> - @on 'focus', => - @hiddenInput.focus() - false - - @on 'mousedown', '.fold-placeholder', (e) => - @destroyFold($(e.currentTarget).attr('foldId')) - false - - @on 'mousedown', (e) => - clickCount = e.originalEvent.detail - - if clickCount == 1 - @setCursorScreenPosition @screenPositionFromMouseEvent(e) - else if clickCount == 2 - @selection.selectWord() - else if clickCount >= 3 - @selection.selectLine() - - @selectTextOnMouseMovement() - - @hiddenInput.on "textInput", (e) => - @insertText(e.originalEvent.data) - - @on 'cursor:position-changed', => - @hiddenInput.css(@pixelPositionForScreenPosition(@cursor.getScreenPosition())) - - @one 'attach', => - @calculateDimensions() - @hiddenInput.width(@charWidth) - @setMaxLineLength() if @softWrap - @focus() - - selectTextOnMouseMovement: -> - moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e)) - @on 'mousemove', moveHandler - $(document).one 'mouseup', => @off 'mousemove', moveHandler - - buildLineElement: (screenLine) -> - { tokens } = screenLine - charWidth = @charWidth - charHeight = @charHeight - $$ -> - @div class: 'line', => - appendNbsp = true - for token in tokens - if token.type is 'fold-placeholder' - @span ' ', class: 'fold-placeholder', style: "width: #{3 * charWidth}px; height: #{charHeight}px;", 'foldId': token.fold.id, => - @div class: "ellipsis", => @raw "…" - else - appendNbsp = false - @span { class: token.type.replace('.', ' ') }, token.value - @raw ' ' if appendNbsp - - renderLines: -> - @lines.find('.line').remove() - for screenLine in @getScreenLines() - @lines.append @buildLineElement(screenLine) - - getScreenLines: -> - @renderer.getLines() - - linesForRows: (start, end) -> - @renderer.linesForRows(start, end) - - screenLineCount: -> - @renderer.lineCount() - - lastRow: -> - @screenLineCount() - 1 - - setBuffer: (@buffer) -> - @renderer = new Renderer(@buffer) - @undoManager = new UndoManager(@buffer) - @renderLines() - @gutter.renderLineNumbers(@getScreenLines()) - - @setCursorScreenPosition(row: 0, column: 0) - - @buffer.on 'change', (e) => - @cursor.bufferChanged(e) - - @renderer.on 'change', (e) => - @gutter.renderLineNumbers(@getScreenLines()) - - @cursor.refreshScreenPosition() unless e.bufferChanged - { oldRange, newRange } = e - screenLines = @linesForRows(newRange.start.row, newRange.end.row) - if newRange.end.row > oldRange.end.row - # update, then insert elements - for row in [newRange.start.row..newRange.end.row] - if row <= oldRange.end.row - @updateLineElement(row, screenLines.shift()) - else - @insertLineElement(row, screenLines.shift()) - else - # traverse in reverse... remove, then update elements - screenLines.reverse() - for row in [oldRange.end.row..oldRange.start.row] - if row > newRange.end.row - @removeLineElement(row) - else - @updateLineElement(row, screenLines.shift()) - - updateLineElement: (row, screenLine) -> - @getLineElement(row).replaceWith(@buildLineElement(screenLine)) - - insertLineElement: (row, screenLine) -> - newLineElement = @buildLineElement(screenLine) - insertBefore = @getLineElement(row) - if insertBefore.length - insertBefore.before(newLineElement) + for line in lines + if line.match(language.comment_matcher) and not line.match(language.comment_filter) + if has_code + save docs_text, code_text + has_code = docs_text = code_text = '' + docs_text += line.replace(language.comment_matcher, '') + '\n' else - @lines.append(newLineElement) + has_code = yes + code_text += line + '\n' + save docs_text, code_text + sections - removeLineElement: (row) -> - @getLineElement(row).remove() +# Highlights a single chunk of CoffeeScript code, using **Pygments** over stdio, +# and runs the text of its corresponding comment through **Markdown**, using +# [Showdown.js](http://attacklab.net/showdown/). +# +# We process the entire file in a single call to Pygments by inserting little +# marker comments between each section and then splitting the result string +# wherever our markers occur. +highlight = (source, sections, callback) -> + language = get_language source + pygments = spawn 'pygmentize', ['-l', language.name, '-f', 'html', '-O', 'encoding=utf-8,tabsize=2'] + output = '' - getLineElement: (row) -> - @lines.find("div.line:eq(#{row})") + pygments.stderr.addListener 'data', (error) -> + console.error error.toString() if error - toggleSoftWrap: -> - @setSoftWrap(not @softWrap) + pygments.stdin.addListener 'error', (error) -> + console.error "Could not use Pygments to highlight the source." + process.exit 1 - setMaxLineLength: (maxLength) -> - maxLength ?= - if @softWrap - Math.floor(@horizontalScroller.width() / @charWidth) - else - Infinity + pygments.stdout.addListener 'data', (result) -> + output += result if result - @renderer.setMaxLineLength(maxLength) if maxLength + pygments.addListener 'exit', -> + output = output.replace(highlight_start, '').replace(highlight_end, '') + fragments = output.split language.divider_html + for section, i in sections + section.code_html = highlight_start + fragments[i] + highlight_end + section.docs_html = showdown.makeHtml section.docs_text + callback() - createFold: (range) -> - @renderer.createFold(range) + if pygments.stdin.writable + pygments.stdin.write((section.code_text for section in sections).join(language.divider_text)) + pygments.stdin.end() - setSoftWrap: (@softWrap) -> - @setMaxLineLength() - if @softWrap - @addClass 'soft-wrap' - @_setMaxLineLength = => @setMaxLineLength() - $(window).on 'resize', @_setMaxLineLength - else - @removeClass 'soft-wrap' - $(window).off 'resize', @_setMaxLineLength +# Once all of the code is finished highlighting, we can generate the HTML file +# and write out the documentation. Pass the completed sections into the template +# found in `resources/docco.jst` +generate_html = (source, sections) -> + title = path.basename source + dest = destination source + html = docco_template { + title: title, sections: sections, sources: sources, path: path, destination: destination + } + console.log "docco: #{source} -> #{dest}" + fs.writeFile dest, html - save: -> - if not @buffer.path - path = $native.saveDialog() - return if not path - @buffer.path = path +#### Helpers & Setup - @buffer.save() +# Require our external dependencies, including **Showdown.js** +# (the JavaScript implementation of Markdown). +fs = require 'fs' +path = require 'path' +showdown = require('./../vendor/showdown').Showdown +{spawn, exec} = require 'child_process' - clipScreenPosition: (screenPosition, options={}) -> - @renderer.clipScreenPosition(screenPosition, options) +# A list of the languages that Docco supports, mapping the file extension to +# the name of the Pygments lexer and the symbol that indicates a comment. To +# add another language to Docco's repertoire, add it here. +languages = + '.coffee': + name: 'coffee-script', symbol: '#' + '.js': + name: 'javascript', symbol: '//' + '.rb': + name: 'ruby', symbol: '#' + '.py': + name: 'python', symbol: '#' + '.tex': + name: 'tex', symbol: '%' + '.latex': + name: 'tex', symbol: '%' + '.c': + name: 'c', symbol: '//' + '.h': + name: 'c', symbol: '//' - pixelPositionForScreenPosition: ({row, column}) -> - { top: row * @lineHeight, left: column * @charWidth } +# Build out the appropriate matchers and delimiters for each language. +for ext, l of languages - screenPositionFromPixelPosition: ({top, left}) -> - screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth)) + # Does the line begin with a comment? + l.comment_matcher = new RegExp('^\\s*' + l.symbol + '\\s?') - screenPositionForBufferPosition: (position) -> - @renderer.screenPositionForBufferPosition(position) + # Ignore [hashbangs](http://en.wikipedia.org/wiki/Shebang_(Unix\)) + # and interpolations... + l.comment_filter = new RegExp('(^#![/]|^\\s*#\\{)') - bufferPositionForScreenPosition: (position) -> - @renderer.bufferPositionForScreenPosition(position) + # The dividing token we feed into Pygments, to delimit the boundaries between + # sections. + l.divider_text = '\n' + l.symbol + 'DIVIDER\n' - screenRangeForBufferRange: (range) -> - @renderer.screenRangeForBufferRange(range) + # The mirror of `divider_text` that we expect Pygments to return. We can split + # on this to recover the original sections. + # Note: the class is "c" for Python and "c1" for the other languages + l.divider_html = new RegExp('\\n*' + l.symbol + 'DIVIDER<\\/span>\\n*') - bufferRangeForScreenRange: (range) -> - @renderer.bufferRangeForScreenRange(range) +# Get the current language we're documenting, based on the extension. +get_language = (source) -> languages[path.extname(source)] - bufferRowsForScreenRows: -> - @renderer.bufferRowsForScreenRows() +# Compute the destination HTML path for an input source file path. If the source +# is `lib/example.coffee`, the HTML will be at `docs/example.html` +destination = (filepath) -> + 'docs/' + path.basename(filepath, path.extname(filepath)) + '.html' - screenPositionFromMouseEvent: (e) -> - { pageX, pageY } = e - @screenPositionFromPixelPosition - top: pageY - @horizontalScroller.offset().top - left: pageX - @horizontalScroller.offset().left + @horizontalScroller.scrollLeft() +# Ensure that the destination directory exists. +ensure_directory = (dir, callback) -> + exec "mkdir -p #{dir}", -> callback() - calculateDimensions: -> - fragment = $('') - @lines.append(fragment) - @charWidth = fragment.width() - @charHeight = fragment.find('span').height() - @lineHeight = fragment.outerHeight() - fragment.remove() +# Micro-templating, originally by John Resig, borrowed by way of +# [Underscore.js](http://documentcloud.github.com/underscore/). +template = (str) -> + new Function 'obj', + 'var p=[],print=function(){p.push.apply(p,arguments);};' + + 'with(obj){p.push(\'' + + str.replace(/[\r\t\n]/g, " ") + .replace(/'(?=[^<]*%>)/g,"\t") + .split("'").join("\\'") + .split("\t").join("'") + .replace(/<%=(.+?)%>/g, "',$1,'") + .split('<%').join("');") + .split('%>').join("p.push('") + + "');}return p.join('');" +# Create the template that we will use to generate the Docco HTML page. +docco_template = template fs.readFileSync(__dirname + '/../resources/docco.jst').toString() - getCursor: -> @cursor - moveCursorUp: -> @cursor.moveUp() - moveCursorDown: -> @cursor.moveDown() - moveCursorRight: -> @cursor.moveRight() - moveCursorLeft: -> @cursor.moveLeft() +# The CSS styles we'd like to apply to the documentation. +docco_styles = fs.readFileSync(__dirname + '/../resources/docco.css').toString() - getCurrentScreenLine: -> @buffer.lineForRow(@getCursorScreenRow()) - getCurrentBufferLine: -> @buffer.lineForRow(@getCursorBufferRow()) - setCursorScreenPosition: (position) -> @cursor.setScreenPosition(position) - getCursorScreenPosition: -> @cursor.getScreenPosition() - setCursorBufferPosition: (position) -> @cursor.setBufferPosition(position) - getCursorBufferPosition: -> @cursor.getBufferPosition() - setCursorScreenRow: (row) -> @cursor.setScreenRow(row) - getCursorScreenRow: -> @cursor.getScreenRow() - getCursorBufferRow: -> @cursor.getBufferRow() - setCursorScreenColumn: (column) -> @cursor.setScreenColumn(column) - getCursorScreenColumn: -> @cursor.getScreenColumn() - setCursorBufferColumn: (column) -> @cursor.setBufferColumn(column) - getCursorBufferColumn: -> @cursor.getBufferColumn() +# The start of each Pygments highlight block. +highlight_start = '
'
 
-  getSelection: -> @selection
-  getSelectedText: -> @selection.getText()
-  selectRight: -> @selection.selectRight()
-  selectLeft: -> @selection.selectLeft()
-  selectUp: -> @selection.selectUp()
-  selectDown: -> @selection.selectDown()
-  selectToScreenPosition: (position) -> @selection.selectToScreenPosition(position)
-  selectToBufferPosition: (position) -> @selection.selectToBufferPosition(position)
+# The end of each Pygments highlight block.
+highlight_end   = '
' - insertText: (text) -> - { text, shouldOutdent } = @autoIndentText(text) - @selection.insertText(text) - @autoOutdentText() if shouldOutdent - - autoIndentText: (text) -> - if @autoIndent - row = @getCursorScreenPosition().row - state = @renderer.lineForRow(row).state - if text[0] == "\n" - indent = @buffer.mode.getNextLineIndent(state, @getCurrentBufferLine(), atom.tabText) - text = text[0] + indent + text[1..] - else if @buffer.mode.checkOutdent(state, @getCurrentBufferLine(), text) - shouldOutdent = true - - {text, shouldOutdent} - - autoOutdentText: -> - screenRow = @getCursorScreenPosition().row - bufferRow = @getCursorBufferPosition().row - state = @renderer.lineForRow(screenRow).state - @buffer.mode.autoOutdent(state, new AceOutdentAdaptor(@buffer, this), bufferRow) - - cutSelection: -> @selection.cut() - copySelection: -> @selection.copy() - paste: -> @insertText($native.readFromPasteboard()) - - foldSelection: -> @selection.fold() - - backspace: -> - @selectLeft() if @selection.isEmpty() - @selection.delete() - - delete: -> - @selectRight() if @selection.isEmpty() - @selection.delete() - - undo: -> - @undoManager.undo() - - redo: -> - @undoManager.redo() - - destroyFold: (foldId) -> - fold = @renderer.foldsById[foldId] - fold.destroy() - @setCursorBufferPosition(fold.start) - - logLines: -> - @renderer.logLines() +# Run the script. +# For each source file passed in as an argument, generate the documentation. +sources = process.ARGV.sort() +if sources.length + ensure_directory 'docs', -> + fs.writeFile 'docs/docco.css', docco_styles + files = sources.slice(0) + next_file = -> generate_documentation files.shift(), next_file if files.length + next_file()