import { stripHtml } from '../services/word-count' import type { Chapter } from '../../shared/types' import type { ExportMatrixOptions } from '../../shared/export-matrix' import { ChapterRepository } from '../db/repositories/chapter.repo' import { VolumeRepository } from '../db/repositories/volume.repo' import { BookmarkRepository } from '../db/repositories/bookmark.repo' import { SnapshotRepository } from '../db/repositories/snapshot.repo' import type { SqliteDb } from '../db/types' export interface ExportChapterBlock { id: string title: string volumeName: string paragraphs: string[] annotations: string[] hasAiContent: boolean } export function htmlToPlainParagraphs(html: string): string[] { const trimmed = html.trim() if (!trimmed) return [] const pMatches = trimmed.match(/
]*>([\s\S]*?)<\/p>/gi) if (pMatches && pMatches.length > 0) { return pMatches.map((p) => stripHtml(p).trim()).filter(Boolean) } const plain = stripHtml(trimmed) if (!plain.trim()) return [] return plain .split(/\n{2,}/) .map((s) => s.trim()) .filter(Boolean) } export function htmlToMarkdownParagraphs(html: string): string[] { return htmlToPlainParagraphs(html) } export function resolveExportChapters( db: SqliteDb, scope: 'chapter' | 'volume' | 'book', scopeId?: string ): Chapter[] { const chapterRepo = new ChapterRepository(db) const all = chapterRepo.list().sort((a, b) => a.sortOrder - b.sortOrder) if (scope === 'chapter') { if (!scopeId) throw new Error('Chapter scope requires scopeId') const ch = chapterRepo.get(scopeId) return ch ? [ch] : [] } if (scope === 'volume') { if (!scopeId) throw new Error('Volume scope requires scopeId') return all.filter((c) => c.volumeId === scopeId) } return all } export function buildExportBlocks( db: SqliteDb, chapters: Chapter[], options: ExportMatrixOptions = {} ): ExportChapterBlock[] { const volumeRepo = new VolumeRepository(db) const bookmarkRepo = new BookmarkRepository(db) const snapshotRepo = new SnapshotRepository(db) const volumes = new Map(volumeRepo.list().map((v) => [v.id, v.name])) return chapters.map((ch) => { const annotations = options.includeAnnotations === true ? bookmarkRepo.listByChapter(ch.id).map((b) => `[${b.landmarkType}] ${b.label}`) : [] const hasAiContent = options.markAiContent === true && snapshotRepo.list(ch.id).some((s) => s.type === 'ai') return { id: ch.id, title: ch.title, volumeName: (ch.volumeId && volumes.get(ch.volumeId)) || '', paragraphs: htmlToPlainParagraphs(ch.content), annotations, hasAiContent } }) } export function buildTxtExport(blocks: ExportChapterBlock[]): string { return blocks .map((block) => { const lines: string[] = [`# ${block.title}`] if (block.hasAiContent) lines.push('[AI生成内容]', '') lines.push(...block.paragraphs) if (block.annotations.length > 0) { lines.push('', '--- 批注 ---', ...block.annotations.map((a) => `- ${a}`)) } return lines.join('\n') }) .join('\n\n') } export function buildMdExport(blocks: ExportChapterBlock[]): string { return blocks .map((block) => { const lines: string[] = [`# ${block.title}`] if (block.hasAiContent) lines.push('', '> *AI 生成内容*', '') lines.push(...block.paragraphs) if (block.annotations.length > 0) { lines.push('', '## 批注', ...block.annotations.map((a) => `- ${a}`)) } return lines.join('\n') }) .join('\n\n') }