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

dg / texy / 12879605443

21 Jan 2025 03:31AM UTC coverage: 92.224% (+0.03%) from 92.197%
12879605443

push

github

dg
regexp: uses unmatched as null (BC break)

14 of 14 new or added lines in 6 files covered. (100.0%)

101 existing lines in 14 files now uncovered.

2372 of 2572 relevant lines covered (92.22%)

0.92 hits per line

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

94.05
/src/Texy/Modules/HeadingModule.php
1
<?php
2

3
/**
4
 * This file is part of the Texy! (https://texy.info)
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\Modifier;
14

15

16
/**
17
 * Heading module.
18
 */
19
final class HeadingModule extends Texy\Module
20
{
21
        public const
22
                DYNAMIC = 1, // auto-leveling
23
                FIXED = 2; // fixed-leveling
24

25
        /** textual content of first heading */
26
        public ?string $title = null;
27

28
        /** @var array<int, array{el: Texy\HtmlElement, level: int, type: string}>  generated Table of Contents */
29
        public array $TOC = [];
30

31
        public bool $generateID = false;
32

33
        /** prefix for autogenerated ID */
34
        public string $idPrefix = 'toc-';
35

36
        /** level of top heading, 1..6 */
37
        public int $top = 1;
38

39
        /** surrounded headings: more #### means higher heading */
40
        public bool $moreMeansHigher = true;
41

42
        /** balancing mode */
43
        public int $balancing = self::DYNAMIC;
44

45
        /** @var array<string, int>  when $balancing = HeadingModule::FIXED */
46
        public array $levels = [
47
                '#' => 0, // # --> $levels['#'] + $top = 0 + 1 = 1 --> <h1> ... </h1>
48
                '*' => 1,
49
                '=' => 2,
50
                '-' => 3,
51
        ];
52

53
        /** @var array<string, true>  used ID's */
54
        private array $usedID = [];
55

56

57
        public function __construct(Texy\Texy $texy)
1✔
58
        {
59
                $this->texy = $texy;
1✔
60

61
                $texy->addHandler('heading', $this->solve(...));
1✔
62
                $texy->addHandler('beforeParse', $this->beforeParse(...));
1✔
63
                $texy->addHandler('afterParse', $this->afterParse(...));
1✔
64

65
                $texy->registerBlockPattern(
1✔
66
                        $this->patternUnderline(...),
1✔
67
                        '~^
68
                                ( \S .{0,1000} )                 # heading text (1)
69
                                ' . Texy\Patterns::MODIFIER_H . '? # modifier (2)
1✔
70
                                \n
71
                                ( \#{3,}+ | \*{3,}+ | ={3,}+ | -{3,}+ )  # underline characters (3)
72
                        $~mU',
73
                        'heading/underlined',
1✔
74
                );
75

76
                $texy->registerBlockPattern(
1✔
77
                        $this->patternSurround(...),
1✔
78
                        '~^
79
                                ( \#{2,}+ | ={2,}+ )             # opening characters (1)
80
                                (.+)                             # heading text (2)
81
                                ' . Texy\Patterns::MODIFIER_H . '? # modifier (2)
1✔
82
                        $~mU',
83
                        'heading/surrounded',
1✔
84
                );
85
        }
1✔
86

87

88
        private function beforeParse(): void
89
        {
90
                $this->title = null;
1✔
91
                $this->usedID = [];
1✔
92
                $this->TOC = [];
1✔
93
        }
1✔
94

95

96
        private function afterParse(Texy\Texy $texy, Texy\HtmlElement $DOM, bool $isSingleLine): void
1✔
97
        {
98
                if ($isSingleLine) {
1✔
UNCOV
99
                        return;
×
100
                }
101

102
                if ($this->balancing === self::DYNAMIC) {
1✔
103
                        $top = $this->top;
1✔
104
                        $map = [];
1✔
105
                        $min = 100;
1✔
106
                        foreach ($this->TOC as $item) {
1✔
107
                                $level = $item['level'];
1✔
108
                                if ($item['type'] === 'surrounded') {
1✔
109
                                        $min = min($level, $min);
1✔
110
                                        $top = $this->top - $min;
1✔
111

112
                                } elseif ($item['type'] === 'underlined') {
1✔
113
                                        $map[$level] = $level;
1✔
114
                                }
115
                        }
116

117
                        asort($map);
1✔
118
                        $map = array_flip(array_values($map));
1✔
119
                }
120

121
                foreach ($this->TOC as $key => $item) {
1✔
122
                        if ($this->balancing === self::DYNAMIC) {
1✔
123
                                if ($item['type'] === 'surrounded') {
1✔
124
                                        $level = $item['level'] + $top;
1✔
125

126
                                } elseif ($item['type'] === 'underlined') {
1✔
127
                                        $level = $map[$item['level']] + $this->top;
1✔
128

129
                                } else {
130
                                        $level = $item['level'];
1✔
131
                                }
132

133
                                $item['el']->setName('h' . min(6, max(1, $level)));
1✔
134
                                $this->TOC[$key]['level'] = $level;
1✔
135
                        }
136

137
                        if ($this->generateID) {
1✔
138
                                if (!empty($item['el']->attrs['style']['toc']) && is_array($item['el']->attrs['style'])) {
1✔
139
                                        $title = $item['el']->attrs['style']['toc'];
1✔
140
                                        unset($item['el']->attrs['style']['toc']);
1✔
141
                                } else {
142
                                        $title = trim($item['el']->toText($this->texy));
1✔
143
                                }
144

145
                                $this->TOC[$key]['title'] = $title;
1✔
146
                                if (empty($item['el']->attrs['id'])) {
1✔
147
                                        $id = $this->idPrefix . Texy\Helpers::webalize($title);
1✔
148
                                        $counter = '';
1✔
149
                                        if (isset($this->usedID[$id . $counter])) {
1✔
UNCOV
150
                                                $counter = 2;
×
UNCOV
151
                                                while (isset($this->usedID[$id . '-' . $counter])) {
×
UNCOV
152
                                                        $counter++;
×
153
                                                }
154

UNCOV
155
                                                $id .= '-' . $counter;
×
156
                                        }
157

158
                                        $this->usedID[$id] = true;
1✔
159
                                        $item['el']->attrs['id'] = $id;
1✔
160
                                }
161
                        }
162
                }
163

164
                // document title
165
                if ($this->title === null && count($this->TOC)) {
1✔
166
                        $item = reset($this->TOC);
1✔
167
                        $this->title = $item['title'] ?? trim($item['el']->toText($this->texy));
1✔
168
                }
169
        }
1✔
170

171

172
        /**
173
         * Callback for underlined heading.
174
         *
175
         * Heading .(title)[class]{style}>
176
         * -------------------------------
177
         */
178
        public function patternUnderline(Texy\BlockParser $parser, array $matches): Texy\HtmlElement|string|null
1✔
179
        {
180
                [, $mContent, $mMod, $mLine] = $matches;
1✔
181
                // $matches:
182
                // [1] => ...
183
                // [2] => .(title)[class]{style}<>
184
                // [3] => ...
185

186
                $mod = new Modifier($mMod);
1✔
187
                $level = $this->levels[$mLine[0]];
1✔
188
                return $this->texy->invokeAroundHandlers('heading', $parser, [$level, $mContent, $mod, false]);
1✔
189
        }
190

191

192
        /**
193
         * Callback for surrounded heading.
194
         *
195
         * ### Heading .(title)[class]{style}>
196
         */
197
        public function patternSurround(Texy\BlockParser $parser, array $matches): Texy\HtmlElement|string|null
1✔
198
        {
199
                [, $mLine, $mContent, $mMod] = $matches;
1✔
200
                // [1] => ###
201
                // [2] => ...
202
                // [3] => .(title)[class]{style}<>
203

204
                $mod = new Modifier($mMod);
1✔
205
                $level = min(7, max(2, strlen($mLine)));
1✔
206
                $level = $this->moreMeansHigher ? 7 - $level : $level - 2;
1✔
207
                $mContent = rtrim($mContent, $mLine[0] . ' ');
1✔
208
                return $this->texy->invokeAroundHandlers('heading', $parser, [$level, $mContent, $mod, true]);
1✔
209
        }
210

211

212
        /**
213
         * Finish invocation.
214
         */
215
        private function solve(
1✔
216
                Texy\HandlerInvocation $invocation,
217
                int $level,
218
                string $content,
219
                Modifier $mod,
220
                bool $isSurrounded,
221
        ): Texy\HtmlElement
222
        {
223
                // as fixed balancing, for block/texysource & correct decorating
224
                $el = new Texy\HtmlElement('h' . min(6, max(1, $level + $this->top)));
1✔
225
                $mod->decorate($this->texy, $el);
1✔
226

227
                $el->parseLine($this->texy, trim($content));
1✔
228

229
                $this->TOC[] = [
1✔
230
                        'el' => $el,
1✔
231
                        'level' => $level,
1✔
232
                        'type' => $isSurrounded ? 'surrounded' : 'underlined',
1✔
233
                ];
234

235
                return $el;
1✔
236
        }
237
}
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