-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ce00bf6
commit 1833493
Showing
13 changed files
with
467 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); | ||
} |
5 changes: 3 additions & 2 deletions
5
packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeLeaf.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
4 changes: 4 additions & 0 deletions
4
packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeWithChildren.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
44 changes: 41 additions & 3 deletions
44
packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeWithRedirect.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.