├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── README.md ├── fp └── _mapping.js ├── known_problems.md ├── lib └── doc │ ├── apply-fp-mapping │ ├── common.js │ ├── description.js │ ├── example.js │ ├── index.js │ └── parameters.js │ └── build.js ├── lodash.js ├── package.json └── test └── test-fp-doc.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.custom.* 3 | *.log 4 | *.map 5 | lodash.compat.min.js 6 | coverage 7 | node_modules 8 | /doc/ 9 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxErrors": "2000", 3 | "maximumLineLength": { 4 | "value": 180, 5 | "allExcept": ["comments", "functionSignature", "regex"] 6 | }, 7 | "requireCurlyBraces": [ 8 | "if", 9 | "else", 10 | "for", 11 | "while", 12 | "do", 13 | "try", 14 | "catch" 15 | ], 16 | "requireOperatorBeforeLineBreak": [ 17 | "=", 18 | "+", 19 | "-", 20 | "/", 21 | "*", 22 | "==", 23 | "===", 24 | "!=", 25 | "!==", 26 | ">", 27 | ">=", 28 | "<", 29 | "<=" 30 | ], 31 | "requireSpaceAfterKeywords": [ 32 | "if", 33 | "else", 34 | "for", 35 | "while", 36 | "do", 37 | "switch", 38 | "return", 39 | "try", 40 | "catch" 41 | ], 42 | "requireSpaceBeforeBinaryOperators": [ 43 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 44 | "&=", "|=", "^=", "+=", 45 | 46 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 47 | "|", "^", "&&", "||", "===", "==", ">=", 48 | "<=", "<", ">", "!=", "!==" 49 | ], 50 | "requireSpacesInFunctionExpression": { 51 | "beforeOpeningCurlyBrace": true 52 | }, 53 | "requireCamelCaseOrUpperCaseIdentifiers": true, 54 | "requireDotNotation": { "allExcept": ["keywords"] }, 55 | "requireEarlyReturn": true, 56 | "requireLineFeedAtFileEnd": true, 57 | "requireSemicolons": true, 58 | "requireSpaceAfterBinaryOperators": true, 59 | "requireSpacesInConditionalExpression": true, 60 | "requireSpaceBeforeObjectValues": true, 61 | "requireSpaceBeforeBlockStatements": true, 62 | "requireSpacesInForStatement": true, 63 | 64 | "validateIndentation": 2, 65 | "validateParameterSeparator": ", ", 66 | "validateQuoteMarks": { "mark": "'", "escape": true }, 67 | 68 | "disallowSpacesInAnonymousFunctionExpression": { 69 | "beforeOpeningRoundBrace": true 70 | }, 71 | "disallowSpacesInFunctionDeclaration": { 72 | "beforeOpeningRoundBrace": true 73 | }, 74 | "disallowSpacesInFunctionExpression": { 75 | "beforeOpeningRoundBrace": true 76 | }, 77 | "disallowKeywords": ["with"], 78 | "disallowMixedSpacesAndTabs": true, 79 | "disallowMultipleLineBreaks": true, 80 | "disallowNewlineBeforeBlockStatements": true, 81 | "disallowSpaceAfterObjectKeys": true, 82 | "disallowSpaceAfterPrefixUnaryOperators": true, 83 | "disallowSpacesInCallExpression": true, 84 | "disallowSpacesInsideArrayBrackets": true, 85 | "disallowSpacesInsideParentheses": true, 86 | "disallowTrailingWhitespace": true, 87 | 88 | "jsDoc": { 89 | "checkRedundantAccess": true, 90 | "checkTypes": true, 91 | "requireNewlineAfterDescription": true, 92 | "requireParamDescription": true, 93 | "requireParamTypes": true, 94 | "requireReturnTypes": true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `lodash/fp` doc generator 2 | 3 | This project aims to generate proper docs for `Lodash`'s functional programming flavor, aka `lodash/fp`. 4 | The end goal would be generate a doc similar to the [official documentation](https://lodash.com/docs). The result can be found **[HERE](https://gist.github.com/jfmengels/6b973b69c491375117dc)**. 5 | 6 | ## Why 7 | 8 | Lodash FP aims to combine the great functionality given by the library and the ideas of functional programming. Only, this behavior is [documented]([FP Guide](https://github.com/lodash/lodash/wiki/FP-Guide)) in a manner unbefitting of what users are used to with the [official documentation](https://lodash.com/docs). 9 | 10 | ## What changed in FP? 11 | 12 | - The order of arguments has been changed, in particular the data argument has been moved to the last position 13 | - No methods mutate the arguments 14 | - Some methods are curried 15 | - Some methods have their arguments capped, meaning some arguments have disappeared 16 | - Some methods with `...values` have that argument now in an array 17 | 18 | ## What now 19 | 20 | Lodash's sources are generated using [`docdown`](https://github.com/jdalton/docdown). What it does is read the source files, extract the JSDoc comments, then generate a Markdown file. By overriding Docdown's behavior, we can change the documentation too, in the methods' signature and example: 21 | - change the order of arguments 22 | - remove now ignored arguments 23 | - group `...values` arguments into an array argument 24 | - remove calls to `console.log()` that aim to demonstrate a value was mutated (only in example) 25 | - inject optional arguments that have become mandatory using their default value (only in example) 26 | 27 | ## Run it 28 | 29 | ``` 30 | npm i 31 | npm run doc:fp 32 | // now open doc/fp.md! 33 | ``` 34 | -------------------------------------------------------------------------------- /fp/_mapping.js: -------------------------------------------------------------------------------- 1 | /** Used to map aliases to their real names. */ 2 | exports.aliasToReal = { 3 | 4 | // Lodash aliases. 5 | 'each': 'forEach', 6 | 'eachRight': 'forEachRight', 7 | 'entries': 'toPairs', 8 | 'entriesIn': 'toPairsIn', 9 | 'extend': 'assignIn', 10 | 'extendAll': 'assignInAll', 11 | 'extendAllWith': 'assignInAllWith', 12 | 'extendWith': 'assignInWith', 13 | 'first': 'head', 14 | 15 | // Methods that are curried variants of others. 16 | 'conforms': 'conformsTo', 17 | 'matches': 'isMatch', 18 | 'property': 'get', 19 | 20 | // Ramda aliases. 21 | '__': 'placeholder', 22 | 'F': 'stubFalse', 23 | 'T': 'stubTrue', 24 | 'all': 'every', 25 | 'allPass': 'overEvery', 26 | 'always': 'constant', 27 | 'any': 'some', 28 | 'anyPass': 'overSome', 29 | 'apply': 'spread', 30 | 'assoc': 'set', 31 | 'assocPath': 'set', 32 | 'complement': 'negate', 33 | 'compose': 'flowRight', 34 | 'contains': 'includes', 35 | 'dissoc': 'unset', 36 | 'dissocPath': 'unset', 37 | 'dropLast': 'dropRight', 38 | 'dropLastWhile': 'dropRightWhile', 39 | 'equals': 'isEqual', 40 | 'identical': 'eq', 41 | 'indexBy': 'keyBy', 42 | 'init': 'initial', 43 | 'invertObj': 'invert', 44 | 'juxt': 'over', 45 | 'omitAll': 'omit', 46 | 'nAry': 'ary', 47 | 'path': 'get', 48 | 'pathEq': 'matchesProperty', 49 | 'pathOr': 'getOr', 50 | 'paths': 'at', 51 | 'pickAll': 'pick', 52 | 'pipe': 'flow', 53 | 'pluck': 'map', 54 | 'prop': 'get', 55 | 'propEq': 'matchesProperty', 56 | 'propOr': 'getOr', 57 | 'props': 'at', 58 | 'symmetricDifference': 'xor', 59 | 'symmetricDifferenceBy': 'xorBy', 60 | 'symmetricDifferenceWith': 'xorWith', 61 | 'takeLast': 'takeRight', 62 | 'takeLastWhile': 'takeRightWhile', 63 | 'unapply': 'rest', 64 | 'unnest': 'flatten', 65 | 'useWith': 'overArgs', 66 | 'where': 'conformsTo', 67 | 'whereEq': 'isMatch', 68 | 'zipObj': 'zipObject' 69 | }; 70 | 71 | /** Used to map ary to method names. */ 72 | exports.aryMethod = { 73 | '1': [ 74 | 'assignAll', 'assignInAll', 'attempt', 'castArray', 'ceil', 'create', 75 | 'curry', 'curryRight', 'defaultsAll', 'defaultsDeepAll', 'floor', 'flow', 76 | 'flowRight', 'fromPairs', 'invert', 'iteratee', 'memoize', 'method', 'mergeAll', 77 | 'methodOf', 'mixin', 'nthArg', 'over', 'overEvery', 'overSome','rest', 'reverse', 78 | 'round', 'runInContext', 'spread', 'template', 'trim', 'trimEnd', 'trimStart', 79 | 'uniqueId', 'words', 'zipAll' 80 | ], 81 | '2': [ 82 | 'add', 'after', 'ary', 'assign', 'assignAllWith', 'assignIn', 'assignInAllWith', 83 | 'at', 'before', 'bind', 'bindAll', 'bindKey', 'chunk', 'cloneDeepWith', 84 | 'cloneWith', 'concat', 'conformsTo', 'countBy', 'curryN', 'curryRightN', 85 | 'debounce', 'defaults', 'defaultsDeep', 'defaultTo', 'delay', 'difference', 86 | 'divide', 'drop', 'dropRight', 'dropRightWhile', 'dropWhile', 'endsWith', 'eq', 87 | 'every', 'filter', 'find', 'findIndex', 'findKey', 'findLast', 'findLastIndex', 88 | 'findLastKey', 'flatMap', 'flatMapDeep', 'flattenDepth', 'forEach', 89 | 'forEachRight', 'forIn', 'forInRight', 'forOwn', 'forOwnRight', 'get', 90 | 'groupBy', 'gt', 'gte', 'has', 'hasIn', 'includes', 'indexOf', 'intersection', 91 | 'invertBy', 'invoke', 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', 92 | 'lastIndexOf', 'lt', 'lte', 'map', 'mapKeys', 'mapValues', 'matchesProperty', 93 | 'maxBy', 'meanBy', 'merge', 'mergeAllWith', 'minBy', 'multiply', 'nth', 'omit', 94 | 'omitBy', 'overArgs', 'pad', 'padEnd', 'padStart', 'parseInt', 'partial', 95 | 'partialRight', 'partition', 'pick', 'pickBy', 'propertyOf', 'pull', 'pullAll', 96 | 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', 'remove', 97 | 'repeat', 'restFrom', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', 98 | 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexOf', 'sortedUniqBy', 99 | 'split', 'spreadFrom', 'startsWith', 'subtract', 'sumBy', 'take', 'takeRight', 100 | 'takeRightWhile', 'takeWhile', 'tap', 'throttle', 'thru', 'times', 'trimChars', 101 | 'trimCharsEnd', 'trimCharsStart', 'truncate', 'union', 'uniqBy', 'uniqWith', 102 | 'unset', 'unzipWith', 'without', 'wrap', 'xor', 'zip', 'zipObject', 103 | 'zipObjectDeep' 104 | ], 105 | '3': [ 106 | 'assignInWith', 'assignWith', 'clamp', 'differenceBy', 'differenceWith', 107 | 'findFrom', 'findIndexFrom', 'findLastFrom', 'findLastIndexFrom', 'getOr', 108 | 'includesFrom', 'indexOfFrom', 'inRange', 'intersectionBy', 'intersectionWith', 109 | 'invokeArgs', 'invokeArgsMap', 'isEqualWith', 'isMatchWith', 'flatMapDepth', 110 | 'lastIndexOfFrom', 'mergeWith', 'orderBy', 'padChars', 'padCharsEnd', 111 | 'padCharsStart', 'pullAllBy', 'pullAllWith', 'rangeStep', 'rangeStepRight', 112 | 'reduce', 'reduceRight', 'replace', 'set', 'slice', 'sortedIndexBy', 113 | 'sortedLastIndexBy', 'transform', 'unionBy', 'unionWith', 'update', 'xorBy', 114 | 'xorWith', 'zipWith' 115 | ], 116 | '4': [ 117 | 'fill', 'setWith', 'updateWith' 118 | ] 119 | }; 120 | 121 | /** Used to map ary to rearg configs. */ 122 | exports.aryRearg = { 123 | '2': [1, 0], 124 | '3': [2, 0, 1], 125 | '4': [3, 2, 0, 1] 126 | }; 127 | 128 | /** Used to map method names to their iteratee ary. */ 129 | exports.iterateeAry = { 130 | 'dropRightWhile': 1, 131 | 'dropWhile': 1, 132 | 'every': 1, 133 | 'filter': 1, 134 | 'find': 1, 135 | 'findFrom': 1, 136 | 'findIndex': 1, 137 | 'findIndexFrom': 1, 138 | 'findKey': 1, 139 | 'findLast': 1, 140 | 'findLastFrom': 1, 141 | 'findLastIndex': 1, 142 | 'findLastIndexFrom': 1, 143 | 'findLastKey': 1, 144 | 'flatMap': 1, 145 | 'flatMapDeep': 1, 146 | 'flatMapDepth': 1, 147 | 'forEach': 1, 148 | 'forEachRight': 1, 149 | 'forIn': 1, 150 | 'forInRight': 1, 151 | 'forOwn': 1, 152 | 'forOwnRight': 1, 153 | 'map': 1, 154 | 'mapKeys': 1, 155 | 'mapValues': 1, 156 | 'partition': 1, 157 | 'reduce': 2, 158 | 'reduceRight': 2, 159 | 'reject': 1, 160 | 'remove': 1, 161 | 'some': 1, 162 | 'takeRightWhile': 1, 163 | 'takeWhile': 1, 164 | 'times': 1, 165 | 'transform': 2 166 | }; 167 | 168 | /** Used to map method names to iteratee rearg configs. */ 169 | exports.iterateeRearg = { 170 | 'mapKeys': [1], 171 | 'reduceRight': [1, 0] 172 | }; 173 | 174 | /** Used to map method names to rearg configs. */ 175 | exports.methodRearg = { 176 | 'assignInAllWith': [1, 0], 177 | 'assignInWith': [1, 2, 0], 178 | 'assignAllWith': [1, 0], 179 | 'assignWith': [1, 2, 0], 180 | 'differenceBy': [1, 2, 0], 181 | 'differenceWith': [1, 2, 0], 182 | 'getOr': [2, 1, 0], 183 | 'intersectionBy': [1, 2, 0], 184 | 'intersectionWith': [1, 2, 0], 185 | 'isEqualWith': [1, 2, 0], 186 | 'isMatchWith': [2, 1, 0], 187 | 'mergeAllWith': [1, 0], 188 | 'mergeWith': [1, 2, 0], 189 | 'padChars': [2, 1, 0], 190 | 'padCharsEnd': [2, 1, 0], 191 | 'padCharsStart': [2, 1, 0], 192 | 'pullAllBy': [2, 1, 0], 193 | 'pullAllWith': [2, 1, 0], 194 | 'rangeStep': [1, 2, 0], 195 | 'rangeStepRight': [1, 2, 0], 196 | 'setWith': [3, 1, 2, 0], 197 | 'sortedIndexBy': [2, 1, 0], 198 | 'sortedLastIndexBy': [2, 1, 0], 199 | 'unionBy': [1, 2, 0], 200 | 'unionWith': [1, 2, 0], 201 | 'updateWith': [3, 1, 2, 0], 202 | 'xorBy': [1, 2, 0], 203 | 'xorWith': [1, 2, 0], 204 | 'zipWith': [1, 2, 0] 205 | }; 206 | 207 | /** Used to map method names to spread configs. */ 208 | exports.methodSpread = { 209 | 'assignAll': { 'start': 0 }, 210 | 'assignAllWith': { 'start': 0 }, 211 | 'assignInAll': { 'start': 0 }, 212 | 'assignInAllWith': { 'start': 0 }, 213 | 'defaultsAll': { 'start': 0 }, 214 | 'defaultsDeepAll': { 'start': 0 }, 215 | 'invokeArgs': { 'start': 2 }, 216 | 'invokeArgsMap': { 'start': 2 }, 217 | 'mergeAll': { 'start': 0 }, 218 | 'mergeAllWith': { 'start': 0 }, 219 | 'partial': { 'start': 1 }, 220 | 'partialRight': { 'start': 1 }, 221 | 'without': { 'start': 1 }, 222 | 'zipAll': { 'start': 0 } 223 | }; 224 | 225 | /** Used to identify methods which mutate arrays or objects. */ 226 | exports.mutate = { 227 | 'array': { 228 | 'fill': true, 229 | 'pull': true, 230 | 'pullAll': true, 231 | 'pullAllBy': true, 232 | 'pullAllWith': true, 233 | 'pullAt': true, 234 | 'remove': true, 235 | 'reverse': true 236 | }, 237 | 'object': { 238 | 'assign': true, 239 | 'assignAll': true, 240 | 'assignAllWith': true, 241 | 'assignIn': true, 242 | 'assignInAll': true, 243 | 'assignInAllWith': true, 244 | 'assignInWith': true, 245 | 'assignWith': true, 246 | 'defaults': true, 247 | 'defaultsAll': true, 248 | 'defaultsDeep': true, 249 | 'defaultsDeepAll': true, 250 | 'merge': true, 251 | 'mergeAll': true, 252 | 'mergeAllWith': true, 253 | 'mergeWith': true, 254 | }, 255 | 'set': { 256 | 'set': true, 257 | 'setWith': true, 258 | 'unset': true, 259 | 'update': true, 260 | 'updateWith': true 261 | } 262 | }; 263 | 264 | /** Used to map real names to their aliases. */ 265 | exports.realToAlias = (function() { 266 | var hasOwnProperty = Object.prototype.hasOwnProperty, 267 | object = exports.aliasToReal, 268 | result = {}; 269 | 270 | for (var key in object) { 271 | var value = object[key]; 272 | if (hasOwnProperty.call(result, value)) { 273 | result[value].push(key); 274 | } else { 275 | result[value] = [key]; 276 | } 277 | } 278 | return result; 279 | }()); 280 | 281 | /** Used to map method names to other names. */ 282 | exports.remap = { 283 | 'assignAll': 'assign', 284 | 'assignAllWith': 'assignWith', 285 | 'assignInAll': 'assignIn', 286 | 'assignInAllWith': 'assignInWith', 287 | 'curryN': 'curry', 288 | 'curryRightN': 'curryRight', 289 | 'defaultsAll': 'defaults', 290 | 'defaultsDeepAll': 'defaultsDeep', 291 | 'findFrom': 'find', 292 | 'findIndexFrom': 'findIndex', 293 | 'findLastFrom': 'findLast', 294 | 'findLastIndexFrom': 'findLastIndex', 295 | 'getOr': 'get', 296 | 'includesFrom': 'includes', 297 | 'indexOfFrom': 'indexOf', 298 | 'invokeArgs': 'invoke', 299 | 'invokeArgsMap': 'invokeMap', 300 | 'lastIndexOfFrom': 'lastIndexOf', 301 | 'mergeAll': 'merge', 302 | 'mergeAllWith': 'mergeWith', 303 | 'padChars': 'pad', 304 | 'padCharsEnd': 'padEnd', 305 | 'padCharsStart': 'padStart', 306 | 'propertyOf': 'get', 307 | 'rangeStep': 'range', 308 | 'rangeStepRight': 'rangeRight', 309 | 'restFrom': 'rest', 310 | 'spreadFrom': 'spread', 311 | 'trimChars': 'trim', 312 | 'trimCharsEnd': 'trimEnd', 313 | 'trimCharsStart': 'trimStart', 314 | 'zipAll': 'zip' 315 | }; 316 | 317 | /** Used to track methods that skip fixing their arity. */ 318 | exports.skipFixed = { 319 | 'castArray': true, 320 | 'flow': true, 321 | 'flowRight': true, 322 | 'iteratee': true, 323 | 'mixin': true, 324 | 'rearg': true, 325 | 'runInContext': true 326 | }; 327 | 328 | /** Used to track methods that skip rearranging arguments. */ 329 | exports.skipRearg = { 330 | 'add': true, 331 | 'assign': true, 332 | 'assignIn': true, 333 | 'bind': true, 334 | 'bindKey': true, 335 | 'concat': true, 336 | 'difference': true, 337 | 'divide': true, 338 | 'eq': true, 339 | 'gt': true, 340 | 'gte': true, 341 | 'isEqual': true, 342 | 'lt': true, 343 | 'lte': true, 344 | 'matchesProperty': true, 345 | 'merge': true, 346 | 'multiply': true, 347 | 'overArgs': true, 348 | 'partial': true, 349 | 'partialRight': true, 350 | 'propertyOf': true, 351 | 'random': true, 352 | 'range': true, 353 | 'rangeRight': true, 354 | 'subtract': true, 355 | 'zip': true, 356 | 'zipObject': true, 357 | 'zipObjectDeep': true 358 | }; 359 | -------------------------------------------------------------------------------- /known_problems.md: -------------------------------------------------------------------------------- 1 | ## Incorrect examples 2 | 3 | ### concat 4 | Should 5 | ```js 6 | _.concat([1], [2, [3], [[4]]]); 7 | // => [ 1, 2, [ 3 ], [ [ 4 ] ] ] 8 | ``` 9 | Does 10 | ```js 11 | // => [ 1, 2, [ 3 ], [ [ 4 ] ] ] 12 | ``` 13 | 14 | ### indexOf 15 | https://gist.github.com/jfmengels/6b973b69c491375117dc#_indexofvalue-array 16 | ```js 17 | // Search from the `fromIndex`. 18 | _.indexOf([2, 2], [1, 2, 1, 2]); 19 | // => 3 20 | ``` 21 | `fromIndex` param is ignored in fp, so it should not be grouped in an array. Even with that, the example would not return the expected result though. 22 | 23 | ### lastIndexOf 24 | 25 | Same as indexOf 26 | 27 | ### intersection 28 | ```js 29 | _.intersection([[4, 2], [1, 2]], [2, 1]); 30 | // => Expected [2] 31 | // => Getting [] 32 | ``` 33 | Intersection does not support more than 2 arrays 34 | 35 | ### union 36 | 37 | Same as intersection 38 | 39 | ### zip 40 | 41 | Same as intersection 42 | 43 | ### unzip 44 | 45 | Same as zip, because it is using zip in the example 46 | 47 | ### unzipWith 48 | 49 | Same as zip, because it is using zip in the example 50 | 51 | ### pullAt 52 | https://gist.github.com/jfmengels/6b973b69c491375117dc#_pullatindexes-array 53 | Too many console.logs removed and nothing remains... 54 | 55 | ### remove 56 | 57 | Same as pullAt 58 | 59 | ### reverse 60 | Working, but there are useless trailing comments 61 | 62 | ### without 63 | Issue filed https://github.com/lodash/lodash/issues/2122 64 | 65 | ### zipWith 66 | 67 | Has one argument too many. Could send a PR for it. 68 | 69 | ### invokeMap 70 | Too many args in example 71 | 72 | ### reduce 73 | Uses key in cb function, which is capped in FP. 74 | 75 | ### defer 76 | Uses defer with 2 args, which now only takes one 77 | 78 | ### now 79 | Uses defer with 2 args, which now only takes one 80 | 81 | ### ary 82 | Arguments are not switched as expected 83 | 84 | ### bind, bindKey 85 | `partials` param is ignored in fp, so it should not be grouped in an array. Even with that, the example would not return the expected result though. 86 | 87 | 88 | 89 | #### TODO 90 | Checked up to curry 91 | -------------------------------------------------------------------------------- /lib/doc/apply-fp-mapping/common.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Entry = require('docdown/lib/entry'); 3 | 4 | var baseGetParams = Entry.prototype.getParams; 5 | 6 | // Function copied from docdown/lib/entry that is not exported. 7 | function getMultilineValue(string, tagName) { 8 | var prelude = tagName == 'description' ? '^ */\\*\\*(?: *\\n *\\* *)?' : ('^ *\\*[\\t ]*@' + _.escapeRegExp(tagName) + '\\b'), 9 | postlude = '(?=\\*\\s+\\@[a-z]|\\*/)', 10 | result = _.result(RegExp(prelude + '([\\s\\S]*?)' + postlude, 'gm').exec(string), 1, ''); 11 | 12 | return _.trim(result.replace(RegExp('(?:^|\\n)[\\t ]*\\*[\\t ]' + (tagName == 'example' ? '?' : '*'), 'g'), '\n')); 13 | 14 | } 15 | 16 | // Function copied from docdown/lib/entry that is not exported. 17 | function hasTag(string, tagName) { 18 | tagName = tagName == '*' ? '\\w+' : _.escapeRegExp(tagName); 19 | return RegExp('^ *\\*[\\t ]*@' + tagName + '\\b', 'm').test(string); 20 | } 21 | 22 | /** 23 | * Return the new ary of a given function. 24 | * 25 | * @param {object} mapping Mapping object that defines the arity of all functions. 26 | * @param {String} name Name of the function associated to the call/function definition. 27 | * @param {boolean} wrapped Flag indicating whether method is wrapped. Will decrement ary if true. 28 | * @return {number} Ary of the function as an integer 29 | */ 30 | function getMethodAry(mapping, name, wrapped) { 31 | var ary = _.find([1, 2, 3, 4], function(cap) { 32 | return _.includes(mapping.aryMethod[cap], name) && cap; 33 | }); 34 | if (_.isNumber(ary) && wrapped) { 35 | return ary - 1; 36 | } 37 | return ary; 38 | } 39 | 40 | /** 41 | * Reorder `params` for a given function definition/call. 42 | * 43 | * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. 44 | * @param {String} name Name of the function associated to the call/function definition. 45 | * @param {*[]} params Parameters/arguments to reorder. 46 | * @param {boolean} wrapped Flag indicating whether method is wrapped. Will decrement ary if true. 47 | * @returns {*[]} Reordered parameters/arguments. 48 | */ 49 | function reorderParams(mapping, name, params, wrapped) { 50 | // Check if reordering is needed. 51 | if (!mapping || mapping.skipRearg[name]) { 52 | return params; 53 | } 54 | var reargOrder = mapping.methodRearg[name] || mapping.aryRearg[getMethodAry(mapping, name, wrapped)]; 55 | if (!reargOrder) { 56 | return params; 57 | } 58 | // Reorder params. 59 | var newParams = []; 60 | reargOrder.forEach(function(newPosition, index) { 61 | newParams[newPosition] = params[index]; 62 | }); 63 | return newParams; 64 | } 65 | 66 | module.exports = { 67 | baseGetParams: baseGetParams, 68 | getMultilineValue: getMultilineValue, 69 | hasTag: hasTag, 70 | getMethodAry: getMethodAry, 71 | reorderParams: reorderParams 72 | }; 73 | -------------------------------------------------------------------------------- /lib/doc/apply-fp-mapping/description.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | j = require('jscodeshift'), 3 | Entry = require('docdown/lib/entry'), 4 | common = require('./common'); 5 | 6 | var baseGetDesc = Entry.prototype.getDesc; 7 | 8 | var lineBreaksRegex = /\n?$/g; 9 | 10 | /** 11 | * Return the `description` of the entry, only without the possible note about mutation. 12 | * 13 | * @returns {Function} Updated description. 14 | */ 15 | function getReorderedExample() { 16 | var lines = baseGetDesc.call(this).split('\n'); 17 | 18 | var indexOfLine = _.findIndex(lines, function(line) { 19 | return _.includes(line, 'mutate'); 20 | }); 21 | 22 | if (indexOfLine === -1) { 23 | return lines.join('\n'); 24 | } 25 | 26 | var linesToDelete = 1; 27 | while (indexOfLine + linesToDelete < lines.length && 28 | !lines[indexOfLine + linesToDelete].startsWith(' 1) { 89 | // defaultValue is probably of the type 'someArg.length' 90 | // Other more complicated cases could be handled but none exist as of this writing. 91 | return memberExpressiontoASTNode(j, args[indexOfReferencedParam], splitDefaultValue[1]); 92 | } 93 | return args[indexOfReferencedParam]; 94 | } 95 | if (defaultValue === '{}') { 96 | return j.objectExpression([]); 97 | } 98 | return stringToASTNode(j, defaultValue); 99 | } 100 | 101 | /** 102 | * Same as _.map, but applied in reverse order. 103 | * 104 | * @param {Array} collection The collection to iterate over. 105 | * @param {Function} iteratee The function invoked per iteration. 106 | * @return {Array} Returns the new mapped array. 107 | */ 108 | function mapRight(array, iteratee) { 109 | var res = []; 110 | var index = array.length; 111 | while (index--) { 112 | res = [iteratee(array[index], index)].concat(res); 113 | } 114 | return res; 115 | } 116 | 117 | /** 118 | * Return the list of arguments, augmented by the default value of the arguments that were ommitted. 119 | * The augmentation only happens when the method call is made without some of the optional arguments, 120 | * and when the arguments these optional arguments have become compulsory. 121 | * For a `function fn(a, b, c=0, d=b.length) { ... }` with an arity of 4, 122 | * when called with `args` [a, ['b']], returns [a, ['b'], 0, ['b'].length]. 123 | * If possible, the value will be evaluated such that ̀`['b'].length` becomes `1`. 124 | * 125 | * @param {object} j JSCodeShift object. 126 | * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. 127 | * @param {String} name Name of the function associated to the call/function definition. 128 | * @param {ASTObject[]} args Arguments to concatenate. 129 | * @param {string[][]} paramsDescription Description of the expected params. 130 | * @return {ASTObject[]} Args along with missing arguments. 131 | */ 132 | function addMissingArguments(j, mapping, name, args, paramsDescription) { 133 | var ary = common.getMethodAry(mapping, name) || 1; 134 | if (ary <= args.length) { 135 | return args; 136 | } 137 | var paramNames = paramsDescription.map(paramName); 138 | var tmpArgs = _.clone(args); 139 | var newArgs = _.compact(mapRight(_.take(paramsDescription, ary), function(paramDescription, index) { 140 | if (index === tmpArgs.length - 1) { 141 | return tmpArgs.pop(); 142 | } 143 | var defaultValue = getDefaultValue(paramDescription); 144 | if (defaultValue !== null) { 145 | return defaultValueToASTNode(j, defaultValue, args, paramNames); 146 | } 147 | return tmpArgs.pop(); 148 | })); 149 | 150 | var nbMissingArgs = Math.max(0, ary - newArgs.length); 151 | var value = stringToASTNode(j, 'null'); 152 | return newArgs.concat(_.times(nbMissingArgs, _.constant(value))); 153 | } 154 | 155 | /** 156 | * Concatenate arguments into an array of arguments. 157 | * For a `function fn(a, b, ...args) { ... }` with an arity of 3, 158 | * when called with `args` [a, b, c, d, e, f], returns [a, b, [c, d, e, f]]. 159 | * 160 | * @param {object} j JSCodeShift object. 161 | * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. 162 | * @param {String} name Name of the function associated to the call/function definition. 163 | * @param {ASTObject[]} args Arguments to concatenate. 164 | * @return {ASTObject[]} Concatenated arguments 165 | */ 166 | function concatExtraArgs(j, mapping, name, args) { 167 | var ary = common.getMethodAry(mapping, name) || 1; 168 | if (args.length <= ary) { 169 | return args; 170 | } 171 | 172 | var concatenatedArgs = j.arrayExpression(_.takeRight(args, args.length - ary + 1)); 173 | return _.take(args, ary - 1).concat(concatenatedArgs); 174 | } 175 | 176 | /** 177 | * Reorder the args in the example if needed, and eventually merges them when 178 | * the method is called with more args than the method's ary. 179 | * 180 | * @param {object} j JSCodeShift object. 181 | * @param {ASTObject} root AST representation of the example 182 | * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. 183 | * @return {ASTObject} AST object where the arguments are reordered/merged 184 | */ 185 | function reorderMethodArgs(j, root, mapping, paramsDescription) { 186 | root.find(j.CallExpression, { callee: { object: {name: '_' }}}) 187 | .replaceWith(function(callExpr, i) { 188 | var value = callExpr.value; 189 | var name = value.callee.property.name; 190 | var argsIncludingMissingOnes = addMissingArguments(j, mapping, name, value.arguments, paramsDescription); 191 | var args = concatExtraArgs(j, mapping, name, argsIncludingMissingOnes); 192 | return j.callExpression( 193 | value.callee, 194 | common.reorderParams(mapping, name, args) 195 | ); 196 | }); 197 | } 198 | 199 | /** 200 | * Remove calls to `console.log` from `codeSample`. 201 | * 202 | * @param {string} codeSample string to remove the calls from. 203 | * @return {string} Updated code sample. 204 | */ 205 | function removeConsoleLogs(codeSample) { 206 | return codeSample 207 | .split('\n') 208 | .filter(function(line) { 209 | return !line.startsWith('console.log'); 210 | }) 211 | .join('\n'); 212 | } 213 | 214 | /** 215 | * Updates a code sample so that the arguments in the call are reordered according to `mapping`. 216 | * 217 | * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. 218 | * @param {string} codeSample Code sample to update. 219 | * @returns {string} Updated code sample. 220 | */ 221 | function reorderParamsInExample(mapping, codeSample, paramsDescription) { 222 | var root = j(removeConsoleLogs(codeSample)); 223 | try { 224 | reorderMethodArgs(j, root, mapping, paramsDescription); 225 | } catch (error) { 226 | console.error(codeSample); 227 | console.error(error.stack); 228 | process.exit(1); 229 | } 230 | return root.toSource(); 231 | } 232 | 233 | /** 234 | * Returns the original, not reordered, list of parameters. 235 | * 236 | * @param {Entry} entryItem Entry whose parameters to get. 237 | * @return {string[][]} List of args. 238 | */ 239 | function getOriginalParams(entryItem) { 240 | var prev = entryItem._params; 241 | entryItem._params = undefined; 242 | common.baseGetParams.call(entryItem); 243 | var result = entryItem._params; 244 | entryItem._params = prev; 245 | return result; 246 | } 247 | 248 | /** 249 | * Returns a function that extracts the entry's `example` data, 250 | * where function call arguments are reordered according to `mapping`. 251 | * 252 | * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. 253 | * @returns {Function} Function that returns the entry's `example` data. 254 | */ 255 | function getReorderedExample(mapping) { 256 | return function() { 257 | var result = common.getMultilineValue(this.entry, 'example'); 258 | if (!result) { 259 | return result; 260 | } 261 | 262 | var paramsDescription = getOriginalParams(this); 263 | var resultReordered = reorderParamsInExample(mapping, result, paramsDescription); 264 | return '```' + this.lang + '\n' + resultReordered + '\n```'; 265 | }; 266 | } 267 | 268 | module.exports = getReorderedExample; 269 | -------------------------------------------------------------------------------- /lib/doc/apply-fp-mapping/index.js: -------------------------------------------------------------------------------- 1 | var Entry = require('docdown/lib/entry'), 2 | getUpdatedDesc = require('./description'), 3 | getReorderedParams = require('./parameters'), 4 | getReorderedExample = require('./example'); 5 | 6 | /** 7 | * Updates `docdown` `Entry`'s prototype so that parameters/arguments are reordered according to `mapping`. 8 | */ 9 | module.exports = function applyFPMapping(mapping) { 10 | Entry.prototype.getDesc = getUpdatedDesc; 11 | Entry.prototype.getParams = getReorderedParams(mapping); 12 | Entry.prototype.getExample = getReorderedExample(mapping); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/doc/apply-fp-mapping/parameters.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | j = require('jscodeshift'), 3 | Entry = require('docdown/lib/entry'), 4 | common = require('./common'); 5 | 6 | var baseGetParams = Entry.prototype.getParams; 7 | 8 | var dotsRegex = /^\.\.\./; 9 | var parensRegex = /^\((.*)\)$/; 10 | var squareBracketsRegex = /^\[(.*)\]$/; 11 | var arrayRegex = /\[\]$/; 12 | 13 | /** 14 | * Return whether method is wrapped. 15 | * 16 | * @param {Entry} entry Entry to look at. 17 | * @return {Boolean} true if the method is wrapped, false if it is static. 18 | */ 19 | function isWrapped(entry) { 20 | return !common.hasTag(entry, 'static'); 21 | } 22 | 23 | /** 24 | * Extract the entry's `name` data. 25 | * Sub-part of Entry.prototype.getCall() that fetches the name. Using `Entry.prototype.getCall()` 26 | * makes a call to getParams(), which itself call getBaseName --> infinite recursion. 27 | * 28 | * @param {object} entry Entry whose name to extract. 29 | * @returns {string} The entry's `name` data. 30 | */ 31 | function getBaseName(entry) { 32 | var result = /\*\/\s*(?:function\s+([^(]*)|(.*?)(?=[:=,]))/.exec(entry); 33 | if (result) { 34 | result = (result[1] || result[2]).split('.').pop(); 35 | result = _.trim(_.trim(result), "'").split('var ').pop(); 36 | result = _.trim(result); 37 | } 38 | // Get the function name. 39 | return _.result(/\*[\t ]*@name\s+(.+)/.exec(entry), 1, result || ''); 40 | } 41 | 42 | /** 43 | * Return `types` as '(X|Y|...)' if `types` contains multiple values, `types[0]` otherwise. 44 | * 45 | * @param {string[]} types Possible types of the parameter. 46 | * @return {string} `types` as a string. 47 | */ 48 | function wrapInParensIfMultiple(types) { 49 | if (types.length > 1) { 50 | return '(' + types.join('|') + ')'; 51 | } 52 | return types[0]; 53 | } 54 | 55 | /** 56 | * Transform parameter type from 'X' to 'X|X[]'. 57 | * 58 | * @param {string[]} param Array whose first item is a description of the parameter type. 59 | * @return {string[]} `param` with the updated type. 60 | */ 61 | function singleItemOrArrayOf(type) { 62 | return type + '|' + type + '[]'; 63 | } 64 | 65 | /** 66 | * Replace parameter type from something like `...number` to `number|number[]`. 67 | * 68 | * @param {string[]} param Array whose first item is a description of the parameter type. 69 | * @return {string[]} `param` with the updated type. 70 | */ 71 | function removeDotsFromTypeAndAllowMultiple(param) { 72 | var type = param[0]; 73 | if (!dotsRegex.test(type)) { 74 | return param; 75 | } 76 | 77 | var newType = _.chain(type) 78 | .replace(dotsRegex, '') 79 | .replace(parensRegex, '$1') 80 | .split('|') 81 | .map(function(s) { 82 | return s.replace(arrayRegex, ''); 83 | }) 84 | .uniq() 85 | .thru(wrapInParensIfMultiple) 86 | .thru(singleItemOrArrayOf) 87 | .value(); 88 | 89 | return [newType].concat(_.tail(param)); 90 | } 91 | 92 | /** 93 | * Replace parameter type from something like `...number` to `number|number[]`. 94 | * 95 | * @param {string[]} param Array whose first item is a description of the parameter type. 96 | * @return {string[]} `param` with the updated type. 97 | */ 98 | function removeDotsFromType(param) { 99 | var type = param[0]; 100 | if (!dotsRegex.test(type)) { 101 | return param; 102 | } 103 | 104 | var newType = type 105 | .replace(dotsRegex, '') 106 | .replace(parensRegex, '$1'); 107 | 108 | return [newType].concat(_.tail(param)); 109 | } 110 | 111 | /** 112 | * Find and duplicate the parameter with a type of the form '...x'. 113 | * 114 | * @param {string} name Name of the method. 115 | * @param {string[]} params Description of the parameters of the method. 116 | * @return {string[]} Updated parameters. 117 | */ 118 | function duplicateRestArrays(name, params) { 119 | var indexOfRestParam = _.findIndex(params, function(param) { 120 | return dotsRegex.test(param[0]); 121 | }); 122 | if (indexOfRestParam === -1) { 123 | console.log('WARNING: method `' + name + '`', 124 | 'is capped to more arguments than its declared number of parameters,', 125 | 'but does not have a parameter like `...x`'); 126 | } 127 | // duplicates param[indexOfRestParam] at its position 128 | return params.slice(0, indexOfRestParam + 1) 129 | .concat(params.slice(indexOfRestParam)); 130 | } 131 | 132 | /** 133 | * Remove the optional default value and brackets around the name of the method. 134 | * 135 | * @param {string[]} param Array whose second item is the name of the param of the form 136 | * 'name', '[name]' or [name=defaultValue]. 137 | * @return {string[]} `param` with the updated name. 138 | */ 139 | function removeDefaultValue(param) { 140 | var name = param[1] 141 | .replace(squareBracketsRegex, '$1') 142 | .split('=') 143 | [0]; 144 | 145 | return [param[0], name, param[2]]; 146 | } 147 | 148 | /** 149 | * Return the updated list of parameters of a method described by `entry`, 150 | * according to changes described by `mapping`. Will, if needed: 151 | * - reorder the arguments 152 | * - remove default values and brackets around previously optional arguments 153 | * - remove ignored arguments 154 | * - duplicate rest arguments if the number of params is less than its cap 155 | * - de-restify arguments 156 | * 157 | * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. 158 | * @param {Entry} entry Method to update. 159 | * @param {string[][]} params List of the original parameters of the method. 160 | * @return {string[][]} Updated list of params. 161 | */ 162 | function updateParamsDescription(mapping, entry, params) { 163 | var tmpParams; 164 | var name = getBaseName(entry); 165 | var ary = common.getMethodAry(mapping, name); 166 | 167 | var wrapped = isWrapped(entry); 168 | if (wrapped) { 169 | // Needs one less argument when wrapped 170 | ary = ary - 1; 171 | params.shift(); 172 | } 173 | 174 | if (ary > params.length) { 175 | tmpParams = duplicateRestArrays(name, params) 176 | .map(removeDotsFromType); 177 | } else { 178 | tmpParams = params 179 | .map(removeDotsFromTypeAndAllowMultiple); 180 | } 181 | tmpParams = _.take(tmpParams, ary).map(removeDefaultValue); 182 | return common.reorderParams(mapping, name, tmpParams, wrapped); 183 | } 184 | 185 | /** 186 | * Return a function that extracts the entry's `param` data, reordered according to `mapping`. 187 | * 188 | * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. 189 | * @returns {Function} Function that returns the entry's `param` data. 190 | */ 191 | function getReorderedParams(mapping) { 192 | return function(index) { 193 | if (!this._params) { 194 | // Call baseGetParams in order to compute `this._params`. 195 | baseGetParams.call(this); 196 | // Reorder params according to the `mapping`. 197 | this._params = updateParamsDescription(mapping, this.entry, this._params); 198 | } 199 | return baseGetParams.call(this, index); 200 | }; 201 | } 202 | 203 | /** 204 | * Updates `docdown` `Entry`'s prototype so that parameters/arguments are reordered according to `mapping`. 205 | */ 206 | module.exports = getReorderedParams; 207 | -------------------------------------------------------------------------------- /lib/doc/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | docdown = require('docdown'), 5 | fs = require('fs-extra'), 6 | path = require('path'); 7 | 8 | var mapping = require('../../fp/_mapping'), 9 | applyFPMapping = require('./apply-fp-mapping'); 10 | 11 | var basePath = path.join(__dirname, '..', '..'), 12 | docPath = path.join(basePath, 'doc'), 13 | readmePath = path.join(docPath, 'README.md'), 14 | fpReadmePath = path.join(docPath, 'fp.md'); 15 | 16 | var pkg = require('../../package.json'), 17 | version = pkg.version; 18 | 19 | var config = { 20 | 'base': { 21 | 'entryLinks': [ 22 | '<% if (name == "templateSettings" || !/^(?:methods|properties|seq)$/i.test(category)) {' + 23 | 'print("[Ⓝ](https://www.npmjs.com/package/lodash." + name.toLowerCase() + " \\"See the npm package\\")")' + 24 | '} %>' 25 | ], 26 | 'path': path.join(basePath, 'lodash.js'), 27 | 'title': 'lodash v' + version + '', 28 | 'toc': 'categories', 29 | 'url': 'https://github.com/lodash/lodash/blob/' + version + '/lodash.js' 30 | }, 31 | 'github': { 32 | 'hash': 'github' 33 | }, 34 | 'site': { 35 | 'tocLink': '#docs' 36 | } 37 | }; 38 | 39 | function postprocess(string) { 40 | // Fix docdown bug by wrapping symbol property identifiers in brackets. 41 | return string.replace(/\.(Symbol\.(?:[a-z]+[A-Z]?)+)/g, '[$1]'); 42 | } 43 | 44 | /*----------------------------------------------------------------------------*/ 45 | 46 | function onComplete(error) { 47 | if (error) { 48 | throw error; 49 | } 50 | } 51 | 52 | function build(fpFlag, type) { 53 | if (fpFlag) { 54 | applyFPMapping(mapping); 55 | } 56 | var options = _.defaults({}, config.base, config[type]), 57 | markdown = docdown(options), 58 | filePath = fpFlag ? fpReadmePath : readmePath; 59 | 60 | fs.writeFile(filePath, postprocess(markdown), onComplete); 61 | } 62 | 63 | build(_.includes(process.argv, '--fp'), _.last(process.argv)); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lodash-fp-docs", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "doc": "mkdir doc ; node lib/doc/build github", 8 | "doc:fp": "mkdir doc ; node lib/doc/build --fp github", 9 | "style": "jscs lib/**/*.js", 10 | "test": "node test/test-fp-doc" 11 | }, 12 | "keywords": [], 13 | "author": "Jeroen Engels ", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "fs-extra": "^0.26.5", 17 | "glob": "^7.0.0", 18 | "jscs": "^2.10.1", 19 | "qunit-extras": "^1.4.5", 20 | "qunitjs": "^1.22.0" 21 | }, 22 | "dependencies": { 23 | "docdown": "~0.5.0", 24 | "jscodeshift": "^0.3.13", 25 | "lodash": "^4.5.1", 26 | "recast": "^0.11.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/test-fp-doc.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | /** Used as a safe reference for `undefined` in pre-ES5 environments. */ 3 | var undefined; 4 | 5 | /** Used as a reference to the global object. */ 6 | var root = (typeof global == 'object' && global) || this; 7 | 8 | var phantom = root.phantom, 9 | amd = root.define && define.amd, 10 | document = !phantom && root.document, 11 | noop = function() {}, 12 | argv = root.process && process.argv; 13 | 14 | /** Use a single "load" function. */ 15 | var load = (!amd && typeof require == 'function') 16 | ? require 17 | : noop; 18 | 19 | /** The unit testing framework. */ 20 | var QUnit = root.QUnit || (root.QUnit = ( 21 | QUnit = load('../node_modules/qunitjs/qunit/qunit.js') || root.QUnit, 22 | QUnit = QUnit.QUnit || QUnit 23 | )); 24 | 25 | /** Load stable Lodash and QUnit Extras. */ 26 | var _ = root._ || load('../lodash.js'); 27 | if (_) { 28 | _ = _.runInContext(root); 29 | } 30 | var QUnitExtras = load('../node_modules/qunit-extras/qunit-extras.js'); 31 | if (QUnitExtras) { 32 | QUnitExtras.runInContext(root); 33 | } 34 | 35 | var mapping = root.mapping || load('../fp/_mapping.js'), 36 | applyFPMapping = load('../lib/doc/apply-fp-mapping'), 37 | Entry = load('docdown/lib/entry'); 38 | 39 | /*--------------------------------------------------------------------------*/ 40 | 41 | function toCommentLine(line) { 42 | return ' * ' + line; 43 | } 44 | 45 | function toEntry(name, paramLines, exampleLines, attachedToPrototype) { 46 | var start = [ 47 | '/**', 48 | ' * ', 49 | ' * Foo', 50 | ' * ' 51 | ]; 52 | var end = [ 53 | ' */', 54 | 'function ' + name + '(a, b, c) {', 55 | '', 56 | '}' 57 | ]; 58 | var staticLine = attachedToPrototype ? [] : [' * @static']; 59 | var params = paramLines.map(function(line) { 60 | return ' * @param ' + line; 61 | }); 62 | var example = (exampleLines || []).map(toCommentLine); 63 | 64 | return [].concat(start, staticLine, params, [' * @example'], example, end).join('\n'); 65 | } 66 | 67 | var differenceBySource = toEntry('differenceBy', [ 68 | '{Array} array The array to inspect.', 69 | '{...Array} [values] The values to exclude.', 70 | '{Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.' 71 | ], [ 72 | '_.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);', 73 | '// → [3.1, 1.3]', 74 | '', 75 | '// The `_.property` iteratee shorthand.', 76 | "_.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');", 77 | "// → [{ 'x': 2 }]" 78 | ]); 79 | 80 | var setParams = [ 81 | '{Object} object The object to modify.', 82 | '{Array|string} path The path of the property to set.', 83 | '{*} value The value to set.' 84 | ]; 85 | 86 | /*--------------------------------------------------------------------------*/ 87 | 88 | if (argv) { 89 | console.log('Running doc generation tests.'); 90 | } 91 | 92 | mapping.aryMethod[2].push('customFun'); 93 | applyFPMapping(mapping); 94 | 95 | /*--------------------------------------------------------------------------*/ 96 | 97 | QUnit.module('getExample'); 98 | 99 | (function() { 100 | QUnit.test('should reorder parameters', function(assert) { 101 | assert.expect(1); 102 | 103 | var entry = new Entry(differenceBySource, differenceBySource); 104 | 105 | var actual = entry.getExample(); 106 | 107 | assert.equal(actual, [ 108 | '```js', 109 | '_.differenceBy(Math.floor, [4.4, 2.5], [3.1, 2.2, 1.3]);', 110 | '// → [3.1, 1.3]', 111 | '', 112 | '// The `_.property` iteratee shorthand.', 113 | "_.differenceBy('x', [{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }]);", 114 | "// → [{ 'x': 2 }]", 115 | '```' 116 | ].join('\n')); 117 | }); 118 | 119 | QUnit.test('should reorder parameters that have a special order', function(assert) { 120 | assert.expect(1); 121 | 122 | var example = toEntry('set', setParams, [ 123 | "var object = { 'a': [{ 'b': { 'c': 3 } }] };", 124 | "_.set(object, 'a[0].b.c', 4);", 125 | "_.set(object, 'x[0].y.z', 5);", 126 | ]); 127 | var entry = new Entry(example, example); 128 | 129 | var actual = entry.getExample(); 130 | 131 | assert.equal(actual, [ 132 | '```js', 133 | "var object = { 'a': [{ 'b': { 'c': 3 } }] };", 134 | "_.set('a[0].b.c', 4, object);", 135 | "_.set('x[0].y.z', 5, object);", 136 | '```' 137 | ].join('\n')); 138 | }); 139 | 140 | QUnit.test('should preserve comments', function(assert) { 141 | assert.expect(1); 142 | 143 | var example = toEntry('set', setParams, [ 144 | "var object = { 'a': [{ 'b': { 'c': 3 } }] };", 145 | "_.set(object, 'a[0].b.c', 4);", 146 | '// => 4', 147 | "_.set(object, 'x[0].y.z', 5);", 148 | ]); 149 | var entry = new Entry(example, example); 150 | 151 | var actual = entry.getExample(); 152 | 153 | assert.equal(actual, [ 154 | '```js', 155 | "var object = { 'a': [{ 'b': { 'c': 3 } }] };", 156 | "_.set('a[0].b.c', 4, object);", 157 | '// => 4', 158 | "_.set('x[0].y.z', 5, object);", 159 | '```' 160 | ].join('\n')); 161 | }); 162 | 163 | QUnit.test('should remove console.logs from example', function(assert) { 164 | assert.expect(1); 165 | 166 | var example = toEntry('set', setParams, [ 167 | "var object = { 'a': [{ 'b': { 'c': 3 } }] };", 168 | '', 169 | "_.set(object, 'a[0].b.c', 4);", 170 | 'console.log(object.a[0].b.c);', 171 | '// => 4', 172 | '', 173 | "_.set(object, 'x[0].y.z', 5);", 174 | 'console.log(object.x[0].y.z);', 175 | '// => 5' 176 | ]); 177 | var entry = new Entry(example, example); 178 | 179 | var actual = entry.getExample(); 180 | 181 | assert.equal(actual, [ 182 | '```js', 183 | "var object = { 'a': [{ 'b': { 'c': 3 } }] };", 184 | '', 185 | "_.set('a[0].b.c', 4, object);", 186 | '// => 4', 187 | '', 188 | "_.set('x[0].y.z', 5, object);", 189 | '// => 5', 190 | '```' 191 | ].join('\n')); 192 | }); 193 | 194 | QUnit.test('should merge extra arguments into an array', function(assert) { 195 | assert.expect(1); 196 | 197 | var example = toEntry('pullAt', [ 198 | '{Array} array The array to modify.', 199 | '{...(number|number[])} [indexes] The indexes of elements to remove,\n' + 200 | ' * specified individually or in arrays.' 201 | ], [ 202 | 'var array = [5, 10, 15, 20];', 203 | 'var evens = _.pullAt(array, 1, 3);', 204 | '', 205 | 'console.log(array);', 206 | '// => [5, 15]', 207 | '', 208 | 'console.log(evens);', 209 | '// => [10, 20]' 210 | ]); 211 | var entry = new Entry(example, example); 212 | 213 | var actual = entry.getExample(); 214 | 215 | assert.equal(actual, [ 216 | '```js', 217 | 'var array = [5, 10, 15, 20];', 218 | 'var evens = _.pullAt([1, 3], array);', 219 | '', 220 | '// => [5, 15]', 221 | '', 222 | '// => [10, 20]', 223 | '```' 224 | ].join('\n')); 225 | }); 226 | 227 | QUnit.test('should inject default values into optional arguments that became compulsory', function(assert) { 228 | assert.expect(1); 229 | 230 | var example = toEntry('sampleSize', [ 231 | '{Array|Object} collection The collection to sample.', 232 | '{number} [n=0] The number of elements to sample.' 233 | ], [ 234 | '_.sampleSize([1, 2, 3]);', 235 | '// => [3, 1]', 236 | '', 237 | '_.sampleSize([1, 2, 3], 4);', 238 | '// => [2, 3, 1]' 239 | ]); 240 | var entry = new Entry(example, example); 241 | 242 | var actual = entry.getExample(); 243 | 244 | assert.equal(actual, [ 245 | '```js', 246 | '_.sampleSize(0, [1, 2, 3]);', 247 | '// => [3, 1]', 248 | '', 249 | '_.sampleSize(4, [1, 2, 3]);', 250 | '// => [2, 3, 1]', 251 | '```' 252 | ].join('\n')); 253 | }); 254 | 255 | QUnit.test('should inject referenced values into optional arguments that became compulsory, ' + 256 | 257 | 'if a parameter\'s default value references parameter (direct reference)', 258 | function(assert) { 259 | assert.expect(1); 260 | 261 | var example = toEntry('customFun', [ 262 | '{Array} array Array', 263 | '{number} [foo=array] Foo' 264 | ], [ 265 | '_.customFun([1, 2, 3]);', 266 | ]); 267 | var entry = new Entry(example, example); 268 | 269 | var actual = entry.getExample(); 270 | 271 | assert.equal(actual, [ 272 | '```js', 273 | '_.customFun([1, 2, 3], [1, 2, 3]);', 274 | '```' 275 | ].join('\n')); 276 | }); 277 | 278 | QUnit.test('should inject referenced values into optional arguments that became compulsory, ' + 279 | 'if a parameter\'s default value references parameter (member expression)', 280 | function(assert) { 281 | assert.expect(1); 282 | 283 | var example = toEntry('fill', [ 284 | '{Array} array The array to fill.', 285 | '{*} value The value to fill `array` with.', 286 | '{number} [start=0] The start position.', 287 | '{number} [end=array.length] The end position.' 288 | ], [ 289 | 'var array = [1, 2, 3];', 290 | '', 291 | "_.fill(array, 'a');", 292 | 'console.log(array);', 293 | "// => ['a', 'a', 'a']", 294 | '', 295 | '_.fill(Array(3), 2, 1);', 296 | '// => [undefined, 2, 2]', 297 | '', 298 | "_.fill([4, 6, 8, 10], '*');", 299 | "// => [*, '*', '*', *]" 300 | ]); 301 | var entry = new Entry(example, example); 302 | 303 | var actual = entry.getExample(); 304 | 305 | assert.equal(actual, [ 306 | '```js', 307 | 'var array = [1, 2, 3];', 308 | '', 309 | "_.fill(0, array.length, 'a', array);", 310 | "// => ['a', 'a', 'a']", 311 | '', 312 | '_.fill(1, 3, 2, Array(3));', 313 | '// => [undefined, 2, 2]', 314 | '', 315 | "_.fill(0, 4, '*', [4, 6, 8, 10]);", 316 | "// => [*, '*', '*', *]", 317 | '```' 318 | ].join('\n')); 319 | }); 320 | 321 | QUnit.test('should inject default values in the middle of the arguments', function(assert) { 322 | assert.expect(1); 323 | 324 | var example = toEntry('inRange', [ 325 | '{number} number The number to check.', 326 | '{number} [start=0] The start of the range.', 327 | '{number} end The end of the range.' 328 | ], [ 329 | '_.inRange(4, 8);', 330 | '// => true' 331 | ]); 332 | var entry = new Entry(example, example); 333 | 334 | var actual = entry.getExample(); 335 | 336 | assert.equal(actual, [ 337 | '```js', 338 | '_.inRange(8, 0, 4);', 339 | '// => true', 340 | '```' 341 | ].join('\n')); 342 | }); 343 | 344 | QUnit.test('should not use ignored params as default values', function(assert) { 345 | assert.expect(1); 346 | 347 | var example = toEntry('drop', [ 348 | '{Array} array The array to query.', 349 | '{number} [n=1] The number of elements to drop.', 350 | '{Object} [guard] Enables use as an iteratee for functions like `_.map`.' 351 | ], [ 352 | '_.drop([1, 2, 3]);', 353 | '// => [2, 3]' 354 | ]); 355 | var entry = new Entry(example, example); 356 | 357 | var actual = entry.getExample(); 358 | 359 | assert.equal(actual, [ 360 | '```js', 361 | '_.drop(1, [1, 2, 3]);', 362 | '// => [2, 3]', 363 | '```' 364 | ].join('\n')); 365 | }); 366 | }()); 367 | 368 | /*--------------------------------------------------------------------------*/ 369 | 370 | QUnit.module('getParams'); 371 | 372 | (function() { 373 | QUnit.test('should reorder arguments and remove default values', function(assert) { 374 | assert.expect(1); 375 | 376 | var example = toEntry('differenceBy', [ 377 | '{Array} array The array to inspect.', 378 | '{...Array} [values] The values to exclude.', 379 | '{Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.' 380 | ]); 381 | var entry = new Entry(example, example); 382 | 383 | var actual = entry.getParams(); 384 | 385 | assert.deepEqual(actual, [ 386 | ['Function|Object|string', 'iteratee', 'The iteratee invoked per element. '], 387 | ['Array|Array[]', 'values', 'The values to exclude. '], 388 | ['Array', 'array', 'The array to inspect. '] 389 | ]); 390 | }); 391 | 392 | QUnit.test('should reorder arguments that have a special order', function(assert) { 393 | assert.expect(1); 394 | 395 | var example = toEntry('set', [ 396 | '{Object} object The object to modify.', 397 | '{Array|string} path The path of the property to set.', 398 | '{*} value The value to set.' 399 | ]); 400 | var entry = new Entry(example, example); 401 | 402 | var actual = entry.getParams(); 403 | 404 | assert.deepEqual(actual, [ 405 | ['Array|string', 'path', 'The path of the property to set. '], 406 | ['*', 'value', 'The value to set. '], 407 | ['Object', 'object', 'The object to modify. '], 408 | ]); 409 | }); 410 | 411 | QUnit.test('should transform rest arguments into an array', function(assert) { 412 | assert.expect(1); 413 | 414 | var example = toEntry('pullAt', [ 415 | '{Array} array The array to modify.', 416 | '{...(number|number[])} [indexes] The indexes of elements to remove,\n' + 417 | ' * specified individually or in arrays.' 418 | ]); 419 | var entry = new Entry(example, example); 420 | 421 | var actual = entry.getParams(); 422 | 423 | assert.deepEqual(actual, [ 424 | // TODO Remove this line in favor of the commented one. 425 | // Is linked to a docdown issue (https://github.com/jdalton/docdown/pull/37) 426 | // that does not handle parens in the arguments well 427 | ['((number|number)|((number|number)[]', 'indexes', 'The indexes of elements to remove, specified individually or in arrays. '], 428 | // ['number|number[]', '[indexes]', 'The indexes of elements to remove, specified individually or in arrays. '], 429 | ['Array', 'array', 'The array to modify. '], 430 | ]); 431 | }); 432 | 433 | QUnit.test('should duplicate and de-restify "rest" parameters if there are less parameters than cap', function(assert) { 434 | assert.expect(1); 435 | 436 | var example = toEntry('intersectionWith', [ 437 | '{...Array} [arrays] The arrays to inspect.', 438 | '{Function} [comparator] The comparator invoked per element.' 439 | ]); 440 | var entry = new Entry(example, example); 441 | 442 | var actual = entry.getParams(); 443 | 444 | assert.deepEqual(actual, [ 445 | ['Function', 'comparator', 'The comparator invoked per element. '], 446 | ['Array', 'arrays', 'The arrays to inspect. '], 447 | ['Array', 'arrays', 'The arrays to inspect. '] 448 | ]); 449 | }); 450 | 451 | QUnit.test('should consider method to have an ary of `ary - 1` when capped and wrapped', function(assert) { 452 | assert.expect(1); 453 | 454 | var wrapped = true; 455 | var example = toEntry('flatMap', [ 456 | '{Array} array The array to iterate over.', 457 | '{Function|Object|string} [iteratee=_.identity] The function invoked per iteration.' 458 | ], [], wrapped); 459 | var entry = new Entry(example, example); 460 | 461 | var actual = entry.getParams(); 462 | 463 | assert.deepEqual(actual, [ 464 | ['Function|Object|string', 'iteratee', 'The function invoked per iteration. '] 465 | ]); 466 | }); 467 | 468 | QUnit.test('should remove arguments ignored because of capping (includes)', function(assert) { 469 | assert.expect(1); 470 | 471 | var example = toEntry('includes', [ 472 | '{Array|Object|string} collection The collection to search.', 473 | '{*} value The value to search for.', 474 | '{number} [fromIndex=0] The index to search from.' 475 | ]); 476 | var entry = new Entry(example, example); 477 | 478 | var actual = entry.getParams(); 479 | 480 | assert.deepEqual(actual, [ 481 | ['*', 'value', 'The value to search for. '], 482 | ['Array|Object|string', 'collection', 'The collection to search. '] 483 | ]); 484 | }); 485 | 486 | QUnit.test('should remove arguments ignored because of capping (trim)', function(assert) { 487 | assert.expect(1); 488 | 489 | var example = toEntry('trim', [ 490 | "{string} [string=''] The string to trim.", 491 | '{string} [chars=whitespace] The characters to trim.' 492 | ]); 493 | var entry = new Entry(example, example); 494 | 495 | var actual = entry.getParams(); 496 | 497 | assert.deepEqual(actual, [ 498 | ['string', 'string', 'The string to trim. '] 499 | ]); 500 | }); 501 | }()); 502 | 503 | /*--------------------------------------------------------------------------*/ 504 | 505 | QUnit.module('getDesc'); 506 | 507 | (function() { 508 | function toSourceWithDescription(name, description) { 509 | var start = [ 510 | '/**', 511 | ' * ' 512 | ]; 513 | 514 | var end = [ 515 | ' * @static', 516 | ' * ', 517 | ' */', 518 | 'function ' + name + '(a, b, c) {', 519 | '', 520 | '}' 521 | ]; 522 | 523 | var descriptionLines = description.map(toCommentLine); 524 | return [].concat(start, descriptionLines, end).join('\n'); 525 | } 526 | 527 | QUnit.test('should remove notes about mutators arguments and remove default values', function(assert) { 528 | assert.expect(1); 529 | 530 | var example = toSourceWithDescription('pullAt', [ 531 | 'Removes elements from `array` corresponding to `indexes` and returns an', 532 | 'array of removed elements.', 533 | '', 534 | '**Note:** Unlike `_.at`, this method mutates `array`.', 535 | '' 536 | ]); 537 | 538 | var entry = new Entry(example, example); 539 | 540 | var actual = entry.getDesc(); 541 | 542 | assert.equal(actual, [ 543 | 'Removes elements from `array` corresponding to `indexes` and returns an', 544 | 'array of removed elements.' 545 | ].join('\n')); 546 | }); 547 | 548 | QUnit.test('should remove following related lines', function(assert) { 549 | assert.expect(1); 550 | 551 | var example = toSourceWithDescription('assign', [ 552 | 'Assigns own enumerable properties of source objects to the destination', 553 | 'object. Source objects are applied from left to right. Subsequent sources', 554 | 'overwrite property assignments of previous sources.', 555 | '', 556 | '**Note:** This method mutates `object` and is loosely based on', 557 | '[`Object.assign`](https://mdn.io/Object/assign).', 558 | '' 559 | ]); 560 | 561 | var entry = new Entry(example, example); 562 | 563 | var actual = entry.getDesc(); 564 | 565 | assert.equal(actual, [ 566 | 'Assigns own enumerable properties of source objects to the destination', 567 | 'object. Source objects are applied from left to right. Subsequent sources', 568 | 'overwrite property assignments of previous sources.', 569 | ].join('\n')); 570 | }); 571 | }()); 572 | 573 | QUnit.config.asyncRetries = 10; 574 | QUnit.config.hidepassed = true; 575 | 576 | if (!document) { 577 | QUnit.config.noglobals = true; 578 | QUnit.load(); 579 | } 580 | }.call(this)); 581 | --------------------------------------------------------------------------------