Improve DOCX export styling and include missing fields
- Add background notes, tags, and final script to the export - Use shaded section labels, accent color, and a callout-style scripture box - Switch accent color from indigo to teal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+125
-36
@@ -3,10 +3,13 @@ import { createPortal } from 'react-dom';
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import mammoth from 'mammoth';
|
import mammoth from 'mammoth';
|
||||||
import {
|
import {
|
||||||
|
AlignmentType,
|
||||||
|
BorderStyle,
|
||||||
Document,
|
Document,
|
||||||
HeadingLevel,
|
HeadingLevel,
|
||||||
Packer,
|
Packer,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
|
ShadingType,
|
||||||
Table,
|
Table,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
@@ -510,6 +513,23 @@ export function createParagraphsFromText(text) {
|
|||||||
.map((line) => new Paragraph({ children: [new TextRun({ text: line })] }));
|
.map((line) => new Paragraph({ children: [new TextRun({ text: line })] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOCX_ACCENT = '0F766E'; // teal-700
|
||||||
|
|
||||||
|
export function sectionLabel(text) {
|
||||||
|
return new Paragraph({
|
||||||
|
spacing: { before: 240, after: 80 },
|
||||||
|
shading: { type: ShadingType.CLEAR, fill: 'F0FDFA' },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: text.toUpperCase(),
|
||||||
|
bold: true,
|
||||||
|
color: DOCX_ACCENT,
|
||||||
|
size: 20,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function renderVerseContent(content) {
|
export function renderVerseContent(content) {
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
return content;
|
return content;
|
||||||
@@ -2171,71 +2191,140 @@ const App = () => {
|
|||||||
const children = [
|
const children = [
|
||||||
new Paragraph({ text: project.title, heading: HeadingLevel.TITLE }),
|
new Paragraph({ text: project.title, heading: HeadingLevel.TITLE }),
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
text: `${project.translation} — ${buildChapterSummary(project)}`,
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: `${project.translation} — ${buildChapterSummary(project)}`,
|
||||||
|
italics: true,
|
||||||
|
color: '64748B',
|
||||||
|
}),
|
||||||
|
],
|
||||||
spacing: { after: 300 },
|
spacing: { after: 300 },
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const noBorder = { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' };
|
||||||
|
|
||||||
project.chapters.forEach((ch, chapterIndex) => {
|
project.chapters.forEach((ch, chapterIndex) => {
|
||||||
children.push(new Paragraph({ text: `${ch.book} ${ch.chapter}`, heading: HeadingLevel.HEADING_1 }));
|
children.push(new Paragraph({
|
||||||
|
text: `${ch.book} ${ch.chapter}`,
|
||||||
|
heading: HeadingLevel.HEADING_1,
|
||||||
|
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: DOCX_ACCENT, space: 4 } },
|
||||||
|
spacing: { before: 360, after: 160 },
|
||||||
|
}));
|
||||||
ch.chunks.forEach((chunk) => {
|
ch.chunks.forEach((chunk) => {
|
||||||
const scriptureHeading = formatChunkReference(project, chapterIndex, chunk, '-');
|
const scriptureHeading = formatChunkReference(project, chapterIndex, chunk, '-');
|
||||||
|
|
||||||
children.push(new Paragraph({ text: scriptureHeading, heading: HeadingLevel.HEADING_2 }));
|
const headingRuns = [new TextRun({ text: scriptureHeading, bold: true, size: 28, color: DOCX_ACCENT })];
|
||||||
getChunkVerseEntries(project, chapterIndex, chunk).forEach((verse) => {
|
if (chunk.episodeNumber || chunk.episodeTitle) {
|
||||||
children.push(new Paragraph({
|
headingRuns.push(new TextRun({
|
||||||
|
text: ` (Ep. ${chunk.episodeNumber || '—'}${chunk.episodeTitle ? `: ${chunk.episodeTitle}` : ''})`,
|
||||||
|
italics: true,
|
||||||
|
size: 22,
|
||||||
|
color: '64748B',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
children.push(new Paragraph({ children: headingRuns, spacing: { before: 320, after: 120 } }));
|
||||||
|
|
||||||
|
// Scripture text in a shaded, left-bordered table cell for a "callout" look
|
||||||
|
const scriptureParas = getChunkVerseEntries(project, chapterIndex, chunk).map((verse) => new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun({ text: `${verse.chapter}:${verse.number} `, bold: true, color: DOCX_ACCENT }),
|
||||||
|
new TextRun({ text: verse.text, italics: true }),
|
||||||
|
],
|
||||||
|
spacing: { after: 80 },
|
||||||
|
}));
|
||||||
|
children.push(new Table({
|
||||||
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||||
|
rows: [
|
||||||
|
new TableRow({
|
||||||
children: [
|
children: [
|
||||||
new TextRun({ text: `${verse.chapter}:${verse.number}. `, bold: true }),
|
new TableCell({
|
||||||
new TextRun({ text: verse.text }),
|
shading: { type: ShadingType.CLEAR, fill: 'F8FAFC' },
|
||||||
|
margins: { top: 120, bottom: 120, left: 180, right: 180 },
|
||||||
|
borders: {
|
||||||
|
...{ top: noBorder, right: noBorder, bottom: noBorder },
|
||||||
|
left: { style: BorderStyle.SINGLE, size: 24, color: DOCX_ACCENT },
|
||||||
|
},
|
||||||
|
children: scriptureParas,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}));
|
}),
|
||||||
});
|
],
|
||||||
|
}));
|
||||||
|
children.push(new Paragraph({ text: '', spacing: { after: 120 } }));
|
||||||
|
|
||||||
children.push(new Paragraph({ text: 'OBSERVATION:', spacing: { before: 240, after: 120 }, bold: true }));
|
if (chunk.generalNotes?.trim()) {
|
||||||
children.push(...createParagraphsFromText(chunk.observation || 'No observation.'));
|
children.push(sectionLabel('Background / General Notes'));
|
||||||
children.push(new Paragraph({ text: 'INTERPRETATION:', spacing: { before: 240, after: 120 }, bold: true }));
|
children.push(...createParagraphsFromText(chunk.generalNotes));
|
||||||
children.push(...createParagraphsFromText(chunk.interpretation || 'No interpretation.'));
|
}
|
||||||
children.push(new Paragraph({ text: 'APPLICATION:', spacing: { before: 240, after: 120 }, bold: true }));
|
|
||||||
children.push(...createParagraphsFromText(chunk.application || 'No application.'));
|
children.push(sectionLabel('Observation'));
|
||||||
|
children.push(...createParagraphsFromText(chunk.observation || 'No observation.'));
|
||||||
if ((chunk.crossReferences ?? []).length > 0) {
|
children.push(sectionLabel('Interpretation'));
|
||||||
children.push(new Paragraph({ text: 'CROSS-REFERENCES:', spacing: { before: 240, after: 120 }, bold: true }));
|
children.push(...createParagraphsFromText(chunk.interpretation || 'No interpretation.'));
|
||||||
children.push(new Paragraph({ text: chunk.crossReferences.join(', ') }));
|
children.push(sectionLabel('Application'));
|
||||||
|
children.push(...createParagraphsFromText(chunk.application || 'No application.'));
|
||||||
|
|
||||||
|
if ((chunk.tags ?? []).length > 0) {
|
||||||
|
children.push(sectionLabel('Tags'));
|
||||||
|
children.push(new Paragraph({
|
||||||
|
children: chunk.tags.map((tag, i) => new TextRun({
|
||||||
|
text: i === 0 ? `#${tag}` : ` #${tag}`,
|
||||||
|
bold: true,
|
||||||
|
color: DOCX_ACCENT,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((chunk.crossReferences ?? []).length > 0) {
|
||||||
|
children.push(sectionLabel('Cross-References'));
|
||||||
|
children.push(new Paragraph({ text: chunk.crossReferences.join(' • ') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(new Paragraph({ text: 'GREEK WORDS:', spacing: { before: 240, after: 120 }, bold: true }));
|
|
||||||
if (chunk.greekWords.length > 0) {
|
if (chunk.greekWords.length > 0) {
|
||||||
|
children.push(sectionLabel('Word Studies'));
|
||||||
|
const headerCellStyle = (label) => new TableCell({
|
||||||
|
width: { size: 20, type: WidthType.PERCENTAGE },
|
||||||
|
shading: { type: ShadingType.CLEAR, fill: DOCX_ACCENT },
|
||||||
|
children: [new Paragraph({ children: [new TextRun({ text: label, bold: true, color: 'FFFFFF' })] })],
|
||||||
|
});
|
||||||
const tableRows = [
|
const tableRows = [
|
||||||
new TableRow({
|
new TableRow({
|
||||||
tableHeader: true,
|
tableHeader: true,
|
||||||
children: ['Strong', 'Greek', 'Transliteration', 'Part of Speech', 'Short Definition'].map((label) =>
|
children: ['Strong', 'Greek', 'Transliteration', 'Part of Speech', 'Short Definition'].map(headerCellStyle),
|
||||||
new TableCell({
|
|
||||||
width: { size: 20, type: WidthType.PERCENTAGE },
|
|
||||||
children: [new Paragraph({ text: label, bold: true })],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
...chunk.greekWords.map((word) => new TableRow({
|
...chunk.greekWords.map((word, i) => new TableRow({
|
||||||
children: [
|
children: [
|
||||||
new TableCell({ children: [new Paragraph(word.strongNumber || '')] }),
|
new TableCell({ shading: i % 2 ? { type: ShadingType.CLEAR, fill: 'F8FAFC' } : undefined, children: [new Paragraph(word.strongNumber || '')] }),
|
||||||
new TableCell({ children: [new Paragraph(word.lexeme || '')] }),
|
new TableCell({ shading: i % 2 ? { type: ShadingType.CLEAR, fill: 'F8FAFC' } : undefined, children: [new Paragraph(word.lexeme || '')] }),
|
||||||
new TableCell({ children: [new Paragraph(word.transliteration || '')] }),
|
new TableCell({ shading: i % 2 ? { type: ShadingType.CLEAR, fill: 'F8FAFC' } : undefined, children: [new Paragraph(word.transliteration || '')] }),
|
||||||
new TableCell({ children: [new Paragraph(word.partOfSpeech || '')] }),
|
new TableCell({ shading: i % 2 ? { type: ShadingType.CLEAR, fill: 'F8FAFC' } : undefined, children: [new Paragraph(word.partOfSpeech || '')] }),
|
||||||
new TableCell({ children: [new Paragraph(word.shortDefinition || '')] }),
|
new TableCell({ shading: i % 2 ? { type: ShadingType.CLEAR, fill: 'F8FAFC' } : undefined, children: [new Paragraph(word.shortDefinition || '')] }),
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
children.push(new Table({ rows: tableRows, width: { size: 100, type: WidthType.PERCENTAGE } }));
|
children.push(new Table({ rows: tableRows, width: { size: 100, type: WidthType.PERCENTAGE } }));
|
||||||
chunk.greekWords.forEach((word) => {
|
chunk.greekWords.forEach((word) => {
|
||||||
if (word.definitionHtml) {
|
if (word.definitionHtml) {
|
||||||
children.push(new Paragraph({ text: `${word.strongNumber} — ${word.lexeme || ''}`, spacing: { before: 180, after: 120 }, bold: true }));
|
children.push(new Paragraph({
|
||||||
|
spacing: { before: 180, after: 80 },
|
||||||
|
children: [new TextRun({ text: `${word.strongNumber} — ${word.lexeme || ''}`, bold: true, color: DOCX_ACCENT })],
|
||||||
|
}));
|
||||||
children.push(...createParagraphsFromText(htmlToPlainText(word.definitionHtml)));
|
children.push(...createParagraphsFromText(htmlToPlainText(word.definitionHtml)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
children.push(new Paragraph('No Greek word notes.'));
|
|
||||||
}
|
}
|
||||||
children.push(new Paragraph({ text: '', spacing: { after: 300 } }));
|
|
||||||
|
if (chunk.finalScript?.trim()) {
|
||||||
|
children.push(sectionLabel('Final Script'));
|
||||||
|
children.push(...createParagraphsFromText(chunk.finalScript));
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(new Paragraph({
|
||||||
|
text: '',
|
||||||
|
spacing: { after: 200 },
|
||||||
|
border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: 'E2E8F0', space: 8 } },
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user