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

dg / texy / 33043376865

27 Aug 2026 05:39AM UTC coverage: 96.335% (+1.6%) from 94.771%
33043376865

push

github

dg
examples: a Markdown parser built on the parsing engine

Shows that Engine carries a language other than Texy: a CommonMark parser
scoring 611 of 652 examples (94%) of the official suite, plus a runner so
the claim can be checked rather than believed.

The instructive part is where the phase boundary falls. Block constructs and
most inline ones are patterns with handlers, but emphasis pairing depends on
the whole sequence of delimiter runs in a container, which no match can see.
So the parser only marks the runs and the pairing happens over the AST; that
alone takes emphasis from 56% to 99%. Reference links go through the
transform phase for the same reason a definition may follow its use.

3391 of 3520 relevant lines covered (96.34%)

0.96 hits per line

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

93.55
/src/Texy/Modules/HtmlModule.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\HtmlCommentNode;
12
use Texy\Nodes\HtmlTagNode;
13
use Texy\Nodes\TextNode;
14
use Texy\Parsing\ParseContext;
15
use Texy\Range;
16
use Texy\Regexp;
17
use Texy\Syntax;
18
use function strlen;
19
use const PREG_SET_ORDER;
20

21

22
/**
23
 * Processes HTML tags, comments and entity references in input text.
24
 */
25
final class HtmlModule extends Texy\Module
26
{
27
        public function __construct(
1✔
28
                private Texy\Texy $texy,
29
        ) {
30
                $texy->addPass($this->processPassthrough(...));
1✔
31
        }
1✔
32

33

34
        /**
35
         * Pairs passthrough tags into HtmlElementNode trees and evaluates the
36
         * tag whitelist over them (transform phase): rejected tags become text.
37
         */
38
        public function processPassthrough(Texy\Nodes\DocumentNode $doc): void
1✔
39
        {
40
                if (empty($this->texy->allowed[Syntax::HtmlTag])) {
1✔
41
                        return;
×
42
                }
43

44
                (new Texy\Passes\HtmlPairingPass)->process($doc);
1✔
45
                (new Texy\Passes\HtmlSanitizePass($this->texy->htmlPolicy))
1✔
46
                        ->process($doc);
1✔
47
        }
1✔
48

49

50
        public function beforeParse(): void
51
        {
52
                $this->texy->addLineSyntax(
1✔
53
                        Syntax::HtmlTag,
1✔
54
                        '~
1✔
55
                                < (/?)                          # tag begin
56
                                ([a-z][a-z0-9_:-]{0,50})        # tag name
57
                                (
58
                                        (?:
59
                                                \s++ [a-z0-9_:-]++ |          # attribute name
60
                                                = \s*+ " [^"]*+ " |           # attribute value in double quotes
61
                                                = \s*+ \' [^\']*+ \' |        # attribute value in single quotes
62
                                                = [^\s>]++                    # attribute value without quotes
63
                                        )*
64
                                )
65
                                \s*+
66
                                (/?)                             # self-closing slash
67
                                >
68
                        ~isx',
69
                        $this->parseTag(...),
1✔
70
                );
71

72
                $this->texy->addLineSyntax(
1✔
73
                        Syntax::HtmlComment,
1✔
74
                        '~
1✔
75
                                <!--
76
                                ( .*? )
77
                                -->
78
                        ~isx',
79
                        $this->parseComment(...),
1✔
80
                );
81

82
                $this->texy->addLineSyntax(
1✔
83
                        Syntax::HtmlEntity,
1✔
84
                        '~' . Texy\Helpers::EntityPattern . '~',
1✔
85
                        $this->parseEntity(...),
1✔
86
                );
87
        }
1✔
88

89

90
        /**
91
         * Parses &amp;, &#160;, &#xA0;
92
         * @param  array{string}  $matches
93
         * @param  array{int}  $offsets
94
         */
95
        public function parseEntity(ParseContext $context, array $matches, array $offsets): ?TextNode
1✔
96
        {
97
                $decoded = Texy\Helpers::decodeEntity($matches[0]);
1✔
98
                return $decoded === null
1✔
99
                        ? null
1✔
100
                        : new TextNode($decoded, new Range($offsets[0], strlen($matches[0])));
1✔
101
        }
102

103

104
        /**
105
         * Parses <!-- comment -->
106
         * @param  array{string, string}  $matches
107
         * @param  array{int, int}  $offsets
108
         */
109
        public function parseComment(ParseContext $context, array $matches, array $offsets): HtmlCommentNode
1✔
110
        {
111
                return new HtmlCommentNode($matches[1], new Range($offsets[0], strlen($matches[0])));
1✔
112
        }
113

114

115
        /**
116
         * Parses <tag attr="...">
117
         * @param  array{string, string, string, string, string}  $matches
118
         * @param  array{int, int, int, int, int}  $offsets
119
         */
120
        public function parseTag(ParseContext $context, array $matches, array $offsets): ?HtmlTagNode
1✔
121
        {
122
                [, $mEnd, $mTag, $mAttr, $mEmpty] = $matches;
1✔
123

124
                $isStart = $mEnd !== '/';
1✔
125
                $isEmpty = $mEmpty === '/';
1✔
126
                if (!$isEmpty && str_ends_with($mAttr, '/')) {
1✔
127
                        $mAttr = substr($mAttr, 0, -1);
×
128
                        $isEmpty = true;
×
129
                }
130

131
                // error - can't close empty element
132
                if ($isEmpty && !$isStart) {
1✔
133
                        return null;
×
134
                }
135

136
                // error - end element with attrs
137
                $mAttr = trim(strtr($mAttr, "\n", ' '));
1✔
138
                if ($mAttr && !$isStart) {
1✔
139
                        return null;
1✔
140
                }
141

142
                return new HtmlTagNode(
1✔
143
                        $mTag,
1✔
144
                        $isStart ? $this->parseAttributes($mAttr) : [],
1✔
145
                        closing: !$isStart,
1✔
146
                        selfClosing: $isEmpty,
147
                        range: new Range($offsets[0], strlen($matches[0])),
1✔
148
                );
149
        }
150

151

152
        /** @return array<string, string|bool> */
153
        private function parseAttributes(string $attrs): array
1✔
154
        {
155
                $res = [];
1✔
156
                $matches = Regexp::matchAll(
1✔
157
                        $attrs,
1✔
158
                        <<<'X'
159
                                ~
1✔
160
                                ([a-z0-9_:-]+)                 # attribute name
161
                                \s*
162
                                (?:
163
                                        = \s*                      # equals sign
164
                                        (
165
                                                ' [^']* ' |            # single quoted value
166
                                                " [^"]* " |            # double quoted value
167
                                                [^'"\s]+               # unquoted value
168
                                        )
169
                                )?
170
                                ~isx
171
                                X,
172
                );
173

174
                /** @var array{string, string, ?string} $m */
175
                foreach ($matches as $m) {
1✔
176
                        $key = strtolower($m[1]);
1✔
177
                        $value = $m[2];
1✔
178
                        if ($value == null) {
1✔
179
                                $res[$key] = true;
1✔
180
                        } elseif ($value[0] === '\'' || $value[0] === '"') {
1✔
181
                                $res[$key] = trim(Texy\Helpers::unescapeHtml(substr($value, 1, -1)));
1✔
182
                        } else {
183
                                $res[$key] = trim(Texy\Helpers::unescapeHtml($value));
1✔
184
                        }
185
                }
186

187
                return $res;
1✔
188
        }
189
}
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