├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── main.yml │ └── release-drafter.yml ├── .gitignore ├── .php_cs ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Marks │ ├── Bold.php │ ├── Code.php │ ├── Italic.php │ ├── Link.php │ ├── Mark.php │ ├── Strike.php │ ├── Subscript.php │ ├── Superscript.php │ └── Underline.php ├── Minify.php ├── Nodes │ ├── Blockquote.php │ ├── BulletList.php │ ├── CodeBlock.php │ ├── CodeBlockWrapper.php │ ├── HardBreak.php │ ├── Heading.php │ ├── HorizontalRule.php │ ├── Image.php │ ├── ListItem.php │ ├── Node.php │ ├── OrderedList.php │ ├── Paragraph.php │ ├── Table.php │ ├── TableCell.php │ ├── TableHeader.php │ ├── TableRow.php │ ├── TableWrapper.php │ ├── Text.php │ └── User.php └── Renderer.php └── tests ├── ConfiguredMarksTest.php ├── ConfiguredNodesTest.php ├── EmojiTest.php ├── EmptyTextNodesTest.php ├── KeepContentOfUnknownTagsTest.php ├── Marks ├── BoldTest.php ├── CodeTest.php ├── Custom.php ├── Custom │ └── Bold.php ├── CustomMarkTest.php ├── ItalicTest.php ├── LinkTest.php ├── NestedMarksTest.php ├── StrikeTest.php ├── SubscriptTest.php ├── SuperscriptTest.php └── UnderlineTest.php ├── Mix ├── MarksInNodesTest.php └── MultipleMarksTest.php ├── Nodes ├── BlockquoteTest.php ├── BulletListTest.php ├── CodeBlockTest.php ├── Custom.php ├── Custom │ └── Paragraph.php ├── CustomNodeTest.php ├── HardBreakTest.php ├── HeadingTest.php ├── HorizontalRuleTest.php ├── ImageTest.php ├── OrderedListTest.php ├── ParagraphTest.php ├── TableTest.php └── UserMentionTest.php ├── SpecialCharacterTest.php ├── TestCase.php └── WhitespaceTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | 5 | updates: 6 | - commit-message: 7 | include: "scope" 8 | prefix: "composer" 9 | directory: "/" 10 | ignore: 11 | - dependency-name: "friendsofphp/php-cs-fixer" 12 | versions: 13 | - ">= 0" 14 | - dependency-name: "mrclay/minify" 15 | versions: 16 | - ">= 0" 17 | labels: 18 | - "dependency" 19 | open-pull-requests-limit: 10 20 | package-ecosystem: "composer" 21 | schedule: 22 | interval: "weekly" 23 | versioning-strategy: "increase" 24 | 25 | - commit-message: 26 | include: "scope" 27 | prefix: "github-actions" 28 | directory: "/" 29 | labels: 30 | - "dependency" 31 | open-pull-requests-limit: 10 32 | package-ecosystem: "github-actions" 33 | schedule: 34 | interval: "weekly" 35 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | $CHANGES 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Integrate" 2 | 3 | on: 4 | pull_request: null 5 | push: 6 | branches: 7 | - "main" 8 | 9 | jobs: 10 | coding-standards: 11 | name: "Coding Standards" 12 | 13 | runs-on: "ubuntu-latest" 14 | 15 | strategy: 16 | matrix: 17 | php-version: 18 | - "7.4" 19 | 20 | dependencies: 21 | - "locked" 22 | 23 | steps: 24 | - name: "Checkout" 25 | uses: "actions/checkout@v2.4.0" 26 | 27 | - name: "Install PHP" 28 | uses: "shivammathur/setup-php@2.16.0" 29 | with: 30 | coverage: "none" 31 | php-version: "${{ matrix.php-version }}" 32 | 33 | - name: "Validate composer.json and composer.lock" 34 | run: "composer validate --strict" 35 | 36 | - name: "Determine composer cache directory" 37 | id: "determine-composer-cache-directory" 38 | run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" 39 | 40 | - name: "Cache dependencies installed with composer" 41 | uses: "actions/cache@v2.1.7" 42 | with: 43 | path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" 44 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}" 45 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-" 46 | 47 | - name: "Install lowest dependencies from composer.json" 48 | if: "matrix.dependencies == 'lowest'" 49 | run: "composer update --no-interaction --no-progress --no-suggest --prefer-lowest" 50 | 51 | - name: "Install locked dependencies from composer.lock" 52 | if: "matrix.dependencies == 'locked'" 53 | run: "composer install --no-interaction --no-progress --no-suggest" 54 | 55 | - name: "Install highest dependencies from composer.json" 56 | if: "matrix.dependencies == 'highest'" 57 | run: "composer update --no-interaction --no-progress --no-suggest" 58 | 59 | - name: "Run friendsofphp/php-cs-fixer" 60 | run: "vendor/bin/php-cs-fixer fix --config=.php_cs --diff --diff-format=udiff --dry-run --verbose" 61 | 62 | tests: 63 | name: "Tests" 64 | 65 | runs-on: "ubuntu-latest" 66 | 67 | strategy: 68 | matrix: 69 | php-version: 70 | - "7.1" 71 | - "7.2" 72 | - "7.3" 73 | - "7.4" 74 | - "8.0" 75 | 76 | dependencies: 77 | - "locked" 78 | - "lowest" 79 | - "highest" 80 | 81 | steps: 82 | - name: "Checkout" 83 | uses: "actions/checkout@v2.4.0" 84 | 85 | - name: "Install PHP" 86 | uses: "shivammathur/setup-php@2.16.0" 87 | with: 88 | coverage: "none" 89 | php-version: "${{ matrix.php-version }}" 90 | 91 | - name: "Determine composer cache directory" 92 | id: "determine-composer-cache-directory" 93 | run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" 94 | 95 | - name: "Cache dependencies installed with composer" 96 | uses: "actions/cache@v2.1.7" 97 | with: 98 | path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" 99 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}" 100 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-" 101 | 102 | - name: "Install lowest dependencies from composer.json" 103 | if: "matrix.dependencies == 'lowest'" 104 | run: "composer update --no-interaction --no-progress --no-suggest --prefer-lowest" 105 | 106 | - name: "Install locked dependencies from composer.lock" 107 | if: "matrix.dependencies == 'locked'" 108 | run: "composer install --no-interaction --no-progress --no-suggest" 109 | 110 | - name: "Install highest dependencies from composer.json" 111 | if: "matrix.dependencies == 'highest'" 112 | run: "composer update --no-interaction --no-progress --no-suggest" 113 | 114 | - name: "Run tests with phpunit/phpunit" 115 | run: "vendor/bin/phpunit --configuration=phpunit.xml.dist" 116 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Drafts your next Release notes as Pull Requests are merged into “main” 2 | # https://github.com/release-drafter/release-drafter 3 | 4 | name: release drafter 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | update_release_draft: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: release-drafter/release-drafter@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | /.phpunit.result.cache 4 | /composer.lock 5 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | notPath('vendor') 5 | ->in(__DIR__) 6 | ->name('*.php'); 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setUsingCache(false) 10 | ->setRules([ 11 | '@PSR2' => true, 12 | 'array_indentation' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'blank_line_after_opening_tag' => true, 15 | 'cast_spaces' => true, 16 | 'concat_space' => ['spacing' => 'one'], 17 | 'elseif' => true, 18 | 'no_blank_lines_after_class_opening' => true, 19 | 'no_closing_tag' => true, 20 | 'no_leading_import_slash' => true, 21 | 'no_trailing_whitespace' => true, 22 | 'no_unused_imports' => true, 23 | 'no_useless_else' => true, 24 | 'ordered_imports' => ['sortAlgorithm' => 'length'], 25 | 'trailing_comma_in_multiline_array' => true, 26 | ]) 27 | ->setFinder($finder); 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, überdosis GbR 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 | > 🚨 We’ve done a rewrite that you probably want to check out: [ueberdosis/tiptap-php](https://github.com/ueberdosis/tiptap-php) 2 | 3 | # HTML to ProseMirror 4 | 5 | [](https://packagist.org/packages/ueberdosis/html-to-prosemirror) 6 | [](https://github.com/ueberdosis/html-to-prosemirror/actions) 7 | [](https://packagist.org/packages/ueberdosis/html-to-prosemirror) 8 | [](https://github.com/sponsors/ueberdosis) 9 | 10 | Takes HTML and outputs ProseMirror compatible JSON. 11 | 12 | ## Installation 13 | ```bash 14 | composer require ueberdosis/html-to-prosemirror 15 | ``` 16 | 17 | ## Usage 18 | ```php 19 | (new \HtmlToProseMirror\Renderer)->render('
Example Text
') 20 | ``` 21 | 22 | ## Output 23 | ```json 24 | { 25 | "type": "doc", 26 | "content": [ 27 | { 28 | "type": "paragraph", 29 | "content": [ 30 | { 31 | "type": "text", 32 | "text": "Example Text" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ``` 39 | 40 | ## Supported nodes 41 | - [Blockquote](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/Blockquote.php) 42 | - [BulletList](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/BulletList.php) 43 | - [CodeBlock](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/CodeBlock.php) 44 | - [HardBreak](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/HardBreak.php) 45 | - [Heading](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/Heading.php) 46 | - [Image](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/Image.php) 47 | - [ListItem](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/ListItem.php) 48 | - [OrderedList](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/OrderedList.php) 49 | - [Paragraph](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/Paragraph.php) 50 | - [Table](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/Table.php) 51 | - [TableCell](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/TableCell.php) 52 | - [TableHeader](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/TableHeader.php) 53 | - [TableRow](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/TableRow.php) 54 | - [User](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Nodes/User.php) 55 | 56 | ## Supported marks 57 | - [Bold](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Bold.php) 58 | - [Code](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Code.php) 59 | - [Italic](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Italic.php) 60 | - [Link](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Link.php) 61 | - [Strike](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Strike.php) 62 | - [Subscript](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Subscript.php) 63 | - [Superscript](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Superscript.php) 64 | - [Underline](https://github.com/ueberdosis/html-to-prosemirror/blob/main/src/Marks/Underline.php) 65 | 66 | ## Custom nodes 67 | Define your custom nodes as PHP classes: 68 | ```php 69 | addNode(CustomNode::class); 81 | ``` 82 | 83 | Or overwrite the enabled nodes: 84 | ```php 85 | $renderer->withNodes([ 86 | CustomNode::class, 87 | ]); 88 | ``` 89 | 90 | Or overwrite the enabled marks: 91 | ```php 92 | $renderer->withMarks([ 93 | Bold::class, 94 | ]); 95 | ``` 96 | 97 | Or replace just one mark or node: 98 | 99 | ```php 100 | $renderer->replaceNode( 101 | CodeBlock::class, CustomCodeBlock::class 102 | ); 103 | 104 | $renderer->replaceMark( 105 | Bold::class, CustomBold::class 106 | ); 107 | ``` 108 | 109 | ## Contributing 110 | Pull Requests are welcome. 111 | 112 | ## Credits 113 | - [Hans Pagel](https://github.com/hanspagel) 114 | - [localheinz](https://github.com/localheinz) 115 | - [sauerbraten](https://github.com/sauerbraten) 116 | - [WiebkeVog](https://github.com/WiebkeVog) 117 | - [pa-bouly](https://github.com/pa-bouly) 118 | - [All Contributors](../../contributors) 119 | 120 | ## Related packages 121 | - [tiptap](https://github.com/ueberdosis/tiptap) by @ueberdosis 122 | - [html-to-prosemirror-js](https://github.com/enVolt/html-to-prosemirror) by @enVolt 123 | 124 | ## License 125 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 126 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ueberdosis/html-to-prosemirror", 3 | "type": "library", 4 | "description": "Takes HTML and outputs ProseMirror compatible JSON.", 5 | "keywords": [ 6 | "prosemirror" 7 | ], 8 | "config": { 9 | "platform": { 10 | "php": "7.1.3" 11 | } 12 | }, 13 | "funding": [ 14 | { 15 | "type": "github", 16 | "url": "https://github.com/sponsors/ueberdosis/" 17 | } 18 | ], 19 | "license": "MIT", 20 | "authors": [ 21 | { 22 | "name": "Hans Pagel" 23 | } 24 | ], 25 | "require": { 26 | "php": "^7.1.3|^8.0" 27 | }, 28 | "require-dev": { 29 | "friendsofphp/php-cs-fixer": "^2.15", 30 | "league/climate": "^3.5", 31 | "phpunit/phpunit": "^7.5.20" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "HtmlToProseMirror\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "HtmlToProseMirror\\Test\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "format": [ 45 | "vendor/bin/php-cs-fixer fix" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 |]*?>[\\s\\S]*?<\\/pre>)\\s*/iu', [$this, '_removePreCB'], $this->_html); 19 | 20 | // trim each line. 21 | $this->_html = preg_replace('/^\\s+|\\s+$/mu', '', $this->_html); 22 | 23 | // remove ws around block/undisplayed elements 24 | $this->_html = preg_replace('/\\s+(<\\/?(?:area|article|aside|base(?:font)?|blockquote|body' 25 | . '|canvas|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|figcaption|figure|footer|form' 26 | . '|frame(?:set)?|h[1-6]|head|header|hgroup|hr|html|legend|li|link|main|map|menu|meta|nav' 27 | . '|ol|opt(?:group|ion)|output|p|param|section|t(?:able|body|head|d|h||r|foot|itle)' 28 | . '|ul|video)\\b[^>]*>)/iu', '$1', $this->_html); 29 | 30 | // fill placeholders 31 | $this->_html = str_replace( 32 | array_keys($this->_placeholders), 33 | array_values($this->_placeholders), 34 | $this->_html 35 | ); 36 | 37 | return $this->_html; 38 | } 39 | 40 | protected function _removePreCB($m) 41 | { 42 | return $this->_reservePlace("_replacementHash . count($this->_placeholders) . '%'; 48 | $this->_placeholders[$placeholder] = $content; 49 | 50 | return $placeholder; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Nodes/Blockquote.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'blockquote'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'blockquote', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nodes/BulletList.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'ul'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'bullet_list', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nodes/CodeBlock.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'code' && 11 | $this->DOMNode->parentNode->nodeName === 'pre'; 12 | } 13 | 14 | private function getLanguage() 15 | { 16 | return preg_replace("/^language-/", "", $this->DOMNode->getAttribute('class')); 17 | } 18 | 19 | public function data() 20 | { 21 | if ($language = $this->getLanguage()) { 22 | return [ 23 | 'type' => 'code_block', 24 | 'attrs' => [ 25 | 'language' => $this->getLanguage(), 26 | ], 27 | ]; 28 | } 29 | 30 | return [ 31 | 'type' => 'code_block', 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Nodes/CodeBlockWrapper.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'pre'; 10 | } 11 | 12 | public function data() 13 | { 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nodes/HardBreak.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'br'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'hard_break', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nodes/Heading.php: -------------------------------------------------------------------------------- 1 | getLevel($this->DOMNode->nodeName); 17 | } 18 | 19 | public function data() 20 | { 21 | return [ 22 | 'type' => 'heading', 23 | 'attrs' => [ 24 | 'level' => $this->getLevel($this->DOMNode->nodeName), 25 | ], 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Nodes/HorizontalRule.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'hr'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'horizontal_rule', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nodes/Image.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'img'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'image', 16 | 'attrs' => [ 17 | 'alt' => $this->DOMNode->hasAttribute('alt') ? $this->DOMNode->getAttribute('alt') : null, 18 | 'src' => $this->DOMNode->hasAttribute('src') ? $this->DOMNode->getAttribute('src') : null, 19 | 'title' => $this->DOMNode->hasAttribute('title') ? $this->DOMNode->getAttribute('title') : null, 20 | ], 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Nodes/ListItem.php: -------------------------------------------------------------------------------- 1 | 'paragraph', 9 | ]; 10 | 11 | public function matching() 12 | { 13 | return $this->DOMNode->nodeName === 'li'; 14 | } 15 | 16 | public function data() 17 | { 18 | if ($this->DOMNode->childNodes->length === 1 19 | && $this->DOMNode->childNodes[0]->nodeName == "p") { 20 | $this->wrapper = null; 21 | } 22 | 23 | return [ 24 | 'type' => 'list_item', 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Nodes/Node.php: -------------------------------------------------------------------------------- 1 | DOMNode = $DOMNode; 16 | } 17 | 18 | public function matching() 19 | { 20 | return false; 21 | } 22 | 23 | public function data() 24 | { 25 | return []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Nodes/OrderedList.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'ol'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'ordered_list', 16 | 'attrs' => [ 17 | 'order' => 18 | $this->DOMNode->getAttribute('start') ? 19 | (int) $this->DOMNode->getAttribute('start') : 20 | 1, 21 | ], 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nodes/Paragraph.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'p'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'paragraph', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nodes/Table.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'tbody' && 11 | $this->DOMNode->parentNode->nodeName === 'table'; 12 | } 13 | 14 | public function data() 15 | { 16 | return [ 17 | 'type' => 'table', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Nodes/TableCell.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === $this->tagName; 13 | } 14 | 15 | public function data() 16 | { 17 | $data = [ 18 | 'type' => $this->nodeType, 19 | ]; 20 | 21 | $attrs = []; 22 | if ($colspan = $this->DOMNode->getAttribute('colspan')) { 23 | $attrs['colspan'] = intval($colspan); 24 | } 25 | if ($colwidth = $this->DOMNode->getAttribute('data-colwidth')) { 26 | $widths = array_map(function ($w) { 27 | return intval($w); 28 | }, explode(',', $colwidth)); 29 | if (count($widths) === $attrs['colspan']) { 30 | $attrs['colwidth'] = $widths; 31 | } 32 | } 33 | if ($rowspan = $this->DOMNode->getAttribute('rowspan')) { 34 | $attrs['rowspan'] = intval($rowspan); 35 | } 36 | 37 | if (!empty($attrs)) { 38 | $data['attrs'] = $attrs; 39 | } 40 | 41 | return $data; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Nodes/TableHeader.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'tr'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'table_row', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nodes/TableWrapper.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'table'; 10 | } 11 | 12 | public function data() 13 | { 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nodes/Text.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === '#text'; 10 | } 11 | 12 | public function data() 13 | { 14 | $text = ltrim($this->DOMNode->nodeValue, "\n"); 15 | 16 | if ($text === '') { 17 | return null; 18 | } 19 | 20 | return [ 21 | 'type' => 'text', 22 | 'text' => $text, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Nodes/User.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'user-mention'; 10 | } 11 | 12 | public function data() 13 | { 14 | return [ 15 | 'type' => 'user', 16 | 'attrs' => [ 17 | 'id' => $this->DOMNode->getAttribute('data-id'), 18 | ], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Renderer.php: -------------------------------------------------------------------------------- 1 | marks = $marks; 50 | } 51 | 52 | return $this; 53 | } 54 | 55 | public function withNodes($nodes = null) 56 | { 57 | if (is_array($nodes)) { 58 | $this->nodes = $nodes; 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | public function addNode($node) 65 | { 66 | $this->nodes[] = $node; 67 | 68 | return $this; 69 | } 70 | 71 | public function addNodes($nodes) 72 | { 73 | foreach ($nodes as $node) { 74 | $this->addNode($node); 75 | } 76 | 77 | return $this; 78 | } 79 | 80 | public function addMark($mark) 81 | { 82 | $this->marks[] = $mark; 83 | 84 | return $this; 85 | } 86 | 87 | public function addMarks($marks) 88 | { 89 | foreach ($marks as $mark) { 90 | $this->addMark($mark); 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | public function replaceNode($search_node, $replace_node) 97 | { 98 | foreach ($this->nodes as $key => $node_class) { 99 | if ($node_class == $search_node) { 100 | $this->nodes[$key] = $replace_node; 101 | } 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | public function replaceMark($search_mark, $replace_mark) 108 | { 109 | foreach ($this->marks as $key => $mark_class) { 110 | if ($mark_class == $search_mark) { 111 | $this->marks[$key] = $replace_mark; 112 | } 113 | } 114 | 115 | return $this; 116 | } 117 | 118 | public function render(string $value): array 119 | { 120 | $this->setDocument($value); 121 | 122 | $content = $this->renderChildren( 123 | $this->getDocumentBody() 124 | ); 125 | 126 | return [ 127 | 'type' => 'doc', 128 | 'content' => $content, 129 | ]; 130 | } 131 | 132 | private function setDocument(string $value): Renderer 133 | { 134 | libxml_use_internal_errors(true); 135 | 136 | $this->document = new DOMDocument; 137 | $this->document->loadHTML( 138 | $this->wrapHtmlDocument( 139 | $this->stripWhitespace($value) 140 | ) 141 | ); 142 | 143 | return $this; 144 | } 145 | 146 | private function wrapHtmlDocument($value) 147 | { 148 | return '' . $value; 149 | } 150 | 151 | private function stripWhitespace(string $value): string 152 | { 153 | return (new Minify)->process($value); 154 | } 155 | 156 | private function getDocumentBody(): DOMElement 157 | { 158 | return $this->document->getElementsByTagName('body')->item(0); 159 | } 160 | 161 | private function renderChildren($node): array 162 | { 163 | $nodes = []; 164 | 165 | foreach ($node->childNodes as $child) { 166 | if ($class = $this->getMatchingNode($child)) { 167 | $item = $class->data(); 168 | 169 | if ($item === null) { 170 | if ($child->hasChildNodes()) { 171 | $nodes = array_merge($nodes, $this->renderChildren($child)); 172 | } 173 | continue; 174 | } 175 | 176 | if ($child->hasChildNodes()) { 177 | $item = array_merge($item, [ 178 | 'content' => $this->renderChildren($child), 179 | ]); 180 | } 181 | 182 | if (count($this->storedMarks)) { 183 | $item = array_merge($item, [ 184 | 'marks' => $this->storedMarks, 185 | ]); 186 | } 187 | 188 | if ($class->wrapper) { 189 | $item['content'] = [ 190 | array_merge($class->wrapper, [ 191 | 'content' => @$item['content'] ?: [], 192 | ]), 193 | ]; 194 | } 195 | 196 | array_push($nodes, $item); 197 | } elseif ($class = $this->getMatchingMark($child)) { 198 | array_push($this->storedMarks, $class->data()); 199 | 200 | if ($child->hasChildNodes()) { 201 | $nodes = array_merge($nodes, $this->renderChildren($child)); 202 | } 203 | 204 | array_pop($this->storedMarks); 205 | } elseif ($child->hasChildNodes()) { 206 | $nodes = array_merge($nodes, $this->renderChildren($child)); 207 | } 208 | } 209 | 210 | return $nodes; 211 | } 212 | 213 | private function getMatchingNode($item) 214 | { 215 | return $this->getMatchingClass($item, $this->nodes); 216 | } 217 | 218 | private function getMatchingMark($item) 219 | { 220 | return $this->getMatchingClass($item, $this->marks); 221 | } 222 | 223 | private function getMatchingClass($node, $classes) 224 | { 225 | foreach ($classes as $class) { 226 | $instance = new $class($node); 227 | 228 | if ($instance->matching()) { 229 | return $instance; 230 | } 231 | } 232 | 233 | return false; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tests/ConfiguredMarksTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 13 | 14 | $json = [ 15 | 'type' => 'doc', 16 | 'content' => [ 17 | [ 18 | 'type' => 'text', 19 | 'text' => 'Example Text', 20 | 'marks' => [ 21 | [ 22 | 'type' => 'bold', 23 | ], 24 | ], 25 | ], 26 | ], 27 | ]; 28 | 29 | $this->assertEquals($json, (new Renderer)->render($html)); 30 | } 31 | 32 | /** @test */ 33 | public function bold_is_enabled_explicitly() 34 | { 35 | $html = 'Example Text'; 36 | 37 | $json = [ 38 | 'type' => 'doc', 39 | 'content' => [ 40 | [ 41 | 'type' => 'text', 42 | 'text' => 'Example Text', 43 | 'marks' => [ 44 | [ 45 | 'type' => 'bold', 46 | ], 47 | ], 48 | ], 49 | ], 50 | ]; 51 | 52 | $this->assertEquals($json, (new Renderer)->withMarks([ 53 | \HtmlToProseMirror\Marks\Bold::class, 54 | ])->render($html)); 55 | } 56 | 57 | /** @test */ 58 | public function all_marks_are_disabled() 59 | { 60 | $html = 'Example Text
'; 61 | 62 | $json = [ 63 | 'type' => 'doc', 64 | 'content' => [ 65 | [ 66 | 'type' => 'paragraph', 67 | 'content' => [ 68 | [ 69 | 'type' => 'text', 70 | 'text' => 'Example Text', 71 | ], 72 | ], 73 | ], 74 | ], 75 | ]; 76 | 77 | $this->assertEquals($json, (new Renderer)->withMarks([])->render($html)); 78 | } 79 | 80 | /** @test */ 81 | public function bold_is_replaced_with_a_custom_integration() 82 | { 83 | $html = 'Example Text
'; 84 | 85 | $json = [ 86 | 'type' => 'doc', 87 | 'content' => [ 88 | [ 89 | 'type' => 'paragraph', 90 | 'content' => [ 91 | [ 92 | 'type' => 'text', 93 | 'text' => 'Example Text', 94 | 'marks' => [ 95 | [ 96 | 'type' => 'bold', 97 | ], 98 | ], 99 | ], 100 | ], 101 | ], 102 | ], 103 | ]; 104 | 105 | $this->assertEquals($json, (new Renderer)->replaceMark( 106 | \HtmlToProseMirror\Marks\Bold::class, 107 | \HtmlToProseMirror\Test\Marks\Custom\Bold::class 108 | )->render($html)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/ConfiguredNodesTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 13 | 14 | $json = [ 15 | 'type' => 'doc', 16 | 'content' => [ 17 | [ 18 | 'type' => 'paragraph', 19 | 'content' => [ 20 | [ 21 | 'type' => 'text', 22 | 'text' => 'Example Text', 23 | ], 24 | ], 25 | ], 26 | ], 27 | ]; 28 | 29 | $this->assertEquals($json, (new Renderer)->render($html)); 30 | } 31 | 32 | /** @test */ 33 | public function paragraph_is_enabled_explicitly() 34 | { 35 | $html = 'Example Text
'; 36 | 37 | $json = [ 38 | 'type' => 'doc', 39 | 'content' => [ 40 | [ 41 | 'type' => 'paragraph', 42 | 'content' => [ 43 | [ 44 | 'type' => 'text', 45 | 'text' => 'Example Text', 46 | ], 47 | ], 48 | ], 49 | ], 50 | ]; 51 | 52 | $this->assertEquals($json, (new Renderer)->withNodes([ 53 | \HtmlToProseMirror\Nodes\Text::class, 54 | \HtmlToProseMirror\Nodes\Paragraph::class, 55 | ])->render($html)); 56 | } 57 | 58 | /** @test */ 59 | public function all_nodes_are_disabled() 60 | { 61 | $html = 'Example Text
'; 62 | 63 | $json = [ 64 | 'type' => 'doc', 65 | 'content' => [], 66 | ]; 67 | 68 | $this->assertEquals($json, (new Renderer)->withNodes([])->render($html)); 69 | } 70 | 71 | /** @test */ 72 | public function paragraph_is_replaced_with_a_custom_integration() 73 | { 74 | $html = 'Example Text'; 75 | 76 | $json = [ 77 | 'type' => 'doc', 78 | 'content' => [ 79 | [ 80 | 'type' => 'paragraph', 81 | 'content' => [ 82 | [ 83 | 'type' => 'text', 84 | 'text' => 'Example Text', 85 | ], 86 | ], 87 | ], 88 | ], 89 | ]; 90 | 91 | $this->assertEquals($json, (new Renderer)->replaceNode( 92 | \HtmlToProseMirror\Nodes\Paragraph::class, 93 | \HtmlToProseMirror\Test\Nodes\Custom\Paragraph::class 94 | )->render($html)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/EmojiTest.php: -------------------------------------------------------------------------------- 1 | 🔥"; 13 | 14 | $json = [ 15 | 'type' => 'doc', 16 | 'content' => [ 17 | [ 18 | 'type' => 'paragraph', 19 | 'content' => [ 20 | [ 21 | 'type' => 'text', 22 | 'text' => "🔥", 23 | ], 24 | ], 25 | ], 26 | ], 27 | ]; 28 | 29 | $this->assertEquals($json, (new Renderer)->render($html)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/EmptyTextNodesTest.php: -------------------------------------------------------------------------------- 1 |
\n"; 13 | 14 | $json = [ 15 | 'type' => 'doc', 16 | 'content' => [ 17 | [ 18 | 'type' => 'hard_break', 19 | 'marks' => [ 20 | [ 21 | 'type' => 'italic', 22 | ], 23 | ], 24 | ], 25 | ], 26 | ]; 27 | 28 | $this->assertEquals($json, (new Renderer)->render($html)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/KeepContentOfUnknownTagsTest.php: -------------------------------------------------------------------------------- 1 | ExampleText "; 13 | 14 | $json = [ 15 | 'type' => 'doc', 16 | 'content' => [ 17 | [ 18 | 'type' => 'paragraph', 19 | 'content' => [ 20 | [ 21 | 'type' => 'text', 22 | 'text' => "Example ", 23 | ], 24 | [ 25 | 'type' => 'text', 26 | 'text' => "Text", 27 | ], 28 | ], 29 | ], 30 | ], 31 | ]; 32 | 33 | $this->assertEquals($json, (new Renderer)->render($html)); 34 | } 35 | 36 | /** @test */ 37 | public function keeps_content_of_unknown_tags_even_if_it_has_known_tags() 38 | { 39 | $html = "Example
"; 40 | 41 | $json = [ 42 | 'type' => 'doc', 43 | 'content' => [ 44 | [ 45 | 'type' => 'paragraph', 46 | 'content' => [ 47 | [ 48 | 'type' => 'text', 49 | 'text' => "Example ", 50 | ], 51 | [ 52 | 'type' => 'text', 53 | 'text' => "Text", 54 | 'marks' => [ 55 | [ 56 | 'type' => 'bold', 57 | ], 58 | ], 59 | ], 60 | ], 61 | ], 62 | ], 63 | ]; 64 | 65 | $this->assertEquals($json, (new Renderer)->render($html)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Marks/BoldTest.php: -------------------------------------------------------------------------------- 1 | Example text using strong and some example text using b'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example text using strong', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'bold', 27 | ], 28 | ], 29 | ], 30 | [ 31 | 'type' => 'text', 32 | 'text' => ' and ', 33 | ], 34 | [ 35 | 'type' => 'text', 36 | 'text' => 'some example text using b', 37 | 'marks' => [ 38 | [ 39 | 'type' => 'bold', 40 | ], 41 | ], 42 | ], 43 | ], 44 | ], 45 | ], 46 | ]; 47 | 48 | $this->assertEquals($json, (new Renderer)->render($html)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Marks/CodeTest.php: -------------------------------------------------------------------------------- 1 |Text Example Text
'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example Text', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'code', 27 | ], 28 | ], 29 | ], 30 | ], 31 | ], 32 | ], 33 | ]; 34 | 35 | $this->assertEquals($json, (new Renderer)->render($html)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Marks/Custom.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'span'; 12 | } 13 | 14 | public function data() 15 | { 16 | $data = [ 17 | 'type' => 'custom', 18 | ]; 19 | 20 | $attrs = []; 21 | 22 | if ($foo = $this->DOMNode->getAttribute('data-foo')) { 23 | $attrs['foo'] = $foo; 24 | } 25 | 26 | if ($bar = $this->DOMNode->getAttribute('bar')) { 27 | $attrs['bar'] = $bar; 28 | } 29 | 30 | $data['attrs'] = $attrs; 31 | 32 | return $data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Marks/Custom/Bold.php: -------------------------------------------------------------------------------- 1 | DOMNode->nodeName === 'strong' || $this->DOMNode->nodeName === 'b'; 12 | } 13 | 14 | public function data() 15 | { 16 | return [ 17 | 'type' => 'bold', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Marks/CustomMarkTest.php: -------------------------------------------------------------------------------- 1 | Example text inside custom mark and some more text.'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example text inside custom mark', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'custom', 27 | 'attrs' => [ 28 | 'foo' => 'bla bla', 29 | 'bar' => 'nanana', 30 | ], 31 | ], 32 | ], 33 | ], 34 | [ 35 | 'type' => 'text', 36 | 'text' => ' and some more text.', 37 | ], 38 | ], 39 | ], 40 | ], 41 | ]; 42 | 43 | $renderer = (new Renderer())->addMark(Custom::class); 44 | 45 | $this->assertEquals($json, $renderer->render($html)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Marks/ItalicTest.php: -------------------------------------------------------------------------------- 1 | Example text using i and some example text using em'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example text using i', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'italic', 27 | ], 28 | ], 29 | ], 30 | [ 31 | 'type' => 'text', 32 | 'text' => ' and ', 33 | ], 34 | [ 35 | 'type' => 'text', 36 | 'text' => 'some example text using em', 37 | 'marks' => [ 38 | [ 39 | 'type' => 'italic', 40 | ], 41 | ], 42 | ], 43 | ], 44 | ], 45 | ], 46 | ]; 47 | 48 | $this->assertEquals($json, (new Renderer)->render($html)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Marks/LinkTest.php: -------------------------------------------------------------------------------- 1 | Example Link'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'text', 20 | 'text' => 'Example Link', 21 | 'marks' => [ 22 | [ 23 | 'type' => 'link', 24 | 'attrs' => [ 25 | 'href' => 'https://scrumpy.io', 26 | ], 27 | ], 28 | ], 29 | ], 30 | ], 31 | ]; 32 | 33 | $this->assertEquals($json, (new Renderer)->render($html)); 34 | } 35 | 36 | /** @test */ 37 | public function link_mark_has_support_for_rel() 38 | { 39 | $html = 'Example Link'; 40 | 41 | $json = [ 42 | 'type' => 'doc', 43 | 'content' => [ 44 | [ 45 | 'type' => 'text', 46 | 'text' => 'Example Link', 47 | 'marks' => [ 48 | [ 49 | 'type' => 'link', 50 | 'attrs' => [ 51 | 'href' => 'https://scrumpy.io', 52 | 'rel' => 'noopener', 53 | ], 54 | ], 55 | ], 56 | ], 57 | ], 58 | ]; 59 | 60 | $this->assertEquals($json, (new Renderer)->render($html)); 61 | } 62 | 63 | /** @test */ 64 | public function link_mark_has_support_for_target() 65 | { 66 | $html = 'Example Link'; 67 | 68 | $json = [ 69 | 'type' => 'doc', 70 | 'content' => [ 71 | [ 72 | 'type' => 'text', 73 | 'text' => 'Example Link', 74 | 'marks' => [ 75 | [ 76 | 'type' => 'link', 77 | 'attrs' => [ 78 | 'href' => 'https://scrumpy.io', 79 | 'target' => '_blank', 80 | ], 81 | ], 82 | ], 83 | ], 84 | ], 85 | ]; 86 | 87 | $this->assertEquals($json, (new Renderer)->render($html)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Marks/NestedMarksTest.php: -------------------------------------------------------------------------------- 1 | only bold bold and italic only bold'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'text', 20 | 'text' => 'only bold ', 21 | 'marks' => [ 22 | [ 23 | 'type' => 'bold', 24 | ], 25 | ], 26 | ], 27 | [ 28 | 'type' => 'text', 29 | 'text' => 'bold and italic', 30 | 'marks' => [ 31 | [ 32 | 'type' => 'bold', 33 | ], 34 | [ 35 | 'type' => 'italic', 36 | ], 37 | ], 38 | ], 39 | [ 40 | 'type' => 'text', 41 | 'text' => ' only bold', 42 | 'marks' => [ 43 | [ 44 | 'type' => 'bold', 45 | ], 46 | ], 47 | ], 48 | ], 49 | ]; 50 | 51 | $this->assertEquals($json, (new Renderer)->render($html)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Marks/StrikeTest.php: -------------------------------------------------------------------------------- 1 |Example text using strikeandexample text using sandexample text using del'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example text using strike', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'strike', 27 | ], 28 | ], 29 | ], 30 | [ 31 | 'type' => 'text', 32 | 'text' => ' and ', 33 | ], 34 | [ 35 | 'type' => 'text', 36 | 'text' => 'example text using s', 37 | 'marks' => [ 38 | [ 39 | 'type' => 'strike', 40 | ], 41 | ], 42 | ], 43 | [ 44 | 'type' => 'text', 45 | 'text' => ' and ', 46 | ], 47 | [ 48 | 'type' => 'text', 49 | 'text' => 'example text using del', 50 | 'marks' => [ 51 | [ 52 | 'type' => 'strike', 53 | ], 54 | ], 55 | ], 56 | ], 57 | ], 58 | ], 59 | ]; 60 | 61 | $this->assertEquals($json, (new Renderer)->render($html)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Marks/SubscriptTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example Text', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'subscript', 27 | ], 28 | ], 29 | ], 30 | ], 31 | ], 32 | ], 33 | ]; 34 | 35 | $this->assertEquals($json, (new Renderer)->render($html)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Marks/SuperscriptTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example Text', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'superscript', 27 | ], 28 | ], 29 | ], 30 | ], 31 | ], 32 | ], 33 | ]; 34 | 35 | $this->assertEquals($json, (new Renderer)->render($html)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Marks/UnderlineTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example Text', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'underline', 27 | ], 28 | ], 29 | ], 30 | ], 31 | ], 32 | ], 33 | ]; 34 | 35 | $this->assertEquals($json, (new Renderer)->render($html)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Mix/MarksInNodesTest.php: -------------------------------------------------------------------------------- 1 | Example Text."; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example ', 24 | ], 25 | [ 26 | 'type' => 'text', 27 | 'text' => 'Text', 28 | 'marks' => [ 29 | [ 30 | 'type' => 'bold', 31 | ], 32 | [ 33 | 'type' => 'italic', 34 | ], 35 | ], 36 | ], 37 | [ 38 | 'type' => 'text', 39 | 'text' => '.', 40 | ], 41 | ], 42 | ], 43 | ], 44 | ]; 45 | 46 | $this->assertEquals($json, (new Renderer)->render($html)); 47 | } 48 | 49 | /** @test */ 50 | public function complex_markup_gets_rendered_correctly() 51 | { 52 | $html = ' 53 |Headline 1
54 |55 | Some text. Bold Text. Italic Text. Bold and italic Text. Here is a Link. 56 |
57 | '; 58 | 59 | $json = [ 60 | 'type' => 'doc', 61 | 'content' => [ 62 | [ 63 | 'type' => 'heading', 64 | 'attrs' => [ 65 | 'level' => '1', 66 | ], 67 | 'content' => [ 68 | [ 69 | 'type' => 'text', 70 | 'text' => 'Headline 1', 71 | ], 72 | ], 73 | ], 74 | [ 75 | 'type' => 'paragraph', 76 | 'content' => [ 77 | [ 78 | 'type' => 'text', 79 | 'text' => 'Some text. ', 80 | ], 81 | [ 82 | 'type' => 'text', 83 | 'text' => 'Bold Text', 84 | 'marks' => [ 85 | [ 86 | 'type' => 'bold', 87 | ], 88 | ], 89 | ], 90 | [ 91 | 'type' => 'text', 92 | 'text' => '. ', 93 | ], 94 | [ 95 | 'type' => 'text', 96 | 'text' => 'Italic Text', 97 | 'marks' => [ 98 | [ 99 | 'type' => 'italic', 100 | ], 101 | ], 102 | ], 103 | [ 104 | 'type' => 'text', 105 | 'text' => '. ', 106 | ], 107 | [ 108 | 'type' => 'text', 109 | 'text' => 'Bold and italic Text', 110 | 'marks' => [ 111 | [ 112 | 'type' => 'bold', 113 | ], 114 | [ 115 | 'type' => 'italic', 116 | ], 117 | ], 118 | ], 119 | [ 120 | 'type' => 'text', 121 | 'text' => '. Here is a ', 122 | ], 123 | [ 124 | 'type' => 'text', 125 | 'text' => 'Link', 126 | 'marks' => [ 127 | [ 128 | 'type' => 'link', 129 | 'attrs' => [ 130 | 'href' => 'https://scrumpy.io', 131 | ], 132 | ], 133 | ], 134 | ], 135 | [ 136 | 'type' => 'text', 137 | 'text' => '.', 138 | ], 139 | ], 140 | ], 141 | ], 142 | ]; 143 | 144 | $this->assertEquals($json, (new Renderer)->render($html)); 145 | } 146 | 147 | /** @test */ 148 | public function multiple_lists_gets_rendered_correctly() 149 | { 150 | $html = ' 151 |Headline 2
152 |153 |
157 |- ordered list item
154 |- ordered list item
155 |- ordered list item
156 |
Some Text.
163 | '; 164 | 165 | $json = [ 166 | 'type' => 'doc', 167 | 'content' => 168 | [ 169 | [ 170 | 'type' => 'heading', 171 | 'attrs' => [ 172 | 'level' => '2', 173 | ], 174 | 'content' => [ 175 | [ 176 | 'type' => 'text', 177 | 'text' => 'Headline 2', 178 | ], 179 | ], 180 | ], 181 | [ 182 | 'type' => 'ordered_list', 183 | 'attrs' => [ 184 | 'order' => 1, 185 | ], 186 | 'content' => [ 187 | [ 188 | 'type' => 'list_item', 189 | 'content' => [ 190 | [ 191 | 'type' => 'paragraph', 192 | 'content' => [ 193 | [ 194 | 'type' => 'text', 195 | 'text' => 'ordered list item', 196 | ], 197 | ], 198 | ], 199 | ], 200 | ], 201 | [ 202 | 'type' => 'list_item', 203 | 'content' => [ 204 | [ 205 | 'type' => 'paragraph', 206 | 'content' => [ 207 | [ 208 | 'type' => 'text', 209 | 'text' => 'ordered list item', 210 | ], 211 | ], 212 | ], 213 | ], 214 | ], 215 | [ 216 | 'type' => 'list_item', 217 | 'content' => [ 218 | [ 219 | 'type' => 'paragraph', 220 | 'content' => [ 221 | [ 222 | 'type' => 'text', 223 | 'text' => 'ordered list item', 224 | ], 225 | ], 226 | ], 227 | ], 228 | ], 229 | ], 230 | ], 231 | [ 232 | 'type' => 'bullet_list', 233 | 'content' => [ 234 | [ 235 | 'type' => 'list_item', 236 | 'content' => [ 237 | [ 238 | 'type' => 'paragraph', 239 | 'content' => [ 240 | [ 241 | 'type' => 'text', 242 | 'text' => 'unordered list item', 243 | ], 244 | ], 245 | ], 246 | ], 247 | ], 248 | [ 249 | 'type' => 'list_item', 250 | 'content' => [ 251 | [ 252 | 'type' => 'paragraph', 253 | 'content' => [ 254 | [ 255 | 'type' => 'text', 256 | 'text' => 'unordered list item with ', 257 | ], 258 | [ 259 | 'type' => 'text', 260 | 'text' => 'link', 261 | 'marks' => [ 262 | [ 263 | 'type' => 'link', 264 | 'attrs' => [ 265 | 'href' => 'https://scrumpy.io', 266 | ], 267 | ], 268 | [ 269 | 'type' => 'bold', 270 | ], 271 | ], 272 | ], 273 | ], 274 | ], 275 | ], 276 | ], 277 | [ 278 | 'type' => 'list_item', 279 | 'content' => [ 280 | [ 281 | 'type' => 'paragraph', 282 | 'content' => [ 283 | [ 284 | 'type' => 'text', 285 | 'text' => 'unordered list item', 286 | ], 287 | ], 288 | ], 289 | ], 290 | ], 291 | ], 292 | ], 293 | [ 294 | 'type' => 'paragraph', 295 | 'content' => [ 296 | [ 297 | 'type' => 'text', 298 | 'text' => 'Some Text.', 299 | ], 300 | ], 301 | ], 302 | ], 303 | ]; 304 | 305 | $this->assertEquals($json, (new Renderer)->render($html)); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /tests/Mix/MultipleMarksTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Example Text', 24 | 'marks' => [ 25 | [ 26 | 'type' => 'bold', 27 | ], 28 | [ 29 | 'type' => 'italic', 30 | ], 31 | ], 32 | ], 33 | ], 34 | ], 35 | ], 36 | ]; 37 | 38 | $this->assertEquals($json, (new Renderer)->render($html)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Nodes/BlockquoteTest.php: -------------------------------------------------------------------------------- 1 |Paragraph
'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'blockquote', 20 | 'content' => [ 21 | [ 22 | 'type' => 'paragraph', 23 | 'content' => [ 24 | [ 25 | 'type' => 'text', 26 | 'text' => 'Paragraph', 27 | ], 28 | ], 29 | ], 30 | ], 31 | ], 32 | ], 33 | ]; 34 | 35 | $this->assertEquals($json, (new Renderer)->render($html)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Nodes/BulletListTest.php: -------------------------------------------------------------------------------- 1 |Example
Text
Example Text
';
14 |
15 | $json = [
16 | 'type' => 'doc',
17 | 'content' => [
18 | [
19 | 'type' => 'code_block',
20 | 'content' => [
21 | [
22 | 'type' => 'text',
23 | 'text' => 'Example Text',
24 | ],
25 | ],
26 | ],
27 | ],
28 | ];
29 |
30 | $this->assertEquals($json, (new Renderer)->render($html));
31 | }
32 |
33 | /** @test */
34 | public function code_block_with_language_gets_rendered_correctly()
35 | {
36 | $html = 'body { display: none }
';
37 |
38 | $json = [
39 | 'type' => 'doc',
40 | 'content' => [
41 | [
42 | 'type' => 'code_block',
43 | 'attrs' => [
44 | 'language' => 'css',
45 | ],
46 | 'content' => [
47 | [
48 | 'type' => 'text',
49 | 'text' => 'body { display: none }',
50 | ],
51 | ],
52 | ],
53 | ],
54 | ];
55 |
56 | $this->assertEquals($json, (new Renderer)->render($html));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Nodes/Custom.php:
--------------------------------------------------------------------------------
1 | DOMNode->nodeName === 'span';
12 | }
13 |
14 | public function data()
15 | {
16 | $data = [
17 | 'type' => 'custom',
18 | ];
19 |
20 | $attrs = [];
21 |
22 | if ($foo = $this->DOMNode->getAttribute('data-foo')) {
23 | $attrs['foo'] = $foo;
24 | }
25 |
26 | if ($bar = $this->DOMNode->getAttribute('bar')) {
27 | $attrs['bar'] = $bar;
28 | }
29 |
30 | $data['attrs'] = $attrs;
31 |
32 | return $data;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Nodes/Custom/Paragraph.php:
--------------------------------------------------------------------------------
1 | DOMNode->nodeName === 'div';
12 | }
13 |
14 | public function data()
15 | {
16 | return [
17 | 'type' => 'paragraph',
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Nodes/CustomNodeTest.php:
--------------------------------------------------------------------------------
1 | A custom node and some normal text.';
14 |
15 | $json = [
16 | 'type' => 'doc',
17 | 'content' => [
18 | [
19 | 'type' => 'paragraph',
20 | 'content' => [
21 | [
22 | 'type' => 'text',
23 | 'text' => 'A custom node ',
24 | ],
25 | [
26 | 'type' => 'custom',
27 | 'attrs' => [
28 | 'foo' => 'bla bla',
29 | 'bar' => 'nanana',
30 | ],
31 | ],
32 | [
33 | 'type' => 'text',
34 | 'text' => ' and some normal text.',
35 | ],
36 | ],
37 | ],
38 | ],
39 | ];
40 |
41 | $renderer = (new Renderer())->addNode(Custom::class);
42 |
43 | $this->assertEquals($json, $renderer->render($html));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Nodes/HardBreakTest.php:
--------------------------------------------------------------------------------
1 | Hard Example
Text
'; 45 | 46 | $json = [ 47 | 'type' => 'doc', 48 | 'content' => [ 49 | [ 50 | 'type' => 'paragraph', 51 | 'content' => [ 52 | [ 53 | 'type' => 'text', 54 | 'text' => 'Example', 55 | ], 56 | ], 57 | ], 58 | [ 59 | 'type' => 'paragraph', 60 | 'content' => [ 61 | [ 62 | 'type' => 'text', 63 | 'text' => 'Text', 64 | ], 65 | ], 66 | ], 67 | ], 68 | ]; 69 | 70 | $this->assertEquals($json, (new Renderer)->render($html)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Nodes/HeadingTest.php: -------------------------------------------------------------------------------- 1 | Example Text'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'heading', 20 | 'attrs' => [ 21 | 'level' => 1, 22 | ], 23 | 'content' => [ 24 | [ 25 | 'type' => 'text', 26 | 'text' => 'Example Text', 27 | ], 28 | ], 29 | ], 30 | ], 31 | ]; 32 | 33 | $this->assertEquals($json, (new Renderer)->render($html)); 34 | } 35 | 36 | /** @test */ 37 | public function h2_gets_rendered_correctly() 38 | { 39 | $html = 'Rule
'; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'paragraph', 20 | 'content' => [ 21 | [ 22 | 'type' => 'text', 23 | 'text' => 'Horizontal', 24 | ], 25 | ], 26 | ], 27 | [ 28 | 'type' => 'horizontal_rule', 29 | ], 30 | [ 31 | 'type' => 'paragraph', 32 | 'content' => [ 33 | [ 34 | 'type' => 'text', 35 | 'text' => 'Rule', 36 | ], 37 | ], 38 | ], 39 | ], 40 | ]; 41 | 42 | $this->assertEquals($json, (new Renderer)->render($html)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Nodes/ImageTest.php: -------------------------------------------------------------------------------- 1 | '; 14 | 15 | $json = [ 16 | 'type' => 'doc', 17 | 'content' => [ 18 | [ 19 | 'type' => 'image', 20 | 'attrs' => [ 21 | 'alt' => 'The Finished Dish', 22 | 'src' => 'https://example.com/eggs.png', 23 | 'title' => 'Eggs in a dish', 24 | ], 25 | ], 26 | ], 27 | ]; 28 | 29 | $this->assertEquals($json, (new Renderer)->render($html)); 30 | } 31 | 32 | /** @test */ 33 | public function image_gets_rendered_correctly_when_title_is_missing() 34 | { 35 | $html = 'Example
Text
Example
Text
Example
Text
'; 38 | 39 | $json = [ 40 | 'type' => 'doc', 41 | 'content' => [ 42 | [ 43 | 'type' => 'paragraph', 44 | 'content' => [ 45 | [ 46 | 'type' => 'text', 47 | 'text' => 'Example', 48 | ], 49 | ], 50 | ], 51 | [ 52 | 'type' => 'paragraph', 53 | 'content' => [ 54 | [ 55 | 'type' => 'text', 56 | 'text' => 'Text', 57 | ], 58 | ], 59 | ], 60 | ], 61 | ]; 62 | 63 | $this->assertEquals($json, (new Renderer)->render($html)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Nodes/TableTest.php: -------------------------------------------------------------------------------- 1 | ' . 14 | 'text in header cell
text in header cell with colspan 2
paragraph 1 in cell with rowspan 2
paragraph 2 in cell with rowspan 2
foo
bar
foo
bar
👩👩👦
"; 36 | 37 | $json = [ 38 | 'type' => 'doc', 39 | 'content' => [ 40 | [ 41 | 'type' => 'paragraph', 42 | 'content' => [ 43 | [ 44 | 'type' => 'text', 45 | 'text' => "👩👩👦", 46 | ], 47 | ], 48 | ], 49 | ], 50 | ]; 51 | 52 | $this->assertEquals($json, (new Renderer)->render($html)); 53 | } 54 | 55 | /** @test */ 56 | public function umlauts_are_transformed_correctly() 57 | { 58 | $html = "äöüÄÖÜß
"; 59 | 60 | $json = [ 61 | 'type' => 'doc', 62 | 'content' => [ 63 | [ 64 | 'type' => 'paragraph', 65 | 'content' => [ 66 | [ 67 | 'type' => 'text', 68 | 'text' => "äöüÄÖÜß", 69 | ], 70 | ], 71 | ], 72 | ], 73 | ]; 74 | 75 | $this->assertEquals($json, (new Renderer)->render($html)); 76 | } 77 | 78 | /** @test */ 79 | public function html_entities_are_transformed_correctly() 80 | { 81 | $html = "<
"; 82 | 83 | $json = [ 84 | 'type' => 'doc', 85 | 'content' => [ 86 | [ 87 | 'type' => 'paragraph', 88 | 'content' => [ 89 | [ 90 | 'type' => 'text', 91 | 'text' => "<", 92 | ], 93 | ], 94 | ], 95 | ], 96 | ]; 97 | 98 | $this->assertEquals($json, (new Renderer)->render($html)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | json($data); 14 | die(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/WhitespaceTest.php: -------------------------------------------------------------------------------- 1 | \nExample\n Text"; 13 | 14 | $json = [ 15 | 'type' => 'doc', 16 | 'content' => [ 17 | [ 18 | 'type' => 'paragraph', 19 | 'content' => [ 20 | [ 21 | 'type' => 'text', 22 | 'text' => "Example\nText", 23 | ], 24 | ], 25 | ], 26 | ], 27 | ]; 28 | 29 | $this->assertEquals($json, (new Renderer)->render($html)); 30 | } 31 | 32 | /** @test */ 33 | public function whitespace_in_code_blocks_is_ignored() 34 | { 35 | $html = "\n" . 36 | " Example Text\n" . 37 | "
\n" . 38 | "\n" .
39 | "Line of Code\n" .
40 | " Line of Code 2\n" .
41 | "Line of Code
";
42 |
43 | $json = [
44 | 'type' => 'doc',
45 | 'content' => [
46 | [
47 | 'type' => 'paragraph',
48 | 'content' => [
49 | [
50 | 'type' => 'text',
51 | 'text' => 'Example Text',
52 | ],
53 | ],
54 | ],
55 | [
56 | 'type' => 'code_block',
57 | 'content' => [
58 | [
59 | 'type' => 'text',
60 | 'text' => "Line of Code\n Line of Code 2\nLine of Code",
61 | ],
62 | ],
63 | ],
64 | ],
65 | ];
66 |
67 | $this->assertEquals($json, (new Renderer)->render($html));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------