HTML converter for note bodies.
*
* Supports headings, bold/italic, links, and (nested) ordered and unordered
* lists. Not a full Markdown spec — just enough for the note UX.
*/
final class Markdown
{
/**
* Convert a Markdown-flavoured string to safe HTML.
*
* The final output is run through {@see wp_kses_post()}, so even a
* regex that accidentally lets a stray tag through is contained at
* the boundary.
*/
public static function toHtml(string $text): string
{
$text = str_replace(["\r\n", "\r"], "\n", $text);
$text = trim($text);
$inlinePatterns = [
'/\#{6}\s?(.*)/' => '
$1
',
'/\#{5}\s?(.*)/' => '$1
',
'/\#{4}\s?(.*)/' => '$1
',
'/\#{3}\s?(.*)/' => '$1
',
'/\#{2}\s?(.*)/' => '$1
',
'/\#{1}\s?(.*)/' => '$1
',
'/\*\*(.*?)\*\*|__(.*?)__/' => '$1$2',
'/\*(.*?)\*|_(.*?)_/' => '$1$2',
'/<(https?:\/\/[^\s<]+)>/' => '$1',
'/\[(.*?)\]\((.*?)\)/' => '$1',
'/(^|[\s>])(https?:\/\/[^\s<]+)(?=\s|$)/' => '$1$2',
];
$html = preg_replace(array_keys($inlinePatterns), array_values($inlinePatterns), $text);
$lines = explode("\n", $html);
$output = '';
$listStack = [];
$currentIndent = 0;
foreach ($lines as $line) {
preg_match('/^(\s*)/', $line, $leading);
$indent = strlen($leading[0]);
$line = ltrim($line);
if (preg_match('/^\d+\.\s+(.*)$/', $line, $matches)) {
[$output, $listStack, $currentIndent] = self::handleListItem(
'ol',
$matches[1],
$indent,
$output,
$listStack,
$currentIndent,
);
continue;
}
if (preg_match('/^-\s+(.*)$/', $line, $matches)) {
[$output, $listStack, $currentIndent] = self::handleListItem(
'ul',
$matches[1],
$indent,
$output,
$listStack,
$currentIndent,
);
continue;
}
while (! empty($listStack)) {
$list = array_pop($listStack);
$output .= $list['type'] === 'ul' ? "\n" : "\n";
}
$currentIndent = 0;
$output .= $line . "\n";
}
while (! empty($listStack)) {
$list = array_pop($listStack);
$output .= $list['type'] === 'ul' ? "\n" : "\n";
}
// Collapse runs of 3+ blank lines down to a single paragraph break
// so wpautop produces clean separators rather than ragged gaps.
$output = preg_replace('/\n{3,}/', "\n\n", $output);
return wp_kses_post(wpautop(trim($output)));
}
/**
* Convert plugin-generated HTML back to Markdown.
*
* Best-effort — used to round-trip note bodies during edits.
*/
public static function toMarkdown(string $html): string
{
$markdown = $html;
// Paragraph and break tags become explicit newlines before the
// generic tag-strip step runs, otherwise paragraph separators get
// swallowed and the source becomes one long run-on line.
$markdown = preg_replace('/
]*>/i', '', $markdown);
$markdown = preg_replace('/<\/p\s*>/i', "\n\n", $markdown);
$markdown = preg_replace('/
/i', "\n", $markdown);
$markdown = preg_replace('/