@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<?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}