Page MenuHomePhabricator

D20511.id48940.diff
No OneTemporary

D20511.id48940.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -394,6 +394,8 @@
'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php',
'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php',
'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
+ 'PhutilRemarkupHyperlinkEngineExtension' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php',
+ 'PhutilRemarkupHyperlinkRef' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php',
'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php',
'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php',
'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
@@ -1055,6 +1057,8 @@
'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
+ 'PhutilRemarkupHyperlinkEngineExtension' => 'Phobject',
+ 'PhutilRemarkupHyperlinkRef' => 'Phobject',
'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php
new file mode 100644
--- /dev/null
+++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php
@@ -0,0 +1,30 @@
+<?php
+
+abstract class PhutilRemarkupHyperlinkEngineExtension
+ extends Phobject {
+
+ private $engine;
+
+ final public function getHyperlinkEngineKey() {
+ return $this->getPhobjectClassConstant('LINKENGINEKEY', 32);
+ }
+
+ final public static function getAllLinkEngines() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getHyperlinkEngineKey')
+ ->execute();
+ }
+
+ final public function setEngine(PhutilRemarkupEngine $engine) {
+ $this->engine = $engine;
+ return $this;
+ }
+
+ final public function getEngine() {
+ return $this->engine;
+ }
+
+ abstract public function processHyperlinks(array $hyperlinks);
+
+}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php
new file mode 100644
--- /dev/null
+++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php
@@ -0,0 +1,38 @@
+<?php
+
+final class PhutilRemarkupHyperlinkRef
+ extends Phobject {
+
+ private $token;
+ private $uri;
+ private $embed;
+ private $result;
+
+ public function __construct(array $map) {
+ $this->token = $map['token'];
+ $this->uri = $map['uri'];
+ $this->embed = ($map['mode'] === '{');
+ }
+
+ public function getToken() {
+ return $this->token;
+ }
+
+ public function getURI() {
+ return $this->uri;
+ }
+
+ public function isEmbed() {
+ return $this->embed;
+ }
+
+ public function setResult($result) {
+ $this->result = $result;
+ return $this;
+ }
+
+ public function getResult() {
+ return $this->result;
+ }
+
+}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php
+++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php
@@ -2,6 +2,8 @@
final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
+ const KEY_HYPERLINKS = 'hyperlinks';
+
public function getPriority() {
return 400.0;
}
@@ -13,7 +15,13 @@
// don't appear in normal text or normal URLs.
$text = preg_replace_callback(
'@<(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+?)>@',
- array($this, 'markupHyperlink'),
+ array($this, 'markupHyperlinkAngle'),
+ $text);
+
+ // We match "{uri}", but do not link it by default.
+ $text = preg_replace_callback(
+ '@{(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+?)}@',
+ array($this, 'markupHyperlinkCurly'),
$text);
// Anything else we match "ungreedily", which means we'll look for
@@ -31,42 +39,49 @@
return $text;
}
- protected function markupHyperlink(array $matches) {
+ public function markupHyperlinkAngle(array $matches) {
+ return $this->markupHyperlink('<', $matches);
+ }
+
+ public function markupHyperlinkCurly(array $matches) {
+ return $this->markupHyperlink('{', $matches);
+ }
+
+ protected function markupHyperlink($mode, array $matches) {
+ $raw_uri = $matches[1];
+
try {
- $uri = new PhutilURI($matches[1]);
+ $uri = new PhutilURI($raw_uri);
} catch (Exception $ex) {
return $matches[0];
}
- $protocol = $uri->getProtocol();
+ $engine = $this->getEngine();
- $protocols = $this->getEngine()->getConfig(
- 'uri.allowed-protocols',
- array());
+ $token = $engine->storeText($raw_uri);
- if (!idx($protocols, $protocol)) {
- // If this URI doesn't use a whitelisted protocol, don't link it. This
- // is primarily intended to prevent javascript:// silliness.
- return $this->getEngine()->storeText($matches[1]);
- }
+ $list_key = self::KEY_HYPERLINKS;
+ $link_list = $engine->getTextMetadata($list_key, array());
- return $this->storeRenderedHyperlink($matches[1]);
- }
+ $link_list[] = array(
+ 'token' => $token,
+ 'uri' => $raw_uri,
+ 'mode' => $mode,
+ );
- protected function storeRenderedHyperlink($link) {
- return $this->getEngine()->storeText($this->renderHyperlink($link));
- }
+ $engine->setTextMetadata($list_key, $link_list);
- protected function renderHyperlink($link) {
- $engine = $this->getEngine();
+ return $token;
+ }
- if ($engine->isTextMode()) {
- return $link;
+ protected function renderHyperlink($link, $is_embed) {
+ // If the URI is "{uri}" and no handler picked it up, we just render it
+ // as plain text.
+ if ($is_embed) {
+ return $this->renderRawLink($link, $is_embed);
}
- if ($engine->getState('toc')) {
- return $link;
- }
+ $engine = $this->getEngine();
$same_window = $engine->getConfig('uri.same-window', false);
if ($same_window) {
@@ -86,6 +101,14 @@
$link);
}
+ private function renderRawLink($link, $is_embed) {
+ if ($is_embed) {
+ return '{'.$link.'}';
+ } else {
+ return $link;
+ }
+ }
+
protected function markupHyperlinkUngreedy($matches) {
$match = $matches[1];
$tail = null;
@@ -116,7 +139,96 @@
return $matches[0];
}
- return hsprintf('%s%s', $this->markupHyperlink(array(null, $match)), $tail);
+ $link = $this->markupHyperlink(null, array(null, $match));
+
+ return hsprintf('%s%s', $link, $tail);
+ }
+
+ public function didMarkupText() {
+ $engine = $this->getEngine();
+
+ $protocols = $engine->getConfig('uri.allowed-protocols', array());
+ $is_toc = $engine->getState('toc');
+ $is_text = $engine->isTextMode();
+ $is_mail = $engine->isHTMLMailMode();
+
+ $list_key = self::KEY_HYPERLINKS;
+ $raw_list = $engine->getTextMetadata($list_key, array());
+
+ $links = array();
+ foreach ($raw_list as $key => $link) {
+ $token = $link['token'];
+ $raw_uri = $link['uri'];
+ $mode = $link['mode'];
+
+ $is_embed = ($mode === '{');
+ $is_literal = ($mode === '<');
+
+ // If we're rendering in a "Table of Contents" or a plain text mode,
+ // we're going to render the raw URI without modifications.
+ if ($is_toc || $is_text) {
+ $result = $this->renderRawLink($raw_uri, $is_embed);
+ $engine->overwriteStoredText($token, $result);
+ continue;
+ }
+
+ // If this URI doesn't use a whitelisted protocol, don't link it. This
+ // is primarily intended to prevent "javascript://" silliness.
+ $uri = new PhutilURI($raw_uri);
+ $protocol = $uri->getProtocol();
+ $valid_protocol = idx($protocols, $protocol);
+ if (!$valid_protocol) {
+ $result = $this->renderRawLink($raw_uri, $is_embed);
+ $engine->overwriteStoredText($token, $result);
+ continue;
+ }
+
+ // If the URI is written as "<uri>", we'll render it literally even if
+ // some handler would otherwise deal with it.
+ // If we're rendering for HTML mail, we also render literally.
+ if ($is_literal || $is_mail) {
+ $result = $this->renderHyperlink($raw_uri, $is_embed);
+ $engine->overwriteStoredText($token, $result);
+ continue;
+ }
+
+ // Otherwise, this link is a valid resource which extensions are allowed
+ // to handle.
+ $links[$key] = $link;
+ }
+
+ if (!$links) {
+ return;
+ }
+
+ foreach ($links as $key => $link) {
+ $links[$key] = new PhutilRemarkupHyperlinkRef($link);
+ }
+
+ $extensions = PhutilRemarkupHyperlinkEngineExtension::getAllLinkEngines();
+ foreach ($extensions as $extension) {
+ $extension = id(clone $extension)
+ ->setEngine($engine)
+ ->processHyperlinks($links);
+
+ foreach ($links as $key => $link) {
+ $result = $link->getResult();
+ if ($result !== null) {
+ $engine->overwriteStoredText($link->getToken(), $result);
+ unset($links[$key]);
+ }
+ }
+
+ if (!$links) {
+ break;
+ }
+ }
+
+ // Render any remaining links in a normal way.
+ foreach ($links as $link) {
+ $result = $this->renderHyperlink($link->getURI(), $link->isEmbed());
+ $engine->overwriteStoredText($link->getToken(), $result);
+ }
}
}

File Metadata

Mime Type
text/plain
Expires
Fri, Mar 21, 11:14 PM (1 w, 6 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7708036
Default Alt Text
D20511.id48940.diff (9 KB)

Event Timeline