@@ -13,6 +13,9 @@ const escapeHtml = (value) => value
. replaceAll ( '>' , '>' ) ;
const renderInline = ( value ) => escapeHtml ( value )
// A literal <br> is the only markup allowed through, so a table cell can list
// several files on separate lines the way the reference package does.
. replaceAll ( '<br>' , '<br>' )
. replace ( /\*\*(.+?)\*\*/g , '<strong>$1</strong>' )
. replace ( /`([^`]+)`/g , '<code>$1</code>' )
. replace ( /_{3,}/g , '<span class="approval-block__line"></span>' )
@@ -44,6 +47,23 @@ function columnAlignment(separatorCells) {
} ) ;
}
function isControlBlock ( lines ) {
return tableCells ( lines [ 0 ] ? ? '' ) [ 0 ] === 'Document No' ;
}
// The document-control block is a label/value grid, not a data table: odd cells are
// labels, even cells are values, and a row with one pair spans the full width.
function renderControlBlock ( lines ) {
const rows = lines . map ( tableCells ) ;
const body = rows . map ( ( cells ) => {
const pairs = [ ] ;
for ( let i = 0 ; i < cells . length ; i += 2 ) pairs . push ( [ cells [ i ] , cells [ i + 1 ] ? ? '' ] ) ;
const span = pairs . length === 1 ? ' colspan="3"' : '' ;
return ` <tr> ${ pairs . map ( ( [ label , value ] ) => ` <th> ${ renderInline ( label ) } </th><td ${ span } > ${ renderInline ( value ) } </td> ` ) . join ( '' ) } </tr> ` ;
} ) . join ( '' ) ;
return ` <table class="document-control"><tbody> ${ body } </tbody></table> ` ;
}
function renderTable ( lines , className = '' ) {
const rows = lines . map ( tableCells ) ;
const header = rows [ 0 ] ;
@@ -92,6 +112,13 @@ function markdownToHtml(markdown) {
if ( isTableLine ( line ) ) {
const table = [ ] ;
while ( index < lines . length && isTableLine ( lines [ index ] ) ) table . push ( lines [ index ++ ] ) ;
if ( isControlBlock ( table ) ) { output . push ( renderControlBlock ( table ) ) ; continue ; }
const previous = output [ output . length - 1 ] ? ? '' ;
// Keep a signature heading and its table together on one page.
if ( /^<h2 class="signature-heading">/ . test ( previous ) ) {
output . push ( ` <div class="signature-block"> ${ output . pop ( ) } ${ renderTable ( table , 'signature-table' ) } </div> ` ) ;
continue ;
}
output . push ( renderTable ( table ) ) ;
continue ;
}
@@ -99,7 +126,9 @@ function markdownToHtml(markdown) {
const heading = line . match ( /^(#{1,3})\s+(.+)$/ ) ;
if ( heading ) {
const level = heading [ 1 ] . length ;
output . push ( ` <h ${ level } > ${ renderInline ( heading [ 2 ] ) } </h ${ level } > ` ) ;
const signature = level === 2 && /^ผู้(จัดทำเอกสาร|ตรวจสอบเอกสาร|อนุมัติ)/ . test ( heading [ 2 ] ) ;
const attr = signature ? ' class="signature-heading"' : '' ;
output . push ( ` <h ${ level } ${ attr } > ${ renderInline ( heading [ 2 ] ) } </h ${ level } > ` ) ;
index += 1 ;
continue ;
}
@@ -136,32 +165,26 @@ function markdownToHtml(markdown) {
function extractDocument ( markdown ) {
const titleMatch = markdown . match ( /^#\s+(.+)\n+/m ) ;
const title = titleMatch ? . [ 1 ] ? ? 'BRN WMS' ;
const afterTitle = markdown . slice ( ( titleMatch ? . index ? ? 0 ) + ( titleMatch ? . [ 0 ] . length ? ? 0 ) ) ;
const lines = afterTitle . split ( '\n' ) ;
const metadata = { } ;
let bodyStart = 0 ;
const footer = markdown . match ( /^<!--\s*footer:\s*(.+?)\s*-->\s*$/m ) ? . [ 1 ] ? ? '' ;
const body = markdown
. slice ( ( titleMatch ? . index ? ? 0 ) + ( titleMatch ? . [ 0 ] . length ? ? 0 ) )
. replace ( /^<!--\s*footer:.*?-->\s*$/m , '' ) ;
// The control block carries the document identity used by the header and footer.
const metadata = { } ;
const lines = body . split ( '\n' ) ;
for ( let index = 0 ; index < lines . length ; index += 1 ) {
if ( ! isTableLine ( lines [ index ] ) ) continue ;
const table = [ ] ;
while ( index < lines . length && isTableLine ( lines [ index ] ) ) table . push ( lines [ index ++ ] ) ;
const candidate = { } ;
const dataStart = isSeparator ( tableCells ( table [ 1 ] ? ? '' ) ) ? 2 : 1 ;
for ( const row of table . slice ( dataStart ) . map ( tableCells ) ) {
if ( row . length >= 2 ) candidate [ row [ 0 ] ] = row [ 1 ] ;
if ( ! isControlBlock ( table ) ) { index - = 1 ; continue ; }
for ( const cells of table . map ( tableCells ) ) {
for ( let i = 0 ; i < cells . length ; i += 2 ) if ( cells [ i ] ) metadata [ cells [ i ] ] = cells [ i + 1 ] ? ? '' ;
}
if ( candidate . Document && candidate . Project ) {
Object . assign ( metadata , candidate ) ;
bodyStart = index ;
break ;
}
index -= 1 ;
break ;
}
return { title , metadata , body : lines . slice ( bodyStart ) . join ( '\n' ) } ;
return { title , footer , metadata , body } ;
}
async function main ( ) {
@@ -176,27 +199,29 @@ async function main() {
fs . readFile ( path . join ( root , 'app/assets/images/logo.svg' ) ) ,
fs . readFile ( path . join ( assets , 'LeelawadeeUI.ttf' ) )
] ) ;
const { title , metadata , body } = extractDocument ( markdown ) ;
const hasWideTable = markdown . split ( '\n' ) . some ( ( line ) => isTableLine ( line ) && tableCells ( line ) . length >= 8 ) ;
const isLandscape = metadata . Document === 'Work Schedule' || title === 'Work Schedule' || hasWideTable ;
const { title , footer , metadata , body } = extractDocument ( markdown ) ;
// Wide reference tables (schedule, register, test cases, traceability) need landscape;
// ordinary 8-column status tables still read well in portrait.
const widestTable = Math . max ( 0 , ... markdown . split ( '\n' ) . filter ( isTableLine ) . map ( ( line ) => tableCells ( line ) . length ) ) ;
const LANDSCAPE _TITLES = new Set ( [ 'Work Schedule' , 'Traceability Record' , 'Traceability Record Table' ] ) ;
const isLandscape = LANDSCAPE _TITLES . has ( title ) || widestTable >= 9 ;
const values = {
logoPath : ` data:image/svg+xml;base64, ${ logo . toString ( 'base64' ) } ` ,
documentTitle : title ,
documentType : metadata . Document ? ? title ,
release : metadata . Release ? ? '' ,
projectName : metadata . Project ? ? 'BRN WMS' ,
projectCode : metadata [ 'Project c ode' ] ? ? '200-WMS-26-001-00' ,
projectPeriod : metadata [ 'Project period' ] ? ? '05/01/26– 24/08/26 ',
documentType : metadata [ 'Document No' ] ? ? title ,
release : metadata [ 'Release, Version, By:' ] ? ? '' ,
projectName : metadata [ 'Project Name' ] ? ? 'BRN WMS' ,
projectCode : metadata [ 'Project C ode' ] ? ? '200-WMS-26-001-00' ,
footerNote : footer ? ` ISO/IEC 29110-4-1:2018 ${ footer } ` : 'ISO/IEC 29110-4-1:2018 ',
pageNumber : '<span class="pageNumber"></span>' ,
totalPages : '<span class="totalPages"></span>'
} ;
const control = await fs . readFile ( path . join ( assets , 'document-control.html' ) , 'utf8' ) ;
const pdfFooter = applyTemplate ( footerTemplate , values ) ;
const fontFace = ` @font-face { font-family: "BRN Thai"; src: url(data:font/ttf;base64, ${ thaiFont . toString ( 'base64' ) } ) format("truetype"); font-weight: 400 700; } ` ;
const pageLayout = isLandscape
? '@page { size: A4 landscape; margin: 18mm 16mm 28mm; }'
: '' ;
const html = ` <!doctype html><html><head><meta charset="utf-8"><style> ${ fontFace } ${ css } ${ pageLayout } </style></head><body><main class="delivery-document"> ${ applyTemplate ( headerTemplate , values ) } ${ applyTemplate ( control , values ) } ${ markdownToHtml ( body ) } </main></body></html> ` ;
const html = ` <!doctype html><html><head><meta charset="utf-8"><style> ${ fontFace } ${ css } ${ pageLayout } </style></head><body><main class="delivery-document"> ${ applyTemplate ( headerTemplate , values ) } ${ markdownToHtml ( body ) } </main></body></html> ` ;
await fs . mkdir ( path . dirname ( destination ) , { recursive : true } ) ;
const browser = await chromium . launch ( { headless : true } ) ;