├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── torchlight.php ├── phpunit.xml.dist ├── src ├── Blade │ ├── BladeManager.php │ ├── CodeComponent.php │ └── EngineDecorator.php ├── Block.php ├── Client.php ├── Commands │ └── Install.php ├── Contracts │ └── PostProcessor.php ├── Exceptions │ ├── ConfigurationException.php │ ├── RequestException.php │ └── TorchlightException.php ├── Manager.php ├── Middleware │ └── RenderTorchlight.php ├── PostProcessors │ └── SimpleSwapProcessor.php ├── Torchlight.php └── TorchlightServiceProvider.php └── tests ├── BaseTestCase.php ├── BlockTest.php ├── ClientTest.php ├── ClientTimeoutTest.php ├── CustomizationTest.php ├── DualThemeTest.php ├── FindIdsTest.php ├── LivewireTest.php ├── MiddlewareAndComponentTest.php ├── PostProcessorTest.php ├── RealClientTest.php └── Support ├── an-inline-component-with-post-processors.blade.php ├── an-inline-component-with-swaps.blade.php ├── an-inline-component.blade.php ├── contents-via-file-2.blade.php ├── contents-via-file.blade.php ├── dedent_works_properly.blade.php ├── file-must-be-passed-through-contents.blade.php ├── simple-js-hello-world.blade.php ├── simple-php-hello-world-new-theme.blade.php ├── simple-php-hello-world-with-attributes.blade.php ├── simple-php-hello-world-with-classes.blade.php ├── simple-php-hello-world-with-style.blade.php ├── simple-php-hello-world.blade.php ├── two-codes-in-one-tag.blade.php └── two-simple-php-hello-world.blade.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | php: ['8.0', '8.1', '8.2', '8.3'] 21 | laravel: ['8.*', '9.*', '10.*', '11.*', '12.*'] 22 | dependency-version: [prefer-lowest, prefer-stable] 23 | include: 24 | - laravel: 8.* 25 | testbench: 6.* 26 | - laravel: 9.* 27 | testbench: 7.* 28 | - laravel: 10.* 29 | testbench: 8.* 30 | - laravel: 11.* 31 | testbench: 9.* 32 | - laravel: 12.* 33 | testbench: 10.* 34 | exclude: 35 | - laravel: 8.* 36 | php: 8.1 37 | dependency-version: prefer-lowest 38 | - laravel: 8.* 39 | php: 8.2 40 | dependency-version: prefer-lowest 41 | - laravel: 8.* 42 | php: 8.3 43 | dependency-version: prefer-lowest 44 | - laravel: 9.* 45 | php: 8.2 46 | dependency-version: prefer-lowest 47 | - laravel: 9.* 48 | php: 8.3 49 | dependency-version: prefer-lowest 50 | - laravel: 10.* 51 | php: 8.0 52 | - laravel: 11.* 53 | php: 8.0 54 | - laravel: 11.* 55 | php: 8.1 56 | - laravel: 12.* 57 | php: '8.0' 58 | - laravel: 12.* 59 | php: '8.1' 60 | 61 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 62 | 63 | steps: 64 | - name: Checkout code 65 | uses: actions/checkout@v4 66 | 67 | - name: Cache dependencies 68 | uses: actions/cache@v4 69 | with: 70 | path: ~/.composer/cache/files 71 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 72 | 73 | - name: Setup PHP 74 | uses: shivammathur/setup-php@v2 75 | with: 76 | php-version: ${{ matrix.php }} 77 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 78 | coverage: none 79 | 80 | - name: Install dependencies 81 | run: | 82 | composer config minimum-stability dev 83 | composer self-update 84 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 85 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 86 | 87 | - name: Execute tests 88 | run: vendor/bin/phpunit 89 | 90 | - name: Install Livewire V2 (Below Laravel 11.0) 91 | if: "! startsWith(matrix.laravel, '11.') && ! startsWith(matrix.laravel, '12.')" 92 | run: | 93 | composer require "livewire/livewire:^2.3.10" -W --${{ matrix.dependency-version }} --no-interaction 94 | 95 | - name: Test with Livewire V2 96 | if: "! startsWith(matrix.laravel, '11.') && ! startsWith(matrix.laravel, '12.')" 97 | run: vendor/bin/phpunit 98 | 99 | - name: Install Livewire V3 (Above Laravel 9.0) 100 | if: "! startsWith(matrix.laravel, '8.') && ! startsWith(matrix.laravel, '9.')" 101 | run: | 102 | composer require "livewire/livewire:^3" -W --${{ matrix.dependency-version }} --no-interaction 103 | 104 | - name: Test with Livewire V3 105 | if: "! startsWith(matrix.laravel, '8.') && ! startsWith(matrix.laravel, '9.')" 106 | run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .env 3 | .phpunit.result.cache 4 | composer.lock -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - concat_without_spaces 5 | - not_operator_with_successor_space 6 | - cast_spaces 7 | - trailing_comma_in_multiline_array 8 | - heredoc_to_nowdoc 9 | - phpdoc_summary 10 | 11 | risky: false -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 0.5.11 - 2022-02-13 6 | 7 | ### Added 8 | - Support for Laravel 9 [#29](https://github.com/torchlight-api/torchlight-laravel/pull/29) 9 | - Better support for PHP 8.1 [#30](https://github.com/torchlight-api/torchlight-laravel/pull/30) 10 | 11 | ## 0.5.10 - 2022-02-01 12 | 13 | ### Added 14 | - Added the ability to define multiple themes for e.g. dark mode. 15 | - Cache time is now configurable. 16 | 17 | ## 0.5.9 - 2022-01-19 18 | 19 | ### Fixed 20 | 21 | - Fix cosmetic trailing space issue 22 | 23 | ## 0.5.8 - 2022-01-19 24 | 25 | ### Added 26 | 27 | - Attributes from the API will now be passed on to the code component. (The API now returns 'data-lang' as an attribute.) 28 | 29 | ## 0.5.7 - 2021-11-02 30 | 31 | ### Added 32 | 33 | - `Block` is now Macroable 34 | 35 | ## 0.5.6 - 2021-11-01 36 | 37 | ### Added 38 | 39 | - Added the ability to run post-processors _per block_ rather than globally. ([#20](https://github.com/torchlight-api/torchlight-laravel/pull/20)) 40 | 41 | ## 0.5.5 - 2021-09-06 42 | 43 | ### Changed 44 | - Changed the signature of the file processor. 45 | 46 | ## 0.5.4 - 2021-09-06 47 | 48 | ### Added 49 | - Added the ability to configure the directories where Torchlight looks for snippets. 50 | 51 | ## 0.5.3 - 2021-08-14 52 | 53 | ### Changed 54 | - Post-processors don't run if Laravel is compiling views. 55 | 56 | ### Added 57 | - You can set `tab_width` to `false` to output literal tabs into the rendered HTML. 58 | 59 | ### Fixed 60 | - Livewire middleware won't be registered for V1 of Livewire, since it's not possible. 61 | 62 | ## 0.5.2 - 2021-08-02 63 | 64 | ### Fixed 65 | - Replace tabs with spaces in code before it's sent to the API. 66 | 67 | ## 0.5.1 - 2021-08-01 68 | 69 | ### Added 70 | - Added support for Laravel Livewire ([#10](https://github.com/torchlight-api/torchlight-laravel/pull/10)) 71 | - Added post-processors to allow your app to hook into the rendered response before it's sent to the browser. 72 | 73 | ## 0.5.0 - 2021-07-31 74 | 75 | ### Changed 76 | - Changed the signature for the Manager class. Remove the requirement for the container to be passed in. 77 | 78 | ## 0.4.6 - 2021-07-28 79 | 80 | ### Added 81 | - Added the ability to send `options` from the config file to the API. 82 | 83 | ## 0.4.5 - 2021-07-18 84 | 85 | ### Changed 86 | - The default response (used if a request fails) now includes the `
` wrappers. 87 | 88 | ## 0.4.4 - 2021-06-16 89 | 90 | ### Fixed 91 | - Catch `ConnectionException`s in addition to exceptions from the Torchlight API. 92 | 93 | ## 0.4.3 - 2021-05-25 94 | 95 | ### Added 96 | - `getConfigUsing` now accepts a plain array in addition to a callback. 97 | 98 | ## 0.4.2 - 2021-05-24 99 | 100 | ### Fixed 101 | - Cover a bug in Laravel pre 8.23.0. 102 | 103 | ## 0.4.1 - 2021-05-23 104 | 105 | ### Added 106 | - Ability to override the environment. 107 | 108 | ## 0.4.0 - 2021-05-23 109 | 110 | ### Added 111 | - `Torchlight::findTorchlightIds` method to search a string and return all Torchlight placeholders. 112 | - `BladeManager::getBlocks` 113 | - `BladeManager::clearBlocks` 114 | 115 | ### Changed 116 | - Added square brackets around the Torchlight ID in the Block placeholder. 117 | - The BladeManager no longer clears the blocks while rendering. Needed for Jigsaw. 118 | 119 | ## 0.3.0 - 2021-05-22 120 | 121 | - Add `Torchlight` facade 122 | - Add ability to set the cache implementation. 123 | - Add ability to abstract config from Laravel's `config` helper. 124 | - Changed package name from `torchlight/laravel` to `torchlight/torchlight-laravel` 125 | 126 | 127 | ## 0.2.1 - 2021-05-20 128 | 129 | - Add `Block::generateIdsUsing` 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hammerstone 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Torchlight Client 2 | 3 | [![Tests](https://github.com/torchlight-api/torchlight-laravel/actions/workflows/tests.yml/badge.svg)](https://github.com/torchlight-api/torchlight-laravel/actions/workflows/tests.yml) [![Latest Stable Version](https://poser.pugx.org/torchlight/torchlight-laravel/v)](//packagist.org/packages/torchlight/torchlight-laravel) [![Total Downloads](https://poser.pugx.org/torchlight/torchlight-laravel/downloads)](//packagist.org/packages/torchlight/torchlight-laravel) [![License](https://poser.pugx.org/torchlight/torchlight-laravel/license)](//packagist.org/packages/torchlight/torchlight-laravel) 4 | 5 | 6 | A [Torchlight](https://torchlight.dev) syntax highlighting extension for the [Laravel](https://laravel.com/) framework. 7 | 8 | Torchlight is a VS Code-compatible syntax highlighter that requires no JavaScript, supports every language, every VS Code theme, line highlighting, git diffing, and more. 9 | 10 | ## Installation 11 | 12 | To install, require the package from composer: 13 | 14 | ``` 15 | composer require torchlight/torchlight-laravel 16 | ``` 17 | 18 | ## Configuration 19 | 20 | Once the package is downloaded, you can run the following command to publish your configuration file: 21 | 22 | ``` 23 | php artisan torchlight:install 24 | ``` 25 | 26 | Once run, you should see a new file `torchlight.php` in you `config` folder, with contents that look like this: 27 | 28 | ```php 29 | env('TORCHLIGHT_CACHE_DRIVER'), 34 | 35 | // Which theme you want to use. You can find all of the themes at 36 | // https://torchlight.dev/themes, or you can provide your own. 37 | 'theme' => env('TORCHLIGHT_THEME', 'material-theme-palenight'), 38 | 39 | // Your API token from torchlight.dev. 40 | 'token' => env('TORCHLIGHT_TOKEN'), 41 | 42 | // If you want to register the blade directives, set this to true. 43 | 'blade_components' => true, 44 | 45 | // The Host of the API. 46 | 'host' => env('TORCHLIGHT_HOST', 'https://api.torchlight.dev'), 47 | ]; 48 | 49 | ``` 50 | ### Cache 51 | 52 | Set the cache driver that Torchlight will use. 53 | 54 | ### Theme 55 | 56 | You can change the theme of all your code blocks by adjusting the `theme` key in your configuration. 57 | 58 | ### Token 59 | 60 | This is your API token from [torchlight.dev](https://torchlight.dev). (Torchlight is completely free for personal and open source projects.) 61 | 62 | ### Blade Components 63 | 64 | By default Torchlight works by using a [custom Laravel component](https://laravel.com/docs/master/blade#components). If you'd like to disable the registration of the component for whatever reason, you can turn this to false. 65 | 66 | ### Host 67 | 68 | You can change the host where your API requests are sent. Not sure why you'd ever want to do that, but you can! -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torchlight/torchlight-laravel", 3 | "description": "A Laravel Client for Torchlight, the syntax highlighting API.", 4 | "homepage": "https://torchlight.dev", 5 | "type": "library", 6 | "license": "MIT", 7 | "keywords": [ 8 | "syntax highlighting", 9 | "code highlighting", 10 | "laravel" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Aaron Francis", 15 | "email": "aaron@hammerstone.dev" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.3|^8.0", 20 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", 21 | "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0", 22 | "illuminate/http": "^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "illuminate/cache": "^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "guzzlehttp/guzzle": "^7.2", 26 | "ramsey/uuid": "^3.7|^4.0" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "^5.0|^6.0|^7.0|^9.0|^10.0", 30 | "mockery/mockery": "^1.3.3", 31 | "phpunit/phpunit": "^8.5.23|^9.5|^10.5|^11.5.3" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Torchlight\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Torchlight\\Tests\\": "tests/" 41 | } 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Torchlight\\TorchlightServiceProvider" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/torchlight.php: -------------------------------------------------------------------------------- 1 | env('TORCHLIGHT_CACHE_DRIVER'), 8 | 9 | // Cache blocks for 30 days. Set null to store permanently 10 | 'cache_seconds' => env('TORCHLIGHT_CACHE_TTL', 60 * 60 * 24 * 30), 11 | 12 | // Which theme you want to use. You can find all of the themes at 13 | // https://torchlight.dev/docs/themes. 14 | 'theme' => env('TORCHLIGHT_THEME', 'material-theme-palenight'), 15 | 16 | // If you want to use two separate themes for dark and light modes, 17 | // you can use an array to define both themes. Torchlight renders 18 | // both on the page, and you will be responsible for hiding one 19 | // or the other depending on the dark / light mode via CSS. 20 | // 'theme' => [ 21 | // 'dark' => 'github-dark', 22 | // 'light' => 'github-light', 23 | // ], 24 | 25 | // Your API token from torchlight.dev. 26 | 'token' => env('TORCHLIGHT_TOKEN'), 27 | 28 | // If you want to register the blade directives, set this to true. 29 | 'blade_components' => true, 30 | 31 | // The Host of the API. 32 | 'host' => env('TORCHLIGHT_HOST', 'https://api.torchlight.dev'), 33 | 34 | // We replace tabs in your code blocks with spaces in HTML. Set 35 | // the number of spaces you'd like to use per tab. Set to 36 | // `false` to leave literal tabs in the HTML. 37 | 'tab_width' => 4, 38 | 39 | // If you pass a filename to the code component or in a markdown 40 | // block, Torchlight will look for code snippets in the 41 | // following directories. 42 | 'snippet_directories' => [ 43 | resource_path() 44 | ], 45 | 46 | // Global options to control blocks-level settings. 47 | // https://torchlight.dev/docs/options 48 | 'options' => [ 49 | // Turn line numbers on or off globally. 50 | // 'lineNumbers' => false, 51 | 52 | // Control the `style` attribute applied to line numbers. 53 | // 'lineNumbersStyle' => '', 54 | 55 | // Turn on +/- diff indicators. 56 | // 'diffIndicators' => true, 57 | 58 | // If there are any diff indicators for a line, put them 59 | // in place of the line number to save horizontal space. 60 | // 'diffIndicatorsInPlaceOfLineNumbers' => true, 61 | 62 | // When lines are collapsed, this is the text that will 63 | // be shown to indicate that they can be expanded. 64 | // 'summaryCollapsedIndicator' => '...', 65 | ] 66 | ]; 67 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Blade/BladeManager.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Blade; 7 | 8 | use Illuminate\Http\Response; 9 | use Illuminate\Support\Arr; 10 | use Illuminate\Support\Str; 11 | use Torchlight\Block; 12 | use Torchlight\Torchlight; 13 | 14 | class BladeManager 15 | { 16 | /** 17 | * Laravel before 8.23.0 has a bug that adds extra spaces around components. 18 | * Obviously this is a problem if your component is wrapped in

 19 |      * tags, which ours usually is.
 20 |      *
 21 |      * @see https://github.com/laravel/framework/blob/8.x/CHANGELOG-8.x.md#v8230-2021-01-19.
 22 |      *
 23 |      * @var bool
 24 |      */
 25 |     public static $affectedBySpacingBug = false;
 26 | 
 27 |     /**
 28 |      * @var array
 29 |      */
 30 |     protected static $blocks = [];
 31 | 
 32 |     public static function registerBlock(Block $block)
 33 |     {
 34 |         static::$blocks[$block->id()] = $block;
 35 |     }
 36 | 
 37 |     public static function getBlocks()
 38 |     {
 39 |         return static::$blocks;
 40 |     }
 41 | 
 42 |     public static function clearBlocks()
 43 |     {
 44 |         static::$blocks = [];
 45 |     }
 46 | 
 47 |     public static function renderResponse(Response $response)
 48 |     {
 49 |         // Bail early if there are no blocks registered.
 50 |         if (!static::$blocks) {
 51 |             return $response;
 52 |         }
 53 | 
 54 |         return $response->setContent(
 55 |             static::renderContent($response->content())
 56 |         );
 57 |     }
 58 | 
 59 |     public static function renderContent($content)
 60 |     {
 61 |         // Bail early if there are no blocks registered.
 62 |         if (!static::$blocks) {
 63 |             return $content;
 64 |         }
 65 | 
 66 |         $response = Torchlight::highlight(static::$blocks);
 67 |         $response = collect($response)->keyBy->id();
 68 | 
 69 |         $ids = Torchlight::findTorchlightIds($content);
 70 | 
 71 |         // The first time through we have to expand all
 72 |         // the blocks to include the clones.
 73 |         foreach ($ids as $id) {
 74 |             // For each block, stash the unadulterated content so
 75 |             // we can duplicate it for clones if we need to.
 76 |             $begin = "";
 77 |             $end = "";
 78 |             $clean = Str::between($content, $begin, $end);
 79 | 
 80 |             $clones = '';
 81 | 
 82 |             if ($block = Arr::get($response, $id)) {
 83 |                 foreach ($block->clones() as $clone) {
 84 |                     // Swap the original ID with the cloned ID.
 85 |                     $clones .= str_replace(
 86 |                         "__torchlight-block-[$id]", "__torchlight-block-[{$clone->id()}]", $clean
 87 |                     );
 88 | 
 89 |                     // Since we've added a new ID to the template, we
 90 |                     // need to make sure we add it to the array of
 91 |                     // IDs that drives the str_replace below.
 92 |                     $ids[] = $clone->id();
 93 |                 }
 94 |             }
 95 | 
 96 |             // Get rid of the first comment no matter what.
 97 |             $content = str_replace($begin, '', $content);
 98 | 
 99 |             // Replace the second comment with the clones.
100 |             $content = str_replace($end, $clones, $content);
101 |         }
102 | 
103 |         $swap = [];
104 | 
105 |         // Second time through we'll populate the replacement array.
106 |         foreach ($ids as $id) {
107 |             /** @var Block $block */
108 |             if (!$block = Arr::get($response, $id)) {
109 |                 continue;
110 |             }
111 | 
112 |             // Swap out all the placeholders that we left.
113 |             $swap[$block->placeholder()] = $block->highlighted;
114 |             $swap[$block->placeholder('classes')] = $block->classes;
115 |             $swap[$block->placeholder('styles')] = $block->styles;
116 |             $swap[$block->placeholder('attrs')] = $block->attrsAsString();
117 |         }
118 | 
119 |         // If this version of Laravel is affected by the spacing bug, then
120 |         // we will swap out our placeholders with a preceding space, and
121 |         // a following space. This effectively fixes the bug.
122 |         if (static::$affectedBySpacingBug) {
123 |             $swap[' ##PRE_TL_COMPONENT##'] = '';
124 |             $swap['##POST_TL_COMPONENT## '] = '';
125 |         }
126 | 
127 |         // No matter what, always get rid of the placeholders.
128 |         $swap['##PRE_TL_COMPONENT##'] = '';
129 |         $swap['##POST_TL_COMPONENT##'] = '';
130 | 
131 |         return str_replace(array_keys($swap), array_values($swap), $content);
132 |     }
133 | }
134 | 


