• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / texy / 30792106735

03 Aug 2026 06:40AM UTC coverage: 94.771% (+0.09%) from 94.677%
30792106735

push

github

dg
Engine: the text decoder is injectable

InlineParser hardwired Helpers::decodeEntities() into every TextNode it
created, which put a content policy inside the parsing mechanics: what
counts as display text belongs to the language, not to the engine. It is
now a Closure(string): string taken by the Engine constructor, with the
old behaviour as the default, so Texy is unaffected and a language where
"&" is not an entity can pass fn($s) => $s. A custom decoder takes
over the control-character sanitization the default performs.

7 of 7 new or added lines in 2 files covered. (100.0%)

99 existing lines in 18 files now uncovered.

3498 of 3691 relevant lines covered (94.77%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.97
/src/Texy/Modules/DirectiveModule.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Texy! (https://texy.nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Texy\Modules;
9

10
use Texy;
11
use Texy\Nodes\DirectiveNode;
12
use Texy\Parsing\ParseContext;
13
use Texy\Range;
14
use Texy\Syntax;
15
use function strlen, trim;
16

17

18
/**
19
 * Processes {{macro}} script commands.
20
 */
21
final class DirectiveModule extends Texy\Module
22
{
23
        /** @var array<string, \Closure(?string, list<string>, DirectiveNode): ?Texy\Node> */
24
        private array $handlers = [];
25

26
        /** @var ?\Closure(?string, list<string>, DirectiveNode): ?Texy\Node */
27
        private ?\Closure $fallback = null;
28

29

30
        public function __construct(
1✔
31
                private Texy\Texy $texy,
32
        ) {
33
                $texy->addPass($this->processDirectives(...));
1✔
34
        }
1✔
35

36

37
        /** @param  callable(?string, list<string>, DirectiveNode): ?Texy\Node  $handler */
38
        public function addDirective(string $name, callable $handler): void
1✔
39
        {
40
                $this->handlers[Texy\Helpers::toLower($name)] = $handler(...);
1✔
41
        }
1✔
42

43

44
        /**
45
         * Registers a handler for directives that have no named handler.
46
         *
47
         * Without it an unhandled directive stays in the document and renders as the author
48
         * wrote it, and the consumer has no way of learning about it. The handler receives
49
         * the same arguments as a named one, so the name is read from the node.
50
         * @param  ?callable(?string, list<string>, DirectiveNode): ?Texy\Node  $handler
51
         */
52
        public function setFallback(?callable $handler): void
1✔
53
        {
54
                $this->fallback = $handler === null ? null : $handler(...);
1✔
55
        }
1✔
56

57

58
        /**
59
         * Consumes {{texy: ...}} into DocumentNode::$meta and hands the rest to registered
60
         * handlers. Runs as the first pass, so a handler may still change configuration the
61
         * later ones depend on. A handled directive is removed unless the handler returns
62
         * a replacement.
63
         */
64
        public function processDirectives(Texy\Nodes\DocumentNode $doc): void
1✔
65
        {
66
                (new Texy\NodeTraverser)->traverse($doc, function (Texy\Node $node) use ($doc): Texy\Node|int|null {
1✔
67
                        if (!$node instanceof DirectiveNode) {
1✔
68
                                return null;
1✔
69
                        }
70

71
                        $parsed = $node->parseContent();
1✔
72
                        if ($parsed['name'] === 'texy' && $parsed['args']) {
1✔
73
                                foreach ($parsed['args'] as $arg) {
1✔
74
                                        $doc->meta[$arg] = true;
1✔
75
                                }
76
                                return Texy\NodeTraverser::RemoveNode;
1✔
77
                        }
78

79
                        $handler = $this->handlers[Texy\Helpers::toLower($parsed['name'])] ?? $this->fallback;
1✔
80
                        return $handler === null
1✔
81
                                ? null
1✔
82
                                : $handler($parsed['value'], $parsed['args'], $node) ?? Texy\NodeTraverser::RemoveNode;
1✔
83
                });
1✔
84
        }
1✔
85

86

87
        public function beforeParse(): void
88
        {
89
                $this->texy->addLineSyntax(
1✔
90
                        Syntax::Directive,
1✔
91
                        '~
1✔
92
                                \{\{
93
                                ((?:
94
                                        [^}]++ |   # content not containing }
95
                                        }          # or single }
96
                                )+)
97
                                }}
98
                        ~Ux',
99
                        $this->parse(...),
1✔
100
                );
101
        }
1✔
102

103

104
        /**
105
         * Parses {{macro}}.
106
         * @param  array{string, string}  $matches
107
         * @param  array{int, int}  $offsets
108
         */
109
        public function parse(ParseContext $context, array $matches, array $offsets): ?DirectiveNode
1✔
110
        {
111
                return trim($matches[1]) === ''
1✔
UNCOV
112
                        ? null
×
113
                        : new DirectiveNode($matches[1], new Range($offsets[0], strlen($matches[0])));
1✔
114
        }
115
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc