From 8e8d5f7974b417a4957c6e7fd36001d429073df3 Mon Sep 17 00:00:00 2001 From: Adrien Furnari Date: Thu, 12 Feb 2026 11:24:25 +0100 Subject: [PATCH] feat: enhanced the tests --- test-server.php | 4 +- tests/index.php | 33 ++++- tests/targets/html.md | 294 +++++++++++++++++++++++++++++++++++++++ tests/targets/php.md | 312 ++++++++++++++++++++++++++++++++++++++++++ tests/targets/test.md | 3 - tests/targets/xml.md | 177 +++++++++++++++++++++++- 6 files changed, 811 insertions(+), 12 deletions(-) create mode 100644 tests/targets/html.md create mode 100644 tests/targets/php.md delete mode 100644 tests/targets/test.md diff --git a/test-server.php b/test-server.php index dfa6ee0..7e6f11a 100644 --- a/test-server.php +++ b/test-server.php @@ -2,4 +2,6 @@ require_once __DIR__ . '/vendor/autoload.php'; -passthru("php -S localhost:8000 -t tests/"); \ No newline at end of file +$port = intval($argv[1] ?? 8000); + +passthru("php -S localhost:$port -t tests/"); diff --git a/tests/index.php b/tests/index.php index 5f2fede..e4d391d 100644 --- a/tests/index.php +++ b/tests/index.php @@ -12,6 +12,8 @@ use Tempest\Highlight\Highlighter; use Tempest\Highlight\Themes\CssTheme; +$tests = glob("./targets/*.md"); + $environment = new Environment(); $highlighter = (new Highlighter(new CssTheme())); @@ -24,7 +26,7 @@ $markdown = new MarkdownConverter($environment); -$target = 'targets' . DIRECTORY_SEPARATOR . 'test.md'; +$target = 'targets' . DIRECTORY_SEPARATOR . 'php.md'; if (isset($_GET['target'])) { $target = $_GET['target']; @@ -46,7 +48,7 @@ ?> - + Test + + + + + +
+
+
+
+
+ photo + + + + + + + + + + + + +

Paragraph one +

Paragraph two — the first <p> auto-closes +

Third with inline that doesn't break

+ + + +
+
Term +
Definition +
Another +
Def 2 +
+ + +
AB +
CD +
+ + + + + + + + + + + +

& < > " ' © © © ©

+

Bare & ampersand, and 3 < 5 > 2 technically invalid but tolerated

+

&nonexistent; entity — parser should handle gracefully

+

Numeric edge: � � 󴈿

+ + + +
JSON in single-quoted attr
+
JSON in double-quoted with entities
+
Attr soup
+
Newline in attribute value
+
Nested quotes
+ +
Case insensitive attrs
+ + + + + + + + + + + + + + +
+

HTML inside SVG foreignObject

+
+
+
+ Text + + Link + + + + Bonjour + Hello + + + +
+ + + + + + + i=0 + + + + xi + i! + + = + ex + + \\sum_{i=0}^{\\infty} \\frac{x^i}{i!} = e^x + + + +
+ HTML context + + + + +
+ + + x + +
+
+
+ Back to HTML +
+ + + + + + + Slotted + + + + + +
8 levels
+ + +

Extra spaces in attributes

+
Multiline attribute formatting
+ + +
Uppercase tag
+
Mixed case tag
+

h1

h2

h3

h4

h5
h6
+ + + + + + + + + + +

Is 5<10? And what about bold after?

+

Bare angle < and > in text

+ + +
+ Open by default +
+ Nested details +

Deeply nested

+
+
+ + +
+ +
+
+ +
Popover
+ + + +
+ +

+
+
+ + +
Obsolete center
+ Obsolete font + Obsolete marquee + Obsolete blink + Obsolete <b>xmp</b> — raw text + + +

+
+ + + + + + + + ", + "description": "Not bold" + } + + + + + +``` \ No newline at end of file diff --git a/tests/targets/php.md b/tests/targets/php.md new file mode 100644 index 0000000..d33345e --- /dev/null +++ b/tests/targets/php.md @@ -0,0 +1,312 @@ +```php + $v !== '', + is_int($v) => $v >= 0, + default => false + }; +} + +interface Renderable extends \Stringable +{ + public function render(): string; +} + +interface Cacheable +{ + public function cacheKey(): string; +} + +trait Timestamped +{ + public \DateTimeImmutable $createdAt { + get => $this->createdAt; + } + + public function touch(): void + { + $this->createdAt = new \DateTimeImmutable(); + } +} + +abstract class Entity implements Renderable, \JsonSerializable +{ + use Timestamped; + + private static int $count = 0; + + public function __construct(public readonly int $id, protected string $name) + { + self::$count++; + $this->createdAt = new \DateTimeImmutable(); + } + + abstract public function toArray(): array; + + public function jsonSerialize(): mixed + { + return $this->toArray(); + } + + public function __toString(): string + { + return $this->render(); + } + + public function __debugInfo(): array + { + return ['id' => $this->id]; + } + + public static function total(): int + { + return self::$count; + } +} + +readonly class Money +{ + public function __construct(public int $amount, public string $currency = 'EUR') + { + } + + public function withAmount(int $a): self + { + return clone($this, ['amount' => $a]); + } + + public function add(self $o): self + { + return clone($this, ['amount' => $this->amount + $o->amount]); + } +} + +class User extends Entity implements Cacheable +{ + public string $displayName { + get => "{$this->name} <{$this->email}>"; + set(string $v) => $this->name = $v; + } + public private(set) Status $status = Status::Active; + + public function __construct(int $id, string $name, public readonly string $email, private(set) Color $color = Color::Blue) + { + parent::__construct($id, $name); + } + + #[Route('/users/{id}')] + public function render(): string + { + return "id}\">{$this->displayName}"; + } + + #[\Override] + public function toArray(): array + { + return ['id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'status' => $this->status->name, 'color' => $this->color->value]; + } + + #[\Override] + public function cacheKey(): string + { + return "u:{$this->id}"; + } + + public function suspend(): void + { + $this->status = Status::Suspended; + } + + public function __serialize(): array + { + return $this->toArray(); + } + + public function __unserialize(array $d): void + { + } +} + +readonly class Config +{ + public function __construct(public string $dsn, public bool $debug = false, public array $opts = []) + { + } + + public function withDebug(): self + { + return clone($this, ['debug' => true]); + } +} + +function process(Renderable&Cacheable $e): string +{ + return "{$e->cacheKey()}:{$e->render()}"; +} + +function fmt(string|int|float|\Stringable|null $v): string +{ + return match (true) { + $v === null => '∅', + is_string($v) => "\"{$v}\"", + default => (string)$v + }; +} + +function either((Renderable&Cacheable)|string $x): string +{ + return is_string($x) ? $x : process($x); +} + +function fib(): Generator +{ + [$a, $b] = [0, 1]; + while (true) { + yield $a; + [$a, $b] = [$b, $a + $b]; + } +} + +function take(Generator $g, int $n): array +{ + $r = []; + for ($i = 0; $i < $n && $g->valid(); $i++) { + $r[] = $g->current(); + $g->next(); + } + return $r; +} + +function slug(string $s): string +{ + return $s + |> trim(...) + |> mb_strtolower(...) + |> (fn($s) => preg_replace('/\s+/', '-', $s)) + |> (fn($s) => preg_replace('/[^a-z0-9\-]/', '', $s)); +} + +function uri(string $u): array +{ + $p = new Uri($u); + return ['host' => $p->getHost(), 'path' => $p->getPath()]; +} + +#[\Attribute(\Attribute::TARGET_METHOD)] +final class Guard +{ + public function __construct(public readonly Closure $fn) + { + } +} + +final class Api +{ + #[Guard(static fn(object $r): bool => $r->admin ?? false)] + public function delete(): void + { + } +} + +function task(string $l): Fiber +{ + return new Fiber(fn(string $in) => Fiber::suspend("{$l}:{$in}")); +} + +(static function (): void { + $ext = [...['a' => 1, 'b' => 2], 'c' => 3]; + ['a' => $a] = $ext; + $x = null; + $v = $x?->foo ?? 'nope'; + $d = []; + $d['k'] ??= 'def'; + $r = match ($a) { + 1 => 'one', + default => 'other' + }; + + $mapped = array_map(strtoupper(...), ['a', 'b']); + $sliced = array_slice(array: [5, 3, 1], offset: 0, length: 2); + $dbl = fn(int $n): int => $n * 2; + $add = fn(int $a) => fn(int $b): int => $a + $b; + $fact = static function (int $n): int { + return $n <= 1 ? 1 : $n * (Closure::getCurrent())($n - 1); + }; + + $wm = new WeakMap(); + $o = new \stdClass(); + $wm[$o] = true; + $f = task('t'); + $f->start('in'); + $fibs = take(fib(), 8); + + $first = array_first($fibs); + $last = array_last($fibs); + $s = slug(' Hello World! '); + $m = (new Money(100))->withAmount(50)->add(new Money(25)); + (void)validate('ok'); + $parts = uri('https://example.com/path?q=1'); + + $u = new User(1, 'Alice', 'a@b.com', Color::Red); + echo process($u), PHP_EOL; + echo json_encode($u, JSON_THROW_ON_ERROR), PHP_EOL; + + $html = <<{$u->displayName} + HTML; + $raw = <<<'RAW' + No $interpolation {$here}. + RAW; + + try { + throw new \RuntimeException('x'); + } catch (\RuntimeException|\LogicException $e) { + $_ = $e->getMessage(); + } finally { + } + + $dev = (new Config(dsn: 'sqlite::memory:', opts: ['a' => 1]))->withDebug(); + echo "slug:{$s} fibs:", implode(',', $fibs), " fact:", $fact(6), " first:{$first} last:{$last}\n"; +})(); +``` \ No newline at end of file diff --git a/tests/targets/test.md b/tests/targets/test.md deleted file mode 100644 index 70dfac2..0000000 --- a/tests/targets/test.md +++ /dev/null @@ -1,3 +0,0 @@ -```php -(string) $bar;(int) $bar;(integer) $bar;(void) $bar;(bool) $bar;(boolean) $bar;(float) $bar;(double) $bar;(real) $bar;(binary) $bar;(array) $bar;;(object) $bar;(unset) $bar; -``` \ No newline at end of file diff --git a/tests/targets/xml.md b/tests/targets/xml.md index ea73fec..11ac16d 100644 --- a/tests/targets/xml.md +++ b/tests/targets/xml.md @@ -1,7 +1,172 @@ ```xml - - - What's the answer? - - -``` + + + + + + + + + + + + + "> + %shared; + + + text"> + + +]> + + + + + + + + + + + + + + + + + Back to no namespace + Back to original + + + + + Prefix 'a' now means something else + + + + & "quotes" & 'apostrophes' that aren't parsed]]> + + + + + d) return ""; } + Multiline CDATA with &possible; "gotcha" + ]]> + + + + + < > & " ' + © © © © + + + © &recursive; + &xml-in-entity; + + + + spaces and + newlines + tabs preserved + normalized whitespace + Attributes split across lines with varied spacing + + + + + + + + + Single-quoted with double inside + Entities in attribute values + Newline entity in attribute + Empty attribute value + + + Text more text bold tail text + + + + 5 deep + + + + + + + + + + beforeafter + + + + + + + + + <_underscore-start xmlns:_ns="http://example.com/us" _ns:_attr="val">Valid name starting with underscore + <élément>Unicode element name (valid in XML 1.1 / XML 1.0 5th ed) + + + Prefixed element with prefixed attribute: + + + Long content: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + + + + + + + texttexttext + + + <?xml version="1.0"?> not a real prolog + + + The sequence ]] > in text (split to be valid) + + + + Has namespace + + + + © Content from DTD-defined element & entity + + + + +``` \ No newline at end of file