🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,35 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->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);
@@ -0,0 +1,28 @@
{
"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": ""
}
]
}
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) überdosis <humans@tiptap.dev>
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.
@@ -0,0 +1,384 @@
# 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
Lets start by converting a HTML snippet to a PHP array with a Tiptap-compatible structure:
```php
(new \Tiptap\Editor)
->setContent('<p>Example Text</p>')
->getDocument();
// Returns:
// ['type' => 'doc', 'content' => …]
```
You can get a JSON string in PHP, too.
```php
(new \Tiptap\Editor)
->setContent('<p>Example Text</p>')
->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:
// <h1>Example Text</h1>
```
This doesnt fully adhere to the ProseMirror schema. Some things are supported too, for example arent marks allowed in a `CodeBlock`.
If you need better schema support, create an issue with the feature youre missing.
### Syntax highlighting for code blocks with [highlight.php](https://github.com/scrivo/highlight.php)
The default `CodeBlock` extension doesnt add syntax highlighting to your code blocks. However, if you want to add syntax highlighting to your code blocks, theres 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('<pre><code>&lt;?php phpinfo()</code></pre>')
->getHTML();
// Returns:
// <pre><code class="hljs php"><span class="hljs-meta">&lt;?php</span> phpinfo()</code></pre>
```
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
<link rel="stylesheet" href="//unpkg.com/@highlightjs/cdn-assets@11.4.0/styles/default.min.css">
```
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('<pre><code>&lt;?php phpinfo()</code></pre>')
->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 isnt passed, it tries to guess the language with highlight.php
]),
],
]))
->setContent('<pre><code>&lt;?php phpinfo()</code></pre>')
->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('<h1>Heading</h1><p>Paragraph</p>')
->getText();
// Returns:
// "Heading
//
// Paragraph"
```
Whats coming between blocks can be configured, too.
```php
(new \Tiptap\Editor)
->setContent('<h1>Heading</h1><p>Paragraph</p>')
->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.
Itll return the same format youre using as the input format.
```php
(new \Tiptap\Editor)
->sanitize('<p>Example Text<script>alert("HACKED!")</script></p>');
// Returns:
// '<p>Example Text</p>'
```
### 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. Theres not much to configure, but at least you can pass the initial content and load specific extensions.
```php
new \Tiptap\Editor([
'content' => '<p>Example Text</p>',
'extensions' => [
new \Tiptap\Extensions\StarterKit,
],
])
```
The `StarterKit` is loaded by default. If you just want to use that, theres 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, thats it. Were 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`, its 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
<?php
class CustomBold extends \Tiptap\Marks\Bold
{
public function renderHTML($mark)
{
// Renders <b> instead of <strong>
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
<?php
use Tiptap\Core\Node;
class CustomNode extends Node
{
public static $name = 'customNode';
public static $priority = 100;
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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.
@@ -0,0 +1,55 @@
{
"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
}
@@ -0,0 +1,971 @@
{
"name": "tiptap-php",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tiptap-php",
"devDependencies": {
"shiki": "^2.0.0"
}
},
"node_modules/@shikijs/core": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.3.2.tgz",
"integrity": "sha512-s7vyL3LzUKm3Qwf36zRWlavX9BQMZTIq9B1almM63M5xBuSldnsTHCmsXzoF/Kyw4k7Xgas7yAyJz9VR/vcP1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/engine-javascript": "2.3.2",
"@shikijs/engine-oniguruma": "2.3.2",
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.4"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.3.2.tgz",
"integrity": "sha512-w3IEMu5HfL/OaJTsMbIfZ1HRPnWVYRANeDtmsdIIEgUOcLjzFJFQwlnkckGjKHekEzNqlMLbgB/twnfZ/EEAGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1",
"oniguruma-to-es": "^3.1.0"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.3.2.tgz",
"integrity": "sha512-vikMY1TroyZXUHIXbMnvY/mjtOxMn+tavcfAeQPgWS9FHcgFSUoEtywF5B5sOLb9NXb8P2vb7odkh3nj15/00A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1"
}
},
"node_modules/@shikijs/langs": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.3.2.tgz",
"integrity": "sha512-UqI6bSxFzhexIJficZLKeB1L2Sc3xoNiAV0yHpfbg5meck93du+EKQtsGbBv66Ki53XZPhnR/kYkOr85elIuFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "2.3.2"
}
},
"node_modules/@shikijs/themes": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.3.2.tgz",
"integrity": "sha512-QAh7D/hhfYKHibkG2tti8vxNt3ekAH5EqkXJeJbTh7FGvTCWEI7BHqNCtMdjFvZ0vav5nvUgdvA7/HI7pfsB4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "2.3.2"
}
},
"node_modules/@shikijs/types": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.3.2.tgz",
"integrity": "sha512-CBaMY+a3pepyC4SETi7+bSzO0f6hxEQJUUuS4uD7zppzjmrN4ZRtBqxaT+wOan26CR9eeJ5iBhc4qvWEwn7Eeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz",
"integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"dev": true,
"license": "ISC"
},
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"dequal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/emoji-regex-xs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
"dev": true,
"license": "MIT"
},
"node_modules/hast-util-to-html": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz",
"integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^6.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@ungap/structured-clone": "^1.0.0",
"devlop": "^1.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"dev": true,
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"dev": true,
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"dev": true,
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"dev": true,
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-types": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz",
"integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==",
"dev": true,
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.0.tgz",
"integrity": "sha512-BJ3Jy22YlgejHSO7Fvmz1kKazlaPmRSUH+4adTDUS/dKQ4wLxI+gALZ8updbaux7/m7fIlpgOZ5fp/Inq5jUAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex-xs": "^1.0.0",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"node_modules/property-information": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
"integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-recursion": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"dev": true,
"license": "MIT"
},
"node_modules/shiki": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-2.3.2.tgz",
"integrity": "sha512-UZhz/gsUz7DHFbQBOJP7eXqvKyYvMGramxQiSDc83M/7OkWm6OdVHAReEc3vMLh6L6TRhgL9dvhXz9XDkCDaaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/core": "2.3.2",
"@shikijs/engine-javascript": "2.3.2",
"@shikijs/engine-oniguruma": "2.3.2",
"@shikijs/langs": "2.3.2",
"@shikijs/themes": "2.3.2",
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4"
}
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"dev": true,
"license": "MIT",
"dependencies": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
"integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
}
},
"dependencies": {
"@shikijs/core": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.3.2.tgz",
"integrity": "sha512-s7vyL3LzUKm3Qwf36zRWlavX9BQMZTIq9B1almM63M5xBuSldnsTHCmsXzoF/Kyw4k7Xgas7yAyJz9VR/vcP1A==",
"dev": true,
"requires": {
"@shikijs/engine-javascript": "2.3.2",
"@shikijs/engine-oniguruma": "2.3.2",
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.4"
}
},
"@shikijs/engine-javascript": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.3.2.tgz",
"integrity": "sha512-w3IEMu5HfL/OaJTsMbIfZ1HRPnWVYRANeDtmsdIIEgUOcLjzFJFQwlnkckGjKHekEzNqlMLbgB/twnfZ/EEAGg==",
"dev": true,
"requires": {
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1",
"oniguruma-to-es": "^3.1.0"
}
},
"@shikijs/engine-oniguruma": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.3.2.tgz",
"integrity": "sha512-vikMY1TroyZXUHIXbMnvY/mjtOxMn+tavcfAeQPgWS9FHcgFSUoEtywF5B5sOLb9NXb8P2vb7odkh3nj15/00A==",
"dev": true,
"requires": {
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1"
}
},
"@shikijs/langs": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.3.2.tgz",
"integrity": "sha512-UqI6bSxFzhexIJficZLKeB1L2Sc3xoNiAV0yHpfbg5meck93du+EKQtsGbBv66Ki53XZPhnR/kYkOr85elIuFw==",
"dev": true,
"requires": {
"@shikijs/types": "2.3.2"
}
},
"@shikijs/themes": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.3.2.tgz",
"integrity": "sha512-QAh7D/hhfYKHibkG2tti8vxNt3ekAH5EqkXJeJbTh7FGvTCWEI7BHqNCtMdjFvZ0vav5nvUgdvA7/HI7pfsB4w==",
"dev": true,
"requires": {
"@shikijs/types": "2.3.2"
}
},
"@shikijs/types": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.3.2.tgz",
"integrity": "sha512-CBaMY+a3pepyC4SETi7+bSzO0f6hxEQJUUuS4uD7zppzjmrN4ZRtBqxaT+wOan26CR9eeJ5iBhc4qvWEwn7Eeg==",
"dev": true,
"requires": {
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4"
}
},
"@shikijs/vscode-textmate": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz",
"integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==",
"dev": true
},
"@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"dev": true,
"requires": {
"@types/unist": "*"
}
},
"@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"dev": true,
"requires": {
"@types/unist": "*"
}
},
"@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"dev": true
},
"@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"dev": true
},
"ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"dev": true
},
"character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"dev": true
},
"character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"dev": true
},
"comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"dev": true
},
"dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true
},
"devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"dev": true,
"requires": {
"dequal": "^2.0.0"
}
},
"emoji-regex-xs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
"dev": true
},
"hast-util-to-html": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz",
"integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==",
"dev": true,
"requires": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^6.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
}
},
"hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"dev": true,
"requires": {
"@types/hast": "^3.0.0"
}
},
"html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"dev": true
},
"mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"dev": true,
"requires": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@ungap/structured-clone": "^1.0.0",
"devlop": "^1.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
}
},
"micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"dev": true,
"requires": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"dev": true
},
"micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"dev": true,
"requires": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"dev": true
},
"micromark-util-types": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz",
"integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==",
"dev": true
},
"oniguruma-to-es": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.0.tgz",
"integrity": "sha512-BJ3Jy22YlgejHSO7Fvmz1kKazlaPmRSUH+4adTDUS/dKQ4wLxI+gALZ8updbaux7/m7fIlpgOZ5fp/Inq5jUAw==",
"dev": true,
"requires": {
"emoji-regex-xs": "^1.0.0",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"property-information": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
"integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
"dev": true
},
"regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"dev": true,
"requires": {
"regex-utilities": "^2.3.0"
}
},
"regex-recursion": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"dev": true,
"requires": {
"regex-utilities": "^2.3.0"
}
},
"regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"dev": true
},
"shiki": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-2.3.2.tgz",
"integrity": "sha512-UZhz/gsUz7DHFbQBOJP7eXqvKyYvMGramxQiSDc83M/7OkWm6OdVHAReEc3vMLh6L6TRhgL9dvhXz9XDkCDaaw==",
"dev": true,
"requires": {
"@shikijs/core": "2.3.2",
"@shikijs/engine-javascript": "2.3.2",
"@shikijs/engine-oniguruma": "2.3.2",
"@shikijs/langs": "2.3.2",
"@shikijs/themes": "2.3.2",
"@shikijs/types": "2.3.2",
"@shikijs/vscode-textmate": "^10.0.1",
"@types/hast": "^3.0.4"
}
},
"space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"dev": true
},
"stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"dev": true,
"requires": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
}
},
"trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"dev": true
},
"unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0"
}
},
"unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0"
}
},
"unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0"
}
},
"unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
}
},
"unist-util-visit-parents": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
}
},
"vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
}
},
"vfile-message": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
"integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
}
},
"zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
"dev": true
}
}
}
@@ -0,0 +1,9 @@
{
"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"
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<psalm
errorLevel="3"
findUnusedVariablesAndParams="true"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
</psalm>
@@ -0,0 +1,373 @@
<?php
namespace Tiptap\Core;
use DOMDocument;
use DOMElement;
use Tiptap\Utils\InlineStyle;
use Tiptap\Utils\Minify;
class DOMParser
{
protected $DOM;
protected $schema;
protected $storedMarks = [];
public function __construct($schema)
{
$this->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 '<?xml encoding="utf-8" ?>' . $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;
}
}
@@ -0,0 +1,401 @@
<?php
namespace Tiptap\Core;
use DOMDocument;
use stdClass;
use Tiptap\Utils\HTML;
class DOMSerializer
{
protected $document;
protected $schema;
public function __construct($schema)
{
$this->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
if (count(array_filter($markTagsToClose, function ($markToClose) use ($markExtension, $mark) {
return $markExtension == $markToClose[0] && $mark == $markToClose[1];
})) == 0) {
$markTagsToReopen[] = $markTag;
} else {
# mark tag does not have to be reopened, but deleted from the 'to close' list
$markTagsToClose = array_udiff($markTagsToClose, [$markTag], function ($a1, $a2) {
return strcmp($a1[1]->type, $a2[1]->type);
});
}
}
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 = '';
}
// <a href="#">
$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[] = "</{$renderInstruction}>";
}
// ['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);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Core;
class Extension
{
public static $name;
public static $priority = 100;
public $options = [];
public function __construct(array $options = [])
{
$this->options = array_merge($this->addOptions(), $options);
}
public function addOptions()
{
return [];
}
public function addGlobalAttributes()
{
return [];
}
public function addExtensions()
{
return [];
}
}
@@ -0,0 +1,15 @@
<?php
namespace Tiptap\Core;
class JSONSerializer
{
protected $document;
public function process(array $value): string
{
$this->document = json_decode(json_encode($value));
return json_encode($this->document);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Tiptap\Core;
class Mark extends Extension
{
public static $priority = 100;
public function addAttributes()
{
return [];
}
public function renderHTML($mark)
{
return null;
}
public function parseHTML()
{
return [];
}
}
@@ -0,0 +1,27 @@
<?php
namespace Tiptap\Core;
class Node extends Extension
{
public static $priority = 100;
public static $topNode = false;
public static $marks = '_';
public function addAttributes()
{
return [];
}
public function parseHTML()
{
return [];
}
public function renderHTML($node)
{
return null;
}
}
@@ -0,0 +1,119 @@
<?php
namespace Tiptap\Core;
class Schema
{
public array $allExtensions = [];
public array $nodes = [];
public array $marks = [];
public array $extensions = [];
public $defaultNode;
public $topNode;
public array $globalAttributes = [];
public function __construct(array $extensions = [])
{
$this->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(),
);
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tiptap\Core;
class TextSerializer
{
protected $document;
protected $schema;
protected $configuration = [
'blockSeparator' => "\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);
}
}
@@ -0,0 +1,150 @@
<?php
namespace Tiptap;
use Exception;
use Tiptap\Core\DOMParser;
use Tiptap\Core\DOMSerializer;
use Tiptap\Core\JSONSerializer;
use Tiptap\Core\Schema;
use Tiptap\Core\TextSerializer;
use Tiptap\Extensions\StarterKit;
class Editor
{
protected $document;
public $schema;
public $configuration = [
'content' => 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 its 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);
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tiptap\Extensions;
use Tiptap\Core\Extension;
use Tiptap\Utils\InlineStyle;
class Color extends Extension
{
public static $name = 'color';
public function addOptions()
{
return [
'types' => ['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}"];
},
],
],
],
];
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tiptap\Extensions;
use Tiptap\Core\Extension;
use Tiptap\Utils\InlineStyle;
class FontFamily extends Extension
{
public static $name = 'fontFamily';
public function addOptions()
{
return [
'types' => ['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}"];
},
],
],
],
];
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tiptap\Extensions;
use Tiptap\Core\Extension;
class StarterKit extends Extension
{
public static $name = 'starterKit';
public function addOptions()
{
return [
'document' => [],
'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,
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Tiptap\Extensions;
use Tiptap\Core\Extension;
use Tiptap\Utils\InlineStyle;
class TextAlign extends Extension
{
public static $name = 'textAlign';
public function addOptions()
{
return [
'types' => [],
'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}"];
},
],
],
],
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
use Tiptap\Utils\InlineStyle;
class Bold extends Mark
{
public static $name = 'bold';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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,
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class Code extends Mark
{
public static $name = 'code';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'code',
],
];
}
public function renderHTML($mark, $HTMLAttributes = [])
{
return ['code', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,67 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
use Tiptap\Utils\InlineStyle;
class Highlight extends Mark
{
public static $name = 'highlight';
public function addOptions()
{
return [
'multicolor' => 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,
];
}
}
@@ -0,0 +1,44 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
use Tiptap\Utils\InlineStyle;
class Italic extends Mark
{
public static $name = 'italic';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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];
}
}
@@ -0,0 +1,89 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class Link extends Mark
{
public static $name = 'link';
// Port of the DOMPurify helper used by Tiptaps Link extension
// https://github.com/ueberdosis/tiptap/blob/next/packages/extension-link/src/link.ts#L161
const ATTR_WHITESPACE = '/[\x00-\x20\x{00A0}\x{1680}\x{180E}\x{2000}-\x{2029}\x{205F}\x{3000}]/u';
public function addOptions()
{
return [
'HTMLAttributes' => [
'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'] = '';
}
return [
'a',
HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes),
0,
];
}
}
@@ -0,0 +1,41 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class Strike extends Mark
{
public static $name = 'strike';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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];
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class Subscript extends Mark
{
public static $name = 'subscript';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'sub',
],
[
'style' => 'vertical-align=sub',
],
];
}
public function renderHTML($mark, $HTMLAttributes = [])
{
return ['sub', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class Superscript extends Mark
{
public static $name = 'superscript';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'sup',
],
[
'style' => 'vertical-align=super',
],
];
}
public function renderHTML($mark, $HTMLAttributes = [])
{
return ['sup', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class TextStyle extends Mark
{
public static $name = 'textStyle';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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];
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tiptap\Marks;
use Tiptap\Core\Mark;
use Tiptap\Utils\HTML;
class Underline extends Mark
{
public static $name = 'underline';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'u',
],
[
'style' => 'text-decoration=underline',
],
];
}
public function renderHTML($mark, $HTMLAttributes = [])
{
return ['u', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class Blockquote extends Node
{
public static $name = 'blockquote';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'blockquote',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['blockquote', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class BulletList extends Node
{
public static $name = 'bulletList';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'ul',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['ul', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,67 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class CodeBlock extends Node
{
public static $name = 'codeBlock';
public static $marks = '';
public function addOptions()
{
return [
'languageClassPrefix' => '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,
],
];
}
}
@@ -0,0 +1,62 @@
<?php
namespace Tiptap\Nodes;
use DomainException;
use Highlight\Highlighter;
use Tiptap\Utils\HTML;
class CodeBlockHighlight extends CodeBlock
{
public function addOptions()
{
return [
'languageClassPrefix' => '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 = "<pre><code" . $renderedAttributes . ">";
$content .= $result->value;
$content .= "</code></pre>";
} catch (DomainException $exception) {
$mergedAttributes = HTML::mergeAttributes(
$this->options['HTMLAttributes'],
$HTMLAttributes,
);
$renderedAttributes = HTML::renderAttributes($mergedAttributes);
$content = "<pre><code" . $renderedAttributes . ">";
$content .= htmlentities($code);
$content .= "</code></pre>";
}
return [
'content' => $content,
];
}
}
@@ -0,0 +1,67 @@
<?php
namespace Tiptap\Nodes;
use DomainException;
use Exception;
use Highlight\Highlighter;
use Spatie\ShikiPhp\Shiki;
use Tiptap\Utils\HTML;
class CodeBlockShiki extends CodeBlock
{
public function addOptions()
{
return [
'languageClassPrefix' => '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 = "<pre><code" . $renderedAttributes . ">";
$content .= htmlentities($code);
$content .= "</code></pre>";
}
return [
'content' => $content,
];
}
}
@@ -0,0 +1,12 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
class Document extends Node
{
public static $name = 'doc';
public static $topNode = true;
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class HardBreak extends Node
{
public static $name = 'hardBreak';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'br',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['br', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes)];
}
}
@@ -0,0 +1,46 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class Heading extends Node
{
public static $name = 'heading';
public function addOptions()
{
return [
'levels' => [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,
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class HorizontalRule extends Node
{
public static $name = 'horizontalRule';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'hr',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['hr', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes)];
}
}
@@ -0,0 +1,41 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class Image extends Node
{
public static $name = 'image';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'img[src]',
],
];
}
public function addAttributes()
{
return [
'src' => [],
'alt' => [],
'title' => [],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['img', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class ListItem extends Node
{
public static $name = 'listItem';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'li',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['li', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,56 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class Mention extends Node
{
public static $name = 'mention';
public function addOptions()
{
return [
'HTMLAttributes' => [],
'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,
];
}
}
@@ -0,0 +1,42 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class OrderedList extends Node
{
public static $name = 'orderedList';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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];
}
}
@@ -0,0 +1,34 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class Paragraph extends Node
{
public static $name = 'paragraph';
public static $priority = 1000;
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'p',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['p', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,36 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class Table extends Node
{
public static $name = 'table';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'table',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return [
'table',
HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes),
['tbody', 0],
];
}
}
@@ -0,0 +1,75 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class TableCell extends Node
{
public static $name = 'tableCell';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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,
];
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Utils\HTML;
class TableHeader extends TableCell
{
public static $name = 'tableHeader';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'th',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return [
'th',
HTML::mergeAttributes(
$this->options['HTMLAttributes'],
$HTMLAttributes,
),
0,
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class TableRow extends Node
{
public static $name = 'tableRow';
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
public function parseHTML()
{
return [
[
'tag' => 'tr',
],
];
}
public function renderHTML($node, $HTMLAttributes = [])
{
return ['tr', HTML::mergeAttributes($this->options['HTMLAttributes'], $HTMLAttributes), 0];
}
}
@@ -0,0 +1,70 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class TaskItem extends Node
{
public static $name = 'taskItem';
public static $priority = 1000;
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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,
],
];
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
use Tiptap\Utils\HTML;
class TaskList extends Node
{
public static $name = 'taskList';
public static $priority = 1000;
public function addOptions()
{
return [
'HTMLAttributes' => [],
];
}
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];
}
}
@@ -0,0 +1,19 @@
<?php
namespace Tiptap\Nodes;
use Tiptap\Core\Node;
class Text extends Node
{
public static $name = 'text';
public function parseHTML()
{
return [
[
'tag' => '#text',
],
];
}
}
@@ -0,0 +1,71 @@
<?php
namespace Tiptap\Utils;
class HTML
{
/**
* Merge an associative array of attributes,
* and make sure to merge classes and inline styles.
*/
public static function mergeAttributes()
{
$args = func_get_args();
$attributes = array_shift($args);
foreach ($args as $moreAttributes) {
foreach ($moreAttributes as $key => $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);
}
}
@@ -0,0 +1,57 @@
<?php
namespace Tiptap\Utils;
use Exception;
class InlineStyle
{
/**
* @return string[]
*
* @psalm-return array<string, string>
*/
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('Cant compare inline styles to ' . json_encode($value));
}
public static function getAttribute($DOMNode, $attribute): ?string
{
return self::get($DOMNode)[$attribute] ?? null;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Tiptap\Utils;
class Minify
{
protected $_replacementHash;
protected $_placeholders = [];
protected $_html;
public function process($html): string
{
$this->_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*<pre(\\b[^>]*?>[\\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("<pre{$m[1]}");
}
protected function _reservePlace($content): string
{
$placeholder = '%' . $this->_replacementHash . count($this->_placeholders) . '%';
$this->_placeholders[$placeholder] = $content;
return $placeholder;
}
}