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:
2026-07-08 13:53:22 +08:00
parent ea4819847f
commit adf877861d
49 changed files with 2450 additions and 87 deletions
+11
View File
@@ -0,0 +1,11 @@
# 第一章 开端
这是第一章的正文内容。
# 第二章 发展
这是第二章的正文内容。
# 第三章 结局
这是第三章的正文内容。
+31
View File
@@ -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)
})
})
+44
View File
@@ -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('第二段')
})
})
+47
View File
@@ -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('第一章')
})
})
+28
View File
@@ -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)
})
})