<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 */

namespace Matomo\Decompress;

/**
 * Unzip wrapper around PclZip
 */
class PclZip implements DecompressInterface
{
    /**
     * @var \PclZip
     */
    private $pclzip;
    /**
     * @var string
     */
    public $filename;

    /**
     * Constructor
     *
     * @param string $filename Name of the .zip archive
     */
    public function __construct($filename)
    {
        $this->pclzip   = new \PclZip($filename);
        $this->filename = $filename;
    }

    /**
     * Extract files from archive to target directory
     *
     * @param string $pathExtracted Absolute path of target directory
     * @return mixed  Array of filenames if successful; or 0 if an error occurred
     */
    public function extract($pathExtracted)
    {
        $pathExtracted = str_replace('\\', '/', $pathExtracted);
        $list = $this->pclzip->listContent();
        if (empty($list)) {
            return 0;
        }

        foreach ($list as $entry) {
            $filename = str_replace('\\', '/', $entry['stored_filename']);
            $parts    = explode('/', $filename);

            if (!strncmp($filename, '/', 1) ||
                array_search('..', $parts) !== false ||
                strpos($filename, ':') !== false
            ) {
                return 0;
            }
        }

        // PCLZIP_CB_PRE_EXTRACT callback returns 0 to skip, 1 to resume, or 2 to abort
        return $this->pclzip->extract(
            PCLZIP_OPT_PATH, $pathExtracted,
            PCLZIP_OPT_STOP_ON_ERROR,
            PCLZIP_OPT_REPLACE_NEWER,
            PCLZIP_CB_PRE_EXTRACT, function ($p_event, &$p_header) use ($pathExtracted) {
                return strncmp($p_header['filename'], $pathExtracted, strlen($pathExtracted)) ? 0 : 1;
            }
        );
    }

    /**
     * Get error status string for the latest error
     *
     * @return string
     */
    public function errorInfo()
    {
        return $this->pclzip->errorInfo(true);
    }
}


<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 */

namespace Matomo\Decompress;

use Exception;

/**
 * Unzip wrapper around ZipArchive
 */
class ZipArchive implements DecompressInterface
{
    /**
     * @var \ZipArchive
     */
    private $ziparchive;
    /**
     * @var string
     */
    public $filename;

    /**
     * Constructor
     *
     * @param string $filename Name of the .zip archive
     * @throws Exception
     */
    public function __construct($filename)
    {
        $this->filename = $filename;
        $this->ziparchive = new \ZipArchive;
        if ($this->ziparchive->open($filename) !== true) {
            throw new Exception('Error opening ' . $filename);
        }
    }

    /**
     * Extract files from archive to target directory
     *
     * @param string $pathExtracted Absolute path of target directory
     * @return mixed  Array of filenames if successful; or 0 if an error occurred
     */
    public function extract($pathExtracted)
    {
        if (substr($pathExtracted, -1) !== '/') {
            $pathExtracted .= '/';
        }

        $fileselector = array();
        $list  = array();
        $count = $this->ziparchive->numFiles;

        if ($count === 0) {
            return 0;
        }

        for ($i = 0; $i < $count; $i++) {
            $entry = $this->ziparchive->statIndex($i);

            $filename = str_replace('\\', '/', $entry['name']);
            $parts    = explode('/', $filename);

            if (!strncmp($filename, '/', 1) ||
                array_search('..', $parts) !== false ||
                strpos($filename, ':') !== false
            ) {
                return 0;
            }

            $fileselector[] = $entry['name'];
            $list[] = array(
                'filename'        => $pathExtracted . $entry['name'],
                'stored_filename' => $entry['name'],
                'size'            => $entry['size'],
                'compressed_size' => $entry['comp_size'],
                'mtime'           => $entry['mtime'],
                'index'           => $i,
                'crc'             => $entry['crc'],
            );
        }

        $res = $this->ziparchive->extractTo($pathExtracted, $fileselector);
        if ($res === false) {
            return 0;
        }

        return $list;
    }

    /**
     * Get error status string for the latest error
     *
     * @return string
     */
    public function errorInfo()
    {
        static $statusStrings = array(
            \ZipArchive::ER_OK          => 'No error',
            \ZipArchive::ER_MULTIDISK   => 'Multi-disk zip archives not supported',
            \ZipArchive::ER_RENAME      => 'Renaming temporary file failed',
            \ZipArchive::ER_CLOSE       => 'Closing zip archive failed',
            \ZipArchive::ER_SEEK        => 'Seek error',
            \ZipArchive::ER_READ        => 'Read error',
            \ZipArchive::ER_WRITE       => 'Write error',
            \ZipArchive::ER_CRC         => 'CRC error',
            \ZipArchive::ER_ZIPCLOSED   => 'Containing zip archive was closed',
            \ZipArchive::ER_NOENT       => 'No such file',
            \ZipArchive::ER_EXISTS      => 'File already exists',
            \ZipArchive::ER_OPEN        => 'Can\'t open file',
            \ZipArchive::ER_TMPOPEN     => 'Failure to create temporary file',
            \ZipArchive::ER_ZLIB        => 'Zlib error',
            \ZipArchive::ER_MEMORY      => 'Malloc failure',
            \ZipArchive::ER_CHANGED     => 'Entry has been changed',
            \ZipArchive::ER_COMPNOTSUPP => 'Compression method not supported',
            \ZipArchive::ER_EOF         => 'Premature EOF',
            \ZipArchive::ER_INVAL       => 'Invalid argument',
            \ZipArchive::ER_NOZIP       => 'Not a zip archive',
            \ZipArchive::ER_INTERNAL    => 'Internal error',
            \ZipArchive::ER_INCONS      => 'Zip archive inconsistent',
            \ZipArchive::ER_REMOVE      => 'Can\'t remove file',
            \ZipArchive::ER_DELETED     => 'Entry has been deleted',
        );

        if (isset($statusStrings[$this->ziparchive->status])) {
            $statusString = $statusStrings[$this->ziparchive->status];
        } else {
            $statusString = 'Unknown status';
        }

        return $statusString . '(' . $this->ziparchive->status . ')';
    }
}


<?php

namespace Ciconia\Extension\Gfm;

use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Markdown;

