Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/IsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Tempest\Markdown;

/**
* The default token support of a rule. Implementations declare the context
* they are written for, and the tokens that deviate from it.
*/
trait IsRule
{
/** @var \Tempest\Markdown\RuleContext[] */
private(set) array $contexts = [RuleContext::BLOCK];

/** @var array<class-string<\Tempest\Markdown\Token>, bool> */
private(set) array $tokenSupport = [];

public function supportsToken(Token $token): bool
{
return (
$this->tokenSupport[$token::class] ?? in_array(
RuleContext::INLINE,
$this->contexts,
strict: true,
)
);
}

/** @param class-string<\Tempest\Markdown\Token> $token */
public function addTokenSupport(string $token): static
{
$this->tokenSupport[$token] = true;

return $this;
}

/** @param class-string<\Tempest\Markdown\Token> $token */
public function removeTokenSupport(string $token): static
{
$this->tokenSupport[$token] = false;

return $this;
}
}
11 changes: 11 additions & 0 deletions src/Markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,15 @@ public function removeRules(string ...$rules): self

return $this;
}

/**
* The registered instance of $rule, to reconfigure it or to change the
* tokens it supports.
*
* @param class-string<\Tempest\Markdown\Rule> $rule
*/
public function getRule(string $rule): ?Rule
{
return $this->parser->getRule($rule);
}
}
91 changes: 78 additions & 13 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@

use Tempest\Highlight\Highlighter;
use Tempest\Markdown\Exceptions\MaximumNestingDepthWasExceeded;
use Tempest\Markdown\Rules\BoldAndItalicRule;
use Tempest\Markdown\Rules\BoldRule;
use Tempest\Markdown\Rules\CodeRule;
use Tempest\Markdown\Rules\DivRule;
use Tempest\Markdown\Rules\FrontMatterRule;
use Tempest\Markdown\Rules\HeadingRule;
use Tempest\Markdown\Rules\HtmlCommentRule;
use Tempest\Markdown\Rules\HtmlRule;
use Tempest\Markdown\Rules\ImageRule;
use Tempest\Markdown\Rules\ItalicRule;
use Tempest\Markdown\Rules\LinkRule;
use Tempest\Markdown\Rules\ListRule;
use Tempest\Markdown\Rules\NewLineRule;
use Tempest\Markdown\Rules\OrderedListRule;
use Tempest\Markdown\Rules\ParagraphRule;
use Tempest\Markdown\Rules\PreRule;
use Tempest\Markdown\Rules\QuoteRule;
use Tempest\Markdown\Rules\RawRule;
use Tempest\Markdown\Rules\SocialHandleRule;
use Tempest\Markdown\Rules\StrikethroughRule;
use Tempest\Markdown\Rules\TableRule;
use Tempest\Markdown\Rules\TextRule;
use Tempest\Markdown\Rules\ThickRulerRule;
use Tempest\Markdown\Rules\ThinRulerRule;
use Tempest\Markdown\Tokens\FrontMatterToken;
Expand All @@ -33,6 +42,7 @@ final class Parser
private(set) ?string $current;
private(set) string $content;
private(set) ?Token $lastToken = null;
/** Every registered rule, whether or not it runs here. */
/** @var \Tempest\Markdown\Rule[] */
private(set) array $rules = [];
/** @var \Tempest\Markdown\Rule[] */
Expand All @@ -45,6 +55,7 @@ final class Parser

private static int $depth = 0;

/** @param \Tempest\Markdown\Rule[] $rules */
public function __construct(
public ?Highlighter $highlighter = new Highlighter(),
public ?ResponsiveImageFactory $imageFactory = null,
Expand All @@ -65,26 +76,64 @@ public function __construct(
new HtmlRule(),
new TableRule(),
new ParagraphRule(),
new BoldAndItalicRule(),
new BoldRule(),
new ItalicRule(),
new StrikethroughRule(),
new CodeRule(),
new LinkRule(),
new SocialHandleRule(),
new ImageRule(),
new TextRule(),
],
) {
$this->setRules($rules);
}

