breaks_enabled = $breaks_enabled; return $this; } private $breaks_enabled = false; # # Synopsis # # Markdown is intended to be easy-to-read by humans - those of us who read # line by line, left to right, top to bottom. In order to take advantage of # this, Parsedown tries to read in a similar way. It breaks texts into # lines, it iterates through them and it looks at how they start and relate # to each other. # # Methods # function parse( $text ) { # standardize line breaks $text = str_replace( "\r\n", "\n", $text ); $text = str_replace( "\r", "\n", $text ); # replace tabs with spaces $text = str_replace( "\t", ' ', $text ); # remove surrounding line breaks $text = trim( $text, "\n" ); # split text into lines $lines = explode( "\n", $text ); # convert lines into html $text = $this->parse_block_elements( $lines ); # remove trailing line breaks $text = chop( $text, "\n" ); return $text; } # # Private private function parse_block_elements( array $lines, $context = '' ) { $blocks = array(); $block = array( 'type' => '', ); foreach ( $lines as $line ) { # context switch ( $block['type'] ) { case 'fenced': if ( ! isset( $block['closed'] ) ) { if ( preg_match( '/^[ ]*' . $block['fence'][0] . '{3,}[ ]*$/', $line ) ) { $block['closed'] = true; } else { if ( $block['text'] !== '' ) { $block['text'] .= "\n"; } $block['text'] .= $line; } continue 2; } break; case 'markup': if ( ! isset( $block['closed'] ) ) { if ( strpos( $line, $block['start'] ) !== false ) { # opening tag $block['depth'] ++; } if ( strpos( $line, $block['end'] ) !== false ) { # closing tag if ( $block['depth'] > 0 ) { $block['depth'] --; } else { $block['closed'] = true; } } $block['text'] .= "\n" . $line; continue 2; } break; } # ~ $indentation = 0; while ( isset( $line[ $indentation ] ) and $line[ $indentation ] === ' ' ) { $indentation ++; } $outdented_line = $indentation > 0 ? ltrim( $line ) : $line; # blank if ( $outdented_line === '' ) { $block['interrupted'] = true; continue; } # context switch ( $block['type'] ) { case 'quote': if ( ! isset( $block['interrupted'] ) ) { $line = preg_replace( '/^[ ]*>[ ]?/', '', $line ); $block['lines'] [] = $line; continue 2; } break; case 'li': if ( $block['indentation'] === $indentation and preg_match( '/^' . $block['marker'] . '[ ]+(.*)/', $outdented_line, $matches ) ) { unset( $block['last'] ); $blocks [] = $block; $block['last'] = true; $block['lines'] = array( $matches[1] ); unset( $block['first'] ); unset( $block['interrupted'] ); continue 2; } if ( ! isset( $block['interrupted'] ) ) { $line = preg_replace( '/^[ ]{0,' . $block['baseline'] . '}/', '', $line ); $block['lines'] [] = $line; continue 2; } elseif ( $line[0] === ' ' ) { $block['lines'] [] = ''; $line = preg_replace( '/^[ ]{0,' . $block['baseline'] . '}/', '', $line ); $block['lines'] [] = $line; unset( $block['interrupted'] ); continue 2; } break; } # indentation sensitive types switch ( $line[0] ) { case ' ': # code if ( $indentation >= 4 ) { $code_line = substr( $line, 4 ); if ( $block['type'] === 'code' ) { if ( isset( $block['interrupted'] ) ) { $block['text'] .= "\n"; unset( $block['interrupted'] ); } $block['text'] .= "\n" . $code_line; } else { $blocks [] = $block; $block = array( 'type' => 'code', 'text' => $code_line, ); } continue 2; } break; case '#': # atx heading (#) if ( isset( $line[1] ) ) { $blocks [] = $block; $level = 1; while ( isset( $line[ $level ] ) and $line[ $level ] === '#' ) { $level ++; } $block = array( 'type' => 'heading', 'text' => trim( $line, '# ' ), 'level' => $level, ); continue 2; } break; case '-': case '=': # setext heading (===) if ( $block['type'] === 'paragraph' and isset( $block['interrupted'] ) === false ) { $chopped_line = chop( $line ); $i = 1; while ( isset( $chopped_line[ $i ] ) ) { if ( $chopped_line[ $i ] !== $line[0] ) { break 2; } $i ++; } $block['type'] = 'heading'; $block['level'] = $line[0] === '-' ? 2 : 1; continue 2; } break; } # indentation insensitive types switch ( $outdented_line[0] ) { case '<': $position = strpos( $outdented_line, '>' ); if ( $position > 1 ) { $substring = substr( $outdented_line, 1, $position - 1 ); $substring = chop( $substring ); if ( substr( $substring, - 1 ) === '/' ) { $is_self_closing = true; $substring = substr( $substring, 0, - 1 ); } $position = strpos( $substring, ' ' ); if ( $position ) { $name = substr( $substring, 0, $position ); } else { $name = $substring; } if ( ! ctype_alpha( $name ) ) { break; } if ( in_array( $name, self::$text_level_elements ) ) { break; } $blocks [] = $block; if ( isset( $is_self_closing ) ) { $block = array( 'type' => 'self-closing tag', 'text' => $outdented_line, ); unset( $is_self_closing ); continue 2; } $block = array( 'type' => 'markup', 'text' => $outdented_line, 'start' => '<' . $name . '>', 'end' => '' . $name . '>', 'depth' => 0, ); if ( strpos( $outdented_line, $block['end'] ) ) { $block['closed'] = true; } continue 2; } break; case '>': # quote if ( preg_match( '/^>[ ]?(.*)/', $outdented_line, $matches ) ) { $blocks [] = $block; $block = array( 'type' => 'quote', 'lines' => array( $matches[1], ), ); continue 2; } break; case '[': # reference $position = strpos( $outdented_line, ']:' ); if ( $position ) { $reference = array(); $label = substr( $outdented_line, 1, $position - 1 ); $label = strtolower( $label ); $substring = substr( $outdented_line, $position + 2 ); $substring = trim( $substring ); if ( $substring === '' ) { break; } if ( $substring[0] === '<' ) { $position = strpos( $substring, '>' ); if ( $position === false ) { break; } $reference['»'] = substr( $substring, 1, $position - 1 ); $substring = substr( $substring, $position + 1 ); } else { $position = strpos( $substring, ' ' ); if ( $position === false ) { $reference['»'] = $substring; $substring = false; } else { $reference['»'] = substr( $substring, 0, $position ); $substring = substr( $substring, $position + 1 ); } } if ( $substring !== false ) { if ( $substring[0] !== '"' and $substring[0] !== "'" and $substring[0] !== '(' ) { break; } $last_char = substr( $substring, - 1 ); if ( $last_char !== '"' and $last_char !== "'" and $last_char !== ')' ) { break; } $reference['#'] = substr( $substring, 1, - 1 ); } $this->reference_map[ $label ] = $reference; continue 2; } break; case '`': case '~': # fenced code block if ( preg_match( '/^([`]{3,}|[~]{3,})[ ]*(\S+)?[ ]*$/', $outdented_line, $matches ) ) { $blocks [] = $block; $block = array( 'type' => 'fenced', 'text' => '', 'fence' => $matches[1], ); if ( isset( $matches[2] ) ) { $block['language'] = $matches[2]; } continue 2; } break; case '*': case '+': case '-': case '_': # hr if ( preg_match( '/^([-*_])([ ]{0,2}\1){2,}[ ]*$/', $outdented_line ) ) { $blocks [] = $block; $block = array( 'type' => 'rule', ); continue 2; } # li if ( preg_match( '/^([*+-][ ]+)(.*)/', $outdented_line, $matches ) ) { $blocks [] = $block; $baseline = $indentation + strlen( $matches[1] ); $block = array( 'type' => 'li', 'indentation' => $indentation, 'baseline' => $baseline, 'marker' => '[*+-]', 'first' => true, 'last' => true, 'lines' => array(), ); $block['lines'] [] = preg_replace( '/^[ ]{0,4}/', '', $matches[2] ); continue 2; } } # li if ( $outdented_line[0] <= '9' and preg_match( '/^(\d+[.][ ]+)(.*)/', $outdented_line, $matches ) ) { $blocks [] = $block; $baseline = $indentation + strlen( $matches[1] ); $block = array( 'type' => 'li', 'indentation' => $indentation, 'baseline' => $baseline, 'marker' => '\d+[.]', 'first' => true, 'last' => true, 'ordered' => true, 'lines' => array(), ); $block['lines'] [] = preg_replace( '/^[ ]{0,4}/', '', $matches[2] ); continue; } # paragraph if ( $block['type'] === 'paragraph' ) { if ( isset( $block['interrupted'] ) ) { $blocks [] = $block; $block['text'] = $line; unset( $block['interrupted'] ); } else { if ( $this->breaks_enabled ) { $block['text'] .= ' '; } $block['text'] .= "\n" . $line; } } else { $blocks [] = $block; $block = array( 'type' => 'paragraph', 'text' => $line, ); } } $blocks [] = $block; unset( $blocks[0] ); # $blocks » HTML $markup = ''; foreach ( $blocks as $block ) { switch ( $block['type'] ) { case 'paragraph': $text = $this->parse_span_elements( $block['text'] ); if ( $context === 'li' and $markup === '' ) { if ( isset( $block['interrupted'] ) ) { $markup .= "\n" . '
' . $text . '
' . "\n"; } else { $markup .= $text; if ( isset( $blocks[2] ) ) { $markup .= "\n"; } } } else { $markup .= '' . $text . '
' . "\n"; } break; case 'quote': $text = $this->parse_block_elements( $block['lines'] ); $markup .= '' . "\n" . $text . '' . "\n"; break; case 'code': $text = htmlspecialchars( $block['text'], ENT_NOQUOTES, 'UTF-8' ); $markup .= '
' . $text . '' . "\n";
break;
case 'fenced':
$text = htmlspecialchars( $block['text'], ENT_NOQUOTES, 'UTF-8' );
$markup .= '' . "\n";
break;
case 'heading':
$text = $this->parse_span_elements( $block['text'] );
$markup .= '