/**
 * Original source code from GitHub Flavored Markdown
 *
 * > Copyright 2013 GitHub Inc.
 * > https://help.github.com/articles/github-flavored-markdown
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class WhiteSpaceExtension implements ExtensionInterface
{

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $markdown->on('inline', array($this, 'processHardBreak'), 75);
    }

    /**
     * Newlines
     *
     * The biggest difference that GFM introduces is in the handling of line breaks.
     * With SM you can hard wrap paragraphs of text and they will be combined into a single paragraph.
     * We find this to be the cause of a huge number of unintentional formatting errors.
     * GFM treats newlines in paragraph-like content as real line breaks, which is probably what you intended.
     *
     * @param Text $text
     */
    public function processHardBreak(Text $text)
    {
        $text->replace('/^[\S\<][^\n]*\n+(?!( |\t)*<)/m', function (Text $w) {
            if ($w->match('/\n{2}/') || $w->match('/  \n/')) {
                return $w;
            }

            return $w->trim()->append("  \n");
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'gfmWhitespace';
    }

}


<?php

namespace Ciconia\Console\Command;

use Ciconia\Ciconia;
use Ciconia\Exception\SyntaxError;
use Ciconia\Extension\Gfm\FencedCodeBlockExtension;
use Ciconia\Extension\Gfm\InlineStyleExtension;
use Ciconia\Extension\Gfm\TableExtension;
use Ciconia\Extension\Gfm\TaskListExtension;
use Ciconia\Extension\Gfm\UrlAutoLinkExtension;
use Ciconia\Extension\Gfm\WhiteSpaceExtension;
use Ciconia\Renderer\XhtmlRenderer;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Helper\TableHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Command-line interface
 *
 * ### Tips: Add ciconia command inside your Symfony bundle
 *
 * 1. Add a dependency to your composer.json `"kzykhys/ciconia":"dev-master"`
 * 2. Just create a command that extends `Ciconia\Console\Command\CiconiaCommand` into bundle's `Command` directory.
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class CiconiaCommand extends Command
{

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('ciconia')
            ->setDescription('Translates markdown into HTML and displays to STDOUT')
            ->addArgument('file', InputArgument::OPTIONAL, 'The input file')
            ->addOption('gfm', null, InputOption::VALUE_NONE, 'Activate Gfm extensions')
            ->addOption('compress', 'c', InputOption::VALUE_NONE, 'Remove whitespace between HTML tags')
            ->addOption('diagnose', null, InputOption::VALUE_NONE, 'Display events and extensions information')
            ->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Output format (html|xhtml)', 'html')
            ->addOption('lint', 'l', InputOption::VALUE_NONE, 'Syntax check only (lint)')
            ->setHelp($this->getHelpContent())
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $content = $this->handleInput($input);
        } catch (\Exception $e) {
            $output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
            $this->runHelp($input, $output);

            return 1;
        }

        $ciconia = $this->createCiconia($input);

        if ($input->getOption('diagnose')) {
            return $this->diagnose($output, $ciconia, $content);
        }

        if ($input->getOption('lint')) {
            return $this->lint($output, $ciconia, $content);
        }

        $html = $ciconia->render($content);

        if ($input->getOption('compress')) {
            $html = preg_replace('/>\s+</', '><', $html);
        }

        $output->write($html, false, OutputInterface::OUTPUT_RAW);

        return 0;
    }

    /**
     * Get a markdown content from input
     *
     * __Warning: Reading from STDIN always fails on Windows__
     *
     * @param InputInterface $input
     *
     * @throws \InvalidArgumentException
     *
     * @return string
     */
    protected function handleInput(InputInterface $input)
    {
        if ($file = $input->getArgument('file')) {
            if (!file_exists($file)) {
                throw new \InvalidArgumentException(sprintf('The input file "%s" not found', $file));
            }

            return file_get_contents($file);
        } else {
            $contents = '';

            if ($stdin = fopen('php://stdin', 'r')) {
                // Warning: stream_set_blocking always fails on Windows
                if (stream_set_blocking($stdin, false)) {
                    $contents = stream_get_contents($stdin);
                }

                fclose($stdin);
            }

            // @codeCoverageIgnoreStart
            if ($contents) {
                return $contents;
            }
            // @codeCoverageIgnoreEnd
        }

        throw new \InvalidArgumentException('No input file');
    }

    /**
     * Creates an instance of Ciconia
     *
     * @param InputInterface $input The InputInterface instance
     *
     * @return Ciconia|\Ciconia\Diagnose\Ciconia
     */
    protected function createCiconia(InputInterface $input)
    {
        if ($input->getOption('diagnose')) {
            $ciconia = new \Ciconia\Diagnose\Ciconia();
        } else {
            $ciconia = new Ciconia();
        }

        if ($input->getOption('format') == 'xhtml') {
            $ciconia->setRenderer(new XhtmlRenderer());
        }

        if ($input->getOption('gfm')) {
            $ciconia->addExtensions([
                new FencedCodeBlockExtension(),
                new InlineStyleExtension(),
                new TaskListExtension(),
                new WhiteSpaceExtension(),
                new TableExtension(),
                new UrlAutoLinkExtension()
            ]);
        }

        return $ciconia;
    }

    /**
     * Runs help command
     *
     * @param InputInterface  $input  The InputInterface instance
     * @param OutputInterface $output The OutputInterface instance
     *
     * @return int
     */
    protected function runHelp(InputInterface $input, OutputInterface $output)
    {
        /* @var HelpCommand $help */
        $help = $this->getApplication()->find('help');
        $help->setCommand($this);
        $help->run($input, $output);
    }

    /**
     * Lints the content
     *
     * @param OutputInterface $output  The OutputInterface instance
     * @param Ciconia         $ciconia The Ciconia instance
     * @param string          $content The markdown content
     *
     * @return int
     */
    protected function lint(OutputInterface $output, Ciconia $ciconia, $content)
    {
        try {
            $ciconia->render($content, array('strict' => true));
            $output->writeln('No syntax errors detected.');

            return 0;
        } catch (SyntaxError $e) {
            $output->writeln('<error>' . $e->getMessage() . '</error>');

            return 1;
        }
    }

    /**
     * Diagnose
     *
     * @param OutputInterface           $output
     * @param \Ciconia\Diagnose\Ciconia $ciconia
     * @param string                    $content
     *
     * @return int
     */
    protected function diagnose(OutputInterface $output, \Ciconia\Diagnose\Ciconia $ciconia, $content)
    {
        /* @var TableHelper $table */
        $table = $this->getHelper('table');
        $table->setHeaders([
            'Event', 'Callback', 'Duration', 'MEM Usage'
        ]);

        $table->addRow(['', 'render()', '-', '-']);

        $events = $ciconia->render($content);

        foreach ($events as $event) {
            $table->addRow([
                $event->getEvent(),
                str_repeat('  ', $event->getDepth()) . $event->getCallback(),
                $event->getDuration(),
                $event->getMemory()
            ]);
        }

        $table->render($output);

        return 0;
    }

    /**
     * --help
     *
     * @return string
     */
    protected function getHelpContent()
    {
        return <<< EOT
Translates markdown into html and displays to STDOUT
  <info>%command.name% /path/to/file.md</info>

Following command saves result to file
  <info>%command.name% /path/to/file.md > /path/to/file.html</info>

Or using pipe (On Windows it does't work)
  <info>echo "Markdown is **awesome**" | %command.name%</info>
EOT;
    }

}


<?php

namespace Ciconia\Common;

/**
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class Collection implements \IteratorAggregate, \Countable
{

    /**
     * @var array
     */
    private $objects;

    /**
     * Constructor
     *
     * @param array $objects [optional]
     */
    public function __construct(array $objects = array())
    {
        $this->objects = $objects;
    }

    /**
     * Appends a new value as the last element.
     *
     * @param mixed $object The value to append
     *
     * @return Collection
     */
    public function add($object)
    {
        $this->objects[] = $object;

        return $this;
    }

    /**
     * Sets the value at the specified index
     *
     * @param string $name   The index
     * @param mixed  $object The value
     *
     * @return Collection
     */
    public function set($name, $object)
    {
        $this->objects[(string)$name] = $object;

        return $this;
    }

    /**
     * Returns the value at the specified index
     *
     * @param string $name The index
     *
     * @throws \OutOfBoundsException When the index is invalid
     *
     * @return mixed
     */
    public function get($name)
    {
        if (!$this->exists($name)) {
            throw new \OutOfBoundsException(sprintf('Undefined offset "%s"', $name));
        }

        return $this->objects[(string)$name];
    }

    /**
     * Returns whether the requested index exists
     *
     * @param string $name The index
     *
     * @return bool True if the index is valid
     */
    public function exists($name)
    {
        return isset($this->objects[(string)$name]);
    }

    /**
     * Returns whether the requested value exists
     *
     * @param mixed $object The value
     *
     * @return bool True if the value exists
     */
    public function contains($object)
    {
        return array_search($object, $this->objects, true) !== false;
    }

    /**
     * Remove
     *
     * @param string $name The index
     *
     * @return Collection
     */
    public function remove($name)
    {
        if ($this->exists($name)) {
            unset($this->objects[$name]);
        }

        return $this;
    }

    /**
     * Join array elements with a string
     *
     * @param string $glue [optional] Defaults to an empty string.
     *
     * @return string
     */
    public function join($glue = '')
    {
        return implode($glue, $this->objects);
    }

    /**
     * Extract a slice of the array
     *
     * @param integer $offset If offset is non-negative, the sequence will start at that offset in the array.
     *                        If offset is negative, the sequence will start that far from the end of the array.
     * @param integer $length If length is given and is positive, then the sequence will have up to that many elements in it.
     *                        If the array is shorter than the length, then only the available array elements will be present.
     *                        If length is given and is negative then the sequence will stop that many elements from the end of the array.
     *                        If it is omitted, then the sequence will have everything from offset up until the end of the array.
     *
     * @return Collection
     */
    public function slice($offset, $length = null)
    {
        return new Collection(array_slice($this->objects, $offset, $length));
    }

    /**
     * Execute the callback for each element
     *
     * @param callable $callable function ($value, $key) {}
     *
     * @return Collection
     */
    public function each(callable $callable)
    {
        foreach ($this->objects as $key => $value) {
            if (false === call_user_func_array($callable, array($value, $key))) {
                break;
            }
        }

        return $this;
    }

    /**
     * Applies the callback to the elements
     *
     * @param callable $callable
     *
     * @return Collection
     */
    public function apply(callable $callable)
    {
        $this->objects = array_map($callable, $this->objects);

        return $this;
    }

    /**
     * Filters elements using a callback function
     *
     * @param callable $callable
     *
     * @return Collection
     */
    public function filter(callable $callable = null)
    {
        return new static(array_filter($this->objects, $callable));
    }

    /**
     * Retrieve an external iterator
     *
     * @return \Traversable An instance of an object implementing Iterator or Traversable
     */
    public function getIterator()
    {
        return new \ArrayIterator($this->objects);
    }

    /**
     * Count elements of an object
     *
     * @return int The custom count as an integer. The return value is cast to an integer.
     */
    public function count()
    {
        return count($this->objects);
    }

}


<?php

namespace Ciconia\Diagnose;

use Ciconia\Ciconia as BaseCiconia;
use Ciconia\Common\Text;

/**
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class Ciconia extends BaseCiconia
{

    /**
     * @param string $text
     * @param array  $options
     *
     * @return Event[]|string
     */
    public function render($text, array $options = array())
    {
        $text = new Text($text);
        $markdown = new Markdown($this->getRenderer(), $text, $options);

        $this->registerExtensions($markdown);

        $markdown->start();
        $markdown->emit('initialize', array($text));
        $markdown->emit('block', array($text));
        $markdown->emit('finalize', array($text));

        return $markdown->stop();
    }

}


<?php

namespace Ciconia\Extension\Core;

use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;
use Ciconia\Markdown;

