├── .npmignore ├── spec ├── metadata_templates │ ├── exports │ │ ├── class_exports.coffee │ │ ├── basic_exports.coffee │ │ ├── class_exports.json │ │ └── basic_exports.json │ ├── subscript_assignments.coffee │ ├── requires │ │ ├── requires_with_call_of_required_module.coffee │ │ ├── requires_with_colon.coffee │ │ ├── requires_with_call_args.coffee │ │ ├── multiple_requires_single_line.coffee │ │ ├── references │ │ │ ├── buffer-patch.coffee │ │ │ └── buffer-patch.json │ │ ├── basic_requires.coffee │ │ ├── requires_with_call_of_required_module.json │ │ ├── requires_with_call_args.json │ │ ├── requires_with_colon.json │ │ ├── multiple_requires_single_line.json │ │ └── basic_requires.json │ ├── top_level_try_catch.coffee │ ├── test_package │ │ ├── Gruntfile.coffee │ │ ├── package.json │ │ ├── src │ │ │ ├── test.coffee │ │ │ ├── range.coffee │ │ │ └── point.coffee │ │ ├── test │ │ │ └── text-buffer-test.coffee │ │ └── test_metadata.json │ ├── top_level_try_catch.json │ ├── classes │ │ ├── basic_class.coffee │ │ ├── class_with_super_class.coffee │ │ ├── class_with_comment_indentation.coffee │ │ ├── classes_with_similar_methods.coffee │ │ ├── class_with_class_properties.coffee │ │ ├── class_with_prototype_properties.coffee │ │ ├── class_with_comment_section.coffee │ │ ├── basic_class.json │ │ ├── class_with_super_class.json │ │ ├── class_with_comment_indentation.json │ │ ├── class_with_class_properties.json │ │ ├── class_with_prototype_properties.json │ │ ├── class_with_comment_section.json │ │ └── classes_with_similar_methods.json │ └── subscript_assignments.json └── metadata_spec.coffee ├── bin └── donna ├── .travis.yml ├── .gitignore ├── src ├── nodes │ ├── node.coffee │ ├── doc.coffee │ ├── property.coffee │ ├── file.coffee │ ├── variable.coffee │ ├── parameter.coffee │ ├── virtual_method.coffee │ ├── mixin.coffee │ ├── method.coffee │ └── class.coffee ├── donna.coffee ├── parser.coffee ├── metadata.coffee └── util │ └── referencer.coffee ├── CONTRIBUTING.md ├── Gruntfile.coffee ├── LICENSE.md ├── package.json ├── Guardfile └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | spec 2 | .npmignore 3 | .travis.yml 4 | -------------------------------------------------------------------------------- /spec/metadata_templates/exports/class_exports.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class Foo 3 | -------------------------------------------------------------------------------- /spec/metadata_templates/subscript_assignments.coffee: -------------------------------------------------------------------------------- 1 | A[0] = 'x' 2 | B[0].key = 'y' 3 | -------------------------------------------------------------------------------- /bin/donna: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('coffee-script/register'); 3 | require('../src/donna').main(); 4 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/requires_with_call_of_required_module.coffee: -------------------------------------------------------------------------------- 1 | foo = require('bar')({}) 2 | -------------------------------------------------------------------------------- /spec/metadata_templates/top_level_try_catch.coffee: -------------------------------------------------------------------------------- 1 | try 2 | 1 + 1 3 | catch error 4 | throw error 5 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/requires_with_colon.coffee: -------------------------------------------------------------------------------- 1 | {defs:foof} = require 'defs2' 2 | 3 | class Bar 4 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/requires_with_call_args.coffee: -------------------------------------------------------------------------------- 1 | foo = require(foo.bar().baz) 2 | 3 | class Bar 4 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/multiple_requires_single_line.coffee: -------------------------------------------------------------------------------- 1 | {faz, baz} = require 'kaz' 2 | 3 | class Bar 4 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON('package.json') 4 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/references/buffer-patch.coffee: -------------------------------------------------------------------------------- 1 | {getTextOG} = require './helpers' 2 | 3 | module.exports = 4 | class TextBuffer 5 | getText: getTextOG 6 | -------------------------------------------------------------------------------- /spec/metadata_templates/top_level_try_catch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "top_level_try_catch.coffee": { 4 | "objects": {}, 5 | "exports": {} 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/basic_requires.coffee: -------------------------------------------------------------------------------- 1 | {Foo} = require 'foo' 2 | {defs:foof} = require 'defs2' 3 | {faz, baz} = require 'kaz' 4 | [boo] = require 'hoo' 5 | 6 | class Bar 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | node_js: 9 | - 0.10 10 | 11 | branches: 12 | only: 13 | - master 14 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/basic_class.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | # 4 | module.exports = 5 | class TextBuffer 6 | -------------------------------------------------------------------------------- /spec/metadata_templates/exports/basic_exports.coffee: -------------------------------------------------------------------------------- 1 | # Public: Returns hello 2 | foo = -> 'hello' 3 | 4 | exports.foo = foo 5 | 6 | # Exporting the answer to life, the universe, and everything. 7 | exports.bar = 42 8 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_super_class.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | # 4 | module.exports = 5 | class TextBuffer extends String 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/**/* 3 | !/spec/metadata_templates/test_package/node_modules/grim 4 | Gemfile.lock 5 | .bundle 6 | .idea 7 | .sass-cache 8 | doc 9 | out.json 10 | atom_src 11 | npm-debug.log 12 | js_src/ 13 | dummy/ 14 | .grunt/ 15 | metadata.json 16 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./lib/test", 3 | "version": "1.2.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/atom/zomgwowcats.git" 7 | }, 8 | "dependencies": { 9 | "grim": "0.11.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/src/test.coffee: -------------------------------------------------------------------------------- 1 | Point = require './point' 2 | Range = require './range' 3 | 4 | # Public: A mutable text container with undo/redo support and the ability to 5 | # annotate logical regions in the text. 6 | module.exports = 7 | class TestClass 8 | @Range: Range 9 | @newlineRegex: newlineRegex 10 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/test/text-buffer-test.coffee: -------------------------------------------------------------------------------- 1 | {join} = require 'path' 2 | temp = require 'temp' 3 | {File} = require 'pathwatcher' 4 | TextBuffer = require '../src/text-buffer' 5 | SampleText = readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8') 6 | 7 | describe "TextBuffer", -> 8 | buffer = null 9 | 10 | afterEach -> 11 | buffer = null 12 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_comment_indentation.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | class TextBuffer 4 | 5 | # Public: Takes an argument and does some stuff. 6 | # 7 | # * one 8 | # * two 9 | # * three 10 | # * four 11 | # * five 12 | @copy: -> 13 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/classes_with_similar_methods.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | class TextBuffer 4 | # Public: Takes an argument and does some stuff. 5 | @copy: -> 6 | 7 | class DisplayBuffer 8 | # Private: Nope, not the same as the other `copy()` 9 | @copy: -> 10 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_class_properties.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | # 4 | class TextBuffer 5 | @prop2: "bar" 6 | 7 | # Public: Takes an argument and does some stuff. 8 | # 9 | # a - A {String} 10 | # 11 | # Returns {Boolean}. 12 | @method2: (a) -> 13 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_prototype_properties.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | # 4 | class TextBuffer 5 | prop2: "bar" 6 | 7 | # Public: Takes an argument and does some stuff. 8 | # 9 | # a - A {String} 10 | # 11 | # Returns {Boolean}. 12 | method2: (a) -> 13 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/requires_with_call_of_required_module.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "requires_with_call_of_required_module.coffee": { 4 | "objects": { 5 | "0": { 6 | "6": { 7 | "name": "foo", 8 | "type": "function", 9 | "range": [[ 0, 6 ], [ 0, 23]] 10 | } 11 | } 12 | }, 13 | "exports": {} 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_comment_section.coffee: -------------------------------------------------------------------------------- 1 | # Public: A mutable text container with undo/redo support and the ability to 2 | # annotate logical regions in the text. 3 | # 4 | class TextBuffer 5 | prop2: "bar" 6 | 7 | ### 8 | Section: Something 9 | ### 10 | 11 | # Public: Takes an argument and does some stuff. 12 | # 13 | # a - A {String} 14 | # 15 | # Returns {Boolean}. 16 | method2: (a) -> 17 | -------------------------------------------------------------------------------- /src/nodes/node.coffee: -------------------------------------------------------------------------------- 1 | # Public: The base class for all nodes. 2 | # 3 | module.exports = class Node 4 | 5 | # Public: Find an ancestor node by type. 6 | # 7 | # type - The type name (a {String}) 8 | # node - The CoffeeScript node to search on (a {Base}) 9 | findAncestor: (type, node = @node) -> 10 | if node.ancestor 11 | if node.ancestor.constructor.name is type 12 | node.ancestor 13 | else 14 | @findAncestor type, node.ancestor 15 | 16 | else 17 | undefined 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to Donna 2 | 3 | ## Report issues 4 | 5 | Issues hosted at [GitHub Issues](https://github.com/atom/donna/issues). 6 | 7 | ## Development 8 | 9 | Source hosted at [GitHub](https://github.com/atom/donna). 10 | 11 | Pull requests are very welcome! Please try to follow these simple rules if applicable: 12 | 13 | * Please create a topic branch for every separate change you make. 14 | * Make sure your patches are well tested. 15 | * Update the documentation. 16 | * Update the README. 17 | * Update the CHANGELOG for noteworthy changes. 18 | * Please **do not change** the version number. 19 | -------------------------------------------------------------------------------- /spec/metadata_templates/exports/class_exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "class_exports.coffee": { 4 | "objects": { 5 | "1": { 6 | "0": { 7 | "type": "class", 8 | "name": "Foo", 9 | "superClass": null, 10 | "bindingType": "exports", 11 | "classProperties": [], 12 | "prototypeProperties": [], 13 | "doc": null, 14 | "range": [ 15 | [ 16 | 1, 17 | 0 18 | ], 19 | [ 20 | 1, 21 | 8 22 | ] 23 | ] 24 | } 25 | } 26 | }, 27 | "exports": 1 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/requires_with_call_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "requires_with_call_args.coffee": { 4 | "objects": { 5 | "2": { 6 | "0": { 7 | "type": "class", 8 | "bindingType": null, 9 | "doc": null, 10 | "name": "Bar", 11 | "classProperties": [], 12 | "prototypeProperties": [], 13 | "range": [ 14 | [ 15 | 2, 16 | 0 17 | ], 18 | [ 19 | 2, 20 | 8 21 | ] 22 | ], 23 | "superClass": null 24 | } 25 | } 26 | }, 27 | "exports": {} 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.loadNpmTasks 'grunt-release' 4 | grunt.loadNpmTasks 'grunt-exec' 5 | grunt.loadNpmTasks 'grunt-gh-pages' 6 | 7 | grunt.initConfig 8 | release: 9 | options: 10 | bump: false 11 | add: false 12 | push: false 13 | tagName: "v<%= version %>" 14 | exec: 15 | test: 16 | command: "./node_modules/jasmine-node/bin/jasmine-node --coffee spec" 17 | build_docs: 18 | command: "./bin/donna src/" 19 | 20 | 'gh-pages': 21 | options: 22 | base: "doc" 23 | src: ['**'] 24 | 25 | grunt.registerTask('test', 'exec:test') 26 | grunt.registerTask('publish', ['exec:build_docs', 'gh-pages']) 27 | grunt.registerTask('default', ['test']) 28 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/basic_class.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "basic_class.coffee": { 4 | "objects": { 5 | "4": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "superClass": null, 10 | "bindingType": "exports", 11 | "classProperties": [], 12 | "prototypeProperties": [], 13 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text.\n\n ", 14 | "range": [ 15 | [ 16 | 4, 17 | 0 18 | ], 19 | [ 20 | 4, 21 | 15 22 | ] 23 | ] 24 | } 25 | } 26 | }, 27 | "exports": 4 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_super_class.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "class_with_super_class.coffee": { 4 | "objects": { 5 | "4": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "superClass": "String", 10 | "bindingType": "exports", 11 | "classProperties": [], 12 | "prototypeProperties": [], 13 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text.\n\n ", 14 | "range": [ 15 | [ 16 | 4, 17 | 0 18 | ], 19 | [ 20 | 4, 21 | 30 22 | ] 23 | ] 24 | } 25 | } 26 | }, 27 | "exports": 4 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/metadata_templates/subscript_assignments.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "subscript_assignments.coffee": { 4 | "objects": { 5 | "0": { 6 | "7": { 7 | "name": "'x'", 8 | "type": "primitive", 9 | "range": [ 10 | [ 11 | 0, 12 | 7 13 | ], 14 | [ 15 | 0, 16 | 9 17 | ] 18 | ] 19 | } 20 | }, 21 | "1": { 22 | "11": { 23 | "name": "'y'", 24 | "type": "primitive", 25 | "range": [ 26 | [ 27 | 1, 28 | 11 29 | ], 30 | [ 31 | 1, 32 | 13 33 | ] 34 | ] 35 | } 36 | } 37 | }, 38 | "exports": {} 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spec/metadata_templates/exports/basic_exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "basic_exports.coffee": { 4 | "objects": { 5 | "1": { 6 | "6": { 7 | "name": "foo", 8 | "bindingType": "variable", 9 | "type": "function", 10 | "paramNames": [], 11 | "range": [ 12 | [ 13 | 1, 14 | 6 15 | ], 16 | [ 17 | 1, 18 | 15 19 | ] 20 | ], 21 | "doc": "Public: Returns hello " 22 | } 23 | }, 24 | "6": { 25 | "0": { 26 | "name": "bar", 27 | "bindingType": "exportsProperty", 28 | "type": "primitive", 29 | "range": [ 30 | [ 31 | 6, 32 | 0 33 | ], 34 | [ 35 | 6, 36 | 6 37 | ] 38 | ] 39 | } 40 | } 41 | }, 42 | "exports": { 43 | "foo": 3, 44 | "bar": 6 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2013 Garen J. Torikian, GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donna", 3 | "description": "A CoffeeScript documentation generator.", 4 | "keywords": [ 5 | "coffeescript", 6 | "doc", 7 | "api" 8 | ], 9 | "author": "Ben Ogle ", 10 | "version": "1.0.16", 11 | "engines": { 12 | "node": ">=0.10.0" 13 | }, 14 | "main": "./src/donna", 15 | "bin": { 16 | "donna": "./bin/donna" 17 | }, 18 | "dependencies": { 19 | "coffee-script": "1.10.x", 20 | "source-map": "0.1.29", 21 | "walkdir": ">= 0.0.2", 22 | "optimist": "~0.6", 23 | "underscore": ">= 0.1.0", 24 | "underscore.string": ">= 0.1.0", 25 | "async": ">= 0.1.22", 26 | "builtins": "0.0.4" 27 | }, 28 | "devDependencies": { 29 | "grunt": "~0.4.1", 30 | "grunt-release": "~0.6.0", 31 | "grunt-exec": "0.4.3", 32 | "grunt-gh-pages": "0.9.0", 33 | "jasmine-node": ">= 1.0.13", 34 | "jasmine-json": "~0.0", 35 | "jasmine-focused": ">= 1.0.5", 36 | "grunt-cli": "~0.1.13" 37 | }, 38 | "homepage": "https://github.com/atom/donna", 39 | "repository": { 40 | "type": "git", 41 | "url": "git://github.com/atom/donna.git" 42 | }, 43 | "scripts": { 44 | "test": "grunt test" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/requires_with_colon.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "requires_with_colon.coffee": { 4 | "objects": { 5 | "0": { 6 | "14": { 7 | "type": "import", 8 | "range": [ 9 | [ 10 | 0, 11 | 14 12 | ], 13 | [ 14 | 0, 15 | 28 16 | ] 17 | ], 18 | "bindingType": "variable", 19 | "module": "defs2", 20 | "name": "foof", 21 | "exportsProperty": "defs" 22 | } 23 | }, 24 | "2": { 25 | "0": { 26 | "type": "class", 27 | "name": "Bar", 28 | "superClass": null, 29 | "bindingType": null, 30 | "classProperties": [], 31 | "prototypeProperties": [], 32 | "doc": null, 33 | "range": [ 34 | [ 35 | 2, 36 | 0 37 | ], 38 | [ 39 | 2, 40 | 8 41 | ] 42 | ] 43 | } 44 | } 45 | }, 46 | "exports": {} 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/nodes/doc.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Node = require './node' 3 | 4 | # Public: A documentation node is responsible for parsing 5 | # the comments for known tags. 6 | # 7 | module.exports = class Doc extends Node 8 | 9 | # Public: Construct a documentation node. 10 | # 11 | # node - The comment node (a {Object}) 12 | # options - The parser options (a {Object}) 13 | constructor: (@node, @options) -> 14 | try 15 | if @node 16 | trimmedComment = @leftTrimBlock(@node.comment.replace(/\u0091/gm, '').split('\n')) 17 | @comment = trimmedComment.join("\n") 18 | 19 | catch error 20 | console.warn('Create doc error:', @node, error) if @options.verbose 21 | 22 | leftTrimBlock: (lines) -> 23 | # Detect minimal left trim amount 24 | trimMap = _.map lines, (line) -> 25 | if line.length is 0 26 | undefined 27 | else 28 | line.length - _.str.ltrim(line).length 29 | 30 | minimalTrim = _.min _.without(trimMap, undefined) 31 | 32 | # If we have a common amount of left trim 33 | if minimalTrim > 0 and minimalTrim < Infinity 34 | 35 | # Trim same amount of left space on each line 36 | lines = for line in lines 37 | line = line.substring(minimalTrim, line.length) 38 | line 39 | 40 | lines 41 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | notification :gntp 2 | 3 | group :donna do 4 | # CoffeeScript for the donna library 5 | guard :coffeescript, input: 'src', output: 'lib', noop: true 6 | 7 | # Run Jasmine specs 8 | guard :shell do 9 | jasmine_node = File.expand_path('../node_modules/jasmine-node/bin/jasmine-node', __FILE__) 10 | watch(%r{src|spec}) { `#{jasmine_node} --coffee --color spec/parser_spec.coffee` } 11 | end 12 | 13 | # Generate donna doc 14 | guard :shell do 15 | watch(%r{src|theme}) { `./bin/donna` } 16 | end 17 | end 18 | 19 | group :theme do 20 | # CoffeeScript for the default template 21 | guard :coffeescript, input: 'theme/default/src/coffee', output: 'theme/default/lib/scripts' 22 | 23 | # Compile the Compass style sheets 24 | guard :compass, configuration_file: 'config/compass.rb' do 25 | watch(%r{^theme\/default\/src\/styles\/(.*)\.scss}) 26 | end 27 | 28 | # Pack assets with Jammit for NPM distribution 29 | guard :jammit, hide_success: true, public_root: 'theme/default' do 30 | watch(/^theme\/default\/lib\/scripts\/(.*)\.js$/) 31 | watch(/^theme\/default\/lib\/styles\/(.*)\.css$/) 32 | end 33 | 34 | # Pack assets with Jammit for LiveReload 35 | guard :jammit, public_root: 'doc' do 36 | watch(/^theme\/default\/lib\/scripts\/(.*)\.js$/) 37 | watch(/^theme\/default\/lib\/styles\/(.*)\.css$/) 38 | end 39 | 40 | # Load changes with LiveReload into browser 41 | guard :livereload do 42 | watch(%r{^doc\/(.+)$}) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_comment_indentation.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "class_with_comment_indentation.coffee": { 4 | "objects": { 5 | "2": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "superClass": null, 10 | "bindingType": null, 11 | "classProperties": [ 12 | [ 13 | 11, 14 | 9 15 | ] 16 | ], 17 | "prototypeProperties": [], 18 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text. ", 19 | "range": [ 20 | [ 21 | 2, 22 | 0 23 | ], 24 | [ 25 | 11, 26 | 11 27 | ] 28 | ] 29 | } 30 | }, 31 | "11": { 32 | "9": { 33 | "name": "copy", 34 | "bindingType": "classProperty", 35 | "type": "function", 36 | "paramNames": [], 37 | "range": [ 38 | [ 39 | 11, 40 | 9 41 | ], 42 | [ 43 | 11, 44 | 10 45 | ] 46 | ], 47 | "doc": " Public: Takes an argument and does some stuff.\n\n* one\n * two\n * three\n * four\n * five " 48 | } 49 | } 50 | }, 51 | "exports": {} 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/multiple_requires_single_line.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "multiple_requires_single_line.coffee": { 4 | "objects": { 5 | "0": { 6 | "1": { 7 | "type": "import", 8 | "range": [ 9 | [ 10 | 0, 11 | 1 12 | ], 13 | [ 14 | 0, 15 | 3 16 | ] 17 | ], 18 | "bindingType": "variable", 19 | "module": "kaz", 20 | "name": "faz", 21 | "exportsProperty": "faz" 22 | }, 23 | "6": { 24 | "type": "import", 25 | "range": [ 26 | [ 27 | 0, 28 | 6 29 | ], 30 | [ 31 | 0, 32 | 8 33 | ] 34 | ], 35 | "bindingType": "variable", 36 | "module": "kaz", 37 | "name": "baz", 38 | "exportsProperty": "baz" 39 | } 40 | }, 41 | "2": { 42 | "0": { 43 | "type": "class", 44 | "name": "Bar", 45 | "superClass": null, 46 | "bindingType": null, 47 | "classProperties": [], 48 | "prototypeProperties": [], 49 | "doc": null, 50 | "range": [ 51 | [ 52 | 2, 53 | 0 54 | ], 55 | [ 56 | 2, 57 | 8 58 | ] 59 | ] 60 | } 61 | } 62 | }, 63 | "exports": {} 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/references/buffer-patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "buffer-patch.coffee": { 4 | "objects": { 5 | "0": { 6 | "1": { 7 | "type": "import", 8 | "range": [ 9 | [ 10 | 0, 11 | 1 12 | ], 13 | [ 14 | 0, 15 | 9 16 | ] 17 | ], 18 | "bindingType": "variable", 19 | "path": "./helpers", 20 | "name": "getTextOG", 21 | "exportsProperty": "getTextOG" 22 | } 23 | }, 24 | "3": { 25 | "0": { 26 | "type": "class", 27 | "name": "TextBuffer", 28 | "superClass": null, 29 | "bindingType": "exports", 30 | "classProperties": [], 31 | "prototypeProperties": [ 32 | [ 33 | 4, 34 | 11 35 | ] 36 | ], 37 | "doc": null, 38 | "range": [ 39 | [ 40 | 3, 41 | 0 42 | ], 43 | [ 44 | 4, 45 | 20 46 | ] 47 | ] 48 | } 49 | }, 50 | "4": { 51 | "11": { 52 | "name": "getText", 53 | "type": "primitive", 54 | "range": [ 55 | [ 56 | 4, 57 | 11 58 | ], 59 | [ 60 | 4, 61 | 19 62 | ] 63 | ], 64 | "doc": null, 65 | "bindingType": "prototypeProperty", 66 | "reference": { 67 | "position": [ 68 | 0, 69 | 1 70 | ] 71 | } 72 | } 73 | } 74 | }, 75 | "exports": 3 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_class_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "class_with_class_properties.coffee": { 4 | "objects": { 5 | "3": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "superClass": null, 10 | "bindingType": null, 11 | "classProperties": [ 12 | [ 13 | 4, 14 | 10 15 | ], 16 | [ 17 | 11, 18 | 12 19 | ] 20 | ], 21 | "prototypeProperties": [], 22 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text.\n\n ", 23 | "range": [ 24 | [ 25 | 3, 26 | 0 27 | ], 28 | [ 29 | 11, 30 | 18 31 | ] 32 | ] 33 | } 34 | }, 35 | "4": { 36 | "10": { 37 | "name": "prop2", 38 | "type": "primitive", 39 | "range": [ 40 | [ 41 | 4, 42 | 10 43 | ], 44 | [ 45 | 4, 46 | 14 47 | ] 48 | ], 49 | "doc": null, 50 | "bindingType": "classProperty" 51 | } 52 | }, 53 | "11": { 54 | "12": { 55 | "name": "method2", 56 | "bindingType": "classProperty", 57 | "type": "function", 58 | "paramNames": [ 59 | "a" 60 | ], 61 | "range": [ 62 | [ 63 | 11, 64 | 12 65 | ], 66 | [ 67 | 11, 68 | 17 69 | ] 70 | ], 71 | "doc": " Public: Takes an argument and does some stuff.\n\na - A {String}\n\nReturns {Boolean}. " 72 | } 73 | } 74 | }, 75 | "exports": {} 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_prototype_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "class_with_prototype_properties.coffee": { 4 | "objects": { 5 | "3": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "bindingType": null, 10 | "superClass": null, 11 | "classProperties": [], 12 | "prototypeProperties": [ 13 | [ 14 | 4, 15 | 9 16 | ], 17 | [ 18 | 11, 19 | 11 20 | ] 21 | ], 22 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text.\n\n ", 23 | "range": [ 24 | [ 25 | 3, 26 | 0 27 | ], 28 | [ 29 | 11, 30 | 17 31 | ] 32 | ] 33 | } 34 | }, 35 | "4": { 36 | "9": { 37 | "name": "prop2", 38 | "type": "primitive", 39 | "range": [ 40 | [ 41 | 4, 42 | 9 43 | ], 44 | [ 45 | 4, 46 | 13 47 | ] 48 | ], 49 | "doc": null, 50 | "bindingType": "prototypeProperty" 51 | } 52 | }, 53 | "11": { 54 | "11": { 55 | "name": "method2", 56 | "bindingType": "prototypeProperty", 57 | "type": "function", 58 | "paramNames": [ 59 | "a" 60 | ], 61 | "range": [ 62 | [ 63 | 11, 64 | 11 65 | ], 66 | [ 67 | 11, 68 | 16 69 | ] 70 | ], 71 | "doc": " Public: Takes an argument and does some stuff.\n\na - A {String}\n\nReturns {Boolean}. " 72 | } 73 | } 74 | }, 75 | "exports": {} 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/nodes/property.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | Doc = require './doc' 3 | 4 | _ = require 'underscore' 5 | _.str = require 'underscore.string' 6 | 7 | # Public: A class property that is defined by custom property set/get methods. 8 | # 9 | # Examples 10 | # 11 | # class Test 12 | # 13 | # get = (props) => @::__defineGetter__ name, getter for name, getter of props 14 | # set = (props) => @::__defineSetter__ name, setter for name, setter of props 15 | # 16 | # get name: -> @name 17 | # set name: (@name) -> 18 | # 19 | module.exports = class Property extends Node 20 | 21 | # Public: Construct a new property node. 22 | # 23 | # entity - The property's {Class} 24 | # node - The property node (a {Object}) 25 | # lineMapping - An object mapping the actual position of a member to its Donna one 26 | # options - The parser options (a {Object}) 27 | # name - The filename (a {String}) 28 | # comment - The comment node (a {Object}) 29 | constructor: (@entity, @node, @lineMapping, @options, @name, comment) -> 30 | @doc = new Doc(comment, @options) 31 | 32 | @setter = false 33 | @getter = false 34 | 35 | # Public: Get the source line number 36 | # 37 | # Returns a {Number}. 38 | getLocation: -> 39 | try 40 | unless @location 41 | {locationData} = @node.variable 42 | firstLine = locationData.first_line 43 | @location = { line: firstLine - @lineMapping[firstLine] + 1 } 44 | 45 | @location 46 | 47 | catch error 48 | console.warn("Get location error at #{@fileName}:", @node, error) if @options.verbose 49 | 50 | # Public: Get the property signature. 51 | # 52 | # Returns the signature (a {String}) 53 | getSignature: -> 54 | try 55 | unless @signature 56 | @signature = '' 57 | 58 | if @doc 59 | @signature += if @doc.property then "(#{ _.str.escapeHTML @doc.property }) " else "(?) " 60 | 61 | @signature += "#{ @name }" 62 | 63 | @signature 64 | 65 | catch error 66 | console.warn('Get property signature error:', @node, error) if @options.verbose 67 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/class_with_comment_section.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "class_with_comment_section.coffee": { 4 | "objects": { 5 | "3": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "superClass": null, 10 | "bindingType": null, 11 | "classProperties": [], 12 | "prototypeProperties": [ 13 | [ 14 | 4, 15 | 9 16 | ], 17 | [ 18 | 15, 19 | 11 20 | ] 21 | ], 22 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text.\n\n ", 23 | "range": [ 24 | [ 25 | 3, 26 | 0 27 | ], 28 | [ 29 | 15, 30 | 17 31 | ] 32 | ] 33 | } 34 | }, 35 | "4": { 36 | "9": { 37 | "name": "prop2", 38 | "type": "primitive", 39 | "range": [ 40 | [ 41 | 4, 42 | 9 43 | ], 44 | [ 45 | 4, 46 | 13 47 | ] 48 | ], 49 | "doc": null, 50 | "bindingType": "prototypeProperty" 51 | } 52 | }, 53 | "6": { 54 | "2": { 55 | "type": "comment", 56 | "doc": "\nSection: Something\n", 57 | "range": [ 58 | [ 59 | 6, 60 | 2 61 | ], 62 | [ 63 | 8, 64 | 4 65 | ] 66 | ] 67 | } 68 | }, 69 | "15": { 70 | "11": { 71 | "name": "method2", 72 | "bindingType": "prototypeProperty", 73 | "type": "function", 74 | "paramNames": [ 75 | "a" 76 | ], 77 | "range": [ 78 | [ 79 | 15, 80 | 11 81 | ], 82 | [ 83 | 15, 84 | 16 85 | ] 86 | ], 87 | "doc": " Public: Takes an argument and does some stuff.\n\na - A {String}\n\nReturns {Boolean}. " 88 | } 89 | } 90 | }, 91 | "exports": {} 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /spec/metadata_templates/classes/classes_with_similar_methods.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "classes_with_similar_methods.coffee": { 4 | "objects": { 5 | "2": { 6 | "0": { 7 | "type": "class", 8 | "name": "TextBuffer", 9 | "superClass": null, 10 | "bindingType": null, 11 | "classProperties": [ 12 | [ 13 | 4, 14 | 9 15 | ] 16 | ], 17 | "prototypeProperties": [], 18 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text. ", 19 | "range": [ 20 | [ 21 | 2, 22 | 0 23 | ], 24 | [ 25 | 5, 26 | 0 27 | ] 28 | ] 29 | } 30 | }, 31 | "4": { 32 | "9": { 33 | "name": "copy", 34 | "bindingType": "classProperty", 35 | "type": "function", 36 | "paramNames": [], 37 | "range": [ 38 | [ 39 | 4, 40 | 9 41 | ], 42 | [ 43 | 4, 44 | 10 45 | ] 46 | ], 47 | "doc": "Public: Takes an argument and does some stuff. " 48 | } 49 | }, 50 | "6": { 51 | "0": { 52 | "type": "class", 53 | "superClass": null, 54 | "bindingType": null, 55 | "name": "DisplayBuffer", 56 | "classProperties": [ 57 | [ 58 | 8, 59 | 9 60 | ] 61 | ], 62 | "prototypeProperties": [], 63 | "doc": null, 64 | "range": [ 65 | [ 66 | 6, 67 | 0 68 | ], 69 | [ 70 | 8, 71 | 11 72 | ] 73 | ] 74 | } 75 | }, 76 | "8": { 77 | "9": { 78 | "name": "copy", 79 | "bindingType": "classProperty", 80 | "type": "function", 81 | "paramNames": [], 82 | "range": [ 83 | [ 84 | 8, 85 | 9 86 | ], 87 | [ 88 | 8, 89 | 10 90 | ] 91 | ], 92 | "doc": "Private: Nope, not the same as the other `copy()` " 93 | } 94 | } 95 | }, 96 | "exports": {} 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/nodes/file.coffee: -------------------------------------------------------------------------------- 1 | Path = require 'path' 2 | Class = require './class' 3 | Method = require './method' 4 | Variable = require './variable' 5 | Doc = require './doc' 6 | 7 | # Public: The file class is a `fake` class that wraps the 8 | # file body to capture top-level assigned methods. 9 | # 10 | module.exports = class File extends Class 11 | 12 | # Public: Construct a `File` object. 13 | # 14 | # node - The class node (a {Object}) 15 | # filename - A {String} representing the current filename 16 | # lineMapping - An object mapping the actual position of a member to its Donna one 17 | # options - Any additional parser options 18 | constructor: (@node, @fileName, @lineMapping, @options) -> 19 | try 20 | @methods = [] 21 | @variables = [] 22 | 23 | previousExp = null 24 | 25 | for exp in @node.expressions 26 | switch exp.constructor.name 27 | 28 | when 'Assign' 29 | doc = previousExp if previousExp?.constructor.name is 'Comment' 30 | 31 | switch exp.value?.constructor.name 32 | when 'Code' 33 | @methods.push(new Method(@, exp, @lineMapping, @options, doc)) 34 | when 'Value' 35 | if exp.value.base.value 36 | @variables.push new Variable(@, exp, @lineMapping, @options, true, doc) 37 | 38 | doc = null 39 | 40 | when 'Value' 41 | previousProp = null 42 | 43 | for prop in exp.base.properties 44 | doc = previousProp if previousProp?.constructor.name is 'Comment' 45 | 46 | if prop.value?.constructor.name is 'Code' 47 | @methods.push new Method(@, prop, @lineMapping, @options, doc) 48 | 49 | doc = null 50 | previousProp = prop 51 | previousExp = exp 52 | 53 | catch error 54 | console.warn('File class error:', @node, error) if @options.verbose 55 | 56 | 57 | # Public: Get the full file name with path 58 | # 59 | # Returns the file name with path as a {String}. 60 | getFullName: -> 61 | fullName = @fileName 62 | 63 | for input in @options.inputs 64 | input = input.replace(///^\.[\/]///, '') # Clean leading `./` 65 | input = input + Path.sep unless ///#{ Path.sep }$///.test input # Append trailing `/` 66 | input = input.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1") # Escape String 67 | fullName = fullName.replace(new RegExp(input), '') 68 | 69 | fullName.replace(Path.sep, '/') 70 | 71 | # Public: Returns the file class name. 72 | # 73 | # Returns the file name without path as a {String}. 74 | getFileName: -> 75 | Path.basename @getFullName() 76 | 77 | # Public: Get the file path 78 | # 79 | # Returns the file path as a {String}. 80 | getPath: -> 81 | path = Path.dirname @getFullName() 82 | path = '' if path is '.' 83 | path 84 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/src/range.coffee: -------------------------------------------------------------------------------- 1 | Grim = require 'grim' 2 | Point = require './point' 3 | {newlineRegex} = require './helpers' 4 | Fs = require 'fs' 5 | 6 | # Public: Represents a region in a buffer in row/column coordinates. 7 | # 8 | # Every public method that takes a range also accepts a *range-compatible* 9 | # {Array}. This means a 2-element array containing {Point}s or point-compatible 10 | # arrays. So the following are equivalent: 11 | # 12 | # ```coffee 13 | # new Range(new Point(0, 1), new Point(2, 3)) 14 | # new Range([0, 1], [2, 3]) 15 | # [[0, 1], [2, 3]] 16 | # ``` 17 | module.exports = 18 | class Range 19 | grim: Grim 20 | 21 | # Public: Call this with the result of {Range::serialize} to construct a new Range. 22 | @deserialize: (array) -> 23 | new this(array...) 24 | 25 | # Public: Convert any range-compatible object to a {Range}. 26 | # 27 | # * object: 28 | # This can be an object that's already a {Range}, in which case it's 29 | # simply returned, or an array containing two {Point}s or point-compatible 30 | # arrays. 31 | # * copy: 32 | # An optional boolean indicating whether to force the copying of objects 33 | # that are already ranges. 34 | # 35 | # Returns: A {Range} based on the given object. 36 | @fromObject: (object, copy) -> 37 | if Array.isArray(object) 38 | new this(object...) 39 | else if object instanceof this 40 | if copy then object.copy() else object 41 | else 42 | new this(object.start, object.end) 43 | # Public: Returns a {Range} that starts at the given point and ends at the 44 | # start point plus the given row and column deltas. 45 | # 46 | # * startPoint: 47 | # A {Point} or point-compatible {Array} 48 | # * rowDelta: 49 | # A {Number} indicating how many rows to add to the start point to get the 50 | # end point. 51 | # * columnDelta: 52 | # A {Number} indicating how many rows to columns to the start point to get 53 | # the end point. 54 | # 55 | # Returns a {Range} 56 | @fromPointWithDelta: (startPoint, rowDelta, columnDelta) -> 57 | startPoint = Point.fromObject(startPoint) 58 | endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta) 59 | new this(startPoint, endPoint) 60 | 61 | constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) -> 62 | pointA = Point.fromObject(pointA) 63 | pointB = Point.fromObject(pointB) 64 | 65 | if pointA.isLessThanOrEqual(pointB) 66 | @start = pointA 67 | @end = pointB 68 | else 69 | @start = pointB 70 | @end = pointA 71 | 72 | # Public: Returns a {Boolean} indicating whether this range has the same start 73 | # and end points as the given {Range} or range-compatible {Array}. 74 | isEqual: (other) -> 75 | return false unless other? 76 | other = @constructor.fromObject(other) 77 | other.start.isEqual(@start) and other.end.isEqual(@end) 78 | -------------------------------------------------------------------------------- /src/nodes/variable.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | Doc = require './doc' 3 | 4 | # Public: The Node representation of a CoffeeScript variable. 5 | module.exports = class Variable extends Node 6 | 7 | # Public: Construct a variable node. 8 | # 9 | # entity - The variable's {Class} 10 | # node - The variable node (a {Object}) 11 | # lineMapping - An object mapping the actual position of a member to its Donna one 12 | # options - The parser options (a {Object}) 13 | # classType - A {Boolean} indicating if the class is a `class` or an `instance` 14 | # comment - The comment node (a {Object}) 15 | constructor: (@entity, @node, @lineMapping, @options, @classType = false, comment = null) -> 16 | try 17 | @doc = new Doc(comment, @options) 18 | @getName() 19 | 20 | catch error 21 | console.warn('Create variable error:', @node, error) if @options.verbose 22 | 23 | # Public: Get the variable type, either `class` or `constant` 24 | # 25 | # Returns the variable type (a {String}). 26 | getType: -> 27 | unless @type 28 | @type = if @classType then 'class' else 'instance' 29 | 30 | @type 31 | 32 | # Public: Test if the given value should be treated ad constant. 33 | # 34 | # Returns true if a constant (a {Boolean}) 35 | # 36 | isConstant: -> 37 | unless @constant 38 | @constant = /^[A-Z_-]*$/.test @getName() 39 | 40 | @constant 41 | 42 | # Public: Get the class doc 43 | # 44 | # Returns the class doc (a [Doc]). 45 | getDoc: -> @doc 46 | 47 | # Public: Get the variable name 48 | # 49 | # Returns the variable name (a {String}). 50 | getName: -> 51 | try 52 | unless @name 53 | @name = @node.variable.base.value 54 | 55 | for prop in @node.variable.properties 56 | @name += ".#{ prop.name.value }" 57 | 58 | if /^this\./.test @name 59 | @name = @name.substring(5) 60 | @type = 'class' 61 | 62 | @name 63 | 64 | catch error 65 | console.warn('Get method name error:', @node, error) if @options.verbose 66 | 67 | 68 | # Public: Get the source line number 69 | # 70 | # Returns a {Number}. 71 | getLocation: -> 72 | try 73 | unless @location 74 | {locationData} = @node.variable 75 | firstLine = locationData.first_line + 1 76 | if !@lineMapping[firstLine]? 77 | @lineMapping[firstLine] = @lineMapping[firstLine - 1] 78 | 79 | @location = { line: @lineMapping[firstLine] } 80 | 81 | @location 82 | 83 | catch error 84 | console.warn("Get location error at #{@fileName}:", @node, error) if @options.verbose 85 | 86 | # Public: Get the variable value. 87 | # 88 | # Returns the value (a {String}). 89 | getValue: -> 90 | try 91 | unless @value 92 | @value = @node.value.base.compile({ indent: '' }) 93 | 94 | @value 95 | 96 | catch error 97 | console.warn('Get method value error:', @node, error) if @options.verbose 98 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/src/point.coffee: -------------------------------------------------------------------------------- 1 | # Public: Represents a point in a buffer in row/column coordinates. 2 | # 3 | # Every public method that takes a point also accepts a *point-compatible* 4 | # {Array}. This means a 2-element array containing {Number}s representing the 5 | # row and column. So the following are equivalent: 6 | # 7 | # ```coffee 8 | # new Point(1, 2) 9 | # [1, 2] 10 | # ``` 11 | module.exports = 12 | class Point 13 | # Public: Convert any point-compatible object to a {Point}. 14 | # 15 | # * object: 16 | # This can be an object that's already a {Point}, in which case it's 17 | # simply returned, or an array containing two {Number}s representing the 18 | # row and column. 19 | # 20 | # * copy: 21 | # An optional boolean indicating whether to force the copying of objects 22 | # that are already points. 23 | # 24 | # Returns: A {Point} based on the given object. 25 | @fromObject: (object, copy) -> 26 | if object instanceof Point 27 | if copy then object.copy() else object 28 | else 29 | if Array.isArray(object) 30 | [row, column] = object 31 | else 32 | { row, column } = object 33 | 34 | new Point(row, column) 35 | 36 | # Public: Returns the given point that is earlier in the buffer. 37 | @min: (point1, point2) -> 38 | point1 = @fromObject(point1) 39 | point2 = @fromObject(point2) 40 | if point1.isLessThanOrEqual(point2) 41 | point1 42 | else 43 | point2 44 | 45 | constructor: (@row=0, @column=0) -> 46 | 47 | # Public: Returns a new {Point} with the same row and column. 48 | copy: -> 49 | new Point(@row, @column) 50 | 51 | # Public: Makes this point immutable and returns itself. 52 | freeze: -> 53 | Object.freeze(this) 54 | 55 | # Public: Return a new {Point} based on shifting this point by the given delta, 56 | # which is represented by another {Point}. 57 | translate: (delta) -> 58 | {row, column} = Point.fromObject(delta) 59 | new Point(@row + row, @column + column) 60 | 61 | add: (other) -> 62 | other = Point.fromObject(other) 63 | row = @row + other.row 64 | if other.row == 0 65 | column = @column + other.column 66 | else 67 | column = other.column 68 | 69 | new Point(row, column) 70 | 71 | splitAt: (column) -> 72 | if @row == 0 73 | rightColumn = @column - column 74 | else 75 | rightColumn = @column 76 | 77 | [new Point(0, column), new Point(@row, rightColumn)] 78 | 79 | # Public: 80 | # 81 | # * other: A {Point} or point-compatible {Array}. 82 | # 83 | # Returns: 84 | # * -1 if this point precedes the argument. 85 | # * 0 if this point is equivalent to the argument. 86 | # * 1 if this point follows the argument. 87 | compare: (other) -> 88 | if @row > other.row 89 | 1 90 | else if @row < other.row 91 | -1 92 | else 93 | if @column > other.column 94 | 1 95 | else if @column < other.column 96 | -1 97 | else 98 | 0 99 | -------------------------------------------------------------------------------- /src/nodes/parameter.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | 3 | _ = require 'underscore' 4 | _.str = require 'underscore.string' 5 | 6 | # Public: The Node representation of a CoffeeScript method parameter. 7 | module.exports = class Parameter extends Node 8 | 9 | # Public: Construct a parameter node. 10 | # 11 | # node - The node (a {Object}) 12 | # options - The parser options (a {Object}) 13 | # optionized - A {Boolean} indicating if the parameter is a set of options 14 | constructor: (@node, @options, @optionized) -> 15 | 16 | # Public: Get the full parameter signature. 17 | # 18 | # Returns the signature (a {String}). 19 | getSignature: -> 20 | try 21 | unless @signature 22 | @signature = @getName() 23 | 24 | if @isSplat() 25 | @signature += '...' 26 | 27 | value = @getDefault() 28 | @signature += " = #{ value.replace(/\n\s*/g, ' ') }" if value 29 | 30 | @signature 31 | 32 | catch error 33 | console.warn('Get parameter signature error:', @node, error) if @options.verbose 34 | 35 | # Public: Get the parameter name 36 | # 37 | # Returns the name (a {String}). 38 | getName: (i = -1) -> 39 | try 40 | # params like `method: ({option1, option2}) ->` 41 | if @optionized && i >= 0 42 | @name = @node.name.properties[i].base.value 43 | 44 | unless @name 45 | 46 | # Normal attribute `do: (it) ->` 47 | @name = @node.name.value 48 | 49 | unless @name 50 | if @node.name.properties 51 | # Assigned attributes `do: (@it) ->` 52 | @name = @node.name.properties[0]?.name?.value 53 | 54 | @name 55 | 56 | catch error 57 | console.warn('Get parameter name error:', @node, error) if @options.verbose 58 | 59 | # Public: Get the parameter default value 60 | # 61 | # Returns the default (a {String}). 62 | getDefault: (i = -1) -> 63 | try 64 | # for optionized arguments 65 | if @optionized && i >= 0 66 | _.str.strip(@node.value?.compile({ indent: '' })[1..-2].split(",")[i]).split(": ")[1] 67 | else 68 | @node.value?.compile({ indent: '' }) 69 | 70 | catch error 71 | if @node?.value?.base?.value is 'this' 72 | "@#{ @node.value.properties[0]?.name.compile({ indent: '' }) }" 73 | else 74 | console.warn('Get parameter default error:', @node, error) if @options.verbose 75 | 76 | # Public: Gets the defaults of the optionized parameters. 77 | # 78 | # Returns the defaults as a {String}. 79 | getOptionizedDefaults: -> 80 | return '' unless @node.value? 81 | 82 | defaults = [] 83 | for k in @node.value.compile({ indent: '' }).split("\n")[1..-2] 84 | defaults.push _.str.strip(k.split(":")[0]) 85 | 86 | return "{" + defaults.join(",") + "}" 87 | 88 | # Public: Checks if the parameters is a splat 89 | # 90 | # Returns `true` if a splat (a {Boolean}). 91 | isSplat: -> 92 | try 93 | @node.splat is true 94 | 95 | catch error 96 | console.warn('Get parameter splat type error:', @node, error) if @options.verbose 97 | -------------------------------------------------------------------------------- /spec/metadata_templates/requires/basic_requires.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "basic_requires.coffee": { 4 | "objects": { 5 | "0": { 6 | "1": { 7 | "type": "import", 8 | "range": [ 9 | [ 10 | 0, 11 | 1 12 | ], 13 | [ 14 | 0, 15 | 3 16 | ] 17 | ], 18 | "bindingType": "variable", 19 | "module": "foo", 20 | "name": "Foo", 21 | "exportsProperty": "Foo" 22 | } 23 | }, 24 | "1": { 25 | "14": { 26 | "type": "import", 27 | "range": [ 28 | [ 29 | 1, 30 | 14 31 | ], 32 | [ 33 | 1, 34 | 28 35 | ] 36 | ], 37 | "bindingType": "variable", 38 | "module": "defs2", 39 | "name": "foof", 40 | "exportsProperty": "defs" 41 | } 42 | }, 43 | "2": { 44 | "1": { 45 | "type": "import", 46 | "range": [ 47 | [ 48 | 2, 49 | 1 50 | ], 51 | [ 52 | 2, 53 | 3 54 | ] 55 | ], 56 | "bindingType": "variable", 57 | "module": "kaz", 58 | "name": "faz", 59 | "exportsProperty": "faz" 60 | }, 61 | "6": { 62 | "type": "import", 63 | "range": [ 64 | [ 65 | 2, 66 | 6 67 | ], 68 | [ 69 | 2, 70 | 8 71 | ] 72 | ], 73 | "bindingType": "variable", 74 | "module": "kaz", 75 | "name": "baz", 76 | "exportsProperty": "baz" 77 | } 78 | }, 79 | "3": { 80 | "1": { 81 | "type": "import", 82 | "range": [ 83 | [ 84 | 3, 85 | 1 86 | ], 87 | [ 88 | 3, 89 | 3 90 | ] 91 | ], 92 | "bindingType": "variable", 93 | "module": "hoo", 94 | "name": "boo", 95 | "exportsProperty": "boo" 96 | } 97 | }, 98 | "5": { 99 | "0": { 100 | "type": "class", 101 | "name": "Bar", 102 | "superClass": null, 103 | "bindingType": null, 104 | "classProperties": [], 105 | "prototypeProperties": [], 106 | "doc": null, 107 | "range": [ 108 | [ 109 | 5, 110 | 0 111 | ], 112 | [ 113 | 5, 114 | 8 115 | ] 116 | ] 117 | } 118 | } 119 | }, 120 | "exports": {} 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/nodes/virtual_method.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | Parameter = require './parameter' 3 | Doc = require './doc' 4 | 5 | _ = require 'underscore' 6 | _.str = require 'underscore.string' 7 | 8 | # Public: The Node representation of a CoffeeScript virtual method that has 9 | # been declared by the `@method` tag. 10 | module.exports = class VirtualMethod extends Node 11 | 12 | # Public: Construct a virtual method node. 13 | # 14 | # entity - The method's {Class} 15 | # doc - The property node (a {Object}) 16 | # options - The parser options (a {Object}) 17 | constructor: (@entity, @doc, @options) -> 18 | 19 | # Public: Get the method type, either `class`, `instance` or `mixin`. 20 | # 21 | # Returns the method type (a {String}). 22 | getType: -> 23 | unless @type 24 | if @doc.signature.substring(0, 1) is '.' 25 | @type = 'instance' 26 | else if @doc.signature.substring(0, 1) is '@' 27 | @type = 'class' 28 | else 29 | @type = 'mixin' 30 | 31 | @type 32 | 33 | # Public: Get the class doc 34 | # 35 | # Returns the class doc (a {Doc}). 36 | getDoc: -> @doc 37 | 38 | # Public: Get the full method signature. 39 | # 40 | # Returns the signature (a {String}). 41 | getSignature: -> 42 | try 43 | unless @signature 44 | @signature = switch @getType() 45 | when 'class' 46 | '+ ' 47 | when 'instance' 48 | '- ' 49 | else 50 | '? ' 51 | 52 | if @getDoc() 53 | @signature += if @getDoc().returnValue then "(#{ _.str.escapeHTML @getDoc().returnValue.type }) " else "(void) " 54 | 55 | @signature += "#{ @getName() }" 56 | @signature += '(' 57 | 58 | params = [] 59 | 60 | for param in @getParameters() 61 | params.push param.name 62 | 63 | @signature += params.join(', ') 64 | @signature += ')' 65 | 66 | @signature 67 | 68 | catch error 69 | console.warn('Get method signature error:', @node, error) if @options.verbose 70 | 71 | # Public: Get the short method signature. 72 | # 73 | # Returns the short signature (a {String}). 74 | getShortSignature: -> 75 | try 76 | unless @shortSignature 77 | @shortSignature = switch @getType() 78 | when 'class' 79 | '@' 80 | when 'instance' 81 | '.' 82 | else 83 | '' 84 | @shortSignature += @getName() 85 | 86 | @shortSignature 87 | 88 | catch error 89 | console.warn('Get method short signature error:', @node, error) if @options.verbose 90 | 91 | # Public: Get the method name 92 | # 93 | # Returns the method name (a {String}). 94 | getName: -> 95 | try 96 | unless @name 97 | if name = /[.#]?([$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*)/i.exec @doc.signature 98 | @name = name[1] 99 | else 100 | @name = 'unknown' 101 | 102 | @name 103 | 104 | catch error 105 | console.warn('Get method name error:', @node, error) if @options.verbose 106 | 107 | # Public: Get the method parameters 108 | # 109 | # params - The method parameters 110 | getParameters: -> @doc.params or [] 111 | 112 | # Public: Get the method source in CoffeeScript 113 | # 114 | # Returns the CoffeeScript source (a {String}). 115 | getCoffeeScriptSource: -> 116 | 117 | # Public: Get the method source in JavaScript 118 | # 119 | # Returns the JavaScript source (a {String}). 120 | getJavaScriptSource: -> 121 | -------------------------------------------------------------------------------- /src/nodes/mixin.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | Method = require './method' 3 | Variable = require './variable' 4 | Doc = require './doc' 5 | 6 | # Public: The Node representation of a CoffeeScript mixins 7 | # 8 | module.exports = class Mixin extends Node 9 | 10 | # Public: Construct a mixin 11 | # 12 | # node - The mixin node (a {Object}) 13 | # fileName - The filename (a {String}) 14 | # options - The parser options (a {Object}) 15 | # comment - The comment node (a {Object}) 16 | constructor: (@node, @fileName, @options, comment) -> 17 | try 18 | @methods = [] 19 | @variables = [] 20 | 21 | @doc = new Doc(comment, @options) 22 | 23 | previousExp = null 24 | 25 | for exp in @node.value.base.properties 26 | 27 | # Recognize assigned code on the mixin 28 | if exp.constructor.name is 'Assign' 29 | doc = previousExp if previousExp?.constructor.name is 'Comment' 30 | 31 | if exp.value?.constructor.name is 'Code' 32 | @methods.push new Method(@, exp, @options, doc) 33 | 34 | # Recognize concerns as inner mixins 35 | if exp.value?.constructor.name is 'Value' 36 | switch exp.variable.base.value 37 | when 'ClassMethods' 38 | @classMixin = new Mixin(exp, @filename, @options, doc) 39 | 40 | when 'InstanceMethods' 41 | @instanceMixin = new Mixin(exp, @filename, options, doc) 42 | 43 | doc = null 44 | previousExp = exp 45 | 46 | if @classMixin? && @instanceMixin? 47 | @concern = true 48 | 49 | for method in @classMixin.getMethods() 50 | method.type = 'class' 51 | @methods.push method 52 | 53 | for method in @instanceMixin.getMethods() 54 | method.type = 'instance' 55 | @methods.push method 56 | else 57 | @concern = false 58 | 59 | catch error 60 | console.warn('Create mixin error:', @node, error) if @options.verbose 61 | 62 | # Public: Get the source file name. 63 | # 64 | # Returns the filename of the mixin (a {String}). 65 | getFileName: -> @fileName 66 | 67 | # Public: Get the mixin doc 68 | # 69 | # Returns the mixin doc (a [Doc]) 70 | getDoc: -> @doc 71 | 72 | # Public: Get the full mixin name 73 | # 74 | # Returns full mixin name (a {String}). 75 | getMixinName: -> 76 | try 77 | unless @mixinName 78 | name = [] 79 | name = [@node.variable.base.value] unless @node.variable.base.value == 'this' 80 | name.push p.name.value for p in @node.variable.properties 81 | @mixinName = name.join('.') 82 | 83 | @mixinName 84 | 85 | catch error 86 | console.warn('Get mixin full name error:', @node, error) if @options.verbose 87 | 88 | # Public: Alias for {::getMixinName} 89 | getFullName: -> 90 | @getMixinName() 91 | 92 | # Public: Gets the mixin name 93 | # 94 | # Returns the name (a {String}). 95 | getName: -> 96 | try 97 | unless @name 98 | @name = @getMixinName().split('.').pop() 99 | 100 | @name 101 | 102 | catch error 103 | console.warn('Get mixin name error:', @node, error) if @options.verbose 104 | 105 | # Public: Get the mixin namespace 106 | # 107 | # Returns the namespace (a {String}). 108 | getNamespace: -> 109 | try 110 | unless @namespace 111 | @namespace = @getMixinName().split('.') 112 | @namespace.pop() 113 | 114 | @namespace = @namespace.join('.') 115 | 116 | @namespace 117 | 118 | catch error 119 | console.warn('Get mixin namespace error:', @node, error) if @options.verbose 120 | 121 | # Public: Get all methods. 122 | # 123 | # Returns an {Array} of all the {Method}s. 124 | getMethods: -> @methods 125 | 126 | # Get all variables. 127 | # 128 | # Returns an {Array} of all the {Variable}s. 129 | getVariables: -> @variables 130 | -------------------------------------------------------------------------------- /src/donna.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | util = require 'util' 3 | path = require 'path' 4 | walkdir = require 'walkdir' 5 | Async = require 'async' 6 | _ = require 'underscore' 7 | CoffeeScript = require 'coffee-script' 8 | 9 | Parser = require './parser' 10 | Metadata = require './metadata' 11 | {exec} = require 'child_process' 12 | 13 | SRC_DIRS = ['src', 'lib', 'app'] 14 | BLACKLIST_FILES = ['Gruntfile.coffee'] 15 | 16 | main = -> 17 | optimist = require('optimist') 18 | .usage(""" 19 | Usage: $0 [options] [source_files] 20 | """) 21 | .options('o', 22 | alias: 'output-dir' 23 | describe: 'The output directory' 24 | default: './doc' 25 | ) 26 | .options('d', 27 | alias: 'debug' 28 | describe: 'Show stacktraces and converted CoffeeScript source' 29 | boolean: true 30 | default: false 31 | ) 32 | .options('h', 33 | alias: 'help' 34 | describe: 'Show the help' 35 | ) 36 | 37 | argv = optimist.argv 38 | 39 | if argv.h 40 | console.log optimist.help() 41 | return 42 | 43 | options = 44 | inputs: argv._ 45 | output: argv.o 46 | 47 | writeMetadata(generateMetadata(options.inputs), options.output) 48 | 49 | generateMetadata = (inputs) -> 50 | metadataSlugs = [] 51 | 52 | for input in inputs 53 | continue unless (fs.existsSync || path.existsSync)(input) 54 | parser = new Parser() 55 | 56 | # collect probable package.json path 57 | packageJsonPath = path.join(input, 'package.json') 58 | stats = fs.lstatSync input 59 | absoluteInput = path.resolve(process.cwd(), input) 60 | 61 | if stats.isDirectory() 62 | for filename in walkdir.sync input 63 | if isAcceptableFile(filename) and isInAcceptableDir(absoluteInput, filename) 64 | try 65 | parser.parseFile(filename, absoluteInput) 66 | catch error 67 | logError(filename, error) 68 | else 69 | if isAcceptableFile(input) 70 | try 71 | parser.parseFile(input, path.dirname(input)) 72 | catch error 73 | logError(filename, error) 74 | 75 | metadataSlugs.push generateMetadataSlug(packageJsonPath, parser) 76 | 77 | metadataSlugs 78 | 79 | logError = (filename, error) -> 80 | if error.location? 81 | console.warn "Cannot parse file #{ filename }@#{error.location.first_line}: #{ error.message }" 82 | else 83 | console.warn "Cannot parse file #{ filename }: #{ error.message }" 84 | 85 | isAcceptableFile = (filePath) -> 86 | try 87 | return false if fs.statSync(filePath).isDirectory() 88 | 89 | for file in BLACKLIST_FILES 90 | return false if new RegExp(file+'$').test(filePath) 91 | 92 | filePath.match(/\._?coffee$/) 93 | 94 | isInAcceptableDir = (inputPath, filePath) -> 95 | # is in the root of the input? 96 | return true if path.join(inputPath, path.basename(filePath)) is filePath 97 | 98 | # is under src, lib, or app? 99 | acceptableDirs = (path.join(inputPath, dir) for dir in SRC_DIRS) 100 | for dir in acceptableDirs 101 | return true if filePath.indexOf(dir) == 0 102 | 103 | false 104 | 105 | writeMetadata = (metadataSlugs, output) -> 106 | fs.writeFileSync path.join(output, 'metadata.json'), JSON.stringify(metadataSlugs, null, " ") 107 | 108 | # Public: Builds and writes to metadata.json 109 | generateMetadataSlug = (packageJsonPath, parser) -> 110 | if fs.existsSync(packageJsonPath) 111 | packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) 112 | 113 | metadata = new Metadata(packageJson?.dependencies ? {}, parser) 114 | slug = 115 | main: findMainFile(packageJsonPath, packageJson?.main) 116 | repository: packageJson?.repository?.url ? packageJson?.repository 117 | version: packageJson?.version 118 | files: {} 119 | 120 | for filename, content of parser.iteratedFiles 121 | metadata.generate(CoffeeScript.nodes(content)) 122 | populateSlug(slug, filename, metadata) 123 | 124 | slug 125 | 126 | # Public: Parse and collect metadata slugs 127 | populateSlug = (slug, filename, {defs:unindexedObjects, exports:exports}) -> 128 | objects = {} 129 | for key, value of unindexedObjects 130 | startLineNumber = value.range[0][0] 131 | startColNumber = value.range[0][1] 132 | objects[startLineNumber] = {} unless objects[startLineNumber]? 133 | objects[startLineNumber][startColNumber] = value 134 | # Update the classProperties/prototypeProperties to be line numbers 135 | if value.type is 'class' 136 | value.classProperties = ( [prop.range[0][0], prop.range[0][1]] for prop in _.clone(value.classProperties)) 137 | value.prototypeProperties = ([prop.range[0][0], prop.range[0][1]] for prop in _.clone(value.prototypeProperties)) 138 | 139 | if exports._default? 140 | exports = exports._default.range[0][0] if exports._default.range? 141 | else 142 | for key, value of exports 143 | exports[key] = value.startLineNumber 144 | 145 | slug["files"][filename] = {objects, exports} 146 | slug 147 | 148 | findMainFile = (packageJsonPath, main_file) -> 149 | return unless main_file? 150 | 151 | if main_file.match(/\.js$/) 152 | main_file = main_file.replace(/\.js$/, ".coffee") 153 | else 154 | main_file += ".coffee" 155 | 156 | filename = path.basename(main_file) 157 | filepath = path.dirname(packageJsonPath) 158 | 159 | for dir in SRC_DIRS 160 | composite_main = path.normalize path.join(filepath, dir, filename) 161 | 162 | if fs.existsSync composite_main 163 | file = path.relative(packageJsonPath, composite_main) 164 | file = file.substring(1, file.length) if file.match /^\.\./ 165 | return file 166 | 167 | # TODO: lessen the suck enough to remove generateMetadataSlug and populateSlug. They really shouldnt be necessary. 168 | module.exports = {Parser, Metadata, main, generateMetadata, generateMetadataSlug, populateSlug} 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # Donna [![Build Status](https://travis-ci.org/atom/donna.svg?branch=master)](https://travis-ci.org/atom/donna) 3 | 4 | Donna is a tool for generating [CoffeeScript](http://coffeescript.org/) metadata 5 | for the purpose of generating API documentation. It reads your CoffeeScript 6 | module, and outputs an object indicating the locations and other data about 7 | your classes, properties, methods, etc. 8 | 9 | It pulled out of [biscotto](https://github.com/atom/biscotto). 10 | 11 | ## Metadata?? 12 | 13 | The Donna [metadata][meta] format is a very raw format indicating the locations 14 | of objects like classes, functions, and imports within files of a CoffeeScript 15 | module. Included in the metadata are unmolested doc strings for these objects. 16 | 17 | An Example: 18 | 19 | ```coffee 20 | # Public: A mutable text container with undo/redo support and the ability to 21 | # annotate logical regions in the text. 22 | # 23 | class TextBuffer 24 | @prop2: "bar" 25 | 26 | # Public: Takes an argument and does some stuff. 27 | # 28 | # a - A {String} 29 | # 30 | # Returns {Boolean}. 31 | @method2: (a) -> 32 | ``` 33 | 34 | Generates metadata: 35 | 36 | ```json 37 | { 38 | "files": { 39 | "spec/metadata_templates/classes/class_with_prototype_properties.coffee": { 40 | "objects": { 41 | "3": { 42 | "0": { 43 | "type": "class", 44 | "name": "TextBuffer", 45 | "bindingType": null, 46 | "classProperties": [], 47 | "prototypeProperties": [ 48 | [ 49 | 4, 50 | 9 51 | ], 52 | [ 53 | 11, 54 | 11 55 | ] 56 | ], 57 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text.\n\n ", 58 | "range": [ 59 | [ 60 | 3, 61 | 0 62 | ], 63 | [ 64 | 11, 65 | 17 66 | ] 67 | ] 68 | } 69 | }, 70 | "4": { 71 | "9": { 72 | "name": "prop2", 73 | "type": "primitive", 74 | "range": [ 75 | [ 76 | 4, 77 | 9 78 | ], 79 | [ 80 | 4, 81 | 13 82 | ] 83 | ], 84 | "bindingType": "prototypeProperty" 85 | } 86 | }, 87 | "11": { 88 | "11": { 89 | "name": "method2", 90 | "bindingType": "prototypeProperty", 91 | "type": "function", 92 | "paramNames": [ 93 | "a" 94 | ], 95 | "range": [ 96 | [ 97 | 11, 98 | 11 99 | ], 100 | [ 101 | 11, 102 | 16 103 | ] 104 | ], 105 | "doc": " Public: Takes an argument and does some stuff.\n\na - A {String}\n\nReturns {Boolean}. " 106 | } 107 | } 108 | }, 109 | "exports": {} 110 | } 111 | } 112 | } 113 | 114 | ``` 115 | 116 | The Donna metadata format is doc-string-format agnostic. Use tomdoc? Javadoc? 117 | Markdown? With this format, you should be able to generate your own API docs 118 | with any doc format parser you like. 119 | 120 | Donna currently has a counterpart named [tello](https://github.com/atom/tello) 121 | that generates an easily digestible json format using the [atomdoc][atomdoc] 122 | format on the docs strings from Donna output. 123 | 124 | ## Usage 125 | 126 | ``` bash 127 | npm install donna 128 | ``` 129 | 130 | ### From your code 131 | 132 | ```coffee 133 | donna = require 'donna' 134 | metadata = donna.generateMetadata(['/path/to/my-module', '/path/to/another-module']) 135 | ``` 136 | 137 | ### From the command line 138 | 139 | Pass it the _top level directory_ of your module. It will read the 140 | `package.json` file and index any `.coffee` files from within the `src`, `app`, 141 | or `lib` directories: 142 | 143 | ``` bash 144 | donna -o donna.json /path/to/my-module 145 | ``` 146 | 147 | It handles multiple modules. Each should have a `package.json` file. It will 148 | place the results from both modules in the `donna.json` file. 149 | 150 | ``` bash 151 | donna -o donna.json /path/to/my-module /path/to/another-module 152 | ``` 153 | 154 | ## License 155 | 156 | (The MIT License) 157 | 158 | Copyright (c) 2014 GitHub 159 | 160 | Permission is hereby granted, free of charge, to any person obtaining 161 | a copy of this software and associated documentation files (the 162 | 'Software'), to deal in the Software without restriction, including 163 | without limitation the rights to use, copy, modify, merge, publish, 164 | distribute, sublicense, and/or sell copies of the Software, and to 165 | permit persons to whom the Software is furnished to do so, subject to 166 | the following conditions: 167 | 168 | The above copyright notice and this permission notice shall be 169 | included in all copies or substantial portions of the Software. 170 | 171 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 172 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 173 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 174 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 175 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 176 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 177 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 178 | 179 | [meta]:https://github.com/atom/donna/blob/master/spec/metadata_templates/test_package/test_metadata.json 180 | [atomdoc]:https://github.com/atom/atomdoc 181 | -------------------------------------------------------------------------------- /src/nodes/method.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | Parameter = require './parameter' 3 | Doc = require './doc' 4 | 5 | _ = require 'underscore' 6 | _.str = require 'underscore.string' 7 | 8 | # Public: The Node representation of a CoffeeScript method. 9 | module.exports = class Method extends Node 10 | 11 | # Public: Constructs the documentation node. 12 | # 13 | # entity - The method's {Class} 14 | # node - The method node (a {Object}) 15 | # fileName - The filename (a {String}) 16 | # lineMapping - An object mapping the actual position of a member to its Donna one 17 | # options - The parser options (a {Object}) 18 | # comment - The comment node (a {Object}) 19 | constructor: (@entity, @node, @lineMapping, @options, comment) -> 20 | try 21 | @parameters = [] 22 | 23 | @doc = new Doc(comment, @options) 24 | 25 | for param in @node.value.params 26 | if param.name.properties? and param.name.properties[0].base? 27 | for property in param.name.properties 28 | @parameters.push new Parameter(param, @options, true) 29 | else 30 | @parameters.push new Parameter(param, @options) 31 | 32 | @getName() 33 | 34 | catch error 35 | console.warn('Create method error:', @node, error) if @options.verbose 36 | 37 | # Get the method type, either `class` or `instance` 38 | # 39 | # @return {String} the method type 40 | # 41 | getType: -> 42 | unless @type 43 | switch @entity.constructor.name 44 | when 'Class' 45 | @type = 'instance' 46 | when 'Mixin' 47 | @type = 'mixin' 48 | when 'File' 49 | @type = 'file' 50 | 51 | @type 52 | 53 | # Gets the original location of a method. This is only used for prototypical methods defined in Files 54 | getOriginalFilename: -> 55 | unless @originalFilename 56 | @originalFilename = @getDoc().originalFilename 57 | 58 | @originalFilename 59 | 60 | # Gets the original name of a method. This is only used for prototypical methods defined in Files 61 | getOriginalName: -> 62 | unless @originalName 63 | @originalName = @getDoc().originalName 64 | 65 | @originalName 66 | 67 | # Gets the original type of a method. This is only used for prototypical methods defined in Files 68 | getOriginalType: -> 69 | unless @originalType 70 | @originalType = @getDoc().originalType 71 | 72 | @originalType 73 | 74 | # Get the class doc 75 | # 76 | # @return [Doc] the class doc 77 | # 78 | getDoc: -> @doc 79 | 80 | # Get the full method signature. 81 | # 82 | # @return {String} the signature 83 | # 84 | getSignature: -> 85 | try 86 | unless @signature 87 | @signature = switch @getType() 88 | when 'class' 89 | '.' 90 | when 'instance' 91 | '::' 92 | else 93 | if @getOriginalFilename()? then "::" else '? ' 94 | 95 | doc = @getDoc() 96 | 97 | # this adds a superfluous space if there's no type defined 98 | if doc.returnValue && doc.returnValue[0].type 99 | retVals = [] 100 | for retVal in doc.returnValue 101 | retVals.push "#{ _.str.escapeHTML retVal.type }" 102 | @signature = retVals.join("|") + " #{@signature}" 103 | 104 | @signature += "#{ @getOriginalName() || @getName() }" 105 | @signature += '(' 106 | 107 | params = [] 108 | paramOptionized = [] 109 | 110 | for param, i in @getParameters() 111 | if param.optionized 112 | @inParamOption = true 113 | optionizedDefaults = param.getOptionizedDefaults() 114 | paramOptionized.push param.getName(i) 115 | else 116 | if @inParamOption 117 | @inParamOption = false 118 | paramValue = "{#{paramOptionized.join(', ')}}" 119 | paramValue += "=#{optionizedDefaults}" if optionizedDefaults 120 | params.push(paramValue) 121 | paramOptionized = [] 122 | else 123 | params.push param.getSignature() 124 | 125 | # that means there was only one argument, a param'ed one 126 | if paramOptionized.length > 0 127 | paramValue = "{#{paramOptionized.join(', ')}}" 128 | paramValue += "=#{optionizedDefaults}" if optionizedDefaults 129 | params.push(paramValue) 130 | 131 | @signature += params.join(', ') 132 | @signature += ')' 133 | 134 | @signature 135 | 136 | catch error 137 | console.warn('Get method signature error:', @node, error) if @options.verbose 138 | 139 | # Get the short method signature. 140 | # 141 | # @return {String} the short signature 142 | # 143 | getShortSignature: -> 144 | try 145 | unless @shortSignature 146 | @shortSignature = switch @getType() 147 | when 'class' 148 | '@' 149 | when 'instance' 150 | '.' 151 | else 152 | '' 153 | @shortSignature += @getName() 154 | 155 | @shortSignature 156 | 157 | catch error 158 | console.warn('Get method short signature error:', @node, error) if @options.verbose 159 | 160 | # Get the method name 161 | # 162 | # @return {String} the method name 163 | # 164 | getName: -> 165 | try 166 | unless @name 167 | @name = @node.variable.base.value 168 | 169 | # Reserved names will result in a name with a reserved: true property. No Bueno. 170 | @name = @name.slice(0) if @name.reserved is true 171 | 172 | for prop in @node.variable.properties 173 | @name += ".#{ prop.name.value }" 174 | 175 | if /^this\./.test @name 176 | @name = @name.substring(5) 177 | @type = 'class' 178 | 179 | if /^module.exports\./.test @name 180 | @name = @name.substring(15) 181 | @type = 'class' 182 | 183 | if /^exports\./.test @name 184 | @name = @name.substring(8) 185 | @type = 'class' 186 | 187 | @name 188 | 189 | catch error 190 | console.warn('Get method name error:', @node, error) if @options.verbose 191 | 192 | # Public: Get the source line number 193 | # 194 | # Returns a {Number} 195 | # 196 | getLocation: -> 197 | try 198 | unless @location 199 | {locationData} = @node.variable 200 | firstLine = locationData.first_line + 1 201 | if !@lineMapping[firstLine]? 202 | @lineMapping[firstLine] = @lineMapping[firstLine - 1] 203 | 204 | @location = { line: @lineMapping[firstLine] } 205 | 206 | @location 207 | 208 | catch error 209 | console.warn("Get location error at #{@fileName}:", @node, error) if @options.verbose 210 | 211 | # Get the method parameters 212 | # 213 | # @param [Array] the method parameters 214 | # 215 | getParameters: -> @parameters 216 | -------------------------------------------------------------------------------- /spec/metadata_spec.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | {inspect} = require 'util' 5 | walkdir = require 'walkdir' 6 | Donna = require '../src/donna' 7 | Parser = require '../src/parser' 8 | Metadata = require '../src/metadata' 9 | 10 | _ = require 'underscore' 11 | 12 | CoffeeScript = require 'coffee-script' 13 | 14 | require 'jasmine-focused' 15 | require 'jasmine-json' 16 | 17 | describe "Metadata", -> 18 | parser = null 19 | 20 | constructDelta = (filename, hasReferences = false) -> 21 | generated = Donna.generateMetadata([filename])[0] 22 | delete generated.version 23 | delete generated.repository 24 | delete generated.main 25 | 26 | expected_filename = filename.replace(/\.coffee$/, '.json') 27 | expected = JSON.parse(fs.readFileSync(expected_filename, 'utf8')) 28 | expect(generated).toEqualJson(expected) 29 | 30 | beforeEach -> 31 | parser = new Parser({ 32 | inputs: [] 33 | output: '' 34 | extras: [] 35 | readme: '' 36 | title: '' 37 | quiet: false 38 | private: true 39 | verbose: true 40 | metadata: true 41 | github: '' 42 | }) 43 | 44 | describe "Classes", -> 45 | it 'understands descriptions', -> 46 | constructDelta("spec/metadata_templates/classes/basic_class.coffee") 47 | 48 | it 'understands subclassing', -> 49 | constructDelta("spec/metadata_templates/classes/class_with_super_class.coffee") 50 | 51 | it 'understands class properties', -> 52 | constructDelta("spec/metadata_templates/classes/class_with_class_properties.coffee") 53 | 54 | it 'understands prototype properties', -> 55 | constructDelta("spec/metadata_templates/classes/class_with_prototype_properties.coffee") 56 | 57 | it 'understands documented prototype properties', -> 58 | str = """ 59 | class TextBuffer 60 | # Public: some property 61 | prop2: "bar" 62 | """ 63 | metadata = TestGenerator.generateMetadata(str)[0] 64 | expect(metadata.files.fakefile.objects['2']['9']).toEqualJson 65 | "name": "prop2", 66 | "type": "primitive", 67 | "doc": "Public: some property ", 68 | "range": [[2, 9], [2, 13]], 69 | "bindingType": "prototypeProperty" 70 | 71 | it 'understands documented class properties', -> 72 | str = """ 73 | class TextBuffer 74 | # Public: some class property 75 | @classProp2: "bar" 76 | """ 77 | metadata = TestGenerator.generateMetadata(str)[0] 78 | expect(metadata.files.fakefile.objects['2']['15']).toEqualJson 79 | "name": "classProp2", 80 | "type": "primitive", 81 | "doc": "Public: some class property ", 82 | "range": [[2, 15], [2, 19]], 83 | "bindingType": "classProperty" 84 | 85 | it 'outputs methods with reserved words', -> 86 | str = """ 87 | class TextBuffer 88 | # Public: deletes things 89 | delete: -> 90 | """ 91 | metadata = TestGenerator.generateMetadata(str)[0] 92 | expect(metadata.files.fakefile.objects['2']['10']).toEqualJson 93 | "name": "delete", 94 | "type": "function", 95 | "doc": "Public: deletes things ", 96 | "paramNames": [] 97 | "range": [[2, 10], [2, 11]], 98 | "bindingType": "prototypeProperty" 99 | 100 | it 'understands comment sections properties', -> 101 | constructDelta("spec/metadata_templates/classes/class_with_comment_section.coffee") 102 | 103 | it 'selects the correct doc string for each function', -> 104 | constructDelta("spec/metadata_templates/classes/classes_with_similar_methods.coffee") 105 | 106 | it 'preserves comment indentation', -> 107 | constructDelta("spec/metadata_templates/classes/class_with_comment_indentation.coffee") 108 | 109 | describe "Exports", -> 110 | it 'understands basic exports', -> 111 | constructDelta("spec/metadata_templates/exports/basic_exports.coffee") 112 | 113 | it 'understands class exports', -> 114 | constructDelta("spec/metadata_templates/exports/class_exports.coffee") 115 | 116 | describe "Requires", -> 117 | it 'understands basic requires', -> 118 | constructDelta("spec/metadata_templates/requires/basic_requires.coffee") 119 | 120 | it 'understands requires of expressions', -> 121 | constructDelta("spec/metadata_templates/requires/requires_with_call_args.coffee") 122 | 123 | it 'does not error on requires with a call of the required module', -> 124 | constructDelta("spec/metadata_templates/requires/requires_with_call_of_required_module.coffee") 125 | 126 | it 'understands multiple requires on a single line', -> 127 | constructDelta("spec/metadata_templates/requires/multiple_requires_single_line.coffee") 128 | 129 | it 'understands requires with a colon', -> 130 | constructDelta("spec/metadata_templates/requires/requires_with_colon.coffee") 131 | 132 | it 'understands importing', -> 133 | constructDelta("spec/metadata_templates/requires/references/buffer-patch.coffee") 134 | 135 | it 'does not throw when reading constructed paths', -> 136 | str = """ 137 | Decoration = require path.join(atom.config.resourcePath, 'src', 'decoration') 138 | """ 139 | 140 | generateMetadata = -> 141 | TestGenerator.generateMetadata(str) 142 | 143 | expect(generateMetadata).not.toThrow() 144 | 145 | describe "Other expressions", -> 146 | it "does not blow up on top-level try/catch blocks", -> 147 | constructDelta("spec/metadata_templates/top_level_try_catch.coffee") 148 | 149 | it "does not blow up on array subscript assignments", -> 150 | constructDelta("spec/metadata_templates/subscript_assignments.coffee") 151 | 152 | describe "when metadata is generated from multiple packages", -> 153 | it 'each slug contains only those files in the respective packages', -> 154 | singleFile = "spec/metadata_templates/requires/multiple_requires_single_line.coffee" 155 | realPackagePath = path.join("spec", "metadata_templates", "test_package") 156 | 157 | metadata = Donna.generateMetadata([singleFile, realPackagePath]) 158 | 159 | expect(_.keys metadata[0].files).toEqual ['multiple_requires_single_line.coffee'] 160 | expect(_.keys metadata[1].files).not.toContain 'multiple_requires_single_line.coffee' 161 | 162 | describe "A real package", -> 163 | it "renders the package correctly", -> 164 | test_path = path.join("spec", "metadata_templates", "test_package") 165 | slug = Donna.generateMetadata([test_path])[0] 166 | 167 | expected_filename = path.join(test_path, 'test_metadata.json') 168 | expected = JSON.parse(fs.readFileSync(expected_filename, 'utf8')) 169 | 170 | expect(slug).toEqualJson expected 171 | expect(_.keys(slug.files)).not.toContain "./Gruntfile.coffee" 172 | expect(_.keys(slug.files)).not.toContain "./spec/text-buffer-spec.coffee" 173 | 174 | class TestGenerator 175 | @generateMetadata: (fileContents, options) -> 176 | parser = new TestGenerator 177 | parser.addFile(fileContents, options) 178 | parser.generateMetadata() 179 | 180 | constructor: -> 181 | @slugs = {} 182 | @parser = new Parser() 183 | 184 | generateMetadata: -> 185 | slugs = [] 186 | for k, slug of @slugs 187 | slugs.push(slug) 188 | slugs 189 | 190 | addFile: (fileContents, {filename, packageJson}={}) -> 191 | filename ?= 'fakefile' 192 | packageJson ?= {} 193 | 194 | slug = @slugs[packageJson.name ? 'default'] ?= 195 | files: {} 196 | 197 | @parser.parseContent(fileContents, filename) 198 | metadata = new Donna.Metadata(packageJson, @parser) 199 | metadata.generate(CoffeeScript.nodes(fileContents)) 200 | Donna.populateSlug(slug, filename, metadata) 201 | -------------------------------------------------------------------------------- /src/nodes/class.coffee: -------------------------------------------------------------------------------- 1 | Node = require './node' 2 | Method = require './method' 3 | VirtualMethod = require './virtual_method' 4 | Variable = require './variable' 5 | Property = require './property' 6 | Doc = require './doc' 7 | _ = require 'underscore' 8 | 9 | # Public: The Node representation of a CoffeeScript class. 10 | module.exports = class Class extends Node 11 | 12 | # Constructs a class. 13 | # 14 | # node - The class node (a {Object}) 15 | # fileName - The filename (a {String}) 16 | # options - The parser options (a {Object}) 17 | # comment - The comment node (a {Object}) 18 | constructor: (@node, @fileName, @lineMapping, @options, comment) -> 19 | try 20 | @methods = [] 21 | @variables = [] 22 | @properties = [] 23 | 24 | @doc = new Doc(comment, @options) 25 | 26 | if @doc.methods 27 | @methods.push new VirtualMethod(@, method, @lineMapping, @options) for method in @doc?.methods 28 | 29 | previousExp = null 30 | 31 | for exp in @node.body.expressions 32 | switch exp.constructor.name 33 | 34 | when 'Assign' 35 | doc = previousExp if previousExp?.constructor.name is 'Comment' 36 | doc or= swallowedDoc 37 | 38 | switch exp.value?.constructor.name 39 | when 'Code' 40 | @methods.push(new Method(@, exp, @lineMapping, @options, doc)) if exp.variable.base.value is 'this' 41 | when 'Value' 42 | @variables.push new Variable(@, exp, @lineMapping, @options, true, doc) 43 | 44 | doc = null 45 | 46 | when 'Value' 47 | previousProp = null 48 | 49 | for prop in exp.base.properties 50 | doc = previousProp if previousProp?.constructor.name is 'Comment' 51 | doc or= swallowedDoc 52 | 53 | switch prop.value?.constructor.name 54 | when 'Code' 55 | @methods.push new Method(@, prop, @lineMapping, @options, doc) 56 | when 'Value' 57 | variable = new Variable(@, prop, @lineMapping, @options, false, doc) 58 | 59 | if variable.doc?.property 60 | property = new Property(@, prop, @lineMapping, @options, variable.getName(), doc) 61 | property.setter = true 62 | property.getter = true 63 | @properties.push property 64 | else 65 | @variables.push variable 66 | 67 | doc = null 68 | previousProp = prop 69 | 70 | when 'Call' 71 | doc = previousExp if previousExp?.constructor.name is 'Comment' 72 | doc or= swallowedDoc 73 | 74 | type = exp.variable?.base?.value 75 | name = exp.args?[0]?.base?.properties?[0]?.variable?.base?.value 76 | 77 | # This is a workaround for a strange CoffeeScript bug: 78 | # Given the following snippet: 79 | # 80 | # class Test 81 | # # Doc a 82 | # set name: -> 83 | # 84 | # # Doc B 85 | # set another: -> 86 | # 87 | # This will be converted to: 88 | # 89 | # class Test 90 | # ### 91 | # Doc A 92 | # ### 93 | # set name: -> 94 | # 95 | # ### 96 | # Doc B 97 | # ### 98 | # set another: -> 99 | # 100 | # BUT, Doc B is now a sibling property of the previous `set name: ->` setter! 101 | # 102 | swallowedDoc = exp.args?[0]?.base?.properties?[1] 103 | 104 | if name && (type is 'set' or type is 'get') 105 | property = _.find(@properties, (p) -> p.name is name) 106 | 107 | unless property 108 | property = new Property(@, exp, @smc, @options, name, doc) 109 | @properties.push property 110 | 111 | property.setter = true if type is 'set' 112 | property.getter = true if type is 'get' 113 | 114 | doc = null 115 | 116 | previousExp = exp 117 | 118 | catch error 119 | console.warn('Create class error:', @node, error) if @options.verbose 120 | 121 | # Public: Get the source file name. 122 | # 123 | # Returns the filename of the class (a {String}) 124 | getFileName: -> @fileName 125 | 126 | # Public: Get the class doc 127 | # 128 | # Returns the class doc (a [Doc]) 129 | getDoc: -> @doc 130 | 131 | # Public: Alias for {::getClassName} 132 | # 133 | # Returns the full class name (a {String}) 134 | getFullName: -> 135 | @getClassName() 136 | 137 | # Public: Get the full class name 138 | # 139 | # Returns the class (a {String}) 140 | getClassName: -> 141 | try 142 | unless @className || !@node.variable 143 | @className = @node.variable.base.value 144 | 145 | # Inner class definition inherits 146 | # the namespace from the outer class 147 | if @className is 'this' 148 | outer = @findAncestor('Class') 149 | 150 | if outer 151 | @className = outer.variable.base.value 152 | for prop in outer.variable.properties 153 | @className += ".#{ prop.name.value }" 154 | 155 | else 156 | @className = '' 157 | 158 | for prop in @node.variable.properties 159 | @className += ".#{ prop.name.value }" 160 | 161 | @className 162 | 163 | catch error 164 | console.warn("Get class classname error at #{@fileName}:", @node, error) if @options.verbose 165 | 166 | # Public: Get the class name 167 | # 168 | # Returns the name (a {String}) 169 | getName: -> 170 | try 171 | unless @name 172 | @name = @getClassName().split('.').pop() 173 | 174 | @name 175 | 176 | catch error 177 | console.warn("Get class name error at #{@fileName}:", @node, error) if @options.verbose 178 | 179 | # Public: Get the source line number 180 | # 181 | # Returns a {Number}. 182 | getLocation: -> 183 | try 184 | unless @location 185 | {locationData} = @node.variable 186 | firstLine = locationData.first_line + 1 187 | if !@lineMapping[firstLine]? 188 | @lineMapping[firstLine] = @lineMapping[firstLine - 1] 189 | 190 | @location = { line: @lineMapping[firstLine] } 191 | @location 192 | 193 | catch error 194 | console.warn("Get location error at #{@fileName}:", @node, error) if @options.verbose 195 | 196 | # Public: Get the class namespace 197 | # 198 | # Returns the namespace (a {String}). 199 | getNamespace: -> 200 | try 201 | unless @namespace 202 | @namespace = @getClassName().split('.') 203 | @namespace.pop() 204 | 205 | @namespace = @namespace.join('.') 206 | 207 | @namespace 208 | 209 | catch error 210 | console.warn("Get class namespace error at #{@fileName}:", @node, error) if @options.verbose 211 | 212 | # Public: Get the full parent class name 213 | # 214 | # Returns the parent class name (a {String}). 215 | getParentClassName: -> 216 | try 217 | unless @parentClassName 218 | if @node.parent 219 | @parentClassName = @node.parent.base.value 220 | 221 | # Inner class parent inherits 222 | # the namespace from the outer class parent 223 | if @parentClassName is 'this' 224 | outer = @findAncestor('Class') 225 | 226 | if outer 227 | @parentClassName = outer.parent.base.value 228 | for prop in outer.parent.properties 229 | @parentClassName += ".#{ prop.name.value }" 230 | 231 | else 232 | @parentClassName = '' 233 | 234 | for prop in @node.parent.properties 235 | @parentClassName += ".#{ prop.name.value }" 236 | 237 | @parentClassName 238 | 239 | catch error 240 | console.warn("Get class parent classname error at #{@fileName}:", @node, error) if @options.verbose 241 | 242 | # Public: Get all methods. 243 | # 244 | # Returns the methods as an {Array}. 245 | getMethods: -> @methods 246 | 247 | # Public: Get all variables. 248 | # 249 | # Returns the variables as an {Array}. 250 | getVariables: -> @variables 251 | -------------------------------------------------------------------------------- /src/parser.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | _.str = require 'underscore.string' 5 | CoffeeScript = require 'coffee-script' 6 | 7 | File = require './nodes/file' 8 | Class = require './nodes/class' 9 | Mixin = require './nodes/mixin' 10 | VirtualMethod = require './nodes/virtual_method' 11 | 12 | {SourceMapConsumer} = require 'source-map' 13 | 14 | # FIXME: The only reason we use the parser right now is for comment conversion. 15 | # We need to convert the comments to block comments so they show up in the AST. 16 | # This could be done by the {Metadata} class, but just isnt at this point. 17 | 18 | # Public: This parser is responsible for converting each file into the intermediate / 19 | # AST representation as a JSON node. 20 | module.exports = class Parser 21 | 22 | # Public: Construct the parser 23 | # 24 | # options - An {Object} of options 25 | constructor: (@options={}) -> 26 | @files = [] 27 | @classes = [] 28 | @mixins = [] 29 | @iteratedFiles = {} 30 | @fileCount = 0 31 | @globalStatus = "Private" 32 | 33 | # Public: Parse the given CoffeeScript file. 34 | # 35 | # filePath - {String} absolute path name 36 | parseFile: (filePath, relativeTo) -> 37 | content = fs.readFileSync(filePath, 'utf8') 38 | relativePath = path.normalize(filePath.replace(relativeTo, ".#{path.sep}")) 39 | @parseContent(content, relativePath) 40 | @iteratedFiles[relativePath] = content 41 | @fileCount += 1 42 | 43 | # Public: Parse the given CoffeeScript content. 44 | # 45 | # content - A {String} representing the CoffeeScript file content 46 | # file - A {String} representing the CoffeeScript file name 47 | # 48 | parseContent: (@content, file='') -> 49 | @previousNodes = [] 50 | @globalStatus = "Private" 51 | 52 | # Defines typical conditions for entities we are looking through nodes 53 | entities = 54 | clazz: (node) -> node.constructor.name is 'Class' && node.variable?.base?.value? 55 | mixin: (node) -> node.constructor.name == 'Assign' && node.value?.base?.properties? 56 | 57 | [convertedContent, lineMapping] = @convertComments(@content) 58 | 59 | sourceMap = CoffeeScript.compile(convertedContent, {sourceMap: true}).v3SourceMap 60 | @smc = new SourceMapConsumer(sourceMap) 61 | 62 | try 63 | root = CoffeeScript.nodes(convertedContent) 64 | catch error 65 | console.log('Parsed CoffeeScript source:\n%s', convertedContent) if @options.debug 66 | throw error 67 | 68 | # Find top-level methods and constants that aren't within a class 69 | fileClass = new File(root, file, lineMapping, @options) 70 | @files.push(fileClass) 71 | 72 | @linkAncestors root 73 | 74 | root.traverseChildren true, (child) => 75 | entity = false 76 | 77 | for type, condition of entities 78 | if entities.hasOwnProperty(type) 79 | entity = type if condition(child) 80 | 81 | if entity 82 | 83 | # Check the previous tokens for comment nodes 84 | previous = @previousNodes[@previousNodes.length-1] 85 | switch previous?.constructor.name 86 | # A comment is preceding the class declaration 87 | when 'Comment' 88 | doc = previous 89 | when 'Literal' 90 | # The class is exported `module.exports = class Class`, take the comment before `module` 91 | if previous.value is 'exports' 92 | node = @previousNodes[@previousNodes.length-6] 93 | doc = node if node?.constructor.name is 'Comment' 94 | 95 | if entity == 'mixin' 96 | name = [child.variable.base.value] 97 | 98 | # If p.name is empty value is going to be assigned to index... 99 | name.push p.name?.value for p in child.variable.properties 100 | 101 | # ... and therefore should be just skipped. 102 | if name.indexOf(undefined) == -1 103 | mixin = new Mixin(child, file, @options, doc) 104 | 105 | if mixin.doc.mixin? && (@options.private || !mixin.doc.private) 106 | @mixins.push mixin 107 | 108 | if entity == 'clazz' 109 | clazz = new Class(child, file, lineMapping, @options, doc) 110 | @classes.push clazz 111 | 112 | @previousNodes.push child 113 | true 114 | 115 | root 116 | 117 | # Public: Converts the comments to block comments, so they appear in the node structure. 118 | # Only block comments are considered by Donna. 119 | # 120 | # content - A {String} representing the CoffeeScript file content 121 | convertComments: (content) -> 122 | result = [] 123 | comment = [] 124 | inComment = false 125 | inBlockComment = false 126 | indentComment = 0 127 | globalCount = 0 128 | lineMapping = {} 129 | 130 | for line, l in content.split('\n') 131 | globalStatusBlock = false 132 | 133 | # key: the translated line number; value: the original number 134 | lineMapping[(l + 1) + globalCount] = l + 1 135 | 136 | if globalStatusBlock = /^\s*#{3} (\w+).+?#{3}/.exec(line) 137 | result.push '' 138 | @globalStatus = globalStatusBlock[1] 139 | 140 | blockComment = /^\s*#{3,}/.exec(line) && !/^\s*#{3,}.+#{3,}/.exec(line) 141 | 142 | if blockComment || inBlockComment 143 | inBlockComment = !inBlockComment if blockComment 144 | result.push line 145 | else 146 | commentLine = /^(\s*#)\s?(\s*.*)/.exec(line) 147 | if commentLine 148 | commentText = commentLine[2]?.replace(/#/g, "\u0091#") 149 | unless inComment 150 | # append current global status flag if needed 151 | if !/^\s*\w+:/.test(commentText) 152 | commentText = @globalStatus + ": " + commentText 153 | inComment = true 154 | indentComment = commentLine[1].length - 1 155 | commentText = "### #{commentText}" 156 | 157 | comment.push whitespace(indentComment) + commentText 158 | 159 | else 160 | if inComment 161 | inComment = false 162 | lastComment = _.last(comment) 163 | 164 | # slight fix for an empty line as the last item 165 | if _.str.isBlank(lastComment) 166 | globalCount++ 167 | comment[comment.length] = lastComment + ' ###' 168 | else 169 | comment[comment.length - 1] = lastComment + ' ###' 170 | 171 | # Push here comments only before certain lines 172 | if /// 173 | ( # Class 174 | class\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]* 175 | | # Mixin or assignment 176 | ^\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff.]*\s+\= 177 | | # Function 178 | [$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*\s*:\s*(\(.*\)\s*)?[-=]> 179 | | # Function 180 | @[A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*\s*=\s*(\(.*\)\s*)?[-=]> 181 | | # Function 182 | [$A-Za-z_\x7f-\uffff][\.$\w\x7f-\uffff]*\s*=\s*(\(.*\)\s*)?[-=]> 183 | | # Constant 184 | ^\s*@[$A-Z_][A-Z_]*) 185 | | # Properties 186 | ^\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*:\s*\S+ 187 | ///.exec line 188 | 189 | result.push c for c in comment 190 | comment = [] 191 | # A member with no preceding description; apply the global status 192 | member = /// 193 | ( # Class 194 | class\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]* 195 | | # Mixin or assignment 196 | ^\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff.]*\s+\= 197 | | # Function 198 | [$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*\s*:\s*(\(.*\)\s*)?[-=]> 199 | | # Function 200 | @[A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*\s*=\s*(\(.*\)\s*)?[-=]> 201 | | # Function 202 | [$A-Za-z_\x7f-\uffff][\.$\w\x7f-\uffff]*\s*=\s*(\(.*\)\s*)?[-=]> 203 | | # Constant 204 | ^\s*@[$A-Z_][A-Z_]*) 205 | | # Properties 206 | ^\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*:\s*\S+ 207 | ///.exec line 208 | 209 | if member and _.str.isBlank(_.last(result)) 210 | indentComment = /^(\s*)/.exec(line) 211 | if indentComment 212 | indentComment = indentComment[1] 213 | else 214 | indentComment = "" 215 | 216 | globalCount++ 217 | 218 | result.push line 219 | 220 | [result.join('\n'), lineMapping] 221 | 222 | # Public: Attach each parent to its children, so we are able 223 | # to traverse the ancestor parse tree. Since the 224 | # parent attribute is already used in the class node, 225 | # the parent is stored as `ancestor`. 226 | # 227 | # nodes - A {Base} representing the CoffeeScript nodes 228 | # 229 | linkAncestors: (node) -> 230 | node.eachChild (child) => 231 | child.ancestor = node 232 | @linkAncestors child 233 | 234 | whitespace = (n) -> 235 | a = [] 236 | while a.length < n 237 | a.push ' ' 238 | a.join '' 239 | -------------------------------------------------------------------------------- /src/metadata.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | _ = require 'underscore' 5 | builtins = require 'builtins' 6 | 7 | module.exports = class Metadata 8 | constructor: (@dependencies, @parser) -> 9 | 10 | generate: (@root) -> 11 | @defs = {} # Local variable definitions 12 | @exports = {} 13 | @bindingTypes = {} 14 | @modules = {} 15 | @classes = @parser.classes 16 | @files = @parser.files 17 | 18 | @root.traverseChildren no, (exp) => @visit(exp) # `no` means Stop at scope boundaries 19 | 20 | visit: (exp) -> 21 | @["visit#{exp.constructor.name}"]?(exp) 22 | eval: (exp) -> 23 | @["eval#{exp.constructor.name}"](exp) 24 | 25 | visitComment: (exp) -> 26 | # Skip the 1st comment which is added by donna 27 | return if exp.comment is '~Private~' 28 | 29 | visitClass: (exp) -> 30 | return unless exp.variable? 31 | @defs[exp.variable.base.value] = @evalClass(exp) 32 | no # Do not traverse into the class methods 33 | 34 | visitAssign: (exp) -> 35 | variable = @eval(exp.variable) 36 | value = @eval(exp.value) 37 | 38 | baseName = exp.variable.base.value 39 | switch baseName 40 | when 'module' 41 | return if exp.variable.properties.length is 0 # Ignore `module = ...` (atom/src/browser/main.coffee) 42 | unless exp.variable.properties?[0]?.name?.value is 'exports' 43 | throw new Error 'BUG: Does not support module.somthingOtherThanExports' 44 | baseName = 'exports' 45 | firstProp = exp.variable.properties[1] 46 | when 'exports' 47 | firstProp = exp.variable.properties[0] 48 | 49 | switch baseName 50 | when 'exports' 51 | # Handle 3 cases: 52 | # 53 | # - `exports.foo = SomeClass` 54 | # - `exports.foo = 42` 55 | # - `exports = bar` 56 | if firstProp 57 | if value.base? && @defs[value.base.value] 58 | # case `exports.foo = SomeClass` 59 | @exports[firstProp.name.value] = @defs[value.base.value] 60 | else 61 | # case `exports.foo = 42` 62 | unless firstProp.name.value == value.name 63 | @defs[firstProp.name.value] = 64 | name: firstProp.name.value 65 | bindingType: 'exportsProperty' 66 | type: value.type 67 | range: [ [exp.variable.base.locationData.first_line, exp.variable.base.locationData.first_column], [exp.variable.base.locationData.last_line, exp.variable.base.locationData.last_column ] ] 68 | @exports[firstProp.name.value] = 69 | startLineNumber: exp.variable.base.locationData.first_line 70 | else 71 | # case `exports = bar` 72 | @exports = {_default: value} 73 | switch value.type 74 | when 'class' 75 | @bindingTypes[value.name] = "exports" 76 | 77 | # case left-hand-side is anything other than `exports...` 78 | else 79 | # Handle 5 common cases: 80 | # 81 | # X = ... 82 | # {X} = ... 83 | # {X:Y} = ... 84 | # X.y = ... 85 | # [X] = ... 86 | switch exp.variable.base.constructor.name 87 | when 'Literal' 88 | # Something we dont care about is on the right side of the `=`. 89 | # This could be some garbage like an if statement. 90 | return unless value?.range 91 | 92 | # case _.str = ... 93 | if exp.variable.properties.length > 0 94 | keyPath = exp.variable.base.value 95 | for prop in exp.variable.properties 96 | if prop.name? 97 | keyPath += ".#{prop.name.value}" 98 | else 99 | keyPath += "[#{prop.index.base.value}]" 100 | @defs[keyPath] = _.extend name: keyPath, value 101 | else # case X = ... 102 | @defs[exp.variable.base.value] = _.extend name: exp.variable.base.value, value 103 | 104 | # satisfies the case of npm module requires (like Grim in our tests) 105 | if @defs[exp.variable.base.value].type == "import" 106 | key = @defs[exp.variable.base.value].path || @defs[exp.variable.base.value].module 107 | if _.isUndefined @modules[key] 108 | @modules[key] = [] 109 | 110 | @modules[key].push { name: @defs[exp.variable.base.value].name, range: @defs[exp.variable.base.value].range } 111 | 112 | switch @defs[exp.variable.base.value].type 113 | when 'function' 114 | # FIXME: Ugh. This is so fucked. We shouldnt match on name in all the files in the entire project. 115 | for file in @files 116 | for method in file.methods 117 | if @defs[exp.variable.base.value].name == method.name 118 | @defs[exp.variable.base.value].doc = method.doc.comment 119 | break 120 | 121 | when 'Obj', 'Arr' 122 | for key in exp.variable.base.objects 123 | switch key.constructor.name 124 | when 'Value' 125 | # case {X} = ... 126 | @defs[key.base.value] = _.extend {}, value, 127 | name: key.base.value 128 | exportsProperty: key.base.value 129 | range: [ [key.base.locationData.first_line, key.base.locationData.first_column], [key.base.locationData.last_line, key.base.locationData.last_column ] ] 130 | 131 | # Store the name of the exported property to the module name 132 | if @defs[key.base.value].type == "import" # I *think* this will always be true 133 | if _.isUndefined @modules[@defs[key.base.value].path] 134 | @modules[@defs[key.base.value].path] = [] 135 | @modules[@defs[key.base.value].path].push {name: @defs[key.base.value].name, range: @defs[key.base.value].range} 136 | when 'Assign' 137 | # case {X:Y} = ... 138 | @defs[key.value.base.value] = _.extend {}, value, 139 | name: key.value.base.value 140 | exportsProperty: key.variable.base.value 141 | return no # Do not continue visiting X 142 | 143 | else throw new Error "BUG: Unsupported require Obj structure: #{key.constructor.name}" 144 | else throw new Error "BUG: Unsupported require structure: #{exp.variable.base.constructor.name}" 145 | 146 | visitCode: (exp) -> 147 | 148 | visitValue: (exp) -> 149 | 150 | visitCall: (exp) -> 151 | 152 | visitLiteral: (exp) -> 153 | 154 | visitObj: (exp) -> 155 | 156 | visitAccess: (exp) -> 157 | 158 | visitBlock: (exp) -> 159 | 160 | visitTry: (exp) -> 161 | 162 | visitIn: (exp) -> 163 | 164 | visitExistence: (exp) -> 165 | 166 | evalComment: (exp) -> 167 | type: 'comment' 168 | doc: exp.comment 169 | range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ] 170 | 171 | evalClass: (exp) -> 172 | className = exp.variable.base.value 173 | superClassName = exp.parent?.base.value 174 | classProperties = [] 175 | prototypeProperties = [] 176 | 177 | classNode = _.find(@classes, (clazz) -> clazz.getFullName() == className) 178 | 179 | for subExp in exp.body.expressions 180 | switch subExp.constructor.name 181 | # case Prototype-level methods (this.foo = (foo) -> ...) 182 | when 'Assign' 183 | value = @eval(subExp.value) 184 | @defs["#{className}.#{value.name}"] = value 185 | classProperties.push(value) 186 | when 'Value' 187 | # case Prototype-level properties (@foo: "foo") 188 | for prototypeExp in subExp.base.properties 189 | switch prototypeExp.constructor.name 190 | when 'Comment' 191 | value = @eval(prototypeExp) 192 | @defs["#{value.range[0][0]}_line_comment"] = value 193 | else 194 | isClassLevel = prototypeExp.variable.this 195 | 196 | if isClassLevel 197 | name = prototypeExp.variable.properties[0].name.value 198 | else 199 | name = prototypeExp.variable.base.value 200 | 201 | # The reserved words are a string with a property: {reserved: true} 202 | # We dont care about the reserved-ness in the name. It is 203 | # detrimental as comparisons fail. 204 | name = name.slice(0) if name.reserved 205 | 206 | value = @eval(prototypeExp.value) 207 | 208 | if value.constructor?.name is 'Value' 209 | lookedUpVar = @defs[value.base.value] 210 | if lookedUpVar 211 | if lookedUpVar.type is 'import' 212 | value = 213 | name: name 214 | range: [ [value.locationData.first_line, value.locationData.first_column], [value.locationData.last_line, value.locationData.last_column ] ] 215 | reference: lookedUpVar 216 | else 217 | value = _.extend name: name, lookedUpVar 218 | 219 | else 220 | # Assigning a simple var 221 | value = 222 | type: 'primitive' 223 | name: name 224 | range: [ [value.locationData.first_line, value.locationData.first_column], [value.locationData.last_line, value.locationData.last_column ] ] 225 | 226 | else 227 | value = _.extend name: name, value 228 | 229 | # TODO: `value = @eval(prototypeExp.value)` is messing this up 230 | # interferes also with evalValue 231 | if isClassLevel 232 | value.name = name 233 | value.bindingType = "classProperty" 234 | @defs["#{className}.#{name}"] = value 235 | classProperties.push(value) 236 | 237 | if reference = @applyReference(prototypeExp) 238 | @defs["#{className}.#{name}"].reference = 239 | position: reference.range[0] 240 | else 241 | value.name = name 242 | value.bindingType = "prototypeProperty" 243 | @defs["#{className}::#{name}"] = value 244 | prototypeProperties.push(value) 245 | 246 | if reference = @applyReference(prototypeExp) 247 | @defs["#{className}::#{name}"].reference = 248 | position: reference.range[0] 249 | 250 | # apply the reference (if one exists) 251 | if value.type is "primitive" 252 | variable = _.find classNode?.getVariables(), (variable) -> variable.name == value.name 253 | value.doc = variable?.doc.comment 254 | else if value.type is "function" 255 | # find the matching method from the parsed files 256 | func = _.find classNode?.getMethods(), (method) -> method.name == value.name 257 | value.doc = func?.doc.comment 258 | true 259 | 260 | type: 'class' 261 | name: className 262 | superClass: superClassName 263 | bindingType: @bindingTypes[className] unless _.isUndefined @bindingTypes[className] 264 | classProperties: classProperties 265 | prototypeProperties: prototypeProperties 266 | doc: classNode?.doc.comment 267 | range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ] 268 | 269 | evalCode: (exp) -> 270 | bindingType: 'variable' 271 | type: 'function' 272 | paramNames: _.map(exp.params, ((param) -> param.name.value)) 273 | range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ] 274 | doc: null 275 | 276 | evalValue: (exp) -> 277 | if exp.base 278 | type: 'primitive' 279 | name: exp.base?.value 280 | range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ] 281 | else 282 | throw new Error 'BUG? Not sure how to evaluate this value if it does not have .base' 283 | 284 | evalCall: (exp) -> 285 | # The only interesting call is `require('foo')` 286 | if exp.variable.base?.value is 'require' 287 | return unless exp.args[0].base? 288 | 289 | return unless moduleName = exp.args[0].base?.value 290 | moduleName = moduleName.substring(1, moduleName.length - 1) 291 | 292 | # For npm modules include the version number 293 | ver = @dependencies[moduleName] 294 | moduleName = "#{moduleName}@#{ver}" if ver 295 | 296 | ret = 297 | type: 'import' 298 | range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ] 299 | bindingType: 'variable' 300 | 301 | if /^\./.test(moduleName) 302 | # Local module 303 | ret.path = moduleName 304 | else 305 | ret.module = moduleName 306 | # Tag builtin NodeJS modules 307 | ret.builtin = true if builtins.indexOf(moduleName) >= 0 308 | 309 | ret 310 | 311 | else 312 | type: 'function' 313 | range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ] 314 | 315 | evalError: (str, exp) -> 316 | throw new Error "BUG: Not implemented yet: #{str}. Line #{exp.locationData.first_line}" 317 | 318 | evalAssign: (exp) -> @eval(exp.value) # Support x = y = z 319 | 320 | evalLiteral: (exp) -> @evalError 'evalLiteral', exp 321 | 322 | evalObj: (exp) -> @evalError 'evalObj', exp 323 | 324 | evalAccess: (exp) -> @evalError 'evalAccess', exp 325 | 326 | evalUnknown: (exp) -> exp 327 | evalIf: -> @evalUnknown(arguments) 328 | visitIf: -> 329 | visitFor: -> 330 | visitParam: -> 331 | visitOp: -> 332 | visitArr: -> 333 | visitNull: -> 334 | visitBool: -> 335 | visitIndex: -> 336 | visitParens: -> 337 | visitReturn: -> 338 | visitUndefined: -> 339 | 340 | evalOp: (exp) -> exp 341 | 342 | applyReference: (prototypeExp) -> 343 | for module, references of @modules 344 | for reference in references 345 | # non-npm module case (local file ref) 346 | if prototypeExp.value.base?.value 347 | ref = prototypeExp.value.base.value 348 | else 349 | ref = prototypeExp.value.base 350 | 351 | if reference.name == ref 352 | return reference 353 | -------------------------------------------------------------------------------- /spec/metadata_templates/test_package/test_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./src/test.coffee", 3 | "version": "1.2.3", 4 | "repository": "git://github.com/atom/zomgwowcats.git", 5 | "files": { 6 | "src/point.coffee": { 7 | "objects": { 8 | "11": { 9 | "0": { 10 | "type": "class", 11 | "name": "Point", 12 | "superClass": null, 13 | "bindingType": "exports", 14 | "classProperties": [ 15 | [ 16 | 24, 17 | 15 18 | ], 19 | [ 20 | 36, 21 | 8 22 | ] 23 | ], 24 | "prototypeProperties": [ 25 | [ 26 | 44, 27 | 15 28 | ], 29 | [ 30 | 47, 31 | 8 32 | ], 33 | [ 34 | 51, 35 | 10 36 | ], 37 | [ 38 | 56, 39 | 13 40 | ], 41 | [ 42 | 60, 43 | 7 44 | ], 45 | [ 46 | 70, 47 | 11 48 | ], 49 | [ 50 | 86, 51 | 11 52 | ] 53 | ], 54 | "doc": " Public: Represents a point in a buffer in row/column coordinates.\n\nEvery public method that takes a point also accepts a *point-compatible*\n{Array}. This means a 2-element array containing {Number}s representing the\nrow and column. So the following are equivalent:\n\n```coffee\nnew Point(1, 2)\n[1, 2]\n``` ", 55 | "range": [ 56 | [ 57 | 11, 58 | 0 59 | ], 60 | [ 61 | 97, 62 | 9 63 | ] 64 | ] 65 | } 66 | }, 67 | "24": { 68 | "15": { 69 | "name": "fromObject", 70 | "bindingType": "classProperty", 71 | "type": "function", 72 | "paramNames": [ 73 | "object", 74 | "copy" 75 | ], 76 | "range": [ 77 | [ 78 | 24, 79 | 15 80 | ], 81 | [ 82 | 36, 83 | 1 84 | ] 85 | ], 86 | "doc": " Public: Convert any point-compatible object to a {Point}.\n\n* object:\n This can be an object that's already a {Point}, in which case it's\n simply returned, or an array containing two {Number}s representing the\n row and column.\n\n* copy:\n An optional boolean indicating whether to force the copying of objects\n that are already points.\n\nReturns: A {Point} based on the given object. " 87 | } 88 | }, 89 | "36": { 90 | "8": { 91 | "name": "min", 92 | "bindingType": "classProperty", 93 | "type": "function", 94 | "paramNames": [ 95 | "point1", 96 | "point2" 97 | ], 98 | "range": [ 99 | [ 100 | 36, 101 | 8 102 | ], 103 | [ 104 | 44, 105 | 1 106 | ] 107 | ], 108 | "doc": "Public: Returns the given point that is earlier in the buffer. " 109 | } 110 | }, 111 | "44": { 112 | "15": { 113 | "name": "constructor", 114 | "bindingType": "prototypeProperty", 115 | "type": "function", 116 | "paramNames": [ 117 | null, 118 | null 119 | ], 120 | "range": [ 121 | [ 122 | 44, 123 | 15 124 | ], 125 | [ 126 | 44, 127 | 36 128 | ] 129 | ], 130 | "doc": null 131 | } 132 | }, 133 | "47": { 134 | "8": { 135 | "name": "copy", 136 | "bindingType": "prototypeProperty", 137 | "type": "function", 138 | "paramNames": [], 139 | "range": [ 140 | [ 141 | 47, 142 | 8 143 | ], 144 | [ 145 | 51, 146 | 1 147 | ] 148 | ], 149 | "doc": "Public: Returns a new {Point} with the same row and column. " 150 | } 151 | }, 152 | "51": { 153 | "10": { 154 | "name": "freeze", 155 | "bindingType": "prototypeProperty", 156 | "type": "function", 157 | "paramNames": [], 158 | "range": [ 159 | [ 160 | 51, 161 | 10 162 | ], 163 | [ 164 | 56, 165 | 1 166 | ] 167 | ], 168 | "doc": "Public: Makes this point immutable and returns itself. " 169 | } 170 | }, 171 | "56": { 172 | "13": { 173 | "name": "translate", 174 | "bindingType": "prototypeProperty", 175 | "type": "function", 176 | "paramNames": [ 177 | "delta" 178 | ], 179 | "range": [ 180 | [ 181 | 56, 182 | 13 183 | ], 184 | [ 185 | 60, 186 | 1 187 | ] 188 | ], 189 | "doc": " Public: Return a new {Point} based on shifting this point by the given delta,\nwhich is represented by another {Point}. " 190 | } 191 | }, 192 | "60": { 193 | "7": { 194 | "name": "add", 195 | "bindingType": "prototypeProperty", 196 | "type": "function", 197 | "paramNames": [ 198 | "other" 199 | ], 200 | "range": [ 201 | [ 202 | 60, 203 | 7 204 | ], 205 | [ 206 | 70, 207 | 1 208 | ] 209 | ], 210 | "doc": null 211 | } 212 | }, 213 | "70": { 214 | "11": { 215 | "name": "splitAt", 216 | "bindingType": "prototypeProperty", 217 | "type": "function", 218 | "paramNames": [ 219 | "column" 220 | ], 221 | "range": [ 222 | [ 223 | 70, 224 | 11 225 | ], 226 | [ 227 | 86, 228 | 1 229 | ] 230 | ], 231 | "doc": null 232 | } 233 | }, 234 | "86": { 235 | "11": { 236 | "name": "compare", 237 | "bindingType": "prototypeProperty", 238 | "type": "function", 239 | "paramNames": [ 240 | "other" 241 | ], 242 | "range": [ 243 | [ 244 | 86, 245 | 11 246 | ], 247 | [ 248 | 97, 249 | 9 250 | ] 251 | ], 252 | "doc": " Public:\n\n* other: A {Point} or point-compatible {Array}.\n\nReturns:\n * -1 if this point precedes the argument.\n * 0 if this point is equivalent to the argument.\n * 1 if this point follows the argument. " 253 | } 254 | } 255 | }, 256 | "exports": 11 257 | }, 258 | "src/range.coffee": { 259 | "objects": { 260 | "0": { 261 | "7": { 262 | "name": "Grim", 263 | "type": "import", 264 | "range": [ 265 | [ 266 | 0, 267 | 7 268 | ], 269 | [ 270 | 0, 271 | 20 272 | ] 273 | ], 274 | "bindingType": "variable", 275 | "module": "grim@0.11.0" 276 | } 277 | }, 278 | "1": { 279 | "8": { 280 | "name": "Point", 281 | "type": "import", 282 | "range": [ 283 | [ 284 | 1, 285 | 8 286 | ], 287 | [ 288 | 1, 289 | 24 290 | ] 291 | ], 292 | "bindingType": "variable", 293 | "path": "./point" 294 | } 295 | }, 296 | "2": { 297 | "1": { 298 | "type": "import", 299 | "range": [ 300 | [ 301 | 2, 302 | 1 303 | ], 304 | [ 305 | 2, 306 | 12 307 | ] 308 | ], 309 | "bindingType": "variable", 310 | "path": "./helpers", 311 | "name": "newlineRegex", 312 | "exportsProperty": "newlineRegex" 313 | } 314 | }, 315 | "3": { 316 | "5": { 317 | "name": "Fs", 318 | "type": "import", 319 | "range": [ 320 | [ 321 | 3, 322 | 5 323 | ], 324 | [ 325 | 3, 326 | 16 327 | ] 328 | ], 329 | "bindingType": "variable", 330 | "module": "fs", 331 | "builtin": true 332 | } 333 | }, 334 | "17": { 335 | "0": { 336 | "type": "class", 337 | "name": "Range", 338 | "superClass": null, 339 | "bindingType": "exports", 340 | "classProperties": [ 341 | [ 342 | 21, 343 | 16 344 | ], 345 | [ 346 | 35, 347 | 15 348 | ], 349 | [ 350 | 55, 351 | 23 352 | ] 353 | ], 354 | "prototypeProperties": [ 355 | [ 356 | 18, 357 | 8 358 | ], 359 | [ 360 | 60, 361 | 15 362 | ], 363 | [ 364 | 73, 365 | 11 366 | ] 367 | ], 368 | "doc": " Public: Represents a region in a buffer in row/column coordinates.\n\nEvery public method that takes a range also accepts a *range-compatible*\n{Array}. This means a 2-element array containing {Point}s or point-compatible\narrays. So the following are equivalent:\n\n```coffee\nnew Range(new Point(0, 1), new Point(2, 3))\nnew Range([0, 1], [2, 3])\n[[0, 1], [2, 3]]\n``` ", 369 | "range": [ 370 | [ 371 | 17, 372 | 0 373 | ], 374 | [ 375 | 76, 376 | 59 377 | ] 378 | ] 379 | } 380 | }, 381 | "18": { 382 | "8": { 383 | "name": "grim", 384 | "type": "primitive", 385 | "range": [ 386 | [ 387 | 18, 388 | 8 389 | ], 390 | [ 391 | 18, 392 | 11 393 | ] 394 | ], 395 | "doc": null, 396 | "bindingType": "prototypeProperty", 397 | "reference": { 398 | "position": [ 399 | 0, 400 | 7 401 | ] 402 | } 403 | } 404 | }, 405 | "21": { 406 | "16": { 407 | "name": "deserialize", 408 | "bindingType": "classProperty", 409 | "type": "function", 410 | "paramNames": [ 411 | "array" 412 | ], 413 | "range": [ 414 | [ 415 | 21, 416 | 16 417 | ], 418 | [ 419 | 35, 420 | 1 421 | ] 422 | ], 423 | "doc": "Public: Call this with the result of {Range::serialize} to construct a new Range. " 424 | } 425 | }, 426 | "35": { 427 | "15": { 428 | "name": "fromObject", 429 | "bindingType": "classProperty", 430 | "type": "function", 431 | "paramNames": [ 432 | "object", 433 | "copy" 434 | ], 435 | "range": [ 436 | [ 437 | 35, 438 | 15 439 | ], 440 | [ 441 | 55, 442 | 1 443 | ] 444 | ], 445 | "doc": " Public: Convert any range-compatible object to a {Range}.\n\n* object:\n This can be an object that's already a {Range}, in which case it's\n simply returned, or an array containing two {Point}s or point-compatible\n arrays.\n* copy:\n An optional boolean indicating whether to force the copying of objects\n that are already ranges.\n\nReturns: A {Range} based on the given object. " 446 | } 447 | }, 448 | "55": { 449 | "23": { 450 | "name": "fromPointWithDelta", 451 | "bindingType": "classProperty", 452 | "type": "function", 453 | "paramNames": [ 454 | "startPoint", 455 | "rowDelta", 456 | "columnDelta" 457 | ], 458 | "range": [ 459 | [ 460 | 55, 461 | 23 462 | ], 463 | [ 464 | 60, 465 | 1 466 | ] 467 | ], 468 | "doc": " Public: Returns a {Range} that starts at the given point and ends at the\nstart point plus the given row and column deltas.\n\n* startPoint:\n A {Point} or point-compatible {Array}\n* rowDelta:\n A {Number} indicating how many rows to add to the start point to get the\n end point.\n* columnDelta:\n A {Number} indicating how many rows to columns to the start point to get\n the end point.\n\nReturns a {Range} " 469 | } 470 | }, 471 | "60": { 472 | "15": { 473 | "name": "constructor", 474 | "bindingType": "prototypeProperty", 475 | "type": "function", 476 | "paramNames": [ 477 | "pointA", 478 | "pointB" 479 | ], 480 | "range": [ 481 | [ 482 | 60, 483 | 15 484 | ], 485 | [ 486 | 73, 487 | 1 488 | ] 489 | ], 490 | "doc": null 491 | } 492 | }, 493 | "73": { 494 | "11": { 495 | "name": "isEqual", 496 | "bindingType": "prototypeProperty", 497 | "type": "function", 498 | "paramNames": [ 499 | "other" 500 | ], 501 | "range": [ 502 | [ 503 | 73, 504 | 11 505 | ], 506 | [ 507 | 76, 508 | 59 509 | ] 510 | ], 511 | "doc": " Public: Returns a {Boolean} indicating whether this range has the same start\nand end points as the given {Range} or range-compatible {Array}. " 512 | } 513 | } 514 | }, 515 | "exports": 17 516 | }, 517 | "src/test.coffee": { 518 | "objects": { 519 | "0": { 520 | "8": { 521 | "name": "Point", 522 | "type": "import", 523 | "range": [ 524 | [ 525 | 0, 526 | 8 527 | ], 528 | [ 529 | 0, 530 | 24 531 | ] 532 | ], 533 | "bindingType": "variable", 534 | "path": "./point" 535 | } 536 | }, 537 | "1": { 538 | "8": { 539 | "name": "Range", 540 | "type": "import", 541 | "range": [ 542 | [ 543 | 1, 544 | 8 545 | ], 546 | [ 547 | 1, 548 | 24 549 | ] 550 | ], 551 | "bindingType": "variable", 552 | "path": "./range" 553 | } 554 | }, 555 | "6": { 556 | "0": { 557 | "type": "class", 558 | "name": "TestClass", 559 | "superClass": null, 560 | "bindingType": "exports", 561 | "classProperties": [ 562 | [ 563 | 7, 564 | 10 565 | ], 566 | [ 567 | 8, 568 | 17 569 | ] 570 | ], 571 | "prototypeProperties": [], 572 | "doc": " Public: A mutable text container with undo/redo support and the ability to\nannotate logical regions in the text. ", 573 | "range": [ 574 | [ 575 | 6, 576 | 0 577 | ], 578 | [ 579 | 8, 580 | 29 581 | ] 582 | ] 583 | } 584 | }, 585 | "7": { 586 | "10": { 587 | "name": "Range", 588 | "type": "primitive", 589 | "range": [ 590 | [ 591 | 7, 592 | 10 593 | ], 594 | [ 595 | 7, 596 | 14 597 | ] 598 | ], 599 | "doc": null, 600 | "bindingType": "classProperty", 601 | "reference": { 602 | "position": [ 603 | 1, 604 | 8 605 | ] 606 | } 607 | } 608 | }, 609 | "8": { 610 | "17": { 611 | "name": "newlineRegex", 612 | "type": "primitive", 613 | "range": [ 614 | [ 615 | 8, 616 | 17 617 | ], 618 | [ 619 | 8, 620 | 28 621 | ] 622 | ], 623 | "doc": null, 624 | "bindingType": "classProperty" 625 | } 626 | } 627 | }, 628 | "exports": 6 629 | } 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /src/util/referencer.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | path = require 'path' 3 | fs = require 'fs' 4 | 5 | # Public: Responsible for resolving class references. 6 | # 7 | module.exports = class Referencer 8 | 9 | # Public: Construct a referencer. 10 | # 11 | # classes - All known classes 12 | # mixins - All known mixins 13 | # options - the parser options (a {Object}) 14 | constructor: (@classes, @mixins, @options) -> 15 | @readStandardJSON() 16 | @resolveParamReferences() 17 | @errors = 0 18 | 19 | # Public: Get all direct subclasses. 20 | # 21 | # clazz - The parent class (a {Class}) 22 | # 23 | # Returns an {Array} of {Class}es. 24 | getDirectSubClasses: (clazz) -> 25 | _.filter @classes, (cl) -> cl.getParentClassName() is clazz.getFullName() 26 | 27 | # Public: Get all inherited methods. 28 | # 29 | # clazz - The parent class (a {Class}) 30 | # 31 | # Returns the inherited methods. 32 | getInheritedMethods: (clazz) -> 33 | unless _.isEmpty clazz.getParentClassName() 34 | parentClass = _.find @classes, (c) -> c.getFullName() is clazz.getParentClassName() 35 | if parentClass then _.union(parentClass.getMethods(), @getInheritedMethods(parentClass)) else [] 36 | 37 | else 38 | [] 39 | 40 | # Public: Get all included mixins in the class hierarchy. 41 | # 42 | # clazz - The parent class (a {Class}) 43 | # 44 | # Returns an {Array} of {Mixin}s. 45 | getIncludedMethods: (clazz) -> 46 | result = {} 47 | 48 | for mixin in clazz.doc?.includeMixins || [] 49 | result[mixin] = @resolveMixinMethods mixin 50 | 51 | unless _.isEmpty clazz.getParentClassName() 52 | parentClass = _.find @classes, (c) -> c.getFullName() is clazz.getParentClassName() 53 | 54 | if parentClass 55 | result = _.extend {}, @getIncludedMethods(parentClass), result 56 | 57 | result 58 | 59 | # Public: Get all extended mixins in the class hierarchy. 60 | # 61 | # clazz - The parent class (a {Class}) 62 | # 63 | # Returns an {Array} of {Mixin}s. 64 | getExtendedMethods: (clazz) -> 65 | result = {} 66 | 67 | for mixin in clazz.doc?.extendMixins || [] 68 | result[mixin] = @resolveMixinMethods mixin 69 | 70 | unless _.isEmpty clazz.getParentClassName() 71 | parentClass = _.find @classes, (c) -> c.getFullName() is clazz.getParentClassName() 72 | 73 | if parentClass 74 | result = _.extend {}, @getExtendedMethods(parentClass), result 75 | 76 | result 77 | 78 | # Public: Get all concerns methods. 79 | # 80 | # clazz - The parent class (a {Class}) 81 | # 82 | # Returns an {Array} of concern {Method}s. 83 | getConcernMethods: (clazz) -> 84 | result = {} 85 | 86 | for mixin in clazz.doc?.concerns || [] 87 | result[mixin] = @resolveMixinMethods mixin 88 | 89 | unless _.isEmpty clazz.getParentClassName() 90 | parentClass = _.find @classes, (c) -> c.getFullName() is clazz.getParentClassName() 91 | 92 | if parentClass 93 | result = _.extend {}, @getConcernMethods(parentClass), result 94 | 95 | result 96 | 97 | # Public: Get a list of all methods from the given mixin name 98 | # 99 | # name - The full name of the {Mixin} 100 | # 101 | # Returns the mixin methods as an {Array}. 102 | resolveMixinMethods: (name) -> 103 | mixin = _.find @mixins, (m) -> m.getMixinName() is name 104 | 105 | if mixin 106 | mixin.getMethods() 107 | else 108 | console.log "[WARN] Cannot resolve mixin name #{ name }" unless @options.quiet 109 | @errors++ 110 | [] 111 | 112 | # Public: Get all inherited variables. 113 | # 114 | # clazz - The parent class (a {Class}) 115 | # 116 | # Returns an {Array} of {Variable}s. 117 | getInheritedVariables: (clazz) -> 118 | unless _.isEmpty clazz.getParentClassName() 119 | parentClass = _.find @classes, (c) -> c.getFullName() is clazz.getParentClassName() 120 | if parentClass then _.union(parentClass.getVariables(), @getInheritedVariables(parentClass)) else [] 121 | 122 | else 123 | [] 124 | 125 | # Public: Get all inherited constants. 126 | # 127 | # clazz - The parent class (a {Class}) 128 | # 129 | # Returns an {Array} of {Variable}s that are constants. 130 | getInheritedConstants: (clazz) -> 131 | _.filter @getInheritedVariables(clazz), (v) -> v.isConstant() 132 | 133 | # Public: Get all inherited properties. 134 | # 135 | # clazz - The parent class (a {Class}) 136 | # 137 | # Returns an {Array} of {Property} types. 138 | getInheritedProperties: (clazz) -> 139 | unless _.isEmpty clazz.getParentClassName() 140 | parentClass = _.find @classes, (c) -> c.getFullName() is clazz.getParentClassName() 141 | if parentClass then _.union(parentClass.properties, @getInheritedProperties(parentClass)) else [] 142 | 143 | else 144 | [] 145 | 146 | # Public: Creates browsable links for known entities. 147 | # 148 | # See {::getLink}. 149 | # 150 | # text - The text to parse (a {String}) 151 | # path - The path prefix (a {String}) 152 | # 153 | # Returns the processed text (a {String}) 154 | linkTypes: (text = '', path) -> 155 | text = text.split ',' 156 | 157 | text = for t in text 158 | @linkType(t.trim(), path) 159 | 160 | text.join(', ') 161 | 162 | # Public: Create browsable links to a known entity. 163 | # 164 | # See {::getLink}. 165 | # 166 | # text - The text to parse (a {String}) 167 | # path - The path prefix (a {String}) 168 | # 169 | # Returns the processed text (a {String}) 170 | linkType: (text = '', path) -> 171 | text = _.str.escapeHTML text 172 | 173 | for clazz in @classes 174 | text = text.replace ///^(#{ clazz.getFullName() })$///g, "$1" 175 | text = text.replace ///(<|[ ])(#{ clazz.getFullName() })(>|[, ])///g, "$1$2$3" 176 | 177 | text 178 | 179 | # Public: Get the link to classname. 180 | # 181 | # See {::linkTypes}. 182 | # 183 | # classname - The class name (a {String}) 184 | # path - The path prefix (a {String}) 185 | # 186 | # Returns the link (if any) 187 | getLink: (classname, path) -> 188 | for clazz in @classes 189 | if classname is clazz.getFullName() then return "#{ path }classes/#{ clazz.getFullName().replace(/\./g, '/') }.html" 190 | 191 | undefined 192 | 193 | # Public: Resolve all tags on class and method json output. 194 | # 195 | # data - The JSON data (a {Object}) 196 | # entity - The entity context (a {Class}) 197 | # path - The path to the asset root (a {String}) 198 | # 199 | # Returns the JSON data with resolved references (a {Object}) 200 | resolveDoc: (data, entity, path) -> 201 | if data.doc 202 | if data.doc.see 203 | for see in data.doc.see 204 | @resolveSee see, entity, path 205 | 206 | if _.isString data.doc.abstract 207 | data.doc.abstract = @resolveTextReferences(data.doc.abstract, entity, path) 208 | 209 | if _.isString data.doc.summary 210 | data.doc.summary = @resolveTextReferences(data.doc.summary, entity, path) 211 | 212 | for name, options of data.doc.options 213 | continue unless options? 214 | for option, index in options 215 | data.doc.options[name][index].desc = @resolveTextReferences(option.desc, entity, path) 216 | 217 | for name, param of data.doc.params 218 | data.doc.params[name].desc = @resolveTextReferences(param.desc, entity, path) 219 | 220 | for option in param.options ? [] 221 | option.desc = @resolveTextReferences(option.desc, entity, path) 222 | 223 | if data.doc.notes 224 | for note, index in data.doc.notes 225 | data.doc.notes[index] = @resolveTextReferences(note, entity, path) 226 | 227 | if data.doc.todos 228 | for todo, index in data.doc.todos 229 | data.doc.todos[index] = @resolveTextReferences(todo, entity, path) 230 | 231 | if data.doc.examples 232 | for example, index in data.doc.examples 233 | data.doc.examples[index].title = @resolveTextReferences(example.title, entity, path) 234 | 235 | if _.isString data.doc.deprecated 236 | data.doc.deprecated = @resolveTextReferences(data.doc.deprecated, entity, path) 237 | 238 | if data.doc.comment 239 | data.doc.comment = @resolveTextReferences(data.doc.comment, entity, path) 240 | 241 | for returnValue in data.doc.returnValue ? [] 242 | returnValue.desc = @resolveTextReferences(returnValue.desc, entity, path) 243 | 244 | for option in returnValue.options ? [] 245 | option.desc = @resolveTextReferences(option.desc, entity, path) 246 | 247 | if data.doc.throwValue 248 | for throws, index in data.doc.throwValue 249 | data.doc.throwValue[index].desc = @resolveTextReferences(throws.desc, entity, path) 250 | 251 | data 252 | 253 | # Public: Search a text to find see links wrapped in curly braces. 254 | # 255 | # Examples 256 | # 257 | # "To get a list of all customers, go to {Customers.getAll}" 258 | # 259 | # text - The text to search (a {String}) 260 | # 261 | # Returns the text with hyperlinks (a {String}) 262 | resolveTextReferences: (text = '', entity, path) -> 263 | # Make curly braces within code blocks undetectable 264 | text = text.replace /`(.|\n)+?`/mg, (match) -> match.replace(/{/mg, "\u0091").replace(/}/mg, "\u0092") 265 | 266 | # Search for references and replace them 267 | text = text.replace /(?:\[((?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*)\])?\{([^\}]+)\}/gm, (match, label, link) => 268 | # Remove the markdown generated autolinks 269 | link = link.replace(/<.+?>/g, '').split(' ') 270 | href = link.shift() 271 | label = _.str.strip(label) 272 | 273 | if label.length < 2 274 | label = "" 275 | 276 | see = @resolveSee({ reference: href, label: label }, entity, path) 277 | 278 | if see.reference 279 | "#{ see.label }" 280 | else 281 | match 282 | 283 | # Restore curly braces within code blocks 284 | text = text.replace /]*)?>(.|\n)+?<\/code>/mg, (match) -> match.replace(/\u0091/mg, '{').replace(/\u0092/mg, '}') 285 | 286 | # Public: Resolves delegations; that is, methods whose source content come from 287 | # another file. 288 | # 289 | # These are basically conrefs. 290 | resolveDelegation: (origin, ref, entity) -> 291 | 292 | # Link to direct class methods 293 | if /^\@/.test(ref) 294 | methods = _.map(_.filter(entity.getMethods(), (m) -> _.indexOf(['class', 'mixin'], m.getType()) >= 0), (m) -> m) 295 | 296 | match = _.find methods, (m) -> 297 | return ref.substring(1) == m.getName() 298 | 299 | if match 300 | if match.doc.delegation 301 | return @resolveDelegation(origin, match.doc.delegation, entity) 302 | else 303 | return [ _.clone(match.doc), match.parameters ] 304 | else 305 | console.log "[WARN] Cannot resolve delegation to #{ ref } in #{ entity.getFullName() }" unless @options.quiet 306 | @errors++ 307 | 308 | # Link to direct instance methods 309 | else if /^\./.test(ref) 310 | methods = _.map(_.filter(entity.getMethods(), (m) -> m.getType() is 'instance'), (m) -> m) 311 | 312 | match = _.find methods, (m) -> 313 | return ref.substring(1) == m.getName() 314 | 315 | if match 316 | if match.doc.delegation 317 | return @resolveDelegation(origin, match.doc.delegation, entity) 318 | else 319 | return [ _.clone(match.doc), match.parameters ] 320 | else 321 | console.log "[WARN] Cannot resolve delegation to #{ ref } in #{ entity.getFullName() }" unless @options.quiet 322 | @errors++ 323 | 324 | # Link to other objects 325 | else 326 | 327 | # Get class and method reference 328 | if match = /^(.*?)([.@][$a-z_\x7f-\uffff][$\w\x7f-\uffff]*)?$/.exec ref 329 | refClass = match[1] 330 | refMethod = match[2] 331 | otherEntity = _.find @classes, (c) -> c.getFullName() is refClass 332 | otherEntity ||= _.find @mixins, (c) -> c.getFullName() is refClass 333 | 334 | if otherEntity 335 | # Link to another class 336 | if _.isUndefined refMethod 337 | # if _.include(_.map(@classes, (c) -> c.getFullName()), refClass) || _.include(_.map(@mixins, (c) -> c.getFullName()), refClass) 338 | # see.reference = "#{ path }#{ if otherEntity.constructor.name == 'Class' then 'classes' else 'modules' }/#{ refClass.replace(/\./g, '/') }.html" 339 | # see.label = ref unless see.label 340 | # else 341 | # console.log "[WARN] Cannot resolve link to entity #{ refClass } in #{ entity.getFullName() }" unless @options.quiet 342 | # @errors++ 343 | 344 | # Link to other class' class methods 345 | else if /^\@/.test(refMethod) 346 | methods = _.map(_.filter(otherEntity.getMethods(), (m) -> _.indexOf(['class', 'mixin'], m.getType()) >= 0), (m) -> m) 347 | 348 | match = _.find methods, (m) -> 349 | return refMethod.substring(1) == m.getName() 350 | 351 | if match 352 | if match.doc.delegation 353 | return @resolveDelegation(origin, match.doc.delegation, otherEntity) 354 | else 355 | return [ _.clone(match.doc), match.parameters ] 356 | else 357 | console.log "[WARN] Cannot resolve delegation to #{ refMethod } in #{ otherEntity.getFullName() }" unless @options.quiet 358 | @errors++ 359 | 360 | # Link to other class instance methods 361 | else if /^\./.test(refMethod) 362 | methods = _.map(_.filter(otherEntity.getMethods(), (m) -> m.getType() is 'instance'), (m) -> m) 363 | 364 | match = _.find methods, (m) -> 365 | return refMethod.substring(1) == m.getName() 366 | 367 | if match 368 | if match.doc.delegation 369 | return @resolveDelegation(origin, match.doc.delegation, otherEntity) 370 | else 371 | return [ _.clone(match.doc), match.parameters ] 372 | else 373 | console.log "[WARN] Cannot resolve delegation to #{ refMethod } in #{ otherEntity.getFullName() }" unless @options.quiet 374 | @errors++ 375 | else 376 | console.log "[WARN] Cannot find delegation to #{ ref } in class #{ entity.getFullName() }" unless @options.quiet 377 | @errors++ 378 | else 379 | console.log "[WARN] Cannot resolve delegation to #{ ref } in class #{ otherEntity.getFullName() }" unless @options.quiet 380 | @errors++ 381 | 382 | return [ origin.doc, origin.parameters ] 383 | 384 | # Public: Resolves curly-bracket reference links. 385 | # 386 | # see - The reference object (a {Object}) 387 | # entity - The entity context (a {Class}) 388 | # path - The path to the asset root (a {String}) 389 | # 390 | # Returns the resolved see (a {Object}). 391 | resolveSee: (see, entity, path) -> 392 | # If a reference starts with a space like `{ a: 1 }`, then it's not a valid reference 393 | return see if see.reference.substring(0, 1) is ' ' 394 | 395 | ref = see.reference 396 | 397 | # Link to direct class methods 398 | if /^\./.test(ref) 399 | methods = _.map(_.filter(entity.getMethods(), (m) -> _.indexOf(['class', 'mixin'], m.getType()) >= 0), (m) -> m.getName()) 400 | 401 | if _.include methods, ref.substring(1) 402 | see.reference = "#{ path }#{if entity.constructor.name == 'Class' then 'classes' else 'modules'}/#{ entity.getFullName().replace(/\./g, '/') }.html##{ ref.substring(1) }-class" 403 | see.label = ref unless see.label 404 | else 405 | see.label = see.reference 406 | see.reference = undefined 407 | console.log "[WARN] Cannot resolve link to #{ ref } in #{ entity.getFullName() }" unless @options.quiet 408 | @errors++ 409 | 410 | # Link to direct instance methods 411 | else if /^::/.test(ref) 412 | instanceMethods = _.map(_.filter(entity.getMethods(), (m) -> m.getType() is 'instance'), (m) -> m.getName()) 413 | 414 | if _.include instanceMethods, ref.substring(2) 415 | see.reference = "#{ path }classes/#{ entity.getFullName().replace(/\./g, '/') }.html##{ ref.substring(2) }-instance" 416 | see.label = ref unless see.label 417 | else 418 | see.label = see.reference 419 | see.reference = undefined 420 | console.log "[WARN] Cannot resolve link to #{ ref } in class #{ entity.getFullName() }" unless @options.quiet 421 | @errors++ 422 | 423 | # Link to other objects 424 | else 425 | # Ignore normal links 426 | unless /^https?:\/\//.test ref 427 | # Get class and method reference 428 | if match = /^(.*?)((\.|::)[$a-z_\x7f-\uffff][$\w\x7f-\uffff]*)?$/.exec ref 429 | refClass = match[1] 430 | refMethod = match[2] 431 | otherEntity = _.find @classes, (c) -> c.getFullName() is refClass 432 | otherEntity ||= _.find @mixins, (c) -> c.getFullName() is refClass 433 | 434 | if otherEntity 435 | # Link to another class 436 | if _.isUndefined refMethod 437 | if _.include(_.map(@classes, (c) -> c.getFullName()), refClass) || _.include(_.map(@mixins, (c) -> c.getFullName()), refClass) 438 | see.reference = "#{ path }#{ if otherEntity.constructor.name == 'Class' then 'classes' else 'modules' }/#{ refClass.replace(/\./g, '/') }.html" 439 | see.label = ref unless see.label 440 | else 441 | see.label = see.reference 442 | see.reference = undefined 443 | console.log "[WARN] Cannot resolve link to entity #{ refClass } in #{ entity.getFullName() }" unless @options.quiet 444 | @errors++ 445 | 446 | # Link to other class' class methods 447 | else if /^\./.test(refMethod) 448 | methods = _.map(_.filter(otherEntity.getMethods(), (m) -> _.indexOf(['class', 'mixin'], m.getType()) >= 0), (m) -> m.getName()) 449 | 450 | if _.include methods, refMethod.substring(1) 451 | see.reference = "#{ path }#{ if otherEntity.constructor.name == 'Class' then 'classes' else 'modules' }/#{ otherEntity.getFullName().replace(/\./g, '/') }.html##{ refMethod.substring(1) }-class" 452 | see.label = ref unless see.label 453 | else 454 | see.label = see.reference 455 | see.reference = undefined 456 | console.log "[WARN] Cannot resolve link to #{ refMethod } of class #{ otherEntity.getFullName() } in class #{ entity.getFullName() }" unless @options.quiet 457 | @errors++ 458 | 459 | # Link to other class instance methods 460 | else if /^::/.test(refMethod) 461 | instanceMethods = _.map(_.filter(otherEntity.getMethods(), (m) -> _.indexOf(['instance', 'mixin'], m.getType()) >= 0), (m) -> m.getName()) 462 | 463 | if _.include instanceMethods, refMethod.substring(2) 464 | see.reference = "#{ path }#{ if otherEntity.constructor.name == 'Class' then 'classes' else 'modules' }/#{ otherEntity.getFullName().replace(/\./g, '/') }.html##{ refMethod.substring(2) }-instance" 465 | see.label = ref unless see.label 466 | else 467 | see.label = see.reference 468 | see.reference = undefined 469 | console.log "[WARN] Cannot resolve link to #{ refMethod } of class #{ otherEntity.getFullName() } in class #{ entity.getFullName() }" unless @options.quiet 470 | @errors++ 471 | else 472 | # controls external reference links 473 | if @verifyExternalObjReference(see.reference) 474 | see.label = see.reference unless see.label 475 | see.reference = @standardObjs[see.reference] 476 | else 477 | see.label = see.reference 478 | see.reference = undefined 479 | console.log "[WARN] Cannot find referenced class #{ refClass } in class #{ entity.getFullName() } (#{see.label})" unless @options.quiet 480 | @errors++ 481 | else 482 | see.label = see.reference 483 | see.reference = undefined 484 | console.log "[WARN] Cannot resolve link to #{ ref } in class #{ entity.getFullName() }" unless @options.quiet 485 | @errors++ 486 | see 487 | 488 | @getLinkMatch: (text) -> 489 | if m = text.match(/\{([\w.]+)\}/) 490 | return m[1] 491 | else 492 | return "" 493 | 494 | # Public: Constructs the documentation links for the standard JS objects. 495 | # 496 | # Returns a JSON {Object}. 497 | readStandardJSON: -> 498 | @standardObjs = JSON.parse(fs.readFileSync(path.join(__dirname, 'standardObjs.json'), 'utf-8')) 499 | 500 | # Public: Checks to make sure that an object that's referenced exists in *standardObjs.json*. 501 | # 502 | # Returns a {Boolean}. 503 | verifyExternalObjReference: (name) -> 504 | @standardObjs[name] != undefined 505 | 506 | # Public: Resolve parameter references. This goes through all 507 | # method parameter and see if a param doc references another 508 | # method. If so, copy over the doc meta data. 509 | resolveParamReferences: -> 510 | entities = _.union @classes, @mixins 511 | 512 | for entity in entities 513 | for method in entity.getMethods() 514 | if method.getDoc() && !_.isEmpty method.getDoc().params 515 | for param in method.getDoc().params 516 | if param.reference 517 | 518 | # Find referenced entity 519 | if ref = /([$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*)([#.])([$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*)/i.test param.reference 520 | otherEntity = _.first entities, (e) -> e.getFullName() is ref[1] 521 | otherMethodType = if ref[2] is '.' then ['instance'] else ['class', 'mixin'] 522 | otherMethod = ref[3] 523 | 524 | # The referenced entity is on the current entity 525 | else 526 | otherEntity = entity 527 | otherMethodType = if param.reference.substring(0, 1) is '.' then ['instance', 'mixin'] else ['class', 'mixin'] 528 | otherMethod = param.reference.substring(1) 529 | 530 | # Find the referenced method 531 | refMethod = _.find otherEntity.getMethods(), (m) -> m.getName() is otherMethod && _.indexOf(otherMethodType, m.getType()) >= 0 532 | 533 | if refMethod 534 | # Filter param name 535 | if param.name 536 | copyParam = _.find refMethod.getDoc().params, (p) -> p.name is param.name 537 | 538 | if copyParam 539 | # Replace a single param 540 | method.getDoc().params ||= [] 541 | method.getDoc().params = _.reject method.getDoc().params, (p) -> p.name = param.name 542 | method.getDoc().params.push copyParam 543 | 544 | # Replace a single option param 545 | if _.isObject refMethod.getDoc().paramsOptions 546 | method.getDoc().paramsOptions ||= {} 547 | method.getDoc().paramsOptions[param.name] = refMethod.getDoc().paramsOptions[param.name] 548 | 549 | else 550 | console.log "[WARN] Parameter #{ param.name } does not exist in #{ param.reference } in class #{ entity.getFullName() }" unless @options.quiet 551 | @errors++ 552 | else 553 | # Copy all parameters that exist on the given method 554 | names = _.map method.getParameters(), (p) -> p.getName() 555 | method.getDoc().params = _.filter refMethod.getDoc().params, (p) -> _.contains names, p.name 556 | 557 | # Copy all matching options 558 | if _.isObject refMethod.getDoc().paramsOptions 559 | method.getDoc().paramsOptions ||= {} 560 | method.getDoc().paramsOptions[name] = refMethod.getDoc().paramsOptions[name] for name in names 561 | 562 | else 563 | console.log "[WARN] Cannot resolve reference tag #{ param.reference } in class #{ entity.getFullName() }" unless @options.quiet 564 | @errors++ 565 | --------------------------------------------------------------------------------