1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | <?php namespace DrSlump\Protobuf\Compiler; class CommentsParser { /** @var array - Hold a mapping of entity => comment */ protected $comments = array(); /** @var array - Define tokenizer regular expressions */ protected $tokens = array( 'comment' => '/\*([\S\s]+?)\*/', 'package' => 'package\s+([A-Z0-9_]+)', 'struct' => '(?:message|enum|service)\s+([A-Z0-9_]+)', 'close' => '}', 'field' => '(?:required|optional|repeated)\s+[^=]+=\s*([0-9]+)[^;]*;', 'rpc' => 'rpc\s+([A-Z0-9_]+)[^;]+' ); /** @var string - The regular expresion for the tokenizer */ protected $regexp; public function __construct() { // Generate a regular expression for all tokens $regexp = array(); foreach ($this->tokens as $token=>$exp) { $regexp[] = '(?<' . $token . '>' . $exp . ')'; } $this->regexp = '@' . implode('|', $regexp) . '@i'; } /** * Reset the currently stored comments */ public function reset() { $this->comments = array(); } /** * Parse a Proto file source code to fetch comments * * @param string $src */ public function parse($src) { // Build an stream of tokens from the regular expression $tokens = array(); $offset = 0; while (preg_match($this->regexp, $src, $m, PREG_OFFSET_CAPTURE, $offset)) { foreach ($this->tokens as $k=>$v) { if (!empty($m[$k]) && 0 < strlen($m[$k][0])) { $tokens[] = array( 'token' => $k, 'value' => array_shift(array_pop($m)), ); } } $offset = $m[0][1] + strlen($m[0][0]); } // Parse the tokens stream to assign comments $comment = null; $stack = array(); foreach ($tokens as $token) { if ($token['token'] === 'comment') { $comment = $token['value']; } elseif ($token['token'] === 'package') { $stack[] = $token['value']; $comment = null; } elseif ($token['token'] === 'struct') { $stack[] = $token['value']; if ($comment) { $this->setComment(implode('.', $stack), $comment); $comment = null; } } elseif ($token['token'] === 'close') { array_pop($stack); $comment = null; } elseif ($token['token'] === 'field' || $token['token'] === 'rpc') { if ($comment) { $this->setComment(implode('.', $stack) . '.' . $token['value'], $comment); $comment = null; } } } } /** * Set a comment for the given identifier. The identifier is composed * of the package, followed by the message (and nested messages). Field * comments are suffixed with the tag number. * * @example * * $this->setComment('MyPackage.MyMessage.Nested.2', 'field comment'); * * @param string $ident * @param string $comment */ public function setComment($ident, $comment) { $comment = str_replace("\r\n", "\n", $comment); $comment = preg_replace('/^[\s\*]+/m', '', $comment); $comment = trim($comment, "* \n"); $this->comments[$ident] = $comment; } /** * Get the comment for a given identifier * * @param string $ident * @return string|null */ public function getComment($ident) { return isset($this->comments[$ident]) ? $this->comments[$ident] : null; } /** * Checks if a comment exists for a given identifier * * @param string $ident * @return bool */ public function hasComment($ident) { return isset($this->comments[$ident]); } } |