Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
12241 views
1
<?php
2
3
abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule {
4
5
private $referencePattern;
6
private $embedPattern;
7
8
const KEY_RULE_OBJECT = 'rule.object';
9
const KEY_MENTIONED_OBJECTS = 'rule.object.mentioned';
10
11
abstract protected function getObjectNamePrefix();
12
abstract protected function loadObjects(array $ids);
13
14
public function getPriority() {
15
return 450.0;
16
}
17
18
protected function getObjectNamePrefixBeginsWithWordCharacter() {
19
$prefix = $this->getObjectNamePrefix();
20
return preg_match('/^\w/', $prefix);
21
}
22
23
protected function getObjectIDPattern() {
24
return '[1-9]\d*';
25
}
26
27
protected function shouldMarkupObject(array $params) {
28
return true;
29
}
30
31
protected function getObjectNameText(
32
$object,
33
PhabricatorObjectHandle $handle,
34
$id) {
35
return $this->getObjectNamePrefix().$id;
36
}
37
38
protected function loadHandles(array $objects) {
39
$phids = mpull($objects, 'getPHID');
40
41
$viewer = $this->getEngine()->getConfig('viewer');
42
$handles = $viewer->loadHandles($phids);
43
$handles = iterator_to_array($handles);
44
45
$result = array();
46
foreach ($objects as $id => $object) {
47
$result[$id] = $handles[$object->getPHID()];
48
}
49
return $result;
50
}
51
52
protected function getObjectHref(
53
$object,
54
PhabricatorObjectHandle $handle,
55
$id) {
56
57
$uri = $handle->getURI();
58
59
if ($this->getEngine()->getConfig('uri.full')) {
60
$uri = PhabricatorEnv::getURI($uri);
61
}
62
63
return $uri;
64
}
65
66
protected function renderObjectRefForAnyMedia(
67
$object,
68
PhabricatorObjectHandle $handle,
69
$anchor,
70
$id) {
71
72
$href = $this->getObjectHref($object, $handle, $id);
73
$text = $this->getObjectNameText($object, $handle, $id);
74
75
if ($anchor) {
76
$href = $href.'#'.$anchor;
77
$text = $text.'#'.$anchor;
78
}
79
80
if ($this->getEngine()->isTextMode()) {
81
return $text.' <'.PhabricatorEnv::getProductionURI($href).'>';
82
} else if ($this->getEngine()->isHTMLMailMode()) {
83
$href = PhabricatorEnv::getProductionURI($href);
84
return $this->renderObjectTagForMail($text, $href, $handle);
85
}
86
87
return $this->renderObjectRef($object, $handle, $anchor, $id);
88
89
}
90
91
protected function renderObjectRef(
92
$object,
93
PhabricatorObjectHandle $handle,
94
$anchor,
95
$id) {
96
97
$href = $this->getObjectHref($object, $handle, $id);
98
$text = $this->getObjectNameText($object, $handle, $id);
99
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
100
101
if ($anchor) {
102
$href = $href.'#'.$anchor;
103
$text = $text.'#'.$anchor;
104
}
105
106
$attr = array(
107
'phid' => $handle->getPHID(),
108
'closed' => ($handle->getStatus() == $status_closed),
109
);
110
111
return $this->renderHovertag($text, $href, $attr);
112
}
113
114
protected function renderObjectEmbedForAnyMedia(
115
$object,
116
PhabricatorObjectHandle $handle,
117
$options) {
118
119
$name = $handle->getFullName();
120
$href = $handle->getURI();
121
122
if ($this->getEngine()->isTextMode()) {
123
return $name.' <'.PhabricatorEnv::getProductionURI($href).'>';
124
} else if ($this->getEngine()->isHTMLMailMode()) {
125
$href = PhabricatorEnv::getProductionURI($href);
126
return $this->renderObjectTagForMail($name, $href, $handle);
127
}
128
129
// See T13678. If we're already rendering embedded content, render a
130
// default reference instead to avoid cycles.
131
if (PhabricatorMarkupEngine::isRenderingEmbeddedContent()) {
132
return $this->renderDefaultObjectEmbed($object, $handle);
133
}
134
135
return $this->renderObjectEmbed($object, $handle, $options);
136
}
137
138
protected function renderObjectEmbed(
139
$object,
140
PhabricatorObjectHandle $handle,
141
$options) {
142
return $this->renderDefaultObjectEmbed($object, $handle);
143
}
144
145
final protected function renderDefaultObjectEmbed(
146
$object,
147
PhabricatorObjectHandle $handle) {
148
149
$name = $handle->getFullName();
150
$href = $handle->getURI();
151
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
152
$attr = array(
153
'phid' => $handle->getPHID(),
154
'closed' => ($handle->getStatus() == $status_closed),
155
);
156
157
return $this->renderHovertag($name, $href, $attr);
158
}
159
160
protected function renderObjectTagForMail(
161
$text,
162
$href,
163
PhabricatorObjectHandle $handle) {
164
165
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
166
$strikethrough = $handle->getStatus() == $status_closed ?
167
'text-decoration: line-through;' :
168
'text-decoration: none;';
169
170
return phutil_tag(
171
'a',
172
array(
173
'href' => $href,
174
'style' => 'background-color: #e7e7e7;
175
border-color: #e7e7e7;
176
border-radius: 3px;
177
padding: 0 4px;
178
font-weight: bold;
179
color: black;'
180
.$strikethrough,
181
),
182
$text);
183
}
184
185
protected function renderHovertag($name, $href, array $attr = array()) {
186
return id(new PHUITagView())
187
->setName($name)
188
->setHref($href)
189
->setType(PHUITagView::TYPE_OBJECT)
190
->setPHID(idx($attr, 'phid'))
191
->setClosed(idx($attr, 'closed'))
192
->render();
193
}
194
195
public function apply($text) {
196
$text = preg_replace_callback(
197
$this->getObjectEmbedPattern(),
198
array($this, 'markupObjectEmbed'),
199
$text);
200
201
$text = preg_replace_callback(
202
$this->getObjectReferencePattern(),
203
array($this, 'markupObjectReference'),
204
$text);
205
206
return $text;
207
}
208
209
private function getObjectEmbedPattern() {
210
if ($this->embedPattern === null) {
211
$prefix = $this->getObjectNamePrefix();
212
$prefix = preg_quote($prefix);
213
$id = $this->getObjectIDPattern();
214
215
$this->embedPattern =
216
'(\B{'.$prefix.'('.$id.')([,\s](?:[^}\\\\]|\\\\.)*)?}\B)u';
217
}
218
219
return $this->embedPattern;
220
}
221
222
private function getObjectReferencePattern() {
223
if ($this->referencePattern === null) {
224
$prefix = $this->getObjectNamePrefix();
225
$prefix = preg_quote($prefix);
226
227
$id = $this->getObjectIDPattern();
228
229
// If the prefix starts with a word character (like "D"), we want to
230
// require a word boundary so that we don't match "XD1" as "D1". If the
231
// prefix does not start with a word character, we want to require no word
232
// boundary for the same reasons. Test if the prefix starts with a word
233
// character.
234
if ($this->getObjectNamePrefixBeginsWithWordCharacter()) {
235
$boundary = '\\b';
236
} else {
237
$boundary = '\\B';
238
}
239
240
// The "(?<![#@-])" prevents us from linking "#abcdef" or similar, and
241
// "ABC-T1" (see T5714), and from matching "@T1" as a task (it is a user)
242
// (see T9479).
243
244
// The "\b" allows us to link "(abcdef)" or similar without linking things
245
// in the middle of words.
246
247
$this->referencePattern =
248
'((?<![#@-])'.$boundary.$prefix.'('.$id.')(?:#([-\w\d]+))?(?!\w))u';
249
}
250
251
return $this->referencePattern;
252
}
253
254
255
/**
256
* Extract matched object references from a block of text.
257
*
258
* This is intended to make it easy to write unit tests for object remarkup
259
* rules. Production code is not normally expected to call this method.
260
*
261
* @param string Text to match rules against.
262
* @return wild Matches, suitable for writing unit tests against.
263
*/
264
public function extractReferences($text) {
265
$embed_matches = null;
266
preg_match_all(
267
$this->getObjectEmbedPattern(),
268
$text,
269
$embed_matches,
270
PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
271
272
$ref_matches = null;
273
preg_match_all(
274
$this->getObjectReferencePattern(),
275
$text,
276
$ref_matches,
277
PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
278
279
$results = array();
280
$sets = array(
281
'embed' => $embed_matches,
282
'ref' => $ref_matches,
283
);
284
foreach ($sets as $type => $matches) {
285
$formatted = array();
286
foreach ($matches as $match) {
287
$format = array(
288
'offset' => $match[1][1],
289
'id' => $match[1][0],
290
);
291
if (isset($match[2][0])) {
292
$format['tail'] = $match[2][0];
293
}
294
$formatted[] = $format;
295
}
296
$results[$type] = $formatted;
297
}
298
299
return $results;
300
}
301
302
public function markupObjectEmbed(array $matches) {
303
if (!$this->isFlatText($matches[0])) {
304
return $matches[0];
305
}
306
307
// If we're rendering a table of contents, just render the raw input.
308
// This could perhaps be handled more gracefully but it seems unusual to
309
// put something like "{P123}" in a header and it's not obvious what users
310
// expect? See T8845.
311
$engine = $this->getEngine();
312
if ($engine->getState('toc')) {
313
return $matches[0];
314
}
315
316
return $this->markupObject(array(
317
'type' => 'embed',
318
'id' => $matches[1],
319
'options' => idx($matches, 2),
320
'original' => $matches[0],
321
'quote.depth' => $engine->getQuoteDepth(),
322
));
323
}
324
325
public function markupObjectReference(array $matches) {
326
if (!$this->isFlatText($matches[0])) {
327
return $matches[0];
328
}
329
330
// If we're rendering a table of contents, just render the monogram.
331
$engine = $this->getEngine();
332
if ($engine->getState('toc')) {
333
return $matches[0];
334
}
335
336
return $this->markupObject(array(
337
'type' => 'ref',
338
'id' => $matches[1],
339
'anchor' => idx($matches, 2),
340
'original' => $matches[0],
341
'quote.depth' => $engine->getQuoteDepth(),
342
));
343
}
344
345
private function markupObject(array $params) {
346
if (!$this->shouldMarkupObject($params)) {
347
return $params['original'];
348
}
349
350
$regex = trim(
351
PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names'));
352
if ($regex && preg_match($regex, $params['original'])) {
353
return $params['original'];
354
}
355
356
$engine = $this->getEngine();
357
$token = $engine->storeText('x');
358
359
$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
360
$metadata = $engine->getTextMetadata($metadata_key, array());
361
362
$metadata[] = array(
363
'token' => $token,
364
) + $params;
365
366
$engine->setTextMetadata($metadata_key, $metadata);
367
368
return $token;
369
}
370
371
public function didMarkupText() {
372
$engine = $this->getEngine();
373
$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
374
$metadata = $engine->getTextMetadata($metadata_key, array());
375
376
if (!$metadata) {
377
return;
378
}
379
380
381
$ids = ipull($metadata, 'id');
382
$objects = $this->loadObjects($ids);
383
384
// For objects that are invalid or which the user can't see, just render
385
// the original text.
386
387
// TODO: We should probably distinguish between these cases and render a
388
// "you can't see this" state for nonvisible objects.
389
390
foreach ($metadata as $key => $spec) {
391
if (empty($objects[$spec['id']])) {
392
$engine->overwriteStoredText(
393
$spec['token'],
394
$spec['original']);
395
unset($metadata[$key]);
396
}
397
}
398
399
$phids = $engine->getTextMetadata(self::KEY_MENTIONED_OBJECTS, array());
400
foreach ($objects as $object) {
401
$phids[$object->getPHID()] = $object->getPHID();
402
}
403
$engine->setTextMetadata(self::KEY_MENTIONED_OBJECTS, $phids);
404
405
$handles = $this->loadHandles($objects);
406
foreach ($metadata as $key => $spec) {
407
$handle = $handles[$spec['id']];
408
$object = $objects[$spec['id']];
409
switch ($spec['type']) {
410
case 'ref':
411
412
$view = $this->renderObjectRefForAnyMedia(
413
$object,
414
$handle,
415
$spec['anchor'],
416
$spec['id']);
417
break;
418
case 'embed':
419
$spec['options'] = $this->assertFlatText($spec['options']);
420
$view = $this->renderObjectEmbedForAnyMedia(
421
$object,
422
$handle,
423
$spec['options']);
424
break;
425
}
426
$engine->overwriteStoredText($spec['token'], $view);
427
}
428
429
$engine->setTextMetadata($metadata_key, array());
430
}
431
432
}
433
434