├── .github ├── scripts │ └── compile-templates.php └── workflows │ ├── formatting.yaml │ ├── static-analysis.yml │ ├── tests.yml │ └── view-compiling.yaml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── art └── banner.png ├── composer.json ├── config └── editor.php ├── phpstan.neon.dist ├── phpunit.xml ├── resources ├── .gitkeep ├── php │ ├── bootstrap-five │ │ ├── attaches.php │ │ ├── checklist.php │ │ ├── code.php │ │ ├── delimiter.php │ │ ├── embed.php │ │ ├── header.php │ │ ├── image.php │ │ ├── linktool.php │ │ ├── list.php │ │ ├── paragraph.php │ │ ├── personality.php │ │ ├── quote.php │ │ ├── table.php │ │ └── warning.php │ └── tailwind │ │ ├── attaches.php │ │ ├── checklist.php │ │ ├── code.php │ │ ├── delimiter.php │ │ ├── embed.php │ │ ├── header.php │ │ ├── image.php │ │ ├── linktool.php │ │ ├── list.php │ │ ├── paragraph.php │ │ ├── personality.php │ │ ├── quote.php │ │ ├── table.php │ │ └── warning.php └── views │ ├── bootstrap-five │ ├── attaches.blade.php │ ├── checklist.blade.php │ ├── code.blade.php │ ├── delimiter.blade.php │ ├── embed.blade.php │ ├── header.blade.php │ ├── image.blade.php │ ├── linktool.blade.php │ ├── list.blade.php │ ├── paragraph.blade.php │ ├── personality.blade.php │ ├── quote.blade.php │ ├── table.blade.php │ └── warning.blade.php │ └── tailwind │ ├── attaches.blade.php │ ├── checklist.blade.php │ ├── code.blade.php │ ├── delimiter.blade.php │ ├── embed.blade.php │ ├── header.blade.php │ ├── image.blade.php │ ├── linktool.blade.php │ ├── list.blade.php │ ├── paragraph.blade.php │ ├── personality.blade.php │ ├── quote.blade.php │ ├── table.blade.php │ └── warning.blade.php ├── src ├── Block │ ├── Block.php │ └── Data.php ├── Blocks │ ├── Attaches.php │ ├── Checklist.php │ ├── Code.php │ ├── Delimiter.php │ ├── Embed.php │ ├── Header.php │ ├── Image.php │ ├── LinkTool.php │ ├── ListBlock.php │ ├── Paragraph.php │ ├── Personality.php │ ├── Quote.php │ ├── Raw.php │ ├── Table.php │ └── Warning.php ├── Casts │ └── EditorPhpCast.php ├── Console │ ├── BlockMakeCommand.php │ └── stubs │ │ └── block.stub ├── EditorPhp.php ├── EditorPhpServiceProvider.php ├── Exceptions │ └── EditorPhpException.php ├── Helpers.php ├── Parser.php └── Purifier.php └── tests ├── Block ├── BlockTest.php └── DataTest.php ├── Datasets ├── Models.php ├── Samples.php └── samples │ ├── broken.json │ ├── unknownType.json │ ├── unmatchingSchema.json │ └── valid.json ├── EditorPhpTest.php ├── Laravel ├── Casts │ └── EditorPhpCastTest.php ├── Console │ └── BlockMakeCommandTest.php ├── EditorPhpTest.php └── TestCase.php ├── ParserTest.php ├── Pest.php └── PurifierTest.php /.github/scripts/compile-templates.php: -------------------------------------------------------------------------------- 1 | "; 25 | } 26 | 27 | protected function compileEndforeach() 28 | { 29 | return ''; 30 | } 31 | } 32 | 33 | $compiler = new Compiler(new Filesystem(), __DIR__, false); 34 | 35 | foreach (['bootstrap-five', 'tailwind'] as $framework) 36 | { 37 | foreach (scandir(__DIR__ . '/../../resources/views/' . $framework) as $template) 38 | { 39 | if (!str_ends_with($template, '.blade.php')) 40 | { 41 | continue; 42 | } 43 | 44 | $bladePath = __DIR__ . '/../../resources/views/' . $framework . '/' . $template; 45 | $compiledPath = __DIR__ . '/../../resources/php/' . $framework . '/' . str_replace('.blade.php', '.php', $template); 46 | 47 | file_put_contents( 48 | $compiledPath, 49 | $compiler->compileString(file_get_contents($bladePath)) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/formatting.yaml: -------------------------------------------------------------------------------- 1 | name: Fix syling 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - "*.x" 8 | paths: 9 | - "**.php" 10 | 11 | pull_request: 12 | paths: 13 | - "**.php" 14 | 15 | permissions: 16 | contents: write 17 | jobs: 18 | php-cs-fixer: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | with: 24 | ref: ${{ github.head_ref }} 25 | 26 | - name: Install dependencies 27 | run: composer install 28 | 29 | - name: Execute php-cs-fixer 30 | run: ./vendor/bin/php-cs-fixer fix 31 | 32 | - uses: stefanzweifel/git-auto-commit-action@v4 33 | with: 34 | commit_message: Fix styling 35 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | # Taken from `https://github.com/laravel/framework/blob/0b3fae2e8dd094433a760b09eb4366bb139fcf88/.github/workflows/static-analysis.yml 2 | name: static analysis 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | - '*.x' 9 | pull_request: 10 | 11 | jobs: 12 | run: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: true 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: 8.1 25 | tools: composer:v2 26 | coverage: none 27 | 28 | - name: Install dependencies 29 | uses: nick-fields/retry@v2 30 | with: 31 | timeout_minutes: 5 32 | max_attempts: 5 33 | command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress 34 | 35 | - name: Execute PHPStan 36 | run: vendor/bin/phpstan --configuration="phpstan.neon.dist" 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - '*.x' 8 | pull_request: 9 | 10 | 11 | jobs: 12 | run: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | php: [ 8.1, 8.2, 8.3, 8.4 ] 17 | laravel: [ 10, 11, 12 ] 18 | exclude: 19 | - php: '8.1' 20 | laravel: 11 21 | - php: '8.1' 22 | laravel: 12 23 | - php: '8.4' 24 | laravel: 10 25 | 26 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} 27 | 28 | steps: 29 | - name: Setup PHP 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: ${{ matrix.php }} 33 | 34 | - name: Checkout code 35 | uses: actions/checkout@v3 36 | 37 | - name: Install dependencies 38 | run: | 39 | composer require "illuminate/support=^${{ matrix.laravel }}" --no-update 40 | composer update --prefer-dist --no-interaction --no-progress 41 | 42 | - name: Execute tests 43 | run: ./vendor/bin/pest 44 | -------------------------------------------------------------------------------- /.github/workflows/view-compiling.yaml: -------------------------------------------------------------------------------- 1 | name: View Compiling 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - "*.x" 8 | paths: 9 | - "**.php" 10 | 11 | pull_request: 12 | paths: 13 | - "**.php" 14 | 15 | permissions: 16 | contents: write 17 | jobs: 18 | run: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | with: 24 | ref: ${{ github.head_ref }} 25 | 26 | - name: Install dependencies 27 | run: composer install 28 | 29 | - name: Execute script 30 | run: php .github/scripts/compile-templates.php 31 | 32 | - uses: stefanzweifel/git-auto-commit-action@v4 33 | with: 34 | commit_message: Compiling Templates 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /build/ 3 | composer.lock 4 | .php-cs-fixer.cache 5 | .php-cs-fixer.php 6 | .phpunit.result.cache 7 | .phpunit.cache 8 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRules([ 13 | 'align_multiline_comment' => true, 14 | 'array_syntax' => true, 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'blank_line_before_statement' => ['statements'=>['case', 'for', 'foreach', 'if', 'phpdoc', 'return', 'switch', 'throw', 'try', 'while']], 18 | 'braces' => ['position_after_anonymous_constructs'=>'next', 'position_after_control_structures'=>'next'], 19 | 'cast_spaces' => true, 20 | 'class_attributes_separation' => true, 21 | 'class_definition' => true, 22 | 'class_reference_name_casing' => true, 23 | 'clean_namespace' => true, 24 | 'combine_consecutive_issets' => true, 25 | 'combine_consecutive_unsets' => true, 26 | 'compact_nullable_typehint' => true, 27 | 'concat_space' => ['spacing'=>'one'], 28 | 'constant_case' => true, 29 | 'control_structure_braces' => true, 30 | 'control_structure_continuation_position' => ['position'=>'next_line'], 31 | 'curly_braces_position' => ['allow_single_line_anonymous_functions'=>false, 'allow_single_line_empty_anonymous_classes'=>false, 'anonymous_classes_opening_brace'=>'next_line_unless_newline_at_signature_end', 'anonymous_functions_opening_brace'=>'same_line', 'control_structures_opening_brace'=>'next_line_unless_newline_at_signature_end', 'functions_opening_brace'=>'next_line_unless_newline_at_signature_end'], 32 | 'declare_equal_normalize' => ['space'=>'single'], 33 | 'declare_parentheses' => true, 34 | 'echo_tag_syntax' => ['format'=>'short'], 35 | 'elseif' => true, 36 | 'encoding' => true, 37 | 'explicit_indirect_variable' => true, 38 | 'explicit_string_variable' => true, 39 | 'full_opening_tag' => true, 40 | 'fully_qualified_strict_types' => true, 41 | 'function_declaration' => ['closure_function_spacing'=>'none'], 42 | 'function_typehint_space' => true, 43 | 'heredoc_indentation' => true, 44 | 'include' => true, 45 | 'increment_style' => ['style'=>'post'], 46 | 'indentation_type' => true, 47 | 'lambda_not_used_import' => true, 48 | 'line_ending' => true, 49 | 'linebreak_after_opening_tag' => true, 50 | 'list_syntax' => true, 51 | 'lowercase_cast' => true, 52 | 'lowercase_keywords' => true, 53 | 'lowercase_static_reference' => true, 54 | 'magic_constant_casing' => true, 55 | 'magic_method_casing' => true, 56 | 'method_argument_space' => true, 57 | 'method_chaining_indentation' => true, 58 | 'multiline_comment_opening_closing' => true, 59 | 'multiline_whitespace_before_semicolons' => true, 60 | 'native_function_casing' => true, 61 | 'native_function_type_declaration_casing' => true, 62 | 'new_with_braces' => true, 63 | 'no_alias_language_construct_call' => true, 64 | 'no_blank_lines_after_class_opening' => true, 65 | 'no_blank_lines_after_phpdoc' => true, 66 | 'no_closing_tag' => true, 67 | 'no_empty_comment' => true, 68 | 'no_empty_phpdoc' => true, 69 | 'no_empty_statement' => true, 70 | 'no_leading_namespace_whitespace' => true, 71 | 'no_multiline_whitespace_around_double_arrow' => true, 72 | 'no_multiple_statements_per_line' => true, 73 | 'no_null_property_initialization' => true, 74 | 'no_short_bool_cast' => true, 75 | 'no_singleline_whitespace_before_semicolons' => true, 76 | 'no_space_around_double_colon' => true, 77 | 'no_spaces_after_function_name' => true, 78 | 'no_spaces_around_offset' => true, 79 | 'no_spaces_inside_parenthesis' => true, 80 | 'no_trailing_comma_in_singleline' => true, 81 | 'no_trailing_whitespace' => true, 82 | 'no_trailing_whitespace_in_comment' => true, 83 | 'no_unneeded_import_alias' => true, 84 | 'no_unused_imports' => true, 85 | 'no_useless_concat_operator' => true, 86 | 'no_useless_else' => true, 87 | 'no_useless_nullsafe_operator' => true, 88 | 'no_useless_return' => true, 89 | 'no_whitespace_before_comma_in_array' => true, 90 | 'no_whitespace_in_blank_line' => true, 91 | 'normalize_index_brace' => true, 92 | 'nullable_type_declaration_for_default_null_value' => true, 93 | 'object_operator_without_whitespace' => true, 94 | 'operator_linebreak' => true, 95 | 'ordered_imports' => true, 96 | 'phpdoc_align' => ['align'=>'left'], 97 | 'phpdoc_indent' => true, 98 | 'phpdoc_line_span' => true, 99 | 'phpdoc_no_access' => true, 100 | 'phpdoc_no_alias_tag' => true, 101 | 'phpdoc_order' => true, 102 | 'phpdoc_separation' => true, 103 | 'phpdoc_single_line_var_spacing' => true, 104 | 'phpdoc_summary' => true, 105 | 'phpdoc_tag_casing' => true, 106 | 'phpdoc_tag_type' => true, 107 | 'phpdoc_trim' => true, 108 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 109 | 'phpdoc_types' => true, 110 | 'phpdoc_types_order' => ['null_adjustment'=>'always_last'], 111 | 'phpdoc_var_annotation_correct_order' => true, 112 | 'phpdoc_var_without_name' => true, 113 | 'return_type_declaration' => true, 114 | 'semicolon_after_instruction' => true, 115 | 'short_scalar_cast' => true, 116 | 'simple_to_complex_string_variable' => true, 117 | 'simplified_null_return' => true, 118 | 'single_blank_line_before_namespace' => true, 119 | 'single_class_element_per_statement' => true, 120 | 'single_import_per_statement' => true, 121 | 'single_line_after_imports' => true, 122 | 'single_line_comment_spacing' => true, 123 | 'single_line_throw' => true, 124 | 'single_space_after_construct' => ['constructs'=>['abstract', 'as', 'attribute', 'break', 'case', 'comment', 'echo', 'if', 'implements', 'namespace', 'new', 'private', 'protected', 'public', 'readonly', 'var']], 125 | 'space_after_semicolon' => true, 126 | 'standardize_increment' => true, 127 | 'standardize_not_equals' => true, 128 | 'statement_indentation' => true, 129 | 'switch_case_space' => true, 130 | 'switch_continue_to_break' => true, 131 | 'ternary_operator_spaces' => true, 132 | 'ternary_to_null_coalescing' => true, 133 | 'trailing_comma_in_multiline' => true, 134 | 'trim_array_spaces' => true, 135 | 'types_spaces' => true, 136 | 'unary_operator_spaces' => true, 137 | 'whitespace_after_comma_in_array' => ['ensure_single_space'=>false], 138 | ]) 139 | ->setFinder( 140 | PhpCsFixer\Finder::create() 141 | ->exclude('vendor') 142 | ->in(__DIR__) 143 | ); 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 BumpCore 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 |

