├── LICENSE ├── README.md ├── composer.json └── src ├── ProcessBuilder.php ├── PygmentizeProcessFailed.php ├── Pygments.php └── PygmentsError.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Kazuyuki Hayashi 2 | Copyright (c) 2017-2025 Ben Ramsey 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ramsey/pygments

2 | 3 |

4 | A PHP wrapper for Pygments, the Python syntax highlighter. 5 |

6 | 7 |

8 | Source Code 9 | Download Package 10 | PHP Programming Language 11 | Read License 12 | Build Status 13 | Codecov Code Coverage 14 |

15 | 16 | ## About 17 | 18 | ramsey/pygments is a PHP wrapper for [Pygments](https://pygments.org), the 19 | Python syntax highlighter, forked from the 20 | [Pygments.php](https://github.com/kzykhys/Pygments.php) project. 21 | 22 | This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). 23 | By participating in this project and its community, you are expected to 24 | uphold this code. 25 | 26 | ## Installation 27 | 28 | Install this package as a dependency using [Composer](https://getcomposer.org). 29 | 30 | ``` bash 31 | composer require ramsey/pygments 32 | ``` 33 | 34 | ### Requirements 35 | 36 | * PHP 8.2 or greater 37 | * Python 38 | * Pygments (`pip install Pygments`) 39 | 40 | Python and Pygments versions tested: 41 | 42 | | Pygments: | 2.17 | 2.18 | 2.19 | 43 | |:------------|:----:|:----:|:----:| 44 | | Python 3.11 | ✔ | ✔ | ✔ | 45 | | Python 3.12 | ✔ | ✔ | ✔ | 46 | | Python 3.13 | ✔ | ✔ | ✔ | 47 | 48 | > [!NOTE] 49 | > ramsey/pygments will likely work on other versions of Python and Pygments, but 50 | > the versions tested against are limited to keep the GitHub Actions job matrix 51 | > at a reasonable size. If you encounter a version of Python or Pygments that 52 | > does not work, please [open an issue](https://github.com/ramsey/pygments/issues). 53 | 54 | ## Usage 55 | 56 | ### Highlight source code 57 | 58 | ``` php 59 | use Ramsey\Pygments\Pygments; 60 | 61 | $pygments = new Pygments(); 62 | $html = $pygments->highlight(file_get_contents('index.php'), 'php', 'html'); 63 | $console = $pygments->highlight('package main', 'go', 'ansi'); 64 | ``` 65 | 66 | ### Generate CSS 67 | 68 | ``` php 69 | use Ramsey\Pygments\Pygments; 70 | 71 | $pygments = new Pygments(); 72 | $css = $pygments->getCss('monokai'); 73 | $prefixedCss = $pygments->getCss('default', '.syntax'); 74 | ``` 75 | 76 | ### Guess lexer name 77 | 78 | ``` php 79 | use Ramsey\Pygments\Pygments; 80 | 81 | $pygments = new Pygments(); 82 | $lexer = $pygments->guessLexer('foo.rb'); // ruby 83 | ``` 84 | 85 | ### Get a list of lexers/formatters/styles 86 | 87 | ``` php 88 | use Ramsey\Pygments\Pygments; 89 | 90 | $pygments = new Pygments(); 91 | $lexers = $pygments->getLexers() 92 | $formatters = $pygments->getFormatters(); 93 | $styles = $pygments->getStyles(); 94 | ``` 95 | 96 | ### Set a custom `pygmentize` path 97 | 98 | ``` php 99 | use Ramsey\Pygments\Pygments; 100 | 101 | $pygments = new Pygments('/path/to/pygmentize'); 102 | ``` 103 | 104 | ## Contributing 105 | 106 | Contributions are welcome! To contribute, please familiarize yourself with 107 | [CONTRIBUTING.md](CONTRIBUTING.md). 108 | 109 | ## Coordinated Disclosure 110 | 111 | Keeping user information safe and secure is a top priority, and we welcome the 112 | contribution of external security researchers. If you believe you've found a 113 | security issue in software that is maintained in this repository, please read 114 | [SECURITY.md](SECURITY.md) for instructions on submitting a vulnerability report. 115 | 116 | ## Copyright and License 117 | 118 | The ramsey/pygments library is copyright © [Ben Ramsey](https://benramsey.com) 119 | and licensed for use under the terms of the MIT License (MIT). 120 | 121 | ramsey/pygments is a fork of [Pygments.php](https://github.com/kzykhys/Pygments.php). 122 | The Pygments.php library is copyright © [Kazuyuki Hayashi](https://github.com/kzykhys) 123 | and licensed for use under the terms of the MIT License (MIT). 124 | 125 | Please see [LICENSE](LICENSE) for more information. 126 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ramsey/pygments", 3 | "description": "A PHP wrapper for Pygments, the Python syntax highlighter, forked from kzykhys/pygments.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "pygments", 7 | "syntax", 8 | "highlight" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Kazuyuki Hayashi", 13 | "email": "hayashi@valnur.net" 14 | }, 15 | { 16 | "name": "Ben Ramsey", 17 | "email": "ben@benramsey.com", 18 | "homepage": "https://benramsey.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.2", 23 | "symfony/process": "^7.1.7" 24 | }, 25 | "require-dev": { 26 | "captainhook/captainhook": "^5.25", 27 | "captainhook/plugin-composer": "^5.3", 28 | "ergebnis/composer-normalize": "^2.45", 29 | "mockery/mockery": "^1.6", 30 | "php-parallel-lint/php-console-highlighter": "^1.0", 31 | "php-parallel-lint/php-parallel-lint": "^1.4", 32 | "phpstan/extension-installer": "^1.4", 33 | "phpstan/phpstan": "^2.1", 34 | "phpstan/phpstan-mockery": "^2.0", 35 | "phpstan/phpstan-phpunit": "^2.0", 36 | "phpunit/phpunit": "^11.5 || ^12.0", 37 | "ramsey/coding-standard": "^2.3", 38 | "ramsey/composer-repl": "^1.5", 39 | "ramsey/conventional-commits": "^1.6", 40 | "roave/security-advisories": "dev-latest", 41 | "spatie/phpunit-snapshot-assertions": "^5.1" 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "autoload": { 46 | "psr-4": { 47 | "Ramsey\\Pygments\\": "src" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Ramsey\\Pygments\\Test\\": "tests" 53 | } 54 | }, 55 | "config": { 56 | "allow-plugins": { 57 | "captainhook/plugin-composer": true, 58 | "dealerdirect/phpcodesniffer-composer-installer": true, 59 | "ergebnis/composer-normalize": true, 60 | "php-http/discovery": true, 61 | "phpstan/extension-installer": true, 62 | "ramsey/composer-repl": true 63 | }, 64 | "sort-packages": true 65 | }, 66 | "extra": { 67 | "captainhook": { 68 | "force-install": true 69 | }, 70 | "ramsey/conventional-commits": { 71 | "configFile": "conventional-commits.json" 72 | } 73 | }, 74 | "scripts": { 75 | "analyze": [ 76 | "@dev:analyze:phpstan" 77 | ], 78 | "build:clean": "git clean -fX build/.", 79 | "build:clean:cache": "git clean -fX build/cache/.", 80 | "build:clean:coverage": "git clean -fX build/coverage/.", 81 | "dev:analyze:phpstan": "phpstan analyze --ansi", 82 | "dev:lint:fix": "phpcbf --cache=build/cache/phpcs.cache", 83 | "dev:lint:style": "phpcs --colors --cache=build/cache/phpcs.cache", 84 | "dev:lint:syntax": "parallel-lint --colors src tests", 85 | "dev:test:coverage:ci": "XDEBUG_MODE=coverage phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", 86 | "dev:test:coverage:html": "XDEBUG_MODE=coverage phpunit --colors=always --coverage-html build/coverage/coverage-html", 87 | "dev:test:unit": "phpunit --colors=always", 88 | "lint": [ 89 | "@dev:lint:syntax", 90 | "@dev:lint:style" 91 | ], 92 | "test": [ 93 | "@dev:lint:syntax", 94 | "@dev:lint:style", 95 | "@dev:analyze:phpstan", 96 | "@dev:test:unit" 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ProcessBuilder.php: -------------------------------------------------------------------------------- 1 | 42 | */ 43 | private array $prefix = []; 44 | 45 | /** 46 | * @param list $arguments An array of arguments 47 | */ 48 | public function __construct(private array $arguments = []) 49 | { 50 | } 51 | 52 | /** 53 | * Creates a process builder instance. 54 | * 55 | * @param list $arguments An array of arguments 56 | */ 57 | public static function create(array $arguments = []): self 58 | { 59 | return new self($arguments); 60 | } 61 | 62 | /** 63 | * Adds an unescaped argument to the command string. 64 | */ 65 | public function add(string $argument): self 66 | { 67 | $this->arguments[] = $argument; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Adds a prefix to the command string. 74 | * 75 | * The prefix is preserved when resetting arguments. 76 | * 77 | * @param string | list $prefix A command prefix or an array of command prefixes 78 | */ 79 | public function setPrefix(array | string $prefix): self 80 | { 81 | $this->prefix = (array) $prefix; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Sets the arguments of the process. 88 | * 89 | * Arguments must not be escaped. Previous arguments are removed. 90 | * 91 | * @param list $arguments 92 | */ 93 | public function setArguments(array $arguments): self 94 | { 95 | $this->arguments = $arguments; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Sets the input of the process. 102 | */ 103 | public function setInput(mixed $input): self 104 | { 105 | $this->input = ProcessUtils::validateInput(__METHOD__, $input); 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Creates a Process instance and returns it. 112 | */ 113 | public function getProcess(): Process 114 | { 115 | if ($this->prefix === [] && $this->arguments === []) { 116 | throw new LogicException('You must add() command arguments before calling getProcess().'); 117 | } 118 | 119 | return new Process( 120 | command: [...$this->prefix, ...$this->arguments], 121 | input: $this->input, 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/PygmentizeProcessFailed.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://opensource.org/licenses/MIT MIT 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace Ramsey\Pygments; 16 | 17 | use RuntimeException; 18 | 19 | class PygmentizeProcessFailed extends RuntimeException implements PygmentsError 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Pygments.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) Ben Ramsey 11 | * @license http://opensource.org/licenses/MIT MIT 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Ramsey\Pygments; 17 | 18 | use Symfony\Component\Process\Process; 19 | 20 | use function explode; 21 | use function preg_match_all; 22 | use function trim; 23 | 24 | use const PREG_SET_ORDER; 25 | 26 | /** 27 | * A PHP wrapper for Pygments, the Python syntax highlighter 28 | */ 29 | final readonly class Pygments 30 | { 31 | /** 32 | * @param string $pygmentize The path to the `pygmentize` command. 33 | * By default, this uses `pygmentize` found in the `PATH`. 34 | */ 35 | public function __construct(private string $pygmentize = 'pygmentize') 36 | { 37 | } 38 | 39 | /** 40 | * Highlight the input code 41 | * 42 | * @param string $code The code to highlight 43 | * @param string | null $lexer The name of the lexer (php, html, ...) 44 | * @param string | null $formatter The name of the formatter (html, ansi, ...) 45 | * @param array $options An array of options 46 | * 47 | * @return string The code with syntax highlighting applied, in the specified format 48 | */ 49 | public function highlight( 50 | string $code, 51 | ?string $lexer = null, 52 | ?string $formatter = null, 53 | array $options = [], 54 | ): string { 55 | $builder = $this->createProcessBuilder(); 56 | 57 | if ($lexer !== null) { 58 | $builder->add('-l')->add($lexer); 59 | } else { 60 | // Guess the lexer. 61 | $builder->add('-g'); 62 | } 63 | 64 | if ($formatter !== null) { 65 | $builder->add('-f')->add($formatter); 66 | } 67 | 68 | foreach ($options as $key => $value) { 69 | $builder->add('-P')->add("$key=$value"); 70 | } 71 | 72 | $process = $builder->setInput($code)->getProcess(); 73 | 74 | return $this->getOutput($process); 75 | } 76 | 77 | /** 78 | * Gets style definition 79 | * 80 | * @param string $style The name of the style (default, colorful, ...) 81 | * @param string | null $selector A CSS selector prefix to prepend to the CSS classes 82 | * 83 | * @return string The Pygments CSS definition for the specified style 84 | */ 85 | public function getCss(string $style = 'default', ?string $selector = null): string 86 | { 87 | $builder = $this->createProcessBuilder(); 88 | $builder->add('-f')->add('html'); 89 | $builder->add('-S')->add($style); 90 | 91 | if ($selector !== null) { 92 | $builder->add('-a')->add($selector); 93 | } 94 | 95 | return $this->getOutput($builder->getProcess()); 96 | } 97 | 98 | /** 99 | * Guesses a lexer name based solely on the given filename 100 | * 101 | * @param string $fileName The name of a file to guess the lexer for. 102 | * The file does not need to exist, or be readable; Pygments uses the 103 | * file extension to guess the lexer. 104 | * 105 | * @return string The name of the lexer guessed 106 | */ 107 | public function guessLexer(string $fileName): string 108 | { 109 | $process = $this->createProcessBuilder() 110 | ->setArguments(['-N', $fileName]) 111 | ->getProcess(); 112 | 113 | return trim($this->getOutput($process)); 114 | } 115 | 116 | /** 117 | * Gets a list of lexers 118 | * 119 | * @return array 120 | */ 121 | public function getLexers(): array 122 | { 123 | $process = $this->createProcessBuilder() 124 | ->setArguments(['-L', 'lexer']) 125 | ->getProcess(); 126 | 127 | $output = $this->getOutput($process); 128 | 129 | return $this->parseList($output); 130 | } 131 | 132 | /** 133 | * Gets a list of formatters 134 | * 135 | * @return array 136 | */ 137 | public function getFormatters(): array 138 | { 139 | $process = $this->createProcessBuilder() 140 | ->setArguments(['-L', 'formatter']) 141 | ->getProcess(); 142 | 143 | $output = $this->getOutput($process); 144 | 145 | return $this->parseList($output); 146 | } 147 | 148 | /** 149 | * Gets a list of styles 150 | * 151 | * @return array 152 | */ 153 | public function getStyles(): array 154 | { 155 | $process = $this->createProcessBuilder() 156 | ->setArguments(['-L', 'style']) 157 | ->getProcess(); 158 | 159 | $output = $this->getOutput($process); 160 | 161 | return $this->parseList($output); 162 | } 163 | 164 | private function createProcessBuilder(): ProcessBuilder 165 | { 166 | return ProcessBuilder::create()->setPrefix($this->pygmentize); 167 | } 168 | 169 | private function getOutput(Process $process): string 170 | { 171 | $process->run(); 172 | 173 | if (!$process->isSuccessful()) { 174 | throw new PygmentizeProcessFailed( 175 | 'An error occurred while running pygmentize: ' . $process->getErrorOutput(), 176 | ); 177 | } 178 | 179 | return $process->getOutput(); 180 | } 181 | 182 | /** 183 | * @return array 184 | */ 185 | private function parseList(string $input): array 186 | { 187 | $list = []; 188 | 189 | if (preg_match_all('/^\* (.*?):\r?\n *([^\r\n]*?)$/m', $input, $matches, PREG_SET_ORDER)) { 190 | foreach ($matches as $match) { 191 | $names = explode(',', $match[1]); 192 | 193 | foreach ($names as $name) { 194 | $list[trim($name)] = trim($match[2]); 195 | } 196 | } 197 | } 198 | 199 | return $list; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/PygmentsError.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://opensource.org/licenses/MIT MIT 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace Ramsey\Pygments; 16 | 17 | use Throwable; 18 | 19 | interface PygmentsError extends Throwable 20 | { 21 | } 22 | --------------------------------------------------------------------------------