Version 5.0.6

This commit is contained in:
Neo committed 2025-12-19 18:55:49 -08:00
1 parent a3a4a9ccbd
commit b89858b2ff
1214 files changed
+92828 -14859

No files matched your search

@@ -0,0 +1,153 @@
# Changelog
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 3.4.2 (5. April 2024)
+ [#87](https://github.com/nadar/quill-delta-parser/issues/87) Fixed a bug where a line break preceding a list containing inline attributes results in improper HTML formatting for next paragraphs.
## 3.4.1 (13. March 2024)
+ [#84](https://github.com/nadar/quill-delta-parser/issues/84) Allow align `left` as possible value.
## 3.4.0 (14. September 2023)
> Please be aware that this release may impact the way Quill data is displayed in your frontend. Checkout the [upgrade document](UPGRADE.md) for more details.
+ [#80](https://github.com/nadar/quill-delta-parser/issues/80) Add missing background color listener (In this update, we have introduced a change where background color information from Quill JSON is now extracted and applied as the background color. This change may result in unexpected behavior if you had background color information present in your documents but it was not rendered correctly in previous versions. see [upgrade document](UPGRADE.md)
## 3.3.1 (24. March 2023)
+ [#78](https://github.com/nadar/quill-delta-parser/issues/78) Fixed a bug where lists with empty contents would break all output.
## 3.3.0 (10. March 2023)
+ [#77](https://github.com/nadar/quill-delta-parser/pull/77) Allow method chaining for `registerListener()` and `overwriteListener()`.
+ [#76](https://github.com/nadar/quill-delta-parser/pull/76) Addded php 8.2 for unit tests
## 3.2.1 (1. November 2022)
+ [#74](https://github.com/nadar/quill-delta-parser/issues/74) Fixed missing support for nested lists
+ [#73](https://github.com/nadar/quill-delta-parser/pull/73) More automation and testing with rector and auto commit on PR's for csfixer and rector.
## 3.2.0 (7. August 2022)
+ [#71](https://github.com/nadar/quill-delta-parser/pull/71) Added image width and height attributes to the image tag if available.
+ [#72](https://github.com/nadar/quill-delta-parser/pull/72) Raised from phpstan level 5 to 6
## 3.1.0 (24. July 2022)
+ [#68](https://github.com/nadar/quill-delta-parser/pull/68) Deprecated the magical getter `$pick->$name` in `nadar\quill\Pick` class, use `$pick->optionValue($name)` instead.
+ [#64](https://github.com/nadar/quill-delta-parser/pull/64) Replaced deprecated public $input property in `Line` with getter and setter methods `getLine()` and `setLine($input)`.
+ [#63](https://github.com/nadar/quill-delta-parser/pull/63/) Removed deprecated methods `loadBuiltinListeneres()` and `renderListeneres()`.
+ [#62](https://github.com/nadar/quill-delta-parser/pull/62/) Prettify the html output a little by adding newlines after every block level element. If you have custom listeners which call `BlockListener->wrapElement()` this will be added for your custom listeners as well, otherwise you'll need to add the newlines yourself.
## 3.0.0 (2. June 2022)
> This release contains breaks which might affect your application. Checkout the [upgrade document](UPGRADE.md) for more details.
+ [#60](https://github.com/nadar/quill-delta-parser/pull/60) Changed the behavior of `nadar\quill\listener\Link`. Property `$wrapper` has been removed and replaced by `$wrapperOpen`, `$wrapperMiddle` and `$wrapperClose`.
+ [#61](https://github.com/nadar/quill-delta-parser/pull/61) Official dropped PHP 7.1 support added PHP 8.1 instead.
## 2.9.0 (5. April 2022)
+ [#58](https://github.com/nadar/quill-delta-parser/pull/58/files) Renamed misspelled method `loadBuiltinListeneres()` to `loadBuiltinListeners()` and `renderListeneres()` to `renderListeners()`. The old methods are still available for backwards compatibility, but deprecated and will be removed in 3.0.
## 2.8.0 (8. March 2022)
+ [#56](https://github.com/nadar/quill-delta-parser/pull/56) Provide new method to override existing listeners with `overwriteListener()`.
## 2.7.2 (27. October 2021)
+ [#53](https://github.com/nadar/quill-delta-parser/pull/53) Lists listener, check for type being an array for compatibility with [Vanilla Forums](https://vanillaforums.com/).
## 2.7.1 (5. October 2021)
+ [#51](https://github.com/nadar/quill-delta-parser/issues/51) Fixed issue where images wrapped in italics renders as text instead of showing the image.
## 2.7.0 (7. June 2021)
+ [#49](https://github.com/nadar/quill-delta-parser/issues/49) Added Code Block Listener, generates `<pre><code>...</code></pre>` enclosed output.
## 2.6.1 (1. June 2021)
+ [#48](https://github.com/nadar/quill-delta-parser/pull/48) PHP 8 compatibility.
## 2.6.0 (2. December 2020)
+ [#42](https://github.com/nadar/quill-delta-parser/pull/42) Added PHP 8 Support.
+ [#43](https://github.com/nadar/quill-delta-parser/pull/42) Moved CI from Travis to GitHub Actions. Added PHP 8 version in Test Scenario.
## 2.5.0 (29. June 2020)
+ [#41](https://github.com/nadar/quill-delta-parser/pull/41) Add option to configure Embed Video `allow` option.
## 2.4.0 (2. November 2019)
+ [#32](https://github.com/nadar/quill-delta-parser/pull/32) Added new `wrapElement` method to simplify building block listeners.
+ [#30](https://github.com/nadar/quill-delta-parser/issues/30) List opening tag process has been simplified in order to support single bullet lists.
+ [#31](https://github.com/nadar/quill-delta-parser/pull/31) Add support for align attribute.
## 2.3.0 (23. October 2019)
+ Add override functionality to links, allowing to customise their wrapper.
## 2.2.0 (17. October 2019)
+ Add inline element to handle script attribute.
## 2.1.1 (2. September 2019)
+ [#25](https://github.com/nadar/quill-delta-parser/issues/25) Ensure that empty heading listeners won't destroy all upcoming elements.
## 2.1.0 (24. August 2019)
+ [#23](https://github.com/nadar/quill-delta-parser/issues/23) Add inline element to handle font attribute.
## 2.0.0 (10. July 2019)
> This release contains breaks which might affect your application. Checkout the [upgrade document](UPGRADE.md) for more details.
+ [#18](https://github.com/nadar/quill-delta-parser/issues/18) Enable `escapeInput` option by default in order to increase security.
## 1.3.2 (9. July 2019)
+ [#13](https://github.com/nadar/quill-delta-parser/pull/13) Fixed bug when lists are interrupted with block level elements (e.g. videos)
+ [#16](https://github.com/nadar/quill-delta-parser/issues/16) Fixed bug with sorting index of inline elements when using prepend.
+ [#15](https://github.com/nadar/quill-delta-parser/pull/15) Fixed bug when header contains (partial) formatting and line before header contains formatting
## 1.3.1 (6. June 2019)
+ [#12](https://github.com/nadar/quill-delta-parser/issues/12) Fixed bug when using quill parser on windows platforms.
## 1.3.0 (28. May 2019)
+ Added new debugInfo option for lines
+ Added new behavior for hasNewline (which is now more consistent according to delta input)
+ [#8](https://github.com/nadar/quill-delta-parser/issues/8) Fixed issue with UL/OL list elements if only one element is inside the list.
## 1.2.0 (30. April 2019)
+ [#7](https://github.com/nadar/quill-delta-parser/pull/7) Makes sure input and attributes from delta is escaped before mixing it with html. Listeners should use `$line->getInput()` instead of `$line->input` to read input. This will properly escape if it is not done already. Values from attributes should be passed through `$line->getLexer()->escape()`. See the [Color listener](src/listener/Color.php) for an example of both. Obviously, escaping should be skipped in case a listener is meant to output raw html.
## 1.1.1 (17. April 2019)
+ [#6](https://github.com/nadar/quill-delta-parser/pull/6) Fixed bug in exception messaged. Added unit tests and improved message.
## 1.1.0 (8. March 2019)
+ [#5](https://github.com/nadar/quill-delta-parser/issues/5) Fixed a bug where paragraphs with attributes where not rendered (color attribute). Added new parameter to disable the rendering of attributes.
## 1.0.2 (7. February 2019)
+ Added Image-Tag for image output.
## 1.0.1 (25. November 2018)
+ Improve Video-Tag output (Remove frameborder, use youtube embed code allow tag).
## 1.0.0 (21. October 2018)
+ First stable API release.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Basil
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,170 @@
# Quill Delta to HTML Parser
A PHP library to parse [Quill WYSIWYG](https://quilljs.com/) editor [deltas](https://github.com/quilljs/delta) into HTML - flexible and extendable for custom elements. Every element is parsed by the same mechanism, making it easy to extend and understand. It also sanitizes the output value, making it more secure, especially when using user-generated text.
![Tests](https://github.com/nadar/quill-delta-parser/workflows/Tests/badge.svg)
[![Maintainability](https://api.codeclimate.com/v1/badges/fdf80e7b61e4505bc421/maintainability)](https://codeclimate.com/github/nadar/quill-delta-parser/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/fdf80e7b61e4505bc421/test_coverage)](https://codeclimate.com/github/nadar/quill-delta-parser/test_coverage)
[![Latest Stable Version](https://poser.pugx.org/nadar/quill-delta-parser/v/stable)](https://packagist.org/packages/nadar/quill-delta-parser)
[![Total Downloads](https://poser.pugx.org/nadar/quill-delta-parser/downloads)](https://packagist.org/packages/nadar/quill-delta-parser)
[![License](https://poser.pugx.org/nadar/quill-delta-parser/license)](https://packagist.org/packages/nadar/quill-delta-parser)
What is Quill? Quill is a free, open source WYSIWYG editor built for the modern web. With its modular architecture and expressive API, it is completely customizable to fit any need.
## Installation
The package is available only through Composer:
```sh
composer require nadar/quill-delta-parser
```
## Usage
```php
// Ensure to load the autoload file from Composer somewhere in your application.
require __DIR__ . '/vendor/autoload.php';
// Create the lexer object with your given Quill JSON delta code (either PHP array or JSON string).
$lexer = new \nadar\quill\Lexer($json);
// Echo the HTML for the given JSON ops.
echo $lexer->render();
```
Where `$json` is the ops JSON array from Quill, for example:
```json
{
"ops": [
{
"insert": "Hello"
},
{
"attributes": {
"header": 1
},
"insert": "\n"
},
{
"insert": "\nThis is the PHP Quill "
},
{
"attributes": {
"bold": true
},
"insert": "parser"
},
{
"insert": "!\n"
}
]
}
```
This would render the following HTML:
```html
<h1>Hello</h1>
<p><br></p>
<p>This is the PHP Quill <strong>parser</strong>!</p>
```
## Extend the Parser
To extend the Parser by adding your own listeners (this can be the case if you are using Quill plugins which generate custom delta code), you have to decide whether it's an:
+ Inline element: Replaces content with new parsed content, mostly the case when working with Quill extensions.
+ Block element: Encloses the whole input with a tag, for example, a heading.
An example for a mention plugin that generates the following delta `{"insert":{"mention":{"id":"1","value":"Basil","denotationChar":"@"}}}`; an inline plugin could look like this:
```php
class Mention extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
// Check if input is JSON, decodes to an array, and checks if the key "mention"
// exists. If yes, return the value for this key.
$mention = $line->insertJsonKey('mention');
if ($mention) {
// Apply the inline behavior, updates the content and append to the next "block" element.
// The value in this example would be "<strong>Basil</strong>".
$this->updateInput($line, '<strong>'.$mention['value'].'</strong>');
}
}
}
```
Now register the listener:
```php
$lexer = new Lexer($json);
$lexer->registerListener(new Mention());
echo $lexer->render();
```
## Override Built-in Listeners
Certain listeners (image, video, color) produce an HTML output which may not suit your use case, so you have the option to override the properties of those plugins and re-register the Listener. Here's an example with the image tag:
```php
$image = new Image();
$image->wrapper = '<img src="{src}" class="my-image" />';
// Override the default listener behavior for image color:
$lexer = new Lexer($json);
$lexer->registerListener($image);
echo $lexer->render();
```
If you want to replace a class with your own image class, use `overwriteListener` to achieve the same result, but with your totally custom class. The reason is that listeners are registered by their class names.
```php
$mySuperClass = new class() extends Image {
// Here is the custom class code ...
};
$lexer->overwriteListener(new Image(), $mySuperClass);
```
Or, of course, when you have a separate file for your class:
```php
class MySuperDuperImageClass extends Image
{
// Here is the custom class code ...
}
$lexer->overwriteListener(new Image(), new MySuperDuperImageClass());
```
## Debugging
Sometimes, understanding how delta is handled and parsed can be challenging to debug. Therefore, you can use the debugger class, which will print a table with information about how the data is parsed.
```php
$lexer = new Lexer($json);
$lexer->render(); // Make sure to run the render before calling debugPrint().
$debug = new Debug($lexer);
echo $debug->debugPrint();
```
There is also a built-in Docker Compose file which provides access to the `output.php` file. The `output.php` helps to directly write content with the Quill editor while displaying what is rendered, including all debug information. To run this Docker webserver, execute the following command in the root directory of your Git repository clone:
```sh
docker-compose up
```
and visit `http://localhost:5555/` in your browser.
#### Credits
+ [Lode Claassen](https://github.com/lode)
+ [Dean Blackborough](https://github.com/deanblackborough)
@@ -0,0 +1,26 @@
# Quill Delta Parser Upgrade
This document will help you upgrading from a version into another. For more detailed informations about the breaking changes **click the issue detail link**, there you can see examples of how to change your code.
## from 3.3.x to 3.4
+ [#80](https://github.com/nadar/quill-delta-parser/issues/80) In this update, we have introduced a change where background color information from Quill JSON is now extracted and applied as the background color. This change may result in unexpected behavior if you had background color information present in your documents but it was not rendered correctly in previous versions. To restore the previous behavior and resolve any unexpected issues related to background color rendering, please follow these steps:
```php
$backgroundColor = new BackgroundColor();
$backgroundColor->ignore = true;
// override the default listener behavior for background color:
$lexer = new Lexer($json);
$lexer->registerListener($backgroundColor);
echo $lexer->render();
```
## from 2.x to 3.0
+ [#61](https://github.com/nadar/quill-delta-parser/pull/61) PHP 7.1 is official dropped in unit tests. The code should still work with PHP 7.1 until next major release.
+ [#60](https://github.com/nadar/quill-delta-parser/pull/60) Changed the behavior of `nadar\quill\listener\Link`. Property `$wrapper` has been removed and replaced by `$wrapperOpen`, `$wrapperMiddle` and `$wrapperClose`. If you are not overriding or have a customized setup the `Link` listenere, upgrading is safe.
## from 1.x to 2.0
+ [#18](https://github.com/nadar/quill-delta-parser/issues/18) The input values from are now safely encoded by default. This might be a problem when using script or other raw html code inside your text(quill delta). To achieve the same output as behavior as before set `$lexer->escapeInput = false`. This will turn off input encoding but is not recommend.
@@ -0,0 +1,52 @@
{
"name": "nadar/quill-delta-parser",
"description": "A PHP library to parse Quill WYSIWYG editor deltas into HTML - flexible and extendible for custom elements.",
"keywords": [
"quill",
"delta",
"html",
"parser",
"renderer",
"quill-editor",
"quill-delta",
"quilljs"
],
"type": "library",
"require-dev": {
"phpunit/phpunit": "^8.0",
"friendsofphp/php-cs-fixer": "^3.2",
"phpstan/phpstan": "^1.7",
"rector/rector": "^0.14.2"
},
"license": "MIT",
"authors": [
{
"name": "Basil Suter",
"email": "git@nadar.io"
}
],
"autoload": {
"psr-4": {
"nadar\\quill\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"nadar\\quill\\tests\\": "tests/"
}
},
"scripts": {
"phpstan": "vendor/bin/phpstan -v",
"phpcsfixer": "vendor/bin/php-cs-fixer fix",
"rector": "vendor/bin/rector"
},
"config": {
"platform": {
"php": "7.4"
}
},
"require": {
"php": ">=7.2",
"ext-json": "*"
}
}
@@ -0,0 +1,19 @@
<?php
$json = <<<'JSON'
[{"insert":"It’s that time of year again, and we have some great new offers and new product reveals for "},{"attributes":{"bold":true},"insert":"Black Friday"},{"insert":"! \n\nThe Black Friday Deals will be available from November 29th at 09:00 CET until Cyber Monday (December 2nd), exclusively through our webshop.\n\n"},{"insert":{"embed-external":{"data":{"embedType":"image","url":"https:\/\/lh3.googleusercontent.com\/8IXf1zlGK8FZ4ttBuLsf7f79WTf4Uuq09o-quE5nmmXQk-PM9gXqoqEZUxRcEpeJjGmydwZ7Ew_hamwZjX5mPVru47lEJlWdDzEIbalt60wC8Dpjyfx-p2ZJb7yg9rbf0LEOMcZJ","name":"","attributes":[],"format":null,"bodyRaw":null},"loaderData":{"type":"image"}}}},{"insert":"\n"},{"attributes":{"bold":true},"insert":"CSL Elite Pro Kit - Officially Licensed for PS4"},{"insert":"™"},{"attributes":{"bold":true},"insert":" at 699.95 € (Europe only)"},{"insert":"\nThis offer is only valid in EU and is strictly limited!"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Race-ready Fanatec package for PS4™ and PS4™ Pro combines the CSL Elite Racing Wheel for PS4 and the CSL Elite Pedals LC"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Wheel base provides realistic force feedback thanks to Fanatec ClubSport technology"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Fully metal, adjustable CSL Elite pedals"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"CSL Elite Loadcell Kit included! Load cell technology greatly improves brake feeling and consistency."},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"\n\n\n"},{"insert":{"embed-external":{"data":{"url":"https:\/\/forum.fanatec.com\/uploads\/394\/R28CIXS9QPRK.jpg","name":"blackfriday_V3_4.jpg","type":"image\/jpeg","size":184063,"width":1200,"height":628,"mediaID":1904,"dateInserted":"2019-11-25T16:33:45+00:00","insertUserID":30220,"foreignType":"embed","foreignID":30220,"embedType":"image","format":null,"bodyRaw":null},"loaderData":{"type":"image"}}}},{"insert":"\n"},{"attributes":{"bold":true},"insert":"ClubSport Pedals V3 + Brake Performance Kit at 299,95 €\/$, 37,900.00¥, AU $499.90"},{"insert":"\nBuy the legendary ClubSport Pedals V3 with the Brake Performance Kit for less than the normal price of the pedals alone!\nHigh performance pedals with Hall sensors on throttle and clutch pedals for precision and durability"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Pressure sensitive load cell brake system with custom-made 90 kg sensor and tool-free stiffness and travel adjustment"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"12-bit resolution (4096 values) on all axis"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Alternative D-shape pedal plates included, with adjustable extensions"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Brake sensitivity can be adjusted through the Tuning Menu while driving"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Vibration motors on throttle and brake pedals to simulate TC, ABS, and other effects"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Exchangeable throttle springs. Two springs with different strengths are included."},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Optional damper kit for gas and brake with adjustable damping resistance."},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Full multi-platform compatibility"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Full metal construction of CNC-machined aluminium"},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Brake Performance Kit stiffens and shortens throw to behave more like a race car pedal, greatly improving on-the-limit threshold brake feel."},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"\n\n\n"},{"insert":{"embed-external":{"data":{"url":"https:\/\/forum.fanatec.com\/uploads\/880\/NLTSAR7P9O2M.jpg","name":"blackfriday_shifter_2.jpg","type":"image\/jpeg","size":186258,"width":1200,"height":628,"mediaID":1916,"dateInserted":"2019-11-26T16:56:01+00:00","insertUserID":30220,"foreignType":"embed","foreignID":30220,"embedType":"image","format":null,"bodyRaw":null},"loaderData":{"type":"image"}}}},{"insert":"\n"},{"attributes":{"bold":true},"insert":"ClubSport Shifter SQ V1.5 Limited Edition 2019 for 259,95€, US $199,95, 28,900.00¥, AU $349,90"},{"insert":"\nThe ClubSport Shifter SQ V1.5 Limited Edition 2019 comes in a unique colour scheme, featuring a matte grey body, anodised red lever, and matte carbon gear knobs."},{"attributes":{"list":{"depth":0,"type":"bullet"}},"insert":"\n"},{"insert":"Shares the same high-performance specification as the standard ClubSport Shifter SQ V1.5: Full Line truncated
JSON;
$json = '{"ops":' . $json . '}';
// Ensure to load the autoload file from Composer somewhere in your application.
require __DIR__ . '/vendor/autoload.php';
// Create the lexer object with your given Quill JSON delta code (either PHP array or JSON string).
$lexer = new \nadar\quill\Lexer($json);
// Echo the HTML for the given JSON ops.
echo $lexer->render();
@@ -0,0 +1,124 @@
<?php
namespace nadar\quill;
/**
* Block Listener
*
* Block listener writes from $line->input into the $line->output.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
abstract class BlockListener extends Listener
{
/**
* {@inheritDoc}
*/
public function type(): int
{
return self::TYPE_BLOCK;
}
/**
* Generate a rendered output from the current picks with a custom Wrapper Template.
*
* The main purpose of this method is so simplify the process of working with block elements. The method
* will take the `picks()` of the current Listener Object to generate the first and list line of the block
* and render its content inbetween.
*
* Example:
*
* ```php
* public function render(Lexer $lexer)
* {
* $this->wrapElement('<blockquote>{__buffer__}</blockquote>');
* }
* ```
*
* The above method will no take the picked element and render a `blockquote` tag with its content stored in `{__buffer__}`.
*
* Assuming you pass options to the picked element during the `process()` stage you might use those options as variable
* in brackes and passed to `$options` param:
*
* ```php
* $this->wrapElement('<h{heading}>{__buffer__}</h{heading}>', ['heading']);
* ```
*
* The above example assumes the heading option is stored during the process stage: `$this->pick($line, ['heading' => $heading]);`
*
* @param string $wrapper html snippet using `{__buffer__}` for the placement of the lines
* @param array<string> $options optional, pass the names of options from the lines you want to search & replace
* e.g. using ['key'] will replace `{key}` in the $wrapper with Pick->$key.
* With version 2.5.0 its allowed to provide the element as key and a callable as value which will be evaluted on request.
* ```php
* 'key' => function($value) {
* return '<p>'.$value.'</p>';
* }
* ```
* @since 2.4.0
* @return void
*/
protected function wrapElement($wrapper, array $options = [])
{
$search = ['{__buffer__}'];
foreach ($options as $key => $value) {
$search[] = is_int($key) ? '{'.$value.'}' : '{'.$key.'}';
}
foreach ($this->picks() as $pick) {
$first = $this->getFirstLine($pick);
// while from first to the pick line and store content in buffer
$buffer = null;
$first->while(static function (&$index, Line $line) use (&$buffer, $pick, $first) {
++$index;
$buffer .= $line->getInput();
$line->setDone();
// if the index of the picked lines is reached or the first element is the picked index.
if ($index == $pick->line->getIndex() || $first->getIndex() === $pick->line->getIndex()) {
return false;
}
});
$replace = [$buffer];
foreach ($options as $key => $value) {
$name = is_int($key) ? $value : $key;
$content = $pick->optionValue($name);
$value = is_callable($value) ? call_user_func($value, $content, $pick, $name) : $content;
$replace[] = $value;
}
$pick->line->output = str_replace($search, $replace, $wrapper).PHP_EOL;
$pick->line->setDone();
}
}
/**
* Returns the first Line from a Pick. If the Pick is the first Line, it will return it's own pick
* This is done because blockItems can consist of multiple inline items
*
* @since 1.3.2
*/
protected function getFirstLine(Pick $pick): Line
{
$first = $pick->line;
$pick->line->while(
static function (&$index, Line $line) use ($pick, &$first) {
--$index;
// its the same line as the start.. skip this one as its by default included in while operations
if ($line === $pick->line) {
return true;
} elseif (($line->hasEndNewline() || $line->hasNewline() || ($line->isJsonInsert() && !$line->isInline()))) {
return false;
}
// assign the line to $first
$first = $line;
return true;
}
);
return $first;
}
}
@@ -0,0 +1,168 @@
<?php
namespace nadar\quill;
/**
* Debug Object.
*
* The Debug class can return informations in a readable way from a lexer object. It will generate a html table
* with additional infos about how each line is parsed line by line.
*
* ```php
* $lexer = new Lexer($json);
* $lexer->render(); // make sure to run the render before call debugPrint();
*
* $debug = new Debug($lexer);
* echo $debug->debugPrint();
* ```
*
* In order to push informations into the Debug Panel, you can use
* the `debugInfo($text)` method which is available on all `Line` objects.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Debug
{
/**
* @var Lexer
*/
public $lexer;
/**
* Debug constructor
*/
public function __construct(Lexer $lexer)
{
$this->lexer = $lexer;
}
/**
* Get an array of lines which does not have status done.
*
* @return array<Line>
*/
protected function getNotDoneLines()
{
$lines = [];
foreach ($this->lexer->getLines() as $line) {
if (!$line->isDone()) {
$lines[] = $line;
}
}
return $lines;
}
/**
* Get an array of lines which does not have the status picked
*
* @return array<Line>
*/
protected function getNotPickedLines()
{
$lines = [];
foreach ($this->lexer->getLines() as $line) {
if (!$line->isPicked() && !$line->isDone()) {
$lines[] = $line;
}
}
return $lines;
}
/**
* return a string with debug informations.
*
* @return string
*/
public function debugPrint()
{
$d = "<p><b>NOT PICKED LINES</b></p>";
$d.= $this->getLinesTable($this->getNotPickedLines());
$d.= "<p><b>NOT DONE LINES</b></p>";
$d.= $this->getLinesTable($this->getNotDoneLines());
$d.= "<p><b>LINE BY LINE</b></p>";
$d.= $this->getLinesTable($this->lexer->getLines());
return nl2br($d);
}
/**
* Get an array with alles lines rendered a string
*
* @param array<Line> $lines
* @return string
*/
public function getLinesTable(array $lines)
{
$_lines = [];
foreach ($lines as $line) {
$_lines[] = [
$line->getIndex(),
$line->getInput(),
htmlentities($line->output, ENT_QUOTES),
htmlentities($line->renderPrepend(), ENT_QUOTES),
implode(", ", $line->getDebugInfo()),
var_export($line->getAttributes(), true),
var_export($line->isInline(), true),
$this->lineStatus($line),
var_export($line->hasEndNewline(), true),
var_export($line->hasNewline(), true),
var_export($line->isEmpty(), true),
];
}
return $this->renderTable($_lines, ['ID', 'input', 'output', 'prepend', 'debug', 'attributes', 'is inline', 'status', 'has end newline', 'has new line', 'is empty']);
}
/**
* Get the status waterfall of a given line
*
* @return string
*/
public function lineStatus(Line $line)
{
if ($line->isDone()) {
return 'Picked => Done';
} elseif ($line->isPicked()) {
return 'Picked';
}
return '';
}
/**
* Render the given table line by line
*
* @param array<mixed> $rows
* @param array<string> $head
* @return string
*/
protected function renderTable(array $rows, array $head = [])
{
$buffer = '<table class="table table-bordered table-striped table-hover table-sm small" border="1" width="100%" cellpadding="3" cellspacing="0">';
if (!empty($head)) {
$buffer.= '<thead><tr>';
foreach ($head as $col) {
$buffer.= '<td><b>'.$col.'</b></td>';
}
$buffer.= '</tr></thead>';
}
foreach ($rows as $cols) {
$buffer .= '<tr onclick="this.style.backgroundColor=(this.style.backgroundColor==\'red\')?(\'transparent\'):(\'red\');">';
foreach ($cols as $col) {
$buffer .= '<td>'.$col.'</td>';
}
$buffer .= '</tr>';
}
return $buffer . '</table>';
}
}
@@ -0,0 +1,63 @@
<?php
namespace nadar\quill;
use Exception;
/**
* Inline listener.
*
* Inline listeners changes the $line->input value!
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
abstract class InlineListener extends Listener
{
/**
* {@inheritDoc}
*/
public function type(): int
{
return self::TYPE_INLINE;
}
/**
* A short hand method for handling inline elements.
*
* 1. change input value
* 2. set as done, inline, and escaped
* 3. Add to pick list, in order to process in render method
*
* @param Line $line
* @param mixed $value
* @return void
*/
public function updateInput(Line $line, $value)
{
// we override the current element, and mark as done and mark as inline
$line->setInput($value);
$line->setDone();
$line->setAsInline();
$line->setAsEscaped();
$this->pick($line);
}
/**
* {@inheritDoc}
*/
public function render(Lexer $lexer)
{
foreach ($this->picks() as $pick) {
$next = $pick->line->next(static function (Line $line) {
return !$line->isInline();
});
if (!$next) {
throw new Exception(sprintf("Unable to find a next element. Invalid DELTA on '%s'. Maybe your delta code does not end with a newline?", $pick->line->getInput()));
}
$next->addPrepend($pick->line->getInput(), $pick->line);
}
}
}
@@ -0,0 +1,462 @@
<?php
namespace nadar\quill;
use nadar\quill\listener\Align;
use nadar\quill\listener\BackgroundColor;
use nadar\quill\listener\Blockquote;
use nadar\quill\listener\Bold;
use nadar\quill\listener\CodeBlock;
use nadar\quill\listener\Color;
use nadar\quill\listener\Font;
use nadar\quill\listener\Heading;
use nadar\quill\listener\Image;
use nadar\quill\listener\Italic;
use nadar\quill\listener\Link;
use nadar\quill\listener\Lists;
use nadar\quill\listener\Script;
use nadar\quill\listener\Strike;
use nadar\quill\listener\Text;
use nadar\quill\listener\Underline;
use nadar\quill\listener\Video;
/**
* Lexer Delta Parser.
*
* The lexer class represents the main thread in how delta input is processed and rendered.
*
* Basically listeners can watch every line of delta and interact with the line, which means
* reading input and writting to output and mark this line as done, so other listeners won't
* take care of this line as well.
*
* ## Basic concept
*
* Listeners are grouped in 2 types:
*
* + inline: For elements which are only inline applied, like writing bold or italic
* + block: Used when the line represents a full html element like heading or lists
*
* Inside this group types there are 2 prioirities:
*
* + early bird: This is default value, the early bird catches the worm...
* + garbage collector: This is mainly used for the text listener which generates the paragraphs and can only be done at the very end of the process.
*
* Every listener has two methods a process() and render():
*
* + process: is triggered by every line, so the listener can choose whether he wants to pick this line, interact or not.
* + render: after all lines are processed, every listener triggers the render() method once, so the picked line from process can be "further" processed and rendered.
*
* ## Lifecycle
*
* 1. lines will be generated
* 2. lines foreached and inline early bird listeners run process() method.
* 3. lines foreached and inline garbage collector listeners run process() method.
* 4. lines foreached and block early bird listeners run process() method.
* 5. lines foreached and block garbage collector listener run process() method.
* 6. inline listeners foreach and run render() method.
* 7. block listeners foreach and run render() method.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Lexer
{
/**
* @var string The quill deltas newline is always \n, thefore do not use PHP_EOL as its different in win platforms.
* @see https://quilljs.com/guides/designing-the-delta-format/
* @see https://github.com/nadar/quill-delta-parser/issues/12
* @since 1.3.1
*/
public const DELTA_EOL = "\n";
/**
* @var string An internal string for newlines, this makes it more easy to debug instead of using \n (newlines).
*/
public const NEWLINE_EXPRESSION = '<!-- <![CDATA[NEWLINE]]> -->';
/**
* @var boolean Whether input should be escaped by listeners when mixed with html elements.
* Note that a specific listener can decide to not escape if their output should be raw html.
* If script and other insecure input values should be allowed this can be turned off but its not recommend!
* @since 1.2.0
*/
public $escapeInput = true;
/**
* @var int These flags are used for escaping values for mixing with a html context.
* @since 1.2.0
*/
public $escapeFlags = ENT_QUOTES|ENT_HTML5;
/**
* @var string The encoding is used for escaping values for mixing with a html context.
* @since 1.2.0
*/
public $escapeEncoding = 'UTF-8';
/**
* @var boolean Whether debbuging is enabled or not. If enabled lines can contain debugInfo messages which can be retrieved with {{Debug}} class.
*/
public $debug = false;
/**
* @var array<mixed>|string The delta ops json as string or as already parsed array
*/
protected $json;
/**
* @var array<Line>
*/
private $_lines = [];
/**
* @var array<mixed> The listeners grouped by type and priority.
*/
protected $listeners = [
Listener::TYPE_INLINE => [
Listener::PRIORITY_EARLY_BIRD => [],
Listener::PRIORITY_GARBAGE_COLLECTOR => [],
],
Listener::TYPE_BLOCK => [
Listener::PRIORITY_EARLY_BIRD => [],
Listener::PRIORITY_GARBAGE_COLLECTOR => [],
],
];
/**
* Initializer
*
* @param string|array<mixed> $json The delta ops json as string or as already parsed array.
* @param boolean $loadBuiltinListeners Whether the built in listeners should be loaded or not.
*/
public function __construct($json, $loadBuiltinListeners = true)
{
$this->json = $json;
if ($loadBuiltinListeners) {
$this->loadBuiltinListeners();
}
}
/**
* Loads the library built in listeners.
*
* @return void
*/
public function loadBuiltinListeners()
{
$this->registerListener(new Image());
$this->registerListener(new Bold());
$this->registerListener(new Italic());
$this->registerListener(new Color());
$this->registerListener(new BackgroundColor());
$this->registerListener(new Link());
$this->registerListener(new Video());
$this->registerListener(new Strike());
$this->registerListener(new Underline());
$this->registerListener(new Heading());
$this->registerListener(new CodeBlock());
$this->registerListener(new Text());
$this->registerListener(new Lists());
$this->registerListener(new Blockquote());
$this->registerListener(new Font());
$this->registerListener(new Script());
$this->registerListener(new Align());
}
/**
* Register a new listener.
*
* @return Lexer
*/
public function registerListener(Listener $listener)
{
$this->listeners[$listener->type()][$listener->priority()][get_class($listener)] = $listener;
return $this;
}
/**
* Overrite an existing listener with a new object
*
* An example could when you like to provide more options or access other elements which are not covered by the base class
* so you can extend from the built in listeners and overrite them. This keeps also the hyrarchical level of the elements.
*
* ```php
* $lexer->overwriteListener(new Image, new MyOwnImage);
* ```
*
* As the `new Image` listener is already registered, it will just replace the object with `new MyOwnImage`.
*
* @param Listener $listener The already registered listener object
* @param Listener $new The new listener object
* @see https://github.com/nadar/quill-delta-parser/issues/55
* @since 2.8.0
* @return Lexer
*/
public function overwriteListener(Listener $listener, Listener $new)
{
$this->listeners[$listener->type()][$listener->priority()][get_class($listener)] = $new;
return $this;
}
/**
* Get the input json as array.
*
* @return array<mixed> The json as array formated.
*/
public function getJsonArray(): array
{
return is_array($this->json) ? $this->json : self::decodeJson($this->json);
}
/**
* Get the ops section from the json otherwise json array.
*
* @return array<mixed>
*/
public function getOps(): array
{
return $this->getJsonArray()['ops'] ?? $this->getJsonArray();
}
/**
* Get the line object for a given id/row/index.
*
* @param integer $index The index of the line.
* @return Line|boolean
*/
public function getLine($index)
{
return $this->_lines[$index] ?? false;
}
/**
* @return array<Line> Returns an array with all line objects.
*/
public function getLines(): array
{
return $this->_lines;
}
/**
* Convert the arrray operations array into lines
*
* @param array<mixed> $ops An array from json with ops data in delta format.
* @return array<Line> An array with Line objects
*/
protected function opsToLines(array $ops)
{
$lines = [];
$i = 0;
foreach ($ops as $delta) {
// replace newline chars with internal expression
$insert = $this->replaceNewlineWithExpression($delta['insert']);
// if its an empty "newline-line"
if ($insert == self::NEWLINE_EXPRESSION) {
$lines[$i] = new Line($i, '', $delta['attributes'] ?? [], $this, true, true);
++$i;
} else {
$insert = $this->normalizeInsert($insert);
// remove the last newline from the line, as it will be splited into lines anyhow.
$line = $this->removeLastNewline($insert);
// check if the original input was changed, in order to determine whether current line had new line char.
$hasEndNewline = ($line !== $insert);
// check if original insert string has a new line char in the string
$hasNewline = $this->lineHasNewline($insert);
// split the input string into parts / lines.
$parts = explode(self::NEWLINE_EXPRESSION, $line);
$count = count($parts);
foreach ($parts as $index => $value) {
// check if this line had the end newline
$isLast = ($index + 1) == $count;
$hadEndNewline = $isLast && $hasEndNewline;
if ($hasNewline && $isLast && !$hasEndNewline) {
$hasNewline = false;
}
$lines[$i] = new Line($i, $value, $delta['attributes'] ?? [], $this, $hadEndNewline, $hasNewline);
++$i;
}
}
}
return $lines;
}
/**
* Whether the current line as a newline char.
*
* @param string $input
* @return boolean
*/
protected function lineHasNewline($input)
{
return strpos($input, self::NEWLINE_EXPRESSION) !== false;
}
/**
* Replace new lines with an internal representation that aids debugging
*
* @param array<mixed>|string $input
* @return array<mixed>|string
*/
protected function replaceNewlineWithExpression($input)
{
return is_array($input) ? $input : str_replace(self::DELTA_EOL, self::NEWLINE_EXPRESSION, $input);
}
/**
* Normamlize the insert values into json
*
* @param array<mixed>|string $insert
* @return string
* @since 1.2.2
*/
protected function normalizeInsert($insert)
{
// plugins can have array values as text.
if (is_array($insert)) {
// convert the array into a json string
return json_encode($insert);
}
return $insert;
}
/**
* Remove the last newline expression from a string
*
* @param string $insert
* @return string
*/
protected function removeLastNewline($insert)
{
$expLength = strlen(self::NEWLINE_EXPRESSION);
// remove new line from the end of the string
// as this explode split well be done anyhow or its already part of a new line
if (substr($insert, -$expLength) == self::NEWLINE_EXPRESSION) {
return substr($insert, 0, -$expLength);
}
return $insert;
}
/**
* Process all listeners for a given type
*
* @param int $type
* @return void
*/
protected function processListeners(Line $line, $type)
{
foreach ($this->listeners[$type] as $prios) {
foreach ($prios as $listener) {
$listener->process($line);
}
}
}
/**
* Render all listeners for a given type
*
* @param int $type
* @return void
*/
protected function renderListeners($type)
{
foreach ($this->listeners[$type] as $prios) {
foreach ($prios as $listener) {
$listener->render($this);
}
}
}
/**
* Renders the current delta into a html string.
*
* @return string The html code for the given delta input.
*/
public function render()
{
$this->_lines = $this->opsToLines($this->getOps());
foreach ($this->_lines as $line) {
$this->processListeners($line, Listener::TYPE_INLINE);
$this->processListeners($line, Listener::TYPE_BLOCK);
}
$this->renderListeners(Listener::TYPE_INLINE);
$this->renderListeners(Listener::TYPE_BLOCK);
$buff = null;
foreach ($this->_lines as $line) {
$buff.= $line->output;
}
return $buff;
}
/**
* Checks if a string is a json or not.
*
* Example values which return true:
*
* ```php
* Json::isJson('{"123":"456"}');
* Json::isJson('{"123":456}');
* Json::isJson('[{"123":"456"}]');
* Json::isJson('[{"123":"456"}]');
* ```
*
* @param mixed $value The value to test if its a json or not.
* @return boolean Whether the string is a json or not.
*/
public static function isJson($value)
{
if (!is_scalar($value)) {
return false;
}
$firstChar = substr($value, 0, 1);
if ($firstChar !== '{' && $firstChar !== '[') {
return false;
}
json_decode($value);
return json_last_error() === JSON_ERROR_NONE;
}
/**
* Decode a given json string into a php array.
*
* @param string $json Input json
* @return array<mixed>
*/
public static function decodeJson($json)
{
return json_decode($json, true);
}
/**
* Escape plain text output before mixing in a html context.
*
* This should be used on any input or attributes in a delta operation.
* Double encoding is prevented on already encoded characters.
* For escaping input, use Line->getInput() instead. Otherwise an inline listener would encode the tags from another nested inline listener.
*
* @since 1.2.0
* @param string $value The value to escape.
* @return string The escaped value, safe for usage in html, as long as $escapeInput is set to true.
*/
public function escape($value)
{
if (!$this->escapeInput) {
return $value;
}
return htmlspecialchars($value, $this->escapeFlags, $this->escapeEncoding, $double = false);
}
}
@@ -0,0 +1,617 @@
<?php
namespace nadar\quill;
/**
* Line Object.
*
* A line object represents a line from the delta input. Lines are splited in the lexer object.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Line
{
/**
* @var integer The status of a line which is not picked or done, which is default.
*/
public const STATUS_CLEAN = 1;
/**
* @var integer The status of the line if its picked by a listener
*/
public const STATUS_PICKED = 2;
/**
* @var integer The status of the line if some of the listener marked this line as done.
*/
public const STATUS_DONE = 3;
/**
* @var array<string> An array with values which can be prependend to the actuall input string. This is mainly used if inline
* elements are passed to the next "not" inline element.
*/
public $prepend = [];
/**
* @var string The output is the value which will actually rendered by the lexer. So lines which directly write to the output
* buffer needs to fill in this variable.
*/
public $output;
/**
* @var string The input string which is assigned from the line parser. This is the actual content of the line itself!
* @since 3.1.0
*/
protected $input;
/**
* @var integer Holds the current status of the line.
*/
protected $status = 1;
/**
* @var integer The ID/Index/Row of the line
*/
protected $index;
/**
* @var array<mixed> An array with all attributes which are assigned to this lines. attribute can be inline markers like
* bold, italic, links and so on.
*/
protected $attributes = [];
/**
* @var Lexer The lexer object in order to access other lines and elements.
*/
protected $lexer;
/**
* @var boolean Whether the current line is handled as "inline-line" or not. Inline lines have different effects when parsing the
* end output. For example those can be skipped as they usual prepend the input value into the next line.
*/
protected $isInline = false;
/**
* @var boolean Whether the current line is already escaped by a listener. If this is false, the next listener should preferable do so.
* If this is true, it should not be done again by a next listener.
* @since 1.2.0
*/
protected $isEscaped = false;
/**
* @var boolean As certain elements has an end of newline but those are removed within the lexer opt to line methods we remember
* this information here. If true this element has an \n element which has been original removed from input (as lines are spliited into
* new lines).
*/
protected $hadEndNewline = false;
/**
* @var boolean Whether this line has a newline or not, this information is already provided by the lines to ops method.
*/
protected $hasNewline;
/**
* @var array<string>
*/
private $_debug = [];
/**
* Constructor
*
* @param integer $index The numberic index of the row within all the lines.
* @param string $input The input value from the line parser for the current line.
* @param array<mixed> $attributes
* @param boolean $hadEndNewline Whether this element orignali had an newline at the end.
* @param boolean $hasNewline
*/
public function __construct($index, $input, array $attributes, Lexer $lexer, $hadEndNewline, $hasNewline)
{
$this->index = $index;
$this->input = $input;
$this->attributes = $attributes;
$this->lexer = $lexer;
$this->hadEndNewline = $hadEndNewline;
$this->hasNewline = $hasNewline;
}
/**
* Whether the current line had a new line char or not, this is very important in terms of finding out wether its a block
* element or inline element.
*
* This informations is assigned in the opsToLine() method in the lexer object.
*
* @return boolean
*/
public function hasNewline()
{
return $this->hasNewline;
}
/**
* Whether this line as an end newline char or not.
*
* @return boolean
*/
public function hasEndNewline()
{
return $this->hadEndNewline;
}
/**
* Whether this line is the first line or not.
*
* @return boolean
*/
public function isFirst()
{
return $this->previous() === false;
}
/**
* Get the Lexer
*
* @since 1.2.0
* @return Lexer
*/
public function getLexer()
{
return $this->lexer;
}
/**
* Get the line's input in a safe way.
*
* Escaping for html is done if this wasn't done by a previous listener already.
*
* @since 1.2.0
* @return string
*/
public function getInput()
{
if ($this->isEscaped()) {
return $this->input;
}
return $this->lexer->escape($this->getUnsafeInput());
}
/**
* Get the raw line's input, this might not be escaped for html context.
*
* > Note it could be escaped if a previous inline listener updated the input value
*
* @since 1.2.0
* @return string
*/
public function getUnsafeInput()
{
return $this->input;
}
/**
* Get the array with attributes, if any.
*
* @return array<mixed>
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Whether the current line has attribute informations or not.
*
* @return boolean
*/
public function hasAttributes()
{
return !empty($this->attributes);
}
/**
* Get the value for a given attribute name, if not exists return false.
*
* @param string $name
* @param mixed $defaultValue (@since 3.2.0)
* @return mixed
*/
public function getAttribute($name, $defaultValue = false)
{
return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $defaultValue;
}
/**
* Add a new value to the prepend array.
*
* Certain elements needs to prepend values into the next element. The Line argument is required in order to
* ensure the correct index for the prepend element.
*
* @param string $value The value to prepend.
* @param Line $line The line which does the prepend, this is used to ensure the correctly order index of the elements. {@since 1.3.2}
* @return void
*/
public function addPrepend($value, Line $line)
{
$this->prepend[$line->getIndex()] = $value;
}
/**
* Returns the string for the prepend values.
*
* @return string The prepend value for this line.
*/
public function renderPrepend()
{
ksort($this->prepend);
return implode("", array_unique($this->prepend));
}
/**
* While trough lines forward or backwards define trough index until false is returned.
*
* An example how to while trough lines, increasing (down) the index until a certain condition
* ($line->isFirst) happens writing lint input into a buffer variable.
*
* > Keep in mind that while() will contain the line where the function applys, so the first line will always be
* > the line you apply the while() function.
*
* ```php
* $buffer = null;
*
* $line->while(function (&$index, Line $line) use (&$buffer) {
* $index++;
* $buffer.= $line->input;
*
* if ($line->isFirst()) {
* return false;
* }
* });
*
* echo $buffer;
* ```
*
* > Keep in mind that `false` must be returned to stop the while process.
*
* @param callable $condition A callable which requires 2 params, the first is the index which is passed as reference,
* second is the current line.
* @return void
*/
public function while(callable $condition)
{
$iterate = true;
$i = $this->getIndex();
while ($iterate) {
$line = $this->lexer->getLine($i);
if (!$line) {
$iterate = false;
break;
}
$iterate = call_user_func_array($condition, [&$i, $line]);
if ($iterate !== false) {
$iterate = true;
}
}
}
/**
* While loop down (to the next elements) until false is returend.
*
* > This method wont return the line.
*
* @param callable $condition The while condition until false is returned. The callable receives the Line as argument.
* @since 1.3.0
* @return void
*/
public function whileNext(callable $condition)
{
$next = $this->next();
if ($next) {
$next->while(static function (&$index, Line $line) use ($condition) {
++$index;
return call_user_func($condition, $line);
});
}
}
/**
* While loop up (to the previous elements) until false is returend.
*
* > This method wont return the line.
*
* @param callable $condition The while condition until false is returned. The callable receives the Line as argument.
* @since 1.3.0
* @return void
* @see see while() for better documentation
*/
public function whilePrevious(callable $condition)
{
$previous = $this->previous();
if ($previous) {
$previous->while(static function (&$index, Line $line) use ($condition) {
--$index;
return call_user_func($condition, $line);
});
}
}
/**
* Iteration helper the go forward and backward in lines.
*
* The condition contains whether index should go up or down.
*
* ```php
* return $this->iterate($line, function ($i) {
* return $i+1;
* }, function(Line $line) {
* // will stop the process and return this current line
* return true;
* });
* ```
*
* @param Line $line
* @param callable $condition The condition callable for the index
* @param callable $fn The function which is returend to determine whether this line should be picked or not.
* @return boolean|Line
*/
protected function iterate(Line $line, callable $condition, callable $fn)
{
$i = $line->getIndex();
$iterate = true;
$response = false;
while ($iterate) {
$i = call_user_func($condition, $i);
$elmn = $this->lexer->getLine($i);
// no next element found
if (!$elmn) {
$iterate = false;
} elseif (call_user_func($fn, $elmn)) {
// fn match (return true) return current element.
$response = $elmn;
$iterate = false;
}
}
return $response;
}
/**
* Get the next element.
*
* If a closure is provided you can define a condition of whether next element should be taken or not.
*
* For example you can iterate to the next element which is not empty:
*
* ```php
* $nextNotEmpty = $line->next(function(Line $line) {
* return !$line->isEmpty();
* });
* ```
*
* if true is returned this line will be assigned.
*
* @param callable $fn A function in order to determined whether this is the next element or not, if not provided the first next element is returned.
* @return Line|boolean The method will return either a Line object or false, if there is no next Line.
*/
public function next($fn = null)
{
if ($fn === null) {
return $this->lexer->getLine($this->index + 1);
}
return $this->iterate($this, static function ($i) {
return $i+1;
}, $fn);
}
/**
* Get the previous line.
*
* If no previous line exists, false is returned.
*
* ```php
* $nextNotEmpty = $line->previous(function(Line $line) {
* return !$line->isEmpty();
* });
* ```
*
* if true is returned this line will be assigned.
*
* @param callable $fn A function in order to determined whether this is the previous element or not, if not provided the first previous element is returned.
* @return Line|boolean The method will return either a Line object or false, if there is no next Line.
*/
public function previous($fn = null)
{
if ($fn === null) {
return $this->lexer->getLine($this->index - 1);
}
return $this->iterate($this, static function ($i) {
return $i-1;
}, $fn);
}
/**
* Adjust the line's input.
*
* The new input is assumed to be escaped.
*
* @since 3.1.0
* @param string $newInput
* @return void
*/
public function setInput($newInput)
{
$this->input = $newInput;
}
/**
* Setter method whether the current element is inline or not.
*
* @return void
*/
public function setAsInline()
{
$this->isInline = true;
}
/**
* Whether current line is an inline line or not.
*/
public function isInline(): bool
{
return $this->isInline;
}
/**
* Setter method whether the current line is escaped or not.
*
* @since 1.2.0
* @return void
*/
public function setAsEscaped()
{
$this->isEscaped = true;
}
/**
* Whether the current line is escaped or not.
*
* @since 1.2.0
* @return boolean
*/
public function isEscaped()
{
return $this->isEscaped;
}
/**
* Getter method for the index of the line.
*
* @return integer
*/
public function getIndex()
{
return $this->index;
}
/**
* Set this line as picked.
*
* @return void
*/
public function setPicked()
{
$this->status = self::STATUS_PICKED;
}
/**
* Whether current line is picked or not. If the line has been picked an is marked as
* done, is picked will return false.
*
* @return boolean
*/
public function isPicked()
{
return $this->status == self::STATUS_PICKED;
}
/**
* Mark this line as done.
*
* @return void
*/
public function setDone()
{
$this->status = self::STATUS_DONE;
}
/**
* Whether this line is done or not.
*
* @return boolean
*/
public function isDone()
{
return $this->status == self::STATUS_DONE;
}
/**
* Whether current line as empty content string or not.
*
* @return boolean
*/
public function isEmpty()
{
return $this->input == '' && empty($this->prepend);
}
/**
* Some plugins have a json as insert value, in order to detected such values
* you can use this method.
*
* This will check if the given insert string is a json. In order to deocde the
* json into a php array afterwards, you can use getArrayInsert();
*
* @return boolean Whether current insert string is a json or not.
*/
public function isJsonInsert()
{
return Lexer::isJson($this->input);
}
/**
* Returns the insert json as array.
*
* @return array<mixed>
*/
public function getArrayInsert()
{
return Lexer::decodeJson($this->input);
}
/**
* Check whether insert is json/array input. if yes return the requres key.
*
* @param string $key The key from the json array
* @return mixed
*/
public function insertJsonKey($key)
{
if (!$this->isJsonInsert()) {
return false;
}
$insert = $this->getArrayInsert();
return array_key_exists($key, $insert) ? $insert[$key] : false;
}
/**
* Add debug message for this line if {{Lexer::$debug}} is enabled.
*
* @param string $message The message which should be logged.
* @since 1.3.0
* @return void
*/
public function debugInfo($message)
{
if ($this->lexer->debug) {
$this->_debug[] = $message;
}
}
/**
* Return an array with all debug informations
*
* @return array<string>
* @since 1.3.0
*/
public function getDebugInfo(): array
{
return $this->_debug;
}
}
@@ -0,0 +1,100 @@
<?php
namespace nadar\quill;
/**
* Listener Object.
*
* Every type of element is a listener. Listeners are "listening" to every line of delta code and can
* pick and process this line.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
abstract class Listener
{
/**
* @var integer Type inline listener
*/
public const TYPE_INLINE = 1;
/**
* @var integer Type block listener
*/
public const TYPE_BLOCK = 2;
/**
* @var integer First priority listener within the given type
*/
public const PRIORITY_EARLY_BIRD = 1;
/**
* @var integer Second priority listener within the given type. This is currently only used
* for TEXT listeners - as they need to be the very last entry.
*/
public const PRIORITY_GARBAGE_COLLECTOR = 2;
/**
* Get the listener type, either INLINE (1) or BLOCK (2)
*/
abstract public function type(): int;
/**
* Process the line
*
* @return void
*/
abstract public function process(Line $line);
/**
* Get the priority either 1 or 2
*/
public function priority(): int
{
return self::PRIORITY_EARLY_BIRD;
}
/**
* @var array<mixed>
*/
private $_picks = [];
/**
* Pick a certain line during the process() process in order to use them later in render method.
*
* If a line is picked, the status of the line switches to picked.
*
* @param Line $line
* @param array<mixed> $options
* @return void
*/
public function pick(Line $line, array $options = [])
{
$line->setPicked();
$line->debugInfo('picked by ' . static::class);
$this->_picks[] = new Pick($this, $line, $options, count($this->_picks));
}
/**
* Returns an array with all picked lineds.
*
* @return Pick[] An array with Line objects, for IDE purposes we return the Line object as phpdoc
*/
public function picks(): array
{
return $this->_picks;
}
/**
* The render method is processed after the process() method is done.
*
* Its the right place to go forward and backward in lines if you need to, as at the point all lines
* are allready processed trough process() method.
*
* @param Lexer $lexer
* @return void
*/
public function render(Lexer $lexer)
{
}
}
@@ -0,0 +1,90 @@
<?php
namespace nadar\quill;
/**
* Pick Object holds the line which is picked with optional data.
*
* @since 1.0.0
* @author Basil Suter <basil@nadar.io>
*/
class Pick
{
/**
* @var Line
*/
public $line;
/**
* @var array<mixed> An array with options which can be read trough magical getter
*/
protected $options = [];
/**
* @var integer Contains the current index number for this pick.
*/
protected $index;
/**
* @var Listener The listener object the picke was made from.
*/
protected $listener;
/**
* Create new line pick
*
* @param array<mixed> $options
* @param integer $index Index of picks, starts with 0
*/
public function __construct(Listener $listener, Line $line, array $options, $index)
{
$this->listener = $listener;
$this->line = $line;
$this->options = $options;
$this->index = $index;
}
/**
* @param mixed $name
* @deprecated Deprecated in 3.1 will be removed in 4.0 use `optionValue($name)` instead.
* @return mixed
*/
public function __get($name)
{
trigger_error(sprintf("Deprecated in 3.1 will be removed in 4.0 use `optionValue('%s')` instead.", $name), E_USER_NOTICE);
return $this->options[$name] ?? null;
}
/**
* Return the value of an option if available
*
* @param string $name
* @param mixed $defaultValue
* @return mixed
* @since 3.1.0
*/
public function optionValue($name, $defaultValue = null)
{
return array_key_exists($name, $this->options) ? $this->options[$name] : $defaultValue;
}
/**
* Whether current pick is the first pick inside the list of picks.
*
* @return boolean
*/
public function isFirst()
{
return $this->index == 0;
}
/**
* Whether current pick is the last pick inside the list of picks.
*
* @return boolean
*/
public function isLast()
{
return (count($this->listener->picks()) - 1) == $this->index;
}
}
@@ -0,0 +1,51 @@
<?php
namespace nadar\quill\listener;
use Exception;
use nadar\quill\BlockListener;
use nadar\quill\Lexer;
use nadar\quill\Line;
/**
* Convert align attributes into p tags with text-align css applied.
*
* @author Gaëtan Faugère <gaetan@fauge.re>
* @since 2.4.0
*/
class Align extends BlockListener
{
/**
* @var array<string> Supported alignments.
*/
public $alignments = ['center', 'right', 'justify', 'left'];
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$alignment = $line->getAttribute('align');
if ($alignment) {
$this->pick($line, ['alignment' => $alignment]);
$line->setDone();
}
}
/**
* {@inheritDoc}
*
* @throws Exception for unknown alignment values
*/
public function render(Lexer $lexer)
{
foreach ($this->picks() as $pick) {
if (!in_array($pick->optionValue('alignment'), $this->alignments)) {
// prevent html injection in case the attribute is user input
throw new Exception('An unknown alignment "' . $pick->optionValue('alignment') . '" has been detected.');
}
}
$this->wrapElement('<p style="text-align: {alignment};">{__buffer__}</p>', ['alignment']);
}
}
@@ -0,0 +1,31 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Convert background color attributes into span tag.
*
* @author Basil Suter <basil@nadar.io>
* @since 3.4.0
*/
class BackgroundColor extends InlineListener
{
/**
* @var boolean If ignore is enabled, the colors won't apply. This can be use full if coloring is disabled in your quill editor
* but people copy past content from somewhere else which will then generate the color attribute.
*/
public $ignore = false;
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if (($color = $line->getAttribute('background'))) {
$this->updateInput($line, $this->ignore ? $line->getInput() : '<span style="background-color:'.$line->getLexer()->escape($color).'">'.$line->getInput().'</span>');
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\BlockListener;
use nadar\quill\Lexer;
use nadar\quill\Line;
/**
* Convert Blockquote Elements
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Blockquote extends BlockListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if ($line->getAttribute('blockquote')) {
$this->pick($line);
$line->setDone();
}
}
/**
* {@inheritDoc}
*/
public function render(Lexer $lexer)
{
$this->wrapElement('<blockquote>{__buffer__}</blockquote>');
}
}
@@ -0,0 +1,25 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Convert Bold attributes into tags.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Bold extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if ($line->getAttribute('bold')) {
$this->updateInput($line, '<strong>'.$line->getInput().'</strong>');
}
}
}
@@ -0,0 +1,36 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\BlockListener;
use nadar\quill\Lexer;
use nadar\quill\Line;
/**
* Code Block
*
* @author Basil Suter <basil@nadar.io>
* @since 2.7.0
*/
class CodeBlock extends BlockListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$heading = $line->getAttribute('code-block');
if ($heading) {
$this->pick($line);
$line->setDone();
}
}
/**
* {@inheritDoc}
*/
public function render(Lexer $lexer)
{
$this->wrapElement('<pre><code>{__buffer__}</code></pre>');
}
}
@@ -0,0 +1,31 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Convert color attributes into span tag.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.1.0
*/
class Color extends InlineListener
{
/**
* @var boolean If ignore is enabled, the colors won't apply. This can be use full if coloring is disabled in your quill editor
* but people copy past content from somewhere else which will then generate the color attribute.
*/
public $ignore = false;
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if (($color = $line->getAttribute('color'))) {
$this->updateInput($line, $this->ignore ? $line->getInput() : '<span style="color:'.$line->getLexer()->escape($color).'">'.$line->getInput().'</span>');
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Renders font attribute which will set font-family.
*
* @author Basil Suter <basil@nadar.io>
* @since 2.1.0
*/
class Font extends InlineListener
{
/**
* @var boolean If ignore is enabled, the font won't apply. This can be use full if font styles is disabled in your quill editor
* but people copy past content from somewhere else which will then generate the font attribute.
*/
public $ignore = false;
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if (($font = $line->getAttribute('font'))) {
$this->updateInput($line, $this->applyTemplate($font, $line));
}
}
/**
* Wrap the font family span tag if ignore is disabled.
*
* @param string $font
* @return string
*/
public function applyTemplate($font, Line $line)
{
if ($this->ignore) {
return $line->getInput();
}
return '<span style="font-family: '.$line->getLexer()->escape($font).';">'. $line->getInput() . '</span>';
}
}
@@ -0,0 +1,52 @@
<?php
namespace nadar\quill\listener;
use Exception;
use nadar\quill\BlockListener;
use nadar\quill\Lexer;
use nadar\quill\Line;
/**
* Convert header into heading elements.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Heading extends BlockListener
{
/**
* @var array<int> Supported header levels.
* @since 1.2.0
*/
public $levels = [1, 2, 3, 4, 5, 6];
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$heading = $line->getAttribute('header');
if ($heading) {
$this->pick($line, ['heading' => $heading]);
$line->setDone();
}
}
/**
* {@inheritDoc}
*
* @throws Exception for unknown heading levels {@since 1.2.0}
*/
public function render(Lexer $lexer)
{
foreach ($this->picks() as $pick) {
if (!in_array($pick->optionValue('heading'), $this->levels)) {
// prevent html injection in case the attribute is user input
throw new Exception('An unknown heading level "' . $pick->optionValue('heading') . '" has been detected.');
}
}
$this->wrapElement('<h{heading}>{__buffer__}</h{heading}>', ['heading']);
}
}
@@ -0,0 +1,47 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Convert Image attributes into image element.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.2
*/
class Image extends InlineListener
{
/**
* @var string
*/
public $wrapper = '<img src="{src}" {width} {height} alt="" class="img-responsive img-fluid" />';
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$embedUrl = $line->insertJsonKey('image');
if ($embedUrl) {
if ($width = $line->getAttribute('width')) {
$width = 'width="'.$line->getLexer()->escape($width).'"';
}
if ($height = $line->getAttribute('height')) {
$height = 'height="'.$line->getLexer()->escape($height).'"';
}
$this->updateInput($line, preg_replace('#\s+#', ' ', str_replace([
'{src}',
'{width}',
'{height}'
], [
$line->getLexer()->escape($embedUrl),
$width,
$height
], $this->wrapper)));
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Convert Italic Inline elements.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Italic extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if ($line->getAttribute('italic')) {
$this->updateInput($line, '<em>'.$line->getInput().'</em>');
}
}
}
@@ -0,0 +1,64 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Convert links into a inline elements.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Link extends InlineListener
{
/**
* @var string The wrapper template which is used to generate the link tag
* @since 3.0.0
*/
public $wrapperOpen = '<a href="{link}" target="_blank">';
/**
* @var string The content element in between
* @since 3.0.0
*/
public $wrapperMiddle = '{text}';
/**
* @var string Closing Tag
* @since 3.0.0
*/
public $wrapperClose = '</a>';
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$link = $line->getAttribute('link');
if ($link) {
$wrapper = '';
$searchArgument = [];
$replaceArgument = [];
$previousLineHasSimilarLink = $line->previous() !== false && $line->previous()->getAttribute('link') === $link;
if ($previousLineHasSimilarLink === false) {
$wrapper .= $this->wrapperOpen;
$searchArgument[] = '{link}';
$replaceArgument[] = $line->getLexer()->escape($link);
}
$wrapper .= $this->wrapperMiddle;
$searchArgument[] = '{text}';
$replaceArgument[] = $line->getInput();
$nextLineHasSimilarLink = $line->next() !== false && $line->next()->getAttribute('link') === $link;
if ($nextLineHasSimilarLink === false) {
$wrapper .= $this->wrapperClose;
}
$this->updateInput($line, str_replace($searchArgument, $replaceArgument, $wrapper));
}
}
}
@@ -0,0 +1,182 @@
<?php
namespace nadar\quill\listener;
use Exception;
use nadar\quill\BlockListener;
use nadar\quill\Lexer;
use nadar\quill\Line;
use nadar\quill\Pick;
/**
* Convert List elements (ul, ol) into Block element.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Lists extends BlockListener
{
/**
* @var string
*/
public const ATTRIBUTE_LIST = 'list';
/**
* @var string
*/
public const LIST_TYPE_BULLET = 'bullet';
/**
* @var string
*/
public const LIST_TYPE_ORDERED = 'ordered';
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$listType = $line->getAttribute(self::ATTRIBUTE_LIST);
if ($listType) {
$this->pick($line, ['type' => $listType]);
$line->setDone();
}
}
/**
* {@inheritDoc}
*/
public function render(Lexer $lexer)
{
$isOpen = false;
$isEmpty = false;
$listTag = null;
foreach ($this->picks() as $pick) {
$first = $this->getFirstLine($pick);
// defines whether this attribute list element is the last one of a list serie.
$isLast = false;
// if this is an empty line .... the first attribute contains the list information, otherwise
// the first line contains content.
if ($first->getAttribute(self::ATTRIBUTE_LIST)) {
$isEmpty = true;
}
// while from first to the pick line and store content in buffer
$buffer = null;
if (!$isEmpty) {
$first->while(static function (&$index, Line $line) use (&$buffer, $pick) {
++$index;
$buffer.= $line->getInput();
$line->setDone();
if ($index == $pick->line->getIndex()) {
return false;
}
});
}
// go to the next element with endlinew and check if it contains a list type until then
$hasNextInside = false;
$pick->line->whileNext(static function (Line $line) use (&$hasNextInside) {
// we found the next list elemnt, stop thie while loop
if ($line->getAttribute(self::ATTRIBUTE_LIST)) {
return false;
}
// if one of those new lines contains a endnew line or newline or is block level store this information
if ($line->hasEndNewline() || $line->hasNewline() || ($line->isJsonInsert() && !$line->isInline())) {
$hasNextInside = true;
}
});
// There was a newline element until next list element, so end of list has reached.
if ($hasNextInside) {
$isLast = true;
}
$output = null;
// this makes sure that when two list types are after each other (OL and UL)
// the previous will be closed so the new one will open
if ($isOpen && $listTag && $listTag !== $this->getListAttribute($pick)) {
$output .= '</'.$listTag.'>'.PHP_EOL;
$isOpen = false;
}
// create the opening OL/UL tag
// opening tag process has been simplified, see https://github.com/nadar/quill-delta-parser/pull/33
// and https://github.com/nadar/quill-delta-parser/issues/30
if (!$isOpen) {
$output .= '<'.$this->getListAttribute($pick).'>'.PHP_EOL;
$isOpen = true;
}
// when the next line has a higher intened, add nested list
$nextIndent = 0;
$pick->line->whileNext(static function (Line $line) use (&$nextIndent) {
$indent = $line->getAttribute('indent', 0);
if ($line->getAttribute(self::ATTRIBUTE_LIST)) {
$nextIndent = $indent;
return false;
}
});
if ($isEmpty) {
$output .= '<li></li>';
} else {
$output .= '<li>';
$output .= $buffer;
if ($nextIndent > $pick->line->getAttribute('indent', 0)) {
$output .= '<'.$this->getListAttribute($pick).'>'.PHP_EOL;
} elseif ($nextIndent < $pick->line->getAttribute('indent', 0)) {
$output .= '</li></'.$this->getListAttribute($pick).'></li>'.PHP_EOL;
$closeGap = $pick->line->getAttribute('indent', 0) - $nextIndent;
if ($closeGap > 1) {
$output .= '</'.$this->getListAttribute($pick).'></li>'.PHP_EOL;
}
} else {
$output.= '</li>'.PHP_EOL;
}
}
// close the opening OL/UL tag if:
// a. its the last element and the tag is opened.
// b. or its the last element in the picked list.
if ($isLast || $pick->isLast()) {
$output .= '</'.$this->getListAttribute($pick).'>'.PHP_EOL;
$isOpen = false;
}
// store the last list type into a variable to determine if type switches
$listTag = $this->getListAttribute($pick);
$pick->line->output = $output;
$pick->line->setDone();
}
}
/**
* Get the html tag for the given value.
*
* @return string
* @throws Exception for unknown list types {@since 1.2.0}
*/
protected function getListAttribute(Pick $pick)
{
$optionValueType = $pick->optionValue('type');
$type = is_array($optionValueType) ? $optionValueType['type'] : $optionValueType;
if ($type === self::LIST_TYPE_ORDERED) {
return 'ol';
}
if ($type === self::LIST_TYPE_BULLET) {
return 'ul';
}
// prevent html injection in case the attribute is user input
throw new Exception('The provided list type "'.$type.'" is not a known list type (ordered or bullet).');
}
}
@@ -0,0 +1,33 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Mention Quill Plugin Listener.
*
* Example mention insert text:
*
* ```json
* {"insert":{"mention":{"id":"1","value":"Basil","denotationChar":"@"}}},{"insert":" \n"}
* ```
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Mention extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$mention = $line->insertJsonKey('mention');
if ($mention) {
$this->updateInput($line, $line->getLexer()->escape($mention['value']));
}
}
}
@@ -0,0 +1,52 @@
<?php
namespace nadar\quill\listener;
use Exception;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Renders script attribute which will generate sup/sub tags.
*
* @author Gaëtan Faugère <gaetan@fauge.re>
* @since 2.2.0
*/
class Script extends InlineListener
{
/**
* @var array<string>
*/
public $scriptTags = ['super', 'sub'];
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if (($script = $line->getAttribute('script'))) {
$this->updateInput($line, $this->applyTemplate($script, $line));
}
}
/**
* Wrap in sup/sub tag.
*
* @param string $script
* @throws Exception for unknown script tag
* @return string
*/
public function applyTemplate($script, Line $line)
{
if (!in_array($script, $this->scriptTags)) {
// prevent html injection and wrong behaviors
throw new Exception('An unknown script tag "' . $script . '" has been detected.');
}
if ($script === 'super') {
$script = 'sup';
}
return '<'.$script.'>'. $line->getInput() . '</'.$script.'>';
}
}
@@ -0,0 +1,25 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Process strike elements
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Strike extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if ($line->getAttribute('strike')) {
$this->updateInput($line, '<del>'.$line->getInput().'</del>');
}
}
}
@@ -0,0 +1,120 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\BlockListener;
use nadar\quill\Lexer;
use nadar\quill\Line;
/**
* Convert all the not done elements into paragraphs.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Text extends BlockListener
{
/**
* @var string
*/
public const CLOSEP = '</p>'.PHP_EOL;
/**
* @var string
*/
public const OPENP = '<p>';
/**
* @var string
*/
public const LINEBREAK = '<br>';
/**
* {@inheritDoc}
*/
public function priority(): int
{
return self::PRIORITY_GARBAGE_COLLECTOR;
}
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if (!$line->isDone()) {
$this->pick($line);
}
}
/**
* {@inheritDoc}
*/
public function render(Lexer $lexer)
{
$isOpen = false;
foreach ($this->picks() as $pick) {
if (!$pick->line->isDone() && !$pick->line->hasAttributes() && !$pick->line->isInline()) {
$pick->line->setDone();
$next = $pick->line->next();
$prev = $pick->line->previous();
$output = [];
// if its close - we just open tag paragraph as we have a line here!
if (!$isOpen) {
$isOpen = $this->output($output, self::OPENP, true);
}
// write the actuall content of the element into the output
$output[] = $pick->line->isEmpty() ? self::LINEBREAK : $pick->line->renderPrepend() . $pick->line->getInput();
// if its open and we have a next element, and the next element is not an inline, we close!
if ($isOpen && ($next && !$next->isInline())) {
$isOpen = $this->output($output, self::CLOSEP, false);
// if its open and we dont have a next element, its the end of the document! lets close this damn paragraph.
} elseif ($isOpen && !$next) {
$isOpen = $this->output($output, self::CLOSEP, false);
// its open, but the previous element was already an inline element, so maybe we should close and the next element
// will take care of the "situation". But only if this current line also had an end new line element, otherwise
// repeated inline elements will close
} elseif ($isOpen && ($prev && $prev->isInline()) && $pick->line->hasEndNewline()) {
$isOpen = $this->output($output, self::CLOSEP, false);
// If this element is empty we should maybe directly close and reopen this paragraph as it could be an empty line with
// a next elmenet
} elseif ($pick->line->isEmpty() && $next && !$next->isDone()) {
$isOpen = $this->output($output, self::CLOSEP.self::OPENP, true);
// if its open, and it had an end newline, lets close
} elseif ($isOpen && $pick->line->hasEndNewline()) {
$isOpen = $this->output($output, self::CLOSEP, false);
}
// we have a next element and the next elmenet is inline and its not open, and the current elemnt is not an endNewline element
if ($next && $next->isInline() && !$isOpen && !$pick->line->hasEndNewline()) {
$isOpen = $this->output($output, self::OPENP, true);
}
$pick->line->output = implode("", $output);
}
}
}
/**
* Helper method simplify output writer.
*
* @param array<string> $output
* @param string $tag
* @param boolean $openState
* @return boolean
*/
protected function output(&$output, $tag, $openState)
{
$output[] = $tag;
return $openState;
}
}
@@ -0,0 +1,25 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\InlineListener;
use nadar\quill\Line;
/**
* Process underline elements
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Underline extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
if ($line->getAttribute('underline')) {
$this->updateInput($line, '<u>'.$line->getInput().'</u>');
}
}
}
@@ -0,0 +1,38 @@
<?php
namespace nadar\quill\listener;
use nadar\quill\BlockListener;
use nadar\quill\Line;
/**
* Convert Video attributes into tags.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Video extends BlockListener
{
/**
* @var array<string> Allow options for iframe allow param
* @since 2.5.0
*/
public $allow = ['accelerometer', 'autoplay', 'encrypted-media', 'gyroscope', 'picture-in-picture'];
/**
* @var string The wrapper template which is taken to generate the video element.
*/
public $wrapper = '<div class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item" src="{url}" frameborder="0" allow="{allow}" allowfullscreen></iframe></div>'.PHP_EOL;
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$embedUrl = $line->insertJsonKey('video');
if ($embedUrl) {
$line->output = str_replace(['{url}', '{allow}'], [$line->getLexer()->escape($embedUrl), implode("; ", $this->allow)], $this->wrapper);
$line->setDone();
}
}
}
@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit3fdcd24ec5e3e581e0fc975610541b71::getLoader();
@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
@@ -0,0 +1,378 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}
@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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,35 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'nadar\\quill\\BlockListener' => $baseDir . '/src/BlockListener.php',
'nadar\\quill\\Debug' => $baseDir . '/src/Debug.php',
'nadar\\quill\\InlineListener' => $baseDir . '/src/InlineListener.php',
'nadar\\quill\\Lexer' => $baseDir . '/src/Lexer.php',
'nadar\\quill\\Line' => $baseDir . '/src/Line.php',
'nadar\\quill\\Listener' => $baseDir . '/src/Listener.php',
'nadar\\quill\\Pick' => $baseDir . '/src/Pick.php',
'nadar\\quill\\listener\\Align' => $baseDir . '/src/listener/Align.php',
'nadar\\quill\\listener\\BackgroundColor' => $baseDir . '/src/listener/BackgroundColor.php',
'nadar\\quill\\listener\\Blockquote' => $baseDir . '/src/listener/Blockquote.php',
'nadar\\quill\\listener\\Bold' => $baseDir . '/src/listener/Bold.php',
'nadar\\quill\\listener\\CodeBlock' => $baseDir . '/src/listener/CodeBlock.php',
'nadar\\quill\\listener\\Color' => $baseDir . '/src/listener/Color.php',
'nadar\\quill\\listener\\Font' => $baseDir . '/src/listener/Font.php',
'nadar\\quill\\listener\\Heading' => $baseDir . '/src/listener/Heading.php',
'nadar\\quill\\listener\\Image' => $baseDir . '/src/listener/Image.php',
'nadar\\quill\\listener\\Italic' => $baseDir . '/src/listener/Italic.php',
'nadar\\quill\\listener\\Link' => $baseDir . '/src/listener/Link.php',
'nadar\\quill\\listener\\Lists' => $baseDir . '/src/listener/Lists.php',
'nadar\\quill\\listener\\Mention' => $baseDir . '/src/listener/Mention.php',
'nadar\\quill\\listener\\Script' => $baseDir . '/src/listener/Script.php',
'nadar\\quill\\listener\\Strike' => $baseDir . '/src/listener/Strike.php',
'nadar\\quill\\listener\\Text' => $baseDir . '/src/listener/Text.php',
'nadar\\quill\\listener\\Underline' => $baseDir . '/src/listener/Underline.php',
'nadar\\quill\\listener\\Video' => $baseDir . '/src/listener/Video.php',
);
@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);
@@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'nadar\\quill\\' => array($baseDir . '/src'),
);
@@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit3fdcd24ec5e3e581e0fc975610541b71
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit3fdcd24ec5e3e581e0fc975610541b71', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit3fdcd24ec5e3e581e0fc975610541b71', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit3fdcd24ec5e3e581e0fc975610541b71::getInitializer($loader));
$loader->register(true);
return $loader;
}
}
@@ -0,0 +1,61 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit3fdcd24ec5e3e581e0fc975610541b71
{
public static $prefixLengthsPsr4 = array (
'n' =>
array (
'nadar\\quill\\' => 12,
),
);
public static $prefixDirsPsr4 = array (
'nadar\\quill\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'nadar\\quill\\BlockListener' => __DIR__ . '/../..' . '/src/BlockListener.php',
'nadar\\quill\\Debug' => __DIR__ . '/../..' . '/src/Debug.php',
'nadar\\quill\\InlineListener' => __DIR__ . '/../..' . '/src/InlineListener.php',
'nadar\\quill\\Lexer' => __DIR__ . '/../..' . '/src/Lexer.php',
'nadar\\quill\\Line' => __DIR__ . '/../..' . '/src/Line.php',
'nadar\\quill\\Listener' => __DIR__ . '/../..' . '/src/Listener.php',
'nadar\\quill\\Pick' => __DIR__ . '/../..' . '/src/Pick.php',
'nadar\\quill\\listener\\Align' => __DIR__ . '/../..' . '/src/listener/Align.php',
'nadar\\quill\\listener\\BackgroundColor' => __DIR__ . '/../..' . '/src/listener/BackgroundColor.php',
'nadar\\quill\\listener\\Blockquote' => __DIR__ . '/../..' . '/src/listener/Blockquote.php',
'nadar\\quill\\listener\\Bold' => __DIR__ . '/../..' . '/src/listener/Bold.php',
'nadar\\quill\\listener\\CodeBlock' => __DIR__ . '/../..' . '/src/listener/CodeBlock.php',
'nadar\\quill\\listener\\Color' => __DIR__ . '/../..' . '/src/listener/Color.php',
'nadar\\quill\\listener\\Font' => __DIR__ . '/../..' . '/src/listener/Font.php',
'nadar\\quill\\listener\\Heading' => __DIR__ . '/../..' . '/src/listener/Heading.php',
'nadar\\quill\\listener\\Image' => __DIR__ . '/../..' . '/src/listener/Image.php',
'nadar\\quill\\listener\\Italic' => __DIR__ . '/../..' . '/src/listener/Italic.php',
'nadar\\quill\\listener\\Link' => __DIR__ . '/../..' . '/src/listener/Link.php',
'nadar\\quill\\listener\\Lists' => __DIR__ . '/../..' . '/src/listener/Lists.php',
'nadar\\quill\\listener\\Mention' => __DIR__ . '/../..' . '/src/listener/Mention.php',
'nadar\\quill\\listener\\Script' => __DIR__ . '/../..' . '/src/listener/Script.php',
'nadar\\quill\\listener\\Strike' => __DIR__ . '/../..' . '/src/listener/Strike.php',
'nadar\\quill\\listener\\Text' => __DIR__ . '/../..' . '/src/listener/Text.php',
'nadar\\quill\\listener\\Underline' => __DIR__ . '/../..' . '/src/listener/Underline.php',
'nadar\\quill\\listener\\Video' => __DIR__ . '/../..' . '/src/listener/Video.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit3fdcd24ec5e3e581e0fc975610541b71::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit3fdcd24ec5e3e581e0fc975610541b71::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit3fdcd24ec5e3e581e0fc975610541b71::$classMap;
}, null, ClassLoader::class);
}
}
@@ -0,0 +1,5 @@
{
"packages": [],
"dev": false,
"dev-package-names": []
}
@@ -0,0 +1,23 @@
<?php return array(
'root' => array(
'name' => 'nadar/quill-delta-parser',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => false,
),
'versions' => array(
'nadar/quill-delta-parser' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}
@@ -0,0 +1,78 @@
<?php
namespace IPS\convert\Tools\Vanilla\QuillListeners;
use IPS\Db;
use nadar\quill\InlineListener;
use nadar\quill\Line;
class EmbedExternal extends InlineListener
{
private $contentType;
private $contentId;
private $commentId;
public function __construct( ?string $contentType=null, ?int $contentId=null, ?int $commentId=null )
{
$this->contentType = $contentType;
$this->contentId = $contentId;
$this->commentId = $commentId;
}
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$embed = $line->insertJsonKey('embed-external');
if( $embed )
{
$return = match( $embed['data']['embedType'] )
{
'quote' => $this->processQuote( $embed ),
'image', 'file', 'imgur', 'giphy' => $this->processMedia( $embed ),
'youtube', 'twitter', 'instagram', 'twitch', 'vimeo' => $this->processOembed( $embed ),
'link' => $this->processLink( $embed ),
default => $this->processMedia( $embed )
};
if ( $return !== NULL ) {
$this->updateInput( $line, $return );
}
}
}
public function processLink( array $data ):? string
{
return ' ' . $data['data']['url'] . " \n";
}
public function processMedia( array $data ):? string
{
if( isset( $data['data']['mediaID'] ) )
{
// dump($this->contentType, $this->commentId);
if( $this->contentType AND $this->commentId )
{
Db::i()->insert( 'convert_vanilla_temp', [ 'media_id' => $data['data']['mediaID'], 'link_type' => $this->contentType, 'content_id' => $this->contentId, 'post_id' => $this->commentId ] );
}
return "[ATTACH={$data['data']['mediaID']}] <br>\n";
}
elseif( !empty( $data['data']['url'] ) )
{
return "[img]{$data['data']['url']}[/img] <br>\n";
}
return null;
}
public function processOembed( array $data ):? string
{
return $data['data']['url'] . " \n";
}
public function processQuote( array $data ):? string
{
return '[quote name="' . $data['data']['insertUser']['name'] . '" timestamp="' . strtotime( $data['data']['dateInserted'] ) . '"]' . $data['data']['body'] . "[/quote]";
}
}
@@ -0,0 +1,20 @@
<?php
namespace IPS\convert\Tools\Vanilla\QuillListeners;
use nadar\quill\InlineListener;
use nadar\quill\Line;
class Emoji extends InlineListener
{
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$emoji = $line->insertJsonKey('emoji');
if ( !empty( $emoji['emojiChar'] ) ) {
$this->updateInput( $line, $emoji['emojiChar'] );
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace IPS\convert\Tools\Vanilla\QuillListeners;
use IPS\convert\App;
use nadar\quill\InlineListener;
use nadar\quill\Line;
class Mention extends InlineListener
{
private $app;
public function __construct( ?App $app=null )
{
$this->app = $app;
}
/**
* {@inheritDoc}
*/
public function process(Line $line)
{
$mention = $line->insertJsonKey('mention');
if ( $this->app AND !empty( $mention['userID'] ) ) {
$ipsId = $this->app->getLink( $mention['userID'], 'core_members', true );
$return = "[mention={$ipsId}]{$mention['name']}[/mention]";
$this->updateInput( $line, $return );
}
elseif ( $mention )
{
$this->updateInput( $line, '@' . $mention['name'] );
}
}
}