/**
 * Converts block and line code
 *
 * Original source code from Markdown.pl
 *
 * > Copyright (c) 2004 John Gruber
 * > <http://daringfireball.net/projects/markdown/>
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class CodeExtension implements ExtensionInterface, RendererAwareInterface
{

    use RendererAwareTrait;

    /**
     * @var Markdown
     */
    private $markdown;

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->markdown = $markdown;

        $markdown->on('block', array($this, 'processCodeBlock'), 40);
        $markdown->on('inline', array($this, 'processCodeSpan'), 10);
    }

    /**
     * @param Text  $text
     * @param array $options
     */
    public function processCodeBlock(Text $text, array $options = array())
    {
        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{
            (?:\n\n|\A)
            (                                               # $1 = the code block -- one or more lines, starting with a space/tab
              (?:
                (?:[ ]{' . $options['tabWidth'] . '} | \t)  # Lines must start with a tab or a tab-width of spaces
                .*\n+
              )+
            )
            (?:(?=^[ ]{0,' . $options['tabWidth'] . '}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
        }mx', function (Text $whole, Text $code) {
            $this->markdown->emit('outdent', array($code));
            $code->escapeHtml(ENT_NOQUOTES);
            $this->markdown->emit('detab', array($code));
            $code->replace('/\A\n+/', '');
            $code->replace('/\s+\z/', '');

            return "\n\n" . $this->getRenderer()->renderCodeBlock($code) . "\n\n";
        });
    }

    /**
     * @param Text $text
     */
    public function processCodeSpan(Text $text)
    {
        if (!$text->contains('`')) {
            return;
        }

        $chars = ['\\\\', '`', '\*', '_', '{', '}', '\[', '\]', '\(', '\)', '>', '#', '\+', '\-', '\.', '!'];
        $chars = implode('|', $chars);

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{
            (`+)        # $1 = Opening run of `
            (.+?)       # $2 = The code block
            (?<!`)
            \1          # Matching closer
            (?!`)
        }x', function (Text $w, Text $b, Text $code) use ($chars) {
            $code->trim()->escapeHtml(ENT_NOQUOTES);
            $code->replace(sprintf('/(?<!\\\\)(%s)/', $chars), '\\\\${1}');

            return $this->getRenderer()->renderCodeSpan($code);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'code';
    }

}


<?php

namespace Ciconia\Extension\Gfm;

use Ciconia\Common\Tag;
use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Markdown;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;

/**
 * Original source code from GitHub Flavored Markdown
 *
 * > Copyright 2013 GitHub Inc.
 * > https://help.github.com/articles/github-flavored-markdown
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class InlineStyleExtension implements ExtensionInterface, RendererAwareInterface
{

    use RendererAwareTrait;

    /**
     * @var array
     */
    private $hashes = array();

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $markdown->on('inline', array($this, 'processMultipleUnderScore'), 10);
        $markdown->on('inline', array($this, 'processStrikeThrough'), 70);
    }

    /**
     * Multiple underscores in words
     *
     * It is not reasonable to italicize just part of a word, especially when you're dealing with code and names often
     * appear with multiple underscores. Therefore, GFM ignores multiple underscores in words.
     *
     * @param Text $text
     */
    public function processMultipleUnderScore(Text $text)
    {
        $text->replace('{<pre>.*?</pre>}m', function (Text $w) {
            $md5 = md5($w);
            $this->hashes[$md5] = $w;

            return "{gfm-extraction-$md5}";
        });

        $text->replace('/^(?! {4}|\t)(\[?\w+_\w+_\w[\w_]*\]?)/', function (Text $w, Text $word) {
            $underscores = $word->split('//')->filter(function (Text $item) {
                return $item == '_';
            });

            if (count($underscores) >= 2) {
                $word->replaceString('_', '\\_');
            }

            return $word;
        });

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('/\{gfm-extraction-([0-9a-f]{32})\}/m', function (Text $w, Text $md5) {
            return "\n\n" . $this->hashes[(string)$md5];
        });
    }

    /**
     * Strike-through `~~word~~` to `<del>word</del>`
     *
     * @param Text $text
     */
    public function processStrikeThrough(Text $text)
    {
        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{ (~~) (?=\S) (.+?) (?<=\S) \1 }sx', function (Text $w, Text $a, Text $target) {
            return $this->getRenderer()->renderTag('del', $target, Tag::TYPE_INLINE);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'gfmInlineStyle';
    }

}


<?php

namespace Ciconia\Extension\Core;

use Ciconia\Common\Collection;
use Ciconia\Common\Text;
use Ciconia\Exception\SyntaxError;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;
use Ciconia\Markdown;

/**
 * Original source code from Markdown.pl
 *
 * > Copyright (c) 2004 John Gruber
 * > <http://daringfireball.net/projects/markdown/>
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class LinkExtension implements ExtensionInterface, RendererAwareInterface
{

    use RendererAwareTrait;

    /**
     * @var Markdown
     */
    private $markdown;

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->markdown = $markdown;

        $markdown->on('initialize', array($this, 'initialize'), 30);
        $markdown->on('inline', array($this, 'processReferencedLink'), 40);
        $markdown->on('inline', array($this, 'processInlineLink'), 40);
        $markdown->on('inline', array($this, 'processAutoLink'), 50);
    }

    /**
     * Strips link definitions from text, stores the URLs and titles in hash references.
     *
     * @param Text  $text
     * @param array $options
     */
    public function initialize(Text $text, array $options = array())
    {
        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{
            ^[ ]{0,' . $options['tabWidth'] . '}\[(.+)\]:   # id = $1
              [ \t]*
              \n?               # maybe *one* newline
              [ \t]*
            <?(\S+?)>?          # url = $2
              [ \t]*
              \n?               # maybe one newline
              [ \t]*
            (?:
                (?<=\s)         # lookbehind for whitespace
                ["\'(]
                (.+?)           # title = $3
                ["\')]
                [ \t]*
            )?  # title is optional
            (?:\n+|\Z)
        }xm', function (Text $whole, Text $id, Text $url, Text $title = null) {
            $id->lower();
            $this->markdown->emit('escape.special_chars', [$url->replace('/(?<!\\\\)_/', '\\\\_')]);
            $this->markdown->getUrlRegistry()->set($id, htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false));

            if ($title) {
                $this->markdown->getTitleRegistry()->set($id, preg_replace('/"/', '"', $title));
            }

            return '';
        });
    }

    /**
     * Handle reference-style links: [link text] [id]
     *
     * @param Text  $text
     * @param array $options
     */
    public function processReferencedLink(Text $text, array $options = array())
    {
        if (!$text->contains('[')) {
            return;
        }

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{
            #(                   # wrap whole match in $1
              \[
                (' . $this->getNestedBrackets() . ')    # link text = $2
              \]

              [ ]?              # one optional space
              (?:\n[ ]*)?       # one optional newline followed by spaces

              \[
                (.*?)       # id = $3
              \]
            #)
        }xs', function (Text $whole, Text $linkText, Text $id = null) use ($options) {
            if (is_null($id) || (string) $id == '') {
                $id = new Text($linkText);
            }

            $id->lower();

            if ($this->markdown->getUrlRegistry()->exists($id)) {
                $url = new Text($this->markdown->getUrlRegistry()->get($id));
                $url->escapeHtml()->replace('/(?<!\\\\)_/', '\\\\_');
                $this->markdown->emit('escape.special_chars', [$url]);

                $linkOptions = [
                    'href' => $url->getString(),
                ];

                if ($this->markdown->getTitleRegistry()->exists($id)) {
                    $title = new Text($this->markdown->getTitleRegistry()->get($id));
                    $linkOptions['title'] = $title->escapeHtml()->getString();
                }

                return $this->getRenderer()->renderLink($linkText->getString(), $linkOptions);
            } else {
                if ($options['strict']) {
                    throw new SyntaxError(
                        sprintf('Unable to find id "%s" in Reference-style link', $id),
                        $this, $whole, $this->markdown
                    );
                }

                return $whole;
            }
        });
    }

    /**
     * Inline-style links: [link text](url "optional title")
     *
     * @param Text $text
     */
    public function processInlineLink(Text $text)
    {
        if (!$text->contains('[')) {
            return;
        }

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{
            #(               # wrap whole match in $1
              \[
                (' . $this->getNestedBrackets() . ')    # link text = $2
              \]
              \(            # literal paren
                [ \t]*
                <?(.*?)>?   # href = $3
                [ \t]*
                (           # $4
                  ([\'"])   # quote char = $5
                  (.*?)     # Title = $6
                  \4        # matching quote
                )?          # title is optional
              \)
            #)
        }xs', function (Text $whole, Text $linkText, Text $url, Text $a = null, Text $q = null, Text $title = null) {
            $url->escapeHtml()->replace('/(?<!\\\\)_/', '\\\\_');
            $this->markdown->emit('escape.special_chars', [$url]);

            $linkOptions = [
                'href' => $url->getString(),
            ];

            if ($title) {
                $linkOptions['title'] = $title->replace('/"/', '"')->escapeHtml()->getString();
            }

            return $this->getRenderer()->renderLink($linkText, $linkOptions);
        });
    }

    /**
     * Make links out of things like `<http://example.com/>`
     *
     * @param Text $text
     */
    public function processAutoLink(Text $text)
    {
        if (!$text->contains('<')) {
            return;
        }

        $text->replace('{<((?:https?|ftp):[^\'">\s]+)>}', function (Text $w, Text $url) {
            $this->markdown->emit('escape.special_chars', [$url->replace('/(?<!\\\\)_/', '\\\\_')]);

            return $this->getRenderer()->renderLink($url, [
                'href' => $url->getString()
            ]);
        });

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('{
            <
            (?:mailto:)?
            (
                [-.\w]+
                \@
                [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
            )
            >
        }ix', function (Text $w, Text $address) {
            $address  = "mailto:" . $address;

            $encode = array(
                function ($char) { return '&#' . ord($char) . ';'; },
                function ($char) { return '&#x' . dechex(ord($char)) . ';'; },
                function ($char) { return $char; }
            );

            $chars = new Collection(str_split($address));

            $chars->apply(function ($char) use ($encode) {
                if ($char == '@') {
                    return $encode[rand(0, 1)]($char);
                } elseif ($char != ':') {
                    $rand = rand(0, 100);
                    $key = ($rand > 90) ? 2 : ($rand < 45 ? 0 : 1);

                    return $encode[$key]($char);
                }

                return $char;
            });

            $address = $chars->join();
            $text = $chars->slice(7)->join();

            return $this->getRenderer()->renderLink($text, ['href' => $address]);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'link';
    }

    /**
     * @return string
     */
    protected function getNestedBrackets()
    {
        return str_repeat('(?>[^\[\]]+|\[', 7) . str_repeat('\])*', 7);
    }

}


