Files
bilin/src/main/lib/export-content.ts
T
bing c5c0b7329b feat(w3): ship v1.4.0 export matrix, cockpit tasks, and focus ambient
Add ExportMatrixService for txt/md/docx/pdf, extend ExportModal, cockpit outline tasks, focus ambient sound, and E2E-PDF-01 with 134 passing unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:56:53 +08:00

112 lines
3.5 KiB
TypeScript

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(/<p[^>]*>([\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')
}