diff --git a/src/Documentation/root.ts b/src/Documentation/root.ts index 59398ad76..3847e6981 100644 --- a/src/Documentation/root.ts +++ b/src/Documentation/root.ts @@ -31,7 +31,7 @@ export const getPage = (title: string): string => { return resolvePage(title).pageContent; }; -export const DocumentationPopUpEvents = new EventEmitter<[string | undefined]>(); +export const DocumentationPopUpEvents = new EventEmitter<[string]>(); export function openDocumentationPopUp(path: string): void { DocumentationPopUpEvents.emit(path); diff --git a/src/Documentation/ui/DocumentationPopUp.tsx b/src/Documentation/ui/DocumentationPopUp.tsx index 4764a0e75..27d55e8ee 100644 --- a/src/Documentation/ui/DocumentationPopUp.tsx +++ b/src/Documentation/ui/DocumentationPopUp.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useRef, useState } from "react"; import { Modal } from "../../ui/React/Modal"; -import { defaultNsApiPage, Navigator, openDocExternally } from "../../ui/React/Documentation"; +import { convertNavigatorHref, Navigator, openDocExternally } from "../../ui/React/Documentation"; import { MD } from "../../ui/MD/MD"; -import { asFilePath, type FilePath, isFilePath, resolveFilePath } from "../../Paths/FilePath"; +import { asFilePath, type FilePath } from "../../Paths/FilePath"; import { DocumentationPopUpEvents } from "../root"; export function DocumentationPopUp({ hidden }: { hidden: boolean }) { @@ -11,8 +11,8 @@ export function DocumentationPopUp({ hidden }: { hidden: boolean }) { useEffect( () => - DocumentationPopUpEvents.subscribe((path?: string) => { - setPath(path ? asFilePath(path) : undefined); + DocumentationPopUpEvents.subscribe((path: string) => { + setPath(asFilePath(path)); }), [], ); @@ -24,39 +24,30 @@ export function DocumentationPopUp({ hidden }: { hidden: boolean }) { modalWrapperRef.current.scrollTo({ top: 0, behavior: "instant" }); }); - const navigator = { - navigate(href: string, openExternally: boolean) { - /** - * This function is used for navigating inside the documentation popup. - * - * Href can be: - * - Internal NS docs. The "markdown" folder does not have any subfolders. All files are at the top-level. "Href" - * is always "./". E.g., "./bitburner.ns.md". - * - HTTP URL (e.g., ns.printf has a link to https://github.com/alexei/sprintf.js). - */ - let path; - if (href.startsWith("https://") || href.startsWith("http://")) { - openExternally = true; - path = href; - } else { - path = resolveFilePath(href, defaultNsApiPage); - } - if (!path) { - console.error(`Bad path ${href} while navigating docs.`); - return; - } - if (openExternally) { - openDocExternally(path); - return; - } - if (isFilePath(path)) { - setPath(path); - } - }, - }; if (!path) { return <>; } + + const navigator = { + /** + * This function is used for navigating inside the documentation popup. + */ + navigate(href: string, openExternally: boolean) { + if (!path) { + return; + } + const { path: newPath, forceOpenExternally } = convertNavigatorHref(href, path); + if (!newPath) { + console.error(`Bad path ${href} while navigating docs.`); + return; + } + if (openExternally || forceOpenExternally) { + openDocExternally(newPath); + return; + } + setPath(newPath); + }, + }; return ( ({ page: defaultPage, pages: [], @@ -92,7 +94,7 @@ export const HistoryProvider = (props: React.PropsWithChildren): React.R return {props.children}; }; -export function openDocExternally(path: string) { +export function openDocExternally(path: string): void { const ver = CONSTANTS.isDevBranch ? "dev" : "stable"; let url; if (path.startsWith("http://") || path.startsWith("https://")) { @@ -111,3 +113,55 @@ export function openDocExternally(path: string) { } window.open(url, "_newtab"); } + +/** + * Href can be: + * - Relative URL from non-NS docs pointing to markdown folder: Open "../../../../markdown/bitburner.ns.md" from "index.md" + * - Relative URL from NS docs to other NS docs (e.g., click the links in NS docs viewer): Open "./bitburner.ns.cloud.md" from "nsDoc/bitburner.ns.md" + * - Internal NS docs (e.g., choose a dropdown option in DocumentationAutocomplete): nsDoc/bitburner.ns.md + * - Internal non-NS docs: help/getting_started.md + * - HTTP URL: + * - Point to NS docs. Some non-NS docs pages include links to NS docs. For example: basic/scripts.md has a + * link to https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/bitburner.ns.flags.md. In + * these cases, the link always points to a file at https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/ + * - Point to other places. + */ +export function convertNavigatorHref( + href: string, + currentPage: FilePath, +): { path: FilePath | null; forceOpenExternally: false } | { path: string; forceOpenExternally: true } { + let path; + if (href.includes(prefixOfRelativeUrlOfNSDoc)) { + // Relative URL from non-NS docs pointing to markdown folder + path = asFilePath( + // Convert "../../../../markdown/bitburner.foo.md" and "deeper" URLs (i.e., having more "../") to "nsDoc/bitburner.foo.md" + href.replace( + href.substring(0, href.indexOf(prefixOfRelativeUrlOfNSDoc) + prefixOfRelativeUrlOfNSDoc.length), + "nsDoc/bitburner.", + ), + ); + } else if (/^\.\/bitburner\.[^/]*\.md$/.test(href)) { + // Relative URL from NS docs to other NS docs. The URL is always ./bitburner.foo.md + // - Start with "./bitburner." + // - End with ".md" + // - Never have "/" between "./bitburner." and ".md" + path = resolveFilePath(href, defaultNsApiPage); + } else if (href.startsWith("nsDoc/")) { + // Internal NS docs + path = asFilePath(href); + } else if (href.startsWith("https://") || href.startsWith("http://")) { + // TODO: Remove this case after converting all these links to relative links. + // HTTP URL pointing to NS docs. + // Convert https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/bitburner.foo.md to nsDoc/bitburner.foo.md + if (href.startsWith(prefixOfHttpUrlOfNsDocs)) { + path = asFilePath(`nsDoc/${href.replace(prefixOfHttpUrlOfNsDocs, "")}`); + } else { + // HTTP URL pointing to other places. + return { path: href, forceOpenExternally: true }; + } + } else { + // Internal non-NS docs + path = resolveFilePath("./" + href, currentPage); + } + return { path, forceOpenExternally: false }; +} diff --git a/test/jest/ui/DocumentationNavigator.test.ts b/test/jest/ui/DocumentationNavigator.test.ts new file mode 100644 index 000000000..735709625 --- /dev/null +++ b/test/jest/ui/DocumentationNavigator.test.ts @@ -0,0 +1,81 @@ +import { asFilePath } from "../../../src/Paths/FilePath"; +import { convertNavigatorHref, defaultNsApiPage, defaultPage } from "../../../src/ui/React/Documentation"; + +describe("convertNavigatorHref", () => { + describe("Valid href", () => { + test("Relative URL from non-NS docs pointing to markdown folder 1", () => { + const { path, forceOpenExternally } = convertNavigatorHref("../../../../markdown/bitburner.ns.md", defaultPage); + expect(path).toStrictEqual("nsDoc/bitburner.ns.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + test("Relative URL from non-NS docs pointing to markdown folder 2", () => { + const { path, forceOpenExternally } = convertNavigatorHref( + "../../../../../markdown/bitburner.ns.flags.md", + asFilePath("basic/scripts.md"), + ); + expect(path).toStrictEqual("nsDoc/bitburner.ns.flags.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + + test("Relative URL from NS docs to other NS docs", () => { + const { path, forceOpenExternally } = convertNavigatorHref("./bitburner.ns.cloud.md", defaultNsApiPage); + expect(path).toStrictEqual("nsDoc/bitburner.ns.cloud.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + + test("Internal NS docs 1", () => { + const { path, forceOpenExternally } = convertNavigatorHref("help/getting_started.md", defaultPage); + expect(path).toStrictEqual("help/getting_started.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + test("Internal NS docs 2", () => { + const { path, forceOpenExternally } = convertNavigatorHref( + "../basic/scripts.md", + asFilePath("help/getting_started.md"), + ); + expect(path).toStrictEqual("basic/scripts.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + test("Internal NS docs 3", () => { + const { path, forceOpenExternally } = convertNavigatorHref("./faq.md", asFilePath("help/getting_started.md")); + expect(path).toStrictEqual("help/faq.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + test("Internal NS docs 4", () => { + const { path, forceOpenExternally } = convertNavigatorHref("faq.md", asFilePath("help/getting_started.md")); + expect(path).toStrictEqual("help/faq.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + + test("HTTP/HTTPS URL - Point to NS docs", () => { + const { path, forceOpenExternally } = convertNavigatorHref( + "https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/bitburner.ns.md", + defaultPage, + ); + expect(path).toStrictEqual("nsDoc/bitburner.ns.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + test("HTTP/HTTPS URL - Point to other places", () => { + const { path, forceOpenExternally } = convertNavigatorHref("https://bitburner-official.github.io", defaultPage); + expect(path).toStrictEqual("https://bitburner-official.github.io"); + expect(forceOpenExternally).toStrictEqual(true); + }); + }); + + describe("Invalid href", () => { + test("Relative URL from non-NS docs not pointing to markdown folder", () => { + const { path, forceOpenExternally } = convertNavigatorHref("../../../markdown/bitburner.ns.md", defaultPage); + expect(path).toStrictEqual(null); + expect(forceOpenExternally).toStrictEqual(false); + }); + + test("Relative URL from NS docs to other NS docs", () => { + // The path is always "./bitburner.foo.md". It never starts with "../". + const { path, forceOpenExternally } = convertNavigatorHref("../bitburner.ns.cloud.md", defaultNsApiPage); + // This is an invalid path. Technically, it's a valid file path, but there is no doc file with this path. The path + // of NS docs is always prefixed with "nsDoc/". + expect(path).toStrictEqual("bitburner.ns.cloud.md"); + expect(forceOpenExternally).toStrictEqual(false); + }); + }); +});