<?php

namespace Ciconia\Diagnose;

use Ciconia\Markdown as BaseMarkdown;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class Markdown extends BaseMarkdown
{

    private static $depth = 0;

    /**
     * @var Stopwatch
     */
    private $stopwatch;

    /**
     * @var array
     */
    private $events = [];

    /**
     *
     */
    public function start()
    {
        $this->stopwatch = new Stopwatch();
    }

    /**
     * @return Event[]
     */
    public function stop()
    {
        return $this->events;
    }

    /**
     * @param $event
     * @param $parameters
     *
     * @return mixed|void
     */
    public function emit($event, $parameters)
    {
        self::$depth++;

        $this->stopwatch->openSection();

        if (isset($this->callbacks[$event])) {
            if (!$this->callbacks[$event][0]) {
                usort($this->callbacks[$event][1], function ($A, $B) {
                    if ($A[0] == $B[0]) {
                        return 0;
                    }

                    return ($A[0] > $B[0]) ? 1 : -1;
                });

                $this->callbacks[$event][0] = true;
            }

            foreach ($this->callbacks[$event][1] as $item) {
                $name = $this->getCallableName($item[1]);
                $this->stopwatch->start($name);

                $diagnoseEvent = Event::create()
                    ->setEvent($event)
                    ->setCallback($name)
                    ->setDepth(self::$depth);

                $this->events[] = $diagnoseEvent;

                call_user_func_array($item[1], $this->buildParameters($parameters));
                $stopwatchEvent = $this->stopwatch->stop($name);

                $diagnoseEvent
                    ->setDuration($stopwatchEvent->getDuration())
                    ->setMemory($stopwatchEvent->getMemory());
            }
        }

        $this->stopwatch->stopSection($event);

        self::$depth--;
    }

    /**
     * @param $callable
     *
     * @return string
     */
    public function getCallableName($callable)
    {
        $name = '(Unknown)';

        if (is_array($callable)) {
            $method = new \ReflectionMethod($callable[0], $callable[1]);
            $className = $method->getDeclaringClass()->getName();
            $className = str_replace('Ciconia\\Extension\\', '', $className);

            $name = $className . ':' . $method->getShortName();
        }

        return $name;
    }

}


<?php

namespace Ciconia\Extension\Core;

use Ciconia\Common\Text;
use Ciconia\Exception\SyntaxError;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;
use Ciconia\Markdown;

/**
 * Turn Markdown image shortcuts into <img> tags.
 *
 * Original source code from Markdown.pl
 *
 * > Copyright (c) 2004 John Gruber
 * > <http://daringfireball.net/projects/markdown/>
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class ImageExtension implements ExtensionInterface, RendererAwareInterface
{

    use RendererAwareTrait;

    /**
     * @var Markdown
     */
    private $markdown;

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->markdown = $markdown;

        $markdown->on('inline', array($this, 'processReferencedImage'), 30);
        $markdown->on('inline', array($this, 'processInlineImage'), 31);
    }

    /**
     * Handle reference-style labeled images: ![alt text][id]
     *
     * @param Text  $text
     * @param array $options
     */
    public function processReferencedImage(Text $text, array $options = array())
    {
        if (!$text->contains('![')) {
            return;
        }

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace(
            '{
                #(               # wrap whole match in $1
                  !\[
                    (.*?)       # alt text = $2
                  \]

                  [ ]?          # one optional space
                  (?:\n[ ]*)?   # one optional newline followed by spaces

                  \[
                    (.*?)       # id = $3
                  \]
                #)
            }xs',
            function (Text $whole, Text $alt, Text $id = null) use ($options) {
                $result = null;

                if ($id->lower() == '') {
                    $id->setString($alt);
                }

                $this->markdown->emit('escape.special_chars', [$alt->replace('/(?<!\\\\)_/', '\\\\_')]);

                $attr = array(
                    'alt' => $alt->replace('/"/', '"')
                );

                if ($this->markdown->getUrlRegistry()->exists($id)) {
                    $url = new Text($this->markdown->getUrlRegistry()->get($id));
                    $url->escapeHtml();

                    if ($this->markdown->getTitleRegistry()->exists($id)) {
                        $title = new Text($this->markdown->getTitleRegistry()->get($id));
                        $attr['title'] = $title->escapeHtml();
                    }

                    return $this->getRenderer()->renderImage($url, array('attr' => $attr));
                } else {
                    if ($options['strict']) {
                        throw new SyntaxError(
                            sprintf('Unable to find id "%s" in Reference-style image', $id),
                            $this, $whole, $this->markdown
                        );
                    }

                    return $whole;
                }
            }
        );
    }

    /**
     * handle inline images:  ![alt text](url "optional title")
     *
     * @param Text  $text
     * @param array $options
     */
    public function processInlineImage(Text $text, array $options = array())
    {
        if (!$text->contains('![')) {
            return;
        }

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace(
            '{
                (               # wrap whole match in $1
                  !\[
                    (.*?)       # alt text = $2
                  \]
                  \(            # literal paren
                    [ \t]*
                    <?(\S+?)>?  # src url = $3
                    [ \t]*
                    (           # $4
                      ([\'"])   # quote char = $5
                      (.*?)     # title = $6
                      \5        # matching quote
                      [ \t]*
                    )?          # title is optional
                  \)
                )
            }xs',
            function (
                Text $w,
                Text $whole,
                Text $alt,
                Text $url,
                Text $a = null,
                Text $q = null,
                Text $title = null
            ) use ($options) {
                $this->markdown->emit('escape.special_chars', [$alt->replace('/(?<!\\\\)_/', '\\\\_')]);

                $attr = array(
                    'alt' => $alt->replace('/"/', '"')
                );

                $this->markdown->emit('escape.special_chars', [$url->replace('/(?<!\\\\)_/', '\\\\_')]);
                $url->escapeHtml();

                if ($title) {
                    $attr['title'] = $title->replace('/"/', '"')->escapeHtml();
                }

                return $this->getRenderer()->renderImage($url,  array('attr' => $attr));
            }
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'image';
    }

}


<?php

namespace Ciconia\Extension\Core;

use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;
use Ciconia\Markdown;

/**
 * Original source code from Markdown.pl
 *
 * > Copyright (c) 2004 John Gruber
 * > <http://daringfireball.net/projects/markdown/>
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class WhitespaceExtension implements ExtensionInterface, RendererAwareInterface
{

    use RendererAwareTrait;

    /**
     * @var Markdown
     */
    private $markdown;

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->markdown = $markdown;

        $markdown->on('initialize', array($this, 'initialize'), 10);
        $markdown->on('detab', array($this, 'detab'), 10);
        $markdown->on('outdent', array($this, 'outdent'), 10);
        $markdown->on('inline', array($this, 'processHardBreak'), 80);
    }

    /**
     * Convert line breaks
     *
     * @param Text $text
     */
    public function initialize(Text $text)
    {
        $text->replaceString("\r\n", "\n");
        $text->replaceString("\r", "\n");

        $text->append("\n\n");
        $this->markdown->emit('detab', array($text));
        $text->replace('/^[ \t]+$/m', '');
    }

    /**
     * Convert tabs to spaces
     *
     * @param Text  $text
     * @param array $options
     */
    public function detab(Text $text, array $options = array())
    {
        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('/(.*?)\t/', function (Text $whole, Text $string) use ($options) {
            return $string . str_repeat(' ', $options['tabWidth'] - $string->getLength() % $options['tabWidth']);
        });
    }

    /**
     * Remove one level of line-leading tabs or spaces
     *
     * @param Text  $text
     * @param array $options
     */
    public function outdent(Text $text, array $options = array())
    {
        $text->replace('/^(\t|[ ]{1,' . $options['tabWidth'] . '})/m', '');
    }

    /**
     * @param Text  $text
     */
    public function processHardBreak(Text $text)
    {
        $text->replace('/ {2,}\n/', $this->getRenderer()->renderLineBreak() .  "\n");
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'whitespace';
    }

}


<?php

namespace Ciconia;

use Ciconia\Common\Collection;
use Ciconia\Common\Text;
use Ciconia\Event\EmitterAwareInterface;
use Ciconia\Extension\Core;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\HtmlRenderer;
use Ciconia\Renderer\RendererInterface;

