diff --git a/CHANGELOG.md b/CHANGELOG.md index fc791ace7..36d12cb9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - Flag option types like `validation` can now be set to true/false to enable/disable all flags within them. +- Source code links now work with Bitbucket repositories. - Added `githubPages` option (default: true), which will create a `.nojekyll` page in the generated output. ### Thanks! diff --git a/examples/self/run b/examples/self/run index d65527d1e..17e47d93d 100755 --- a/examples/self/run +++ b/examples/self/run @@ -1,3 +1,3 @@ #!/bin/sh cd ${0%/*} -node ../../bin/typedoc --mode file --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc/ ../../src/lib/ +node ../../bin/typedoc --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc/ --entryPointStrategy expand ../../src/lib/ diff --git a/examples/self/run.bat b/examples/self/run.bat index deeb24368..5f084ae11 100644 --- a/examples/self/run.bat +++ b/examples/self/run.bat @@ -2,6 +2,6 @@ set curr_dir=%cd% chdir /D "%~dp0" -node ..\..\bin\typedoc --mode file --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc\ ..\..\src\lib\ +node ..\..\bin\typedoc --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc\ --entryPointStrategy expand ..\..\src\lib\ chdir /D "%curr_dir%" diff --git a/src/lib/converter/plugins/GitHubPlugin.ts b/src/lib/converter/plugins/GitHubPlugin.ts index 0740a67be..ba598a5da 100644 --- a/src/lib/converter/plugins/GitHubPlugin.ts +++ b/src/lib/converter/plugins/GitHubPlugin.ts @@ -7,6 +7,7 @@ import { BasePath } from "../utils/base-path"; import { Converter } from "../converter"; import type { Context } from "../context"; import { BindOption } from "../../utils"; +import { RepositoryType } from "../../models"; function git(...args: string[]) { return spawnSync("git", args, { encoding: "utf-8", windowsHide: true }); @@ -34,22 +35,27 @@ export class Repository { /** * The user/organization name of this repository on GitHub. */ - gitHubUser?: string; + user?: string; /** * The project name of this repository on GitHub. */ - gitHubProject?: string; + project?: string; /** - * The hostname for this github project. + * The hostname for this GitHub or Bitbucket project. * * Defaults to: `github.com` (for normal, public GitHub instance projects) * * Or the hostname for an enterprise version of GitHub, e.g. `github.acme.com` * (if found as a match in the list of git remotes). */ - gitHubHostname = "github.com"; + hostname = "github.com"; + + /** + * Whether this is a GitHub or Bitbucket repository. + */ + type: RepositoryType = RepositoryType.GitHub; /** * Create a new Repository instance. @@ -61,25 +67,33 @@ export class Repository { this.branch = gitRevision || "master"; for (let i = 0, c = repoLinks.length; i < c; i++) { - const url = + let match = /(github(?:\.[a-z]+)*\.[a-z]{2,})[:/]([^/]+)\/(.*)/.exec( repoLinks[i] ); - if (url) { - this.gitHubHostname = url[1]; - this.gitHubUser = url[2]; - this.gitHubProject = url[3]; - if (this.gitHubProject.substr(-4) === ".git") { - this.gitHubProject = this.gitHubProject.substr( + if (!match) { + match = /(bitbucket.org)[:/]([^/]+)\/(.*)/.exec(repoLinks[i]); + } + + if (match) { + this.hostname = match[1]; + this.user = match[2]; + this.project = match[3]; + if (this.project.substr(-4) === ".git") { + this.project = this.project.substr( 0, - this.gitHubProject.length - 4 + this.project.length - 4 ); } break; } } + if (this.hostname.includes("bitbucket.org")) + this.type = RepositoryType.Bitbucket; + else this.type = RepositoryType.GitHub; + let out = git("-C", path, "ls-files"); if (out.status === 0) { out.stdout.split("\n").forEach((file) => { @@ -108,25 +122,21 @@ export class Repository { } /** - * Get the URL of the given file on GitHub. + * Get the URL of the given file on GitHub or Bitbucket. * - * @param fileName The file whose GitHub URL should be determined. - * @returns An url pointing to the web preview of the given file or NULL. + * @param fileName The file whose URL should be determined. + * @returns A URL pointing to the web preview of the given file or undefined. */ - getGitHubURL(fileName: string): string | undefined { - if ( - !this.gitHubUser || - !this.gitHubProject || - !this.contains(fileName) - ) { + getURL(fileName: string): string | undefined { + if (!this.user || !this.project || !this.contains(fileName)) { return; } return [ - `https://${this.gitHubHostname}`, - this.gitHubUser, - this.gitHubProject, - "blob", + `https://${this.hostname}`, + this.user, + this.project, + this.type === "github" ? "blob" : "src", this.branch, fileName.substr(this.path.length + 1), ].join("/"); @@ -159,6 +169,19 @@ export class Repository { remotesOutput.stdout.split("\n") ); } + + static getLineNumberAnchor( + lineNumber: number, + repositoryType: RepositoryType | undefined + ): string { + switch (repositoryType) { + default: + case RepositoryType.GitHub: + return "L" + lineNumber; + case RepositoryType.Bitbucket: + return "lines-" + lineNumber; + } + } } /** @@ -248,9 +271,8 @@ export class GitHubPlugin extends ConverterComponent { project.files.forEach((sourceFile) => { const repository = this.getRepository(sourceFile.fullFileName); if (repository) { - sourceFile.url = repository.getGitHubURL( - sourceFile.fullFileName - ); + sourceFile.url = repository.getURL(sourceFile.fullFileName); + sourceFile.repositoryType = repository.type; } }); @@ -259,7 +281,13 @@ export class GitHubPlugin extends ConverterComponent { if (reflection.sources) { reflection.sources.forEach((source: SourceReference) => { if (source.file && source.file.url) { - source.url = source.file.url + "#L" + source.line; + source.url = + source.file.url + + "#" + + Repository.getLineNumberAnchor( + source.line, + source.file.repositoryType + ); } }); } diff --git a/src/lib/models/sources/file.ts b/src/lib/models/sources/file.ts index 5d4d85b12..32a94d214 100644 --- a/src/lib/models/sources/file.ts +++ b/src/lib/models/sources/file.ts @@ -3,6 +3,7 @@ import * as Path from "path"; import type { Reflection } from "../reflections/abstract"; import type { ReflectionGroup } from "../ReflectionGroup"; import type { SourceDirectory } from "./directory"; +import type { RepositoryType } from "./repository"; /** * Represents references of reflections to their defining source files. @@ -61,10 +62,15 @@ export class SourceFile { name: string; /** - * A url pointing to a page displaying the contents of this file. + * A URL pointing to a page displaying the contents of this file. */ url?: string; + /** + * The type of repository where this file is hosted. + */ + repositoryType?: RepositoryType; + /** * The representation of the parent directory of this source file. */ diff --git a/src/lib/models/sources/index.ts b/src/lib/models/sources/index.ts index 09cecc574..e09c10a40 100644 --- a/src/lib/models/sources/index.ts +++ b/src/lib/models/sources/index.ts @@ -1,3 +1,4 @@ export { SourceDirectory } from "./directory"; export { SourceFile } from "./file"; export type { SourceReference } from "./file"; +export { RepositoryType } from "./repository"; diff --git a/src/lib/models/sources/repository.ts b/src/lib/models/sources/repository.ts new file mode 100644 index 000000000..b6d3837a6 --- /dev/null +++ b/src/lib/models/sources/repository.ts @@ -0,0 +1,4 @@ +export enum RepositoryType { + GitHub = "github", + Bitbucket = "bitbucket", +} diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 6c9bd135d..612982fbd 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -232,11 +232,11 @@ export function addTypeDocOptions(options: Pick) { }); options.addDeclaration({ name: "gitRevision", - help: "Use specified revision instead of the last revision for linking to GitHub source files.", + help: "Use specified revision instead of the last revision for linking to GitHub/Bitbucket source files.", }); options.addDeclaration({ name: "gitRemote", - help: "Use the specified remote for linking to GitHub source files.", + help: "Use the specified remote for linking to GitHub/Bitbucket source files.", defaultValue: "origin", }); options.addDeclaration({ diff --git a/src/test/converter3/plugins/GitHubPlugin.test.ts b/src/test/converter3/plugins/GitHubPlugin.test.ts new file mode 100644 index 000000000..f182cd3a6 --- /dev/null +++ b/src/test/converter3/plugins/GitHubPlugin.test.ts @@ -0,0 +1,99 @@ +import * as github from "../../../lib/converter/plugins/GitHubPlugin"; +import { RepositoryType } from "../../../lib/models"; +import Assert = require("assert"); + +describe("Repository", function () { + describe("constructor", function () { + it("defaults to github.com hostname", function () { + const repository = new github.Repository("", "", []); + + Assert.equal(repository.hostname, "github.com"); + Assert.equal(repository.type, RepositoryType.GitHub); + }); + + it("handles a personal GitHub HTTPS URL", function () { + const mockRemotes = ["https://github.com/joebloggs/foobar.git"]; + + const repository = new github.Repository("", "", mockRemotes); + + Assert.equal(repository.hostname, "github.com"); + Assert.equal(repository.user, "joebloggs"); + Assert.equal(repository.project, "foobar"); + Assert.equal(repository.type, RepositoryType.GitHub); + }); + + it("handles an enterprise GitHub URL", function () { + const mockRemotes = ["git@github.acme.com:joebloggs/foobar.git"]; + + const repository = new github.Repository("", "", mockRemotes); + + Assert.equal(repository.hostname, "github.acme.com"); + Assert.equal(repository.user, "joebloggs"); + Assert.equal(repository.project, "foobar"); + Assert.equal(repository.type, RepositoryType.GitHub); + }); + + it("handles a Bitbucket HTTPS URL", function () { + const mockRemotes = [ + "https://joebloggs@bitbucket.org/joebloggs/foobar.git", + ]; + + const repository = new github.Repository("", "", mockRemotes); + + Assert.equal(repository.hostname, "bitbucket.org"); + Assert.equal(repository.user, "joebloggs"); + Assert.equal(repository.project, "foobar"); + Assert.equal(repository.type, RepositoryType.Bitbucket); + }); + + it("handles a Bitbucket SSH URL", function () { + const mockRemotes = ["git@bitbucket.org:joebloggs/foobar.git"]; + + const repository = new github.Repository("", "", mockRemotes); + + Assert.equal(repository.hostname, "bitbucket.org"); + Assert.equal(repository.user, "joebloggs"); + Assert.equal(repository.project, "foobar"); + Assert.equal(repository.type, RepositoryType.Bitbucket); + }); + }); + + describe("getURL", () => { + const repositoryPath = "C:/Projects/foobar"; + const filePath = repositoryPath + "/src/index.ts"; + + it("returns a GitHub URL", function () { + const mockRemotes = ["https://github.com/joebloggs/foobar.git"]; + + const repository = new github.Repository( + repositoryPath, + "main", + mockRemotes + ); + repository.files = [filePath]; + + Assert.equal( + repository.getURL(filePath), + "https://github.com/joebloggs/foobar/blob/main/src/index.ts" + ); + }); + + it("returns a Bitbucket URL", function () { + const mockRemotes = [ + "https://joebloggs@bitbucket.org/joebloggs/foobar.git", + ]; + + const repository = new github.Repository( + repositoryPath, + "main", + mockRemotes + ); + repository.files = [filePath]; + + Assert.equal( + repository.getURL(filePath), + "https://bitbucket.org/joebloggs/foobar/src/main/src/index.ts" + ); + }); + }); +}); diff --git a/src/test/githubplugin.test.ts b/src/test/githubplugin.test.ts deleted file mode 100644 index 01bcac25d..000000000 --- a/src/test/githubplugin.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as github from "../lib/converter/plugins/GitHubPlugin"; -import Assert = require("assert"); - -describe("GitHubRepository", function () { - describe("constructor", function () { - it("must default to github.com hostname", function () { - const repository = new github.Repository("", "", []); - - Assert.equal(repository.gitHubHostname, "github.com"); - }); - - it("must correctly handle an enterprise github URL hostname", function () { - const mockRemotes = ["git@github.acme.com:joebloggs/foobar.git"]; - - const repository = new github.Repository("", "", mockRemotes); - - Assert.equal(repository.gitHubHostname, "github.acme.com"); - }); - }); -});