@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
at recaptime-dev/main 185 lines 4.7 kB view raw
1<?php 2 3final class PhutilRemarkupHeaderBlockRule extends PhutilRemarkupBlockRule { 4 5 public function getMatchingLineCount(array $lines, $cursor) { 6 $num_lines = 0; 7 if (preg_match('/^(={1,5}|#{2,5}|# ).*+$/', $lines[$cursor])) { 8 $num_lines = 1; 9 } else { 10 if (isset($lines[$cursor + 1])) { 11 $line = $lines[$cursor].$lines[$cursor + 1]; 12 if (preg_match('/^([^\n]+)\n[-=]{2,}\s*$/', $line)) { 13 $num_lines = 2; 14 $cursor++; 15 } 16 } 17 } 18 19 if ($num_lines) { 20 $cursor++; 21 while (isset($lines[$cursor]) && !strlen(trim($lines[$cursor]))) { 22 $num_lines++; 23 $cursor++; 24 } 25 } 26 27 return $num_lines; 28 } 29 30 const KEY_HEADER_TOC = 'headers.toc'; 31 32 public function markupText($text, $children) { 33 $text = trim($text); 34 35 $lines = phutil_split_lines($text); 36 if (count($lines) > 1) { 37 $level = ($lines[1][0] == '=') ? 1 : 2; 38 $text = trim($lines[0]); 39 } else { 40 $level = 0; 41 for ($ii = 0; $ii < min(5, strlen($text)); $ii++) { 42 if ($text[$ii] == '=' || $text[$ii] == '#') { 43 ++$level; 44 } else { 45 break; 46 } 47 } 48 $text = trim($text, ' =#'); 49 } 50 51 $engine = $this->getEngine(); 52 53 if ($engine->isTextMode()) { 54 $char = ($level == 1) ? '=' : '-'; 55 return $text."\n".str_repeat($char, phutil_utf8_strlen($text)); 56 } 57 58 $use_anchors = $engine->getConfig('header.generate-toc'); 59 60 $anchor = null; 61 if ($use_anchors) { 62 $anchor = $this->generateAnchor($level, $text); 63 } 64 65 $text = phutil_tag( 66 'h'.($level + 1), 67 array( 68 'class' => 'remarkup-header', 69 ), 70 array($anchor, $this->applyRules($text))); 71 72 return $text; 73 } 74 75 private function generateAnchor($level, $text) { 76 $engine = $this->getEngine(); 77 78 // When a document contains a link inside a header, like this: 79 // 80 // = [[ http://wwww.example.com/ | example ]] = 81 // 82 // ...we want to generate a TOC entry with just "example", but link the 83 // header itself. We push the 'toc' state so all the link rules generate 84 // just names. 85 $engine->pushState('toc'); 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 } 96 97 $base = $anchor; 98 99 $key = self::KEY_HEADER_TOC; 100 $anchors = $engine->getTextMetadata($key, array()); 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); 110 $engine->setTextMetadata($key, $anchors); 111 112 return phutil_tag( 113 'a', 114 array( 115 'name' => $anchor, 116 ), 117 ''); 118 } 119 120 public static function renderTableOfContents(PhutilRemarkupEngine $engine) { 121 122 $key = self::KEY_HEADER_TOC; 123 $anchors = $engine->getTextMetadata($key, array()); 124 125 if (count($anchors) < 2) { 126 // Don't generate a TOC if there are no headers, or if there's only 127 // one header (since such a TOC would be silly). 128 return null; 129 } 130 131 $depth = 0; 132 $toc = array(); 133 foreach ($anchors as $anchor => $info) { 134 list($level, $name) = $info; 135 136 while ($depth < $level) { 137 $toc[] = hsprintf('<ul>'); 138 $depth++; 139 } 140 while ($depth > $level) { 141 $toc[] = hsprintf('</ul>'); 142 $depth--; 143 } 144 145 $toc[] = phutil_tag( 146 'li', 147 array(), 148 phutil_tag( 149 'a', 150 array( 151 'href' => '#'.$anchor, 152 ), 153 $name)); 154 } 155 while ($depth > 0) { 156 $toc[] = hsprintf('</ul>'); 157 $depth--; 158 } 159 160 return phutil_implode_html("\n", $toc); 161 } 162 163 public static function getAnchorNameFromHeaderText($text) { 164 $anchor = phutil_utf8_strtolower($text); 165 $anchor = PhutilRemarkupAnchorRule::normalizeAnchor($anchor); 166 167 // Truncate the fragment to something reasonable. 168 $anchor = id(new PhutilUTF8StringTruncator()) 169 ->setMaximumGlyphs(32) 170 ->setTerminator('') 171 ->truncateString($anchor); 172 173 // If the fragment is terminated by a word which "The U.S. Government 174 // Printing Office Style Manual" normally discourages capitalizing in 175 // titles, discard it. This is an arbitrary heuristic intended to avoid 176 // awkward hanging words in anchors. 177 $anchor = preg_replace( 178 '/-(a|an|the|at|by|for|in|of|on|per|to|up|and|as|but|if|or|nor)\z/', 179 '', 180 $anchor); 181 182 return $anchor; 183 } 184 185}