1
0
mirror of https://github.com/erusev/parsedown.git synced 2023-08-10 21:13:06 +03:00
parsedown/src/Parsedown.php

248 lines
6.7 KiB
PHP

<?php
namespace Erusev\Parsedown;
use Erusev\Parsedown\AST\StateRenderable;
use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Components\Blocks\Paragraph;
use Erusev\Parsedown\Components\ContinuableBlock;
use Erusev\Parsedown\Components\Inline;
use Erusev\Parsedown\Components\Inlines\PlainText;
use Erusev\Parsedown\Components\StateUpdatingBlock;
use Erusev\Parsedown\Configurables\BlockTypes;
use Erusev\Parsedown\Configurables\InlineTypes;
use Erusev\Parsedown\Html\Renderable;
use Erusev\Parsedown\Html\Renderables\Text;
use Erusev\Parsedown\Parsing\Context;
use Erusev\Parsedown\Parsing\Excerpt;
use Erusev\Parsedown\Parsing\Line;
use Erusev\Parsedown\Parsing\Lines;
final class Parsedown
{
const version = '2.0.0-dev';
/** @var State */
private $State;
public function __construct(State $State = null)
{
$this->State = $State ?: new State;
}
/**
* @param string $text
* @return string
*/
public function text($text)
{
list($StateRenderables, $State) = self::lines(
Lines::fromTextLines($text, 0),
$this->State
);
$Renderables = $State->applyTo($StateRenderables);
$html = self::render($Renderables);
# trim line breaks
$html = \trim($html, "\n");
return $html;
}
/**
* @return array{0: StateRenderable[], 1: State}
*/
public static function lines(Lines $Lines, State $State)
{
list($Blocks, $State) = self::blocks($Lines, $State);
$StateRenderables = \array_map(
/** @return StateRenderable */
function (Block $Block) { return $Block->stateRenderable(); },
$Blocks
);
return [$StateRenderables, $State];
}
/**
* @return array{0: Block[], 1: State}
*/
public static function blocks(Lines $Lines, State $State)
{
/** @var Block[] */
$Blocks = [];
/** @var Block|null */
$Block = null;
/** @var Block|null */
$CurrentBlock = null;
foreach ($Lines->contexts() as $Context) {
$Line = $Context->line();
if (
isset($CurrentBlock)
&& $CurrentBlock instanceof ContinuableBlock
&& ! $CurrentBlock instanceof Paragraph
) {
$Block = $CurrentBlock->advance($Context);
if (isset($Block)) {
$CurrentBlock = $Block;
continue;
}
}
$marker = $Line->text()[0];
$potentialBlockTypes = \array_merge(
$State->get(BlockTypes::class)->unmarked(),
$State->get(BlockTypes::class)->markedBy($marker)
);
foreach ($potentialBlockTypes as $blockType) {
$Block = $blockType::build($Context, $CurrentBlock, $State);
if (isset($Block)) {
if ($Block instanceof StateUpdatingBlock) {
$State = $Block->latestState();
}
if (isset($CurrentBlock) && ! $Block->acquiredPrevious()) {
$Blocks[] = $CurrentBlock;
}
$CurrentBlock = $Block;
continue 2;
}
}
if (isset($CurrentBlock) && $CurrentBlock instanceof Paragraph) {
$Block = $CurrentBlock->advance($Context);
}
if (isset($Block)) {
$CurrentBlock = $Block;
} else {
if (isset($CurrentBlock)) {
$Blocks[] = $CurrentBlock;
}
$CurrentBlock = Paragraph::build($Context);
}
}
if (isset($CurrentBlock)) {
$Blocks[] = $CurrentBlock;
}
return [$Blocks, $State];
}
/**
* @param string $text
* @return StateRenderable[]
*/
public static function line($text, State $State)
{
return \array_map(
/** @return StateRenderable */
function (Inline $Inline) { return $Inline->stateRenderable(); },
self::inlines($text, $State)
);
}
/**
* @param string $text
* @return Inline[]
*/
public static function inlines($text, State $State)
{
# standardize line breaks
$text = \str_replace(["\r\n", "\r"], "\n", $text);
/** @var Inline[] */
$Inlines = [];
# $excerpt is based on the first occurrence of a marker
$InlineTypes = $State->get(InlineTypes::class);
$markerMask = $InlineTypes->markers();
for (
$Excerpt = (new Excerpt($text, 0))->pushingOffsetTo($markerMask);
$Excerpt->text() !== '';
$Excerpt = $Excerpt->pushingOffsetTo($markerMask)
) {
$marker = \substr($Excerpt->text(), 0, 1);
foreach ($InlineTypes->markedBy($marker) as $inlineType) {
# check to see if the current inline type is nestable in the current context
$Inline = $inlineType::build($Excerpt, $State);
if (! isset($Inline)) {
continue;
}
$startPosition = $Inline->modifyStartPositionTo();
if (! isset($startPosition)) {
$startPosition = $Excerpt->offset();
}
# makes sure that the inline belongs to "our" marker
if ($startPosition > $Excerpt->offset()) {
continue;
}
# the text that comes before the inline
# compile the unmarked text
$Inlines[] = Plaintext::build($Excerpt->choppingUpToOffset($startPosition));
# compile the inline
$Inlines[] = $Inline;
# remove the examined text
/** @psalm-suppress LoopInvalidation */
$Excerpt = $Excerpt->choppingFromOffset($startPosition + $Inline->width());
continue 2;
}
/** @psalm-suppress LoopInvalidation */
$Excerpt = $Excerpt->addingToOffset(1);
}
$Inlines[] = Plaintext::build($Excerpt->choppingFromOffset(0));
return $Inlines;
}
/**
* @param Renderable[] $Renderables
* @return string
*/
public static function render(array $Renderables)
{
return \array_reduce(
$Renderables,
/**
* @param string $html
* @return string
*/
function ($html, Renderable $Renderable) {
$newHtml = $Renderable->getHtml();
return $html . ($newHtml === '' ? '' : "\n") . $newHtml;
},
''
);
}
}