@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 DiffusionSourceLinkRemarkupRule
4 extends PhutilRemarkupRule {
5
6 const KEY_SOURCELINKS = 'diffusion.links';
7
8 public function getPriority() {
9 return 200.0;
10 }
11
12 public function apply($text) {
13 return preg_replace_callback(
14 '@{(?:src|source)\b((?:[^}\\\\]+|\\\\.)*)}@m',
15 array($this, 'markupSourceLink'),
16 $text);
17 }
18
19 public function markupSourceLink(array $matches) {
20 $engine = $this->getEngine();
21 $text_mode = $engine->isTextMode();
22 $mail_mode = $engine->isHTMLMailMode();
23
24 if (!$this->isFlatText($matches[0]) || $text_mode || $mail_mode) {
25 // We could do better than this in text mode and mail mode, but focus
26 // on web mode first.
27 return $matches[0];
28 }
29
30 $metadata_key = self::KEY_SOURCELINKS;
31 $metadata = $engine->getTextMetadata($metadata_key, array());
32
33 $token = $engine->storeText($matches[0]);
34
35 $metadata[] = array(
36 'token' => $token,
37 'raw' => $matches[0],
38 'input' => $matches[1],
39 );
40
41 $engine->setTextMetadata($metadata_key, $metadata);
42
43 return $token;
44 }
45
46 public function didMarkupText() {
47 $engine = $this->getEngine();
48 $metadata_key = self::KEY_SOURCELINKS;
49 $metadata = $engine->getTextMetadata($metadata_key, array());
50
51 if (!$metadata) {
52 return;
53 }
54
55 $viewer = $engine->getConfig('viewer');
56 if (!$viewer) {
57 return;
58 }
59
60 $defaults = array(
61 'repository' => null,
62 'line' => null,
63 'commit' => null,
64 'ref' => null,
65 );
66
67 $tags = array();
68 foreach ($metadata as $ref) {
69 $token = $ref['token'];
70 $raw = $ref['raw'];
71 $input = $ref['input'];
72
73 $pattern =
74 '(^'.
75 '[\s,]*'.
76 '(?:"(?P<quotedpath>(?:[^\\\\"]+|\\.)+)"|(?P<rawpath>[^\s,]+))'.
77 '[\s,]*'.
78 '(?P<options>.*)'.
79 '\z)';
80 $matches = null;
81 if (!preg_match($pattern, $input, $matches)) {
82 $hint_text = pht(
83 'Missing path, expected "{src path ...}" in: %s',
84 $raw);
85 $hint = $this->newSyntaxHint($hint_text);
86
87 $engine->overwriteStoredText($token, $hint);
88 continue;
89 }
90
91 $path = idx($matches, 'rawpath');
92 if (!strlen($path)) {
93 $path = idx($matches, 'quotedpath');
94 $path = stripcslashes($path);
95 }
96
97 $parts = explode(':', $path, 2);
98 if (count($parts) == 2) {
99 $repository = nonempty($parts[0], null);
100 $path = $parts[1];
101 } else {
102 $repository = null;
103 $path = $parts[0];
104 }
105
106 $options = $matches['options'];
107
108 $parser = new PhutilSimpleOptions();
109 $options = $parser->parse($options) + $defaults;
110
111 foreach ($options as $key => $value) {
112 if (!array_key_exists($key, $defaults)) {
113 $hint_text = pht(
114 'Unknown option "%s" in: %s',
115 $key,
116 $raw);
117 $hint = $this->newSyntaxHint($hint_text);
118
119 $engine->overwriteStoredText($token, $hint);
120 continue 2;
121 }
122 }
123
124 if ($options['repository'] !== null) {
125 $repository = $options['repository'];
126 }
127
128 if ($repository === null) {
129 $hint_text = pht(
130 'Missing repository, expected "{src repository:path ...}" '.
131 'or "{src path repository=...}" in: %s',
132 $raw);
133 $hint = $this->newSyntaxHint($hint_text);
134
135 $engine->overwriteStoredText($token, $hint);
136 continue;
137 }
138
139 $tags[] = array(
140 'token' => $token,
141 'raw' => $raw,
142 'identifier' => $repository,
143 'path' => $path,
144 'options' => $options,
145 );
146 }
147
148 if (!$tags) {
149 return;
150 }
151
152 $query = id(new PhabricatorRepositoryQuery())
153 ->setViewer($viewer)
154 ->withIdentifiers(ipull($tags, 'identifier'));
155
156 $query->execute();
157
158 $repository_map = $query->getIdentifierMap();
159
160 foreach ($tags as $tag) {
161 $token = $tag['token'];
162
163 $identifier = $tag['identifier'];
164 $repository = idx($repository_map, $identifier);
165 if (!$repository) {
166 // For now, just bail out here. Ideally, we should distingiush between
167 // restricted and invalid repositories.
168 continue;
169 }
170
171 $drequest = DiffusionRequest::newFromDictionary(
172 array(
173 'user' => $viewer,
174 'repository' => $repository,
175 ));
176
177 $options = $tag['options'];
178
179 $line = $options['line'];
180 $commit = $options['commit'];
181 $ref_name = $options['ref'];
182
183 $link_uri = $drequest->generateURI(
184 array(
185 'action' => 'browse',
186 'path' => $tag['path'],
187 'commit' => $commit,
188 'line' => $line,
189 'branch' => $ref_name,
190 ));
191
192 $view = id(new DiffusionSourceLinkView())
193 ->setRepository($repository)
194 ->setPath($tag['path'])
195 ->setURI($link_uri);
196
197 if ($line !== null) {
198 $view->setLine($line);
199 }
200
201 if ($commit !== null) {
202 $view->setCommit($commit);
203 }
204
205 if ($ref_name !== null) {
206 $view->setRefName($ref_name);
207 }
208
209 $engine->overwriteStoredText($token, $view);
210 }
211 }
212
213 private function newSyntaxHint($text) {
214 return id(new PHUITagView())
215 ->setType(PHUITagView::TYPE_SHADE)
216 ->setColor('red')
217 ->setIcon('fa-exclamation-triangle')
218 ->setName($text);
219 }
220
221}