// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/file_highlighter.h"
#include <stack>
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
namespace extensions {
namespace {
// Keys for a highlighted dictionary.
const char kBeforeHighlightKey[] = "beforeHighlight";
const char kHighlightKey[] = "highlight";
const char kAfterHighlightKey[] = "afterHighlight";
// Increment |index| to the position of the next quote ('"') in |str|, skipping
// over any escaped quotes. If no next quote is found, |index| is set to
// std::string::npos. Assumes |index| currently points to a quote.
void QuoteIncrement(const std::string& str, size_t* index) {
size_t i = *index + 1; // Skip over the first quote.
bool found = false;
while (!found && i < str.size()) {
if (str[i] == '\\')
i += 2; // if we find an escaped character, skip it.
else if (str[i] == '"')
found = true;
else
++i;
}
*index = found ? i : std::string::npos;
}
// Increment |index| by one if the next character is not a comment. Increment
// index until the end of the comment if it is a comment.
void CommentSafeIncrement(const std::string& str, size_t* index) {
size_t i = *index;
if (str[i] == '/' && i + 1 < str.size()) {
// Eat a single-line comment.
if (str[i + 1] == '/') {
i += 2; // Eat the '//'.
while (i < str.size() && str[i] != '\n' && str[i] != '\r')
++i;
} else if (str[i + 1] == '*') { // Eat a multi-line comment.
i += 3; // Advance to the first possible comment end.
while (i < str.size() && !(str[i - 1] == '*' && str[i] == '/'))
++i;
}
}
*index = i + 1;
}
// Increment index until the end of the current "chunk"; a "chunk" is a JSON-
// style list, object, or string literal, without exceeding |end|. Assumes
// |index| currently points to a chunk's starting character ('{', '[', or '"').
void ChunkIncrement(const std::string& str, size_t* index, size_t end) {
char c = str[*index];
std::stack<char> stack;
do {
if (c == '"')
QuoteIncrement(str, index);
else if (c == '[')
stack.push(']');
else if (c == '{')
stack.push('}');
else if (!stack.empty() && c == stack.top())
stack.pop();
CommentSafeIncrement(str, index);
c = str[*index];
} while (!stack.empty() && *index < end);
}
} // namespace
FileHighlighter::FileHighlighter(const std::string& contents)
: contents_(contents), start_(0u), end_(contents_.size()) {
}
FileHighlighter::~FileHighlighter() {
}
std::string FileHighlighter::GetBeforeFeature() const {
return contents_.substr(0, start_);
}
std::string FileHighlighter::GetFeature() const {
return contents_.substr(start_, end_ - start_);
}
std::string FileHighlighter::GetAfterFeature() const {
return contents_.substr(end_);
}
void FileHighlighter::SetHighlightedRegions(base::DictionaryValue* dict) const {
std::string before_feature = GetBeforeFeature();
if (!before_feature.empty())
dict->SetString(kBeforeHighlightKey, base::UTF8ToUTF16(before_feature));
std::string feature = GetFeature();
if (!feature.empty())
dict->SetString(kHighlightKey, base::UTF8ToUTF16(feature));
std::string after_feature = GetAfterFeature();
if (!after_feature.empty())
dict->SetString(kAfterHighlightKey, base::UTF8ToUTF16(after_feature));
}
ManifestHighlighter::ManifestHighlighter(const std::string& manifest,
const std::string& key,
const std::string& specific)
: FileHighlighter(manifest) {
start_ = contents_.find('{') + 1;
end_ = contents_.rfind('}');
Parse(key, specific);
}
ManifestHighlighter::~ManifestHighlighter() {
}
void ManifestHighlighter::Parse(const std::string& key,
const std::string& specific) {
// First, try to find the bounds of the full key.
if (FindBounds(key, true) /* enforce at top level */ ) {
// If we succeed, and we have a specific location, find the bounds of the
// specific.
if (!specific.empty())
FindBounds(specific, false /* don't enforce at top level */ );
// We may have found trailing whitespace. Don't use base::TrimWhitespace,
// because we want to keep any whitespace we find - just not highlight it.
size_t trim = contents_.find_last_not_of(" \t\n\r", end_ - 1);
if (trim < end_ && trim > start_)
end_ = trim + 1;
} else {
// If we fail, then we set start to end so that the highlighted portion is
// empty.
start_ = end_;
}
}
bool ManifestHighlighter::FindBounds(const std::string& feature,
bool enforce_at_top_level) {
char c = '\0';
while (start_ < end_) {
c = contents_[start_];
if (c == '"') {
// The feature may be quoted.
size_t quote_end = start_;
QuoteIncrement(contents_, "e_end);
if (contents_.substr(start_ + 1, quote_end - 1 - start_) == feature) {
FindBoundsEnd(feature, quote_end + 1);
return true;
} else {
// If it's not the feature, then we can skip the quoted section.
start_ = quote_end + 1;
}
} else if (contents_.substr(start_, feature.size()) == feature) {
FindBoundsEnd(feature, start_ + feature.size() + 1);
return true;
} else if (enforce_at_top_level && (c == '{' || c == '[')) {
// If we don't have to be at the top level, then we can skip any chunks
// we find.
ChunkIncrement(contents_, &start_, end_);
} else {
CommentSafeIncrement(contents_, &start_);
}
}
return false;
}
void ManifestHighlighter::FindBoundsEnd(const std::string& feature,
size_t local_start) {
char c = '\0';
while (local_start < end_) {
c = contents_[local_start];
// We're done when we find a terminating character (i.e., either a comma or
// an ending bracket.
if (c == ',' || c == '}' || c == ']') {
end_ = local_start;
return;
}
// We can skip any chunks we find, since we are looking for the end of the
// current feature, and don't want to go any deeper.
if (c == '"' || c == '{' || c == '[')
ChunkIncrement(contents_, &local_start, end_);
else
CommentSafeIncrement(contents_, &local_start);
}
}
SourceHighlighter::SourceHighlighter(const std::string& contents,
size_t line_number)
: FileHighlighter(contents) {
Parse(line_number);
}
SourceHighlighter::~SourceHighlighter() {
}
void SourceHighlighter::Parse(size_t line_number) {
// If line 0 is requested, highlight nothing.
if (line_number == 0) {
start_ = contents_.size();
return;
}
for (size_t i = 1; i < line_number; ++i) {
start_ = contents_.find('\n', start_);
if (start_ == std::string::npos)
break;
start_ += 1;
}
end_ = contents_.find('\n', start_);
// If we went off the end of the string (i.e., the line number was invalid),
// then move start and end to the end of the string, so that the highlighted
// portion is empty.
start_ = start_ == std::string::npos ? contents_.size() : start_;
end_ = end_ == std::string::npos ? contents_.size() : end_;
}
} // namespace extensions