Files
atom/extensions/docs/cdoc.coffee
Chris Wanstrath bc6e2597b0 docs extension
Only works for TomDoc'd CoffeeScript right now.

Needs to work for all languages.

I had to modify handlebars to satisfy 'use strict'

Also had to modify the path stuff in tdoc, which works
in node but not atom. We need some way to install npm packages
or something similar maybe?
2011-11-13 22:22:41 -08:00

167 lines
5.1 KiB
CoffeeScript

# cdoc is a bite sized library which scans CoffeeScript
# source code and returns a js object containing class, module
# and method names, along with the comments and line numbers
# associated with them.
#
# It pairs finely with TomDoc, but really doesn't care which
# documentation format you use. As long as your class, module, and
# method definitions are preceded by a comment, cdoc will do its job.
module.exports = cdoc =
# Parses CoffeeScript source code into an object describing
# the class names and modules defined therein. Method names,
# comments, and line numbers are also included.
#
# text - The String CoffeeScript source code to parse into a js object.
#
# Returns an object like this:
#
# [
# name: "App"
# comment:"Singleton object representing the application.",
# line: 5,
# methods: [
# name: "setTitle"
# signature: "setTitle: (title) ->"
# params: [
# name: "title"
# ]
# comment: "Sets the title of the app."
# line: 7
# ,
# name: "title"
# signature: "title: ->"
# params: []
# comment: "Returns the String title of the app."
# line: 11
# ]
# name: "class Robot"
# comment: "A Robot that can walk and talk, just like a real boy."
# line: 15
# methods: [
# name: "constructor"
# signature: "constructor: (path) ->"
# params: [
# name: "path"
# ]
# comment: "Robots receive messages from a..."
# line: 20
# ]
# ]
#
# In the above, App is an object while Robot is a class.
parse: (text) ->
scopes = []
lastComment = ''
methodIndent = null
moduleIsFn = false
text.split("\n").forEach (line, lineno) =>
return if moduleIsFn
lineno++
# Skip empty lines and lines containing only a comment symbol.
return if not line.trim()
# If this line is a comment, record it and move on.
if line.trim()[0] is '#'
lastComment += line.trim().match(/^\s*\#\s?(.*)$/)[1] + "\n"
return
# detect:
# 1. module.exports = (params) ->
# 2. module.exports = (params) =>
if match = line.match /^\s*module.exports\s*=\s*((?:\((.+)\))?\s*(-|=)>)/
moduleIsFn = true
[ signature, params ] = match[1..2]
params = for param in params?.split(',') or []
{ name: param.trim() }
scopes.push
name: 'module'
signature: signature
params: params
comment: lastComment.trim()
line: lineno
# detect:
# 1. module.exports =
# 2. module.exports = MyModule =
# 3. module.exports = class MyClass
# 4. exports.MyClass = class MyClass
else if match = line.match /^\s*(?:module.exports|exports\.[\w\.]+)\s*=\s*(?:(class [\w\.]+)(?:\s+extends [\w.]+)?|([\w\.]+)\s*=)\s*$/
methodIndent = null
scopes.push
name: match[1] or match[2] or 'module'
comment: lastComment.trim()
line: lineno
methods: []
# detect: class Window
else if newScope = line.match(/^\s*class ([\w\.]+)/)?[0]
methodIndent = null
scopes.push
name: newScope
comment: lastComment.trim()
line: lineno
methods: []
# detect:
# 1. hear: ->
# 2. hear: (regex, args...) ->
# 3. hear = ->
# 4. hear = (regex, args...) ->
# also detect `@hear` and `=>` forms of the above
# as well as `exports.head = (regex, args...) ->` form
else if match = line.match /^\s*(@|exports\.)?(([\w\.]+)\s*(:|=)\s*(?:\((.*?)\))?\s*(-|=)>)\s*/
# If the indentation of this method is deeper than the
# previous method definition, and we're still in the same
# scope, bail out. Probably an inner function.
indent = line.match(/^(\s*)/)[0].length
return if methodIndent? and methodIndent < indent
methodIndent = indent
[ prefix, fn, name, type, params ] = match[1..-1]
# floating function
if type is '=' and prefix isnt 'exports.'
return
# Normalize the method signature.
# 1. exports.method = (param) ->
# exports.method: (param) ->
# 2. method: () ->
# method: ->
fn = fn.trim()
fn = fn.replace /\s+=\s+((\(|-|=))/, ': $1'
fn = fn.replace /\s*\(\)/, ''
params = for param in params?.split(',') or []
{ name: param.trim() }
if prefix is 'exports.'
modules = scopes.filter (scope) -> scope?.name is 'module'
scope = modules[0]
if not scope
scopes.push scope =
name: 'module'
comment: ""
line: 0
methods: []
else
scope = scopes[scopes.length - 1]
scope.methods ?= []
scope.methods.push
name: name
signature: fn
params: params
comment: lastComment.trim()
line: lineno
# Clear the last comment, we're done with it.
lastComment = ''
scopes