@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scm-manager/ui-components",
3
- "version": "3.11.6",
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.6",
36
- "@scm-manager/ui-types": "3.11.6",
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.6",
72
- "@scm-manager/ui-shortcuts": "3.11.6",
73
- "@scm-manager/ui-text": "3.11.6"
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.6",
77
- "@scm-manager/ui-overlays": "3.11.6",
78
- "@scm-manager/ui-layout": "3.11.6",
79
- "@scm-manager/ui-buttons": "3.11.6",
80
- "@scm-manager/ui-api": "3.11.6",
81
- "@scm-manager/ui-extensions": "3.11.6",
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/./some_image.jpg"
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="/scm/buttons"
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 revision = "revision";
22
- const basePath = `/repo/namespace/name/code/sources/${revision}/`;
23
- const contentLink = "http://localhost:8081/scm/api/v2/repositories/namespace/name/content/{revision}/{path}";
24
- const currentPath = basePath + "README.md/";
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 link", () => {
28
- const internalScmLink = "/repo/namespace/name/code/sources/develop/myImg.png";
29
- expect(createLocalLink(basePath, contentLink, revision, currentPath, internalScmLink)).toBe(internalScmLink);
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 modified contentLink for absolute link", () => {
33
- expect(createLocalLink(basePath, contentLink, revision, currentPath, "/path/anotherImg.jpg")).toBe(
34
- "http://localhost:8081/scm/api/v2/repositories/namespace/name/content/revision/path/anotherImg.jpg"
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 branch", () => {
39
- expect(
40
- createLocalLink(
41
- "/repo/namespace/name/code/sources/feature/awesome/",
42
- contentLink,
43
- "feature/awesome",
44
- currentPath,
45
- link
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
- if (isAbsolute(link)) {
43
- return apiBasePath.replace("{path}", link.substring(1));
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(base, (repository._links.content as Link).href, revision, location.pathname, src);
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 { isAnchorLink, isExternalLink, isLinkWithProtocol, isInternalScmRepoLink } from "./paths";
17
+ import { Repository } from "@scm-manager/ui-types";
18
18
  import { createLocalLink } from "./MarkdownLinkRenderer";
19
19
 
20
- describe("test isAnchorLink", () => {
21
- it("should return true", () => {
22
- expect(isAnchorLink("#some-thing")).toBe(true);
23
- expect(isAnchorLink("#/some/more/complicated-link")).toBe(true);
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 resolve .. with in path", () => {
106
- expectLocalLink("/src", "/src/docs/installation/index.md", "../../README.md", "/src/README.md");
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 resolve .. to / if we reached the end", () => {
110
- expectLocalLink("/", "/index.md", "../../README.md", "/README.md");
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 resolve . with in path", () => {
114
- expectLocalLink("/src", "/src/README.md", "./SHAPESHIPS.md", "/src/SHAPESHIPS.md");
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 resolve . with the current directory", () => {
118
- expectLocalLink("/", "/README.md", "././HITCHHIKER.md", "/HITCHHIKER.md");
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 complex path", () => {
122
- expectLocalLink("/src", "/src/docs/installation/index.md", "./.././../docs/index.md", "/src/docs/index.md");
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
- isAbsolute, isAnchorLink,
24
+ isAnchorLink,
24
25
  isExternalLink,
25
26
  isInternalScmRepoLink,
26
27
  isLinkWithProtocol,
27
- isSubDirectoryOf,
28
28
  join,
29
- normalizePath
29
+ resolveInternalPath,
30
30
  } from "./paths";
31
31
 
32
- export const createLocalLink = (basePath: string, currentPath: string, link: string) => {
32
+ export const createLocalLink = (repository: Repository, revision: string, currentPath: string, link: string) => {
33
33
  if (isInternalScmRepoLink(link)) {
34
34
  return link;
35
35
  }
36
- if (isAbsolute(link)) {
37
- return join(basePath, link);
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(base, location.pathname, href);
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
+ });
@@ -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
+ };