"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Comment = exports.CommentTag = void 0;
const utils_1 = require("../../utils");
const kind_1 = require("../reflections/kind");
const ReflectionSymbolId_1 = require("../reflections/ReflectionSymbolId");
/**
* A model that represents a single TypeDoc comment tag.
*
* Tags are stored in the {@link Comment.blockTags} property.
* @category Comments
*/
class CommentTag {
/**
* Create a new CommentTag instance.
*/
constructor(tag, text) {
this.tag = tag;
this.content = text;
}
clone() {
const tag = new CommentTag(this.tag, Comment.cloneDisplayParts(this.content));
if (this.name) {
tag.name = this.name;
}
return tag;
}
toObject(serializer) {
return {
tag: this.tag,
name: this.name,
content: Comment.serializeDisplayParts(serializer, this.content),
};
}
fromObject(de, obj) {
// tag already set by Comment.fromObject
this.name = obj.name;
this.content = Comment.deserializeDisplayParts(de, obj.content);
}
}
exports.CommentTag = CommentTag;
/**
* A model that represents a comment.
*
* Instances of this model are created by the CommentPlugin. You can retrieve comments
* through the {@link DeclarationReflection.comment} property.
* @category Comments
*/
class Comment {
/**
* Debugging utility for combining parts into a simple string. Not suitable for
* rendering, but can be useful in tests.
*/
static combineDisplayParts(parts) {
let result = "";
for (const item of parts || []) {
switch (item.kind) {
case "text":
case "code":
result += item.text;
break;
case "inline-tag":
result += `{${item.tag} ${item.text}}`;
break;
default:
(0, utils_1.assertNever)(item);
}
}
return result;
}
/**
* Helper function to convert an array of comment display parts into markdown suitable for
* passing into Marked. `urlTo` will be used to resolve urls to any reflections linked to with
* `@link` tags.
*/
static displayPartsToMarkdown(parts, urlTo) {
const result = [];
for (const part of parts) {
switch (part.kind) {
case "text":
case "code":
result.push(part.text);
break;
case "inline-tag":
switch (part.tag) {
case "@label":
case "@inheritdoc": // Shouldn't happen
break; // Not rendered.
case "@link":
case "@linkcode":
case "@linkplain": {
if (part.target) {
let url;
let kindClass;
if (typeof part.target === "string") {
url = part.target;
}
else if (part.target && "id" in part.target) {
// No point in trying to resolve a ReflectionSymbolId at this point, we've already
// tried and failed during the resolution step.
url = urlTo(part.target);
kindClass = kind_1.ReflectionKind.classString(part.target.kind);
}
const text = part.tag === "@linkcode"
? `${part.text}`
: part.text;
result.push(url
? `${text}`
: part.text);
}
else {
result.push(part.text);
}
break;
}
default:
// Hmm... probably want to be able to render these somehow, so custom inline tags can be given
// special rendering rules. Future capability. For now, just render their text.
result.push(`{${part.tag} ${part.text}}`);
break;
}
break;
default:
(0, utils_1.assertNever)(part);
}
}
return result.join("");
}
/**
* Helper utility to clone {@link Comment.summary} or {@link CommentTag.content}
*/
static cloneDisplayParts(parts) {
return parts.map((p) => ({ ...p }));
}
static serializeDisplayParts(serializer, parts) {
return parts?.map((part) => {
switch (part.kind) {
case "text":
case "code":
return { ...part };
case "inline-tag": {
let target;
if (typeof part.target === "string") {
target = part.target;
}
else if (part.target) {
if ("id" in part.target) {
target = part.target.id;
}
else {
target = part.target.toObject(serializer);
}
}
return {
...part,
target,
};
}
}
});
}
//Since display parts are plain objects, this lives here
static deserializeDisplayParts(de, parts) {
const links = [];
const result = parts.map((part) => {
switch (part.kind) {
case "text":
case "code":
return { ...part };
case "inline-tag": {
if (typeof part.target === "number") {
const part2 = {
kind: part.kind,
tag: part.tag,
text: part.text,
target: undefined,
tsLinkText: part.tsLinkText,
};
links.push([part.target, part2]);
return part2;
}
else if (typeof part.target === "string" ||
part.target === undefined) {
return {
kind: "inline-tag",
tag: part.tag,
text: part.text,
target: part.target,
tsLinkText: part.tsLinkText,
};
}
else if (typeof part.target === "object") {
return {
kind: "inline-tag",
tag: part.tag,
text: part.text,
target: new ReflectionSymbolId_1.ReflectionSymbolId(part.target),
tsLinkText: part.tsLinkText,
};
}
else {
(0, utils_1.assertNever)(part.target);
}
}
}
});
if (links.length) {
de.defer((project) => {
for (const [oldId, part] of links) {
part.target = project.getReflectionById(de.oldIdToNewId[oldId] ?? -1);
if (!part.target) {
de.logger.warn(`Serialized project contained a link to ${oldId} (${part.text}), which was not a part of the project.`);
}
}
});
}
return result;
}
/**
* Splits the provided parts into a header (first line, as a string)
* and body (remaining lines). If the header line contains inline tags
* they will be serialized to a string.
*/
static splitPartsToHeaderAndBody(parts) {
let index = parts.findIndex((part) => {
switch (part.kind) {
case "text":
case "code":
return part.text.includes("\n");
case "inline-tag":
return false;
}
});
if (index === -1) {
return {
header: Comment.combineDisplayParts(parts),
body: [],
};
}
// Do not split a code block, stop the header at the end of the previous block
if (parts[index].kind === "code") {
--index;
}
if (index === -1) {
return { header: "", body: Comment.cloneDisplayParts(parts) };
}
let header = Comment.combineDisplayParts(parts.slice(0, index));
const split = parts[index].text.indexOf("\n");
let body;
if (split === -1) {
header += parts[index].text;
body = Comment.cloneDisplayParts(parts.slice(index + 1));
}
else {
header += parts[index].text.substring(0, split);
body = Comment.cloneDisplayParts(parts.slice(index));
body[0].text = body[0].text.substring(split + 1);
}
if (!body[0].text) {
body.shift();
}
return { header: header.trim(), body };
}
/**
* Creates a new Comment instance.
*/
constructor(summary = [], blockTags = [], modifierTags = new Set()) {
/**
* All associated block level tags.
*/
this.blockTags = [];
/**
* All modifier tags present on the comment, e.g. `@alpha`, `@beta`.
*/
this.modifierTags = new Set();
this.summary = summary;
this.blockTags = blockTags;
this.modifierTags = modifierTags;
extractLabelTag(this);
}
/**
* Create a deep clone of this comment.
*/
clone() {
return new Comment(Comment.cloneDisplayParts(this.summary), this.blockTags.map((tag) => tag.clone()), new Set(this.modifierTags));
}
/**
* Returns true if this comment is completely empty.
* @internal
*/
isEmpty() {
return !this.hasVisibleComponent() && this.modifierTags.size === 0;
}
/**
* Has this comment a visible component?
*
* @returns TRUE when this comment has a visible component.
*/
hasVisibleComponent() {
return (this.summary.some((x) => x.kind !== "text" || x.text !== "") ||
this.blockTags.length > 0);
}
/**
* Test whether this comment contains a tag with the given name.
*
* @param tagName The name of the tag to look for.
* @returns TRUE when this comment contains a tag with the given name, otherwise FALSE.
*/
hasModifier(tagName) {
return this.modifierTags.has(tagName);
}
removeModifier(tagName) {
this.modifierTags.delete(tagName);
}
/**
* Return the first tag with the given name.
*
* @param tagName The name of the tag to look for.
* @returns The found tag or undefined.
*/
getTag(tagName) {
return this.blockTags.find((tag) => tag.tag === tagName);
}
/**
* Get all tags with the given tag name.
*/
getTags(tagName) {
return this.blockTags.filter((tag) => tag.tag === tagName);
}
getIdentifiedTag(identifier, tagName) {
return this.blockTags.find((tag) => tag.tag === tagName && tag.name === identifier);
}
/**
* Removes all block tags with the given tag name from the comment.
* @param tagName
*/
removeTags(tagName) {
(0, utils_1.removeIf)(this.blockTags, (tag) => tag.tag === tagName);
}
toObject(serializer) {
return {
summary: Comment.serializeDisplayParts(serializer, this.summary),
blockTags: serializer.toObjectsOptional(this.blockTags),
modifierTags: this.modifierTags.size > 0
? Array.from(this.modifierTags)
: undefined,
label: this.label,
};
}
fromObject(de, obj) {
this.summary = Comment.deserializeDisplayParts(de, obj.summary);
this.blockTags =
obj.blockTags?.map((tagObj) => {
const tag = new CommentTag(tagObj.tag, []);
de.fromObject(tag, tagObj);
return tag;
}) || [];
this.modifierTags = new Set(obj.modifierTags);
this.label = obj.label;
}
}
exports.Comment = Comment;
function extractLabelTag(comment) {
const index = comment.summary.findIndex((part) => part.kind === "inline-tag" && part.tag === "@label");
if (index !== -1) {
comment.label = comment.summary.splice(index, 1)[0].text;
}
}