[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 1 | // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 5 | #include "extensions/browser/file_highlighter.h" |
| 6 | |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 7 | #include <stack> |
| 8 | |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 9 | #include "base/strings/utf_string_conversions.h" |
| 10 | #include "base/values.h" |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 11 | |
| 12 | namespace extensions { |
| 13 | |
| 14 | namespace { |
| 15 | |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 16 | // Keys for a highlighted dictionary. |
| 17 | const char kBeforeHighlightKey[] = "beforeHighlight"; |
| 18 | const char kHighlightKey[] = "highlight"; |
| 19 | const char kAfterHighlightKey[] = "afterHighlight"; |
| 20 | |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 21 | // Increment |index| to the position of the next quote ('"') in |str|, skipping |
| 22 | // over any escaped quotes. If no next quote is found, |index| is set to |
| 23 | // std::string::npos. Assumes |index| currently points to a quote. |
| 24 | void QuoteIncrement(const std::string& str, size_t* index) { |
| 25 | size_t i = *index + 1; // Skip over the first quote. |
| 26 | bool found = false; |
| 27 | while (!found && i < str.size()) { |
| 28 | if (str[i] == '\\') |
| 29 | i += 2; // if we find an escaped character, skip it. |
| 30 | else if (str[i] == '"') |
| 31 | found = true; |
| 32 | else |
| 33 | ++i; |
| 34 | } |
| 35 | *index = found ? i : std::string::npos; |
| 36 | } |
| 37 | |
| 38 | // Increment |index| by one if the next character is not a comment. Increment |
| 39 | // index until the end of the comment if it is a comment. |
| 40 | void CommentSafeIncrement(const std::string& str, size_t* index) { |
| 41 | size_t i = *index; |
| 42 | if (str[i] == '/' && i + 1 < str.size()) { |
| 43 | // Eat a single-line comment. |
| 44 | if (str[i + 1] == '/') { |
| 45 | i += 2; // Eat the '//'. |
| 46 | while (i < str.size() && str[i] != '\n' && str[i] != '\r') |
| 47 | ++i; |
| 48 | } else if (str[i + 1] == '*') { // Eat a multi-line comment. |
| 49 | i += 3; // Advance to the first possible comment end. |
| 50 | while (i < str.size() && !(str[i - 1] == '*' && str[i] == '/')) |
| 51 | ++i; |
| 52 | } |
| 53 | } |
| 54 | *index = i + 1; |
| 55 | } |
| 56 | |
| 57 | // Increment index until the end of the current "chunk"; a "chunk" is a JSON- |
| 58 | // style list, object, or string literal, without exceeding |end|. Assumes |
| 59 | // |index| currently points to a chunk's starting character ('{', '[', or '"'). |
| 60 | void ChunkIncrement(const std::string& str, size_t* index, size_t end) { |
| 61 | char c = str[*index]; |
| 62 | std::stack<char> stack; |
| 63 | do { |
| 64 | if (c == '"') |
| 65 | QuoteIncrement(str, index); |
| 66 | else if (c == '[') |
| 67 | stack.push(']'); |
| 68 | else if (c == '{') |
| 69 | stack.push('}'); |
| 70 | else if (!stack.empty() && c == stack.top()) |
| 71 | stack.pop(); |
| 72 | CommentSafeIncrement(str, index); |
| 73 | c = str[*index]; |
| 74 | } while (!stack.empty() && *index < end); |
| 75 | } |
| 76 | |
| 77 | } // namespace |
| 78 | |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 79 | FileHighlighter::FileHighlighter(const std::string& contents) |
| 80 | : contents_(contents), start_(0u), end_(contents_.size()) { |
| 81 | } |
| 82 | |
| 83 | FileHighlighter::~FileHighlighter() { |
| 84 | } |
| 85 | |
| 86 | std::string FileHighlighter::GetBeforeFeature() const { |
| 87 | return contents_.substr(0, start_); |
| 88 | } |
| 89 | |
| 90 | std::string FileHighlighter::GetFeature() const { |
| 91 | return contents_.substr(start_, end_ - start_); |
| 92 | } |
| 93 | |
| 94 | std::string FileHighlighter::GetAfterFeature() const { |
| 95 | return contents_.substr(end_); |
| 96 | } |
| 97 | |
| 98 | void FileHighlighter::SetHighlightedRegions(base::DictionaryValue* dict) const { |
| 99 | std::string before_feature = GetBeforeFeature(); |
| 100 | if (!before_feature.empty()) |
| 101 | dict->SetString(kBeforeHighlightKey, base::UTF8ToUTF16(before_feature)); |
| 102 | |
| 103 | std::string feature = GetFeature(); |
| 104 | if (!feature.empty()) |
| 105 | dict->SetString(kHighlightKey, base::UTF8ToUTF16(feature)); |
| 106 | |
| 107 | std::string after_feature = GetAfterFeature(); |
| 108 | if (!after_feature.empty()) |
| 109 | dict->SetString(kAfterHighlightKey, base::UTF8ToUTF16(after_feature)); |
| 110 | } |
| 111 | |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 112 | ManifestHighlighter::ManifestHighlighter(const std::string& manifest, |
| 113 | const std::string& key, |
| 114 | const std::string& specific) |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 115 | : FileHighlighter(manifest) { |
| 116 | start_ = contents_.find('{') + 1; |
| 117 | end_ = contents_.rfind('}'); |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 118 | Parse(key, specific); |
| 119 | } |
| 120 | |
| 121 | ManifestHighlighter::~ManifestHighlighter() { |
| 122 | } |
| 123 | |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 124 | |
| 125 | void ManifestHighlighter::Parse(const std::string& key, |
| 126 | const std::string& specific) { |
| 127 | // First, try to find the bounds of the full key. |
| 128 | if (FindBounds(key, true) /* enforce at top level */ ) { |
| 129 | // If we succeed, and we have a specific location, find the bounds of the |
| 130 | // specific. |
| 131 | if (!specific.empty()) |
| 132 | FindBounds(specific, false /* don't enforce at top level */ ); |
| 133 | |
| 134 | // We may have found trailing whitespace. Don't use base::TrimWhitespace, |
| 135 | // because we want to keep any whitespace we find - just not highlight it. |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 136 | size_t trim = contents_.find_last_not_of(" \t\n\r", end_ - 1); |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 137 | if (trim < end_ && trim > start_) |
| 138 | end_ = trim + 1; |
| 139 | } else { |
| 140 | // If we fail, then we set start to end so that the highlighted portion is |
| 141 | // empty. |
| 142 | start_ = end_; |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | bool ManifestHighlighter::FindBounds(const std::string& feature, |
| 147 | bool enforce_at_top_level) { |
| 148 | char c = '\0'; |
| 149 | while (start_ < end_) { |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 150 | c = contents_[start_]; |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 151 | if (c == '"') { |
| 152 | // The feature may be quoted. |
| 153 | size_t quote_end = start_; |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 154 | QuoteIncrement(contents_, "e_end); |
| 155 | if (contents_.substr(start_ + 1, quote_end - 1 - start_) == feature) { |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 156 | FindBoundsEnd(feature, quote_end + 1); |
| 157 | return true; |
| 158 | } else { |
| 159 | // If it's not the feature, then we can skip the quoted section. |
| 160 | start_ = quote_end + 1; |
| 161 | } |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 162 | } else if (contents_.substr(start_, feature.size()) == feature) { |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 163 | FindBoundsEnd(feature, start_ + feature.size() + 1); |
| 164 | return true; |
| 165 | } else if (enforce_at_top_level && (c == '{' || c == '[')) { |
| 166 | // If we don't have to be at the top level, then we can skip any chunks |
| 167 | // we find. |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 168 | ChunkIncrement(contents_, &start_, end_); |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 169 | } else { |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 170 | CommentSafeIncrement(contents_, &start_); |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 171 | } |
| 172 | } |
| 173 | return false; |
| 174 | } |
| 175 | |
| 176 | void ManifestHighlighter::FindBoundsEnd(const std::string& feature, |
| 177 | size_t local_start) { |
| 178 | char c = '\0'; |
| 179 | while (local_start < end_) { |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 180 | c = contents_[local_start]; |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 181 | // We're done when we find a terminating character (i.e., either a comma or |
| 182 | // an ending bracket. |
| 183 | if (c == ',' || c == '}' || c == ']') { |
| 184 | end_ = local_start; |
| 185 | return; |
| 186 | } |
| 187 | // We can skip any chunks we find, since we are looking for the end of the |
| 188 | // current feature, and don't want to go any deeper. |
| 189 | if (c == '"' || c == '{' || c == '[') |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 190 | ChunkIncrement(contents_, &local_start, end_); |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 191 | else |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 192 | CommentSafeIncrement(contents_, &local_start); |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 193 | } |
| 194 | } |
| 195 | |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 196 | SourceHighlighter::SourceHighlighter(const std::string& contents, |
| 197 | size_t line_number) |
| 198 | : FileHighlighter(contents) { |
| 199 | Parse(line_number); |
| 200 | } |
| 201 | |
| 202 | SourceHighlighter::~SourceHighlighter() { |
| 203 | } |
| 204 | |
| 205 | void SourceHighlighter::Parse(size_t line_number) { |
[email protected] | 5516496 | 2013-09-10 20:33:55 | [diff] [blame] | 206 | // If line 0 is requested, highlight nothing. |
| 207 | if (line_number == 0) { |
| 208 | start_ = contents_.size(); |
| 209 | return; |
| 210 | } |
| 211 | |
| 212 | for (size_t i = 1; i < line_number; ++i) { |
| 213 | start_ = contents_.find('\n', start_); |
| 214 | if (start_ == std::string::npos) |
| 215 | break; |
| 216 | start_ += 1; |
| 217 | } |
| 218 | |
[email protected] | 2fb9bd2 | 2013-09-07 00:08:08 | [diff] [blame] | 219 | end_ = contents_.find('\n', start_); |
| 220 | |
| 221 | // If we went off the end of the string (i.e., the line number was invalid), |
| 222 | // then move start and end to the end of the string, so that the highlighted |
| 223 | // portion is empty. |
| 224 | start_ = start_ == std::string::npos ? contents_.size() : start_; |
| 225 | end_ = end_ == std::string::npos ? contents_.size() : end_; |
| 226 | } |
| 227 | |
[email protected] | fa5fed3 | 2013-09-05 21:56:22 | [diff] [blame] | 228 | } // namespace extensions |