Fix SDLC delivery rendering and regenerate package
Nine layout defects were fixed in the generator so the delivery package is reproducible from `scripts/build-sdlc-delivery.sh`. CSS (scripts/sdlc-delivery/assets/sdlc-delivery.css): - table-layout: fixed -> auto, so columns fit their content instead of each taking an equal share of the page width - overflow-wrap: anywhere -> break-word, removing mid-word breaks such as "Representativ|es" and "communicati|on" - break-inside: avoid on cells; text-align: left on th so headers match body - new .a-right/.a-center/.tight column classes; ul/ol/li and code rules - trimmed padding: @page bottom 28->20mm, h2 9->6mm, h3 6->4mm, cells 2->1.6mm, .approval-fields; .document-control th shrinks to its label Renderer (scripts/sdlc-delivery/render-sdlc.mjs): - ordered lists emit <ol> instead of collapsing into a run-on paragraph - column alignment read from the Markdown separator row (:---, ---:, :---:) - columns whose longest cell is <= 12 chars marked "tight" to shrink to content - underscore runs become styled signature rules - each signatory's heading and fields wrapped so they cannot split across pages - backslash escapes such as \* are unescaped Regenerated sdlc-delivery/ with the fixed pipeline: 46 PDFs, 163 pages, both content-coverage and A4-format verification passing. Work Schedule 14 -> 9 pages, Correction Register 11 -> 10, Acceptance Report 7 -> 6. The four Meeting Records are included for the first time, and PDFs for renamed sources follow their new paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
155cf783ae
commit
36ab46b9d1
@@ -1,6 +1,6 @@
|
|||||||
@page {
|
@page {
|
||||||
size: A4;
|
size: A4;
|
||||||
margin: 18mm 16mm 28mm;
|
margin: 18mm 16mm 20mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -64,7 +64,7 @@ body { margin: 0; }
|
|||||||
font-size: 15pt;
|
font-size: 15pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
flex: 0 1 72mm;
|
flex: 0 1 72mm;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: break-word;
|
||||||
min-width: 52mm;
|
min-width: 52mm;
|
||||||
padding: 4mm 6mm;
|
padding: 4mm 6mm;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -80,7 +80,7 @@ body { margin: 0; }
|
|||||||
.document-control th,
|
.document-control th,
|
||||||
.document-control td {
|
.document-control td {
|
||||||
border: .5pt solid var(--brn-line);
|
border: .5pt solid var(--brn-line);
|
||||||
padding: 2.2mm 3mm;
|
padding: 2mm 3mm;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
@@ -88,43 +88,75 @@ body { margin: 0; }
|
|||||||
.document-control th {
|
.document-control th {
|
||||||
background: var(--brn-pale-blue);
|
background: var(--brn-pale-blue);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
width: 26%;
|
white-space: nowrap;
|
||||||
|
width: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3 { break-after: avoid; color: var(--brn-ink); }
|
h1, h2, h3 { break-after: avoid; color: var(--brn-ink); }
|
||||||
h1 { font-size: 18pt; margin: 0 0 5mm; }
|
h1 { font-size: 18pt; margin: 0 0 5mm; }
|
||||||
h2 { border-left: 4pt solid var(--brn-blue); font-size: 14pt; margin: 9mm 0 4mm; padding-left: 3mm; }
|
h2 { border-left: 4pt solid var(--brn-blue); font-size: 14pt; margin: 6mm 0 3mm; padding-left: 3mm; }
|
||||||
h3 { color: var(--brn-blue); font-size: 11.5pt; margin: 6mm 0 3mm; }
|
h3 { color: var(--brn-blue); font-size: 11.5pt; margin: 4mm 0 2mm; }
|
||||||
|
|
||||||
p { margin: 0 0 3.5mm; }
|
p { margin: 0 0 3mm; }
|
||||||
|
|
||||||
|
ul, ol { margin: 0 0 3.5mm; padding-left: 7mm; }
|
||||||
|
li { margin: 0 0 1mm; }
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "DejaVu Sans Mono", "Courier New", monospace;
|
||||||
|
font-size: .9em;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
table:not(.document-control) {
|
table:not(.document-control) {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: .75pt solid #718096;
|
border: .75pt solid #718096;
|
||||||
|
break-inside: auto;
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
margin: 0 0 5mm;
|
margin: 0 0 5mm;
|
||||||
table-layout: fixed;
|
table-layout: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table:not(.document-control) th,
|
table:not(.document-control) th,
|
||||||
table:not(.document-control) td {
|
table:not(.document-control) td {
|
||||||
border: .5pt solid var(--brn-line);
|
border: .5pt solid var(--brn-line);
|
||||||
overflow-wrap: anywhere;
|
break-inside: avoid;
|
||||||
padding: 2mm;
|
overflow-wrap: break-word;
|
||||||
|
padding: 1.6mm 2mm;
|
||||||
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
table:not(.document-control) th { background: var(--brn-pale-blue); font-weight: 700; }
|
table:not(.document-control) th { background: var(--brn-pale-blue); font-weight: 700; }
|
||||||
|
|
||||||
|
/* Column alignment carried over from the Markdown separator row. */
|
||||||
|
table:not(.document-control) th.a-right,
|
||||||
|
table:not(.document-control) td.a-right { text-align: right; }
|
||||||
|
table:not(.document-control) th.a-center,
|
||||||
|
table:not(.document-control) td.a-center { text-align: center; }
|
||||||
|
|
||||||
|
/* Short columns (IDs, dates, counts) shrink to their content instead of
|
||||||
|
taking an equal share of the table width. */
|
||||||
|
table:not(.document-control) th.tight,
|
||||||
|
table:not(.document-control) td.tight { white-space: nowrap; width: 1%; }
|
||||||
|
|
||||||
thead { display: table-header-group; }
|
thead { display: table-header-group; }
|
||||||
tr { break-inside: avoid; }
|
tr { break-inside: avoid; }
|
||||||
|
|
||||||
.approval-block { break-inside: avoid; margin-top: 9mm; }
|
.approval-block__person { break-inside: avoid; margin: 0 0 6mm; }
|
||||||
.approval-block__person { margin: 5mm 0; min-height: 21mm; }
|
.approval-block__person h3 { margin: 0 0 2mm; }
|
||||||
.approval-block__line { border-bottom: .5pt solid var(--brn-ink); display: inline-block; min-width: 70mm; }
|
|
||||||
|
.approval-block__line {
|
||||||
|
border-bottom: .5pt solid var(--brn-ink);
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 68mm;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
.approval-fields {
|
.approval-fields {
|
||||||
line-height: 1.62;
|
line-height: 1.55;
|
||||||
margin: 0 0 8mm;
|
margin: 0;
|
||||||
padding: 2.5mm 0 4mm;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ const escapeHtml = (value) => value
|
|||||||
|
|
||||||
const renderInline = (value) => escapeHtml(value)
|
const renderInline = (value) => escapeHtml(value)
|
||||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||||
|
.replace(/_{3,}/g, '<span class="approval-block__line"></span>')
|
||||||
|
.replace(/\\([\\*_`~])/g, '$1');
|
||||||
|
|
||||||
const applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? '');
|
const applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? '');
|
||||||
|
|
||||||
@@ -30,11 +32,41 @@ function isSeparator(cells) {
|
|||||||
return cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
return cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TIGHT_COLUMN_MAX = 12;
|
||||||
|
|
||||||
|
function columnAlignment(separatorCells) {
|
||||||
|
return separatorCells.map((cell) => {
|
||||||
|
const left = cell.startsWith(':');
|
||||||
|
const right = cell.endsWith(':');
|
||||||
|
if (left && right) return 'a-center';
|
||||||
|
if (right) return 'a-right';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderTable(lines, className = '') {
|
function renderTable(lines, className = '') {
|
||||||
const rows = lines.map(tableCells);
|
const rows = lines.map(tableCells);
|
||||||
const header = rows[0];
|
const header = rows[0];
|
||||||
const data = isSeparator(rows[1] ?? []) ? rows.slice(2) : rows.slice(1);
|
const hasSeparator = isSeparator(rows[1] ?? []);
|
||||||
const cells = (tag, row) => row.map((cell) => `<${tag}>${renderInline(cell)}</${tag}>`).join('');
|
const align = hasSeparator ? columnAlignment(rows[1]) : header.map(() => '');
|
||||||
|
const data = hasSeparator ? rows.slice(2) : rows.slice(1);
|
||||||
|
|
||||||
|
// A column whose longest cell is short (IDs, dates, counts, statuses) is
|
||||||
|
// marked "tight" so auto table layout shrinks it to its content instead of
|
||||||
|
// handing every column an equal share of the page width.
|
||||||
|
const widest = header.map((_, column) => Math.max(
|
||||||
|
(header[column] ?? '').length,
|
||||||
|
...data.map((row) => (row[column] ?? '').replace(/[`*]/g, '').length)
|
||||||
|
));
|
||||||
|
const classFor = (column) => [
|
||||||
|
align[column] ?? '',
|
||||||
|
widest[column] <= TIGHT_COLUMN_MAX ? 'tight' : ''
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
const cells = (tag, row) => row.map((cell, column) => {
|
||||||
|
const attr = classFor(column);
|
||||||
|
return `<${tag}${attr ? ` class="${attr}"` : ''}>${renderInline(cell)}</${tag}>`;
|
||||||
|
}).join('');
|
||||||
return `<table${className ? ` class="${className}"` : ''}><thead><tr>${cells('th', header)}</tr></thead><tbody>${data.map((row) => `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`;
|
return `<table${className ? ` class="${className}"` : ''}><thead><tr>${cells('th', header)}</tr></thead><tbody>${data.map((row) => `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +111,23 @@ function markdownToHtml(markdown) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/^\d+\.\s+/.test(line)) {
|
||||||
|
const items = [];
|
||||||
|
while (index < lines.length && /^\d+\.\s+/.test(lines[index])) items.push(`<li>${renderInline(lines[index++].replace(/^\d+\.\s+/, ''))}</li>`);
|
||||||
|
output.push(`<ol>${items.join('')}</ol>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const paragraph = [];
|
const paragraph = [];
|
||||||
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index])) paragraph.push(lines[index++]);
|
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index]) && !/^\d+\.\s+/.test(lines[index])) paragraph.push(lines[index++]);
|
||||||
const approvalClass = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd())) ? ' class="approval-fields"' : '';
|
const isApproval = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd()));
|
||||||
output.push(`<p${approvalClass}>${renderParagraph(paragraph)}</p>`);
|
const html = `<p${isApproval ? ' class="approval-fields"' : ''}>${renderParagraph(paragraph)}</p>`;
|
||||||
|
// Keep a signatory's heading and fields on one page.
|
||||||
|
if (isApproval && /^<h3>/.test(output[output.length - 1] ?? '')) {
|
||||||
|
output.push(`<div class="approval-block__person">${output.pop()}${html}</div>`);
|
||||||
|
} else {
|
||||||
|
output.push(html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.join('\n');
|
return output.join('\n');
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user