Skip to content

Commit

Permalink
prune the tree
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Oct 4, 2024
1 parent ce00bf6 commit 1833493
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 14 deletions.
56 changes: 56 additions & 0 deletions packages/fdr-sdk/src/navigation/utils/followRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import visitDiscriminatedUnion from "@fern-ui/core-utils/visitDiscriminatedUnion";
import { FernNavigation } from "../../../dist";

export function followRedirect(
nodeToFollow: FernNavigation.NavigationNode | undefined,
): FernNavigation.Slug | undefined {
if (nodeToFollow == null) {
return undefined;
}
return visitDiscriminatedUnion(nodeToFollow)._visit<FernNavigation.Slug | undefined>({
link: () => undefined,

// leaf nodes
page: (node) => node.slug,
changelog: (node) => node.slug,
changelogYear: (node) => node.slug,
changelogMonth: (node) => node.slug,
changelogEntry: (node) => node.slug,
endpoint: (node) => node.slug,
webSocket: (node) => node.slug,
webhook: (node) => node.slug,
landingPage: (node) => node.slug,

// nodes with overview
apiPackage: (node) => (node.overviewPageId != null ? node.slug : followRedirects(node.children)),
section: (node) => (node.overviewPageId != null ? node.slug : followRedirects(node.children)),
apiReference: (node) => (node.overviewPageId != null ? node.slug : followRedirects(node.children)),

// version is a special case where it should only consider it's first child (the first version)
product: (node) => followRedirect(node.child),
productgroup: (node) => followRedirect(node.children.filter((node) => !node.hidden)[0]),
versioned: (node) => followRedirect(node.children.filter((node) => !node.hidden)[0]),
unversioned: (node) => followRedirect(node.landingPage ?? node.child),
tabbed: (node) => followRedirects(node.children),
sidebarRoot: (node) => followRedirects(node.children),
endpointPair: (node) => followRedirect(node.nonStream),
root: (node) => followRedirect(node.child),
version: (node) => followRedirect(node.child),
tab: (node) => followRedirect(node.child),
sidebarGroup: (node) => followRedirects(node.children),
});
}

export function followRedirects(nodes: FernNavigation.NavigationNode[]): FernNavigation.Slug | undefined {
for (const node of nodes) {
// skip hidden nodes
if (FernNavigation.hasMetadata(node) && node.hidden) {
continue;
}
const redirect = followRedirect(node);
if (redirect != null) {
return redirect;
}
}
return;
}
43 changes: 43 additions & 0 deletions packages/fdr-sdk/src/navigation/utils/hasChildren.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { UnreachableCaseError } from "ts-essentials";
import { NavigationNodeWithChildren } from "../versions";

export function hasChildren(node: NavigationNodeWithChildren): boolean {
switch (node.type) {
case "apiPackage":
return node.children.length > 0;
case "apiReference":
return node.children.length > 0 || node.changelog != null;
case "changelog":
return node.children.length > 0;
case "changelogMonth":
return node.children.length > 0;
case "changelogYear":
return node.children.length > 0;
case "endpointPair":
return true;
case "productgroup":
return node.children.length > 0 || node.landingPage != null;
case "product":
return true;
case "root":
return true;
case "unversioned":
return true;
case "section":
return node.children.length > 0;
case "sidebarGroup":
return node.children.length > 0;
case "tab":
return true;
case "sidebarRoot":
return node.children.length > 0;
case "tabbed":
return node.children.length > 0;
case "version":
return true;
case "versioned":
return node.children.length > 0;
default:
throw new UnreachableCaseError(node);
}
}
1 change: 1 addition & 0 deletions packages/fdr-sdk/src/navigation/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from "./createBreadcrumbs";
export * from "./findNode";
export * from "./getApiReferenceId";
export * from "./getNoIndexFromFrontmatter";
export * from "./pruneNavigationTree";
export * from "./toRootNode";
144 changes: 144 additions & 0 deletions packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { UnreachableCaseError } from "ts-essentials";
import { FernNavigation } from "../..";
import { hasChildren } from "./hasChildren";
import { updatePointsTo } from "./updatePointsTo";

/**
*
* @param root the root node of the navigation tree
* @param keep a function that returns true if the node should be kept
* @returns
*/
export function pruneNavigationTree<ROOT extends FernNavigation.NavigationNode>(
root: ROOT,
keep: (node: FernNavigation.NavigationNode) => boolean,
): ROOT | undefined {
const clone = structuredClone(root);

// keeps track of deleted nodes to avoid deleting them multiple times
const deleted = new Set<FernNavigation.NodeId>();

FernNavigation.traverseNavigationLevelOrder(clone, (node, parents) => {
// if the node was already deleted, we don't need to traverse it
if (deleted.has(node.id)) {
return "skip";
}

// continue traversal if the node is not to be deleted
if (keep(node)) {
return;
}

deleteChild(node, parents, deleted).forEach((id) => {
deleted.add(id);
});

// since the node was deleted, its children are deleted too
// we don't need to traverse them, nor do we need to keep them in the tree.
return "skip";
});

if (deleted.has(clone.id)) {
return undefined;
}

if (deleted.size > 0) {
// since the tree has been pruned, we need to update the pointsTo property
updatePointsTo(clone);
}

return clone;
}

