Skip to content

Commit 12ea1aa

Browse files
committed
feat: support html, icon and custom markup
1 parent b7ff1c6 commit 12ea1aa

18 files changed

+437
-49
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"phpat/phpat": "^0.11.0",
6161
"phpbench/phpbench": "84.x-dev",
6262
"phpstan/phpstan": "^2.0",
63-
"phpunit/phpunit": "^11.5.17",
63+
"phpunit/phpunit": "^11.5.24",
6464
"rector/rector": "^2.0-rc2",
6565
"spatie/phpunit-snapshot-assertions": "^5.1.8",
6666
"spaze/phpstan-disallowed-calls": "^4.0",

packages/intl/src/GenericTranslator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
final readonly class GenericTranslator implements Translator
1212
{
1313
public function __construct(
14-
private IntlConfig $config,
14+
private(set) IntlConfig $config,
1515
private(set) Catalog $catalog,
1616
private MessageFormatter $formatter,
1717
private ?EventBus $eventBus = null,

packages/intl/src/IntlConfig.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
use Tempest\Intl\Locale;
66
use Tempest\Intl\MessageFormat\FormattingFunction;
7+
use Tempest\Intl\MessageFormat\MarkupFormatter;
78
use Tempest\Intl\MessageFormat\SelectorFunction;
9+
use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter;
810

911
final class IntlConfig
1012
{
1113
/** @var FormattingFunction[] */
1214
public array $functions = [];
1315

16+
/** @var array<MarkupFormatter|StandaloneMarkupFormatter> */
17+
public array $markupFormatters = [];
18+
1419
/** @var array<string,string[]> */
1520
public array $translationMessagePaths = [];
1621

@@ -31,6 +36,11 @@ public function addFunction(FormattingFunction|SelectorFunction $fn): void
3136
$this->functions[] = $fn;
3237
}
3338

39+
public function addMarkupFormatter(MarkupFormatter|StandaloneMarkupFormatter $formatter): void
40+
{
41+
$this->markupFormatters[] = $formatter;
42+
}
43+
3444
public function addTranslationMessageFile(Locale $locale, string $path): void
3545
{
3646
$this->translationMessagePaths[$locale->value] ??= [];

packages/intl/src/MessageFormat/Formatter/LocalVariable.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
use Tempest\Intl\MessageFormat\SelectorFunction;
66

7-
final class LocalVariable
7+
final readonly class LocalVariable
88
{
99
public function __construct(
10-
public readonly string $identifier,
11-
public readonly mixed $value,
12-
public readonly ?SelectorFunction $function = null,
13-
public readonly array $parameters = [],
10+
public string $identifier,
11+
public mixed $value,
12+
public ?SelectorFunction $function = null,
13+
public array $parameters = [],
1414
) {}
1515
}

packages/intl/src/MessageFormat/Formatter/MessageFormatter.php

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tempest\Intl\MessageFormat\Formatter;
44

55
use Tempest\Intl\MessageFormat\FormattingFunction;
6+
use Tempest\Intl\MessageFormat\MarkupFormatter;
67
use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\ComplexBody;
78
use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\Matcher;
89
use Tempest\Intl\MessageFormat\Parser\Node\ComplexBody\SimplePatternBody;
@@ -13,6 +14,7 @@
1314
use Tempest\Intl\MessageFormat\Parser\Node\Expression\FunctionCall;
1415
use Tempest\Intl\MessageFormat\Parser\Node\Expression\FunctionExpression;
1516
use Tempest\Intl\MessageFormat\Parser\Node\Expression\LiteralExpression;
17+
use Tempest\Intl\MessageFormat\Parser\Node\Expression\Option;
1618
use Tempest\Intl\MessageFormat\Parser\Node\Expression\VariableExpression;
1719
use Tempest\Intl\MessageFormat\Parser\Node\Key\WildcardKey;
1820
use Tempest\Intl\MessageFormat\Parser\Node\Literal\Literal;
@@ -28,6 +30,8 @@
2830
use Tempest\Intl\MessageFormat\Parser\Node\Variable;
2931
use Tempest\Intl\MessageFormat\Parser\Parser;
3032
use Tempest\Intl\MessageFormat\SelectorFunction;
33+
use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter;
34+
use Tempest\Support\Arr;
3135

3236
use function Tempest\Support\arr;
3337

@@ -37,8 +41,10 @@ final class MessageFormatter
3741
private array $variables = [];
3842

3943
public function __construct(
40-
/** @var FormattingFunction[] */
44+
/** @var array<FormattingFunction|SelectorFunction> */
4145
private readonly array $functions = [],
46+
/** @var array<MarkupFormatter|StandaloneMarkupFormatter> */
47+
private readonly array $markupFormatters = [],
4248
) {}
4349

4450
/**
@@ -273,28 +279,6 @@ private function evaluateExpression(Expression $expression): FormattedValue
273279
return new FormattedValue($value, $formatted);
274280
}
275281

276-
private function getSelectorFunction(?string $name): ?SelectorFunction
277-
{
278-
if (! $name) {
279-
return null;
280-
}
281-
282-
return arr($this->functions)
283-
->filter(fn (FormattingFunction|SelectorFunction $fn) => $fn instanceof SelectorFunction)
284-
->first(fn (SelectorFunction $fn) => $fn->name === $name);
285-
}
286-
287-
private function getFormattingFunction(?string $name): ?FormattingFunction
288-
{
289-
if (! $name) {
290-
return null;
291-
}
292-
293-
return arr($this->functions)
294-
->filter(fn (FormattingFunction|SelectorFunction $fn) => $fn instanceof FormattingFunction)
295-
->first(fn (FormattingFunction $fn) => $fn->name === $name);
296-
}
297-
298282
private function evaluateOptions(array $options): array
299283
{
300284
$result = [];
@@ -338,15 +322,69 @@ private function parseLocalVariables(array $variables): array
338322

339323
private function formatMarkup(Markup $markup): string
340324
{
341-
// TODO: more advanced with options
342-
// built-in HtmlMarkup
343325
$tag = (string) $markup->identifier;
326+
$options = Arr\map_with_keys($markup->options, fn (Option $option) => yield $option->identifier->name => $option->value->value);
327+
328+
if ($markup->type === MarkupType::STANDALONE) {
329+
if (is_null($formatter = $this->getStandaloneMarkupFormatter($tag))) {
330+
return '';
331+
}
332+
333+
return $formatter->format($tag, $options);
334+
}
335+
336+
if (is_null($formatter = $this->getMarkupFormatter($tag))) {
337+
return '';
338+
}
344339

345340
return match ($markup->type) {
346-
MarkupType::OPEN => "<{$tag}>",
347-
MarkupType::CLOSE => "</{$tag}>",
348-
MarkupType::STANDALONE => "<{$tag}/>",
341+
MarkupType::OPEN => $formatter->formatOpenTag($tag, $options),
342+
MarkupType::CLOSE => $formatter->formatCloseTag($tag),
349343
default => '',
350344
};
351345
}
346+
347+
private function getMarkupFormatter(?string $tag): ?MarkupFormatter
348+
{
349+
if (! $tag) {
350+
return null;
351+
}
352+
353+
return arr($this->markupFormatters)
354+
->filter(fn (MarkupFormatter|StandaloneMarkupFormatter $fn) => $fn instanceof MarkupFormatter)
355+
->first(fn (MarkupFormatter $fn) => $fn->supportsTag($tag));
356+
}
357+
358+
private function getStandaloneMarkupFormatter(?string $tag): ?StandaloneMarkupFormatter
359+
{
360+
if (! $tag) {
361+
return null;
362+
}
363+
364+
return arr($this->markupFormatters)
365+
->filter(fn (MarkupFormatter|StandaloneMarkupFormatter $fn) => $fn instanceof StandaloneMarkupFormatter)
366+
->first(fn (StandaloneMarkupFormatter $fn) => $fn->supportsTag($tag));
367+
}
368+
369+
private function getSelectorFunction(?string $name): ?SelectorFunction
370+
{
371+
if (! $name) {
372+
return null;
373+
}
374+
375+
return arr($this->functions)
376+
->filter(fn (FormattingFunction|SelectorFunction $fn) => $fn instanceof SelectorFunction)
377+
->first(fn (SelectorFunction $fn) => $fn->name === $name);
378+
}
379+
380+
private function getFormattingFunction(?string $name): ?FormattingFunction
381+
{
382+
if (! $name) {
383+
return null;
384+
}
385+
386+
return arr($this->functions)
387+
->filter(fn (FormattingFunction|SelectorFunction $fn) => $fn instanceof FormattingFunction)
388+
->first(fn (FormattingFunction $fn) => $fn->name === $name);
389+
}
352390
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Tempest\Intl\MessageFormat\Markup;
4+
5+
use Tempest\Intl\MessageFormat\MarkupFormatter;
6+
use Tempest\Support\Html;
7+
8+
final class HtmlTagFormatter implements MarkupFormatter
9+
{
10+
public function supportsTag(string $tag): bool
11+
{
12+
return Html\is_html_tag($tag) && ! Html\is_void_tag($tag);
13+
}
14+
15+
public function formatOpenTag(string $tag, array $options): string
16+
{
17+
return sprintf('<%s%s>', $tag, Html\format_attributes($options));
18+
}
19+
20+
public function formatCloseTag(string $tag): string
21+
{
22+
return sprintf('</%s>', $tag);
23+
}
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Tempest\Intl\MessageFormat\Markup;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter;
7+
use Tempest\Support\Arr;
8+
use Tempest\Support\Str;
9+
use Tempest\View\Components\Icon;
10+
11+
final readonly class IconMarkupFormatter implements StandaloneMarkupFormatter
12+
{
13+
public function __construct(
14+
private Container $container,
15+
) {}
16+
17+
public function supportsTag(string $tag): bool
18+
{
19+
return Str\starts_with($tag, 'icon-');
20+
}
21+
22+
public function format(string $tag, array $options): string
23+
{
24+
if (! class_exists(Icon::class)) {
25+
throw new \RuntimeException('The Icon component is not available. Please ensure the `tempest\view` package is installed.');
26+
}
27+
28+
return $this->container->get(Icon::class)->render(
29+
name: Str\after_first($tag, 'icon-'),
30+
class: Arr\get_by_key($options, 'class'),
31+
);
32+
}
33+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Tempest\Intl\MessageFormat\Markup;
4+
5+
use Tempest\Intl\MessageFormat\StandaloneMarkupFormatter;
6+
use Tempest\Support\Html;
7+
8+
final class VoidHtmlTagFormatter implements StandaloneMarkupFormatter
9+
{
10+
public function supportsTag(string $tag): bool
11+
{
12+
return Html\is_void_tag($tag);
13+
}
14+
15+
public function format(string $tag, array $options): string
16+
{
17+
return Html\create_tag($tag, $options)->toString();
18+
}
19+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Tempest\Intl\MessageFormat;
4+
5+
interface MarkupFormatter
6+
{
7+
/**
8+
* Checks if the formatter supports a specific tag.
9+
*/
10+
public function supportsTag(string $tag): bool;
11+
12+
/**
13+
* Returns the formatted markup for an opening tag with options.
14+
*/
15+
public function formatOpenTag(string $tag, array $options): string;
16+
17+
/**
18+
* Returns the formatted markup for a closing tag.
19+
*/
20+
public function formatCloseTag(string $tag): string;
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Tempest\Intl\MessageFormat;
4+
5+
interface StandaloneMarkupFormatter
6+
{
7+
/**
8+
* Checks if the formatter supports a specific tag.
9+
*/
10+
public function supportsTag(string $tag): bool;
11+
12+
/**
13+
* Returns the formatted standalone tag.
14+
*/
15+
public function format(string $tag, array $options): string;
16+
}

0 commit comments

Comments
 (0)