@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 PhabricatorKeyboardRemarkupRule extends PhutilRemarkupRule {
4
5 public function getPriority() {
6 return 200.0;
7 }
8
9 public function apply($text) {
10 return preg_replace_callback(
11 '@{key\b((?:[^}\\\\]+|\\\\.)*)}@m',
12 array($this, 'markupKeystrokes'),
13 $text);
14 }
15
16 public function markupKeystrokes(array $matches) {
17 if (!$this->isFlatText($matches[0])) {
18 return $matches[0];
19 }
20
21 $keys = explode(' ', $matches[1]);
22 foreach ($keys as $k => $v) {
23 $v = trim($v, " \n");
24 $v = preg_replace('/\\\\(.)/', '\\1', $v);
25 if ($v === '') {
26 unset($keys[$k]);
27 continue;
28 }
29 $keys[$k] = $v;
30 }
31
32 $special = array(
33 array(
34 'name' => pht('Command'),
35 'symbol' => "\xE2\x8C\x98",
36 'aliases' => array(
37 'cmd',
38 'command',
39 ),
40 ),
41 array(
42 'name' => pht('Option'),
43 'symbol' => "\xE2\x8C\xA5",
44 'aliases' => array(
45 'opt',
46 'option',
47 ),
48 ),
49 array(
50 'name' => pht('Shift'),
51 'symbol' => "\xE2\x87\xA7",
52 'aliases' => array(
53 'shift',
54 ),
55 ),
56 array(
57 'name' => pht('Escape'),
58 'symbol' => "\xE2\x8E\x8B",
59 'aliases' => array(
60 'esc',
61 'escape',
62 ),
63 ),
64 array(
65 'name' => pht('Enter'),
66 'symbol' => "\xE2\x8F\x8E",
67 'aliases' => array(
68 'enter',
69 'return',
70 ),
71 ),
72 array(
73 'name' => pht('Control'),
74 'symbol' => "\xE2\x8C\x83",
75 'aliases' => array(
76 'ctrl',
77 'control',
78 ),
79 ),
80 array(
81 'name' => pht('Up'),
82 'symbol' => "\xE2\x86\x91",
83 'heavy' => "\xE2\xAC\x86",
84 'aliases' => array(
85 'up',
86 'arrow-up',
87 'up-arrow',
88 'north',
89 ),
90 ),
91 array(
92 'name' => pht('Tab'),
93 'symbol' => "\xE2\x87\xA5",
94 'aliases' => array(
95 'tab',
96 ),
97 ),
98 array(
99 'name' => pht('Right'),
100 'symbol' => "\xE2\x86\x92",
101 'heavy' => "\xE2\x9E\xA1",
102 'aliases' => array(
103 'right',
104 'right-arrow',
105 'arrow-right',
106 'east',
107 ),
108 ),
109 array(
110 'name' => pht('Left'),
111 'symbol' => "\xE2\x86\x90",
112 'heavy' => "\xE2\xAC\x85",
113 'aliases' => array(
114 'left',
115 'left-arrow',
116 'arrow-left',
117 'west',
118 ),
119 ),
120 array(
121 'name' => pht('Down'),
122 'symbol' => "\xE2\x86\x93",
123 'heavy' => "\xE2\xAC\x87",
124 'aliases' => array(
125 'down',
126 'down-arrow',
127 'arrow-down',
128 'south',
129 ),
130 ),
131 array(
132 'name' => pht('Up Right'),
133 'symbol' => "\xE2\x86\x97",
134 'heavy' => "\xE2\xAC\x88",
135 'aliases' => array(
136 'up-right',
137 'upright',
138 'up-right-arrow',
139 'upright-arrow',
140 'arrow-up-right',
141 'arrow-upright',
142 'northeast',
143 'north-east',
144 ),
145 ),
146 array(
147 'name' => pht('Down Right'),
148 'symbol' => "\xE2\x86\x98",
149 'heavy' => "\xE2\xAC\x8A",
150 'aliases' => array(
151 'down-right',
152 'downright',
153 'down-right-arrow',
154 'downright-arrow',
155 'arrow-down-right',
156 'arrow-downright',
157 'southeast',
158 'south-east',
159 ),
160 ),
161 array(
162 'name' => pht('Down Left'),
163 'symbol' => "\xE2\x86\x99",
164 'heavy' => "\xE2\xAC\x8B",
165 'aliases' => array(
166 'down-left',
167 'downleft',
168 'down-left-arrow',
169 'downleft-arrow',
170 'arrow-down-left',
171 'arrow-downleft',
172 'southwest',
173 'south-west',
174 ),
175 ),
176 array(
177 'name' => pht('Up Left'),
178 'symbol' => "\xE2\x86\x96",
179 'heavy' => "\xE2\xAC\x89",
180 'aliases' => array(
181 'up-left',
182 'upleft',
183 'up-left-arrow',
184 'upleft-arrow',
185 'arrow-up-left',
186 'arrow-upleft',
187 'northwest',
188 'north-west',
189 ),
190 ),
191 );
192
193 $map = array();
194 foreach ($special as $spec) {
195 foreach ($spec['aliases'] as $alias) {
196 $map[$alias] = $spec;
197 }
198 }
199
200 $is_text = $this->getEngine()->isTextMode();
201 $is_html_mail = $this->getEngine()->isHTMLMailMode();
202
203 if ($is_html_mail) {
204 $key_style = array(
205 'display: inline-block;',
206 'min-width: 1em;',
207 'padding: 4px 5px 5px;',
208 'font-weight: normal;',
209 'font-size: 0.8rem;',
210 'text-align: center;',
211 'text-decoration: none;',
212 'line-height: 0.6rem;',
213 'border-radius: 3px;',
214 'box-shadow: inset 0 -1px 0 rgba(71, 87, 120, 0.08);',
215 '-webkit-user-select: none;',
216 'user-select: none;',
217 'background: #f7f7f7;',
218 'border: 1px solid #C7CCD9;',
219 );
220 $key_style = implode(' ', $key_style);
221
222 $join_style = array(
223 'padding: 0 4px;',
224 'color: #92969D;',
225 );
226 $join_style = implode(' ', $join_style);
227 } else {
228 $key_style = null;
229 $join_style = null;
230 }
231
232 $parts = array();
233 foreach ($keys as $k => $v) {
234 $normal = phutil_utf8_strtolower($v);
235 if (isset($map[$normal])) {
236 $spec = $map[$normal];
237 } else {
238 $spec = array(
239 'name' => null,
240 'symbol' => $v,
241 );
242 }
243
244 if ($is_text) {
245 $parts[] = '['.$spec['symbol'].']';
246 } else {
247 $parts[] = phutil_tag(
248 'kbd',
249 array(
250 'title' => $spec['name'],
251 'style' => $key_style,
252 ),
253 $spec['symbol']);
254 }
255 }
256
257 if ($is_text) {
258 $parts = implode(' + ', $parts);
259 } else {
260 $glue = phutil_tag(
261 'span',
262 array(
263 'class' => 'kbd-join',
264 'style' => $join_style,
265 ),
266 '+');
267 $parts = phutil_implode_html($glue, $parts);
268 }
269
270 return $this->getEngine()->storeText($parts);
271 }
272
273}