public function forToken(Token $token, array $rules): self
/**
* A parser for the content of $token, running every registered rule that
* supports it. Rules are never rebuilt here: they are the configured
* instances, so adding, removing or reconfiguring a rule reaches nested
* content too.
*/
public function forToken(Token $token): self
{
if (isset($this->cache[$token::class])) {
return $this->cache[$token::class];
}

$clone = clone $this;

$clone->setRules($rules);
$clone->activateRules(array_values(array_filter(
$this->rules,
static fn (Rule $rule) => $rule->supportsToken($token),
)));

$this->cache[$token::class] = $clone;

return $clone;
}

/**
* The registered instance of $rule, to reconfigure it or to change the
* tokens it supports. Sub-parsers are derived from the rules, so handing
* one out drops the ones already built from it.
*
* @param class-string<\Tempest\Markdown\Rule> $rule
*/
public function getRule(string $rule): ?Rule
{
$this->cache = [];

foreach ($this->rules as $registered) {
if ($registered::class === $rule) {
return $registered;
}
}

return null;
}

public function withRules(Rule ...$rules): self
{
$clone = clone $this;
Expand Down Expand Up @@ -125,24 +174,41 @@ public function removeRules(string ...$rules): self
return $clone;
}

/** @param \Tempest\Markdown\Rule[] $rules */
private function setRules(array $rules): self
{
/** @var \Tempest\Markdown\NeedsStopChars[] $needsStopChars */
$needsStopChars = [];
$this->rules = $rules;

return $this->activateRules($rules);
}

/** @param \Tempest\Markdown\Rule[] $rules */
private function activateRules(array $rules): self
{
$providedStopChars = '';

foreach ($rules as $rule) {
if ($rule instanceof ProvidesStopChar) {
$providedStopChars .= $rule->stopChar;
if (! $rule instanceof ProvidesStopChar) {
continue;
}

if ($rule instanceof NeedsStopChars) {
$needsStopChars[] = $rule;
}
$providedStopChars .= $rule->stopChar;
}

foreach ($needsStopChars as $rule) {
$rule->stopChars .= $providedStopChars;
foreach ($rules as $key => $rule) {
if (! $rule instanceof NeedsStopChars) {
continue;
}

if ($rule->stopChars === $providedStopChars) {
continue;
}

// Stop chars belong to a rule set, not to a rule: the registered
// instance is shared between sets, so this set gets its own copy
// instead of overwriting the stop chars of the previous one.
$rules[$key] = clone $rule;
$rules[$key]->stopChars = $providedStopChars;
}

$perCharRules = [];
Expand All @@ -167,12 +233,11 @@ private function setRules(array $rules): self
}
}

/** @var \Tempest\Markdown\Rule[] $rules */
/** @var \Tempest\Markdown\Rule[] $defaultRules */
/** @var \Tempest\Markdown\Rule[][] $perCharRules */
$this->rules = $rules;
$this->defaultRules = $defaultRules;
$this->perCharRules = $perCharRules;
$this->cache = [];

return $this;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,13 @@ interface Rule
public function shouldParse(Parser $parser): bool;

public function parse(Parser $parser): ?Token;

/** Whether this rule runs on the content of $token. */
public function supportsToken(Token $token): bool;

/** @param class-string<\Tempest\Markdown\Token> $token */
public function addTokenSupport(string $token): static;

/** @param class-string<\Tempest\Markdown\Token> $token */
public function removeTokenSupport(string $token): static;
}
17 changes: 17 additions & 0 deletions src/RuleContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Tempest\Markdown;

/**
* The content a rule is written for. It decides which tokens a rule supports
* by default; {@see \Tempest\Markdown\Rule::addTokenSupport()} and
* {@see \Tempest\Markdown\Rule::removeTokenSupport()} override that per token.
*/
enum RuleContext
{
/** Structure: the rule only runs on the document, and on the containers that opt in. */
case BLOCK;

/** A run of text: the rule runs inside every token that holds inline content. */
case INLINE;
}
15 changes: 15 additions & 0 deletions src/Rules/BoldAndItalicRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,36 @@