2 | 3 | # Editor.php 4 | 5 | Editor.php is a package designed to assist in parsing and manipulating the output of [Editor.js](https://editorjs.io/) with ease. It can be used with either vanilla PHP or with Larave. Laravel offers few additional features. 6 | 7 | ## Table Of Contents 8 | 9 | * [Version Table](#version-table) 10 | * [Quick Start](#quick-start) 11 | * [EditorPhp](#editorphp) 12 | + [Creating Instance](#creating-instance) 13 | + [Accessing Blocks](#accessing-blocks) 14 | + [Rendering HTML](#rendering-html) 15 | + [Faking](#faking) 16 | + [Additional](#additional) 17 | + [Converting to an array](#converting-to-an-array) 18 | + [Converting to JSON](#converting-to-json) 19 | + [Time & Version](#time--version) 20 | + [Macros](#macros) 21 | * [Blocks](#blocks) 22 | + [Registering Blocks](#registering-blocks) 23 | + [Extending Blocks](#extending-blocks) 24 | + [Creating Custom Blocks](#creating-custom-blocks) 25 | + [Accessing Block's Data](#accessing-blocks-data) 26 | + [Validating Block Data](#validating-block-data) 27 | + [Sanitizing Block Data](#sanitizing-block-data) 28 | + [Fake Data Generation](#fake-data-generation) 29 | * [Laravel Only Features](#laravel-only-features) 30 | + [Cast](#cast) 31 | + [Response](#response) 32 | + [Views](#views) 33 | + [Publishing Views and Configuration](#publishing-views-and-configuration) 34 | + [Commands](#commands) 35 | * [Contribution](#contribution) 36 | 37 | # Version Table 38 | | Editor.php | Laravel | PHP | 39 | | --- | --- | --- | 40 | | 1.x | 10.x | 8.1 ~ 8.3 | 41 | | 1.x | 11.x | 8.2 ~ 8.4 | 42 | | 1.x | 12.x | 8.2 ~ 8.4 | 43 | 44 | # Quick Start 45 | 46 | Install package by: 47 | 48 | ```bash 49 | composer require bumpcore/editor.php 50 | ``` 51 | 52 | Editor.php is really simple to get started; 53 | 54 | ```php 55 | use BumpCore\EditorPhp\EditorPhp; 56 | 57 | // Passing Editor.js's output directly to the `make`. 58 | // This will render blocks into html. 59 | echo EditorPhp::make($json)->render(); 60 | ``` 61 | 62 | Editor.php supports following blocks; 63 | 64 | * [Attaches](https://github.com/editor-js/attaches) 65 | * [Checklist](https://github.com/editor-js/checklist) 66 | * [Code](https://github.com/editor-js/code) 67 | * [Delimiter](https://github.com/editor-js/delimiter) 68 | * [Embed](https://github.com/editor-js/embed) 69 | * [Header](https://github.com/editor-js/header) 70 | * [Image](https://github.com/editor-js/image) 71 | * [Link Tool](https://github.com/editor-js/link) 72 | * [List](https://github.com/editor-js/list) 73 | * [Paragraph](https://github.com/editor-js/paragraph) 74 | * [Personality](https://github.com/editor-js/personality) 75 | * [Quote](https://github.com/editor-js/quote) 76 | * [Table](https://github.com/editor-js/table) 77 | * [Warning](https://github.com/editor-js/warning) 78 | 79 | All of them have default validation rules and views to render. However, customizing validation and views is highly recommended. 80 | 81 | # EditorPhp 82 | 83 | The `EditorPhp` class is the main class for managing blocks. You can access, render, convert to an array, and convert to JSON through this class. 84 | 85 | ## Creating Instance 86 | 87 | There are two ways to create a new instance of EditorPhp: 88 | 89 | ```php 90 | use BumpCore\EditorPhp\EditorPhp; 91 | 92 | // Using the `new` syntax. 93 | $editor = new EditorPhp($json); 94 | 95 | // Using the `make` syntax. 96 | $editor = EditorPhp::make($json); 97 | ``` 98 | 99 | Both syntaxes are equal, and there's almost no difference between them. 100 | 101 | ## Accessing Blocks 102 | 103 | You can access blocks through the blocks property. 104 | 105 | ```php 106 | use BumpCore\EditorPhp\EditorPhp; 107 | use BumpCore\EditorPhp\Block\Block; 108 | use BumpCore\EditorPhp\Blocks\Paragraph; 109 | 110 | $editor = EditorPhp::make($json); 111 | 112 | // Stripping all tags from paragraph block's text. 113 | $editor->blocks->transform(function(Block $block) 114 | { 115 | if($block instanceof Paragraph) 116 | { 117 | $block->set('text', strip_tags($block->get('text'))); 118 | } 119 | 120 | return $block; 121 | }); 122 | ``` 123 | 124 | Blocks are stored as `Illuminate\Support\Collection` . By using collection methods, you can manipulate blocks as you wish. You can learn about collections in [Laravel's documentation](https://laravel.com/docs/10.x/collections). 125 | 126 | ## Rendering HTML 127 | 128 | Rendering HTML is very straightforward. There are multiple ways to render your instance: 129 | 130 | ```php 131 | use BumpCore\EditorPhp\EditorPhp; 132 | 133 | $editor = EditorPhp::make($json); 134 | 135 | // Using the `render` function. 136 | echo $editor->render(); 137 | 138 | // Using the `toHtml` function. 139 | echo $editor->toHtml(); 140 | 141 | // Or by casting to a string. 142 | echo $editor; 143 | ``` 144 | 145 | Again, all three cases are the same, with no one above another. You can use whichever one you like the most. 146 | 147 | By the default, you have two options for the default block's templates; `tailwindcss` and `Bootstrap 5` . Default used template is `tailwindcss` You may switch templates by: 148 | 149 | ```php 150 | use BumpCore\EditorPhp\EditorPhp; 151 | 152 | // Using tailwind. 153 | EditorPhp::useTailwind(); 154 | 155 | // Using Bootstrap. 156 | EditorPhp::useBootstrapFive(); 157 | ``` 158 | 159 | You can learn more about rendering in [creating custom blocks](#creating-custom-blocks) section. 160 | 161 | ## Faking 162 | 163 | You can generate fake data with `EditorPhp` . 164 | 165 | ```php 166 | use BumpCore\EditorPhp\EditorPhp; 167 | 168 | // This will return a generated fake JSON. 169 | $fake = EditorPhp::fake(); 170 | 171 | // If we pass first argument true, it will return new `EditorPhp` instance with fake data. 172 | $fakeEditor = EditorPhp::fake(true); 173 | 174 | // You can also pass min lenght and max lenght of blocks. 175 | // Below code will generate blocks between 1 and 3. 176 | $fakeEditor = EditorPhp::fake(true, 1, 3); 177 | 178 | echo $fakeEditor->render(); 179 | ``` 180 | 181 | You can learn more about generating fake data for the blocks in [fake data generation](#fake-data-generation). 182 | 183 | ## Additional 184 | 185 | ### Converting to an array 186 | 187 | You can convert your instance to an array using the `toArray()` method. 188 | 189 | ```php 190 | use BumpCore\EditorPhp\EditorPhp; 191 | 192 | $editor = EditorPhp::make($json); 193 | 194 | // This will return ['time' => ..., 'blocks' => [...], 'version' => '...'] 195 | $array = $editor->toArray(); 196 | ``` 197 | 198 | ### Converting to JSON 199 | 200 | You can convert your instance to JSON using the `toJson(/** options */)` method. This method is useful when you manipulate your instance. 201 | 202 | ```php 203 | use BumpCore\EditorPhp\EditorPhp; 204 | 205 | $editor = EditorPhp::make($json); 206 | 207 | // This will return encoded JSON. 208 | $json = $editor->toJson(JSON_PRETTY_PRINT); 209 | ``` 210 | 211 | ### Time & Version 212 | 213 | You can access time and version: 214 | 215 | ```php 216 | use BumpCore\EditorPhp\EditorPhp; 217 | 218 | $editor = EditorPhp::make($json); 219 | 220 | $editor->time; 221 | $editor->version; 222 | ``` 223 | 224 | The `time` property is a `Carbon` instance. You can learn more about it in [Carbon's documentation](https://carbon.nesbot.com/docs/). 225 | 226 | ### Macros 227 | 228 | You can register macros and use them later. Macros are based on Laravel. 229 | 230 | ```php 231 | use BumpCore\EditorPhp\EditorPhp; 232 | 233 | // Registering new macro. 234 | EditorPhp::macro( 235 | 'getParagraphs', 236 | fn () => $this->blocks->filter(fn (Block $block) => $block instanceof Paragraph) 237 | ); 238 | 239 | $editor = EditorPhp::make($json); 240 | 241 | // This will return a collection that only contains paragraphs. 242 | $paragraphs = $editor->getParagraphs(); 243 | ``` 244 | 245 | # Blocks 246 | 247 | Blocks are the main building parts of the `EditorPhp` editor. You can manipulate them as you wish, and the best part is that you can use them to store your block's logic. For example, the [image](https://github.com/editor-js/image) block requires an uploader to work. You can implement the corresponding functionality in the `BumpCore\EditorPhp\Blocks\Image` class. 248 | 249 | ## Registering Blocks 250 | 251 | Before we jump into learning how to customize blocks, here's how you can register your blocks: 252 | 253 | ```php 254 | use BumpCore\EditorPhp\EditorPhp; 255 | 256 | // This will merge without erasing already registered blocks. Other blocks will still remain with the recently registered `image` and `paragraph` blocks. 257 | EditorPhp::register([ 258 | 'image' => \Blocks\MyCustomImageBlock::class, 259 | 'paragraph' => \Blocks\MyCustomParagraphBlock::class, 260 | ]); 261 | 262 | // This will override already registered blocks. We now only have `image` and `paragraph` blocks. 263 | EditorPhp::register([ 264 | 'image' => \Blocks\MyCustomImageBlock::class, 265 | 'paragraph' => \Blocks\MyCustomParagraphBlock::class, 266 | ], true); 267 | ``` 268 | 269 | When registering blocks, it's important to use the correct key. The key must be the same as the `Editor.js` 's `type` key. To clarify: 270 | 271 | ```json 272 | { 273 | "time": 1672852569662, 274 | "blocks": [ 275 | { 276 | "type": "paragraph", 277 | "data": { 278 | "text": "..." 279 | } 280 | } 281 | ], 282 | "version": "2.26.4" 283 | } 284 | ``` 285 | 286 | In this output, our type key is `paragraph` , so we should register it as `'paragraph' => Paragraph::class` . This might vary depending on how you register your blocks in `Editor.js` . Default blocks in `EditorPhp` are registered using `camelCase` . 287 | 288 | ## Extending Blocks 289 | 290 | As mentioned previously, almost all blocks are supported in `EditorPhp` . However, they mostly handle the validation of block data and rendering. For the `Image` block to work properly, it requires an upload. We can implement this upload logic in the `Image` class: 291 | 292 | ```php 293 | use BumpCore\EditorPhp\Blocks\Image; 294 | 295 | class MyImageBlock extends Image 296 | { 297 | public static function uploadTemp(string $fileName = 'image'): array 298 | { 299 | // ... 300 | 301 | // Temporary upload logic. 302 | 303 | return [ 304 | 'success' => ..., 305 | 'file' => [ 306 | 'url' => ..., 307 | ], 308 | ]; 309 | } 310 | 311 | public function upload(): void 312 | { 313 | $file = $this->get('file.url'); 314 | 315 | // Your logic. 316 | 317 | // ... 318 | 319 | // Altering the current block's data. 320 | $this->set('file.url', ...); 321 | } 322 | } 323 | 324 | // ... 325 | 326 | // Registering customized block. 327 | EditorPhp::register([ 328 | 'image' => MyImageBlock::class 329 | ]); 330 | ``` 331 | 332 | As you can see, we have extended the `Image` block and added two functions to handle our uploads. 333 | 334 | The `uploadTemp` function performs a temporary file upload. This method is static and can be used anywhere using `Image::uploadTemp()` . It returns the data required by the [image](https://github.com/editor-js/image) tool. 335 | 336 | The `upload` function serves a different purpose. It represents the final upload for the block but is not static. This method assumes that the image has already been uploaded temporarily and the `$json` has been loaded and parsed. Therefore, we can use this function as follows: 337 | 338 | ```php 339 | use BumpCore\EditorPhp\EditorPhp; 340 | use Blocks\MyImageBlock; 341 | 342 | $editor = EditorPhp::make($json); 343 | 344 | $editor->blocks->each(function(Block $block) 345 | { 346 | if ($block instanceof MyImageBlock) 347 | { 348 | $block->upload(); 349 | } 350 | }); 351 | 352 | return $editor->toJson(); 353 | ``` 354 | 355 | Now the block performs the final upload and is saved as JSON. 356 | 357 | ## Creating Custom Blocks 358 | 359 | It is impossible to support all blocks out there, so we can implement our own blocks in an easy way. A standard block looks like the following: 360 | 361 | ```php 362 | use BumpCore\EditorPhp\Block\Block; 363 | 364 | class MyCustomBlock extends Block 365 | { 366 | public function render(): string 367 | { 368 | return view('blocks.my-custom-block', ['data' => $this->data]); 369 | } 370 | } 371 | ``` 372 | 373 | As you can see, by default, we just need to implement the rendering logic. However, there's more than just rendering. 374 | 375 | ## Accessing Block's Data 376 | 377 | There are multiple ways to access a block's data. In the example below, you can see different methods for accessing block data: 378 | 379 | ```php 380 | public function render(): string 381 | { 382 | // ... 383 | 384 | // Method 1: Accessing through the data object. 385 | $data = $this->data; 386 | $data->get('custom.data'); 387 | $data->set('custom.data', 'Hello World!'); 388 | 389 | // Method 2: Accessing by invoking the data object. 390 | $data('custom.data'); // Hello World! 391 | 392 | // Method 3: Using shortcuts. 393 | $this->get('custom.data'); 394 | $this->set('custom.data', 'Nice!'); 395 | 396 | // ... 397 | } 398 | ``` 399 | 400 | You can choose any of the above methods to access and manipulate the block's data. Additionally, you can also check whether the data exists or not using the following methods: 401 | 402 | ```php 403 | $data->has('custom.data'); 404 | // or 405 | $this->has('custom.data'); 406 | ``` 407 | 408 | ## Validating Block Data 409 | 410 | Validating data is not required, but it can make your data safer. Validating block data is quite easy. We just have to add a `rules` method to our block: 411 | 412 | ```php 413 | use BumpCore\EditorPhp\Block\Block; 414 | 415 | class MyCustomBlock extends Block 416 | { 417 | // ... 418 | 419 | public function rules(): array 420 | { 421 | return [ 422 | 'text' => 'required|string|max:255', 423 | 'items' => 'sometimes|array', 424 | 'items.*.name' => 'required|string|max:255', 425 | 'items.*.html' => 'required|string|min:255', 426 | ]; 427 | } 428 | 429 | // ... 430 | } 431 | ``` 432 | 433 | When validating the block's data fails, the data will be empty. Data validation is performed using Laravel's validation library. You can learn more about it in [Laravel's documentation](https://laravel.com/docs/10.x/validation). 434 | 435 | ## Sanitizing Block Data 436 | 437 | You can purify the HTML of your data if you wish. It's important to prevent injections. Purifying data looks much like validation: 438 | 439 | ```php 440 | use BumpCore\EditorPhp\Block\Block; 441 | 442 | class MyCustomBlock extends Block 443 | { 444 | // ... 445 | 446 | public function allow(): array|string 447 | { 448 | // Specifying one by one. 449 | return [ 450 | 'text' => [ 451 | 'a:href,target,title', // Will allow `a` tag and href, target, and title attributes. 452 | 'b', // Will only allow `b` tag with no attributes. 453 | ], 454 | 'items.*.name' => 'b:*', // Will allow `b` tag with all attributes. 455 | 'items.*.html' => '*', // Will allow every tag and every attribute. 456 | ]; 457 | 458 | // Or just allowing all attributes and tags for all data. 459 | return '*'; 460 | } 461 | 462 | // ... 463 | } 464 | ``` 465 | 466 | Unlike validation, purifying will only strip unwanted tags and attributes. 467 | 468 | ## Fake Data Generation 469 | 470 | As we mentioned earlier, we can generate fake data with `EditorPhp` . But it requires to generate each block's own fake data. To generate fake data we should add static method to our block: 471 | 472 | ```php 473 | use BumpCore\EditorPhp\Block\Block; 474 | 475 | class MyCustomBlock extends Block 476 | { 477 | // ... 478 | 479 | public static function fake(\Faker\Generator $faker): array 480 | { 481 | $items = []; 482 | 483 | foreach (range(0, $faker->numberBetween(0, 10)) as $index) 484 | { 485 | $items[] = [ 486 | 'name' => fake()->name(), 487 | 'html' => $faker->randomHtml(), 488 | ]; 489 | } 490 | 491 | return [ 492 | 'text' => fake()->text(255), 493 | 'items' => $items, 494 | ]; 495 | } 496 | 497 | // ... 498 | } 499 | ``` 500 | 501 | By adding `fake` method to our block, now `EditorPhp` will also include `MyCustomBlock` when generating fake data. You can learn more about at [FakerPHP's documentation](https://fakerphp.github.io/). 502 | 503 | # Laravel Only Features 504 | 505 | There's few Laravel features that will make your life little bit easier. 506 | 507 | ## Cast 508 | 509 | You can use `EditorPhpCast` to cast your model's attribute to `EditorPhp` instance. 510 | 511 | ```php 512 | use BumpCore\EditorPhp\Casts\EditorPhpCast; 513 | use Illuminate\Database\Eloquent\Model; 514 | 515 | class Post extends Model 516 | { 517 | protected $casts = [ 518 | 'content' => EditorPhpCast::class, 519 | ]; 520 | } 521 | 522 | // ... 523 | 524 | $post = Post::find(1); 525 | 526 | // Content is `EditorPhp` instance in here. 527 | echo $post->content->render(); 528 | ``` 529 | 530 | Also if you are using cast, you may access your model within block instances: 531 | 532 | ```php 533 | use BumpCore\EditorPhp\Block\Block; 534 | use App\Models\Post; 535 | 536 | class MyBlock extends Block 537 | { 538 | // ... 539 | 540 | public static function render(): string 541 | { 542 | if($this->root->model instanceof Post) 543 | { 544 | // Do the other thing. 545 | } 546 | } 547 | 548 | // ... 549 | } 550 | ``` 551 | 552 | You can also alter the model from the block. 553 | 554 | ## Response 555 | 556 | `EditorPhp` instance can be returned as response. If request expects JSON it will encode it self to JSON. Otherwise it will be rendered into html. 557 | 558 | ```php 559 | namespace App\Http\Controllers; 560 | 561 | use App\Http\Controllers\Controller; 562 | use App\Models\Post; 563 | 564 | class ShowPostController extends Controller 565 | { 566 | public function __invoke(Post $post) 567 | { 568 | // Depending on the request it will return json or rendered html. 569 | return $post->content; 570 | } 571 | } 572 | ``` 573 | 574 | ## Views 575 | 576 | You may also use `EditorPhp` instance to render inside view directly: 577 | 578 | ```blade 579 | {{-- blog.show.blade.php --}} 580 | 581 |
582 |