--------------------------------------------------------------------------------
/src/Blade/CodeComponent.php:
--------------------------------------------------------------------------------
  1 | 
  4 |  */
  5 | 
  6 | namespace Torchlight\Blade;
  7 | 
  8 | use Illuminate\Support\Arr;
  9 | use Illuminate\Support\Str;
 10 | use Illuminate\View\Component;
 11 | use Torchlight\Block;
 12 | use Torchlight\PostProcessors\SimpleSwapProcessor;
 13 | use Torchlight\Torchlight;
 14 | 
 15 | class CodeComponent extends Component
 16 | {
 17 |     public $language;
 18 | 
 19 |     public $theme;
 20 | 
 21 |     public $contents;
 22 | 
 23 |     public $block;
 24 | 
 25 |     protected $trimFixDelimiter = '##LARAVEL_TRIM_FIXER##';
 26 | 
 27 |     /**
 28 |      * Create a new component instance.
 29 |      *
 30 |      * @param  $language
 31 |      * @param  null  $theme
 32 |      * @param  null  $contents
 33 |      * @param  null  $torchlightId
 34 |      */
 35 |     public function __construct($language, $theme = null, $contents = null, $swap = null, $postProcessors = [], $torchlightId = null)
 36 |     {
 37 |         $this->language = $language;
 38 |         $this->theme = $theme;
 39 |         $this->contents = $contents;
 40 | 
 41 |         $this->block = Block::make($torchlightId)->language($this->language)->theme($this->theme);
 42 | 
 43 |         $postProcessors = Arr::wrap($postProcessors);
 44 | 
 45 |         if ($swap) {
 46 |             $postProcessors[] = SimpleSwapProcessor::make($swap);
 47 |         }
 48 | 
 49 |         foreach ($postProcessors as $processor) {
 50 |             $this->block->addPostProcessor($processor);
 51 |         }
 52 |     }
 53 | 
 54 |     public function withAttributes(array $attributes)
 55 |     {
 56 |         // By default Laravel trims slot content in the ManagesComponents
 57 |         // trait. The line that does the trimming looks like this:
 58 |         // `$defaultSlot = new HtmlString(trim(ob_get_clean()));`
 59 | 
 60 |         // The problem with this is that when you have a Blade Component
 61 |         // that is indented in this way:
 62 | 
 63 |         // 
 64 |         //    
 65 |         //        public function {
 66 |         //            // test
 67 |         //        }
 68 |         //    
 69 |         // 
70 | 71 | // Then Laravel will strip the leading whitespace off of the first 72 | // line, of content making it impossible for us to know how 73 | // much to dedent the rest of the code. 74 | 75 | // We're hijacking this `withAttributes` method because it is called 76 | // _after_ the buffer is opened but before the content. So we echo 77 | // out some nonsense which will prevent Laravel from trimming 78 | // the whitespace. We'll replace it later. We only do this 79 | // if it's not a file-based-contents component. 80 | if (is_null($this->contents)) { 81 | echo $this->trimFixDelimiter; 82 | } 83 | 84 | return parent::withAttributes($attributes); 85 | } 86 | 87 | public function capture($contents) 88 | { 89 | $contents = $contents ?: $this->contents; 90 | $contents = Torchlight::processFileContents($contents) ?: $contents; 91 | 92 | if (Str::startsWith($contents, $this->trimFixDelimiter)) { 93 | $contents = Str::replaceFirst($this->trimFixDelimiter, '', $contents); 94 | } 95 | 96 | BladeManager::registerBlock($this->block->code($contents)); 97 | } 98 | 99 | /** 100 | * Get the view / contents that represent the component. 101 | * 102 | * @return string 103 | */ 104 | public function render() 105 | { 106 | // Put all of the attributes on the code element, merging in our placeholder 107 | // classes and style string. Echo out the slot, but capture it using output 108 | // buffering. We then pass it through as the contents to highlight, leaving 109 | // the placeholder so we can replace it later with fully highlighted code. 110 | // We have to add the ##PRE## and ##POST## tags to cover a framework bug. 111 | // @see BladeManager::renderContent. 112 | return <<<'EOT' 113 | ##PRE_TL_COMPONENT##placeholder('attrs') }}{{ 114 | $attributes->except('style')->merge([ 115 | 'class' => $block->placeholder('classes'), 116 | 'style' => $attributes->get('style') . $block->placeholder('styles') 117 | ]) 118 | }}>{{ $slot }}{{ $block->placeholder() }}##POST_TL_COMPONENT## 119 | EOT; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Blade/EngineDecorator.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Blade; 7 | 8 | use Illuminate\Contracts\View\Engine; 9 | use Torchlight\Torchlight; 10 | 11 | class EngineDecorator implements Engine 12 | { 13 | public $decorated; 14 | 15 | public function __construct($resolved) 16 | { 17 | $this->decorated = $resolved; 18 | } 19 | 20 | public function __get($name) 21 | { 22 | return $this->decorated->{$name}; 23 | } 24 | 25 | public function __set($name, $value) 26 | { 27 | $this->decorated->{$name} = $value; 28 | } 29 | 30 | public function __call($name, $arguments) 31 | { 32 | return call_user_func_array([$this->decorated, $name], $arguments); 33 | } 34 | 35 | public function get($path, array $data = []) 36 | { 37 | Torchlight::currentlyCompilingViews(true); 38 | 39 | $result = $this->decorated->get($path, $data); 40 | 41 | Torchlight::currentlyCompilingViews(false); 42 | 43 | return $result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Block.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight; 7 | 8 | use Illuminate\Support\Arr; 9 | use Illuminate\Support\Str; 10 | use Illuminate\Support\Traits\Macroable; 11 | 12 | class Block 13 | { 14 | use Macroable; 15 | 16 | /** 17 | * @var null|callable 18 | */ 19 | public static $generateIdsUsing; 20 | 21 | /** 22 | * The language of the code that is being highlighted. 23 | * 24 | * @var string 25 | */ 26 | public $language; 27 | 28 | /** 29 | * The theme of the code. 30 | * 31 | * @var string 32 | */ 33 | public $theme; 34 | 35 | /** 36 | * The code itself. 37 | * 38 | * @var string 39 | */ 40 | public $code; 41 | 42 | /** 43 | * The post processors. 44 | * 45 | * @var array 46 | */ 47 | public $postProcessors = []; 48 | 49 | /** 50 | * The highlighted code, wrapped in pre+code tags. 51 | * 52 | * @var string 53 | */ 54 | public $wrapped; 55 | 56 | /** 57 | * The highlighted code, not wrapped. 58 | * 59 | * @var string 60 | */ 61 | public $highlighted; 62 | 63 | /** 64 | * Classes that should be applied to the code tag. 65 | * 66 | * @var string 67 | */ 68 | public $classes; 69 | 70 | /** 71 | * Styles that should be applied to the code tag. 72 | * 73 | * @var string 74 | */ 75 | public $styles; 76 | 77 | /** 78 | * Attributes to apply to the code tag. 79 | * 80 | * @var array 81 | */ 82 | public $attrs = []; 83 | 84 | /** 85 | * The unique ID for the block. 86 | * 87 | * @var string 88 | */ 89 | protected $id; 90 | 91 | /** 92 | * @var array 93 | */ 94 | protected $clones = []; 95 | 96 | /** 97 | * @param null|string $id 98 | * @return static 99 | */ 100 | public static function make($id = null) 101 | { 102 | return new static($id); 103 | } 104 | 105 | /** 106 | * @param null|string $id 107 | */ 108 | public function __construct($id = null) 109 | { 110 | // Generate a unique UUID. 111 | $this->id = $id ?? $this->generateId(); 112 | 113 | // Set a default theme. 114 | $this->theme(Torchlight::config('theme')); 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function id() 121 | { 122 | return $this->id; 123 | } 124 | 125 | /** 126 | * @return string 127 | */ 128 | protected function generateId() 129 | { 130 | $id = is_callable(static::$generateIdsUsing) ? call_user_func(static::$generateIdsUsing) : Str::uuid(); 131 | 132 | return (string)$id; 133 | } 134 | 135 | /** 136 | * @return string 137 | */ 138 | public function hash() 139 | { 140 | return md5( 141 | $this->language 142 | . $this->theme 143 | . $this->code 144 | . Torchlight::config('bust') 145 | . json_encode(Torchlight::config('options')) 146 | ); 147 | } 148 | 149 | /** 150 | * @return array 151 | */ 152 | public function clones() 153 | { 154 | return $this->clones; 155 | } 156 | 157 | /** 158 | * @param $num 159 | * @return $this 160 | */ 161 | public function cloned($num) 162 | { 163 | $this->id = Str::finish($this->id, "_clone_$num"); 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * @param string $extra 170 | * @return string 171 | */ 172 | public function placeholder($extra = '') 173 | { 174 | if ($extra) { 175 | $extra = "_$extra"; 176 | } 177 | 178 | return "__torchlight-block-[{$this->id()}]{$extra}__"; 179 | } 180 | 181 | /** 182 | * @param $language 183 | * @return $this 184 | */ 185 | public function language($language) 186 | { 187 | $this->language = $language; 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * @param $theme 194 | * @return $this 195 | */ 196 | public function theme($theme) 197 | { 198 | $theme = $this->normalizeArrayTheme($theme); 199 | 200 | if ($theme) { 201 | $this->theme = $theme; 202 | } 203 | 204 | return $this; 205 | } 206 | 207 | /** 208 | * @param $code 209 | * @return $this 210 | */ 211 | public function code($code) 212 | { 213 | $this->code = $this->clean($code); 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * @return string 220 | */ 221 | public function attrsAsString() 222 | { 223 | $attrs = []; 224 | 225 | foreach ($this->attrs as $key => $value) { 226 | $value = addslashes($value ?? ''); 227 | $attrs[] = "$key=\"$value\""; 228 | } 229 | 230 | if (count($attrs)) { 231 | return implode(' ', $attrs) . ' '; 232 | } 233 | } 234 | 235 | /** 236 | * @param $processor 237 | * @return $this 238 | */ 239 | public function addPostProcessor($processor) 240 | { 241 | if ($processor) { 242 | $this->postProcessors[] = Torchlight::validatedPostProcessor($processor); 243 | } 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * @param $wrapped 250 | * @return $this 251 | */ 252 | public function wrapped($wrapped) 253 | { 254 | $this->wrapped = $wrapped; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * @return Block[] 261 | */ 262 | public function spawnClones() 263 | { 264 | $this->clones = []; 265 | 266 | $themes = explode(',', $this->theme ?? ''); 267 | 268 | // Set the theme for the current block, so that we 269 | // don't break the reference to it. 270 | $this->theme(array_shift($themes)); 271 | 272 | // Then generate any clones for the remaining themes. 273 | $this->clones = collect($themes)->map(function ($theme, $num) { 274 | return (clone $this)->theme($theme)->cloned($num); 275 | })->toArray(); 276 | 277 | return $this->clones; 278 | } 279 | 280 | /** 281 | * @return array 282 | */ 283 | public function toRequestParams() 284 | { 285 | return [ 286 | 'id' => $this->id(), 287 | 'hash' => $this->hash(), 288 | 'language' => $this->language, 289 | 'theme' => $this->theme, 290 | 'code' => $this->code, 291 | ]; 292 | } 293 | 294 | /** 295 | * @param $theme 296 | * @return mixed|string 297 | */ 298 | protected function normalizeArrayTheme($theme) 299 | { 300 | if (!is_array($theme)) { 301 | return $theme; 302 | } 303 | 304 | if (Arr::isAssoc($theme)) { 305 | return collect($theme)->map(function ($name, $label) { 306 | return "$label:$name"; 307 | })->join(','); 308 | } 309 | 310 | return implode(',', $theme); 311 | } 312 | 313 | /** 314 | * @param $code 315 | * @return string 316 | */ 317 | protected function clean($code) 318 | { 319 | $code = rtrim($code); 320 | $code = $this->replaceTabs($code); 321 | 322 | return $this->dedent($code); 323 | } 324 | 325 | /** 326 | * @param $code 327 | * @return string 328 | */ 329 | protected function replaceTabs($code) 330 | { 331 | $multiplier = Torchlight::config('tab_width', 4); 332 | 333 | if ($multiplier === false) { 334 | return $code; 335 | } 336 | 337 | return str_replace("\t", str_repeat(' ', $multiplier), $code); 338 | } 339 | 340 | /** 341 | * @param $code 342 | * @return string 343 | */ 344 | protected function dedent($code) 345 | { 346 | $lines = explode("\n", $code); 347 | 348 | $dedent = collect($lines) 349 | ->map(function ($line) { 350 | if (!$line || $line === "\n") { 351 | return false; 352 | } 353 | 354 | // Figure out how many spaces are at the start of the line. 355 | return strlen($line) - strlen(ltrim($line, ' ')); 356 | }) 357 | ->reject(function ($count) { 358 | return $count === false; 359 | }) 360 | // Take the smallest number of left-spaces. We'll 361 | // dedent everything by that amount. 362 | ->min(); 363 | 364 | // Make the string out of the right number of spaces. 365 | $dedent = str_repeat(' ', $dedent); 366 | 367 | return collect($lines) 368 | ->map(function ($line) use ($dedent) { 369 | $line = rtrim($line); 370 | 371 | // Replace the first n-many spaces that 372 | // are common to every line. 373 | return Str::replaceFirst($dedent, '', $line); 374 | }) 375 | ->implode("\n"); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight; 7 | 8 | use Illuminate\Http\Client\ConnectionException; 9 | use Illuminate\Support\Arr; 10 | use Illuminate\Support\Collection; 11 | use Illuminate\Support\Facades\Http; 12 | use Throwable; 13 | use Torchlight\Exceptions\ConfigurationException; 14 | use Torchlight\Exceptions\RequestException; 15 | use Torchlight\Exceptions\TorchlightException; 16 | 17 | class Client 18 | { 19 | public function highlight($blocks) 20 | { 21 | $blocks = Arr::wrap($blocks); 22 | 23 | $blocks = $this->collectionOfBlocks($blocks)->values(); 24 | $blocks = $blocks->merge($blocks->map->spawnClones())->flatten(); 25 | $blocks = $blocks->keyBy->id(); 26 | 27 | // First set the html from the cache if it is already stored. 28 | $this->setBlocksFromCache($blocks); 29 | 30 | // Then reject all the blocks that already have the html, which 31 | // will leave us with only the blocks we need to request. 32 | $needed = $blocks->reject->wrapped; 33 | 34 | // If there are any blocks that don't have html yet, 35 | // we fire a request. 36 | if ($needed->count()) { 37 | // This method will set the html on the block objects, 38 | // so we don't do anything with the return value. 39 | $this->request($needed); 40 | } 41 | 42 | return $blocks->values()->toArray(); 43 | } 44 | 45 | protected function request(Collection $blocks) 46 | { 47 | try { 48 | $host = Torchlight::config('host', 'https://api.torchlight.dev'); 49 | $timeout = Torchlight::config('request_timeout', 5); 50 | 51 | $response = Http::baseUrl($host) 52 | ->timeout($timeout) 53 | ->withToken($this->getToken()) 54 | ->post('highlight', [ 55 | 'blocks' => $this->blocksAsRequestParam($blocks)->values()->toArray(), 56 | 'options' => $this->getOptions(), 57 | ]); 58 | 59 | if ($response->failed()) { 60 | $this->potentiallyThrowRequestException($response->toException()); 61 | $response = []; 62 | } else { 63 | $response = $response->json(); 64 | } 65 | } catch (Throwable $e) { 66 | $e instanceof ConnectionException 67 | ? $this->potentiallyThrowRequestException($e) 68 | : $this->throwUnlessProduction($e); 69 | 70 | $response = []; 71 | } 72 | 73 | $response = Arr::get($response, 'blocks', []); 74 | $response = collect($response)->keyBy('id'); 75 | 76 | $blocks->each(function (Block $block) use ($response) { 77 | $blockFromResponse = Arr::get($response, "{$block->id()}", $this->defaultResponse($block)); 78 | 79 | foreach ($this->applyDirectlyFromResponse() as $key) { 80 | if (Arr::has($blockFromResponse, $key)) { 81 | $block->{$key} = $blockFromResponse[$key]; 82 | } 83 | } 84 | }); 85 | 86 | // Only store the ones we got back from the API. 87 | $this->setCacheFromBlocks($blocks, $response->keys()); 88 | 89 | return $blocks; 90 | } 91 | 92 | protected function collectionOfBlocks($blocks) 93 | { 94 | return collect($blocks)->each(function ($block) { 95 | if (!$block instanceof Block) { 96 | throw new TorchlightException('Block not instance of ' . Block::class); 97 | } 98 | }); 99 | } 100 | 101 | protected function getToken() 102 | { 103 | $token = Torchlight::config('token'); 104 | 105 | if (!$token) { 106 | $this->throwUnlessProduction( 107 | new ConfigurationException('No Torchlight token configured.') 108 | ); 109 | } 110 | 111 | return $token; 112 | } 113 | 114 | protected function getOptions() 115 | { 116 | $options = Torchlight::config('options', []); 117 | 118 | if (!is_array($options)) { 119 | $options = []; 120 | } 121 | 122 | return $options; 123 | } 124 | 125 | protected function potentiallyThrowRequestException($exception) 126 | { 127 | if ($exception) { 128 | $wrapped = new RequestException('A Torchlight request exception has occurred.', 0, $exception); 129 | 130 | $this->throwUnlessProduction($wrapped); 131 | } 132 | } 133 | 134 | protected function throwUnlessProduction($exception) 135 | { 136 | throw_unless(Torchlight::environment() === 'production', $exception); 137 | } 138 | 139 | public function cachePrefix() 140 | { 141 | return 'torchlight::'; 142 | } 143 | 144 | public function cacheKey(Block $block) 145 | { 146 | return $this->cachePrefix() . 'block-' . $block->hash(); 147 | } 148 | 149 | protected function blocksAsRequestParam(Collection $blocks) 150 | { 151 | return $blocks->map(function (Block $block) { 152 | return $block->toRequestParams(); 153 | }); 154 | } 155 | 156 | protected function applyDirectlyFromResponse() 157 | { 158 | return ['wrapped', 'highlighted', 'styles', 'classes', 'attrs']; 159 | } 160 | 161 | protected function setCacheFromBlocks(Collection $blocks, Collection $ids) 162 | { 163 | $keys = $this->applyDirectlyFromResponse(); 164 | 165 | $blocks->only($ids)->each(function (Block $block) use ($keys) { 166 | $value = []; 167 | 168 | foreach ($keys as $key) { 169 | if ($block->{$key}) { 170 | $value[$key] = $block->{$key}; 171 | } 172 | } 173 | 174 | if (count($value)) { 175 | $seconds = Torchlight::config('cache_seconds', 7 * 24 * 60 * 60); 176 | 177 | if (is_null($seconds)) { 178 | Torchlight::cache()->forever($this->cacheKey($block), $value); 179 | } else { 180 | Torchlight::cache()->put($this->cacheKey($block), $value, (int)$seconds); 181 | } 182 | } 183 | }); 184 | } 185 | 186 | protected function setBlocksFromCache(Collection $blocks) 187 | { 188 | $keys = $this->applyDirectlyFromResponse(); 189 | 190 | $blocks->each(function (Block $block) use ($keys) { 191 | if (!$cached = Torchlight::cache()->get($this->cacheKey($block))) { 192 | return; 193 | } 194 | 195 | if (is_string($cached)) { 196 | return; 197 | } 198 | 199 | foreach ($keys as $key) { 200 | if (Arr::has($cached, $key)) { 201 | $block->{$key} = $cached[$key]; 202 | } 203 | } 204 | }); 205 | } 206 | 207 | /** 208 | * In the case where nothing returns from the API, we have to show _something_. 209 | * 210 | * @param Block $block 211 | * @return array 212 | */ 213 | protected function defaultResponse(Block $block) 214 | { 215 | $lines = array_map(function ($line) { 216 | return "
" . htmlentities($line) . '
'; 217 | }, explode("\n", $block->code)); 218 | 219 | $highlighted = implode('', $lines); 220 | 221 | return [ 222 | 'highlighted' => $highlighted, 223 | 'classes' => 'torchlight', 224 | 'styles' => '', 225 | 'attrs' => [ 226 | 'data-theme' => $block->theme, 227 | 'data-lang' => $block->language, 228 | ], 229 | 'wrapped' => "
{$highlighted}
", 230 | ]; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Commands/Install.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Commands; 7 | 8 | use Illuminate\Console\Command; 9 | use Illuminate\Support\Facades\Artisan; 10 | use Torchlight\TorchlightServiceProvider; 11 | 12 | class Install extends Command 13 | { 14 | /** 15 | * The name and signature of the console command. 16 | * 17 | * @var string 18 | */ 19 | protected $signature = 'torchlight:install'; 20 | 21 | /** 22 | * The console command description. 23 | * 24 | * @var string 25 | */ 26 | protected $description = 'Install the Torchlight config file into your app.'; 27 | 28 | public function __construct() 29 | { 30 | parent::__construct(); 31 | 32 | if (file_exists(config_path('torchlight.php'))) { 33 | $this->setHidden(true); 34 | } 35 | } 36 | 37 | /** 38 | * @throws Exception 39 | */ 40 | public function handle() 41 | { 42 | Artisan::call('vendor:publish', [ 43 | '--provider' => TorchlightServiceProvider::class 44 | ]); 45 | 46 | $this->info('Config file published!'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/PostProcessor.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Contracts; 7 | 8 | use Torchlight\Block; 9 | 10 | interface PostProcessor 11 | { 12 | public function process(Block $block); 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Exceptions; 7 | 8 | class ConfigurationException extends TorchlightException 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/Exceptions/RequestException.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Exceptions; 7 | 8 | class RequestException extends TorchlightException 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/Exceptions/TorchlightException.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Exceptions; 7 | 8 | class TorchlightException extends \Exception 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight; 7 | 8 | use Illuminate\Contracts\Cache\Repository; 9 | use Illuminate\Support\Arr; 10 | use Illuminate\Support\Facades\Cache; 11 | use Illuminate\Support\Str; 12 | use Illuminate\Support\Traits\Macroable; 13 | use Torchlight\Contracts\PostProcessor; 14 | use Torchlight\Exceptions\ConfigurationException; 15 | 16 | class Manager 17 | { 18 | use Macroable; 19 | 20 | /** 21 | * @var null|callable 22 | */ 23 | protected $getConfigUsing; 24 | 25 | /** 26 | * @var Repository 27 | */ 28 | protected $cache; 29 | 30 | /** 31 | * @var Client 32 | */ 33 | protected $client; 34 | 35 | /** 36 | * @var null|string 37 | */ 38 | protected $environment; 39 | 40 | /** 41 | * @var array 42 | */ 43 | protected $postProcessors = []; 44 | 45 | /** 46 | * @var bool 47 | */ 48 | protected $currentlyCompilingViews = false; 49 | 50 | /** 51 | * @param Client $client 52 | * @return Manager 53 | */ 54 | public function setClient(Client $client) 55 | { 56 | $this->client = $client; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @return Client 63 | */ 64 | public function client() 65 | { 66 | if (!$this->client) { 67 | $this->client = new Client; 68 | } 69 | 70 | return $this->client; 71 | } 72 | 73 | /** 74 | * @param $value 75 | */ 76 | public function currentlyCompilingViews($value) 77 | { 78 | $this->currentlyCompilingViews = $value; 79 | } 80 | 81 | /** 82 | * @param $blocks 83 | * @return mixed 84 | */ 85 | public function highlight($blocks) 86 | { 87 | $blocks = $this->client()->highlight($blocks); 88 | 89 | $this->postProcessBlocks($blocks); 90 | 91 | return $blocks; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function environment() 98 | { 99 | return $this->environment ?? app()->environment(); 100 | } 101 | 102 | /** 103 | * @param string|null $environment 104 | */ 105 | public function overrideEnvironment($environment = null) 106 | { 107 | $this->environment = $environment; 108 | } 109 | 110 | /** 111 | * @param array|string $classes 112 | */ 113 | public function addPostProcessors($classes) 114 | { 115 | $classes = Arr::wrap($classes); 116 | 117 | foreach ($classes as $class) { 118 | $this->postProcessors[] = $this->validatedPostProcessor($class); 119 | } 120 | } 121 | 122 | /** 123 | * @param $blocks 124 | */ 125 | public function postProcessBlocks($blocks) 126 | { 127 | // Global post-processors 128 | foreach ($this->postProcessors as $processor) { 129 | if ($this->shouldSkipProcessor($processor)) { 130 | continue; 131 | } 132 | 133 | foreach ($blocks as $block) { 134 | $processor->process($block); 135 | } 136 | } 137 | 138 | // Block specific post-processors 139 | foreach ($blocks as $block) { 140 | foreach ($block->postProcessors as $processor) { 141 | if ($this->shouldSkipProcessor($processor)) { 142 | continue; 143 | } 144 | 145 | $processor->process($block); 146 | } 147 | } 148 | } 149 | 150 | public function processFileContents($file) 151 | { 152 | if (Str::startsWith($file, '##LARAVEL_TRIM_FIXER##')) { 153 | return false; 154 | } 155 | 156 | $directories = $this->config('snippet_directories', []); 157 | 158 | // Add a blank path to account for absolute paths. 159 | array_unshift($directories, ''); 160 | 161 | foreach ($directories as $directory) { 162 | if (!empty($directory)) { 163 | $directory = Str::finish($directory, DIRECTORY_SEPARATOR); 164 | } 165 | 166 | if (is_file($directory . $file)) { 167 | return file_get_contents($directory . $file); 168 | } 169 | } 170 | 171 | return false; 172 | } 173 | 174 | /** 175 | * Get an item out of the config using dot notation. 176 | * 177 | * @param $key 178 | * @param null $default 179 | * @return mixed 180 | */ 181 | public function config($key, $default = null) 182 | { 183 | // Default to Laravel's config method. 184 | $method = $this->getConfigUsing ?? 'config'; 185 | 186 | // If we are using Laravel's config method, then we'll prepend 187 | // the key with `torchlight` if it isn't already there. 188 | if ($method === 'config') { 189 | $key = Str::start($key, 'torchlight.'); 190 | } 191 | 192 | return call_user_func($method, $key, $default); 193 | } 194 | 195 | /** 196 | * A callback function used to access configuration. By default this 197 | * is null, which will fall through to Laravel's `config` function. 198 | * 199 | * @param $callback 200 | */ 201 | public function getConfigUsing($callback) 202 | { 203 | if (is_array($callback)) { 204 | $callback = function ($key, $default) use ($callback) { 205 | return Arr::get($callback, $key, $default); 206 | }; 207 | } 208 | 209 | $this->getConfigUsing = $callback; 210 | } 211 | 212 | /** 213 | * Set the cache implementation directly instead of using a driver. 214 | * 215 | * @param Repository $cache 216 | */ 217 | public function setCacheInstance(Repository $cache) 218 | { 219 | $this->cache = $cache; 220 | } 221 | 222 | /** 223 | * The cache store to use. 224 | * 225 | * @return Repository 226 | */ 227 | public function cache() 228 | { 229 | if ($this->cache) { 230 | return $this->cache; 231 | } 232 | 233 | // If the developer has requested a particular store, we'll use it. 234 | // If the config value is null, the default cache will be used. 235 | return Cache::store($this->config('cache')); 236 | } 237 | 238 | /** 239 | * Return all the Torchlight IDs in a given string. 240 | * 241 | * @param string $content 242 | * @return array 243 | */ 244 | public function findTorchlightIds($content) 245 | { 246 | preg_match_all('/__torchlight-block-\[(.+?)\]/', $content, $matches); 247 | 248 | return array_values(array_unique(Arr::get($matches, 1, []))); 249 | } 250 | 251 | /** 252 | * @param $processor 253 | * @return PostProcessor 254 | * 255 | * @throws ConfigurationException 256 | */ 257 | public function validatedPostProcessor($processor) 258 | { 259 | if (is_string($processor)) { 260 | $processor = app($processor); 261 | } 262 | 263 | if (!in_array(PostProcessor::class, class_implements($processor))) { 264 | $class = get_class($processor); 265 | throw new ConfigurationException("Post-processor '$class' does not implement " . PostProcessor::class); 266 | } 267 | 268 | return $processor; 269 | } 270 | 271 | protected function shouldSkipProcessor($processor) 272 | { 273 | // By default we do _not_ run post-processors when Laravel is compiling 274 | // views, because it could lead to data leaks if a post-processor swaps 275 | // user data in. If the developer understands this, they can turn 276 | // `processEvenWhenCompiling` on and we'll happily run them. 277 | $processWhenCompiling = property_exists($processor, 'processEvenWhenCompiling') 278 | && $processor->processEvenWhenCompiling; 279 | 280 | return $this->currentlyCompilingViews && !$processWhenCompiling; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/Middleware/RenderTorchlight.php: -------------------------------------------------------------------------------- 1 | handleLivewireRequest($response); 27 | } 28 | 29 | // Must be a regular, HTML response. 30 | if (!$response instanceof Response || !Str::contains($response->headers->get('content-type'), 'html')) { 31 | return $response; 32 | } 33 | 34 | $response = BladeManager::renderResponse($response); 35 | 36 | // Clear blocks from memory to prevent memory leak when using Laravel Octane 37 | BladeManager::clearBlocks(); 38 | 39 | return $response; 40 | } 41 | 42 | protected function handleLivewireRequest(JsonResponse $response) 43 | { 44 | if (!BladeManager::getBlocks()) { 45 | return $response; 46 | } 47 | 48 | $data = $response->getData(); 49 | 50 | if (data_get($data, 'effects.html')) { 51 | // Livewire v2 52 | $html = BladeManager::renderContent(data_get($data, 'effects.html')); 53 | 54 | data_set($data, 'effects.html', $html); 55 | } else { 56 | // Livewire v3 57 | foreach (data_get($data, 'components.*.effects.html') as $componentIndex => $componentHtml) { 58 | $html = BladeManager::renderContent($componentHtml); 59 | data_set($data, "components.$componentIndex.effects.html", $html); 60 | } 61 | } 62 | 63 | return $response->setData($data); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/PostProcessors/SimpleSwapProcessor.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\PostProcessors; 7 | 8 | use Torchlight\Block; 9 | use Torchlight\Contracts\PostProcessor; 10 | 11 | class SimpleSwapProcessor implements PostProcessor 12 | { 13 | public $swap = []; 14 | 15 | public static function make($swap) 16 | { 17 | return new static($swap); 18 | } 19 | 20 | public function __construct($swap) 21 | { 22 | $this->swap = $swap; 23 | } 24 | 25 | public function process(Block $block) 26 | { 27 | $block->highlighted = str_replace(array_keys($this->swap), array_values($this->swap), $block->highlighted); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Torchlight.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight; 7 | 8 | use Illuminate\Support\Facades\Facade; 9 | 10 | class Torchlight extends Facade 11 | { 12 | /** 13 | * @return string 14 | * 15 | * @see Manager 16 | */ 17 | protected static function getFacadeAccessor() 18 | { 19 | return Manager::class; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/TorchlightServiceProvider.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight; 7 | 8 | use Illuminate\Support\ServiceProvider; 9 | use Torchlight\Blade\BladeManager; 10 | use Torchlight\Blade\CodeComponent; 11 | use Torchlight\Blade\EngineDecorator; 12 | use Torchlight\Commands\Install; 13 | use Torchlight\Middleware\RenderTorchlight; 14 | 15 | class TorchlightServiceProvider extends ServiceProvider 16 | { 17 | public function boot() 18 | { 19 | $this->bindManagerSingleton(); 20 | $this->registerCommands(); 21 | $this->publishConfig(); 22 | $this->registerBladeComponent(); 23 | $this->registerLivewire(); 24 | $this->decorateGrahamCampbellEngines(); 25 | } 26 | 27 | public function bindManagerSingleton() 28 | { 29 | $this->app->singleton(Manager::class, function () { 30 | return new Manager; 31 | }); 32 | } 33 | 34 | public function registerCommands() 35 | { 36 | if ($this->app->runningInConsole()) { 37 | $this->commands([ 38 | Install::class, 39 | ]); 40 | } 41 | } 42 | 43 | public function publishConfig() 44 | { 45 | $this->publishes([ 46 | __DIR__ . '/../config/torchlight.php' => config_path('torchlight.php') 47 | ], 'config'); 48 | } 49 | 50 | public function registerBladeComponent() 51 | { 52 | if (!Torchlight::config('torchlight.blade_components')) { 53 | return; 54 | } 55 | 56 | // Laravel before 8.23.0 has a bug that adds extra spaces around components. 57 | // Obviously this is a problem if your component is wrapped in

 58 |         // tags, which ours usually is.
 59 |         // See https://github.com/laravel/framework/blob/8.x/CHANGELOG-8.x.md#v8230-2021-01-19.
 60 |         BladeManager::$affectedBySpacingBug = version_compare(app()->version(), '8.23.0', '<');
 61 | 
 62 |         $this->loadViewComponentsAs('torchlight', [
 63 |             'code' => CodeComponent::class
 64 |         ]);
 65 |     }
 66 | 
 67 |     public function registerLivewire()
 68 |     {
 69 |         // Check for the Livewire Facade.
 70 |         if (!class_exists('\\Livewire\\Livewire')) {
 71 |             return;
 72 |         }
 73 | 
 74 |         // Livewire 1.x does not have the `addPersistentMiddleware` method.
 75 |         if (method_exists(\Livewire\LivewireManager::class, 'addPersistentMiddleware')) {
 76 |             \Livewire\Livewire::addPersistentMiddleware([
 77 |                 RenderTorchlight::class,
 78 |             ]);
 79 |         }
 80 |     }
 81 | 
 82 |     /**
 83 |      * Graham Campbell's Markdown package is a common (and excellent) package that many
 84 |      * Laravel developers use for markdown. It registers a few view engines so you can
 85 |      * just return e.g. `view("file.md")` and the markdown will get rendered to HTML.
 86 |      *
 87 |      * The markdown file will get parsed *once* and saved to the disk, which could lead
 88 |      * to data leaks if you're using a post processor that injects some sort of user
 89 |      * details. The first user that hits the page will have their information saved
 90 |      * into the compiled views.
 91 |      *
 92 |      * We decorate the engines that Graham uses so we can alert our post processors
 93 |      * not to run when the views are being compiled.
 94 |      */
 95 |     public function decorateGrahamCampbellEngines()
 96 |     {
 97 |         if (!class_exists('\\GrahamCampbell\\Markdown\\MarkdownServiceProvider')) {
 98 |             return;
 99 |         }
100 | 
101 |         // The engines won't be registered if this is false.
102 |         if (!$this->app->config->get('markdown.views')) {
103 |             return;
104 |         }
105 | 
106 |         // Decorate all the engines that Graham's package registers.
107 |         $this->decorateEngine('md');
108 |         $this->decorateEngine('phpmd');
109 |         $this->decorateEngine('blademd');
110 |     }
111 | 
112 |     /**
113 |      * Decorate a single view engine.
114 |      *
115 |      * @param  $name
116 |      */
117 |     protected function decorateEngine($name)
118 |     {
119 |         // No engine registered.
120 |         if (!$resolved = $this->app->view->getEngineResolver()->resolve($name)) {
121 |             return;
122 |         }
123 | 
124 |         // Wrap the existing engine in our decorator.
125 |         $this->app->view->getEngineResolver()->register($name, function () use ($resolved) {
126 |             return new EngineDecorator($resolved);
127 |         });
128 |     }
129 | 
130 |     public function register()
131 |     {
132 |         $this->mergeConfigFrom(__DIR__ . '/../config/torchlight.php', 'torchlight');
133 |     }
134 | }
135 | 


