Skip to content

fix: duplicating steps headings in docs toc #3502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions content/docs/ai/ai-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ enableTableOfContents: true
updatedOn: '2025-03-07T21:44:32.252Z'
---

## First Issue

First Issue

<Steps>

## Requirements

Requirements

## Solution

Solution

</Steps>

## Second Issue

Second Issue

<Steps>

## Requirements

Requirements

## Solution

Solution

</Steps>

This guide collects resources for building AI applications with Neon Postgres. You'll find core concepts, starter applications, framework integrations, and deployment guides. Use these resources to build applications like RAG chatbots, semantic search engines, or custom AI tools.

<CTA title="Start building AI apps with Neon" description="Sign up for Neon Postgres and jumpstart your AI application with our starter apps and resources." buttonText="Sign Up" buttonUrl="https://console.neon.tech/signup" />
Expand Down
3 changes: 1 addition & 2 deletions src/components/shared/anchor-heading/anchor-heading.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ const AnchorHeading = (Tag) => {
<a
className="anchor absolute right-0 top-1/2 flex h-full -translate-y-1/2 translate-x-full items-center justify-center px-2 no-underline opacity-0 transition-opacity duration-200 hover:border-none hover:opacity-100 group-hover:opacity-100 sm:hidden"
href={`#${id}`}
tabIndex="-1"
aria-hidden
aria-label={`Link to ${extractText(children)}`}
>
<HashIcon
className={clsx(Tag === 'h2' && 'w-3.5', Tag === 'h3' && 'w-3', 'text-green-45')}
Expand Down
51 changes: 27 additions & 24 deletions src/utils/get-table-of-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,29 @@ const parseMDXHeading = require('./parse-mdx-heading');

const buildNestedToc = (headings, currentLevel, currentIndex = 0) => {
const toc = [];
let numberedStep = 0;
let localIndex = currentIndex;
let currentStepsIndex = -1;

while (headings.length > 0) {
const currentHeading = headings[0];

// Handle object format
const { isNumbered, stepsIndex } = currentHeading;
const { numberedStep } = currentHeading;
const depthMatch = currentHeading.title.match(/^#+/);
const depth = (depthMatch ? depthMatch[0].length : 1) - 1;
const title = currentHeading.title.replace(/(#+)\s/, '');

const titleWithInlineCode = title.replace(/`([^`]+)`/g, '<code>$1</code>');

if (depth === currentLevel) {
if (isNumbered && stepsIndex !== currentStepsIndex) {
numberedStep = 0;
currentStepsIndex = stepsIndex;
}

const tocItem = {
title: titleWithInlineCode,
id: slugify(title, { lower: true, strict: true, remove: /[*+~.()'"!:@]/g }),
level: depth,
numberedStep: isNumbered ? numberedStep + 1 : null,
numberedStep,
index: localIndex,
};

localIndex += 1;

if (isNumbered) {
numberedStep += 1;
}

headings.shift();

if (headings.length > 0) {
Expand Down Expand Up @@ -71,6 +59,7 @@ const buildNestedToc = (headings, currentLevel, currentIndex = 0) => {
const getTableOfContents = (content) => {
const mdxComponentRegex = /<(\w+)\/>/g;
let match;
let newContent = content;
// check if the content has any mdx shared components
while ((match = mdxComponentRegex.exec(content)) !== null) {
const componentName = match[1];
Expand All @@ -81,38 +70,52 @@ const getTableOfContents = (content) => {
// Check if the MD file exists
if (fs.existsSync(mdFilePath)) {
const mdContent = fs.readFileSync(mdFilePath, 'utf8');
content = content.replace(new RegExp(`<${componentName}\/>`, 'g'), mdContent);
newContent = newContent.replace(new RegExp(`<${componentName}\/>`, 'g'), mdContent);
}
}

const codeBlockRegex = /```[\s\S]*?```/g;
const headingRegex = /^(#+)\s(.*)$/gm;
const contentWithoutCodeBlocks = content.replace(codeBlockRegex, '');
const contentWithoutCodeBlocks = newContent.replace(codeBlockRegex, '');

// Get all headings first
const allHeadings = contentWithoutCodeBlocks.match(headingRegex) || [];

// Find steps sections
// Find steps sections and headings
const stepsRegex = /<Steps>([\s\S]*?)<\/Steps>/g;
const stepsMatches = [...content.matchAll(stepsRegex)];
const stepsHeadings = stepsMatches.map((match) => {
const stepsContent = match[0];
const stepsHeading = stepsContent.match(/^##\s(.*)$/gm);
return stepsHeading;
});

let stepsIndex = 0;
let numberedStep = 0;

// Convert headings to objects while preserving order
const arr = allHeadings.map((heading) => {
// Check if this heading is inside any Steps section and is h2
let stepsIndex = -1;
const isInSteps = stepsMatches.some((match, index) => {
const stepsContent = match[0];
if (stepsContent.includes(heading) && /^##\s(.*)$/gm.test(heading)) {
stepsIndex = index;
const isInSteps = stepsHeadings.some((matchArray, index) => {
const headingIndex = matchArray ? matchArray.indexOf(heading) : -1;
if (headingIndex !== -1) {
// Remove only this specific heading from the array
matchArray.splice(headingIndex, 1);

if (stepsIndex === index) {
numberedStep += 1;
} else {
stepsIndex = index;
numberedStep = 1;
}
return true;
}
return false;
});

return {
title: heading,
isNumbered: isInSteps,
stepsIndex: isInSteps ? stepsIndex : -1,
numberedStep: isInSteps ? numberedStep : null,
};
});

Expand Down