Fix SDLC delivery rendering and regenerate package

This commit is contained in:
Thanakorn
2026-08-19 10:18:32 +07:00
parent 849158059b
commit bbc3654180
48 changed files with 99 additions and 22 deletions
+48 -16
View File
@@ -1,6 +1,6 @@
@page {
size: A4;
margin: 18mm 16mm 28mm;
margin: 18mm 16mm 20mm;
}
:root {
@@ -64,7 +64,7 @@ body { margin: 0; }
font-size: 15pt;
font-weight: 500;
flex: 0 1 72mm;
overflow-wrap: anywhere;
overflow-wrap: break-word;
min-width: 52mm;
padding: 4mm 6mm;
text-align: center;
@@ -80,7 +80,7 @@ body { margin: 0; }
.document-control th,
.document-control td {
border: .5pt solid var(--brn-line);
padding: 2.2mm 3mm;
padding: 2mm 3mm;
text-align: left;
vertical-align: top;
}
@@ -88,43 +88,75 @@ body { margin: 0; }
.document-control th {
background: var(--brn-pale-blue);
font-weight: 700;
width: 26%;
white-space: nowrap;
width: 1%;
}
h1, h2, h3 { break-after: avoid; color: var(--brn-ink); }
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; }
h3 { color: var(--brn-blue); font-size: 11.5pt; margin: 6mm 0 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: 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) {
border-collapse: collapse;
border: .75pt solid #718096;
break-inside: auto;
font-size: 9pt;
margin: 0 0 5mm;
table-layout: fixed;
table-layout: auto;
width: 100%;
}
table:not(.document-control) th,
table:not(.document-control) td {
border: .5pt solid var(--brn-line);
overflow-wrap: anywhere;
padding: 2mm;
break-inside: avoid;
overflow-wrap: break-word;
padding: 1.6mm 2mm;
text-align: left;
vertical-align: top;
word-break: normal;
}
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; }
tr { break-inside: avoid; }
.approval-block { break-inside: avoid; margin-top: 9mm; }
.approval-block__person { margin: 5mm 0; min-height: 21mm; }
.approval-block__line { border-bottom: .5pt solid var(--brn-ink); display: inline-block; min-width: 70mm; }
.approval-block__person { break-inside: avoid; margin: 0 0 6mm; }
.approval-block__person h3 { margin: 0 0 2mm; }
.approval-block__line {
border-bottom: .5pt solid var(--brn-ink);
display: inline-block;
min-width: 68mm;
vertical-align: baseline;
}
.approval-fields {
line-height: 1.62;
margin: 0 0 8mm;
padding: 2.5mm 0 4mm;
line-height: 1.55;
margin: 0;
padding: 0;
}
+51 -6
View File
@@ -14,7 +14,9 @@ const escapeHtml = (value) => value
const renderInline = (value) => escapeHtml(value)
.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] ?? '');
@@ -30,11 +32,41 @@ function isSeparator(cells) {
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 = '') {
const rows = lines.map(tableCells);
const header = rows[0];
const data = isSeparator(rows[1] ?? []) ? rows.slice(2) : rows.slice(1);
const cells = (tag, row) => row.map((cell) => `<${tag}>${renderInline(cell)}</${tag}>`).join('');
const hasSeparator = isSeparator(rows[1] ?? []);
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>`;
}
@@ -79,10 +111,23 @@ function markdownToHtml(markdown) {
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 = [];
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\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"' : '';
output.push(`<p${approvalClass}>${renderParagraph(paragraph)}</p>`);
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 isApproval = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd()));
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');