Files
pos-system/apps/web-docs/src/lib/docs-generator.ts
Ho Ngoc Hai 3f5d9715dc feat(docs): Enhance deployment guides with improved formatting and visual elements
- Updated the introductory notes in both English and Vietnamese deployment guides for clarity.
- Changed subgraph titles in Mermaid diagrams for consistency and improved readability.
- Added color styling to Mermaid diagram elements for better visual distinction.
2026-01-08 15:45:18 +07:00

257 lines
9.1 KiB
TypeScript

// EN: Server-side utility to generate docs navigation from filesystem
// VI: Utility phía server để tạo docs navigation từ filesystem
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import type { DocsLocale, NavItem, NavGroup } from './docs-types';
interface DocFile {
slug: string;
title: string;
category: string;
filePath: string;
}
// EN: Get absolute path to docs directory
// VI: Lấy đường dẫn tuyệt đối tới thư mục docs
const DOCS_ROOT = path.join(process.cwd(), '../../docs');
// EN: Category display order and labels
// VI: Thứ tự hiển thị và nhãn cho các danh mục
const CATEGORY_CONFIG: Record<string, { order: number; label: { en: string; vi: string } }> = {
'guides': { order: 1, label: { en: 'Guides', vi: 'Hướng dẫn' } },
'architecture': { order: 2, label: { en: 'Architecture', vi: 'Kiến trúc' } },
'skills': { order: 3, label: { en: 'Skills', vi: 'Kỹ năng' } },
'templates': { order: 4, label: { en: 'Templates', vi: 'Mẫu' } },
'api': { order: 5, label: { en: 'API Reference', vi: 'Tài liệu API' } },
'runbooks': { order: 6, label: { en: 'Runbooks', vi: 'Sổ tay vận hành' } },
'onboarding': { order: 7, label: { en: 'Onboarding', vi: 'Hướng dẫn mới' } },
};
// EN: Custom ordering for specific items within categories
// VI: Thứ tự tùy chỉnh cho các items cụ thể trong mỗi category
// Items not listed here will be sorted alphabetically after the ordered ones
const ITEM_ORDER: Record<string, string[]> = {
'guides': [
'getting-started', // EN: Always show getting started first / VI: Luôn hiển thị getting started đầu tiên
'local-development', // EN: Then local setup / VI: Sau đó là local setup
'development', // EN: Then development / VI: Sau đó là development
'deployment', // EN: Then deployment / VI: Sau đó là deployment
// EN: Other items will be alphabetically sorted / VI: Các items khác sẽ được sắp xếp theo alphabet
],
'architecture': [
'system-design', // EN: Overview first / VI: Overview trước
'service-communication', // EN: Then communication / VI: Sau đó là communication
],
'skills': [
'project-rules', // EN: Project rules first / VI: Project rules trước
'comment-code', // EN: Then coding standards / VI: Sau đó là coding standards
'api-design', // EN: Then API design / VI: Sau đó là API design
],
};
/**
* EN: Extract title from markdown file
* VI: Trích xuất tiêu đề từ file markdown
*/
function extractTitle(filePath: string): string {
try {
const content = fs.readFileSync(filePath, 'utf-8');
// EN: Try to parse frontmatter first
// VI: Thử parse frontmatter trước
try {
const { data } = matter(content);
if (data.name) return data.name;
if (data.title) return data.title;
} catch (e) {
// Ignore frontmatter parsing errors
}
// EN: Fallback to first H1 heading
// VI: Fallback về H1 heading đầu tiên
const h1Match = content.match(/^#\s+(.+)$/m);
if (h1Match) {
// EN: Remove bilingual suffix if exists (e.g., "Title / Tiêu đề" -> "Title")
// VI: Xóa hậu tố bilingual nếu tồn tại
const title = h1Match[1].split('/')[0].trim();
return title;
}
// EN: Final fallback to filename
// VI: Fallback cuối cùng là tên file
const filename = path.basename(filePath, '.md');
return filename
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
} catch (error) {
// EN: If file read fails, use filename
// VI: Nếu đọc file thất bại, dùng tên file
const filename = path.basename(filePath, '.md');
return filename
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
}
/**
* EN: Scan docs directory for a specific locale
* VI: Quét thư mục docs cho một locale cụ thể
*/
function scanDocsForLocale(locale: DocsLocale): DocFile[] {
const localeDir = path.join(DOCS_ROOT, locale);
const docs: DocFile[] = [];
if (!fs.existsSync(localeDir)) {
return docs;
}
// EN: Read all category directories
// VI: Đọc tất cả thư mục danh mục
const categories = fs.readdirSync(localeDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const category of categories) {
const categoryPath = path.join(localeDir, category);
const files = fs.readdirSync(categoryPath)
.filter(file => file.endsWith('.md') && file !== 'README.md');
for (const file of files) {
const filePath = path.join(categoryPath, file);
const slug = `${category}/${path.basename(file, '.md')}`;
const title = extractTitle(filePath);
docs.push({
slug,
title,
category,
filePath
});
}
}
return docs;
}
/**
* EN: Generate navigation structure with bilingual support
* VI: Tạo cấu trúc navigation với hỗ trợ đa ngôn ngữ
*/
export function generateDocsNavigation(): NavGroup[] {
const enDocs = scanDocsForLocale('en');
const viDocs = scanDocsForLocale('vi');
// EN: Create a map of slug to titles in both languages
// VI: Tạo map từ slug tới tiêu đề ở cả hai ngôn ngữ
const docTitles = new Map<string, { en: string; vi: string }>();
enDocs.forEach(doc => {
docTitles.set(doc.slug, {
en: doc.title,
vi: doc.title // Default to EN if VI not found
});
});
viDocs.forEach(doc => {
const existing = docTitles.get(doc.slug);
if (existing) {
existing.vi = doc.title;
} else {
docTitles.set(doc.slug, { en: doc.title, vi: doc.title });
}
});
// EN: Group by category
// VI: Nhóm theo danh mục
const groups = new Map<string, NavItem[]>();
docTitles.forEach((labels, slug) => {
const category = slug.split('/')[0];
if (!groups.has(category)) {
groups.set(category, []);
}
groups.get(category)!.push({
id: slug.replace(/\//g, '-'),
slug,
label: labels
});
});
// EN: Convert to NavGroup array with sorting
// VI: Chuyển thành mảng NavGroup với sắp xếp
const navGroups: NavGroup[] = [];
groups.forEach((items, category) => {
const config = CATEGORY_CONFIG[category] || {
order: 999,
label: { en: category.charAt(0).toUpperCase() + category.slice(1), vi: category.charAt(0).toUpperCase() + category.slice(1) }
};
// EN: Sort items by custom order first, then alphabetically
// VI: Sắp xếp items theo thứ tự tùy chỉnh trước, sau đó theo alphabet
const customOrder = ITEM_ORDER[category] || [];
items.sort((a, b) => {
const aSlugName = a.slug.split('/')[1]; // EN: Get filename from slug / VI: Lấy filename từ slug
const bSlugName = b.slug.split('/')[1];
const aIndex = customOrder.indexOf(aSlugName);
const bIndex = customOrder.indexOf(bSlugName);
// EN: If both have custom order, use that / VI: Nếu cả hai có thứ tự tùy chỉnh, dùng nó
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
// EN: Items with custom order come first / VI: Items có thứ tự tùy chỉnh xuất hiện trước
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
// EN: Otherwise sort alphabetically / VI: Ngược lại sắp xếp theo alphabet
return a.label.en.localeCompare(b.label.en);
});
navGroups.push({
id: category,
label: config.label,
items
});
});
// EN: Sort groups by order
// VI: Sắp xếp groups theo thứ tự
navGroups.sort((a, b) => {
const orderA = CATEGORY_CONFIG[a.id]?.order || 999;
const orderB = CATEGORY_CONFIG[b.id]?.order || 999;
return orderA - orderB;
});
return navGroups;
}
/**
* EN: Get list of available doc slugs for a locale
* VI: Lấy danh sách slugs có sẵn cho một locale
*/
export function getAvailableDocs(locale: DocsLocale): string[] {
const docs = scanDocsForLocale(locale);
return docs.map(doc => doc.slug);
}
/**
* EN: Check if a doc exists for a locale
* VI: Kiểm tra xem một doc có tồn tại cho locale không
*/
export function docExists(locale: DocsLocale, slug: string): boolean {
const slugPath = slug.replace(/\//g, path.sep);
const filePath = path.join(DOCS_ROOT, locale, `${slugPath}.md`);
return fs.existsSync(filePath);
}