blob: d159cd2e9cde201883bfbf1b00e200e752d7eadd [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: jmaessen@google.com (Jan Maessen)
// Author: morlovich@google.com (Maksim Orlovich)
#include "net/instaweb/rewriter/public/javascript_code_block.h"
#include <cstddef>
#include "net/instaweb/rewriter/public/javascript_library_identification.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/source_map.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/js/js_minify.h"
namespace pagespeed { namespace js { struct JsTokenizerPatterns; } }
namespace net_instaweb {
// Statistics names
const char JavascriptRewriteConfig::kBlocksMinified[] =
"javascript_blocks_minified";
const char JavascriptRewriteConfig::kLibrariesIdentified[] =
"javascript_libraries_identified";
const char JavascriptRewriteConfig::kMinificationFailures[] =
"javascript_minification_failures";
const char JavascriptRewriteConfig::kTotalBytesSaved[] =
"javascript_total_bytes_saved";
const char JavascriptRewriteConfig::kTotalOriginalBytes[] =
"javascript_total_original_bytes";
const char JavascriptRewriteConfig::kMinifyUses[] = "javascript_minify_uses";
const char JavascriptRewriteConfig::kNumReducingMinifications[] =
"javascript_reducing_minifications";
const char JavascriptRewriteConfig::kJSMinificationDisabled[] =
"javascript_minification_disabled";
const char JavascriptRewriteConfig::kJSDidNotShrink[] =
"javascript_did_not_shrink";
const char JavascriptRewriteConfig::kJSFailedToWrite[] =
"javascript_failed_to_write";
const char JavascriptCodeBlock::kIntrospectionComment[] =
"This script contains introspective JavaScript and is unsafe to replace.";
JavascriptRewriteConfig::JavascriptRewriteConfig(
Statistics* stats, bool minify, bool use_experimental_minifier,
const JavascriptLibraryIdentification* identification,
const pagespeed::js::JsTokenizerPatterns* js_tokenizer_patterns)
: minify_(minify),
use_experimental_minifier_(use_experimental_minifier),
library_identification_(identification),
js_tokenizer_patterns_(js_tokenizer_patterns),
blocks_minified_(stats->GetVariable(kBlocksMinified)),
libraries_identified_(stats->GetVariable(kLibrariesIdentified)),
minification_failures_(stats->GetVariable(kMinificationFailures)),
total_bytes_saved_(stats->GetVariable(kTotalBytesSaved)),
total_original_bytes_(stats->GetVariable(kTotalOriginalBytes)),
num_uses_(stats->GetVariable(kMinifyUses)),
num_reducing_minifications_(
stats->GetVariable(kNumReducingMinifications)),
minification_disabled_(stats->GetVariable(kJSMinificationDisabled)),
did_not_shrink_(stats->GetVariable(kJSDidNotShrink)),
failed_to_write_(stats->GetVariable(kJSFailedToWrite)) {
}
void JavascriptRewriteConfig::InitStats(Statistics* statistics) {
statistics->AddVariable(kBlocksMinified);
statistics->AddVariable(kLibrariesIdentified);
statistics->AddVariable(kMinificationFailures);
statistics->AddVariable(kTotalBytesSaved);
statistics->AddVariable(kTotalOriginalBytes);
statistics->AddVariable(kMinifyUses);
statistics->AddVariable(kNumReducingMinifications);
statistics->AddVariable(kJSMinificationDisabled);
statistics->AddVariable(kJSDidNotShrink);
statistics->AddVariable(kJSFailedToWrite);
}
JavascriptCodeBlock::JavascriptCodeBlock(
const StringPiece& original_code, JavascriptRewriteConfig* config,
const StringPiece& message_id, MessageHandler* handler)
: config_(config),
message_id_(message_id.data(), message_id.size()),
original_code_(original_code.data(), original_code.size()),
rewritten_(false),
successfully_rewritten_(false),
handler_(handler) {
}
JavascriptCodeBlock::~JavascriptCodeBlock() { }
// Is this URL sanitary to be appended (in a line comment) to the JS doc?
bool JavascriptCodeBlock::IsSanitarySourceMapUrl(StringPiece url) {
for (int i = 0, n = url.size(); i < n; ++i) {
if (!IsNonControlAscii(url[i])) {
// This is a bit broader than necessary. JS line comments can only be
// terminated by Unicode line/paragraph separators (Zl/Zp). Instead of
// searching for all of these, we simply check any non-standard chars.
// Specifically, we reject any URL with control chars (0x00-0x1F,0x7F)
// or any non-ASCII UTF-8 chars (Bytes 0x80-0xFF).
// Because URLs passed in here are .pagespeed. rewritten URLs, we do
// not expect any of them to be of this form anyway, so this case
// shouldn't be hit.
return false;
}
}
return true;
}
void JavascriptCodeBlock::AppendSourceMapUrl(StringPiece url) {
DCHECK(rewritten_);
DCHECK(successfully_rewritten_);
if (!IsSanitarySourceMapUrl(url)) {
LOG(DFATAL) << "Unsanitary source map URL could not be added to JS " << url;
return;
}
StrAppend(&rewritten_code_, "\n//# sourceMappingURL=", url, "\n");
}
StringPiece JavascriptCodeBlock::ComputeJavascriptLibrary() const {
// TODO(jmaessen): when we compute minified version and find
// a match, consider adding the un-minified hash to the library
// identifier, and then using that to speed up identification
// in future (at the cost of a double lookup for a miss). Also
// consider pruning candidate JS that is simply too small to match
// a registered library.
DCHECK(rewritten_);
StringPiece result;
if (rewritten_) {
const JavascriptLibraryIdentification* library_identification =
config_->library_identification();
if (library_identification != NULL) {
result = library_identification->Find(rewritten_code_);
if (!result.empty()) {
config_->libraries_identified()->Add(1);
}
}
}
return result;
}
bool JavascriptCodeBlock::UnsafeToRename(const StringPiece& script) {
// If you're pulling out script elements it's probably because
// you're trying to do a kind of reflection that would break if we
// minified the code and mutated its url.
return script.find("document.getElementsByTagName('script')")
!= StringPiece::npos ||
script.find("document.getElementsByTagName(\"script\")")
!= StringPiece::npos ||
script.find("$('script')") // jquery version
!= StringPiece::npos ||
script.find("$(\"script\")")
!= StringPiece::npos;
}
bool JavascriptCodeBlock::Rewrite() {
DCHECK(!rewritten_);
if (rewritten_) {
return successfully_rewritten_;
}
rewritten_ = true;
successfully_rewritten_ = false;
// We minify for two reasons: because the user wants minified js code (in
// which case output_code_ should point to the minified code when we're
// done), or because we're trying to identify a javascript library.
// Bail if we're not doing one of these things.
if (!config_->minify() && (config_->library_identification() == NULL)) {
return successfully_rewritten_;
}
if (MinifyJs(original_code_, &rewritten_code_, &source_mappings_)) {
// Minification succeeded. The fact that it succeeded doesn't imply that
// it actually saved anything; we increment num_reducing_uses when there
// were actual savings.
config_->blocks_minified()->Add(1);
if (config_->minify() && rewritten_code_.size() < original_code_.size()) {
// Minification will actually be used.
successfully_rewritten_ = true;
config_->num_reducing_uses()->Add(1);
config_->total_original_bytes()->Add(original_code_.size());
// Note: This unsigned arithmetic is guaranteed not to underflow because
// of the if statement above.
config_->total_bytes_saved()->Add(
original_code_.size() - rewritten_code_.size());
}
} else { // Minification failed.
handler_->Message(kInfo, "%s: Javascript minification failed. "
"Preserving old code.", message_id_.c_str());
// Note: Although we set rewritten_code_, we do not consider this a
// successful rewrite and thus will not minify. This is only used for
// canonical library identification.
TrimWhitespace(original_code_, &rewritten_code_);
// Update stats.
config_->minification_failures()->Add(1);
}
return successfully_rewritten_;
}
void JavascriptCodeBlock::SwapRewrittenString(GoogleString* other) {
DCHECK(rewritten_);
DCHECK(successfully_rewritten_);
other->swap(rewritten_code_);
rewritten_code_.clear();
rewritten_ = false;
successfully_rewritten_ = false;
}
bool JavascriptCodeBlock::MinifyJs(
StringPiece input, GoogleString* output,
source_map::MappingVector* source_mappings) {
if (config_->use_experimental_minifier()) {
return pagespeed::js::MinifyUtf8JsWithSourceMap(
config_->js_tokenizer_patterns(), input, output, source_mappings);
} else {
return pagespeed::js::MinifyJs(input, output);
}
}
} // namespace net_instaweb