{{ $post->title }}

583 |
{{ $post->content }}
584 |
585 | ``` 586 | 587 | ## Publishing Views and Configuration 588 | 589 | Got to check this before documenting it. 590 | 591 | ## Commands 592 | 593 | You can create brand new block with `block:make ` command: 594 | 595 | ```bash 596 | php artisan make:block CustomImageBlock 597 | ``` 598 | 599 | New block will be placed under `app/Blocks` directory. 600 | 601 | # Contribution 602 | 603 | Contributions are welcome! If you find a bug or have a suggestion for improvement, please open an issue or create a pull request. Below are some guidelines to follow: 604 | 605 | * Fork the repository and clone it to your local machine. 606 | * Create a new branch for your contribution. 607 | * Make your changes and test them thoroughly. 608 | * Ensure that your code adheres to the existing coding style and conventions. 609 | * Commit your changes and push them to your forked repository. 610 | * Submit a pull request to the main repository. 611 | 612 | Please provide a detailed description of your changes and the problem they solve. Your contribution will be reviewed, and feedback may be provided. Thank you for your help in making this project better! 613 | -------------------------------------------------------------------------------- /art/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bumpcore/editor.php/b0bba499037f12f4f68a71f3be65b75f037a72b6/art/banner.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bumpcore/editor.php", 3 | "description": "PHP integration for Editor.js", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "BumpCore\\EditorPhp\\": "src/" 9 | } 10 | }, 11 | "autoload-dev": { 12 | "psr-4": { 13 | "BumpCore\\EditorPhp\\Tests\\": "tests/" 14 | } 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Abdulkadir Cemiloğlu", 19 | "email": "kadir.cemiloglu1@gmail.com", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.1", 25 | "illuminate/support": "^10.0|^11.0|^12.0", 26 | "illuminate/validation": "^10.0|^11.0|^12.0", 27 | "fakerphp/faker": "^1.21" 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "BumpCore\\EditorPhp\\EditorPhpServiceProvider" 33 | ] 34 | } 35 | }, 36 | "require-dev": { 37 | "pestphp/pest": "^1.22.4|^2.0|^3.0", 38 | "orchestra/testbench": "^8.0|^9.0|^10.0", 39 | "friendsofphp/php-cs-fixer": "^3.14.4", 40 | "phpstan/phpstan": "^1.10" 41 | }, 42 | "config": { 43 | "allow-plugins": { 44 | "pestphp/pest-plugin": true 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/editor.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'attaches' => BumpCore\EditorPhp\Blocks\Attaches::class, 6 | 'checklist' => BumpCore\EditorPhp\Blocks\Checklist::class, 7 | 'code' => BumpCore\EditorPhp\Blocks\Code::class, 8 | 'delimiter' => BumpCore\EditorPhp\Blocks\Delimiter::class, 9 | 'embed' => BumpCore\EditorPhp\Blocks\Embed::class, 10 | 'header' => BumpCore\EditorPhp\Blocks\Header::class, 11 | 'image' => BumpCore\EditorPhp\Blocks\Image::class, 12 | 'linkTool' => BumpCore\EditorPhp\Blocks\LinkTool::class, 13 | 'list' => BumpCore\EditorPhp\Blocks\ListBlock::class, 14 | 'paragraph' => BumpCore\EditorPhp\Blocks\Paragraph::class, 15 | 'personality' => BumpCore\EditorPhp\Blocks\Personality::class, 16 | 'quote' => BumpCore\EditorPhp\Blocks\Quote::class, 17 | 'raw' => BumpCore\EditorPhp\Blocks\Raw::class, 18 | 'table' => BumpCore\EditorPhp\Blocks\Table::class, 19 | 'warning' => BumpCore\EditorPhp\Blocks\Warning::class, 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - config 4 | - resources 5 | - src 6 | - tests 7 | level: 4 8 | ignoreErrors: 9 | - "#Unsafe usage of new static#" 10 | - "#Class BumpCore\\\\EditorPhp\\\\EditorPhp has an uninitialized readonly property \\$model. Assign it in the constructor.#" 11 | - "#Readonly property BumpCore\\\\EditorPhp\\\\EditorPhp::\\$time is already assigned.#" 12 | - "#Readonly property BumpCore\\\\EditorPhp\\\\EditorPhp::\\$model is assigned outside of the constructor.#" 13 | - "#Undefined variable: \\$this#" 14 | - "#Call to an undefined method BumpCore\\\\EditorPhp\\\\EditorPhp::getParagraphs\\(\\).#" 15 | - "#Variable \\$data might not be defined.#" 16 | - "#Unreachable statement - code above always terminates.#" 17 | reportUnmatchedIgnoredErrors: false 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./app 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bumpcore/editor.php/b0bba499037f12f4f68a71f3be65b75f037a72b6/resources/.gitkeep -------------------------------------------------------------------------------- /resources/php/bootstrap-five/attaches.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 10 | 11 | 19 | 23 | 27 | 28 | 29 |

30 |

MiB

31 |
32 |
33 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/checklist.php: -------------------------------------------------------------------------------- 1 |
5 | 6 |
  • 7 | 8 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
  • 27 | 28 |
    29 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/code.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/delimiter.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/embed.php: -------------------------------------------------------------------------------- 1 |
    2 | 9 | 10 |

    11 | 12 | 13 |

    14 | 15 |

    16 | 17 | 22 | 23 |

    24 | 25 |
    26 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/header.php: -------------------------------------------------------------------------------- 1 | 3 |

    4 | 5 | 6 | 7 |

    8 | 9 | 10 | 11 |

    12 | 13 | 14 | 15 |

    16 | 17 | 18 | 19 |
    20 | 21 | 22 | 23 |
    24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/image.php: -------------------------------------------------------------------------------- 1 | <?= $data('caption'); ?> 6 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/linktool.php: -------------------------------------------------------------------------------- 1 |
    2 | <?= $data('meta.title'); ?> 8 |
    9 |

    10 |

    11 | 12 | 17 | 18 |

    19 |

    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/list.php: -------------------------------------------------------------------------------- 1 | 2 |
      3 | 4 |
    1. 5 | 6 |
    7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/paragraph.php: -------------------------------------------------------------------------------- 1 |

    2 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/personality.php: -------------------------------------------------------------------------------- 1 |
    2 | <?= $data('name'); ?> 8 |
    9 |

    10 |

    11 | 12 | 17 | 18 |

    19 |

    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/quote.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 10 | 13 | 14 |
    15 |
    16 |

    17 | 18 |
    19 |
    20 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/table.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $row): ?> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 | -------------------------------------------------------------------------------- /resources/php/bootstrap-five/warning.php: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /resources/php/tailwind/attaches.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    5 | 10 | 11 | 19 | 23 | 27 | 28 | 29 |

    30 |

    MiB

    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /resources/php/tailwind/checklist.php: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /resources/php/tailwind/code.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 | -------------------------------------------------------------------------------- /resources/php/tailwind/delimiter.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/php/tailwind/embed.php: -------------------------------------------------------------------------------- 1 |
    2 | 9 | 10 |

    11 | 12 | 13 |

    14 | 15 |

    16 | 17 | 22 | 23 |

    24 | 25 |
    26 | -------------------------------------------------------------------------------- /resources/php/tailwind/header.php: -------------------------------------------------------------------------------- 1 | 3 |

    4 | 5 | 6 | 7 |

    8 | 9 | 10 | 11 |

    12 | 13 | 14 | 15 |

    16 | 17 | 18 | 19 |
    20 | 21 | 22 | 23 |
    24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/php/tailwind/image.php: -------------------------------------------------------------------------------- 1 | <?= $data('caption'); ?> 6 | -------------------------------------------------------------------------------- /resources/php/tailwind/linktool.php: -------------------------------------------------------------------------------- 1 |
    2 | <?= $data('meta.title'); ?> 7 |
    8 |

    9 |

    10 | 11 | 16 | 17 |

    18 |

    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /resources/php/tailwind/list.php: -------------------------------------------------------------------------------- 1 | 2 |
      3 | 4 |
    1. 5 | 6 |
    7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /resources/php/tailwind/paragraph.php: -------------------------------------------------------------------------------- 1 |

    2 | -------------------------------------------------------------------------------- /resources/php/tailwind/personality.php: -------------------------------------------------------------------------------- 1 |
    2 | <?= $data('name'); ?> 7 |
    8 |

    9 |

    10 | 11 | 16 | 17 |

    18 |

    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /resources/php/tailwind/quote.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 8 | 11 | 12 |
    13 |
    14 |

    15 | 16 |
    17 |
    18 | -------------------------------------------------------------------------------- /resources/php/tailwind/table.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $row): ?> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 | -------------------------------------------------------------------------------- /resources/php/tailwind/warning.php: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/attaches.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    {!! $data('file.extension') !!}
    3 |
    4 |

    5 | 10 | {!! $data('title') ?? $data('file.name') !!} 11 | 19 | 23 | 27 | 28 | 29 |

    30 |

    {!! number_format($data('file.size') * 0.000001, 2) !!}MiB

    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/checklist.blade.php: -------------------------------------------------------------------------------- 1 |
    5 | @foreach ($data('items', []) as $item) 6 |
  • 7 | 8 | 15 | 18 | 19 | 20 | 21 | @if ($item['checked']) 22 | {!! $item['text'] !!} 23 | @else 24 | {!! $item['text'] !!} 25 | @endif 26 |
  • 27 | @endforeach 28 |
    29 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/code.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    {!! $data('code') !!}
    4 |
    5 |
    6 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/delimiter.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/embed.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 9 | 10 |

    11 | {!! $data('caption') !!} 12 |

    13 | 14 |

    15 | 16 | {!! $data('source') !!} 21 | 22 |

    23 | 24 |
    25 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/header.blade.php: -------------------------------------------------------------------------------- 1 | @switch($data('level')) 2 | @case(1) 3 |

    {!! $data('text') !!}

    4 | @break 5 | 6 | @case(2) 7 |

    {!! $data('text') !!}

    8 | @break 9 | 10 | @case(3) 11 |

    {!! $data('text') !!}

    12 | @break 13 | 14 | @case(4) 15 |

    {!! $data('text') !!}

    16 | @break 17 | 18 | @case(5) 19 |
    {!! $data('text') !!}
    20 | @break 21 | 22 | @case(6) 23 |
    {!! $data('text') !!}
    24 | @break 25 | @endswitch 26 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/image.blade.php: -------------------------------------------------------------------------------- 1 | {!! $data('caption') !!} 6 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/linktool.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {!! $data('meta.title') !!} 8 |
    9 |

    {!! $data('meta.title') !!}

    10 |

    11 | 12 | {!! $data('link') !!} 17 | 18 |

    19 |

    {!! $data('meta.description') !!}

    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/list.blade.php: -------------------------------------------------------------------------------- 1 | @if ($data('style') === 'ordered') 2 |
      3 | @foreach ($data('items', []) as $item) 4 |
    1. {!! $item !!}
    2. 5 | @endforeach 6 |
    7 | @else 8 | 13 | @endif 14 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/paragraph.blade.php: -------------------------------------------------------------------------------- 1 |

    {!! $data('text') !!}

    2 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/personality.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {!! $data('name') !!} 8 |
    9 |

    {!! $data('name') !!}

    10 |

    11 | 12 | {!! $data('link') !!} 17 | 18 |

    19 |

    {!! $data('description') !!}

    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/quote.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 10 | 13 | 14 |
    15 |
    16 |

    {!! $data('text') !!}

    17 | {!! $data('caption') !!} 18 |
    19 |
    20 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/table.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | @if ($data('withHeadings') && ($headings = $data('content')[array_key_first($data('content'))])) 4 | 5 | 6 | @foreach ($headings as $heading) 7 | 8 | @endforeach 9 | 10 | 11 | @endif 12 | 13 | 14 | @foreach ($data('content', []) as $index => $row) 15 | @if ($data('withHeadings') && array_key_first($data('content')) === $index) 16 | @continue 17 | @endif 18 | 19 | 20 | @foreach ($row as $cell) 21 | 22 | @endforeach 23 | 24 | @endforeach 25 | 26 |
    {!! $heading !!}
    {!! $cell !!}
    27 |
    28 | -------------------------------------------------------------------------------- /resources/views/bootstrap-five/warning.blade.php: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /resources/views/tailwind/attaches.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    {!! $data('file.extension') !!}
    3 |
    4 |

    5 | 10 | {!! $data('title') ?? $data('file.name') !!} 11 | 19 | 23 | 27 | 28 | 29 |

    30 |

    {!! number_format($data('file.size') * 0.000001, 2) !!}MiB

    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /resources/views/tailwind/checklist.blade.php: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /resources/views/tailwind/code.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    {!! $data('code') !!}
    4 |
    5 |
    6 | -------------------------------------------------------------------------------- /resources/views/tailwind/delimiter.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/tailwind/embed.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 9 | 10 |

    11 | {!! $data('caption') !!} 12 |

    13 | 14 |

    15 | 16 | {!! $data('source') !!} 21 | 22 |

    23 | 24 |
    25 | -------------------------------------------------------------------------------- /resources/views/tailwind/header.blade.php: -------------------------------------------------------------------------------- 1 | @switch($data('level')) 2 | @case(1) 3 |

    {!! $data('text') !!}

    4 | @break 5 | 6 | @case(2) 7 |

    {!! $data('text') !!}

    8 | @break 9 | 10 | @case(3) 11 |

    {!! $data('text') !!}

    12 | @break 13 | 14 | @case(4) 15 |

    {!! $data('text') !!}

    16 | @break 17 | 18 | @case(5) 19 |
    {!! $data('text') !!}
    20 | @break 21 | 22 | @case(6) 23 |
    {!! $data('text') !!}
    24 | @break 25 | @endswitch 26 | -------------------------------------------------------------------------------- /resources/views/tailwind/image.blade.php: -------------------------------------------------------------------------------- 1 | {!! $data('caption') !!} 6 | -------------------------------------------------------------------------------- /resources/views/tailwind/linktool.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {!! $data('meta.title') !!} 7 |
    8 |

    {!! $data('meta.title') !!}

    9 |

    10 | 11 | {!! $data('link') !!} 16 | 17 |

    18 |

    {!! $data('meta.description') !!}

    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /resources/views/tailwind/list.blade.php: -------------------------------------------------------------------------------- 1 | @if ($data('style') === 'ordered') 2 |
      3 | @foreach ($data('items', []) as $item) 4 |
    1. {!! $item !!}
    2. 5 | @endforeach 6 |
    7 | @else 8 | 13 | @endif 14 | -------------------------------------------------------------------------------- /resources/views/tailwind/paragraph.blade.php: -------------------------------------------------------------------------------- 1 |

    {!! $data('text') !!}

    2 | -------------------------------------------------------------------------------- /resources/views/tailwind/personality.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {!! $data('name') !!} 7 |
    8 |

    {!! $data('name') !!}

    9 |

    10 | 11 | {!! $data('link') !!} 16 | 17 |

    18 |

    {!! $data('description') !!}

    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /resources/views/tailwind/quote.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 8 | 11 | 12 |
    13 |
    14 |

    {!! $data('text') !!}

    15 | {!! $data('caption') !!} 16 |
    17 |
    18 | -------------------------------------------------------------------------------- /resources/views/tailwind/table.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | @if ($data('withHeadings') && ($headings = $data('content')[array_key_first($data('content'))])) 4 | 5 | 6 | @foreach ($headings as $heading) 7 | 8 | @endforeach 9 | 10 | 11 | @endif 12 | 13 | 14 | @foreach ($data('content', []) as $index => $row) 15 | @if ($data('withHeadings') && array_key_first($data('content')) === $index) 16 | @continue 17 | @endif 18 | 19 | 20 | @foreach ($row as $cell) 21 | 22 | @endforeach 23 | 24 | @endforeach 25 | 26 |
    {!! $heading !!}
    {!! $cell !!}
    27 |
    28 | -------------------------------------------------------------------------------- /resources/views/tailwind/warning.blade.php: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/Block/Block.php: -------------------------------------------------------------------------------- 1 | type = array_flip(Parser::$blocks)[static::class]; 65 | $this->root = $root; 66 | $this->data = new Data($data, $this->allows(), $this->rules()); 67 | } 68 | 69 | /** 70 | * Tag allow list for purifying data. 71 | * 72 | * @return array|string 73 | */ 74 | public function allows(): array|string 75 | { 76 | return '*'; 77 | } 78 | 79 | /** 80 | * Rules to validate data of the block. 81 | * 82 | * @return array 83 | */ 84 | public function rules(): array 85 | { 86 | return []; 87 | } 88 | 89 | /** 90 | * Gets block data by given key. 91 | * 92 | * @param string $key 93 | * @param mixed $default 94 | * 95 | * @return mixed 96 | */ 97 | public function get(string $key, mixed $default = null): mixed 98 | { 99 | return $this->data->get($key, $default); 100 | } 101 | 102 | /** 103 | * Sets block data by given key. 104 | * 105 | * @param string $key 106 | * @param mixed $value 107 | * 108 | * @return $this 109 | */ 110 | public function set(string $key, mixed $value): self 111 | { 112 | $this->data->set($key, $value); 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Check if an item or items exist in the data. 119 | * 120 | * @param array|string $key 121 | * 122 | * @return bool 123 | */ 124 | public function has(string|array $key): bool 125 | { 126 | return $this->data->has($key); 127 | } 128 | 129 | /** 130 | * Converts the `Block` as an array. 131 | * 132 | * @return array 133 | */ 134 | public function toArray(): array 135 | { 136 | return [ 137 | 'type' => $this->type, 138 | 'data' => $this->data->toArray(), 139 | ]; 140 | } 141 | 142 | /** 143 | * Renders block into HTML. 144 | * 145 | * @return string 146 | */ 147 | public function toHtml() 148 | { 149 | return $this->render(); 150 | } 151 | 152 | /** 153 | * Renders block into HTML. 154 | * 155 | * @return string 156 | */ 157 | public function __toString(): string 158 | { 159 | return $this->render(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Block/Data.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected array $data; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param array $data 23 | * @param array|string $allows 24 | * @param array $rules 25 | * 26 | * @return void 27 | */ 28 | public function __construct(array $data = [], array|string $allows = '*', array $rules = []) 29 | { 30 | $this->data = $this->purify( 31 | $this->validate($data, $rules), 32 | $allows, 33 | ); 34 | } 35 | 36 | /** 37 | * Gets data by given key. 38 | * 39 | * @param string $key 40 | * @param mixed $default 41 | * 42 | * @return mixed 43 | */ 44 | public function __invoke(string $key, mixed $default = null): mixed 45 | { 46 | return $this->get($key, $default); 47 | } 48 | 49 | /** 50 | * Gets data by given key. 51 | * 52 | * @param string $key 53 | * @param mixed $default 54 | * 55 | * @return mixed 56 | */ 57 | public function get(string $key, mixed $default = null): mixed 58 | { 59 | return Arr::get($this->data, $key, $default); 60 | } 61 | 62 | /** 63 | * Sets a data by given key. 64 | * 65 | * @param string $key 66 | * @param mixed $value 67 | * 68 | * @return void 69 | */ 70 | public function set(string $key, mixed $value): void 71 | { 72 | Arr::set($this->data, $key, $value); 73 | } 74 | 75 | /** 76 | * Check if an item or items exist in the data. 77 | * 78 | * @param array|string $key 79 | * 80 | * @return bool 81 | */ 82 | public function has(string|array $key): bool 83 | { 84 | return Arr::has($this->data, $key); 85 | } 86 | 87 | /** 88 | * Validates raw data. 89 | * 90 | * @return array 91 | */ 92 | protected function validate(array $data, array $rules): array 93 | { 94 | $validator = Helpers::makeValidator($data, $rules); 95 | 96 | if ($validator->fails()) 97 | { 98 | return []; 99 | } 100 | 101 | return $validator->validated(); 102 | } 103 | 104 | /** 105 | * Purifies validated data. 106 | * 107 | * @param array $data 108 | * @param array|string $allows 109 | * 110 | * @return array 111 | */ 112 | protected function purify(array $data, array|string $allows) 113 | { 114 | // Allows all tags and attributes for all fields. 115 | if ($allows === '*') 116 | { 117 | return $data; 118 | } 119 | 120 | $allows = $this->parseAllows($allows); 121 | $data = Arr::dot($data); 122 | 123 | foreach ($data as $key => $value) 124 | { 125 | if (!is_string($value)) 126 | { 127 | continue; 128 | } 129 | 130 | // Find matching key E.g. `content*.*.text` = `content.1.5.text` 131 | $allow = Arr::first(Arr::where( 132 | $allows, 133 | // Below regex taken from `https://github.com/laravel/framework/blob/46ac3ec77ed4b07e3c6e47f97979822696bb7f1d/src/Illuminate/Validation/ValidationData.php#L57` 134 | fn ($tags, $attribute) => (bool) preg_match('/^' . str_replace('\*', '[^\.]+', preg_quote($attribute)) . '/', $key, $matches) 135 | )); 136 | 137 | if ($allow) 138 | { 139 | // Allows all tags and attributes for field. 140 | if ($allow === '*') 141 | { 142 | continue; 143 | } 144 | 145 | $tags = array_keys($allow); 146 | 147 | $purified = Purifier::stripTags($value, $tags); 148 | 149 | foreach ($tags as $tag) 150 | { 151 | $attributes = $allow[$tag]; 152 | 153 | // Allows all attributes. 154 | if (Arr::first($attributes) === '*') 155 | { 156 | continue; 157 | } 158 | 159 | $purified = Purifier::stripAttributes($purified, $tag, $attributes); 160 | } 161 | 162 | $data[$key] = $purified; 163 | } 164 | } 165 | 166 | return Arr::undot($data); 167 | } 168 | 169 | /** 170 | * Parses following syntax: `a:href,class,title`. 171 | * 172 | * @param array $allows 173 | * 174 | * @return array 175 | */ 176 | protected function parseAllows(array $allows): array 177 | { 178 | return Arr::map($allows, function(array|string $tags) { 179 | if ($tags === '*') 180 | { 181 | return $tags; 182 | } 183 | 184 | $tags = Arr::wrap($tags); 185 | 186 | $parsed = []; 187 | 188 | foreach ($tags as $allow) 189 | { 190 | $allow = explode(':', $allow); 191 | 192 | // First key is tag. 193 | $tag = Arr::first(array_slice($allow, 0, 1)); 194 | 195 | // Second key is attributes. 196 | $attributes = array_filter(explode( 197 | ',', 198 | Arr::first(array_slice($allow, 1, 2), null, '') 199 | )); 200 | 201 | $parsed[$tag] = $attributes; 202 | } 203 | 204 | return $parsed; 205 | }); 206 | } 207 | 208 | /** 209 | * Converts the `Data` as an array. 210 | * 211 | * @return array 212 | */ 213 | public function toArray(): array 214 | { 215 | return $this->data; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Blocks/Attaches.php: -------------------------------------------------------------------------------- 1 | [], 21 | 'file.url' => [], 22 | 'file.name' => [], 23 | 'file.extension' => [], 24 | ]; 25 | } 26 | 27 | /** 28 | * Rules to validate data of the block. 29 | * 30 | * @return array 31 | */ 32 | public function rules(): array 33 | { 34 | return [ 35 | 'title' => 'string', 36 | 'file.url' => 'url', 37 | 'file.size' => 'numeric', 38 | 'file.name' => 'string', 39 | 'file.extension' => 'string', 40 | ]; 41 | } 42 | 43 | /** 44 | * Renderer for the block. 45 | * 46 | * @return string 47 | */ 48 | public function render(): string 49 | { 50 | if (View::getFacadeRoot()) 51 | { 52 | return view(sprintf('editor.php::%s.attaches', EditorPhp::usingTemplate())) 53 | ->with(['data' => $this->data]) 54 | ->render(); 55 | } 56 | 57 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/attaches.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 58 | } 59 | 60 | /** 61 | * Generates fake data for the block. 62 | * 63 | * @param \Faker\Generator $faker 64 | * 65 | * @return array 66 | */ 67 | public static function fake(\Faker\Generator $faker): array 68 | { 69 | return [ 70 | 'title' => $faker->text(64), 71 | 'file' => [ 72 | 'url' => $faker->url(), 73 | 'size' => $faker->numberBetween(250000, 10000000), 74 | 'name' => $faker->text(64), 75 | 'extension' => $faker->fileExtension(), 76 | ], 77 | ]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Blocks/Checklist.php: -------------------------------------------------------------------------------- 1 | [], 21 | ]; 22 | } 23 | 24 | /** 25 | * Rules to validate data of the block. 26 | * 27 | * @return array 28 | */ 29 | public function rules(): array 30 | { 31 | return [ 32 | 'items' => 'array', 33 | 'items.*' => 'array', 34 | 'items.*.text' => 'string', 35 | 'items.*.checked' => 'boolean', 36 | ]; 37 | } 38 | 39 | /** 40 | * Renderer for the block. 41 | * 42 | * @return string 43 | */ 44 | public function render(): string 45 | { 46 | if (View::getFacadeRoot()) 47 | { 48 | return view(sprintf('editor.php::%s.checklist', EditorPhp::usingTemplate())) 49 | ->with(['data' => $this->data]) 50 | ->render(); 51 | } 52 | 53 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/checklist.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 54 | } 55 | 56 | /** 57 | * Generates fake data for the block. 58 | * 59 | * @param \Faker\Generator $faker 60 | * 61 | * @return array 62 | */ 63 | public static function fake(\Faker\Generator $faker): array 64 | { 65 | $items = []; 66 | 67 | foreach (range(0, $faker->numberBetween(1, 10)) as $index) 68 | { 69 | $items[] = [ 70 | 'text' => $faker->text(48), 71 | 'checked' => $faker->boolean(), 72 | ]; 73 | } 74 | 75 | return ['items' => $items]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Blocks/Code.php: -------------------------------------------------------------------------------- 1 | '*', 21 | ]; 22 | } 23 | 24 | /** 25 | * Rules to validate data of the block. 26 | * 27 | * @return array 28 | */ 29 | public function rules(): array 30 | { 31 | return [ 32 | 'code' => 'string', 33 | ]; 34 | } 35 | 36 | /** 37 | * Renderer for the block. 38 | * 39 | * @return string 40 | */ 41 | public function render(): string 42 | { 43 | if (View::getFacadeRoot()) 44 | { 45 | return view(sprintf('editor.php::%s.code', EditorPhp::usingTemplate())) 46 | ->with(['data' => $this->data]) 47 | ->render(); 48 | } 49 | 50 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/code.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 51 | } 52 | 53 | /** 54 | * Generates fake data for the block. 55 | * 56 | * @param \Faker\Generator $faker 57 | * 58 | * @return array 59 | */ 60 | public static function fake(\Faker\Generator $faker): array 61 | { 62 | return ['code' => $faker->text()]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Blocks/Delimiter.php: -------------------------------------------------------------------------------- 1 | with(['data' => $this->data]) 43 | ->render(); 44 | } 45 | 46 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/delimiter.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 47 | } 48 | 49 | /** 50 | * Generates fake data for the block. 51 | * 52 | * @param \Faker\Generator $faker 53 | * 54 | * @return array 55 | */ 56 | public static function fake(\Faker\Generator $faker): array 57 | { 58 | return []; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Blocks/Embed.php: -------------------------------------------------------------------------------- 1 | [], 21 | 'source' => [], 22 | 'embed' => [], 23 | 'caption' => [], 24 | ]; 25 | } 26 | 27 | /** 28 | * Rules to validate data of the block. 29 | * 30 | * @return array 31 | */ 32 | public function rules(): array 33 | { 34 | return [ 35 | 'service' => 'string', 36 | 'source' => 'url', 37 | 'embed' => 'url', 38 | 'width' => 'numeric', 39 | 'height' => 'numeric', 40 | 'caption' => 'string', 41 | ]; 42 | } 43 | 44 | /** 45 | * Renderer for the block. 46 | * 47 | * @return string 48 | */ 49 | public function render(): string 50 | { 51 | if (View::getFacadeRoot()) 52 | { 53 | return view(sprintf('editor.php::%s.embed', EditorPhp::usingTemplate())) 54 | ->with(['data' => $this->data]) 55 | ->render(); 56 | } 57 | 58 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/embed.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 59 | } 60 | 61 | /** 62 | * Generates fake data for the block. 63 | * 64 | * @param \Faker\Generator $faker 65 | * 66 | * @return array 67 | */ 68 | public static function fake(\Faker\Generator $faker): array 69 | { 70 | return [ 71 | 'service' => $faker->text(32), 72 | 'source' => $faker->url(), 73 | 'embed' => $faker->url(), 74 | 'width' => $faker->numberBetween(64, 1024), 75 | 'height' => $faker->numberBetween(64, 1024), 76 | 'caption' => $faker->text(32), 77 | ]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Blocks/Header.php: -------------------------------------------------------------------------------- 1 | [], 21 | ]; 22 | } 23 | 24 | /** 25 | * Rules to validate data of the block. 26 | * 27 | * @return array 28 | */ 29 | public function rules(): array 30 | { 31 | return [ 32 | 'text' => 'string', 33 | 'level' => 'integer|min:1|max:6', 34 | ]; 35 | } 36 | 37 | /** 38 | * Renderer for the block. 39 | * 40 | * @return string 41 | */ 42 | public function render(): string 43 | { 44 | if (View::getFacadeRoot()) 45 | { 46 | return view(sprintf('editor.php::%s.header', EditorPhp::usingTemplate())) 47 | ->with(['data' => $this->data]) 48 | ->render(); 49 | } 50 | 51 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/header.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 52 | } 53 | 54 | /** 55 | * Generates fake data for the block. 56 | * 57 | * @param \Faker\Generator $faker 58 | * 59 | * @return array 60 | */ 61 | public static function fake(\Faker\Generator $faker): array 62 | { 63 | return [ 64 | 'text' => $faker->text(64), 65 | 'level' => $faker->numberBetween(1, 6), 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Blocks/Image.php: -------------------------------------------------------------------------------- 1 | [], 21 | 'caption' => [], 22 | ]; 23 | } 24 | 25 | /** 26 | * Rules to validate data of the block. 27 | * 28 | * @return array 29 | */ 30 | public function rules(): array 31 | { 32 | return [ 33 | 'file.url' => 'url', 34 | 'caption' => 'string', 35 | 'withBorder' => 'boolean', 36 | 'stretched' => 'boolean', 37 | 'withBackground' => 'boolean', 38 | ]; 39 | } 40 | 41 | /** 42 | * Renderer for the block. 43 | * 44 | * @return string 45 | */ 46 | public function render(): string 47 | { 48 | if (View::getFacadeRoot()) 49 | { 50 | return view(sprintf('editor.php::%s.image', EditorPhp::usingTemplate())) 51 | ->with(['data' => $this->data]) 52 | ->render(); 53 | } 54 | 55 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/image.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 56 | } 57 | 58 | /** 59 | * Generates fake data for the block. 60 | * 61 | * @param \Faker\Generator $faker 62 | * 63 | * @return array 64 | */ 65 | public static function fake(\Faker\Generator $faker): array 66 | { 67 | return [ 68 | 'file' => ['url' => 'https://picsum.photos/200/300'], 69 | 'caption' => $faker->text(), 70 | 'withBorder' => $faker->boolean(), 71 | 'stretched' => $faker->boolean(), 72 | 'withBackground' => $faker->boolean(), 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Blocks/LinkTool.php: -------------------------------------------------------------------------------- 1 | [], 21 | 'meta.title' => [], 22 | 'meta.site_name' => [], 23 | 'meta.description' => [], 24 | 'meta.image.url' => [], 25 | ]; 26 | } 27 | 28 | /** 29 | * Rules to validate data of the block. 30 | * 31 | * @return array 32 | */ 33 | public function rules(): array 34 | { 35 | return [ 36 | 'link' => 'url', 37 | 'meta.title' => 'string', 38 | 'meta.site_name' => 'string', 39 | 'meta.description' => 'string', 40 | 'meta.image.url' => 'url', 41 | ]; 42 | } 43 | 44 | /** 45 | * Renderer for the block. 46 | * 47 | * @return string 48 | */ 49 | public function render(): string 50 | { 51 | if (View::getFacadeRoot()) 52 | { 53 | return view(sprintf('editor.php::%s.linktool', EditorPhp::usingTemplate())) 54 | ->with(['data' => $this->data]) 55 | ->render(); 56 | } 57 | 58 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/linktool.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 59 | } 60 | 61 | /** 62 | * Generates fake data for the block. 63 | * 64 | * @param \Faker\Generator $faker 65 | * 66 | * @return array 67 | */ 68 | public static function fake(\Faker\Generator $faker): array 69 | { 70 | return [ 71 | 'link' => $faker->url(), 72 | 'meta' => [ 73 | 'title' => $faker->text(32), 74 | 'site_name' => $faker->text(32), 75 | 'description' => $faker->text(96), 76 | 'image' => ['url' => 'https://picsum.photos/200/300'], 77 | ], 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Blocks/ListBlock.php: -------------------------------------------------------------------------------- 1 | [], 22 | 'item.*' => [], 23 | ]; 24 | } 25 | 26 | /** 27 | * Rules to validate data of the block. 28 | * 29 | * @return array 30 | */ 31 | public function rules(): array 32 | { 33 | return [ 34 | 'style' => ['string', Rule::in(['ordered', 'unordered'])], 35 | 'items' => 'array', 36 | 'item.*' => 'string', 37 | ]; 38 | } 39 | 40 | /** 41 | * Renderer for the block. 42 | * 43 | * @return string 44 | */ 45 | public function render(): string 46 | { 47 | if (View::getFacadeRoot()) 48 | { 49 | return view(sprintf('editor.php::%s.list', EditorPhp::usingTemplate())) 50 | ->with(['data' => $this->data]) 51 | ->render(); 52 | } 53 | 54 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/list.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 55 | } 56 | 57 | /** 58 | * Generates fake data for the block. 59 | * 60 | * @param \Faker\Generator $faker 61 | * 62 | * @return array 63 | */ 64 | public static function fake(\Faker\Generator $faker): array 65 | { 66 | $items = []; 67 | 68 | foreach (range(0, $faker->numberBetween(1, 10)) as $index) 69 | { 70 | $items[] = $faker->text(64); 71 | } 72 | 73 | return [ 74 | 'style' => $faker->randomElement(['ordered', 'unordered']), 75 | 'items' => $items, 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Blocks/Paragraph.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'a:href,target,title', 22 | 'abbr:title', 23 | 'b', 24 | 'cite', 25 | 'code', 26 | 'em', 27 | 'i', 28 | 'kbd', 29 | 'q', 30 | 'samp', 31 | 'small', 32 | 'strong', 33 | 'sub', 34 | 'sup', 35 | 'time:datetime', 36 | 'var', 37 | 'u', 38 | 's', 39 | 'del', 40 | 'ins', 41 | 'strike', 42 | 'mark', 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * Rules to validate data of the block. 49 | * 50 | * @return array 51 | */ 52 | public function rules(): array 53 | { 54 | return [ 55 | 'text' => 'string', 56 | ]; 57 | } 58 | 59 | /** 60 | * Renderer for the block. 61 | * 62 | * @return string 63 | */ 64 | public function render(): string 65 | { 66 | if (View::getFacadeRoot()) 67 | { 68 | return view(sprintf('editor.php::%s.paragraph', EditorPhp::usingTemplate())) 69 | ->with(['data' => $this->data]) 70 | ->render(); 71 | } 72 | 73 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/paragraph.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 74 | } 75 | 76 | /** 77 | * Generates fake data for the block. 78 | * 79 | * @param \Faker\Generator $faker 80 | * 81 | * @return array 82 | */ 83 | public static function fake(\Faker\Generator $faker): array 84 | { 85 | return ['text' => $faker->text(256)]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Blocks/Personality.php: -------------------------------------------------------------------------------- 1 | [], 21 | 'description' => [], 22 | 'link' => [], 23 | 'photo' => [], 24 | ]; 25 | } 26 | 27 | /** 28 | * Rules to validate data of the block. 29 | * 30 | * @return array 31 | */ 32 | public function rules(): array 33 | { 34 | return [ 35 | 'name' => 'string', 36 | 'description' => 'string', 37 | 'link' => 'url', 38 | 'photo' => 'url', 39 | ]; 40 | } 41 | 42 | /** 43 | * Renderer for the block. 44 | * 45 | * @return string 46 | */ 47 | public function render(): string 48 | { 49 | if (View::getFacadeRoot()) 50 | { 51 | return view(sprintf('editor.php::%s.personality', EditorPhp::usingTemplate())) 52 | ->with(['data' => $this->data]) 53 | ->render(); 54 | } 55 | 56 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/personality.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 57 | } 58 | 59 | /** 60 | * Generates fake data for the block. 61 | * 62 | * @param \Faker\Generator $faker 63 | * 64 | * @return array 65 | */ 66 | public static function fake(\Faker\Generator $faker): array 67 | { 68 | return [ 69 | 'name' => $faker->name(), 70 | 'description' => $faker->text(), 71 | 'link' => $faker->url(), 72 | 'photo' => 'https://picsum.photos/200/300', 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Blocks/Quote.php: -------------------------------------------------------------------------------- 1 | [], 22 | 'caption' => [], 23 | 'alignment' => [], 24 | ]; 25 | } 26 | 27 | /** 28 | * Rules to validate data of the block. 29 | * 30 | * @return array 31 | */ 32 | public function rules(): array 33 | { 34 | return [ 35 | 'text' => 'string', 36 | 'caption' => 'string', 37 | 'alignment' => ['string', Rule::in(['left', 'center'])], 38 | ]; 39 | } 40 | 41 | /** 42 | * Renderer for the block. 43 | * 44 | * @return string 45 | */ 46 | public function render(): string 47 | { 48 | if (View::getFacadeRoot()) 49 | { 50 | return view(sprintf('editor.php::%s.quote', EditorPhp::usingTemplate())) 51 | ->with(['data' => $this->data]) 52 | ->render(); 53 | } 54 | 55 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/quote.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 56 | } 57 | 58 | /** 59 | * Generates fake data for the block. 60 | * 61 | * @param \Faker\Generator $faker 62 | * 63 | * @return array 64 | */ 65 | public static function fake(\Faker\Generator $faker): array 66 | { 67 | return [ 68 | 'text' => $faker->text(), 69 | 'caption' => $faker->name(), 70 | 'alignment' => $faker->randomElement(['left', 'center']), 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Blocks/Raw.php: -------------------------------------------------------------------------------- 1 | '*', 18 | ]; 19 | } 20 | 21 | /** 22 | * Rules to validate data of the block. 23 | * 24 | * @return array 25 | */ 26 | public function rules(): array 27 | { 28 | return [ 29 | 'html' => 'string', 30 | ]; 31 | } 32 | 33 | /** 34 | * Renderer for the block. 35 | * 36 | * @return string 37 | */ 38 | public function render(): string 39 | { 40 | return $this->data->get('html', ''); 41 | } 42 | 43 | /** 44 | * Generates fake data for the block. 45 | * 46 | * @param \Faker\Generator $faker 47 | * 48 | * @return array 49 | */ 50 | public static function fake(\Faker\Generator $faker): array 51 | { 52 | return ['html' => $faker->randomHtml()]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Blocks/Table.php: -------------------------------------------------------------------------------- 1 | [], 22 | ]; 23 | } 24 | 25 | /** 26 | * Rules to validate data of the block. 27 | * 28 | * @return array 29 | */ 30 | public function rules(): array 31 | { 32 | return [ 33 | 'withHeadings' => 'boolean', 34 | 'content' => 'array', 35 | 'content.*' => 'array', 36 | 'content.*.*' => 'string', 37 | ]; 38 | } 39 | 40 | /** 41 | * Renderer for the block. 42 | * 43 | * @return string 44 | */ 45 | public function render(): string 46 | { 47 | if (View::getFacadeRoot()) 48 | { 49 | return view(sprintf('editor.php::%s.table', EditorPhp::usingTemplate())) 50 | ->with(['data' => $this->data]) 51 | ->render(); 52 | } 53 | 54 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/table.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 55 | } 56 | 57 | /** 58 | * Generates fake data for the block. 59 | * 60 | * @param \Faker\Generator $faker 61 | * 62 | * @return array 63 | */ 64 | public static function fake(\Faker\Generator $faker): array 65 | { 66 | $content = []; 67 | $width = $faker->numberBetween(2, 8); 68 | 69 | foreach (range(0, $faker->numberBetween(1, 10)) as $index) 70 | { 71 | $row = []; 72 | 73 | foreach (range(0, $width) as $index) 74 | { 75 | $row[] = $faker->text(64); 76 | } 77 | 78 | $content[] = $row; 79 | } 80 | 81 | return [ 82 | 'withHeadings' => $faker->boolean(), 83 | 'content' => $content, 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Blocks/Warning.php: -------------------------------------------------------------------------------- 1 | [], 21 | 'message' => [], 22 | ]; 23 | } 24 | 25 | /** 26 | * Rules to validate data of the block. 27 | * 28 | * @return array 29 | */ 30 | public function rules(): array 31 | { 32 | return [ 33 | 'title' => 'string', 34 | 'message' => 'string', 35 | ]; 36 | } 37 | 38 | /** 39 | * Renderer for the block. 40 | * 41 | * @return string 42 | */ 43 | public function render(): string 44 | { 45 | if (View::getFacadeRoot()) 46 | { 47 | return view(sprintf('editor.php::%s.warning', EditorPhp::usingTemplate())) 48 | ->with(['data' => $this->data]) 49 | ->render(); 50 | } 51 | 52 | return Helpers::renderNative(__DIR__ . sprintf('/../../resources/php/%s/warning.php', EditorPhp::usingTemplate()), ['data' => $this->data]); 53 | } 54 | 55 | /** 56 | * Generates fake data for the block. 57 | * 58 | * @param \Faker\Generator $faker 59 | * 60 | * @return array 61 | */ 62 | public static function fake(\Faker\Generator $faker): array 63 | { 64 | return [ 65 | 'title' => $faker->text(32), 66 | 'message' => $faker->text(), 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Casts/EditorPhpCast.php: -------------------------------------------------------------------------------- 1 | setModel($model); 26 | } 27 | 28 | /** 29 | * @param Model $model 30 | * @param string $key 31 | * @param mixed $value 32 | * @param array $attributes 33 | * 34 | * @return mixed 35 | */ 36 | public function set($model, string $key, $value, array $attributes) 37 | { 38 | if ($value instanceof \BumpCore\EditorPhp\EditorPhp) 39 | { 40 | return $value->setModel($model)->toJson(); 41 | } 42 | 43 | return $value; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Console/BlockMakeCommand.php: -------------------------------------------------------------------------------- 1 | with(['data' => $this->data]) 30 | ->render(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/EditorPhp.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public Collection $blocks; 36 | 37 | /** 38 | * @var string 39 | */ 40 | public readonly ?string $version; 41 | 42 | /** 43 | * Belonging model, if casted. 44 | * 45 | * @var Model 46 | */ 47 | public readonly Model $model; 48 | 49 | /** 50 | * Fluent method to create new `EditorPhp` instance. 51 | * 52 | * @param string|null $input 53 | * 54 | * @return EditorPhp 55 | */ 56 | public static function make(?string $input = null): self 57 | { 58 | return new static($input); 59 | } 60 | 61 | /** 62 | * Constructor. 63 | * 64 | * @param string|null $input 65 | * 66 | * @return void 67 | */ 68 | public function __construct(?string $input = null) 69 | { 70 | if (empty($input)) 71 | { 72 | $this->time = Carbon::now(); 73 | $this->blocks = new Collection(); 74 | $this->version = null; 75 | } 76 | else 77 | { 78 | $parser = new Parser($input); 79 | $this->time = $parser->time(); 80 | $this->blocks = $parser->blocks($this); 81 | $this->version = $parser->version(); 82 | } 83 | } 84 | 85 | /** 86 | * Registers new block. 87 | * 88 | * @param array $blocks 89 | * @param bool $override 90 | * 91 | * @return void 92 | */ 93 | public static function register(array $blocks, bool $override = false): void 94 | { 95 | Parser::register($blocks, $override); 96 | } 97 | 98 | /** 99 | * Sets model to be used with casting. 100 | * 101 | * @param Model $model 102 | * 103 | * @return EditorPhp 104 | */ 105 | public function setModel(Model &$model): self 106 | { 107 | if (!isset($this->model)) 108 | { 109 | $this->model = $model; 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Renders with `Bootstrap 5` templates. 117 | * 118 | * @return void 119 | */ 120 | public static function useBootstrapFive(): void 121 | { 122 | static::$template = 'bootstrap-five'; 123 | } 124 | 125 | /** 126 | * Renders with `tailwindcss` templates. 127 | * 128 | * @return void 129 | */ 130 | public static function useTailwind(): void 131 | { 132 | static::$template = 'tailwind'; 133 | } 134 | 135 | /** 136 | * Returns used template. 137 | * 138 | * @return string 139 | */ 140 | public static function usingTemplate(): string 141 | { 142 | return static::$template; 143 | } 144 | 145 | /** 146 | * Converts the `Editor.php` as an array. 147 | * 148 | * @return array 149 | */ 150 | public function toArray(): array 151 | { 152 | return [ 153 | 'time' => (int) $this->time->getPreciseTimestamp(3), 154 | 'blocks' => $this->blocks->toArray(), 155 | 'version' => $this->version, 156 | ]; 157 | } 158 | 159 | /** 160 | * Converts the `Editor.php` to its JSON representation. 161 | * 162 | * @param int $options 163 | * 164 | * @return string 165 | */ 166 | public function toJson($options = 0): string 167 | { 168 | return json_encode($this->toArray(), $options); 169 | } 170 | 171 | /** 172 | * Creates an HTTP response that represents the `Editor.php`. 173 | * 174 | * @param \Illuminate\Http\Request $request 175 | * 176 | * @return \Symfony\Component\HttpFoundation\Response 177 | */ 178 | public function toResponse($request) 179 | { 180 | return $request->expectsJson() ? response($this->toArray()) : response($this->render()); 181 | } 182 | 183 | /** 184 | * Renders blocks into HTML. 185 | * 186 | * @return string 187 | */ 188 | public function render(): string 189 | { 190 | return $this->blocks 191 | ->map(fn (Block $block) => $block->render()) 192 | ->implode(''); 193 | } 194 | 195 | /** 196 | * Renders blocks into HTML. 197 | * 198 | * @return string 199 | */ 200 | public function toHtml() 201 | { 202 | return $this->render(); 203 | } 204 | 205 | /** 206 | * Renders blocks into HTML. 207 | * 208 | * @return string 209 | */ 210 | public function __toString(): string 211 | { 212 | return $this->render(); 213 | } 214 | 215 | /** 216 | * Generates fake instance. 217 | * 218 | * @param bool $instance 219 | * @param int $minLength 220 | * @param int $maxLength 221 | * 222 | * @return EditorPhp|string 223 | */ 224 | public static function fake(bool $instance = false, int $minLength = 8, int $maxLength = 30): EditorPhp|string 225 | { 226 | $faker = \Faker\Factory::create(); 227 | $blocks = array_filter(Parser::$blocks, fn (string $provider) => method_exists($provider, 'fake')); 228 | $generatedBlocks = []; 229 | 230 | foreach (range(0, fake()->numberBetween($minLength, $maxLength)) as $index) 231 | { 232 | $block = fake()->randomElement($blocks); 233 | $generatedBlocks[] = (new ($block)($block::fake($faker)))->toArray(); 234 | } 235 | 236 | $generated = json_encode([ 237 | 'time' => (int) Carbon::now()->getPreciseTimestamp(3), 238 | 'blocks' => $generatedBlocks, 239 | 'version' => fake()->semver(), 240 | ]); 241 | 242 | return $instance ? static::make($generated) : $generated; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/EditorPhpServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) 21 | { 22 | $this->commands(BlockMakeCommand::class); 23 | } 24 | 25 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'editor.php'); 26 | 27 | $this->publishes([ 28 | __DIR__ . '/../resources/views' => resource_path('views/vendor/editor.php'), 29 | ], 'editor.php'); 30 | 31 | $this->publishes([ 32 | __DIR__ . '/../config/editor.php' => config_path('editor.php'), 33 | ]); 34 | 35 | Parser::register(config('editor.blocks') ?? [], !empty(config('editor.blocks'))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exceptions/EditorPhpException.php: -------------------------------------------------------------------------------- 1 | make($data, $rules); 31 | } 32 | 33 | /** 34 | * Basic renderer to render native templates. 35 | * 36 | * @param string $file 37 | * @param array $data 38 | * 39 | * @return string 40 | */ 41 | public static function renderNative(string $file, array $data): string 42 | { 43 | ob_start(); 44 | extract($data); 45 | require $file; 46 | 47 | return ob_get_clean(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static array $blocks = [ 20 | 'attaches' => Blocks\Attaches::class, 21 | 'checklist' => Blocks\Checklist::class, 22 | 'code' => Blocks\Code::class, 23 | 'delimiter' => Blocks\Delimiter::class, 24 | 'embed' => Blocks\Embed::class, 25 | 'header' => Blocks\Header::class, 26 | 'image' => Blocks\Image::class, 27 | 'linkTool' => Blocks\LinkTool::class, 28 | 'list' => Blocks\ListBlock::class, 29 | 'paragraph' => Blocks\Paragraph::class, 30 | 'personality' => Blocks\Personality::class, 31 | 'quote' => Blocks\Quote::class, 32 | 'raw' => Blocks\Raw::class, 33 | 'table' => Blocks\Table::class, 34 | 'warning' => Blocks\Warning::class, 35 | ]; 36 | 37 | /** 38 | * Converted input JSON. 39 | * 40 | * @var array 41 | */ 42 | protected array $input; 43 | 44 | /** 45 | * Constructor. 46 | * 47 | * @param string $input 48 | * 49 | * @return void 50 | */ 51 | public function __construct(string $input) 52 | { 53 | $this->input = $this->handleInput($input); 54 | } 55 | 56 | /** 57 | * Registers new block. 58 | * 59 | * @param array $blocks 60 | * @param bool $override 61 | * 62 | * @return void 63 | */ 64 | public static function register(array $blocks, bool $override = false): void 65 | { 66 | if ($override) 67 | { 68 | static::$blocks = []; 69 | } 70 | 71 | foreach ($blocks as $type => $block) 72 | { 73 | if (!in_array(Block::class, class_parents($block))) 74 | { 75 | throw new EditorPhpException($block . ' must extend ' . Block::class); 76 | } 77 | 78 | static::$blocks[$type] = $block; 79 | } 80 | } 81 | 82 | /** 83 | * Returns the time of given `Editor.js` output. 84 | * 85 | * @return Carbon 86 | */ 87 | public function time(): Carbon 88 | { 89 | return Carbon::parse(Arr::get($this->input, 'time') / 1000); 90 | } 91 | 92 | /** 93 | * Returns parsed blocks of given `Editor.js` output. 94 | * 95 | * @param EditorPhp|null $root 96 | * 97 | * @return Collection 98 | */ 99 | public function blocks(?EditorPhp &$root = null): Collection 100 | { 101 | $blocks = new Collection(); 102 | 103 | foreach (Arr::get($this->input, 'blocks') as $block) 104 | { 105 | $type = Arr::get($block, 'type'); 106 | 107 | if (!key_exists($type, static::$blocks)) 108 | { 109 | throw new EditorPhpException('Unknown block type: ' . $type); 110 | } 111 | 112 | $blocks->push(new (static::$blocks[$type])(Arr::get($block, 'data'), $root)); 113 | } 114 | 115 | return $blocks; 116 | } 117 | 118 | /** 119 | * Returns the version of given `Editor.js` output. 120 | * 121 | * @return string 122 | */ 123 | public function version(): string 124 | { 125 | return Arr::get($this->input, 'version'); 126 | } 127 | 128 | /** 129 | * Parses given `Editor.js` output JSON. 130 | * 131 | * @param string $input 132 | * 133 | * @return array 134 | */ 135 | protected function handleInput(string $input): array 136 | { 137 | if (!Str::isJson($input)) 138 | { 139 | throw new EditorPhpException('Given Editor.js output is not a valid JSON.'); 140 | } 141 | 142 | $input = json_decode($input, true); 143 | 144 | if (!$this->validateSchema($input)) 145 | { 146 | throw new EditorPhpException('Given Editor.js output is not matching schema.'); 147 | } 148 | 149 | return $input; 150 | } 151 | 152 | /** 153 | * Validates given `Editor.js` output. 154 | * 155 | * @param array $input 156 | * 157 | * @return bool 158 | */ 159 | public function validateSchema(array $input): bool 160 | { 161 | $validator = Helpers::makeValidator($input, [ 162 | 'time' => 'required|numeric', 163 | 'blocks' => 'present|array', 164 | 'blocks.*' => 'present|array', 165 | 'blocks.*.type' => 'required|string', 166 | 'blocks.*.data' => 'present|array', 167 | 'version' => 'required|string', 168 | ]); 169 | 170 | return !$validator->fails(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Purifier.php: -------------------------------------------------------------------------------- 1 | $allowedTags 14 | * 15 | * @return string 16 | */ 17 | public static function stripTags(string $string, array $allowedTags = []): string 18 | { 19 | return strip_tags($string, $allowedTags); 20 | } 21 | 22 | /** 23 | * Strips unwanted attributes from given tag and string. 24 | * 25 | * @param string $string 26 | * @param string $tag 27 | * @param array $allowedAttributes 28 | * 29 | * @return string 30 | */ 31 | public static function stripAttributes(string $string, string $tag, array $allowedAttributes = []): string 32 | { 33 | $domDocument = new DOMDocument(); 34 | libxml_use_internal_errors(true); 35 | // We're using template tags only for wrapping. In normal case It would be `p`. 36 | $domDocument->loadHTML(implode('', ['']), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 37 | 38 | /** 39 | * @var \DOMElement $node 40 | */ 41 | foreach ($domDocument->getElementsByTagName($tag) as $node) 42 | { 43 | /** 44 | * @var \DOMAttr $attribute 45 | */ 46 | foreach ($node->attributes as $attribute) 47 | { 48 | if (!in_array($attribute->name, $allowedAttributes)) 49 | { 50 | $node->removeAttribute($attribute->name); 51 | } 52 | } 53 | } 54 | 55 | // DomDocument little bit old, isn't it? 56 | return str_replace( 57 | [''], 58 | '', 59 | mb_convert_encoding($domDocument->saveHTML($domDocument->documentElement), 'ISO-8859-1', 'UTF-8') 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Block/BlockTest.php: -------------------------------------------------------------------------------- 1 | expect(new Paragraph(['text' => 'foo']))->toBeInstanceOf(Block::class), 9 | ); 10 | 11 | test( 12 | 'Can be converted to array', 13 | fn () => expect(Paragraph::make(['text' => 'foo'])->toArray())->toBeArray(), 14 | ); 15 | 16 | test( 17 | 'Can be access data via get method', 18 | fn () => expect(Paragraph::make(['text' => 'foo'])->get('text')) 19 | ->toEqual('foo') 20 | ); 21 | 22 | test( 23 | 'Can be set data via get method', 24 | fn () => expect(Paragraph::make(['text' => 'foo'])->set('text', 'baz')->get('text')) 25 | ->toEqual('baz') 26 | ); 27 | 28 | test( 29 | 'Can be check data exists via has method', 30 | fn () => expect(Paragraph::make(['text' => 'foo'])->has('text'))->toBeTrue() 31 | ); 32 | 33 | test( 34 | 'Can be rendered', 35 | fn () => expect((new Paragraph(['text' => 'foo']))->render())->toBeString(), 36 | ); 37 | 38 | test( 39 | 'Can be rendered via casting to string', 40 | fn () => expect((string) (new Paragraph(['text' => 'foo'])))->toBeString(), 41 | ); 42 | 43 | test( 44 | 'Can be rendered via toHtml', 45 | fn () => expect(Paragraph::make(['text' => 'foo'])->toHtml())->toBeString(), 46 | ); 47 | -------------------------------------------------------------------------------- /tests/Block/DataTest.php: -------------------------------------------------------------------------------- 1 | expect(new Data(['foo' => 'bar'], '*', ['foo' => 'string'])) 8 | ->toBeInstanceOf(Data::class), 9 | ); 10 | 11 | test( 12 | 'Can empty on validation fail', 13 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'numeric']))->toArray()) 14 | ->toBeEmpty() 15 | ); 16 | 17 | test( 18 | 'Can be access data by invoking', 19 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'string']))('foo')) 20 | ->toEqual('bar'), 21 | ); 22 | 23 | test( 24 | 'Can be access data by invoking with fallback', 25 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'string']))('bar', 'baz')) 26 | ->toEqual('baz'), 27 | ); 28 | 29 | test( 30 | 'Can be access data by get method', 31 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'string']))->get('foo')) 32 | ->toEqual('bar'), 33 | ); 34 | 35 | test( 36 | 'Can be access data by get method with fallback', 37 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'string']))->get('bar', 'baz')) 38 | ->toEqual('baz'), 39 | ); 40 | 41 | test( 42 | 'Can be set data by get method', 43 | function() { 44 | $data = new Data(['foo' => 'bar'], '*', ['foo' => 'string']); 45 | $data->set('foo', 'baz'); 46 | 47 | expect($data->get('foo'))->toEqual('baz'); 48 | }, 49 | ); 50 | 51 | test( 52 | 'Can be check data exists via has method', 53 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'string']))->has('bar'))->toBeFalse() 54 | ); 55 | 56 | test( 57 | 'Can be converted to array', 58 | fn () => expect((new Data(['foo' => 'bar'], '*', ['foo' => 'string']))->toArray())->toBeArray(), 59 | ); 60 | 61 | test( 62 | 'Can allow all tags', 63 | function() { 64 | $data = new Data( 65 | ['foo' => '
    This should be allowed!
    '], 66 | '*', 67 | ['foo' => 'string'] 68 | ); 69 | 70 | expect(str_contains($data->get('foo'), 'div'))->toBeTrue(); 71 | expect(str_contains($data->get('foo'), 'script'))->toBeTrue(); 72 | } 73 | ); 74 | 75 | test( 76 | 'Can allow only anchor with href attribute', 77 | function() { 78 | $data = new Data( 79 | ['foo' => '
    This should be not allowed!
    This should be allowed'], 80 | ['foo' => ['a:href']], 81 | ['foo' => 'string'] 82 | ); 83 | 84 | expect(str_contains($data->get('foo'), '
    '))->toBeFalse(); 85 | expect(str_contains($data->get('foo'), '
    This should be not allowed!
    This should be allowed'], 96 | ['foo' => '*'], 97 | ['foo' => 'string'] 98 | ); 99 | expect(str_contains($data->get('foo'), '
    '))->toBeTrue(); 100 | expect(str_contains($data->get('foo'), '