\s|^```|\*\*[^*\n]+\*\*|\[[^\]\n]+\]\([^)\s]+\)/m', $text ); } public static function to_html( $text ) { $text = str_replace( array( "\r\n", "\r" ), "\n", (string) $text ); $lines = explode( "\n", $text ); $html = array(); $para = array(); $count = count( $lines ); $flush = function () use ( &$para, &$html ) { if ( $para ) { $html[] = '

' . implode( "
\n", array_map( array( __CLASS__, 'inline' ), $para ) ) . '

'; $para = array(); } }; for ( $i = 0; $i < $count; $i++ ) { $line = $lines[ $i ]; // Fenced code block. if ( preg_match( '/^\s*```/', $line ) ) { $flush(); $code = array(); for ( $i++; $i < $count && ! preg_match( '/^\s*```/', $lines[ $i ] ); $i++ ) { $code[] = $lines[ $i ]; } $html[] = '
' . esc_html( implode( "\n", $code ) ) . '
'; continue; } if ( '' === trim( $line ) ) { $flush(); continue; } if ( preg_match( '/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/', $line, $m ) ) { $flush(); $level = strlen( $m[1] ); $html[] = "" . self::inline( $m[2] ) . ""; continue; } if ( preg_match( '/^\s{0,3}([-*_])(\s*\1){2,}\s*$/', $line ) ) { $flush(); $html[] = '
'; continue; } if ( preg_match( '/^\s{0,3}>\s?(.*)$/', $line ) ) { $flush(); $quote = array(); while ( $i < $count && preg_match( '/^\s{0,3}>\s?(.*)$/', $lines[ $i ], $q ) ) { $quote[] = $q[1]; ++$i; } --$i; $html[] = '
' . self::to_html( implode( "\n", $quote ) ) . '
'; continue; } if ( self::list_item( $line ) ) { $flush(); $html[] = self::render_list( $lines, $i ); continue; } $para[] = $line; } $flush(); return implode( "\n", $html ); } /** @return array|null array( indent, ordered, text ) */ protected static function list_item( $line ) { if ( preg_match( '/^(\s*)([-*+]|\d+[.)])\s+(.*)$/', $line, $m ) ) { return array( strlen( str_replace( "\t", ' ', $m[1] ) ), ctype_digit( rtrim( $m[2], '.)' ) ), $m[3] ); } return null; } /** Consume list lines from $i (left on the last list line). */ protected static function render_list( $lines, &$i ) { $first = self::list_item( $lines[ $i ] ); $base = $first[0]; $tag = $first[1] ? 'ol' : 'ul'; $items = array(); $count = count( $lines ); for ( ; $i < $count; $i++ ) { $item = self::list_item( $lines[ $i ] ); if ( ! $item ) { // A non-blank, indented continuation line belongs to the last item. if ( $items && '' !== trim( $lines[ $i ] ) && preg_match( '/^\s+/', $lines[ $i ] ) ) { $items[ count( $items ) - 1 ]['text'] .= ' ' . trim( $lines[ $i ] ); continue; } break; } if ( $item[0] > $base && $items ) { $items[ count( $items ) - 1 ]['sub'] = self::render_list( $lines, $i ); continue; } if ( $item[0] < $base ) { break; } $items[] = array( 'text' => $item[2], 'sub' => '', ); } --$i; $out = "<{$tag}>"; foreach ( $items as $item ) { $out .= '
  • ' . self::inline( $item['text'] ) . $item['sub'] . '
  • '; } return $out . ""; } /** * A link or image target. esc_url() would turn a bare "photo.png" into * "http://photo.png", and a bare filename is exactly how an image from the * uploaded zip is referred to — so only absolute URLs go through it. */ protected static function url( $target ) { return preg_match( '#^[a-z][a-z0-9+.-]*:|^//#i', $target ) ? esc_url( $target ) : esc_attr( $target ); } /** Inline spans: code, images, links, bold, italic, strikethrough. */ public static function inline( $text ) { $codes = array(); $text = preg_replace_callback( '/`([^`]+)`/', function ( $m ) use ( &$codes ) { $codes[] = '' . esc_html( $m[1] ) . ''; return "\x1A" . ( count( $codes ) - 1 ) . "\x1A"; }, $text ); $text = preg_replace_callback( '/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+"([^"]*)")?\s*\)/', function ( $m ) { return '' . esc_attr( $m[1] ) . ''; }, $text ); $text = preg_replace_callback( '/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+"([^"]*)")?\s*\)/', function ( $m ) { return '' . $m[1] . ''; }, $text ); $text = preg_replace( '/<(https?:\/\/[^>\s]+)>/', '$1', $text ); $text = preg_replace( '/(\*\*|__)(?=\S)(.+?)(?<=\S)\1/', '$2', $text ); $text = preg_replace( '/(?$1', $text ); $text = preg_replace( '/(?$1', $text ); $text = preg_replace( '/~~(?=\S)(.+?)(?<=\S)~~/', '$1', $text ); return preg_replace_callback( "/\x1A(\d+)\x1A/", function ( $m ) use ( $codes ) { return $codes[ (int) $m[1] ]; }, $text ); } }