feat: ship v1.1.0 writer toolkit with templates, import, export, and inspiration capture
Implement chapter templates, txt/md/docx import, submission export presets, and global inspiration shortcut. Split import handlers into a separate main bundle and fix EditorLayout setTarget loop that broke E2E. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { splitPlainText } from '../../src/main/lib/chapter-splitter'
|
||||
|
||||
const SAMPLE = `前言内容
|
||||
|
||||
# 第一章 开端
|
||||
|
||||
第一段正文。
|
||||
|
||||
# 第二章 发展
|
||||
|
||||
第二段正文。
|
||||
|
||||
# 第三章 结局
|
||||
|
||||
第三段正文。`
|
||||
|
||||
describe('chapter-splitter', () => {
|
||||
it('UT-IMPORT-01: auto split finds 3 chapters', () => {
|
||||
const parts = splitPlainText(SAMPLE, 'auto')
|
||||
expect(parts).toHaveLength(3)
|
||||
expect(parts[0]!.title).toContain('第一章')
|
||||
expect(parts[2]!.title).toContain('第三章')
|
||||
})
|
||||
|
||||
it('paragraph mode splits by blank lines', () => {
|
||||
const text = '标题一\n\n正文一\n\n标题二\n\n正文二'
|
||||
const parts = splitPlainText(text, 'paragraph')
|
||||
expect(parts.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { replaceChapterTemplate, plainTextToEditorHtml } from '../../src/shared/chapter-template'
|
||||
|
||||
const baseCtx = {
|
||||
chapterNumber: 1,
|
||||
chapterTitle: '测试',
|
||||
penName: '笔名',
|
||||
date: '2026-07-08',
|
||||
volumeName: '第一卷',
|
||||
previousChapterTitle: '上一章',
|
||||
outlineItem: '',
|
||||
characterList: '甲,乙'
|
||||
}
|
||||
|
||||
describe('chapter-template', () => {
|
||||
it('UT-TEMPL-01: replaces chapterNumber and chapterTitle', () => {
|
||||
const result = replaceChapterTemplate('第{chapterNumber}章 {chapterTitle}', {
|
||||
...baseCtx,
|
||||
chapterNumber: 5,
|
||||
chapterTitle: '觉醒'
|
||||
})
|
||||
expect(result).toBe('第5章 觉醒')
|
||||
})
|
||||
|
||||
it('UT-TEMPL-02: replaces volumeName', () => {
|
||||
const result = replaceChapterTemplate('卷:{volumeName}', {
|
||||
...baseCtx,
|
||||
volumeName: '第一卷 觉醒'
|
||||
})
|
||||
expect(result).toBe('卷:第一卷 觉醒')
|
||||
})
|
||||
|
||||
it('UT-TEMPL-03: replaces characterList', () => {
|
||||
const result = replaceChapterTemplate('出场:{characterList}', baseCtx)
|
||||
expect(result).toBe('出场:甲,乙')
|
||||
})
|
||||
|
||||
it('plainTextToEditorHtml wraps paragraphs', () => {
|
||||
const html = plainTextToEditorHtml('第一段\n\n第二段')
|
||||
expect(html).toContain('<p>')
|
||||
expect(html).toContain('第一段')
|
||||
expect(html).toContain('第二段')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { readFileSync, mkdtempSync, rmSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { BookRegistryService } from '../../src/main/services/book-registry'
|
||||
import { splitPlainText } from '../../src/main/lib/chapter-splitter'
|
||||
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
|
||||
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
|
||||
import { closeAllBookDbs } from '../../src/main/db/connection'
|
||||
|
||||
const FIXTURE = join(import.meta.dirname, '../fixtures/import-three-chapters.txt')
|
||||
|
||||
describe('Import integration', () => {
|
||||
let dir: string
|
||||
let registry: BookRegistryService
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'bilin-import-'))
|
||||
registry = new BookRegistryService(dir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeAllBookDbs()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('IT-IMPORT-01: split + create book chapters', () => {
|
||||
const parts = splitPlainText(readFileSync(FIXTURE, 'utf8'), 'auto')
|
||||
expect(parts).toHaveLength(3)
|
||||
|
||||
const meta = registry.create({
|
||||
name: '导入测试书',
|
||||
category: '玄幻',
|
||||
createSampleChapter: false
|
||||
})
|
||||
const db = registry.getDb(meta.id)
|
||||
const volId = new VolumeRepository(db).list()[0]!.id
|
||||
const chapterRepo = new ChapterRepository(db)
|
||||
parts.forEach((p, i) => {
|
||||
chapterRepo.create(volId, p.title, i, `<p>${p.body}</p>`)
|
||||
})
|
||||
|
||||
const opened = registry.open(meta.id)
|
||||
expect(opened.chapters).toHaveLength(3)
|
||||
expect(opened.chapters[0]!.title).toContain('第一章')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatChapterForSubmission } from '../../src/main/lib/submission-formatter'
|
||||
import { BUILTIN_SUBMISSION_PRESETS } from '../../src/shared/builtin-templates'
|
||||
|
||||
const preset = BUILTIN_SUBMISSION_PRESETS[0]!
|
||||
|
||||
describe('submission-formatter', () => {
|
||||
it('UT-EXPORT-01: indentParagraphs adds fullwidth spaces', () => {
|
||||
const text = formatChapterForSubmission({
|
||||
chapterNumber: 1,
|
||||
chapterTitle: '测试',
|
||||
contentHtml: '<p>第一段</p><p>第二段</p>',
|
||||
preset
|
||||
})
|
||||
expect(text).toContain(' 第一段')
|
||||
expect(text).toContain(' 第二段')
|
||||
})
|
||||
|
||||
it('UT-EXPORT-02: title format', () => {
|
||||
const text = formatChapterForSubmission({
|
||||
chapterNumber: 1,
|
||||
chapterTitle: '觉醒',
|
||||
contentHtml: '<p>正文</p>',
|
||||
preset
|
||||
})
|
||||
expect(text.startsWith('# 第1章 觉醒')).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user