Skip to content

Commit 22dc1e5

Browse files
authored
Merge pull request #383 from ghostty-org/fix-sidebar
Ensure Header IDs are unique
2 parents 3e49996 + b9af352 commit 22dc1e5

File tree

2 files changed

+46
-3
lines changed

2 files changed

+46
-3
lines changed

src/components/jumplink-header/index.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ interface JumplinkHeaderProps {
1111
as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
1212
className?: string;
1313
children?: React.ReactNode;
14+
"data-index"?: string;
1415
}
1516

1617
export default function JumplinkHeader({
1718
className,
1819
children,
1920
as,
21+
"data-index": dataIndex,
2022
}: JumplinkHeaderProps) {
21-
const id = headerDeeplinkIdentifier(children);
23+
const id = headerDeeplinkIdentifier(children, dataIndex);
2224
const { ref, inView } = useInView({
2325
// This is our header height! This also impacts our
2426
// margin below, but TBH I actually like it needing to
@@ -62,8 +64,14 @@ export default function JumplinkHeader({
6264

6365
/**
6466
* @param children the MDX children node of the header element
67+
* @param dataIndex optional data-index attribute value, which is precomputed and set via
68+
* our fetch-docs remark parser in the event that this is the 2nd+ time we have
69+
* encountered this ID, to ensure that the generated ID is unique.
6570
* @returns The resulting id string which should be applied to the jumplink-header
66-
*/ function headerDeeplinkIdentifier(children?: React.ReactNode): string {
71+
*/ function headerDeeplinkIdentifier(
72+
children?: React.ReactNode,
73+
dataIndex?: string,
74+
): string {
6775
let flattenedTitle = "";
6876

6977
const extractText = (child: React.ReactNode): void => {
@@ -88,5 +96,10 @@ export default function JumplinkHeader({
8896
${JSON.stringify(children, null, 2)}`);
8997
}
9098

99+
// Append data-index if it exists
100+
if (dataIndex) {
101+
flattenedTitle += `-${dataIndex}`;
102+
}
103+
91104
return slugify(flattenedTitle, { lower: true });
92105
}

src/lib/fetch-docs.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,17 +149,47 @@ function parseAnchorLinks({
149149
type: string;
150150
value: string;
151151
}[];
152+
data?: any;
152153
};
154+
153155
return () => {
156+
// We need to keep track of how many times that we have encountered a
157+
// given header ID, as to ensure that we don't run into any conflicts.
158+
// If there is a conflict, the sidecar will run into issues, and only
159+
// the first header will be able to be deep-linked to.
160+
//
161+
// In the event that we encounter a duplicate Header ID, we'll simply
162+
// add a suffix to the ID to make it unique. e.g. if there are two headers
163+
// with the same name "Foo", the first ID will be "foo", while the second
164+
// will be "foo-2".
165+
const encounteredIDs = new Map<string, number>();
166+
154167
return function (node: Node) {
155168
visit(node, "heading", (node: Node) => {
156169
if (node.type === "heading") {
157170
let headingNode = node as HeadingNode;
158171
if (headingNode.children.length > 0) {
159172
const text = headingNode.children.map((v) => v.value).join("");
173+
const baseId = slugify(text.toLowerCase());
174+
175+
// If this is not the first occurrence, add a data-index attribute
176+
const encounteredCount = (encounteredIDs.get(baseId) || 0) + 1;
177+
encounteredIDs.set(baseId, encounteredCount);
178+
if (encounteredCount >= 2) {
179+
if (!headingNode.data) {
180+
headingNode.data = {};
181+
}
182+
headingNode.data.hProperties = {
183+
...headingNode.data.hProperties,
184+
"data-index": encounteredCount.toString(),
185+
};
186+
}
187+
const resolvedID =
188+
encounteredCount >= 2 ? `${baseId}-${encounteredCount}` : baseId;
189+
160190
pageHeaders.push({
161191
depth: headingNode.depth,
162-
id: slugify(text.toLowerCase()),
192+
id: resolvedID,
163193
title: text,
164194
});
165195
}

0 commit comments

Comments
 (0)