namespace Tempest\Markdown\Rules;

use Tempest\Markdown\IsRule;
use Tempest\Markdown\Parser;
use Tempest\Markdown\ProvidesFirstChar;
use Tempest\Markdown\ProvidesStopChar;
use Tempest\Markdown\Rule;
use Tempest\Markdown\RuleContext;
use Tempest\Markdown\Token;
use Tempest\Markdown\Tokens\BoldAndItalicToken;
use Tempest\Markdown\Tokens\BoldToken;
use Tempest\Markdown\Tokens\ItalicToken;

final class BoldAndItalicRule implements
Rule,
ProvidesFirstChar,
ProvidesStopChar
{
use IsRule;

private(set) string $firstChar = '*_';
private(set) string $stopChar = '_*';

public function __construct()
{
$this->contexts = [RuleContext::INLINE];

$this->removeTokenSupport(BoldToken::class);
$this->removeTokenSupport(ItalicToken::class);
$this->removeTokenSupport(BoldAndItalicToken::class);
}

public function shouldParse(Parser $parser): bool
{
return (
Expand Down
13 changes: 13 additions & 0 deletions src/Rules/BoldRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@

namespace Tempest\Markdown\Rules;

use Tempest\Markdown\IsRule;
use Tempest\Markdown\Parser;
use Tempest\Markdown\ProvidesFirstChar;
use Tempest\Markdown\ProvidesStopChar;
use Tempest\Markdown\Rule;
use Tempest\Markdown\RuleContext;
use Tempest\Markdown\Token;
use Tempest\Markdown\Tokens\BoldAndItalicToken;
use Tempest\Markdown\Tokens\BoldToken;

final class BoldRule implements Rule, ProvidesFirstChar, ProvidesStopChar
{
use IsRule;

private(set) string $firstChar = '*_';
private(set) string $stopChar = '*_';

public function __construct()
{
$this->contexts = [RuleContext::INLINE];

$this->removeTokenSupport(BoldToken::class);
$this->removeTokenSupport(BoldAndItalicToken::class);
}

public function shouldParse(Parser $parser): bool
{
$stopToken = $parser->lookaheadUntil('*_')[0] ?? null;
Expand Down
9 changes: 9 additions & 0 deletions src/Rules/CodeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@

namespace Tempest\Markdown\Rules;

use Tempest\Markdown\IsRule;
use Tempest\Markdown\Parser;
use Tempest\Markdown\ProvidesFirstChar;
use Tempest\Markdown\ProvidesStopChar;
use Tempest\Markdown\Rule;
use Tempest\Markdown\RuleContext;
use Tempest\Markdown\Token;
use Tempest\Markdown\Tokens\CodeToken;

final class CodeRule implements Rule, ProvidesFirstChar, ProvidesStopChar
{
use IsRule;

private(set) string $firstChar = '`';
private(set) string $stopChar = '`';

public function __construct()
{
$this->contexts = [RuleContext::INLINE];
}

public function shouldParse(Parser $parser): bool
{
return $parser->comesNext('`', 1);
Expand Down
9 changes: 9 additions & 0 deletions src/Rules/DivRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@

namespace Tempest\Markdown\Rules;

use Tempest\Markdown\IsRule;
use Tempest\Markdown\Parser;
use Tempest\Markdown\ProvidesFirstChar;
use Tempest\Markdown\ProvidesStopChar;
use Tempest\Markdown\Rule;
use Tempest\Markdown\Token;
use Tempest\Markdown\Tokens\DivToken;
use Tempest\Markdown\Tokens\ParagraphToken;

final class DivRule implements Rule, ProvidesFirstChar, ProvidesStopChar
{
use IsRule;

public string $stopChar = ':';
public string $firstChar = ':';

public function __construct()
{
$this->addTokenSupport(ParagraphToken::class);
}

public function shouldParse(Parser $parser): bool
{
return $parser->comesNext(':::', 3);
Expand Down
Loading
Loading