/**
 * Ciconia - The New Markdown Parser
 *
 * This is just the central point to manage `renderer` and `extensions`.
 *
 * The `Core` extensions are based on Markdown.pl
 * The `Gfm` extensions are based on Github Flavored Markdown
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class Ciconia
{

    const VERSION = '1.1.x-dev';

    /**
     * @var RendererInterface
     */
    private $renderer;

    /**
     * @var Collection|ExtensionInterface[]
     */
    private $extensions;

    /**
     * @param RendererInterface $renderer
     */
    public function __construct(RendererInterface $renderer = null)
    {
        $this->extensions = new Collection();
        $this->renderer = $renderer;

        if (is_null($this->renderer)) {
            $this->setRenderer($this->getDefaultRenderer());
        }

        $this->addExtensions($this->getDefaultExtensions());
    }

    /**
     * @param string $text
     * @param array  $options
     *
     * @return string
     */
    public function render($text, array $options = array())
    {
        $text = new Text($text);
        $markdown = new Markdown($this->renderer, $text, $options);

        $this->registerExtensions($markdown);

        $markdown->emit('initialize', array($text));
        $markdown->emit('block', array($text));
        $markdown->emit('finalize', array($text));

        return (string) $text;
    }

    /**
     * @param \Ciconia\Renderer\RendererInterface $renderer
     *
     * @return Ciconia
     */
    public function setRenderer($renderer)
    {
        $this->renderer = $renderer;

        return $this;
    }

    /**
     * @return \Ciconia\Renderer\RendererInterface
     */
    public function getRenderer()
    {
        return $this->renderer;
    }

    /**
     * @param ExtensionInterface $extension
     *
     * @return Ciconia
     */
    public function addExtension(ExtensionInterface $extension)
    {
        $this->extensions->set($extension->getName(), $extension);

        return $this;
    }

    /**
     * @param ExtensionInterface[] $extensions
     *
     * @return Ciconia
     */
    public function addExtensions(array $extensions)
    {
        foreach ($extensions as $extension) {
            $this->addExtension($extension);
        }

        return $this;
    }

    /**
     * @param string|object $extension
     *
     * @return Ciconia
     */
    public function removeExtension($extension)
    {
        if ($extension instanceof ExtensionInterface) {
           $extension = $extension->getName();
        }

        $this->extensions->remove($extension);

        return $this;
    }

    /**
     * @param string|object $extension
     *
     * @return boolean
     */
    public function hasExtension($extension)
    {
        if ($extension instanceof ExtensionInterface) {
            $extension = $extension->getName();
        }

        return $this->extensions->exists($extension);
    }

    /**
     * @return RendererInterface
     */
    protected function getDefaultRenderer()
    {
        return new HtmlRenderer();
    }

    /**
     * @return ExtensionInterface[]
     */
    protected function getDefaultExtensions()
    {
        return array(
            new Core\WhitespaceExtension(),
            new Core\HeaderExtension(),
            new Core\ParagraphExtension(),
            new Core\HtmlBlockExtension(),
            new Core\LinkExtension(),
            new Core\HorizontalRuleExtension(),
            new Core\ListExtension(),
            new Core\CodeExtension(),
            new Core\BlockQuoteExtension(),
            new Core\ImageExtension(),
            new Core\InlineStyleExtension(),
            new Core\EscaperExtension()
        );
    }

    /**
     * @param Markdown $markdown
     */
    protected function registerExtensions(Markdown $markdown)
    {
        foreach ($this->extensions as $extension) {
            if ($extension instanceof RendererAwareInterface) {
                $extension->setRenderer($this->renderer);
            }

            if ($extension instanceof EmitterAwareInterface) {
                $extension->setEmitter($markdown);
            }

            $extension->register($markdown);
        }
    }

}


<?php

namespace Ciconia\Common;

/**
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class Text implements \Serializable
{

    /**
     * @var string
     */
    private $text;

    /**
     * @param string $text
     */
    public function __construct($text = '')
    {
        $this->text = (string) $text;
    }

    /**
     * @param string $text
     *
     * @return Text
     */
    public function append($text)
    {
        $this->text .= (string) $text;

        return $this;
    }

    /**
     * @param string $text
     *
     * @return Text
     */
    public function prepend($text)
    {
        $this->text = $text . $this->text;

        return $this;
    }

    /**
     * Surround text with given string
     *
     * @param string $start
     * @param string $end
     *
     * @return Text
     */
    public function wrap($start, $end)
    {
        $this->text = $start . $this->text . $end;

        return $this;
    }

    /**
     * Make a string lowercase
     *
     * @return Text
     */
    public function lower()
    {
        $this->text = strtolower($this->text);

        return $this;
    }

    /**
     * Make a string uppercase
     *
     * @return Text
     */
    public function upper()
    {
        $this->text = strtoupper($this->text);

        return $this;
    }

    /**
     * Strip whitespace (or other characters) from the beginning and end of a string
     *
     * @param string $charList Optionally, the stripped characters can also be specified using the charlist parameter.
     *                          Simply list all characters that you want to be stripped. With .. you can specify a range of characters.
     *
     * @return Text
     */
    public function trim($charList = null)
    {
        if (is_null($charList)) {
            $this->text = trim($this->text);
        } else {
            $this->text = trim($this->text, $charList);
        }

        return $this;
    }

    /**
     * Strip whitespace (or other characters) from the end of a string
     *
     * @param string $charList You can also specify the characters you want to strip, by means of the charlist parameter.
     *                          Simply list all characters that you want to be stripped. With .. you can specify a range of characters.
     *
     * @return Text
     */
    public function rtrim($charList = null)
    {
        if (is_null($charList)) {
            $this->text = rtrim($this->text);
        } else {
            $this->text = rtrim($this->text, $charList);
        }

        return $this;
    }

    /**
     * @param string $charList You can also specify the characters you want to strip, by means of the charlist parameter.
     *                          Simply list all characters that you want to be stripped. With .. you can specify a range of characters.
     *
     * @since  1.1
     * @return Text
     */
    public function ltrim($charList = null)
    {
        if (is_null($charList)) {
            $this->text = ltrim($this->text);
        } else {
            $this->text = ltrim($this->text, $charList);
        }

        return $this;
    }

    /**
     * @return bool
     */
    public function isEmpty()
    {
        return $this->text === '';
    }

    /**
     * Checks whether the text contains $text
     *
     * @param string $text A string to test
     *
     * @return bool True if the text contains $text
     */
    public function contains($text)
    {
        return strpos($this->text, $text) !== false;
    }

    /**
     * Convert special characters to HTML entities
     *
     * @param int $option
     *
     * @return Text
     */
    public function escapeHtml($option = ENT_QUOTES)
    {
        $this->text = htmlspecialchars($this->text, $option, 'UTF-8', false);

        return $this;
    }

    /**
     * Perform a regular expression search and replace
     *
     * @param string          $pattern     The pattern to search for. It can be either a string or an array with strings.
     * @param string|callable $replacement The string or an array with strings to replace.
     *                                     If $replacement is the callable, a callback that will be called and passed an array of matched elements in the subject string.
     *
     * @return Text
     */
    public function replace($pattern, $replacement)
    {
        if (is_callable($replacement)) {
            $this->text = preg_replace_callback($pattern, function ($matches) use ($replacement) {
                $args = array_map(function ($item) {
                    return new Text($item);
                }, $matches);

                return call_user_func_array($replacement, $args);
            }, $this->text);
        } else {
            $this->text = preg_replace($pattern, $replacement, $this->text);
        }

        return $this;
    }

    /**
     * Replace all occurrences of the search string with the replacement string
     *
     * @param string|array $search  The value being searched for, otherwise known as the needle. An array may be used to designate multiple needles.
     * @param string|array $replace The replacement value that replaces found search values. An array may be used to designate multiple replacements.
     *
     * @return Text
     */
    public function replaceString($search, $replace)
    {
        $this->text = str_replace($search, $replace, $this->text);

        return $this;
    }

    /**
     * Perform a regular expression match
     *
     * @param string     $pattern The pattern to search for, as a string.
     * @param array|null $matches If matches is provided, then it is filled with the results of search.
     *
     * @return boolean
     */
    public function match($pattern, &$matches = null)
    {
        return preg_match($pattern, $this->text, $matches) > 0;
    }

    /**
     * Split string by a regular expression
     *
     * @param string $pattern The pattern to search for, as a string.
     * @param int    $flags   [optional] PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_OFFSET_CAPTURE
     *
     * @return Text[]|Collection
     */
    public function split($pattern, $flags = PREG_SPLIT_DELIM_CAPTURE)
    {
        return new Collection(
            array_map(function ($item) {
                return new static($item);
            }, preg_split($pattern, $this->text, -1, $flags))
        );
    }

    /**
     * Gets the length of a string
     *
     * @return int Returns the number of characters in string str having character encoding encoding.
     *             A multi-byte character is counted as 1.
     */
    public function getLength()
    {
        if (function_exists('mb_strlen')) {
            return mb_strlen($this->text, 'UTF-8');
        }

        // @codeCoverageIgnoreStart
        return preg_match_all("/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", $this->text);
        // @codeCoverageIgnoreEnd
    }

    /**
     * Set internal string value
     *
     * @param Text
     */
    public function setString($text)
    {
        $this->text = (string) $text;
    }

    /**
     * Returns internal string value
     *
     * @return string
     */
    public function getString()
    {
        return $this->text;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->getString();
    }

    /**
     * String representation of object
     *
     * @return string the string representation of the object or null
     */
    public function serialize()
    {
        return serialize($this->text);
    }

    /**
     * Constructs the object
     *
     * @param string $serialized The string representation of the object.
     */
    public function unserialize($serialized)
    {
        $this->text = unserialize($serialized);
    }

}


<?php

namespace Ciconia\Event;

