docs(sdlc): content-sized columns in Word, trace record with test cases, PSRs presented at minuted meetings
This commit is contained in:
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { chromium } from 'playwright';
|
||||
import { headerValues } from './letterhead.mjs';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(here, '../..');
|
||||
@@ -101,7 +102,7 @@ function renderTable(lines, className = '') {
|
||||
const attr = classFor(column);
|
||||
return `<${tag}${attr ? ` class="${attr}"` : ''}>${renderInline(cell)}</${tag}>`;
|
||||
}).join('');
|
||||
const classes = [className, header.length >= 8 ? 'dense' : ''].filter(Boolean).join(' ');
|
||||
const classes = [className, header.length >= 7 ? 'dense' : ''].filter(Boolean).join(' ');
|
||||
return `<table${classes ? ` class="${classes}"` : ''}><thead><tr>${cells('th', header)}</tr></thead><tbody>${data.map((row) => isGroup(row)
|
||||
? `<tr class="group"><td colspan="${header.length}">${renderInline(row[0])}</td></tr>`
|
||||
: `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`;
|
||||
@@ -233,9 +234,6 @@ async function main() {
|
||||
const LANDSCAPE_TITLES = new Set(['List of Evidence', 'Traceability Record Table']);
|
||||
const values = {
|
||||
logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`,
|
||||
documentTitle: title,
|
||||
// The banner holds about 22 characters at 13.5pt; longer titles shrink to fit.
|
||||
titleSize: `${Math.min(13.5, (13.5 * 22) / title.length).toFixed(1)}pt`,
|
||||
documentType: metadata['Document No'] ?? title,
|
||||
ciBlue: '#0797d5',
|
||||
ciGrey: '#8e8e8e', // ribbon grey measured from the audited reference documents
|
||||
@@ -247,13 +245,7 @@ async function main() {
|
||||
pageNumber: '<span class="pageNumber"></span>',
|
||||
totalPages: '<span class="totalPages"></span>'
|
||||
};
|
||||
// Landscape pages are 87mm wider: the ribbon stretches and the banner keeps its distance from the right edge.
|
||||
const headerFor = (landscape) => applyTemplate(headerTemplate, {
|
||||
...values,
|
||||
pageWidth: landscape ? '297mm' : '210mm',
|
||||
ribbonWidth: `${64.7 + (landscape ? 87 : 0)}mm`,
|
||||
bannerLeft: `${127.2 + (landscape ? 87 : 0)}mm`
|
||||
});
|
||||
const headerFor = (landscape) => applyTemplate(headerTemplate, { ...values, ...headerValues(title, landscape) });
|
||||
const footerFor = (landscape) => applyTemplate(footerTemplate, {
|
||||
...values,
|
||||
pageWidth: landscape ? '297mm' : '210mm',
|
||||
@@ -280,6 +272,18 @@ async function main() {
|
||||
if (landscape) await page.setContent(htmlFor(true), { waitUntil: 'networkidle' });
|
||||
const textMm = landscape ? 267 : PORTRAIT_TEXT_MM;
|
||||
const clipped = await applyProportionalWidths(page, textMm);
|
||||
// The Word copy is rebuilt from the PDF by pdf2docx, which loses these widths;
|
||||
// the converter reads them back from this file (see convert-to-docx.py).
|
||||
if (process.env.SDLC_WIDTHS_FILE) {
|
||||
const sized = await page.evaluate(() => [...document.querySelectorAll('main table')]
|
||||
.filter((table) => table.querySelector('colgroup'))
|
||||
.map((table) => ({
|
||||
header: [...table.tHead.rows[0].cells].map((cell) => cell.textContent.trim()),
|
||||
widths: [...table.querySelectorAll('colgroup col')].map((col) => parseFloat(col.style.width))
|
||||
})));
|
||||
await fs.mkdir(path.dirname(process.env.SDLC_WIDTHS_FILE), { recursive: true });
|
||||
await fs.writeFile(process.env.SDLC_WIDTHS_FILE, `${JSON.stringify(sized)}\n`);
|
||||
}
|
||||
for (const what of clipped) console.error(`WARNING ${path.basename(source)}: ${what} does not fit its column`);
|
||||
// Unbreakable IDs can make a table wider than the page; say so rather than print it clipped.
|
||||
const overflowMm = await page.evaluate((mm) => {
|
||||
@@ -360,7 +364,17 @@ async function applyProportionalWidths(page, textMm) {
|
||||
+ '.width-probe.narrowest table { width: 1px !important; } '
|
||||
+ '.width-probe.narrowest th, .width-probe.narrowest td { overflow-wrap: normal !important; }';
|
||||
document.head.append(probe);
|
||||
// Per column: every single-column cell's rendered width in one probe layout.
|
||||
// Per column: the width of every single-column cell's own content (plus its
|
||||
// padding) in one probe layout. A cell's box is as wide as its whole column,
|
||||
// so the content is measured with a range rather than the cell's rectangle.
|
||||
const contentWidth = (cell) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(cell);
|
||||
const rects = [...range.getClientRects()];
|
||||
const text = rects.length ? Math.max(...rects.map((r) => r.right)) - Math.min(...rects.map((r) => r.left)) : 0;
|
||||
const style = getComputedStyle(cell);
|
||||
return text + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + 1;
|
||||
};
|
||||
const measure = (table, mode) => {
|
||||
const holder = document.createElement('div');
|
||||
holder.className = `delivery-document width-probe ${mode}`;
|
||||
@@ -370,7 +384,7 @@ async function applyProportionalWidths(page, textMm) {
|
||||
for (const row of holder.querySelector('table').rows) {
|
||||
let index = 0;
|
||||
for (const cell of row.cells) {
|
||||
if (cell.colSpan === 1) (columns[index] ??= []).push({ width: cell.getBoundingClientRect().width / mm, head: cell.tagName === 'TH' });
|
||||
if (cell.colSpan === 1) (columns[index] ??= []).push({ width: contentWidth(cell) / mm, head: cell.tagName === 'TH' });
|
||||
index += cell.colSpan;
|
||||
}
|
||||
}
|
||||
@@ -424,6 +438,13 @@ async function applyProportionalWidths(page, textMm) {
|
||||
widths = widths.map((w, i) => w + ((availMm - used) * weight[i]) / total);
|
||||
}
|
||||
}
|
||||
// Topic columns (CR Topic, SRS Topic, Unit Topic) share their combined width equally.
|
||||
const heads = [...(table.tHead?.rows[0]?.cells ?? [])].map((cell) => cell.textContent.trim());
|
||||
const topics = heads.map((h, i) => (/Topic$/i.test(h) ? i : -1)).filter((i) => i >= 0);
|
||||
if (topics.length > 1 && heads.length === n) {
|
||||
const even = topics.reduce((sum, i) => sum + widths[i], 0) / topics.length;
|
||||
if (topics.every((i) => even >= floor[i])) for (const i of topics) widths[i] = even;
|
||||
}
|
||||
const colgroup = document.createElement('colgroup');
|
||||
for (const w of widths) {
|
||||
const col = document.createElement('col');
|
||||
|
||||
Reference in New Issue
Block a user