";
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);