diff --git a/CHANGELOG.md b/CHANGELOG.md index 453aa1c72..2eebee73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Unreleased +### Features + +- Added `titleLink`, `navigationLinks` and `sidebarLinks` options to add additional links to the rendered output, #1830. +- Added `sourceLinkTemplate` option to allow more flexible specification of remote urls. + Deprecated now redundant `gitRevision` detection starting with `https?://` introduced in v0.23.16, #2068. + +### Thanks! + +- @futurGH + ## v0.23.16 (2022-10-10) ### Features diff --git a/src/lib/converter/plugins/SourcePlugin.ts b/src/lib/converter/plugins/SourcePlugin.ts index e6fe8aa20..dfaa7873e 100644 --- a/src/lib/converter/plugins/SourcePlugin.ts +++ b/src/lib/converter/plugins/SourcePlugin.ts @@ -25,6 +25,9 @@ export class SourcePlugin extends ConverterComponent { @BindOption("gitRemote") readonly gitRemote!: string; + @BindOption("sourceLinkTemplate") + readonly sourceLinkTemplate!: string; + @BindOption("basePath") readonly basePath!: string; @@ -141,12 +144,7 @@ export class SourcePlugin extends ConverterComponent { for (const source of refl.sources || []) { if (gitIsInstalled) { const repo = this.getRepository(source.fullFileName); - source.url = repo?.getURL(source.fullFileName); - if (source.url) { - source.url += `#${repo!.getLineNumberAnchor( - source.line - )}`; - } + source.url = repo?.getURL(source.fullFileName, source.line); } source.fileName = normalizePath( @@ -182,6 +180,7 @@ export class SourcePlugin extends ConverterComponent { // Try to create a new repository const repository = Repository.tryCreateRepository( dirName, + this.sourceLinkTemplate, this.gitRevision, this.gitRemote, this.application.logger diff --git a/src/lib/converter/utils/repository.ts b/src/lib/converter/utils/repository.ts index e74d585d7..de8d7e46d 100644 --- a/src/lib/converter/utils/repository.ts +++ b/src/lib/converter/utils/repository.ts @@ -28,25 +28,18 @@ export class Repository { */ files = new Set(); - /** - * The base url for link creation. - */ - baseUrl: string; - - /** - * The anchor prefix used to select lines, usually `L` - */ - anchorPrefix: string; + urlTemplate: string; + gitRevision: string; /** * Create a new Repository instance. * * @param path The root path of the repository. */ - constructor(path: string, baseUrl: string) { + constructor(path: string, gitRevision: string, urlTemplate: string) { this.path = path; - this.baseUrl = baseUrl; - this.anchorPrefix = guessAnchorPrefix(this.baseUrl); + this.gitRevision = gitRevision; + this.urlTemplate = urlTemplate; const out = git("-C", path, "ls-files"); if (out.status === 0) { @@ -64,16 +57,21 @@ export class Repository { * @param fileName The file whose URL should be determined. * @returns A URL pointing to the web preview of the given file or undefined. */ - getURL(fileName: string): string | undefined { + getURL(fileName: string, line: number): string | undefined { if (!this.files.has(fileName)) { return; } - return `${this.baseUrl}/${fileName.substring(this.path.length + 1)}`; - } + const replacements = { + gitRevision: this.gitRevision, + path: fileName.substring(this.path.length + 1), + line, + }; - getLineNumberAnchor(lineNumber: number): string { - return `${this.anchorPrefix}${lineNumber}`; + return this.urlTemplate.replace( + /\{(gitRevision|path|line)\}/g, + (_, key) => replacements[key as never] + ); } /** @@ -87,6 +85,7 @@ export class Repository { */ static tryCreateRepository( path: string, + sourceLinkTemplate: string, gitRevision: string, gitRemote: string, logger: Logger @@ -103,14 +102,18 @@ export class Repository { ).stdout.trim(); if (!gitRevision) return; // Will only happen in a repo with no commits. - let baseUrl: string | undefined; - if (/^https?:\/\//.test(gitRemote)) { - baseUrl = `${gitRemote}/${gitRevision}`; + let urlTemplate: string | undefined; + if (sourceLinkTemplate) { + urlTemplate = sourceLinkTemplate; + } else if (/^https?:\/\//.test(gitRemote)) { + logger.warn( + "Using a link as the gitRemote is deprecated and will be removed in 0.24." + ); + urlTemplate = `${gitRemote}/{gitRevision}`; } else { const remotesOut = git("-C", path, "remote", "get-url", gitRemote); if (remotesOut.status === 0) { - baseUrl = guessBaseUrl( - gitRevision, + urlTemplate = guessSourceUrlTemplate( remotesOut.stdout.split("\n") ); } else { @@ -120,11 +123,12 @@ export class Repository { } } - if (!baseUrl) return; + if (!urlTemplate) return; return new Repository( BasePath.normalize(topLevel.stdout.replace("\n", "")), - baseUrl + gitRevision, + urlTemplate ); } } @@ -142,10 +146,7 @@ const repoExpressions = [ /(gitlab.com)[:/]([^/]+)\/(.*)/, ]; -export function guessBaseUrl( - gitRevision: string, - remotes: string[] -): string | undefined { +export function guessSourceUrlTemplate(remotes: string[]): string | undefined { let hostname = ""; let user = ""; let project = ""; @@ -168,19 +169,13 @@ export function guessBaseUrl( } let sourcePath = "blob"; + let anchorPrefix = "L"; if (hostname.includes("gitlab")) { sourcePath = "-/blob"; } else if (hostname.includes("bitbucket")) { sourcePath = "src"; + anchorPrefix = "lines-"; } - return `https://${hostname}/${user}/${project}/${sourcePath}/${gitRevision}`; -} - -function guessAnchorPrefix(url: string) { - if (url.includes("bitbucket")) { - return "lines-"; - } - - return "L"; + return `https://${hostname}/${user}/${project}/${sourcePath}/{gitRevision}/{path}#${anchorPrefix}{line}`; } diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index ead4959c9..e0965ca57 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -111,6 +111,7 @@ export interface TypeDocOptionMap { excludeTags: `@${string}`[]; readme: string; cname: string; + sourceLinkTemplate: string; gitRevision: string; gitRemote: string; htmlLang: string; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index e1015481f..0661cd08e 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -266,6 +266,10 @@ export function addTypeDocOptions(options: Pick) { name: "cname", help: "Set the CNAME file text, it's useful for custom domains on GitHub Pages.", }); + options.addDeclaration({ + name: "sourceLinkTemplate", + help: "Specify a link template to be used when generating source urls. If not set, will be automatically created using the git remote. Supports {path}, {line}, {gitRevision} placeholders.", + }); options.addDeclaration({ name: "gitRevision", help: "Use specified revision instead of the last revision for linking to GitHub/Bitbucket source files.", diff --git a/src/test/Repository.test.ts b/src/test/Repository.test.ts index 403ff4b63..95e9f49ee 100644 --- a/src/test/Repository.test.ts +++ b/src/test/Repository.test.ts @@ -1,14 +1,14 @@ -import { guessBaseUrl } from "../lib/converter/utils/repository"; +import { guessSourceUrlTemplate } from "../lib/converter/utils/repository"; import { strictEqual as equal } from "assert"; describe("Repository", function () { - describe("guessBaseUrl helper", () => { + describe("guessSourceUrlTemplate helper", () => { it("handles a personal GitHub HTTPS URL", () => { const mockRemotes = ["https://github.com/joebloggs/foobar.git"]; equal( - guessBaseUrl("rev", mockRemotes), - "https://github.com/joebloggs/foobar/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://github.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}" ); }); @@ -16,16 +16,16 @@ describe("Repository", function () { const mockRemotes = ["git@github.com:TypeStrong/typedoc.git"]; equal( - guessBaseUrl("rev", mockRemotes), - "https://github.com/TypeStrong/typedoc/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://github.com/TypeStrong/typedoc/blob/{gitRevision}/{path}#L{line}" ); }); it("handles an enterprise GitHub URL", () => { const mockRemotes = ["git@github.acme.com:joebloggs/foobar.git"]; equal( - guessBaseUrl("rev", mockRemotes), - "https://github.acme.com/joebloggs/foobar/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://github.acme.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}" ); }); @@ -34,8 +34,8 @@ describe("Repository", function () { "ssh://org@bigcompany.githubprivate.com/joebloggs/foobar.git", ]; equal( - guessBaseUrl("rev", mockRemotes), - "https://bigcompany.githubprivate.com/joebloggs/foobar/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://bigcompany.githubprivate.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}" ); }); @@ -44,8 +44,8 @@ describe("Repository", function () { "ssh://org@bigcompany.ghe.com/joebloggs/foobar.git", ]; equal( - guessBaseUrl("rev", mockRemotes), - "https://bigcompany.ghe.com/joebloggs/foobar/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://bigcompany.ghe.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}" ); }); @@ -55,8 +55,8 @@ describe("Repository", function () { ]; equal( - guessBaseUrl("rev", mockRemotes), - "https://bigcompany.github.us/joebloggs/foobar/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://bigcompany.github.us/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}" ); }); @@ -65,38 +65,38 @@ describe("Repository", function () { "https://joebloggs@bitbucket.org/joebloggs/foobar.git", ]; equal( - guessBaseUrl("rev", mockRemotes), - "https://bitbucket.org/joebloggs/foobar/src/rev" + guessSourceUrlTemplate(mockRemotes), + "https://bitbucket.org/joebloggs/foobar/src/{gitRevision}/{path}#lines-{line}" ); }); it("handles a bitbucket SSH URL", () => { const mockRemotes = ["git@bitbucket.org:joebloggs/foobar.git"]; equal( - guessBaseUrl("rev", mockRemotes), - "https://bitbucket.org/joebloggs/foobar/src/rev" + guessSourceUrlTemplate(mockRemotes), + "https://bitbucket.org/joebloggs/foobar/src/{gitRevision}/{path}#lines-{line}" ); }); it("handles a GitLab URL", () => { const mockRemotes = ["https://gitlab.com/joebloggs/foobar.git"]; equal( - guessBaseUrl("rev", mockRemotes), - "https://gitlab.com/joebloggs/foobar/-/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://gitlab.com/joebloggs/foobar/-/blob/{gitRevision}/{path}#L{line}" ); }); it("handles a GitLab SSH URL", () => { const mockRemotes = ["git@gitlab.com:joebloggs/foobar.git"]; equal( - guessBaseUrl("rev", mockRemotes), - "https://gitlab.com/joebloggs/foobar/-/blob/rev" + guessSourceUrlTemplate(mockRemotes), + "https://gitlab.com/joebloggs/foobar/-/blob/{gitRevision}/{path}#L{line}" ); }); it("Gracefully handles unknown urls", () => { const mockRemotes = ["git@example.com"]; - equal(guessBaseUrl("rev", mockRemotes), undefined); + equal(guessSourceUrlTemplate(mockRemotes), undefined); }); }); });