src/BlockListener.php
<?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;
}
}