--------------------------------------------------------------------------------
/tests/BaseTestCase.php:
--------------------------------------------------------------------------------
  1 | 
  4 |  */
  5 | 
  6 | namespace Torchlight\Tests;
  7 | 
  8 | use GuzzleHttp\Exception\ConnectException;
  9 | use GuzzleHttp\Exception\TransferException;
 10 | use GuzzleHttp\Promise\FulfilledPromise;
 11 | use http\Client\Response;
 12 | use Illuminate\Http\Client\Request;
 13 | use Illuminate\Support\Arr;
 14 | use Illuminate\Support\Facades\Http;
 15 | use Livewire\LivewireServiceProvider;
 16 | use Orchestra\Testbench\TestCase;
 17 | use Torchlight\TorchlightServiceProvider;
 18 | 
 19 | abstract class BaseTestCase extends TestCase
 20 | {
 21 |     protected $apiFaked = false;
 22 | 
 23 |     protected $fakeResponseBlocks = [];
 24 | 
 25 |     protected function setUp(): void
 26 |     {
 27 |         parent::setUp();
 28 |     }
 29 | 
 30 |     protected function getPackageProviders($app)
 31 |     {
 32 |         $providers = [
 33 |             TorchlightServiceProvider::class,
 34 |         ];
 35 | 
 36 |         if (class_exists('\\Livewire\\LivewireServiceProvider')) {
 37 |             $providers[] = LivewireServiceProvider::class;
 38 |         }
 39 | 
 40 |         return $providers;
 41 |     }
 42 | 
 43 |     protected function fakeApi()
 44 |     {
 45 |         $this->apiFaked = true;
 46 | 
 47 |         $this->fakeResponseBlocks = [];
 48 | 
 49 |         Http::fake([
 50 |             'api.torchlight.dev/*' => function (Request $request) {
 51 |                 $response = [];
 52 | 
 53 |                 foreach ($request->data()['blocks'] as $block) {
 54 |                     if (!Arr::has($this->fakeResponseBlocks, $block['id'])) {
 55 |                         throw new TransferException('Torchlight block response not set for ' . $block['id']);
 56 |                     }
 57 | 
 58 |                     $fake = $this->fakeResponseBlocks[$block['id']];
 59 | 
 60 |                     if (is_array($fake)) {
 61 |                         $highlighted = "
" . $block['code'] . '
'; 62 | 63 | $response[] = array_merge($block, [ 64 | 'classes' => 'torchlight', 65 | 'styles' => 'background-color: #000000;', 66 | 'attrs' => [ 67 | 'data-theme' => $block['theme'], 68 | 'data-lang' => $block['language'] 69 | ], 70 | 'wrapped' => "
$highlighted
", 71 | 'highlighted' => $highlighted, 72 | ], $fake); 73 | } 74 | 75 | if ($fake === ConnectException::class) { 76 | throw new ConnectException('Connection timed out', $request->toPsrRequest()); 77 | } 78 | 79 | if ($fake instanceof Response || $fake instanceof FulfilledPromise) { 80 | return $fake; 81 | } 82 | } 83 | 84 | return Http::response([ 85 | 'duration' => 100, 86 | 'engine' => 1, 87 | 'blocks' => $response 88 | ], 200); 89 | }, 90 | ]); 91 | } 92 | 93 | protected function fakeSuccessfulResponse($id, $response = []) 94 | { 95 | $this->addFake($id, $response); 96 | } 97 | 98 | protected function fakeTimeout($id) 99 | { 100 | $this->addFake($id, ConnectException::class); 101 | } 102 | 103 | protected function fakeNullResponse($id) 104 | { 105 | $this->addFake($id, Http::response(null, 200)); 106 | } 107 | 108 | protected function addFake($id, $response) 109 | { 110 | if (!$this->apiFaked) { 111 | $this->fakeApi(); 112 | } 113 | 114 | $this->fakeResponseBlocks[$id] = $response; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/BlockTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Torchlight\Block; 9 | use Torchlight\Torchlight; 10 | 11 | class BlockTest extends BaseTestCase 12 | { 13 | /** @test */ 14 | public function it_dedents_code() 15 | { 16 | $block = Block::make(); 17 | 18 | $code = <<code($code); 26 | 27 | $dedented = <<assertEquals($block->code, $dedented); 35 | } 36 | 37 | /** @test */ 38 | public function it_replaces_tabs() 39 | { 40 | $block = Block::make(); 41 | 42 | $block->code("if (1) {\n\tif (1) {\n\t\treturn;\n\t}\n}"); 43 | 44 | $cleaned = <<assertEquals($block->code, $cleaned); 53 | } 54 | 55 | /** @test */ 56 | public function can_change_tab_size() 57 | { 58 | Torchlight::getConfigUsing([ 59 | 'tab_width' => 2 60 | ]); 61 | 62 | $block = Block::make(); 63 | 64 | $block->code("if (1) {\n\tif (1) {\n\t\treturn;\n\t}\n}"); 65 | 66 | $cleaned = <<assertEquals($block->code, $cleaned); 75 | } 76 | 77 | /** @test */ 78 | public function can_leave_tabs_in() 79 | { 80 | Torchlight::getConfigUsing([ 81 | 'tab_width' => false 82 | ]); 83 | 84 | $block = Block::make(); 85 | 86 | $block->code("if (1) {\n\tif (1) {\n\t\treturn;\n\t}\n}"); 87 | 88 | $cleaned = "if (1) {\n\tif (1) {\n\t\treturn;\n\t}\n}"; 89 | 90 | $this->assertEquals($block->code, $cleaned); 91 | } 92 | 93 | /** @test */ 94 | public function it_right_trims() 95 | { 96 | $block = Block::make()->code('echo 1; '); 97 | 98 | $this->assertEquals($block->code, 'echo 1;'); 99 | } 100 | 101 | /** @test */ 102 | public function you_can_set_your_own_id() 103 | { 104 | $block = Block::make('custom_id'); 105 | 106 | $this->assertEquals($block->id(), 'custom_id'); 107 | } 108 | 109 | /** @test */ 110 | public function it_will_set_an_id() 111 | { 112 | $block = Block::make(); 113 | 114 | $this->assertNotNull($block->id()); 115 | } 116 | 117 | /** @test */ 118 | public function hash_is_calculated() 119 | { 120 | $block = Block::make(); 121 | 122 | $this->assertNotNull($hash = $block->hash()); 123 | 124 | $block->code('new code'); 125 | 126 | $this->assertNotEquals($hash, $hash = $block->hash()); 127 | 128 | $block->theme('new theme'); 129 | 130 | $this->assertNotEquals($hash, $hash = $block->hash()); 131 | 132 | $block->language('new language'); 133 | 134 | $this->assertNotEquals($hash, $hash = $block->hash()); 135 | 136 | config()->set('torchlight.bust', 'new bust'); 137 | 138 | $this->assertNotEquals($hash, $hash = $block->hash()); 139 | 140 | // Hashes are stable if nothing changes. 141 | $this->assertEquals($hash, $block->hash()); 142 | } 143 | 144 | /** @test */ 145 | public function to_request_params_includes_required_info() 146 | { 147 | $block = Block::make('id'); 148 | $block->code('new code'); 149 | $block->theme('new theme'); 150 | $block->language('new language'); 151 | 152 | $this->assertEquals([ 153 | 'id' => 'id', 154 | 'hash' => 'e3db0a2768764be87d79e90063d21009', 155 | 'language' => 'new language', 156 | 'theme' => 'new theme', 157 | 'code' => 'new code', 158 | ], $block->toRequestParams()); 159 | } 160 | 161 | /** @test */ 162 | public function default_theme_is_used() 163 | { 164 | config()->set('torchlight.theme', 'a new default'); 165 | 166 | $block = Block::make('id'); 167 | 168 | $this->assertEquals('a new default', $block->theme); 169 | } 170 | 171 | /** @test */ 172 | public function can_specify_an_id_generator() 173 | { 174 | Block::$generateIdsUsing = function () { 175 | return 'generated_via_test'; 176 | }; 177 | 178 | $block = Block::make(); 179 | 180 | $this->assertEquals('generated_via_test', $block->id()); 181 | 182 | Block::$generateIdsUsing = null; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Illuminate\Http\Client\Request; 9 | use Illuminate\Support\Facades\Cache; 10 | use Illuminate\Support\Facades\Http; 11 | use Torchlight\Block; 12 | use Torchlight\Client; 13 | use Torchlight\Torchlight; 14 | 15 | class ClientTest extends BaseTestCase 16 | { 17 | public function getEnvironmentSetUp($app) 18 | { 19 | $this->setUpCache(); 20 | $this->fakeApi(); 21 | $this->setUpTorchlight(); 22 | } 23 | 24 | protected function setUpCache() 25 | { 26 | config()->set('cache', [ 27 | 'default' => 'array', 28 | 'stores' => [ 29 | 'array' => [ 30 | 'driver' => 'array', 31 | 'serialize' => false, 32 | ], 33 | ], 34 | ]); 35 | } 36 | 37 | protected function setUpTorchlight() 38 | { 39 | config()->set('torchlight', [ 40 | 'theme' => 'material', 41 | 'token' => 'token', 42 | 'bust' => 0, 43 | 'options' => [ 44 | 'lineNumbers' => true 45 | ] 46 | ]); 47 | } 48 | 49 | /** @test */ 50 | public function it_sends_a_simple_request() 51 | { 52 | $this->fakeSuccessfulResponse('id'); 53 | 54 | Torchlight::highlight( 55 | Block::make('id')->language('php')->code('echo "hello world";') 56 | ); 57 | 58 | Http::assertSent(function ($request) { 59 | return $request->hasHeader('Authorization', 'Bearer token') 60 | && $request['options'] === [ 61 | 'lineNumbers' => true 62 | ] 63 | && $request['blocks'] === [[ 64 | 'id' => 'id', 65 | 'hash' => 'e937def4cb365a758d1bf55ecc7fea5b', 66 | 'language' => 'php', 67 | 'theme' => 'material', 68 | 'code' => 'echo "hello world";', 69 | ]]; 70 | }); 71 | } 72 | 73 | /** @test */ 74 | public function block_theme_overrides_config() 75 | { 76 | $this->fakeSuccessfulResponse('id'); 77 | 78 | Torchlight::highlight( 79 | Block::make('id')->language('php')->theme('nord')->code('echo "hello world";') 80 | ); 81 | 82 | Http::assertSent(function ($request) { 83 | return $request['blocks'][0]['theme'] === 'nord'; 84 | }); 85 | } 86 | 87 | /** @test */ 88 | public function a_block_with_html_wont_be_requested() 89 | { 90 | $block = Block::make('id')->language('php')->code('echo "hello world";'); 91 | 92 | // Fake HTML, as if it had already been rendered. 93 | $block->wrapped('echo hello'); 94 | 95 | Torchlight::highlight($block); 96 | 97 | Http::assertNothingSent(); 98 | } 99 | 100 | /** @test */ 101 | public function only_blocks_without_html_get_sent() 102 | { 103 | $this->fakeSuccessfulResponse('1'); 104 | $this->fakeSuccessfulResponse('2'); 105 | 106 | $shouldNotSend = Block::make('1')->language('php')->code('echo "hello world";'); 107 | // Fake HTML, as if it had already been rendered. 108 | $shouldNotSend->wrapped('echo hello'); 109 | 110 | $shouldSend = Block::make('2')->language('php')->code('echo "hello world";'); 111 | 112 | Torchlight::highlight([ 113 | $shouldNotSend, 114 | $shouldSend 115 | ]); 116 | 117 | Http::assertSent(function ($request) { 118 | // Only 1 block 119 | return count($request['blocks']) === 1 120 | // And only the second block 121 | && $request['blocks'][0]['id'] === '2'; 122 | }); 123 | } 124 | 125 | /** @test */ 126 | public function a_block_gets_its_html_set() 127 | { 128 | $this->fakeSuccessfulResponse('success'); 129 | 130 | $block = Block::make('success')->language('php')->code('echo "hello world";'); 131 | 132 | $this->assertNull($block->wrapped); 133 | 134 | Torchlight::highlight($block); 135 | 136 | $this->assertNotNull($block->wrapped); 137 | } 138 | 139 | /** @test */ 140 | public function cache_gets_set() 141 | { 142 | $this->fakeSuccessfulResponse('success'); 143 | 144 | $block = Block::make('success')->language('php')->code('echo "hello world";'); 145 | 146 | $client = new Client; 147 | 148 | $cacheKey = $client->cacheKey($block); 149 | 150 | $this->assertNull(Cache::get($cacheKey)); 151 | 152 | $client->highlight($block); 153 | 154 | $this->assertNotNull(Cache::get($cacheKey)); 155 | } 156 | 157 | /** @test */ 158 | public function already_cached_doesnt_get_sent_again() 159 | { 160 | $this->fakeSuccessfulResponse('success'); 161 | 162 | $block = Block::make('success')->language('php')->code('echo "hello world";'); 163 | 164 | Torchlight::highlight(clone $block); 165 | Torchlight::highlight(clone $block); 166 | Torchlight::highlight(clone $block); 167 | Torchlight::highlight(clone $block); 168 | Torchlight::highlight(clone $block); 169 | 170 | // One request to set the cache, none after that. 171 | Http::assertSentCount(1); 172 | } 173 | 174 | /** @test */ 175 | public function if_theres_no_response_then_it_sets_a_default() 176 | { 177 | $this->fakeNullResponse('unknown_id'); 178 | 179 | $block = Block::make('unknown_id')->language('php')->code('echo "hello world";'); 180 | 181 | Torchlight::highlight($block); 182 | 183 | $this->assertEquals('
echo "hello world";
', $block->highlighted); 184 | $this->assertEquals('
echo "hello world";
', $block->wrapped); 185 | } 186 | 187 | /** @test */ 188 | public function a_500_error_returns_a_default_in_production() 189 | { 190 | Torchlight::overrideEnvironment('production'); 191 | 192 | $this->addFake('unknown_id', Http::response(null, 500)); 193 | 194 | $block = Block::make('unknown_id')->language('php')->code('echo "hello world";'); 195 | 196 | Torchlight::highlight($block); 197 | 198 | $this->assertEquals('
echo "hello world";
', $block->highlighted); 199 | $this->assertEquals('
echo "hello world";
', $block->wrapped); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/ClientTimeoutTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Torchlight\Block; 9 | use Torchlight\Exceptions\RequestException; 10 | use Torchlight\Torchlight; 11 | 12 | class ClientTimeoutTest extends BaseTestCase 13 | { 14 | public function getEnvironmentSetUp($app) 15 | { 16 | config()->set('torchlight', [ 17 | 'theme' => 'material', 18 | 'token' => 'token', 19 | ]); 20 | } 21 | 22 | /** @test */ 23 | public function it_catches_the_connect_exception() 24 | { 25 | $this->fakeTimeout('timeout'); 26 | 27 | // Our exception, not the default Laravel one. 28 | $this->expectException(RequestException::class); 29 | 30 | Torchlight::highlight( 31 | Block::make('timeout')->language('php')->code('echo "hello world";') 32 | ); 33 | } 34 | 35 | /** @test */ 36 | public function it_catches_the_connect_exception_in_prod() 37 | { 38 | $this->fakeTimeout('timeout'); 39 | 40 | Torchlight::overrideEnvironment('production'); 41 | 42 | Torchlight::highlight( 43 | Block::make('timeout')->language('php')->code('echo "hello world";') 44 | ); 45 | 46 | // Just want to make sure we got past the highlight with no exception. 47 | $this->assertTrue(true); 48 | } 49 | 50 | /** @test */ 51 | public function it_catches_a_real_connection_exception() 52 | { 53 | config()->set('torchlight.host', 'https://nonexistent.torchlight.dev'); 54 | 55 | // Our exception, not the default Laravel one. 56 | $this->expectException(RequestException::class); 57 | 58 | Torchlight::highlight( 59 | Block::make('timeout')->language('php')->code('echo "hello world";') 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/CustomizationTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Illuminate\Support\Arr; 9 | use Illuminate\Support\Facades\Cache; 10 | use Torchlight\Torchlight; 11 | 12 | class CustomizationTest extends BaseTestCase 13 | { 14 | public function getEnvironmentSetUp($app) 15 | { 16 | config()->set('torchlight.token', 'token from config'); 17 | } 18 | 19 | /** @test */ 20 | public function you_can_use_your_own_config_callback() 21 | { 22 | $this->assertEquals('token from config', Torchlight::config('token')); 23 | 24 | Torchlight::getConfigUsing(function ($key, $default) { 25 | return Arr::get([ 26 | 'token' => 'token from callback' 27 | ], $key); 28 | }); 29 | 30 | $this->assertEquals('token from callback', Torchlight::config('token')); 31 | } 32 | 33 | /** @test */ 34 | public function prefixing_default_config_with_torchlight_is_ok() 35 | { 36 | $this->assertEquals('token from config', Torchlight::config('torchlight.token')); 37 | $this->assertEquals('token from config', Torchlight::config('token')); 38 | } 39 | 40 | /** @test */ 41 | public function cache_implementation_can_be_set() 42 | { 43 | // The default store will be the file store. 44 | config()->set('torchlight.cache', 'file'); 45 | // Grab an instance of it so we can use it in the test. 46 | $originalStore = Cache::store('file'); 47 | 48 | // This is the one we'll swap in. 49 | $newStore = Cache::store('array'); 50 | 51 | Torchlight::cache()->set('original_key', 1, 60); 52 | 53 | // Swap in the new cache instance 54 | Torchlight::setCacheInstance($newStore); 55 | Torchlight::cache()->put('new_key', 1, 60); 56 | 57 | $this->assertTrue($originalStore->has('original_key')); 58 | $this->assertFalse($originalStore->has('new_key')); 59 | 60 | $this->assertFalse($newStore->has('original_key')); 61 | $this->assertTrue($newStore->has('new_key')); 62 | } 63 | 64 | /** @test */ 65 | public function environment_can_be_set() 66 | { 67 | $this->assertEquals('testing', Torchlight::environment()); 68 | 69 | Torchlight::overrideEnvironment('production'); 70 | 71 | $this->assertEquals('production', Torchlight::environment()); 72 | 73 | Torchlight::overrideEnvironment(null); 74 | 75 | $this->assertEquals('testing', Torchlight::environment()); 76 | } 77 | 78 | /** @test */ 79 | public function config_can_be_array() 80 | { 81 | $this->assertEquals('token from config', Torchlight::config('token')); 82 | 83 | Torchlight::getConfigUsing([ 84 | 'token' => 'plain ol array' 85 | ]); 86 | 87 | $this->assertEquals('plain ol array', Torchlight::config('token')); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/DualThemeTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Illuminate\Support\Facades\Route; 9 | use Illuminate\Support\Facades\View; 10 | use Torchlight\Middleware\RenderTorchlight; 11 | 12 | class DualThemeTest extends BaseTestCase 13 | { 14 | public function getEnvironmentSetUp($app) 15 | { 16 | config()->set('torchlight.blade_components', true); 17 | config()->set('torchlight.token', 'token'); 18 | config()->set('torchlight.theme', [ 19 | 'github-dark', 20 | 'github-light' 21 | ]); 22 | } 23 | 24 | protected function getView($view) 25 | { 26 | // This helps when testing multiple Laravel versions locally. 27 | $this->artisan('view:clear'); 28 | 29 | Route::get('/torchlight', function () use ($view) { 30 | return View::file(__DIR__ . '/Support/' . $view); 31 | })->middleware(RenderTorchlight::class); 32 | 33 | return $this->call('GET', 'torchlight'); 34 | } 35 | 36 | /** @test */ 37 | public function multiple_themes_with_comma() 38 | { 39 | config()->set('torchlight.theme', [ 40 | 'github-dark,github-light' 41 | ]); 42 | 43 | $this->assertDarkLight('github-dark', 'github-light'); 44 | } 45 | 46 | /** @test */ 47 | public function multiple_themes_no_labels() 48 | { 49 | config()->set('torchlight.theme', [ 50 | 'github-dark', 51 | 'github-light' 52 | ]); 53 | 54 | $this->assertDarkLight('github-dark', 'github-light'); 55 | } 56 | 57 | /** @test */ 58 | public function multiple_themes_with_labels() 59 | { 60 | config()->set('torchlight.theme', [ 61 | 'dark' => 'github-dark', 62 | 'light' => 'github-light' 63 | ]); 64 | 65 | $this->assertDarkLight('dark:github-dark', 'light:github-light'); 66 | } 67 | 68 | protected function assertDarkLight($theme1, $theme2) 69 | { 70 | $this->fakeSuccessfulResponse('component', [ 71 | 'classes' => 'torchlight1', 72 | 'styles' => 'background-color: #111111;', 73 | 'highlighted' => 'response 1', 74 | ]); 75 | 76 | $this->fakeSuccessfulResponse('component_clone_0', [ 77 | 'classes' => 'torchlight2', 78 | 'styles' => 'background-color: #222222;', 79 | 'highlighted' => 'response 2', 80 | ]); 81 | 82 | $response = $this->getView('simple-php-hello-world.blade.php'); 83 | 84 | $this->assertEquals( 85 | "
response 1response 2
", 86 | $response->content() 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/FindIdsTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Torchlight\Block; 9 | use Torchlight\Torchlight; 10 | 11 | class FindIdsTest extends BaseTestCase 12 | { 13 | /** @test */ 14 | public function it_will_find_all_the_ids() 15 | { 16 | $standard = Block::make(); 17 | $custom1 = Block::make('custom-id'); 18 | $custom2 = Block::make('custom-1234'); 19 | 20 | $content = <<placeholder()} 22 | {$standard->placeholder('styles')} 23 | 24 | {$custom1->placeholder()} 25 | {$custom1->placeholder('styles')} 26 | 27 | {$custom2->placeholder()} 28 | EOT; 29 | 30 | $found = Torchlight::findTorchlightIds($content); 31 | 32 | $this->assertContains($standard->id(), $found); 33 | $this->assertContains('custom-id', $found); 34 | $this->assertContains('custom-1234', $found); 35 | } 36 | 37 | /** @test */ 38 | public function it_only_returns_one_per() 39 | { 40 | $standard = Block::make(); 41 | 42 | $content = <<placeholder()} 44 | {$standard->placeholder()} 45 | {$standard->placeholder()} 46 | {$standard->placeholder()} 47 | {$standard->placeholder()} 48 | EOT; 49 | 50 | $found = Torchlight::findTorchlightIds($content); 51 | 52 | $this->assertContains($standard->id(), $found); 53 | $this->assertCount(1, $found); 54 | } 55 | 56 | /** @test */ 57 | public function its_always_an_array() 58 | { 59 | $this->assertEquals([], Torchlight::findTorchlightIds('not found')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/LivewireTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Composer\InstalledVersions; 9 | use Livewire\Livewire; 10 | use Torchlight\Middleware\RenderTorchlight; 11 | 12 | class LivewireTest extends BaseTestCase 13 | { 14 | /** @test */ 15 | public function livewire_registers_a_middleware() 16 | { 17 | // Check for the Livewire Facade. 18 | if (!class_exists('\\Livewire\\Livewire')) { 19 | return $this->markTestSkipped('Livewire not installed.'); 20 | } 21 | 22 | $this->assertTrue(in_array( 23 | RenderTorchlight::class, Livewire::getPersistentMiddleware() 24 | )); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/MiddlewareAndComponentTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Illuminate\Support\Facades\Http; 9 | use Illuminate\Support\Facades\Route; 10 | use Illuminate\Support\Facades\View; 11 | use Torchlight\Blade\BladeManager; 12 | use Torchlight\Middleware\RenderTorchlight; 13 | 14 | class MiddlewareAndComponentTest extends BaseTestCase 15 | { 16 | public function getEnvironmentSetUp($app) 17 | { 18 | config()->set('torchlight.blade_components', true); 19 | config()->set('torchlight.token', 'token'); 20 | } 21 | 22 | protected function getView($view) 23 | { 24 | // This helps when testing multiple Laravel versions locally. 25 | $this->artisan('view:clear'); 26 | 27 | Route::get('/torchlight', function () use ($view) { 28 | return View::file(__DIR__ . '/Support/' . $view); 29 | })->middleware(RenderTorchlight::class); 30 | 31 | return $this->call('GET', 'torchlight'); 32 | } 33 | 34 | /** @test */ 35 | public function it_sends_a_simple_request_with_no_response() 36 | { 37 | $this->fakeNullResponse('component'); 38 | 39 | $response = $this->getView('simple-php-hello-world.blade.php'); 40 | 41 | $this->assertEquals( 42 | '
echo "hello world";
', 43 | $response->content() 44 | ); 45 | 46 | Http::assertSent(function ($request) { 47 | return $request['blocks'][0] === [ 48 | 'id' => 'component', 49 | 'hash' => '66192c35bf8a710bee532ac328c76977', 50 | 'language' => 'php', 51 | 'theme' => 'material-theme-palenight', 52 | 'code' => 'echo "hello world";', 53 | ]; 54 | }); 55 | } 56 | 57 | /** @test */ 58 | public function it_sends_a_simple_request_with_highlighted_response() 59 | { 60 | $this->fakeSuccessfulResponse('component', [ 61 | 'classes' => 'torchlight', 62 | 'styles' => 'background-color: #292D3E;', 63 | 'highlighted' => 'this is the highlighted response from the server', 64 | ]); 65 | 66 | $response = $this->getView('simple-php-hello-world.blade.php'); 67 | 68 | $this->assertEquals( 69 | '
this is the highlighted response from the server
', 70 | $response->content() 71 | ); 72 | } 73 | 74 | /** @test */ 75 | public function it_sends_a_simple_request_with_style() 76 | { 77 | $this->fakeSuccessfulResponse('component', [ 78 | 'classes' => 'torchlight', 79 | 'styles' => 'background-color: #292D3E;', 80 | 'highlighted' => 'this is the highlighted response from the server', 81 | ]); 82 | 83 | $response = $this->getView('simple-php-hello-world-with-style.blade.php'); 84 | 85 | $this->assertEquals( 86 | '
', 87 | $response->content() 88 | ); 89 | } 90 | 91 | /** @test */ 92 | public function no_attrs_no_trailing_space() 93 | { 94 | $this->fakeSuccessfulResponse('component', [ 95 | 'classes' => 'torchlight', 96 | 'styles' => 'background-color: #292D3E;', 97 | 'highlighted' => 'this is the highlighted response from the server', 98 | 'attrs' => [] 99 | ]); 100 | 101 | $response = $this->getView('simple-php-hello-world-with-style.blade.php'); 102 | 103 | $this->assertEquals( 104 | '
', 105 | $response->content() 106 | ); 107 | } 108 | 109 | /** @test */ 110 | public function classes_get_merged() 111 | { 112 | $this->fakeSuccessfulResponse('component', [ 113 | 'classes' => 'torchlight', 114 | 'styles' => 'background-color: #292D3E;', 115 | 'highlighted' => 'this is the highlighted response from the server', 116 | ]); 117 | 118 | $response = $this->getView('simple-php-hello-world-with-classes.blade.php'); 119 | 120 | $this->assertEquals( 121 | 'this is the highlighted response from the server', 122 | $response->content() 123 | ); 124 | } 125 | 126 | /** @test */ 127 | public function attributes_are_preserved() 128 | { 129 | $this->fakeSuccessfulResponse('component', [ 130 | 'classes' => 'torchlight', 131 | 'styles' => 'background-color: #292D3E;', 132 | 'highlighted' => 'this is the highlighted response from the server', 133 | ]); 134 | 135 | $response = $this->getView('simple-php-hello-world-with-attributes.blade.php'); 136 | 137 | $this->assertEquals( 138 | 'this is the highlighted response from the server', 139 | $response->content() 140 | ); 141 | } 142 | 143 | /** @test */ 144 | public function inline_keeps_its_spaces() 145 | { 146 | $this->fakeSuccessfulResponse('component', [ 147 | 'classes' => 'torchlight', 148 | 'styles' => 'background-color: #292D3E;', 149 | 'highlighted' => 'this is the highlighted response from the server', 150 | ]); 151 | 152 | $response = $this->getView('an-inline-component.blade.php'); 153 | 154 | $this->assertEquals( 155 | 'this is this is the highlighted response from the server inline', 156 | $response->content() 157 | ); 158 | } 159 | 160 | /** @test */ 161 | public function inline_swaps_run() 162 | { 163 | $this->fakeSuccessfulResponse('component', [ 164 | 'classes' => 'torchlight', 165 | 'styles' => 'background-color: #292D3E;', 166 | 'highlighted' => 'echo "hello world"', 167 | ]); 168 | 169 | $response = $this->getView('an-inline-component-with-swaps.blade.php'); 170 | 171 | $this->assertEquals( 172 | 'this is echo "goodbye world" inline', 173 | $response->content() 174 | ); 175 | } 176 | 177 | /** @test */ 178 | public function inline_processors_run() 179 | { 180 | $this->fakeSuccessfulResponse('component', [ 181 | 'classes' => 'torchlight', 182 | 'styles' => 'background-color: #292D3E;', 183 | 'highlighted' => 'echo "hello world"', 184 | ]); 185 | 186 | $response = $this->getView('an-inline-component-with-post-processors.blade.php'); 187 | 188 | $this->assertEquals( 189 | 'this is echo "goodbye world" inline', 190 | $response->content() 191 | ); 192 | } 193 | 194 | /** @test */ 195 | public function language_can_be_set_via_component() 196 | { 197 | $this->fakeNullResponse('component'); 198 | 199 | $this->getView('simple-js-hello-world.blade.php'); 200 | 201 | Http::assertSent(function ($request) { 202 | return $request['blocks'][0]['language'] === 'javascript'; 203 | }); 204 | } 205 | 206 | /** @test */ 207 | public function theme_can_be_set_via_component() 208 | { 209 | $this->fakeNullResponse('component'); 210 | 211 | $this->getView('simple-php-hello-world-new-theme.blade.php'); 212 | 213 | Http::assertSent(function ($request) { 214 | return $request['blocks'][0]['theme'] === 'a new theme'; 215 | }); 216 | } 217 | 218 | /** @test */ 219 | public function code_contents_can_be_a_file() 220 | { 221 | $this->fakeNullResponse('component'); 222 | 223 | $this->getView('contents-via-file.blade.php'); 224 | 225 | Http::assertSent(function ($request) { 226 | return $request['blocks'][0]['code'] === rtrim(file_get_contents(config_path('app.php'), '\n')); 227 | }); 228 | } 229 | 230 | /** @test */ 231 | public function code_contents_can_be_a_file_2() 232 | { 233 | $this->fakeNullResponse('component'); 234 | 235 | $this->getView('contents-via-file-2.blade.php'); 236 | 237 | Http::assertSent(function ($request) { 238 | return $request['blocks'][0]['code'] === rtrim(file_get_contents(config_path('app.php'), '\n')); 239 | }); 240 | } 241 | 242 | /** @test */ 243 | public function file_must_be_passed_via_contents() 244 | { 245 | $this->fakeNullResponse('component'); 246 | 247 | $this->getView('file-must-be-passed-through-contents.blade.php'); 248 | 249 | Http::assertSent(function ($request) { 250 | return $request['blocks'][0]['code'] === config_path('app.php'); 251 | }); 252 | } 253 | 254 | /** @test */ 255 | public function dedent_works_properly() 256 | { 257 | $this->withoutExceptionHandling(); 258 | $this->fakeNullResponse('component'); 259 | 260 | $response = $this->getView('dedent_works_properly.blade.php'); 261 | 262 | $result = "
public function {
// test
}
"; 263 | 264 | if (BladeManager::$affectedBySpacingBug) { 265 | $this->assertEquals( 266 | "
\n    $result\n
\n
$result
\n
$result
", 267 | $response->content() 268 | ); 269 | } else { 270 | $this->assertEquals( 271 | "
\n    $result
\n
$result
\n
$result
", 272 | $response->content() 273 | ); 274 | } 275 | } 276 | 277 | /** @test */ 278 | public function two_code_in_one_pre() 279 | { 280 | $this->withoutExceptionHandling(); 281 | $this->fakeNullResponse('component'); 282 | 283 | $response = $this->getView('two-codes-in-one-tag.blade.php'); 284 | 285 | $result = "
public function {
// test
}
"; 286 | 287 | if (BladeManager::$affectedBySpacingBug) { 288 | $this->assertEquals( 289 | "
\n    {$result}\n    {$result}\n
", 290 | $response->content() 291 | ); 292 | } else { 293 | $this->assertEquals( 294 | "
\n    $result    $result
", 295 | $response->content() 296 | ); 297 | } 298 | } 299 | 300 | /** @test */ 301 | public function two_components_work() 302 | { 303 | $this->fakeSuccessfulResponse('component1', [ 304 | 'id' => 'component1', 305 | 'classes' => 'torchlight1', 306 | 'styles' => 'background-color: #111111;', 307 | 'highlighted' => 'response 1', 308 | ]); 309 | 310 | $this->fakeSuccessfulResponse('component2', [ 311 | 'id' => 'component2', 312 | 'classes' => 'torchlight2', 313 | 'styles' => 'background-color: #222222;', 314 | 'highlighted' => 'response 2', 315 | ]); 316 | 317 | $response = $this->getView('two-simple-php-hello-world.blade.php'); 318 | 319 | $expected = <<response 1 321 | 322 |
response 2
323 | EOT; 324 | 325 | $this->assertEquals($expected, $response->content()); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /tests/PostProcessorTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Torchlight\Block; 9 | use Torchlight\Contracts\PostProcessor; 10 | use Torchlight\Exceptions\ConfigurationException; 11 | use Torchlight\PostProcessors\SimpleSwapProcessor; 12 | use Torchlight\Torchlight; 13 | 14 | class PostProcessorTest extends BaseTestCase 15 | { 16 | public function getEnvironmentSetUp($app) 17 | { 18 | config()->set('torchlight', [ 19 | 'theme' => 'material', 20 | 'token' => 'token', 21 | ]); 22 | } 23 | 24 | /** @test */ 25 | public function it_runs_post_processors() 26 | { 27 | $this->fakeSuccessfulResponse('id'); 28 | 29 | Torchlight::addPostProcessors([ 30 | GoodbyePostProcessor::class 31 | ]); 32 | 33 | $blocks = Torchlight::highlight( 34 | Block::make('id')->language('php')->code('echo "hello world";') 35 | ); 36 | 37 | $this->assertEquals($blocks[0]->highlighted, '
echo "goodbye world";
'); 38 | } 39 | 40 | /** @test */ 41 | public function it_doesnt_run_when_compiling() 42 | { 43 | $this->fakeSuccessfulResponse('id'); 44 | 45 | Torchlight::addPostProcessors([ 46 | GoodbyePostProcessor::class 47 | ]); 48 | 49 | Torchlight::currentlyCompilingViews(true); 50 | 51 | $blocks = Torchlight::highlight( 52 | Block::make('id')->language('php')->code('echo "hello world";') 53 | ); 54 | 55 | $this->assertEquals($blocks[0]->highlighted, '
echo "hello world";
'); 56 | } 57 | 58 | /** @test */ 59 | public function it_runs_when_compiling_if_requested() 60 | { 61 | $this->fakeSuccessfulResponse('id'); 62 | 63 | Torchlight::addPostProcessors([ 64 | GoodbyePostProcessor::class, 65 | RunWhileCompilingProcessor::class, 66 | ]); 67 | 68 | Torchlight::currentlyCompilingViews(true); 69 | 70 | $blocks = Torchlight::highlight( 71 | Block::make('id')->language('php')->code('echo "hello world";') 72 | ); 73 | 74 | $this->assertEquals($blocks[0]->highlighted, '
echo "compiled world";
'); 75 | } 76 | 77 | /** @test */ 78 | public function null_processor_works() 79 | { 80 | $this->fakeSuccessfulResponse('id'); 81 | 82 | Torchlight::addPostProcessors([ 83 | NullPostProcessor::class 84 | ]); 85 | 86 | $blocks = Torchlight::highlight( 87 | Block::make('id')->language('php')->code('echo "hello world";') 88 | ); 89 | 90 | $this->assertEquals($blocks[0]->highlighted, '
echo "hello world";
'); 91 | } 92 | 93 | /** @test */ 94 | public function they_run_in_order() 95 | { 96 | $this->fakeSuccessfulResponse('id'); 97 | 98 | Torchlight::addPostProcessors([ 99 | GoodbyePostProcessor::class, 100 | GoodbyeCruelPostProcessor::class 101 | ]); 102 | 103 | $blocks = Torchlight::highlight( 104 | Block::make('id')->language('php')->code('echo "hello world";') 105 | ); 106 | 107 | $this->assertEquals($blocks[0]->highlighted, '
echo "goodbye cruel world";
'); 108 | } 109 | 110 | /** @test */ 111 | public function it_runs_inline_post_processors() 112 | { 113 | $this->fakeSuccessfulResponse('id'); 114 | 115 | $blocks = Torchlight::highlight( 116 | Block::make('id')->language('php')->code('echo "hello world";') 117 | ->addPostProcessor(SimpleSwapProcessor::make(['hello world' => 'goodbye world'])) 118 | ); 119 | 120 | $this->assertEquals($blocks[0]->highlighted, '
echo "goodbye world";
'); 121 | } 122 | 123 | /** @test */ 124 | public function must_implement_interface() 125 | { 126 | $this->expectException(ConfigurationException::class); 127 | $this->expectExceptionMessage('Post-processor \'Torchlight\Block\' does not implement Torchlight\Contracts\PostProcessor'); 128 | 129 | Torchlight::addPostProcessors([ 130 | Block::class 131 | ]); 132 | } 133 | } 134 | 135 | class NullPostProcessor implements PostProcessor 136 | { 137 | public function process(Block $block) 138 | { 139 | } 140 | } 141 | 142 | class GoodbyePostProcessor implements PostProcessor 143 | { 144 | public function process(Block $block) 145 | { 146 | $block->highlighted = str_replace('hello', 'goodbye', $block->highlighted); 147 | } 148 | } 149 | 150 | class GoodbyeCruelPostProcessor implements PostProcessor 151 | { 152 | public function process(Block $block) 153 | { 154 | $block->highlighted = str_replace('goodbye', 'goodbye cruel', $block->highlighted); 155 | } 156 | } 157 | 158 | class RunWhileCompilingProcessor implements PostProcessor 159 | { 160 | public $processEvenWhenCompiling = true; 161 | 162 | public function process(Block $block) 163 | { 164 | $block->highlighted = str_replace('hello', 'compiled', $block->highlighted); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /tests/RealClientTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Torchlight\Tests; 7 | 8 | use Illuminate\Support\Facades\Route; 9 | use Illuminate\Support\Facades\View; 10 | use Torchlight\Middleware\RenderTorchlight; 11 | 12 | class RealClientTest extends BaseTestCase 13 | { 14 | public function getEnvironmentSetUp($app) 15 | { 16 | $this->setUpTorchlight(); 17 | } 18 | 19 | protected function setUpCache() 20 | { 21 | config()->set('cache', [ 22 | 'default' => 'array', 23 | 'stores' => [ 24 | 'array' => [ 25 | 'driver' => 'array', 26 | 'serialize' => false, 27 | ], 28 | ], 29 | ]); 30 | } 31 | 32 | protected function setUpTorchlight() 33 | { 34 | config()->set('torchlight', [ 35 | 'theme' => 'material-theme-lighter', 36 | 'token' => '', 37 | 'bust' => 0, 38 | 'blade_components' => true, 39 | 'options' => [ 40 | 'lineNumbers' => false 41 | ] 42 | ]); 43 | } 44 | 45 | protected function getView($view) 46 | { 47 | // This helps when testing multiple Laravel versions locally. 48 | $this->artisan('view:clear'); 49 | 50 | Route::get('/torchlight', function () use ($view) { 51 | return View::file(__DIR__ . '/Support/' . $view); 52 | })->middleware(RenderTorchlight::class); 53 | 54 | return $this->call('GET', 'torchlight'); 55 | } 56 | 57 | /** @test */ 58 | public function it_sends_a_simple_request_with_highlighted_response_real() 59 | { 60 | return $this->markTestSkipped(); 61 | 62 | $response = $this->getView('simple-php-hello-world.blade.php'); 63 | 64 | $this->assertEquals( 65 | '
echo "hello world";
', 66 | $response->content() 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Support/an-inline-component-with-post-processors.blade.php: -------------------------------------------------------------------------------- 1 | @php($p = \Torchlight\PostProcessors\SimpleSwapProcessor::make(['hello' => 'goodbye'])) 2 | this is echo "hello world" inline -------------------------------------------------------------------------------- /tests/Support/an-inline-component-with-swaps.blade.php: -------------------------------------------------------------------------------- 1 | this is echo "hello world" inline -------------------------------------------------------------------------------- /tests/Support/an-inline-component.blade.php: -------------------------------------------------------------------------------- 1 | this is echo "hello world" inline -------------------------------------------------------------------------------- /tests/Support/contents-via-file-2.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Support/contents-via-file.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Support/dedent_works_properly.blade.php: -------------------------------------------------------------------------------- 1 |
 2 |     
 3 |         public function {
 4 |             // test
 5 |         }
 6 |     
 7 | 
8 |

 9 |     public function {
10 |         // test
11 |     }
12 | 
13 |
public function {
14 |     // test
15 | }
16 | 
-------------------------------------------------------------------------------- /tests/Support/file-must-be-passed-through-contents.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{ config_path("app.php") }} 3 | -------------------------------------------------------------------------------- /tests/Support/simple-js-hello-world.blade.php: -------------------------------------------------------------------------------- 1 | 2 | console.log('hello world'); 3 | -------------------------------------------------------------------------------- /tests/Support/simple-php-hello-world-new-theme.blade.php: -------------------------------------------------------------------------------- 1 | 2 | echo "hello world"; 3 | -------------------------------------------------------------------------------- /tests/Support/simple-php-hello-world-with-attributes.blade.php: -------------------------------------------------------------------------------- 1 | 2 | echo "hello world"; 3 | -------------------------------------------------------------------------------- /tests/Support/simple-php-hello-world-with-classes.blade.php: -------------------------------------------------------------------------------- 1 | 2 | echo "hello world"; 3 | -------------------------------------------------------------------------------- /tests/Support/simple-php-hello-world-with-style.blade.php: -------------------------------------------------------------------------------- 1 |

2 |     echo "hello world";
3 | 
-------------------------------------------------------------------------------- /tests/Support/simple-php-hello-world.blade.php: -------------------------------------------------------------------------------- 1 |

2 |     echo "hello world";
3 | 
-------------------------------------------------------------------------------- /tests/Support/two-codes-in-one-tag.blade.php: -------------------------------------------------------------------------------- 1 |
 2 |     
 3 |         public function {
 4 |             // test
 5 |         }
 6 |     
 7 |     
 8 |         public function {
 9 |             // test
10 |         }
11 |     
12 | 
-------------------------------------------------------------------------------- /tests/Support/two-simple-php-hello-world.blade.php: -------------------------------------------------------------------------------- 1 |

2 |     echo "hello world 1";
3 | 
4 | 5 |

6 |     echo "hello world 2";
7 | 
--------------------------------------------------------------------------------