Skip to content

Commit

Permalink
Expose Converter.resolveLinks
Browse files Browse the repository at this point in the history
Resolves #2004.
  • Loading branch information
Gerrit0 committed Jul 30, 2022
1 parent 5d9a51d commit 3830f96
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 265 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@
- Improved schema generation to give better autocomplete for the `sort` option.
- Optional properties are now visually distinguished in the index/sidebar by rendering `prop` as `prop?`, #2023.
- `DefaultThemeRenderContext.markdown` now also accepts a `CommentDisplayPart[]` for rendering, #2004.
- Expose `Converter.resolveLinks` method for use with `Converter.parseRawComment`, #2004.

### Bug Fixes

Expand Down
265 changes: 265 additions & 0 deletions src/lib/converter/comments/linkResolver.ts
@@ -0,0 +1,265 @@
import * as ts from "typescript";
import {
Comment,
CommentDisplayPart,
DeclarationReflection,
InlineTagDisplayPart,
Reflection,
} from "../../models";
import type { Logger, ValidationOptions } from "../../utils";
import { parseDeclarationReference } from "./declarationReference";
import { resolveDeclarationReference } from "./declarationReferenceResolver";

const urlPrefix = /^(http|ftp)s?:\/\//;
const brackets = /\[\[(?!include:)([^\]]+)\]\]/g;

export function resolveLinks(
comment: Comment,
reflection: Reflection,
validation: ValidationOptions,
logger: Logger
) {
let warned = false;
const warn = () => {
if (!warned) {
warned = true;
logger.warn(
`${reflection.getFriendlyFullName()}: Comment [[target]] style links are deprecated and will be removed in 0.24`
);
}
};

comment.summary = resolvePartLinks(
reflection,
comment.summary,
warn,
validation,
logger
);
for (const tag of comment.blockTags) {
tag.content = resolvePartLinks(
reflection,
tag.content,
warn,
validation,
logger
);
}

if (reflection instanceof DeclarationReflection && reflection.readme) {
reflection.readme = resolvePartLinks(
reflection,
reflection.readme,
warn,
validation,
logger
);
}
}

export function resolvePartLinks(
reflection: Reflection,
parts: readonly CommentDisplayPart[],
warn: () => void,
validation: ValidationOptions,
logger: Logger
): CommentDisplayPart[] {
return parts.flatMap((part) =>
processPart(reflection, part, warn, validation, logger)
);
}

function processPart(
reflection: Reflection,
part: CommentDisplayPart,
warn: () => void,
validation: ValidationOptions,
logger: Logger
): CommentDisplayPart | CommentDisplayPart[] {
if (part.kind === "text" && brackets.test(part.text)) {
warn();
return replaceBrackets(reflection, part.text, validation, logger);
}

if (part.kind === "inline-tag") {
if (
part.tag === "@link" ||
part.tag === "@linkcode" ||
part.tag === "@linkplain"
) {
return resolveLinkTag(reflection, part, (msg: string) => {
if (validation.invalidLink) {
logger.warn(msg);
}
});
}
}

return part;
}

function resolveLinkTag(
reflection: Reflection,
part: InlineTagDisplayPart,
warn: (message: string) => void
) {
let pos = 0;
const end = part.text.length;
while (pos < end && ts.isWhiteSpaceLike(part.text.charCodeAt(pos))) {
pos++;
}
const origText = part.text;

// Try to parse one
const declRef = parseDeclarationReference(part.text, pos, end);

let target: Reflection | string | undefined;
if (declRef) {
// Got one, great! Try to resolve the link
target = resolveDeclarationReference(reflection, declRef[0]);
pos = declRef[1];
}

if (!target) {
if (urlPrefix.test(part.text)) {
const wsIndex = part.text.search(/\s/);
target =
wsIndex === -1 ? part.text : part.text.substring(0, wsIndex);
pos = target.length;
}
}

// If resolution via a declaration reference failed, revert to the legacy "split and check"
// method... this should go away in 0.24, once people have had a chance to migrate any failing links.
if (!target) {
const resolved = legacyResolveLinkTag(reflection, part);
if (resolved) {
warn(
`Failed to resolve {@link ${origText}} in ${reflection.getFriendlyFullName()} with declaration references. This link will break in v0.24.`
);
}
return resolved;
}

// Remaining text after an optional pipe is the link text, so advance
// until that's consumed.
while (pos < end && ts.isWhiteSpaceLike(part.text.charCodeAt(pos))) {
pos++;
}
if (pos < end && part.text[pos] === "|") {
pos++;
}

part.target = target;
part.text =
part.text.substring(pos).trim() ||
(typeof target === "string" ? target : target.name);

return part;
}

