Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
index 8e2d0cf0c9..9f37bdcaab 100644
--- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
@@ -1,327 +1,358 @@
<?php
final class PhabricatorEmbedFileRemarkupRule
extends PhabricatorObjectRemarkupRule {
private $viewer;
- const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids';
+ const KEY_ATTACH_INTENT_FILE_PHIDS = 'files.attach-intent';
protected function getObjectNamePrefix() {
return 'F';
}
protected function loadObjects(array $ids) {
$engine = $this->getEngine();
$this->viewer = $engine->getConfig('viewer');
$objects = id(new PhabricatorFileQuery())
->setViewer($this->viewer)
->withIDs($ids)
->needTransforms(
array(
PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW,
))
->execute();
+ $objects = mpull($objects, null, 'getID');
- $phids_key = self::KEY_EMBED_FILE_PHIDS;
- $phids = $engine->getTextMetadata($phids_key, array());
- foreach (mpull($objects, 'getPHID') as $phid) {
- $phids[] = $phid;
+
+ // Identify files embedded in the block with "attachment intent", i.e.
+ // those files which the user appears to want to attach to the object.
+ // Files referenced inside quoted blocks are not considered to have this
+ // attachment intent.
+
+ $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
+ $metadata = $engine->getTextMetadata($metadata_key, array());
+
+ $attach_key = self::KEY_ATTACH_INTENT_FILE_PHIDS;
+ $attach_phids = $engine->getTextMetadata($attach_key, array());
+
+ foreach ($metadata as $item) {
+
+ // If this reference was inside a quoted block, don't count it. Quoting
+ // someone else doesn't establish an intent to attach a file.
+ $depth = idx($item, 'quote.depth');
+ if ($depth > 0) {
+ continue;
+ }
+
+ $id = $item['id'];
+ $file = idx($objects, $id);
+
+ if (!$file) {
+ continue;
+ }
+
+ $attach_phids[] = $file->getPHID();
}
- $engine->setTextMetadata($phids_key, $phids);
+
+ $attach_phids = array_fuse($attach_phids);
+ $attach_phids = array_keys($attach_phids);
+
+ $engine->setTextMetadata($attach_key, $attach_phids);
+
return $objects;
}
protected function renderObjectEmbed(
$object,
PhabricatorObjectHandle $handle,
$options) {
$options = $this->getFileOptions($options) + array(
'name' => $object->getName(),
);
$is_viewable_image = $object->isViewableImage();
$is_audio = $object->isAudio();
$is_video = $object->isVideo();
$force_link = ($options['layout'] == 'link');
// If a file is both audio and video, as with "application/ogg" by default,
// render it as video but allow the user to specify `media=audio` if they
// want to force it to render as audio.
if ($is_audio && $is_video) {
$media = $options['media'];
if ($media == 'audio') {
$is_video = false;
} else {
$is_audio = false;
}
}
$options['viewable'] = ($is_viewable_image || $is_audio || $is_video);
if ($is_viewable_image && !$force_link) {
return $this->renderImageFile($object, $handle, $options);
} else if ($is_video && !$force_link) {
return $this->renderVideoFile($object, $handle, $options);
} else if ($is_audio && !$force_link) {
return $this->renderAudioFile($object, $handle, $options);
} else {
return $this->renderFileLink($object, $handle, $options);
}
}
private function getFileOptions($option_string) {
$options = array(
'size' => null,
'layout' => 'left',
'float' => false,
'width' => null,
'height' => null,
'alt' => null,
'media' => null,
'autoplay' => null,
'loop' => null,
);
if ($option_string) {
$option_string = trim($option_string, ', ');
$parser = new PhutilSimpleOptions();
$options = $parser->parse($option_string) + $options;
}
return $options;
}
private function renderImageFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
require_celerity_resource('phui-lightbox-css');
$attrs = array();
$image_class = 'phabricator-remarkup-embed-image';
$use_size = true;
if (!$options['size']) {
$width = $this->parseDimension($options['width']);
$height = $this->parseDimension($options['height']);
if ($width || $height) {
$use_size = false;
$attrs += array(
'src' => $file->getBestURI(),
'width' => $width,
'height' => $height,
);
}
}
if ($use_size) {
switch ((string)$options['size']) {
case 'full':
$attrs += array(
'src' => $file->getBestURI(),
'height' => $file->getImageHeight(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-full';
break;
// Displays "full" in normal Remarkup, "wide" in Documents
case 'wide':
$attrs += array(
'src' => $file->getBestURI(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-wide';
break;
case 'thumb':
default:
$preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW;
$xform = PhabricatorFileTransform::getTransformByKey($preview_key);
$existing_xform = $file->getTransform($preview_key);
if ($existing_xform) {
$xform_uri = $existing_xform->getCDNURI('data');
} else {
$xform_uri = $file->getURIForTransform($xform);
}
$attrs['src'] = $xform_uri;
$dimensions = $xform->getTransformedDimensions($file);
if ($dimensions) {
list($x, $y) = $dimensions;
$attrs['width'] = $x;
$attrs['height'] = $y;
}
break;
}
}
$alt = null;
if (isset($options['alt'])) {
$alt = $options['alt'];
}
if (!strlen($alt)) {
$alt = $file->getAltText();
}
$attrs['alt'] = $alt;
$img = phutil_tag('img', $attrs);
$embed = javelin_tag(
'a',
array(
'href' => $file->getBestURI(),
'class' => $image_class,
'sigil' => 'lightboxable',
'meta' => array(
'phid' => $file->getPHID(),
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'alt' => $alt,
'viewable' => true,
'monogram' => $file->getMonogram(),
),
),
$img);
switch ($options['layout']) {
case 'right':
case 'center':
case 'inline':
case 'left':
$layout_class = 'phabricator-remarkup-embed-layout-'.$options['layout'];
break;
default:
$layout_class = 'phabricator-remarkup-embed-layout-left';
break;
}
if ($options['float']) {
switch ($options['layout']) {
case 'center':
case 'inline':
break;
case 'right':
$layout_class .= ' phabricator-remarkup-embed-float-right';
break;
case 'left':
default:
$layout_class .= ' phabricator-remarkup-embed-float-left';
break;
}
}
return phutil_tag(
($options['layout'] == 'inline' ? 'span' : 'div'),
array(
'class' => $layout_class,
),
$embed);
}
private function renderAudioFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return $this->renderMediaFile('audio', $file, $handle, $options);
}
private function renderVideoFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return $this->renderMediaFile('video', $file, $handle, $options);
}
private function renderMediaFile(
$tag,
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
$is_video = ($tag == 'video');
if (idx($options, 'autoplay')) {
$preload = 'auto';
$autoplay = 'autoplay';
} else {
// If we don't preload video, the user can't see the first frame and
// has no clue what they're looking at, so always preload.
if ($is_video) {
$preload = 'auto';
} else {
$preload = 'none';
}
$autoplay = null;
}
// Rendering contexts like feed can disable autoplay.
$engine = $this->getEngine();
if ($engine->getConfig('autoplay.disable')) {
$autoplay = null;
}
if ($is_video) {
// See T13135. Chrome refuses to play videos with type "video/quicktime",
// even though it may actually be able to play them. The least awful fix
// based on available information is to simply omit the "type" attribute
// from `<source />` tags. This causes Chrome to try to play the video
// and realize it can, and does not appear to produce any bad behavior in
// any other browser.
$mime_type = null;
} else {
$mime_type = $file->getMimeType();
}
return $this->newTag(
$tag,
array(
'controls' => 'controls',
'preload' => $preload,
'autoplay' => $autoplay,
'loop' => idx($options, 'loop') ? 'loop' : null,
'alt' => $options['alt'],
'class' => 'phabricator-media',
),
$this->newTag(
'source',
array(
'src' => $file->getBestURI(),
'type' => $mime_type,
)));
}
private function renderFileLink(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return id(new PhabricatorFileLinkView())
->setViewer($this->viewer)
->setFilePHID($file->getPHID())
->setFileName($this->assertFlatText($options['name']))
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable((bool)$options['viewable'])
->setFileSize(phutil_format_bytes($file->getByteSize()))
->setFileMonogram($file->getMonogram());
}
private function parseDimension($string) {
$string = trim($string);
if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
return $string;
}
return null;
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index d897314245..cedd0398e3 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,743 +1,743 @@
<?php
/**
* Manages markup engine selection, configuration, application, caching and
* pipelining.
*
* @{class:PhabricatorMarkupEngine} can be used to render objects which
* implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
* way. For example, if you have a list of comments written in remarkup (and
* the objects implement the correct interface) you can render them by first
* building an engine and adding the fields with @{method:addObject}.
*
* $field = 'field:body'; // Field you want to render. Each object exposes
* // one or more fields of markup.
*
* $engine = new PhabricatorMarkupEngine();
* foreach ($comments as $comment) {
* $engine->addObject($comment, $field);
* }
*
* Now, call @{method:process} to perform the actual cache/rendering
* step. This is a heavyweight call which does batched data access and
* transforms the markup into output.
*
* $engine->process();
*
* Finally, do something with the results:
*
* $results = array();
* foreach ($comments as $comment) {
* $results[] = $engine->getOutput($comment, $field);
* }
*
* If you have a single object to render, you can use the convenience method
* @{method:renderOneObject}.
*
* @task markup Markup Pipeline
* @task engine Engine Construction
*/
final class PhabricatorMarkupEngine extends Phobject {
private $objects = array();
private $viewer;
private $contextObject;
private $version = 21;
private $engineCaches = array();
private $auxiliaryConfig = array();
private static $engineStack = array();
/* -( Markup Pipeline )---------------------------------------------------- */
/**
* Convenience method for pushing a single object through the markup
* pipeline.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @param PhabricatorUser User viewing the markup.
* @param object A context object for policy checks
* @return string Marked up output.
* @task markup
*/
public static function renderOneObject(
PhabricatorMarkupInterface $object,
$field,
PhabricatorUser $viewer,
$context_object = null) {
return id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->setContextObject($context_object)
->addObject($object, $field)
->process()
->getOutput($object, $field);
}
/**
* Queue an object for markup generation when @{method:process} is
* called. You can retrieve the output later with @{method:getOutput}.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @return this
* @task markup
*/
public function addObject(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->objects[$key] = array(
'object' => $object,
'field' => $field,
);
return $this;
}
/**
* Process objects queued with @{method:addObject}. You can then retrieve
* the output with @{method:getOutput}.
*
* @return this
* @task markup
*/
public function process() {
self::$engineStack[] = $this;
try {
$result = $this->execute();
} finally {
array_pop(self::$engineStack);
}
return $result;
}
public static function isRenderingEmbeddedContent() {
// See T13678. This prevents cycles when rendering embedded content that
// itself has remarkup fields.
return (count(self::$engineStack) > 1);
}
private function execute() {
$keys = array();
foreach ($this->objects as $key => $info) {
if (!isset($info['markup'])) {
$keys[] = $key;
}
}
if (!$keys) {
return $this;
}
$objects = array_select_keys($this->objects, $keys);
// Build all the markup engines. We need an engine for each field whether
// we have a cache or not, since we still need to postprocess the cache.
$engines = array();
foreach ($objects as $key => $info) {
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
$engines[$key]->setConfig('viewer', $this->viewer);
$engines[$key]->setConfig('contextObject', $this->contextObject);
foreach ($this->auxiliaryConfig as $aux_key => $aux_value) {
$engines[$key]->setConfig($aux_key, $aux_value);
}
}
// Load or build the preprocessor caches.
$blocks = $this->loadPreprocessorCaches($engines, $objects);
$blocks = mpull($blocks, 'getCacheData');
$this->engineCaches = $blocks;
// Finalize the output.
foreach ($objects as $key => $info) {
$engine = $engines[$key];
$field = $info['field'];
$object = $info['object'];
$output = $engine->postprocessText($blocks[$key]);
$output = $object->didMarkupText($field, $output, $engine);
$this->objects[$key]['output'] = $output;
}
return $this;
}
/**
* Get the output of markup processing for a field queued with
* @{method:addObject}. Before you can call this method, you must call
* @{method:process}.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @return string Processed output.
* @task markup
*/
public function getOutput(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return $this->objects[$key]['output'];
}
/**
* Retrieve engine metadata for a given field.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @param string The engine metadata field to retrieve.
* @param wild Optional default value.
* @task markup
*/
public function getEngineMetadata(
PhabricatorMarkupInterface $object,
$field,
$metadata_key,
$default = null) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
}
/**
* @task markup
*/
private function requireKeyProcessed($key) {
if (empty($this->objects[$key])) {
throw new Exception(
pht(
"Call %s before using results (key = '%s').",
'addObject()',
$key));
}
if (!isset($this->objects[$key]['output'])) {
throw new PhutilInvalidStateException('process');
}
}
/**
* @task markup
*/
private function getMarkupFieldKey(
PhabricatorMarkupInterface $object,
$field) {
static $custom;
if ($custom === null) {
$custom = array_merge(
self::loadCustomInlineRules(),
self::loadCustomBlockRules());
$custom = mpull($custom, 'getRuleVersion', null);
ksort($custom);
$custom = PhabricatorHash::digestForIndex(serialize($custom));
}
return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
}
/**
* @task markup
*/
private function loadPreprocessorCaches(array $engines, array $objects) {
$blocks = array();
$use_cache = array();
foreach ($objects as $key => $info) {
if ($info['object']->shouldUseMarkupCache($info['field'])) {
$use_cache[$key] = true;
}
}
if ($use_cache) {
try {
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
'cacheKey IN (%Ls)',
array_keys($use_cache));
$blocks = mpull($blocks, null, 'getCacheKey');
} catch (Exception $ex) {
phlog($ex);
}
}
$is_readonly = PhabricatorEnv::isReadOnly();
foreach ($objects as $key => $info) {
// False check in case MySQL doesn't support unicode characters
// in the string (T1191), resulting in unserialize returning false.
if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
// If we already have a preprocessing cache, we don't need to rebuild
// it.
continue;
}
$text = $info['object']->getMarkupText($info['field']);
$data = $engines[$key]->preprocessText($text);
// NOTE: This is just debugging information to help sort out cache issues.
// If one machine is misconfigured and poisoning caches you can use this
// field to hunt it down.
$metadata = array(
'host' => php_uname('n'),
);
$blocks[$key] = id(new PhabricatorMarkupCache())
->setCacheKey($key)
->setCacheData($data)
->setMetadata($metadata);
if (isset($use_cache[$key]) && !$is_readonly) {
// This is just filling a cache and always safe, even on a read pathway.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$blocks[$key]->replace();
unset($unguarded);
}
}
return $blocks;
}
/**
* Set the viewing user. Used to implement object permissions.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task markup
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Set the context object. Used to implement object permissions.
*
* @param The object in which context this remarkup is used.
* @return this
* @task markup
*/
public function setContextObject($object) {
$this->contextObject = $object;
return $this;
}
public function setAuxiliaryConfig($key, $value) {
// TODO: This is gross and should be removed. Avoid use.
$this->auxiliaryConfig[$key] = $value;
return $this;
}
/* -( Engine Construction )------------------------------------------------ */
/**
* @task engine
*/
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function newPhameMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'uri.full' => true,
'uri.same-window' => true,
'uri.base' => PhabricatorEnv::getURI('/'),
));
}
/**
* @task engine
*/
public static function newFeedMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'youtube' => false,
));
}
/**
* @task engine
*/
public static function newCalendarMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'differential.diff' => idx($options, 'differential.diff'),
));
}
/**
* @task engine
*/
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function getEngine($ruleset = 'default') {
static $engines = array();
if (isset($engines[$ruleset])) {
return $engines[$ruleset];
}
$engine = null;
switch ($ruleset) {
case 'default':
$engine = self::newMarkupEngine(array());
break;
case 'feed':
$engine = self::newMarkupEngine(array());
$engine->setConfig('autoplay.disable', true);
break;
case 'nolinebreaks':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
break;
case 'diffusion-readme':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
$engine->setConfig('header.generate-toc', true);
break;
case 'diviner':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
// $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
$engine->setConfig('header.generate-toc', true);
break;
case 'extract':
// Engine used for reference/edge extraction. Turn off anything which
// is slow and doesn't change reference extraction.
$engine = self::newMarkupEngine(array());
$engine->setConfig('pygments.enabled', false);
break;
default:
throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset));
}
$engines[$ruleset] = $engine;
return $engine;
}
/**
* @task engine
*/
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
'uri.full' => false,
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
'syntax-highlighter.engine'),
'preserve-linebreaks' => true,
);
}
/**
* @task engine
*/
public static function newMarkupEngine(array $options) {
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$engine->setConfig(
'syntax-highlighter.engine',
$options['syntax-highlighter.engine']);
$style_map = id(new PhabricatorDefaultSyntaxStyle())
->getRemarkupStyleMap();
$engine->setConfig('phutil.codeblock.style-map', $style_map);
$engine->setConfig('uri.full', $options['uri.full']);
if (isset($options['uri.base'])) {
$engine->setConfig('uri.base', $options['uri.base']);
}
if (isset($options['uri.same-window'])) {
$engine->setConfig('uri.same-window', $options['uri.same-window']);
}
$rules = array();
$rules[] = new PhutilRemarkupEscapeRemarkupRule();
$rules[] = new PhutilRemarkupEvalRule();
$rules[] = new PhutilRemarkupMonospaceRule();
$rules[] = new PhutilRemarkupDocumentLinkRule();
$rules[] = new PhabricatorNavigationRemarkupRule();
$rules[] = new PhabricatorKeyboardRemarkupRule();
$rules[] = new PhabricatorConfigRemarkupRule();
if ($options['youtube']) {
$rules[] = new PhabricatorYoutubeRemarkupRule();
}
$rules[] = new PhabricatorIconRemarkupRule();
$rules[] = new PhabricatorEmojiRemarkupRule();
$rules[] = new PhabricatorHandleRemarkupRule();
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
foreach ($application->getRemarkupRules() as $rule) {
$rules[] = $rule;
}
}
$rules[] = new PhutilRemarkupHyperlinkRule();
if ($options['macros']) {
$rules[] = new PhabricatorImageMacroRemarkupRule();
$rules[] = new PhabricatorMemeRemarkupRule();
}
$rules[] = new PhutilRemarkupBoldRule();
$rules[] = new PhutilRemarkupItalicRule();
$rules[] = new PhutilRemarkupDelRule();
$rules[] = new PhutilRemarkupUnderlineRule();
$rules[] = new PhutilRemarkupHighlightRule();
$rules[] = new PhutilRemarkupAnchorRule();
foreach (self::loadCustomInlineRules() as $rule) {
$rules[] = clone $rule;
}
$blocks = array();
$blocks[] = new PhutilRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupReplyBlockRule();
$blocks[] = new PhutilRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
$blocks[] = new PhutilRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupTableBlockRule();
$blocks[] = new PhutilRemarkupSimpleTableBlockRule();
$blocks[] = new PhutilRemarkupInterpreterBlockRule();
$blocks[] = new PhutilRemarkupDefaultBlockRule();
foreach (self::loadCustomBlockRules() as $rule) {
$blocks[] = $rule;
}
foreach ($blocks as $block) {
$block->setMarkupRules($rules);
}
$engine->setBlockRules($blocks);
return $engine;
}
public static function extractPHIDsFromMentions(
PhabricatorUser $viewer,
array $content_blocks) {
$mentions = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
if ($content_block === null) {
continue;
}
if (!strlen($content_block)) {
continue;
}
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorMentionRemarkupRule::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
public static function extractFilePHIDsFromEmbeddedFiles(
PhabricatorUser $viewer,
array $content_blocks) {
$files = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
- PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS,
+ PhabricatorEmbedFileRemarkupRule::KEY_ATTACH_INTENT_FILE_PHIDS,
array());
foreach ($phids as $phid) {
$files[$phid] = $phid;
}
}
return array_values($files);
}
public static function summarizeSentence($corpus) {
$corpus = trim($corpus);
$blocks = preg_split('/\n+/', $corpus, 2);
$block = head($blocks);
$sentences = preg_split(
'/\b([.?!]+)\B/u',
$block,
2,
PREG_SPLIT_DELIM_CAPTURE);
if (count($sentences) > 1) {
$result = $sentences[0].$sentences[1];
} else {
$result = head($sentences);
}
return id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($result);
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.
*
* TODO: We could do a better job of this.
*
* @param string Remarkup corpus to summarize.
* @return string Summarized corpus.
*/
public static function summarize($corpus) {
// Major goals here are:
// - Don't split in the middle of a character (utf-8).
// - Don't split in the middle of, e.g., **bold** text, since
// we end up with hanging '**' in the summary.
// - Try not to pick an image macro, header, embedded file, etc.
// - Hopefully don't return too much text. We don't explicitly limit
// this right now.
$blocks = preg_split("/\n *\n\s*/", $corpus);
$best = null;
foreach ($blocks as $block) {
// This is a test for normal spaces in the block, i.e. a heuristic to
// distinguish standard paragraphs from things like image macros. It may
// not work well for non-latin text. We prefer to summarize with a
// paragraph of normal words over an image macro, if possible.
$has_space = preg_match('/\w\s\w/', $block);
// This is a test to find embedded images and headers. We prefer to
// summarize with a normal paragraph over a header or an embedded object,
// if possible.
$has_embed = preg_match('/^[{=]/', $block);
if ($has_space && !$has_embed) {
// This seems like a good summary, so return it.
return $block;
}
if (!$best) {
// This is the first block we found; if everything is garbage just
// use the first block.
$best = $block;
}
}
return $best;
}
private static function loadCustomInlineRules() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
->execute();
}
private static function loadCustomBlockRules() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
->execute();
}
public static function digestRemarkupContent($object, $content) {
$parts = array();
$parts[] = get_class($object);
if ($object instanceof PhabricatorLiskDAO) {
$parts[] = $object->getID();
}
$parts[] = $content;
$message = implode("\n", $parts);
return PhabricatorHash::digestWithNamedKey($message, 'remarkup');
}
}
diff --git a/src/infrastructure/markup/blockrule/PhutilRemarkupBlockRule.php b/src/infrastructure/markup/blockrule/PhutilRemarkupBlockRule.php
index feac6cffa9..ac3a308d06 100644
--- a/src/infrastructure/markup/blockrule/PhutilRemarkupBlockRule.php
+++ b/src/infrastructure/markup/blockrule/PhutilRemarkupBlockRule.php
@@ -1,170 +1,178 @@
<?php
abstract class PhutilRemarkupBlockRule extends Phobject {
private $engine;
private $rules = array();
/**
* Determine the order in which blocks execute. Blocks with smaller priority
* numbers execute sooner than blocks with larger priority numbers. The
* default priority for blocks is `500`.
*
* Priorities are used to disambiguate syntax which can match multiple
* patterns. For example, ` - Lorem ipsum...` may be a code block or a
* list.
*
* @return int Priority at which this block should execute.
*/
public function getPriority() {
return 500;
}
final public function getPriorityVector() {
return id(new PhutilSortVector())
->addInt($this->getPriority())
->addString(get_class($this));
}
abstract public function markupText($text, $children);
/**
* This will get an array of unparsed lines and return the number of lines
* from the first array value that it can parse.
*
* @param array $lines
* @param int $cursor
*
* @return int
*/
abstract public function getMatchingLineCount(array $lines, $cursor);
protected function didMarkupText() {
return;
}
+ public function willMarkupChildBlocks() {
+ return;
+ }
+
+ public function didMarkupChildBlocks() {
+ return;
+ }
+
final public function setEngine(PhutilRemarkupEngine $engine) {
$this->engine = $engine;
$this->updateRules();
return $this;
}
final protected function getEngine() {
return $this->engine;
}
public function setMarkupRules(array $rules) {
assert_instances_of($rules, 'PhutilRemarkupRule');
$this->rules = $rules;
$this->updateRules();
return $this;
}
private function updateRules() {
$engine = $this->getEngine();
if ($engine) {
$this->rules = msort($this->rules, 'getPriority');
foreach ($this->rules as $rule) {
$rule->setEngine($engine);
}
}
return $this;
}
final public function getMarkupRules() {
return $this->rules;
}
final public function postprocess() {
$this->didMarkupText();
}
final protected function applyRules($text) {
foreach ($this->getMarkupRules() as $rule) {
$text = $rule->apply($text);
}
return $text;
}
public function supportsChildBlocks() {
return false;
}
public function extractChildText($text) {
throw new PhutilMethodNotImplementedException();
}
protected function renderRemarkupTable(array $out_rows) {
assert_instances_of($out_rows, 'array');
if ($this->getEngine()->isTextMode()) {
$lengths = array();
foreach ($out_rows as $r => $row) {
foreach ($row['content'] as $c => $cell) {
$text = $this->getEngine()->restoreText($cell['content']);
$lengths[$c][$r] = phutil_utf8_strlen($text);
}
}
$max_lengths = array_map('max', $lengths);
$out = array();
foreach ($out_rows as $r => $row) {
$headings = false;
foreach ($row['content'] as $c => $cell) {
$length = $max_lengths[$c] - $lengths[$c][$r];
$out[] = '| '.$cell['content'].str_repeat(' ', $length).' ';
if ($cell['type'] == 'th') {
$headings = true;
}
}
$out[] = "|\n";
if ($headings) {
foreach ($row['content'] as $c => $cell) {
$char = ($cell['type'] == 'th' ? '-' : ' ');
$out[] = '| '.str_repeat($char, $max_lengths[$c]).' ';
}
$out[] = "|\n";
}
}
return rtrim(implode('', $out), "\n");
}
if ($this->getEngine()->isHTMLMailMode()) {
$table_attributes = array(
'style' => 'border-collapse: separate;
border-spacing: 1px;
background: #d3d3d3;
margin: 12px 0;',
);
$cell_attributes = array(
'style' => 'background: #ffffff;
padding: 3px 6px;',
);
} else {
$table_attributes = array(
'class' => 'remarkup-table',
);
$cell_attributes = array();
}
$out = array();
$out[] = "\n";
foreach ($out_rows as $row) {
$cells = array();
foreach ($row['content'] as $cell) {
$cells[] = phutil_tag(
$cell['type'],
$cell_attributes,
$cell['content']);
}
$out[] = phutil_tag($row['type'], array(), $cells);
$out[] = "\n";
}
$table = phutil_tag('table', $table_attributes, $out);
return phutil_tag_div('remarkup-table-wrap', $table);
}
}
diff --git a/src/infrastructure/markup/blockrule/PhutilRemarkupQuotedBlockRule.php b/src/infrastructure/markup/blockrule/PhutilRemarkupQuotedBlockRule.php
index 9f2bd7a297..90c9a2c33a 100644
--- a/src/infrastructure/markup/blockrule/PhutilRemarkupQuotedBlockRule.php
+++ b/src/infrastructure/markup/blockrule/PhutilRemarkupQuotedBlockRule.php
@@ -1,110 +1,126 @@
<?php
abstract class PhutilRemarkupQuotedBlockRule
extends PhutilRemarkupBlockRule {
final public function supportsChildBlocks() {
return true;
}
+ public function willMarkupChildBlocks() {
+ $engine = $this->getEngine();
+
+ $depth = $engine->getQuoteDepth();
+ $depth = $depth + 1;
+ $engine->setQuoteDepth($depth);
+ }
+
+ public function didMarkupChildBlocks() {
+ $engine = $this->getEngine();
+
+ $depth = $engine->getQuoteDepth();
+ $depth = $depth - 1;
+ $engine->setQuoteDepth($depth);
+ }
+
final protected function normalizeQuotedBody($text) {
$text = phutil_split_lines($text, true);
foreach ($text as $key => $line) {
$text[$key] = substr($line, 1);
}
// If every line in the block is empty or begins with at least one leading
// space, strip the initial space off each line. When we quote text, we
// normally add "> " (with a space) to the beginning of each line, which
// can disrupt some other rules. If the block appears to have this space
// in front of each line, remove it.
$strip_space = true;
foreach ($text as $key => $line) {
$len = strlen($line);
if (!$len) {
// We'll still strip spaces if there are some completely empty
// lines, they may have just had trailing whitespace trimmed.
continue;
}
// If this line is part of a nested quote block, just ignore it when
// realigning this quote block. It's either an author attribution
// line with ">>!", or we'll deal with it in a subrule when processing
// the nested quote block.
if ($line[0] == '>') {
continue;
}
if ($line[0] == ' ' || $line[0] == "\n") {
continue;
}
// The first character of this line is something other than a space, so
// we can't strip spaces.
$strip_space = false;
break;
}
if ($strip_space) {
foreach ($text as $key => $line) {
$len = strlen($line);
if (!$len) {
continue;
}
if ($line[0] !== ' ') {
continue;
}
$text[$key] = substr($line, 1);
}
}
// Strip leading empty lines.
foreach ($text as $key => $line) {
if (!strlen(trim($line))) {
unset($text[$key]);
} else {
break;
}
}
return implode('', $text);
}
final protected function getQuotedText($text) {
$text = rtrim($text, "\n");
$no_whitespace = array(
// For readability, we render nested quotes as ">> quack",
// not "> > quack".
'>' => true,
// If the line is empty except for a newline, do not add an
// unnecessary dangling space.
"\n" => true,
);
$text = phutil_split_lines($text, true);
foreach ($text as $key => $line) {
$c = null;
if (isset($line[0])) {
$c = $line[0];
} else {
$c = null;
}
if (isset($no_whitespace[$c])) {
$text[$key] = '>'.$line;
} else {
$text[$key] = '> '.$line;
}
}
$text = implode('', $text);
return $text;
}
}
diff --git a/src/infrastructure/markup/remarkup/PhutilRemarkupEngine.php b/src/infrastructure/markup/remarkup/PhutilRemarkupEngine.php
index a0a379aaf9..bedbf0bab7 100644
--- a/src/infrastructure/markup/remarkup/PhutilRemarkupEngine.php
+++ b/src/infrastructure/markup/remarkup/PhutilRemarkupEngine.php
@@ -1,357 +1,371 @@
<?php
final class PhutilRemarkupEngine extends PhutilMarkupEngine {
const MODE_DEFAULT = 0;
const MODE_TEXT = 1;
const MODE_HTML_MAIL = 2;
const MAX_CHILD_DEPTH = 32;
private $blockRules = array();
private $config = array();
private $mode;
private $metadata = array();
private $states = array();
private $postprocessRules = array();
private $storage;
public function setConfig($key, $value) {
$this->config[$key] = $value;
return $this;
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
public function setMode($mode) {
$this->mode = $mode;
return $this;
}
public function isTextMode() {
return $this->mode & self::MODE_TEXT;
}
public function isAnchorMode() {
return $this->getState('toc');
}
public function isHTMLMailMode() {
return $this->mode & self::MODE_HTML_MAIL;
}
+ public function getQuoteDepth() {
+ return $this->getConfig('runtime.quote.depth', 0);
+ }
+
+ public function setQuoteDepth($depth) {
+ return $this->setConfig('runtime.quote.depth', $depth);
+ }
+
public function setBlockRules(array $rules) {
assert_instances_of($rules, 'PhutilRemarkupBlockRule');
$rules = msortv($rules, 'getPriorityVector');
$this->blockRules = $rules;
foreach ($this->blockRules as $rule) {
$rule->setEngine($this);
}
$post_rules = array();
foreach ($this->blockRules as $block_rule) {
foreach ($block_rule->getMarkupRules() as $rule) {
$key = $rule->getPostprocessKey();
if ($key !== null) {
$post_rules[$key] = $rule;
}
}
}
$this->postprocessRules = $post_rules;
return $this;
}
public function getTextMetadata($key, $default = null) {
if (isset($this->metadata[$key])) {
return $this->metadata[$key];
}
return idx($this->metadata, $key, $default);
}
public function setTextMetadata($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function storeText($text) {
if ($this->isTextMode()) {
$text = phutil_safe_html($text);
}
return $this->storage->store($text);
}
public function overwriteStoredText($token, $new_text) {
if ($this->isTextMode()) {
$new_text = phutil_safe_html($new_text);
}
$this->storage->overwrite($token, $new_text);
return $this;
}
public function markupText($text) {
return $this->postprocessText($this->preprocessText($text));
}
public function pushState($state) {
if (empty($this->states[$state])) {
$this->states[$state] = 0;
}
$this->states[$state]++;
return $this;
}
public function popState($state) {
if (empty($this->states[$state])) {
throw new Exception(pht("State '%s' pushed more than popped!", $state));
}
$this->states[$state]--;
if (!$this->states[$state]) {
unset($this->states[$state]);
}
return $this;
}
public function getState($state) {
return !empty($this->states[$state]);
}
public function preprocessText($text) {
$this->metadata = array();
$this->storage = new PhutilRemarkupBlockStorage();
$blocks = $this->splitTextIntoBlocks($text);
$output = array();
foreach ($blocks as $block) {
$output[] = $this->markupBlock($block);
}
$output = $this->flattenOutput($output);
$map = $this->storage->getMap();
$this->storage = null;
$metadata = $this->metadata;
return array(
'output' => $output,
'storage' => $map,
'metadata' => $metadata,
);
}
private function splitTextIntoBlocks($text, $depth = 0) {
// Apply basic block and paragraph normalization to the text. NOTE: We don't
// strip trailing whitespace because it is semantic in some contexts,
// notably inlined diffs that the author intends to show as a code block.
$text = phutil_split_lines($text, true);
$block_rules = $this->blockRules;
$blocks = array();
$cursor = 0;
$can_merge = array();
foreach ($block_rules as $key => $block_rule) {
if ($block_rule instanceof PhutilRemarkupDefaultBlockRule) {
$can_merge[$key] = true;
}
}
$last_block = null;
$last_block_key = -1;
// See T13487. For very large inputs, block separation can dominate
// runtime. This is written somewhat clumsily to attempt to handle
// very large inputs as gracefully as is practical.
while (isset($text[$cursor])) {
$starting_cursor = $cursor;
foreach ($block_rules as $block_key => $block_rule) {
$num_lines = $block_rule->getMatchingLineCount($text, $cursor);
if ($num_lines) {
$current_block = array(
'start' => $cursor,
'num_lines' => $num_lines,
'rule' => $block_rule,
'empty' => self::isEmptyBlock($text, $cursor, $num_lines),
'children' => array(),
'merge' => isset($can_merge[$block_key]),
);
$should_merge = self::shouldMergeParagraphBlocks(
$text,
$last_block,
$current_block);
if ($should_merge) {
$last_block['num_lines'] =
($last_block['num_lines'] + $current_block['num_lines']);
$last_block['empty'] =
($last_block['empty'] && $current_block['empty']);
$blocks[$last_block_key] = $last_block;
} else {
$blocks[] = $current_block;
$last_block = $current_block;
$last_block_key++;
}
$cursor += $num_lines;
break;
}
}
if ($starting_cursor === $cursor) {
throw new Exception(pht('Block in text did not match any block rule.'));
}
}
// See T13487. It's common for blocks to be small, and this loop seems to
// measure as faster if we manually concatenate blocks than if we
// "array_slice()" and "implode()" blocks. This is a bit muddy.
foreach ($blocks as $key => $block) {
$min = $block['start'];
$max = $min + $block['num_lines'];
$lines = '';
for ($ii = $min; $ii < $max; $ii++) {
$lines .= $text[$ii];
}
$blocks[$key]['text'] = $lines;
}
// Stop splitting child blocks apart if we get too deep. This arrests
// any blocks which have looping child rules, and stops the stack from
// exploding if someone writes a hilarious comment with 5,000 levels of
// quoted text.
if ($depth < self::MAX_CHILD_DEPTH) {
foreach ($blocks as $key => $block) {
$rule = $block['rule'];
if (!$rule->supportsChildBlocks()) {
continue;
}
list($parent_text, $child_text) = $rule->extractChildText(
$block['text']);
$blocks[$key]['text'] = $parent_text;
$blocks[$key]['children'] = $this->splitTextIntoBlocks(
$child_text,
$depth + 1);
}
}
return $blocks;
}
private function markupBlock(array $block) {
+ $rule = $block['rule'];
+
+ $rule->willMarkupChildBlocks();
+
$children = array();
foreach ($block['children'] as $child) {
$children[] = $this->markupBlock($child);
}
+ $rule->didMarkupChildBlocks();
+
if ($children) {
$children = $this->flattenOutput($children);
} else {
$children = null;
}
- return $block['rule']->markupText($block['text'], $children);
+ return $rule->markupText($block['text'], $children);
}
private function flattenOutput(array $output) {
if ($this->isTextMode()) {
$output = implode("\n\n", $output)."\n";
} else {
$output = phutil_implode_html("\n\n", $output);
}
return $output;
}
private static function shouldMergeParagraphBlocks(
$text,
$last_block,
$current_block) {
// If we're at the beginning of the input, we can't merge.
if ($last_block === null) {
return false;
}
// If the previous block wasn't a default block, we can't merge.
if (!$last_block['merge']) {
return false;
}
// If the current block isn't a default block, we can't merge.
if (!$current_block['merge']) {
return false;
}
// If the last block was empty, we definitely want to merge.
if ($last_block['empty']) {
return true;
}
// If this block is empty, we definitely want to merge.
if ($current_block['empty']) {
return true;
}
// Check if the last line of the previous block or the first line of this
// block have any non-whitespace text. If they both do, we're going to
// merge.
// If either of them are a blank line or a line with only whitespace, we
// do not merge: this means we've found a paragraph break.
$tail = $text[$current_block['start'] - 1];
$head = $text[$current_block['start']];
if (strlen(trim($tail)) && strlen(trim($head))) {
return true;
}
return false;
}
private static function isEmptyBlock($text, $start, $num_lines) {
for ($cursor = $start; $cursor < $start + $num_lines; $cursor++) {
if (strlen(trim($text[$cursor]))) {
return false;
}
}
return true;
}
public function postprocessText(array $dict) {
$this->metadata = idx($dict, 'metadata', array());
$this->storage = new PhutilRemarkupBlockStorage();
$this->storage->setMap(idx($dict, 'storage', array()));
foreach ($this->blockRules as $block_rule) {
$block_rule->postprocess();
}
foreach ($this->postprocessRules as $rule) {
$rule->didMarkupText();
}
return $this->restoreText(idx($dict, 'output'));
}
public function restoreText($text) {
return $this->storage->restore($text, $this->isTextMode());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
index 5ab033d6b1..b0399527b0 100644
--- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
@@ -1,430 +1,432 @@
<?php
abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule {
private $referencePattern;
private $embedPattern;
const KEY_RULE_OBJECT = 'rule.object';
const KEY_MENTIONED_OBJECTS = 'rule.object.mentioned';
abstract protected function getObjectNamePrefix();
abstract protected function loadObjects(array $ids);
public function getPriority() {
return 450.0;
}
protected function getObjectNamePrefixBeginsWithWordCharacter() {
$prefix = $this->getObjectNamePrefix();
return preg_match('/^\w/', $prefix);
}
protected function getObjectIDPattern() {
return '[1-9]\d*';
}
protected function shouldMarkupObject(array $params) {
return true;
}
protected function getObjectNameText(
$object,
PhabricatorObjectHandle $handle,
$id) {
return $this->getObjectNamePrefix().$id;
}
protected function loadHandles(array $objects) {
$phids = mpull($objects, 'getPHID');
$viewer = $this->getEngine()->getConfig('viewer');
$handles = $viewer->loadHandles($phids);
$handles = iterator_to_array($handles);
$result = array();
foreach ($objects as $id => $object) {
$result[$id] = $handles[$object->getPHID()];
}
return $result;
}
protected function getObjectHref(
$object,
PhabricatorObjectHandle $handle,
$id) {
$uri = $handle->getURI();
if ($this->getEngine()->getConfig('uri.full')) {
$uri = PhabricatorEnv::getURI($uri);
}
return $uri;
}
protected function renderObjectRefForAnyMedia(
$object,
PhabricatorObjectHandle $handle,
$anchor,
$id) {
$href = $this->getObjectHref($object, $handle, $id);
$text = $this->getObjectNameText($object, $handle, $id);
if ($anchor) {
$href = $href.'#'.$anchor;
$text = $text.'#'.$anchor;
}
if ($this->getEngine()->isTextMode()) {
return $text.' <'.PhabricatorEnv::getProductionURI($href).'>';
} else if ($this->getEngine()->isHTMLMailMode()) {
$href = PhabricatorEnv::getProductionURI($href);
return $this->renderObjectTagForMail($text, $href, $handle);
}
return $this->renderObjectRef($object, $handle, $anchor, $id);
}
protected function renderObjectRef(
$object,
PhabricatorObjectHandle $handle,
$anchor,
$id) {
$href = $this->getObjectHref($object, $handle, $id);
$text = $this->getObjectNameText($object, $handle, $id);
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
if ($anchor) {
$href = $href.'#'.$anchor;
$text = $text.'#'.$anchor;
}
$attr = array(
'phid' => $handle->getPHID(),
'closed' => ($handle->getStatus() == $status_closed),
);
return $this->renderHovertag($text, $href, $attr);
}
protected function renderObjectEmbedForAnyMedia(
$object,
PhabricatorObjectHandle $handle,
$options) {
$name = $handle->getFullName();
$href = $handle->getURI();
if ($this->getEngine()->isTextMode()) {
return $name.' <'.PhabricatorEnv::getProductionURI($href).'>';
} else if ($this->getEngine()->isHTMLMailMode()) {
$href = PhabricatorEnv::getProductionURI($href);
return $this->renderObjectTagForMail($name, $href, $handle);
}
// See T13678. If we're already rendering embedded content, render a
// default reference instead to avoid cycles.
if (PhabricatorMarkupEngine::isRenderingEmbeddedContent()) {
return $this->renderDefaultObjectEmbed($object, $handle);
}
return $this->renderObjectEmbed($object, $handle, $options);
}
protected function renderObjectEmbed(
$object,
PhabricatorObjectHandle $handle,
$options) {
return $this->renderDefaultObjectEmbed($object, $handle);
}
final protected function renderDefaultObjectEmbed(
$object,
PhabricatorObjectHandle $handle) {
$name = $handle->getFullName();
$href = $handle->getURI();
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
$attr = array(
'phid' => $handle->getPHID(),
'closed' => ($handle->getStatus() == $status_closed),
);
return $this->renderHovertag($name, $href, $attr);
}
protected function renderObjectTagForMail(
$text,
$href,
PhabricatorObjectHandle $handle) {
$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
$strikethrough = $handle->getStatus() == $status_closed ?
'text-decoration: line-through;' :
'text-decoration: none;';
return phutil_tag(
'a',
array(
'href' => $href,
'style' => 'background-color: #e7e7e7;
border-color: #e7e7e7;
border-radius: 3px;
padding: 0 4px;
font-weight: bold;
color: black;'
.$strikethrough,
),
$text);
}
protected function renderHovertag($name, $href, array $attr = array()) {
return id(new PHUITagView())
->setName($name)
->setHref($href)
->setType(PHUITagView::TYPE_OBJECT)
->setPHID(idx($attr, 'phid'))
->setClosed(idx($attr, 'closed'))
->render();
}
public function apply($text) {
$text = preg_replace_callback(
$this->getObjectEmbedPattern(),
array($this, 'markupObjectEmbed'),
$text);
$text = preg_replace_callback(
$this->getObjectReferencePattern(),
array($this, 'markupObjectReference'),
$text);
return $text;
}
private function getObjectEmbedPattern() {
if ($this->embedPattern === null) {
$prefix = $this->getObjectNamePrefix();
$prefix = preg_quote($prefix);
$id = $this->getObjectIDPattern();
$this->embedPattern =
'(\B{'.$prefix.'('.$id.')([,\s](?:[^}\\\\]|\\\\.)*)?}\B)u';
}
return $this->embedPattern;
}
private function getObjectReferencePattern() {
if ($this->referencePattern === null) {
$prefix = $this->getObjectNamePrefix();
$prefix = preg_quote($prefix);
$id = $this->getObjectIDPattern();
// If the prefix starts with a word character (like "D"), we want to
// require a word boundary so that we don't match "XD1" as "D1". If the
// prefix does not start with a word character, we want to require no word
// boundary for the same reasons. Test if the prefix starts with a word
// character.
if ($this->getObjectNamePrefixBeginsWithWordCharacter()) {
$boundary = '\\b';
} else {
$boundary = '\\B';
}
// The "(?<![#@-])" prevents us from linking "#abcdef" or similar, and
// "ABC-T1" (see T5714), and from matching "@T1" as a task (it is a user)
// (see T9479).
// The "\b" allows us to link "(abcdef)" or similar without linking things
// in the middle of words.
$this->referencePattern =
'((?<![#@-])'.$boundary.$prefix.'('.$id.')(?:#([-\w\d]+))?(?!\w))u';
}
return $this->referencePattern;
}
/**
* Extract matched object references from a block of text.
*
* This is intended to make it easy to write unit tests for object remarkup
* rules. Production code is not normally expected to call this method.
*
* @param string Text to match rules against.
* @return wild Matches, suitable for writing unit tests against.
*/
public function extractReferences($text) {
$embed_matches = null;
preg_match_all(
$this->getObjectEmbedPattern(),
$text,
$embed_matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
$ref_matches = null;
preg_match_all(
$this->getObjectReferencePattern(),
$text,
$ref_matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
$results = array();
$sets = array(
'embed' => $embed_matches,
'ref' => $ref_matches,
);
foreach ($sets as $type => $matches) {
$formatted = array();
foreach ($matches as $match) {
$format = array(
'offset' => $match[1][1],
'id' => $match[1][0],
);
if (isset($match[2][0])) {
$format['tail'] = $match[2][0];
}
$formatted[] = $format;
}
$results[$type] = $formatted;
}
return $results;
}
public function markupObjectEmbed(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
// If we're rendering a table of contents, just render the raw input.
// This could perhaps be handled more gracefully but it seems unusual to
// put something like "{P123}" in a header and it's not obvious what users
// expect? See T8845.
$engine = $this->getEngine();
if ($engine->getState('toc')) {
return $matches[0];
}
return $this->markupObject(array(
'type' => 'embed',
'id' => $matches[1],
'options' => idx($matches, 2),
'original' => $matches[0],
+ 'quote.depth' => $engine->getQuoteDepth(),
));
}
public function markupObjectReference(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
// If we're rendering a table of contents, just render the monogram.
$engine = $this->getEngine();
if ($engine->getState('toc')) {
return $matches[0];
}
return $this->markupObject(array(
'type' => 'ref',
'id' => $matches[1],
'anchor' => idx($matches, 2),
'original' => $matches[0],
+ 'quote.depth' => $engine->getQuoteDepth(),
));
}
private function markupObject(array $params) {
if (!$this->shouldMarkupObject($params)) {
return $params['original'];
}
$regex = trim(
PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names'));
if ($regex && preg_match($regex, $params['original'])) {
return $params['original'];
}
$engine = $this->getEngine();
$token = $engine->storeText('x');
$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
$metadata = $engine->getTextMetadata($metadata_key, array());
$metadata[] = array(
'token' => $token,
) + $params;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
$ids = ipull($metadata, 'id');
$objects = $this->loadObjects($ids);
// For objects that are invalid or which the user can't see, just render
// the original text.
// TODO: We should probably distinguish between these cases and render a
// "you can't see this" state for nonvisible objects.
foreach ($metadata as $key => $spec) {
if (empty($objects[$spec['id']])) {
$engine->overwriteStoredText(
$spec['token'],
$spec['original']);
unset($metadata[$key]);
}
}
$phids = $engine->getTextMetadata(self::KEY_MENTIONED_OBJECTS, array());
foreach ($objects as $object) {
$phids[$object->getPHID()] = $object->getPHID();
}
$engine->setTextMetadata(self::KEY_MENTIONED_OBJECTS, $phids);
$handles = $this->loadHandles($objects);
foreach ($metadata as $key => $spec) {
$handle = $handles[$spec['id']];
$object = $objects[$spec['id']];
switch ($spec['type']) {
case 'ref':
$view = $this->renderObjectRefForAnyMedia(
$object,
$handle,
$spec['anchor'],
$spec['id']);
break;
case 'embed':
$spec['options'] = $this->assertFlatText($spec['options']);
$view = $this->renderObjectEmbedForAnyMedia(
$object,
$handle,
$spec['options']);
break;
}
$engine->overwriteStoredText($spec['token'], $view);
}
$engine->setTextMetadata($metadata_key, array());
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 20, 5:05 AM (1 d, 2 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
241450
Default Alt Text
(63 KB)

Event Timeline