separate chapter numbers and titles as export options

This commit is contained in:
TC
2026-06-10 23:01:35 +10:00
parent b7ef99b116
commit ca104870ab
3 changed files with 61 additions and 18 deletions

View File

@@ -370,6 +370,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle('export:projectPdf', async (event, opts: {
romanNumerals: boolean
showChapterTitle: boolean
includeCover: boolean
includeFrontMatter: boolean
pageFrom: number | null
@@ -507,21 +508,35 @@ export function registerIpcHandlers(): void {
})
}
let chapterTitle: string
const fileName = doc.relativePath.split('/').pop()?.replace(/\.md$/, '') ?? doc.relativePath
let headerTitle: string
if (!isFrontMatter && opts.romanNumerals) {
let headingHtml: string
if (!isFrontMatter) {
chapterIndex++
chapterTitle = toRoman(chapterIndex)
headerTitle = toRoman(chapterIndex)
const numeral = opts.romanNumerals ? toRoman(chapterIndex) : null
const title = opts.showChapterTitle ? fileName : null
const safe = (s: string): string => s.replace(/&/g, '&amp;').replace(/</g, '&lt;')
if (numeral && title) {
headingHtml = `<h2>${safe(numeral)}</h2><h3>${safe(title)}</h3>`
headerTitle = `${numeral}${title}`
} else if (numeral) {
headingHtml = `<h2>${safe(numeral)}</h2>`
headerTitle = numeral
} else if (title) {
headingHtml = `<h2>${safe(title)}</h2>`
headerTitle = title
} else {
headingHtml = ''
headerTitle = fileName
}
} else {
chapterTitle = doc.relativePath.split('/').pop()?.replace(/\.md$/, '') ?? doc.relativePath
headerTitle = chapterTitle
headingHtml = `<h2>${fileName.replace(/&/g, '&amp;').replace(/</g, '&lt;')}</h2>`
headerTitle = fileName
}
const safeChapterTitle = chapterTitle.replace(/&/g, '&amp;').replace(/</g, '&lt;')
const contentHtml = await marked(normalize(doc.content))
sections.push({
title: headerTitle,
bodyHtml: `<div class="chapter"><h2>${safeChapterTitle}</h2>${contentHtml}</div>`
bodyHtml: `<div class="chapter">${headingHtml}${contentHtml}</div>`
})
}

View File

@@ -3,21 +3,34 @@ import './Export.css'
export interface ExportOptions {
romanNumerals: boolean
showChapterTitle: boolean
includeCover: boolean
includeFrontMatter: boolean
pageFrom: number | null
pageTo: number | null
}
const STORAGE_KEY = 'exportDialogPrefs'
function loadPrefs(): Pick<ExportOptions, 'romanNumerals' | 'showChapterTitle' | 'includeCover' | 'includeFrontMatter'> {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) return JSON.parse(raw)
} catch { /* ignore */ }
return { romanNumerals: true, showChapterTitle: false, includeCover: true, includeFrontMatter: true }
}
interface Props {
onClose: () => void
onExport: (opts: ExportOptions) => void
}
export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
const [romanNumerals, setRomanNumerals] = useState(true)
const [includeCover, setIncludeCover] = useState(true)
const [includeFrontMatter, setIncludeFrontMatter] = useState(true)
const prefs = loadPrefs()
const [romanNumerals, setRomanNumerals] = useState(prefs.romanNumerals)
const [showChapterTitle, setShowChapterTitle] = useState(prefs.showChapterTitle)
const [includeCover, setIncludeCover] = useState(prefs.includeCover)
const [includeFrontMatter, setIncludeFrontMatter] = useState(prefs.includeFrontMatter)
const [pageFrom, setPageFrom] = useState('')
const [pageTo, setPageTo] = useState('')
const overlayRef = useRef<HTMLDivElement>(null)
@@ -34,10 +47,15 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
if (e.target === overlayRef.current) onClose()
}
const savePrefs = (patch: Partial<typeof prefs>): void => {
const next = { romanNumerals, showChapterTitle, includeCover, includeFrontMatter, ...patch }
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
}
const handleExport = (): void => {
const from = pageFrom.trim() ? parseInt(pageFrom, 10) : null
const to = pageTo.trim() ? parseInt(pageTo, 10) : null
onExport({ romanNumerals, includeCover, includeFrontMatter, pageFrom: from, pageTo: to })
onExport({ romanNumerals, showChapterTitle, includeCover, includeFrontMatter, pageFrom: from, pageTo: to })
}
const pageRangeValid = (): boolean => {
@@ -63,17 +81,27 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
<input
type="checkbox"
checked={romanNumerals}
onChange={e => setRomanNumerals(e.target.checked)}
onChange={e => { setRomanNumerals(e.target.checked); savePrefs({ romanNumerals: e.target.checked }) }}
/>
<span className="export-toggle-label">Roman numeral chapter titles</span>
<span className="export-toggle-hint">I, II, III instead of file names</span>
<span className="export-toggle-label">Roman numeral chapter numbers</span>
<span className="export-toggle-hint">I, II, III before the chapter heading</span>
</label>
<label className="export-toggle">
<input
type="checkbox"
checked={showChapterTitle}
onChange={e => { setShowChapterTitle(e.target.checked); savePrefs({ showChapterTitle: e.target.checked }) }}
/>
<span className="export-toggle-label">Show chapter title</span>
<span className="export-toggle-hint">File name as the chapter heading</span>
</label>
<label className="export-toggle">
<input
type="checkbox"
checked={includeCover}
onChange={e => setIncludeCover(e.target.checked)}
onChange={e => { setIncludeCover(e.target.checked); savePrefs({ includeCover: e.target.checked }) }}
/>
<span className="export-toggle-label">Include cover page</span>
<span className="export-toggle-hint">Author contact block and word count</span>
@@ -83,7 +111,7 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
<input
type="checkbox"
checked={includeFrontMatter}
onChange={e => setIncludeFrontMatter(e.target.checked)}
onChange={e => { setIncludeFrontMatter(e.target.checked); savePrefs({ includeFrontMatter: e.target.checked }) }}
/>
<span className="export-toggle-label">Include front &amp; back matter</span>
<span className="export-toggle-hint">Prologue, Content Warning, Epilogue</span>

File diff suppressed because one or more lines are too long