/**
* Deletes a child from a parent node
*
* If the parent node cannot be deleted, it will deleted too via recursion.
*
* @param node the child node to delete
* @param parent the parent node
* @param deleted a set of nodes that have already been deleted
* @returns a list of deleted nodes
*/
function deleteChild(
node: FernNavigation.NavigationNode,
parents: readonly FernNavigation.NavigationNodeWithChildren[],
deleted: Set<FernNavigation.NodeId> = new Set(),
): FernNavigation.NodeId[] {
const ancestors = [...parents];
const parent = ancestors.pop(); // the parent node is the last element in the array
if (parent == null) {
return [];
} else if (deleted.has(parent.id)) {
return [node.id];
}

const internalDeleted = (() => {
switch (parent.type) {
case "apiPackage":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "apiReference":
parent.children = parent.children.filter((child) => child.id !== node.id);
parent.changelog = parent.changelog?.id === node.id ? undefined : parent.changelog;
return [node.id];
case "changelog":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "changelogYear":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "changelogMonth":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "endpointPair":
return [...deleteChild(parent, ancestors), node.id];
case "productgroup":
parent.children = parent.children.filter((child) => child.id !== node.id);
parent.landingPage = parent.landingPage?.id === node.id ? undefined : parent.landingPage;
return [node.id];
case "product":
return [...deleteChild(parent, ancestors), node.id];
case "root":
return [...deleteChild(parent, ancestors), node.id];
case "unversioned":
if (node.id === parent.landingPage?.id) {
parent.landingPage = undefined;
return [node.id];
}
return [...deleteChild(parent, ancestors), node.id];
case "section":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "sidebarGroup":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "tab":
return [...deleteChild(parent, ancestors), node.id];
case "sidebarRoot":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "tabbed":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
case "version":
if (node.id === parent.landingPage?.id) {
parent.landingPage = undefined;
return [node.id];
}
return [...deleteChild(parent, ancestors), node.id];
case "versioned":
parent.children = parent.children.filter((child) => child.id !== node.id);
return [node.id];
default:
throw new UnreachableCaseError(parent);
}
})();

// after deletion, if the node has no children, we can delete the parent node too
if (!hasChildren(parent) && !internalDeleted.includes(parent.id)) {
return [...deleteChild(parent, ancestors), ...internalDeleted];
}

return internalDeleted;
}
13 changes: 13 additions & 0 deletions packages/fdr-sdk/src/navigation/utils/updatePointsTo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NavigationNode, hasPointsTo, traverseNavigationLevelOrder } from "../versions/latest";
import { followRedirect } from "./followRedirect";

/**
* @param input will be mutated
*/
export function updatePointsTo(input: NavigationNode): void {
traverseNavigationLevelOrder(input, (node) => {
if (hasPointsTo(node)) {
node.pointsTo = followRedirect(node);
}
});
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { LinkNode } from ".";
import type { NavigationNode } from "./NavigationNode";
import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf";
import { isMarkdownLeaf, type NavigationNodeMarkdownLeaf } from "./NavigationNodePageLeaf";

/**
* A navigation node that represents a leaf in the navigation tree (i.e. a node that does not have children)
*/
export type NavigationNodeLeaf = NavigationNodeApiLeaf | NavigationNodeMarkdownLeaf;
export type NavigationNodeLeaf = NavigationNodeApiLeaf | NavigationNodeMarkdownLeaf | LinkNode;

export function isLeaf(node: NavigationNode): node is NavigationNodeLeaf {
return isApiLeaf(node) || isMarkdownLeaf(node);
return isApiLeaf(node) || isMarkdownLeaf(node) || node.type === "link";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { NavigationNode } from "./NavigationNode";
import { NavigationNodeLeaf } from "./NavigationNodeLeaf";

export type NavigationNodeWithChildren = Exclude<NavigationNode, NavigationNodeLeaf>;
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
import type { WithRedirect } from ".";
import type { Exact, MarkRequired } from "ts-essentials";
import type {
ApiPackageNode,
ApiReferenceNode,
ProductNode,
RootNode,
SectionNode,
TabNode,
VersionNode,
WithRedirect,
} from ".";
import type { NavigationNode } from "./NavigationNode";

/**
* Navigation nodes that can have a redirect
*/
export type NavigationNodeWithPointsTo =
| RootNode
| ProductNode
| VersionNode
| TabNode
| SectionNode
| ApiReferenceNode
| ApiPackageNode;

export function hasPointsTo(node: NavigationNode): node is NavigationNodeWithRedirect {
return (
node.type === "root" ||
node.type === "product" ||
node.type === "version" ||
node.type === "tab" ||
node.type === "section" ||
node.type === "apiReference" ||
node.type === "apiPackage"
);
}

/**
* Navigation nodes that extend WithRedirect
*/
export type NavigationNodeWithRedirect = Extract<NavigationNode, WithRedirect>;
export type NavigationNodeWithRedirect = Exact<NavigationNodeWithPointsTo, Extract<NavigationNode, WithRedirect>> &
MarkRequired<WithRedirect, "pointsTo">;

export function hasRedirect(node: NavigationNode): node is NavigationNodeWithRedirect {
return typeof (node as NavigationNodeWithRedirect).pointsTo === "string";
if (!hasPointsTo(node)) {
return false;
}
return node.pointsTo != null;
}
3 changes: 2 additions & 1 deletion packages/fdr-sdk/src/navigation/versions/latest/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from "../../../client/generated/api/resources/commons";
export * from "../../../client/generated/api/resources/navigation/resources/latest/types";
export * from "./getPageId";
export * from "./NavigationNode";
export * from "./NavigationNodeApiLeaf";
export * from "./NavigationNodeLeaf";
Expand All @@ -10,8 +9,10 @@ export * from "./NavigationNodePage";
export * from "./NavigationNodePageLeaf";
export * from "./NavigationNodeSection";
export * from "./NavigationNodeSectionOverview";
export * from "./NavigationNodeWithChildren";
export * from "./NavigationNodeWithMetadata";
export * from "./NavigationNodeWithRedirect";
export * from "./getPageId";
export * from "./slugjoin";
export * from "./toDefaultSlug";
export * from "./traverseNavigation";
Loading

0 comments on commit 1833493

Please sign in to comment.