mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 09:37:22 +00:00
feat(docs): added generating tutorial and docs json files
Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
This commit is contained in:
parent
50676498de
commit
1efe2ec1a8
4 changed files with 432 additions and 0 deletions
2
website/.gitignore
vendored
2
website/.gitignore
vendored
|
|
@ -11,6 +11,8 @@
|
|||
.docusaurus
|
||||
.cache-loader
|
||||
static/release-notes
|
||||
docs.json
|
||||
tutorials.json
|
||||
|
||||
# Cache
|
||||
.eslintcache
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { resolve } from 'node:path';
|
||||
import { createNotesFiles } from './release-notes-parser';
|
||||
import Storybook from './storybook';
|
||||
import { generateJsonOverviewFile } from './sidebar-content-parser';
|
||||
|
||||
const lightCodeTheme = require('prism-react-renderer').themes.github;
|
||||
const darkCodeTheme = require('prism-react-renderer').themes.dracula;
|
||||
|
|
@ -339,6 +340,22 @@ const config = {
|
|||
id: 'tutorial',
|
||||
path: 'tutorial',
|
||||
routeBasePath: 'tutorial',
|
||||
|
||||
// Extract tutorial navigation using the navigation utils
|
||||
/** @param {{ defaultSidebarItemsGenerator: any, [key: string]: any }} param0 */
|
||||
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
|
||||
const sidebarItems = await defaultSidebarItemsGenerator(args);
|
||||
|
||||
// Generate tutorials navigation using the utility function
|
||||
await generateJsonOverviewFile(
|
||||
sidebarItems,
|
||||
'tutorial',
|
||||
'https://podman-desktop.io',
|
||||
'./static/tutorials.json',
|
||||
);
|
||||
|
||||
return sidebarItems;
|
||||
},
|
||||
},
|
||||
],
|
||||
'./src/plugins/github-metadata-plugin.ts',
|
||||
|
|
@ -375,6 +392,19 @@ const config = {
|
|||
sidebarCollapsed: false,
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
editUrl: 'https://github.com/podman-desktop/podman-desktop/tree/main/website',
|
||||
|
||||
// Enhanced function to extract navigation using the navigation utils
|
||||
/** @param {{ defaultSidebarItemsGenerator: any, [key: string]: any }} param0 */
|
||||
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
|
||||
// Get the default sidebar items (what Docusaurus normally generates)
|
||||
const sidebarItems = await defaultSidebarItemsGenerator(args);
|
||||
|
||||
// Generate docs navigation using the utility function
|
||||
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
|
||||
|
||||
// Return the original sidebar items (unchanged)
|
||||
return sidebarItems;
|
||||
},
|
||||
},
|
||||
blog: {
|
||||
blogTitle: 'Podman Desktop blog!',
|
||||
|
|
|
|||
304
website/sidebar-content-parser.spec.ts
Normal file
304
website/sidebar-content-parser.spec.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { extractNavigationFromSidebar, generateJsonOverviewFile } from './sidebar-content-parser';
|
||||
|
||||
// Mock the writeFile function
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
writeFile: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('extractNavigationFromSidebar', () => {
|
||||
test('should extract navigation items from simple doc items', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'running-a-kubernetes-cluster',
|
||||
label: 'Running a Kubernetes cluster',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/docs/intro',
|
||||
},
|
||||
{
|
||||
name: 'Running a kubernetes cluster',
|
||||
url: 'https://podman-desktop.io/docs/running-a-kubernetes-cluster',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle items without labels by using id and formatting it', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'deploying-a-kubernetes-application',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Deploying a kubernetes application',
|
||||
url: 'https://podman-desktop.io/docs/deploying-a-kubernetes-application',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle tutorial index case correctly', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'index',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'tutorial');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/tutorial',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle category items recursively', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'category' as const,
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'installation',
|
||||
label: 'Installation',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/docs/intro',
|
||||
},
|
||||
{
|
||||
name: 'Installation',
|
||||
url: 'https://podman-desktop.io/docs/installation',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should format names correctly by replacing hyphens with spaces and capitalizing first letter', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'getting-started-with-compose',
|
||||
label: 'Getting started with Compose',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'running-an-ai-application',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Getting started with compose',
|
||||
url: 'https://podman-desktop.io/docs/getting-started-with-compose',
|
||||
},
|
||||
{
|
||||
name: 'Running an AI application',
|
||||
url: 'https://podman-desktop.io/docs/running-an-ai-application',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should keep AI capitalized in names', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'ai-lab-start-recipe',
|
||||
label: 'AI Lab Start Recipe',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'running-an-ai-application',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'ai-powered-tools',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'AI lab start recipe',
|
||||
url: 'https://podman-desktop.io/docs/ai-lab-start-recipe',
|
||||
},
|
||||
{
|
||||
name: 'Running an AI application',
|
||||
url: 'https://podman-desktop.io/docs/running-an-ai-application',
|
||||
},
|
||||
{
|
||||
name: 'AI powered tools',
|
||||
url: 'https://podman-desktop.io/docs/ai-powered-tools',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should skip items with empty names', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: '',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'valid-item',
|
||||
label: 'Valid Item',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Valid item',
|
||||
url: 'https://podman-desktop.io/docs/valid-item',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateJsonOverviewFile', () => {
|
||||
test('should generate JSON file for docs section', async () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith(
|
||||
'./static/docs.json',
|
||||
JSON.stringify([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/docs/intro',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should generate JSON file for tutorial section', async () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'index',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
await generateJsonOverviewFile(sidebarItems, 'tutorial', 'https://podman-desktop.io', './static/tutorials.json');
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith(
|
||||
'./static/tutorials.json',
|
||||
JSON.stringify([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/tutorial',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should sort items alphabetically by name', async () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'zebra',
|
||||
label: 'Zebra',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'apple',
|
||||
label: 'Apple',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'banana',
|
||||
label: 'Banana',
|
||||
},
|
||||
];
|
||||
|
||||
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
|
||||
|
||||
const expectedContent = JSON.stringify([
|
||||
{
|
||||
name: 'Apple',
|
||||
url: 'https://podman-desktop.io/docs/apple',
|
||||
},
|
||||
{
|
||||
name: 'Banana',
|
||||
url: 'https://podman-desktop.io/docs/banana',
|
||||
},
|
||||
{
|
||||
name: 'Zebra',
|
||||
url: 'https://podman-desktop.io/docs/zebra',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith('./static/docs.json', expectedContent);
|
||||
});
|
||||
|
||||
test('should propagate writeFile errors', async () => {
|
||||
const mockError = new Error('Write failed');
|
||||
vi.mocked(writeFile).mockRejectedValueOnce(mockError);
|
||||
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
// Should throw the error
|
||||
await expect(
|
||||
generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json'),
|
||||
).rejects.toThrow('Write failed');
|
||||
});
|
||||
});
|
||||
96
website/sidebar-content-parser.ts
Normal file
96
website/sidebar-content-parser.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
// Type definitions for Docusaurus sidebar items
|
||||
interface DocSidebarItem {
|
||||
type: 'doc';
|
||||
id: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface CategorySidebarItem {
|
||||
type: 'category';
|
||||
label?: string;
|
||||
items: SidebarItem[];
|
||||
}
|
||||
|
||||
type SidebarItem = DocSidebarItem | CategorySidebarItem;
|
||||
|
||||
interface NavigationItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract navigation items from sidebar structure
|
||||
* @param items - Sidebar items from Docusaurus
|
||||
* @param baseUrl - Base URL for the site
|
||||
* @param section - Section type ('docs' or 'tutorial')
|
||||
* @returns Navigation items
|
||||
*/
|
||||
export function extractNavigationFromSidebar(
|
||||
items: SidebarItem[],
|
||||
baseUrl: string,
|
||||
section: 'docs' | 'tutorial' = 'docs',
|
||||
): NavigationItem[] {
|
||||
const navItems: NavigationItem[] = [];
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.type === 'doc') {
|
||||
// Use the label (display name) instead of the id for better readability
|
||||
const rawName = item.label ?? item.id;
|
||||
if (!rawName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace hashes with spaces and capitalize only the first letter of the whole name
|
||||
const processedName = rawName
|
||||
.replace(/-/g, ' ') // Replace hyphens with spaces
|
||||
.replace(/\s+/g, ' ') // Normalize multiple spaces to single space
|
||||
.trim() // Remove leading/trailing spaces
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.replace(/\bai\b/g, 'AI'); // Keep "AI" capitalized (word boundary to avoid partial matches)
|
||||
|
||||
let name = processedName.charAt(0).toUpperCase() + processedName.slice(1);
|
||||
|
||||
// Handle tutorial index case - it should point to base /tutorial URL
|
||||
let url: string;
|
||||
if (section === 'tutorial' && item.id === 'index') {
|
||||
url = `${baseUrl}/tutorial`;
|
||||
name = 'Introduction';
|
||||
} else {
|
||||
url = `${baseUrl}/${section}/${item.id}`;
|
||||
}
|
||||
|
||||
navItems.push({
|
||||
name: name,
|
||||
url: url,
|
||||
});
|
||||
} else if (item.type === 'category' && item.items) {
|
||||
// Recursively extract from categories
|
||||
navItems.push(...extractNavigationFromSidebar(item.items, baseUrl, section));
|
||||
}
|
||||
});
|
||||
|
||||
return navItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JSON overview file for navigation
|
||||
* @param sidebarItems - Sidebar items from Docusaurus
|
||||
* @param section - Section type ('docs' or 'tutorial')
|
||||
* @param baseUrl - Base URL for the site
|
||||
* @param outputPath - Path to output the JSON file
|
||||
*/
|
||||
export async function generateJsonOverviewFile(
|
||||
sidebarItems: SidebarItem[],
|
||||
section: 'docs' | 'tutorial',
|
||||
baseUrl: string,
|
||||
outputPath: string,
|
||||
): Promise<void> {
|
||||
const navItems = extractNavigationFromSidebar(sidebarItems, baseUrl, section);
|
||||
|
||||
await writeFile(
|
||||
outputPath,
|
||||
JSON.stringify(navItems.toSorted((a: NavigationItem, b: NavigationItem) => a.name.localeCompare(b.name))),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue