@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.

Generate more friendly anchor names for header sections in Remarkup

Summary:
Depends on D20820. Ref T13410. We currently cut anchor names in the middle, don't support emoji in anchors, and generate relatively short anchors.

Generate slightly longer anchors, allow more unicode, and try not to cut things in the middle.

Test Plan: Created a document with a variety of different anchors and saw them generate more usable names.

Maniphest Tasks: T13410

Differential Revision: https://secure.phabricator.com/D20821

+100 -24
+2
src/__phutil_library_map__.php
··· 2150 2150 'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php', 2151 2151 'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php', 2152 2152 'PhabricatorAmazonSNSFuture' => 'applications/metamta/future/PhabricatorAmazonSNSFuture.php', 2153 + 'PhabricatorAnchorTestCase' => 'infrastructure/markup/__tests__/PhabricatorAnchorTestCase.php', 2153 2154 'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php', 2154 2155 'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php', 2155 2156 'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php', ··· 8314 8315 'PhabricatorAlmanacApplication' => 'PhabricatorApplication', 8315 8316 'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider', 8316 8317 'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture', 8318 + 'PhabricatorAnchorTestCase' => 'PhabricatorTestCase', 8317 8319 'PhabricatorAnchorView' => 'AphrontView', 8318 8320 'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow', 8319 8321 'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
+38
src/infrastructure/markup/__tests__/PhabricatorAnchorTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorAnchorTestCase 4 + extends PhabricatorTestCase { 5 + 6 + public function testAnchors() { 7 + 8 + $low_ascii = ''; 9 + for ($ii = 19; $ii <= 127; $ii++) { 10 + $low_ascii .= chr($ii); 11 + } 12 + 13 + $snowman = "\xE2\x9B\x84"; 14 + 15 + $map = array( 16 + '' => '', 17 + 'Bells and Whistles' => 'bells-and-whistles', 18 + 'Termination for Nonpayment' => 'termination-for-nonpayment', 19 + $low_ascii => '0123456789-abcdefghijklmnopqrstu', 20 + 'xxxx xxxx xxxx xxxx xxxx on' => 'xxxx-xxxx-xxxx-xxxx-xxxx', 21 + 'xxxx xxxx xxxx xxxx xxxx ox' => 'xxxx-xxxx-xxxx-xxxx-xxxx-ox', 22 + "So, You Want To Build A {$snowman}?" => 23 + "so-you-want-to-build-a-{$snowman}", 24 + str_repeat($snowman, 128) => str_repeat($snowman, 32), 25 + ); 26 + 27 + foreach ($map as $input => $expect) { 28 + $anchor = PhutilRemarkupHeaderBlockRule::getAnchorNameFromHeaderText( 29 + $input); 30 + 31 + $this->assertEqual( 32 + $expect, 33 + $anchor, 34 + pht('Anchor for "%s".', $input)); 35 + } 36 + } 37 + 38 + }
+49 -21
src/infrastructure/markup/blockrule/PhutilRemarkupHeaderBlockRule.php
··· 73 73 } 74 74 75 75 private function generateAnchor($level, $text) { 76 - $anchor = strtolower($text); 77 - $anchor = preg_replace('/[^a-z0-9]/', '-', $anchor); 78 - $anchor = preg_replace('/--+/', '-', $anchor); 79 - $anchor = trim($anchor, '-'); 80 - $anchor = substr($anchor, 0, 24); 81 - $anchor = trim($anchor, '-'); 82 - $base = $anchor; 83 - 84 - $key = self::KEY_HEADER_TOC; 85 76 $engine = $this->getEngine(); 86 - $anchors = $engine->getTextMetadata($key, array()); 87 - 88 - $suffix = 1; 89 - while (!strlen($anchor) || isset($anchors[$anchor])) { 90 - $anchor = $base.'-'.$suffix; 91 - $anchor = trim($anchor, '-'); 92 - $suffix++; 93 - } 94 77 95 78 // When a document contains a link inside a header, like this: 96 79 // ··· 100 83 // header itself. We push the 'toc' state so all the link rules generate 101 84 // just names. 102 85 $engine->pushState('toc'); 103 - $text = $this->applyRules($text); 104 - $text = $engine->restoreText($text); 86 + $plain_text = $text; 87 + $plain_text = $this->applyRules($plain_text); 88 + $plain_text = $engine->restoreText($plain_text); 89 + $engine->popState('toc'); 90 + 91 + $anchor = self::getAnchorNameFromHeaderText($plain_text); 92 + 93 + if (!strlen($anchor)) { 94 + return null; 95 + } 105 96 106 - $anchors[$anchor] = array($level, $text); 107 - $engine->popState('toc'); 97 + $base = $anchor; 98 + 99 + $key = self::KEY_HEADER_TOC; 100 + $anchors = $engine->getTextMetadata($key, array()); 108 101 102 + $suffix = 1; 103 + while (isset($anchors[$anchor])) { 104 + $anchor = $base.'-'.$suffix; 105 + $anchor = trim($anchor, '-'); 106 + $suffix++; 107 + } 108 + 109 + $anchors[$anchor] = array($level, $plain_text); 109 110 $engine->setTextMetadata($key, $anchors); 110 111 111 112 return phutil_tag( ··· 157 158 } 158 159 159 160 return phutil_implode_html("\n", $toc); 161 + } 162 + 163 + public static function getAnchorNameFromHeaderText($text) { 164 + $anchor = phutil_utf8_strtolower($text); 165 + 166 + // Replace all latin characters which are not "a-z" or "0-9" with "-". 167 + // Preserve other characters, since non-latin letters and emoji work 168 + // fine in anchors. 169 + $anchor = preg_replace('/[\x00-\x2F\x3A-\x60\x7B-\x7F]+/', '-', $anchor); 170 + $anchor = trim($anchor, '-'); 171 + 172 + // Truncate the fragment to something reasonable. 173 + $anchor = id(new PhutilUTF8StringTruncator()) 174 + ->setMaximumGlyphs(32) 175 + ->setTerminator('') 176 + ->truncateString($anchor); 177 + 178 + // If the fragment is terminated by a word which "The U.S. Government 179 + // Printing Office Style Manual" normally discourages capitalizing in 180 + // titles, discard it. This is an arbitrary heuristic intended to avoid 181 + // awkward hanging words in anchors. 182 + $anchor = preg_replace( 183 + '/-(a|an|the|at|by|for|in|of|on|per|to|up|and|as|but|if|or|nor)\z/', 184 + '', 185 + $anchor); 186 + 187 + return $anchor; 160 188 } 161 189 162 190 }
+4
src/infrastructure/markup/markuprule/PhutilRemarkupBoldRule.php
··· 18 18 } 19 19 20 20 protected function applyCallback(array $matches) { 21 + if ($this->getEngine()->isAnchorMode()) { 22 + return $matches[1]; 23 + } 24 + 21 25 return hsprintf('<strong>%s</strong>', $matches[1]); 22 26 } 23 27
+4
src/infrastructure/markup/remarkup/PhutilRemarkupEngine.php
··· 34 34 return $this->mode & self::MODE_TEXT; 35 35 } 36 36 37 + public function isAnchorMode() { 38 + return $this->getState('toc'); 39 + } 40 + 37 41 public function isHTMLMailMode() { 38 42 return $this->mode & self::MODE_HTML_MAIL; 39 43 }
+3 -3
src/infrastructure/markup/remarkup/__tests__/remarkup/toc.txt
··· 6 6 7 7 ~~~~~~~~~~ 8 8 <ul> 9 - <li><a href="#http-www-example-com-lin">link_name</a></li> 9 + <li><a href="#link-name">link_name</a></li> 10 10 <ul> 11 - <li><a href="#bold"><strong>bold</strong></a></li> 11 + <li><a href="#bold">bold</a></li> 12 12 </ul> 13 13 <li><a href="#http-www-example-com">http://www.example.com</a></li> 14 14 </ul> 15 15 16 - <h2 class="remarkup-header"><a name="http-www-example-com-lin"></a><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">link_name</a></h2> 16 + <h2 class="remarkup-header"><a name="link-name"></a><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">link_name</a></h2> 17 17 18 18 <h3 class="remarkup-header"><a name="bold"></a><strong>bold</strong></h3> 19 19