typedoc
Version:
Create api documentation for TypeScript projects.
251 lines (250 loc) • 11.6 kB
JavaScript
import path from "path";
import { ConverterComponent } from "../components.js";
import { ConverterEvents } from "../converter-events.js";
import { MinimalSourceFile } from "#utils";
import { isFile, readFile } from "../../utils/fs.js";
import { dedent, escapeRegExp, i18n } from "#utils";
import { normalizePath } from "#node-utils";
/**
* Handles `@include` and `@includeCode` within comments/documents.
*/
export class IncludePlugin extends ConverterComponent {
get logger() {
return this.application.logger;
}
constructor(owner) {
super(owner);
const onCreate = this.onCreate.bind(this);
owner.on(ConverterEvents.CREATE_PROJECT, onCreate);
owner.on(ConverterEvents.CREATE_DOCUMENT, onCreate);
owner.on(ConverterEvents.CREATE_DECLARATION, onCreate);
owner.on(ConverterEvents.CREATE_PARAMETER, onCreate);
owner.on(ConverterEvents.CREATE_SIGNATURE, onCreate);
owner.on(ConverterEvents.CREATE_TYPE_PARAMETER, onCreate);
}
onCreate(_context, refl) {
if (refl.isDocument()) {
// We know it must be present as documents are always associated with a file.
const relative = this.application.files.getReflectionPath(refl);
this.checkIncludeTagsParts(refl, path.dirname(relative), refl.content);
}
if (!refl.comment?.sourcePath)
return;
const relative = path.dirname(refl.comment.sourcePath);
this.checkIncludeTagsParts(refl, relative, refl.comment.summary);
for (const tag of refl.comment.blockTags) {
this.checkIncludeTagsParts(refl, relative, tag.content);
}
}
checkIncludeTagsParts(refl, relative, parts, included = []) {
for (let i = 0; i < parts.length; ++i) {
const part = parts[i];
if (part.kind !== "inline-tag" ||
!["@include", "@includeCode"].includes(part.tag)) {
continue;
}
const { filename, regionTarget, requestedLines } = parseIncludeCodeTextPart(part.text);
const file = normalizePath(path.resolve(relative, filename));
this.application.watchFile(file);
if (included.includes(file) && part.tag === "@include") {
this.logger.error(i18n.include_0_in_1_specified_2_circular_include_3(part.tag, refl.getFriendlyFullName(), part.text, included.join("\n\t")));
}
else if (isFile(file)) {
const text = readFile(file).replaceAll("\r\n", "\n");
const ext = path.extname(file).substring(1);
const includedText = regionTarget
? this.getRegions(refl, file, ext, part.text, text, regionTarget, part.tag, part.tag === "@includeCode")
: requestedLines
? this.getLines(refl, file, part.text, text, requestedLines, part.tag)
: text;
if (part.tag === "@include") {
const sf = new MinimalSourceFile(includedText, file);
const { content } = this.owner.parseRawComment(sf, refl.project.files);
this.checkIncludeTagsParts(refl, path.dirname(file), content, [...included, file]);
parts.splice(i, 1, ...content);
}
else {
parts[i] = {
kind: "code",
text: makeCodeBlock(ext, includedText),
};
}
}
else {
this.logger.error(i18n.include_0_in_1_specified_2_resolved_to_3_does_not_exist(part.tag, refl.getFriendlyFullName(), part.text, file));
}
}
}
getRegions(refl, file, ext, textPart, text, regionTargets, tag, ignoreIndent) {
const regionTagsList = regionTagREsByExt[ext];
if (!regionTagsList) {
this.logger.error(i18n.include_0_tag_in_1_region_2_region_not_supported(tag, refl.getFriendlyFullName(), textPart));
return "";
}
const targets = regionTargets.split(",").map((s) => s.trim());
let content = "";
for (const target of targets) {
let found = false;
for (const [startTag, endTag] of regionTagsList) {
const safeTarget = escapeRegExp(target);
const start = text.match(startTag(safeTarget));
const end = text.match(endTag(safeTarget));
const foundStart = start && start.length > 0;
const foundEnd = end && end.length > 0;
if (foundStart && !foundEnd) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_close_not_found(tag, refl.getFriendlyFullName(), textPart, file, target));
return "";
}
if (!foundStart && foundEnd) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_open_not_found(tag, refl.getFriendlyFullName(), textPart, file, target));
return "";
}
if (foundStart && foundEnd) {
if (start.length > 1) {
this.logger.error(i18n
.include_0_tag_in_1_specified_2_file_3_region_4_region_open_found_multiple_times(tag, refl.getFriendlyFullName(), textPart, file, target));
return "";
}
if (end.length > 1) {
this.logger.error(i18n
.include_0_tag_in_1_specified_2_file_3_region_4_region_close_found_multiple_times(tag, refl.getFriendlyFullName(), textPart, file, target));
return "";
}
if (found) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_found_multiple_times(tag, refl.getFriendlyFullName(), textPart, file, target));
return "";
}
found = text.substring(text.indexOf(start[0]) + start[0].length, text.indexOf(end[0]));
}
}
if (found === false) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_not_found(tag, refl.getFriendlyFullName(), textPart, file, target));
return "";
}
if (found.trim() === "") {
this.logger.warn(i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_empty(tag, refl.getFriendlyFullName(), textPart, file, target));
}
content += ignoreIndent ? dedent(found) + "\n" : found;
}
return content;
}
getLines(refl, file, textPart, text, requestedLines, tag) {
let output = "";
const lines = text.split(/\r\n|\r|\n/);
requestedLines.split(",").forEach((requestedLineString) => {
if (requestedLineString.includes("-")) {
const [start, end] = requestedLineString.split("-").map(Number);
if (start > end) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_lines_4_invalid_range(tag, refl.getFriendlyFullName(), textPart, file, requestedLines));
return "";
}
if (start > lines.length || end > lines.length) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_lines_4_but_only_5_lines(tag, refl.getFriendlyFullName(), textPart, file, requestedLines, lines.length.toString()));
return "";
}
output += lines.slice(start - 1, end).join("\n") + "\n";
}
else {
const requestedLine = Number(requestedLineString);
if (requestedLine > lines.length) {
this.logger.error(i18n.include_0_tag_in_1_specified_2_file_3_lines_4_but_only_5_lines(tag, refl.getFriendlyFullName(), textPart, file, requestedLines, lines.length.toString()));
return "";
}
output += lines[requestedLine - 1] + "\n";
}
});
return output;
}
}
function makeCodeBlock(lang, code) {
const escaped = code.replace(/`(?=`)/g, "`\u200B");
return "\n\n```" + lang + "\n" + escaped.trimEnd() + "\n```";
}
function parseIncludeCodeTextPart(text) {
let filename = text.trim();
let regionTarget;
let requestedLines;
if (filename.includes("#")) {
const parsed = filename.split("#");
filename = parsed[0];
regionTarget = parsed[1];
}
else if (filename.includes(":")) {
const parsed = filename.split(":");
filename = parsed[0];
requestedLines = parsed[1];
}
return { filename, regionTarget, requestedLines };
}
const regionTagREsByExt = {
bat: [
[
(regionName) => new RegExp(`:: *#region *${regionName} *\n`, "g"),
(regionName) => new RegExp(`:: *#endregion *${regionName} *\n`, "g"),
],
[
(regionName) => new RegExp(`REM *#region *${regionName} *\n`, "g"),
(regionName) => new RegExp(`REM *#endregion *${regionName} *\n`, "g"),
],
],
cs: [
[
(regionName) => new RegExp(`#region *${regionName} *\n`, "g"),
(regionName) => new RegExp(`#endregion *${regionName} *\n`, "g"),
],
],
c: [
[
(regionName) => new RegExp(`#pragma *region *${regionName} *\n`, "g"),
(regionName) => new RegExp(`#pragma *endregion *${regionName} *\n`, "g"),
],
],
css: [
[
(regionName) => new RegExp(`/\\* *#region *\\*/ *${regionName} *\n`, "g"),
(regionName) => new RegExp(`/\\* *#endregion *\\*/ *${regionName} *\n`, "g"),
],
],
md: [
[
(regionName) => new RegExp(`<!-- *#region *${regionName} *--> *\n`, "g"),
(regionName) => new RegExp(`<!-- *#endregion *${regionName} *--> *\n`, "g"),
],
],
ts: [
[
(regionName) => new RegExp(`// *#region *${regionName} *\n`, "g"),
(regionName) => new RegExp(`// *#endregion *${regionName} *\n`, "g"),
],
],
vb: [
[
(regionName) => new RegExp(`#Region *${regionName} *\n`, "g"),
(regionName) => new RegExp(`#End Region *${regionName} *\n`, "g"),
],
],
};
regionTagREsByExt["fs"] = [
...regionTagREsByExt["ts"],
[
(regionName) => new RegExp(`(#_region) *${regionName} *\n`, "g"),
(regionName) => new RegExp(`(#_endregion) *${regionName} *\n`, "g"),
],
];
regionTagREsByExt["java"] = [
...regionTagREsByExt["ts"],
[
(regionName) => new RegExp(`// *<editor-fold> *${regionName} *\n`, "g"),
(regionName) => new RegExp(`// *</editor-fold> *${regionName} *\n`, "g"),
],
];
regionTagREsByExt["cpp"] = regionTagREsByExt["c"];
regionTagREsByExt["less"] = regionTagREsByExt["css"];
regionTagREsByExt["scss"] = regionTagREsByExt["css"];
regionTagREsByExt["coffee"] = regionTagREsByExt["cs"];
regionTagREsByExt["php"] = regionTagREsByExt["cs"];
regionTagREsByExt["ps1"] = regionTagREsByExt["cs"];
regionTagREsByExt["py"] = regionTagREsByExt["cs"];
regionTagREsByExt["js"] = regionTagREsByExt["ts"];
regionTagREsByExt["mts"] = regionTagREsByExt["ts"];
regionTagREsByExt["cts"] = regionTagREsByExt["ts"];