Repository: ueberdosis/tiptap-php Branch: main Commit: 6ea321fa6650 Files: 150 Total size: 287.1 KB Directory structure: gitextract_xdo5lijc/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── SECURITY.md │ └── workflows/ │ ├── php-cs-fixer.yml │ ├── psalm.yml │ └── run-tests.yml ├── .gitignore ├── .php_cs.dist.php ├── .vscode/ │ └── launch.json ├── LICENSE.md ├── README.md ├── composer.json ├── package.json ├── phpunit.xml.dist ├── psalm.xml.dist ├── src/ │ ├── Core/ │ │ ├── DOMParser.php │ │ ├── DOMSerializer.php │ │ ├── Extension.php │ │ ├── JSONSerializer.php │ │ ├── Mark.php │ │ ├── Node.php │ │ ├── Schema.php │ │ └── TextSerializer.php │ ├── Editor.php │ ├── Extensions/ │ │ ├── Color.php │ │ ├── FontFamily.php │ │ ├── StarterKit.php │ │ └── TextAlign.php │ ├── Marks/ │ │ ├── Bold.php │ │ ├── Code.php │ │ ├── Highlight.php │ │ ├── Italic.php │ │ ├── Link.php │ │ ├── Strike.php │ │ ├── Subscript.php │ │ ├── Superscript.php │ │ ├── TextStyle.php │ │ └── Underline.php │ ├── Nodes/ │ │ ├── Blockquote.php │ │ ├── BulletList.php │ │ ├── CodeBlock.php │ │ ├── CodeBlockHighlight.php │ │ ├── CodeBlockShiki.php │ │ ├── Details.php │ │ ├── DetailsContent.php │ │ ├── DetailsSummary.php │ │ ├── Document.php │ │ ├── HardBreak.php │ │ ├── Heading.php │ │ ├── HorizontalRule.php │ │ ├── Image.php │ │ ├── ListItem.php │ │ ├── Mention.php │ │ ├── OrderedList.php │ │ ├── Paragraph.php │ │ ├── Table.php │ │ ├── TableCell.php │ │ ├── TableHeader.php │ │ ├── TableRow.php │ │ ├── TaskItem.php │ │ ├── TaskList.php │ │ └── Text.php │ └── Utils/ │ ├── HTML.php │ ├── InlineStyle.php │ └── Minify.php └── tests/ ├── DOMParser/ │ ├── EmojiTest.php │ ├── EmptyNodesTest.php │ ├── EmptyTextNodesTest.php │ ├── Extensions/ │ │ ├── ColorTest.php │ │ ├── FontFamilyTest.php │ │ └── TextAlignTest.php │ ├── KeepContentOfUnknownTagsTest.php │ ├── Marks/ │ │ ├── BoldTest.php │ │ ├── CodeTest.php │ │ ├── CustomMarkTest.php │ │ ├── HighlightTest.php │ │ ├── ItalicTest.php │ │ ├── LinkTest.php │ │ ├── NestedMarksTest.php │ │ ├── StrikeTest.php │ │ ├── SubscriptTest.php │ │ ├── SuperscriptTest.php │ │ ├── TextStyleTest.php │ │ └── UnderlineTest.php │ ├── MarksInNodesTest.php │ ├── MultipleMarksTest.php │ ├── Nodes/ │ │ ├── BlockquoteTest.php │ │ ├── BulletListTest.php │ │ ├── CodeBlockTest.php │ │ ├── DetailsTest.php │ │ ├── HardBreakTest.php │ │ ├── HeadingTest.php │ │ ├── HighPriorityParagraph.php │ │ ├── HorizontalRuleTest.php │ │ ├── ImageTest.php │ │ ├── MentionTest.php │ │ ├── OrderedListTest.php │ │ ├── ParagraphTest.php │ │ └── TableTest.php │ ├── ParseHTMLPriorityTest.php │ ├── SpecialCharacterTest.php │ ├── TaskListTest.php │ └── WhitespaceTest.php ├── DOMSerializer/ │ ├── ExampleJsonTest.php │ ├── Extensions/ │ │ ├── ColorTest.php │ │ ├── FontFamilyTest.php │ │ └── TextAlignTest.php │ ├── InputTest.php │ ├── Marks/ │ │ ├── BoldTest.php │ │ ├── CodeTest.php │ │ ├── HighlightTest.php │ │ ├── ItalicTest.php │ │ ├── LinkTest.php │ │ ├── StrikeTest.php │ │ ├── SubscriptTest.php │ │ ├── SuperscriptTest.php │ │ └── UnderlineTest.php │ ├── MultipleMarksTest.php │ ├── Nodes/ │ │ ├── BlockquoteTest.php │ │ ├── BulletListTest.php │ │ ├── CodeBlockHighlightTest.php │ │ ├── CodeBlockShikiTest.php │ │ ├── CodeBlockTest.php │ │ ├── DetailsTest.php │ │ ├── HardBreakNodeTest.php │ │ ├── HeadingTest.php │ │ ├── HorizontalRuleNodeTest.php │ │ ├── ImageTest.php │ │ ├── MentionTest.php │ │ ├── OrderedListTest.php │ │ ├── ParagraphTest.php │ │ ├── TableTest.php │ │ ├── TaskListTest.php │ │ └── XSSTest.php │ └── WrongFormatTest.php ├── Editor/ │ ├── DescendantsTest.php │ ├── GetDocumentTest.php │ ├── GetHTMLTest.php │ ├── GetJSONTest.php │ ├── GetTextTest.php │ ├── SanitizeTest.php │ └── SetContentTest.php ├── Pest.php ├── Schema/ │ ├── GetTopNodeTest.php │ └── PriorityTest.php └── Utils/ └── HTMLTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_size = 4 indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 ================================================ FILE: .gitattributes ================================================ # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". /.gitattributes export-ignore /.gitignore export-ignore /phpunit.xml.dist export-ignore /tests export-ignore /.editorconfig export-ignore /.php_cs export-ignore /.github export-ignore /psalm.xml export-ignore ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Contributions are **welcome** and will be fully **credited**. Please read and understand the contribution guide before creating an issue or pull request. ## Etiquette This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. ## Viability When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. ## Procedure Before filing an issue: - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. - Check to make sure your feature suggestion isn't already present within the project. - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. - Check the pull requests tab to ensure that the feature isn't already in progress. Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. ## Requirements If the project maintainer has any additional requirements, you will find them listed here. - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). - **Add tests!** - Your patch won't be accepted if it doesn't have tests. - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. **Happy coding**! ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report title: "[Bug]: " description: Found a bug? Report it here to help us improve. labels: - "bug" body: - type: markdown attributes: value: "### Please provide details to help us diagnose the bug." - type: input id: php_version attributes: label: PHP Version description: The version of PHP you are using. placeholder: e.g. 8.0 validations: required: true - type: input id: version attributes: label: Version description: Specify the version of Tiptap PHP you are using. placeholder: 1.0.0 validations: required: true - type: textarea id: problem attributes: label: Bug Description description: Provide a clear and concise description of what the bug is. placeholder: "The issue occurs when..." validations: required: true - type: markdown attributes: value: | ### Additional Information Please provide any additional information that may help us understand the issue. - type: textarea id: expectation attributes: label: Expected Behavior description: Describe what you expected to happen. validations: required: true - type: textarea id: context attributes: label: Additional Context (Optional) description: "Add any other context about the problem here, such as screenshots or videos." - type: checkboxes attributes: label: Dependency Updates description: "Have you updated your dependencies? This can often resolve issues." options: - label: Yes, I've updated all my dependencies. required: true - type: markdown attributes: value: "Thank you for helping us improve our open-source projects by reporting this issue!" ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Request a feature description: Share ideas for new features title: "[Feature Request]: " labels: - "enhancement" body: - type: markdown attributes: value: | ## Feature Request Thank you for considering contributing to Tiptap PHP! We welcome your ideas and suggestions for new features. - type: textarea id: feature_description attributes: label: "Feature Description" description: "Please provide a clear and concise description of the feature you would like to see." placeholder: "e.g. I would like to have a new command to..." validations: required: true - type: textarea id: additional_context attributes: label: "Additional Context" description: "Please provide any additional context or information that may be helpful in understanding your request." placeholder: "e.g. use case, related features, etc." ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy If you discover any security related issues, please email humans@tiptap.dev instead of using the issue tracker. ================================================ FILE: .github/workflows/php-cs-fixer.yml ================================================ name: Check & fix styling on: push: branches: - main - develop - next - release/* pull_request: branches: - main - develop - next workflow_dispatch: jobs: php-cs-fixer: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - name: Run PHP CS Fixer uses: docker://oskarstark/php-cs-fixer-ga with: args: --config=.php_cs.dist.php --allow-risky=yes - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: Fix styling ================================================ FILE: .github/workflows/psalm.yml ================================================ name: Psalm on: push: branches: - main - develop - next - release/* pull_request: branches: - main - develop - next workflow_dispatch: jobs: psalm: name: psalm runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "8.3" extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick coverage: none - name: Cache composer dependencies uses: actions/cache@v4 with: path: vendor key: composer-${{ hashFiles('composer.lock') }} - name: Run composer install run: composer install -n --prefer-dist - name: Run psalm run: ./vendor/bin/psalm --output-format=github ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: Tests on: push: branches: - main - develop - next - release/* pull_request: branches: - main - develop - next workflow_dispatch: jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] php: [8.1, 8.2, 8.3] stability: [prefer-lowest, prefer-stable] node-version: [22] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2.5.1 with: node-version: ${{ matrix.node-version }} - name: Load cached dependencies uses: actions/cache@v4 id: cache with: path: | **/node_modules key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} - name: Install dependencies id: install-dependencies if: steps.cache.outputs.cache-hit != 'true' run: npm install - name: Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests run: vendor/bin/pest ================================================ FILE: .gitignore ================================================ .idea .php_cs .php_cs.cache .phpunit.result.cache build composer.lock coverage docs phpunit.xml psalm.xml vendor .php-cs-fixer.cache node_modules ================================================ FILE: .php_cs.dist.php ================================================ in([ __DIR__ . '/src', __DIR__ . '/tests', ]) ->name('*.php') ->notName('*.blade.php') ->ignoreDotFiles(true) ->ignoreVCS(true); return (new PhpCsFixer\Config()) ->setRules([ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, 'not_operator_with_successor_space' => true, 'trailing_comma_in_multiline' => true, 'phpdoc_scalar' => true, 'unary_operator_spaces' => true, 'binary_operator_spaces' => true, 'blank_line_before_statement' => [ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], ], 'phpdoc_single_line_var_spacing' => true, 'phpdoc_var_without_name' => true, 'method_argument_space' => [ 'on_multiline' => 'ensure_fully_multiline', 'keep_multiple_spaces_after_comma' => true, ], 'single_trait_insert_per_statement' => true, ]) ->setFinder($finder); ================================================ FILE: .vscode/launch.json ================================================ { "configurations": [ { "type": "php", "request": "launch", "name": "Run Test", "program": "${workspaceFolder}/vendor/bin/pest", "args": [ "--filter", "${input:testFilter}" ], "runtimeArgs": [ "-dxdebug.mode=debug", "-dxdebug.start_with_request=trigger" ], "cwd": "${workspaceFolder}", "port": 9003 } ], "inputs": [ { "type": "promptString", "id": "testFilter", "description": "Filter by test", "default": "" } ] } ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) überdosis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Tiptap for PHP [![Latest Version on Packagist](https://img.shields.io/packagist/v/ueberdosis/tiptap-php.svg?style=flat-square)](https://packagist.org/packages/ueberdosis/tiptap-php) [![GitHub Tests Action Status](https://github.com/ueberdosis/tiptap-php/actions/workflows/run-tests.yml/badge.svg)](https://github.com/ueberdosis/tiptap-php/actions/workflows/run-tests.yml) [![Total Downloads](https://img.shields.io/packagist/dt/ueberdosis/tiptap-php.svg?style=flat-square)](https://packagist.org/packages/ueberdosis/tiptap-php) [![License](https://img.shields.io/packagist/l/ueberdosis/tiptap-php?style=flat-square)](https://packagist.org/packages/ueberdosis/tiptap-php) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg?sanitize=true)](https://discord.gg/WtJ49jGshW) [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) A PHP package to work with [Tiptap](https://tiptap.dev/) content. You can transform Tiptap-compatible JSON to HTML, and the other way around, sanitize your content, or just modify it. ## Installation You can install the package via composer: ```bash composer require ueberdosis/tiptap-php ``` ## Usage The PHP package mimics large parts of the JavaScript package. If you know your way around Tiptap, the PHP syntax will feel familiar to you. ### Convert Tiptap HTML to JSON Let’s start by converting a HTML snippet to a PHP array with a Tiptap-compatible structure: ```php (new \Tiptap\Editor) ->setContent('

Example Text

') ->getDocument(); // Returns: // ['type' => 'doc', 'content' => …] ``` You can get a JSON string in PHP, too. ```php (new \Tiptap\Editor) ->setContent('

Example Text

') ->getJSON(); // Returns: // {"type": "doc", "content": …} ``` ### Convert Tiptap JSON to HTML The other way works aswell. Just pass a JSON string or an PHP array to generate the HTML. ```php (new \Tiptap\Editor) ->setContent([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ] ] ], ]) ->getHTML(); // Returns: //

Example Text

