@vanillaes/jsdown 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/bin/jsdown.js +21 -0
- package/package.json +40 -0
- package/src/alias.js +231 -0
- package/src/docdown.js +31 -0
- package/src/entry.js +532 -0
- package/src/generator.js +369 -0
- package/src/index.js +6 -0
- package/src/jsdown.js +55 -0
- package/src/util.js +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<p align="center"><strong>✓ NOTICE: This project is currently a WIP ✓</strong></p>
|
|
2
|
+
|
|
3
|
+
<h1 align="center">JSDown</h1>
|
|
4
|
+
|
|
5
|
+
<div align="center">📜 Markdown Doc Generator for JSDoc 📜</div>
|
|
6
|
+
|
|
7
|
+
<br />
|
|
8
|
+
|
|
9
|
+
<div align="center">
|
|
10
|
+
<a href="https://github.com/vanillaes/jsdown/releases"><img src="https://badgen.net/github/tag/vanillaes/jsdown?cache-control=no-cache" alt="GitHub Release"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@vanillaes/jsdown"><img src="https://badgen.net/npm/v/@vanillaes/jsdown?icon=npm" alt="NPM Version"></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@vanillaes/jsdown"><img src="https://badgen.net/npm/dm/@vanillaes/jsdown?icon=npm" alt="NPM Downloads"></a>
|
|
13
|
+
<a href="https://github.com/vanillaes/jsdown/actions"><img src="https://github.com/vanillaes/jsdown/workflows/Latest/badge.svg" alt="Latest Status"></a>
|
|
14
|
+
<a href="https://github.com/vanillaes/jsdown/actions"><img src="https://github.com/vanillaes/jsdown/workflows/Release/badge.svg" alt="Release Status"></a>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **No Configuration**
|
|
20
|
+
- Feed it JSDoc types and it spits out Markdown
|
|
21
|
+
|
|
22
|
+
## jsdown
|
|
23
|
+
|
|
24
|
+
### Arguments
|
|
25
|
+
|
|
26
|
+
`jsdown [...options] [files...]`
|
|
27
|
+
|
|
28
|
+
- `[files]` - File(s) to lint (default `**/!(*.spec|index).js`)
|
|
29
|
+
|
|
30
|
+
### Usage
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
# generate documentation
|
|
34
|
+
jsdown
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
# generate documentation (matching a different file(s))
|
|
38
|
+
lint-es '**/!(*.spec|index).cjs'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
*Note: In Linux/OSX, matcher patterns must be delimited in quotes.*
|
package/bin/jsdown.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createDocs } from '../src/index.js'
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
const pkg = require('../package.json')
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
.name('jsdown')
|
|
11
|
+
.description('Markdown Doc Generator for JSDoc')
|
|
12
|
+
.version(pkg.version, '-v, --version')
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.description('Lint file(s) matching the provided pattern (default *.spec.js)')
|
|
16
|
+
.argument('[files]', 'file(s) to lint', '**/!(*.spec|index).js')
|
|
17
|
+
.action((files, options) => {
|
|
18
|
+
createDocs(files, options)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
program.parse(process.argv)
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vanillaes/jsdown",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Markdown Doc Generator for JSDoc",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ecmascript",
|
|
7
|
+
"esm",
|
|
8
|
+
"esmodules",
|
|
9
|
+
"cli",
|
|
10
|
+
"markdown",
|
|
11
|
+
"jsdoc",
|
|
12
|
+
"documentation"
|
|
13
|
+
],
|
|
14
|
+
"repository": "https://github.com/vanillaes/jsdown",
|
|
15
|
+
"author": "Evan Plaice <evanplaice@gmail.com> (https://evanplaice.com)",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"jsdown": "bin/jsdown.js"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/index.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "esmtk test",
|
|
26
|
+
"lint": "esmtk lint",
|
|
27
|
+
"type": "esmtk type",
|
|
28
|
+
"typings": "esmtk typings",
|
|
29
|
+
"clean": "esmtk clean --typings",
|
|
30
|
+
"preview": "esmtk preview",
|
|
31
|
+
"preversion": "npm run test && npm run lint",
|
|
32
|
+
"postversion": "git push --follow-tags"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@vanillaes/esmtk": "^1.8.0",
|
|
36
|
+
"commander": "^14.0.3",
|
|
37
|
+
"doctrine": "^3.0.0",
|
|
38
|
+
"lodash-es": "^4.18.1"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/alias.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/check-tag-names */
|
|
2
|
+
|
|
3
|
+
import _ from 'lodash-es'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Alias constructor.
|
|
7
|
+
* @constructor
|
|
8
|
+
* @param {string} name The alias name.
|
|
9
|
+
* @param {object} owner The alias owner.
|
|
10
|
+
*/
|
|
11
|
+
export function Alias (name, owner) {
|
|
12
|
+
this._owner = owner
|
|
13
|
+
this._name = name
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts the entry's `alias` objects.
|
|
18
|
+
* @memberOf Alias
|
|
19
|
+
* @param {number} [index] The index of the array value to return.
|
|
20
|
+
* @returns {Array|string} Returns the entry's `alias` objects.
|
|
21
|
+
*/
|
|
22
|
+
function getAliases (index) {
|
|
23
|
+
return index == null ? [] : undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts the function call from the owner entry.
|
|
28
|
+
* @memberOf Alias
|
|
29
|
+
* @returns {string} Returns the function call.
|
|
30
|
+
*/
|
|
31
|
+
function getCall () {
|
|
32
|
+
return this._owner.getCall()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extracts the owner entry's `category` data.
|
|
37
|
+
* @memberOf Alias
|
|
38
|
+
* @returns {string} Returns the owner entry's `category` data.
|
|
39
|
+
*/
|
|
40
|
+
function getCategory () {
|
|
41
|
+
return this._owner.getCategory()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extracts the owner entry's description.
|
|
46
|
+
* @memberOf Alias
|
|
47
|
+
* @returns {string} Returns the owner entry's description.
|
|
48
|
+
*/
|
|
49
|
+
function getDesc () {
|
|
50
|
+
return this._owner.getDesc()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extracts the owner entry's `example` data.
|
|
55
|
+
* @memberOf Alias
|
|
56
|
+
* @returns {string} Returns the owner entry's `example` data.
|
|
57
|
+
*/
|
|
58
|
+
function getExample () {
|
|
59
|
+
return this._owner.getExample()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracts the entry's hash value for permalinking.
|
|
64
|
+
* @memberOf Alias
|
|
65
|
+
* @param {string} [style] The hash style.
|
|
66
|
+
* @returns {string} Returns the entry's hash value (without a hash itself).
|
|
67
|
+
*/
|
|
68
|
+
function getHash (style) {
|
|
69
|
+
return this._owner.getHash(style)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the owner entry's line number.
|
|
74
|
+
* @memberOf Alias
|
|
75
|
+
* @returns {number} Returns the owner entry's line number.
|
|
76
|
+
*/
|
|
77
|
+
function getLineNumber () {
|
|
78
|
+
return this._owner.getLineNumber()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extracts the owner entry's `member` data.
|
|
83
|
+
* @memberOf Alias
|
|
84
|
+
* @param {number} [index] The index of the array value to return.
|
|
85
|
+
* @returns {Array|string} Returns the owner entry's `member` data.
|
|
86
|
+
*/
|
|
87
|
+
function getMembers (index) {
|
|
88
|
+
return this._owner.getMembers(index)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extracts the owner entry's `name` data.
|
|
93
|
+
* @memberOf Alias
|
|
94
|
+
* @returns {string} Returns the owner entry's `name` data.
|
|
95
|
+
*/
|
|
96
|
+
function getName () {
|
|
97
|
+
return this._name
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Gets the owner entry object.
|
|
102
|
+
* @memberOf Alias
|
|
103
|
+
* @returns {object} Returns the owner entry.
|
|
104
|
+
*/
|
|
105
|
+
function getOwner () {
|
|
106
|
+
return this._owner
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extracts the owner entry's `param` data.
|
|
111
|
+
* @memberOf Alias
|
|
112
|
+
* @param {number} [index] The index of the array value to return.
|
|
113
|
+
* @returns {Array} Returns the owner entry's `param` data.
|
|
114
|
+
*/
|
|
115
|
+
function getParams (index) {
|
|
116
|
+
return this._owner.getParams(index)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extracts the owner entry's `returns` data.
|
|
121
|
+
* @memberOf Alias
|
|
122
|
+
* @returns {string} Returns the owner entry's `returns` data.
|
|
123
|
+
*/
|
|
124
|
+
function getReturns () {
|
|
125
|
+
return this._owner.getReturns()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extracts the owner entry's `since` data.
|
|
130
|
+
* @memberOf Alias
|
|
131
|
+
* @returns {string} Returns the owner entry's `since` data.
|
|
132
|
+
*/
|
|
133
|
+
function getSince () {
|
|
134
|
+
return this._owner.getSince()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extracts the owner entry's `type` data.
|
|
139
|
+
* @memberOf Alias
|
|
140
|
+
* @returns {string} Returns the owner entry's `type` data.
|
|
141
|
+
*/
|
|
142
|
+
function getType () {
|
|
143
|
+
return this._owner.getType()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Checks if the entry is an alias.
|
|
148
|
+
* @memberOf Alias
|
|
149
|
+
* @returns {boolean} Returns `true`.
|
|
150
|
+
*/
|
|
151
|
+
function isAlias () {
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Checks if the owner entry is a constructor.
|
|
157
|
+
* @memberOf Alias
|
|
158
|
+
* @returns {boolean} Returns `true` if a constructor, else `false`.
|
|
159
|
+
*/
|
|
160
|
+
function isCtor () {
|
|
161
|
+
return this._owner.isCtor()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Checks if the entry is a function reference.
|
|
166
|
+
* @memberOf Alias
|
|
167
|
+
* @returns {boolean} Returns `true` if the entry is a function reference, else `false`.
|
|
168
|
+
*/
|
|
169
|
+
function isFunction () {
|
|
170
|
+
return this._owner.isFunction()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Checks if the owner entry is a license.
|
|
175
|
+
* @memberOf Alias
|
|
176
|
+
* @returns {boolean} Returns `true` if a license, else `false`.
|
|
177
|
+
*/
|
|
178
|
+
function isLicense () {
|
|
179
|
+
return this._owner.isLicense()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Checks if the owner entry *is* assigned to a prototype.
|
|
184
|
+
* @memberOf Alias
|
|
185
|
+
* @returns {boolean} Returns `true` if assigned to a prototype, else `false`.
|
|
186
|
+
*/
|
|
187
|
+
function isPlugin () {
|
|
188
|
+
return this._owner.isPlugin()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Checks if the owner entry is private.
|
|
193
|
+
* @memberOf Alias
|
|
194
|
+
* @returns {boolean} Returns `true` if private, else `false`.
|
|
195
|
+
*/
|
|
196
|
+
function isPrivate () {
|
|
197
|
+
return this._owner.isPrivate()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Checks if the owner entry is *not* assigned to a prototype.
|
|
202
|
+
* @memberOf Alias
|
|
203
|
+
* @returns {boolean} Returns `true` if not assigned to a prototype, else `false`.
|
|
204
|
+
*/
|
|
205
|
+
function isStatic () {
|
|
206
|
+
return this._owner.isStatic()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_.assign(Alias.prototype, {
|
|
210
|
+
getAliases,
|
|
211
|
+
getCall,
|
|
212
|
+
getCategory,
|
|
213
|
+
getDesc,
|
|
214
|
+
getExample,
|
|
215
|
+
getHash,
|
|
216
|
+
getLineNumber,
|
|
217
|
+
getMembers,
|
|
218
|
+
getName,
|
|
219
|
+
getOwner,
|
|
220
|
+
getParams,
|
|
221
|
+
getReturns,
|
|
222
|
+
getSince,
|
|
223
|
+
getType,
|
|
224
|
+
isAlias,
|
|
225
|
+
isCtor,
|
|
226
|
+
isFunction,
|
|
227
|
+
isLicense,
|
|
228
|
+
isPlugin,
|
|
229
|
+
isPrivate,
|
|
230
|
+
isStatic
|
|
231
|
+
})
|
package/src/docdown.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { generateDoc } from './index.js'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { basename } from 'node:path'
|
|
4
|
+
import _ from 'lodash-es'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates Markdown documentation based on JSDoc comments.
|
|
8
|
+
* @param {object} options The options object.
|
|
9
|
+
* @param {string} options.path The input file path.
|
|
10
|
+
* @param {string} options.url The source URL.
|
|
11
|
+
* @param {string} [options.lang] The language indicator for code blocks (default: `js`).
|
|
12
|
+
* @param {boolean} [options.sort] Specify whether entries are sorted (default: `true`).
|
|
13
|
+
* @param {string} [options.style] The hash style for links ('default' or 'github').
|
|
14
|
+
* @param {string} [options.title] The documentation title (default: `${path} API documentation`).
|
|
15
|
+
* @param {string} [options.toc] The table of contents organization style ('categories' or 'properties').
|
|
16
|
+
* @returns {string} The generated Markdown code.
|
|
17
|
+
*/
|
|
18
|
+
export function docdown (options) {
|
|
19
|
+
options = _.defaults(options, {
|
|
20
|
+
lang: 'js',
|
|
21
|
+
sort: true,
|
|
22
|
+
style: 'default',
|
|
23
|
+
title: basename(options.path) + ' API documentation',
|
|
24
|
+
toc: 'properties'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (!options.path || !options.url) {
|
|
28
|
+
throw new Error('Path and URL must be specified')
|
|
29
|
+
}
|
|
30
|
+
return generateDoc(readFileSync(options.path, 'utf8'), options)
|
|
31
|
+
}
|
package/src/entry.js
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/check-tag-names,jsdoc/reject-function-type,@stylistic/multiline-ternary */
|
|
2
|
+
|
|
3
|
+
import { Alias, compareNatural, format, parse } from './index.js'
|
|
4
|
+
import _ from 'lodash-es'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gets the param type of `tag`.
|
|
8
|
+
* @private
|
|
9
|
+
* @param {object} tag The param tag to inspect.
|
|
10
|
+
* @returns {string} Returns the param type.
|
|
11
|
+
*/
|
|
12
|
+
export function getParamType (tag) {
|
|
13
|
+
let expression = tag.expression
|
|
14
|
+
let result = ''
|
|
15
|
+
const type = tag.type
|
|
16
|
+
|
|
17
|
+
switch (type) {
|
|
18
|
+
case 'AllLiteral':
|
|
19
|
+
result = '*'
|
|
20
|
+
break
|
|
21
|
+
|
|
22
|
+
case 'NameExpression':
|
|
23
|
+
result = _.toString(tag.name)
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
case 'RestType':
|
|
27
|
+
result = '...' + result
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
case 'TypeApplication':
|
|
31
|
+
expression = undefined
|
|
32
|
+
result = _(tag)
|
|
33
|
+
.chain()
|
|
34
|
+
.get('applications')
|
|
35
|
+
.map(_.flow(getParamType, _.add))
|
|
36
|
+
.sort(compareNatural)
|
|
37
|
+
.join('|')
|
|
38
|
+
.value()
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
case 'UnionType':
|
|
42
|
+
result = _(tag)
|
|
43
|
+
.chain()
|
|
44
|
+
.get('elements')
|
|
45
|
+
.map(getParamType)
|
|
46
|
+
.sort(compareNatural)
|
|
47
|
+
.join('|')
|
|
48
|
+
.value()
|
|
49
|
+
}
|
|
50
|
+
if (expression) {
|
|
51
|
+
result += getParamType(expression)
|
|
52
|
+
}
|
|
53
|
+
return type === 'UnionType'
|
|
54
|
+
? ('(' + result + ')')
|
|
55
|
+
: result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gets an `entry` tag by `tagName`.
|
|
60
|
+
* @private
|
|
61
|
+
* @param {object} entry The entry to inspect.
|
|
62
|
+
* @param {string} tagName The name of the tag.
|
|
63
|
+
* @returns {object | null} Returns the tag.
|
|
64
|
+
*/
|
|
65
|
+
function getTag (entry, tagName) {
|
|
66
|
+
const parsed = entry.parsed
|
|
67
|
+
return _.find(parsed.tags, ['title', tagName]) || null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets an `entry` tag value by `tagName`.
|
|
72
|
+
* @private
|
|
73
|
+
* @param {object} entry The entry to inspect.
|
|
74
|
+
* @param {string} tagName The name of the tag.
|
|
75
|
+
* @returns {string} Returns the tag value.
|
|
76
|
+
*/
|
|
77
|
+
function getValue (entry, tagName) {
|
|
78
|
+
const parsed = entry.parsed
|
|
79
|
+
let result = parsed.description
|
|
80
|
+
const tag = getTag(entry, tagName)
|
|
81
|
+
|
|
82
|
+
if (tagName === 'alias') {
|
|
83
|
+
result = _.get(tag, 'name')
|
|
84
|
+
|
|
85
|
+
// Doctrine can't parse alias tags containing multiple values so extract
|
|
86
|
+
// them from the error message.
|
|
87
|
+
const error = _.first(_.get(tag, 'errors'))
|
|
88
|
+
if (error) {
|
|
89
|
+
result += error.replace(/^[^']*'|'[^']*$/g, '')
|
|
90
|
+
}
|
|
91
|
+
} else if (tagName === 'type') {
|
|
92
|
+
result = _.get(tag, 'type.name')
|
|
93
|
+
} else if (tagName !== 'description') {
|
|
94
|
+
result = _.get(tag, 'name') || _.get(tag, 'description')
|
|
95
|
+
}
|
|
96
|
+
return tagName === 'example'
|
|
97
|
+
? _.toString(result)
|
|
98
|
+
: format(result)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Checks if `entry` has a tag of `tagName`.
|
|
103
|
+
* @private
|
|
104
|
+
* @param {object} entry The entry to inspect.
|
|
105
|
+
* @param {string} tagName The name of the tag.
|
|
106
|
+
* @returns {boolean} Returns `true` if the tag is found, else `false`.
|
|
107
|
+
*/
|
|
108
|
+
function hasTag (entry, tagName) {
|
|
109
|
+
return getTag(entry, tagName) !== null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Converts CR+LF line endings to LF.
|
|
114
|
+
* @private
|
|
115
|
+
* @param {string} string The string to convert.
|
|
116
|
+
* @returns {string} Returns the converted string.
|
|
117
|
+
*/
|
|
118
|
+
function normalizeEOL (string) {
|
|
119
|
+
return string.replace(/\r\n/g, '\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The Entry constructor.
|
|
124
|
+
* @constructor
|
|
125
|
+
* @param {string} entry The documentation entry to analyse.
|
|
126
|
+
* @param {string} source The source code.
|
|
127
|
+
* @param {string} [lang] The language highlighter used for code examples (default: 'js').
|
|
128
|
+
*/
|
|
129
|
+
export function Entry (entry, source, lang) {
|
|
130
|
+
entry = normalizeEOL(entry)
|
|
131
|
+
|
|
132
|
+
this.entry = entry
|
|
133
|
+
this.lang = lang == null ? 'js' : lang
|
|
134
|
+
this.parsed = parse(entry.replace(/(\*)\/\s*.+$/, '*'))
|
|
135
|
+
this.source = normalizeEOL(source)
|
|
136
|
+
this.getCall = _.memoize(this.getCall)
|
|
137
|
+
this.getCategory = _.memoize(this.getCategory)
|
|
138
|
+
this.getDesc = _.memoize(this.getDesc)
|
|
139
|
+
this.getExample = _.memoize(this.getExample)
|
|
140
|
+
this.getHash = _.memoize(this.getHash)
|
|
141
|
+
this.getLineNumber = _.memoize(this.getLineNumber)
|
|
142
|
+
this.getName = _.memoize(this.getName)
|
|
143
|
+
this.getRelated = _.memoize(this.getRelated)
|
|
144
|
+
this.getReturns = _.memoize(this.getReturns)
|
|
145
|
+
this.getSince = _.memoize(this.getSince)
|
|
146
|
+
this.getType = _.memoize(this.getType)
|
|
147
|
+
this.isAlias = _.memoize(this.isAlias)
|
|
148
|
+
this.isCtor = _.memoize(this.isCtor)
|
|
149
|
+
this.isFunction = _.memoize(this.isFunction)
|
|
150
|
+
this.isLicense = _.memoize(this.isLicense)
|
|
151
|
+
this.isPlugin = _.memoize(this.isPlugin)
|
|
152
|
+
this.isPrivate = _.memoize(this.isPrivate)
|
|
153
|
+
this.isStatic = _.memoize(this.isStatic)
|
|
154
|
+
this._aliases = this._members = this._params = undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extracts the documentation entries from source code.
|
|
159
|
+
* @static
|
|
160
|
+
* @memberOf Entry
|
|
161
|
+
* @param {string} source The source code.
|
|
162
|
+
* @returns {Array} Returns the array of entries.
|
|
163
|
+
*/
|
|
164
|
+
export function getEntries (source) {
|
|
165
|
+
return _.toString(source).match(/\/\*\*(?![-!])[\s\S]*?\*\/\s*.+/g) || []
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extracts the entry's `alias` objects.
|
|
170
|
+
* @memberOf Entry
|
|
171
|
+
* @param {number} index The index of the array value to return.
|
|
172
|
+
* @returns {Array|string} Returns the entry's `alias` objects.
|
|
173
|
+
*/
|
|
174
|
+
function getAliases (index) {
|
|
175
|
+
if (this._aliases === undefined) {
|
|
176
|
+
const owner = this
|
|
177
|
+
this._aliases = _(getValue(this, 'alias'))
|
|
178
|
+
.split(/,\s*/)
|
|
179
|
+
.compact()
|
|
180
|
+
.sort(compareNatural)
|
|
181
|
+
.map(function (value) { return new Alias(value, owner) })
|
|
182
|
+
.value()
|
|
183
|
+
}
|
|
184
|
+
const result = this._aliases
|
|
185
|
+
return index === undefined ? result : result[index]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extracts the function call from the entry.
|
|
190
|
+
* @memberOf Entry
|
|
191
|
+
* @returns {string} Returns the function call.
|
|
192
|
+
*/
|
|
193
|
+
function getCall () {
|
|
194
|
+
let result = _.trim(_.get(/\*\/\s*(?:function\s+)?([^\s(]+)\s*\(/.exec(this.entry), 1))
|
|
195
|
+
if (!result) {
|
|
196
|
+
result = _.trim(_.get(/\*\/\s*(.*?)[:=,]/.exec(this.entry), 1))
|
|
197
|
+
result = /['"]$/.test(result)
|
|
198
|
+
? _.trim(result, '"\'')
|
|
199
|
+
: result.split('.').pop().split(/^(?:const|let|var) /).pop()
|
|
200
|
+
}
|
|
201
|
+
const name = getValue(this, 'name') || result
|
|
202
|
+
if (!this.isFunction()) {
|
|
203
|
+
return name
|
|
204
|
+
}
|
|
205
|
+
const params = this.getParams()
|
|
206
|
+
result = _.castArray(result)
|
|
207
|
+
|
|
208
|
+
// Compile the function call syntax.
|
|
209
|
+
_.each(params, function (param) {
|
|
210
|
+
const paramValue = param[1]
|
|
211
|
+
const parentParam = _.get(/\w+(?=\.[\w.]+)/.exec(paramValue), 0)
|
|
212
|
+
|
|
213
|
+
const parentIndex = parentParam == null ? -1 : _.findIndex(params, function (param) {
|
|
214
|
+
return _.trim(param[1], '[]').split(/\s*=/)[0] === parentParam
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Skip params that are properties of other params (e.g. `options.leading`).
|
|
218
|
+
if (_.get(params[parentIndex], 0) !== 'Object') {
|
|
219
|
+
result.push(paramValue)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Format the function call.
|
|
224
|
+
return name + '(' + result.slice(1).join(', ') + ')'
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extracts the entry's `category` data.
|
|
229
|
+
* @memberOf Entry
|
|
230
|
+
* @returns {string} Returns the entry's `category` data.
|
|
231
|
+
*/
|
|
232
|
+
function getCategory () {
|
|
233
|
+
const result = getValue(this, 'category')
|
|
234
|
+
return result || (this.getType() === 'Function' ? 'Methods' : 'Properties')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extracts the entry's description.
|
|
239
|
+
* @memberOf Entry
|
|
240
|
+
* @returns {string} Returns the entry's description.
|
|
241
|
+
*/
|
|
242
|
+
function getDesc () {
|
|
243
|
+
const type = this.getType()
|
|
244
|
+
const result = getValue(this, 'description')
|
|
245
|
+
|
|
246
|
+
return (!result || type === 'Function' || type === 'unknown')
|
|
247
|
+
? result
|
|
248
|
+
: ('(' + _.trim(type.replace(/\|/g, ', '), '()') + '): ' + result)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Extracts the entry's `example` data.
|
|
253
|
+
* @memberOf Entry
|
|
254
|
+
* @returns {string} Returns the entry's `example` data.
|
|
255
|
+
*/
|
|
256
|
+
function getExample () {
|
|
257
|
+
const result = getValue(this, 'example')
|
|
258
|
+
return result && ('```' + this.lang + '\n' + result + '\n```')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Extracts the entry's hash value for permalinking.
|
|
263
|
+
* @memberOf Entry
|
|
264
|
+
* @param {string} [style] The hash style.
|
|
265
|
+
* @returns {string} Returns the entry's hash value (without a hash itself).
|
|
266
|
+
*/
|
|
267
|
+
function getHash (style) {
|
|
268
|
+
let result = _.toString(this.getMembers(0))
|
|
269
|
+
if (style === 'github') {
|
|
270
|
+
if (result) {
|
|
271
|
+
result += this.isPlugin() ? 'prototype' : ''
|
|
272
|
+
}
|
|
273
|
+
result += this.getCall()
|
|
274
|
+
return result
|
|
275
|
+
.replace(/[\\.=|'"(){}[\]\t ]/g, '')
|
|
276
|
+
.replace(/[#,]+/g, '-')
|
|
277
|
+
.toLowerCase()
|
|
278
|
+
}
|
|
279
|
+
if (result) {
|
|
280
|
+
result += '-' + (this.isPlugin() ? 'prototype-' : '')
|
|
281
|
+
}
|
|
282
|
+
result += this.isAlias() ? this.getOwner().getName() : this.getName()
|
|
283
|
+
return result
|
|
284
|
+
.replace(/\./g, '-')
|
|
285
|
+
.replace(/^_-/, '')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolves the entry's line number.
|
|
290
|
+
* @memberOf Entry
|
|
291
|
+
* @returns {number} Returns the entry's line number.
|
|
292
|
+
*/
|
|
293
|
+
function getLineNumber () {
|
|
294
|
+
const lines = this.source
|
|
295
|
+
.slice(0, this.source.indexOf(this.entry) + this.entry.length)
|
|
296
|
+
.match(/\n/g)
|
|
297
|
+
.slice(1)
|
|
298
|
+
|
|
299
|
+
// Offset by 2 because the first line number is before a line break and the
|
|
300
|
+
// last line doesn't include a line break.
|
|
301
|
+
return lines.length + 2
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Extracts the entry's `member` data.
|
|
306
|
+
* @memberOf Entry
|
|
307
|
+
* @param {number} [index] The index of the array value to return.
|
|
308
|
+
* @returns {Array|string} Returns the entry's `member` data.
|
|
309
|
+
*/
|
|
310
|
+
function getMembers (index) {
|
|
311
|
+
if (this._members === undefined) {
|
|
312
|
+
this._members = _(getValue(this, 'member') || getValue(this, 'memberOf'))
|
|
313
|
+
.split(/,\s*/)
|
|
314
|
+
.compact()
|
|
315
|
+
.sort(compareNatural)
|
|
316
|
+
.value()
|
|
317
|
+
}
|
|
318
|
+
const result = this._members
|
|
319
|
+
return index === undefined ? result : result[index]
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extracts the entry's `name` data.
|
|
324
|
+
* @memberOf Entry
|
|
325
|
+
* @returns {string} Returns the entry's `name` data.
|
|
326
|
+
*/
|
|
327
|
+
function getName () {
|
|
328
|
+
return hasTag(this, 'name')
|
|
329
|
+
? getValue(this, 'name')
|
|
330
|
+
: _.toString(_.first(this.getCall().split('(')))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Extracts the entry's `param` data.
|
|
335
|
+
* @memberOf Entry
|
|
336
|
+
* @param {number} [index] The index of the array value to return.
|
|
337
|
+
* @returns {Array} Returns the entry's `param` data.
|
|
338
|
+
*/
|
|
339
|
+
function getParams (index) {
|
|
340
|
+
if (this._params === undefined) {
|
|
341
|
+
this._params = _(this.parsed.tags)
|
|
342
|
+
.filter(['title', 'param'])
|
|
343
|
+
.filter('name')
|
|
344
|
+
.map(function (tag) {
|
|
345
|
+
const defaultValue = tag['default']
|
|
346
|
+
const desc = format(tag.description)
|
|
347
|
+
let name = _.toString(tag.name)
|
|
348
|
+
const type = getParamType(tag.type)
|
|
349
|
+
|
|
350
|
+
if (defaultValue != null) {
|
|
351
|
+
name += '=' + defaultValue
|
|
352
|
+
}
|
|
353
|
+
if (_.get(tag, 'type.type') === 'OptionalType') {
|
|
354
|
+
name = '[' + name + ']'
|
|
355
|
+
}
|
|
356
|
+
return [type, name, desc]
|
|
357
|
+
})
|
|
358
|
+
.value()
|
|
359
|
+
}
|
|
360
|
+
const result = this._params
|
|
361
|
+
return index === undefined ? result : result[index]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Extracts the entry's `see` data.
|
|
366
|
+
* @memberOf Entry
|
|
367
|
+
* @returns {Array} Returns the entry's `see` data as links.
|
|
368
|
+
*/
|
|
369
|
+
function getRelated () {
|
|
370
|
+
const relatedValues = getValue(this, 'see')
|
|
371
|
+
if (relatedValues && relatedValues.trim().length > 0) {
|
|
372
|
+
const relatedItems = relatedValues.split(',').map((relatedItem) => relatedItem.trim())
|
|
373
|
+
return relatedItems.map((relatedItem) => '[' + relatedItem + '](#' + relatedItem + ')')
|
|
374
|
+
} else {
|
|
375
|
+
return []
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Extracts the entry's `returns` data.
|
|
381
|
+
* @memberOf Entry
|
|
382
|
+
* @returns {Array} Returns the entry's `returns` data.
|
|
383
|
+
*/
|
|
384
|
+
function getReturns () {
|
|
385
|
+
const tag = getTag(this, 'returns')
|
|
386
|
+
const desc = _.toString(_.get(tag, 'description'))
|
|
387
|
+
const type = _.toString(_.get(tag, 'type.name')) || '*'
|
|
388
|
+
|
|
389
|
+
return tag ? [type, desc] : []
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Extracts the entry's `since` data.
|
|
394
|
+
* @memberOf Entry
|
|
395
|
+
* @returns {string} Returns the entry's `since` data.
|
|
396
|
+
*/
|
|
397
|
+
function getSince () {
|
|
398
|
+
return getValue(this, 'since')
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Extracts the entry's `type` data.
|
|
403
|
+
* @memberOf Entry
|
|
404
|
+
* @returns {string} Returns the entry's `type` data.
|
|
405
|
+
*/
|
|
406
|
+
function getType () {
|
|
407
|
+
const result = getValue(this, 'type')
|
|
408
|
+
if (!result) {
|
|
409
|
+
return this.isFunction() ? 'Function' : 'unknown'
|
|
410
|
+
}
|
|
411
|
+
return /^(?:array|function|object|regexp)$/.test(result)
|
|
412
|
+
? _.capitalize(result)
|
|
413
|
+
: result
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Checks if the entry is an alias.
|
|
418
|
+
* @memberOf Entry
|
|
419
|
+
* @type {Function}
|
|
420
|
+
* @returns {boolean} Returns `false`.
|
|
421
|
+
*/
|
|
422
|
+
const isAlias = _.constant(false)
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Checks if the entry is a constructor.
|
|
426
|
+
* @memberOf Entry
|
|
427
|
+
* @returns {boolean} Returns `true` if a constructor, else `false`.
|
|
428
|
+
*/
|
|
429
|
+
function isCtor () {
|
|
430
|
+
return hasTag(this, 'constructor')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Checks if the entry is a function reference.
|
|
435
|
+
* @memberOf Entry
|
|
436
|
+
* @returns {boolean} Returns `true` if the entry is a function reference, else `false`.
|
|
437
|
+
*/
|
|
438
|
+
function isFunction () {
|
|
439
|
+
return !!(
|
|
440
|
+
this.isCtor() ||
|
|
441
|
+
_.size(this.getParams()) ||
|
|
442
|
+
_.size(this.getReturns()) ||
|
|
443
|
+
hasTag(this, 'function') ||
|
|
444
|
+
/\*\/\s*(?:function\s+)?[^\s(]+\s*\(/.test(this.entry)
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Checks if the entry is a license.
|
|
450
|
+
* @memberOf Entry
|
|
451
|
+
* @returns {boolean} Returns `true` if a license, else `false`.
|
|
452
|
+
*/
|
|
453
|
+
function isLicense () {
|
|
454
|
+
return hasTag(this, 'license')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Checks if the entry *is* assigned to a prototype.
|
|
459
|
+
* @memberOf Entry
|
|
460
|
+
* @returns {boolean} Returns `true` if assigned to a prototype, else `false`.
|
|
461
|
+
*/
|
|
462
|
+
function isPlugin () {
|
|
463
|
+
return (
|
|
464
|
+
!this.isCtor() &&
|
|
465
|
+
!this.isPrivate() &&
|
|
466
|
+
!this.isStatic()
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Checks if the entry is private.
|
|
472
|
+
* @memberOf Entry
|
|
473
|
+
* @returns {boolean} Returns `true` if private, else `false`.
|
|
474
|
+
*/
|
|
475
|
+
function isPrivate () {
|
|
476
|
+
return (
|
|
477
|
+
this.isLicense() ||
|
|
478
|
+
hasTag(this, 'private') ||
|
|
479
|
+
_.isEmpty(this.parsed.tags)
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Checks if the entry is *not* assigned to a prototype.
|
|
485
|
+
* @memberOf Entry
|
|
486
|
+
* @returns {boolean} Returns `true` if not assigned to a prototype, else `false`.
|
|
487
|
+
*/
|
|
488
|
+
function isStatic () {
|
|
489
|
+
const isPublic = !this.isPrivate()
|
|
490
|
+
let result = isPublic && hasTag(this, 'static')
|
|
491
|
+
|
|
492
|
+
// Get the result in cases where it isn't explicitly stated.
|
|
493
|
+
if (isPublic && !result) {
|
|
494
|
+
const parent = _.last(_.toString(this.getMembers(0)).split(/[#.]/))
|
|
495
|
+
if (!parent) {
|
|
496
|
+
return true
|
|
497
|
+
}
|
|
498
|
+
const source = this.source
|
|
499
|
+
_.each(getEntries(source), function (entry) {
|
|
500
|
+
entry = new Entry(entry, source)
|
|
501
|
+
if (entry.getName() === parent) {
|
|
502
|
+
result = !entry.isCtor()
|
|
503
|
+
return false
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
return result
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_.assign(Entry.prototype, {
|
|
511
|
+
getAliases,
|
|
512
|
+
getCall,
|
|
513
|
+
getCategory,
|
|
514
|
+
getDesc,
|
|
515
|
+
getExample,
|
|
516
|
+
getHash,
|
|
517
|
+
getLineNumber,
|
|
518
|
+
getMembers,
|
|
519
|
+
getName,
|
|
520
|
+
getParams,
|
|
521
|
+
getRelated,
|
|
522
|
+
getReturns,
|
|
523
|
+
getSince,
|
|
524
|
+
getType,
|
|
525
|
+
isAlias,
|
|
526
|
+
isCtor,
|
|
527
|
+
isFunction,
|
|
528
|
+
isLicense,
|
|
529
|
+
isPlugin,
|
|
530
|
+
isPrivate,
|
|
531
|
+
isStatic
|
|
532
|
+
})
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
|
|
3
|
+
import { Entry, compareNatural, getEntries, format } from './index.js'
|
|
4
|
+
import _ from 'lodash-es'
|
|
5
|
+
|
|
6
|
+
const push = Array.prototype.push
|
|
7
|
+
const specialCategories = ['Methods', 'Properties']
|
|
8
|
+
const token = '@@token@@'
|
|
9
|
+
|
|
10
|
+
const reCode = /`.*?`/g
|
|
11
|
+
const reToken = /@@token@@/g
|
|
12
|
+
const reSpecialCategory = RegExp('^(?:' + specialCategories.join('|') + ')$')
|
|
13
|
+
|
|
14
|
+
const htmlEscapes = {
|
|
15
|
+
'*': '*',
|
|
16
|
+
'[': '[',
|
|
17
|
+
']': ']'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Escape special Markdown characters in a string.
|
|
22
|
+
* @private
|
|
23
|
+
* @param {string} string The string to escape.
|
|
24
|
+
* @returns {string} Returns the escaped string.
|
|
25
|
+
*/
|
|
26
|
+
function escape (string) {
|
|
27
|
+
const snippets = []
|
|
28
|
+
|
|
29
|
+
// Replace all code snippets with a token.
|
|
30
|
+
string = string.replace(reCode, function (match) {
|
|
31
|
+
snippets.push(match)
|
|
32
|
+
return token
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
_.forOwn(htmlEscapes, function (replacement, chr) {
|
|
36
|
+
string = string.replace(RegExp('(\\\\?)\\' + chr, 'g'), function (match, backslash) {
|
|
37
|
+
return backslash ? match : replacement
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Replace all tokens with code snippets.
|
|
42
|
+
return string.replace(reToken, function (match) {
|
|
43
|
+
return snippets.shift()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the seperator (`.` or `.prototype.`)
|
|
49
|
+
* @private
|
|
50
|
+
* @param {Entry} entry object to get selector for.
|
|
51
|
+
* @returns {string} Returns the member seperator.
|
|
52
|
+
*/
|
|
53
|
+
function getSeparator (entry) {
|
|
54
|
+
return entry.isPlugin() ? '.prototype.' : '.'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Modify a string by replacing named tokens with matching associated object values.
|
|
59
|
+
* @private
|
|
60
|
+
* @param {string} string The string to modify.
|
|
61
|
+
* @param {object} data The template data object.
|
|
62
|
+
* @returns {string} Returns the modified string.
|
|
63
|
+
*/
|
|
64
|
+
function interpolate (string, data) {
|
|
65
|
+
return format(_.template(string)(data))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Make an anchor link.
|
|
70
|
+
* @private
|
|
71
|
+
* @param {string} href The anchor href.
|
|
72
|
+
* @param {string} text The anchor text.
|
|
73
|
+
* @returns {string} Returns the anchor HTML.
|
|
74
|
+
*/
|
|
75
|
+
function makeAnchor (href, text) {
|
|
76
|
+
return '<a href="' + href + '">' + _.toString(text) + '</a>'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ---------------------------------------------------------------------------- */
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generates the documentation from JS source.
|
|
83
|
+
* @param {string} source source code to generate the documentation for.
|
|
84
|
+
* @param {object} options options object.
|
|
85
|
+
* @returns {string} Returns the documentation markdown.
|
|
86
|
+
*/
|
|
87
|
+
export function generateDoc (source, options) {
|
|
88
|
+
const api = []
|
|
89
|
+
const noTOC = options.toc === 'none'
|
|
90
|
+
const byCategories = options.toc === 'categories'
|
|
91
|
+
const entries = getEntries(source)
|
|
92
|
+
const organized = Object.create(null)
|
|
93
|
+
const sortEntries = options.sort
|
|
94
|
+
const style = options.style
|
|
95
|
+
const url = options.url
|
|
96
|
+
|
|
97
|
+
// Add entries and aliases to the API list.
|
|
98
|
+
_.each(entries, function (entry) {
|
|
99
|
+
entry = new Entry(entry, source)
|
|
100
|
+
api.push(entry)
|
|
101
|
+
|
|
102
|
+
const aliases = entry.getAliases()
|
|
103
|
+
if (!_.isEmpty(aliases)) {
|
|
104
|
+
push.apply(api, aliases)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Build the list of categories for the TOC and generate content for each entry.
|
|
109
|
+
_.each(api, function (entry) {
|
|
110
|
+
// Exit early if the entry is private or has no name.
|
|
111
|
+
const name = entry.getName()
|
|
112
|
+
if (!name || entry.isPrivate()) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
let tocGroup
|
|
116
|
+
const member = entry.getMembers(0) || ''
|
|
117
|
+
const separator = member ? getSeparator(entry) : ''
|
|
118
|
+
|
|
119
|
+
// Add the entry to the TOC.
|
|
120
|
+
if (byCategories) {
|
|
121
|
+
const category = entry.getCategory()
|
|
122
|
+
tocGroup = organized[category] || (organized[category] = [])
|
|
123
|
+
} else {
|
|
124
|
+
let memberGroup
|
|
125
|
+
if (!member ||
|
|
126
|
+
entry.isCtor() ||
|
|
127
|
+
(entry.getType() === 'Object' &&
|
|
128
|
+
!/[=:]\s*(?:null|undefined)\s*[,;]?$/gi.test(entry.entry))
|
|
129
|
+
) {
|
|
130
|
+
memberGroup = (member ? member + getSeparator(entry) : '') + name
|
|
131
|
+
} else if (entry.isStatic()) {
|
|
132
|
+
memberGroup = member
|
|
133
|
+
} else if (!entry.isCtor()) {
|
|
134
|
+
memberGroup = member + getSeparator(entry).slice(0, -1)
|
|
135
|
+
}
|
|
136
|
+
tocGroup = organized[memberGroup] || (organized[memberGroup] = [])
|
|
137
|
+
}
|
|
138
|
+
tocGroup.push(entry)
|
|
139
|
+
|
|
140
|
+
// Skip aliases.
|
|
141
|
+
if (entry.isAlias()) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
// Start markdown for the entry.
|
|
145
|
+
const entryMarkdown = ['\n<!-- div -->\n']
|
|
146
|
+
|
|
147
|
+
const entryData = {
|
|
148
|
+
call: entry.getCall(),
|
|
149
|
+
category: entry.getCategory(),
|
|
150
|
+
entryHref: '#${hash}',
|
|
151
|
+
entryLink: _.get(options, 'entryLink', style === 'github' ? '' : '<a href="${entryHref}">#</a> '),
|
|
152
|
+
hash: entry.getHash(style),
|
|
153
|
+
member,
|
|
154
|
+
name,
|
|
155
|
+
separator,
|
|
156
|
+
sourceHref: url + '#L' + entry.getLineNumber(),
|
|
157
|
+
sourceLink: _.get(options, 'sourceLink', '[Ⓢ](${sourceHref} "View in source")'),
|
|
158
|
+
tocHref: '1',
|
|
159
|
+
tocLink: _.get(options, 'tocLink', '[Ⓣ][${tocHref}]')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_.each([
|
|
163
|
+
'entryHref', 'sourceHref', 'tocHref',
|
|
164
|
+
'entryLink', 'sourceLink', 'tocLink'
|
|
165
|
+
], function (option) {
|
|
166
|
+
entryData[option] = interpolate(entryData[option], entryData)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Add the heading.
|
|
170
|
+
entryMarkdown.push(interpolate(
|
|
171
|
+
'<h3 id="${hash}">${entryLink}<code>${member}${separator}${call}</code></h3>\n' +
|
|
172
|
+
interpolate(
|
|
173
|
+
_([
|
|
174
|
+
'${sourceLink}',
|
|
175
|
+
_.get(options, 'sublinks', []),
|
|
176
|
+
'${tocLink}'
|
|
177
|
+
])
|
|
178
|
+
.flatten()
|
|
179
|
+
.compact()
|
|
180
|
+
.join(' '),
|
|
181
|
+
entryData
|
|
182
|
+
)
|
|
183
|
+
.replace(/ {2,}/g, ' '),
|
|
184
|
+
entryData
|
|
185
|
+
))
|
|
186
|
+
|
|
187
|
+
// Add the description.
|
|
188
|
+
entryMarkdown.push('\n' + entry.getDesc() + '\n')
|
|
189
|
+
|
|
190
|
+
// Add optional related.
|
|
191
|
+
const relatedItems = entry.getRelated()
|
|
192
|
+
if (!_.isEmpty(relatedItems)) {
|
|
193
|
+
entryMarkdown.push(
|
|
194
|
+
'#### Related',
|
|
195
|
+
relatedItems.join(', '),
|
|
196
|
+
''
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
// Add optional since version.
|
|
200
|
+
const since = entry.getSince()
|
|
201
|
+
if (!_.isEmpty(since)) {
|
|
202
|
+
entryMarkdown.push(
|
|
203
|
+
'#### Since',
|
|
204
|
+
since,
|
|
205
|
+
''
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
// Add optional aliases.
|
|
209
|
+
const aliases = entry.getAliases()
|
|
210
|
+
if (!_.isEmpty(aliases)) {
|
|
211
|
+
entryMarkdown.push(
|
|
212
|
+
'#### Aliases',
|
|
213
|
+
'*' +
|
|
214
|
+
_.map(aliases, function (alias) {
|
|
215
|
+
return `${member}${separator}${alias.getName()}`
|
|
216
|
+
}).join(', ') +
|
|
217
|
+
'*',
|
|
218
|
+
''
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
// Add optional function parameters.
|
|
222
|
+
const params = entry.getParams()
|
|
223
|
+
if (!_.isEmpty(params)) {
|
|
224
|
+
entryMarkdown.push('#### Arguments')
|
|
225
|
+
_.each(params, function (param, index) {
|
|
226
|
+
let paramType = param[0]
|
|
227
|
+
if (_.startsWith(paramType, '(')) {
|
|
228
|
+
paramType = _.trim(paramType, '()')
|
|
229
|
+
}
|
|
230
|
+
const entryValues = {
|
|
231
|
+
desc: escape(param[2]),
|
|
232
|
+
name: param[1],
|
|
233
|
+
num: index + 1,
|
|
234
|
+
type: escape(paramType)
|
|
235
|
+
}
|
|
236
|
+
entryMarkdown.push(`${entryValues.num}. ${entryValues.name} (${entryValues.type}): ${entryValues.desc}`)
|
|
237
|
+
})
|
|
238
|
+
entryMarkdown.push('')
|
|
239
|
+
}
|
|
240
|
+
// Add optional functions returns.
|
|
241
|
+
const returns = entry.getReturns()
|
|
242
|
+
if (!_.isEmpty(returns)) {
|
|
243
|
+
let returnType = returns[0]
|
|
244
|
+
if (_.startsWith(returnType, '(')) {
|
|
245
|
+
returnType = _.trim(returnType, '()')
|
|
246
|
+
}
|
|
247
|
+
const entryValues = {
|
|
248
|
+
desc: escape(returns[1]),
|
|
249
|
+
type: escape(returnType)
|
|
250
|
+
}
|
|
251
|
+
entryMarkdown.push(
|
|
252
|
+
'#### Returns',
|
|
253
|
+
`(${entryValues.type}): ${entryValues.desc}`,
|
|
254
|
+
''
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
// Add optional function example.
|
|
258
|
+
const example = entry.getExample()
|
|
259
|
+
if (example) {
|
|
260
|
+
entryMarkdown.push('#### Example', example)
|
|
261
|
+
}
|
|
262
|
+
// End markdown for the entry.
|
|
263
|
+
entryMarkdown.push('---\n\n<!-- /div -->')
|
|
264
|
+
|
|
265
|
+
entry.markdown = entryMarkdown.join('\n')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Add TOC headers.
|
|
269
|
+
const tocGroups = _.keys(organized)
|
|
270
|
+
if (byCategories) {
|
|
271
|
+
// Remove special categories before sorting.
|
|
272
|
+
const catogoriesUsed = _.intersection(tocGroups, specialCategories)
|
|
273
|
+
_.pullAll(tocGroups, catogoriesUsed)
|
|
274
|
+
|
|
275
|
+
// Sort categories and add special categories back.
|
|
276
|
+
if (sortEntries) {
|
|
277
|
+
tocGroups.sort(compareNatural)
|
|
278
|
+
}
|
|
279
|
+
push.apply(tocGroups, catogoriesUsed)
|
|
280
|
+
} else {
|
|
281
|
+
tocGroups.sort(compareNatural)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let tocMarkdown = []
|
|
285
|
+
if (!noTOC) {
|
|
286
|
+
// Start markdown for TOC categories.
|
|
287
|
+
tocMarkdown = ['<!-- div class="toc-container" -->\n']
|
|
288
|
+
_.each(tocGroups, function (group) {
|
|
289
|
+
tocMarkdown.push(
|
|
290
|
+
'<!-- div -->\n',
|
|
291
|
+
'## `' + group + '`'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if (sortEntries && organized[group]) {
|
|
295
|
+
// Sort the TOC groups.
|
|
296
|
+
organized[group].sort(function (value, other) {
|
|
297
|
+
const valMember = value.getMembers(0)
|
|
298
|
+
const othMember = other.getMembers(0)
|
|
299
|
+
|
|
300
|
+
return compareNatural(
|
|
301
|
+
(valMember ? (valMember + getSeparator(value)) : '') + value.getName(),
|
|
302
|
+
(othMember ? (othMember + getSeparator(other)) : '') + other.getName()
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
// Add TOC entries for each category.
|
|
307
|
+
_.each(organized[group], function (entry) {
|
|
308
|
+
const member = entry.getMembers(0) || ''
|
|
309
|
+
const name = entry.getName()
|
|
310
|
+
const sep = getSeparator(entry)
|
|
311
|
+
const title = escape((member ? (member + sep) : '') + name)
|
|
312
|
+
|
|
313
|
+
if (entry.isAlias()) {
|
|
314
|
+
// An alias has a more complex html structure.
|
|
315
|
+
const owner = entry.getOwner()
|
|
316
|
+
tocMarkdown.push(
|
|
317
|
+
'* <a href="#' + owner.getHash(style) + '" class="alias">`' +
|
|
318
|
+
title + '` -> `' + owner.getName() + '`' +
|
|
319
|
+
'</a>'
|
|
320
|
+
)
|
|
321
|
+
} else {
|
|
322
|
+
// Add a simple TOC entry.
|
|
323
|
+
tocMarkdown.push(
|
|
324
|
+
'* ' +
|
|
325
|
+
makeAnchor(
|
|
326
|
+
'#' + entry.getHash(style),
|
|
327
|
+
'`' + title + '`'
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
tocMarkdown.push('\n<!-- /div -->\n')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// End markdown for the TOC.
|
|
336
|
+
tocMarkdown.push('<!-- /div -->\n')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const docMarkdown = ['# ' + options.title + '\n']
|
|
340
|
+
push.apply(docMarkdown, tocMarkdown)
|
|
341
|
+
docMarkdown.push('<!-- div class="doc-container" -->\n')
|
|
342
|
+
|
|
343
|
+
_.each(tocGroups, function (group) {
|
|
344
|
+
docMarkdown.push('<!-- div -->\n')
|
|
345
|
+
let groupName = ''
|
|
346
|
+
if (byCategories && !reSpecialCategory.test(group)) {
|
|
347
|
+
groupName = '“' + group + '” Methods'
|
|
348
|
+
}
|
|
349
|
+
if (!noTOC) {
|
|
350
|
+
docMarkdown.push('## `' + (groupName || group) + '`')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_.each(organized[group], function (entry) {
|
|
354
|
+
if (entry.markdown) {
|
|
355
|
+
docMarkdown.push(entry.markdown)
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
docMarkdown.push('\n<!-- /div -->\n')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
docMarkdown.push('<!-- /div -->\n')
|
|
362
|
+
|
|
363
|
+
// Add link back to the top of the TOC.
|
|
364
|
+
const tocHref = _.get(options, 'tocHref', '#' + _.get(tocGroups, 0, '').toLowerCase())
|
|
365
|
+
if (tocHref) {
|
|
366
|
+
docMarkdown.push(' [1]: ' + tocHref + ' "Jump back to the TOC."\n')
|
|
367
|
+
}
|
|
368
|
+
return docMarkdown.join('\n')
|
|
369
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Alias } from './alias.js'
|
|
2
|
+
export { docdown } from './docdown.js'
|
|
3
|
+
export { Entry, getEntries } from './entry.js'
|
|
4
|
+
export { generateDoc } from './generator.js'
|
|
5
|
+
export { createDocs } from './jsdown.js'
|
|
6
|
+
export { compareNatural, format, parse } from './util.js'
|
package/src/jsdown.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { docdown } from './index.js'
|
|
2
|
+
import { fileExists, match } from '@vanillaes/esmtk'
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { basename, dirname, join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create markdown documents for the JS + JSDoc sources
|
|
8
|
+
* @param {string} files the pattern(s) of file(s) to include
|
|
9
|
+
* @param {object} options 'jsdown' options
|
|
10
|
+
*/
|
|
11
|
+
export async function createDocs (files, options = {}) {
|
|
12
|
+
const sources = await match(files)
|
|
13
|
+
sources.forEach(file => createDoc(file))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a markdown document for a JS + JSDoc source
|
|
18
|
+
* @private
|
|
19
|
+
* @param {string} path path to JS source
|
|
20
|
+
*/
|
|
21
|
+
async function createDoc (path) {
|
|
22
|
+
// extract names
|
|
23
|
+
const dir = dirname(path)
|
|
24
|
+
const file = basename(path)
|
|
25
|
+
const name = basename(path, '.js')
|
|
26
|
+
const mdName = `${name}.md`
|
|
27
|
+
|
|
28
|
+
const srcPath = join(process.cwd(), 'src') // TODO: make this configurable?
|
|
29
|
+
const docPath = join(process.cwd(), 'docs') // TODO: make this configurable?
|
|
30
|
+
|
|
31
|
+
const exists = await !fileExists(docPath)
|
|
32
|
+
if (!exists) {
|
|
33
|
+
await mkdir(docPath, { recursive: true })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// build the docdown config
|
|
37
|
+
const config = {
|
|
38
|
+
title: `${dir}.${name}`,
|
|
39
|
+
toc: 'none',
|
|
40
|
+
tocHref: '',
|
|
41
|
+
tocLink: '',
|
|
42
|
+
path: join(srcPath, file),
|
|
43
|
+
sourceLink: '',
|
|
44
|
+
style: 'github',
|
|
45
|
+
url: 'https://github.com/vanillaes/absurdum/doc/README.md' // TODO: look up project name
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// create the markdown file
|
|
49
|
+
const markdown = docdown(config)
|
|
50
|
+
await writeFile(join(docPath, mdName), markdown, { flag: 'w+' }, (err) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
throw Error(err)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
package/src/util.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/check-tag-names,jsdoc/reject-any-type */
|
|
2
|
+
|
|
3
|
+
import doctrine from 'doctrine'
|
|
4
|
+
import _ from 'lodash-es'
|
|
5
|
+
|
|
6
|
+
const reCode = /`.*?`/g
|
|
7
|
+
const reToken = /@@token@@/g
|
|
8
|
+
const split = String.prototype.split
|
|
9
|
+
const token = '@@token@@'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `Array#sort` comparator to produce a
|
|
13
|
+
* [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
|
|
14
|
+
* @memberOf util
|
|
15
|
+
* @param {*} value The value to compare.
|
|
16
|
+
* @param {*} other The other value to compare.
|
|
17
|
+
* @returns {number} Returns the sort order indicator for `value`.
|
|
18
|
+
*/
|
|
19
|
+
export function compareNatural (value, other) {
|
|
20
|
+
let index = -1
|
|
21
|
+
const valParts = split.call(value, '.')
|
|
22
|
+
const valLength = valParts.length
|
|
23
|
+
const othParts = split.call(other, '.')
|
|
24
|
+
const othLength = othParts.length
|
|
25
|
+
const length = Math.min(valLength, othLength)
|
|
26
|
+
|
|
27
|
+
while (++index < length) {
|
|
28
|
+
const valPart = valParts[index]
|
|
29
|
+
const othPart = othParts[index]
|
|
30
|
+
|
|
31
|
+
if (valPart > othPart && othPart !== 'prototype') {
|
|
32
|
+
return 1
|
|
33
|
+
} else if (valPart < othPart && valPart !== 'prototype') {
|
|
34
|
+
return -1
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return valLength > othLength ? 1 : (valLength < othLength ? -1 : 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Performs common string formatting operations.
|
|
42
|
+
* @memberOf util
|
|
43
|
+
* @param {string} string The string to format.
|
|
44
|
+
* @returns {string} Returns the formatted string.
|
|
45
|
+
*/
|
|
46
|
+
export function format (string) {
|
|
47
|
+
string = _.toString(string)
|
|
48
|
+
|
|
49
|
+
// Replace all code snippets with a token.
|
|
50
|
+
const snippets = []
|
|
51
|
+
string = string.replace(reCode, function (match) {
|
|
52
|
+
snippets.push(match)
|
|
53
|
+
return token
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return string
|
|
57
|
+
// Add line breaks.
|
|
58
|
+
.replace(/:\n(?=[\t ]*\S)/g, ':<br>\n')
|
|
59
|
+
.replace(/\n( *)[-*](?=[\t ]+\S)/g, '\n<br>\n$1*')
|
|
60
|
+
.replace(/^[\t ]*\n/gm, '<br>\n<br>\n')
|
|
61
|
+
// Normalize whitespace.
|
|
62
|
+
.replace(/\n +/g, ' ')
|
|
63
|
+
// Italicize parentheses.
|
|
64
|
+
.replace(/(^|\s)(\(.+\))/g, '$1*$2*')
|
|
65
|
+
// Mark numbers as inline code.
|
|
66
|
+
.replace(/[\t ](-?\d+(?:.\d+)?)(?!\.[^\n])/g, ' `$1`')
|
|
67
|
+
// Replace all tokens with code snippets.
|
|
68
|
+
.replace(reToken, function (match) {
|
|
69
|
+
return snippets.shift()
|
|
70
|
+
})
|
|
71
|
+
.trim()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parses the JSDoc `comment` into an object.
|
|
76
|
+
* @memberOf util
|
|
77
|
+
* @param {string} comment The comment to parse.
|
|
78
|
+
* @returns {object} Returns the parsed object.
|
|
79
|
+
*/
|
|
80
|
+
export const parse = _.partial(doctrine.parse, _, {
|
|
81
|
+
lineNumbers: true,
|
|
82
|
+
recoverable: true,
|
|
83
|
+
sloppy: true,
|
|
84
|
+
unwrap: true
|
|
85
|
+
})
|