Skip to content

Commit 4c81b3d

Browse files
committed
Add sourceLinkTemplate option
1 parent 2eaa476 commit 4c81b3d

File tree

6 files changed

+75
-66
lines changed

6 files changed

+75
-66
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Unreleased
22

3+
### Features
4+
5+
- Added `titleLink`, `navigationLinks` and `sidebarLinks` options to add additional links to the rendered output, #1830.
6+
- Added `sourceLinkTemplate` option to allow more flexible specification of remote urls.
7+
Deprecated now redundant `gitRevision` detection starting with `https?://` introduced in v0.23.16, #2068.
8+
9+
### Thanks!
10+
11+
- @futurGH
12+
313
## v0.23.16 (2022-10-10)
414

515
### Features

src/lib/converter/plugins/SourcePlugin.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export class SourcePlugin extends ConverterComponent {
2525
@BindOption("gitRemote")
2626
readonly gitRemote!: string;
2727

28+
@BindOption("sourceLinkTemplate")
29+
readonly sourceLinkTemplate!: string;
30+
2831
@BindOption("basePath")
2932
readonly basePath!: string;
3033

@@ -141,12 +144,7 @@ export class SourcePlugin extends ConverterComponent {
141144
for (const source of refl.sources || []) {
142145
if (gitIsInstalled) {
143146
const repo = this.getRepository(source.fullFileName);
144-
source.url = repo?.getURL(source.fullFileName);
145-
if (source.url) {
146-
source.url += `#${repo!.getLineNumberAnchor(
147-
source.line
148-
)}`;
149-
}
147+
source.url = repo?.getURL(source.fullFileName, source.line);
150148
}
151149

152150
source.fileName = normalizePath(
@@ -182,6 +180,7 @@ export class SourcePlugin extends ConverterComponent {
182180
// Try to create a new repository
183181
const repository = Repository.tryCreateRepository(
184182
dirName,
183+
this.sourceLinkTemplate,
185184
this.gitRevision,
186185
this.gitRemote,
187186
this.application.logger

src/lib/converter/utils/repository.ts

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,18 @@ export class Repository {
2828
*/
2929
files = new Set<string>();
3030

31-
/**
32-
* The base url for link creation.
33-
*/
34-
baseUrl: string;
35-
36-
/**
37-
* The anchor prefix used to select lines, usually `L`
38-
*/
39-
anchorPrefix: string;
31+
urlTemplate: string;
32+
gitRevision: string;
4033

4134
/**
4235
* Create a new Repository instance.
4336
*
4437
* @param path The root path of the repository.
4538
*/
46-
constructor(path: string, baseUrl: string) {
39+
constructor(path: string, gitRevision: string, urlTemplate: string) {
4740
this.path = path;
48-
this.baseUrl = baseUrl;
49-
this.anchorPrefix = guessAnchorPrefix(this.baseUrl);
41+
this.gitRevision = gitRevision;
42+
this.urlTemplate = urlTemplate;
5043

5144
const out = git("-C", path, "ls-files");
5245
if (out.status === 0) {
@@ -64,16 +57,21 @@ export class Repository {
6457
* @param fileName The file whose URL should be determined.
6558
* @returns A URL pointing to the web preview of the given file or undefined.
6659
*/
67-
getURL(fileName: string): string | undefined {
60+
getURL(fileName: string, line: number): string | undefined {
6861
if (!this.files.has(fileName)) {
6962
return;
7063
}
7164

72-
return `${this.baseUrl}/${fileName.substring(this.path.length + 1)}`;
73-
}
65+
const replacements = {
66+
gitRevision: this.gitRevision,
67+
path: fileName.substring(this.path.length + 1),
68+
line,
69+
};
7470

75-
getLineNumberAnchor(lineNumber: number): string {
76-
return `${this.anchorPrefix}${lineNumber}`;
71+
return this.urlTemplate.replace(
72+
/\{(gitRevision|path|line)\}/g,
73+
(_, key) => replacements[key as never]
74+
);
7775
}
7876

7977
/**
@@ -87,6 +85,7 @@ export class Repository {
8785
*/
8886
static tryCreateRepository(
8987
path: string,
88+
sourceLinkTemplate: string,
9089
gitRevision: string,
9190
gitRemote: string,
9291
logger: Logger
@@ -103,14 +102,18 @@ export class Repository {
103102
).stdout.trim();
104103
if (!gitRevision) return; // Will only happen in a repo with no commits.
105104

106-
let baseUrl: string | undefined;
107-
if (/^https?:\/\//.test(gitRemote)) {
108-
baseUrl = `${gitRemote}/${gitRevision}`;
105+
let urlTemplate: string | undefined;
106+
if (sourceLinkTemplate) {
107+
urlTemplate = sourceLinkTemplate;
108+
} else if (/^https?:\/\//.test(gitRemote)) {
109+
logger.warn(
110+
"Using a link as the gitRemote is deprecated and will be removed in 0.24."
111+
);
112+
urlTemplate = `${gitRemote}/{gitRevision}`;
109113
} else {
110114
const remotesOut = git("-C", path, "remote", "get-url", gitRemote);
111115
if (remotesOut.status === 0) {
112-
baseUrl = guessBaseUrl(
113-
gitRevision,
116+
urlTemplate = guessSourceUrlTemplate(
114117
remotesOut.stdout.split("\n")
115118
);
116119
} else {
@@ -120,11 +123,12 @@ export class Repository {
120123
}
121124
}
122125

123-
if (!baseUrl) return;
126+
if (!urlTemplate) return;
124127

125128
return new Repository(
126129
BasePath.normalize(topLevel.stdout.replace("\n", "")),
127-
baseUrl
130+
gitRevision,
131+
urlTemplate
128132
);
129133
}
130134
}
@@ -142,10 +146,7 @@ const repoExpressions = [
142146
/(gitlab.com)[:/]([^/]+)\/(.*)/,
143147
];
144148

145-
export function guessBaseUrl(
146-
gitRevision: string,
147-
remotes: string[]
148-
): string | undefined {
149+
export function guessSourceUrlTemplate(remotes: string[]): string | undefined {
149150
let hostname = "";
150151
let user = "";
151152
let project = "";
@@ -168,19 +169,13 @@ export function guessBaseUrl(
168169
}
169170

170171
let sourcePath = "blob";
172+
let anchorPrefix = "L";
171173
if (hostname.includes("gitlab")) {
172174
sourcePath = "-/blob";
173175
} else if (hostname.includes("bitbucket")) {
174176
sourcePath = "src";
177+
anchorPrefix = "lines-";
175178
}
176179

177-
return `https://${hostname}/${user}/${project}/${sourcePath}/${gitRevision}`;
178-
}
179-
180-
function guessAnchorPrefix(url: string) {
181-
if (url.includes("bitbucket")) {
182-
return "lines-";
183-
}
184-
185-
return "L";
180+
return `https://${hostname}/${user}/${project}/${sourcePath}/{gitRevision}/{path}#${anchorPrefix}{line}`;
186181
}

src/lib/utils/options/declaration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export interface TypeDocOptionMap {
111111
excludeTags: `@${string}`[];
112112
readme: string;
113113
cname: string;
114+
sourceLinkTemplate: string;
114115
gitRevision: string;
115116
gitRemote: string;
116117
htmlLang: string;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
266266
name: "cname",
267267
help: "Set the CNAME file text, it's useful for custom domains on GitHub Pages.",
268268
});
269+
options.addDeclaration({
270+
name: "sourceLinkTemplate",
271+
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.",
272+
});
269273
options.addDeclaration({
270274
name: "gitRevision",
271275
help: "Use specified revision instead of the last revision for linking to GitHub/Bitbucket source files.",

src/test/Repository.test.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
1-
import { guessBaseUrl } from "../lib/converter/utils/repository";
1+
import { guessSourceUrlTemplate } from "../lib/converter/utils/repository";
22
import { strictEqual as equal } from "assert";
33

44
describe("Repository", function () {
5-
describe("guessBaseUrl helper", () => {
5+
describe("guessSourceUrlTemplate helper", () => {
66
it("handles a personal GitHub HTTPS URL", () => {
77
const mockRemotes = ["https://github.com/joebloggs/foobar.git"];
88

99
equal(
10-
guessBaseUrl("rev", mockRemotes),
11-
"https://github.com/joebloggs/foobar/blob/rev"
10+
guessSourceUrlTemplate(mockRemotes),
11+
"https://github.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}"
1212
);
1313
});
1414

1515
it("handles a personal GitHub SSH URL", () => {
1616
const mockRemotes = ["[email protected]:TypeStrong/typedoc.git"];
1717

1818
equal(
19-
guessBaseUrl("rev", mockRemotes),
20-
"https://github.com/TypeStrong/typedoc/blob/rev"
19+
guessSourceUrlTemplate(mockRemotes),
20+
"https://github.com/TypeStrong/typedoc/blob/{gitRevision}/{path}#L{line}"
2121
);
2222
});
2323

2424
it("handles an enterprise GitHub URL", () => {
2525
const mockRemotes = ["[email protected]:joebloggs/foobar.git"];
2626
equal(
27-
guessBaseUrl("rev", mockRemotes),
28-
"https://github.acme.com/joebloggs/foobar/blob/rev"
27+
guessSourceUrlTemplate(mockRemotes),
28+
"https://github.acme.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}"
2929
);
3030
});
3131

@@ -34,8 +34,8 @@ describe("Repository", function () {
3434
"ssh://[email protected]/joebloggs/foobar.git",
3535
];
3636
equal(
37-
guessBaseUrl("rev", mockRemotes),
38-
"https://bigcompany.githubprivate.com/joebloggs/foobar/blob/rev"
37+
guessSourceUrlTemplate(mockRemotes),
38+
"https://bigcompany.githubprivate.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}"
3939
);
4040
});
4141

@@ -44,8 +44,8 @@ describe("Repository", function () {
4444
"ssh://[email protected]/joebloggs/foobar.git",
4545
];
4646
equal(
47-
guessBaseUrl("rev", mockRemotes),
48-
"https://bigcompany.ghe.com/joebloggs/foobar/blob/rev"
47+
guessSourceUrlTemplate(mockRemotes),
48+
"https://bigcompany.ghe.com/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}"
4949
);
5050
});
5151

@@ -55,8 +55,8 @@ describe("Repository", function () {
5555
];
5656

5757
equal(
58-
guessBaseUrl("rev", mockRemotes),
59-
"https://bigcompany.github.us/joebloggs/foobar/blob/rev"
58+
guessSourceUrlTemplate(mockRemotes),
59+
"https://bigcompany.github.us/joebloggs/foobar/blob/{gitRevision}/{path}#L{line}"
6060
);
6161
});
6262

@@ -65,38 +65,38 @@ describe("Repository", function () {
6565
"https://[email protected]/joebloggs/foobar.git",
6666
];
6767
equal(
68-
guessBaseUrl("rev", mockRemotes),
69-
"https://bitbucket.org/joebloggs/foobar/src/rev"
68+
guessSourceUrlTemplate(mockRemotes),
69+
"https://bitbucket.org/joebloggs/foobar/src/{gitRevision}/{path}#lines-{line}"
7070
);
7171
});
7272

7373
it("handles a bitbucket SSH URL", () => {
7474
const mockRemotes = ["[email protected]:joebloggs/foobar.git"];
7575
equal(
76-
guessBaseUrl("rev", mockRemotes),
77-
"https://bitbucket.org/joebloggs/foobar/src/rev"
76+
guessSourceUrlTemplate(mockRemotes),
77+
"https://bitbucket.org/joebloggs/foobar/src/{gitRevision}/{path}#lines-{line}"
7878
);
7979
});
8080

8181
it("handles a GitLab URL", () => {
8282
const mockRemotes = ["https://gitlab.com/joebloggs/foobar.git"];
8383
equal(
84-
guessBaseUrl("rev", mockRemotes),
85-
"https://gitlab.com/joebloggs/foobar/-/blob/rev"
84+
guessSourceUrlTemplate(mockRemotes),
85+
"https://gitlab.com/joebloggs/foobar/-/blob/{gitRevision}/{path}#L{line}"
8686
);
8787
});
8888

8989
it("handles a GitLab SSH URL", () => {
9090
const mockRemotes = ["[email protected]:joebloggs/foobar.git"];
9191
equal(
92-
guessBaseUrl("rev", mockRemotes),
93-
"https://gitlab.com/joebloggs/foobar/-/blob/rev"
92+
guessSourceUrlTemplate(mockRemotes),
93+
"https://gitlab.com/joebloggs/foobar/-/blob/{gitRevision}/{path}#L{line}"
9494
);
9595
});
9696

9797
it("Gracefully handles unknown urls", () => {
9898
const mockRemotes = ["[email protected]"];
99-
equal(guessBaseUrl("rev", mockRemotes), undefined);
99+
equal(guessSourceUrlTemplate(mockRemotes), undefined);
100100
});
101101
});
102102
});

0 commit comments

Comments
 (0)