``` This doesn’t fully adhere to the ProseMirror schema. Some things are supported too, for example aren’t marks allowed in a `CodeBlock`. If you need better schema support, create an issue with the feature you’re missing. ### Syntax highlighting for code blocks with [highlight.php](https://github.com/scrivo/highlight.php) The default `CodeBlock` extension doesn’t add syntax highlighting to your code blocks. However, if you want to add syntax highlighting to your code blocks, there’s a special `CodeBlockHighlight` extension. Swapping our the default one works like that: ```php (new \Tiptap\Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit([ 'codeBlock' => false, ]), new \Tiptap\Nodes\CodeBlockHighlight(), ], ])) ->setContent('
<?php phpinfo()
') ->getHTML(); // Returns: //
<?php phpinfo()
``` This is still unstyled. You need to [load a CSS file](https://highlightjs.org/download/) to add colors to the output, for example like that: ```html ``` Boom, syntax highlighting! By the way, this is powered by the amazing [scrivo/highlight.php](https://github.com/scrivo/highlight.php). ### Syntax highlighting for code blocks with [Shiki](https://github.com/shikijs/shiki) (Requires Node.js) There is an alternate syntax highlighter that utilizes [Shiki](https://github.com/shikijs/shiki). Shiki is a beautiful syntax highlighter powered by the same language engine that many code editors use. The major differences from the `CodeBlockHighlight` extensions are: 1. you must install the `shiki` npm package. 2. Shiki code highlighting works by injecting inline styles so pulling in a external css file is not required. 3. you can use most VS Code themes to highlight your code. To use the Shiki extension, first install the npm package ```bash npm install shiki ``` Then follow the example below: ```php (new \Tiptap\Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit([ 'codeBlock' => false, ]), new \Tiptap\Nodes\CodeBlockShiki(), ], ])) ->setContent('
<?php phpinfo()
') ->getHTML(); ``` To configure the theme or default language for code blocks pass additonal configuration into the constructor as show below: ```php (new \Tiptap\Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit([ 'codeBlock' => false, ]), new \Tiptap\Nodes\CodeBlockShiki([ 'theme' => 'github-dark', // default: nord, see https://github.com/shikijs/shiki/blob/main/docs/themes.md 'defaultLanguage' => 'php', // default: html, see https://github.com/shikijs/shiki/blob/main/docs/languages.md 'guessLanguage' => true, // default: true, if the language isn’t passed, it tries to guess the language with highlight.php ]), ], ])) ->setContent('
<?php phpinfo()
') ->getHTML(); ``` Under the hood the Shiki extension utilizes [Shiki PHP by Spatie](https://github.com/spatie/shiki-php), so please see the documentation for additional details and considerations. ### Convert content to plain text Content can also be transformed to plain text, for example to put it into a search index. ```php (new \Tiptap\Editor) ->setContent('

Heading

Paragraph

') ->getText(); // Returns: // "Heading // // Paragraph" ``` What’s coming between blocks can be configured, too. ```php (new \Tiptap\Editor) ->setContent('

Heading

Paragraph

') ->getText([ 'blockSeparator' => "\n", ]); // Returns: // "Heading // Paragraph" ``` ### Sanitize content A great use case for the PHP package is to clean (or “sanitize”) the content. You can do that with the `sanitize()` method. Works with JSON strings, PHP arrays and HTML. It’ll return the same format you’re using as the input format. ```php (new \Tiptap\Editor) ->sanitize('

Example Text

'); // Returns: // '

Example Text

' ``` ### Modifying the content With the `descendants()` method you can loop through all nodes recursively as you are used to from the JavaScript package. But in PHP, you can even modify the node to update attributes and all that. > Warning: You need to add `&` to the parameter. Thats keeping a reference to the original item and allows to modify the original one, instead of just a copy. ```php $editor->descendants(function (&$node) { if ($node->type !== 'heading') { return; } $node->attrs->level = 1; }); ``` ### Configuration Pass the configuration to the constructor of the editor. There’s not much to configure, but at least you can pass the initial content and load specific extensions. ```php new \Tiptap\Editor([ 'content' => '

Example Text

', 'extensions' => [ new \Tiptap\Extensions\StarterKit, ], ]) ``` The `StarterKit` is loaded by default. If you just want to use that, there’s no need to set it. ### Extensions By default, the [`StarterKit`](https://tiptap.dev/api/extensions/starter-kit) is loaded, but you can pass a custom array of extensions aswell. ```php new \Tiptap\Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Link, ], ]) ``` ### Configure extensions Some extensions can be configured. Just pass an array to the constructor, that’s it. We’re aiming to support the same configuration as the JavaScript package. ```php new \Tiptap\Editor([ 'extensions' => [ // … new \Tiptap\Nodes\Heading([ 'levels' => [1, 2, 3], ]), ], ]) ``` You can pass custom HTML attributes through the configuration, too. ```php new \Tiptap\Editor([ 'extensions' => [ // … new \Tiptap\Nodes\Heading([ 'HTMLAttributes' => [ 'class' => 'my-custom-class', ], ]), ], ]) ``` For the `StarterKit`, it’s slightly different, but works as you are used to from the JavaScript package. ```php new \Tiptap\Editor([ 'extensions' => [ new Tiptap\Extensions\StarterKit([ 'codeBlock' => false, 'heading' => [ 'HTMLAttributes' => [ 'class' => 'my-custom-class', ], ] ]), ], ]) ``` ### Extend existing extensions If you need to change minor details of the supported extensions, you can just extend an extension. ```php instead of return ['b', 0] } } new \Tiptap\Editor([ 'extensions' => [ new Paragraph, new Text, new CustomBold, ], ]) ``` #### Custom extensions You can even build custom extensions. If you are used to the JavaScript API, you will be surprised how much of that works in PHP, too. 🤯 Find a simple example below. Make sure to dig through the extensions in this repository to learn more about the PHP extension API. ```php [], ]; } public function parseHTML() { return [ [ 'tag' => 'my-custom-tag[data-id]', ], [ 'tag' => 'my-custom-tag', 'getAttrs' => function ($DOMNode) { return ! \Tiptap\Utils\InlineStyle::hasAttribute($DOMNode, [ 'background-color' => '#000000', ]) ? null : false; }, ], [ 'style' => 'background-color', 'getAttrs' => function ($value) { return (bool) preg_match('/^(black)$/', $value) ? null : false; }, ], ]; } public function renderHTML($node) { return ['my-custom-tag', ['class' => 'foobar'], 0] } } ``` #### Extension priority Extensions are evaluated in the order of descending priority. By default, all Nodes, Marks, and Extensions, have a priority value of `100`. Priority should be defined when creating a Node extension to match markup that could be matched be other Nodes - an example of this is the [TaskItem Node](src/Nodes/TaskItem.php) which has evaluation priority over the [ListItem Node](src/Nodes/ListItem.php). ## Testing ```bash composer test ``` You can install nodemon (`npm install -g nodemon`) to keep the test suite running and watch for file changes: ```bash composer test-watch ``` ## Contributing Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits - [Hans Pagel](https://github.com/hanspagel) - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. ================================================ FILE: composer.json ================================================ { "name": "ueberdosis/tiptap-php", "description": "A PHP package to work with Tiptap output", "keywords": [ "ueberdosis", "tiptap", "prosemirror" ], "homepage": "https://github.com/ueberdosis/tiptap-php", "license": "MIT", "authors": [ { "name": "Hans Pagel", "email": "humans@tiptap.dev", "role": "Developer" } ], "require": { "php": "^8.0", "scrivo/highlight.php": "^9.18", "spatie/shiki-php": "^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.5", "pestphp/pest": "^1.21", "phpunit/phpunit": "^9.5", "vimeo/psalm": "^4.3" }, "autoload": { "psr-4": { "Tiptap\\": "src" } }, "autoload-dev": { "psr-4": { "Tiptap\\Tests\\": "tests" } }, "scripts": { "psalm": "vendor/bin/psalm", "psalm-watch": "nodemon --exec './vendor/bin/psalm || exit 1' --ext php", "test": "./vendor/bin/pest", "test-watch": "nodemon --exec './vendor/bin/pest || exit 1' --ext php", "test-coverage": "./vendor/bin/pest --coverage-html coverage", "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes --config=.php_cs.dist.php" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true } }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: package.json ================================================ { "name": "tiptap-php", "private": true, "description": "This package.json has all Node dependencies for the local development of the package.", "homepage": "https://github.com/ueberdosis/tiptap-php", "devDependencies": { "shiki": "^2.0.0" } } ================================================ FILE: phpunit.xml.dist ================================================ tests ./src ================================================ FILE: psalm.xml.dist ================================================ ================================================ FILE: src/Core/DOMParser.php ================================================ schema = $schema; } public function process(string $value): array { $this->setDocument($value); $content = $this->processChildren( $this->getDocumentBody() ); return [ 'type' => $this->schema->topNode::$name, 'content' => $content, ]; } private function setDocument(string $value): DOMParser { libxml_use_internal_errors(true); $this->DOM = new DOMDocument; /** * @psalm-suppress ArgumentTypeCoercion */ $this->DOM->loadHTML( $this->makeValidXMLDocument( $this->minify($value) ) ); return $this; } private function minify(string $value): string { return (new Minify)->process($value); } private function makeValidXMLDocument($value): string { return '' . $value; } private function getDocumentBody(): DOMElement { return $this->DOM->getElementsByTagName('body')->item(0); } private function processChildren($node): array { $nodes = []; foreach ($node->childNodes as $child) { if ($class = $this->getNodeFor($child)) { $item = $this->parseAttributes($class, $child); if ($item === null) { if ($child->hasChildNodes()) { $nodes = array_merge($nodes, $this->processChildren($child)); } continue; } if ($child->hasChildNodes()) { $item = array_merge($item, [ 'content' => $this->processChildren($child), ]); } if (count($this->storedMarks)) { $item = array_merge($item, [ 'marks' => $this->storedMarks, ]); } array_push($nodes, $item); } elseif ($class = $this->getMarkFor($child)) { array_push($this->storedMarks, $this->parseAttributes($class, $child)); if ($child->hasChildNodes()) { $nodes = array_merge($nodes, $this->processChildren($child)); } array_pop($this->storedMarks); } elseif ($child->hasChildNodes()) { $nodes = array_merge($nodes, $this->processChildren($child)); } } // If similar nodes with different text follow each other, // we can merge them into a single node. return $this->mergeSimilarNodes($nodes); } private function isMultidimensionalArray($array) { foreach ($array as $value) { if (is_array($value)) { return true; } } return false; } private function mergeSimilarNodes($nodes) { $result = []; /** * @psalm-suppress UnusedFunctionCall */ array_reduce($nodes, function ($carry, $node) use (&$result) { // Ignore multidimensional arrays if ($this->isMultidimensionalArray($node) || $this->isMultidimensionalArray($carry)) { $result[] = $node; return $node; } // Check if text is the only difference $differentKeys = array_keys(array_diff($carry, $node)); if ($differentKeys != ['text']) { $result[] = $node; return $node; } // Merge it! $result[count($result) - 1]['text'] .= $node['text']; return $result[count($result) - 1]; }, []); return $result; } private function getNodeFor($item) { return $this->getExtensionFor($item, $this->schema->nodes); } private function getMarkFor($item) { return $this->getExtensionFor($item, $this->schema->marks); } private function getExtensionFor($node, $classes) { $parseRules = []; foreach ($classes as $class) { $classParseRules = $this->getClassParseRules($class, $node); $parseRules = array_merge($parseRules, $classParseRules); } usort($parseRules, fn ($parseRuleA, $parseRuleB) => $parseRuleB['priority'] - $parseRuleA['priority']); foreach ($parseRules as $parseRule) { if ($this->checkParseRule($parseRule, $node)) { return $parseRule['class']; } } return false; } private function getClassParseRules($class, $node): array { $parseRules = $class->parseHTML($node); if (! is_array($parseRules)) { return []; } $classParseRules = []; foreach ($parseRules as $parseRule) { $parseRule['class'] = $class; $parseRule['priority'] = $parseRule['priority'] ?? 50; $classParseRules[] = $parseRule; } return $classParseRules; } private function checkParseRule($parseRule, $DOMNode): bool { // ['tag' => 'span[type="mention"]'] if (isset($parseRule['tag'])) { if (preg_match('/([a-zA-Z-]*)\[([a-z-]+)(="?([a-zA-Z]*)"?)?\]$/', $parseRule['tag'], $matches)) { $tag = $matches[1]; $attribute = $matches[2]; if (isset($matches[4])) { $value = $matches[4]; } } else { $tag = $parseRule['tag']; } if ($tag !== $DOMNode->nodeName) { return false; } if (isset($attribute) && ! $DOMNode->hasAttribute($attribute)) { return false; } if (isset($attribute) && isset($value) && $DOMNode->getAttribute($attribute) !== $value) { return false; } } // ['style' => 'font-weight=italic'] if (isset($parseRule['style'])) { if (preg_match('/([a-zA-Z-]*)(="?([a-zA-Z-]*)"?)?$/', $parseRule['style'], $matches)) { $style = $matches[1]; if (isset($matches[3])) { $value = $matches[3]; } } else { $style = $parseRule['style']; } if (! InlineStyle::hasAttribute($DOMNode, $style)) { return false; } if (isset($value) && InlineStyle::getAttribute($DOMNode, $style) !== $value) { return false; } } // ['getAttrs' => function($DOMNode) { … }] if (isset($parseRule['getAttrs'])) { if (isset($parseRule['style']) && InlineStyle::hasAttribute($DOMNode, $parseRule['style'])) { $parameter = InlineStyle::getAttribute($DOMNode, $parseRule['style']); } else { $parameter = $DOMNode; } if ($parseRule['getAttrs']($parameter) === false) { return false; } } if ( ! is_array($parseRule) || ! count($parseRule) || ( ! isset($parseRule['tag']) && ! isset($parseRule['style']) && ! isset($parseRule['getAttrs']) )) { return false; } return true; } /** * @return (array|mixed|string)[]|null * * @psalm-return array{type: mixed, text?: string, attrs?: array}|null */ private function parseAttributes($class, $DOMNode): ?array { $item = [ 'type' => $class::$name, ]; if ($class::$name === 'text') { $text = ltrim($DOMNode->nodeValue, "\n"); if ($text === '') { return null; } $item = array_merge($item, [ 'text' => $text, ]); } $parseRules = $class->parseHTML(); if (! is_array($parseRules)) { return $item; } foreach ($parseRules as $parseRule) { if (! $this->checkParseRule($parseRule, $DOMNode)) { continue; } $attributes = $parseRule['attrs'] ?? []; if (count($attributes)) { if (! isset($item['attrs'])) { $item['attrs'] = []; } $item['attrs'] = array_merge($item['attrs'], $attributes); } if (isset($parseRule['getAttrs'])) { if (isset($parseRule['style']) && InlineStyle::hasAttribute($DOMNode, $parseRule['style'])) { $parameter = InlineStyle::getAttribute($DOMNode, $parseRule['style']); } else { $parameter = $DOMNode; } $attributes = $parseRule['getAttrs']($parameter); if (! is_array($attributes)) { continue; } if (! isset($item['attrs'])) { $item['attrs'] = []; } $item['attrs'] = array_merge($item['attrs'], $attributes); } } /** * public function addAttributes() * { * return [ * 'href' => [ * 'parseHTML' => function ($DOMNode) { * $attrs['href'] = $DOMNode->getAttribute('href'); * } * ], * ]; * } */ foreach ($this->schema->getAttributeConfigurations($class) as $attribute => $configuration) { if (isset($configuration['parseHTML'])) { $value = $configuration['parseHTML']($DOMNode); } else { $value = $DOMNode->getAttribute($attribute) ?: null; } if ($value !== null) { $item['attrs'][$attribute] = $value; } } return $item; } } ================================================ FILE: src/Core/DOMSerializer.php ================================================ schema = $schema; } private function renderNode($node, $previousNode = null, $nextNode = null, &$markStack = []): string { $html = []; $markTagsToClose = []; if (isset($node->marks)) { foreach ($node->marks as $mark) { foreach ($this->schema->marks as $class) { $renderClass = $class; if (! $this->isMarkOrNode($mark, $renderClass)) { continue; } if (! $this->markShouldOpen($mark, $previousNode)) { continue; } $html[] = $this->renderOpeningTag($renderClass, $mark); # push recently created mark tag to the stack $markStack[] = [$renderClass, $mark]; } } } foreach ($this->schema->nodes as $extension) { if (! $this->isMarkOrNode($node, $extension)) { continue; } $html[] = $this->renderOpeningTag($extension, $node); break; } // ["content" => …] $lastKey = array_key_last($html); $lastElement = $html[$lastKey] ?? null; if (! is_null($lastKey) && isset($lastElement['content'])) { $html[$lastKey] = $lastElement['content']; } // child nodes elseif (isset($node->content)) { $nestedNodeMarkStack = []; foreach ($node->content as $index => $nestedNode) { $previousNestedNode = $node->content[$index - 1] ?? null; $nextNestedNode = $node->content[$index + 1] ?? null; $html[] = $this->renderNode($nestedNode, $previousNestedNode, $nextNestedNode, $nestedNodeMarkStack); } } // renderText($node) elseif (isset($extension) && method_exists($extension, 'renderText')) { $html[] = $extension->renderText($node); } // text elseif (isset($node->text)) { $html[] = htmlspecialchars($node->text, ENT_QUOTES, 'UTF-8'); } foreach ($this->schema->nodes as $extension) { if (! $this->isMarkOrNode($node, $extension)) { continue; } $html[] = $this->renderClosingTag($extension->renderHTML($node)); } if (isset($node->marks)) { foreach (array_reverse($node->marks) as $mark) { foreach ($this->schema->marks as $extension) { if (! $this->isMarkOrNode($mark, $extension)) { continue; } if (! $this->markShouldClose($mark, $nextNode)) { continue; } # remember which mark tags to close $markTagsToClose[] = [$extension, $mark]; } } # close mark tags and reopen when necessary $html = array_merge($html, $this->closeAndReopenTags($markTagsToClose, $markStack)); } return join($html); } private function closeAndReopenTags(array $markTagsToClose, array &$markStack): array { $markTagsToReopen = []; $closingTags = $this->closeMarkTags($markTagsToClose, $markStack, $markTagsToReopen); $reopeningTags = $this->reopenMarkTags($markTagsToReopen, $markStack); return array_merge($closingTags, $reopeningTags); } private function closeMarkTags($markTagsToClose, &$markStack, &$markTagsToReopen): array { $html = []; while (! empty($markTagsToClose)) { # close mark tag from the top of the stack $markTag = array_pop($markStack); $markExtension = $markTag[0]; $mark = $markTag[1]; $html[] = $this->renderClosingTag($markExtension->renderHTML($mark)); # check if the last closed tag is overlapping and has to be reopened # find the first matching mark to close $foundIndex = null; foreach ($markTagsToClose as $index => $markToClose) { if ($markExtension == $markToClose[0] && $mark == $markToClose[1]) { $foundIndex = $index; break; } } if ($foundIndex === null) { $markTagsToReopen[] = $markTag; } else { # specific mark tag does not have to be reopened, but deleted from the 'to close' list unset($markTagsToClose[$foundIndex]); $markTagsToClose = array_values($markTagsToClose); // Re-index array } } return $html; } private function reopenMarkTags($markTagsToReopen, &$markStack): array { $html = []; # reopen the overlapping mark tags and push them to the stack foreach (array_reverse($markTagsToReopen) as $markTagToOpen) { $renderClass = $markTagToOpen[0]; $mark = $markTagToOpen[1]; $html[] = $this->renderOpeningTag($renderClass, $mark); $markStack[] = [$renderClass, $mark]; } return $html; } private function isMarkOrNode($markOrNode, $renderClass): bool { return isset($markOrNode->type) && $markOrNode->type === $renderClass::$name; } private function markShouldOpen($mark, $previousNode): bool { return $this->nodeHasMark($previousNode, $mark); } private function markShouldClose($mark, $nextNode): bool { return $this->nodeHasMark($nextNode, $mark); } private function nodeHasMark($node, $mark): bool { if (! $node) { return true; } if (! property_exists($node, 'marks')) { return true; } // The other node has same mark foreach ($node->marks as $otherMark) { if ($mark == $otherMark) { return false; } } return true; } private function renderOpeningTag($extension, $nodeOrMark, $renderHTML = false) { /** * public function addAttributes() * { * return [ * 'color' => [ * 'renderHTML' => function ($attributes) { * return [ * 'style' => "color: {$attributes['color']}", * ]; * } * ], * ]; * } */ $HTMLAttributes = []; foreach ($this->schema->getAttributeConfigurations($extension) as $attribute => $configuration) { // 'rendered' => false if (isset($configuration['rendered']) && $configuration['rendered'] === false) { continue; } // 'default' => 'foobar' if (! isset($nodeOrMark->attrs->{$attribute}) && isset($configuration['default'])) { if (! isset($nodeOrMark->attrs)) { $nodeOrMark->attrs = new stdClass; } $nodeOrMark->attrs->{$attribute} = $configuration['default']; } // 'renderHTML' => fn($attributes) … if (isset($configuration['renderHTML'])) { $value = $configuration['renderHTML']($nodeOrMark->attrs ?? new stdClass); } else { $value = [ $attribute => $nodeOrMark->attrs->{$attribute} ?? null, ]; } if ($value !== null) { $HTMLAttributes = HTML::mergeAttributes($HTMLAttributes, $value); } } // Remove empty attributes $HTMLAttributes = array_filter($HTMLAttributes, fn ($HTMLAttribute) => $HTMLAttribute !== null); if ($renderHTML === false) { $renderHTML = $extension->renderHTML($nodeOrMark, $HTMLAttributes); } // ["content" => …] if (isset($renderHTML['content'])) { return $renderHTML; } // null if (is_null($renderHTML)) { return ''; } // ['table', ['tbody', 0]] // ['table', ['class' => 'foobar'], ['tbody', 0]] if (is_array($renderHTML)) { $html = []; foreach ($renderHTML as $index => $renderInstruction) { // ['div', …] if (is_string($renderInstruction)) { if (is_integer($index) && $nextTag = $renderHTML[$index + 1] ?? null) { // ['table', ['class' => 'custom-class']] if (! in_array(0, $nextTag, true)) { if (is_array($nextTag) && $this->isAnAttributeArray($nextTag)) { $attributes = HTML::renderAttributes($nextTag); } else { $attributes = ''; } // $html[] = "<{$renderInstruction}{$attributes}>"; } else { $html[] = "<{$renderInstruction}>"; } } else { $html[] = "<{$renderInstruction}>"; } // ['div', 'span'] if (isset($nextTag) && is_array($nextTag) && ! in_array(0, $nextTag, true)) { if (! $this->isAnAttributeArray($nextTag)) { $html[] = $this->renderOpeningTag($extension, $nodeOrMark, $nextTag); $html[] = $this->renderClosingTag($nextTag); } } // ['div', ?, 'span'] if (is_integer($index) && $nextTag = $renderHTML[$index + 2] ?? null) { if (! in_array(0, $nextTag, true)) { if (! $this->isAnAttributeArray($nextTag)) { $html[] = $this->renderOpeningTag($extension, $nodeOrMark, $nextTag); $html[] = $this->renderClosingTag($nextTag); } } } continue; } // ['tbody', 0] elseif (is_array($renderInstruction) && in_array(0, $renderInstruction, true)) { $html[] = $this->renderOpeningTag($extension, $nodeOrMark, $renderInstruction); } // ['class' => 'foobar'] elseif (is_array($renderInstruction)) { continue; } } return join($html); } throw new \Exception('[renderOpeningTag] Failed to use renderHTML: ' . json_encode($renderHTML)); } private function isAnAttributeArray($items): bool { if (! is_array($items)) { return false; } $keys = array_keys($items); return $keys !== array_keys($keys); } private function isSelfClosing($tag): bool { $dom = new DOMDocument('1.0', 'utf-8'); $element = $dom->createElement($tag, 'test'); $dom->appendChild($element); $rendered = $dom->saveHTML(); return substr_count($rendered, $tag) === 1; } /** * @return null|string */ private function renderClosingTag($renderHTML) { // null if (is_null($renderHTML)) { return ''; } // ["content" => …] if (isset($renderHTML['content'])) { return; } // ['table', ['tbody']] if (is_array($renderHTML)) { $html = []; foreach (array_reverse($renderHTML) as $renderInstruction) { // 'div' if (is_string($renderInstruction)) { if ($this->isSelfClosing($renderInstruction)) { return null; } $html[] = ""; } // ['div', 0] elseif (is_array($renderInstruction) && in_array(0, $renderInstruction, true)) { $html[] = $this->renderClosingTag($renderInstruction); } } return join($html); } throw new \Exception('[renderClosingTag] Failed to use renderHTML: ' . json_encode($renderHTML)); } public function process(array $value): string { $html = []; // transform document to object $this->document = json_decode(json_encode($value)); $content = is_array($this->document->content) ? $this->document->content : []; $markStack = []; foreach ($content as $index => $node) { $previousNode = $content[$index - 1] ?? null; $nextNode = $content[$index + 1] ?? null; $html[] = $this->renderNode($node, $previousNode, $nextNode, $markStack); } return join($html); } } ================================================ FILE: src/Core/Extension.php ================================================ options = array_merge($this->addOptions(), $options); } public function addOptions() { return []; } public function addGlobalAttributes() { return []; } public function addExtensions() { return []; } } ================================================ FILE: src/Core/JSONSerializer.php ================================================ document = json_decode(json_encode($value)); return json_encode($this->document); } } ================================================ FILE: src/Core/Mark.php ================================================ allExtensions = $this->loadExtensions($extensions); usort($this->allExtensions, fn ($a, $b) => $b::$priority - $a::$priority); $this->nodes = array_filter($this->allExtensions, function ($extension) { return is_subclass_of($extension, \Tiptap\Core\Node::class); }); $this->marks = array_filter($this->allExtensions, function ($extension) { return is_subclass_of($extension, \Tiptap\Core\Mark::class); }); $this->extensions = array_filter($this->allExtensions, function ($extension) { return is_subclass_of($extension, \Tiptap\Core\Extension::class); }); $this->defaultNode = reset($this->nodes); $this->topNode = current(array_filter($this->nodes, fn ($node) => $node::$topNode)); return $this; } private function loadExtensions($extensions = []) { foreach ($extensions as $extension) { if (method_exists($extension, 'addExtensions') && count($extension->addExtensions())) { $extensions = array_merge( $extensions, $this->loadExtensions($extension->addExtensions()), ); } if (method_exists($extension, 'addGlobalAttributes')) { $globalAttributes = $extension->addGlobalAttributes(); foreach ($globalAttributes as $globalAttributeConfiguration) { foreach ($globalAttributeConfiguration['types'] ?? [] as $type) { $this->globalAttributes[$type] = array_merge( $this->globalAttributes[$type] ?? [], $globalAttributeConfiguration['attributes'] ); } } } } return $extensions; } public function apply($document) { if (! is_array($document['content'])) { return $document; } $document['content'] = array_map(function ($node) { foreach ($this->allExtensions as $extension) { if (! isset($node['type']) || $node['type'] !== $extension::$name) { continue; } if (property_exists($extension, 'marks')) { if ($extension::$marks === '') { $node = $this->filterMarks($node); unset($node['marks']); } // TODO: Support for multiple marks is missing } break; } return $node; }, $document['content']); return $document; } public function filterMarks(&$node) { unset($node['marks']); if (isset($node['content'])) { $node['content'] = array_map(function ($child) { return $this->filterMarks($child); }, $node['content']); } return $node; } public function getAttributeConfigurations($class): array { return array_merge( $this->globalAttributes[$class::$name] ?? [], $class->addAttributes(), ); } } ================================================ FILE: src/Core/TextSerializer.php ================================================ "\n\n", ]; public function __construct($schema, $configuration = []) { $this->schema = $schema; $this->configuration = array_merge($this->configuration, $configuration); } public function process(array $value): string { $html = []; // transform document to object $this->document = json_decode(json_encode($value)); $content = is_array($this->document->content) ? $this->document->content : []; foreach ($content as $node) { $html[] = $this->renderNode($node); } return join($this->configuration['blockSeparator'], $html); } private function renderNode($node): string { $text = []; if (isset($node->content)) { foreach ($node->content as $nestedNode) { $text[] = $this->renderNode($nestedNode); } } elseif (isset($node->text)) { $text[] = htmlspecialchars($node->text, ENT_QUOTES, 'UTF-8'); } return join($this->configuration['blockSeparator'], $text); } } ================================================ FILE: src/Editor.php ================================================ null, 'extensions' => [], ]; public function __construct(array $configuration = []) { if (! isset($configuration['extensions'])) { $configuration['extensions'] = [ new StarterKit, ]; } $this->configuration = array_merge_recursive($this->configuration, $configuration); $this->schema = new Schema($this->configuration['extensions']); if (isset($configuration['content'])) { $this->setContent($configuration['content']); } } /** * @return static */ public function setContent($value): self { if ($this->getContentType($value) === 'HTML') { $this->document = (new DOMParser($this->schema))->process($value); } elseif ($this->getContentType($value) === 'Array') { $this->document = json_decode(json_encode($value), true); } elseif ($this->getContentType($value) === 'JSON') { $this->document = json_decode($value, true); } $this->document = $this->schema->apply($this->document); return $this; } public function getDocument() { return $this->document; } public function getJSON(): string { return (new JSONSerializer)->process($this->document); } public function getHTML(): string { return (new DOMSerializer($this->schema))->process($this->document); } public function getText($configuration = []): string { return (new TextSerializer($this->schema, $configuration))->process($this->document); } public function sanitize($value) { if ($this->getContentType($value) === 'HTML') { return $this->setContent($value)->getHTML(); } elseif ($this->getContentType($value) === 'Array') { return $this->setContent($value)->getDocument(); } elseif ($this->getContentType($value) === 'JSON') { return $this->setContent($value)->getJSON(); } } public function getContentType($value): string { if (is_string($value)) { try { /** * @psalm-suppress UnusedFunctionCall */ json_decode($value, true, 512, JSON_THROW_ON_ERROR); return 'JSON'; } catch (Exception $exception) { return 'HTML'; } } if (is_array($value)) { return 'Array'; } throw new Exception('Unknown format passed to setContent(). Try passing HTML, JSON or an Array.'); } public function descendants($closure): Editor { // Transform the document to an object $node = json_decode(json_encode($this->document)); $this->walkThroughNodes($node, $closure); // Store the updated document. $this->setContent(json_decode(json_encode($node), true)); return $this; } /** * @return void */ private function walkThroughNodes(&$node, $closure) { // Skip, if it’s just text. if ($node->type === 'text') { return; } // Call the closure. $closure($node); // Skip, if there are no children. if (! isset($node->content)) { return; } // Make sure content is an Array. $content = is_array($node->content) ? $node->content : []; // Loop through all children. foreach ($content as $child) { $this->walkThroughNodes($child, $closure); } } } ================================================ FILE: src/Extensions/Color.php ================================================ ['textStyle'], ]; } public function addGlobalAttributes() { return [ [ 'types' => $this->options['types'], 'attributes' => [ 'color' => [ 'default' => null, 'parseHTML' => function ($DOMNode) { $attribute = InlineStyle::getAttribute($DOMNode, 'color'); if ($attribute === null) { return null; } return preg_replace('/[\'"]+/', '', $attribute); }, 'renderHTML' => function ($attributes) { $color = $attributes?->color ?? null; if ($color === null) { return null; } return ['style' => "color: {$color}"]; }, ], ], ], ]; } } ================================================ FILE: src/Extensions/FontFamily.php ================================================ ['textStyle'], ]; } public function addGlobalAttributes() { return [ [ 'types' => $this->options['types'], 'attributes' => [ 'fontFamily' => [ 'default' => null, 'parseHTML' => function ($DOMNode) { $attribute = InlineStyle::getAttribute($DOMNode, 'font-family'); if ($attribute === null) { return null; } return $attribute; }, 'renderHTML' => function ($attributes) { $fontFamily = $attributes?->fontFamily ?? null; if ($fontFamily === null) { return null; } return ['style' => "font-family: {$fontFamily}"]; }, ], ], ], ]; } } ================================================ FILE: src/Extensions/StarterKit.php ================================================ [], 'blockquote' => [], 'bulletList' => [], 'codeBlock' => [], 'hardBreak' => [], 'heading' => [], 'horizontalRule' => [], 'listItem' => [], 'orderedList' => [], 'paragraph' => [], 'text' => [], 'bold' => [], 'code' => [], 'italic' => [], 'strike' => [], ]; } public function addExtensions() { return array_filter([ $this->options['document'] !== false ? new \Tiptap\Nodes\Document($this->options['document']) : null, $this->options['blockquote'] !== false ? new \Tiptap\Nodes\Blockquote($this->options['blockquote']) : null, $this->options['bulletList'] !== false ? new \Tiptap\Nodes\BulletList($this->options['bulletList']) : null, $this->options['codeBlock'] !== false ? new \Tiptap\Nodes\CodeBlock($this->options['codeBlock']) : null, $this->options['hardBreak'] !== false ? new \Tiptap\Nodes\HardBreak($this->options['hardBreak']) : null, $this->options['heading'] !== false ? new \Tiptap\Nodes\Heading($this->options['heading']) : null, $this->options['horizontalRule'] !== false ? new \Tiptap\Nodes\HorizontalRule($this->options['horizontalRule']) : null, $this->options['listItem'] !== false ? new \Tiptap\Nodes\ListItem($this->options['listItem']) : null, $this->options['orderedList'] !== false ? new \Tiptap\Nodes\OrderedList($this->options['orderedList']) : null, $this->options['paragraph'] !== false ? new \Tiptap\Nodes\Paragraph($this->options['paragraph']) : null, $this->options['text'] !== false ? new \Tiptap\Nodes\Text($this->options['text']) : null, $this->options['bold'] !== false ? new \Tiptap\Marks\Bold($this->options['bold']) : null, $this->options['code'] !== false ? new \Tiptap\Marks\Code($this->options['code']) : null, $this->options['italic'] !== false ? new \Tiptap\Marks\Italic($this->options['italic']) : null, $this->options['strike'] !== false ? new \Tiptap\Marks\Strike($this->options['strike']) : null, ]); } } ================================================ FILE: src/Extensions/TextAlign.php ================================================ [], 'alignments' => ['left', 'center', 'right', 'justify'], 'defaultAlignment' => 'left', ]; } public function addGlobalAttributes() { return [ [ 'types' => $this->options['types'], 'attributes' => [ 'textAlign' => [ 'default' => $this->options['defaultAlignment'], 'parseHTML' => fn ($DOMNode) => InlineStyle::getAttribute($DOMNode, 'text-align') ?? $this->options['defaultAlignment'], 'renderHTML' => function ($attributes) { if ($attributes->textAlign === $this->options['defaultAlignment']) { return null; } return ['style' => "text-align: {$attributes->textAlign}"]; }, ], ], ], ]; } } ================================================ FILE: src/Marks/Bold.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'strong', ], [ 'tag' => 'b', 'getAttrs' => function ($DOMNode) { return ! InlineStyle::hasAttribute($DOMNode, [ 'font-weight' => 'normal', ]) ? null : false; }, ], [ 'style' => 'font-weight', 'getAttrs' => function ($value) { return (bool) preg_match('/^(bold(er)?|[5-9]\d{2,})$/', $value) ? null : false; }, ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return [ 'strong', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0, ]; } } ================================================ FILE: src/Marks/Code.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'code', ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['code', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Marks/Highlight.php ================================================ false, 'HTMLAttributes' => [], ]; } public function parseHTML() { return [ [ 'tag' => 'mark', ], ]; } public function addAttributes() { if (! $this->options['multicolor']) { return []; } return [ 'color' => [ 'parseHTML' => function ($DOMNode) { if ($color = $DOMNode->getAttribute('data-color')) { return $color; } return InlineStyle::getAttribute($DOMNode, 'background-color') ?: null; }, 'renderHTML' => function ($attributes) { if (! $attributes->color) { return null; } return [ 'data-color' => $attributes->color, 'style' => "background-color: {$attributes->color}", ]; }, ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return [ 'mark', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0, ]; } } ================================================ FILE: src/Marks/Italic.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'em', ], [ 'tag' => 'i', 'getAttrs' => function ($DOMNode) { return ! InlineStyle::hasAttribute($DOMNode, [ 'font-style' => 'normal', ]) ? null : false; }, ], [ 'style' => 'font-style=italic', ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['em', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Marks/Link.php ================================================ [ 'target' => '_blank', 'rel' => 'noopener noreferrer nofollow', ], 'allowedProtocols' => [ 'http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp', ], 'isAllowedUri' => fn ($uri) => $this->isAllowedUri($uri), ]; } public function isAllowedUri($uri) { if ($uri === null || $uri === '') { return true; } $sanitised = preg_replace(self::ATTR_WHITESPACE, '', $uri); $pattern = '/^(?:(?:' . implode('|', array_map('preg_quote', $this->options['allowedProtocols'])) . '):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))/i'; return (bool) preg_match($pattern, $sanitised); } public function parseHTML() { return [ [ 'tag' => 'a[href]', 'getAttrs' => function ($DOMNode) { $href = $DOMNode->getAttribute('href'); if ( $href === '' || ! $this->options['isAllowedUri']($href) ) { return false; } return null; }, ], ]; } public function addAttributes() { return [ 'href' => [], 'target' => [], 'rel' => [], 'class' => [], ]; } public function renderHTML($mark, $HTMLAttributes = []) { $isAllowed = $this->options['isAllowedUri']($HTMLAttributes['href'] ?? ''); if (! $isAllowed) { $HTMLAttributes['href'] = ''; } $attributes = HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes); if (isset($mark->attrs)) { foreach ((array) $mark->attrs as $key => $value) { if ($value === null) { unset($attributes[$key]); } } } return [ 'a', $attributes, 0, ]; } } ================================================ FILE: src/Marks/Strike.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 's', ], [ 'tag' => 'del', ], [ 'tag' => 'strike', ], [ 'style' => 'text-decoration=line-through', ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['s', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Marks/Subscript.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'sub', ], [ 'style' => 'vertical-align=sub', ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['sub', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Marks/Superscript.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'sup', ], [ 'style' => 'vertical-align=super', ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['sup', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Marks/TextStyle.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'span', 'getAttrs' => function ($DOMNode) { return $DOMNode->hasAttribute('style') ? null : false; }, ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['span', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Marks/Underline.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'u', ], [ 'style' => 'text-decoration=underline', ], ]; } public function renderHTML($mark, $HTMLAttributes = []) { return ['u', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/Blockquote.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'blockquote', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['blockquote', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/BulletList.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'ul', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['ul', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/CodeBlock.php ================================================ 'language-', 'HTMLAttributes' => [], ]; } public function parseHTML() { return [ [ 'tag' => 'pre', ], ]; } public function addAttributes() { return [ 'language' => [ 'parseHTML' => function ($DOMNode) { if (! ($DOMNode->childNodes[0] instanceof \DOMElement)) { return null; } return preg_replace( "/^" . $this->options['languageClassPrefix']. "/", "", $DOMNode->childNodes[0]->getAttribute('class') ) ?: null; }, 'rendered' => false, ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return [ 'pre', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), [ 'code', [ 'class' => $node->attrs->language ?? null ? $this->options['languageClassPrefix'] . $node->attrs->language : null, ], 0, ], ]; } } ================================================ FILE: src/Nodes/CodeBlockHighlight.php ================================================ 'hljs ', 'HTMLAttributes' => [], ]; } public function renderHTML($node, $HTMLAttributes = []) { $code = $node->content[0]->text ?? ''; try { $highlighter = new Highlighter(); if ($node->attrs->language ?? null) { $result = $highlighter->highlight($node->attrs->language, $code); } else { $result = $highlighter->highlightAuto($code); } $mergedAttributes = HTML::mergeAttributes( [ 'class' => $this->options['languageClassPrefix'] . $result->language, ], $this->options['HTMLAttributes'], $HTMLAttributes, ); $renderedAttributes = HTML::renderAttributes($mergedAttributes); $content = "
";
            $content .= $result->value;
            $content .= "
"; } catch (DomainException $exception) { $mergedAttributes = HTML::mergeAttributes( $this->options['HTMLAttributes'], $HTMLAttributes, ); $renderedAttributes = HTML::renderAttributes($mergedAttributes); $content = "
";
            $content .= htmlentities($code);
            $content .= "
"; } return [ 'content' => $content, ]; } } ================================================ FILE: src/Nodes/CodeBlockShiki.php ================================================ 'language-', 'HTMLAttributes' => [], 'defaultLanguage' => 'html', 'theme' => 'nord', 'guessLanguage' => true, ]; } public function renderHTML($node, $HTMLAttributes = []) { $code = $node->content[0]->text ?? ''; // Language is set if ($node->attrs->language === null) { $language = $node->attrs->language; } // Auto-detect the language elseif ($this->options['guessLanguage']) { try { $highlighter = new Highlighter(); $result = $highlighter->highlightAuto($code); $language = $result->language; } catch (Exception $exception) { // } } // Use the default language if (! isset($language)) { $language = $this->options['defaultLanguage']; } try { $content = Shiki::highlight($code, $language, 'nord'); } catch (DomainException $exception) { $mergedAttributes = HTML::mergeAttributes( $this->options['HTMLAttributes'], $HTMLAttributes, ); $renderedAttributes = HTML::renderAttributes($mergedAttributes); $content = "
";
            $content .= htmlentities($code);
            $content .= "
"; } return [ 'content' => $content, ]; } } ================================================ FILE: src/Nodes/Details.php ================================================ false, 'openClassName' => 'is-open', 'HTMLAttributes' => [], ]; } public function parseHTML() { return [ [ 'tag' => 'details', ], ]; } public function addAttributes() { if (! $this->options['persist']) { return []; } return [ 'open' => [ 'default' => false, 'parseHTML' => fn ($DOMNode) => $DOMNode->hasAttribute('open'), 'renderHTML' => fn ($attributes) => $attributes->open ? ['open' => 'open'] : [], ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return [ 'details', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0, ]; } } ================================================ FILE: src/Nodes/DetailsContent.php ================================================ [], ]; } public function parseHTML(): array { return [ [ 'tag' => 'div[data-type]', 'getAttrs' => fn ($value): bool => (bool) $value == 'detailsContent', ], ]; } public function renderHTML($node, $HTMLAttributes = []): array { return [ 'div', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes, ['data-type' => 'detailsContent']), 0, ]; } } ================================================ FILE: src/Nodes/DetailsSummary.php ================================================ [], ]; } public function parseHTML(): array { return [ [ 'tag' => 'summary', ], ]; } public function renderHTML($node, $HTMLAttributes = []): array { return [ 'summary', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0, ]; } } ================================================ FILE: src/Nodes/Document.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'br', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['br', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes)]; } } ================================================ FILE: src/Nodes/Heading.php ================================================ [1, 2, 3, 4, 5, 6], 'HTMLAttributes' => [], ]; } public function parseHTML() { return array_map(function ($level) { return [ 'tag' => "h{$level}", 'attrs' => [ 'level' => $level, ], ]; }, $this->options['levels']); } public function renderHTML($node, $HTMLAttributes = []) { $hasLevel = in_array($node->attrs->level, $this->options['levels']); $level = $hasLevel ? $node->attrs->level : $this->options['levels'][0]; return [ "h{$level}", HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0, ]; } } ================================================ FILE: src/Nodes/HorizontalRule.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'hr', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['hr', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes)]; } } ================================================ FILE: src/Nodes/Image.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'img[src]', ], ]; } public function addAttributes() { return [ 'src' => [], 'alt' => [], 'title' => [], 'width' => [], 'height' => [], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['img', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/ListItem.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'li', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['li', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/Mention.php ================================================ [], 'renderLabel' => fn () => null, ]; } public function parseHTML() { return [ [ 'tag' => 'span[data-type="' . self::$name . '"]', ], ]; } public function addAttributes() { return [ 'id' => [ 'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-id') ?: null, 'renderHTML' => fn ($attributes) => ['data-id' => $attributes->id ?? null], ], ]; } public function renderText($node) { return $this->options['renderLabel']($node); } public function renderHTML($node, $HTMLAttributes = []) { return [ 'span', HTML::mergeAttributes( ['data-type' => self::$name], $this->options['HTMLAttributes'], $HTMLAttributes, ), 0, ]; } } ================================================ FILE: src/Nodes/OrderedList.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'ol', ], ]; } public function addAttributes() { return [ 'start' => [ 'parseHTML' => fn ($DOMNode) => (int) $DOMNode->getAttribute('start') ?: null, 'renderHTML' => fn ($attributes) => ($attributes->start ?? null) ? ['start' => $attributes->start] : null, ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['ol', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/Paragraph.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'p', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['p', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/Table.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'table', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return [ 'table', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), ['tbody', 0], ]; } } ================================================ FILE: src/Nodes/TableCell.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'td', ], ]; } public function addAttributes() { return [ 'rowspan' => [ 'parseHTML' => fn ($DOMNode) => intval($DOMNode->getAttribute('rowspan')) ?: null, ], 'colspan' => [ 'parseHTML' => fn ($DOMNode) => intval($DOMNode->getAttribute('colspan')) ?: null, ], 'colwidth' => [ 'parseHTML' => function ($DOMNode) { $colwidth = $DOMNode->getAttribute('data-colwidth'); if (! $colwidth) { return null; } $widths = array_map(function ($w) { return intval($w); }, explode(',', $colwidth)); return $widths; }, 'renderHTML' => function ($attributes) { if (! isset($attributes->colwidth)) { return null; } return [ 'data-colwidth' => join(',', $attributes->colwidth), ]; }, ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return [ 'td', HTML::mergeAttributes( $this->options['HTMLAttributes'], $HTMLAttributes, ), 0, ]; } } ================================================ FILE: src/Nodes/TableHeader.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'th', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return [ 'th', HTML::mergeAttributes( $this->options['HTMLAttributes'], $HTMLAttributes, ), 0, ]; } } ================================================ FILE: src/Nodes/TableRow.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'tr', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['tr', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0]; } } ================================================ FILE: src/Nodes/TaskItem.php ================================================ [], ]; } public function addAttributes() { return [ 'checked' => [ 'default' => false, 'renderHTML' => fn ($attributes) => [ 'data-checked' => $attributes->checked ?? null, ], ], ]; } public function parseHTML() { return [ [ 'tag' => 'li[data-type="' . self::$name . '"]', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return [ 'li', HTML::mergeAttributes( $this->options['HTMLAttributes'], $HTMLAttributes, ['data-type' => self::$name], ), [ 'label', [ 'input', [ 'type' => 'checkbox', 'checked' => $node->attrs->checked ?? null ? 'checked' : null, ], ], ['span'], ], [ 'div', 0, ], ]; } } ================================================ FILE: src/Nodes/TaskList.php ================================================ [], ]; } public function parseHTML() { return [ [ 'tag' => 'ul[data-type="' . self::$name . '"]', ], ]; } public function renderHTML($node, $HTMLAttributes = []) { return ['ul', HTML::mergeAttributes( $this->options['HTMLAttributes'], $HTMLAttributes, ['data-type' => self::$name], ), 0]; } } ================================================ FILE: src/Nodes/Text.php ================================================ '#text', ], ]; } } ================================================ FILE: src/Utils/HTML.php ================================================ $value) { // class="foo bar" if ($key === 'class') { $attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . $value); continue; } // style="color: red;" if ($key === 'style') { $style = rtrim($attributes['style'] ?? '', '; ') . '; ' . rtrim($value ?? '', ';') . '; '; $attributes['style'] = ltrim(trim($style), '; '); continue; } $attributes[$key] = $value; } } return $attributes; } /** * Render an associative array of attributes * as a HTML string. */ public static function renderAttributes(array $attrs): string { // Make boolean values a string, so they can be rendered in HTML $attrs = array_map(function ($attribute) { if ($attribute === true) { return 'true'; } if ($attribute === false) { return 'false'; } return $attribute; }, $attrs); $attributes = []; // class="custom" foreach (array_filter($attrs) as $name => $value) { $escapedValue = htmlentities($value); $attributes[] = " {$name}=\"{$escapedValue}\""; } return join($attributes); } } ================================================ FILE: src/Utils/InlineStyle.php ================================================ */ public static function get($DOMNode): array { $results = []; if (! method_exists($DOMNode, 'getAttribute')) { return []; } $style = $DOMNode->getAttribute('style'); preg_match_all( "/([\w-]+)\s*:\s*([^;]+)\s*;?/", $style, $matches, PREG_SET_ORDER ); foreach ($matches as $match) { $results[$match[1]] = $match[2]; } return $results; } public static function hasAttribute($DOMNode, $value): bool { $styles = self::get($DOMNode); if (is_string($value)) { return in_array($value, array_keys($styles)); } if (is_array($value)) { return array_diff($value, $styles) == []; } throw new Exception('Can’t compare inline styles to ' . json_encode($value)); } public static function getAttribute($DOMNode, $attribute): ?string { return self::get($DOMNode)[$attribute] ?? null; } } ================================================ FILE: src/Utils/Minify.php ================================================ _html = str_replace("\r\n", "\n", trim($html)); $hash = isset($_SERVER['REQUEST_TIME']) ? (string) $_SERVER['REQUEST_TIME'] : (string) time(); $this->_replacementHash = 'MINIFYHTML' . md5($hash); // replace PREs with placeholders $this->_html = preg_replace_callback('/\\s*]*?>[\\s\\S]*?<\\/pre>)\\s*/iu', [$this, '_removePreCB'], $this->_html); // trim each line. $this->_html = preg_replace('/^\\s+|\\s+$/mu', '', $this->_html); // remove ws around block/undisplayed elements $this->_html = preg_replace('/\\s+(<\\/?(?:area|article|aside|base(?:font)?|blockquote|body' . '|canvas|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|figcaption|figure|footer|form' . '|frame(?:set)?|h[1-6]|head|header|hgroup|hr|html|legend|li|link|main|map|menu|meta|nav' . '|ol|opt(?:group|ion)|output|p|param|section|t(?:able|body|head|d|h||r|foot|itle)' . '|ul|video)\\b[^>]*>)/iu', '$1', $this->_html); // fill placeholders $this->_html = str_replace( array_keys($this->_placeholders), array_values($this->_placeholders), $this->_html ); return $this->_html; } protected function _removePreCB($m): string { return $this->_reservePlace("_replacementHash . count($this->_placeholders) . '%'; $this->_placeholders[$placeholder] = $content; return $placeholder; } } ================================================ FILE: tests/DOMParser/EmojiTest.php ================================================ 🔥

"; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "🔥", ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/EmptyNodesTest.php ================================================

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [], ], [ 'type' => 'paragraph', 'content' => [], ], ], ]); }); ================================================ FILE: tests/DOMParser/EmptyTextNodesTest.php ================================================
\n"; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'hardBreak', 'marks' => [ [ 'type' => 'italic', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Extensions/ColorTest.php ================================================ red text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextStyle(), new Color(), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'textStyle', 'attrs' => [ 'color' => 'red', ], ], ], 'text' => 'red text', ], ], ], ], ]); }); test('color extension respects the types option', function () { $html = '

red heading

'; $result = (new Editor([ 'extensions' => [ new StarterKit(), new Color([ 'types' => ['heading'], ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 1, 'color' => 'red', ], 'content' => [ [ 'type' => 'text', 'text' => 'red heading', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Extensions/FontFamilyTest.php ================================================ Arial text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextStyle(), new FontFamily(), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'textStyle', 'attrs' => [ 'fontFamily' => 'Arial', ], ], ], 'text' => 'Arial text', ], ], ], ], ]); }); test('multiple font family values are parsed correctly', function () { $html = '

Multiple fonts

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextStyle(), new FontFamily(), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'textStyle', 'attrs' => [ 'fontFamily' => 'Helvetica Neue, Arial, \'Times New Roman\', "Open Sans", sans-serif', ], ], ], 'text' => 'Multiple fonts', ], ], ], ], ]); }); test('font family extension respects the types option', function () { $html = '

Times New Roman heading

'; $result = (new Editor([ 'extensions' => [ new StarterKit(), new FontFamily([ 'types' => ['heading'], ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 1, 'fontFamily' => 'Times New Roman', ], 'content' => [ [ 'type' => 'text', 'text' => 'Times New Roman heading', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Extensions/TextAlignTest.php ================================================ Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextAlign([ 'types' => ['paragraph'], ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'attrs' => [ 'textAlign' => 'center', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('text align uses default value', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextAlign([ 'types' => ['paragraph'], ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'attrs' => [ 'textAlign' => 'left', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('default text align is configureable', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextAlign([ 'types' => ['paragraph'], 'defaultAlignment' => 'center', ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'attrs' => [ 'textAlign' => 'center', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/KeepContentOfUnknownTagsTest.php ================================================ Example Text

"; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "Example Text", ], ], ], ], ]); }); test('keeps content of unknown tags even if it has known tags', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "Example ", ], [ 'type' => 'text', 'text' => "Text", 'marks' => [ [ 'type' => 'bold', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/BoldTest.php ================================================ Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'bold', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('strong gets rendered correctly', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'bold', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('b with font weight normal is ignored', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('span with font weight bold is parsed', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'bold', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('span with font weight 500 is parsed', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'bold', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/CodeTest.php ================================================ Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'code', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/CustomMarkTest.php ================================================ 'span', ], ]; } public function addAttributes() { return [ 'foo' => [ 'parseHTML' => fn ($DOMNode) => $DOMNode->getAttribute('data-foo') ?: null, ], 'fruit' => [], ]; } } test('b and strong get rendered correctly', function () { $html = '

Example text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new CustomMark, ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'custom', 'attrs' => [ 'foo' => 'bar', 'fruit' => 'banana', ], ], ], ], [ 'type' => 'text', 'text' => ' text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/HighlightTest.php ================================================ Example Text

'; $result = (new Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Highlight, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'highlight', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('color is ignored by default', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Highlight, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'highlight', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('color is parsed from data attribute', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Highlight([ 'multicolor' => true, ]), ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'highlight', 'attrs' => [ 'color' => 'red', ], ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('color is parsed from the background color inline style', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Highlight([ 'multicolor' => true, ]), ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'highlight', 'attrs' => [ 'color' => '#ffcc00', ], ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/ItalicTest.php ================================================ Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('em gets rendered correctly', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('i with font style normal is ignored', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('span with font style italic is parsed', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/LinkTest.php ================================================ Example Link
'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', ], ], ], ], ], ]); }); test('link_mark_has_support_for_rel', function () { $html = 'Example Link'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'rel' => 'noopener', ], ], ], ], ], ]); }); test('link_mark_has_support_for_class', function () { $html = 'Example Link'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'class' => 'tiptap', ], ], ], ], ], ]); }); test('link_mark_has_support_for_target', function () { $html = 'Example Link'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'target' => '_blank', ], ], ], ], ], ]); }); function getValidUrls() { return [ 'https://example.com', 'http://example.com', '/same-site/index.html', '../relative.html', 'mailto:info@example.com', 'ftp://info@example.com', ]; } function getInvalidUrls() { // Copied from https://github.com/ueberdosis/tiptap/blob/next/tests/cypress/integration/extensions/link.spec.ts return [ // A standard JavaScript protocol "javascript:alert(window.origin)", // The protocol is not case sensitive "jAvAsCrIpT:alert(window.origin)", // Characters \x01-\x20 are allowed before the protocol "\x00javascript:alert(window.origin)", "\x01javascript:alert(window.origin)", "\x02javascript:alert(window.origin)", "\x03javascript:alert(window.origin)", "\x04javascript:alert(window.origin)", "\x05javascript:alert(window.origin)", "\x06javascript:alert(window.origin)", "\x07javascript:alert(window.origin)", "\x08javascript:alert(window.origin)", "\x09javascript:alert(window.origin)", "\x0ajavascript:alert(window.origin)", "\x0bjavascript:alert(window.origin)", "\x0cjavascript:alert(window.origin)", "\x0djavascript:alert(window.origin)", "\x0ejavascript:alert(window.origin)", "\x0fjavascript:alert(window.origin)", "\x10javascript:alert(window.origin)", "\x11javascript:alert(window.origin)", "\x12javascript:alert(window.origin)", "\x13javascript:alert(window.origin)", "\x14javascript:alert(window.origin)", "\x15javascript:alert(window.origin)", "\x16javascript:alert(window.origin)", "\x17javascript:alert(window.origin)", "\x18javascript:alert(window.origin)", "\x19javascript:alert(window.origin)", "\x1ajavascript:alert(window.origin)", "\x1bjavascript:alert(window.origin)", "\x1cjavascript:alert(window.origin)", "\x1djavascript:alert(window.origin)", "\x1ejavascript:alert(window.origin)", "\x1fjavascript:alert(window.origin)", // Characters \x09,\x0a,\x0d are allowed inside the protocol "java\x09script:alert(window.origin)", "java\x0ascript:alert(window.origin)", "java\x0dscript:alert(window.origin)", // Characters \x09,\x0a,\x0d are allowed after protocol name before the colon "javascript\x09:alert(window.origin)", "javascript\x0a:alert(window.origin)", "javascript\x0d:alert(window.origin)", ]; } function getJsonContent($url) { return [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Click me', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => $url, ], ], ], ], ], ], ], ]; } function getHtmlContent($url) { return '

Click me

'; } test('link_mark_does_output_href_tag_for_valid_JSON_schemas', function () { foreach (getValidUrls() as $url) { $content = getJsonContent($url); $editor = (new Editor([ 'content' => $content, 'extensions' => [ new StarterKit, new Link, ], ])); $result = $editor->getHTML(); expect($result)->toContain($url); } }); test('link_mark_does_not_output_href_tag_for_valid_JSON_schemas', function () { foreach (getInvalidUrls() as $url) { $content = getJsonContent($url); $editor = (new Editor([ 'content' => $content, 'extensions' => [ new StarterKit, new Link, ], ])); $result = $editor->getHTML(); expect($result)->not->toContain($url); } }); test('link_mark_does_output_href_tag_for_valid_HTML_schemas', function () { foreach (getValidUrls() as $url) { $content = getHtmlContent($url); $editor = (new Editor([ 'content' => $content, 'extensions' => [ new StarterKit, new Link, ], ])); $result = $editor->getHTML(); expect($result)->toContain($url); } }); test('link_mark_does_not_output_href_tag_for_valid_HTML_schemas', function () { foreach (getInvalidUrls() as $url) { $content = getHtmlContent($url); $editor = (new Editor([ 'content' => $content, 'extensions' => [ new StarterKit, new Link, ], ])); $result = $editor->getJson(); expect($result)->not->toContain($url); } }); ================================================ FILE: tests/DOMParser/Marks/NestedMarksTest.php ================================================ only bold bold and italic only bold
'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'only bold ', 'marks' => [ [ 'type' => 'bold', ], ], ], [ 'type' => 'text', 'text' => 'bold and italic', 'marks' => [ [ 'type' => 'bold', ], [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => ' only bold', 'marks' => [ [ 'type' => 'bold', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/StrikeTest.php ================================================ Example text using strike and example text using s and example text using del

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example text using strike', 'marks' => [ [ 'type' => 'strike', ], ], ], [ 'type' => 'text', 'text' => ' and ', ], [ 'type' => 'text', 'text' => 'example text using s', 'marks' => [ [ 'type' => 'strike', ], ], ], [ 'type' => 'text', 'text' => ' and ', ], [ 'type' => 'text', 'text' => 'example text using del', 'marks' => [ [ 'type' => 'strike', ], ], ], ], ], ], ]); }); test('inline style is parsed correctly', function () { $html = '

Example Text

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'strike', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/SubscriptTest.php ================================================ Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Subscript, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'subscript', ], ], ], ], ], ], ]); }); test('inline style is parsed correctly', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Subscript, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'subscript', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/SuperscriptTest.php ================================================ Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Superscript, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'superscript', ], ], ], ], ], ], ]); }); test('inline style is parsed correctly', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Superscript, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'superscript', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/TextStyleTest.php ================================================ Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextStyle, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'textStyle', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]); }); test('span without inline style is ignored', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextStyle, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Marks/UnderlineTest.php ================================================ Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Underline, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'underline', ], ], ], ], ], ], ]); }); test('inline style is parsed correctly', function () { $html = '

Example Text

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Underline, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'underline', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/MarksInNodesTest.php ================================================ Example Text.

"; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example ', ], [ 'type' => 'text', 'text' => 'Text', 'marks' => [ [ 'type' => 'bold', ], [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => '.', ], ], ], ], ]); }); test('complex markup gets rendered correctly', function () { $html = '

Headline 1

Some text. Bold Text. Italic Text. Bold and italic Text. Here is a Link.

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => '1', ], 'content' => [ [ 'type' => 'text', 'text' => 'Headline 1', ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Some text. ', ], [ 'type' => 'text', 'text' => 'Bold Text', 'marks' => [ [ 'type' => 'bold', ], ], ], [ 'type' => 'text', 'text' => '. ', ], [ 'type' => 'text', 'text' => 'Italic Text', 'marks' => [ [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => '. ', ], [ 'type' => 'text', 'text' => 'Bold and italic Text', 'marks' => [ [ 'type' => 'bold', ], [ 'type' => 'italic', ], ], ], [ 'type' => 'text', 'text' => '. Here is a ', ], [ 'type' => 'text', 'text' => 'Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', ], ], ], ], [ 'type' => 'text', 'text' => '.', ], ], ], ], ]); }); test('multiple lists gets rendered correctly', function () { $html = '

Headline 2

  1. ordered list item

  2. ordered list item

  3. ordered list item

Some Text.

'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => '2', ], 'content' => [ [ 'type' => 'text', 'text' => 'Headline 2', ], ], ], [ 'type' => 'orderedList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'ordered list item', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'ordered list item', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'ordered list item', ], ], ], ], ], ], ], [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'unordered list item', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'unordered list item with ', ], [ 'type' => 'text', 'text' => 'link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', ], ], [ 'type' => 'bold', ], ], ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'unordered list item', ], ], ], ], ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Some Text.', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/MultipleMarksTest.php ================================================ Example Text

