1*7cb424c9SSioc de Narf<?php 2*7cb424c9SSioc de Narf 3*7cb424c9SSioc de Narfdeclare(strict_types=1); 4*7cb424c9SSioc de Narf 5*7cb424c9SSioc de Narf/** 6*7cb424c9SSioc de Narf * Converts Markdown content to DokuWiki syntax. 7*7cb424c9SSioc de Narf * 8*7cb424c9SSioc de Narf * This class processes Markdown line by line, maintaining state for 9*7cb424c9SSioc de Narf * code blocks, tables, lists (with nesting), and paragraphs. It supports: 10*7cb424c9SSioc de Narf * - Headers (levels 1-6) 11*7cb424c9SSioc de Narf * - Bold, italic, inline code 12*7cb424c9SSioc de Narf * - Links and images 13*7cb424c9SSioc de Narf * - Unordered and ordered lists (with indentation) 14*7cb424c9SSioc de Narf * - Tables (with alignment detection for headers) 15*7cb424c9SSioc de Narf * - Code blocks (```) 16*7cb424c9SSioc de Narf * - Blockquotes (simple) 17*7cb424c9SSioc de Narf * - Horizontal rules 18*7cb424c9SSioc de Narf * 19*7cb424c9SSioc de Narf * @license GPL 3 http://www.gnu.org/licenses/gpl-3.0.html 20*7cb424c9SSioc de Narf * @author sioc-de-narf 21*7cb424c9SSioc de Narf */ 22*7cb424c9SSioc de Narfclass MarkdownToDokuWikiConverter 23*7cb424c9SSioc de Narf{ 24*7cb424c9SSioc de Narf /** @var bool Whether we are currently inside a code block */ 25*7cb424c9SSioc de Narf private bool $inCodeBlock = false; 26*7cb424c9SSioc de Narf 27*7cb424c9SSioc de Narf /** @var bool Whether we are currently inside a table */ 28*7cb424c9SSioc de Narf private bool $inTable = false; 29*7cb424c9SSioc de Narf 30*7cb424c9SSioc de Narf /** @var array<int, array<int, string>> Rows of the current table */ 31*7cb424c9SSioc de Narf private array $tableRows = []; 32*7cb424c9SSioc de Narf 33*7cb424c9SSioc de Narf /** @var array<int, string> Alignments for each column of the current table */ 34*7cb424c9SSioc de Narf private array $tableAlignments = []; 35*7cb424c9SSioc de Narf 36*7cb424c9SSioc de Narf /** @var array<int, array{indent: int, type: string}> Stack tracking list nesting (indentation and type) */ 37*7cb424c9SSioc de Narf private array $listStack = []; 38*7cb424c9SSioc de Narf 39*7cb424c9SSioc de Narf /** @var array<int, string> Buffer for paragraph lines before they are flushed */ 40*7cb424c9SSioc de Narf private array $paragraphBuffer = []; 41*7cb424c9SSioc de Narf 42*7cb424c9SSioc de Narf /** 43*7cb424c9SSioc de Narf * Remove YAML front matter from the beginning of the document. 44*7cb424c9SSioc de Narf * 45*7cb424c9SSioc de Narf * Detects a block starting with '---' at the very first line, 46*7cb424c9SSioc de Narf * followed by any lines, and ending with '---' or '...'. 47*7cb424c9SSioc de Narf * If such a block is found, it is stripped. 48*7cb424c9SSioc de Narf * 49*7cb424c9SSioc de Narf * @param string $markdown The raw Markdown. 50*7cb424c9SSioc de Narf * @return string Markdown without the front matter. 51*7cb424c9SSioc de Narf */ 52*7cb424c9SSioc de Narf private function stripYamlFrontMatter(string $markdown): string 53*7cb424c9SSioc de Narf { 54*7cb424c9SSioc de Narf $lines = explode("\n", $markdown); 55*7cb424c9SSioc de Narf if (count($lines) === 0) { 56*7cb424c9SSioc de Narf return $markdown; 57*7cb424c9SSioc de Narf } 58*7cb424c9SSioc de Narf 59*7cb424c9SSioc de Narf // Trim leading empty lines to find the first non-empty line 60*7cb424c9SSioc de Narf $firstNonEmpty = 0; 61*7cb424c9SSioc de Narf while ($firstNonEmpty < count($lines) && trim($lines[$firstNonEmpty]) === '') { 62*7cb424c9SSioc de Narf $firstNonEmpty++; 63*7cb424c9SSioc de Narf } 64*7cb424c9SSioc de Narf 65*7cb424c9SSioc de Narf // If the first non-empty line is exactly '---', we have a front matter candidate 66*7cb424c9SSioc de Narf if ($firstNonEmpty < count($lines) && trim($lines[$firstNonEmpty]) === '---') { 67*7cb424c9SSioc de Narf $endLine = null; 68*7cb424c9SSioc de Narf // Look for the closing '---' or '...' after the opening 69*7cb424c9SSioc de Narf for ($i = $firstNonEmpty + 1; $i < count($lines); $i++) { 70*7cb424c9SSioc de Narf if (trim($lines[$i]) === '---' || trim($lines[$i]) === '...') { 71*7cb424c9SSioc de Narf $endLine = $i; 72*7cb424c9SSioc de Narf break; 73*7cb424c9SSioc de Narf } 74*7cb424c9SSioc de Narf } 75*7cb424c9SSioc de Narf // If we found a closing delimiter, remove all lines from start to end (inclusive) 76*7cb424c9SSioc de Narf if ($endLine !== null) { 77*7cb424c9SSioc de Narf $lines = array_slice($lines, $endLine + 1); 78*7cb424c9SSioc de Narf return implode("\n", $lines); 79*7cb424c9SSioc de Narf } 80*7cb424c9SSioc de Narf } 81*7cb424c9SSioc de Narf 82*7cb424c9SSioc de Narf // No front matter detected, return original 83*7cb424c9SSioc de Narf return $markdown; 84*7cb424c9SSioc de Narf } 85*7cb424c9SSioc de Narf 86*7cb424c9SSioc de Narf /** 87*7cb424c9SSioc de Narf * Convert Markdown to DokuWiki syntax. 88*7cb424c9SSioc de Narf * 89*7cb424c9SSioc de Narf * @param string $markdown The input Markdown text. 90*7cb424c9SSioc de Narf * @return string The converted DokuWiki text. 91*7cb424c9SSioc de Narf */ 92*7cb424c9SSioc de Narf public function convert(string $markdown): string 93*7cb424c9SSioc de Narf { 94*7cb424c9SSioc de Narf // Strip YAML front matter 95*7cb424c9SSioc de Narf $markdown = $this->stripYamlFrontMatter($markdown); 96*7cb424c9SSioc de Narf 97*7cb424c9SSioc de Narf // Normalize line endings and replace tabs with 4 spaces 98*7cb424c9SSioc de Narf $lines = explode("\n", str_replace(["\r\n", "\r", "\t"], ["\n", "\n", " "], $markdown)); 99*7cb424c9SSioc de Narf $output = []; 100*7cb424c9SSioc de Narf $this->reset(); 101*7cb424c9SSioc de Narf 102*7cb424c9SSioc de Narf $i = 0; 103*7cb424c9SSioc de Narf while ($i < count($lines)) { 104*7cb424c9SSioc de Narf $line = $lines[$i]; 105*7cb424c9SSioc de Narf $nextLine = $i + 1 < count($lines) ? $lines[$i + 1] : null; 106*7cb424c9SSioc de Narf 107*7cb424c9SSioc de Narf // Code block handling 108*7cb424c9SSioc de Narf if (str_starts_with(trim($line), '```')) { 109*7cb424c9SSioc de Narf $this->handleCodeBlock($line, $output); 110*7cb424c9SSioc de Narf $i++; 111*7cb424c9SSioc de Narf continue; 112*7cb424c9SSioc de Narf } 113*7cb424c9SSioc de Narf if ($this->inCodeBlock) { 114*7cb424c9SSioc de Narf $output[] = $line; 115*7cb424c9SSioc de Narf $i++; 116*7cb424c9SSioc de Narf continue; 117*7cb424c9SSioc de Narf } 118*7cb424c9SSioc de Narf 119*7cb424c9SSioc de Narf // Table detection 120*7cb424c9SSioc de Narf if ($this->isTableStart($line, $nextLine)) { 121*7cb424c9SSioc de Narf $this->parseTable($lines, $i); 122*7cb424c9SSioc de Narf $output[] = $this->renderTable(); 123*7cb424c9SSioc de Narf continue; 124*7cb424c9SSioc de Narf } 125*7cb424c9SSioc de Narf 126*7cb424c9SSioc de Narf // Horizontal rule 127*7cb424c9SSioc de Narf if ($this->isHorizontalRule($line)) { 128*7cb424c9SSioc de Narf $this->flushParagraph($output); 129*7cb424c9SSioc de Narf $output[] = '----'; 130*7cb424c9SSioc de Narf $i++; 131*7cb424c9SSioc de Narf continue; 132*7cb424c9SSioc de Narf } 133*7cb424c9SSioc de Narf 134*7cb424c9SSioc de Narf // Blockquote 135*7cb424c9SSioc de Narf if ($this->isBlockquote($line)) { 136*7cb424c9SSioc de Narf $this->flushParagraph($output); 137*7cb424c9SSioc de Narf $output[] = $this->renderBlockquote($line); 138*7cb424c9SSioc de Narf $i++; 139*7cb424c9SSioc de Narf continue; 140*7cb424c9SSioc de Narf } 141*7cb424c9SSioc de Narf 142*7cb424c9SSioc de Narf // List item 143*7cb424c9SSioc de Narf if ($this->isListItem($line)) { 144*7cb424c9SSioc de Narf $this->handleList($line, $output); 145*7cb424c9SSioc de Narf $i++; 146*7cb424c9SSioc de Narf continue; 147*7cb424c9SSioc de Narf } 148*7cb424c9SSioc de Narf 149*7cb424c9SSioc de Narf // Header 150*7cb424c9SSioc de Narf if ($this->isTitle($line)) { 151*7cb424c9SSioc de Narf $this->flushParagraph($output); 152*7cb424c9SSioc de Narf $output[] = $this->renderTitle($line); 153*7cb424c9SSioc de Narf $i++; 154*7cb424c9SSioc de Narf continue; 155*7cb424c9SSioc de Narf } 156*7cb424c9SSioc de Narf 157*7cb424c9SSioc de Narf // Empty line 158*7cb424c9SSioc de Narf if (trim($line) === '') { 159*7cb424c9SSioc de Narf $this->flushParagraph($output); 160*7cb424c9SSioc de Narf $output[] = ''; 161*7cb424c9SSioc de Narf $i++; 162*7cb424c9SSioc de Narf continue; 163*7cb424c9SSioc de Narf } 164*7cb424c9SSioc de Narf 165*7cb424c9SSioc de Narf // Normal paragraph line 166*7cb424c9SSioc de Narf $this->paragraphBuffer[] = $this->convertInline($line); 167*7cb424c9SSioc de Narf $i++; 168*7cb424c9SSioc de Narf } 169*7cb424c9SSioc de Narf 170*7cb424c9SSioc de Narf $this->flushParagraph($output); 171*7cb424c9SSioc de Narf $this->closeLists($output); 172*7cb424c9SSioc de Narf 173*7cb424c9SSioc de Narf return implode("\n", $output); 174*7cb424c9SSioc de Narf } 175*7cb424c9SSioc de Narf 176*7cb424c9SSioc de Narf /** 177*7cb424c9SSioc de Narf * Reset internal state. 178*7cb424c9SSioc de Narf */ 179*7cb424c9SSioc de Narf private function reset(): void 180*7cb424c9SSioc de Narf { 181*7cb424c9SSioc de Narf $this->inCodeBlock = false; 182*7cb424c9SSioc de Narf $this->inTable = false; 183*7cb424c9SSioc de Narf $this->tableRows = []; 184*7cb424c9SSioc de Narf $this->tableAlignments = []; 185*7cb424c9SSioc de Narf $this->listStack = []; 186*7cb424c9SSioc de Narf $this->paragraphBuffer = []; 187*7cb424c9SSioc de Narf } 188*7cb424c9SSioc de Narf 189*7cb424c9SSioc de Narf /** 190*7cb424c9SSioc de Narf * Handle a code block delimiter (```). 191*7cb424c9SSioc de Narf * 192*7cb424c9SSioc de Narf * @param string $line The current line. 193*7cb424c9SSioc de Narf * @param string[] &$output The output array being built. 194*7cb424c9SSioc de Narf */ 195*7cb424c9SSioc de Narf private function handleCodeBlock(string $line, array &$output): void 196*7cb424c9SSioc de Narf { 197*7cb424c9SSioc de Narf if (!$this->inCodeBlock) { 198*7cb424c9SSioc de Narf $lang = trim(substr(trim($line), 3)); 199*7cb424c9SSioc de Narf $output[] = "<code" . ($lang ? " $lang" : "") . ">"; 200*7cb424c9SSioc de Narf $this->inCodeBlock = true; 201*7cb424c9SSioc de Narf } else { 202*7cb424c9SSioc de Narf $output[] = "</code>"; 203*7cb424c9SSioc de Narf $this->inCodeBlock = false; 204*7cb424c9SSioc de Narf } 205*7cb424c9SSioc de Narf } 206*7cb424c9SSioc de Narf 207*7cb424c9SSioc de Narf /** 208*7cb424c9SSioc de Narf * Determine if a line starts a Markdown table. 209*7cb424c9SSioc de Narf * 210*7cb424c9SSioc de Narf * @param string $line The current line. 211*7cb424c9SSioc de Narf * @param string|null $nextLine The next line (if any). 212*7cb424c9SSioc de Narf * @return bool True if a table starts here. 213*7cb424c9SSioc de Narf */ 214*7cb424c9SSioc de Narf private function isTableStart(string $line, ?string $nextLine): bool 215*7cb424c9SSioc de Narf { 216*7cb424c9SSioc de Narf return strpos($line, '|') !== false && $nextLine && preg_match('/^[\s\|:\-]+$/', $nextLine); 217*7cb424c9SSioc de Narf } 218*7cb424c9SSioc de Narf 219*7cb424c9SSioc de Narf /** 220*7cb424c9SSioc de Narf * Parse a Markdown table from the current position. 221*7cb424c9SSioc de Narf * 222*7cb424c9SSioc de Narf * @param string[] $lines The whole array of lines. 223*7cb424c9SSioc de Narf * @param int &$i Current index (will be advanced to after the table). 224*7cb424c9SSioc de Narf */ 225*7cb424c9SSioc de Narf private function parseTable(array $lines, int &$i): void 226*7cb424c9SSioc de Narf { 227*7cb424c9SSioc de Narf $headerLine = $lines[$i++]; 228*7cb424c9SSioc de Narf $separatorLine = $lines[$i++]; 229*7cb424c9SSioc de Narf 230*7cb424c9SSioc de Narf // Detect column alignments from separator line 231*7cb424c9SSioc de Narf $this->tableAlignments = array_map( 232*7cb424c9SSioc de Narf fn($part) => match (true) { 233*7cb424c9SSioc de Narf str_starts_with(trim($part), ':') && str_ends_with(trim($part), ':') => 'center', 234*7cb424c9SSioc de Narf str_ends_with(trim($part), ':') => 'right', 235*7cb424c9SSioc de Narf str_starts_with(trim($part), ':') => 'left', 236*7cb424c9SSioc de Narf default => 'left', 237*7cb424c9SSioc de Narf }, 238*7cb424c9SSioc de Narf explode('|', trim($separatorLine, '|')) 239*7cb424c9SSioc de Narf ); 240*7cb424c9SSioc de Narf 241*7cb424c9SSioc de Narf $this->tableRows = [$this->parseTableRow($headerLine)]; 242*7cb424c9SSioc de Narf while ($i < count($lines) && strpos($lines[$i], '|') !== false && !preg_match('/^[\s\|:\-]+$/', $lines[$i])) { 243*7cb424c9SSioc de Narf $this->tableRows[] = $this->parseTableRow($lines[$i]); 244*7cb424c9SSioc de Narf $i++; 245*7cb424c9SSioc de Narf } 246*7cb424c9SSioc de Narf } 247*7cb424c9SSioc de Narf 248*7cb424c9SSioc de Narf /** 249*7cb424c9SSioc de Narf * Parse a single Markdown table row into an array of cells. 250*7cb424c9SSioc de Narf * 251*7cb424c9SSioc de Narf * @param string $line The table row line. 252*7cb424c9SSioc de Narf * @return string[] Array of cell contents. 253*7cb424c9SSioc de Narf */ 254*7cb424c9SSioc de Narf private function parseTableRow(string $line): array 255*7cb424c9SSioc de Narf { 256*7cb424c9SSioc de Narf return array_map('trim', explode('|', trim($line, '|'))); 257*7cb424c9SSioc de Narf } 258*7cb424c9SSioc de Narf 259*7cb424c9SSioc de Narf /** 260*7cb424c9SSioc de Narf * Render the parsed table as DokuWiki syntax. 261*7cb424c9SSioc de Narf * 262*7cb424c9SSioc de Narf * @return string DokuWiki table representation. 263*7cb424c9SSioc de Narf */ 264*7cb424c9SSioc de Narf private function renderTable(): string 265*7cb424c9SSioc de Narf { 266*7cb424c9SSioc de Narf $output = []; 267*7cb424c9SSioc de Narf foreach ($this->tableRows as $rowIndex => $row) { 268*7cb424c9SSioc de Narf $dokuRow = []; 269*7cb424c9SSioc de Narf foreach ($row as $colIndex => $cell) { 270*7cb424c9SSioc de Narf $cell = $this->convertInline($cell); 271*7cb424c9SSioc de Narf $dokuRow[] = ($rowIndex === 0 ? '^ ' : '| ') . $cell . ($rowIndex === 0 ? ' ^' : ' |'); 272*7cb424c9SSioc de Narf } 273*7cb424c9SSioc de Narf $output[] = implode('', $dokuRow); 274*7cb424c9SSioc de Narf } 275*7cb424c9SSioc de Narf return implode("\n", $output); 276*7cb424c9SSioc de Narf } 277*7cb424c9SSioc de Narf 278*7cb424c9SSioc de Narf /** 279*7cb424c9SSioc de Narf * Check if a line is a Markdown list item. 280*7cb424c9SSioc de Narf * 281*7cb424c9SSioc de Narf * @param string $line The line. 282*7cb424c9SSioc de Narf * @return bool True if it's a list item. 283*7cb424c9SSioc de Narf */ 284*7cb424c9SSioc de Narf private function isListItem(string $line): bool 285*7cb424c9SSioc de Narf { 286*7cb424c9SSioc de Narf return preg_match('/^\s*([\*\-\+]|\d+\.)\s/', $line) === 1; 287*7cb424c9SSioc de Narf } 288*7cb424c9SSioc de Narf 289*7cb424c9SSioc de Narf /** 290*7cb424c9SSioc de Narf * Handle a list item line, managing nesting via indentation. 291*7cb424c9SSioc de Narf * 292*7cb424c9SSioc de Narf * @param string $line The list item line. 293*7cb424c9SSioc de Narf * @param string[] &$output The output array. 294*7cb424c9SSioc de Narf */ 295*7cb424c9SSioc de Narf private function handleList(string $line, array &$output): void 296*7cb424c9SSioc de Narf { 297*7cb424c9SSioc de Narf $this->flushParagraph($output); 298*7cb424c9SSioc de Narf $indent = $this->calculateIndent($line); 299*7cb424c9SSioc de Narf $type = preg_match('/^\s*\d+\.\s/', $line) ? 'ordered' : 'unordered'; 300*7cb424c9SSioc de Narf 301*7cb424c9SSioc de Narf // Close deeper lists if indentation decreased 302*7cb424c9SSioc de Narf while (!empty($this->listStack) && $indent <= $this->listStack[count($this->listStack) - 1]['indent']) { 303*7cb424c9SSioc de Narf array_pop($this->listStack); 304*7cb424c9SSioc de Narf } 305*7cb424c9SSioc de Narf 306*7cb424c9SSioc de Narf $this->listStack[] = ['indent' => $indent, 'type' => $type]; 307*7cb424c9SSioc de Narf $dokuIndent = str_repeat(' ', count($this->listStack) - 1); 308*7cb424c9SSioc de Narf 309*7cb424c9SSioc de Narf // Remove the list marker and any leading spaces, then convert inline 310*7cb424c9SSioc de Narf $content = $this->convertInline(preg_replace('/^\s*([\*\-\+]|\d+\.)\s+/', '', $line)); 311*7cb424c9SSioc de Narf $output[] = $dokuIndent . ($type === 'ordered' ? '- ' : '* ') . $content; 312*7cb424c9SSioc de Narf } 313*7cb424c9SSioc de Narf 314*7cb424c9SSioc de Narf /** 315*7cb424c9SSioc de Narf * Calculate the indentation level (number of leading spaces) of a line. 316*7cb424c9SSioc de Narf * 317*7cb424c9SSioc de Narf * @param string $line The line. 318*7cb424c9SSioc de Narf * @return int Number of leading spaces. 319*7cb424c9SSioc de Narf */ 320*7cb424c9SSioc de Narf private function calculateIndent(string $line): int 321*7cb424c9SSioc de Narf { 322*7cb424c9SSioc de Narf return strlen($line) - strlen(ltrim($line)); 323*7cb424c9SSioc de Narf } 324*7cb424c9SSioc de Narf 325*7cb424c9SSioc de Narf /** 326*7cb424c9SSioc de Narf * Close any remaining open lists (reset stack). 327*7cb424c9SSioc de Narf * 328*7cb424c9SSioc de Narf * @param string[] &$output The output array (unused, kept for consistency). 329*7cb424c9SSioc de Narf */ 330*7cb424c9SSioc de Narf private function closeLists(array &$output): void 331*7cb424c9SSioc de Narf { 332*7cb424c9SSioc de Narf $this->listStack = []; 333*7cb424c9SSioc de Narf } 334*7cb424c9SSioc de Narf 335*7cb424c9SSioc de Narf /** 336*7cb424c9SSioc de Narf * Check if a line is a Markdown header (starts with #). 337*7cb424c9SSioc de Narf * 338*7cb424c9SSioc de Narf * @param string $line The line. 339*7cb424c9SSioc de Narf * @return bool True if it's a header. 340*7cb424c9SSioc de Narf */ 341*7cb424c9SSioc de Narf private function isTitle(string $line): bool 342*7cb424c9SSioc de Narf { 343*7cb424c9SSioc de Narf return preg_match('/^(#{1,6})\s+(.+)$/', trim($line)) === 1; 344*7cb424c9SSioc de Narf } 345*7cb424c9SSioc de Narf 346*7cb424c9SSioc de Narf /** 347*7cb424c9SSioc de Narf * Render a Markdown header as a DokuWiki header. 348*7cb424c9SSioc de Narf * 349*7cb424c9SSioc de Narf * @param string $line The header line. 350*7cb424c9SSioc de Narf * @return string DokuWiki header. 351*7cb424c9SSioc de Narf */ 352*7cb424c9SSioc de Narf private function renderTitle(string $line): string 353*7cb424c9SSioc de Narf { 354*7cb424c9SSioc de Narf preg_match('/^(#{1,6})\s+(.+)$/', trim($line), $matches); 355*7cb424c9SSioc de Narf $level = strlen($matches[1]); 356*7cb424c9SSioc de Narf $title = trim($matches[2]); 357*7cb424c9SSioc de Narf $equals = str_repeat('=', 7 - $level); 358*7cb424c9SSioc de Narf return "$equals $title $equals"; 359*7cb424c9SSioc de Narf } 360*7cb424c9SSioc de Narf 361*7cb424c9SSioc de Narf /** 362*7cb424c9SSioc de Narf * Check if a line is a horizontal rule (three or more -, *, _). 363*7cb424c9SSioc de Narf * 364*7cb424c9SSioc de Narf * @param string $line The line. 365*7cb424c9SSioc de Narf * @return bool True if it's a horizontal rule. 366*7cb424c9SSioc de Narf */ 367*7cb424c9SSioc de Narf private function isHorizontalRule(string $line): bool 368*7cb424c9SSioc de Narf { 369*7cb424c9SSioc de Narf return preg_match('/^[-*_]{3,}\s*$/', trim($line)) === 1; 370*7cb424c9SSioc de Narf } 371*7cb424c9SSioc de Narf 372*7cb424c9SSioc de Narf /** 373*7cb424c9SSioc de Narf * Check if a line is a blockquote (starts with >). 374*7cb424c9SSioc de Narf * 375*7cb424c9SSioc de Narf * @param string $line The line. 376*7cb424c9SSioc de Narf * @return bool True if it's a blockquote. 377*7cb424c9SSioc de Narf */ 378*7cb424c9SSioc de Narf private function isBlockquote(string $line): bool 379*7cb424c9SSioc de Narf { 380*7cb424c9SSioc de Narf return str_starts_with(ltrim($line), '>'); 381*7cb424c9SSioc de Narf } 382*7cb424c9SSioc de Narf 383*7cb424c9SSioc de Narf /** 384*7cb424c9SSioc de Narf * Render a blockquote line. 385*7cb424c9SSioc de Narf * 386*7cb424c9SSioc de Narf * @param string $line The blockquote line. 387*7cb424c9SSioc de Narf * @return string DokuWiki blockquote (>> ...). 388*7cb424c9SSioc de Narf */ 389*7cb424c9SSioc de Narf private function renderBlockquote(string $line): string 390*7cb424c9SSioc de Narf { 391*7cb424c9SSioc de Narf // Remove leading '>' and any following space, then convert inline 392*7cb424c9SSioc de Narf return '>> ' . $this->convertInline(substr(ltrim($line), 1)); 393*7cb424c9SSioc de Narf } 394*7cb424c9SSioc de Narf 395*7cb424c9SSioc de Narf /** 396*7cb424c9SSioc de Narf * Convert inline Markdown formatting to DokuWiki. 397*7cb424c9SSioc de Narf * 398*7cb424c9SSioc de Narf * Handles bold, italic, inline code, images, and links. 399*7cb424c9SSioc de Narf * 400*7cb424c9SSioc de Narf * @param string $text The text to convert. 401*7cb424c9SSioc de Narf * @return string Converted text. 402*7cb424c9SSioc de Narf */ 403*7cb424c9SSioc de Narf private function convertInline(string $text): string 404*7cb424c9SSioc de Narf { 405*7cb424c9SSioc de Narf // Bold: **text** or __text__ → **text** (same in DokuWiki) 406*7cb424c9SSioc de Narf $text = preg_replace('/\*\*(.+?)\*\*/', '**$1**', $text); 407*7cb424c9SSioc de Narf $text = preg_replace('/__(.+?)__/', '**$1**', $text); 408*7cb424c9SSioc de Narf 409*7cb424c9SSioc de Narf // Italic: *text* or _text_ → //text// 410*7cb424c9SSioc de Narf $text = preg_replace('/\*(.+?)\*/', '//$1//', $text); 411*7cb424c9SSioc de Narf $text = preg_replace('/_(.+?)_/', '//$1//', $text); 412*7cb424c9SSioc de Narf 413*7cb424c9SSioc de Narf // Inline code: `code` → ''code'' 414*7cb424c9SSioc de Narf $text = preg_replace('/`(.+?)`/', "''$1''", $text); 415*7cb424c9SSioc de Narf 416*7cb424c9SSioc de Narf // Images:  → {{url|alt}} 417*7cb424c9SSioc de Narf $text = preg_replace('/!\[([^\]]*)\]\(([^)]+)\)/', '{{$2|$1}}', $text); 418*7cb424c9SSioc de Narf 419*7cb424c9SSioc de Narf // Links: [text](url) → [[url|text]] 420*7cb424c9SSioc de Narf $text = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '[[$2|$1]]', $text); 421*7cb424c9SSioc de Narf 422*7cb424c9SSioc de Narf return $text; 423*7cb424c9SSioc de Narf } 424*7cb424c9SSioc de Narf 425*7cb424c9SSioc de Narf /** 426*7cb424c9SSioc de Narf * Flush any buffered paragraph lines to the output. 427*7cb424c9SSioc de Narf * 428*7cb424c9SSioc de Narf * @param string[] &$output The output array. 429*7cb424c9SSioc de Narf */ 430*7cb424c9SSioc de Narf private function flushParagraph(array &$output): void 431*7cb424c9SSioc de Narf { 432*7cb424c9SSioc de Narf if (!empty($this->paragraphBuffer)) { 433*7cb424c9SSioc de Narf $output[] = implode(' ', $this->paragraphBuffer); 434*7cb424c9SSioc de Narf $this->paragraphBuffer = []; 435*7cb424c9SSioc de Narf } 436*7cb424c9SSioc de Narf } 437*7cb424c9SSioc de Narf} 438