├── CHANGELOG.md ├── src ├── SnippetLine.php ├── Bounds.php ├── File.php └── CodeSnippet.php ├── LICENSE.md ├── composer.json ├── .php_cs.dist.php └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `code-snippets` will be documented in this file. 4 | 5 | ## 1.2.0 - 2021-07-27 6 | 7 | - add `getLineNumbers()` method 8 | - add `getSelectedBounds()` method 9 | - add `Bounds::size()` method 10 | 11 | ## 1.1.0 - 2021-07-27 12 | 13 | - add `toString()` and `__toString()` methods 14 | 15 | ## 1.0.2 - 2021-07-25 16 | 17 | - fix minor bugs, refactor line boundary calculations 18 | 19 | ## 1.0.1 - 2021-07-25 20 | 21 | - fix issue with accessing `surroundingLines` prop before being populated 22 | 23 | ## 1.0.0 - 2021-07-25 24 | 25 | - initial release 26 | -------------------------------------------------------------------------------- /src/SnippetLine.php: -------------------------------------------------------------------------------- 1 | lineNumber = $lineNumber; 19 | $this->value = $value; 20 | $this->isSelected = $isSelected; 21 | } 22 | 23 | public static function create(int $lineNumber, string $value, bool $isSelected): self 24 | { 25 | return new static(...func_get_args()); 26 | } 27 | 28 | public function value(): string 29 | { 30 | return $this->value; 31 | } 32 | 33 | public function lineNumber(): int 34 | { 35 | return $this->lineNumber; 36 | } 37 | 38 | public function isSelected(): bool 39 | { 40 | return $this->isSelected; 41 | } 42 | 43 | public function __toString() 44 | { 45 | return $this->value(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Permafrost Software 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": "permafrost-dev/code-snippets", 3 | "description": "Easily work with code snippets in PHP", 4 | "keywords": [ 5 | "permafrost", 6 | "code", 7 | "snippets", 8 | "code-snippets" 9 | ], 10 | "homepage": "https://github.com/permafrost-dev/code-snippets", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Patrick Organ", 15 | "email": "patrick@permafrost.dev", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^7.3|^8.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^9.5", 24 | "spatie/phpunit-snapshot-assertions": "^4.2" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Permafrost\\CodeSnippets\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Permafrost\\CodeSnippets\\Tests\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/phpunit", 38 | "test-coverage": "vendor/bin/phpunit --coverage-html build/coverage" 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /src/Bounds.php: -------------------------------------------------------------------------------- 1 | start = $start; 16 | $this->end = $end; 17 | } 18 | 19 | public static function create(int $start, int $end): self 20 | { 21 | return new static(...func_get_args()); 22 | } 23 | 24 | public static function createFromArray(array $lineNumbers): self 25 | { 26 | sort($lineNumbers, SORT_NUMERIC); 27 | 28 | return static::create($lineNumbers[0], $lineNumbers[count($lineNumbers) - 1]); 29 | } 30 | 31 | public function toArray(): array 32 | { 33 | return [$this->start, $this->end]; 34 | } 35 | 36 | public function mergeWith(self $bounds): self 37 | { 38 | $data = array_merge($this->toArray(), $bounds->toArray()); 39 | 40 | return static::createFromArray($data); 41 | } 42 | 43 | public function copy(self $bounds): self 44 | { 45 | $this->start = $bounds->start; 46 | $this->end = $bounds->end; 47 | 48 | return $this; 49 | } 50 | 51 | public function size(): int 52 | { 53 | return count(range($this->start, $this->end)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /src/File.php: -------------------------------------------------------------------------------- 1 | path = $path; 26 | $this->filename = $this->getRealPath(); 27 | } 28 | 29 | public function file(): SplFileObject 30 | { 31 | if (! $this->file) { 32 | $this->file = new SplFileObject($this->path); 33 | } 34 | 35 | return $this->file; 36 | } 37 | 38 | public function exists(): bool 39 | { 40 | return file_exists($this->getRealPath()); 41 | } 42 | 43 | public function contents(): string 44 | { 45 | return file_get_contents($this->path); 46 | } 47 | 48 | public function getRealPath(): string 49 | { 50 | return realpath($this->path); 51 | } 52 | 53 | public function numberOfLines(): int 54 | { 55 | $this->file()->seek(PHP_INT_MAX); 56 | 57 | return $this->file()->key(); 58 | } 59 | 60 | public function getLine(?int $lineNumber = null): string 61 | { 62 | if ($lineNumber === null) { 63 | return $this->getNextLine(); 64 | } 65 | 66 | $this->file()->seek($lineNumber - 1); 67 | 68 | return $this->file()->current(); 69 | } 70 | 71 | public function getNextLine(): string 72 | { 73 | $this->file()->next(); 74 | 75 | return $this->file()->current(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code-snippets 2 | 3 |