'; $result = (new Editor)->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'bold', ], [ 'type' => 'italic', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/BlockquoteTest.php ================================================

Paragraph

'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'blockquote', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Paragraph', ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/BulletListTest.php ================================================
  • Example

  • Text

  • '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text', ], ], ], ], ], ], ], ], ]); }); test('bulletlistItem with text only gets wrapped in paragraph', function () { $html = ''; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text ', ], [ 'type' => 'text', 'text' => 'Test', 'marks' => [ [ 'type' => 'italic', ], ], ], ], ], ], ], ], ], ], ]); }); test('listItems with space get rendered correctly', function () { $html = ''; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', ], ], ], ], ], ], ]); }); test('listItems content get rendered correctly', function () { $html = ''; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Tiptap', ], ], ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/CodeBlockTest.php ================================================ Example Text'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'codeBlock', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('codeBlock with language gets rendered correctly', function () { $html = '
    body { display: none }
    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'codeBlock', 'attrs' => [ 'language' => 'css', ], 'content' => [ [ 'type' => 'text', 'text' => 'body { display: none }', ], ], ], ], ]); }); test('language class prefix is configureable', function () { $html = '
    body { display: none }
    '; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => [ 'languageClassPrefix' => 'custom-language-prefix-', ], ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'codeBlock', 'attrs' => [ 'language' => 'css', ], 'content' => [ [ 'type' => 'text', 'text' => 'body { display: none }', ], ], ], ], ]); }); test('code block and inline code are rendered correctly', function () { $html = '

    Example Text

    body { display: none }
    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'code', ], ], ], ], ], [ 'type' => 'codeBlock', 'content' => [ [ 'type' => 'text', 'text' => 'body { display: none }', ], ], ], ], ]); }); test('it handles code blocks without a code tag', function () { $html = '
    body { display: none }
    '; $result = (new Editor)->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'codeBlock', 'content' => [ [ 'type' => 'text', 'text' => 'body { display: none }', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/DetailsTest.php ================================================ Summary

    Content

    '; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details, new DetailsSummary, new DetailsContent, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'details', 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content', ], ], ], ], ], ], ], ], ]); }); test('details open attribute is ignored by default', function () { $html = '
    Summary

    Content

    '; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details, new DetailsSummary, new DetailsContent, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'details', 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content', ], ], ], ], ], ], ], ], ]); }); test('details open attribute is parsed when persist is enabled', function () { $html = '
    Summary

    Content

    '; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details(['persist' => true]), new DetailsSummary, new DetailsContent, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'details', 'attrs' => [ 'open' => true, ], 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content', ], ], ], ], ], ], ], ], ]); }); test('details without open attribute sets open to false when persist is enabled', function () { $html = '
    Summary

    Content

    '; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details(['persist' => true]), new DetailsSummary, new DetailsContent, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'details', 'attrs' => [ 'open' => false, ], 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content', ], ], ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/HardBreakTest.php ================================================ Hard
    Break

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Hard ', ], [ 'type' => 'hardBreak', ], [ 'type' => 'text', 'text' => 'Break', ], ], ], ], ]); }); test('multiple nodes get rendered correctly', function () { $html = '

    Example

    Text

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/HeadingTest.php ================================================ Example Text'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 1, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('h2 is rendered correctly', function () { $html = '

    Example Text

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 2, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('h7 is ignored', function () { $html = 'Example Text'; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/HighPriorityParagraph.php ================================================ 'p', 'priority' => 60, ], ]; } } ================================================ FILE: tests/DOMParser/Nodes/HorizontalRuleTest.php ================================================ Horizontal


    Rule

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Horizontal', ], ], ], [ 'type' => 'horizontalRule', ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Rule', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/ImageTest.php ================================================ '; $result = (new Editor([ 'extensions' => [ new StarterKit, new Image, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'image', 'attrs' => [ 'alt' => 'The Finished Dish', 'src' => 'https://example.com/eggs.png', 'title' => 'Eggs in a dish', ], ], ], ]); }); test('image gets rendered correctly when title is missing', function () { $html = 'The Finished Dish'; $result = (new Editor([ 'extensions' => [ new StarterKit, new Image, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'image', 'attrs' => [ 'alt' => 'The Finished Dish', 'src' => 'https://example.com/eggs.png', ], ], ], ]); }); test('image gets rendered correctly when alt is missing', function () { $html = ''; $result = (new Editor([ 'extensions' => [ new StarterKit, new Image, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'image', 'attrs' => [ 'src' => 'https://example.com/eggs.png', 'title' => 'Eggs in a dish', ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/MentionTest.php ================================================ Hey , was geht?

    '; $output = (new Editor([ 'extensions' => [ new StarterKit, new Mention, ], ]))->setContent($html)->getDocument(); expect($output)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Hey ', ], [ 'type' => 'mention', 'attrs' => [ 'id' => 123, ], ], [ 'type' => 'text', 'text' => ', was geht?', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/OrderedListTest.php ================================================
  • Example

  • Text

  • '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'orderedList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text', ], ], ], ], ], ], ], ], ]); }); test('orderedList has correct offset', function () { $html = '
    1. Example

    2. Text

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'orderedList', 'attrs' => [ 'start' => 3, ], 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text', ], ], ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/ParagraphTest.php ================================================ Example Text

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('multiple nodes get rendered correctly', function () { $html = '

    Example

    Text

    '; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text', ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/Nodes/TableTest.php ================================================ ' . '' . '

    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

    ' . '' . ''; $result = (new Editor([ 'extensions' => [ new StarterKit, new Table, new TableRow, new TableCell, new TableHeader, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'table', 'content' => [ [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableHeader', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'text in header cell', ], ], ], ], ], [ 'type' => 'tableHeader', 'attrs' => [ 'colspan' => 2, 'colwidth' => [ 100, 0, ], ], 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'text in header cell with colspan 2', ], ], ], ], ], ], ], [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableCell', 'attrs' => [ 'rowspan' => 2, ], 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'paragraph 1 in cell with rowspan 2', ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'paragraph 2 in cell with rowspan 2', ], ], ], ], ], [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'foo', ], ], ], ], ], [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'bar', ], ], ], ], ], ], ], [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'foo', ], ], ], ], ], [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'bar', ], ], ], ], ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/ParseHTMLPriorityTest.php ================================================ Example

    '; $result = (new Editor([ 'extensions' => [ new StarterKit, new HighPriorityParagraph, ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'highPriorityParagraph', 'content' => [ ['type' => 'text', 'text' => 'Example'], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/SpecialCharacterTest.php ================================================ 🔥

    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "🔥", ], ], ], ], ]); }); test('extended emojis are transformed correctly()', function () { $html = "

    👩‍👩‍👦

    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "👩‍👩‍👦", ], ], ], ], ]); }); test('umlauts are transformed correctly()', function () { $html = "

    äöüÄÖÜß

    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "äöüÄÖÜß", ], ], ], ], ]); }); test('html entities are transformed correctly()', function () { $html = "

    <

    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "<", ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/TaskListTest.php ================================================
  • Example Text

  • '; $result = (new Editor([ 'extensions' => [ new StarterKit(), new TaskList(), new TaskItem(), ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'taskList', 'content' => [ [ 'type' => 'taskItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ], ], ], ], ]); }); test('bullet lists are still parsed correctly', function () { $html = ''; $result = (new Editor([ 'extensions' => [ new StarterKit(), new TaskList(), new TaskItem(), ], ]))->setContent($html)->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ], ], ], ], ]); }); ================================================ FILE: tests/DOMParser/WhitespaceTest.php ================================================ \nExample\n Text

    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => "Example\nText", ], ], ], ], ]); }); test('whitespace in codeBlocks is ignored', function () { $html = "

    \n" . " Example Text\n" . "

    \n" . "
    \n" .
                "Line of Code\n" .
                "    Line of Code 2\n" .
                "Line of Code
    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], [ 'type' => 'codeBlock', 'content' => [ [ 'type' => 'text', 'text' => "Line of Code\n Line of Code 2\nLine of Code", ], ], ], ], ]); }); ================================================ FILE: tests/DOMSerializer/ExampleJsonTest.php ================================================ setContent($document)->getHTML(); expect($result)->toEqual('

    Export HTML or JSON

    You are able to export your data as HTML or JSON. To pass HTML to the editor use the content slot. To pass JSON to the editor use the doc prop.

    '); }); ================================================ FILE: tests/DOMSerializer/Extensions/ColorTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'textStyle', 'attrs' => [ 'color' => 'red', ], ], ], 'text' => 'red text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit(), new TextStyle(), new Color(), ], ])) ->setContent($json) ->getHTML(); expect($result)->toEqual('

    red text

    '); }); ================================================ FILE: tests/DOMSerializer/Extensions/FontFamilyTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'textStyle', 'attrs' => [ 'fontFamily' => 'Helvetica, Arial, sans-serif', ], ], ], 'text' => 'custom font text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit(), new TextStyle(), new FontFamily(), ], ])) ->setContent($json) ->getHTML(); expect($result)->toEqual('

    custom font text

    '); }); ================================================ FILE: tests/DOMSerializer/Extensions/TextAlignTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'attrs' => [ 'textAlign' => 'center', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextAlign([ 'types' => ['paragraph'], ]), ], ])) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); test('default text align isn’t rendered', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'attrs' => [ 'textAlign' => 'left', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextAlign([ 'types' => ['paragraph'], ]), ], ])) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); test('default text align is configureable', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'attrs' => [ 'textAlign' => 'center', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextAlign([ 'types' => ['paragraph'], 'defaultAlignment' => 'center', ]), ], ])) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); ================================================ FILE: tests/DOMSerializer/InputTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Example Text'); }); test('json gets rendered to html', function () { $document = json_encode([ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ]); $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Example Text'); }); test('encoding is correct', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Äffchen', ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Äffchen'); }); test('quotes are not escaped', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => '"Example Text"', ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('"Example Text"'); }); test('escaped attribute values', function () { $result = (new Editor([ 'extensions' => [ new StarterKit, new Image, ], ]))->setContent([ 'type' => 'doc', 'content' => [ [ 'type' => 'image', 'attrs' => [ 'src' => '">'); }); test('reasonable attribute names', function () { $result = (new Editor([ 'extensions' => [ new StarterKit, new Image, ], ]))->setContent([ 'type' => 'doc', 'content' => [ [ 'type' => 'image', 'attrs' => [ 'onerror' => 'alert(1)', 'src' => '', ], ], ], ])->getHTML(); expect($result)->toEqual(''); }); ================================================ FILE: tests/DOMSerializer/Marks/BoldTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'bold', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/Marks/CodeTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'code', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/Marks/HighlightTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'highlight', 'attrs' => [ 'color' => 'red', ], ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Highlight, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); test('mark allows specific colors when configured', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'highlight', 'attrs' => [ 'color' => 'red', ], ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new \Tiptap\Extensions\StarterKit, new \Tiptap\Marks\Highlight([ 'multicolor' => true, ]), ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); ================================================ FILE: tests/DOMSerializer/Marks/ItalicTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'italic', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/Marks/LinkTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); test('link mark has support for rel', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'rel' => 'noopener', ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); test('link mark has support for class', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'rel' => 'noopener', 'class' => 'tiptap', ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); test('link mark has support for target', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'target' => '_self', ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); test('link with marks generates clean output', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://example.com', ], ], ], 'text' => 'Example ', ], [ 'type' => 'text', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://example.com', ], ], [ 'type' => 'bold', ], ], 'text' => 'Link', ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); test('link with marks inside node generates clean output', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://example.com', ], ], ], 'text' => 'Example ', ], [ 'type' => 'text', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://example.com', ], ], [ 'type' => 'bold', ], ], 'text' => 'Link', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('

    Example Link

    '); }); test('link mark can disable rel', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'rel' => null, ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); test('link mark can disable target', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', 'target' => null, ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); ================================================ FILE: tests/DOMSerializer/Marks/StrikeTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'strike', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/Marks/SubscriptTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'subscript', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Subscript, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/Marks/SuperscriptTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'superscript', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Superscript, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/Marks/UnderlineTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'underline', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Underline, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Text'); }); ================================================ FILE: tests/DOMSerializer/MultipleMarksTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'bold', ], [ 'type' => 'italic', ], ], ], ], ], ], ]; $result = (new Editor)->setContent($document)->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); test('multiple marks get rendered correctly, with additional mark at the first node', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'italic', ], [ 'type' => 'bold', ], ], 'text' => 'lorem ', ], [ 'type' => 'text', 'marks' => [ [ 'type' => 'bold', ], ], 'text' => 'ipsum', ], ], ]; $result = (new Editor)->setContent($document)->getHTML(); expect($result)->toEqual('lorem ipsum'); }); test('multiple marks get rendered correctly, with additional mark at the last node', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'text', 'marks' => [ [ 'type' => 'italic', ], ], 'text' => 'lorem ', ], [ 'type' => 'text', 'marks' => [ [ 'type' => 'italic', ], [ 'type' => 'bold', ], ], 'text' => 'ipsum', ], ], ]; $result = (new Editor)->setContent($document)->getHTML(); expect($result)->toEqual('lorem ipsum'); }); test('multiple marks get rendered correctly, when overlapping marks exist', function () { $document = [ "type" => "doc", "content" => [ [ "type" => "paragraph", "content" => [ [ "type" => "text", "marks" => [ [ "type" => "bold", ], ], "text" => "lorem ", ], [ "type" => "text", "marks" => [ [ "type" => "bold", ], [ "type" => "italic", ], ], "text" => "ipsum", ], [ "type" => "text", "marks" => [ [ "type" => "italic", ], ], "text" => " dolor", ], [ "type" => "text", "text" => " sit", ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    lorem ipsum dolor sit

    '); }); test('multiple marks get rendered correctly, when overlapping passage with multiple marks exist', function () { $document = [ "type" => "doc", "content" => [ [ "type" => "paragraph", "content" => [ [ "type" => "text", "marks" => [ [ "type" => "bold", ], [ "type" => "strike", ], ], "text" => "lorem ", ], [ "type" => "text", "marks" => [ [ "type" => "italic", ], [ "type" => "bold", ], [ "type" => "strike", ], ], "text" => "ipsum", ], [ "type" => "text", "marks" => [ [ "type" => "strike", ], [ "type" => "italic", ], ], "text" => " dolor", ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    lorem ipsum dolor

    '); }); test('renders duplicate mark types as nested elements', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', 'marks' => [ [ 'type' => 'bold', ], [ 'type' => 'textStyle', ], [ 'type' => 'textStyle', ], ], ], [ 'type' => 'text', 'text' => ' Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new TextStyle, ], ])) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/BlockquoteTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'blockquote', 'content' => [ [ 'type' => 'text', 'text' => 'Example Quote', ], ], ], ], ]; $result = (new Editor)->setContent($document)->getHTML(); expect($result)->toEqual('
    Example Quote
    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/BulletListTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'text', 'text' => 'first list item', ], ], ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual(''); }); ================================================ FILE: tests/DOMSerializer/Nodes/CodeBlockHighlightTest.php ================================================ body { display: none }'; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockHighlight(), ], ])) ->setContent($html) ->getHTML(); expect($result)->toEqual('
    body { display: none }
    '); }); test('codeBlockHighlight uses the specified language', function () { $html = '
    <?php phpinfo()
    '; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockHighlight(), ], ])) ->setContent($html) ->getHTML(); expect($result)->toEqual('
    <?php phpinfo()
    '); }); test('codeBlockHighlight uses the configured languageClassPrefix', function () { $html = '
    <?php phpinfo()
    '; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockHighlight([ 'languageClassPrefix' => 'foo ', ]), ], ])) ->setContent($html) ->getHTML(); expect($result)->toEqual('
    <?php phpinfo()
    '); }); test('codeBlockHighlight falls back to just a pre and code tag', function () { $html = '
    <?php phpinfo()
    '; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockHighlight(), ], ])) ->setContent($html) ->getHTML(); expect($result)->toEqual('
    <?php phpinfo()
    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/CodeBlockShikiTest.php ================================================ [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockShiki, ], ])); expect($editor->configuration['extensions'][1]) ->toBeInstanceOf(CodeBlockShiki::class); }); test('default theme can be set for codeBlockShiki extension', function () { $editor = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockShiki([ 'theme' => 'mojave', ]), ], ])); expect($editor->configuration['extensions'][1]->options['theme']) ->toEqual('mojave'); }); test('default language can be set for codeBlockShiki extension', function () { $editor = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockShiki([ 'defaultLanguage' => 'css', ]), ], ])); expect($editor->configuration['extensions'][1]->options['defaultLanguage']) ->toEqual('css'); }); test('code block and inline code are rendered correctly', function () { $html = '

    Example Text

    body { display: none }
    '; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockShiki([ 'defaultLanguage' => 'css', ]), ], ])) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', 'marks' => [ [ 'type' => 'code', ], ], ], ], ], [ 'type' => 'codeBlock', 'attrs' => [ 'language' => 'css', ], 'content' => [ [ 'type' => 'text', 'text' => 'body { display: none }', ], ], ], ], ]); }); test('html result is properly rendered', function () { $html = '

    Example Text

    body { display: none }
    '; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => false, ]), new CodeBlockShiki([ 'defaultLanguage' => 'css', ]), ], ])) ->setContent($html) ->getHtml(); // expect($result) // ->toEqual('

    Example Text

    body { display: none }
    '); // Build a regex pattern that allows an optional whitespace (\s?) after the colon in each style attribute. $expectedPattern = '/^

    Example Text<\/code><\/p>' . '

    '
            . ''
            . 'body<\/span>'
            . '\s?<\/span>'
            . '{<\/span>'
            . '\s?<\/span>'
            . 'display<\/span>'
            . ':<\/span>'
            . '\s?<\/span>'
            . 'none<\/span>'
            . '\s?<\/span>'
            . '}<\/span>'
            . '<\/span><\/code><\/pre>$/';
    
        expect($result)->toMatch($expectedPattern);
    });
    
    
    ================================================
    FILE: tests/DOMSerializer/Nodes/CodeBlockTest.php
    ================================================
     'doc',
            'content' => [
                [
                    'type' => 'codeBlock',
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => 'Example Text',
                        ],
                    ],
                ],
            ],
        ];
    
        $result = (new Editor)
            ->setContent($document)
            ->getHTML();
    
        expect($result)->toEqual('
    Example Text
    '); }); test('codeBlock language is rendered correctly', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'codeBlock', 'attrs' => [ 'language' => 'css', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('
    Example Text
    '); }); test('codeBlock language prefix is configureable', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'codeBlock', 'attrs' => [ 'language' => 'css', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit([ 'codeBlock' => [ 'languageClassPrefix' => 'custom-language-prefix-', ], ]), ], ])) ->setContent($document) ->getHTML(); expect($result)->toEqual('
    Example Text
    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/DetailsTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'details', 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary Text', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content Text', ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details, new DetailsSummary, new DetailsContent, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('
    Summary Text

    Content Text

    '); }); test('details node with open true renders open attribute when persist is enabled', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'details', 'attrs' => [ 'open' => true, ], 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary Text', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content Text', ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details(['persist' => true]), new DetailsSummary, new DetailsContent, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('
    Summary Text

    Content Text

    '); }); test('details node with open false does not render open attribute when persist is enabled', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'details', 'attrs' => [ 'open' => false, ], 'content' => [ [ 'type' => 'detailsSummary', 'content' => [ [ 'type' => 'text', 'text' => 'Summary Text', ], ], ], [ 'type' => 'detailsContent', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Content Text', ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Details(['persist' => true]), new DetailsSummary, new DetailsContent, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('
    Summary Text

    Content Text

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/HardBreakNodeTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'some text', ], [ 'type' => 'hardBreak', ], [ 'type' => 'text', 'text' => 'some more text', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    some text
    some more text

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/HeadingTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 2, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; expect((new Editor)->setContent($document)->getHTML())->toEndWith($html); }); test('forbidden heading levels are transformed to a heading with an allowed level', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 7, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; expect((new Editor)->setContent($document)->getHTML())->toEqual($html); }); test('depending on the configuration heading levels are allowed', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 3, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; expect((new Editor([ 'extensions' => [ new \Tiptap\Nodes\Heading(['levels' => [1, 2, 3]]), ], ]))->setContent($document)->getHTML())->toEqual($html); }); test('depending on the configuration heading levels are transformed', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 4, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; expect((new Editor([ 'extensions' => [ new \Tiptap\Nodes\Heading(['levels' => [1, 2, 3]]), ], ]))->setContent($document)->getHTML())->toEqual($html); }); test('configured HTMLAttributes are rendered to HTML', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 1, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; expect((new Editor([ 'extensions' => [ new \Tiptap\Nodes\Heading(['HTMLAttributes' => [ 'class' => 'custom-heading-class', ]]), ], ]))->setContent($document)->getHTML())->toEqual($html); }); test('custom attributes are rendered too', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 1, 'color' => 'red', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; class CustomHeading extends \Tiptap\Nodes\Heading { public function addAttributes() { return [ 'color' => [ 'renderHTML' => function ($attributes) { if (! isset($attributes->color)) { return null; } return [ 'style' => "color: {$attributes->color}", ]; }, ], ]; } } $result = (new Editor([ 'extensions' => [ new CustomHeading([ 'HTMLAttributes' => [ 'class' => 'custom-heading-class', ], ]), ], ]))->setContent($document)->getHTML(); expect($result)->toEqual($html); }); test('inline styles are merged properly', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 1, 'color' => 'red', ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Headline', ], ], ], ], ]; $html = '

    Example Headline

    '; class AnotherCustomHeading extends \Tiptap\Nodes\Heading { public function addAttributes() { return [ 'color' => [ 'renderHTML' => function ($attributes) { if (! isset($attributes->color)) { return null; } return [ 'style' => "background-color: {$attributes->color}", ]; }, ], ]; } } $result = (new Editor([ 'extensions' => [ new AnotherCustomHeading([ 'HTMLAttributes' => [ 'style' => 'color: white; ', ], ]), ], ]))->setContent($document)->getHTML(); expect($result)->toEqual($html); }); ================================================ FILE: tests/DOMSerializer/Nodes/HorizontalRuleNodeTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'some text', ], ], ], [ 'type' => 'horizontalRule', ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'some more text', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    some text


    some more text

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/ImageTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'image', 'attrs' => [ 'alt' => 'an image', 'src' => 'image/source', 'title' => 'The image title', ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Image, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('an image'); }); ================================================ FILE: tests/DOMSerializer/Nodes/MentionTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Hey ', ], [ 'type' => 'mention', 'attrs' => [ 'id' => 123, ], ], [ 'type' => 'text', 'text' => ', was geht?', ], ], ], ], ]; $output = (new Editor([ 'extensions' => [ new StarterKit, new Mention, ], ]))->setContent($document)->getHTML(); expect($output)->toEqual('

    Hey , was geht?

    '); }); test('label can be customized', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Hey ', ], [ 'type' => 'mention', 'attrs' => [ 'id' => 123, ], ], [ 'type' => 'text', 'text' => ', was geht?', ], ], ], ], ]; $output = (new Editor([ 'extensions' => [ new StarterKit, new Mention([ 'renderLabel' => fn ($node) => '@Philipp', ]), ], ]))->setContent($document)->getHTML(); expect($output)->toEqual('

    Hey @Philipp, was geht?

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/OrderedListTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'orderedList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'text', 'text' => 'first list item', ], ], ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('
    1. first list item
    '); }); test('function orderedList has offset', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'orderedList', 'attrs' => [ 'start' => 3, ], 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'text', 'text' => 'first list item', ], ], ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('
    1. first list item
    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/ParagraphTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Paragraph', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getHTML(); expect($result)->toEqual('

    Example Paragraph

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/TableTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'table', 'content' => [ [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableHeader', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'text in header cell', ], ], ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new Table, new TableRow, new TableCell, new TableHeader, new Paragraph, new Text, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('

    text in header cell

    '); }); test('table node gets rendered correctly', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'table', 'content' => [ [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableHeader', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'text in header cell', ], ], ], ], ], [ 'type' => 'tableHeader', 'attrs' => [ 'colspan' => 2, 'colwidth' => [ 100, 0, ], ], 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'text in header cell with colspan 2', ], ], ], ], ], ], ], [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableCell', 'attrs' => [ 'rowspan' => 2, ], 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'paragraph 1 in cell with rowspan 2', ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'paragraph 2 in cell with rowspan 2', ], ], ], ], ], [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'foo', ], ], ], ], ], [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'bar', ], ], ], ], ], ], ], [ 'type' => 'tableRow', 'content' => [ [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'foo', ], ], ], ], ], [ 'type' => 'tableCell', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'bar', ], ], ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new Table, new TableRow, new TableCell, new TableHeader, new Paragraph, new Text, ], ]))->setContent($document)->getHTML(); $html = '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '

    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

    '; expect($result)->toEqual($html); }); ================================================ FILE: tests/DOMSerializer/Nodes/TaskListTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'taskList', 'content' => [ [ 'type' => 'taskItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit(), new TaskList(), new TaskItem(), ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('
    • Example Text

    '); }); test('task item status is rendered correctly', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'taskList', 'content' => [ [ 'type' => 'taskItem', 'attrs' => [ 'checked' => true, ], 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ], ], ], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit(), new TaskList(), new TaskItem(), ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('
    • Example Text

    '); }); ================================================ FILE: tests/DOMSerializer/Nodes/XSSTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'text', 'text' => '', ], ], ]; $result = (new Editor)->setContent($document)->getHTML(); expect($result)->toEqual('<script>alert(1)</script>'); }); ================================================ FILE: tests/DOMSerializer/WrongFormatTest.php ================================================ 'doc', 'content' => 'test', ]; $result = (new Editor([ 'extensions' => [ new StarterKit, ], ]))->setContent($document)->getHTML(); expect($result)->toBeEmpty(); }); test('node content is empty array gets rendered correctly 1', function () { $document = [ 'type' => 'doc', 'content' => [], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, ], ]))->setContent($document)->getHTML(); expect($result)->toBeEmpty(); }); test('node content is empty array gets rendered correctly 2', function () { $document = [ 'type' => 'doc', 'content' => [ [], [], ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, ], ]))->setContent($document)->getHTML(); expect($result)->toBeEmpty(); }); test('node content contains empty array gets rendered correctly 3', function () { $document = [ 'type' => 'doc', 'content' => [ [], 'test', [], '', [], [ 'type' => 'codeBlock', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], [], [], [], '', ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('
    Example Text
    '); }); test('node content contains empty array empty mark gets rendered correctly', function () { $document = [ 'type' => 'doc', 'content' => [ [], 'test', [], '', [], [ 'type' => 'text', 'text' => 'Example Link', 'marks' => [ [], '', 'test', [ 'type' => 'link', 'attrs' => [ 'href' => 'https://tiptap.dev', ], ], ], ], [], [], [], '', ], ]; $result = (new Editor([ 'extensions' => [ new StarterKit, new Link, ], ]))->setContent($document)->getHTML(); expect($result)->toEqual('Example Link'); }); ================================================ FILE: tests/Editor/DescendantsTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'bulletList', 'content' => [ [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], ], ], [ 'type' => 'listItem', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Text ', ], [ 'type' => 'text', 'text' => 'Test', 'marks' => [ [ 'type' => 'italic', ], ], ], ], ], ], ], ], ], [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example', ], ], ], ], ]; $editor = (new Editor)->setContent($document); $result = []; $editor->descendants(function ($node) use (&$result) { $result[] = $node->type; }); expect($result)->toEqual([ 'doc', 'bulletList', 'listItem', 'paragraph', 'listItem', 'paragraph', 'paragraph', ]); }); test('updating node attributes in descendants() works', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'heading', 'attrs' => [ 'level' => 2, ], 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $editor = (new Editor)->setContent($document); // Set the level for all headings to 1 $html = $editor->descendants(function (&$node) { if ($node->type !== 'heading') { return; } $node->attrs->level = 1; })->getHTML(); expect($html)->toEqual('

    Example Text

    '); }); ================================================ FILE: tests/Editor/GetDocumentTest.php ================================================ Example Text

    "; $result = (new Editor) ->setContent($html) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); ================================================ FILE: tests/Editor/GetHTMLTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor) ->setContent($input) ->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); ================================================ FILE: tests/Editor/GetJSONTest.php ================================================ Example

    "; $result = (new Editor) ->setContent($html) ->getJSON(); expect($result)->toEqual('{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Example"}]}]}'); }); ================================================ FILE: tests/Editor/GetTextTest.php ================================================ Heading

    Paragraph

    "; $result = (new Editor) ->setContent($html) ->getText(); expect($result)->toEqual("Heading\n\nParagraph"); }); test('getText() only returns one blockSeparator between blocks', function () { $html = "

    Heading

    Paragraph

    • ListItem

    "; $result = (new Editor) ->setContent($html) ->getText(); expect($result)->toEqual("Heading\n\nParagraph\n\nListItem"); }); test('the blockSeparator is configureable', function () { $html = "

    Heading

    Paragraph

    "; $result = (new Editor) ->setContent($html) ->getText([ 'blockSeparator' => "\n", ]); expect($result)->toEqual("Heading\nParagraph"); }); ================================================ FILE: tests/Editor/SanitizeTest.php ================================================ 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ]); }); test('unknown nodes are removed from the document with the sanitized method', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor)->sanitize($document); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ]); }); test('unknown HTML tags are removed', function () { $document = '

    Example Text

    '; $result = (new Editor)->setContent($document)->getHTML(); expect($result)->toEqual('

    Example Text

    '); }); test('unknown HTML tags are removed with the sanitize method', function () { $document = '

    Example Text

    '; $result = (new Editor)->sanitize($document); expect($result)->toEqual('

    Example Text

    '); }); test('unknown nodes are removed from the JSON', function () { $document = json_encode([ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ]); $result = (new Editor) ->setContent($document) ->getJSON(); expect($result)->toEqual(json_encode([ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ])); }); test('unknown nodes are removed from the json with the sanitized method', function () { $document = json_encode([ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ]); $result = (new Editor)->sanitize($document); expect($result)->toEqual(json_encode([ 'type' => 'doc', 'content' => [ [ 'type' => 'foo', 'content' => [ [ 'type' => 'foo', 'text' => 'Example Text', ], ], ], ], ])); }); ================================================ FILE: tests/Editor/SetContentTest.php ================================================ setContent('{ "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Example Text" } ] } ] }')->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); test('arrays are detected', function () { $document = [ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]; $result = (new Editor) ->setContent($document) ->getDocument(); expect($result)->toEqual($document); }); test('html is detected', function () { $result = (new Editor) ->setContent('

    Example Text

    ') ->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example ', ], [ 'type' => 'text', 'text' => 'Text', 'marks' => [ [ 'type' => 'bold', ], ], ], ], ], ], ]); }); test('content can be passed to the configuration', function () { $result = (new Editor([ 'content' => '

    Example Text

    ', ]))->getDocument(); expect($result)->toEqual([ 'type' => 'doc', 'content' => [ [ 'type' => 'paragraph', 'content' => [ [ 'type' => 'text', 'text' => 'Example Text', ], ], ], ], ]); }); ================================================ FILE: tests/Pest.php ================================================ in('Feature'); /* |-------------------------------------------------------------------------- | Expectations |-------------------------------------------------------------------------- | | When you're writing tests, you often need to check that values meet certain conditions. The | "expect()" function gives you access to a set of "expectations" methods that you can use | to assert different things. Of course, you may extend the Expectation API at any time. | */ expect()->extend('toBeOne', function () { return $this->toBe(1); }); /* |-------------------------------------------------------------------------- | Functions |-------------------------------------------------------------------------- | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your | project that you don't want to repeat in every file. Here you can also expose helpers as | global functions to help you to reduce the number of lines of code in your test files. | */ function something() { // .. } ================================================ FILE: tests/Schema/GetTopNodeTest.php ================================================ topNode::$name)->toEqual('doc'); }); ================================================ FILE: tests/Schema/PriorityTest.php ================================================ defaultNode::$name)->toEqual('paragraph'); }); ================================================ FILE: tests/Utils/HTMLTest.php ================================================ 'a'], ['class' => 'b'], ]; $result = HTML::mergeAttributes(...$attributes); expect($result)->toEqual(['class' => 'a b']); });