forked from public/fvtt-cthulhu-eternal
505 lines
13 KiB
JavaScript
505 lines
13 KiB
JavaScript
import {parse as jsdocTypePrattParse} from 'jsdoc-type-pratt-parser';
|
|
|
|
/**
|
|
* Removes initial and ending brackets from `rawType`
|
|
* @param {JsdocTypeLine[]|JsdocTag} container
|
|
* @param {boolean} [isArr]
|
|
* @returns {void}
|
|
*/
|
|
const stripEncapsulatingBrackets = (container, isArr) => {
|
|
if (isArr) {
|
|
const firstItem = /** @type {JsdocTypeLine[]} */ (container)[0];
|
|
firstItem.rawType = firstItem.rawType.replace(
|
|
/^\{/u, ''
|
|
);
|
|
|
|
const lastItem = /** @type {JsdocTypeLine} */ (
|
|
/** @type {JsdocTypeLine[]} */ (
|
|
container
|
|
).at(-1)
|
|
);
|
|
lastItem.rawType = lastItem.rawType.replace(/\}$/u, '');
|
|
|
|
return;
|
|
}
|
|
/** @type {JsdocTag} */ (container).rawType =
|
|
/** @type {JsdocTag} */ (container).rawType.replace(
|
|
/^\{/u, ''
|
|
).replace(/\}$/u, '');
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* delimiter: string,
|
|
* postDelimiter: string,
|
|
* rawType: string,
|
|
* initial: string,
|
|
* type: "JsdocTypeLine"
|
|
* }} JsdocTypeLine
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* delimiter: string,
|
|
* description: string,
|
|
* postDelimiter: string,
|
|
* initial: string,
|
|
* type: "JsdocDescriptionLine"
|
|
* }} JsdocDescriptionLine
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* format: 'pipe' | 'plain' | 'prefix' | 'space',
|
|
* namepathOrURL: string,
|
|
* tag: string,
|
|
* text: string,
|
|
* }} JsdocInlineTagNoType
|
|
*/
|
|
/**
|
|
* @typedef {JsdocInlineTagNoType & {
|
|
* type: "JsdocInlineTag"
|
|
* }} JsdocInlineTag
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* delimiter: string,
|
|
* description: string,
|
|
* descriptionLines: JsdocDescriptionLine[],
|
|
* initial: string,
|
|
* inlineTags: JsdocInlineTag[]
|
|
* name: string,
|
|
* postDelimiter: string,
|
|
* postName: string,
|
|
* postTag: string,
|
|
* postType: string,
|
|
* rawType: string,
|
|
* parsedType: import('jsdoc-type-pratt-parser').RootResult|null
|
|
* tag: string,
|
|
* type: "JsdocTag",
|
|
* typeLines: JsdocTypeLine[],
|
|
* }} JsdocTag
|
|
*/
|
|
|
|
/**
|
|
* @typedef {number} Integer
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* delimiter: string,
|
|
* delimiterLineBreak: string,
|
|
* description: string,
|
|
* descriptionEndLine?: Integer,
|
|
* descriptionLines: JsdocDescriptionLine[],
|
|
* descriptionStartLine?: Integer,
|
|
* hasPreterminalDescription: 0|1,
|
|
* hasPreterminalTagDescription?: 1,
|
|
* initial: string,
|
|
* inlineTags: JsdocInlineTag[]
|
|
* lastDescriptionLine?: Integer,
|
|
* endLine: Integer,
|
|
* lineEnd: string,
|
|
* postDelimiter: string,
|
|
* tags: JsdocTag[],
|
|
* terminal: string,
|
|
* preterminalLineBreak: string,
|
|
* type: "JsdocBlock",
|
|
* }} JsdocBlock
|
|
*/
|
|
|
|
/**
|
|
* @param {object} cfg
|
|
* @param {string} cfg.text
|
|
* @param {string} cfg.tag
|
|
* @param {'pipe' | 'plain' | 'prefix' | 'space'} cfg.format
|
|
* @param {string} cfg.namepathOrURL
|
|
* @returns {JsdocInlineTag}
|
|
*/
|
|
const inlineTagToAST = ({text, tag, format, namepathOrURL}) => ({
|
|
text,
|
|
tag,
|
|
format,
|
|
namepathOrURL,
|
|
type: 'JsdocInlineTag'
|
|
});
|
|
|
|
/**
|
|
* Converts comment parser AST to ESTree format.
|
|
* @param {import('.').JsdocBlockWithInline} jsdoc
|
|
* @param {import('jsdoc-type-pratt-parser').ParseMode} mode
|
|
* @param {object} opts
|
|
* @param {'compact'|'preserve'} [opts.spacing] By default, empty lines are
|
|
* compacted; set to 'preserve' to preserve empty comment lines.
|
|
* @param {boolean} [opts.throwOnTypeParsingErrors]
|
|
* @returns {JsdocBlock}
|
|
*/
|
|
const commentParserToESTree = (jsdoc, mode, {
|
|
spacing = 'compact',
|
|
throwOnTypeParsingErrors = false
|
|
} = {}) => {
|
|
/**
|
|
* Strips brackets from a tag's `rawType` values and adds `parsedType`
|
|
* @param {JsdocTag} lastTag
|
|
* @returns {void}
|
|
*/
|
|
const cleanUpLastTag = (lastTag) => {
|
|
// Strip out `}` that encapsulates and is not part of
|
|
// the type
|
|
stripEncapsulatingBrackets(lastTag);
|
|
|
|
if (lastTag.typeLines.length) {
|
|
stripEncapsulatingBrackets(lastTag.typeLines, true);
|
|
}
|
|
|
|
// Remove single empty line description.
|
|
if (lastTag.descriptionLines.length === 1 &&
|
|
lastTag.descriptionLines[0].description === '') {
|
|
lastTag.descriptionLines.length = 0;
|
|
}
|
|
|
|
// With even a multiline type now in full, add parsing
|
|
let parsedType = null;
|
|
|
|
try {
|
|
parsedType = jsdocTypePrattParse(lastTag.rawType, mode);
|
|
} catch (err) {
|
|
// Ignore
|
|
if (lastTag.rawType && throwOnTypeParsingErrors) {
|
|
/** @type {Error} */ (
|
|
err
|
|
).message = `Tag @${lastTag.tag} with raw type ` +
|
|
`\`${lastTag.rawType}\` had parsing error: ${
|
|
/** @type {Error} */ (err).message}`;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
lastTag.parsedType = parsedType;
|
|
};
|
|
|
|
const {source, inlineTags: blockInlineTags} = jsdoc;
|
|
|
|
const {tokens: {
|
|
delimiter: delimiterRoot,
|
|
lineEnd: lineEndRoot,
|
|
postDelimiter: postDelimiterRoot,
|
|
start: startRoot,
|
|
end: endRoot
|
|
}} = source[0];
|
|
|
|
const endLine = source.length - 1;
|
|
|
|
/** @type {JsdocBlock} */
|
|
const ast = {
|
|
delimiter: delimiterRoot,
|
|
delimiterLineBreak: '\n',
|
|
description: '',
|
|
|
|
descriptionLines: [],
|
|
inlineTags: blockInlineTags.map((t) => inlineTagToAST(t)),
|
|
|
|
initial: startRoot,
|
|
tags: [],
|
|
// `terminal` will be overwritten if there are other entries
|
|
terminal: endRoot,
|
|
preterminalLineBreak: '\n',
|
|
hasPreterminalDescription: 0,
|
|
endLine,
|
|
postDelimiter: postDelimiterRoot,
|
|
lineEnd: lineEndRoot,
|
|
|
|
type: 'JsdocBlock'
|
|
};
|
|
|
|
/**
|
|
* @type {JsdocTag[]}
|
|
*/
|
|
const tags = [];
|
|
|
|
/** @type {Integer|undefined} */
|
|
let lastDescriptionLine;
|
|
|
|
/** @type {JsdocTag|null} */
|
|
let lastTag = null;
|
|
|
|
// Tracks when first valid tag description line is seen.
|
|
let tagDescriptionSeen = false;
|
|
|
|
let descLineStateOpen = true;
|
|
|
|
source.forEach((info, idx) => {
|
|
const {tokens} = info;
|
|
const {
|
|
delimiter,
|
|
description,
|
|
postDelimiter,
|
|
start: initial,
|
|
tag,
|
|
end,
|
|
type: rawType
|
|
} = tokens;
|
|
|
|
if (!tag && description && descLineStateOpen) {
|
|
if (ast.descriptionStartLine === undefined) {
|
|
ast.descriptionStartLine = idx;
|
|
}
|
|
ast.descriptionEndLine = idx;
|
|
}
|
|
|
|
if (tag || end) {
|
|
descLineStateOpen = false;
|
|
if (lastDescriptionLine === undefined) {
|
|
lastDescriptionLine = idx;
|
|
}
|
|
|
|
// Clean-up with last tag before end or new tag
|
|
if (lastTag) {
|
|
cleanUpLastTag(lastTag);
|
|
}
|
|
|
|
// Stop the iteration when we reach the end
|
|
// but only when there is no tag earlier in the line
|
|
// to still process
|
|
if (end && !tag) {
|
|
ast.terminal = end;
|
|
|
|
// Check if there are any description lines and if not then this is a
|
|
// one line comment block.
|
|
const isDelimiterLine = ast.descriptionLines.length === 0 &&
|
|
delimiter === '/**';
|
|
|
|
// Remove delimiter line break for one line comments blocks.
|
|
if (isDelimiterLine) {
|
|
ast.delimiterLineBreak = '';
|
|
}
|
|
|
|
if (description) {
|
|
// Remove terminal line break at end when description is defined.
|
|
if (ast.terminal === '*/') {
|
|
ast.preterminalLineBreak = '';
|
|
}
|
|
|
|
if (lastTag) {
|
|
ast.hasPreterminalTagDescription = 1;
|
|
} else {
|
|
ast.hasPreterminalDescription = 1;
|
|
}
|
|
|
|
const holder = lastTag || ast;
|
|
holder.description += (holder.description ? '\n' : '') + description;
|
|
|
|
// Do not include `delimiter` / `postDelimiter` for opening
|
|
// delimiter line.
|
|
|
|
holder.descriptionLines.push({
|
|
delimiter: isDelimiterLine ? '' : delimiter,
|
|
description,
|
|
postDelimiter: isDelimiterLine ? '' : postDelimiter,
|
|
initial,
|
|
type: 'JsdocDescriptionLine'
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const {
|
|
// eslint-disable-next-line no-unused-vars -- Discarding
|
|
end: ed,
|
|
delimiter: de,
|
|
postDelimiter: pd,
|
|
start: init,
|
|
...tkns
|
|
} = tokens;
|
|
|
|
if (!tokens.name) {
|
|
let i = 1;
|
|
while (source[idx + i]) {
|
|
const {tokens: {
|
|
name,
|
|
postName,
|
|
postType,
|
|
tag: tg
|
|
}} = source[idx + i];
|
|
if (tg) {
|
|
break;
|
|
}
|
|
if (name) {
|
|
tkns.postType = postType;
|
|
tkns.name = name;
|
|
tkns.postName = postName;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {JsdocInlineTag[]}
|
|
*/
|
|
let tagInlineTags = [];
|
|
if (tag) {
|
|
// Assuming the tags from `source` are in the same order as `jsdoc.tags`
|
|
// we can use the `tags` length as index into the parser result tags.
|
|
tagInlineTags =
|
|
/**
|
|
* @type {import('comment-parser').Spec & {
|
|
* inlineTags: JsdocInlineTagNoType[]
|
|
* }}
|
|
*/ (
|
|
jsdoc.tags[tags.length]
|
|
).inlineTags.map(
|
|
(t) => inlineTagToAST(t)
|
|
);
|
|
}
|
|
|
|
/** @type {JsdocTag} */
|
|
const tagObj = {
|
|
...tkns,
|
|
initial: endLine ? init : '',
|
|
postDelimiter: lastDescriptionLine ? pd : '',
|
|
delimiter: lastDescriptionLine ? de : '',
|
|
descriptionLines: [],
|
|
inlineTags: tagInlineTags,
|
|
parsedType: null,
|
|
rawType: '',
|
|
type: 'JsdocTag',
|
|
typeLines: []
|
|
};
|
|
tagObj.tag = tagObj.tag.replace(/^@/u, '');
|
|
|
|
lastTag = tagObj;
|
|
tagDescriptionSeen = false;
|
|
|
|
tags.push(tagObj);
|
|
}
|
|
|
|
if (rawType) {
|
|
// Will strip rawType brackets after this tag
|
|
/** @type {JsdocTag} */ (lastTag).typeLines.push(
|
|
/** @type {JsdocTag} */ (lastTag).typeLines.length
|
|
? {
|
|
delimiter,
|
|
postDelimiter,
|
|
rawType,
|
|
initial,
|
|
type: 'JsdocTypeLine'
|
|
}
|
|
: {
|
|
delimiter: '',
|
|
postDelimiter: '',
|
|
rawType,
|
|
initial: '',
|
|
type: 'JsdocTypeLine'
|
|
}
|
|
);
|
|
/** @type {JsdocTag} */ (lastTag).rawType += /** @type {JsdocTag} */ (
|
|
lastTag
|
|
).rawType
|
|
? '\n' + rawType
|
|
: rawType;
|
|
}
|
|
|
|
// In `compact` mode skip processing if `description` is an empty string
|
|
// unless lastTag is being processed.
|
|
//
|
|
// In `preserve` mode process when `description` is not the `empty string
|
|
// or the `delimiter` is not `/**` ensuring empty lines are preserved.
|
|
if (((spacing === 'compact' && description) || lastTag) ||
|
|
(spacing === 'preserve' && (description || delimiter !== '/**'))) {
|
|
const holder = lastTag || ast;
|
|
|
|
// Check if there are any description lines and if not then this is a
|
|
// multi-line comment block with description on 0th line. Treat
|
|
// `delimiter` / `postDelimiter` / `initial` as being on a new line.
|
|
const isDelimiterLine = holder.descriptionLines.length === 0 &&
|
|
delimiter === '/**';
|
|
|
|
// Remove delimiter line break for one line comments blocks.
|
|
if (isDelimiterLine) {
|
|
ast.delimiterLineBreak = '';
|
|
}
|
|
|
|
// Track when the first description line is seen to avoid adding empty
|
|
// description lines for tag type lines.
|
|
tagDescriptionSeen ||= Boolean(lastTag &&
|
|
(rawType === '' || rawType?.endsWith('}')));
|
|
|
|
if (lastTag) {
|
|
if (tagDescriptionSeen) {
|
|
// The first tag description line is a continuation after type /
|
|
// name parsing.
|
|
const isFirstDescriptionLine = holder.descriptionLines.length === 0;
|
|
|
|
// For `compact` spacing must allow through first description line.
|
|
if ((spacing === 'compact' &&
|
|
(description || isFirstDescriptionLine)) ||
|
|
spacing === 'preserve') {
|
|
holder.descriptionLines.push({
|
|
delimiter: isFirstDescriptionLine ? '' : delimiter,
|
|
description,
|
|
postDelimiter: isFirstDescriptionLine ? '' : postDelimiter,
|
|
initial: isFirstDescriptionLine ? '' : initial,
|
|
type: 'JsdocDescriptionLine'
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
holder.descriptionLines.push({
|
|
delimiter: isDelimiterLine ? '' : delimiter,
|
|
description,
|
|
postDelimiter: isDelimiterLine ? '' : postDelimiter,
|
|
initial: isDelimiterLine ? `` : initial,
|
|
type: 'JsdocDescriptionLine'
|
|
});
|
|
}
|
|
|
|
if (!tag) {
|
|
if (lastTag) {
|
|
// For `compact` spacing must filter out any empty description lines
|
|
// after the initial `holder.description` has content.
|
|
if (tagDescriptionSeen && !(spacing === 'compact' &&
|
|
holder.description && description === '')) {
|
|
holder.description += !holder.description
|
|
? description
|
|
: '\n' + description;
|
|
}
|
|
} else {
|
|
holder.description += !holder.description
|
|
? description
|
|
: '\n' + description;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean-up where last line itself has tag content
|
|
if (end && tag) {
|
|
ast.terminal = end;
|
|
ast.hasPreterminalTagDescription = 1;
|
|
|
|
// Remove terminal line break at end when tag is defined on last line.
|
|
if (ast.terminal === '*/') {
|
|
ast.preterminalLineBreak = '';
|
|
}
|
|
|
|
cleanUpLastTag(/** @type {JsdocTag} */ (lastTag));
|
|
}
|
|
});
|
|
|
|
ast.lastDescriptionLine = lastDescriptionLine;
|
|
ast.tags = tags;
|
|
|
|
return ast;
|
|
};
|
|
|
|
const jsdocVisitorKeys = {
|
|
JsdocBlock: ['descriptionLines', 'tags', 'inlineTags'],
|
|
JsdocDescriptionLine: [],
|
|
JsdocTypeLine: [],
|
|
JsdocTag: ['parsedType', 'typeLines', 'descriptionLines', 'inlineTags'],
|
|
JsdocInlineTag: []
|
|
};
|
|
|
|
export {commentParserToESTree, jsdocVisitorKeys};
|