4 | Package Version 5 | license 6 | Test Run Status 7 | code coverage 8 |

9 | 10 | Easily create and work with code snippets from source code files of any type in PHP. 11 | 12 | _The original code this package is based on was borrowed from the [`spatie/backtrace`](https://github.com/spatie/backtrace) package._ 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require permafrost-dev/code-snippets 20 | ``` 21 | 22 | ## Usage 23 | 24 | _Note: Although the examples here reference php files, any file type can be used when creating a `CodeSnippet`._ 25 | 26 | ### Creating a snippet 27 | 28 | Use the `surroundingLine($num)` method to select the "target" line, which will be returned as the middle line of the snippet: 29 | 30 | ```php 31 | use Permafrost\CodeSnippets\CodeSnippet; 32 | 33 | $snippet = (new CodeSnippet()) 34 | ->surroundingLine(4) 35 | ->snippetLineCount(6) 36 | ->fromFile('/path/to/a/file.php'); 37 | ``` 38 | 39 | Use the `surroundingLines($first, $last)` method to select a range of "target" lines, which will be returned as the middle lines of the snippet: 40 | 41 | ```php 42 | $snippet = (new CodeSnippet()) 43 | ->surroundingLines(4, 7) 44 | ->snippetLineCount(6) 45 | ->fromFile('/path/to/a/file.php'); 46 | ``` 47 | 48 | Use the `linesBefore()` and `linesAfter()` methods to specify the number of context lines to display before and after the "target" lines: 49 | 50 | ```php 51 | // the "target" line isn't displayed in the middle, but as the second line 52 | $snippet = (new CodeSnippet()) 53 | ->surroundingLine(4) 54 | ->linesBefore(1) 55 | ->linesAfter(3) 56 | ->fromFile('/path/to/a/file.php'); 57 | ``` 58 | 59 | ### Getting the snippet contents 60 | 61 | The `getLines()` method returns an array of `SnippetLine` instances. The keys of the resulting array are the line numbers. 62 | 63 | The `SnippetLine` instances may be cast to strings to display the value. When working with `SnippetLine` instances, use `isSelected()` to determine if the line was selected using either the `surroundingLine()` or `surroundingLines()` method on the `CodeSnippet` instance. 64 | 65 | To get the value of a `SnippetLine`, use the `value()` method or cast the object to a string. 66 | 67 | ```php 68 | $snippet = (new CodeSnippet()) 69 | ->surroundingLine(4) 70 | ->snippetLineCount(5) 71 | ->fromFile('/path/to/a/file.php'); 72 | 73 | foreach($snippet->getLines() as $lineNum => $line) { 74 | $prefix = $line->isSelected() ? ' * ' : ' '; 75 | 76 | echo $prefix . $line->lineNumber() . ' - ' . $line->value() . PHP_EOL; 77 | } 78 | ``` 79 | 80 | ### Snippet line count 81 | 82 | To determine the number of lines in the snippet, use the `getSnippetLineCount()` method: 83 | 84 | ```php 85 | $snippet = (new CodeSnippet()) 86 | ->surroundingLines(4, 7) 87 | ->linesBefore(3) 88 | ->linesAfter(3) 89 | ->fromFile('/path/to/a/file.php'); 90 | 91 | echo "Snippet line count: " . $snippet->getSnippetLineCount() . PHP_EOL; 92 | ``` 93 | 94 | You can also use `count()` on the result of the `getLines()` method: 95 | 96 | ```php 97 | $snippet = (new CodeSnippet()) 98 | ->surroundingLines(4, 7) 99 | ->linesBefore(3) 100 | ->linesAfter(3) 101 | ->fromFile('/path/to/a/file.php'); 102 | 103 | echo "Snippet line count: " . count($snippet->getLines()) . PHP_EOL; 104 | ``` 105 | 106 | To return an array containing the line numbers for the snippet, use `getLineNumbers()`: 107 | 108 | ```php 109 | print_r($snippet->getLineNumbers()); 110 | ``` 111 | 112 | ### Returning the snippet as a string 113 | 114 | Return the contents of the snippet as as string using the `toString()` method or by casting the snippet to a string directly: 115 | 116 | ```php 117 | $snippet = (new CodeSnippet()) 118 | ->surroundingLines(4, 7) 119 | ->linesBefore(3) 120 | ->linesAfter(3) 121 | ->fromFile('/path/to/a/file.php'); 122 | 123 | echo "Snippet: \n" . $snippet->toString() . PHP_EOL; 124 | ``` 125 | 126 | ## Testing 127 | 128 | ```bash 129 | ./vendor/bin/phpunit 130 | ``` 131 | 132 | ## Changelog 133 | 134 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 135 | 136 | ## Contributing 137 | 138 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 139 | 140 | ## Security Vulnerabilities 141 | 142 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 143 | 144 | ## Credits 145 | 146 | - [Patrick Organ](https://github.com/patinthehat) 147 | - [All Contributors](../../contributors) 148 | 149 | ## License 150 | 151 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 152 | -------------------------------------------------------------------------------- /src/CodeSnippet.php: -------------------------------------------------------------------------------- 1 | surroundingLines = [$surroundingLine]; 32 | 33 | return $this; 34 | } 35 | 36 | public function surroundingLines(int $surroundingLineFirst, int $surroundingLineLast): self 37 | { 38 | $this->surroundingLines = range($surroundingLineFirst, $surroundingLineLast); 39 | 40 | return $this; 41 | } 42 | 43 | public function snippetLineCount(int $snippetLineCount): self 44 | { 45 | $this->snippetLineCount = $snippetLineCount; 46 | 47 | return $this; 48 | } 49 | 50 | public function linesBefore(int $linesBefore): self 51 | { 52 | $this->linesBefore = $linesBefore; 53 | 54 | if (is_int($this->linesAfter) && is_int($this->linesBefore)) { 55 | $range = range($this->surroundingLines[0], $this->surroundingLines[count($this->surroundingLines[0]) - 1]); 56 | 57 | $this->snippetLineCount = ($this->linesAfter + $this->linesBefore) + count($range); 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | public function linesAfter(int $linesAfter): self 64 | { 65 | $this->linesAfter = $linesAfter; 66 | 67 | if (is_int($this->linesAfter) && is_int($this->linesBefore)) { 68 | $range = range($this->surroundingLines[0], $this->surroundingLines[count($this->surroundingLines) - 1]); 69 | 70 | $this->snippetLineCount = ($this->linesAfter + $this->linesBefore) + count($range); 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | public function getSnippetLineCount(): int 77 | { 78 | return $this->snippetLineCount; 79 | } 80 | 81 | /** 82 | * @param File|string $file 83 | * @return static 84 | */ 85 | public function fromFile($file): self 86 | { 87 | if (is_string($file)) { 88 | $file = new File($file); 89 | } 90 | 91 | if (! $file instanceof File) { 92 | $this->code = []; 93 | 94 | return $this; 95 | } 96 | 97 | if (! $file->exists()) { 98 | $this->code = []; 99 | 100 | return $this; 101 | } 102 | 103 | try { 104 | $code = []; 105 | $bounds = $this->getBoundsMulti($file->numberOfLines()); 106 | $line = $file->getLine($bounds->start); 107 | $currentLineNumber = $bounds->start; 108 | 109 | while ($currentLineNumber <= $bounds->end) { 110 | $value = rtrim(substr($line, 0, 250)); 111 | $isSelected = $this->isSurroundedLineNumber($currentLineNumber); 112 | 113 | $code[$currentLineNumber] = SnippetLine::create($currentLineNumber, $value, $isSelected); 114 | 115 | $line = $file->getNextLine(); 116 | $currentLineNumber++; 117 | } 118 | 119 | $this->code = $code; 120 | } catch (RuntimeException $exception) { 121 | $this->code = []; 122 | } 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * @return array|SnippetLine[] 129 | */ 130 | public function getLines(): array 131 | { 132 | return $this->code; 133 | } 134 | 135 | public function getLineNumberStart(): int 136 | { 137 | return $this->surroundingLines[0] ?? 0; 138 | } 139 | 140 | public function getLineNumberEnd(): int 141 | { 142 | return $this->surroundingLines[count($this->surroundingLines) - 1] ?? 0; 143 | } 144 | 145 | public function getLineNumbers(): array 146 | { 147 | return array_keys($this->getLines()); 148 | } 149 | 150 | public function getSelectedBounds(): Bounds 151 | { 152 | return Bounds::create($this->getLineNumberStart(), $this->getLineNumberEnd()); 153 | } 154 | 155 | public function toString() 156 | { 157 | $result = ''; 158 | 159 | foreach ($this->getLines() as $line) { 160 | $result .= $line->value() . PHP_EOL; 161 | } 162 | 163 | return $result; 164 | } 165 | 166 | public function __toString() 167 | { 168 | return $this->toString(); 169 | } 170 | 171 | protected function isSurroundedLineNumber(int $lineNumber): bool 172 | { 173 | return in_array($lineNumber, $this->surroundingLines, true); 174 | } 175 | 176 | protected function getBounds(int $surroundingLine, int $totalNumberOfLineInFile): Bounds 177 | { 178 | $startLine = max($surroundingLine - floor($this->snippetLineCount / 2), 1); 179 | 180 | $endLine = $startLine + ($this->snippetLineCount - 1); 181 | 182 | if ($endLine > $totalNumberOfLineInFile) { 183 | $endLine = $totalNumberOfLineInFile; 184 | $startLine = max($endLine - ($this->snippetLineCount - 1), 1); 185 | } 186 | 187 | return Bounds::createFromArray([$startLine, $endLine]); 188 | } 189 | 190 | protected function getBoundsMulti(int $totalNumberOfLineInFile): Bounds 191 | { 192 | $bounds = Bounds::createFromArray($this->surroundingLines); 193 | 194 | // snippetLineCount() was used 195 | if (! is_int($this->linesAfter) || ! is_int($this->linesBefore)) { 196 | $this->getBoundsMultiForSnippetLineCount($bounds, $totalNumberOfLineInFile); 197 | } 198 | 199 | // linesBefore() and linesAfter() were used 200 | if (is_int($this->linesAfter) && is_int($this->linesBefore)) { 201 | $bounds->start -= $this->linesBefore; 202 | $bounds->end += $this->linesAfter; 203 | 204 | $this->updateSnippetLineCount($bounds); 205 | } 206 | 207 | $this->ensureBoundsAreWithinLimits($bounds, $totalNumberOfLineInFile); 208 | $this->trimSnippetSize($bounds); 209 | $this->updateSnippetLineCount($bounds); 210 | 211 | // $surroundedBounds = Bounds::createFromArray($this->surroundingLines); 212 | // 213 | // if ($surroundedBounds->end > $bounds->end) { 214 | // $bounds->end = $surroundedBounds->end + 1; 215 | // } 216 | // 217 | // $this->ensureBoundsAreWithinLimits($bounds, $totalNumberOfLineInFile); 218 | 219 | return $bounds; 220 | } 221 | 222 | protected function getBoundsMultiForSnippetLineCount(Bounds $bounds, int $totalNumberOfLineInFile): void 223 | { 224 | $startBounds = $this->getBounds($bounds->start, $totalNumberOfLineInFile); 225 | $endBounds = $this->getBounds($bounds->end, $totalNumberOfLineInFile); 226 | 227 | $bounds->copy($startBounds->mergeWith($endBounds)); 228 | } 229 | 230 | protected function updateSnippetLineCount(Bounds $bounds): void 231 | { 232 | $this->snippetLineCount = ($bounds->end - $bounds->start) + 1; 233 | } 234 | 235 | protected function trimSnippetSize(Bounds $bounds): void 236 | { 237 | if (count(range($bounds->start, $bounds->end)) > $this->snippetLineCount) { 238 | if (! in_array($bounds->end, $this->surroundingLines, true)) { 239 | $bounds->end--; 240 | } 241 | } 242 | 243 | if (count(range($bounds->start, $bounds->end)) > $this->snippetLineCount) { 244 | if (! in_array($bounds->start, $this->surroundingLines, true)) { 245 | $bounds->start++; 246 | } 247 | } 248 | } 249 | 250 | protected function ensureBoundsAreWithinLimits(Bounds $bounds, int $totalNumberOfLineInFile): void 251 | { 252 | if ($bounds->start <= 0) { 253 | $bounds->start = 1; 254 | } 255 | 256 | if ($bounds->end > $totalNumberOfLineInFile) { 257 | $bounds->end = $totalNumberOfLineInFile; 258 | 259 | if (count($this->surroundingLines) === 1) { 260 | $bounds->start = max($bounds->end - ($this->snippetLineCount - 1), 1); 261 | } 262 | } 263 | } 264 | } 265 | --------------------------------------------------------------------------------