├── LICENSE ├── composer.json ├── config └── markdown.php └── src ├── Facades └── Markdown.php ├── MarkdownServiceProvider.php └── View ├── Compiler └── CommonMarkCompiler.php ├── Directive ├── CommonMarkDirective.php └── DirectiveInterface.php └── Engine ├── BladeCommonMarkEngine.php └── PhpCommonMarkEngine.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2025 Graham Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graham-campbell/markdown", 3 | "description": "Markdown Is A CommonMark Wrapper For Laravel", 4 | "keywords": ["laravel", "framework", "markdown", "commonmark", "common mark", "Laravel Markdown", "Laravel-Markdown", "Graham Campbell", "GrahamCampbell"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Graham Campbell", 9 | "email": "hello@gjcampbell.co.uk", 10 | "homepage": "https://github.com/GrahamCampbell" 11 | } 12 | ], 13 | "require": { 14 | "php": "^8.1", 15 | "illuminate/contracts": "^10.44 || ^11.0 || ^12.0", 16 | "illuminate/filesystem": "^10.44 || ^11.0 || ^12.0", 17 | "illuminate/support": "^10.44 || ^11.0 || ^12.0", 18 | "illuminate/view": "^10.44 || ^11.0 || ^12.0", 19 | "league/commonmark": "^2.6.1" 20 | }, 21 | "require-dev": { 22 | "graham-campbell/analyzer": "^5.0", 23 | "graham-campbell/testbench": "^6.2", 24 | "mockery/mockery": "^1.6.6", 25 | "phpunit/phpunit": "^10.5.45 || ^11.5.10" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "GrahamCampbell\\Markdown\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "GrahamCampbell\\Tests\\Markdown\\": "tests/" 35 | } 36 | }, 37 | "config": { 38 | "preferred-install": "dist" 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "GrahamCampbell\\Markdown\\MarkdownServiceProvider" 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/markdown.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | return [ 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Enable View Integration 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This option specifies if the view integration is enabled so you can write 22 | | markdown views and have them rendered as html. The following extensions 23 | | are currently supported: ".md", ".md.php", and ".md.blade.php". You may 24 | | disable this integration if it is conflicting with another package. 25 | | 26 | | Default: true 27 | | 28 | */ 29 | 30 | 'views' => true, 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | CommonMark Extensions 35 | |-------------------------------------------------------------------------- 36 | | 37 | | This option specifies what extensions will be automatically enabled. 38 | | Simply provide your extension class names here. 39 | | 40 | | Default: [ 41 | | League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class, 42 | | League\CommonMark\Extension\Table\TableExtension::class, 43 | | ] 44 | | 45 | */ 46 | 47 | 'extensions' => [ 48 | League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class, 49 | League\CommonMark\Extension\Table\TableExtension::class, 50 | ], 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Renderer Configuration 55 | |-------------------------------------------------------------------------- 56 | | 57 | | This option specifies an array of options for rendering HTML. 58 | | 59 | | Default: [ 60 | | 'block_separator' => "\n", 61 | | 'inner_separator' => "\n", 62 | | 'soft_break' => "\n", 63 | | ] 64 | | 65 | */ 66 | 67 | 'renderer' => [ 68 | 'block_separator' => "\n", 69 | 'inner_separator' => "\n", 70 | 'soft_break' => "\n", 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Commonmark Configuration 76 | |-------------------------------------------------------------------------- 77 | | 78 | | This option specifies an array of options for commonmark. 79 | | 80 | | Default: [ 81 | | 'enable_em' => true, 82 | | 'enable_strong' => true, 83 | | 'use_asterisk' => true, 84 | | 'use_underscore' => true, 85 | | 'unordered_list_markers' => ['-', '+', '*'], 86 | | ] 87 | | 88 | */ 89 | 90 | 'commonmark' => [ 91 | 'enable_em' => true, 92 | 'enable_strong' => true, 93 | 'use_asterisk' => true, 94 | 'use_underscore' => true, 95 | 'unordered_list_markers' => ['-', '+', '*'], 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | HTML Input 101 | |-------------------------------------------------------------------------- 102 | | 103 | | This option specifies how to handle untrusted HTML input. 104 | | 105 | | Default: 'strip' 106 | | 107 | */ 108 | 109 | 'html_input' => 'strip', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Allow Unsafe Links 114 | |-------------------------------------------------------------------------- 115 | | 116 | | This option specifies whether to allow risky image URLs and links. 117 | | 118 | | Default: true 119 | | 120 | */ 121 | 122 | 'allow_unsafe_links' => true, 123 | 124 | /* 125 | |-------------------------------------------------------------------------- 126 | | Maximum Nesting Level 127 | |-------------------------------------------------------------------------- 128 | | 129 | | This option specifies the maximum permitted block nesting level. 130 | | 131 | | Default: PHP_INT_MAX 132 | | 133 | */ 134 | 135 | 'max_nesting_level' => PHP_INT_MAX, 136 | 137 | /* 138 | |-------------------------------------------------------------------------- 139 | | Slug Normalizer 140 | |-------------------------------------------------------------------------- 141 | | 142 | | This option specifies an array of options for slug normalization. 143 | | 144 | | Default: [ 145 | | 'max_length' => 255, 146 | | 'unique' => 'document', 147 | | ] 148 | | 149 | */ 150 | 151 | 'slug_normalizer' => [ 152 | 'max_length' => 255, 153 | 'unique' => 'document', 154 | ], 155 | 156 | ]; 157 | -------------------------------------------------------------------------------- /src/Facades/Markdown.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown\Facades; 15 | 16 | use Illuminate\Support\Facades\Facade; 17 | 18 | /** 19 | * @author Graham Campbell 20 | */ 21 | class Markdown extends Facade 22 | { 23 | /** 24 | * Get the registered name of the component. 25 | * 26 | * @return string 27 | */ 28 | protected static function getFacadeAccessor(): string 29 | { 30 | return 'markdown.converter'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MarkdownServiceProvider.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown; 15 | 16 | use GrahamCampbell\Markdown\View\Compiler\CommonMarkCompiler; 17 | use GrahamCampbell\Markdown\View\Directive\CommonMarkDirective; 18 | use GrahamCampbell\Markdown\View\Directive\DirectiveInterface; 19 | use GrahamCampbell\Markdown\View\Engine\BladeCommonMarkEngine; 20 | use GrahamCampbell\Markdown\View\Engine\PhpCommonMarkEngine; 21 | use Illuminate\Contracts\Container\Container; 22 | use Illuminate\Foundation\Application as LaravelApplication; 23 | use Illuminate\Support\Arr; 24 | use Illuminate\Support\ServiceProvider; 25 | use Illuminate\View\Engines\CompilerEngine; 26 | use Laravel\Lumen\Application as LumenApplication; 27 | use League\CommonMark\ConverterInterface; 28 | use League\CommonMark\Environment\Environment; 29 | use League\CommonMark\Environment\EnvironmentBuilderInterface; 30 | use League\CommonMark\Environment\EnvironmentInterface; 31 | use League\CommonMark\MarkdownConverter; 32 | 33 | /** 34 | * This is the markdown service provider class. 35 | * 36 | * @author Graham Campbell 37 | */ 38 | class MarkdownServiceProvider extends ServiceProvider 39 | { 40 | /** 41 | * Boot the service provider. 42 | * 43 | * @return void 44 | */ 45 | public function boot(): void 46 | { 47 | $this->setupConfig(); 48 | 49 | if ($this->app->config->get('markdown.views')) { 50 | $this->enableCompiler(); 51 | $this->enablePhpEngine(); 52 | $this->enableBladeEngine(); 53 | $this->enableBladeDirective(); 54 | } 55 | } 56 | 57 | /** 58 | * Setup the config. 59 | * 60 | * @return void 61 | */ 62 | private function setupConfig(): void 63 | { 64 | $source = realpath($raw = __DIR__.'/../config/markdown.php') ?: $raw; 65 | 66 | if ($this->app instanceof LaravelApplication && $this->app->runningInConsole()) { 67 | $this->publishes([$source => config_path('markdown.php')]); 68 | } elseif ($this->app instanceof LumenApplication) { 69 | $this->app->configure('markdown'); 70 | } 71 | 72 | $this->mergeConfigFrom($source, 'markdown'); 73 | } 74 | 75 | /** 76 | * Enable the markdown compiler. 77 | * 78 | * @return void 79 | */ 80 | private function enableCompiler(): void 81 | { 82 | $app = $this->app; 83 | 84 | $app->view->getEngineResolver()->register('md', function () use ($app): CompilerEngine { 85 | $compiler = $app['markdown.compiler']; 86 | 87 | return new CompilerEngine($compiler); 88 | }); 89 | 90 | $app->view->addExtension('md', 'md'); 91 | } 92 | 93 | /** 94 | * Enable the php markdown engine. 95 | * 96 | * @return void 97 | */ 98 | private function enablePhpEngine(): void 99 | { 100 | $app = $this->app; 101 | 102 | $app->view->getEngineResolver()->register('phpmd', function () use ($app): PhpCommonMarkEngine { 103 | $files = $app['files']; 104 | $converter = $app['markdown.converter']; 105 | 106 | return new PhpCommonMarkEngine($files, $converter); 107 | }); 108 | 109 | $app->view->addExtension('md.php', 'phpmd'); 110 | } 111 | 112 | /** 113 | * Enable the blade markdown engine. 114 | * 115 | * @return void 116 | */ 117 | private function enableBladeEngine(): void 118 | { 119 | $app = $this->app; 120 | 121 | $app->view->getEngineResolver()->register('blademd', function () use ($app): BladeCommonMarkEngine { 122 | $compiler = $app['blade.compiler']; 123 | $files = $app['files']; 124 | $converter = $app['markdown.converter']; 125 | 126 | return new BladeCommonMarkEngine($compiler, $files, $converter); 127 | }); 128 | 129 | $app->view->addExtension('md.blade.php', 'blademd'); 130 | } 131 | 132 | private function enableBladeDirective(): void 133 | { 134 | $app = $this->app; 135 | 136 | $app['blade.compiler']->directive('markdown', function (string $markdown): string { 137 | if ($markdown) { 138 | return "convert((string) {$markdown})->getContent(); ?>"; 139 | } 140 | 141 | return ''; 142 | }); 143 | 144 | $app['blade.compiler']->directive('endmarkdown', function (): string { 145 | return "render(ob_get_clean()); ?>"; 146 | }); 147 | } 148 | 149 | /** 150 | * Register the service provider. 151 | * 152 | * @return void 153 | */ 154 | public function register(): void 155 | { 156 | $this->registerEnvironment(); 157 | $this->registerMarkdown(); 158 | $this->registerCompiler(); 159 | $this->registerDirective(); 160 | } 161 | 162 | /** 163 | * Register the environment class. 164 | * 165 | * @return void 166 | */ 167 | private function registerEnvironment(): void 168 | { 169 | $this->app->singleton('markdown.environment', function (Container $app): Environment { 170 | $config = $app->config->get('markdown'); 171 | 172 | $environment = new Environment(Arr::except($config, ['extensions', 'views'])); 173 | 174 | foreach ((array) Arr::get($config, 'extensions') as $extension) { 175 | $environment->addExtension($app->make($extension)); 176 | } 177 | 178 | return $environment; 179 | }); 180 | 181 | $this->app->alias('markdown.environment', Environment::class); 182 | $this->app->alias('markdown.environment', EnvironmentInterface::class); 183 | $this->app->alias('markdown.environment', EnvironmentBuilderInterface::class); 184 | } 185 | 186 | /** 187 | * Register the markdowm class. 188 | * 189 | * @return void 190 | */ 191 | private function registerMarkdown(): void 192 | { 193 | $this->app->singleton('markdown.converter', function (Container $app): MarkdownConverter { 194 | $environment = $app['markdown.environment']; 195 | 196 | return new MarkdownConverter($environment); 197 | }); 198 | 199 | $this->app->alias('markdown.converter', MarkdownConverter::class); 200 | $this->app->alias('markdown.converter', ConverterInterface::class); 201 | } 202 | 203 | /** 204 | * Register the markdown compiler class. 205 | * 206 | * @return void 207 | */ 208 | private function registerCompiler(): void 209 | { 210 | $this->app->singleton('markdown.compiler', function (Container $app): CommonMarkCompiler { 211 | $converter = $app['markdown.converter']; 212 | $files = $app['files']; 213 | $storagePath = $app->config->get('view.compiled'); 214 | 215 | return new CommonMarkCompiler($converter, $files, $storagePath); 216 | }); 217 | 218 | $this->app->alias('markdown.compiler', CommonMarkCompiler::class); 219 | } 220 | 221 | /** 222 | * Register the markdown directive class. 223 | * 224 | * @return void 225 | */ 226 | private function registerDirective(): void 227 | { 228 | $this->app->singleton('markdown.directive', function (Container $app): CommonMarkDirective { 229 | $converter = $app['markdown.converter']; 230 | 231 | return new CommonMarkDirective($converter); 232 | }); 233 | 234 | $this->app->alias('markdown.directive', CommonMarkDirective::class); 235 | $this->app->alias('markdown.directive', DirectiveInterface::class); 236 | } 237 | 238 | /** 239 | * Get the services provided by the provider. 240 | * 241 | * @return string[] 242 | */ 243 | public function provides(): array 244 | { 245 | return [ 246 | 'markdown.environment', 247 | 'markdown.converter', 248 | 'markdown.compiler', 249 | 'markdown.directive', 250 | ]; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/View/Compiler/CommonMarkCompiler.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown\View\Compiler; 15 | 16 | use Illuminate\Filesystem\Filesystem; 17 | use Illuminate\View\Compilers\Compiler; 18 | use Illuminate\View\Compilers\CompilerInterface; 19 | use League\CommonMark\ConverterInterface; 20 | 21 | /** 22 | * @author Graham Campbell 23 | */ 24 | final class CommonMarkCompiler extends Compiler implements CompilerInterface 25 | { 26 | private readonly ConverterInterface $converter; 27 | 28 | /** 29 | * @param \League\CommonMark\ConverterInterface $converter 30 | * @param \Illuminate\Filesystem\Filesystem $files 31 | * @param string $cachePath 32 | * 33 | * @return void 34 | */ 35 | public function __construct(ConverterInterface $converter, Filesystem $files, string $cachePath) 36 | { 37 | parent::__construct($files, $cachePath); 38 | 39 | $this->converter = $converter; 40 | } 41 | 42 | /** 43 | * Compile the view at the given path. 44 | * 45 | * @param string $path 46 | * 47 | * @return void 48 | */ 49 | public function compile($path): void 50 | { 51 | $content = $this->converter->convert($this->files->get($path))->getContent(); 52 | 53 | $this->files->put($this->getCompiledPath($path), $content); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/View/Directive/CommonMarkDirective.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown\View\Directive; 15 | 16 | use League\CommonMark\ConverterInterface; 17 | 18 | /** 19 | * @author Graham Campbell 20 | */ 21 | final class CommonMarkDirective implements DirectiveInterface 22 | { 23 | private readonly ConverterInterface $converter; 24 | 25 | /** 26 | * @param \League\CommonMark\ConverterInterface $converter 27 | * 28 | * @return void 29 | */ 30 | public function __construct(ConverterInterface $converter) 31 | { 32 | $this->converter = $converter; 33 | } 34 | 35 | /** 36 | * Normalize and render the markdown. 37 | * 38 | * @param string $markdown 39 | * 40 | * @return string 41 | */ 42 | public function render(string $markdown): string 43 | { 44 | return $this->converter->convert(self::adjust($markdown))->getContent(); 45 | } 46 | 47 | /** 48 | * Adjust for indentation. 49 | * 50 | * @param string $markdown 51 | * 52 | * @return string 53 | */ 54 | private static function adjust(string $markdown): string 55 | { 56 | $lines = preg_split("/(\r\n|\n|\r)/", $markdown); 57 | 58 | if (!$lines) { 59 | return $markdown; 60 | } 61 | 62 | $last = array_values(array_slice($lines, -1))[0]; 63 | if ($indent = trim($last) === '' ? $last : '') { 64 | $len = strlen($indent); 65 | foreach ($lines as $key => $value) { 66 | if (substr($value, 0, $len) === $indent) { 67 | $lines[$key] = substr($value, $len); 68 | } elseif (trim($value) !== '') { 69 | return $markdown; // bail out 70 | } 71 | } 72 | } 73 | 74 | return implode("\n", $lines); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/View/Directive/DirectiveInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown\View\Directive; 15 | 16 | /** 17 | * @author Graham Campbell 18 | */ 19 | interface DirectiveInterface 20 | { 21 | /** 22 | * Normalize and render the markdown. 23 | * 24 | * @param string $markdown 25 | * 26 | * @return string 27 | */ 28 | public function render(string $markdown): string; 29 | } 30 | -------------------------------------------------------------------------------- /src/View/Engine/BladeCommonMarkEngine.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown\View\Engine; 15 | 16 | use Illuminate\Filesystem\Filesystem; 17 | use Illuminate\View\Compilers\CompilerInterface; 18 | use Illuminate\View\Engines\CompilerEngine; 19 | use League\CommonMark\ConverterInterface; 20 | 21 | /** 22 | * @author Graham Campbell 23 | */ 24 | final class BladeCommonMarkEngine extends CompilerEngine 25 | { 26 | private readonly ConverterInterface $converter; 27 | 28 | /** 29 | * @param \Illuminate\View\Compilers\CompilerInterface $compiler 30 | * @param \Illuminate\Filesystem\Filesystem $files 31 | * @param \League\CommonMark\ConverterInterface $converter 32 | * 33 | * @return void 34 | */ 35 | public function __construct(CompilerInterface $compiler, Filesystem $files, ConverterInterface $converter) 36 | { 37 | parent::__construct($compiler, $files); 38 | 39 | $this->converter = $converter; 40 | } 41 | 42 | /** 43 | * Get the evaluated contents of the view. 44 | * 45 | * @param string $path 46 | * @param array $data 47 | * 48 | * @return string 49 | */ 50 | public function get($path, array $data = []): string 51 | { 52 | $contents = parent::get($path, $data); 53 | 54 | return $this->converter->convert($contents)->getContent(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/View/Engine/PhpCommonMarkEngine.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace GrahamCampbell\Markdown\View\Engine; 15 | 16 | use Illuminate\Filesystem\Filesystem; 17 | use Illuminate\View\Engines\PhpEngine; 18 | use League\CommonMark\ConverterInterface; 19 | 20 | /** 21 | * @author Graham Campbell 22 | */ 23 | final class PhpCommonMarkEngine extends PhpEngine 24 | { 25 | private readonly ConverterInterface $converter; 26 | 27 | /** 28 | * @param \Illuminate\Filesystem\Filesystem $files 29 | * @param \League\CommonMark\ConverterInterface $converter 30 | * 31 | * @return void 32 | */ 33 | public function __construct(Filesystem $files, ConverterInterface $converter) 34 | { 35 | parent::__construct($files); 36 | 37 | $this->converter = $converter; 38 | } 39 | 40 | /** 41 | * Get the evaluated contents of the view. 42 | * 43 | * @param string $path 44 | * @param array $data 45 | * 46 | * @return string 47 | */ 48 | public function get($path, array $data = []): string 49 | { 50 | $contents = parent::get($path, $data); 51 | 52 | return $this->converter->convert($contents)->getContent(); 53 | } 54 | } 55 | --------------------------------------------------------------------------------