Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/markup/PhabricatorMarkupEngine.php
12241 views
1
<?php
2
3
/**
4
* Manages markup engine selection, configuration, application, caching and
5
* pipelining.
6
*
7
* @{class:PhabricatorMarkupEngine} can be used to render objects which
8
* implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
9
* way. For example, if you have a list of comments written in remarkup (and
10
* the objects implement the correct interface) you can render them by first
11
* building an engine and adding the fields with @{method:addObject}.
12
*
13
* $field = 'field:body'; // Field you want to render. Each object exposes
14
* // one or more fields of markup.
15
*
16
* $engine = new PhabricatorMarkupEngine();
17
* foreach ($comments as $comment) {
18
* $engine->addObject($comment, $field);
19
* }
20
*
21
* Now, call @{method:process} to perform the actual cache/rendering
22
* step. This is a heavyweight call which does batched data access and
23
* transforms the markup into output.
24
*
25
* $engine->process();
26
*
27
* Finally, do something with the results:
28
*
29
* $results = array();
30
* foreach ($comments as $comment) {
31
* $results[] = $engine->getOutput($comment, $field);
32
* }
33
*
34
* If you have a single object to render, you can use the convenience method
35
* @{method:renderOneObject}.
36
*
37
* @task markup Markup Pipeline
38
* @task engine Engine Construction
39
*/
40
final class PhabricatorMarkupEngine extends Phobject {
41
42
private $objects = array();
43
private $viewer;
44
private $contextObject;
45
private $version = 21;
46
private $engineCaches = array();
47
private $auxiliaryConfig = array();
48
49
private static $engineStack = array();
50
51
52
/* -( Markup Pipeline )---------------------------------------------------- */
53
54
55
/**
56
* Convenience method for pushing a single object through the markup
57
* pipeline.
58
*
59
* @param PhabricatorMarkupInterface The object to render.
60
* @param string The field to render.
61
* @param PhabricatorUser User viewing the markup.
62
* @param object A context object for policy checks
63
* @return string Marked up output.
64
* @task markup
65
*/
66
public static function renderOneObject(
67
PhabricatorMarkupInterface $object,
68
$field,
69
PhabricatorUser $viewer,
70
$context_object = null) {
71
return id(new PhabricatorMarkupEngine())
72
->setViewer($viewer)
73
->setContextObject($context_object)
74
->addObject($object, $field)
75
->process()
76
->getOutput($object, $field);
77
}
78
79
80
/**
81
* Queue an object for markup generation when @{method:process} is
82
* called. You can retrieve the output later with @{method:getOutput}.
83
*
84
* @param PhabricatorMarkupInterface The object to render.
85
* @param string The field to render.
86
* @return this
87
* @task markup
88
*/
89
public function addObject(PhabricatorMarkupInterface $object, $field) {
90
$key = $this->getMarkupFieldKey($object, $field);
91
$this->objects[$key] = array(
92
'object' => $object,
93
'field' => $field,
94
);
95
96
return $this;
97
}
98
99
100
/**
101
* Process objects queued with @{method:addObject}. You can then retrieve
102
* the output with @{method:getOutput}.
103
*
104
* @return this
105
* @task markup
106
*/
107
public function process() {
108
self::$engineStack[] = $this;
109
110
try {
111
$result = $this->execute();
112
} finally {
113
array_pop(self::$engineStack);
114
}
115
116
return $result;
117
}
118
119
public static function isRenderingEmbeddedContent() {
120
// See T13678. This prevents cycles when rendering embedded content that
121
// itself has remarkup fields.
122
return (count(self::$engineStack) > 1);
123
}
124
125
private function execute() {
126
$keys = array();
127
foreach ($this->objects as $key => $info) {
128
if (!isset($info['markup'])) {
129
$keys[] = $key;
130
}
131
}
132
133
if (!$keys) {
134
return $this;
135
}
136
137
$objects = array_select_keys($this->objects, $keys);
138
139
// Build all the markup engines. We need an engine for each field whether
140
// we have a cache or not, since we still need to postprocess the cache.
141
$engines = array();
142
foreach ($objects as $key => $info) {
143
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
144
$engines[$key]->setConfig('viewer', $this->viewer);
145
$engines[$key]->setConfig('contextObject', $this->contextObject);
146
147
foreach ($this->auxiliaryConfig as $aux_key => $aux_value) {
148
$engines[$key]->setConfig($aux_key, $aux_value);
149
}
150
}
151
152
// Load or build the preprocessor caches.
153
$blocks = $this->loadPreprocessorCaches($engines, $objects);
154
$blocks = mpull($blocks, 'getCacheData');
155
156
$this->engineCaches = $blocks;
157
158
// Finalize the output.
159
foreach ($objects as $key => $info) {
160
$engine = $engines[$key];
161
$field = $info['field'];
162
$object = $info['object'];
163
164
$output = $engine->postprocessText($blocks[$key]);
165
$output = $object->didMarkupText($field, $output, $engine);
166
$this->objects[$key]['output'] = $output;
167
}
168
169
return $this;
170
}
171
172
173
/**
174
* Get the output of markup processing for a field queued with
175
* @{method:addObject}. Before you can call this method, you must call
176
* @{method:process}.
177
*
178
* @param PhabricatorMarkupInterface The object to retrieve.
179
* @param string The field to retrieve.
180
* @return string Processed output.
181
* @task markup
182
*/
183
public function getOutput(PhabricatorMarkupInterface $object, $field) {
184
$key = $this->getMarkupFieldKey($object, $field);
185
$this->requireKeyProcessed($key);
186
187
return $this->objects[$key]['output'];
188
}
189
190
191
/**
192
* Retrieve engine metadata for a given field.
193
*
194
* @param PhabricatorMarkupInterface The object to retrieve.
195
* @param string The field to retrieve.
196
* @param string The engine metadata field to retrieve.
197
* @param wild Optional default value.
198
* @task markup
199
*/
200
public function getEngineMetadata(
201
PhabricatorMarkupInterface $object,
202
$field,
203
$metadata_key,
204
$default = null) {
205
206
$key = $this->getMarkupFieldKey($object, $field);
207
$this->requireKeyProcessed($key);
208
209
return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
210
}
211
212
213
/**
214
* @task markup
215
*/
216
private function requireKeyProcessed($key) {
217
if (empty($this->objects[$key])) {
218
throw new Exception(
219
pht(
220
"Call %s before using results (key = '%s').",
221
'addObject()',
222
$key));
223
}
224
225
if (!isset($this->objects[$key]['output'])) {
226
throw new PhutilInvalidStateException('process');
227
}
228
}
229
230
231
/**
232
* @task markup
233
*/
234
private function getMarkupFieldKey(
235
PhabricatorMarkupInterface $object,
236
$field) {
237
238
static $custom;
239
if ($custom === null) {
240
$custom = array_merge(
241
self::loadCustomInlineRules(),
242
self::loadCustomBlockRules());
243
244
$custom = mpull($custom, 'getRuleVersion', null);
245
ksort($custom);
246
$custom = PhabricatorHash::digestForIndex(serialize($custom));
247
}
248
249
return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
250
}
251
252
253
/**
254
* @task markup
255
*/
256
private function loadPreprocessorCaches(array $engines, array $objects) {
257
$blocks = array();
258
259
$use_cache = array();
260
foreach ($objects as $key => $info) {
261
if ($info['object']->shouldUseMarkupCache($info['field'])) {
262
$use_cache[$key] = true;
263
}
264
}
265
266
if ($use_cache) {
267
try {
268
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
269
'cacheKey IN (%Ls)',
270
array_keys($use_cache));
271
$blocks = mpull($blocks, null, 'getCacheKey');
272
} catch (Exception $ex) {
273
phlog($ex);
274
}
275
}
276
277
$is_readonly = PhabricatorEnv::isReadOnly();
278
279
foreach ($objects as $key => $info) {
280
// False check in case MySQL doesn't support unicode characters
281
// in the string (T1191), resulting in unserialize returning false.
282
if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
283
// If we already have a preprocessing cache, we don't need to rebuild
284
// it.
285
continue;
286
}
287
288
$text = $info['object']->getMarkupText($info['field']);
289
$data = $engines[$key]->preprocessText($text);
290
291
// NOTE: This is just debugging information to help sort out cache issues.
292
// If one machine is misconfigured and poisoning caches you can use this
293
// field to hunt it down.
294
295
$metadata = array(
296
'host' => php_uname('n'),
297
);
298
299
$blocks[$key] = id(new PhabricatorMarkupCache())
300
->setCacheKey($key)
301
->setCacheData($data)
302
->setMetadata($metadata);
303
304
if (isset($use_cache[$key]) && !$is_readonly) {
305
// This is just filling a cache and always safe, even on a read pathway.
306
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
307
$blocks[$key]->replace();
308
unset($unguarded);
309
}
310
}
311
312
return $blocks;
313
}
314
315
316
/**
317
* Set the viewing user. Used to implement object permissions.
318
*
319
* @param PhabricatorUser The viewing user.
320
* @return this
321
* @task markup
322
*/
323
public function setViewer(PhabricatorUser $viewer) {
324
$this->viewer = $viewer;
325
return $this;
326
}
327
328
/**
329
* Set the context object. Used to implement object permissions.
330
*
331
* @param The object in which context this remarkup is used.
332
* @return this
333
* @task markup
334
*/
335
public function setContextObject($object) {
336
$this->contextObject = $object;
337
return $this;
338
}
339
340
public function setAuxiliaryConfig($key, $value) {
341
// TODO: This is gross and should be removed. Avoid use.
342
$this->auxiliaryConfig[$key] = $value;
343
return $this;
344
}
345
346
347
/* -( Engine Construction )------------------------------------------------ */
348
349
350
351
/**
352
* @task engine
353
*/
354
public static function newManiphestMarkupEngine() {
355
return self::newMarkupEngine(array(
356
));
357
}
358
359
360
/**
361
* @task engine
362
*/
363
public static function newPhrictionMarkupEngine() {
364
return self::newMarkupEngine(array(
365
'header.generate-toc' => true,
366
));
367
}
368
369
370
/**
371
* @task engine
372
*/
373
public static function newPhameMarkupEngine() {
374
return self::newMarkupEngine(
375
array(
376
'macros' => false,
377
'uri.full' => true,
378
'uri.same-window' => true,
379
'uri.base' => PhabricatorEnv::getURI('/'),
380
));
381
}
382
383
384
/**
385
* @task engine
386
*/
387
public static function newFeedMarkupEngine() {
388
return self::newMarkupEngine(
389
array(
390
'macros' => false,
391
'youtube' => false,
392
));
393
}
394
395
/**
396
* @task engine
397
*/
398
public static function newCalendarMarkupEngine() {
399
return self::newMarkupEngine(array(
400
));
401
}
402
403
404
/**
405
* @task engine
406
*/
407
public static function newDifferentialMarkupEngine(array $options = array()) {
408
return self::newMarkupEngine(array(
409
'differential.diff' => idx($options, 'differential.diff'),
410
));
411
}
412
413
414
/**
415
* @task engine
416
*/
417
public static function newDiffusionMarkupEngine(array $options = array()) {
418
return self::newMarkupEngine(array(
419
'header.generate-toc' => true,
420
));
421
}
422
423
/**
424
* @task engine
425
*/
426
public static function getEngine($ruleset = 'default') {
427
static $engines = array();
428
if (isset($engines[$ruleset])) {
429
return $engines[$ruleset];
430
}
431
432
$engine = null;
433
switch ($ruleset) {
434
case 'default':
435
$engine = self::newMarkupEngine(array());
436
break;
437
case 'feed':
438
$engine = self::newMarkupEngine(array());
439
$engine->setConfig('autoplay.disable', true);
440
break;
441
case 'nolinebreaks':
442
$engine = self::newMarkupEngine(array());
443
$engine->setConfig('preserve-linebreaks', false);
444
break;
445
case 'diffusion-readme':
446
$engine = self::newMarkupEngine(array());
447
$engine->setConfig('preserve-linebreaks', false);
448
$engine->setConfig('header.generate-toc', true);
449
break;
450
case 'diviner':
451
$engine = self::newMarkupEngine(array());
452
$engine->setConfig('preserve-linebreaks', false);
453
// $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
454
$engine->setConfig('header.generate-toc', true);
455
break;
456
case 'extract':
457
// Engine used for reference/edge extraction. Turn off anything which
458
// is slow and doesn't change reference extraction.
459
$engine = self::newMarkupEngine(array());
460
$engine->setConfig('pygments.enabled', false);
461
break;
462
default:
463
throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset));
464
}
465
466
$engines[$ruleset] = $engine;
467
return $engine;
468
}
469
470
/**
471
* @task engine
472
*/
473
private static function getMarkupEngineDefaultConfiguration() {
474
return array(
475
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
476
'youtube' => PhabricatorEnv::getEnvConfig(
477
'remarkup.enable-embedded-youtube'),
478
'differential.diff' => null,
479
'header.generate-toc' => false,
480
'macros' => true,
481
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
482
'uri.allowed-protocols'),
483
'uri.full' => false,
484
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
485
'syntax-highlighter.engine'),
486
'preserve-linebreaks' => true,
487
);
488
}
489
490
491
/**
492
* @task engine
493
*/
494
public static function newMarkupEngine(array $options) {
495
$options += self::getMarkupEngineDefaultConfiguration();
496
497
$engine = new PhutilRemarkupEngine();
498
499
$engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
500
501
$engine->setConfig('pygments.enabled', $options['pygments']);
502
$engine->setConfig(
503
'uri.allowed-protocols',
504
$options['uri.allowed-protocols']);
505
$engine->setConfig('differential.diff', $options['differential.diff']);
506
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
507
$engine->setConfig(
508
'syntax-highlighter.engine',
509
$options['syntax-highlighter.engine']);
510
511
$style_map = id(new PhabricatorDefaultSyntaxStyle())
512
->getRemarkupStyleMap();
513
$engine->setConfig('phutil.codeblock.style-map', $style_map);
514
515
$engine->setConfig('uri.full', $options['uri.full']);
516
517
if (isset($options['uri.base'])) {
518
$engine->setConfig('uri.base', $options['uri.base']);
519
}
520
521
if (isset($options['uri.same-window'])) {
522
$engine->setConfig('uri.same-window', $options['uri.same-window']);
523
}
524
525
$rules = array();
526
$rules[] = new PhutilRemarkupEscapeRemarkupRule();
527
$rules[] = new PhutilRemarkupEvalRule();
528
$rules[] = new PhutilRemarkupMonospaceRule();
529
530
531
$rules[] = new PhutilRemarkupDocumentLinkRule();
532
$rules[] = new PhabricatorNavigationRemarkupRule();
533
$rules[] = new PhabricatorKeyboardRemarkupRule();
534
$rules[] = new PhabricatorConfigRemarkupRule();
535
536
if ($options['youtube']) {
537
$rules[] = new PhabricatorYoutubeRemarkupRule();
538
}
539
540
$rules[] = new PhabricatorIconRemarkupRule();
541
$rules[] = new PhabricatorEmojiRemarkupRule();
542
$rules[] = new PhabricatorHandleRemarkupRule();
543
544
$applications = PhabricatorApplication::getAllInstalledApplications();
545
foreach ($applications as $application) {
546
foreach ($application->getRemarkupRules() as $rule) {
547
$rules[] = $rule;
548
}
549
}
550
551
$rules[] = new PhutilRemarkupHyperlinkRule();
552
553
if ($options['macros']) {
554
$rules[] = new PhabricatorImageMacroRemarkupRule();
555
$rules[] = new PhabricatorMemeRemarkupRule();
556
}
557
558
$rules[] = new PhutilRemarkupBoldRule();
559
$rules[] = new PhutilRemarkupItalicRule();
560
$rules[] = new PhutilRemarkupDelRule();
561
$rules[] = new PhutilRemarkupUnderlineRule();
562
$rules[] = new PhutilRemarkupHighlightRule();
563
$rules[] = new PhutilRemarkupAnchorRule();
564
565
foreach (self::loadCustomInlineRules() as $rule) {
566
$rules[] = clone $rule;
567
}
568
569
$blocks = array();
570
$blocks[] = new PhutilRemarkupQuotesBlockRule();
571
$blocks[] = new PhutilRemarkupReplyBlockRule();
572
$blocks[] = new PhutilRemarkupLiteralBlockRule();
573
$blocks[] = new PhutilRemarkupHeaderBlockRule();
574
$blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
575
$blocks[] = new PhutilRemarkupListBlockRule();
576
$blocks[] = new PhutilRemarkupCodeBlockRule();
577
$blocks[] = new PhutilRemarkupNoteBlockRule();
578
$blocks[] = new PhutilRemarkupTableBlockRule();
579
$blocks[] = new PhutilRemarkupSimpleTableBlockRule();
580
$blocks[] = new PhutilRemarkupInterpreterBlockRule();
581
$blocks[] = new PhutilRemarkupDefaultBlockRule();
582
583
foreach (self::loadCustomBlockRules() as $rule) {
584
$blocks[] = $rule;
585
}
586
587
foreach ($blocks as $block) {
588
$block->setMarkupRules($rules);
589
}
590
591
$engine->setBlockRules($blocks);
592
593
return $engine;
594
}
595
596
public static function extractPHIDsFromMentions(
597
PhabricatorUser $viewer,
598
array $content_blocks) {
599
600
$mentions = array();
601
602
$engine = self::newDifferentialMarkupEngine();
603
$engine->setConfig('viewer', $viewer);
604
605
foreach ($content_blocks as $content_block) {
606
if ($content_block === null) {
607
continue;
608
}
609
610
if (!strlen($content_block)) {
611
continue;
612
}
613
614
$engine->markupText($content_block);
615
$phids = $engine->getTextMetadata(
616
PhabricatorMentionRemarkupRule::KEY_MENTIONED,
617
array());
618
$mentions += $phids;
619
}
620
621
return $mentions;
622
}
623
624
public static function extractFilePHIDsFromEmbeddedFiles(
625
PhabricatorUser $viewer,
626
array $content_blocks) {
627
$files = array();
628
629
$engine = self::newDifferentialMarkupEngine();
630
$engine->setConfig('viewer', $viewer);
631
632
foreach ($content_blocks as $content_block) {
633
$engine->markupText($content_block);
634
$phids = $engine->getTextMetadata(
635
PhabricatorEmbedFileRemarkupRule::KEY_ATTACH_INTENT_FILE_PHIDS,
636
array());
637
foreach ($phids as $phid) {
638
$files[$phid] = $phid;
639
}
640
}
641
642
return array_values($files);
643
}
644
645
public static function summarizeSentence($corpus) {
646
$corpus = trim($corpus);
647
$blocks = preg_split('/\n+/', $corpus, 2);
648
$block = head($blocks);
649
650
$sentences = preg_split(
651
'/\b([.?!]+)\B/u',
652
$block,
653
2,
654
PREG_SPLIT_DELIM_CAPTURE);
655
656
if (count($sentences) > 1) {
657
$result = $sentences[0].$sentences[1];
658
} else {
659
$result = head($sentences);
660
}
661
662
return id(new PhutilUTF8StringTruncator())
663
->setMaximumGlyphs(128)
664
->truncateString($result);
665
}
666
667
/**
668
* Produce a corpus summary, in a way that shortens the underlying text
669
* without truncating it somewhere awkward.
670
*
671
* TODO: We could do a better job of this.
672
*
673
* @param string Remarkup corpus to summarize.
674
* @return string Summarized corpus.
675
*/
676
public static function summarize($corpus) {
677
678
// Major goals here are:
679
// - Don't split in the middle of a character (utf-8).
680
// - Don't split in the middle of, e.g., **bold** text, since
681
// we end up with hanging '**' in the summary.
682
// - Try not to pick an image macro, header, embedded file, etc.
683
// - Hopefully don't return too much text. We don't explicitly limit
684
// this right now.
685
686
$blocks = preg_split("/\n *\n\s*/", $corpus);
687
688
$best = null;
689
foreach ($blocks as $block) {
690
// This is a test for normal spaces in the block, i.e. a heuristic to
691
// distinguish standard paragraphs from things like image macros. It may
692
// not work well for non-latin text. We prefer to summarize with a
693
// paragraph of normal words over an image macro, if possible.
694
$has_space = preg_match('/\w\s\w/', $block);
695
696
// This is a test to find embedded images and headers. We prefer to
697
// summarize with a normal paragraph over a header or an embedded object,
698
// if possible.
699
$has_embed = preg_match('/^[{=]/', $block);
700
701
if ($has_space && !$has_embed) {
702
// This seems like a good summary, so return it.
703
return $block;
704
}
705
706
if (!$best) {
707
// This is the first block we found; if everything is garbage just
708
// use the first block.
709
$best = $block;
710
}
711
}
712
713
return $best;
714
}
715
716
private static function loadCustomInlineRules() {
717
return id(new PhutilClassMapQuery())
718
->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
719
->execute();
720
}
721
722
private static function loadCustomBlockRules() {
723
return id(new PhutilClassMapQuery())
724
->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
725
->execute();
726
}
727
728
public static function digestRemarkupContent($object, $content) {
729
$parts = array();
730
$parts[] = get_class($object);
731
732
if ($object instanceof PhabricatorLiskDAO) {
733
$parts[] = $object->getID();
734
}
735
736
$parts[] = $content;
737
738
$message = implode("\n", $parts);
739
740
return PhabricatorHash::digestWithNamedKey($message, 'remarkup');
741
}
742
743
}
744
745