mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42:41 +01:00
Remarkup: make less internal links open in new tabs
Summary: This is an attempt to improve the default behavior in Remarkup about links. It does not change any behaviors manually specified in the engine and it does not change any behaviors related to external domains. As default, now these kind of links will open in the same tab: - anchors - relative URLs - absolute URLs pointing to the base-URI domain All the other cases are kept as before - so they open in another tab. In short, assuming you are we.phorge.it, here the changes: | |https://gnu.org|[[changelog/]]|[[#anchor|#anchor]]|https://we.phorge.it/|[[/config/|/config/]]| |Before|external |internal |internal |external |external | |After |external |internal |internal |**internal** |**internal** | This situation can further improve but it already covers most of the cases where most users do not expect to break their navigation into several tabs. Moreover, if an user wants to open a link in another window, no one prevents from using the middle mouse button, or CTRL+click or any other nice really basic feature from their browser. Also, this change introduces a new CSS class, allowing web designers to style these external resources. Example CSS rule to try: ```css .remarkup-link-ext::before { content: "[external] "; } ``` Closes T15161 Closes T15182 Test Plan: - Copy the example text from this Task: https://we.phorge.it/T15161 - Verify that "internal resources" are internal links as default now - Verify that "external resources" are still external links as before Reviewers: O1 Blessed Committers, Cigaryno, avivey, speck Reviewed By: O1 Blessed Committers, Cigaryno, speck Subscribers: avivey, speck, tobiaswiese, Matthew, Cigaryno Maniphest Tasks: T15182, T15161 Differential Revision: https://we.phorge.it/D25118
This commit is contained in:
parent
59678094fb
commit
349f006904
6 changed files with 178 additions and 9 deletions
|
@ -5799,6 +5799,8 @@ phutil_register_library_map(array(
|
|||
'PhutilTranslatedHTMLTestCase' => 'infrastructure/markup/__tests__/PhutilTranslatedHTMLTestCase.php',
|
||||
'PhutilTwitchAuthAdapter' => 'applications/auth/adapter/PhutilTwitchAuthAdapter.php',
|
||||
'PhutilTwitterAuthAdapter' => 'applications/auth/adapter/PhutilTwitterAuthAdapter.php',
|
||||
'PhutilURIHelper' => 'infrastructure/parser/PhutilURIHelper.php',
|
||||
'PhutilURIHelperTestCase' => 'infrastructure/parser/__tests__/PhutilURIHelperTestCase.php',
|
||||
'PhutilWordPressAuthAdapter' => 'applications/auth/adapter/PhutilWordPressAuthAdapter.php',
|
||||
'PhutilXHPASTSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
|
||||
'PhutilXHPASTSyntaxHighlighterFuture' => 'infrastructure/markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
|
||||
|
@ -12705,6 +12707,8 @@ phutil_register_library_map(array(
|
|||
'PhutilTranslatedHTMLTestCase' => 'PhutilTestCase',
|
||||
'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
|
||||
'PhutilURIHelper' => 'Phobject',
|
||||
'PhutilURIHelperTestCase' => 'PhabricatorTestCase',
|
||||
'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
|
||||
'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
|
||||
|
|
|
@ -44,16 +44,16 @@ final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule {
|
|||
protected function renderHyperlink($link, $name) {
|
||||
$engine = $this->getEngine();
|
||||
|
||||
$is_anchor = false;
|
||||
if (strncmp($link, '/', 1) == 0) {
|
||||
$uri = new PhutilURIHelper($link);
|
||||
$is_anchor = $uri->isAnchor();
|
||||
$starts_with_slash = $uri->isStartingWithSlash();
|
||||
if ($starts_with_slash) {
|
||||
$base = phutil_string_cast($engine->getConfig('uri.base'));
|
||||
$base = rtrim($base, '/');
|
||||
$link = $base.$link;
|
||||
} else if (strncmp($link, '#', 1) == 0) {
|
||||
} else if ($is_anchor) {
|
||||
$here = $engine->getConfig('uri.here');
|
||||
$link = $here.$link;
|
||||
|
||||
$is_anchor = true;
|
||||
}
|
||||
|
||||
if ($engine->isTextMode()) {
|
||||
|
@ -76,7 +76,13 @@ final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule {
|
|||
return $name;
|
||||
}
|
||||
|
||||
$same_window = $engine->getConfig('uri.same-window', false);
|
||||
// Check if this link points to Phorge itself. Micro-optimized.
|
||||
$is_self = $is_anchor || $starts_with_slash || $uri->isSelf();
|
||||
|
||||
// For historical reasons, links opened in a different tab
|
||||
// for most links as default.
|
||||
// Now internal resources keep internal link, as default.
|
||||
$same_window = $engine->getConfig('uri.same-window', $is_self);
|
||||
if ($same_window) {
|
||||
$target = null;
|
||||
} else {
|
||||
|
@ -92,7 +98,7 @@ final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule {
|
|||
'a',
|
||||
array(
|
||||
'href' => $link,
|
||||
'class' => 'remarkup-link',
|
||||
'class' => $this->getRemarkupLinkClass($is_self),
|
||||
'target' => $target,
|
||||
'rel' => 'noreferrer',
|
||||
),
|
||||
|
|
|
@ -116,7 +116,9 @@ final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
|
|||
|
||||
$engine = $this->getEngine();
|
||||
|
||||
$same_window = $engine->getConfig('uri.same-window', false);
|
||||
$uri = new PhutilURIHelper($link);
|
||||
$is_self = $uri->isSelf();
|
||||
$same_window = $engine->getConfig('uri.same-window', $is_self);
|
||||
if ($same_window) {
|
||||
$target = null;
|
||||
} else {
|
||||
|
@ -127,7 +129,7 @@ final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
|
|||
'a',
|
||||
array(
|
||||
'href' => $link,
|
||||
'class' => 'remarkup-link',
|
||||
'class' => $this->getRemarkupLinkClass($is_self),
|
||||
'target' => $target,
|
||||
'rel' => 'noreferrer',
|
||||
),
|
||||
|
|
|
@ -112,4 +112,20 @@ abstract class PhutilRemarkupRule extends Phobject {
|
|||
return (strpos($text, PhutilRemarkupBlockStorage::MAGIC_BYTE) === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSS class="" attribute for a Remarkup link.
|
||||
* It's just "remarkup-link" for all cases, plus the possibility for
|
||||
* designers to style external links differently.
|
||||
* @param boolean $is_internal Whenever the link was internal or not.
|
||||
* @return string
|
||||
*/
|
||||
protected function getRemarkupLinkClass($is_internal) {
|
||||
// Allow developers to style esternal links differently
|
||||
$classes = array('remarkup-link');
|
||||
if (!$is_internal) {
|
||||
$classes[] = 'remarkup-link-ext';
|
||||
}
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
78
src/infrastructure/parser/PhutilURIHelper.php
Normal file
78
src/infrastructure/parser/PhutilURIHelper.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A simple wrapper for PhutilURI, to be aware of the
|
||||
* relative/absolute context, and other minor things.
|
||||
*/
|
||||
final class PhutilURIHelper extends Phobject {
|
||||
|
||||
/**
|
||||
* String version of your original URI.
|
||||
* @var string
|
||||
*/
|
||||
private $uriStr;
|
||||
|
||||
/**
|
||||
* Structured version of your URI.
|
||||
* @var PhutilURI
|
||||
*/
|
||||
private $phutilUri;
|
||||
|
||||
/**
|
||||
* @param string|PhutilURI
|
||||
*/
|
||||
public function __construct($uri) {
|
||||
|
||||
// Keep the original string for basic checks.
|
||||
$this->uriStr = phutil_string_cast($uri);
|
||||
|
||||
// A PhutilURI may be useful. If available, import that as-is.
|
||||
// Note that the constructor PhutilURI(string) is a bit expensive.
|
||||
if ($uri instanceof PhutilURI) {
|
||||
$this->phutilUri = $uri;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the URI points to Phorge itself.
|
||||
* @return bool
|
||||
*/
|
||||
public function isSelf() {
|
||||
// The backend prefers a PhutilURI object, if available.
|
||||
$uri = $this->phutilUri ? $this->phutilUri : $this->uriStr;
|
||||
return PhabricatorEnv::isSelfURI($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whenever an URI is just a simple fragment without path and protocol.
|
||||
* @return bool
|
||||
*/
|
||||
public function isAnchor() {
|
||||
return $this->isStartingWithChar('#');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whenever an URI starts with a slash (no protocol, etc.)
|
||||
* @return bool
|
||||
*/
|
||||
public function isStartingWithSlash() {
|
||||
return $this->isStartingWithChar('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* A sane default.
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->uriStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whenever the URI starts with the provided character.
|
||||
* @param string $char String that MUST have length of 1.
|
||||
* @return boolean
|
||||
*/
|
||||
private function isStartingWithChar($char) {
|
||||
return strncmp($this->uriStr, $char, 1) === 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
final class PhutilURIHelperTestCase extends PhabricatorTestCase {
|
||||
|
||||
public function testPhutilURIHelper() {
|
||||
|
||||
// Every row is a test. Every column is:
|
||||
// - 0: name of the test
|
||||
// - 1: test input value
|
||||
// - 2: is the URI pointing to Phorge itself?
|
||||
// - 3: is the URI an anchor? (no domain, no protocol)
|
||||
// - 4: is the URI starting with a slash? (no domain, no protocol)
|
||||
$tests = array(
|
||||
array('internal anchor', '#asd', true, true, false),
|
||||
array('internal relative dir', '/foo/', true, false, true),
|
||||
array('internal relative dir also', 'foo/', true, false, false),
|
||||
array('internal root dir', '/', true, false, true),
|
||||
array('internal root dir', './', true, false, false),
|
||||
array('internal root dir', '../', true, false, false),
|
||||
array('internal root dir', '/#asd', true, false, true),
|
||||
array('external', 'https://gnu.org/', false, false, false),
|
||||
array('external anchor', 'https://gnu.org/#asd', false, false, false),
|
||||
);
|
||||
|
||||
// Add additional self-tests if base URI is available.
|
||||
$base = PhabricatorEnv::getEnvConfigIfExists('phabricator.base-uri');
|
||||
if ($base) {
|
||||
$domain = id(new PhutilURI($base))->getDomain();
|
||||
$tests[] = array('base uri', $base, true, false, false);
|
||||
$tests[] = array('base uri anchor', "{$base}#asd", true, false, false);
|
||||
}
|
||||
|
||||
foreach ($tests as $test) {
|
||||
$name = $test[0];
|
||||
$uri = $test[1];
|
||||
$is_self = $test[2];
|
||||
$is_anchor = $test[3];
|
||||
$is_slash = $test[4];
|
||||
|
||||
// Test input variants for the constructor of PhutilURIHelper.
|
||||
$uri_variants = array(
|
||||
$uri,
|
||||
new PhutilURI($uri),
|
||||
);
|
||||
foreach ($uri_variants as $variant_uri) {
|
||||
|
||||
$test_name = pht("test %s value '%s' (from '%s' type %s)",
|
||||
$name, $variant_uri, $uri, phutil_describe_type($variant_uri));
|
||||
|
||||
$uri = new PhutilURIHelper($variant_uri);
|
||||
|
||||
$this->assertEqual($is_self, $uri->isSelf(),
|
||||
pht('%s - points to myself', $test_name));
|
||||
|
||||
$this->assertEqual($is_anchor, $uri->isAnchor(),
|
||||
pht('%s - is just an anchor', $test_name));
|
||||
|
||||
$this->assertEqual($is_slash, $uri->isStartingWithSlash(),
|
||||
pht('%s - is starting with slash', $test_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue