├── .gitignore ├── tests ├── .gitignore ├── guess_cant_guess_prop.js ├── property_redefinition.js ├── email_in_text_block.js ├── type_doesnt_accept_desc.js ├── function_redefinition.js ├── property_cant_return.js ├── guess_failure.js ├── property_cant_have_params.js ├── constructor_in_global_context.js ├── class_redefinition.js ├── module_override_name.js ├── return_needs_argument.js ├── throws_needs_argument.js ├── function_basic.js ├── typedef_not_allowed.js ├── property_type_first.js ├── guess_function.js ├── class_basic.js ├── function_and_property_in_same_block.js ├── module_basic.js ├── type_optional_braces.js ├── guess_basic_function.js ├── class_simple_nested.js ├── property_omit_name_and_tag.js ├── module_override_name.out ├── property_implicit_with_type.js ├── email_in_text_block.out ├── type_and_returns_in_same_function_bad.js ├── params_type_last.js ├── throws_multiple_types.js ├── params_one_way.js ├── params_type_first.js ├── params_plural_allowed.js ├── type_and_returns_in_same_function_good.js ├── function_omit_name_and_tag.js ├── params_type_last_newline.js ├── throws_needs_argument_not_tag.js ├── module_basic.out ├── params_type_first_newline.js ├── function_omit_name.js ├── module_inside_class.js ├── class_with_constructor.js ├── guess_getsetter.js ├── class_basic.out ├── fun_with_types.js ├── function_basic.out ├── class_with_prop_and_func.js ├── guess_name_object_property.out ├── events.js ├── guess_basic_function.out ├── events.out ├── class_with_constructors.js ├── guess_getsetter.out ├── property_type_first.out ├── module_explicit.js ├── guess_function.out ├── property_omit_name_and_tag.out ├── description_recurring.js ├── function_omit_name_and_tag.out ├── module_explicit.out ├── property_implicit_with_type.out ├── function_omit_name.out ├── function_supports_type_or_return_or_returns.js ├── type_optional_braces.out ├── module_inside_class.out ├── typedef_not_allowed.out ├── constructor_in_global_context.out ├── property_cant_return.out ├── params_one_way.out ├── params_type_last.out ├── property_cant_have_params.out ├── params_type_first.out ├── function_and_property_in_same_block.out ├── params_plural_allowed.out ├── params_type_last_newline.out ├── params_type_first_newline.out ├── type_and_returns_in_same_function_good.out ├── guess_name_object_property.js ├── events_inside_class.js ├── events_redefined.js ├── see_tag.js ├── return_needs_argument.out ├── throws_needs_argument.out ├── throws_needs_argument_not_tag.out ├── throws_multiple_types.out ├── class_redefinition.out ├── type_and_returns_in_same_function_bad.out ├── events_redefined.out ├── guess_cant_guess_prop.out ├── fun_with_types.out ├── function_redefinition.out ├── property_redefinition.out ├── guess_obj_property.out ├── type_doesnt_accept_desc.out ├── class_with_constructor.out ├── guess_firstblock_precedence_over_assignment.js ├── guess_failure.out ├── guess_firstblock_precedence_over_assignment.out ├── description_recurring.out ├── guess_obj_property.js ├── see_tag.out ├── class_simple_nested.out ├── events_inside_class.out ├── class_with_prop_and_func.out ├── class_with_constructors.out ├── function_supports_type_or_return_or_returns.out ├── params_complex.js ├── class_full.js ├── yucky_nested_curlies.out ├── yucky_nested_curlies.js ├── params_complex.out └── class_full.out ├── test.py ├── README.md └── docstract.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.pyc 2 | *~ 3 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /*.outactual 2 | -------------------------------------------------------------------------------- /tests/guess_cant_guess_prop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a property, but we can't guess the name! 3 | * @type {string} 4 | */ 5 | 6 | -------------------------------------------------------------------------------- /tests/property_redefinition.js: -------------------------------------------------------------------------------- 1 | /** It's an error to declare the same property twice */ 2 | 3 | /** @prop Foo */ 4 | /** @prop Foo */ 5 | -------------------------------------------------------------------------------- /tests/email_in_text_block.js: -------------------------------------------------------------------------------- 1 | /** Emails should not be interpreted as tags, so lloyd@hilaiel.com 2 | * shouldn't be broken at @@hilaiel */ 3 | -------------------------------------------------------------------------------- /tests/type_doesnt_accept_desc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {string} no no, no desc on a @type 3 | */ 4 | exports.register = function() { 5 | }; 6 | -------------------------------------------------------------------------------- /tests/function_redefinition.js: -------------------------------------------------------------------------------- 1 | /** It's an error to declare the same function twice */ 2 | 3 | /** @function Foo */ 4 | /** @function Foo */ 5 | -------------------------------------------------------------------------------- /tests/property_cant_return.js: -------------------------------------------------------------------------------- 1 | /** A property cannot return data */ 2 | 3 | /** @prop foo 4 | * @returns {string} whatever, this is silly. */ 5 | -------------------------------------------------------------------------------- /tests/guess_failure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module has an unguessable documentation block 3 | */ 4 | 5 | /** 6 | * WTF am I trying to document here? 7 | */ 8 | -------------------------------------------------------------------------------- /tests/property_cant_have_params.js: -------------------------------------------------------------------------------- 1 | /** A property cannot have parameters */ 2 | 3 | /** @prop foo 4 | * @param {string} whatever this is silly 5 | */ 6 | -------------------------------------------------------------------------------- /tests/constructor_in_global_context.js: -------------------------------------------------------------------------------- 1 | /** And you certainly can't have a constructor in the 2 | * *global* context! */ 3 | 4 | /** @constructor that builds things */ 5 | -------------------------------------------------------------------------------- /tests/class_redefinition.js: -------------------------------------------------------------------------------- 1 | /** It's an error to declare the same class twice */ 2 | 3 | /** @class Foo */ 4 | /** @endclass */ 5 | /** @class Foo */ 6 | /** @endclass */ 7 | -------------------------------------------------------------------------------- /tests/module_override_name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module some_other_name 3 | * 4 | * This module overrides the logic that assumes that the filename 5 | * *is* the module name. 6 | */ 7 | -------------------------------------------------------------------------------- /tests/return_needs_argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a module with a function thats returns something unknown. error! 3 | */ 4 | 5 | /** 6 | * @function throweyFunc 7 | * @return 8 | */ 9 | -------------------------------------------------------------------------------- /tests/throws_needs_argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a module with a function thats throws something we don't knows. 3 | */ 4 | 5 | /** 6 | * @function throweyFunc 7 | * @throws 8 | */ 9 | -------------------------------------------------------------------------------- /tests/function_basic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module with a basic function definition in it 3 | */ 4 | 5 | /** 6 | * @function toFilename 7 | * a function which converts URLs to filenames 8 | */ 9 | -------------------------------------------------------------------------------- /tests/typedef_not_allowed.js: -------------------------------------------------------------------------------- 1 | /** Simple typedef usage */ 2 | 3 | /** 4 | * @typedef object 5 | */ 6 | 7 | /** 8 | * @prop foo {string} 9 | */ 10 | 11 | /** 12 | * @endtypedef 13 | */ 14 | -------------------------------------------------------------------------------- /tests/property_type_first.js: -------------------------------------------------------------------------------- 1 | /** A module with a single property, and type before name */ 2 | 3 | /** 4 | * @property {integer} port 5 | * The port number of the URL, `null` if none was specified. 6 | */ 7 | -------------------------------------------------------------------------------- /tests/guess_function.js: -------------------------------------------------------------------------------- 1 | /** if @@return appears, we should be able to guess 2 | * it's a function */ 3 | 4 | /** 5 | * a guessable function documentation block 6 | * @returns {string} */ 7 | exports.getValue = someFuncRef; 8 | -------------------------------------------------------------------------------- /tests/class_basic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A test of basic class parsing 3 | */ 4 | 5 | /** 6 | * @class URL 7 | * A class which parses a url and exposes its various 8 | * components separately. 9 | */ 10 | 11 | /** @endclass */ 12 | -------------------------------------------------------------------------------- /tests/function_and_property_in_same_block.js: -------------------------------------------------------------------------------- 1 | /** You can have a single docblock that's both a function 2 | * AND a property, and they'll be automagically split into 3 | * two separate blocks */ 4 | 5 | /** @function foo @property bar */ 6 | -------------------------------------------------------------------------------- /tests/module_basic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This is a basic module, it doens't explicitly specify it's own name 4 | * and it uses the short form for doc-wide comments. 5 | * 6 | * // this is code, it uses some markdown! 7 | * 8 | */ 9 | -------------------------------------------------------------------------------- /tests/type_optional_braces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * when defining the @@type of a property, you chose to omit braces 3 | */ 4 | 5 | /** 6 | * @property foo 7 | * @type {string} 8 | */ 9 | 10 | /** 11 | * @property bar 12 | * @type string 13 | */ 14 | -------------------------------------------------------------------------------- /tests/guess_basic_function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * documenation of fairly vanilla functions should be simple. let's see! 3 | */ 4 | 5 | /** 6 | * Transmorgifies the input 7 | */ 8 | function transmorgify(foo, bar, baz) { 9 | return baz + foo + bar; 10 | } 11 | -------------------------------------------------------------------------------- /tests/class_simple_nested.js: -------------------------------------------------------------------------------- 1 | /** Simple test of nested class support */ 2 | 3 | /** @class Foo */ 4 | /** @constructor 5 | * foo's constructor! */ 6 | /** @class Bar */ 7 | /** @constructor 8 | * bar's constructor! */ 9 | /** @endclass */ 10 | /** @endclass */ 11 | -------------------------------------------------------------------------------- /tests/property_omit_name_and_tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module to test implicit property documentation, 3 | * where the developer provides only a description 4 | */ 5 | 6 | /** 7 | * a property which exposes a name to you 8 | */ 9 | exports.name = "Lloyd Hilaiel"; 10 | -------------------------------------------------------------------------------- /tests/module_override_name.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "This module overrides the logic that assumes that the filename\n*is* the module name.", 3 | "filename": "tests/module_override_name.js", 4 | "module": "some_other_name", 5 | "source_lines": [ 6 | 1, 7 | 6 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/property_implicit_with_type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module to test implicit property documentation, 3 | * where the developer provides only a description 4 | */ 5 | 6 | /** 7 | * a property which exposes a name to you 8 | * @type string 9 | */ 10 | exports.name = "Lloyd Hilaiel"; 11 | -------------------------------------------------------------------------------- /tests/email_in_text_block.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "Emails should not be interpreted as tags, so lloyd@hilaiel.com\nshouldn't be broken at @hilaiel", 3 | "filename": "tests/email_in_text_block.js", 4 | "module": "email_in_text_block", 5 | "source_lines": [ 6 | 1, 7 | 2 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/type_and_returns_in_same_function_bad.js: -------------------------------------------------------------------------------- 1 | /** @@type is an alias for @@return when it occurs in a function 2 | * block.*/ 3 | 4 | /** 5 | * @function foo 6 | * But having both @@type and @@return is bogus. 7 | * @returns {string} This is the first return type. 8 | * @type {boolean} 9 | */ 10 | -------------------------------------------------------------------------------- /tests/params_type_last.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function which contains parameters expressed 3 | * with name before type. 4 | */ 5 | 6 | /** 7 | * a function which converts URLs to filenames. 8 | * @param url {string} The url to convert 9 | */ 10 | let toFilename = exports.toFilename = function toFilename(url) { 11 | } 12 | -------------------------------------------------------------------------------- /tests/throws_multiple_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a module with a function thats really throwey. 3 | */ 4 | 5 | /** 6 | * @function throweyFunc 7 | * @throws When you are sleeping 8 | * @throws When you're awake 9 | * @throws {object} if you've been bad or good. 10 | * @throws So be good for goodness sake. 11 | */ 12 | -------------------------------------------------------------------------------- /tests/params_one_way.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function which contains parameters expressed 3 | * with type before param name. 4 | */ 5 | 6 | /** 7 | * a function which converts URLs to filenames. 8 | * @param {string} url The url to convert 9 | */ 10 | let toFilename = exports.toFilename = function toFilename(url) { 11 | } 12 | -------------------------------------------------------------------------------- /tests/params_type_first.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function which contains parameters expressed 3 | * with type before param name. 4 | */ 5 | 6 | /** 7 | * a function which converts URLs to filenames. 8 | * @param {string} url The url to convert 9 | */ 10 | let toFilename = exports.toFilename = function toFilename(url) { 11 | } 12 | -------------------------------------------------------------------------------- /tests/params_plural_allowed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function which contains parameters expressed 3 | * with type before param name. 4 | */ 5 | 6 | /** 7 | * a function which converts URLs to filenames. 8 | * @params {string} url The url to convert 9 | */ 10 | let toFilename = exports.toFilename = function toFilename(url) { 11 | } 12 | -------------------------------------------------------------------------------- /tests/type_and_returns_in_same_function_good.js: -------------------------------------------------------------------------------- 1 | /** If so desired, the documentor may break up desc and 2 | * type of return value (like in jsdoc) .*/ 3 | 4 | /** 5 | * @function imDocumentedWithBothTypeAndReturn 6 | * This function returns a String. 7 | * @return The name of the current user 8 | * @type String 9 | */ 10 | -------------------------------------------------------------------------------- /tests/function_omit_name_and_tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module with a basic function definition in it, 3 | * which omits all tags, letting the system figure out 4 | * what we mean. 5 | */ 6 | 7 | /** 8 | * a function which converts URLs to filenames 9 | */ 10 | let toFilename = exports.toFilename = function toFilename(url) { 11 | } 12 | -------------------------------------------------------------------------------- /tests/params_type_last_newline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function which contains parameters expressed 3 | * with name before type. 4 | */ 5 | 6 | /** 7 | * a function which converts URLs to filenames. 8 | * @param url {string} 9 | * The url to convert 10 | */ 11 | let toFilename = exports.toFilename = function toFilename(url) { 12 | } 13 | -------------------------------------------------------------------------------- /tests/throws_needs_argument_not_tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a module with a function thats throws something we don't knows. 3 | * in this case, the tag requiring an argument is not the last 4 | * item in the codeblock. 5 | */ 6 | 7 | /** 8 | * @function throweyFunc 9 | * @throws 10 | * @throws {string} a description of why 11 | */ 12 | -------------------------------------------------------------------------------- /tests/module_basic.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "This is a basic module, it doens't explicitly specify it's own name\nand it uses the short form for doc-wide comments.\n\n // this is code, it uses some markdown!", 3 | "filename": "tests/module_basic.js", 4 | "module": "module_basic", 5 | "source_lines": [ 6 | 1, 7 | 8 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/params_type_first_newline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function which contains parameters expressed 3 | * with type before param name. 4 | */ 5 | 6 | /** 7 | * a function which converts URLs to filenames. 8 | * @param {string} url 9 | * The url to convert 10 | */ 11 | let toFilename = exports.toFilename = function toFilename(url) { 12 | } 13 | -------------------------------------------------------------------------------- /tests/function_omit_name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module with a basic function definition in it, 3 | * but without explicit mention of the name of the function 4 | * given the heuristic name guessing stuff. 5 | */ 6 | 7 | /** 8 | * @function 9 | * a function which converts URLs to filenames 10 | */ 11 | 12 | let toFilename = exports.toFilename = function toFilename(url) { 13 | } -------------------------------------------------------------------------------- /tests/module_inside_class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class URL 3 | * A class which parses a url and exposes its various 4 | * components separately. 5 | */ 6 | 7 | /** 8 | * @module names_changed 9 | * This is a module who's wierd, it has its declaration 10 | * inside of a class declaration. We could make this an error, 11 | * but why? 12 | */ 13 | 14 | /** @endclass */ 15 | -------------------------------------------------------------------------------- /tests/class_with_constructor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A test of basic class parsing 3 | */ 4 | 5 | /** 6 | * @class URL 7 | * A class which parses a url and exposes its various 8 | * components separately. 9 | */ 10 | 11 | /** 12 | * @constructor 13 | * This is the constructor of the class 14 | * @param {foo} bar it accepts a bar argument of type foo 15 | */ 16 | 17 | /** @endclass */ 18 | -------------------------------------------------------------------------------- /tests/guess_getsetter.js: -------------------------------------------------------------------------------- 1 | // __defineGetter__ / __defineSetter__ suggests a property, name is first 2 | // arg to function. 3 | 4 | /** 5 | * True iff the stream is closed. 6 | */ 7 | stream.__defineGetter__("closed", function stream_closed() { 8 | return !self.opened; 9 | }); 10 | 11 | /** 12 | * True iff the stream is open. 13 | */ 14 | stream.__defineSetter__('opened', function(x) { 15 | self.opened = x; 16 | }); 17 | -------------------------------------------------------------------------------- /tests/class_basic.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "desc": "A class which parses a url and exposes its various\ncomponents separately.", 5 | "name": "URL", 6 | "source_lines": [ 7 | 5, 8 | 10 9 | ] 10 | } 11 | ], 12 | "desc": "A test of basic class parsing", 13 | "filename": "tests/class_basic.js", 14 | "module": "class_basic", 15 | "source_lines": [ 16 | 1, 17 | 4 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/fun_with_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an array of strings 3 | * @type {array of strings} 4 | */ 5 | exports.arrayOfStrings = [ "foo", "bar", "baz" ]; 6 | 7 | /** 8 | * This is an another array of strings 9 | * @type {strings[]} 10 | */ 11 | exports.arrayOfStrings2 = [ "foo", "bar", "baz" ]; 12 | 13 | /** 14 | * This is a *third* array of strings 15 | * @type { Strings [ ] } 16 | */ 17 | exports.arrayOfStrings3 = [ "foo", "bar", "baz" ]; 18 | -------------------------------------------------------------------------------- /tests/function_basic.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module with a basic function definition in it", 3 | "filename": "tests/function_basic.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames", 7 | "name": "toFilename", 8 | "source_lines": [ 9 | 5, 10 | 8 11 | ] 12 | } 13 | ], 14 | "module": "function_basic", 15 | "source_lines": [ 16 | 1, 17 | 4 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/class_with_prop_and_func.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple class with a property and a function 3 | */ 4 | 5 | /** 6 | * @class URL 7 | * A class which parses a url and exposes its various 8 | * components separately. 9 | */ 10 | 11 | /** 12 | * @property foo 13 | * the foo property of my url classs 14 | * @type string 15 | */ 16 | 17 | /** 18 | * @function bar 19 | * the foo property of my url classs 20 | * @returns nothing. 21 | */ 22 | 23 | /** @endclass */ 24 | -------------------------------------------------------------------------------- /tests/guess_name_object_property.out: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "tests/guess_name_object_property.js", 3 | "functions": [ 4 | { 5 | "desc": "Stops the server from accepting new connections. This function is\nasynchronous, the server is finally closed when the server emits a\n'close' event.", 6 | "name": "destroy", 7 | "source_lines": [ 8 | 4, 9 | 8 10 | ] 11 | } 12 | ], 13 | "module": "guess_name_object_property" 14 | } 15 | -------------------------------------------------------------------------------- /tests/events.js: -------------------------------------------------------------------------------- 1 | // docstract supports a somewhat generic notion of "events". These may 2 | // be associated with a class or the top level file. An event has a 3 | // name, description, and payload. That's it. 4 | 5 | /** 6 | * @event progress 7 | * Allows the listener to understand approximately how much of the 8 | * page has loaded. 9 | * @payload {number} The percentage (0..100) of page load that is complete 10 | */ 11 | this.eventEmitter._emit("progress", curProgress); 12 | -------------------------------------------------------------------------------- /tests/guess_basic_function.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "documenation of fairly vanilla functions should be simple. let's see!", 3 | "filename": "tests/guess_basic_function.js", 4 | "functions": [ 5 | { 6 | "desc": "Transmorgifies the input", 7 | "name": "transmorgify", 8 | "source_lines": [ 9 | 5, 10 | 7 11 | ] 12 | } 13 | ], 14 | "module": "guess_basic_function", 15 | "source_lines": [ 16 | 1, 17 | 4 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/events.out: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "desc": "Allows the listener to understand approximately how much of the\npage has loaded.", 5 | "name": "progress", 6 | "payload": { 7 | "desc": "The percentage (0..100) of page load that is complete", 8 | "type": "number" 9 | }, 10 | "source_lines": [ 11 | 5, 12 | 10 13 | ] 14 | } 15 | ], 16 | "filename": "tests/events.js", 17 | "module": "events" 18 | } 19 | -------------------------------------------------------------------------------- /tests/class_with_constructors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A test of basic class parsing 3 | */ 4 | 5 | /** 6 | * @class URL 7 | * A class which parses a url and exposes its various 8 | * components separately. 9 | */ 10 | 11 | /** 12 | * @constructor 13 | * This is the first constructor of the class 14 | * @param {foo} bar it accepts a bar argument of type foo 15 | */ 16 | 17 | 18 | /** 19 | * @constructor 20 | * This is the second (no-arg) constructor of the class 21 | */ 22 | 23 | /** @endclass */ 24 | -------------------------------------------------------------------------------- /tests/guess_getsetter.out: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "tests/guess_getsetter.js", 3 | "module": "guess_getsetter", 4 | "properties": [ 5 | { 6 | "desc": "True iff the stream is closed.", 7 | "name": "closed", 8 | "source_lines": [ 9 | 4, 10 | 6 11 | ] 12 | }, 13 | { 14 | "desc": "True iff the stream is open.", 15 | "name": "opened", 16 | "source_lines": [ 17 | 11, 18 | 13 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/property_type_first.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module with a single property, and type before name", 3 | "filename": "tests/property_type_first.js", 4 | "module": "property_type_first", 5 | "properties": [ 6 | { 7 | "desc": "The port number of the URL, `null` if none was specified.", 8 | "name": "port", 9 | "source_lines": [ 10 | 3, 11 | 6 12 | ], 13 | "type": "integer" 14 | } 15 | ], 16 | "source_lines": [ 17 | 1, 18 | 2 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/module_explicit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In YUIDOC like fashion, it's completely allowable to have 3 | * the first comment block of a file provide module level 4 | * documentation. One may also... 5 | * 6 | * @module module_explicit_renamed 7 | * 8 | * @description 9 | * ... use the description tag to explicitly provide module level 10 | * documentation, or they may combine both mechanisms (top commenting 11 | * (and desc tag). In the latter case, docs will be concatenated 12 | * with a couple newlines. 13 | */ 14 | -------------------------------------------------------------------------------- /tests/guess_function.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "if @return appears, we should be able to guess\n it's a function", 3 | "filename": "tests/guess_function.js", 4 | "functions": [ 5 | { 6 | "desc": "a guessable function documentation block", 7 | "name": "getValue", 8 | "returns": { 9 | "type": "string" 10 | }, 11 | "source_lines": [ 12 | 4, 13 | 6 14 | ] 15 | } 16 | ], 17 | "module": "guess_function", 18 | "source_lines": [ 19 | 1, 20 | 3 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/property_omit_name_and_tag.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module to test implicit property documentation,\nwhere the developer provides only a description", 3 | "filename": "tests/property_omit_name_and_tag.js", 4 | "module": "property_omit_name_and_tag", 5 | "properties": [ 6 | { 7 | "desc": "a property which exposes a name to you", 8 | "name": "name", 9 | "source_lines": [ 10 | 6, 11 | 8 12 | ] 13 | } 14 | ], 15 | "source_lines": [ 16 | 1, 17 | 5 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/description_recurring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description may recur. lots of times. 3 | * 4 | * @desc Here's a little bit. 5 | * @description 6 | * Here's a little bit more. 7 | * 8 | * @desc and how bout a final bit of module desc? 9 | */ 10 | 11 | /** 12 | * @function foo 13 | * The function that's documented all over because in functions... 14 | * @param {string} bar An AWESOME param. 15 | * @description 16 | * ..description can be interspersed... 17 | * @param {string} baz Another AWESOME param. 18 | * @desc between other tags! 19 | */ 20 | -------------------------------------------------------------------------------- /tests/function_omit_name_and_tag.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module with a basic function definition in it,\nwhich omits all tags, letting the system figure out\nwhat we mean.", 3 | "filename": "tests/function_omit_name_and_tag.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames", 7 | "name": "toFilename", 8 | "source_lines": [ 9 | 7, 10 | 9 11 | ] 12 | } 13 | ], 14 | "module": "function_omit_name_and_tag", 15 | "source_lines": [ 16 | 1, 17 | 6 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/module_explicit.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "In YUIDOC like fashion, it's completely allowable to have\nthe first comment block of a file provide module level\ndocumentation. One may also...\n\n... use the description tag to explicitly provide module level\ndocumentation, or they may combine both mechanisms (top commenting\n(and desc tag). In the latter case, docs will be concatenated\nwith a couple newlines.", 3 | "filename": "tests/module_explicit.js", 4 | "module": "module_explicit_renamed", 5 | "source_lines": [ 6 | 1, 7 | 13 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/property_implicit_with_type.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module to test implicit property documentation,\nwhere the developer provides only a description", 3 | "filename": "tests/property_implicit_with_type.js", 4 | "module": "property_implicit_with_type", 5 | "properties": [ 6 | { 7 | "desc": "a property which exposes a name to you", 8 | "name": "name", 9 | "source_lines": [ 10 | 6, 11 | 9 12 | ], 13 | "type": "string" 14 | } 15 | ], 16 | "source_lines": [ 17 | 1, 18 | 5 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/function_omit_name.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module with a basic function definition in it,\nbut without explicit mention of the name of the function\ngiven the heuristic name guessing stuff.", 3 | "filename": "tests/function_omit_name.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames", 7 | "name": "toFilename", 8 | "source_lines": [ 9 | 7, 10 | 11 11 | ] 12 | } 13 | ], 14 | "module": "function_omit_name", 15 | "source_lines": [ 16 | 1, 17 | 6 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/function_supports_type_or_return_or_returns.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module with the three different ways to delcare 3 | * return types. 4 | */ 5 | 6 | /** 7 | * @function toFilename 8 | * a function which converts URLs to filenames 9 | * @returns {string} A string. 10 | */ 11 | 12 | /** 13 | * @function toFilename2 14 | * a function which converts URLs to filenames 15 | * @return {string} A String. 16 | */ 17 | 18 | /** 19 | * @function toFilename3 20 | * a function which converts URLs to filenames 21 | * @return Yeah, A String. 22 | * @type {string} 23 | */ 24 | -------------------------------------------------------------------------------- /tests/type_optional_braces.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "when defining the @type of a property, you chose to omit braces", 3 | "filename": "tests/type_optional_braces.js", 4 | "module": "type_optional_braces", 5 | "properties": [ 6 | { 7 | "name": "foo", 8 | "source_lines": [ 9 | 5, 10 | 9 11 | ], 12 | "type": "string" 13 | }, 14 | { 15 | "name": "bar", 16 | "source_lines": [ 17 | 10, 18 | 13 19 | ], 20 | "type": "string" 21 | } 22 | ], 23 | "source_lines": [ 24 | 1, 25 | 4 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tests/module_inside_class.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@module not allowed in class context at line 7" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "thisContext))" 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/typedef_not_allowed.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@typedef not allowed in global context at line 3" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "thisContext))" 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/constructor_in_global_context.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@constructor not allowed in global context at line 4" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "thisContext))" 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/property_cant_return.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@return not allowed in @property block at line 3" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "(tag, parseData['blockHandler'].tagName))" 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/params_one_way.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A function which contains parameters expressed\nwith type before param name.", 3 | "filename": "tests/params_one_way.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames.", 7 | "name": "toFilename", 8 | "params": [ 9 | { 10 | "desc": "The url to convert", 11 | "name": "url", 12 | "type": "string" 13 | } 14 | ], 15 | "source_lines": [ 16 | 6, 17 | 9 18 | ] 19 | } 20 | ], 21 | "module": "params_one_way", 22 | "source_lines": [ 23 | 1, 24 | 5 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/params_type_last.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A function which contains parameters expressed\nwith name before type.", 3 | "filename": "tests/params_type_last.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames.", 7 | "name": "toFilename", 8 | "params": [ 9 | { 10 | "desc": "The url to convert", 11 | "name": "url", 12 | "type": "string" 13 | } 14 | ], 15 | "source_lines": [ 16 | 6, 17 | 9 18 | ] 19 | } 20 | ], 21 | "module": "params_type_last", 22 | "source_lines": [ 23 | 1, 24 | 5 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/property_cant_have_params.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@param not allowed in @property block at line 3" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "(tag, parseData['blockHandler'].tagName))" 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/params_type_first.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A function which contains parameters expressed\nwith type before param name.", 3 | "filename": "tests/params_type_first.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames.", 7 | "name": "toFilename", 8 | "params": [ 9 | { 10 | "desc": "The url to convert", 11 | "name": "url", 12 | "type": "string" 13 | } 14 | ], 15 | "source_lines": [ 16 | 6, 17 | 9 18 | ] 19 | } 20 | ], 21 | "module": "params_type_first", 22 | "source_lines": [ 23 | 1, 24 | 5 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/function_and_property_in_same_block.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "You can have a single docblock that's both a function\nAND a property, and they'll be automagically split into\ntwo separate blocks", 3 | "filename": "tests/function_and_property_in_same_block.js", 4 | "functions": [ 5 | { 6 | "name": "foo", 7 | "source_lines": [ 8 | 5, 9 | 5 10 | ] 11 | } 12 | ], 13 | "module": "function_and_property_in_same_block", 14 | "properties": [ 15 | { 16 | "name": "bar", 17 | "source_lines": [ 18 | 5, 19 | 5 20 | ] 21 | } 22 | ], 23 | "source_lines": [ 24 | 1, 25 | 4 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tests/params_plural_allowed.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A function which contains parameters expressed\nwith type before param name.", 3 | "filename": "tests/params_plural_allowed.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames.", 7 | "name": "toFilename", 8 | "params": [ 9 | { 10 | "desc": "The url to convert", 11 | "name": "url", 12 | "type": "string" 13 | } 14 | ], 15 | "source_lines": [ 16 | 6, 17 | 9 18 | ] 19 | } 20 | ], 21 | "module": "params_plural_allowed", 22 | "source_lines": [ 23 | 1, 24 | 5 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/params_type_last_newline.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A function which contains parameters expressed\nwith name before type.", 3 | "filename": "tests/params_type_last_newline.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames.", 7 | "name": "toFilename", 8 | "params": [ 9 | { 10 | "desc": "The url to convert", 11 | "name": "url", 12 | "type": "string" 13 | } 14 | ], 15 | "source_lines": [ 16 | 6, 17 | 10 18 | ] 19 | } 20 | ], 21 | "module": "params_type_last_newline", 22 | "source_lines": [ 23 | 1, 24 | 5 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/params_type_first_newline.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A function which contains parameters expressed\nwith type before param name.", 3 | "filename": "tests/params_type_first_newline.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames.", 7 | "name": "toFilename", 8 | "params": [ 9 | { 10 | "desc": "The url to convert", 11 | "name": "url", 12 | "type": "string" 13 | } 14 | ], 15 | "source_lines": [ 16 | 6, 17 | 10 18 | ] 19 | } 20 | ], 21 | "module": "params_type_first_newline", 22 | "source_lines": [ 23 | 1, 24 | 5 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/type_and_returns_in_same_function_good.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "If so desired, the documentor may break up desc and\n type of return value (like in jsdoc) .", 3 | "filename": "tests/type_and_returns_in_same_function_good.js", 4 | "functions": [ 5 | { 6 | "desc": "This function returns a String.", 7 | "name": "imDocumentedWithBothTypeAndReturn", 8 | "returns": { 9 | "desc": "The name of the current user", 10 | "type": "String" 11 | }, 12 | "source_lines": [ 13 | 4, 14 | 9 15 | ] 16 | } 17 | ], 18 | "module": "type_and_returns_in_same_function_good", 19 | "source_lines": [ 20 | 1, 21 | 3 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/guess_name_object_property.js: -------------------------------------------------------------------------------- 1 | // We should be able to guess both the name and type of this property. 2 | // (a function named "destroy") 3 | 4 | /** 5 | * Stops the server from accepting new connections. This function is 6 | * asynchronous, the server is finally closed when the server emits a 7 | * 'close' event. 8 | */ 9 | destroy: function() { 10 | if (this._process) { 11 | this._process.kill(); 12 | this._process = null; 13 | } 14 | this.stdin.destroy(); 15 | this.stdout.destroy(); 16 | this.stderr.destroy(); 17 | this._removeAllListeners("exit") 18 | delete processes[this._guid]; 19 | } 20 | -------------------------------------------------------------------------------- /tests/events_inside_class.js: -------------------------------------------------------------------------------- 1 | // docstract supports a somewhat generic notion of "events". These may 2 | // be associated with a class or the top level file. An event has a 3 | // name, description, and payload. That's it. 4 | 5 | /** @class EventEmittingClass */ 6 | 7 | /** 8 | * @event progress 9 | * Allows the listener to understand approximately how much of the 10 | * page has loaded. 11 | * @payload {number} The percentage (0..100) of page load that is complete 12 | */ 13 | this.eventEmitter._emit("progress", curProgress); 14 | 15 | /** 16 | * @event foo 17 | * A foo event, duh. 18 | * @payload {string} A string with lotsa foo inside! 19 | */ 20 | this.eventEmitter._emit("foo", bar); 21 | 22 | /** @endclass */ 23 | -------------------------------------------------------------------------------- /tests/events_redefined.js: -------------------------------------------------------------------------------- 1 | // docstract supports a somewhat generic notion of "events". These may 2 | // be associated with a class or the top level file. An event has a 3 | // name, description, and payload. That's it. 4 | 5 | /** 6 | * @event progress 7 | * Allows the listener to understand approximately how much of the 8 | * page has loaded. 9 | * @payload {number} The percentage (0..100) of page load that is complete 10 | */ 11 | this.eventEmitter._emit("progress", curProgress); 12 | 13 | /** 14 | * @event progress 15 | * Allows the listener to understand approximately how much of the 16 | * page has loaded. 17 | * @payload {number} The percentage (0..100) of page load that is complete 18 | */ 19 | this.eventEmitter._emit("progress", curProgress); 20 | -------------------------------------------------------------------------------- /tests/see_tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The see tag lets you reference other things. 3 | * You can pass whatever text you want to the see tag. 4 | * 5 | * @see #module/file/read 6 | * 7 | * @desc in the case above, we're passing something that 8 | * looks like a hashtag. 9 | * 10 | * @see #methodName 11 | * @seealso #ClassName 12 | * @see_also ClassName#methodName 13 | * 14 | * @desc 15 | * Those three examples use the forms that jsdoc likes to 16 | * eat. But really, it's all up to your rendering code. 17 | * So go ahead, make a convention, it's fun! 18 | */ 19 | 20 | /** 21 | * @func MyFunc 22 | * Now functions too can have see tags. They can occur 23 | * in almost any type of documentation block. 24 | * @see DoYouSeeNow? 25 | * @see HowAboutNow? 26 | */ 27 | -------------------------------------------------------------------------------- /tests/return_needs_argument.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@return tag requires an argument at line 5" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "self._consumeToks(tokens, parseData)" 22 | ], 23 | [ 24 | "_consumeToks", 25 | "raise RuntimeError(\"%s tag requires an argument\" % cur)" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/throws_needs_argument.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@throws tag requires an argument at line 5" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "self._consumeToks(tokens, parseData)" 22 | ], 23 | [ 24 | "_consumeToks", 25 | "raise RuntimeError(\"%s tag requires an argument\" % cur)" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/throws_needs_argument_not_tag.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "@throws tag requires an argument at line 7" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "self._consumeToks(tokens, parseData)" 22 | ], 23 | [ 24 | "_consumeToks", 25 | "raise RuntimeError(\"%s tag requires an argument\" % cur)" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/throws_multiple_types.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "a module with a function thats really throwey.", 3 | "filename": "tests/throws_multiple_types.js", 4 | "functions": [ 5 | { 6 | "name": "throweyFunc", 7 | "source_lines": [ 8 | 5, 9 | 11 10 | ], 11 | "throws": [ 12 | { 13 | "desc": "When you are sleeping" 14 | }, 15 | { 16 | "desc": "When you're awake" 17 | }, 18 | { 19 | "desc": "if you've been bad or good.", 20 | "type": "object" 21 | }, 22 | { 23 | "desc": "So be good for goodness sake." 24 | } 25 | ] 26 | } 27 | ], 28 | "module": "throws_multiple_types", 29 | "source_lines": [ 30 | 1, 31 | 4 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tests/class_redefinition.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "class 'Foo' redefined at line 6" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "parseData['blockHandler'].merge(doc, stack[-1][1], guessedName, self._whatContext(stack))" 22 | ], 23 | [ 24 | "merge", 25 | "raise RuntimeError(\"class '%s' redefined\" % doc[\"name\"])" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/type_and_returns_in_same_function_bad.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "Return type redefined (@type and @returns in same function block?) at line 4" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "handler.attach(v, doc, parseData['blockHandler'].tagName)" 22 | ], 23 | [ 24 | "attach", 25 | "\"same function block?)\")" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/events_redefined.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "'progress' event redefined at line 13" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "parseData['blockHandler'].merge(doc, stack[-1][1], guessedName, self._whatContext(stack))" 22 | ], 23 | [ 24 | "merge", 25 | "raise RuntimeError(\"'%s' event redefined\" % doc[\"name\"])" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/guess_cant_guess_prop.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "can't determine property name at line 1" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "parseData['blockHandler'].merge(doc, stack[-1][1], guessedName, self._whatContext(stack))" 22 | ], 23 | [ 24 | "merge", 25 | "raise RuntimeError(\"can't determine property name\")" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/fun_with_types.out: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "tests/fun_with_types.js", 3 | "module": "fun_with_types", 4 | "properties": [ 5 | { 6 | "desc": "This is an array of strings", 7 | "name": "arrayOfStrings", 8 | "source_lines": [ 9 | 1, 10 | 4 11 | ], 12 | "type": "array of strings" 13 | }, 14 | { 15 | "desc": "This is an another array of strings", 16 | "name": "arrayOfStrings2", 17 | "source_lines": [ 18 | 7, 19 | 10 20 | ], 21 | "type": "strings[]" 22 | }, 23 | { 24 | "desc": "This is a *third* array of strings", 25 | "name": "arrayOfStrings3", 26 | "source_lines": [ 27 | 13, 28 | 16 29 | ], 30 | "type": "Strings [ ]" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tests/function_redefinition.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "function 'Foo' redefined at line 4" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "parseData['blockHandler'].merge(doc, stack[-1][1], guessedName, self._whatContext(stack))" 22 | ], 23 | [ 24 | "merge", 25 | "raise RuntimeError(\"function '%s' redefined\" % doc[\"name\"])" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/property_redefinition.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "property 'Foo' redefined at line 4" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "parseData['blockHandler'].merge(doc, stack[-1][1], guessedName, self._whatContext(stack))" 22 | ], 23 | [ 24 | "merge", 25 | "raise RuntimeError(\"property '%s' redefined\" % doc[\"name\"])" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/guess_obj_property.out: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "tests/guess_obj_property.js", 3 | "functions": [ 4 | { 5 | "desc": "Closes both the stream and its backing stream. If the stream is already\nclosed, an exception is thrown. For TextWriters, this first flushes the\nbacking stream's buffer.", 6 | "name": "close", 7 | "source_lines": [ 8 | 4, 9 | 8 10 | ] 11 | }, 12 | { 13 | "desc": "Writes a string to the stream. If the stream is closed, an exception is\nthrown.", 14 | "name": "write", 15 | "params": [ 16 | { 17 | "desc": "The string to write.", 18 | "name": "str" 19 | } 20 | ], 21 | "source_lines": [ 22 | 14, 23 | 20 24 | ] 25 | } 26 | ], 27 | "module": "guess_obj_property" 28 | } 29 | -------------------------------------------------------------------------------- /tests/type_doesnt_accept_desc.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "Bogus arguments to @type: (2): {string} | no no, no ... at line 1" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "self._consumeToks(tokens, parseData)" 22 | ], 23 | [ 24 | "_consumeToks", 25 | "ctx = handler.parse(args)" 26 | ], 27 | [ 28 | "parse", 29 | "(self.tagName, self._argPrint(args)))" 30 | ] 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/class_with_constructor.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "constructors": [ 5 | { 6 | "desc": "This is the constructor of the class", 7 | "params": [ 8 | { 9 | "desc": "it accepts a bar argument of type foo", 10 | "name": "bar", 11 | "type": "foo" 12 | } 13 | ], 14 | "source_lines": [ 15 | 11, 16 | 16 17 | ] 18 | } 19 | ], 20 | "desc": "A class which parses a url and exposes its various\ncomponents separately.", 21 | "name": "URL", 22 | "source_lines": [ 23 | 5, 24 | 10 25 | ] 26 | } 27 | ], 28 | "desc": "A test of basic class parsing", 29 | "filename": "tests/class_with_constructor.js", 30 | "module": "class_with_constructor", 31 | "source_lines": [ 32 | 1, 33 | 4 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tests/guess_firstblock_precedence_over_assignment.js: -------------------------------------------------------------------------------- 1 | // This test checks the precedence rules of guessing functions. 2 | // specifically the first block should be considered the module 3 | // documentation, even when there's assignment in the code chunk 4 | // after it. 5 | 6 | /** 7 | * Allows one to control fullscreen view for the main application window 8 | */ 9 | 10 | var mainWin = require("window-utils"); 11 | 12 | /** 13 | * Size the main application window to consume the full screen 14 | */ 15 | exports.enable = function() { 16 | mainWin.activeWindow.fullScreen=true; 17 | }; 18 | 19 | /** 20 | * Disable fullscreen mode (noop if it wasn't enabled) 21 | */ 22 | exports.disable = function() { 23 | mainWin.activeWindow.fullScreen=false; 24 | }; 25 | 26 | /** 27 | * Toggle fullscreen. 28 | */ 29 | exports.toggle = function() { 30 | mainWin.activeWindow.fullScreen=!mainWin.activeWindow.fullScreen; 31 | }; 32 | -------------------------------------------------------------------------------- /tests/guess_failure.out: -------------------------------------------------------------------------------- 1 | { 2 | "args": [ 3 | "Can't determine what this block documents (from @function, @event, @property, @class, @module) at line 5" 4 | ], 5 | "exception_type": "", 6 | "stack": [ 7 | [ 8 | "", 9 | "got = ds.extractFromFile(os.path.join(testDir, test + \".js\"))" 10 | ], 11 | [ 12 | "extractFromFile", 13 | "data = self.extract(contents)" 14 | ], 15 | [ 16 | "extract", 17 | "self._analyzeBlock(block, context, firstBlock, stack, lineStart, line)" 18 | ], 19 | [ 20 | "_analyzeBlock", 21 | "guessedType = self._guessBlockType(firstBlock, codeChunk, thisContext, parseData['tagData'].keys())" 22 | ], 23 | [ 24 | "_guessBlockType", 25 | "raise RuntimeError(\"Can't determine what this block documents (from %s)\" % \", \".join(possibilities))" 26 | ] 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/guess_firstblock_precedence_over_assignment.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "Allows one to control fullscreen view for the main application window", 3 | "filename": "tests/guess_firstblock_precedence_over_assignment.js", 4 | "functions": [ 5 | { 6 | "desc": "Size the main application window to consume the full screen", 7 | "name": "enable", 8 | "source_lines": [ 9 | 12, 10 | 14 11 | ] 12 | }, 13 | { 14 | "desc": "Disable fullscreen mode (noop if it wasn't enabled)", 15 | "name": "disable", 16 | "source_lines": [ 17 | 19, 18 | 21 19 | ] 20 | }, 21 | { 22 | "desc": "Toggle fullscreen.", 23 | "name": "toggle", 24 | "source_lines": [ 25 | 26, 26 | 28 27 | ] 28 | } 29 | ], 30 | "module": "guess_firstblock_precedence_over_assignment", 31 | "source_lines": [ 32 | 6, 33 | 9 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tests/description_recurring.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "Description may recur. lots of times.\n\nHere's a little bit.\n\nHere's a little bit more.\n\nand how bout a final bit of module desc?", 3 | "filename": "tests/description_recurring.js", 4 | "functions": [ 5 | { 6 | "desc": "The function that's documented all over because in functions...\n\n..description can be interspersed...\n\nbetween other tags!", 7 | "name": "foo", 8 | "params": [ 9 | { 10 | "desc": "An AWESOME param.", 11 | "name": "bar", 12 | "type": "string" 13 | }, 14 | { 15 | "desc": "Another AWESOME param.", 16 | "name": "baz", 17 | "type": "string" 18 | } 19 | ], 20 | "source_lines": [ 21 | 11, 22 | 19 23 | ] 24 | } 25 | ], 26 | "module": "description_recurring", 27 | "source_lines": [ 28 | 1, 29 | 10 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tests/guess_obj_property.js: -------------------------------------------------------------------------------- 1 | // assignment to an object property should be name-guessable, this test 2 | // has two examples. 3 | 4 | /** 5 | * Closes both the stream and its backing stream. If the stream is already 6 | * closed, an exception is thrown. For TextWriters, this first flushes the 7 | * backing stream's buffer. 8 | */ 9 | stream.close = function stream_close() { 10 | self.ensureOpened(); 11 | self.unload(); 12 | }; 13 | 14 | /** 15 | * Writes a string to the stream. If the stream is closed, an exception is 16 | * thrown. 17 | * 18 | * @param str 19 | * The string to write. 20 | */ 21 | this.write = function TextWriter_write(str) { 22 | manager.ensureOpened(); 23 | let istream = uconv.convertToInputStream(str); 24 | let len = istream.available(); 25 | while (len > 0) { 26 | stream.writeFrom(istream, len); 27 | len = istream.available(); 28 | } 29 | istream.close(); 30 | }; 31 | -------------------------------------------------------------------------------- /tests/see_tag.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "The see tag lets you reference other things.\nYou can pass whatever text you want to the see tag.\n\nin the case above, we're passing something that\nlooks like a hashtag.\n\nThose three examples use the forms that jsdoc likes to\neat. But really, it's all up to your rendering code.\nSo go ahead, make a convention, it's fun!", 3 | "filename": "tests/see_tag.js", 4 | "functions": [ 5 | { 6 | "desc": "Now functions too can have see tags. They can occur\nin almost any type of documentation block.", 7 | "name": "MyFunc", 8 | "see": [ 9 | "DoYouSeeNow?", 10 | "HowAboutNow?" 11 | ], 12 | "source_lines": [ 13 | 20, 14 | 26 15 | ] 16 | } 17 | ], 18 | "module": "see_tag", 19 | "see": [ 20 | "#module/file/read", 21 | "#methodName", 22 | "#ClassName", 23 | "ClassName#methodName" 24 | ], 25 | "source_lines": [ 26 | 1, 27 | 19 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tests/class_simple_nested.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "classes": [ 5 | { 6 | "constructors": [ 7 | { 8 | "desc": "bar's constructor!", 9 | "source_lines": [ 10 | 7, 11 | 8 12 | ] 13 | } 14 | ], 15 | "name": "Bar", 16 | "source_lines": [ 17 | 6, 18 | 6 19 | ] 20 | } 21 | ], 22 | "constructors": [ 23 | { 24 | "desc": "foo's constructor!", 25 | "source_lines": [ 26 | 4, 27 | 5 28 | ] 29 | } 30 | ], 31 | "name": "Foo", 32 | "source_lines": [ 33 | 3, 34 | 3 35 | ] 36 | } 37 | ], 38 | "desc": "Simple test of nested class support", 39 | "filename": "tests/class_simple_nested.js", 40 | "module": "class_simple_nested", 41 | "source_lines": [ 42 | 1, 43 | 2 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /tests/events_inside_class.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "events": [ 5 | { 6 | "desc": "Allows the listener to understand approximately how much of the\npage has loaded.", 7 | "name": "progress", 8 | "payload": { 9 | "desc": "The percentage (0..100) of page load that is complete", 10 | "type": "number" 11 | }, 12 | "source_lines": [ 13 | 7, 14 | 12 15 | ] 16 | }, 17 | { 18 | "desc": "A foo event, duh.", 19 | "name": "foo", 20 | "payload": { 21 | "desc": "A string with lotsa foo inside!", 22 | "type": "string" 23 | }, 24 | "source_lines": [ 25 | 15, 26 | 19 27 | ] 28 | } 29 | ], 30 | "name": "EventEmittingClass", 31 | "source_lines": [ 32 | 5, 33 | 6 34 | ] 35 | } 36 | ], 37 | "filename": "tests/events_inside_class.js", 38 | "module": "events_inside_class" 39 | } 40 | -------------------------------------------------------------------------------- /tests/class_with_prop_and_func.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "desc": "A class which parses a url and exposes its various\ncomponents separately.", 5 | "functions": [ 6 | { 7 | "desc": "the foo property of my url classs", 8 | "name": "bar", 9 | "returns": { 10 | "desc": "nothing." 11 | }, 12 | "source_lines": [ 13 | 17, 14 | 22 15 | ] 16 | } 17 | ], 18 | "name": "URL", 19 | "properties": [ 20 | { 21 | "desc": "the foo property of my url classs", 22 | "name": "foo", 23 | "source_lines": [ 24 | 11, 25 | 16 26 | ], 27 | "type": "string" 28 | } 29 | ], 30 | "source_lines": [ 31 | 5, 32 | 10 33 | ] 34 | } 35 | ], 36 | "desc": "A simple class with a property and a function", 37 | "filename": "tests/class_with_prop_and_func.js", 38 | "module": "class_with_prop_and_func", 39 | "source_lines": [ 40 | 1, 41 | 4 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /tests/class_with_constructors.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "constructors": [ 5 | { 6 | "desc": "This is the first constructor of the class", 7 | "params": [ 8 | { 9 | "desc": "it accepts a bar argument of type foo", 10 | "name": "bar", 11 | "type": "foo" 12 | } 13 | ], 14 | "source_lines": [ 15 | 11, 16 | 17 17 | ] 18 | }, 19 | { 20 | "desc": "This is the second (no-arg) constructor of the class", 21 | "source_lines": [ 22 | 18, 23 | 22 24 | ] 25 | } 26 | ], 27 | "desc": "A class which parses a url and exposes its various\ncomponents separately.", 28 | "name": "URL", 29 | "source_lines": [ 30 | 5, 31 | 10 32 | ] 33 | } 34 | ], 35 | "desc": "A test of basic class parsing", 36 | "filename": "tests/class_with_constructors.js", 37 | "module": "class_with_constructors", 38 | "source_lines": [ 39 | 1, 40 | 4 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /tests/function_supports_type_or_return_or_returns.out: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "A module with the three different ways to delcare\nreturn types.", 3 | "filename": "tests/function_supports_type_or_return_or_returns.js", 4 | "functions": [ 5 | { 6 | "desc": "a function which converts URLs to filenames", 7 | "name": "toFilename", 8 | "returns": { 9 | "desc": "A string.", 10 | "type": "string" 11 | }, 12 | "source_lines": [ 13 | 6, 14 | 11 15 | ] 16 | }, 17 | { 18 | "desc": "a function which converts URLs to filenames", 19 | "name": "toFilename2", 20 | "returns": { 21 | "desc": "A String.", 22 | "type": "string" 23 | }, 24 | "source_lines": [ 25 | 12, 26 | 17 27 | ] 28 | }, 29 | { 30 | "desc": "a function which converts URLs to filenames", 31 | "name": "toFilename3", 32 | "returns": { 33 | "desc": "Yeah, A String.", 34 | "type": "string" 35 | }, 36 | "source_lines": [ 37 | 18, 38 | 23 39 | ] 40 | } 41 | ], 42 | "module": "function_supports_type_or_return_or_returns", 43 | "source_lines": [ 44 | 1, 45 | 5 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /tests/params_complex.js: -------------------------------------------------------------------------------- 1 | // a function which accepts arguments embedded inside an object 2 | 3 | /** 4 | * @function foo 5 | * 6 | * This function ... blah blah blah 7 | * 8 | * @param options {object 9 | * 10 | * @prop url {string} 11 | * This is the url to which the request will be made. 12 | * 13 | * @prop [onComplete] {function} 14 | * 15 | * This function will be called when the request has received a response (or in 16 | * terms of XHR, when `readyState == 4`). The function is passed a `Response` 17 | * object. 18 | * 19 | * @prop [headers] {object} 20 | * An unordered collection of name/value pairs representing headers to send 21 | * with the request. 22 | * 23 | * @prop [content] {string,object} 24 | * 25 | * The content to send to the server. If `content` is a string, it 26 | * should be URL-encoded (use `encodeURIComponent`). If `content` is 27 | * an object, it should be a collection of name/value pairs. Nested 28 | * objects & arrays should encode safely. 29 | * 30 | * For `GET` requests, the query string (`content`) will be appended 31 | * to the URL. For `POST` requests, the query string will be sent as 32 | * the body of the request. 33 | * 34 | * @prop [contentType] {string} 35 | * 36 | * The type of content to send to the server. This explicitly sets the 37 | * `Content-Type` header. The default value is `application/x-www-form-urlencoded`. 38 | * } 39 | */ 40 | -------------------------------------------------------------------------------- /tests/class_full.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A test of generation of documentation for nested classes. 3 | * (test data is a snapshot in time of a url parsing library) 4 | */ 5 | /** 6 | * @function fromPath 7 | * build a URL from a filename. 8 | * @param {string} path The path to convert. 9 | * @returns {string} 10 | * A string representation of a URL. 11 | */ 12 | /** 13 | * @function toPath 14 | * build a filename from a url. 15 | * @param {string} path The path to convert. 16 | * @returns {string} 17 | * A string representation of a URL. 18 | */ 19 | /** 20 | * @class URL 21 | * A class which parses a url and exposes its various 22 | * components separately. 23 | */ 24 | /** 25 | * @constructor 26 | * 27 | * The URL constructor creates an object that represents a URL, verifying that 28 | * the provided string is a valid URL in the process. 29 | * 30 | * @param {string} url A string to be converted into a URL. 31 | * @param {string} [base] A optional base url which will be used to resolve the 32 | * `url` argument if it is a relative url. 33 | * 34 | * @throws If `source` is not a valid URI. 35 | */ 36 | /** 37 | * @property scheme {string} 38 | * The name of the protocol in the URL. 39 | */ 40 | /** 41 | * @property userPass {string} 42 | * The username:password part of the URL, `null` if not present. 43 | */ 44 | /** 45 | * @property host {string} 46 | * The host of the URL, `null` if not present. 47 | */ 48 | /** 49 | * @property port {integer} 50 | * The port number of the URL, `null` if none was specified. 51 | */ 52 | /** 53 | * @property path {string} 54 | * The path component of the URL. 55 | */ 56 | /** 57 | * @function toString 58 | * Converts a URL to a string. 59 | * @returns {string} The URL as a string. 60 | */ 61 | /** @endclass */ 62 | -------------------------------------------------------------------------------- /tests/yucky_nested_curlies.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "desc": "The `Request` object is used to make `GET` or `POST` network requests. It is\nconstructed with a URL to which the request is sent. Optionally the user may\nspecify a collection of headers and content to send alongside the request and\na callback which will be executed once the request completes.\n\nOnce a `Request` object has been created a `GET` request can be executed by\ncalling its `get()` method, or a `POST` request by calling its `post()` method.\n\nWhen the server completes the request, the `Request` object emits a \"complete\"\nevent. Registered event listeners are passed a `Response` object.\n\nEach `Request` object is designed to be used once. Once `GET` or `POST` are\ncalled, attempting to call either will throw an error.\n\nSince the request is not being made by any particular website, requests made\nhere are not subject to the same-domain restriction that requests made in web\npages are subject to.\n\nWith the exception of `response`, all of a `Request` object's properties\ncorrespond with the options in the constructor. Each can be set by simply\nperforming an assignment. However, keep in mind that the same validation rules\nthat apply to `options` in the constructor will apply during assignment. Thus,\neach can throw if given an invalid value.\n\nThe example below shows how to use Request to get the most recent public tweet.\n\n var Request = require('request').Request;\n var latestTweetRequest = Request({\n url: \"http://api.twitter.com/1/statuses/public_timeline.json\",\n onComplete: function (response) {\n var tweet = response.json[0];\n console.log(\"User: \" + tweet.user.screen_name);\n console.log(\"Tweet: \" + tweet.text);\n }\n });\n\n // Be a good consumer and check for rate limiting before doing more.\n Request({\n url: \"http://api.twitter.com/1/account/rate_limit_status.json\",\n onComplete: function (response) {\n if (response.json.remaining_hits) {\n latestTweetRequest.get();\n } else {\n console.log(\"You have been rate limited!\");\n }\n }\n }).get();", 5 | "name": "Request", 6 | "source_lines": [ 7 | 1, 8 | 52 9 | ] 10 | } 11 | ], 12 | "filename": "tests/yucky_nested_curlies.js", 13 | "module": "yucky_nested_curlies" 14 | } 15 | -------------------------------------------------------------------------------- /tests/yucky_nested_curlies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Request 3 | * The `Request` object is used to make `GET` or `POST` network requests. It is 4 | * constructed with a URL to which the request is sent. Optionally the user may 5 | * specify a collection of headers and content to send alongside the request and 6 | * a callback which will be executed once the request completes. 7 | * 8 | * Once a `Request` object has been created a `GET` request can be executed by 9 | * calling its `get()` method, or a `POST` request by calling its `post()` method. 10 | * 11 | * When the server completes the request, the `Request` object emits a "complete" 12 | * event. Registered event listeners are passed a `Response` object. 13 | * 14 | * Each `Request` object is designed to be used once. Once `GET` or `POST` are 15 | * called, attempting to call either will throw an error. 16 | * 17 | * Since the request is not being made by any particular website, requests made 18 | * here are not subject to the same-domain restriction that requests made in web 19 | * pages are subject to. 20 | * 21 | * With the exception of `response`, all of a `Request` object's properties 22 | * correspond with the options in the constructor. Each can be set by simply 23 | * performing an assignment. However, keep in mind that the same validation rules 24 | * that apply to `options` in the constructor will apply during assignment. Thus, 25 | * each can throw if given an invalid value. 26 | * 27 | * The example below shows how to use Request to get the most recent public tweet. 28 | * 29 | * var Request = require('request').Request; 30 | * var latestTweetRequest = Request({ 31 | * url: "http://api.twitter.com/1/statuses/public_timeline.json", 32 | * onComplete: function (response) { 33 | * var tweet = response.json[0]; 34 | * console.log("User: " + tweet.user.screen_name); 35 | * console.log("Tweet: " + tweet.text); 36 | * } 37 | * }); 38 | * 39 | * // Be a good consumer and check for rate limiting before doing more. 40 | * Request({ 41 | * url: "http://api.twitter.com/1/account/rate_limit_status.json", 42 | * onComplete: function (response) { 43 | * if (response.json.remaining_hits) { 44 | * latestTweetRequest.get(); 45 | * } else { 46 | * console.log("You have been rate limited!"); 47 | * } 48 | * } 49 | * }).get(); 50 | * @endclass 51 | */ 52 | 53 | -------------------------------------------------------------------------------- /tests/params_complex.out: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "tests/params_complex.js", 3 | "functions": [ 4 | { 5 | "desc": "This function ... blah blah blah", 6 | "name": "foo", 7 | "params": [ 8 | { 9 | "name": "options", 10 | "type": { 11 | "name": "object", 12 | "properties": [ 13 | { 14 | "desc": " This is the url to which the request will be made.", 15 | "name": "url", 16 | "source_lines": [ 17 | 3, 18 | 39 19 | ], 20 | "type": "string" 21 | }, 22 | { 23 | "desc": " This function will be called when the request has received a response (or in\n terms of XHR, when `readyState == 4`). The function is passed a `Response`\n object.", 24 | "name": "onComplete", 25 | "optional": true, 26 | "source_lines": [ 27 | 3, 28 | 39 29 | ], 30 | "type": "function" 31 | }, 32 | { 33 | "desc": " An unordered collection of name/value pairs representing headers to send\n with the request.", 34 | "name": "headers", 35 | "optional": true, 36 | "source_lines": [ 37 | 3, 38 | 39 39 | ], 40 | "type": "object" 41 | }, 42 | { 43 | "desc": " The content to send to the server. If `content` is a string, it\n should be URL-encoded (use `encodeURIComponent`). If `content` is\n an object, it should be a collection of name/value pairs. Nested\n objects & arrays should encode safely.\n\n For `GET` requests, the query string (`content`) will be appended\n to the URL. For `POST` requests, the query string will be sent as\n the body of the request.", 44 | "name": "content", 45 | "optional": true, 46 | "source_lines": [ 47 | 3, 48 | 39 49 | ], 50 | "type": "string,object" 51 | }, 52 | { 53 | "desc": " The type of content to send to the server. This explicitly sets the\n `Content-Type` header. The default value is `application/x-www-form-urlencoded`.", 54 | "name": "contentType", 55 | "optional": true, 56 | "source_lines": [ 57 | 3, 58 | 39 59 | ], 60 | "type": "string" 61 | } 62 | ] 63 | } 64 | } 65 | ], 66 | "source_lines": [ 67 | 3, 68 | 39 69 | ] 70 | } 71 | ], 72 | "module": "params_complex" 73 | } 74 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2011, Lloyd Hilaiel 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # running this file from the command line executes its self tests. 18 | import re 19 | import os 20 | import json 21 | import sys 22 | import difflib 23 | import traceback 24 | from docstract import DocStract 25 | 26 | ds = DocStract() 27 | 28 | # because docextractor embeds filenames into output files, let's 29 | # change into the directory of the script for consistency 30 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 31 | 32 | testDir = "tests" 33 | 34 | # create a list of the tests to run (.js files in tests/ dir) 35 | tests = [] 36 | 37 | # allow invoker on command line to pass in tests explicitly for 38 | # selective testing 39 | if len (sys.argv) > 1: 40 | for x in sys.argv[1:]: 41 | x = os.path.basename(x) 42 | tests.append(x[:-3] if x.endswith(".js") else x) 43 | else: 44 | for x in os.listdir(testDir): 45 | if x.endswith(".js"): 46 | tests.append(x[:-3]) 47 | 48 | # now run! 49 | ranTests = 0 50 | failedTests = 0 51 | for test in tests: 52 | print "Running '%s'..." % test 53 | failed = False 54 | try: 55 | got = ds.extractFromFile(os.path.join(testDir, test + ".js")) 56 | except Exception as e: 57 | stack = [] 58 | l = traceback.extract_tb(sys.exc_info()[2]) 59 | for x in l: 60 | stack.append([ x[2], x[3] ]) 61 | got = { "exception_type": str(type(e)), "args": e.args, "stack": stack } 62 | want = None 63 | try: 64 | with open(os.path.join(testDir, test + ".out"), "r") as f: 65 | want = json.loads(f.read()) 66 | except: 67 | pass 68 | 69 | gotJSON = json.dumps(got, indent=2, sort_keys=True) + "\n" 70 | 71 | # now let's compare actual with expected 72 | if want == None: 73 | print " FAILED: no expected test output file available (%s.out)" % test 74 | failed = True 75 | else: 76 | wantJSON = json.dumps(want, indent=2, sort_keys=True) + "\n" 77 | 78 | diff = difflib.unified_diff(wantJSON.splitlines(1), gotJSON.splitlines(1), "expected.out", "actual.out") 79 | 80 | # diff does poorly when newlines are ommitted, let's fix that 81 | diff = [l if len(l) > 0 and l[-1] == '\n' else l + "\n" for l in diff] 82 | diffText = ' '.join(diff) 83 | 84 | if len(diffText): 85 | diffText = ' ' + diffText 86 | print " FAILED: actual output doesn't match expected:" 87 | print diffText 88 | failed = True 89 | else: 90 | print " ... passed." 91 | 92 | if failed: 93 | failedTests += 1 94 | # write actual output to disk, so that it's easy to write new tests 95 | actualPath = os.path.join(testDir, test + ".outactual") 96 | with open(actualPath, "w+") as f: 97 | f.write(gotJSON) 98 | 99 | print " (expected output left in '%s')" % actualPath 100 | 101 | print "Complete, (%d/%d) tests passed..." % (len(tests) - failedTests, len(tests)) 102 | sys.exit(failedTests) 103 | -------------------------------------------------------------------------------- /tests/class_full.out: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "constructors": [ 5 | { 6 | "desc": "The URL constructor creates an object that represents a URL, verifying that\nthe provided string is a valid URL in the process.", 7 | "params": [ 8 | { 9 | "desc": "A string to be converted into a URL.", 10 | "name": "url", 11 | "type": "string" 12 | }, 13 | { 14 | "desc": "A optional base url which will be used to resolve the\n`url` argument if it is a relative url.", 15 | "name": "base", 16 | "optional": true, 17 | "type": "string" 18 | } 19 | ], 20 | "source_lines": [ 21 | 24, 22 | 35 23 | ], 24 | "throws": [ 25 | { 26 | "desc": "If `source` is not a valid URI." 27 | } 28 | ] 29 | } 30 | ], 31 | "desc": "A class which parses a url and exposes its various\ncomponents separately.", 32 | "functions": [ 33 | { 34 | "desc": "Converts a URL to a string.", 35 | "name": "toString", 36 | "returns": { 37 | "desc": "The URL as a string.", 38 | "type": "string" 39 | }, 40 | "source_lines": [ 41 | 56, 42 | 60 43 | ] 44 | } 45 | ], 46 | "name": "URL", 47 | "properties": [ 48 | { 49 | "desc": "The name of the protocol in the URL.", 50 | "name": "scheme", 51 | "source_lines": [ 52 | 36, 53 | 39 54 | ], 55 | "type": "string" 56 | }, 57 | { 58 | "desc": "The username:password part of the URL, `null` if not present.", 59 | "name": "userPass", 60 | "source_lines": [ 61 | 40, 62 | 43 63 | ], 64 | "type": "string" 65 | }, 66 | { 67 | "desc": "The host of the URL, `null` if not present.", 68 | "name": "host", 69 | "source_lines": [ 70 | 44, 71 | 47 72 | ], 73 | "type": "string" 74 | }, 75 | { 76 | "desc": "The port number of the URL, `null` if none was specified.", 77 | "name": "port", 78 | "source_lines": [ 79 | 48, 80 | 51 81 | ], 82 | "type": "integer" 83 | }, 84 | { 85 | "desc": "The path component of the URL.", 86 | "name": "path", 87 | "source_lines": [ 88 | 52, 89 | 55 90 | ], 91 | "type": "string" 92 | } 93 | ], 94 | "source_lines": [ 95 | 19, 96 | 23 97 | ] 98 | } 99 | ], 100 | "desc": "A test of generation of documentation for nested classes.\n(test data is a snapshot in time of a url parsing library)", 101 | "filename": "tests/class_full.js", 102 | "functions": [ 103 | { 104 | "desc": "build a URL from a filename.", 105 | "name": "fromPath", 106 | "params": [ 107 | { 108 | "desc": "The path to convert.", 109 | "name": "path", 110 | "type": "string" 111 | } 112 | ], 113 | "returns": { 114 | "desc": "A string representation of a URL.", 115 | "type": "string" 116 | }, 117 | "source_lines": [ 118 | 5, 119 | 11 120 | ] 121 | }, 122 | { 123 | "desc": "build a filename from a url.", 124 | "name": "toPath", 125 | "params": [ 126 | { 127 | "desc": "The path to convert.", 128 | "name": "path", 129 | "type": "string" 130 | } 131 | ], 132 | "returns": { 133 | "desc": "A string representation of a URL.", 134 | "type": "string" 135 | }, 136 | "source_lines": [ 137 | 12, 138 | 18 139 | ] 140 | } 141 | ], 142 | "module": "class_full", 143 | "source_lines": [ 144 | 1, 145 | 4 146 | ] 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The Warning 2 | 3 | This is an idea for a project and some pretty rough code. When you 4 | don't see this message anymore, that shall indicate that this might 5 | acutally be useful. 6 | 7 | ## What 8 | 9 | **docstract** parses documentation out of JavaScript source and 10 | outputs JSON. It is built for JavaScript, it won't ever work with 11 | other languages. Nor will it ever render documentation, that's your 12 | job. 13 | 14 | ## A Tirade 15 | 16 | Most existing documentation generators work with multiple languages, 17 | and many generate documentation in many different formats. The 18 | problem with applying these tools to a language as dynamic as 19 | JavaScript, is that they never do a great job. Most non-trivial 20 | JavaScript APIs include custom conventions and idioms that confuse the 21 | snot out of your average documentation extractor. 22 | 23 | Further the problem with documentation extractors that generate their 24 | own human readable output (i.e. in PDF or HTML) is that there's never 25 | any one format that will please everyone. It may be desired to render 26 | output in a custom format, or at least to allow multiple different 27 | rendering frameworks. Breaking the tasks of extraction and rendering 28 | apart, allows for more flexibility in both phases. 29 | 30 | **docstract** does less. It's goal is to be as small as possible to 31 | solve the problem of extraction: flexibly parsing human generated 32 | in-source comments, and turning them into well structured computer 33 | readable JSON output. 34 | 35 | ## (Desired) Features 36 | 37 | * command line or programmatic usage (from python) 38 | * (not yet) extensible to add support for custom tags 39 | * developer-ergonomic parsing - lets you be sloppy wherever possible. 40 | * (not yet) highly configurable 41 | * super fun unit test system. 42 | 43 | ## Example 44 | 45 | Input: 46 | 47 | /** 48 | * My simple module that is really, really awesome. Really. 49 | */ 50 | 51 | /** 52 | * A function which can join members of an array together with strings 53 | * @param {array} strings Strings to join together 54 | */ 55 | exports.join = function join(strings) { 56 | return strings.join(","); 57 | }; 58 | 59 | Output: 60 | 61 | { 62 | "desc": "My simple module that is really, really awesome. Really.", 63 | "filename": "my_module.js", 64 | "functions": [ 65 | { 66 | "desc": "A function which can join members of an array together with strings", 67 | "name": "join", 68 | "params": [ 69 | { 70 | "desc": "Strings to join together", 71 | "name": "strings", 72 | "type": "array" 73 | } 74 | ] 75 | } 76 | ], 77 | "module": "my_module" 78 | } 79 | 80 | ## Usage 81 | 82 | From the command line: 83 | 84 | ./docstract.py myfile.js 85 | 86 | or from a python program: 87 | 88 | from docstract import DocStract 89 | ds = DocStract() 90 | docsFromString = ds.extract(javascriptFileContents) 91 | docsFromFile = ds.extractFromFile(javascriptFileName) 92 | 93 | ## Documentation 94 | 95 | See the tests/ directory for now. Better docs? Real soon now. 96 | 97 | ## Shout-Outs 98 | 99 | [YUIDOC](http://developer.yahoo.com/yui/yuidoc/) provided inspiration and 100 | some regular expressions that are used in docstract. 101 | 102 | ## License 103 | 104 | Copyright (c) 2011, Lloyd Hilaiel 105 | 106 | Permission to use, copy, modify, and/or distribute this software for any 107 | purpose with or without fee is hereby granted, provided that the above 108 | copyright notice and this permission notice appear in all copies. 109 | 110 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 111 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 112 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 113 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 114 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 115 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 116 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 117 | -------------------------------------------------------------------------------- /docstract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2011, Lloyd Hilaiel 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | import re 18 | import os 19 | import types 20 | 21 | class DocStract(): 22 | class Type(): 23 | def __init__(self, val, orig): 24 | val.pop('source_lines') 25 | if (val.has_key('name') and len(val) == 1): 26 | self.value = val['name'] 27 | else: 28 | self.value = val 29 | self.orig = orig 30 | 31 | def __repr__(self): 32 | return self.orig 33 | 34 | def __init__(self): 35 | # the patterns for finding and processing documentation blocks (and the source line 36 | # Note: these two patterns are identical, except the latter captures groups. The 37 | # first is used to split a source file into chunks of text which are either doc blocks 38 | # or source code, the second extracts information from doc blocks. 39 | self.docBlockFindPat = re.compile('(/\*\* .*? \*/ (?:[\s\n]*[^\/\n]*)?)', re.S | re.X) 40 | self.docBlockProcessPat = re.compile('(/\*\*)(.*?)(\*/)( [\s\n]*[^\/\n]*)? ', re.S | re.X) 41 | 42 | # after extracting the comment, fix it up (remove *s and leading spaces) 43 | self.blockFilterPat = re.compile('^\s*\* ?', re.M) 44 | 45 | # '@' can be escaped with an '@', i.e. @@function when occurs in text blocks 46 | # will not be eaten by the parser. This pattern is used to unescape text 47 | # blocks. 48 | self.unescapeTagPat = re.compile('@@(?=\w+)', re.M) 49 | 50 | # the pattern used to split a comment block to create our token stream. 51 | # The token stream consists of: 52 | # * tags: "@tagname" 53 | # * types: "{typename [optional text content]} 54 | # * text: "freeform text that's not either of the above" 55 | # 56 | # This pattern checks for either of the top two, and is applied using 57 | # .split() which handles the third. 58 | self.tokenizePat = re.compile(r''' 59 | (?: 60 | (? 0 and not self._isMarker(self._peekTok(tokens)): 180 | t = tokens.pop(0) 181 | args.append(t) 182 | 183 | # do we have a handler for this tag? 184 | if not handler == None: 185 | arg = None 186 | 187 | # get argument if required 188 | if handler.takesArg: 189 | if len(args) == 0 and not handler.argOptional: 190 | raise RuntimeError("%s tag requires an argument" % cur) 191 | elif not len(args) == 0: 192 | raise RuntimeError("no arguments allowed to %s tag" % cur) 193 | 194 | ctx = handler.parse(args) 195 | 196 | if handler.mayRecur: 197 | if cur not in currentObj["tagData"]: 198 | currentObj["tagData"][cur] = [] 199 | currentObj["tagData"][cur].append(ctx) 200 | else: 201 | if cur in currentObj["tagData"]: 202 | raise RuntimeError("%s tag may not occur multiple times in the same documentation block" % cur) 203 | currentObj["tagData"][cur] = ctx 204 | 205 | # ooops. Dunno what that is! 206 | else: 207 | raise RuntimeError("unrecognized tag: %s" % cur) 208 | 209 | def _guessBlockName(self, codeChunk, blockType): 210 | # given the first line of source code after the block, and it's type 211 | # we'll invoke our name guessers to try to figure out the name of the 212 | # construct being documented 213 | 214 | # now let's invoke our type guessers, in order 215 | for func in self.nameGuessers: 216 | t = func(codeChunk, blockType) 217 | if t != None: 218 | return t 219 | 220 | return None 221 | 222 | def _guessBlockType(self, firstBlock, codeChunk, context, tags): 223 | # first we'll prune possibilities by figuring out which supported blocktypes 224 | # are valid in the current context, and support all of the required tags 225 | tagSet = set(tags) 226 | possibilities = [ ] 227 | for bt in self.blockTypes: 228 | bt = self.blockTypes[bt] 229 | if context not in bt.allowedContexts: 230 | continue 231 | if not tagSet.issubset(bt.allowedTags): 232 | continue 233 | possibilities.append(bt.tagName) 234 | 235 | # if we've reduced to exactly one possibility, then we don't need to guess 236 | if len(possibilities) == 1: 237 | return possibilities[0] 238 | 239 | # now let's invoke our type guessers, in order 240 | for func in self.typeGuessers: 241 | t = func(firstBlock, codeChunk, context, tags, possibilities) 242 | if t != None: 243 | return t 244 | 245 | raise RuntimeError("Can't determine what this block documents (from %s)" % ", ".join(possibilities)) 246 | 247 | def _whatContext(self, stack): 248 | return stack[-1][0] 249 | 250 | def _analyzeBlock(self, block, codeChunk, firstBlock, stack, lineStart, lineEnd): 251 | # Ye' ol' block analysis process. block at this point contains 252 | # a chunk of text that has already had comment markers stripped out. 253 | 254 | # Step 1: split the chunk of text into a token stream, each token 255 | # is either a tag /@\w+/ or a chunk of text (tag argument). 256 | # whitespace on either side of tokens is stripped. Also, unescape 257 | # @@tags. 258 | tokens = self.tokenizePat.split(block) 259 | tokens = [n for n in tokens if not n == None] 260 | tokens = [n.lstrip(" \t").lstrip('\r\n').rstrip() for n in tokens if n.strip()] 261 | tokens = [self.unescapeTagPat.sub("@", t) for t in tokens] 262 | 263 | # Step 3: Treat initial text as if it were a description. 264 | if not self._isMarker(tokens[0]): 265 | tokens.insert(0, '@desc') 266 | 267 | # Step 4: collapse aliases 268 | tokens = [self.aliases[n] if self.aliases.has_key(n) else n for n in tokens] 269 | 270 | # "autosplitting". this feature allows multiple blocks to reside in the 271 | # same documentation block (/** */) 272 | tokenGroups = [] 273 | while len(tokens): 274 | i = 1 275 | while i < len(tokens): 276 | if (tokens[i] in self.blockTypes): 277 | break 278 | i += 1 279 | tokenGroups.append(tokens[:i]) 280 | tokens = tokens[i:] 281 | 282 | for tokens in tokenGroups: 283 | 284 | # Step 2: initialize an object which will hold the intermediate 285 | # representation of parsed block data. 286 | parseData = { 287 | 'blockHandler': None, 288 | 'tagData': { } 289 | } 290 | 291 | # Step 4.5: depth first recursion for inline type parsing 292 | newtoks = [] 293 | for t in tokens: 294 | if len(t) >= 2 and t[0:1] == '{' and t[-1:] == '}': 295 | stack.append( ('embedded', {}) ) 296 | tokObj = { } 297 | t2 = "@typedef " + t[1:-1] 298 | self._analyzeBlock(t2, codeChunk, False, stack, lineStart, lineEnd) 299 | self._analyzeBlock("@endtypedef", codeChunk, False, stack, lineStart, lineEnd) 300 | newtoks.append(DocStract.Type(stack[-1][1]['val'], t)) 301 | stack.pop() 302 | else: 303 | newtoks.append(t) 304 | tokens = newtoks 305 | 306 | # Step 5: parse all tokens from the token stream, populating the 307 | # output representation as we go. 308 | while len(tokens): 309 | self._consumeToks(tokens, parseData) 310 | 311 | thisContext = self._whatContext(stack) 312 | 313 | # Step 6: Heuristics! Apply a set of functions which use the current state of 314 | # documentation extractor and some source code to figure out what 315 | # type of construct (@function, @property, etc) this documentation 316 | # block is documenting, and what its name is. 317 | 318 | # only invoke guessing logic if type wasn't explicitly declared 319 | if parseData['blockHandler'] == None: 320 | guessedType = self._guessBlockType(firstBlock, codeChunk, thisContext, parseData['tagData'].keys()) 321 | 322 | if guessedType not in self.blockTypes: 323 | raise RuntimeError("Don't know how to handle a '%s' documentation block" % guessedType) 324 | parseData['blockHandler'] = self.blockTypes[guessedType] 325 | 326 | # always try to guess the name, a name guesser has the first interesting line of code 327 | # after the documentation block and the type of block (it's string name) to work with 328 | guessedName = self._guessBlockName(codeChunk, parseData['blockHandler'].tagName) 329 | 330 | # Step 7: Validation phase! Not all tags are allowed in all types of 331 | # documentation blocks. like '@returns' inside a '@classend' block 332 | # would just be nutty. let's scrutinize this block to make sure it's 333 | # sane. 334 | 335 | # first check that this doc block type is valid in present context 336 | if thisContext not in parseData['blockHandler'].allowedContexts: 337 | raise RuntimeError("%s not allowed in %s context" % 338 | (parseData['blockHandler'].tagName, 339 | thisContext)) 340 | 341 | # now check that all present tags are allowed in this block 342 | for tag in parseData['tagData']: 343 | if not tag == parseData['blockHandler'].tagName and tag not in parseData['blockHandler'].allowedTags: 344 | raise RuntimeError("%s not allowed in %s block" % 345 | (tag, parseData['blockHandler'].tagName)) 346 | 347 | # Step 8: Generation of output document 348 | doc = { } 349 | 350 | for tag in parseData['tagData']: 351 | val = parseData['tagData'][tag] 352 | if not type(val) == types.ListType: 353 | val = [ val ] 354 | for v in val: 355 | handler = self.tags[tag] if tag in self.tags else self.blockTypes[tag] 356 | handler.attach(v, doc, parseData['blockHandler'].tagName) 357 | 358 | parseData['blockHandler'].setLineNumber(lineStart, lineEnd, doc) 359 | 360 | # special case for classes and typedefs 361 | if parseData['blockHandler'].tagName in ('@endclass', '@endtypedef'): 362 | doc = stack.pop()[1] 363 | 364 | parseData['blockHandler'].merge(doc, stack[-1][1], guessedName, self._whatContext(stack)) 365 | 366 | if parseData['blockHandler'].tagName == '@class': 367 | stack.append( ('class', doc) ) 368 | elif parseData['blockHandler'].tagName == '@typedef': 369 | stack.append( ('type', doc) ) 370 | 371 | def extractFromFile(self, filename): 372 | # next read the whole file into memory 373 | contents = "" 374 | with open(filename, "r") as f: 375 | contents = f.read() 376 | 377 | data = self.extract(contents) 378 | 379 | # first determine the module name, it's always the same as the file name 380 | mod = os.path.basename(filename) 381 | dotLoc = mod.rfind(".") 382 | if (dotLoc > 0): 383 | mod = mod[:dotLoc] 384 | if not "module" in data: 385 | data["module"] = mod 386 | if not "filename" in data: 387 | data["filename"] = filename 388 | 389 | return data 390 | 391 | def extract(self, contents): 392 | # clear the lil' context flag that lets us know when we're parsing 393 | # classes (class definitions cannot span files) 394 | stack = [ ( 'global', {} ) ] 395 | 396 | # now parse out and combine comment blocks 397 | firstBlock = True 398 | line = 0 399 | for text in self.docBlockFindPat.split(contents): 400 | lineStart = line + 1 401 | line += text.count('\n') 402 | 403 | # if this isn't a documentation block, carry on 404 | m = self.docBlockProcessPat.match(text) 405 | if m: 406 | block = self.blockFilterPat.sub("", m.group(2)).strip() 407 | context = m.group(4).strip() 408 | # data will be mutated! 409 | try: 410 | self._analyzeBlock(block, context, firstBlock, stack, lineStart, line) 411 | except RuntimeError, exc: 412 | args = exc.args 413 | if not args: 414 | arg0 = '' 415 | else: 416 | arg0 = args[0] 417 | arg0 += ' at line %s' % lineStart 418 | exc.args = (arg0,) + args[1:] 419 | raise 420 | firstBlock = False 421 | 422 | return stack[0][1] 423 | 424 | # begin definition of Tag Handler classes. 425 | 426 | # TagHandler is the base class for a handler of tags. This is an 427 | # object that is capable of parsing tags and merging them into 428 | # the output JSON document. 429 | class TagHandler(object): 430 | # if takesArg is true, then text may occur after the tag 431 | # (it "accepts" a single text blob as an argument) 432 | takesArg = False 433 | # if takesArg is True, argOptional specifies whether the 434 | # argument is required 435 | argOptional = False 436 | # if mayRecur is True the tag may be specified multiple times 437 | # in a single document text blob. 438 | mayRecur = False 439 | def __init__(self, tagname): 440 | self.tagName = tagname 441 | 442 | # the parse method attempts to parse the text blob and returns 443 | # any representation of it that it likes. This method should throw 444 | # if there's a syntactic error in the text argument. text may be 445 | # 'None' if the tag accepts no argument. 446 | def parse(self, args): 447 | return " ".join([str(a) for a in args]) if len(args) > 0 else None 448 | 449 | # attach merges the results of parsing a tag into the output 450 | # JSON document for a documentation block. `obj` is the value 451 | # returned by parse(), and parent is the json document that 452 | # the function should mutate 453 | def attach(self, obj, parent, blockType): 454 | parent[self.tagName[1:]] = obj 455 | 456 | # utility function for determining if an argument is a type 457 | def _isType(self, arg): 458 | return isinstance(arg, DocStract.Type) 459 | 460 | # utility function for rendering arguments 461 | def _argPrint(self, args): 462 | return ("(" + str(len(args)) + "): ") + " | ".join([str(x)[:10] for x in args])[:40] + "..." 463 | 464 | 465 | class ParamTagHandler(TagHandler): 466 | mayRecur = True 467 | takesArg = True 468 | 469 | _nameAndDescPat = re.compile('^([\w.\[\]]+)?\s*(.*)$', re.S); 470 | 471 | # a pattern used to detect & strip optional brackets 472 | _optionalPat = re.compile('^\[(.*)\]$') 473 | 474 | def parse(self, args): 475 | 476 | p = { } 477 | # collapse two arg case into one arg 478 | if (len(args) == 2 and self._isType(args[0])): 479 | p['type'] = args[0].value 480 | args = args[1:] 481 | 482 | if len(args) == 1: 483 | m = self._nameAndDescPat.match(args[0]) 484 | if not m or self._isType(args[0]): 485 | raise RuntimeError("Malformed args to %s: %s" % 486 | (self.tagName, self._argPrint(args))) 487 | if m.group(1): 488 | p['name'] = m.group(1) 489 | if m.group(2): 490 | p['desc'] = m.group(2) 491 | elif len(args) == 2: 492 | # @param name {type} 493 | if self._isType(args[0]) or not self._isType(args[1]): 494 | raise RuntimeError("Malformed args to %s: %s" % 495 | (self.tagName, self._argPrint(args))) 496 | p['name'] = args[0] 497 | p['type'] = args[1].value 498 | elif len(args) == 3: 499 | # this is 500 | # @param name {type} desc 501 | if self._isType(args[0]) or not self._isType(args[1]) or self._isType(args[2]): 502 | raise RuntimeError("Malformed args to %s: %s" % 503 | (self.tagName, self._argPrint(args))) 504 | p['name'] = args[0] 505 | p['type'] = args[1].value 506 | p['desc'] = args[2] 507 | else: 508 | raise RuntimeError("Malformed args to %s: %s" % 509 | (self.tagName, self._argPrint(args))) 510 | return p 511 | 512 | def _handleOptionalSyntax(self, obj): 513 | # handle optional syntax: [name] 514 | if ('name' in obj): 515 | m = self._optionalPat.match(obj['name']) 516 | if m: 517 | obj['name'] = m.group(1) 518 | obj['optional'] = True 519 | 520 | def attach(self, obj, current, blockType): 521 | self._handleOptionalSyntax(obj) 522 | if not 'params' in current: 523 | current['params'] = [ ] 524 | current['params'].append(obj) 525 | 526 | class SeeTagHandler(TagHandler): 527 | takesArg = True 528 | mayRecur = True 529 | def attach(self, obj, current, blockType): 530 | if not 'see' in current: 531 | current['see'] = [ ] 532 | current['see'].append(obj) 533 | 534 | class DescTagHandler(TagHandler): 535 | takesArg = True 536 | mayRecur = True 537 | def attach(self, obj, current, blockType): 538 | if 'desc' in current: 539 | current['desc'] = current['desc'] + "\n\n" + obj 540 | else: 541 | current['desc'] = obj 542 | 543 | class ReturnTagHandler(TagHandler): 544 | takesArg = True 545 | _pat = re.compile('^\s*(?:{(\w+)})?\s*(.*)$', re.S); 546 | 547 | def parse(self, args): 548 | rv = { } 549 | for a in args: 550 | if self._isType(a): 551 | if 'type' in rv: 552 | raise RuntimeError("Return type multiply decalared") 553 | rv['type'] = a.value 554 | else: 555 | if 'desc' in rv: 556 | raise RuntimeError("Bogus arguments to %s: %s" % 557 | (self.tagName, self._argPrint(args))) 558 | rv['desc'] = a 559 | 560 | return rv 561 | 562 | def attach(self, obj, current, blockType): 563 | # The only way this can occur (returns already defined) is if 564 | # someone added an extension that behaves badly, or if @type and 565 | # @returns occur in the same block. 566 | if 'returns' in current: 567 | for k in current['returns']: 568 | if k in obj: 569 | raise RuntimeError("Return %s redefined (@type and @returns in " % k + 570 | "same function block?)") 571 | else: 572 | current['returns'] = {} 573 | 574 | for k in obj: 575 | current['returns'][k] = obj[k] 576 | 577 | class TypeTagHandler(TagHandler): 578 | takesArg = True 579 | 580 | _isWordPat = re.compile('^\w+$', re.S); 581 | 582 | def parse(self, args): 583 | if len(args) > 1: 584 | raise RuntimeError("Bogus arguments to %s: %s" % 585 | (self.tagName, self._argPrint(args))) 586 | if self._isType(args[0]): 587 | args[0] = args[0].value.strip() 588 | else: 589 | m = self._isWordPat.match(args[0]) 590 | if not m: 591 | raise RuntimeError("Bogus argument to %s: %s" % (self.tagName, args[0])) 592 | return args[0] 593 | 594 | 595 | # type is special. it means different things 596 | # when it occurs in a '@property' vs. a '@function' 597 | # context. in the former it's the property type, in 598 | # the later, it's an alias for '@return' 599 | def attach(self, obj, current, blockType): 600 | if (blockType == '@property'): 601 | current['type'] = obj 602 | else: 603 | if 'returns' not in current: 604 | current['returns'] = { } 605 | if 'type' in current['returns']: 606 | raise RuntimeError("Return type redefined (@type and @returns in " + 607 | "same function block?)") 608 | current['returns']['type'] = obj 609 | 610 | class ThrowsTagHandler(ReturnTagHandler): 611 | mayRecur = True 612 | def attach(self, obj, current, blockType): 613 | if 'throws' not in current: 614 | current['throws'] = [ ] 615 | current['throws'].append(obj) 616 | 617 | class PayloadTagHandler(ReturnTagHandler): 618 | mayRecur = False 619 | def attach(self, obj, current, blockType): 620 | if 'payload' in current: 621 | raise RuntimeError("an event can't have multiple payloads"); 622 | current['payload'] = obj 623 | 624 | 625 | # a block handler is slightly different than a tag 626 | # handler. Each document block is of a certain type, 627 | # it describes *something*. Block handlers do 628 | # everything that TagHandlers do, but also: 629 | # * one block handler per code block, they're mutually 630 | # exclusive (a docblock can't describe a *function* 631 | # AND a *property*) 632 | # * express what tags may occur inside of them 633 | # * express what contexts they may occur in ('global' 634 | # and 'class' are the only two meaninful contexts at 635 | # present). 636 | class BlockHandler(TagHandler): 637 | allowedTags = [ ] 638 | allowedContexts = [ 'global', 'class' ] 639 | def merge(self, doc, parent, guessedName, context): 640 | for k in doc: 641 | parent[k] = doc[k] 642 | 643 | def setLineNumber(self, lineStart, lineEnd, doc): 644 | doc['source_lines'] = [ lineStart, lineEnd ] 645 | 646 | class ModuleBlockHandler(BlockHandler): 647 | allowedTags = [ '@desc', '@see' ] 648 | allowedContexts = [ 'global' ] 649 | takesArg = True 650 | _pat = re.compile('^(\w+)$|^(?:([\w.\[\]]+)\s*\n)?\s*(.*)$', re.S); 651 | def parse(self, args): 652 | if len(args) != 1: 653 | raise RuntimeError("You may not pass args (like, {string}) to %s" % 654 | self.tagName) 655 | text = args[0] 656 | m = self._pat.match(text) 657 | if not m: 658 | raise RuntimeError("Malformed args to %s: %s" % 659 | (self.tagName, (text[:20] + "..."))) 660 | a = { } 661 | if m.group(1): 662 | a["name"] = m.group(1) 663 | else: 664 | if m.group(2): 665 | a["name"] = m.group(2) 666 | if m.group(3): 667 | a["desc"] = m.group(3) 668 | return a 669 | 670 | def attach(self, obj, current, blockType): 671 | if "name" in obj: 672 | current['module'] = obj["name"] 673 | if "desc" in obj: 674 | if "desc" in current: 675 | obj['desc'] = current['desc'] + "\n\n" + obj['desc'] 676 | current['desc'] = obj['desc'] 677 | 678 | def merge(self, doc, parent, guessedName, context): 679 | # first fields that we wish to not overwrite 680 | for f in doc: 681 | if f == 'desc': 682 | parent['desc'] = parent['desc'] + "\n\n" + doc['desc'] if 'desc' in parent else doc['desc'] 683 | elif f == "module": 684 | parent['module'] = doc['module'] 685 | elif (f in doc and f not in parent): 686 | parent[f] = doc[f] 687 | 688 | 689 | class FunctionBlockHandler(ModuleBlockHandler): 690 | allowedTags = [ '@see', '@param', '@return', '@throws', '@desc', '@type' ] 691 | allowedContexts = [ 'global', 'class' ] 692 | 693 | def attach(self, obj, current, blockType): 694 | if "name" in obj: 695 | current['name'] = obj["name"] 696 | if "desc" in obj: 697 | if "desc" in current: 698 | obj['desc'] = current['desc'] + "\n\n" + obj['desc'] 699 | current['desc'] = obj['desc'] 700 | 701 | def merge(self, doc, parent, guessedName, context): 702 | if "name" not in doc: 703 | doc['name'] = guessedName 704 | if doc['name'] == None: 705 | raise RuntimeError("can't determine function name") 706 | if not "functions" in parent: 707 | parent["functions"] = [] 708 | for f in parent["functions"]: 709 | if doc["name"] == f['name']: 710 | raise RuntimeError("function '%s' redefined" % doc["name"]) 711 | 712 | parent["functions"].append(doc) 713 | 714 | class EventBlockHandler(ModuleBlockHandler): 715 | allowedTags = [ '@see', '@desc', '@payload' ] 716 | allowedContexts = [ 'global', 'class' ] 717 | 718 | def attach(self, obj, current, blockType): 719 | if "name" in obj: 720 | current['name'] = obj["name"] 721 | if "desc" in obj: 722 | if "desc" in current: 723 | obj['desc'] = current['desc'] + "\n\n" + obj['desc'] 724 | current['desc'] = obj['desc'] 725 | 726 | def merge(self, doc, parent, guessedName, context): 727 | if "name" not in doc: 728 | doc['name'] = guessedName 729 | if doc['name'] == None: 730 | raise RuntimeError("can't determine event name") 731 | if not "events" in parent: 732 | parent["events"] = [] 733 | for e in parent["events"]: 734 | if doc["name"] == e['name']: 735 | raise RuntimeError("'%s' event redefined" % doc["name"]) 736 | 737 | parent["events"].append(doc) 738 | 739 | class ConstructorBlockHandler(BlockHandler): 740 | allowedTags = [ '@see', '@param', '@throws', '@desc', '@return', '@type' ] 741 | takesArg = True 742 | argOptional = True 743 | allowedContexts = [ 'class' ] 744 | def attach(self, obj, current, blockType): 745 | if obj: 746 | if "desc" in current: 747 | obj = current['desc'] + "\n\n" + obj 748 | current['desc'] = obj 749 | 750 | def merge(self, doc, parent, guessedName, context): 751 | if not "constructors" in parent: 752 | parent["constructors"] = [] 753 | parent["constructors"].append(doc) 754 | 755 | 756 | class ClassBlockHandler(FunctionBlockHandler): 757 | allowedTags = [ '@see', '@desc' ] 758 | def merge(self, doc, parent, guessedName, context): 759 | if "name" not in doc: 760 | doc['name'] = guessedName 761 | return doc 762 | 763 | class EndClassBlockHandler(BlockHandler): 764 | allowedContexts = [ 'class' ] 765 | def attach(self, obj, current, blockType): 766 | pass 767 | 768 | def merge(self, doc, parent, guessedName, context): 769 | if not "classes" in parent: 770 | parent["classes"] = [] 771 | for c in parent["classes"]: 772 | if doc["name"] == c['name']: 773 | raise RuntimeError("class '%s' redefined" % doc["name"]) 774 | parent["classes"].append(doc) 775 | 776 | class TypedefBlockHandler(FunctionBlockHandler): 777 | allowedTags = [ ] 778 | allowedContexts = [ 'embedded' ] 779 | takesArg = True 780 | _pat = re.compile('^(\w+)$|^(?:([\w.\[\]]+)\s*\n)?\s*(.*)$', re.S); 781 | def parse(self, args): 782 | if len(args) != 1 or self._isType(args[0]): 783 | raise RuntimeError("%s accepts a string argument" % self.tagName) 784 | return args[0] 785 | 786 | def attach(self, obj, current, blockType): 787 | current['name'] = obj 788 | 789 | def merge(self, doc, parent, guessedName, context): 790 | parent['val'] = doc 791 | 792 | class EndTypedefBlockHandler(BlockHandler): 793 | allowedContexts = [ 'type' ] 794 | def attach(self, obj, current, blockType): 795 | pass 796 | def merge(self, doc, parent, guessedName, context): 797 | pass 798 | 799 | class PropertyBlockHandler(ParamTagHandler, BlockHandler): 800 | allowedTags = [ '@see', '@throws', '@desc', '@type' ] 801 | allowedContexts = [ 'type', 'class', 'global' ] 802 | def attach(self, obj, current, blockType): 803 | for x in obj: 804 | current[x] = obj[x] 805 | 806 | def merge(self, doc, parent, guessedName, context): 807 | if "name" not in doc: 808 | doc['name'] = guessedName 809 | if doc["name"] == None: 810 | raise RuntimeError("can't determine property name") 811 | if not "properties" in parent: 812 | parent["properties"] = [] 813 | for p in parent["properties"]: 814 | if doc["name"] == p['name']: 815 | raise RuntimeError("property '%s' redefined" % doc["name"]) 816 | 817 | if context == "type": 818 | self._handleOptionalSyntax(doc) 819 | 820 | parent["properties"].append(doc) 821 | 822 | # A type guesser that assumes the first documentation block of a source file is 823 | # probably a '@module' documentation block 824 | def firstBlockIsModuleTypeGuesser(firstBlock, codeChunk, context, tags, possibilities): 825 | if '@module' in possibilities and firstBlock: 826 | return '@module' 827 | return None 828 | 829 | # A type guesser that checks the codeChunk for appearance of the keyword 'function' 830 | _functionKeywordPat = re.compile('(?> sys.stderr, "Usage: docstract [file]" 909 | sys.exit(1) 910 | 911 | print json.dumps(docs, indent=2, sort_keys=True) + "\n" 912 | --------------------------------------------------------------------------------