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

dg / texy / 21501721037

30 Jan 2026 02:00AM UTC coverage: 91.159% (-1.3%) from 92.426%
21501721037

push

github

dg
wip

2681 of 2941 relevant lines covered (91.16%)

0.91 hits per line

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

88.89
/src/Texy/Modules/HtmlModule.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Texy\Modules;
11

12
use Texy;
13
use Texy\Nodes\HtmlCommentNode;
14
use Texy\Nodes\HtmlTagNode;
15
use Texy\ParseContext;
16
use Texy\Patterns;
17
use Texy\Position;
18
use Texy\Regexp;
19
use Texy\Syntax;
20
use function str_ends_with, strlen, strtolower, strtr, substr, trim;
21

22

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

33

34
        public function beforeParse(string &$text): void
1✔
35
        {
36
                $this->texy->registerLinePattern(
1✔
37
                        $this->parseTag(...),
1✔
38
                        '~
39
                                < (/?)                          # tag begin
40
                                ([a-z][a-z0-9_:-]{0,50})        # tag name
41
                                (
42
                                        (?:
43
                                                \s++ [a-z0-9_:-]++ |   # attribute name
44
                                                = \s*+ " [^"' . Patterns::MARK . ']*+ " |     # attribute value in double quotes
1✔
45
                                                = \s*+ \' [^\'' . Patterns::MARK . ']*+ \' |  # attribute value in single quotes
1✔
46
                                                = [^\s>' . Patterns::MARK . ']++              # attribute value without quotes
1✔
47
                                        )*
48
                                )
49
                                \s*+
50
                                (/?)                             # self-closing slash
51
                                >
52
                        ~is',
53
                        Syntax::HtmlTag,
1✔
54
                );
55

56
                $this->texy->registerLinePattern(
1✔
57
                        fn(?ParseContext $context, array $matches, array $offsets) => new HtmlCommentNode($matches[1], new Position($offsets[0], strlen($matches[0]))),
1✔
58
                        '~
59
                                <!--
60
                                ( [^' . Patterns::MARK . ']*? )
1✔
61
                                -->
62
                        ~is',
63
                        Syntax::HtmlComment,
1✔
64
                );
65
        }
1✔
66

67

68
        /**
69
         * Parses <tag attr="...">
70
         * @param  array<?string>  $matches
71
         * @param  array<?int>  $offsets
72
         */
73
        public function parseTag(?ParseContext $context, array $matches, array $offsets): ?HtmlTagNode
1✔
74
        {
75
                [, $mEnd, $mTag, $mAttr, $mEmpty] = $matches;
1✔
76

77
                $isStart = $mEnd !== '/';
1✔
78
                $isEmpty = $mEmpty === '/';
1✔
79
                if (!$isEmpty && str_ends_with($mAttr, '/')) {
1✔
80
                        $mAttr = substr($mAttr, 0, -1);
×
81
                        $isEmpty = true;
×
82
                }
83

84
                // error - can't close empty element
85
                if ($isEmpty && !$isStart) {
1✔
86
                        return null;
×
87
                }
88

89
                // error - end element with attrs
90
                $mAttr = trim(strtr($mAttr, "\n", ' '));
1✔
91
                if ($mAttr && !$isStart) {
1✔
92
                        return null;
×
93
                }
94

95
                return new HtmlTagNode(
1✔
96
                        $mTag,
1✔
97
                        $isStart ? $this->parseAttributes($mAttr) : [],
1✔
98
                        closing: !$isStart,
1✔
99
                        selfClosing: $isEmpty,
100
                        position: new Position($offsets[0], strlen($matches[0])),
1✔
101
                );
102
        }
103

104

105
        /** @return array<string, string|bool> */
106
        private function parseAttributes(string $attrs): array
1✔
107
        {
108
                $res = [];
1✔
109
                $matches = Regexp::matchAll(
1✔
110
                        $attrs,
1✔
111
                        <<<'X'
112
                                ~
1✔
113
                                ([a-z0-9_:-]+)                 # attribute name
114
                                \s*
115
                                (?:
116
                                        = \s*                      # equals sign
117
                                        (
118
                                                ' [^']* ' |            # single quoted value
119
                                                " [^"]* " |            # double quoted value
120
                                                [^'"\s]+               # unquoted value
121
                                        )
122
                                )?
123
                                ~is
124
                                X,
125
                );
126

127
                foreach ($matches as $m) {
1✔
128
                        $key = strtolower($m[1]);
1✔
129
                        $value = $m[2];
1✔
130
                        if ($value == null) {
1✔
131
                                $res[$key] = true;
×
132
                        } elseif ($value[0] === '\'' || $value[0] === '"') {
1✔
133
                                $res[$key] = trim(Texy\Helpers::unescapeHtml(substr($value, 1, -1)));
1✔
134
                        } else {
135
                                $res[$key] = trim(Texy\Helpers::unescapeHtml($value));
1✔
136
                        }
137
                }
138

139
                return $res;
1✔
140
        }
141
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc