fix docx toc and callouts

This commit is contained in:
2026-06-15 14:46:39 -05:00
parent 7da39831ab
commit 4f74b03324
2 changed files with 17 additions and 34 deletions
+12 -30
View File
@@ -7,7 +7,6 @@ const { escapeXml } = require('../core/util');
const { encodePng } = require('../core/png');
const { guideSlug, renderAllImages, LEVEL_LABEL, stepContentGroups, codeBlockText } = require('./common');
const { guideMetaLines, guideSummary } = require('./document-layout');
const raster = require('../core/raster');
/**
* DOCX exporter: WordprocessingML built directly (no dependency), one
@@ -67,24 +66,20 @@ function drawing(relId, widthPx, heightPx, maxWidthTwips) {
`</pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
}
function calloutIcon(level) {
const img = raster.createImage(24, 24, [0, 0, 0, 0]);
const fill = {
info: [37, 99, 235, 255],
success: [16, 185, 129, 255],
warn: [245, 158, 11, 255],
error: [239, 68, 68, 255],
}[level] || [37, 99, 235, 255];
raster.fillOval(img, 1, 1, 22, 22, fill);
const glyph = level === 'success' ? 'v' : level === 'warn' ? '!' : level === 'error' ? 'x' : 'i';
raster.drawTextCentered(img, 12, 13, glyph, 12, [255, 255, 255, 255]);
return img;
}
function pageBreak() {
return p('<w:r><w:br w:type="page"/></w:r>');
}
function tocFieldParagraph() {
return p([
'<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>',
'<w:r><w:instrText xml:space="preserve"> TOC \\o "1-3" \\h \\z \\u </w:instrText></w:r>',
'<w:r><w:fldChar w:fldCharType="separate"/></w:r>',
'<w:r><w:rPr><w:i/></w:rPr><w:t xml:space="preserve">Update contents in Word</w:t></w:r>',
'<w:r><w:fldChar w:fldCharType="end"/></w:r>',
].join(''));
}
function headingStyleForDepth(depth) {
return `Heading${Math.min(3, depth + 1)}`;
}
@@ -184,16 +179,6 @@ function exportDocx(ast, outDir, template = {}) {
rels.push(`<Relationship Id="rId${++relCounter}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`);
const iconRelIds = {};
for (const level of ['info', 'success', 'warn', 'error']) {
const icon = calloutIcon(level);
const relId = ++relCounter;
iconRelIds[level] = relId;
const name = `callout-${level}.png`;
media.push({ name, data: encodePng(icon) });
rels.push(`<Relationship Id="rId${relId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${name}"/>`);
}
const body = [];
body.push(p(
run(ast.guide.title, { bold: true, size: 48 }),
@@ -210,18 +195,15 @@ function exportDocx(ast, outDir, template = {}) {
run('Contents', { bold: true, size: 28 }),
'<w:pBdr><w:bottom w:val="single" w:sz="20" w:space="8" w:color="2563EB"/></w:pBdr>'
));
body.push(p(
`<w:fldSimple w:instr="TOC \\o &quot;1-3&quot; \\h \\z \\u"><w:r><w:rPr><w:i/></w:rPr><w:t xml:space="preserve">Update contents in Word</w:t></w:r></w:fldSimple>`
));
body.push(tocFieldParagraph());
body.push(pageBreak());
}
const emitTextBlock = (tb) => {
const style = LEVEL_STYLE[tb.level] || LEVEL_STYLE.info;
const iconRelId = iconRelIds[tb.level] || iconRelIds.info;
const label = `${LEVEL_LABEL[tb.level] || 'Note'}${tb.title ? `: ${tb.title}` : ''}`;
body.push(p(
`${drawing(iconRelId, 16, 16, 240)}${run(label, { bold: true, size: 20, color: style.color })}${tb.descriptionText ? run('\n' + tb.descriptionText, { size: 20, color: '1F2937' }) : ''}`,
`${run(label, { bold: true, size: 20, color: style.color })}${tb.descriptionText ? run('\n' + tb.descriptionText, { size: 20, color: '1F2937' }) : ''}`,
`<w:shd w:val="clear" w:fill="${style.fill}"/><w:pBdr><w:left w:val="single" w:sz="24" w:space="4" w:color="${style.color}"/></w:pBdr>`
));
};
+5 -4
View File
@@ -296,15 +296,15 @@ test('DOCX export: valid OPC package, well-formed XML, resolvable image rels', (
// Every relationship target exists in the package, every embed has a rel.
const relsXml = entries.get('word/_rels/document.xml.rels').toString('utf8');
const relTargets = [...relsXml.matchAll(/Target="([^"]+)"/g)].map((m) => m[1]);
assert.equal(relTargets.length, 8);
assert.equal(relTargets.length, 4);
assert.ok(relTargets.includes('settings.xml'));
assert.ok(relTargets.includes('styles.xml'));
const mediaTargets = relTargets.filter((target) => target.startsWith('media/'));
assert.equal(mediaTargets.length, 6);
assert.equal(mediaTargets.length, 2);
const iconTargets = mediaTargets.filter((target) => target.includes('callout-'));
const imageTargets = mediaTargets.filter((target) => target.includes('image'));
assert.equal(iconTargets.length, 4);
assert.equal(iconTargets.length, 0);
assert.equal(imageTargets.length, 2);
for (const target of iconTargets) {
@@ -323,7 +323,8 @@ test('DOCX export: valid OPC package, well-formed XML, resolvable image rels', (
for (const id of embeds) {
assert.ok(relIds.includes(id), `missing relationship for ${id}`);
}
assert.ok(docXml.includes('TOC \\o &quot;1-3&quot; \\h \\z \\u'));
assert.ok(docXml.includes('w:fldChar w:fldCharType="begin" w:dirty="true"'));
assert.ok(docXml.includes('TOC \\o "1-3" \\h \\z \\u'));
assert.ok(docXml.includes('w:pStyle w:val="Heading1"'));
assert.ok(docXml.includes('w:pStyle w:val="Heading2"'));
assert.ok(docXml.includes('w:outlineLvl w:val="0"'));