1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
<?php
namespace SourceSpan\Highlighter;
use SourceSpan\SimpleSourceLocation;
use SourceSpan\SimpleSourceSpanWithContext;
use SourceSpan\SourceSpan;
use SourceSpan\SourceSpanWithContext;
use SourceSpan\Util;
/**
* Information about how to highlight a single section of a source file.
*
* @internal
*/
final class Highlight
{
/**
* The section of the source file to highlight.
*
* This is normalized to make it easier for {@see Highlighter} to work with.
*/
public readonly SourceSpanWithContext $span;
/**
* The label to include inline when highlighting {@see $span}.
*
* This helps distinguish clarify what each highlight means when multiple are
* used in the same message.
*/
public readonly ?string $label;
public function __construct(
SourceSpan $span,
private readonly bool $primary = false,
?string $label = null,
) {
$this->span = self::normalizeSpan($span);
$this->label = $label === null ? null : str_replace("\r\n", "\n", $label);
}
/**
* Whether this is the primary span in the highlight.
*
* The primary span is highlighted with a different character than
* non-primary spans.
*/
public function isPrimary(): bool
{
return $this->primary;
}
private static function normalizeSpan(SourceSpan $span): SourceSpanWithContext
{
$newSpan = self::normalizeContext($span);
$newSpan = self::normalizeNewlines($newSpan);
$newSpan = self::normalizeTrailingNewline($newSpan);
return self::normalizeEndOfLine($newSpan);
}
/**
* Normalizes $span to ensure that it's a {@see SourceSpanWithContext} whose
* context actually contains its text at the expected column.
*
* If it's not already a {@see SourceSpanWithContext}, adjust the start and end
* locations' line and column fields so that the highlighter can assume they
* match up with the context.
*/
private static function normalizeContext(SourceSpan $span): SourceSpanWithContext
{
if ($span instanceof SourceSpanWithContext && Util::findLineStart($span->getContext(), $span->getText(), $span->getStart()->getColumn()) !== null) {
return $span;
}
return new SimpleSourceSpanWithContext(
new SimpleSourceLocation($span->getStart()->getOffset(), $span->getSourceUrl(), 0, 0),
new SimpleSourceLocation($span->getEnd()->getOffset(), $span->getSourceUrl(), substr_count($span->getText(), "\n"), self::lastLineLength($span->getText())),
$span->getText(),
$span->getText()
);
}
/**
* Normalizes $span to replace Windows-style newlines with Unix-style
* newlines.
*/
private static function normalizeNewlines(SourceSpanWithContext $span): SourceSpanWithContext
{
$text = $span->getText();
if (!str_contains($text, "\r\n")) {
return $span;
}
$endOffset = $span->getEnd()->getOffset() - substr_count($text, "\r\n");
return new SimpleSourceSpanWithContext(
$span->getStart(),
new SimpleSourceLocation($endOffset, $span->getSourceUrl(), $span->getEnd()->getLine(), $span->getEnd()->getColumn()),
str_replace("\r\n", "\n", $text),
str_replace("\r\n", "\n", $span->getContext())
);
}
/**
* Normalizes $span to remove a trailing newline from `$span->getContext()`.
*
* If necessary, also adjust `$span->getEnd()` so that it doesn't point past where
* the trailing newline used to be.
*/
private static function normalizeTrailingNewline(SourceSpanWithContext $span): SourceSpanWithContext
{
if (!str_ends_with($span->getContext(), "\n")) {
return $span;
}
// If there's a full blank line on the end of `$span->getContext()`, it's probably
// significant, so we shouldn't trim it.
if (str_ends_with($span->getText(), "\n\n")) {
return $span;
}
$context = substr($span->getContext(), 0, -1);
$text = $span->getText();
$start = $span->getStart();
$end = $span->getEnd();
if (str_ends_with($text, "\n") && self::isTextAtEndOfContext($span)) {
$text = substr($text, 0, -1);
if ($text === '') {
$end = $start;
} else {
$end = new SimpleSourceLocation(
$end->getOffset() - 1,
$span->getSourceUrl(),
$end->getLine() - 1,
self::lastLineLength($context)
);
$start = $span->getStart()->getOffset() === $span->getEnd()->getOffset() ? $end : $span->getStart();
}
}
return new SimpleSourceSpanWithContext($start, $end, $text, $context);
}
/**
* Normalizes $span so that the end location is at the end of a line rather
* than at the beginning of the next line.
*/
private static function normalizeEndOfLine(SourceSpanWithContext $span): SourceSpanWithContext
{
if ($span->getEnd()->getColumn() !== 0) {
return $span;
}
if ($span->getEnd()->getLine() === $span->getStart()->getLine()) {
return $span;
}
$text = substr($span->getText(), 0, -1);
return new SimpleSourceSpanWithContext(
$span->getStart(),
new SimpleSourceLocation(
$span->getEnd()->getOffset() - 1,
$span->getSourceUrl(),
$span->getEnd()->getLine() - 1,
\strlen($text) - Util::lastIndexOf($text, "\n") - 1
),
$text,
// If the context also ends with a newline, it's possible that we don't
// have the full context for that line, so we shouldn't print it at all.
str_ends_with($span->getContext(), "\n") ? substr($span->getContext(), 0, -1) : $span->getContext()
);
}
/**
* Returns the length of the last line in $text, whether or not it ends in a
* newline.
*/
private static function lastLineLength(string $text): int
{
if ($text === '') {
return 0;
}
if ($text[\strlen($text) - 1] === '\n') {
return \strlen($text) === 1 ? 0 : \strlen($text) - Util::lastIndexOf($text, "\n", \strlen($text) - 2) - 1;
}
return \strlen($text) - Util::lastIndexOf($text, "\n") - 1;
}
/**
* Returns whether $span's text runs all the way to the end of its context.
*/
private static function isTextAtEndOfContext(SourceSpanWithContext $span): bool
{
$lineStart = Util::findLineStart($span->getContext(), $span->getText(), $span->getStart()->getColumn());
\assert($lineStart !== null);
return $lineStart + $span->getStart()->getColumn() + $span->getLength() === \strlen($span->getContext());
}
}
|