/**
 * Central event manager (heavily inspired by Node.js's EventEmitter)
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
trait EmitterTrait
{

    /**
     * @var array
     */
    protected $callbacks = array();

    /**
     * Adds a listener to the end of the listeners array for the specified event.
     *
     * @param string   $event    The name of the event
     * @param callable $callback The callback
     * @param int      $priority The lower this value, the earlier an event listener will be emitted.
     *
     * @return EmitterInterface
     */
    public function on($event, callable $callback, $priority = 10)
    {
        if (!isset($this->callbacks[$event])) {
            $this->callbacks[$event] = [true, []];
        }

        $this->callbacks[$event][0] = false;
        $this->callbacks[$event][1][] = array($priority, $callback);

        return $this;
    }

    /**
     * Execute each of the subscribed listeners
     *
     * @param string $event      The name of the event to emit
     * @param array  $parameters The arguments
     */
    public function emit($event, $parameters)
    {
        if (!isset($this->callbacks[$event])) {
            return;
        }

        if (!$this->callbacks[$event][0]) {
            usort($this->callbacks[$event][1], function ($A, $B) {
                if ($A[0] == $B[0]) {
                    return 0;
                }

                return ($A[0] > $B[0]) ? 1 : -1;
            });

            $this->callbacks[$event][0] = true;
        }

        foreach ($this->callbacks[$event][1] as $item) {
            call_user_func_array($item[1], $this->buildParameters($parameters));
        }
    }

    /**
     * Build parameters to pass to the listeners
     *
     * @codeCoverageIgnore
     *
     * @param array $parameters The arguments to pass to the callback
     *
     * @return array
     */
    protected function buildParameters(array $parameters = array()) {
        return $parameters;
    }

}


<?php

namespace Ciconia\Extension\Gfm;

use Ciconia\Common\Collection;
use Ciconia\Common\Tag;
use Ciconia\Common\Text;
use Ciconia\Exception\SyntaxError;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Markdown;
use Ciconia\Renderer\HtmlRenderer;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;

/**
 * Gfm tables
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class TableExtension implements ExtensionInterface, RendererAwareInterface
{

    use RendererAwareTrait;

    /**
     * @var Markdown
     */
    private $markdown;

    /**
     * @var string
     */
    private $hash;

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->markdown = $markdown;
        $this->hash = '{gfm:table:escape(' . md5('|') . ')}';

        if ($this->getRenderer() instanceof HtmlRenderer) {
            // Output format depends on HTML for now
            $markdown->on('block', array($this, 'processTable'));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'table'; // Not gfmTable
    }

    /**
     * Gfm tables
     *
     * @param Text  $text
     * @param array $options
     */
    public function processTable(Text $text, array $options = array())
    {
        $lessThanTab = $options['tabWidth'] - 1;

        $text->replace('/
            (?:\n\n|\A)
            (?:[ ]{0,' . $lessThanTab . '}      #  table header
                (?:\|?)                         #  optional outer pipe
                ([^\n]*?\|[^\n]*?)              #1 table header
                (?:\|?)                         #  optional outer pipe
            )\n
            (?:[ ]{0,' . $lessThanTab . '}      #  second line
                (?:\|?)                         #  optional outer pipe
                ([-:\| ]+?\|[-:\| ]+?)          #2 dashes and pipes
                (?:\|?)                         #  optional outer pipe
            )\n
            (.*?)\n{2,}                         #3 table body
        /smx', function (Text $w, Text $header, Text $rule, Text $body) use ($options) {
            // Escape pipe to hash, so you can include pipe in cells by escaping it like this: `\\|`
            $this->escapePipes($header);
            $this->escapePipes($rule);
            $this->escapePipes($body);

            try {
                $baseTags    = $this->createBaseTags($rule->split('/\|/'));
                $headerCells = $this->parseHeader($header, $baseTags);
                $bodyRows    = $this->parseBody($body, $baseTags);
            } catch (SyntaxError $e) {
                if ($options['strict']) {
                    throw $e;
                }

                return $w;
            }

            $html = $this->createView($headerCells, $bodyRows);
            $this->unescapePipes($html);

            return "\n\n" . $html . "\n\n";
        });
    }

    /**
     * @param Collection $headerCells
     * @param Collection $bodyRows
     *
     * @return Text
     */
    protected function createView(Collection $headerCells, Collection $bodyRows)
    {
        $tHeadRow = new Tag('tr');
        $tHeadRow->setText("\n" . $headerCells->join("\n") . "\n");

        $tHead = new Tag('thead');
        $tHead->setText("\n" . $tHeadRow . "\n");

        $tBody = new Tag('tbody');

        $bodyRows->apply(function (Collection $row) use (&$options) {
            $tr = new Tag('tr');
            $tr->setText("\n" . $row->join("\n") . "\n");

            return $tr;
        });

        $tBody->setText("\n" .$bodyRows->join("\n") . "\n");

        $table = new Tag('table');
        $table->setText("\n" . $tHead . "\n" . $tBody . "\n");

        return new Text($table->render());
    }

    /**
     * @param Collection $rules
     *
     * @return Collection|\Ciconia\Common\Tag[]
     */
    protected function createBaseTags(Collection $rules)
    {
        /* @var Collection|Tag[] $baseTags */
        $baseTags = new Collection();

        $rules->each(function (Text $cell) use (&$baseTags) {
            $cell->trim();
            $tag = new Tag('td');

            if ($cell->match('/^-.*:$/')) {
                $tag->setAttribute('align', 'right');
            } elseif ($cell->match('/^:.*:$/')) {
                $tag->setAttribute('align', 'center');
            }

            $baseTags->add($tag);
        });

        return $baseTags;
    }

    /**
     * @param Text       $header
     * @param Collection $baseTags
     *
     * @throws \Ciconia\Exception\SyntaxError
     *
     * @return Collection
     */
    protected function parseHeader(Text $header, Collection $baseTags)
    {
        $cells = new Collection();

        try {
            $header->split('/\|/')->each(function (Text $cell, $index) use ($baseTags, &$cells) {
                /* @var Tag $tag */
                $tag = clone $baseTags->get($index);
                $tag->setName('th');
                $this->markdown->emit('inline', array($cell));
                $tag->setText($cell->trim());

                $cells->add($tag);
            });
        } catch (\OutOfBoundsException $e) {
            throw new SyntaxError(
                'Too much cells on table header.',
                $this, $header, $this->markdown, $e
            );
        }

        if ($baseTags->count() != $cells->count()) {
            throw new SyntaxError(
                'Unexpected number of table cells in header.',
                $this, $header, $this->markdown
            );
        }

        return $cells;
    }

    /**
     * @param Text       $body
     * @param Collection $baseTags
     *
     * @return Collection
     */
    protected function parseBody(Text $body, Collection $baseTags)
    {
        $rows = new Collection();

        $body->split('/\n/')->each(function (Text $row, $index) use ($baseTags, &$rows) {
            $row->trim()->trim('|');

            $cells = new Collection();

            try {
                $row->split('/\|/')->each(function (Text $cell, $index) use (&$baseTags, &$cells) {
                    /* @var Tag $tag */
                    $tag = clone $baseTags->get($index);
                    $this->markdown->emit('inline', array($cell));
                    $tag->setText($cell->trim());

                    $cells->add($tag);
                });
            } catch (\OutOfBoundsException $e) {
                throw new SyntaxError(
                    sprintf('Too much cells on table body (row #%d).', $index),
                    $this, $row, $this->markdown, $e
                );
            }

            if ($baseTags->count() != $cells->count()) {
                throw new SyntaxError(
                    'Unexpected number of table cells in body.',
                    $this, $row, $this->markdown
                );
            }

            $rows->add($cells);
        });

        return $rows;
    }

    /**
     * @param Text $text
     */
    protected function escapePipes(Text $text)
    {
        $text->replaceString('\\|', $this->hash);
    }

    /**
     * @param Text $text
     */
    protected function unescapePipes(Text $text)
    {
        $text->replaceString($this->hash, '|');
    }

}


<?php

namespace Ciconia\Extension\Gfm;

use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Markdown;

