jsduckify
Version:
Enables the use of Sencha's JSDuck for documenting CoffeeScript projects.
210 lines (185 loc) • 8.73 kB
text/coffeescript
{DoublyLinkedList} = require('doubly-linked-list')
_space = (level, multiplier = 1) ->
return new Array(level * multiplier + 1).join(' ')
_contains = (s, s2) ->
return s.indexOf(s2) >= 0
_stripLeadingBlanks = (filename, line, count) ->
startOfLine = line.slice(0, count)
minLength = Math.min(startOfLine.length, count)
unless startOfLine == _space(minLength)
console.error("Non whitespace found within the indent space for the docstring in file: #{filename}. At line:\n" +
"#{line}"
)
return line.slice(count)
_convertLinesFromCSToJS = (filename, start, end, indent) ->
current = start
while current != end.after
current.value = _space(indent) + ' * ' + _stripLeadingBlanks(filename, current.value, indent)
current = current.after
_convertDocstringFromCSToJS = (filename, docstringStart, docstringEnd) ->
# Figure out the number of indention characters
indent = docstringStart.value.indexOf('###')
unless indent == docstringEnd.value.indexOf('###')
console.error("Starting and ending docstrings do not have the same indention in file #{filename}.\n" +
"start:\n#{docstringStart.value}\n" + "end:\n#{docstringEnd.value}"
)
docstringStart.insertBefore('</CoffeeScript> */') # ends the JS comment wrapping the entire file
docstringStart.value = _space(indent) + '/**'
_convertLinesFromCSToJS(filename, docstringStart.after, docstringEnd, indent)
docstringEnd.value = _space(indent) + ' */'
docstringEnd.insertAfter('/* <CoffeeScript>') # restarts the JS comment wrapping the entire file
_processNameAndMember = (nameLine, memberLine, exportsAPI, prefix, fullNameForCurrentClass) ->
# !TODO: (round 2) Marks it as processed in the fileDocumentation
a = nameLine.value.match(/@(class|method|property)\s([\$|\w]+)\b/)
type = a[1]
name = a[2]
fullName = null
for e in exportsAPI
if name in e.fullName.split('.')
e.processed = true
fullName = prefix + '.' + e.fullName
break
space = nameLine.value.indexOf('@')
unless fullName?
fullName = prefix + '.' + name
nameArray = fullName.split('.')
baseName = nameArray[nameArray.length - 1]
if baseName.indexOf('_') == 0
nameLine.insertAfter(_space(space) + '@private') # !TODO: This will add it twice if it's already there but OK.
nameLine.value = _space(space)
switch type
when 'class'
nameLine.value += "@class #{fullName}"
fullNameForCurrentClass = fullName
when 'method', 'property'
nameLine.value += "@#{type} #{name}"
unless memberLine? # !TODO: Maybe I should manipulate the existing memberLine
if fullNameForCurrentClass?
member = fullNameForCurrentClass
else
member = prefix
memberLine = nameLine.insertAfter(_space(space) + "@member #{member}")
return fullNameForCurrentClass
_processDocstring = (filename, docstringStart, docstringEnd, currentClass, exportsAPI, prefix) ->
foundSomething = false
# Look for the @class, @method, @constructor, or @property JSDuck tags.
current = docstringStart.after
nameLine = null
type = null
memberLine = null
while current? and current != docstringEnd
s = current.value
if _contains(s, "@class")
if type?
throw new Error("Found @class when docstring already has an @#{type} in: #{filename}.")
else
type = 'class'
nameLine = current
if _contains(s, "@method")
if type?
throw new Error("Found @method when docstring already has an @#{type} in: #{filename}.")
else
type = 'method'
nameLine = current
if _contains(s, "@property")
if type?
throw new Error("Found @property when docstring already has an @#{type} in: #{filename}.")
else
type = 'property'
nameLine = current
if _contains(s, "@constructor")
if type?
unless type == 'class'
throw new Error("Cannot define an @constructor inside an @#{type} docstring in: #{filename}.")
# We can essentially do nothing because the @constructor is already part of the @class docstring
else
unless currentClass.end?
throw new Error("I have no idea where to associate this constructor.")
type = 'constructor'
nameLine = current
if _contains(s, "@member")
memberLine = current
current = current.after
if type == 'class'
currentClass.start = docstringStart
currentClass.end = docstringEnd
fullNameForCurrentClass = _processNameAndMember(nameLine, memberLine, exportsAPI, prefix, currentClass.fullName)
currentClass.fullName = fullNameForCurrentClass
if type == 'constructor'
# * Append the current docstring to the end of the currentClass
constructorBodyStart = docstringStart.after
constructorBodyStart.before.remove()
docstringStart = constructorBodyStart
constructorBodyEnd = docstringEnd.before
constructorBodyEnd.after.remove()
docstringEnd = constructorBodyEnd
# Move the constructor to the bottom of the class
indent = currentClass.end.before.value.indexOf('*') - 1
if indent < 0
throw new Error("Expected * to be at the top of the docstring in #{filename}")
indentConstructor = constructorBodyStart.value.indexOf('@')
if indentConstructor < 0
throw new Error("Expected @constructor to be at the top of the docstring in #{filename}")
newLine = _space(indent) + ' * '
currentClass.end.insertBefore(newLine) # Blank line between Class and Constructor
current = constructorBodyStart.before
while current != constructorBodyEnd and current?
current = current.after
newLine = _space(indent) + ' * ' + _stripLeadingBlanks(filename, current.value, indentConstructor)
currentClass.end.insertBefore(newLine)
current.before.remove()
if type in ['property', 'method']
_processNameAndMember(nameLine, memberLine, exportsAPI, prefix, currentClass.fullName)
# * (round 2) Mark it as @static if it is according to fileDocumentation.
# * (round 2) Uses fileDocumentation to specify the @membership.
# (round 2) For each element in fileDocumentation that is not marked as processed, it tries to locate it and create
# the appropriate JSDuck header. Note, in my old code, I was able to identify parameters.
foundSomething = type?
if foundSomething and type != 'constructor'
_convertDocstringFromCSToJS(filename, docstringStart, docstringEnd)
return foundSomething
###
@method duckifyFile
Once it's used, it's marked as such in the exportsAPI
@param {String} sourceFileString CoffeeScript source
@param {Array} exportsAPI each row contains {baseName, fullName, type}
@param {String} [prefix] the root for this documentation
@return {String} duckified source file
###
exports.duckifyFile = (filename, sourceFileString, exportsAPI, prefix = '', isMain = false) ->
# !TODO: (round 2) Runs documentFile on it creating fileDocumentation. This runs the CoffeeScript compiler extracting classes
# and functions. It can distinguish static methods from instance methods within your classes. It can also identify
# functions not associated with a CoffeeScript class. However, it cannot identify any properties.
# fileDocumentation = documentFile(sourceFileString)
# Breaks the file down into lines.
sourceFileString = sourceFileString.replace(/\/\*/g,'/ *') # This is a hack but good enough
sourceFileString = sourceFileString.replace(/\*\//g,'* /')
lineList = new DoublyLinkedList(sourceFileString.split('\n'))
# Turn the entire file into a big JavaScript block comment
lineList.unshift('/* <CoffeeScript>')
lineList.push('</CoffeeScript> */')
# Walks through the file line-by-line looking for docstrings starting with ### and ending with ###.
foundSomething = false
current = lineList.head
currentClassDocstring = {}
while current?
if current.value.match(/(\n|\r|^)\s*###/)?
# !TODO: If a starting markers is not on its own line, make it so.
docstringStart = current
current = current.after
while current? and !current.value.match(/(\n|\r|^)\s*###/)?
current = current.after
unless current?
throw new Error("Found a docstring start without a corresponding docstring end in file: #{filename}")
docstringEnd = current
if isMain
_convertDocstringFromCSToJS(filename, docstringStart, docstringEnd)
foundSomething = true
else if _processDocstring(filename, docstringStart, docstringEnd, currentClassDocstring, exportsAPI, prefix)
foundSomething = true
current = current.after
if foundSomething
fileString = lineList.toArray().join('\n')
else
fileString = null
return fileString