183 lines
5.4 KiB
TypeScript
183 lines
5.4 KiB
TypeScript
|
|
import fs from "fs"
|
|||
|
|
import path from "path"
|
|||
|
|
import { Part, Chapter, Section } from "./book-data"
|
|||
|
|
|
|||
|
|
const BOOK_DIR = path.join(process.cwd(), "book")
|
|||
|
|
|
|||
|
|
const CHINESE_NUM_MAP: Record<string, string> = {
|
|||
|
|
一: "01",
|
|||
|
|
二: "02",
|
|||
|
|
三: "03",
|
|||
|
|
四: "04",
|
|||
|
|
五: "05",
|
|||
|
|
六: "06",
|
|||
|
|
七: "07",
|
|||
|
|
八: "08",
|
|||
|
|
九: "09",
|
|||
|
|
十: "10",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const SUBTITLES: Record<string, string> = {
|
|||
|
|
"真实的人": "人性观察与社交逻辑",
|
|||
|
|
"真实的行业": "社会运作的底层规则",
|
|||
|
|
"真实的错误": "错过机会比失败更贵",
|
|||
|
|
"真实的赚钱": "所有行业的杠杆结构",
|
|||
|
|
"真实的社会": "人与系统的关系",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parsePartFolderName(folderName: string): { number: string; title: string } | null {
|
|||
|
|
// Example: _第一篇|真实的人
|
|||
|
|
const match = folderName.match(/_第([一二三四五六七八九十]+)篇|(.+)/)
|
|||
|
|
if (match) {
|
|||
|
|
return {
|
|||
|
|
number: CHINESE_NUM_MAP[match[1]] || match[1],
|
|||
|
|
title: match[2],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseChapterFolderName(folderName: string): { id: string; title: string } | null {
|
|||
|
|
// Example: 第1章|人与人之间的底层逻辑
|
|||
|
|
const match = folderName.match(/第(\d+)章|(.+)/)
|
|||
|
|
if (match) {
|
|||
|
|
return {
|
|||
|
|
id: match[1],
|
|||
|
|
title: match[2],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseSectionFileName(fileName: string): { id: string; title: string } | null {
|
|||
|
|
// Example: 1.1 自行车荷总:一个行业做到极致是什么样.md
|
|||
|
|
if (!fileName.endsWith(".md")) return null
|
|||
|
|
const name = fileName.replace(".md", "")
|
|||
|
|
const match = name.match(/^([\d.]+)\s+(.+)/)
|
|||
|
|
if (match) {
|
|||
|
|
return {
|
|||
|
|
id: match[1],
|
|||
|
|
title: match[2],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Fallback for files without number prefix if any (though project seems to use them)
|
|||
|
|
return {
|
|||
|
|
id: fileName,
|
|||
|
|
title: name,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getBookStructure(): Part[] {
|
|||
|
|
if (!fs.existsSync(BOOK_DIR)) {
|
|||
|
|
return []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const partFolders = fs.readdirSync(BOOK_DIR).filter((f) => {
|
|||
|
|
const fullPath = path.join(BOOK_DIR, f)
|
|||
|
|
return fs.statSync(fullPath).isDirectory() && f.startsWith("_")
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const parts: Part[] = partFolders
|
|||
|
|
.map((folderName) => {
|
|||
|
|
const parsed = parsePartFolderName(folderName)
|
|||
|
|
if (!parsed) return null
|
|||
|
|
|
|||
|
|
const partPath = path.join(BOOK_DIR, folderName)
|
|||
|
|
const chapterFolders = fs.readdirSync(partPath).filter((f) => {
|
|||
|
|
const fullPath = path.join(partPath, f)
|
|||
|
|
return fs.statSync(fullPath).isDirectory() && f.startsWith("第")
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const chapters: Chapter[] = chapterFolders
|
|||
|
|
.map((chapterFolderName) => {
|
|||
|
|
const parsedChapter = parseChapterFolderName(chapterFolderName)
|
|||
|
|
if (!parsedChapter) return null
|
|||
|
|
|
|||
|
|
const chapterPath = path.join(partPath, chapterFolderName)
|
|||
|
|
const sectionFiles = fs.readdirSync(chapterPath).filter((f) => f.endsWith(".md"))
|
|||
|
|
|
|||
|
|
const sections: Section[] = sectionFiles
|
|||
|
|
.map((fileName) => {
|
|||
|
|
const parsedSection = parseSectionFileName(fileName)
|
|||
|
|
if (!parsedSection) return null
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: parsedSection.id,
|
|||
|
|
title: parsedSection.title,
|
|||
|
|
price: 1, // Default price
|
|||
|
|
isFree: parsedSection.id.endsWith(".1"), // Assuming first section of chapter is free logic for now, or use logic from requirement
|
|||
|
|
filePath: path.relative(process.cwd(), path.join(chapterPath, fileName)),
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.filter((s): s is Section => s !== null)
|
|||
|
|
.sort((a, b) => {
|
|||
|
|
// Numeric sort for section IDs (1.1, 1.2, 1.10)
|
|||
|
|
const partsA = a.id.split('.').map(Number);
|
|||
|
|
const partsB = b.id.split('.').map(Number);
|
|||
|
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|||
|
|
const valA = partsA[i] || 0;
|
|||
|
|
const valB = partsB[i] || 0;
|
|||
|
|
if (valA !== valB) return valA - valB;
|
|||
|
|
}
|
|||
|
|
return 0;
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: parsedChapter.id,
|
|||
|
|
title: parsedChapter.title,
|
|||
|
|
sections,
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.filter((c): c is Chapter => c !== null)
|
|||
|
|
.sort((a, b) => Number(a.id) - Number(b.id))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: parsed.number,
|
|||
|
|
number: parsed.number,
|
|||
|
|
title: parsed.title,
|
|||
|
|
subtitle: SUBTITLES[parsed.title] || "",
|
|||
|
|
chapters,
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.filter((p): p is Part => p !== null)
|
|||
|
|
.sort((a, b) => Number(a.number) - Number(b.number))
|
|||
|
|
|
|||
|
|
return parts
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getSectionBySlug(slug: string): Section | null {
|
|||
|
|
const parts = getBookStructure()
|
|||
|
|
for (const part of parts) {
|
|||
|
|
for (const chapter of part.chapters) {
|
|||
|
|
for (const section of chapter.sections) {
|
|||
|
|
if (section.id === slug) {
|
|||
|
|
return section
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getChapterBySectionSlug(slug: string): { part: Part; chapter: Chapter } | null {
|
|||
|
|
const parts = getBookStructure()
|
|||
|
|
for (const part of parts) {
|
|||
|
|
for (const chapter of part.chapters) {
|
|||
|
|
for (const section of chapter.sections) {
|
|||
|
|
if (section.id === slug) {
|
|||
|
|
return { part, chapter }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getSectionContent(filePath: string): string {
|
|||
|
|
const fullPath = path.join(process.cwd(), filePath)
|
|||
|
|
if (!fs.existsSync(fullPath)) {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return fs.readFileSync(fullPath, "utf-8")
|
|||
|
|
}
|