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

dg / texy / 29654833201

18 Jul 2026 04:09PM UTC coverage: 93.039% (+0.04%) from 93.004%
29654833201

push

github

dg
Restored test coverage dropped during the AST migration

The migration removed five test files coupled to v3 contracts (block,
content-model, formatter-indent, html, latte) with the intent to bring
them back once the output layer settled. Restored here, ported to the
AST-era API; latte and most of content-model pass unchanged.

Expected outputs were re-derived and every difference against the v3
files reviewed case by case. The differences fall into the intentional
categories: list items merge continuation lines, whitespace-sensitive
typography follows visible length instead of protection-mark key length,
rejected tags are escaped consistently (both halves of a pair) with
typography applied to the escaped text, paragraphs of escaped markup are
<p>-wrapped, and <p> inside <form> is preserved. Inner texysource
fragments are parsed without the transform phase, so their passthrough
tags stay live - a documented wrinkle.

content-model.phpt again guards the WellFormer content model, nesting
prohibitions, transparent and unknown elements and auto-closing in the
public suite, independently of the private regression corpus.

3248 of 3491 relevant lines covered (93.04%)

0.93 hits per line

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

92.16
/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\ParseContext;
14
use Texy\Position;
15
use Texy\Regexp;
16
use Texy\Syntax;
17
use function str_ends_with, strlen, strtolower, strtr, substr, trim;
18

19

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

31

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

42
                (new Texy\HtmlPairingPass)->process($doc);
1✔
43
                (new Texy\HtmlSanitizePass(new Texy\Output\Html\Sanitizer($this->texy, $this->texy->htmlGenerator)))
1✔
44
                        ->process($doc);
1✔
45
        }
1✔
46

47

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

70
                $this->texy->registerLinePattern(
1✔
71
                        fn(?ParseContext $context, array $matches, array $offsets) => new HtmlCommentNode((string) $matches[1], new Position($offsets[0], strlen((string) $matches[0]))),
1✔
72
                        '~
1✔
73
                                <!--
74
                                ( .*? )
75
                                -->
76
                        ~is',
77
                        Syntax::HtmlComment,
1✔
78
                );
79
        }
1✔
80

81

82
        /**
83
         * Parses <tag attr="...">
84
         * @param  array<?string>  $matches
85
         * @param  array<?int>  $offsets
86
         */
87
        public function parseTag(ParseContext $context, array $matches, array $offsets): ?HtmlTagNode
1✔
88
        {
89
                /** @var array{string, string, string, string, string} $matches */
90
                [, $mEnd, $mTag, $mAttr, $mEmpty] = $matches;
1✔
91

92
                $isStart = $mEnd !== '/';
1✔
93
                $isEmpty = $mEmpty === '/';
1✔
94
                if (!$isEmpty && str_ends_with($mAttr, '/')) {
1✔
95
                        $mAttr = substr($mAttr, 0, -1);
×
96
                        $isEmpty = true;
×
97
                }
98

99
                // error - can't close empty element
100
                if ($isEmpty && !$isStart) {
1✔
101
                        return null;
×
102
                }
103

104
                // error - end element with attrs
105
                $mAttr = trim(strtr($mAttr, "\n", ' '));
1✔
106
                if ($mAttr && !$isStart) {
1✔
107
                        return null;
1✔
108
                }
109

110
                return new HtmlTagNode(
1✔
111
                        $mTag,
1✔
112
                        $isStart ? $this->parseAttributes($mAttr) : [],
1✔
113
                        closing: !$isStart,
1✔
114
                        selfClosing: $isEmpty,
115
                        position: new Position($offsets[0], strlen($matches[0])),
1✔
116
                );
117
        }
118

119

120
        /** @return array<string, string|bool> */
121
        private function parseAttributes(string $attrs): array
1✔
122
        {
123
                $res = [];
1✔
124
                $matches = Regexp::matchAll(
1✔
125
                        $attrs,
1✔
126
                        <<<'X'
127
                                ~
1✔
128
                                ([a-z0-9_:-]+)                 # attribute name
129
                                \s*
130
                                (?:
131
                                        = \s*                      # equals sign
132
                                        (
133
                                                ' [^']* ' |            # single quoted value
134
                                                " [^"]* " |            # double quoted value
135
                                                [^'"\s]+               # unquoted value
136
                                        )
137
                                )?
138
                                ~is
139
                                X,
140
                );
141

142
                /** @var array{string, string, ?string} $m */
143
                foreach ($matches as $m) {
1✔
144
                        $key = strtolower($m[1]);
1✔
145
                        $value = $m[2];
1✔
146
                        if ($value == null) {
1✔
147
                                $res[$key] = true;
1✔
148
                        } elseif ($value[0] === '\'' || $value[0] === '"') {
1✔
149
                                $res[$key] = trim(Texy\Helpers::unescapeHtml(substr($value, 1, -1)));
1✔
150
                        } else {
151
                                $res[$key] = trim(Texy\Helpers::unescapeHtml($value));
1✔
152
                        }
153
                }
154

155
                return $res;
1✔
156
        }
157
}
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