Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/markup/blockrule/PhutilRemarkupHeaderBlockRule.php
12242 views
1
<?php
2
3
final 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
}
186
187