Comparing version 1.1.0 to 1.2.0
{ | ||
"name": "atomdoc", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "An atomdoc parser", | ||
@@ -5,0 +5,0 @@ "main": "./src/atomdoc.js", |
@@ -18,3 +18,3 @@ # AtomDoc parser | ||
It has only one method, `parse`, which takes no options. | ||
It has only one method, `parse`: | ||
@@ -29,2 +29,5 @@ ```coffee | ||
doc = AtomDoc.parse(docString) | ||
# Alternatively, you can avoid parsing "Returns" statements in documentation (useful for class-level documentation): | ||
doc = AtomDoc.parse(docString, {parseReturns: false}) | ||
``` | ||
@@ -31,0 +34,0 @@ |
@@ -22,333 +22,373 @@ const marked = require('marked') | ||
// * `docString` a string from the documented object to be parsed | ||
// * `options` an optional {Object} with the following optional keys: | ||
// * `parseReturns`. A {Boolean} describing whether "Returns" statements | ||
// should be parsed or not. Defaults to true. | ||
// | ||
// Returns a {Doc} object | ||
const parse = function (docString) { | ||
const lexer = new marked.Lexer() | ||
const tokens = lexer.lex(docString) | ||
const firstToken = tokens[0] | ||
const parse = function (docString, {parseReturns} = {}) { | ||
if (parseReturns == null) { | ||
parseReturns = true | ||
} | ||
if (!firstToken || (firstToken.type !== 'paragraph')) { | ||
throw new Error('Doc string must start with a paragraph!') | ||
return new Parser(parseReturns).parse(docString) | ||
} | ||
class Parser { | ||
constructor (parseReturns) { | ||
this.parseReturns = parseReturns | ||
this.stopOnSectionBoundaries = this.stopOnSectionBoundaries.bind(this) | ||
} | ||
const doc = new Doc(docString) | ||
parse (docString) { | ||
const lexer = new marked.Lexer() | ||
const tokens = lexer.lex(docString) | ||
const firstToken = tokens[0] | ||
Object.assign(doc, parseSummaryAndDescription(tokens)) | ||
while (tokens.length) { | ||
let args, events, examples, returnValues, titledArgs | ||
if ((titledArgs = parseTitledArgumentsSection(tokens))) { | ||
if (doc.titledArguments == null) doc.titledArguments = [] | ||
doc.titledArguments.push(titledArgs) | ||
} else if ((args = parseArgumentsSection(tokens))) { | ||
doc.arguments = args | ||
} else if ((events = parseEventsSection(tokens))) { | ||
doc.events = events | ||
} else if ((examples = parseExamplesSection(tokens))) { | ||
doc.examples = examples | ||
} else if ((returnValues = parseReturnValues(tokens, true))) { | ||
doc.setReturnValues(returnValues) | ||
} else { | ||
// These tokens are basically in no-mans land. We'll add them to the | ||
// description so they dont get lost. | ||
const extraDescription = generateDescription(tokens, stopOnSectionBoundaries) | ||
doc.description += `\n\n${extraDescription}` | ||
if (!firstToken || (firstToken.type !== 'paragraph')) { | ||
throw new Error('Doc string must start with a paragraph!') | ||
} | ||
} | ||
return doc | ||
} | ||
const doc = new Doc(docString) | ||
const parseSummaryAndDescription = function (tokens, tokenCallback) { | ||
if (tokenCallback == null) { tokenCallback = stopOnSectionBoundaries } | ||
let summary = '' | ||
let description = '' | ||
let visibility = 'Private' | ||
Object.assign(doc, this.parseSummaryAndDescription(tokens)) | ||
let rawVisibility = null | ||
let rawSummary = tokens[0].text | ||
if (rawSummary) { | ||
const visibilityMatch = VisibilityRegex.exec(rawSummary) | ||
if (visibilityMatch) { | ||
visibility = visibilityMatch[1] | ||
rawVisibility = visibilityMatch[0] | ||
if (rawVisibility) rawSummary = rawSummary.replace(rawVisibility, '') | ||
while (tokens.length) { | ||
let args, events, examples, returnValues, titledArgs | ||
if ((titledArgs = this.parseTitledArgumentsSection(tokens))) { | ||
if (doc.titledArguments == null) doc.titledArguments = [] | ||
doc.titledArguments.push(titledArgs) | ||
} else if ((args = this.parseArgumentsSection(tokens))) { | ||
doc.arguments = args | ||
} else if ((events = this.parseEventsSection(tokens))) { | ||
doc.events = events | ||
} else if ((examples = this.parseExamplesSection(tokens))) { | ||
doc.examples = examples | ||
} else if (this.parseReturns && (returnValues = this.parseReturnValues(tokens, true))) { | ||
doc.setReturnValues(returnValues) | ||
} else { | ||
// These tokens are basically in no-mans land. We'll add them to the | ||
// description so they dont get lost. | ||
const extraDescription = generateDescription(tokens, this.stopOnSectionBoundaries) | ||
doc.description += `\n\n${extraDescription}` | ||
} | ||
} | ||
} | ||
if (isReturnValue(rawSummary)) { | ||
const returnValues = parseReturnValues(tokens, false) | ||
return {summary, description, visibility, returnValues} | ||
} else { | ||
summary = rawSummary | ||
description = generateDescription(tokens, tokenCallback) | ||
if (rawVisibility) description = description.replace(rawVisibility, '') | ||
return {description, summary, visibility} | ||
return doc | ||
} | ||
} | ||
const parseArgumentsSection = function (tokens) { | ||
const firstToken = tokens[0] | ||
if (firstToken && firstToken.type === 'heading') { | ||
if (firstToken.text !== 'Arguments' || firstToken.depth !== SpecialHeadingDepth) return | ||
} else if (firstToken && firstToken.type === 'list_start') { | ||
if (!isAtArgumentList(tokens)) { return } | ||
} else { | ||
return | ||
} | ||
parseSummaryAndDescription (tokens, tokenCallback) { | ||
if (tokenCallback == null) { tokenCallback = this.stopOnSectionBoundaries } | ||
let summary = '' | ||
let description = '' | ||
let visibility = 'Private' | ||
let args = null | ||
let rawVisibility = null | ||
let rawSummary = tokens[0].text | ||
if (rawSummary) { | ||
const visibilityMatch = VisibilityRegex.exec(rawSummary) | ||
if (visibilityMatch) { | ||
visibility = visibilityMatch[1] | ||
rawVisibility = visibilityMatch[0] | ||
if (rawVisibility) rawSummary = rawSummary.replace(rawVisibility, '') | ||
} | ||
} | ||
if (firstToken.type === 'list_start') { | ||
args = parseArgumentList(tokens) | ||
} else { | ||
tokens.shift() // consume the header | ||
// consume any BS before the args list | ||
generateDescription(tokens, stopOnSectionBoundaries) | ||
args = parseArgumentList(tokens) | ||
if (isReturnValue(rawSummary)) { | ||
const returnValues = this.parseReturnValues(tokens, false) | ||
return {summary, description, visibility, returnValues} | ||
} else { | ||
summary = rawSummary | ||
description = generateDescription(tokens, tokenCallback) | ||
if (rawVisibility) description = description.replace(rawVisibility, '') | ||
return {description, summary, visibility} | ||
} | ||
} | ||
return args | ||
} | ||
parseArgumentsSection (tokens) { | ||
const firstToken = tokens[0] | ||
if (firstToken && firstToken.type === 'heading') { | ||
if (firstToken.text !== 'Arguments' || firstToken.depth !== SpecialHeadingDepth) return | ||
} else if (firstToken && firstToken.type === 'list_start') { | ||
if (!isAtArgumentList(tokens)) { return } | ||
} else { | ||
return | ||
} | ||
const parseTitledArgumentsSection = function (tokens) { | ||
const firstToken = tokens[0] | ||
if (!firstToken || firstToken.type !== 'heading') return | ||
if (!firstToken.text.startsWith('Arguments:') || | ||
firstToken.depth !== SpecialHeadingDepth | ||
) { | ||
return | ||
} | ||
let args = null | ||
return { | ||
title: tokens.shift().text.replace('Arguments:', '').trim(), | ||
description: generateDescription(tokens, stopOnSectionBoundaries), | ||
arguments: parseArgumentList(tokens) | ||
if (firstToken.type === 'list_start') { | ||
args = this.parseArgumentList(tokens) | ||
} else { | ||
tokens.shift() // consume the header | ||
// consume any BS before the args list | ||
generateDescription(tokens, this.stopOnSectionBoundaries) | ||
args = this.parseArgumentList(tokens) | ||
} | ||
return args | ||
} | ||
} | ||
const parseEventsSection = function (tokens) { | ||
let firstToken = tokens[0] | ||
if ( | ||
!firstToken || | ||
firstToken.type !== 'heading' || | ||
firstToken.text !== 'Events' || | ||
firstToken.depth !== SpecialHeadingDepth | ||
) { return } | ||
parseTitledArgumentsSection (tokens) { | ||
const firstToken = tokens[0] | ||
if (!firstToken || firstToken.type !== 'heading') return | ||
if (!firstToken.text.startsWith('Arguments:') || | ||
firstToken.depth !== SpecialHeadingDepth | ||
) { | ||
return | ||
} | ||
const eventHeadingDepth = SpecialHeadingDepth + 1 | ||
// We consume until there is a heading of h3 which denotes the beginning of an event. | ||
const stopTokenCallback = function (token, tokens) { | ||
if ((token.type === 'heading') && (token.depth === eventHeadingDepth)) { | ||
return false | ||
return { | ||
title: tokens.shift().text.replace('Arguments:', '').trim(), | ||
description: generateDescription(tokens, this.stopOnSectionBoundaries), | ||
arguments: this.parseArgumentList(tokens) | ||
} | ||
return stopOnSectionBoundaries(token, tokens) | ||
} | ||
const events = [] | ||
tokens.shift() // consume the header | ||
parseEventsSection (tokens) { | ||
let firstToken = tokens[0] | ||
if ( | ||
!firstToken || | ||
firstToken.type !== 'heading' || | ||
firstToken.text !== 'Events' || | ||
firstToken.depth !== SpecialHeadingDepth | ||
) { return } | ||
while (tokens.length) { | ||
const eventHeadingDepth = SpecialHeadingDepth + 1 | ||
// We consume until there is a heading of h3 which denotes the beginning of an event. | ||
generateDescription(tokens, stopTokenCallback) | ||
firstToken = tokens[0] | ||
if ( | ||
firstToken && | ||
firstToken.type === 'heading' && | ||
firstToken.depth === eventHeadingDepth | ||
) { | ||
tokens.shift() // consume the header | ||
const {summary, description, visibility} = parseSummaryAndDescription( | ||
tokens, stopTokenCallback) | ||
const name = firstToken.text | ||
let args = parseArgumentList(tokens) | ||
if (args.length === 0) args = null | ||
events.push({name, summary, description, visibility, arguments: args}) | ||
} else { | ||
break | ||
const stopTokenCallback = (token, tokens) => { | ||
if ((token.type === 'heading') && (token.depth === eventHeadingDepth)) { | ||
return false | ||
} | ||
return this.stopOnSectionBoundaries(token, tokens) | ||
} | ||
} | ||
if (events.length) { return events } | ||
} | ||
const events = [] | ||
tokens.shift() // consume the header | ||
const parseExamplesSection = function (tokens) { | ||
let firstToken = tokens[0] | ||
if ( | ||
!firstToken || | ||
firstToken.type !== 'heading' || | ||
firstToken.text !== 'Examples' || | ||
firstToken.depth !== SpecialHeadingDepth | ||
) { return } | ||
while (tokens.length) { | ||
// We consume until there is a heading of h3 which denotes the beginning of an event. | ||
generateDescription(tokens, stopTokenCallback) | ||
const examples = [] | ||
tokens.shift() // consume the header | ||
while (tokens.length) { | ||
const description = generateDescription(tokens, function (token, tokens) { | ||
if (token.type === 'code') return false | ||
return stopOnSectionBoundaries(token, tokens) | ||
}) | ||
firstToken = tokens[0] | ||
if (firstToken.type === 'code') { | ||
const example = { | ||
description, | ||
lang: firstToken.lang, | ||
code: firstToken.text, | ||
raw: generateCode(tokens) | ||
firstToken = tokens[0] | ||
if ( | ||
firstToken && | ||
firstToken.type === 'heading' && | ||
firstToken.depth === eventHeadingDepth | ||
) { | ||
tokens.shift() // consume the header | ||
const {summary, description, visibility} = this.parseSummaryAndDescription( | ||
tokens, stopTokenCallback) | ||
const name = firstToken.text | ||
let args = this.parseArgumentList(tokens) | ||
if (args.length === 0) args = null | ||
events.push({name, summary, description, visibility, arguments: args}) | ||
} else { | ||
break | ||
} | ||
examples.push(example) | ||
} else { | ||
break | ||
} | ||
if (events.length) { return events } | ||
} | ||
if (examples.length) { return examples } | ||
} | ||
parseExamplesSection (tokens) { | ||
let firstToken = tokens[0] | ||
if ( | ||
!firstToken || | ||
firstToken.type !== 'heading' || | ||
firstToken.text !== 'Examples' || | ||
firstToken.depth !== SpecialHeadingDepth | ||
) { return } | ||
const parseReturnValues = function (tokens, consumeTokensAfterReturn) { | ||
let normalizedString | ||
if (consumeTokensAfterReturn == null) { consumeTokensAfterReturn = false } | ||
const firstToken = tokens[0] | ||
if ( | ||
!firstToken || | ||
!['paragraph', 'text'].includes(firstToken.type) || | ||
!isReturnValue(firstToken.text) | ||
) { return } | ||
const examples = [] | ||
tokens.shift() // consume the header | ||
// there might be a `Public: ` in front of the return. | ||
const returnsMatches = ReturnsRegex.exec(firstToken.text) | ||
if (consumeTokensAfterReturn) { | ||
normalizedString = generateDescription(tokens, () => true) | ||
if (returnsMatches[1]) { | ||
normalizedString = normalizedString.replace(returnsMatches[1], '') | ||
while (tokens.length) { | ||
const description = generateDescription(tokens, (token, tokens) => { | ||
if (token.type === 'code') return false | ||
return this.stopOnSectionBoundaries(token, tokens) | ||
}) | ||
firstToken = tokens[0] | ||
if (firstToken.type === 'code') { | ||
const example = { | ||
description, | ||
lang: firstToken.lang, | ||
code: firstToken.text, | ||
raw: generateCode(tokens) | ||
} | ||
examples.push(example) | ||
} else { | ||
break | ||
} | ||
} | ||
} else { | ||
const token = tokens.shift() | ||
normalizedString = token.text | ||
if (returnsMatches[1]) { | ||
normalizedString = normalizedString.replace(returnsMatches[1], '') | ||
} | ||
normalizedString = normalizedString.replace(/\s{2,}/g, ' ') | ||
if (examples.length) { return examples } | ||
} | ||
let returnValues = null | ||
parseReturnValues (tokens, consumeTokensAfterReturn) { | ||
let normalizedString | ||
if (consumeTokensAfterReturn == null) { consumeTokensAfterReturn = false } | ||
const firstToken = tokens[0] | ||
if ( | ||
!firstToken || | ||
!['paragraph', 'text'].includes(firstToken.type) || | ||
!isReturnValue(firstToken.text) | ||
) { return } | ||
while (normalizedString) { | ||
const nextIndex = normalizedString.indexOf('Returns', 1) | ||
let returnString = normalizedString | ||
if (nextIndex > -1) { | ||
returnString = normalizedString.substring(0, nextIndex) | ||
normalizedString = normalizedString.substring(nextIndex, normalizedString.length) | ||
// there might be a `Public: ` in front of the return. | ||
const returnsMatches = ReturnsRegex.exec(firstToken.text) | ||
if (consumeTokensAfterReturn) { | ||
normalizedString = generateDescription(tokens, () => true) | ||
if (returnsMatches[1]) { | ||
normalizedString = normalizedString.replace(returnsMatches[1], '') | ||
} | ||
} else { | ||
normalizedString = null | ||
const token = tokens.shift() | ||
normalizedString = token.text | ||
if (returnsMatches[1]) { | ||
normalizedString = normalizedString.replace(returnsMatches[1], '') | ||
} | ||
normalizedString = normalizedString.replace(/\s{2,}/g, ' ') | ||
} | ||
if (returnValues == null) { returnValues = [] } | ||
returnValues.push({ | ||
type: getLinkMatch(returnString), | ||
description: returnString.trim() | ||
}) | ||
let returnValues = null | ||
while (normalizedString) { | ||
const nextIndex = normalizedString.indexOf('Returns', 1) | ||
let returnString = normalizedString | ||
if (nextIndex > -1) { | ||
returnString = normalizedString.substring(0, nextIndex) | ||
normalizedString = normalizedString.substring(nextIndex, normalizedString.length) | ||
} else { | ||
normalizedString = null | ||
} | ||
if (returnValues == null) { returnValues = [] } | ||
returnValues.push({ | ||
type: getLinkMatch(returnString), | ||
description: returnString.trim() | ||
}) | ||
} | ||
return returnValues | ||
} | ||
return returnValues | ||
} | ||
// Parses argument lists like this one: | ||
// | ||
// * `something` A {Bool} | ||
// * `somethingNested` A nested object | ||
parseArgumentList (tokens) { | ||
let depth = 0 | ||
let args = [] | ||
let argumentsList = null | ||
const argumentsListStack = [] | ||
let argument = null | ||
const argumentStack = [] | ||
// Parses argument lists like this one: | ||
// | ||
// * `something` A {Bool} | ||
// * `somethingNested` A nested object | ||
const parseArgumentList = function (tokens) { | ||
let depth = 0 | ||
let args = [] | ||
let argumentsList = null | ||
const argumentsListStack = [] | ||
let argument = null | ||
const argumentStack = [] | ||
while (tokens.length && (tokens[0].type === 'list_start' || depth)) { | ||
const token = tokens[0] | ||
switch (token.type) { | ||
case 'list_start': | ||
// This list might not be a argument list. Check... | ||
const parseAsArgumentList = isAtArgumentList(tokens) | ||
if (parseAsArgumentList) { | ||
depth++ | ||
if (argumentsList) argumentsListStack.push(argumentsList) | ||
argumentsList = [] | ||
tokens.shift() | ||
} else if (argument) { | ||
// If not, consume the list as part of the description | ||
if (!argument.text) argument.text = [] | ||
argument.text.push(`\n${generateList(tokens)}`) | ||
} | ||
break | ||
while (tokens.length && (tokens[0].type === 'list_start' || depth)) { | ||
const token = tokens[0] | ||
switch (token.type) { | ||
case 'list_start': | ||
// This list might not be a argument list. Check... | ||
const parseAsArgumentList = isAtArgumentList(tokens) | ||
if (parseAsArgumentList) { | ||
depth++ | ||
if (argumentsList) argumentsListStack.push(argumentsList) | ||
argumentsList = [] | ||
case 'list_item_start': | ||
case 'loose_item_start': | ||
if (argument) { argumentStack.push(argument) } | ||
argument = {} | ||
tokens.shift() | ||
} else if (argument) { | ||
// If not, consume the list as part of the description | ||
break | ||
case 'code': | ||
if (!argument.text) argument.text = [] | ||
argument.text.push(`\n${generateList(tokens)}`) | ||
} | ||
break | ||
argument.text.push(`\n${generateCode(tokens)}`) | ||
break | ||
case 'list_item_start': | ||
case 'loose_item_start': | ||
if (argument) { argumentStack.push(argument) } | ||
argument = {} | ||
tokens.shift() | ||
break | ||
case 'text': | ||
if (!argument.text) argument.text = [] | ||
argument.text.push(token.text) | ||
tokens.shift() | ||
break | ||
case 'code': | ||
if (!argument.text) argument.text = [] | ||
argument.text.push(`\n${generateCode(tokens)}`) | ||
break | ||
case 'list_item_end': | ||
case 'loose_item_end': | ||
if (argument) { | ||
Object.assign(argument, | ||
this.parseListItem(argument.text.join(' ').replace(/ \n/g, '\n'))) | ||
argumentsList.push(argument) | ||
delete argument.text | ||
} | ||
case 'text': | ||
if (!argument.text) argument.text = [] | ||
argument.text.push(token.text) | ||
tokens.shift() | ||
break | ||
argument = argumentStack.pop() | ||
tokens.shift() | ||
break | ||
case 'list_item_end': | ||
case 'loose_item_end': | ||
if (argument) { | ||
Object.assign(argument, | ||
parseListItem(argument.text.join(' ').replace(/ \n/g, '\n'))) | ||
argumentsList.push(argument) | ||
delete argument.text | ||
} | ||
case 'list_end': | ||
depth-- | ||
if (argument) { | ||
argument.children = argumentsList | ||
argumentsList = argumentsListStack.pop() | ||
} else { | ||
args = argumentsList | ||
} | ||
tokens.shift() | ||
break | ||
argument = argumentStack.pop() | ||
tokens.shift() | ||
break | ||
default: tokens.shift() | ||
} | ||
} | ||
case 'list_end': | ||
depth-- | ||
if (argument) { | ||
argument.children = argumentsList | ||
argumentsList = argumentsListStack.pop() | ||
} else { | ||
args = argumentsList | ||
} | ||
tokens.shift() | ||
break | ||
return args | ||
} | ||
default: tokens.shift() | ||
parseListItem (argumentString) { | ||
let isOptional | ||
let name = null | ||
let type = null | ||
let description = argumentString | ||
const nameMatches = ArgumentListItemRegex.exec(argumentString) | ||
if (nameMatches) { | ||
name = nameMatches[1] | ||
description = description.replace(nameMatches[0], '') | ||
type = getLinkMatch(description) | ||
isOptional = !!nameMatches[3] | ||
} | ||
return {name, description, type, isOptional} | ||
} | ||
return args | ||
} | ||
stopOnSectionBoundaries (token, tokens) { | ||
if (['paragraph', 'text'].includes(token.type)) { | ||
if (this.parseReturns && isReturnValue(token.text)) { | ||
return false | ||
} | ||
} else if (token.type === 'heading') { | ||
if (token.depth === SpecialHeadingDepth && SpecialHeadings.test(token.text)) { | ||
return false | ||
} | ||
} else if (token.type === 'list_start') { | ||
let listToken = null | ||
for (listToken of tokens) { | ||
if (listToken.type === 'text') break | ||
} | ||
const parseListItem = function (argumentString) { | ||
let isOptional | ||
let name = null | ||
let type = null | ||
let description = argumentString | ||
// Check if list is an arguments list. If it starts with `someVar`, it is. | ||
if (listToken && ArgumentListItemRegex.test(listToken.text)) return false | ||
} | ||
const nameMatches = ArgumentListItemRegex.exec(argumentString) | ||
if (nameMatches) { | ||
name = nameMatches[1] | ||
description = description.replace(nameMatches[0], '') | ||
type = getLinkMatch(description) | ||
isOptional = !!nameMatches[3] | ||
return true | ||
} | ||
return {name, description, type, isOptional} | ||
} | ||
@@ -380,24 +420,2 @@ | ||
const stopOnSectionBoundaries = function (token, tokens) { | ||
if (['paragraph', 'text'].includes(token.type)) { | ||
if (isReturnValue(token.text)) { | ||
return false | ||
} | ||
} else if (token.type === 'heading') { | ||
if (token.depth === SpecialHeadingDepth && SpecialHeadings.test(token.text)) { | ||
return false | ||
} | ||
} else if (token.type === 'list_start') { | ||
let listToken = null | ||
for (listToken of tokens) { | ||
if (listToken.type === 'text') break | ||
} | ||
// Check if list is an arguments list. If it starts with `someVar`, it is. | ||
if (listToken && ArgumentListItemRegex.test(listToken.text)) return false | ||
} | ||
return true | ||
} | ||
// Will read / consume tokens down to a special section (args, events, examples) | ||
@@ -404,0 +422,0 @@ const generateDescription = function (tokens, tokenCallback) { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
23883
486
188