Path: blob/master/src/infrastructure/markup/blockrule/PhutilRemarkupHeaderBlockRule.php
12242 views
<?php12final class PhutilRemarkupHeaderBlockRule extends PhutilRemarkupBlockRule {34public function getMatchingLineCount(array $lines, $cursor) {5$num_lines = 0;6if (preg_match('/^(={1,5}|#{2,5}|# ).*+$/', $lines[$cursor])) {7$num_lines = 1;8} else {9if (isset($lines[$cursor + 1])) {10$line = $lines[$cursor].$lines[$cursor + 1];11if (preg_match('/^([^\n]+)\n[-=]{2,}\s*$/', $line)) {12$num_lines = 2;13$cursor++;14}15}16}1718if ($num_lines) {19$cursor++;20while (isset($lines[$cursor]) && !strlen(trim($lines[$cursor]))) {21$num_lines++;22$cursor++;23}24}2526return $num_lines;27}2829const KEY_HEADER_TOC = 'headers.toc';3031public function markupText($text, $children) {32$text = trim($text);3334$lines = phutil_split_lines($text);35if (count($lines) > 1) {36$level = ($lines[1][0] == '=') ? 1 : 2;37$text = trim($lines[0]);38} else {39$level = 0;40for ($ii = 0; $ii < min(5, strlen($text)); $ii++) {41if ($text[$ii] == '=' || $text[$ii] == '#') {42++$level;43} else {44break;45}46}47$text = trim($text, ' =#');48}4950$engine = $this->getEngine();5152if ($engine->isTextMode()) {53$char = ($level == 1) ? '=' : '-';54return $text."\n".str_repeat($char, phutil_utf8_strlen($text));55}5657$use_anchors = $engine->getConfig('header.generate-toc');5859$anchor = null;60if ($use_anchors) {61$anchor = $this->generateAnchor($level, $text);62}6364$text = phutil_tag(65'h'.($level + 1),66array(67'class' => 'remarkup-header',68),69array($anchor, $this->applyRules($text)));7071return $text;72}7374private function generateAnchor($level, $text) {75$engine = $this->getEngine();7677// When a document contains a link inside a header, like this:78//79// = [[ http://wwww.example.com/ | example ]] =80//81// ...we want to generate a TOC entry with just "example", but link the82// header itself. We push the 'toc' state so all the link rules generate83// just names.84$engine->pushState('toc');85$plain_text = $text;86$plain_text = $this->applyRules($plain_text);87$plain_text = $engine->restoreText($plain_text);88$engine->popState('toc');8990$anchor = self::getAnchorNameFromHeaderText($plain_text);9192if (!strlen($anchor)) {93return null;94}9596$base = $anchor;9798$key = self::KEY_HEADER_TOC;99$anchors = $engine->getTextMetadata($key, array());100101$suffix = 1;102while (isset($anchors[$anchor])) {103$anchor = $base.'-'.$suffix;104$anchor = trim($anchor, '-');105$suffix++;106}107108$anchors[$anchor] = array($level, $plain_text);109$engine->setTextMetadata($key, $anchors);110111return phutil_tag(112'a',113array(114'name' => $anchor,115),116'');117}118119public static function renderTableOfContents(PhutilRemarkupEngine $engine) {120121$key = self::KEY_HEADER_TOC;122$anchors = $engine->getTextMetadata($key, array());123124if (count($anchors) < 2) {125// Don't generate a TOC if there are no headers, or if there's only126// one header (since such a TOC would be silly).127return null;128}129130$depth = 0;131$toc = array();132foreach ($anchors as $anchor => $info) {133list($level, $name) = $info;134135while ($depth < $level) {136$toc[] = hsprintf('<ul>');137$depth++;138}139while ($depth > $level) {140$toc[] = hsprintf('</ul>');141$depth--;142}143144$toc[] = phutil_tag(145'li',146array(),147phutil_tag(148'a',149array(150'href' => '#'.$anchor,151),152$name));153}154while ($depth > 0) {155$toc[] = hsprintf('</ul>');156$depth--;157}158159return phutil_implode_html("\n", $toc);160}161162public static function getAnchorNameFromHeaderText($text) {163$anchor = phutil_utf8_strtolower($text);164$anchor = PhutilRemarkupAnchorRule::normalizeAnchor($anchor);165166// Truncate the fragment to something reasonable.167$anchor = id(new PhutilUTF8StringTruncator())168->setMaximumGlyphs(32)169->setTerminator('')170->truncateString($anchor);171172// If the fragment is terminated by a word which "The U.S. Government173// Printing Office Style Manual" normally discourages capitalizing in174// titles, discard it. This is an arbitrary heuristic intended to avoid175// awkward hanging words in anchors.176$anchor = preg_replace(177'/-(a|an|the|at|by|for|in|of|on|per|to|up|and|as|but|if|or|nor)\z/',178'',179$anchor);180181return $anchor;182}183184}185186187