Repository: aptoma/twig-markdown Branch: master Commit: 3729cc890049 Files: 19 Total size: 27.5 KB Directory structure: gitextract_3um_xo79/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src/ │ └── Aptoma/ │ └── Twig/ │ ├── Extension/ │ │ ├── MarkdownEngine/ │ │ │ ├── GitHubMarkdownEngine.php │ │ │ ├── MichelfMarkdownEngine.php │ │ │ ├── PHPLeagueCommonMarkEngine.php │ │ │ └── ParsedownEngine.php │ │ ├── MarkdownEngineInterface.php │ │ └── MarkdownExtension.php │ ├── Node/ │ │ └── MarkdownNode.php │ └── TokenParser/ │ └── MarkdownTokenParser.php └── tests/ └── Aptoma/ └── Twig/ ├── Extension/ │ ├── MarkdownEngine/ │ │ ├── GithubMarkdownEngineTest.php │ │ ├── PHPLeagueCommonMarkEngineTest.php │ │ └── ParsedownEngineTest.php │ └── MarkdownExtensionTest.php └── TokenParser/ └── MarkdownTokenParserTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: [push, pull_request] jobs: run: runs-on: 'ubuntu-latest' strategy: matrix: php-versions: ['7.2', '7.3', '7.4', '8.0'] steps: - name: Checkout uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} coverage: xdebug - run: mkdir -p build/logs - name: Install composer dependencies uses: ramsey/composer-install@v1 - name: Run Tests run: vendor/bin/phpunit ================================================ FILE: .gitignore ================================================ vendor composer.phar composer.lock phpunit.xml .phpunit.result.cache ================================================ FILE: CHANGELOG.md ================================================ CHANGELOG ========= 3.4.1 ----- - **Added**: Define implements \Twig\Extension\ExtensionInterface 3.4.0 ----- - **Added**: PHP 8.0 compatibility - **Changed**: Migrate test suite from Travis to GitHub 3.3.1 ----- - **FIX**: Fix invalid cacheDir for GH engine on Windows 3.3.0 ----- - **Added**: Support for Twig 3 3.2.0 - **Added**: Support for HMTL escaping in ParsdownEngine 2.1.0 ----- - **FIX**: Upgrade to Twig 2.7 and namespace, due to [a security issue](https://symfony.com/blog/twig-sandbox-information-disclosure) 2.0.0 ----- - **BC**: Require Twig v1.12, in order to replace deprecated Twig_Filter_Method with Twig_SimpleFilter - **Added**: Add support for ParsedownEngine 1.2.0 ----- - **Added**: Add support for GitHub's markdown engine 1.1.0 ----- - **Added**: Add support for PHP League CommonMark engine 1.0.0 ----- - **BC**: Remove deprecated dflydev-markdown parser ================================================ FILE: README.md ================================================ Twig Markdown Extension ======================= [![Build Status](https://github.com/aptoma/twig-markdown/workflows/Test/badge.svg?branch=master)](https://github.com/aptoma/twig-markdown/actions?query=branch%3Amaster) **THIS EXTENSION IS NO LONGER MAINTAINED, AND WE WILL NOT MAKE ANY NEW RELEASES. IF ANYONE WANTS TO TAKE OVER, PLEASE MAKE A FORK AND PUBLISH A NEW PACKAGE, AND WE'LL LINK TO THE NEW VERSION.** Twig Markdown extension provides a new filter and a tag to allow parsing of content as Markdown in [Twig][1] templates. This extension could be integrated with several Markdown parser as it provides an interface, which allows you to customize your Markdown parser. ### Supported parsers * [michelf/php-markdown](https://github.com/michelf/php-markdown) (+ MarkdownExtra) * [league/commonmark](http://commonmark.thephpleague.com/) * [KnpLabs/php-github-api](https://github.com/KnpLabs/php-github-api) * [erusev/parsedown](https://github.com/erusev/parsedown) ## Features * Filter support `{{ "# Heading Level 1"|markdown }}` * Tag support `{% markdown %}{% endmarkdown %}` When used as a tag, the indentation level of the first line sets the default indentation level for the rest of the tag content. From this indentation level, all same indentation or outdented levels text will be transformed as regular text. This feature allows you to write your Markdown content at any indentation level without caring of Markdown internal transformation: ```php

{{ title }}

{% markdown %} This is a list that is indented to match the context around the markdown tag: * List item 1 * List item 2 * Sub List Item * Sub Sub List Item The following block will be transformed as code, as it is indented more than the surrounding content: $code = "good"; {% endmarkdown %}
``` ## Installation Run the composer command to install the latest stable version: ```bash composer require aptoma/twig-markdown ``` Or update your `composer.json`: ```json { "require": { "aptoma/twig-markdown": "~3.0" } } ``` You can choose to provide your own Markdown engine, although we recommend using [michelf/php-markdown](https://github.com/michelf/php-markdown): ```bash composer require michelf/php-markdown ~1.8 ``` ```json { "require": { "michelf/php-markdown": "~1.8" } } ``` You may also use the [PHP League CommonMark engine](http://commonmark.thephpleague.com/): ```bash composer require league/commonmark ~0.19 ``` ```json { "require": { "league/commonmark": "~0.19" } } ``` ## Usage ### Twig Extension The Twig extension provides the `markdown` tag and filter support. Assuming that you are using [composer](http://getcomposer.org) autoloading, add the extension to the Twig environment: ```php use Aptoma\Twig\Extension\MarkdownExtension; use Aptoma\Twig\Extension\MarkdownEngine; $engine = new MarkdownEngine\MichelfMarkdownEngine(); $twig->addExtension(new MarkdownExtension($engine)); ``` ### Twig Token Parser The Twig token parser provides the `markdown` tag only! ```php use Aptoma\Twig\TokenParser\MarkdownTokenParser; $twig->addTokenParser(new MarkdownTokenParser()); ``` ### Symfony To use this extension in a [Symfony 3/4 app](https://symfony.com) (including [Pimcore](https://pimcore.com/)), add the following snippet to your app's `app/config/services.yml` file: ```yaml services: # ... markdown.engine: class: Aptoma\Twig\Extension\MarkdownEngine\MichelfMarkdownEngine twig.markdown: class: Aptoma\Twig\Extension\MarkdownExtension arguments: ['@markdown.engine'] tags: - { name: twig.extension } ``` ### GitHub Markdown Engine `MarkdownEngine\GitHubMarkdownEngine` provides an interface to GitHub's markdown engine using their public API via [`KnpLabs\php-github-api`][2]. To reduce API calls, rendered documents are hashed and cached in the filesystem. You can pass a GitHub repository and the path to be used for caching to the constructor: ```php use Aptoma\Twig\Extension\MarkdownEngine; $engine = new MarkdownEngine\GitHubMarkdownEngine( 'aptoma/twig-markdown', // The GitHub repository to use as a context true, // Whether to use GitHub's Flavored Markdown (GFM) '/tmp/markdown-cache', // Path on filesystem to store rendered documents ); ``` In order to authenticate the API client (for instance), it's possible to pass an own instance of `\GitHub\Client` instead of letting the engine create one itself: ```php $client = new \Github\Client; $client->authenticate('GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', \Github\Client::AUTH_URL_CLIENT_ID); $engine = new MarkdownEngine\GitHubMarkdownEngine('aptoma/twig-markdown', true, '/tmp/markdown-cache', $client); ``` ### Using a different Markdown parser engine If you want to use a different Markdown parser, you need to create an adapter that implements `Aptoma\Twig\Extension\MarkdownEngineInterface.php`. Have a look at `Aptoma\Twig\Extension\MarkdownEngine\MichelfMarkdownEngine` for an example. ## Tests The test suite uses PHPUnit: $ phpunit ## License Twig Markdown Extension is licensed under the MIT license. [1]: http://twig.sensiolabs.org [2]: https://github.com/knplabs/php-github-api ================================================ FILE: composer.json ================================================ { "name": "aptoma/twig-markdown", "description": "Twig extension to work with Markdown content", "keywords": ["twig", "markdown"], "license": "MIT", "authors": [ { "name": "Gunnar Lium", "email": "gunnar@aptoma.com" }, { "name": "Joris Berthelot", "email": "joris@berthelot.tel" } ], "require": { "php": "^7.0|^8.0", "twig/twig": "^2.7.0|^3.0" }, "require-dev": { "php": "^7.2.5|^8.0", "phpunit/phpunit": "~6.0|~5.0|~8.0", "michelf/php-markdown": "~1", "league/commonmark": "~0.5", "knplabs/github-api": "~3.0", "erusev/parsedown": "^1.6", "guzzlehttp/guzzle": "^7.2", "http-interop/http-factory-guzzle": "^1.0" }, "suggest": { "michelf/php-markdown": "Original Markdown engine with MarkdownExtra.", "knplabs/github-api": "Needed for using GitHub's Markdown engine provided through their API." }, "autoload": { "psr-0": { "Aptoma": "src/" } } } ================================================ FILE: phpunit.xml.dist ================================================ ./tests/ src/ ================================================ FILE: src/Aptoma/Twig/Extension/MarkdownEngine/GitHubMarkdownEngine.php ================================================ */ class GitHubMarkdownEngine implements MarkdownEngineInterface { /** * Constructor * * @param string $contextRepo The repository context. Pass a GitHub repo * such as 'aptoma/twig-markdown' to render e.g. issues #23 in the * context of the repo. * @param bool $gfm Whether to use GitHub's Flavored Markdown or the * standard markdown. Default is true. * @param string $cacheDir Location on disk where rendered documents should * be stored. Defaults to 'github-markdown-cache' folder in system * temp directory if no path is provided. * @param \Github\Client $client Client object to use. A new Github\Client() * object is constructed automatically if $client is null. */ public function __construct($contextRepo = null, $gfm = true, $cacheDir = null, \GitHub\Client $client=null) { $this->repo = $contextRepo; $this->mode = $gfm ? 'gfm' : 'markdown'; if (is_null($cacheDir)) { $cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'github-markdown-cache'; } $this->cacheDir = rtrim($cacheDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if (!is_dir($this->cacheDir)) { @mkdir($this->cacheDir, 0777, true); } if ($client === null) { $client = new \Github\Client(); } $this->api = $client->api('markdown'); } /** * {@inheritdoc} */ public function transform($content) { $cacheFile = $this->getCachePath($content); if (file_exists($cacheFile)) { return file_get_contents($cacheFile);; } $response = $this->api->render($content, $this->mode, $this->repo); file_put_contents($cacheFile, $response); return $response; } /** * {@inheritdoc} */ public function getName() { return 'KnpLabs\php-github-api'; } private function getCachePath($content) { return $this->cacheDir . md5($content) . '_' . $this->mode. '_' . str_replace('/', '.', $this->repo); } private $api; private $cacheDir; private $repo; private $mode; } ================================================ FILE: src/Aptoma/Twig/Extension/MarkdownEngine/MichelfMarkdownEngine.php ================================================ */ class MichelfMarkdownEngine implements MarkdownEngineInterface { /** * {@inheritdoc} */ public function transform($content) { return MarkdownExtra::defaultTransform($content); } /** * {@inheritdoc} */ public function getName() { return 'Michelf\Markdown'; } } ================================================ FILE: src/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngine.php ================================================ */ class PHPLeagueCommonMarkEngine implements MarkdownEngineInterface { /** * @var \League\CommonMark\CommonMarkConverter */ private $converter; /** * Constructor * * Accepts CommonMarkConverter or creates one automatically * * @param \League\CommonMark\CommonMarkConverter $converter */ public function __construct(CommonMarkConverter $converter = null) { $this->converter = $converter ?: new CommonMarkConverter(); } /** * {@inheritdoc} */ public function transform($content) { return $this->converter->convertToHtml($content); } /** * {@inheritdoc} */ public function getName() { return 'League\CommonMark'; } } ================================================ FILE: src/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngine.php ================================================ */ class ParsedownEngine implements MarkdownEngineInterface { /** * @var Parsedown */ protected $engine; /** * @param string|null $instanceName */ public function __construct($instanceName = null) { $this->engine = Parsedown::instance($instanceName); } /** * {@inheritdoc} */ public function transform($content) { return $this->engine->parse($content); } /** * {@inheritdoc} */ public function getName() { return 'erusev/parsedown'; } /** * Turn on/off escaping within the generated HTML. Should be * turned on for untrusted user input. * * @param bool $bool Flag to set Safe Mode to */ public function setSafeMode($bool) { $this->engine->setSafeMode($bool === true); } /** * Turn on/off escaping HTML in trusted user input. * * @param bool $bool Flag to set markup escaped to */ public function setMarkupEscaped($bool) { $this->engine->setMarkupEscaped($bool === true); } } ================================================ FILE: src/Aptoma/Twig/Extension/MarkdownEngineInterface.php ================================================ */ interface MarkdownEngineInterface { /** * Transforms the given markdown data in HTML * * @param string $content Markdown data * @return string */ public function transform($content); /** * Return Markdown engine vendor ID * * @return string */ public function getName(); } ================================================ FILE: src/Aptoma/Twig/Extension/MarkdownExtension.php ================================================ * @author Joris Berthelot */ class MarkdownExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\ExtensionInterface { /** * @var MarkdownEngineInterface $markdownEngine */ private $markdownEngine; /** * @param MarkdownEngineInterface $markdownEngine The Markdown parser engine */ public function __construct(MarkdownEngineInterface $markdownEngine) { $this->markdownEngine = $markdownEngine; } /** * {@inheritdoc} */ public function getFilters() { return array( new \Twig\TwigFilter( 'markdown', array($this, 'parseMarkdown'), array('is_safe' => array('html')) ) ); } /** * Transform Markdown content to HTML * * @param string $content The Markdown content to be transformed * @return string The result of the Markdown engine transformation */ public function parseMarkdown($content) { return $this->markdownEngine->transform($content); } /** * {@inheritdoc} */ public function getTokenParsers() { return array(new MarkdownTokenParser()); } } ================================================ FILE: src/Aptoma/Twig/Node/MarkdownNode.php ================================================ * @author Joris Berthelot */ class MarkdownNode extends \Twig\Node\Node { public function __construct(\Twig\Node\Node $body, $lineno, $tag = 'markdown') { parent::__construct(array('body' => $body), array(), $lineno, $tag); } /** * Compiles the node to PHP. * * @param \Twig\Compiler A Twig\Compiler instance */ public function compile(\Twig\Compiler $compiler) { $compiler ->addDebugInfo($this) ->write('ob_start();' . PHP_EOL) ->subcompile($this->getNode('body')) ->write('$content = ob_get_clean();' . PHP_EOL) ->write('preg_match("/^\s*/", $content, $matches);' . PHP_EOL) ->write('$lines = explode("\n", $content);' . PHP_EOL) ->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL) ->write('$content = join("\n", $content);' . PHP_EOL) ->write('echo $this->env->getExtension(\'Aptoma\Twig\Extension\MarkdownExtension\')->parseMarkdown($content);' . PHP_EOL); } } ================================================ FILE: src/Aptoma/Twig/TokenParser/MarkdownTokenParser.php ================================================ * @author Joris Berthelot */ class MarkdownTokenParser extends \Twig\TokenParser\AbstractTokenParser { /** * {@inheritdoc} */ public function parse(\Twig\Token $token) { $lineno = $token->getLine(); $this->parser->getStream()->expect(\Twig\Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideMarkdownEnd'), true); $this->parser->getStream()->expect(\Twig\Token::BLOCK_END_TYPE); return new MarkdownNode($body, $lineno, $this->getTag()); } /** * Decide if current token marks end of Markdown block. * * @param \Twig\Token $token * @return bool */ public function decideMarkdownEnd(\Twig\Token $token) { return $token->test('endmarkdown'); } /** * {@inheritdoc} */ public function getTag() { return 'markdown'; } } ================================================ FILE: tests/Aptoma/Twig/Extension/MarkdownEngine/GithubMarkdownEngineTest.php ================================================ */ class GitHubMarkdownEngineTest extends MarkdownExtensionTest { /** * @dataProvider getParseMarkdownTests */ public function testParseMarkdown($template, $expected, $context = array()) { try { $this->assertEquals($expected, $this->getTemplate($template)->render($context)); } catch (\Exception $e) { $this->markTestSkipped($e->getMessage()); } } public function getParseMarkdownTests() { return array( array('{{ "# Main Title"|markdown }}', '

Main Title

'), array('{{ content|markdown }}', '

Main Title

', array('content' => '# Main Title')), // Check if GFM is working array('{{ "@aptoma"|markdown }}', '

@aptoma

'), ); } protected function getEngine() { $client = new Client(); if ($client->rateLimit()->getResource('core')->getLimit() < 1) { $this->markTestSkipped('The github API rate limit is reached, so this engine cannot be tested.'); } return new GitHubMarkdownEngine(); } } ================================================ FILE: tests/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngineTest.php ================================================ */ class PHPLeagueCommonMarkEngineTest extends MarkdownExtensionTest { protected function getEngine() { return new PHPLeagueCommonMarkEngine(); } } ================================================ FILE: tests/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngineTest.php ================================================ */ class ParsedownEngineTest extends MarkdownExtensionTest { public function getParseMarkdownTests() { return array( array('{{ "# Main Title"|markdown }}', '

Main Title

'), array('{{ content|markdown }}', '

Main Title

', array('content' => '# Main Title')), array('{% markdown %}{{ content }}{% endmarkdown %}', '

Main Title

', array('content' => '# Main Title')) ); } protected function getEngine() { return new ParsedownEngine(); } public function testSafeMode() { $engine = $this->getEngine(); $loader = new \Twig\Loader\ArrayLoader(array('index' => '{{ "_Test_Test[xss](javascript:alert%281%29)"|markdown }}')); $twig = new \Twig\Environment($loader, array('debug' => true, 'cache' => false)); $twig->addExtension(new MarkdownExtension($engine)); $this->assertEquals('

TestTestxss

', $twig->load('index')->render()); $engine->setSafeMode(true); $this->assertEquals('

Test<em>Test</em>xss

', $twig->load('index')->render()); $engine->setSafeMode(false); } public function testMarkupEscape() { $engine = $this->getEngine(); $loader = new \Twig\Loader\ArrayLoader(array('index' => '{{ "_Test_Test[xss](javascript:alert%281%29)"|markdown }}')); $twig = new \Twig\Environment($loader, array('debug' => true, 'cache' => false)); $twig->addExtension(new MarkdownExtension($engine)); $this->assertEquals('

TestTestxss

', $twig->load('index')->render()); $engine->setMarkupEscaped(true); $this->assertEquals('

Test<em>Test</em>xss

', $twig->load('index')->render()); $engine->setMarkupEscaped(false); } } ================================================ FILE: tests/Aptoma/Twig/Extension/MarkdownExtensionTest.php ================================================ */ class MarkdownExtensionTest extends TestCase { /** * @dataProvider getParseMarkdownTests */ public function testParseMarkdown($template, $expected, $context = array()) { $this->assertEquals($expected, $this->getTemplate($template)->render($context)); } public function getParseMarkdownTests() { return array( array('{{ "# Main Title"|markdown }}', '

Main Title

' . PHP_EOL), array('{{ content|markdown }}', '

Main Title

' . PHP_EOL, array('content' => '# Main Title')) ); } protected function getEngine() { return new MichelfMarkdownEngine(); } protected function getTemplate($template) { $loader = new \Twig\Loader\ArrayLoader(array('index' => $template)); $twig = new \Twig\Environment($loader, array('debug' => true, 'cache' => false)); $twig->addExtension(new MarkdownExtension($this->getEngine())); return $twig->load('index'); } } ================================================ FILE: tests/Aptoma/Twig/TokenParser/MarkdownTokenParserTest.php ================================================ */ class MarkdownTokenParserTest extends TestCase { public function testConstructor() { $body = new Node(array(new TextNode("#Title\n\nparagraph\n", 1))); $node = new MarkdownNode($body, 1); $this->assertEquals($body, $node->getNode('body')); } /** * Test that the generated code actually do what we expect * * The contents of this test is the same that we write in the compile method. * This requires manual synchronization, which we should probably not rely on. */ public function testMarkdownPrepareBehavior() { $body = " #Title\n\n paragraph\n\n code"; $bodyPrepared = "#Title\n\nparagraph\n\n code"; ob_start(); echo $body; $content = ob_get_clean(); preg_match("/^\s*/", $content, $matches); $lines = explode("\n", $content); $content = preg_replace('/^' . $matches[0]. '/', "", $lines); $content = join("\n", $content); // Assert prepared content looks right $this->assertEquals($bodyPrepared, $content); // Assert Markdown output $expectedOutput = "

Title

\n\n

paragraph

\n\n
code\n
\n"; $this->assertEquals($expectedOutput, $this->getEngine()->transform($content)); } /** * Test that the generated code looks as expected * * @dataProvider getTests */ public function testCompile($node, $source, $environment = null, $isPattern = false) { $this->assertNodeCompilation($source, $node, $environment, $isPattern = false); } protected function getEngine() { return new MichelfMarkdownEngine(); } public function getTests() { $tests = array(); $body = new Node(array(new TextNode("#Title\n\nparagraph\n", 1))); $node = new MarkdownNode($body, 1); $tests['simple text'] = array($node, <<env->getExtension('Aptoma\Twig\Extension\MarkdownExtension')->parseMarkdown(\$content); EOF ); $body = new Node(array(new TextNode(" #Title\n\n paragraph\n\n code\n", 1))); $node = new MarkdownNode($body, 1); $tests['text with leading indent'] = array($node, <<env->getExtension('Aptoma\Twig\Extension\MarkdownExtension')->parseMarkdown(\$content); EOF ); return $tests; } public function assertNodeCompilation($source, Node $node, Environment $environment = null, $isPattern = false) { $compiler = $this->getCompiler($environment); $compiler->compile($node); if ($isPattern) { $this->assertStringMatchesFormat($source, trim($compiler->getSource())); } else { $this->assertEquals($source, trim($compiler->getSource())); } } protected function getCompiler(Environment $environment = null) { return new Compiler(null === $environment ? $this->getEnvironment() : $environment); } protected function getEnvironment() { return new Environment(new ArrayLoader(array())); } }