#include "ui/base/template_expressions.h"
#include <stddef.h>
#include <optional>
#include <ostream>
#include <string_view>
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/values.h"
#if DCHECK_IS_ON()
#include "third_party/re2/src/re2/re2.h"
#endif
namespace {
const char kLeader[] = "$i18n";
const size_t kLeaderSize = std::size(kLeader) - 1;
const char kKeyOpen = '{';
const char kKeyClose = '}';
const char kHtmlTemplateEnd[] = "<!--_html_template_end_-->";
const char kHtmlTemplateStart[] = "<!--_html_template_start_-->";
const size_t kHtmlTemplateStartSize = std::size(kHtmlTemplateStart) - 1;
enum HtmlTemplateType { INVALID = 0, NONE = 1, VALID = 2 };
struct HtmlTemplate {
std::string_view::size_type start;
std::string_view::size_type length;
HtmlTemplateType type;
};
HtmlTemplate FindHtmlTemplate(std::string_view source) {
HtmlTemplate out;
std::string_view::size_type found = source.find(kHtmlTemplateStart);
if (found == std::string_view::npos) {
out.type = NONE;
return out;
}
out.start = found + kHtmlTemplateStartSize;
std::string_view::size_type found_end =
source.find(kHtmlTemplateEnd, out.start);
if (found_end == std::string_view::npos) {
out.type = INVALID;
return out;
}
out.length = found_end - out.start;
if (source.substr(out.start, out.length).find(kHtmlTemplateStart) !=
std::string_view::npos) {
out.type = INVALID;
return out;
}
out.type = VALID;
return out;
}
std::string PolymerParameterEscape(const std::string& in_string,
bool is_javascript) {
std::string out;
out.reserve(in_string.size() * 2);
for (const char c : in_string) {
switch (c) {
case '\\':
out.append(is_javascript ? R"(\\\\)" : R"(\\)");
break;
case '\'':
out.append(is_javascript ? R"(\\')" : R"(\')");
break;
case '"':
out.append(""");
break;
case ',':
out.append(is_javascript ? R"(\\,)" : R"(\,)");
break;
default:
out += c;
}
}
return out;
}
bool EscapeForJS(const std::string& in_string,
std::optional<char> in_previous,
std::string* out_string) {
out_string->reserve(in_string.size() * 2);
bool last_was_dollar = in_previous && in_previous.value() == '$';
for (const char c : in_string) {
switch (c) {
case '`':
out_string->append("\\`");
break;
case '{':
if (last_was_dollar)
return false;
*out_string += c;
break;
default:
*out_string += c;
}
last_was_dollar = c == '$';
}
return true;
}
#if DCHECK_IS_ON()
bool HasUnexpectedPlaceholder(const std::string& key,
const std::string& replacement) {
#if BUILDFLAG(IS_CHROMEOS)
if (key == "displayResolutionText")
return false;
#endif
static const base::NoDestructor<re2::RE2> placeholder_regex(R"(\$\d)");
return re2::RE2::PartialMatch(replacement, *placeholder_regex.get());
}
#endif
bool ReplaceTemplateExpressionsInternal(
std::string_view source,
const ui::TemplateReplacements& replacements,
bool is_javascript,
std::string* formatted,
bool skip_unexpected_placeholder_check = false) {
const size_t kValueLengthGuess = 16;
formatted->reserve(source.length() + replacements.size() * kValueLengthGuess);
size_t current_pos = 0;
while (true) {
size_t next_pos = source.find(kLeader, current_pos);
if (next_pos == std::string::npos) {
formatted->append(UNSAFE_TODO(source.data() + current_pos),
source.size() - current_pos);
break;
}
formatted->append(UNSAFE_TODO(source.data() + current_pos),
next_pos - current_pos);
current_pos = next_pos + kLeaderSize;
size_t context_end = source.find(kKeyOpen, current_pos);
CHECK_NE(context_end, std::string::npos);
std::string context(source.substr(current_pos, context_end - current_pos));
current_pos = context_end + sizeof(kKeyOpen);
size_t key_end = source.find(kKeyClose, current_pos);
CHECK_NE(key_end, std::string::npos);
std::string key(source.substr(current_pos, key_end - current_pos));
CHECK(!key.empty());
auto value = replacements.find(key);
CHECK(value != replacements.end()) << "$i18n replacement key \"" << key
<< "\" not found";
std::string replacement = value->second;
if (is_javascript) {
std::optional<char> last = formatted->empty()
? std::nullopt
: std::make_optional(formatted->back());
std::string escaped_replacement;
if (!EscapeForJS(replacement, last, &escaped_replacement))
return false;
replacement = escaped_replacement;
}
if (context.empty()) {
replacement = base::EscapeForHTML(replacement);
} else if (context == "Raw") {
} else if (context == "Polymer") {
replacement = PolymerParameterEscape(replacement, is_javascript);
} else {
NOTREACHED() << "Unknown context " << context;
}
#if DCHECK_IS_ON()
if (!skip_unexpected_placeholder_check && context != "Polymer") {
DCHECK(!HasUnexpectedPlaceholder(key, replacement))
<< "Dangling placeholder found in " << key;
}
#endif
formatted->append(replacement);
current_pos = key_end + sizeof(kKeyClose);
}
return true;
}
}
namespace ui {
void TemplateReplacementsFromDictionaryValue(
const base::Value::Dict& dictionary,
TemplateReplacements* replacements) {
for (auto pair : dictionary) {
const std::string* value = pair.second.GetIfString();
if (value)
(*replacements)[pair.first] = pair.second.GetString();
}
}
bool ReplaceTemplateExpressionsInJS(std::string_view source,
const TemplateReplacements& replacements,
std::string* formatted) {
CHECK(formatted->empty());
std::string_view remaining = source;
while (true) {
HtmlTemplate current_template = FindHtmlTemplate(remaining);
if (current_template.type == INVALID)
return false;
if (current_template.type == NONE) {
formatted->append(std::string(remaining));
return true;
}
formatted->append(std::string(remaining.substr(0, current_template.start)));
std::string_view html_template =
remaining.substr(current_template.start, current_template.length);
std::string formatted_html;
if (!ReplaceTemplateExpressionsInternal(html_template, replacements, true,
&formatted_html)) {
return false;
}
formatted->append(formatted_html);
remaining =
remaining.substr(current_template.start + current_template.length);
}
}
std::string ReplaceTemplateExpressions(std::string_view source,
const TemplateReplacements& replacements,
bool skip_unexpected_placeholder_check) {
std::string formatted;
ReplaceTemplateExpressionsInternal(source, replacements, false, &formatted,
skip_unexpected_placeholder_check);
return formatted;
}
}