/**
 * Turn standard URL into markdown URL (http://example.com -> <http://example.com>)
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class UrlAutoLinkExtension implements ExtensionInterface
{

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $markdown->on('inline', array($this, 'processStandardUrl'), 35);
    }

    /**
     * Turn standard URL into markdown URL
     *
     * @param Text $text
     */
    public function processStandardUrl(Text $text)
    {
        $hashes = array();

        // escape
        $text->replace('{.*?}m', function (Text $w) use (&$hashes) {
            $md5 = md5($w);
            $hashes[$md5] = $w;

            return "{gfm-extraction-$md5}";
        });

        $text->replace('{(?<!]\(|"|<|\[)((?:https?|ftp)://[^\'">\s]+)(?!>|\"|\])}', '<\1>');

        /** @noinspection PhpUnusedParameterInspection */
        $text->replace('/\{gfm-extraction-([0-9a-f]{32})\}/m', function (Text $w, Text $md5) use (&$hashes) {
            return $hashes[(string)$md5];
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'urlAutoLink';
    }

}


<?php

namespace Ciconia\Extension\Html;

use Ciconia\Common\Tag;
use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Markdown;

/**
 * [Experimental] Emmet-style HTML attributes
 *
 * @since 1.1
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class AttributesExtension implements ExtensionInterface
{

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $markdown->on('tag', array($this, 'processBlockTags'));
    }

    /**
     * Parse emmet style attributes
     *
     * @param Tag $tag
     */
    public function processBlockTags(Tag $tag)
    {
        if ($tag->isInline()) {
            return;
        }

        $text = null;
        $tag->getText()->replace('/(^{([^:\(\)]+)}[ \t]*\n?|(?:[ \t]*|\n?){([^:\(\)]+)}\n*$)/', function (Text $w) use (&$text) {
            $text = $w->trim()->trim('{}');

            return '';
        });

        if ($text) {
            $tag->setAttributes($this->parseAttributes($text));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'htmlAttributes';
    }

    /**
     * @param Text $text
     *
     * @return array
     */
    protected function parseAttributes(Text $text)
    {
        $patterns = [
            'id'    => '/^#([a-zA-Z0-9_-]+)/',
            'class' => '/^\.([a-zA-Z0-9_-]+)/',
            'attr'  => '/^\[([^\]]+)\]/',
            'ident' => '/^(.)/'
        ];

        $tokens = [
            'id' => [], 'class' => [], 'attr' => [], 'ident' => []
        ];

        while (!$text->isEmpty()) {
            foreach ($patterns as $name => $pattern) {
                if ($text->match($pattern, $matches)) {
                    $tokens[$name][] = $matches[1];
                    $text->setString(
                        substr($text->getString(), strlen($matches[0]))
                    );

                    break;
                }
            }
        }

        $attributes = array();

        if (count($tokens['id'])) {
            $attributes['id'] = array_pop($tokens['id']);
        }

        if (count($tokens['class'])) {
            $attributes['class'] = implode(' ', $tokens['class']);
        }

        if (count($tokens['attr'])) {
            foreach ($tokens['attr'] as $raw) {
                $items = explode(' ', $raw);
                foreach ($items as $item) {
                    if (strpos($item, '=') !== false) {
                        list ($key, $value) = explode('=', $item);
                        $attributes[$key] = trim($value, '"');
                    } else {
                        $attributes[$item] = $item;
                    }
                }
            }
        }

        return $attributes;
    }

}


<?php

namespace Ciconia\Extension\Gfm;

use Ciconia\Common\Tag;
use Ciconia\Common\Text;
use Ciconia\Extension\Core\ListExtension;

/**
 * Further, lists can be turned into Task Lists by prefacing list items with [ ] or [x] (incomplete or complete, respectively).
 *
 * This extension replaces Core\ListExtension
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class TaskListExtension extends ListExtension
{

    /**
     * {@inheritdoc}
     */
    public function processListItems(Text $list, array $options = array(), $level = 0)
    {
        $list->replace('/\n{2,}\z/', "\n");

        /** @noinspection PhpUnusedParameterInspection */
        $list->replace('{
            (\n)?                                 # leading line = $1
            (^[ \t]*)                             # leading whitespace = $2
            (' . $this->getPattern() . ') [ \t]+  # list marker = $3
            (([ ]*(\[([ ]|x)\]) [ \t]+)?(?s:.+?)  # list item text = $4, checkbox = $5, checked = %6
            (\n{1,2}))
            (?= \n* (\z | \2 (' . $this->getPattern() . ') [ \t]+))
        }mx', function (Text $w, Text $leadingLine, Text $ls, Text $m, Text $item, Text $checkbox, Text $check) use ($options, $level) {
            if (!$checkbox->isEmpty()) {
                $item->replace('/^\[( |x)\]/', function (Text $w, Text $check) {
                    $attr = array('type' => 'checkbox');

                    if ($check == 'x') {
                        $attr['checked'] = 'checked';
                    }

                    return $this->getRenderer()->renderTag('input', new Text(), Tag::TYPE_INLINE, array('attr' => $attr));
                });
            }

            if ((string)$leadingLine || $item->match('/\n{2,}/')) {
                $this->getEmitter()->emit('outdent', array($item));
                $this->getEmitter()->emit('block', array($item));
            } else {
                $this->getEmitter()->emit('outdent', array($item));
                $this->processList($item, $options, ++$level);
                $item->rtrim();
                $this->getEmitter()->emit('inline', array($item));
            }

            return $this->getRenderer()->renderListItem($item) . "\n";
        });
    }

}


<?php

namespace Ciconia;

use Ciconia\Common\Collection;
use Ciconia\Common\Text;

/**
 * Manages hashes and raw text
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class HashRegistry extends Collection
{

    /**
     * Register a string to be hashed
     *
     * @param Text $text The string to be hashed
     *
     * @return string The hashed string
     */
    public function register(Text $text)
    {
        $hash = $this->generateHash($text);
        $this->set($hash, $text);

        return new Text($hash);
    }

    /**
     * Generates a hash
     *
     * @param Text $text The string to be hashed
     *
     * @return string The hashed string
     */
    protected function generateHash(Text $text)
    {
        return '{boundary:md5(' . md5($text) . ')}';
    }

}


<?php

namespace Ciconia\Extension\Core;

use Ciconia\Common\Text;
use Ciconia\Event\EmitterAwareInterface;
use Ciconia\Event\EmitterAwareTrait;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Renderer\RendererAwareInterface;
use Ciconia\Renderer\RendererAwareTrait;
use Ciconia\Markdown;

/**
 * Form HTML ordered (numbered) and unordered (bulleted) lists.
 *
 * Original source code from Markdown.pl
 *
 * > Copyright (c) 2004 John Gruber
 * > <http://daringfireball.net/projects/markdown/>
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class ListExtension implements ExtensionInterface, RendererAwareInterface, EmitterAwareInterface
{

    use RendererAwareTrait;
    use EmitterAwareTrait;

    /**
     * @var Markdown
     */
    private $markdown;

    /**
     * @var string
     */
    private $ul = '[*+-]';

    /**
     * @var string
     */
    private $ol = '\d+[.]';

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->markdown = $markdown;

        $markdown->on('block', array($this, 'processList'), 30);
    }

    /**
     * Form HTML ordered (numbered) and unordered (bulleted) lists.
     *
     * @param Text  $text
     * @param array $options
     * @param int   $level
     */
    public function processList(Text $text, array $options = array(), $level = 0)
    {
        $lessThanTab = $options['tabWidth'] - 1;

        $wholeList = '
            #(                                 # $1 = whole list
              (                               # $2
                [ ]{0,' . $lessThanTab . '}
                (' . $this->getPattern() . ') # $3 = first list item marker
                [ \t]+
              )
              (?s:.+?)
              (?:                             # $4
                  \z
                |
                  \n{2,}
                  (?=\S)
                  (?!                         # Negative lookahead for another list item marker
                    [ \t]*
                    ' . $this->getPattern() . '[ \t]+
                  )
              )
            #)
        ';

        /** @noinspection PhpUnusedParameterInspection */
        $callback = function (Text $list, Text $marker) use ($options, $level) {
            $type = preg_match('{' . $this->ul . '}', $marker) ? 'ul' : 'ol';
            $list->replace('/\n{2,}/', "\n\n\n");
            $this->processListItems($list, $options, $level);

            return $this->getRenderer()->renderList($list, array('type' => $type)) . "\n\n";
        };

        if ($level) {
            $text->replace("{^$wholeList}mx", $callback);
        } else {
            $text->replace('{(?:(?<=\n\n)|\A\n?)' . $wholeList . '}mx', $callback);
        }
    }

    /**
     * Process the contents of a single ordered or unordered list, splitting it into individual list items.
     *
     * @param Text  $list
     * @param array $options
     * @param int   $level
     */
    public function processListItems(Text $list, array $options = array(), $level = 0)
    {
        $list->replace('/\n{2,}\z/', "\n");

        /** @noinspection PhpUnusedParameterInspection */
        $list->replace('{
            (\n)?                                 # leading line = $1
            (^[ \t]*)                             # leading whitespace = $2
            (' . $this->getPattern() . ') [ \t]+  # list marker = $3
            ((?s:.+?)                             # list item text   = $4
            (\n{1,2}))
            (?= \n* (\z | \2 (' . $this->getPattern() . ') [ \t]+))
        }mx', function (Text $w, Text $leadingLine, Text $ls, Text $m, Text $item) use ($options, $level) {
            if ((string)$leadingLine || $item->match('/\n{2,}/')) {
                $this->markdown->emit('outdent', array($item));
                $this->markdown->emit('block', array($item));
            } else {
                $this->markdown->emit('outdent', array($item));
                $this->processList($item, $options, ++$level);
                $item->rtrim();
                $this->markdown->emit('inline', array($item));
            }

            return $this->getRenderer()->renderListItem($item) . "\n";
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'list';
    }

    /**
     * @return string
     */
    protected function getPattern()
    {
        return '(?:' . $this->ul . '|' . $this->ol . ')';
    }

}


<?php

namespace Ciconia\Renderer;

use Ciconia\Common\Tag;
use Ciconia\Common\Text;
use Ciconia\Event\EmitterAwareInterface;
use Ciconia\Event\EmitterAwareTrait;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Renders markdown result to HTML format
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class HtmlRenderer implements RendererInterface, EmitterAwareInterface
{

    use EmitterAwareTrait;

    /**
     * {@inheritdoc}
     */
    public function renderParagraph($content, array $options = array())
    {
        $options = $this->createResolver()->resolve($options);

        $tag = new Tag('p');
        $tag->setText($content);
        $tag->setAttributes($options['attr']);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderHeader($content, array $options = array())
    {
        $options = $this->createResolver()
            ->setRequired(['level'])
            ->setAllowedValues(['level' => [1, 2, 3, 4, 5, 6]])
            ->resolve($options);

        $tag = new Tag('h' . $options['level']);
        $tag->setAttributes($options['attr']);
        $tag->setText($content);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderCodeBlock($content, array $options = array())
    {
        if (!$content instanceof Text) {
            $content = new Text($content);
        }

        $options = $this->createResolver()->resolve($options);

        $tag = Tag::create('pre')
            ->setAttributes($options['attr'])
            ->setText(
                Tag::create('code')
                    ->setText($content->append("\n"))
                    ->render()
            );

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderCodeSpan($content, array $options = array())
    {
        $tag = new Tag('code');
        $tag->setType(Tag::TYPE_INLINE);
        $tag->setText($content);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * @param string|Text $content
     * @param array       $options
     *
     * @return string
     */
    public function renderLink($content, array $options = array())
    {
        $options = $this->createResolver()
            ->setRequired(array('href'))
            ->setDefaults(array('href' => '#', 'title' => ''))
            ->setAllowedTypes(array('href' => 'string', 'title' => 'string'))
            ->resolve($options);

        $tag = new Tag('a');
        $tag->setText($content);
        $tag->setAttribute('href', $options['href']);

        if ($options['title']) {
            $tag->setAttribute('title', $options['title']);
        }

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderBlockQuote($content, array $options = array())
    {
        $tag = Tag::create('blockquote')
            ->setText($content->wrap("\n", "\n"));

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderList($content, array $options = array())
    {
        if (!$content instanceof Text) {
            $content = new Text($content);
        }

        $options = $this->createResolver()
            ->setRequired(array('type'))
            ->setAllowedValues(array('type' => array('ul', 'ol')))
            ->setDefaults(array('type' => 'ul'))
            ->resolve($options);

        $tag = new Tag($options['type']);
        $tag->setText($content->prepend("\n"));
        $tag->setAttributes($options['attr']);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderListItem($content, array $options = array())
    {
        $tag = Tag::create('li')
            ->setText($content);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderHorizontalRule(array $options = array())
    {
        $tag = Tag::create('hr')
            ->setType(Tag::TYPE_INLINE)
            ->setEmptyTagSuffix($this->getEmptyTagSuffix());

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderImage($src, array $options = array())
    {
        $options = $this->createResolver()->resolve($options);

        $tag = new Tag('img');
        $tag
            ->setEmptyTagSuffix($this->getEmptyTagSuffix())
            ->setType(Tag::TYPE_INLINE)
            ->setAttribute('src', $src)
            ->setAttributes($options['attr']);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderBoldText($text, array $options = array())
    {
        $tag = Tag::create('strong')
            ->setText($text);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderItalicText($text, array $options = array())
    {
        $tag = Tag::create('em')
            ->setText($text);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderLineBreak(array $options = array())
    {
        $tag = Tag::create('br')
            ->setType(Tag::TYPE_INLINE)
            ->setEmptyTagSuffix($this->getEmptyTagSuffix());

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * {@inheritdoc}
     */
    public function renderTag($tagName, $content, $tagType = Tag::TYPE_BLOCK, array $options = array())
    {
        $options = $this->createResolver()->resolve($options);

        $tag = new Tag($tagName);
        $tag->setType($tagType);
        $tag->setText($content);
        $tag->setAttributes($options['attr']);

        $this->getEmitter()->emit('tag', [$tag]);

        return $tag->render();
    }

    /**
     * @return OptionsResolver
     */
    protected function createResolver()
    {
        $resolver = new OptionsResolver();
        $resolver->setDefaults(array('attr' => array()));
        $resolver->setAllowedTypes(array('attr' => 'array'));

        return $resolver;
    }

    /**
     * @return string
     */
    protected function getEmptyTagSuffix()
    {
        return '>';
    }

}


<?php

namespace Ciconia\Common;

/**
 * HTML/XHTML/XML tag definition
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class Tag
{

    /**
     * Block level tag
     */
    const TYPE_BLOCK = 'block';

    /**
     * Inline level tag
     */
    const TYPE_INLINE = 'inline';

    /**
     * @var string
     */
    private $name;

    /**
     * @var Text
     */
    private $text;

    /**
     * @var Collection
     */
    private $attributes;

    /**
     * @var string
     */
    private $type = self::TYPE_BLOCK;

    /**
     * @var string
     */
    private $emptyTagSuffix = '>';

    /**
     * Creates a new Tag object
     *
     * @param string $name The name of the tag
     *
     * @return Tag
     */
    public static function create($name)
    {
        return new static($name);
    }

    /**
     * Constructor
     *
     * @param string $name The name of the tag
     */
    public function __construct($name)
    {
        $this->name       = $name;
        $this->attributes = new Collection();
        $this->text       = new Text();
    }

    /**
     * Sets the inner text
     *
     * @param Text|string $text A string to set
     *
     * @return Tag
     */
    public function setText($text)
    {
        $this->text = $text;

        if (!$this->text instanceof Text) {
            $this->text = new Text($this->text);
        }

        return $this;
    }

    /**
     * Gets the inner text
     *
     * @return Text
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Sets the name of the tag
     *
     * @param string $name The name of the tag
     *
     * @return $this
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Returns the name of the tag
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Sets the empty tag suffix
     *
     * @param string $emptyTagSuffix The suffix
     *
     * @return Tag
     */
    public function setEmptyTagSuffix($emptyTagSuffix)
    {
        $this->emptyTagSuffix = $emptyTagSuffix;

        return $this;
    }

    /**
     * Returns the empty tag suffix
     *
     * @return string The suffix
     */
    public function getEmptyTagSuffix()
    {
        return $this->emptyTagSuffix;
    }

    /**
     * Sets the type of the tag (Tag::TYPE_BLOCK or Tag::TYPE_INLINE)
     *
     * @param string $type The type of the tag
     *
     * @return Tag
     */
    public function setType($type)
    {
        $this->type = $type;

        return $this;
    }

    /**
     * Returns the type of the tag
     *
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return bool
     */
    public function isBlock()
    {
        return $this->type === self::TYPE_BLOCK;
    }

    /**
     * @return bool
     */
    public function isInline()
    {
        return $this->type === self::TYPE_INLINE;
    }

    /**
     * Sets an attribute
     *
     * @param string $attribute The name of an attribute
     * @param string $value     [optional] The value of an attribute
     *
     * @return Tag
     */
    public function setAttribute($attribute, $value = null)
    {
        $this->attributes->set($attribute, $value);

        return $this;
    }

    /**
     * Sets attributes
     *
     * @param array $attributes An array of attributes
     *
     * @return Tag
     */
    public function setAttributes(array $attributes)
    {
        foreach ($attributes as $attr => $value) {
            $this->setAttribute($attr, $value);
        }

        return $this;
    }

    /**
     * Returns a collection of attributes
     *
     * @return Collection
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Renders the tag
     *
     * @return string The HTML string
     */
    public function render()
    {
        $html = new Text();
        $html
            ->append('<')
            ->append($this->getName());

        foreach ($this->attributes as $name => $value) {
            $html->append(' ')
                ->append($name)
                ->append('=')
                ->append('"')
                ->append($value)
                ->append('"');
        }

        if ($this->text->isEmpty()) {
            if ($this->type == self::TYPE_BLOCK) {
                return (string)$html->append('>')->append('</')->append($this->getName())->append('>');
            } else {
                return (string)$html->append($this->emptyTagSuffix);
            }
        }

        return (string)$html
            ->append('>')
            ->append($this->text)
            ->append('</')
            ->append($this->getName())
            ->append('>');
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }

}


<?php

namespace Ciconia\Extension\Core;

use Ciconia\Common\Text;
use Ciconia\Extension\ExtensionInterface;
use Ciconia\Markdown;

/**
 * Escape special characters
 *
 * Original source code from Markdown.pl
 *
 * > Copyright (c) 2004 John Gruber
 * > <http://daringfireball.net/projects/markdown/>
 *
 * @author Kazuyuki Hayashi <hayashi@valnur.net>
 */
class EscaperExtension implements ExtensionInterface
{

    /**
     * @var array
     */
    private $hashes = array();

    /**
     * {@inheritdoc}
     */
    public function register(Markdown $markdown)
    {
        $this->hashes = $this->generateSpecialCharsHash();

        $markdown->on('escape.special_chars', array($this, 'escapeSpecialChars'));
        $markdown->on('inline', array($this, 'escapeSpecialChars'), 20);
        $markdown->on('inline', array($this, 'escapeAmpsAndBrackets'), 60);
        $markdown->on('finalize', array($this, 'unescapeSpecialChars'), 20);
    }

    /**
     * @param Text $text
     */
    public function escapeSpecialChars(Text $text)
    {
        foreach ($this->hashes as $char => $hash) {
            $text->replaceString('\\'.$char, $hash);
        }
    }

    /**
     * @param Text $text
     */
    public function escapeAmpsAndBrackets(Text $text)
    {
        if ($text->contains('&')) {
            $text->replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', '&');
        }

        if ($text->contains('<')) {
            $text->replace('{<(?![a-z/?\$!])}i', '<');
        }
    }

    /**
     * @param Text $text
     */
    public function unescapeSpecialChars(Text $text)
    {
        foreach ($this->hashes as $char => $hash) {
            $text->replaceString($hash, $char);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'escaper';
    }

    /**
     * @return array
     */
    protected function generateSpecialCharsHash()
    {
        $hashes = array();
        $chars  = array('\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!');

        foreach ($chars as $char) {
            $hashes[$char] = sprintf('{escape:md5(%s)}', md5($char));
        }

        return $hashes;
    }

}