@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+178 -9
+4
src/__phutil_library_map__.php
··· 5799 5799 'PhutilTranslatedHTMLTestCase' => 'infrastructure/markup/__tests__/PhutilTranslatedHTMLTestCase.php', 5800 5800 'PhutilTwitchAuthAdapter' => 'applications/auth/adapter/PhutilTwitchAuthAdapter.php', 5801 5801 'PhutilTwitterAuthAdapter' => 'applications/auth/adapter/PhutilTwitterAuthAdapter.php', 5802 + 'PhutilURIHelper' => 'infrastructure/parser/PhutilURIHelper.php', 5803 + 'PhutilURIHelperTestCase' => 'infrastructure/parser/__tests__/PhutilURIHelperTestCase.php', 5802 5804 'PhutilWordPressAuthAdapter' => 'applications/auth/adapter/PhutilWordPressAuthAdapter.php', 5803 5805 'PhutilXHPASTSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 5804 5806 'PhutilXHPASTSyntaxHighlighterFuture' => 'infrastructure/markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php', ··· 12705 12707 'PhutilTranslatedHTMLTestCase' => 'PhutilTestCase', 12706 12708 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 12707 12709 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', 12710 + 'PhutilURIHelper' => 'Phobject', 12711 + 'PhutilURIHelperTestCase' => 'PhabricatorTestCase', 12708 12712 'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter', 12709 12713 'PhutilXHPASTSyntaxHighlighter' => 'Phobject', 12710 12714 'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
+13 -7
src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
··· 44 44 protected function renderHyperlink($link, $name) { 45 45 $engine = $this->getEngine(); 46 46 47 - $is_anchor = false; 48 - if (strncmp($link, '/', 1) == 0) { 47 + $uri = new PhutilURIHelper($link); 48 + $is_anchor = $uri->isAnchor(); 49 + $starts_with_slash = $uri->isStartingWithSlash(); 50 + if ($starts_with_slash) { 49 51 $base = phutil_string_cast($engine->getConfig('uri.base')); 50 52 $base = rtrim($base, '/'); 51 53 $link = $base.$link; 52 - } else if (strncmp($link, '#', 1) == 0) { 54 + } else if ($is_anchor) { 53 55 $here = $engine->getConfig('uri.here'); 54 56 $link = $here.$link; 55 - 56 - $is_anchor = true; 57 57 } 58 58 59 59 if ($engine->isTextMode()) { ··· 76 76 return $name; 77 77 } 78 78 79 - $same_window = $engine->getConfig('uri.same-window', false); 79 + // Check if this link points to Phorge itself. Micro-optimized. 80 + $is_self = $is_anchor || $starts_with_slash || $uri->isSelf(); 81 + 82 + // For historical reasons, links opened in a different tab 83 + // for most links as default. 84 + // Now internal resources keep internal link, as default. 85 + $same_window = $engine->getConfig('uri.same-window', $is_self); 80 86 if ($same_window) { 81 87 $target = null; 82 88 } else { ··· 92 98 'a', 93 99 array( 94 100 'href' => $link, 95 - 'class' => 'remarkup-link', 101 + 'class' => $this->getRemarkupLinkClass($is_self), 96 102 'target' => $target, 97 103 'rel' => 'noreferrer', 98 104 ),
+4 -2
src/infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRule.php
··· 116 116 117 117 $engine = $this->getEngine(); 118 118 119 - $same_window = $engine->getConfig('uri.same-window', false); 119 + $uri = new PhutilURIHelper($link); 120 + $is_self = $uri->isSelf(); 121 + $same_window = $engine->getConfig('uri.same-window', $is_self); 120 122 if ($same_window) { 121 123 $target = null; 122 124 } else { ··· 127 129 'a', 128 130 array( 129 131 'href' => $link, 130 - 'class' => 'remarkup-link', 132 + 'class' => $this->getRemarkupLinkClass($is_self), 131 133 'target' => $target, 132 134 'rel' => 'noreferrer', 133 135 ),
+16
src/infrastructure/markup/markuprule/PhutilRemarkupRule.php
··· 112 112 return (strpos($text, PhutilRemarkupBlockStorage::MAGIC_BYTE) === false); 113 113 } 114 114 115 + /** 116 + * Get the CSS class="" attribute for a Remarkup link. 117 + * It's just "remarkup-link" for all cases, plus the possibility for 118 + * designers to style external links differently. 119 + * @param boolean $is_internal Whenever the link was internal or not. 120 + * @return string 121 + */ 122 + protected function getRemarkupLinkClass($is_internal) { 123 + // Allow developers to style esternal links differently 124 + $classes = array('remarkup-link'); 125 + if (!$is_internal) { 126 + $classes[] = 'remarkup-link-ext'; 127 + } 128 + return implode(' ', $classes); 129 + } 130 + 115 131 }
+78
src/infrastructure/parser/PhutilURIHelper.php
··· 1 + <?php 2 + 3 + /** 4 + * A simple wrapper for PhutilURI, to be aware of the 5 + * relative/absolute context, and other minor things. 6 + */ 7 + final class PhutilURIHelper extends Phobject { 8 + 9 + /** 10 + * String version of your original URI. 11 + * @var string 12 + */ 13 + private $uriStr; 14 + 15 + /** 16 + * Structured version of your URI. 17 + * @var PhutilURI 18 + */ 19 + private $phutilUri; 20 + 21 + /** 22 + * @param string|PhutilURI 23 + */ 24 + public function __construct($uri) { 25 + 26 + // Keep the original string for basic checks. 27 + $this->uriStr = phutil_string_cast($uri); 28 + 29 + // A PhutilURI may be useful. If available, import that as-is. 30 + // Note that the constructor PhutilURI(string) is a bit expensive. 31 + if ($uri instanceof PhutilURI) { 32 + $this->phutilUri = $uri; 33 + } 34 + } 35 + 36 + /** 37 + * Check if the URI points to Phorge itself. 38 + * @return bool 39 + */ 40 + public function isSelf() { 41 + // The backend prefers a PhutilURI object, if available. 42 + $uri = $this->phutilUri ? $this->phutilUri : $this->uriStr; 43 + return PhabricatorEnv::isSelfURI($uri); 44 + } 45 + 46 + /** 47 + * Check whenever an URI is just a simple fragment without path and protocol. 48 + * @return bool 49 + */ 50 + public function isAnchor() { 51 + return $this->isStartingWithChar('#'); 52 + } 53 + 54 + /** 55 + * Check whenever an URI starts with a slash (no protocol, etc.) 56 + * @return bool 57 + */ 58 + public function isStartingWithSlash() { 59 + return $this->isStartingWithChar('/'); 60 + } 61 + 62 + /** 63 + * A sane default. 64 + */ 65 + public function __toString() { 66 + return $this->uriStr; 67 + } 68 + 69 + /** 70 + * Check whenever the URI starts with the provided character. 71 + * @param string $char String that MUST have length of 1. 72 + * @return boolean 73 + */ 74 + private function isStartingWithChar($char) { 75 + return strncmp($this->uriStr, $char, 1) === 0; 76 + } 77 + 78 + }
+63
src/infrastructure/parser/__tests__/PhutilURIHelperTestCase.php
··· 1 + <?php 2 + 3 + final class PhutilURIHelperTestCase extends PhabricatorTestCase { 4 + 5 + public function testPhutilURIHelper() { 6 + 7 + // Every row is a test. Every column is: 8 + // - 0: name of the test 9 + // - 1: test input value 10 + // - 2: is the URI pointing to Phorge itself? 11 + // - 3: is the URI an anchor? (no domain, no protocol) 12 + // - 4: is the URI starting with a slash? (no domain, no protocol) 13 + $tests = array( 14 + array('internal anchor', '#asd', true, true, false), 15 + array('internal relative dir', '/foo/', true, false, true), 16 + array('internal relative dir also', 'foo/', true, false, false), 17 + array('internal root dir', '/', true, false, true), 18 + array('internal root dir', './', true, false, false), 19 + array('internal root dir', '../', true, false, false), 20 + array('internal root dir', '/#asd', true, false, true), 21 + array('external', 'https://gnu.org/', false, false, false), 22 + array('external anchor', 'https://gnu.org/#asd', false, false, false), 23 + ); 24 + 25 + // Add additional self-tests if base URI is available. 26 + $base = PhabricatorEnv::getEnvConfigIfExists('phabricator.base-uri'); 27 + if ($base) { 28 + $domain = id(new PhutilURI($base))->getDomain(); 29 + $tests[] = array('base uri', $base, true, false, false); 30 + $tests[] = array('base uri anchor', "{$base}#asd", true, false, false); 31 + } 32 + 33 + foreach ($tests as $test) { 34 + $name = $test[0]; 35 + $uri = $test[1]; 36 + $is_self = $test[2]; 37 + $is_anchor = $test[3]; 38 + $is_slash = $test[4]; 39 + 40 + // Test input variants for the constructor of PhutilURIHelper. 41 + $uri_variants = array( 42 + $uri, 43 + new PhutilURI($uri), 44 + ); 45 + foreach ($uri_variants as $variant_uri) { 46 + 47 + $test_name = pht("test %s value '%s' (from '%s' type %s)", 48 + $name, $variant_uri, $uri, phutil_describe_type($variant_uri)); 49 + 50 + $uri = new PhutilURIHelper($variant_uri); 51 + 52 + $this->assertEqual($is_self, $uri->isSelf(), 53 + pht('%s - points to myself', $test_name)); 54 + 55 + $this->assertEqual($is_anchor, $uri->isAnchor(), 56 + pht('%s - is just an anchor', $test_name)); 57 + 58 + $this->assertEqual($is_slash, $uri->isStartingWithSlash(), 59 + pht('%s - is starting with slash', $test_name)); 60 + } 61 + } 62 + } 63 + }