@scm-manager/ui-components 3.11.6 → 3.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -12
- package/src/__snapshots__/storyshots.test.ts.snap +2 -3
- package/src/markdown/LazyMarkdownView.tsx +1 -1
- package/src/markdown/MarkdownImageRenderer.test.ts +20 -23
- package/src/markdown/MarkdownImageRenderer.tsx +11 -40
- package/src/markdown/MarkdownLinkRenderer.test.tsx +27 -100
- package/src/markdown/MarkdownLinkRenderer.tsx +16 -25
- package/src/markdown/paths.test.ts +143 -0
- package/src/markdown/paths.ts +29 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scm-manager/ui-components",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.7",
|
|
4
4
|
"description": "UI Components for SCM-Manager and its plugins",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"files": [
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"react-query": "^3.39.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@scm-manager/ui-tests": "3.11.
|
|
36
|
-
"@scm-manager/ui-types": "3.11.
|
|
35
|
+
"@scm-manager/ui-tests": "3.11.7",
|
|
36
|
+
"@scm-manager/ui-types": "3.11.7",
|
|
37
37
|
"@types/fetch-mock": "^7.3.1",
|
|
38
38
|
"@types/react-select": "^2.0.19",
|
|
39
39
|
"@types/unist": "^2.0.3",
|
|
@@ -68,17 +68,17 @@
|
|
|
68
68
|
"@scm-manager/jest-preset": "^2.14.1",
|
|
69
69
|
"@scm-manager/prettier-config": "^2.12.0",
|
|
70
70
|
"@scm-manager/tsconfig": "^2.13.0",
|
|
71
|
-
"@scm-manager/ui-syntaxhighlighting": "3.11.
|
|
72
|
-
"@scm-manager/ui-shortcuts": "3.11.
|
|
73
|
-
"@scm-manager/ui-text": "3.11.
|
|
71
|
+
"@scm-manager/ui-syntaxhighlighting": "3.11.7",
|
|
72
|
+
"@scm-manager/ui-shortcuts": "3.11.7",
|
|
73
|
+
"@scm-manager/ui-text": "3.11.7"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@scm-manager/ui-core": "3.11.
|
|
77
|
-
"@scm-manager/ui-overlays": "3.11.
|
|
78
|
-
"@scm-manager/ui-layout": "3.11.
|
|
79
|
-
"@scm-manager/ui-buttons": "3.11.
|
|
80
|
-
"@scm-manager/ui-api": "3.11.
|
|
81
|
-
"@scm-manager/ui-extensions": "3.11.
|
|
76
|
+
"@scm-manager/ui-core": "3.11.7",
|
|
77
|
+
"@scm-manager/ui-overlays": "3.11.7",
|
|
78
|
+
"@scm-manager/ui-layout": "3.11.7",
|
|
79
|
+
"@scm-manager/ui-buttons": "3.11.7",
|
|
80
|
+
"@scm-manager/ui-api": "3.11.7",
|
|
81
|
+
"@scm-manager/ui-extensions": "3.11.7",
|
|
82
82
|
"deepmerge": "^4.2.2",
|
|
83
83
|
"hast-util-sanitize": "^3.0.2",
|
|
84
84
|
"react-diff-view": "^2.4.10",
|
|
@@ -14392,7 +14392,7 @@ the story is mostly for checking if the src links are rendered correct.
|
|
|
14392
14392
|
<p>
|
|
14393
14393
|
<img
|
|
14394
14394
|
alt="path starting with a '.'"
|
|
14395
|
-
src="https://my.scm/scm/api/v2/some/repository/content/42
|
|
14395
|
+
src="https://my.scm/scm/api/v2/some/repository/content/42/some_image.jpg"
|
|
14396
14396
|
/>
|
|
14397
14397
|
</p>
|
|
14398
14398
|
|
|
@@ -14868,8 +14868,7 @@ the story is mostly for checking if the links are rendered correct.
|
|
|
14868
14868
|
<p>
|
|
14869
14869
|
Internal links should be rendered by react-router:
|
|
14870
14870
|
<a
|
|
14871
|
-
href="/
|
|
14872
|
-
onClick={[Function]}
|
|
14871
|
+
href="/buttons"
|
|
14873
14872
|
>
|
|
14874
14873
|
internal link
|
|
14875
14874
|
</a>
|
|
@@ -171,7 +171,7 @@ class LazyMarkdownView extends React.Component<Props, State> {
|
|
|
171
171
|
remarkRendererList.heading = createMarkdownHeadingRenderer(permalink);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
remarkRendererList.image = createMarkdownImageRenderer(basePath);
|
|
174
|
+
remarkRendererList.image = createMarkdownImageRenderer(basePath, permalink);
|
|
175
175
|
|
|
176
176
|
let protocolLinkRendererExtensions: ProtocolLinkRendererExtensionMap = {};
|
|
177
177
|
if (!remarkRendererList.link) {
|
|
@@ -14,36 +14,33 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React from "react";
|
|
18
17
|
import { createLocalLink } from "./MarkdownImageRenderer";
|
|
19
18
|
|
|
20
|
-
describe("createLocalLink tests", () => {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const currentPath =
|
|
25
|
-
const link = "image.png";
|
|
19
|
+
describe("MarkdownImageRenderer createLocalLink tests", () => {
|
|
20
|
+
const contentLinkBase = "http://localhost:8081/scm/api/v2/repositories/ns/name/content/";
|
|
21
|
+
const contentLink = contentLinkBase + "{revision}/{path}";
|
|
22
|
+
const revision = "main";
|
|
23
|
+
const currentPath = "/repo/ns/name/code/sources/main/folder/README.md/";
|
|
26
24
|
|
|
27
|
-
it("should return link for internal scm repo
|
|
28
|
-
const internalScmLink = "/repo/
|
|
29
|
-
expect(createLocalLink(
|
|
25
|
+
it("should return link unchanged for internal scm repo links", () => {
|
|
26
|
+
const internalScmLink = "/repo/ns/name/code/sources/develop/myImg.png";
|
|
27
|
+
expect(createLocalLink(contentLink, revision, currentPath, internalScmLink)).toBe(internalScmLink);
|
|
30
28
|
});
|
|
31
29
|
|
|
32
|
-
it("should return
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
it("should return normalized absolute path starting with slash", () => {
|
|
31
|
+
const absoluteLink = "/path/anotherImg.jpg";
|
|
32
|
+
expect(createLocalLink(contentLink, revision, currentPath, absoluteLink)).toBe(
|
|
33
|
+
`${contentLinkBase}${revision}${absoluteLink}`
|
|
35
34
|
);
|
|
36
35
|
});
|
|
37
36
|
|
|
38
|
-
it("should URI encode
|
|
39
|
-
expect(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
).toContain("feature%2Fawesome");
|
|
37
|
+
it("should URI encode revision", () => {
|
|
38
|
+
expect(createLocalLink(contentLink, "feature/awesome", currentPath, "image.png")).toContain("feature%2Fawesome");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should inject the resolved path into {path} placeholder", () => {
|
|
42
|
+
const link = "image.png";
|
|
43
|
+
const result = createLocalLink(contentLink, revision, currentPath, link);
|
|
44
|
+
expect(result).toBe(`${contentLinkBase}${revision}/folder/image.png`);
|
|
48
45
|
});
|
|
49
46
|
});
|
|
@@ -17,47 +17,16 @@
|
|
|
17
17
|
import React, { FC } from "react";
|
|
18
18
|
import { useLocation } from "react-router-dom";
|
|
19
19
|
import { Link } from "@scm-manager/ui-types";
|
|
20
|
-
import {
|
|
21
|
-
isAbsolute,
|
|
22
|
-
isExternalLink,
|
|
23
|
-
isInternalScmRepoLink,
|
|
24
|
-
isLinkWithProtocol,
|
|
25
|
-
isSubDirectoryOf,
|
|
26
|
-
join,
|
|
27
|
-
normalizePath,
|
|
28
|
-
} from "./paths";
|
|
20
|
+
import { isExternalLink, isInternalScmRepoLink, isLinkWithProtocol, resolveInternalPath } from "./paths";
|
|
29
21
|
import { useRepositoryContext, useRepositoryRevisionContext } from "@scm-manager/ui-api";
|
|
30
22
|
|
|
31
|
-
export const createLocalLink = (
|
|
32
|
-
basePath: string,
|
|
33
|
-
contentLink: string,
|
|
34
|
-
revision: string,
|
|
35
|
-
currentPath: string,
|
|
36
|
-
link: string
|
|
37
|
-
) => {
|
|
38
|
-
const apiBasePath = contentLink.replace("{revision}", encodeURIComponent(revision));
|
|
23
|
+
export const createLocalLink = (contentLink: string, revision: string, currentPath: string, link: string) => {
|
|
39
24
|
if (isInternalScmRepoLink(link)) {
|
|
40
25
|
return link;
|
|
41
26
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
const decodedCurrentPath = currentPath.replace(encodeURIComponent(revision), revision);
|
|
46
|
-
if (!isSubDirectoryOf(basePath, decodedCurrentPath)) {
|
|
47
|
-
return apiBasePath.replace("{path}", link);
|
|
48
|
-
}
|
|
49
|
-
const relativePath = decodedCurrentPath.substring(basePath.length);
|
|
50
|
-
let path = relativePath;
|
|
51
|
-
if (decodedCurrentPath.endsWith("/")) {
|
|
52
|
-
path = relativePath.substring(0, relativePath.length - 1);
|
|
53
|
-
}
|
|
54
|
-
const lastSlash = path.lastIndexOf("/");
|
|
55
|
-
if (lastSlash < 0) {
|
|
56
|
-
path = "";
|
|
57
|
-
} else {
|
|
58
|
-
path = path.substring(0, lastSlash);
|
|
59
|
-
}
|
|
60
|
-
return apiBasePath.replace("{path}", normalizePath(join(path, link)));
|
|
27
|
+
const apiBasePath = contentLink.replace("{revision}", encodeURIComponent(revision));
|
|
28
|
+
const path = resolveInternalPath(currentPath, revision, link);
|
|
29
|
+
return apiBasePath.replace("{path}", path);
|
|
61
30
|
};
|
|
62
31
|
|
|
63
32
|
type LinkProps = {
|
|
@@ -67,13 +36,15 @@ type LinkProps = {
|
|
|
67
36
|
|
|
68
37
|
type Props = LinkProps & {
|
|
69
38
|
base?: string;
|
|
39
|
+
permalink?: string;
|
|
70
40
|
contentLink?: string;
|
|
71
41
|
};
|
|
72
42
|
|
|
73
|
-
const MarkdownImageRenderer: FC<Props> = ({ src = "", alt = "", base, contentLink, children, ...props }) => {
|
|
43
|
+
const MarkdownImageRenderer: FC<Props> = ({ src = "", alt = "", base, contentLink, children, permalink, ...props }) => {
|
|
74
44
|
const location = useLocation();
|
|
75
45
|
const repository = useRepositoryContext();
|
|
76
46
|
const revision = useRepositoryRevisionContext();
|
|
47
|
+
const pathname = permalink || location.pathname;
|
|
77
48
|
|
|
78
49
|
if (isExternalLink(src) || isLinkWithProtocol(src)) {
|
|
79
50
|
return (
|
|
@@ -82,7 +53,7 @@ const MarkdownImageRenderer: FC<Props> = ({ src = "", alt = "", base, contentLin
|
|
|
82
53
|
</img>
|
|
83
54
|
);
|
|
84
55
|
} else if (base && repository && revision) {
|
|
85
|
-
const localLink = createLocalLink(
|
|
56
|
+
const localLink = createLocalLink((repository._links.content as Link).href, revision, pathname, src);
|
|
86
57
|
return (
|
|
87
58
|
<img src={localLink} alt={alt}>
|
|
88
59
|
{children}
|
|
@@ -101,9 +72,9 @@ const MarkdownImageRenderer: FC<Props> = ({ src = "", alt = "", base, contentLin
|
|
|
101
72
|
|
|
102
73
|
// we use a factory method, because react-markdown does not pass
|
|
103
74
|
// base as prop down to our link component.
|
|
104
|
-
export const create = (base: string | undefined): FC<LinkProps> => {
|
|
75
|
+
export const create = (base: string | undefined, permalink?: string): FC<LinkProps> => {
|
|
105
76
|
return (props) => {
|
|
106
|
-
return <MarkdownImageRenderer base={base} {...props} />;
|
|
77
|
+
return <MarkdownImageRenderer base={base} permalink={permalink} {...props} />;
|
|
107
78
|
};
|
|
108
79
|
};
|
|
109
80
|
|
|
@@ -14,116 +14,43 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { Repository } from "@scm-manager/ui-types";
|
|
18
18
|
import { createLocalLink } from "./MarkdownLinkRenderer";
|
|
19
19
|
|
|
20
|
-
describe("
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
it("should return false", () => {
|
|
27
|
-
expect(isAnchorLink("https://cloudogu.com")).toBe(false);
|
|
28
|
-
expect(isAnchorLink("/some/path/link")).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("test isExternalLink", () => {
|
|
33
|
-
it("should return true", () => {
|
|
34
|
-
expect(isExternalLink("https://cloudogu.com")).toBe(true);
|
|
35
|
-
expect(isExternalLink("http://cloudogu.com")).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("should return false", () => {
|
|
39
|
-
expect(isExternalLink("some/path/link")).toBe(false);
|
|
40
|
-
expect(isExternalLink("/some/path/link")).toBe(false);
|
|
41
|
-
expect(isExternalLink("#some-anchor")).toBe(false);
|
|
42
|
-
expect(isExternalLink("mailto:trillian@hitchhiker.com")).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("test isLinkWithProtocol", () => {
|
|
47
|
-
it("should return true", () => {
|
|
48
|
-
expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBeTruthy();
|
|
49
|
-
expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBeTruthy();
|
|
50
|
-
expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBeTruthy();
|
|
51
|
-
expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBeTruthy();
|
|
52
|
-
expect(isLinkWithProtocol("about:config")).toBeTruthy();
|
|
53
|
-
expect(isLinkWithProtocol("http://cloudogu.com")).toBeTruthy();
|
|
54
|
-
expect(isLinkWithProtocol("file:///srv/git/project.git")).toBeTruthy();
|
|
55
|
-
expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBeTruthy();
|
|
56
|
-
});
|
|
57
|
-
it("should return false", () => {
|
|
58
|
-
expect(isLinkWithProtocol("some/path/link")).toBeFalsy();
|
|
59
|
-
expect(isLinkWithProtocol("/some/path/link")).toBeFalsy();
|
|
60
|
-
expect(isLinkWithProtocol("#some-anchor")).toBeFalsy();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("test isInternalScmRepoLink", () => {
|
|
65
|
-
it("should return true", () => {
|
|
66
|
-
expect(isInternalScmRepoLink("/repo/scmadmin/git/code/changeset/1234567")).toBe(true);
|
|
67
|
-
expect(isInternalScmRepoLink("/repo/scmadmin/git")).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
it("should return false", () => {
|
|
70
|
-
expect(isInternalScmRepoLink("repo/path/link")).toBe(false);
|
|
71
|
-
expect(isInternalScmRepoLink("/some/path/link")).toBe(false);
|
|
72
|
-
expect(isInternalScmRepoLink("#some-anchor")).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("test createLocalLink", () => {
|
|
77
|
-
it("should handle relative links", () => {
|
|
78
|
-
expectLocalLink("/src", "/src/README.md", "docs/Home.md", "/src/docs/Home.md");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("should handle absolute links", () => {
|
|
82
|
-
expectLocalLink("/src", "/src/README.md", "/docs/CHANGELOG.md", "/src/docs/CHANGELOG.md");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("should handle relative links from locations with trailing slash", () => {
|
|
86
|
-
expectLocalLink("/src", "/src/README.md/", "/docs/LICENSE.md", "/src/docs/LICENSE.md");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("should handle relative links from location outside of base", () => {
|
|
90
|
-
expectLocalLink("/src", "/info/readme", "docs/index.md", "/src/docs/index.md");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("should handle absolute links from location outside of base", () => {
|
|
94
|
-
expectLocalLink("/src", "/info/readme", "/info/index.md", "/src/info/index.md");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("should handle relative links from sub directories", () => {
|
|
98
|
-
expectLocalLink("/src", "/src/docs/index.md", "installation/linux.md", "/src/docs/installation/linux.md");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("should handle absolute links from sub directories", () => {
|
|
102
|
-
expectLocalLink("/src", "/src/docs/index.md", "/docs/CONTRIBUTIONS.md", "/src/docs/CONTRIBUTIONS.md");
|
|
103
|
-
});
|
|
20
|
+
describe("MarkdownLinkRenderer createLocalLink tests", () => {
|
|
21
|
+
const basePath = "/repo/ns/name/code/sources/";
|
|
22
|
+
const repository: Repository = { _links: {}, name: "name", namespace: "ns", type: "" };
|
|
23
|
+
const revision = "main";
|
|
24
|
+
const currentPath = "/repo/ns/name/code/sources/main/folder/README.md/";
|
|
104
25
|
|
|
105
|
-
it("should
|
|
106
|
-
|
|
26
|
+
it("should return link unchanged for internal scm repo links", () => {
|
|
27
|
+
const internalScmLink = "/repo/ns/name/code/changeset/12345";
|
|
28
|
+
expect(createLocalLink(repository, revision, currentPath, internalScmLink)).toBe(internalScmLink);
|
|
107
29
|
});
|
|
108
30
|
|
|
109
|
-
it("should
|
|
110
|
-
|
|
31
|
+
it("should return normalized absolute path starting with slash", () => {
|
|
32
|
+
const absoluteLink = "/docs/CONTRIBUTE.md";
|
|
33
|
+
expect(createLocalLink(repository, revision, currentPath, absoluteLink)).toBe(
|
|
34
|
+
basePath + revision + "/docs/CONTRIBUTE.md"
|
|
35
|
+
);
|
|
111
36
|
});
|
|
112
37
|
|
|
113
|
-
it("should
|
|
114
|
-
|
|
38
|
+
it("should handle absolute links with redundant segments", () => {
|
|
39
|
+
const messyAbsoluteLink = "//docs/./CONTRIBUTE.md";
|
|
40
|
+
expect(createLocalLink(repository, revision, currentPath, messyAbsoluteLink)).toBe(
|
|
41
|
+
basePath + revision + "/docs/CONTRIBUTE.md"
|
|
42
|
+
);
|
|
115
43
|
});
|
|
116
44
|
|
|
117
|
-
it("should
|
|
118
|
-
|
|
45
|
+
it("should return internal link starting with slash for relative paths", () => {
|
|
46
|
+
const relativeLink = "img/image.png";
|
|
47
|
+
const result = createLocalLink(repository, revision, currentPath, relativeLink);
|
|
48
|
+
expect(result).toBe(basePath + revision + "/folder/img/image.png");
|
|
119
49
|
});
|
|
120
50
|
|
|
121
|
-
it("should handle
|
|
122
|
-
|
|
51
|
+
it("should handle relative links navigating up with ..", () => {
|
|
52
|
+
const parentLink = "../docs/CHANGELOG.md";
|
|
53
|
+
const result = createLocalLink(repository, revision, currentPath, parentLink);
|
|
54
|
+
expect(result).toBe(basePath + revision + "/docs/CHANGELOG.md");
|
|
123
55
|
});
|
|
124
|
-
|
|
125
|
-
const expectLocalLink = (basePath: string, currentPath: string, link: string, expected: string) => {
|
|
126
|
-
const localLink = createLocalLink(basePath, currentPath, link);
|
|
127
|
-
expect(localLink).toBe(expected);
|
|
128
|
-
};
|
|
129
56
|
});
|
|
@@ -17,39 +17,25 @@
|
|
|
17
17
|
import React, { FC } from "react";
|
|
18
18
|
import { Link, useLocation } from "react-router-dom";
|
|
19
19
|
import ExternalLink from "../navigation/ExternalLink";
|
|
20
|
-
import { urls } from "@scm-manager/ui-api";
|
|
20
|
+
import { urls, useRepositoryContext, useRepositoryRevisionContext } from "@scm-manager/ui-api";
|
|
21
|
+
import { Repository } from "@scm-manager/ui-types";
|
|
21
22
|
import { ProtocolLinkRendererExtensionMap } from "./markdownExtensions";
|
|
22
23
|
import {
|
|
23
|
-
|
|
24
|
+
isAnchorLink,
|
|
24
25
|
isExternalLink,
|
|
25
26
|
isInternalScmRepoLink,
|
|
26
27
|
isLinkWithProtocol,
|
|
27
|
-
isSubDirectoryOf,
|
|
28
28
|
join,
|
|
29
|
-
|
|
29
|
+
resolveInternalPath,
|
|
30
30
|
} from "./paths";
|
|
31
31
|
|
|
32
|
-
export const createLocalLink = (
|
|
32
|
+
export const createLocalLink = (repository: Repository, revision: string, currentPath: string, link: string) => {
|
|
33
33
|
if (isInternalScmRepoLink(link)) {
|
|
34
34
|
return link;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!isSubDirectoryOf(basePath, currentPath)) {
|
|
40
|
-
return join(basePath, link);
|
|
41
|
-
}
|
|
42
|
-
let path = currentPath;
|
|
43
|
-
if (currentPath.endsWith("/")) {
|
|
44
|
-
path = currentPath.substring(0, currentPath.length - 2);
|
|
45
|
-
}
|
|
46
|
-
const lastSlash = path.lastIndexOf("/");
|
|
47
|
-
if (lastSlash < 0) {
|
|
48
|
-
path = "";
|
|
49
|
-
} else {
|
|
50
|
-
path = path.substring(0, lastSlash);
|
|
51
|
-
}
|
|
52
|
-
return "/" + normalizePath(join(path, link));
|
|
36
|
+
const basePath = `/repo/${repository.namespace}/${repository.name}/code/sources/${revision}/`;
|
|
37
|
+
const internalPath = resolveInternalPath(currentPath, revision, link);
|
|
38
|
+
return join(basePath, internalPath);
|
|
53
39
|
};
|
|
54
40
|
|
|
55
41
|
type LinkProps = {
|
|
@@ -58,18 +44,23 @@ type LinkProps = {
|
|
|
58
44
|
|
|
59
45
|
type Props = LinkProps & {
|
|
60
46
|
base?: string;
|
|
47
|
+
permalink?: string;
|
|
61
48
|
};
|
|
62
49
|
|
|
63
|
-
const MarkdownLinkRenderer: FC<Props> = ({ href = "", base, children, ...props }) => {
|
|
50
|
+
const MarkdownLinkRenderer: FC<Props> = ({ href = "", base, children, permalink, ...props }) => {
|
|
64
51
|
const location = useLocation();
|
|
52
|
+
const repository = useRepositoryContext();
|
|
53
|
+
const revision = useRepositoryRevisionContext();
|
|
54
|
+
const pathname = permalink || location.pathname;
|
|
55
|
+
|
|
65
56
|
if (isExternalLink(href)) {
|
|
66
57
|
return <ExternalLink to={href}>{children}</ExternalLink>;
|
|
67
58
|
} else if (isLinkWithProtocol(href)) {
|
|
68
59
|
return <a href={href}>{children}</a>;
|
|
69
60
|
} else if (isAnchorLink(href)) {
|
|
70
61
|
return <a href={urls.withContextPath(location.pathname) + href}>{children}</a>;
|
|
71
|
-
} else if (base) {
|
|
72
|
-
const localLink = createLocalLink(
|
|
62
|
+
} else if (base && repository && revision) {
|
|
63
|
+
const localLink = createLocalLink(repository, revision, pathname, href);
|
|
73
64
|
return <Link to={localLink}>{children}</Link>;
|
|
74
65
|
} else if (href) {
|
|
75
66
|
return (
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2020 - present Cloudogu GmbH
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
* the terms of the GNU Affero General Public License as published by the Free
|
|
6
|
+
* Software Foundation, version 3.
|
|
7
|
+
*
|
|
8
|
+
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
9
|
+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
10
|
+
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
11
|
+
* details.
|
|
12
|
+
*
|
|
13
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
14
|
+
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isAnchorLink, isExternalLink, isLinkWithProtocol, isInternalScmRepoLink, resolveInternalPath } from "./paths";
|
|
18
|
+
|
|
19
|
+
describe("isExternalLink tests", () => {
|
|
20
|
+
it("should return true", () => {
|
|
21
|
+
expect(isExternalLink("https://cloudogu.com")).toBe(true);
|
|
22
|
+
expect(isExternalLink("http://cloudogu.com")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should return false", () => {
|
|
26
|
+
expect(isExternalLink("some/path/link")).toBe(false);
|
|
27
|
+
expect(isExternalLink("/some/path/link")).toBe(false);
|
|
28
|
+
expect(isExternalLink("#some-anchor")).toBe(false);
|
|
29
|
+
expect(isExternalLink("mailto:trillian@hitchhiker.com")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("isAnchorLink tests", () => {
|
|
34
|
+
it("should return true", () => {
|
|
35
|
+
expect(isAnchorLink("#some-thing")).toBe(true);
|
|
36
|
+
expect(isAnchorLink("#/some/more/complicated-link")).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return false", () => {
|
|
40
|
+
expect(isAnchorLink("https://cloudogu.com")).toBe(false);
|
|
41
|
+
expect(isAnchorLink("/some/path/link")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("isInternalScmRepoLink tests", () => {
|
|
46
|
+
it("should return true", () => {
|
|
47
|
+
expect(isInternalScmRepoLink("/repo/scmadmin/git/code/changeset/1234567")).toBe(true);
|
|
48
|
+
expect(isInternalScmRepoLink("/repo/scmadmin/git")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return false", () => {
|
|
52
|
+
expect(isInternalScmRepoLink("repo/path/link")).toBe(false);
|
|
53
|
+
expect(isInternalScmRepoLink("/some/path/link")).toBe(false);
|
|
54
|
+
expect(isInternalScmRepoLink("#some-anchor")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("isLinkWithProtocol tests", () => {
|
|
59
|
+
it("should return true", () => {
|
|
60
|
+
expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBeTruthy();
|
|
61
|
+
expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBeTruthy();
|
|
62
|
+
expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBeTruthy();
|
|
63
|
+
expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBeTruthy();
|
|
64
|
+
expect(isLinkWithProtocol("about:config")).toBeTruthy();
|
|
65
|
+
expect(isLinkWithProtocol("http://cloudogu.com")).toBeTruthy();
|
|
66
|
+
expect(isLinkWithProtocol("file:///srv/git/project.git")).toBeTruthy();
|
|
67
|
+
expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return false", () => {
|
|
71
|
+
expect(isLinkWithProtocol("some/path/link")).toBeFalsy();
|
|
72
|
+
expect(isLinkWithProtocol("/some/path/link")).toBeFalsy();
|
|
73
|
+
expect(isLinkWithProtocol("#some-anchor")).toBeFalsy();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("resolveInternalPath tests", () => {
|
|
78
|
+
const revision = "main";
|
|
79
|
+
const repoPath = "/repo/ns/name/code/sources/main/";
|
|
80
|
+
|
|
81
|
+
it("should resolve . within the path", () => {
|
|
82
|
+
const currentPath = repoPath + "README.md";
|
|
83
|
+
const link = "./SHAPESHIPS.md";
|
|
84
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
85
|
+
expect(result).toBe("SHAPESHIPS.md");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should resolve . with the current directory", () => {
|
|
89
|
+
const currentPath = repoPath + "README.md";
|
|
90
|
+
const link = "././HITCHHIKER.md";
|
|
91
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
92
|
+
expect(result).toBe("HITCHHIKER.md");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should resolve .. within the path", () => {
|
|
96
|
+
const currentPath = repoPath + "docs/gui/index.md";
|
|
97
|
+
const link = "../../img/image.png";
|
|
98
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
99
|
+
expect(result).toBe("img/image.png");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle complex redundant segments (./.././..)", () => {
|
|
103
|
+
const currentPath = repoPath + "docs/installation/index.md";
|
|
104
|
+
const link = "./.././../docs/index.md";
|
|
105
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
106
|
+
expect(result).toBe("docs/index.md");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should resolve .. to root if we reach the end", () => {
|
|
110
|
+
const currentPath = repoPath + "index.md";
|
|
111
|
+
const link = "../../README.md";
|
|
112
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
113
|
+
expect(result).toBe("README.md");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should resolve root link within root path", () => {
|
|
117
|
+
const currentPath = repoPath + "README.md";
|
|
118
|
+
const link = "/SHAPESHIPS.md";
|
|
119
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
120
|
+
expect(result).toBe("SHAPESHIPS.md");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should resolve root link within folder path", () => {
|
|
124
|
+
const currentPath = repoPath + "dir/README.md";
|
|
125
|
+
const link = "/SHAPESHIPS.md";
|
|
126
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
127
|
+
expect(result).toBe("SHAPESHIPS.md");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should resolve relative link within root path", () => {
|
|
131
|
+
const currentPath = repoPath + "README.md";
|
|
132
|
+
const link = "SHAPESHIPS.md";
|
|
133
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
134
|
+
expect(result).toBe("SHAPESHIPS.md");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should resolve relative link within folder path", () => {
|
|
138
|
+
const currentPath = repoPath + "dir/README.md";
|
|
139
|
+
const link = "SHAPESHIPS.md";
|
|
140
|
+
const result = resolveInternalPath(currentPath, revision, link);
|
|
141
|
+
expect(result).toBe("dir/SHAPESHIPS.md");
|
|
142
|
+
});
|
|
143
|
+
});
|
package/src/markdown/paths.ts
CHANGED
|
@@ -27,8 +27,8 @@ export const isInternalScmRepoLink = (link: string) => {
|
|
|
27
27
|
return link.startsWith("/repo/");
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const linkWithProtocolRegex = new RegExp("^([a-z]+):(.+)");
|
|
31
30
|
export const isLinkWithProtocol = (link: string) => {
|
|
31
|
+
const linkWithProtocolRegex = new RegExp("^([a-z]+):(.+)");
|
|
32
32
|
const match = link.match(linkWithProtocolRegex);
|
|
33
33
|
return match && { protocol: match[1], link: match[2] };
|
|
34
34
|
};
|
|
@@ -47,8 +47,10 @@ export const normalizePath = (path: string) => {
|
|
|
47
47
|
const parts = path.split("/");
|
|
48
48
|
for (const part of parts) {
|
|
49
49
|
if (part === "..") {
|
|
50
|
+
// Go up
|
|
50
51
|
stack.pop();
|
|
51
|
-
} else if (part !== ".") {
|
|
52
|
+
} else if (part !== "." && part !== "") {
|
|
53
|
+
// Skip current dir and empty parts
|
|
52
54
|
stack.push(part);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -66,3 +68,28 @@ export const isAbsolute = (link: string) => {
|
|
|
66
68
|
export const isSubDirectoryOf = (basePath: string, currentPath: string) => {
|
|
67
69
|
return currentPath.startsWith(basePath);
|
|
68
70
|
};
|
|
71
|
+
|
|
72
|
+
export const resolveInternalPath = (currentPath: string, revision: string, link: string) => {
|
|
73
|
+
// Extract path relative to revision
|
|
74
|
+
const pathForMatching = currentPath.replace(encodeURIComponent(revision), revision);
|
|
75
|
+
const revisionWithSlashes = `/${revision}/`;
|
|
76
|
+
const revIndex = pathForMatching.indexOf(revisionWithSlashes);
|
|
77
|
+
let internalPath = "";
|
|
78
|
+
if (revIndex !== -1) {
|
|
79
|
+
internalPath = pathForMatching.substring(revIndex + revisionWithSlashes.length);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Determine if path is file or directory
|
|
83
|
+
let directoryPath = internalPath.endsWith("/") ? internalPath.slice(0, -1) : internalPath;
|
|
84
|
+
if (directoryPath.toLowerCase().endsWith(".md")) {
|
|
85
|
+
const parts = directoryPath.split("/");
|
|
86
|
+
parts.pop(); // Removes filename
|
|
87
|
+
directoryPath = parts.join("/");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Normalize path
|
|
91
|
+
if (isAbsolute(link)) {
|
|
92
|
+
return normalizePath(link);
|
|
93
|
+
}
|
|
94
|
+
return normalizePath(join(directoryPath, link));
|
|
95
|
+
};
|