• 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

98.51
/src/Texy/Modules/BlockQuoteModule.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\BlockQuoteNode;
14
use Texy\ParseContext;
15
use Texy\Position;
16
use Texy\Syntax;
17
use function max, strlen;
18

19

20
/**
21
 * Processes blockquote syntax with nested content.
22
 */
23
final class BlockQuoteModule extends Texy\Module
24
{
25
        public function __construct(
1✔
26
                private Texy\Texy $texy,
27
        ) {
28
        }
1✔
29

30

31
        public function beforeParse(string &$text): void
1✔
32
        {
33
                $this->texy->registerBlockPattern(
1✔
34
                        $this->parse(...),
1✔
35
                        '~^
36
                                (?: ' . Texy\Patterns::MODIFIER_H . '\n)? # modifier (1)
1✔
37
                                >                                      # blockquote char
38
                                ( [ \t]++ | : )                        # space/tab or colon (2)
39
                                ( \S.*+ )                              # content (3)
40
                        $~mU',
41
                        Syntax::Blockquote,
1✔
42
                );
43
        }
1✔
44

45

46
        /**
47
         * Parses blockquote.
48
         * @param  array<?string>  $matches
49
         * @param  array<?int>  $offsets
50
         */
51
        public function parse(ParseContext $context, array $matches, array $offsets): ?BlockQuoteNode
1✔
52
        {
53
                [, $mMod, $mPrefix, $mContent] = $matches;
1✔
54

55
                $startOffset = $offsets[0];
1✔
56
                $totalLength = strlen($matches[0]);
1✔
57
                $contentOffset = $offsets[3] ?? $offsets[0] + 2; // offset of first line content
1✔
58

59
                // Collect lines with their absolute offsets
60
                $lines = [['content' => $mContent ?? '', 'offset' => $contentOffset]];
1✔
61
                $spaces = '';
1✔
62

63
                do {
64
                        if ($spaces === '') {
1✔
65
                                $spaces = max(1, strlen($mPrefix));
1✔
66
                        }
67

68
                        if (!$context->getBlockParser()->next("~^>(?: | ([ \\t]{1,$spaces} | :) (.*))$~mA", $matches, $nextOffsets)) {
1✔
69
                                break;
1✔
70
                        }
71

72
                        $totalLength += strlen($matches[0]) + 1; // +1 for \n
1✔
73
                        [, $mPrefix, $mContent] = $matches;
1✔
74

75
                        // Track where this line's content starts in absolute terms
76
                        $lineContentOffset = $nextOffsets[2] ?? ($nextOffsets[0] + 2); // after "> "
1✔
77
                        $lines[] = ['content' => $mContent ?? '', 'offset' => $lineContentOffset];
1✔
78
                } while (true);
1✔
79

80
                // Join content for parsing, but track line boundaries
81
                $content = implode("\n", array_column($lines, 'content'));
1✔
82

83
                // Parse nested content
84
                $parsed = $context->parseBlock(trim($content));
1✔
85
                if (!$parsed->children) {
1✔
86
                        return null;
×
87
                }
88

89
                // Build offset map: [localOffset => absoluteOffset] for each line start
90
                $offsetMap = [];
1✔
91
                $localPos = 0;
1✔
92
                foreach ($lines as $line) {
1✔
93
                        $offsetMap[$localPos] = $line['offset'];
1✔
94
                        $localPos += strlen((string) $line['content']) + 1; // +1 for \n
1✔
95
                }
96

97
                // Fix positions in parsed content using offset map
98
                $this->fixPositions($parsed, $offsetMap, $lines);
1✔
99

100
                return new BlockQuoteNode(
1✔
101
                        $parsed,
1✔
102
                        Texy\Modifier::parse($mMod),
1✔
103
                        new Position($startOffset, $totalLength),
1✔
104
                );
105
        }
106

107

108
        /**
109
         * Fix positions in parsed content using offset mapping.
110
         * For content spanning multiple lines, adjusts offset and length.
111
         * @param array<int, int> $offsetMap [localLineStart => absoluteLineStart]
112
         * @param array<array{content: string, offset: int}> $lines
113
         */
114
        private function fixPositions(Texy\Node $node, array $offsetMap, array $lines): void
1✔
115
        {
116
                if ($node->position !== null) {
1✔
117
                        $localOffset = $node->position->offset;
1✔
118
                        $localLength = $node->position->length;
1✔
119

120
                        // Find which line this position starts on
121
                        $lineIndex = 0;
1✔
122
                        $lineStart = 0;
1✔
123
                        $lineKeys = array_keys($offsetMap);
1✔
124
                        foreach ($lineKeys as $i => $localLineStart) {
1✔
125
                                if ($localLineStart <= $localOffset) {
1✔
126
                                        $lineIndex = $i;
1✔
127
                                        $lineStart = $localLineStart;
1✔
128
                                } else {
129
                                        break;
1✔
130
                                }
131
                        }
132

133
                        $absoluteStart = $offsetMap[$lineStart];
1✔
134
                        $newOffset = $absoluteStart + ($localOffset - $lineStart);
1✔
135

136
                        // Check if content spans multiple lines
137
                        $localEnd = $localOffset + $localLength;
1✔
138
                        $endLineIndex = $lineIndex;
1✔
139
                        foreach ($lineKeys as $i => $localLineStart) {
1✔
140
                                if ($localLineStart < $localEnd) {
1✔
141
                                        $endLineIndex = $i;
1✔
142
                                }
143
                        }
144

145
                        if ($endLineIndex > $lineIndex) {
1✔
146
                                // Content spans multiple lines - calculate correct length
147
                                // Each line boundary in original has extra characters ("> ")
148
                                $extraChars = 0;
1✔
149
                                for ($i = $lineIndex + 1; $i <= $endLineIndex; $i++) {
1✔
150
                                        // Each new line in original has "> " prefix (2 chars) that's not in transformed
151
                                        $extraChars += 2;
1✔
152
                                }
153
                                $newLength = $localLength + $extraChars;
1✔
154
                        } else {
155
                                $newLength = $localLength;
1✔
156
                        }
157

158
                        $node->position = new Position($newOffset, $newLength);
1✔
159
                }
160

161
                foreach ($node->getNodes() as $child) {
1✔
162
                        $this->fixPositions($child, $offsetMap, $lines);
1✔
163
                }
164
        }
1✔
165
}
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