diff --git a/tsdoc/src/configuration/TSDocConfiguration.ts b/tsdoc/src/configuration/TSDocConfiguration.ts index 20e39a5f..a62352b5 100644 --- a/tsdoc/src/configuration/TSDocConfiguration.ts +++ b/tsdoc/src/configuration/TSDocConfiguration.ts @@ -18,6 +18,7 @@ export class TSDocConfiguration { private readonly _validation: TSDocValidationConfiguration; private readonly _docNodeManager: DocNodeManager; private readonly _supportedHtmlElements: Set; + private readonly _linkUrlPrefixes: Set; public constructor() { this._tagDefinitions = []; @@ -26,6 +27,7 @@ export class TSDocConfiguration { this._validation = new TSDocValidationConfiguration(); this._docNodeManager = new DocNodeManager(); this._supportedHtmlElements = new Set(); + this._linkUrlPrefixes = new Set(); this.clear(false); @@ -46,6 +48,7 @@ export class TSDocConfiguration { this._validation.reportUnsupportedTags = false; this._validation.reportUnsupportedHtmlElements = false; this._supportedHtmlElements.clear(); + this._linkUrlPrefixes.clear(); if (!noStandardTags) { // Define all the standard tags @@ -89,6 +92,29 @@ export class TSDocConfiguration { return Array.from(this._supportedHtmlElements.values()); } + /** + * URI-style prefixes that should be treated as URL destinations in `{@link ...}` tags. + */ + public get linkUrlPrefixes(): string[] { + return Array.from(this._linkUrlPrefixes.values()); + } + + public addLinkUrlPrefix(prefix: string): void { + if (!/^[A-Za-z][A-Za-z0-9+.-]*$/.test(prefix)) { + throw new Error(`Invalid link URL prefix: "${prefix}"`); + } + + this._linkUrlPrefixes.add(prefix.toLowerCase()); + } + + public clearLinkUrlPrefixes(): void { + this._linkUrlPrefixes.clear(); + } + + public isLinkUrlPrefixAllowed(prefix: string): boolean { + return this._linkUrlPrefixes.has(prefix.toLowerCase()); + } + /** * Register custom DocNode subclasses. */ diff --git a/tsdoc/src/parser/NodeParser.ts b/tsdoc/src/parser/NodeParser.ts index 328553d3..c583a437 100644 --- a/tsdoc/src/parser/NodeParser.ts +++ b/tsdoc/src/parser/NodeParser.ts @@ -78,6 +78,20 @@ export class NodeParser { this._currentSection = parserContext.docComment.summarySection; } + private _isConfiguredLinkUrlDestination(linkDestination: string): boolean { + const colonIndex: number = linkDestination.indexOf(':'); + if (colonIndex <= 0) { + return false; + } + + if (linkDestination.indexOf('://') > 0) { + return false; + } + + const prefix: string = linkDestination.substring(0, colonIndex); + return this._configuration.isLinkUrlPrefixAllowed(prefix); + } + public parse(): void { const tokenReader: TokenReader = new TokenReader(this._parserContext); diff --git a/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts b/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts index e9b07c26..4b3cd710 100644 --- a/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts +++ b/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { TSDocConfiguration } from '../../configuration/TSDocConfiguration'; import { TestHelpers } from './TestHelpers'; test('00 Link text: positive examples', () => { @@ -54,6 +55,22 @@ test('03 URL destination: negative examples', () => { ); }); +test('03b URL destination: configured URI prefixes', () => { + const configuration: TSDocConfiguration = new TSDocConfiguration(); + configuration.addLinkUrlPrefix('mailto'); + configuration.addLinkUrlPrefix('xref'); + + TestHelpers.parseAndMatchNodeParserSnapshot( + [ + '/**', + ' * {@link mailto:bob@example.com}', + ' * {@link xref:MyNamespace.MySymbol|link text}', + ' */' + ].join('\n'), + configuration + ); +}); + test('04 Declaration reference with package name: positive examples', () => { TestHelpers.parseAndMatchNodeParserSnapshot( [