mirror of
https://github.com/StepanovPlaton/SSAUGenerator.git
synced 2026-10-31 10:18:29 +04:00
Fix DOCX headings, captions and page breaks; polish PPTX layout
- docx: zero paragraph indent for centered headings/captions/figure notes so they no longer shift right (empty <w:ind/> inherited 709 twips) - docx: use pageBreakBefore instead of a separate break paragraph; drop the merger page break to remove the blank page after the title page - docx: heading numbers separated by space instead of tab - docx: bullet marker is the Symbol dash U+F02D - pptx: narrower slide number moved left and bold; content title lower; section title on the right half like title/final
This commit is contained in:
@@ -33,6 +33,10 @@ async function buildDocument(config, { check = false } = {}) {
|
||||
const ctx = buildContext(config);
|
||||
const state = createState(md, sections);
|
||||
|
||||
const titlePage = config.document.titlePage;
|
||||
const hasTitlePage = Boolean(titlePage && fs.existsSync(titlePage));
|
||||
ctx.hasTitlePage = hasTitlePage;
|
||||
|
||||
collectNumbers(sections, ctx, state);
|
||||
await resolvePlantUml(ctx, sections, state);
|
||||
const children = renderSections(ctx, state);
|
||||
@@ -42,8 +46,7 @@ async function buildDocument(config, { check = false } = {}) {
|
||||
const doc = buildDocx(ctx, children);
|
||||
let buffer = await Packer.toBuffer(doc);
|
||||
|
||||
const titlePage = config.document.titlePage;
|
||||
if (titlePage && fs.existsSync(titlePage)) {
|
||||
if (hasTitlePage) {
|
||||
buffer = await mergeDocxBuffers(fs.readFileSync(titlePage), buffer);
|
||||
} else if (titlePage) {
|
||||
state.warnings.push(`Титульник не найден, пропущен: ${titlePage}`);
|
||||
|
||||
@@ -53,7 +53,8 @@ const DEFAULT_CONFIG = {
|
||||
},
|
||||
|
||||
lists: {
|
||||
bulletChar: '-',
|
||||
// Тире маркера — глиф Symbol U+F02D (рисуется шрифтом Symbol).
|
||||
bulletChar: '\uF02D',
|
||||
letterSuffix: ')',
|
||||
numberSuffix: ')',
|
||||
forbiddenLetters: ['ё', 'з', 'й', 'о', 'ч', 'ь', 'ы', 'ъ'],
|
||||
|
||||
@@ -349,9 +349,9 @@ function renderTokens(ctx, tokens, state, opts = {}) {
|
||||
const text = tokens[i + 1] ? tokens[i + 1].content : '';
|
||||
if (mode === 'numbered') {
|
||||
if (level <= 1) {
|
||||
if (!(firstH1 && opts.suppressFirstPageBreak)) out.push(pageBreak());
|
||||
const breakBefore = !(firstH1 && opts.suppressFirstPageBreak);
|
||||
firstH1 = false;
|
||||
out.push(sectionHeading(ctx, text));
|
||||
out.push(sectionHeading(ctx, text, { pageBreakBefore: breakBefore }));
|
||||
} else if (level === 2) {
|
||||
out.push(subsectionHeading(ctx, text));
|
||||
} else {
|
||||
@@ -399,7 +399,9 @@ function renderTokens(ctx, tokens, state, opts = {}) {
|
||||
function renderSections(ctx, state) {
|
||||
resetListInstances();
|
||||
const out = [];
|
||||
let first = true;
|
||||
// При наличии титульного листа первая часть тоже должна начинаться
|
||||
// с новой страницы (титульник склеивается перед документом).
|
||||
let first = !ctx.hasTitlePage;
|
||||
|
||||
for (const section of state.sections) {
|
||||
const entry = section.entry;
|
||||
|
||||
@@ -44,6 +44,10 @@ function alignmentOf(name) {
|
||||
|
||||
// ── базовые абзацы ────────────────────────────────────────────────
|
||||
|
||||
// Явное обнуление абзацного отступа. Пустой <w:ind/> наследует отступ
|
||||
// базового стиля, поэтому для центрированных абзацев задаём нули явно.
|
||||
const NO_INDENT = { left: 0, firstLine: 0 };
|
||||
|
||||
function textRun(ctx, text, opts = {}) {
|
||||
const t = ctx.typography;
|
||||
const run = {
|
||||
@@ -73,7 +77,7 @@ function paraRuns(ctx, runs, opts = {}) {
|
||||
return new Paragraph({
|
||||
children: runs,
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 },
|
||||
indent: opts.noFirstIndent ? {} : { firstLine: ctx.typography.indentFirst },
|
||||
indent: opts.noFirstIndent ? NO_INDENT : { firstLine: ctx.typography.indentFirst },
|
||||
alignment: opts.alignment || AlignmentType.BOTH,
|
||||
...opts.extra,
|
||||
});
|
||||
@@ -87,40 +91,37 @@ function emptyLine(ctx) {
|
||||
return new Paragraph({
|
||||
children: [textRun(ctx, ' ')],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 },
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
});
|
||||
}
|
||||
|
||||
// ── заголовки ─────────────────────────────────────────────────────
|
||||
|
||||
function structHeading(ctx, title, opts = {}) {
|
||||
const elements = [];
|
||||
if (opts.pageBreakBefore !== false) elements.push(pageBreak());
|
||||
const cfg = ctx.config.headings.struct;
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
children: [textRun(ctx, String(title).toUpperCase())],
|
||||
spacing: {
|
||||
...ctx.typography.lineSpacing,
|
||||
before: cfg.spacingBefore,
|
||||
after: cfg.spacingAfter,
|
||||
},
|
||||
alignment: alignmentOf(cfg.align),
|
||||
indent: {},
|
||||
})
|
||||
);
|
||||
return elements;
|
||||
return new Paragraph({
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
pageBreakBefore: opts.pageBreakBefore === true,
|
||||
children: [textRun(ctx, String(title).toUpperCase())],
|
||||
spacing: {
|
||||
...ctx.typography.lineSpacing,
|
||||
before: cfg.spacingBefore,
|
||||
after: cfg.spacingAfter,
|
||||
},
|
||||
alignment: alignmentOf(cfg.align),
|
||||
indent: NO_INDENT,
|
||||
});
|
||||
}
|
||||
|
||||
function structSection(ctx, title, children, opts = {}) {
|
||||
return flatten(structHeading(ctx, title, opts), children);
|
||||
}
|
||||
|
||||
function sectionHeading(ctx, title) {
|
||||
function sectionHeading(ctx, title, opts = {}) {
|
||||
const cfg = ctx.config.headings.section;
|
||||
return new Paragraph({
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
pageBreakBefore: opts.pageBreakBefore === true,
|
||||
numbering: { reference: REFS.outline, level: 0 },
|
||||
children: [textRun(ctx, title)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: cfg.spacingBefore, after: cfg.spacingAfter },
|
||||
@@ -152,7 +153,7 @@ function plainHeading(ctx, text, level = 2) {
|
||||
children: [textRun(ctx, text)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 240, after: 240 },
|
||||
alignment: AlignmentType.LEFT,
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ function figureCaption(ctx, label, title) {
|
||||
children: [textRun(ctx, `${prefix} ${label} ${dash} ${title}`)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 },
|
||||
alignment: alignmentOf(ctx.config.captions.figure.align),
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,7 +187,7 @@ function tableCaption(ctx, label, title) {
|
||||
children: [textRun(ctx, `${prefix} ${label} ${dash} ${title}`)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 240, after: 0 },
|
||||
alignment: alignmentOf(ctx.config.captions.table.align),
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ function tableContinuation(ctx, label, { pageBreakBefore = false } = {}) {
|
||||
children: [textRun(ctx, `${prefix} ${label}`)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 240, after: 0 },
|
||||
alignment: alignmentOf(ctx.config.captions.table.align),
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
...(pageBreakBefore ? { pageBreakBefore: true } : {}),
|
||||
keepNext: true,
|
||||
});
|
||||
@@ -273,7 +274,7 @@ function figureBlock(ctx, { label, title, imagePath, notesLines = [] }) {
|
||||
new Paragraph({
|
||||
children: [new ImageRun({ type, data, transformation: scaled })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
keepNext: true,
|
||||
})
|
||||
);
|
||||
@@ -285,7 +286,7 @@ function figureBlock(ctx, { label, title, imagePath, notesLines = [] }) {
|
||||
children: [textRun(ctx, line)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 },
|
||||
alignment: AlignmentType.CENTER,
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
keepNext: true,
|
||||
})
|
||||
);
|
||||
@@ -419,7 +420,7 @@ function mathPara(ctx, mathChildren, formulaNum, opts = {}) {
|
||||
children: [textRun(ctx, '\t'), new DocxMath({ children: mathChildren }), textRun(ctx, `\t(${num})`)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 },
|
||||
alignment: AlignmentType.LEFT,
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
tabStops: [
|
||||
{ type: TabStopType.CENTER, position: ctx.page.contentWidth / 2 },
|
||||
{ type: TabStopType.RIGHT, position: ctx.page.contentWidth },
|
||||
@@ -447,7 +448,7 @@ function whereBlock(ctx, items) {
|
||||
return new Paragraph({
|
||||
children: [prefix, ...symbolRuns, textRun(ctx, ` ${dash} ${item.desc}`)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 },
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
tabStops: [{ type: TabStopType.LEFT, position: ctx.config.formula.whereTabPosition }],
|
||||
});
|
||||
});
|
||||
@@ -479,7 +480,7 @@ function listParagraphRuns(ctx, runs, reference, level, instance) {
|
||||
// ── прочее ────────────────────────────────────────────────────────
|
||||
|
||||
function pageBreak() {
|
||||
return new Paragraph({ children: [new PageBreak()], indent: {} });
|
||||
return new Paragraph({ children: [new PageBreak()], indent: NO_INDENT });
|
||||
}
|
||||
|
||||
function noteSingle(ctx, text) {
|
||||
@@ -524,13 +525,13 @@ function definitionsBlock(ctx, pairs) {
|
||||
|
||||
function appendixStart(ctx, letter, title) {
|
||||
return [
|
||||
pageBreak(),
|
||||
new Paragraph({
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
pageBreakBefore: true,
|
||||
children: [textRun(ctx, `${ctx.config.appendix.labelPrefix} ${letter.toUpperCase()}`)],
|
||||
spacing: { ...ctx.typography.lineSpacing, before: ctx.config.headings.struct.spacingBefore, after: 0 },
|
||||
alignment: AlignmentType.CENTER,
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [textRun(ctx, title)],
|
||||
@@ -540,7 +541,7 @@ function appendixStart(ctx, letter, title) {
|
||||
after: ctx.config.headings.struct.spacingAfter,
|
||||
},
|
||||
alignment: AlignmentType.CENTER,
|
||||
indent: {},
|
||||
indent: NO_INDENT,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@ function buildNumberingConfig(ctx) {
|
||||
left: lists.nestedLevelStep * lvl,
|
||||
firstLine: typography.indentFirst,
|
||||
});
|
||||
const level = (lvl, format, text, { suffix = LevelSuffix.SPACE, indentOverride } = {}) => ({
|
||||
const level = (
|
||||
lvl,
|
||||
format,
|
||||
text,
|
||||
{ suffix = LevelSuffix.SPACE, indentOverride, runFont } = {}
|
||||
) => ({
|
||||
level: lvl,
|
||||
format,
|
||||
text,
|
||||
@@ -75,14 +80,14 @@ function buildNumberingConfig(ctx) {
|
||||
indent: indentOverride || indent(lvl),
|
||||
spacing: { ...typography.lineSpacing, before: 0, after: 0 },
|
||||
},
|
||||
run,
|
||||
run: runFont ? { ...run, font: runFont } : run,
|
||||
},
|
||||
});
|
||||
const outlineLevel = (lvl, text) => ({
|
||||
level: lvl,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text,
|
||||
suffix: LevelSuffix.TAB,
|
||||
suffix: LevelSuffix.SPACE,
|
||||
alignment: AlignmentType.LEFT,
|
||||
start: 1,
|
||||
style: {
|
||||
@@ -95,10 +100,10 @@ function buildNumberingConfig(ctx) {
|
||||
});
|
||||
|
||||
const dashRef = [
|
||||
level(0, LevelFormat.BULLET, lists.bulletChar),
|
||||
level(0, LevelFormat.BULLET, lists.bulletChar, { runFont: 'Symbol' }),
|
||||
level(1, LevelFormat.RUSSIAN_LOWER, `%2${lists.letterSuffix}`),
|
||||
level(2, LevelFormat.DECIMAL, `%3${lists.numberSuffix}`),
|
||||
level(3, LevelFormat.BULLET, lists.bulletChar),
|
||||
level(3, LevelFormat.BULLET, lists.bulletChar, { runFont: 'Symbol' }),
|
||||
];
|
||||
const numberRef = [
|
||||
level(0, LevelFormat.DECIMAL, `%1${lists.numberSuffix}`),
|
||||
|
||||
@@ -177,7 +177,7 @@ async function mergeDocxBuffers(prependBuffer, mainBuffer) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const docx = new DocxMerger({ pageBreak: true }, [prependBin, mainBin]);
|
||||
const docx = new DocxMerger({ pageBreak: false }, [prependBin, mainBin]);
|
||||
docx.save('nodebuffer', async (data) => {
|
||||
try {
|
||||
const merged = Buffer.from(data);
|
||||
|
||||
Reference in New Issue
Block a user