source: Dev/trunk/src/client/util/docscripts/lib/parser/DojoFunctionBody.php

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 23.0 KB
Line 
1<?php
2
3require_once('DojoBlock.php');
4
5class DojoFunctionBody extends DojoBlock
6{
7  private $object = 'DojoFunctionBody';
8
9  private $comment_end;
10
11  public static $prefix = '';
12  public static $suffix = '';
13
14  private $keys = array();
15  private $key_sets = array();
16  private $comments = array();
17  private $return_comments = array();
18  private $instance_variables = array();
19  private $resolved_parameters = array();
20  private $externalized = array();
21  private $externalized_objects = array();
22  private $externalized_avariables = array();
23  private $externalized_ivariables = array();
24  private $externalized_variables = array(); 
25  private $externalized_mixins = array();
26  private $this_inheritance_calls = array();
27  private $extra_initial_comment_block = array();
28
29  public function destroy() {
30    if (!$this->destroyed) {
31      $this->destroyed = true;
32      array_walk($this->externalized, 'destroy_all');
33      unset($this->externalized);
34      array_walk($this->externalized_objects, 'destroy_all');
35      unset($this->externalized_objects);
36      array_walk($this->externalized_mixins, 'destroy_all');
37      unset($this->externalized_mixins);
38    }
39  }
40
41  public function build() {
42    if (!$this->start) {
43      die("DojoFunctionBody->build() used before setting a start position");
44    }
45    if ($this->end) {
46      return $this->end;
47    }
48
49    $balance = 0;
50    $start_position = $this->start[1];
51    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], false, false, true);
52        return $this->end = Text::findTermination($lines, '}', '{}');
53  }
54
55  public function addBlockCommentLine($line) {
56    $this->extra_initial_comment_block[] = $line;
57  }
58
59  public function addBlockCommentBreak() {
60    $this->extra_initial_comment_block[] = -1;
61  }
62
63  public function addBlockCommentKey($key) {
64    $this->comments = array();
65    if ($key) {
66      $this->keys[] = $key;
67    }
68  }
69
70  /**
71   * This key can occur multiple times. eg: example
72   */
73  public function addBlockCommentKeySet($key) {
74    $this->comments = array();
75    if ($key) {
76      $this->key_sets[] = $key;
77    }
78  }
79
80  public function getSource() {
81    $this->getBlockCommentKeys();
82    $source = array();
83    $lines = Text::chop($this->package->getSource(), $this->comment_end[0], $this->comment_end[1], $this->end[0], $this->end[1]);
84    if ($this->start[0] == $this->comment_end[0] && $this->start[1] == $this->comment_end[1]) {
85      $lines[$this->start[0]] = substr($lines[$this->start[0]], $this->start[1] + 1);
86    }
87    $lines[$this->end[0]] = substr($lines[$this->end[0]], 0, $this->end[1]);
88    foreach ($lines as $line_number => $line) {
89      $trimmed_line = trim($line);
90      if ($trimmed_line === '') {
91        $source[] = '';
92      }
93      $source[] = $line;
94    }
95    while (!empty($source)) {
96      if (trim($source[0]) === '') {
97        array_shift($source);
98        continue;
99      }
100      break;
101    }
102    while (!empty($source)) {
103      if (trim($source[count($source) - 1]) === '') {
104        array_pop($source);
105        continue;
106      }
107      break;
108    }
109    return implode("\n", $source);
110  }
111
112  private function cleanBlock($text){
113    $lines = explode("\n", trim($text));
114    $output = array();
115    $indented = false;
116    $blank = false;
117    foreach ($lines as $i => $line) {
118      if ($line{0} == "|") {
119        if(!$indented){
120          $indented = true;
121          if (!$blank) {
122            $output[] = "";
123            if (!$i) {
124              $output[] = "";
125            }
126          }
127        }
128        $output[] = substr($line, 1);
129      }
130      else {
131        if($indented){
132          $indented = false;
133          if (empty($line)) {
134            if (!$blank) {
135              $output[] = "";
136            }
137          }
138        }
139        if (empty($line)) {
140          $blank = true;
141        }
142        $output[] = $line;
143      }
144    }
145    return implode("\n", $output);
146  }
147
148  public function getBlockComment($key) {
149    $this->getBlockCommentKeys();
150    $value = $this->comments[$key];
151    if (!empty($value)) {
152      if (is_array($value)) {
153        for ($i = 0; $i < count($value); $i++){
154          $value[$i] = $this->cleanBlock($value[$i]);
155        }
156      }
157      else {
158        $value = $this->cleanBlock($value);
159      }
160    }
161    return $value;
162  }
163
164  public function getBlockCommentKeys() {
165    if ($this->comments) {
166      return array_keys($this->comments);
167    }
168
169    $this->build();
170
171    $prefix = '\b';
172    if (self::$prefix) {
173      $prefix = preg_quote(self::$prefix, '%');
174    }
175    $suffix = '\b';
176    if (self::$suffix) {
177      $suffix = preg_quote(self::$suffix, '%');
178    }
179    $expression = '%^' . $prefix . '(' . implode('|', array_merge($this->keys, $this->key_sets)) . ')' . $suffix . '\W*%';
180
181    $lines = Text::chop($this->package->getSource(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
182    for ($i = 0; $i < 2; $i++) {
183      $started = false;
184      $between_blocks = true;
185      $wait_for_break = false;
186      $buffer = array();
187      $key = '';
188      if ($i == 1) {
189        $lines = $this->extra_initial_comment_block;
190      }
191      foreach ($lines as $line_number =>  $line) {
192        if ($line === -1) {
193          // Comes from manually added lines (block breaks, e.g. between object keys)
194          $between_blocks = true;
195          continue;
196        }
197
198        list($comment, , , $data, $multiline) = Text::findComments($line, $multiline);
199
200        if ($between_blocks) {
201          if ($comment) {
202            if (preg_match($expression, $comment)) {
203              $between_blocks = false;
204            }
205            elseif (!$i) {
206              continue 2;
207            }
208            else {
209              $wait_for_break = true;
210              continue;
211            }
212          }
213        }
214        elseif ($comment === false) {
215          $between_blocks = true;
216          continue;
217        }
218
219        if (preg_match($expression, $comment, $match)) {
220          if ($buffer && $key) {
221            if (in_array($key, $this->key_sets)) {
222              $this->comments[$key][] = implode("\n", $buffer);
223            }
224            else {
225              $this->comments[$key] = implode("\n", $buffer);
226            }
227            $buffer = array();
228          }
229          $key = $match[1];
230          if ($match[0] == $comment) {
231            $comment = '';
232          }else{
233            $comment = substr($comment, strlen($match[0]));
234          }
235        }
236
237        if ($data) {
238          $this->comment_end = array($line_number, 0);
239          break;
240        }
241
242        $buffer[] = $comment;
243      }
244
245      if ($buffer && $key) {
246        if (in_array($key, $this->key_sets)) {
247          $this->comments[$key][] = implode("\n", $buffer);
248        }
249        else {
250          $this->comments[$key] = implode("\n", $buffer);
251        }
252      }
253
254      if ($i == 0 && !$this->comment_end) {
255        $this->comment_end = $this->start;
256      }
257    }
258
259    return array_keys($this->comments);
260  }
261
262  public function addResolvedParameter($parameter, $value) {
263    $this->resolved_parameters[$parameter] = $value;
264  }
265
266  public function getLocalVariableNames() {
267    $internals = array();
268
269    $this->build();
270    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
271    // Find simple external variable assignment.
272    $matches = preg_grep('%(?:var|this)%', $lines);
273    foreach ($matches as $line_number => $line) {
274      // Check for var groups, or var name =
275      if (preg_match('%var\s+[a-zA-Z0-9_.$]+([^;]+,\s*[a-zA-Z0-9_.$]+)*%', $line, $match)) {
276        preg_match_all('%(?:var\s+([a-zA-Z_.$][\w.$]*)|,\s*([a-zA-Z_.$][\w.$]*))%', $match[0], $named_matches, PREG_SET_ORDER);
277        foreach ($named_matches as $named_match) {
278          if (!empty($named_match[1])) {
279            $internals[$named_match[1]] = false;
280          }
281          if (!empty($named_match[2])) {
282            $internals[$named_match[2]] = false;
283          }
284        }
285      }
286      if (preg_match('%var\s+([a-zA-Z_.$][\w.$]*)\s*=\s*([a-zA-Z_.$][a-zA-Z0-9_.$]*)\s*[;\n]%', $line, $match)) {
287        if (in_array($match[2], array('null', 'true', 'false', 'this'))) continue;
288        $internals[$match[1]] = $match[2];
289      }
290      if (preg_match('%(this\.[a-zA-Z_.$][\w.$]*)\s*=\s*([a-zA-Z_.$][\w.$]*)%', $line, $match)) {
291        $internals[$match[2]] = $match[1];
292      }
293    }
294
295    foreach ($this->resolved_parameters as $parameter => $value) {
296      $internals[$parameter] = $value;
297    }
298
299    return $internals;
300  }
301
302  /**
303   * If these occur inside this function AND reference a local variable, remove them
304   */
305  public function removeSwallowedMixins(&$possible_mixins) {
306    // If any of the mixins happened inside of an executed function, we need to see if
307    // they were used on external variables.
308    if ($this->externalized_mixins) {
309      return $this->externalized_mixins;
310    }
311
312    $this->build();
313    $internals = $this->getLocalVariableNames();
314
315    foreach ($possible_mixins as $i => $mixin) {
316      if (($this->start[0] < $mixin->start[0] || ($this->start[0] == $mixin->start[0] && $this->start[1] < $mixin->start[1])) &&
317          ($this->end[0] > $mixin->end[0] || ($this->end[0] == $mixin->end[0] && $this->end[1] > $mixin->end[1]))) {
318        $parameter = $mixin->getParameter(0);
319        if ($mixin->getName() == 'dojo.extend' && $parameter->isA(DojoFunctionDeclare)) {
320          $code = $this->package->getCode();
321          $line = substr($code[$parameter->start[0]], 0, $parameter->start[1]);
322          $line = substr($line, 0, strrpos($line, $mixin->getName()));
323          preg_match_all('%(?:([a-zA-Z0-9_.$\s]+)\s*=\s*)+%', $line, $matches);
324          foreach ($matches[1] as $match) {
325            $match = trim($match);
326            if (!preg_match('%^var\s+%', $match)) {
327              $found = true;
328              while ($found) {
329                $found = false;
330                foreach ($internals as $internal_name => $external_name) {
331                  if ($internal_name == 'this') continue;
332                  if (strpos($match, $internal_name . '.') === 0) {
333                    $last = $match;
334                    $match = $external_name . substr($match, strlen($internal_name));
335                    if ($last != $match) {
336                      $found = true;
337                    }
338                  }
339                }
340              }
341              $parameter->getFunction()->setFunctionName($match);
342            }
343          }
344          $this->externalized_mixins[] = $mixin;
345        }
346        elseif ($parameter->isA(DojoVariable)) {
347          $object = $parameter->getVariable();
348          if ($object == "this") {
349            unset($possible_mixins[$i]);
350          }
351          elseif (($mixin->start[0] > $this->start[0] || ($mixin->start[0] == $this->start[0] && $mixin->start[1] > $this->start[1]))
352              && ($mixin->end[0] < $this->end[0] || ($mixin->end[0] == $this->end[0] && $mixin->end[1] < $this->end[1]))) {
353            if (array_key_exists($object, $internals)) {
354                unset($possible_mixins[$i]);
355            }
356            else {
357              foreach ($internals as $internal_name => $external_name) {
358                if (strpos($object, $internal_name . '.') === 0) {
359                  $object = $external_name . substr($object, strlen($internal_name));
360                }
361              }
362
363              $parameter->setVariable($object);
364            }
365          }
366          $this->externalized_mixins[] = $mixin;
367        }
368        else {
369          $this->externalized_mixins[] = $mixin;
370          array_splice($possible_mixins, $i--, 1);
371        }
372      }
373    }
374  }
375
376  public function getExternalizedObjects($function_name=false, $parameter_names=array()){
377    if ($this->externalized_objects) {
378      return $this->externalized_objects;
379    }
380
381    $this->build();
382    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
383    foreach ($this->externalized_mixins as $mixin) {
384      $lines = Text::blankOutAtPositions($lines, $mixin->start[0], $mixin->start[1], $mixin->end[0], $mixin->end[1]);
385    }
386    $internals = $this->getLocalVariableNames();
387
388    $matches = preg_grep('%=\s*\{%', $lines);
389    foreach ($matches as $line_number => $line) {
390      if (preg_match('%(\b[a-zA-Z_.$][\w.$]*(?:\.[a-zA-Z_.$][\w.$]|\["[^"]+"\])*)\s*=\s*{%', $line, $match)) {
391        if (array_key_exists($match[1], $internals)) continue;
392
393        $externalized_object = new DojoObject($this->package, $line_number, strpos($line, '{', strpos($line, $match[0])));
394        $end = $externalized_object->build();
395
396        $name = $match[1];
397        if (strpos($name, 'this.') === 0) continue;
398
399        foreach ($internals as $internal_name => $external_name) {
400          if (strpos($name, $internal_name . '.') === 0) {
401            if (!$external_name) continue 2;
402            $name = $external_name . substr($name, strlen($internal_name));
403          }
404        }
405
406        if (strpos($name, '[') !== false) {
407          $source_lines = Text::chop($this->package->getSource(), $line_number, 0);
408          $source_line = trim($source_lines[$line_number]);
409          preg_match('%\b([a-zA-Z_.$][\w.$]*(?:\.[a-zA-Z_.$][\w.$]|\["[^"]+"\])*)\s*=\s*function%', $source_line, $source_match);
410          $name = preg_replace('%\["([^"]+)"\]%', '.$1', $source_match[1]);
411        }
412
413        if (strpos($name, 'this.') === 0) {
414          if (!$function_name) continue;
415
416          $name = $function_name . substr($name, 4);
417        }
418
419        if (in_array($name, $parameter_names)) {
420          continue;
421        }
422
423        $externalized_object->setName($name);
424        $this->externalized_objects[] = $externalized_object;
425      }
426    }
427
428    return $this->externalized_objects;
429  }
430
431  public function getExternalizedFunctionDeclarations($function_name=false) {
432    if ($this->externalized) {
433      return $this->externalized;
434    }
435
436    $this->build();
437    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
438    $internals = $this->getLocalVariableNames();
439
440    $matches = preg_grep('%function\s*\(%', $lines);
441    $last_line = 0;
442    foreach ($matches as $line_number => $line) {
443      if ($line_number < $last_line) continue;
444      if (preg_match('%(var)?\s*(\b[a-zA-Z_.$][\w.$]*(?:\.[a-zA-Z_.$][\w.$]*|\["[^"]+"\])*)\s*=\s*function\s*\(%', $line, $match)) {
445        if ($match[1] || array_key_exists($match[2], $internals)) continue;
446
447        $externalized = new DojoFunctionDeclare($this->package, $line_number, strpos($line, $match[0]));
448        $end = $externalized->build();
449        $last_line = $end[0];
450
451        $externalized->rebuildAliases($internals);
452        $externalized->setExecutedFunction($this);
453
454        $name = $match[2];
455        if (strpos($name, 'this.') === 0) continue;
456
457        foreach ($internals as $internal_name => $external_name) {
458          if (strpos($name, $internal_name . '.') === 0) {
459            if (!$external_name) continue 2;
460            $name = $external_name . substr($name, strlen($internal_name));
461          }
462        }
463
464        if (strpos($name, '[') !== false) {
465          $source_lines = Text::chop($this->package->getSource(), $line_number, 0);
466          $source_line = trim($source_lines[$line_number]);
467          preg_match('%\b([a-zA-Z_.$][\w.$]*(?:\.[a-zA-Z_.$][\w.$]|\["[^"]+"\])*)\s*=\s*function\b%', $source_line, $source_match);
468          $name = preg_replace('%\["([^"]+)"\]%', '.$1', $source_match[1]);
469        }
470
471        if (strpos($name, 'this.') === 0) {
472          if (!$function_name) continue;
473
474          $name = $function_name . substr($name, 4);
475        }
476
477        $parts = explode('.', $name);
478        if (count($parts) > 2 && array_pop(array_slice($parts, -2, 1)) == 'prototype') {
479          array_splice($parts, -2, 1);
480          $name = implode('.', $parts);
481          array_pop($parts);
482          $externalized->setPrototype(implode('.', $parts));
483        }
484
485        $externalized->setFunctionName($name);
486        $this->externalized[] = $externalized;
487      }
488      else {
489        $skipped = new DojoFunctionDeclare($this->package, $line_number, strpos($line, 'function'));
490        $end = $skipped->build();
491        $last_line = $end[0];
492      }
493    }
494
495    return $this->externalized;
496  }
497
498  public function getExternalizedAllVariableNames($function_name, $parameter_names=array()) {
499    if ($this->externalized_avariables) {
500      return $this->externalized_avariables;
501    }
502
503    $this->build();
504    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
505    $internals = $this->getLocalVariableNames();
506
507    $declarations = $this->getExternalizedFunctionDeclarations($function_name);
508    foreach ($declarations as $declaration) {
509      $declaration->build();
510      $lines = Text::blankOutAtPositions($lines, $declaration->start[0], $declaration->start[1], $declaration->end[0], $declaration->end[1]);
511    }
512
513    $objects = $this->getExternalizedObjects(false, $parameter_names);
514    foreach ($objects as $object) {
515      $object->build();
516      $lines = Text::blankOutAtPositions($lines, $object->start[0], $object->start[1], $object->end[0], $object->end[1]);
517    }
518
519    $lines = $this->package->removeCodeFrom($lines);
520
521    $variables = array();
522
523    foreach (preg_grep('%function%', $lines) as $line) {
524      if (preg_match('%function\s*\(([^)]+)\)%', $line, $match)) {
525        if (preg_match_all('%[a-zA-Z_.$][\w.$]+%', $match[0], $matches)) {
526          foreach ($matches[0] as $match) {
527            $internals[$match] = true;
528          }
529        }
530      }
531    }
532
533    foreach (preg_grep('%=%', $lines) as $line_number => $line) {
534      if (preg_match('%^\s*var\b%', $line)) continue;
535      if (preg_match('%\b([a-zA-Z_.$][\w.$]*(?:\.[a-zA-Z_.$][\w.$]|\["[^"]+"\])*)\s*=(?!=)\s*(function\s*\()?%', $line, $match)) {
536        if ($match[2] || array_key_exists($match[1], $internals)) continue;
537
538        $name = $match[1];
539
540        if (strpos($name, '[') !== false) {
541          $source_lines = Text::chop($this->package->getSource(), $line_number, 0);
542          $source_line = trim($source_lines[$line_number]);
543          preg_match('%\b([a-zA-Z_.$][\w.$]*(?:\.[a-zA-Z_.$][\w.$]|\["[^"]+"\])*)%', $source_line, $source_match);
544          $name = preg_replace('%\["([^"]+)"\]%', '.$1', $source_match[1]);
545        }
546
547        if (strpos($name, 'this.') === 0) {
548          if ($function_name) {
549            continue;
550          }
551          else {
552            $name = substr($name, 5);
553          }
554        }
555
556        $found = true;
557        while ($found) {
558          $found = false;
559          foreach ($internals as $internal_name => $external_name) {
560            if ($internal_name == 'this') continue;
561            if (strpos($name, $internal_name . '.') === 0) {
562              if (!$external_name) continue 2;
563              $last = $name;
564              $name = $external_name . substr($name, strlen($internal_name));
565              if ($last != $name) {
566                $found = true;
567              }
568            }
569          }
570        }
571
572        $variables[] = $name;
573      }
574    }
575
576    return $this->externalized_avariables = $variables;
577  }
578
579  public function getExternalizedInstanceVariableNames($function_name, $parameter_names=array()) {
580    if ($this->externalized_ivariables) {
581      return $this->externalized_ivariables;
582    }
583
584    $ivariables = array();
585
586    $variables = $this->getExternalizedAllVariableNames($function_name, $parameter_names);
587    foreach ($variables as $variable) {
588      if (strpos($variable, 'this.') === 0) {
589        $ivariables[] = substr($variable, 5);
590      }
591    }
592
593    return $this->externalized_ivariables = $ivariables;
594  }
595
596  public function getExternalizedVariableNames($function_name, $parameter_names=array()) {
597    if ($this->externalized_variables) {
598      return $this->externalized_variables;
599    }
600
601    $evariables = array();
602
603    $variables = $this->getExternalizedAllVariableNames($function_name, $parameter_names);
604    foreach ($variables as $variable) {
605      if (strpos($variable, 'this.') !== 0) {
606        $evariables[] = $variable;
607      }
608    }
609
610    return $this->externalized_variables = $evariables;
611  }
612
613  public function getInstanceVariableNames() {
614    if ($this->instance_variables) {
615      return $this->instance_variables;
616    }
617
618    $this->build();
619    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
620    foreach ($lines as $line) {
621      if (preg_match('%\bthis\.([a-zA-Z0-9._$]+)\s*=\s*(?!function)%', $line, $match)) {
622        $parts = explode('.', $match[1]);
623        if (count($parts) && !in_array(array_pop(array_slice($parts, -1, 1)), array('prototype', 'constructor'))){
624          $this->instance_variables[] = $match[1];
625        }
626      }
627    }
628    return $this->instance_variables;
629  }
630
631  public function getInstanceFunctions($function_name) {
632    $functions = array();
633    $this->build();
634    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
635    foreach ($lines as $line_number => $line) {
636      if (preg_match('%\bthis\.([a-zA-Z0-9._$]+)\s*=\s*function\b%', $line, $match, PREG_OFFSET_CAPTURE)) {
637        $function = new DojoFunctionDeclare($this->package, $line_number, $match[0][1]);
638        $function->setFunctionName($function_name);
639        $end = $function->build();
640        $functions[] = $function;
641      }
642    }
643    return $functions;
644  }
645
646  public function getReturnComments() {
647    if ($this->return_comments) {
648      return $this->return_comments;
649    }
650
651    $buffer = array();
652    $this->getBlockCommentKeys();
653    $lines = Text::chop($this->package->getSource(), $this->comment_end[0], $this->comment_end[1], $this->end[0], $this->end[1], true);
654    foreach ($lines as $line) {
655      if ($multiline) {
656        list($first, $middle, $last, $data, $multiline) = Text::findComments($line, $multiline);
657        if ($first) {
658          $buffer[] = trim($first);
659        }
660        if ($data) {
661          $multiline = false;
662          if ($buffer) {
663            $this->return_comments[] = implode(' ', array_diff($buffer, array('')));
664            $buffer = array();
665          }
666        }
667      }
668      if (strpos($line, 'return') !== false) {
669        if ($data && $buffer) {
670          $this->return_comments[] = implode(' ', array_diff($buffer, array('')));
671          $buffer = array();
672        }
673        list($first, $middle, $last, $data, $multiline) = Text::findComments($line, $multiline);
674        if ($last) {
675          $buffer[] = $last;
676        }
677      }
678    }
679
680    if ($data && $buffer) {
681      $this->return_comments[] = implode(' ', array_diff($buffer, array('')));
682    }
683
684    $this->return_comment = array_unique($this->return_comments);
685
686    return $this->return_comments;
687  }
688
689  public function getThisInheritanceCalls() {
690    if ($this->this_inheritance_calls) {
691      return $this->this_inheritance_calls;
692    }
693
694    $internalized = $this->getLocalVariableNames();
695
696    $this->build();
697    $lines = Text::chop($this->package->getCode(), $this->start[0], $this->start[1], $this->end[0], $this->end[1], true);
698    foreach ($lines as $line) {
699      if (preg_match('%\b([a-zA-Z0-9_.$]+)\.(?:apply|call)\s*\(%', $line, $match) && !array_key_exists($match[1], $internalized)) {
700        $this->this_inheritance_calls[] = $match[1];
701      }
702    }
703    return $this->this_inheritance_calls;
704  }
705}
706
707?>
Note: See TracBrowser for help on using the repository browser.