Files
soul/lib/book-file-system.ts
2026-01-09 11:58:08 +08:00

170 lines
4.7 KiB
TypeScript

import fs from "fs"
import path from "path"
import type { 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 {
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 {
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 {
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],
}
}
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,
isFree: parsedSection.id.endsWith(".1"),
filePath: path.relative(process.cwd(), path.join(chapterPath, fileName)),
}
})
.filter((s): s is Section => s !== null)
.sort((a, b) => {
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
}