241 lines
9.3 KiB
C++
241 lines
9.3 KiB
C++
/*
|
|
* 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"
|
|
#include "pagespeed/kernel/js/js_tokenizer.h"
|
|
|
|
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
|