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")
|
||
}
|