function legacyResolveLinkTag(
reflection: Reflection,
part: InlineTagDisplayPart
) {
const { caption, target } = splitLinkText(part.text);

if (urlPrefix.test(target)) {
part.text = caption;
part.target = target;
} else {
const targetRefl = reflection.findReflectionByName(target);
if (targetRefl) {
part.text = caption;
part.target = targetRefl;
}
}

return part;
}

function replaceBrackets(
reflection: Reflection,
text: string,
validation: ValidationOptions,
logger: Logger
): CommentDisplayPart[] {
const parts: CommentDisplayPart[] = [];

let begin = 0;
brackets.lastIndex = 0;
for (const match of text.matchAll(brackets)) {
if (begin != match.index) {
parts.push({
kind: "text",
text: text.substring(begin, match.index),
});
}
begin = match.index! + match[0].length;
const content = match[1];

const { target, caption } = splitLinkText(content);

if (urlPrefix.test(target)) {
parts.push({
kind: "inline-tag",
tag: "@link",
text: caption,
target,
});
} else {
const targetRefl = reflection.findReflectionByName(target);
if (targetRefl) {
parts.push({
kind: "inline-tag",
tag: "@link",
text: caption,
target: targetRefl,
});
} else {
if (validation.invalidLink) {
logger.warn("Failed to find target: " + content);
}
parts.push({
kind: "inline-tag",
tag: "@link",
text: content,
});
}
}
}
parts.push({
kind: "text",
text: text.substring(begin),
});

return parts;
}

/**
* Split the given link into text and target at first pipe or space.
*
* @param text The source string that should be checked for a split character.
* @returns An object containing the link text and target.
*/
function splitLinkText(text: string): { caption: string; target: string } {
let splitIndex = text.indexOf("|");
if (splitIndex === -1) {
splitIndex = text.search(/\s/);
}

if (splitIndex !== -1) {
return {
caption: text
.substring(splitIndex + 1)
.replace(/\n+/, " ")
.trim(),
target: text.substring(0, splitIndex).trim(),
};
} else {
return {
caption: text,
target: text,
};
}
}
51 changes: 49 additions & 2 deletions src/lib/converter/converter.ts
@@ -1,7 +1,14 @@
import * as ts from "typescript";

import type { Application } from "../application";
import { ProjectReflection, ReflectionKind, SomeType } from "../models/index";
import {
Comment,
CommentDisplayPart,
ProjectReflection,
Reflection,
ReflectionKind,
SomeType,
} from "../models/index";
import { Context } from "./context";
import { ConverterComponent } from "./components";
import { Component, ChildableComponent } from "../utils/component";
Expand All @@ -14,9 +21,13 @@ import type { IMinimatch } from "minimatch";
import { hasAllFlags, hasAnyFlag } from "../utils/enum";
import type { DocumentationEntryPoint } from "../utils/entry-point";
import { CommentParserConfig, getComment } from "./comments";
import type { CommentStyle } from "../utils/options/declaration";
import type {
CommentStyle,
ValidationOptions,
} from "../utils/options/declaration";
import { parseComment } from "./comments/parser";
import { lexCommentString } from "./comments/rawLexer";
import { resolvePartLinks, resolveLinks } from "./comments/linkResolver";

/**
* Compiles source files using TypeScript and converts compiler symbols to reflections.
Expand Down Expand Up @@ -56,6 +67,10 @@ export class Converter extends ChildableComponent<
@BindOption("commentStyle")
commentStyle!: CommentStyle;

/** @internal */
@BindOption("validation")
validation!: ValidationOptions;

private _config?: CommentParserConfig;

get config(): CommentParserConfig {
Expand Down Expand Up @@ -196,6 +211,38 @@ export class Converter extends ChildableComponent<
);
}

resolveLinks(comment: Comment, owner: Reflection): void;
resolveLinks(
parts: readonly CommentDisplayPart[],
owner: Reflection
): CommentDisplayPart[];
resolveLinks(
comment: Comment | readonly CommentDisplayPart[],
owner: Reflection
): CommentDisplayPart[] | undefined {
if (comment instanceof Comment) {
resolveLinks(comment, owner, this.validation, this.owner.logger);
} else {
let warned = false;
const warn = () => {
if (!warned) {
warned = true;
this.application.logger.warn(
`${owner.name}: Comment [[target]] style links are deprecated and will be removed in 0.24`
);
}
};

return resolvePartLinks(
owner,
comment,
warn,
this.validation,
this.owner.logger
);
}
}

/**
* Compile the files within the given context and convert the compiler symbols to reflections.
*
Expand Down

0 comments on commit 3830f96

Please sign in to comment.