"; if ($bold || $isCode) { $rPr[] = ''; } if ($italic) { $rPr[] = ''; } if ($fontSize) { $rPr[] = ""; } if ($fontColor) { $rPr[] = ""; } if ($isCode) { $rPr[] = ''; $rPr[] = ''; } $rPrXml = !empty($rPr) ? '' . implode('', $rPr) . '' : ''; return '' . $rPrXml . '' . escape_xml($text) . ''; } function parse_inline_formatting(string $line, int $defaultSize = 22, string $defaultColor = '333333'): string { $parts = preg_split('/(\*\*.*?\*\*|`.*?`)/u', $line, -1, PREG_SPLIT_DELIM_CAPTURE); $runs = []; foreach ($parts as $part) { if ($part === '') continue; if (str_starts_with($part, '**') && str_ends_with($part, '**') && strlen($part) >= 4) { $runs[] = make_run(substr($part, 2, -2), bold: true, fontSize: $defaultSize, fontColor: $defaultColor); } elseif (str_starts_with($part, '`') && str_ends_with($part, '`') && strlen($part) >= 2) { $runs[] = make_run(substr($part, 1, -1), isCode: true, fontSize: $defaultSize - 2, fontColor: '0F172A'); } else { $runs[] = make_run($part, fontSize: $defaultSize, fontColor: $defaultColor); } } return implode('', $runs); } function make_paragraph(string $runsXml, ?string $style = null, ?string $align = null, int $spaceBefore = 100, int $spaceAfter = 100, ?string $bgColor = null): string { $pPr = []; if ($style) { $pPr[] = ""; } if ($align) { $pPr[] = ""; } $pPr[] = ""; if ($bgColor) { $pPr[] = ""; } $pPrXml = !empty($pPr) ? '' . implode('', $pPr) . '' : ''; return '' . $pPrXml . $runsXml . ''; } function make_table(array $rows, bool $headers = true): string { if (empty($rows)) return ''; $numCols = 0; foreach ($rows as $r) { $numCols = max($numCols, count($r)); } $colWidth = (int) (9000 / max(1, $numCols)); $tbl = []; $tbl[] = ''; $tbl[] = ' '; $tbl[] = ''; for ($i = 0; $i < $numCols; $i++) { $tbl[] = ""; } $tbl[] = ''; foreach ($rows as $rowIdx => $row) { $isHeader = ($rowIdx === 0 && $headers); $tbl[] = ''; $trPr = ''; if ($isHeader) { $trPr .= ''; } $trPr .= ''; $tbl[] = $trPr; for ($c = 0; $c < $numCols; $c++) { $cellText = $row[$c] ?? ''; $cellBg = $isHeader ? 'E2E8F0' : ($rowIdx % 2 === 1 ? 'FFFFFF' : 'F8FAFC'); $tcPr = " "; $tbl[] = '' . $tcPr; $runs = parse_inline_formatting(trim($cellText), $isHeader ? 19 : 18, $isHeader ? '0F172A' : '334155'); $tbl[] = make_paragraph($runs, spaceBefore: 40, spaceAfter: 40); $tbl[] = ''; } $tbl[] = ''; } $tbl[] = ''; return implode('', $tbl); } function convert_markdown_to_docx(string $mdFile, string $docxFile): void { if (!file_exists($mdFile)) { fwrite(STDERR, "Error: Source markdown file not found: {$mdFile}\n"); exit(1); } $mdContent = file_get_contents($mdFile); $lines = explode("\n", $mdContent); $docElements = []; $inCodeBlock = false; $codeLines = []; $inTable = false; $tableRows = []; foreach ($lines as $line) { $trimmed = trim($line); // Code block if (str_starts_with($trimmed, '```')) { if ($inCodeBlock) { $inCodeBlock = false; $codeText = implode("\n", $codeLines); $codeRuns = make_run($codeText, fontName: 'Consolas', fontSize: 17, fontColor: '1E293B'); $docElements[] = make_paragraph($codeRuns, spaceBefore: 120, spaceAfter: 120, bgColor: 'F1F5F9'); $codeLines = []; } else { $inCodeBlock = true; $codeLines = []; } continue; } if ($inCodeBlock) { $codeLines[] = $line; continue; } // Table if (str_contains($line, '|') && (str_starts_with($trimmed, '|') || str_ends_with($trimmed, '|'))) { if (preg_match('/^[\|\s\-\:]+$/', $trimmed)) { continue; // Table separator line } $cells = array_map('trim', explode('|', $trimmed)); if (!empty($cells) && $cells[0] === '') array_shift($cells); if (!empty($cells) && end($cells) === '') array_pop($cells); $tableRows[] = $cells; $inTable = true; continue; } else { if ($inTable) { $docElements[] = make_table($tableRows); $tableRows = []; $inTable = false; } } if ($trimmed === '') { continue; } // Horizontal line if (in_array($trimmed, ['---', '***', '___'], true)) { $docElements[] = ''; continue; } // Headings if (str_starts_with($trimmed, '# ')) { $runs = parse_inline_formatting(substr($trimmed, 2), defaultSize: 34, defaultColor: '1E3A8A'); $docElements[] = make_paragraph($runs, spaceBefore: 320, spaceAfter: 120); } elseif (str_starts_with($trimmed, '## ')) { $runs = parse_inline_formatting(substr($trimmed, 3), defaultSize: 26, defaultColor: '1E40AF'); $docElements[] = make_paragraph($runs, spaceBefore: 240, spaceAfter: 100); } elseif (str_starts_with($trimmed, '### ')) { $runs = parse_inline_formatting(substr($trimmed, 4), defaultSize: 22, defaultColor: '0F172A'); $docElements[] = make_paragraph($runs, spaceBefore: 180, spaceAfter: 80); } elseif (str_starts_with($trimmed, '#### ')) { $runs = parse_inline_formatting(substr($trimmed, 5), defaultSize: 20, defaultColor: '334155'); $docElements[] = make_paragraph($runs, spaceBefore: 140, spaceAfter: 60); } elseif (str_starts_with($trimmed, '- ') || str_starts_with($trimmed, '* ')) { $bullet = make_run("• ", bold: true, fontSize: 21, fontColor: '2563EB'); $textRuns = parse_inline_formatting(substr($trimmed, 2), defaultSize: 21, defaultColor: '334155'); $docElements[] = make_paragraph($bullet . $textRuns, spaceBefore: 40, spaceAfter: 40); } elseif (preg_match('/^(\d+\.)\s+(.+)$/u', $trimmed, $matches)) { $numRun = make_run($matches[1] . ' ', bold: true, fontSize: 21, fontColor: '1E40AF'); $textRuns = parse_inline_formatting($matches[2], defaultSize: 21, defaultColor: '334155'); $docElements[] = make_paragraph($numRun . $textRuns, spaceBefore: 40, spaceAfter: 40); } elseif (str_starts_with($trimmed, '> ')) { $quoteRuns = parse_inline_formatting(substr($trimmed, 2), defaultSize: 21, defaultColor: '475569'); $docElements[] = make_paragraph($quoteRuns, spaceBefore: 100, spaceAfter: 100, bgColor: 'F8FAFC'); } else { $runs = parse_inline_formatting($trimmed, defaultSize: 21, defaultColor: '334155'); $docElements[] = make_paragraph($runs, spaceBefore: 60, spaceAfter: 60); } } if ($inTable && !empty($tableRows)) { $docElements[] = make_table($tableRows); } $documentXml = ' ' . implode('', $docElements) . ' '; $contentTypesXml = ' '; $relsXml = ' '; $docRelsXml = ' '; $stylesXml = ' '; $settingsXml = ' '; if (file_exists($docxFile)) { unlink($docxFile); } $zip = new ZipArchive(); if ($zip->open($docxFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { fwrite(STDERR, "Error: Could not create zip archive: {$docxFile}\n"); exit(1); } $zip->addFromString('[Content_Types].xml', $contentTypesXml); $zip->addFromString('_rels/.rels', $relsXml); $zip->addFromString('word/_rels/document.xml.rels', $docRelsXml); $zip->addFromString('word/styles.xml', $stylesXml); $zip->addFromString('word/settings.xml', $settingsXml); $zip->addFromString('word/document.xml', $documentXml); $zip->close(); echo "Successfully generated Microsoft Word Document: {$docxFile}\n"; } $mdFile = $argv[1] ?? 'docs/SYSTEM_SPECIFICATIONS_AND_LIFECYCLE_MANUAL.md'; $docxFile = $argv[2] ?? str_replace('.md', '.docx', $mdFile); convert_markdown_to_docx($mdFile, $docxFile);