diff --git a/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php b/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php index 9b988c7e8f..48a2438c1b 100644 --- a/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php +++ b/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php @@ -1,131 +1,131 @@ <?php final class DivinerRemarkupRuleSymbol extends PhutilRemarkupRule { const KEY_RULE_ATOM_REF = 'rule.diviner.atomref'; public function apply($text) { // Grammar here is: // // rule = '@{' maybe_type name maybe_title '}' // maybe_type = null | type ':' | type '@' book ':' // name = name | name '@' context // maybe_title = null | '|' title // // So these are all valid: // // @{name} // @{type : name} // @{name | title} // @{type @ book : name @ context | title} - return $this->replaceHTML( + return preg_replace_callback( '/(?:^|\B)@{'. '(?:(?P<type>[^:]+?):)?'. '(?P<name>[^}|]+?)'. '(?:[|](?P<title>[^}]+))?'. '}/', array($this, 'markupSymbol'), $text); } public function markupSymbol($matches) { $type = (string)idx($matches, 'type'); $name = (string)$matches['name']; $title = (string)idx($matches, 'title'); // Collapse sequences of whitespace into a single space. $type = preg_replace('/\s+/', ' ', trim($type)); $name = preg_replace('/\s+/', ' ', trim($name)); $title = preg_replace('/\s+/', ' ', trim($title)); $ref = array(); if (strpos($type, '@') !== false) { list($type, $book) = explode('@', $type, 2); $ref['type'] = trim($type); $ref['book'] = trim($book); } else { $ref['type'] = $type; } if (strpos($name, '@') !== false) { list($name, $context) = explode('@', $name, 2); $ref['name'] = trim($name); $ref['context'] = trim($context); } else { $ref['name'] = $name; } $ref['title'] = $title; foreach ($ref as $key => $value) { if ($value === '') { unset($ref[$key]); } } $engine = $this->getEngine(); $token = $engine->storeText(''); $key = self::KEY_RULE_ATOM_REF; $data = $engine->getTextMetadata($key, array()); $data[$token] = $ref; $engine->setTextMetadata($key, $data); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $key = self::KEY_RULE_ATOM_REF; $data = $engine->getTextMetadata($key, array()); $renderer = $engine->getConfig('diviner.renderer'); foreach ($data as $token => $ref_dict) { $ref = DivinerAtomRef::newFromDictionary($ref_dict); $title = nonempty($ref->getTitle(), $ref->getName()); $href = null; if ($renderer) { // Here, we're generating documentation. If possible, we want to find // the real atom ref so we can render the correct default title and // render invalid links in an alternate style. $ref = $renderer->normalizeAtomRef($ref); if ($ref) { $title = nonempty($ref->getTitle(), $ref->getName()); $href = $renderer->getHrefForAtomRef($ref); } } else { // Here, we're generating commment text or something like that. Just // link to Diviner and let it sort things out. $href = id(new PhutilURI('/diviner/find/')) ->setQueryParams($ref_dict + array('jump' => true)); } if ($href) { $link = phutil_tag( 'a', array( 'class' => 'atom-ref', 'href' => $href, ), $title); } else { $link = phutil_tag( 'span', array( 'class' => 'atom-ref-invalid', ), $title); } $engine->overwriteStoredText($token, $link); } } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php index 3356aed8e2..0fbe87ebfc 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php @@ -1,71 +1,71 @@ <?php /** * @group markup */ final class PhabricatorRemarkupRuleCountdown extends PhutilRemarkupRule { const KEY_RULE_COUNTDOWN = 'rule.countdown'; public function apply($text) { - return $this->replaceHTML( + return preg_replace_callback( "@\B{C(\d+)}\B@", array($this, 'markupCountdown'), $text); } protected function markupCountdown($matches) { $countdown = id(new PhabricatorTimer())->load($matches[1]); if (!$countdown) { return $matches[0]; } $id = celerity_generate_unique_node_id(); $engine = $this->getEngine(); $token = $engine->storeText(''); $metadata_key = self::KEY_RULE_COUNTDOWN; $metadata = $engine->getTextMetadata($metadata_key, array()); $metadata[$id] = array($countdown->getDatepoint(), $token); $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_COUNTDOWN; $metadata = $engine->getTextMetadata($metadata_key, array()); if (!$metadata) { return; } require_celerity_resource('javelin-behavior-countdown-timer'); foreach ($metadata as $id => $info) { list($time, $token) = $info; $prefix = 'phabricator-timer-'; $count = phutil_tag( 'span', array( 'id' => $id, ), array( javelin_tag('span', array('sigil' => $prefix.'days'), ''), 'd', javelin_tag('span', array('sigil' => $prefix.'hours'), ''), 'h', javelin_tag('span', array('sigil' => $prefix.'minutes'), ''), 'm', javelin_tag('span', array('sigil' => $prefix.'seconds'), ''), 's', )); Javelin::initBehavior('countdown-timer', array( 'timestamp' => $time, 'container' => $id, )); $engine->overwriteStoredText($token, $count); } $engine->setTextMetadata($metadata_key, array()); } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php index e7c25dd1fe..9ef0ac8ad9 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php @@ -1,187 +1,187 @@ <?php /** * @group markup */ final class PhabricatorRemarkupRuleEmbedFile extends PhutilRemarkupRule { const KEY_RULE_EMBED_FILE = 'rule.embed.file'; const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids'; public function apply($text) { - return $this->replaceHTML( + return preg_replace_callback( "@{F(\d+)([^}]+?)?}@", array($this, 'markupEmbedFile'), $text); } public function markupEmbedFile($matches) { $file = null; if ($matches[1]) { // TODO: This is pretty inefficient if there are a bunch of files. $file = id(new PhabricatorFile())->load($matches[1]); } if (!$file) { return $matches[0]; } $phid = $file->getPHID(); $engine = $this->getEngine(); $token = $engine->storeText(''); $metadata_key = self::KEY_RULE_EMBED_FILE; $metadata = $engine->getTextMetadata($metadata_key, array()); $bundle = array('token' => $token); $options = array( 'size' => 'thumb', 'layout' => 'left', 'float' => false, 'name' => null, ); if (!empty($matches[2])) { $matches[2] = trim($matches[2], ', '); $parser = new PhutilSimpleOptions(); $options = $parser->parse($matches[2]) + $options; } $file_name = coalesce($options['name'], $file->getName()); $options['name'] = $file_name; $is_viewable_image = $file->isViewableImage(); $attrs = array(); if ($is_viewable_image) { switch ((string)$options['size']) { case 'full': $attrs['src'] = $file->getBestURI(); $options['image_class'] = null; $file_data = $file->getMetadata(); $height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT); if ($height) { $attrs['height'] = $height; } $width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH); if ($width) { $attrs['width'] = $width; } break; case 'thumb': default: $attrs['src'] = $file->getPreview220URI(); $dimensions = PhabricatorImageTransformer::getPreviewDimensions($file, 220); $attrs['width'] = $dimensions['sdx']; $attrs['height'] = $dimensions['sdy']; $options['image_class'] = 'phabricator-remarkup-embed-image'; break; } } $bundle['attrs'] = $attrs; $bundle['options'] = $options; $bundle['meta'] = array( 'phid' => $file->getPHID(), 'viewable' => $is_viewable_image, 'uri' => $file->getBestURI(), 'dUri' => $file->getDownloadURI(), 'name' => $options['name'], ); $metadata[$phid][] = $bundle; $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_EMBED_FILE; $metadata = $engine->getTextMetadata($metadata_key, array()); if (!$metadata) { return; } $file_phids = array(); foreach ($metadata as $phid => $bundles) { foreach ($bundles as $data) { $options = $data['options']; $meta = $data['meta']; if (!$meta['viewable'] || $options['layout'] == 'link') { $link = id(new PhabricatorFileLinkView()) ->setFilePHID($meta['phid']) ->setFileName($meta['name']) ->setFileDownloadURI($meta['dUri']) ->setFileViewURI($meta['uri']) ->setFileViewable($meta['viewable']); $embed = $link->render(); $engine->overwriteStoredText($data['token'], $embed); continue; } require_celerity_resource('lightbox-attachment-css'); $img = phutil_tag('img', $data['attrs']); $embed = javelin_tag( 'a', array( 'href' => $meta['uri'], 'class' => $options['image_class'], 'sigil' => 'lightboxable', 'mustcapture' => true, 'meta' => $meta, ), $img); $layout_class = null; 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; } } if ($layout_class) { $embed = phutil_tag( 'div', array( 'class' => $layout_class, ), $embed); } $engine->overwriteStoredText($data['token'], $embed); } $file_phids[] = $phid; } $engine->setTextMetadata(self::KEY_EMBED_FILE_PHIDS, $file_phids); $engine->setTextMetadata($metadata_key, array()); } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php index 2bf0d89fcf..7ad83ebc58 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php @@ -1,63 +1,63 @@ <?php /** * @group markup */ final class PhabricatorRemarkupRuleImageMacro extends PhutilRemarkupRule { private $images; public function apply($text) { - return $this->replaceHTML( + return preg_replace_callback( '@^([a-zA-Z0-9:_\-]+)$@m', array($this, 'markupImageMacro'), $text); } public function markupImageMacro($matches) { if ($this->images === null) { $this->images = array(); $rows = id(new PhabricatorFileImageMacro())->loadAllWhere( 'isDisabled = 0'); foreach ($rows as $row) { $this->images[$row->getName()] = $row->getFilePHID(); } } $name = (string)$matches[1]; if (array_key_exists($name, $this->images)) { $phid = $this->images[$name]; $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $phid); $style = null; $src_uri = null; if ($file) { $src_uri = $file->getBestURI(); $file_data = $file->getMetadata(); $height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT); $width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH); if ($height && $width) { $style = sprintf( 'height: %dpx; width: %dpx;', $height, $width); } } $img = phutil_tag( 'img', array( 'src' => $src_uri, 'alt' => $matches[1], 'title' => $matches[1], 'style' => $style, )); return $this->getEngine()->storeText($img); } else { return $matches[1]; } } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php index b569eb60ac..1254d044ad 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php @@ -1,42 +1,42 @@ <?php /** * @group markup */ final class PhabricatorRemarkupRuleMeme extends PhutilRemarkupRule { private $images; public function apply($text) { - return $this->replaceHTML( + return preg_replace_callback( '@{meme,([^}]+)}$@m', array($this, 'markupMeme'), $text); } public function markupMeme($matches) { $options = array( 'src' => null, 'above' => null, 'below' => null, ); $parser = new PhutilSimpleOptions(); $options = $parser->parse($matches[1]) + $options; $uri = id(new PhutilURI('/macro/meme/')) ->alter('macro', $options['src']) ->alter('uppertext', $options['above']) ->alter('lowertext', $options['below']); $img = phutil_tag( 'img', array( 'src' => (string)$uri, )); return $this->getEngine()->storeText($img); } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php index b2a98fc9af..7f05ff95a1 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php @@ -1,130 +1,130 @@ <?php /** * @group markup */ final class PhabricatorRemarkupRuleMention extends PhutilRemarkupRule { const KEY_RULE_MENTION = 'rule.mention'; const KEY_RULE_MENTION_ORIGINAL = 'rule.mention.original'; const KEY_MENTIONED = 'phabricator.mentioned-user-phids'; // NOTE: The negative lookbehind prevents matches like "mail@lists", while // allowing constructs like "@tomo/@mroch". Since we now allow periods in // usernames, we can't resonably distinguish that "@company.com" isn't a // username, so we'll incorrectly pick it up, but there's little to be done // about that. We forbid terminal periods so that we can correctly capture // "@joe" instead of "@joe." in "Hey, @joe.". const REGEX = '/(?<!\w)@([a-zA-Z0-9._-]*[a-zA-Z0-9_-])/'; public function apply($text) { - return $this->replaceHTML( + return preg_replace_callback( self::REGEX, array($this, 'markupMention'), $text); } protected function markupMention($matches) { $engine = $this->getEngine(); $token = $engine->storeText(''); // Store the original text exactly so we can preserve casing if it doesn't // resolve into a username. $original_key = self::KEY_RULE_MENTION_ORIGINAL; $original = $engine->getTextMetadata($original_key, array()); $original[$token] = $matches[1]; $engine->setTextMetadata($original_key, $original); $metadata_key = self::KEY_RULE_MENTION; $metadata = $engine->getTextMetadata($metadata_key, array()); $username = strtolower($matches[1]); if (empty($metadata[$username])) { $metadata[$username] = array(); } $metadata[$username][] = $token; $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_MENTION; $metadata = $engine->getTextMetadata($metadata_key, array()); if (empty($metadata)) { // No mentions, or we already processed them. return; } $original_key = self::KEY_RULE_MENTION_ORIGINAL; $original = $engine->getTextMetadata($original_key, array()); $usernames = array_keys($metadata); $user_table = new PhabricatorUser(); $real_user_names = queryfx_all( $user_table->establishConnection('r'), 'SELECT username, phid, realName, isDisabled FROM %T WHERE username IN (%Ls)', $user_table->getTableName(), $usernames); $actual_users = array(); $mentioned_key = self::KEY_MENTIONED; $mentioned = $engine->getTextMetadata($mentioned_key, array()); foreach ($real_user_names as $row) { $actual_users[strtolower($row['username'])] = $row; $mentioned[$row['phid']] = $row['phid']; } $engine->setTextMetadata($mentioned_key, $mentioned); foreach ($metadata as $username => $tokens) { $exists = isset($actual_users[$username]); if (!$exists) { $class = 'phabricator-remarkup-mention-unknown'; } else if ($actual_users[$username]['isDisabled']) { $class = 'phabricator-remarkup-mention-disabled'; } else { $class = 'phabricator-remarkup-mention-exists'; } if ($exists) { $tag = phutil_tag( 'a', array( 'class' => $class, 'href' => '/p/'.$actual_users[$username]['username'].'/', 'target' => '_blank', 'title' => $actual_users[$username]['realName'], ), '@'.$actual_users[$username]['username']); foreach ($tokens as $token) { $engine->overwriteStoredText($token, $tag); } } else { // NOTE: The structure here is different from the 'exists' branch, // because we want to preserve the original text capitalization and it // may differ for each token. foreach ($tokens as $token) { $tag = phutil_tag( 'span', array( 'class' => $class, ), '@'.idx($original, $token, $username)); $engine->overwriteStoredText($token, $tag); } } } // Don't re-process these mentions. $engine->setTextMetadata($metadata_key, array()); } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php index 8aa082b621..c47681a7f7 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php @@ -1,66 +1,66 @@ <?php /** * @group markup */ abstract class PhabricatorRemarkupRuleObjectHandle extends PhutilRemarkupRule { const KEY_RULE_HANDLE = 'rule.handle'; abstract protected function getObjectNamePrefix(); abstract protected function loadObjectPHID($id); public function apply($text) { $prefix = $this->getObjectNamePrefix(); - return $this->replaceHTML( + return preg_replace_callback( "@\B{{$prefix}(\d+)}\B@", array($this, 'markupObjectHandle'), $text); } protected function markupObjectHandle($matches) { // TODO: These are single gets but should be okay for now, they're behind // the cache. $phid = $this->loadObjectPHID($matches[1]); if (!$phid) { return $matches[0]; } $engine = $this->getEngine(); $token = $engine->storeText(''); $metadata_key = self::KEY_RULE_HANDLE; $metadata = $engine->getTextMetadata($metadata_key, array()); if (empty($metadata[$phid])) { $metadata[$phid] = array(); } $metadata[$phid][] = $token; $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_HANDLE; $metadata = $engine->getTextMetadata($metadata_key, array()); if (empty($metadata)) { return; } $handles = id(new PhabricatorObjectHandleData(array_keys($metadata))) ->loadHandles(); foreach ($metadata as $phid => $tokens) { $link = $handles[$phid]->renderLink(); foreach ($tokens as $token) { $engine->overwriteStoredText($token, $link); } } $engine->setTextMetadata($metadata_key, array()); } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php index 859bebdc74..b81c37cc2f 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php @@ -1,51 +1,51 @@ <?php /** * @group markup */ abstract class PhabricatorRemarkupRuleObjectName extends PhutilRemarkupRule { abstract protected function getObjectNamePrefix(); protected function getObjectIDPattern() { return '[1-9]\d*'; } public function apply($text) { $prefix = $this->getObjectNamePrefix(); $id = $this->getObjectIDPattern(); - return $this->replaceHTML( + return preg_replace_callback( "@\b({$prefix})({$id})(?:#([-\w\d]+))?\b@", array($this, 'markupObjectNameLink'), $text); } public function markupObjectNameLink($matches) { list(, $prefix, $id) = $matches; if (isset($matches[3])) { $href = $matches[3]; $text = $matches[3]; if (preg_match('@^(?:comment-)?(\d{1,7})$@', $href, $matches)) { // Maximum length is 7 because 12345678 could be a file hash. $href = "comment-{$matches[1]}"; $text = $matches[1]; } $href = "/{$prefix}{$id}#{$href}"; $text = "{$prefix}{$id}#{$text}"; } else { $href = "/{$prefix}{$id}"; $text = "{$prefix}{$id}"; } return $this->getEngine()->storeText( phutil_tag( 'a', array( 'href' => $href, ), $text)); } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php index 56a84d0d75..e42c680a63 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php @@ -1,45 +1,45 @@ <?php /** * @group markup */ final class PhabricatorRemarkupRulePhriction extends PhutilRemarkupRule { public function apply($text) { - return $this->replaceHTML( + return preg_replace_callback( '@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U', array($this, 'markupDocumentLink'), $text); } public function markupDocumentLink($matches) { $link = trim($matches[1]); $name = trim(idx($matches, 2, $link)); $name = explode('/', trim($name, '/')); $name = end($name); $uri = new PhutilURI($link); $slug = $uri->getPath(); $fragment = $uri->getFragment(); $slug = PhabricatorSlug::normalize($slug); $slug = PhrictionDocument::getSlugURI($slug); $href = (string) id(new PhutilURI($slug))->setFragment($fragment); if ($this->getEngine()->getState('toc')) { $text = $name; } else { $text = phutil_tag( 'a', array( 'href' => $href, 'class' => 'phriction-link', ), $name); } return $this->getEngine()->storeText($text); } }