@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 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
+ })
@@ -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
+ '*': '&#42;',
16
+ '[': '&#91;',
17
+ ']': '&#93;'
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>&nbsp;'),
152
+ hash: entry.getHash(style),
153
+ member,
154
+ name,
155
+ separator,
156
+ sourceHref: url + '#L' + entry.getLineNumber(),
157
+ sourceLink: _.get(options, 'sourceLink', '[&#x24C8;](${sourceHref} "View in source")'),
158
+ tocHref: '1',
159
+ tocLink: _.get(options, 'tocLink', '[&#x24C9;][${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
+ })