| /* |
| * Copyright 2011 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: morlovich@google.com (Maksim Orlovich) |
| // |
| // Implementation of JsCombineFilter class which combines multiple external JS |
| // scripts into a single one. JsCombineFilter contains logic to decide when to |
| // combine based on the HTML event stream, while the actual combining and |
| // content-based vetoing is delegated to the JsCombineFilter::JsCombiner helper. |
| // That in turn largely relies on the common logic in its parent classes to |
| // deal with resource management. |
| |
| #include "net/instaweb/rewriter/public/js_combine_filter.h" |
| |
| #include <map> |
| #include <vector> |
| #include <utility> |
| |
| #include "base/logging.h" |
| #include "net/instaweb/rewriter/cached_result.pb.h" |
| #include "net/instaweb/rewriter/public/javascript_code_block.h" |
| #include "net/instaweb/rewriter/public/javascript_filter.h" |
| #include "net/instaweb/rewriter/public/output_resource.h" |
| #include "net/instaweb/rewriter/public/output_resource_kind.h" |
| #include "net/instaweb/rewriter/public/resource.h" |
| #include "net/instaweb/rewriter/public/resource_combiner.h" |
| #include "net/instaweb/rewriter/public/resource_slot.h" |
| #include "net/instaweb/rewriter/public/rewrite_context.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_result.h" |
| #include "net/instaweb/rewriter/public/script_tag_scanner.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "net/instaweb/rewriter/public/url_partnership.h" |
| #include "pagespeed/kernel/base/basictypes.h" |
| #include "pagespeed/kernel/base/function.h" |
| #include "pagespeed/kernel/base/scoped_ptr.h" |
| #include "pagespeed/kernel/base/statistics.h" |
| #include "pagespeed/kernel/base/stl_util.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/writer.h" |
| #include "pagespeed/kernel/html/html_element.h" |
| #include "pagespeed/kernel/html/html_name.h" |
| #include "pagespeed/kernel/html/html_node.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/js/js_keywords.h" |
| #include "pagespeed/kernel/js/js_tokenizer.h" |
| |
| using pagespeed::JsKeywords; |
| |
| namespace net_instaweb { |
| |
| class MessageHandler; |
| |
| const char JsCombineFilter::kJsFileCountReduction[] = "js_file_count_reduction"; |
| |
| // See file comment and ResourceCombiner docs for this class's role. |
| class JsCombineFilter::JsCombiner : public ResourceCombiner { |
| public: |
| JsCombiner(JsCombineFilter* filter, RewriteDriver* driver) |
| : ResourceCombiner(driver, kContentTypeJavascript.file_extension() + 1, |
| filter), |
| filter_(filter), |
| combined_js_size_(0) { |
| Statistics* stats = server_context_->statistics(); |
| js_file_count_reduction_ = stats->GetVariable(kJsFileCountReduction); |
| } |
| |
| virtual ~JsCombiner() { |
| STLDeleteValues(&code_blocks_); |
| } |
| |
| virtual bool ResourceCombinable( |
| Resource* resource, GoogleString* failure_reason, |
| MessageHandler* handler) { |
| // Get the charset for the given resource. |
| StringPiece this_charset = RewriteFilter::GetCharsetForScript( |
| resource, attribute_charset_, rewrite_driver_->containing_charset()); |
| |
| // This resource's charset must match that of the combination so far. |
| // TODO(matterbury): Correctly handle UTF-16 and UTF-32 without the BE/LE |
| // suffixes, which are legal if we can determine endianness some other way. |
| if (num_urls() == 0) { |
| combined_charset_ = this_charset; |
| } else if (!StringCaseEqual(combined_charset_, this_charset)) { |
| *failure_reason = StrCat("Charset mismatch; combination thus far is ", |
| combined_charset_, " file is ", this_charset); |
| return false; |
| } |
| |
| // In strict mode of ES262-5 eval runs in a private variable scope, |
| // (see 10.4.2 step 3 and 10.4.2.1), so our transformation is not safe. |
| if (IsLikelyStrictMode(filter_->server_context()->js_tokenizer_patterns(), |
| resource->ExtractUncompressedContents())) { |
| *failure_reason = "Combining strict mode files unsupported"; |
| return false; |
| } |
| const RewriteOptions* options = rewrite_driver_->options(); |
| if (options->avoid_renaming_introspective_javascript() && |
| JavascriptCodeBlock::UnsafeToRename( |
| resource->ExtractUncompressedContents())) { |
| *failure_reason = "File seems to look for its URL"; |
| return false; |
| } |
| |
| if (options->Enabled( |
| RewriteOptions::kCanonicalizeJavascriptLibraries)) { |
| JavascriptCodeBlock* code_block = BlockForResource(resource); |
| if (!code_block->ComputeJavascriptLibrary().empty()) { |
| // TODO(morlovich): We may be double-counting some stats here. |
| *failure_reason = "Will be handled as standard library"; |
| return false; |
| } |
| } |
| |
| // TODO(morlovich): define a pragma that javascript authors can |
| // include in their source to prevent inclusion in a js combination |
| return true; |
| } |
| |
| virtual bool ContentSizeTooBig() const { |
| int64 combined_js_max_size = |
| rewrite_driver_->options()->max_combined_js_bytes(); |
| |
| if (combined_js_max_size >= 0 && |
| combined_js_size_ > combined_js_max_size) { |
| return true; |
| } |
| return false; |
| } |
| |
| virtual void AccumulateCombinedSize(const ResourcePtr& resource) { |
| combined_js_size_ += resource->UncompressedContentsSize(); |
| } |
| |
| virtual void Clear() { |
| ResourceCombiner::Clear(); |
| STLDeleteValues(&code_blocks_); |
| combined_js_size_ = 0; |
| } |
| |
| // This eventually calls WritePiece(). |
| bool Write(const ResourceVector& in, const OutputResourcePtr& out) { |
| return WriteCombination(in, out, rewrite_driver_->message_handler()); |
| } |
| |
| // Create the output resource for this combination. |
| OutputResourcePtr MakeOutput() { |
| return Combine(rewrite_driver_->message_handler()); |
| } |
| |
| // Stats. |
| void AddFileCountReduction(int files) { |
| js_file_count_reduction_->Add(files); |
| if (files >= 1) { |
| filter_->LogFilterModifiedContent(); |
| } |
| } |
| |
| // Set the attribute charset of the resource being combined. This is the |
| // charset taken from the resource's element's charset= attribute, if any. |
| void set_resources_attribute_charset(StringPiece charset) { |
| attribute_charset_ = charset; |
| } |
| |
| private: |
| typedef std::map<const Resource*, JavascriptCodeBlock*> CodeBlockMap; |
| |
| virtual const ContentType* CombinationContentType() { |
| return &kContentTypeJavascript; |
| } |
| |
| virtual bool WritePiece(int index, int num_pieces, const Resource* input, |
| OutputResource* combination, Writer* writer, |
| MessageHandler* handler); |
| |
| JavascriptCodeBlock* BlockForResource(const Resource* input); |
| |
| JsCombineFilter* filter_; |
| int64 combined_js_size_; |
| Variable* js_file_count_reduction_; |
| // The charset from the resource's element, set by our owning Context's |
| // Partition() method each time it checks if a resource can be added to the |
| // current combination. The value is only safe to use in ResourceCombinable() |
| // since it's set just before that's called and its life past that is not |
| // guaranteed. |
| StringPiece attribute_charset_; |
| // The charset of the combination so far. |
| StringPiece combined_charset_; |
| |
| scoped_ptr<JavascriptRewriteConfig> config_; |
| CodeBlockMap code_blocks_; |
| |
| DISALLOW_COPY_AND_ASSIGN(JsCombiner); |
| }; |
| |
| class JsCombineFilter::Context : public RewriteContext { |
| public: |
| Context(RewriteDriver* driver, JsCombineFilter* filter) |
| : RewriteContext(driver, NULL, NULL), |
| combiner_(filter, driver), |
| filter_(filter), |
| fresh_combination_(true) { |
| } |
| |
| // Create and add the slot that corresponds to this element. |
| bool AddElement(HtmlElement* element, HtmlElement::Attribute* href) { |
| ResourcePtr resource(filter_->CreateInputResourceOrInsertDebugComment( |
| href->DecodedValueOrNull(), element)); |
| if (resource.get() == NULL) { |
| return false; |
| } |
| ResourceSlotPtr slot(Driver()->GetSlot(resource, element, href)); |
| AddSlot(slot); |
| fresh_combination_ = false; |
| elements_.push_back(element); |
| // Extract the charset, if any, from the element while it's valid. |
| StringPiece elements_charset(element->AttributeValue(HtmlName::kCharset)); |
| elements_charset.CopyToString(StringVectorAdd(&elements_charsets_)); |
| return true; |
| } |
| |
| // If we get a flush in the middle of things, we may have put a |
| // script tag on that now can't be re-written and should be removed |
| // from the combination. Remove the corresponding slot as well, |
| // because we are no longer handling the resource associated with it. |
| void RemoveLastElement() { |
| RemoveLastSlot(); |
| elements_.pop_back(); |
| elements_charsets_.pop_back(); |
| } |
| |
| bool HasElementLast(HtmlElement* element) { |
| return !empty() && elements_.back() == element; |
| } |
| |
| JsCombiner* combiner() { return &combiner_; } |
| bool empty() const { return elements_.empty(); } |
| bool fresh_combination() { return fresh_combination_; } |
| |
| void Reset() { |
| fresh_combination_ = true; |
| combiner_.Reset(); |
| } |
| |
| protected: |
| virtual void PartitionAsync(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| // Partitioning here requires JS minification, so we want to |
| // move it to a different thread. |
| Driver()->AddLowPriorityRewriteTask(MakeFunction( |
| this, &Context::PartitionImpl, &Context::PartitionCancel, |
| partitions, outputs)); |
| } |
| |
| void PartitionCancel(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| CrossThreadPartitionDone(kTooBusy); |
| } |
| |
| // Divide the slots into partitions according to which js files can |
| // be combined together. |
| void PartitionImpl(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| MessageHandler* handler = Driver()->message_handler(); |
| CachedResult* partition = NULL; |
| CHECK_EQ(static_cast<int>(elements_.size()), num_slots()); |
| CHECK_EQ(static_cast<int>(elements_charsets_.size()), num_slots()); |
| |
| // For each slot, try to add its resource to the current partition. |
| // If we can't, then finalize the last combination, and then |
| // move on to the next slot. |
| for (int i = 0, n = num_slots(); i < n; ++i) { |
| bool add_input = false; |
| ResourcePtr resource(slot(i)->resource()); |
| if (resource->IsSafeToRewrite(rewrite_uncacheable())) { |
| combiner_.set_resources_attribute_charset(elements_charsets_[i]); |
| if (combiner_.AddResourceNoFetch(resource, handler).value) { |
| add_input = true; |
| } else if (partition != NULL) { |
| FinalizePartition(partitions, partition, outputs); |
| partition = NULL; |
| if (combiner_.AddResourceNoFetch(resource, handler).value) { |
| add_input = true; |
| } |
| } |
| } else { |
| FinalizePartition(partitions, partition, outputs); |
| partition = NULL; |
| } |
| if (add_input) { |
| if (partition == NULL) { |
| partition = partitions->add_partition(); |
| } |
| resource->AddInputInfoToPartition( |
| Resource::kIncludeInputHash, i, partition); |
| } |
| } |
| FinalizePartition(partitions, partition, outputs); |
| CrossThreadPartitionDone(partitions->partition_size() != 0 ? |
| kRewriteOk : kRewriteFailed); |
| } |
| |
| // Actually write the new resource. |
| virtual void Rewrite(int partition_index, |
| CachedResult* partition, |
| const OutputResourcePtr& output) { |
| RewriteResult result = kRewriteOk; |
| if (!output->IsWritten()) { |
| ResourceVector resources; |
| for (int i = 0, n = num_slots(); i < n; ++i) { |
| ResourcePtr resource(slot(i)->resource()); |
| resources.push_back(resource); |
| } |
| if (!combiner_.Write(resources, output)) { |
| result = kRewriteFailed; |
| } |
| } |
| RewriteDone(result, partition_index); |
| } |
| |
| // For every partition, write a new script tag that points to the |
| // combined resource. Then create new script tags for each slot |
| // in the partition that evaluate the variable that refers to the |
| // original script for that tag. |
| virtual void Render() { |
| for (int p = 0, np = num_output_partitions(); p < np; ++p) { |
| CachedResult* partition = output_partition(p); |
| int partition_size = partition->input_size(); |
| if (partition_size > 1) { |
| // Make sure we can edit every element here. |
| bool can_rewrite = true; |
| for (int i = 0; i < partition_size; ++i) { |
| int slot_index = partition->input(i).index(); |
| HtmlResourceSlot* html_slot = |
| static_cast<HtmlResourceSlot*>(slot(slot_index).get()); |
| if (!Driver()->IsRewritable(html_slot->element())) { |
| can_rewrite = false; |
| } |
| } |
| |
| if (can_rewrite) { |
| MakeCombinedElement(partition); |
| // we still need to add eval() in place of the |
| // other slots. |
| for (int i = 0; i < partition_size; ++i) { |
| int slot_index = partition->input(i).index(); |
| MakeScriptElement(slot_index); |
| } |
| combiner_.AddFileCountReduction(partition_size - 1); |
| } else { |
| // Disable slot rendering, because we're doing all the rendering here. |
| for (int i = 0; i < partition_size; ++i) { |
| slot(partition->input(i).index())->set_disable_rendering(true); |
| } |
| } // if (can_rewrite) |
| } // if (partition_size > 1) |
| } |
| } |
| |
| virtual const UrlSegmentEncoder* encoder() const { |
| return filter_->encoder(); |
| } |
| virtual const char* id() const { return filter_->id(); } |
| virtual OutputResourceKind kind() const { return kRewrittenResource; } |
| |
| virtual GoogleString CacheKeySuffix() const { |
| // Updated to make sure certain bugfixes actually deploy, and we don't |
| // end up using old broken cached version. |
| return "v4"; |
| } |
| |
| private: |
| // If we can combine, put the result into outputs and then reset |
| // the context (and the combiner) so we start with a fresh slate |
| // for any new slots. |
| void FinalizePartition(OutputPartitions* partitions, |
| CachedResult* partition, |
| OutputResourceVector* outputs) { |
| if (partition != NULL) { |
| OutputResourcePtr combination_output(combiner_.MakeOutput()); |
| if (combination_output.get() == NULL) { |
| partitions->mutable_partition()->RemoveLast(); |
| } else { |
| combination_output->UpdateCachedResultPreservingInputInfo(partition); |
| outputs->push_back(combination_output); |
| } |
| Reset(); |
| } |
| } |
| |
| // Create an element for the combination of all the elements in the |
| // partition. Insert it before first one. |
| void MakeCombinedElement(CachedResult* partition) { |
| int first_index = partition->input(0).index(); |
| HtmlResourceSlot* first_slot = |
| static_cast<HtmlResourceSlot*>(slot(first_index).get()); |
| HtmlElement* combine_element = |
| Driver()->NewElement(NULL, // no parent yet. |
| HtmlName::kScript); |
| Driver()->InsertNodeBeforeNode(first_slot->element(), combine_element); |
| Driver()->AddAttribute(combine_element, HtmlName::kSrc, |
| ResourceSlot::RelativizeOrPassthrough( |
| Driver()->options(), partition->url(), |
| first_slot->url_relativity(), |
| Driver()->base_url())); |
| } |
| |
| // Make a script element with eval(<variable name>), and replace |
| // the existing element with it. |
| void MakeScriptElement(int slot_index) { |
| HtmlResourceSlot* html_slot = static_cast<HtmlResourceSlot*>( |
| slot(slot_index).get()); |
| // Create a new element that doesn't have any children the |
| // original element had. |
| HtmlElement* original = html_slot->element(); |
| HtmlElement* element = Driver()->NewElement(NULL, HtmlName::kScript); |
| Driver()->InsertNodeBeforeNode(original, element); |
| GoogleString var_name = filter_->VarName(Driver(), |
| html_slot->resource()->url()); |
| HtmlNode* script_code = Driver()->NewCharactersNode( |
| element, StrCat("eval(", var_name, ");")); |
| Driver()->AppendChild(element, script_code); |
| html_slot->RequestDeleteElement(); |
| } |
| |
| JsCombineFilter::JsCombiner combiner_; |
| JsCombineFilter* filter_; |
| bool fresh_combination_; |
| // Each of the elements for the resources being combined are added to this |
| // vector, but those elements will be free'd after the end of the document, |
| // though this context might survive past that (as it's an asynchronous |
| // rewriting thread). Therefore the contents of this vector are not usable |
| // in any of the rewriting callbacks: Partition, Rewrite, and Render. |
| std::vector<HtmlElement*> elements_; |
| StringVector elements_charsets_; // charset for each element added, if any. |
| }; |
| |
| bool JsCombineFilter::JsCombiner::WritePiece( |
| int index, int num_pieces, const Resource* input, |
| OutputResource* combination, Writer* writer, MessageHandler* handler) { |
| // Minify if needed. |
| StringPiece not_escaped = input->ExtractUncompressedContents(); |
| |
| // TODO(morlovich): And now we're not updating some stats instead. |
| // Factor out that bit in JsFilter. |
| const RewriteOptions* options = rewrite_driver_->options(); |
| if (options->Enabled(RewriteOptions::kRewriteJavascriptExternal)) { |
| JavascriptCodeBlock* code_block = BlockForResource(input); |
| if (code_block->successfully_rewritten()) { |
| not_escaped = code_block->rewritten_code(); |
| } |
| } |
| |
| // We write out code of each script into a variable. |
| writer->Write(StrCat("var ", |
| JsCombineFilter::VarName( |
| rewrite_driver_, input->url()), |
| " = "), |
| handler); |
| |
| GoogleString escaped; |
| JavascriptCodeBlock::ToJsStringLiteral(not_escaped, &escaped); |
| |
| writer->Write(escaped, handler); |
| writer->Write(";\n", handler); |
| return true; |
| } |
| |
| JavascriptCodeBlock* JsCombineFilter::JsCombiner::BlockForResource( |
| const Resource* input) { |
| std::pair<CodeBlockMap::iterator, bool> insert_result = |
| code_blocks_.insert(CodeBlockMap::value_type(input, NULL)); |
| |
| if (insert_result.second) { |
| // Actually inserted, so we need a value. |
| if (config_.get() == NULL) { |
| config_.reset(JavascriptFilter::InitializeConfig(rewrite_driver_)); |
| } |
| |
| scoped_ptr<JavascriptCodeBlock> new_block(new JavascriptCodeBlock( |
| input->ExtractUncompressedContents(), config_.get(), input->url(), |
| rewrite_driver_->message_handler())); |
| new_block->Rewrite(); |
| insert_result.first->second = new_block.release(); |
| } |
| return insert_result.first->second; |
| } |
| |
| JsCombineFilter::JsCombineFilter(RewriteDriver* driver) |
| : RewriteFilter(driver), |
| script_scanner_(driver), |
| script_depth_(0), |
| current_js_script_(NULL), |
| context_(MakeContext()) { |
| } |
| |
| JsCombineFilter::~JsCombineFilter() { |
| } |
| |
| void JsCombineFilter::InitStats(Statistics* statistics) { |
| statistics->AddVariable(kJsFileCountReduction); |
| } |
| |
| bool JsCombineFilter::IsLikelyStrictMode( |
| const pagespeed::js::JsTokenizerPatterns* jstp, StringPiece input) { |
| pagespeed::js::JsTokenizer tokenizer(jstp, input); |
| |
| // The prolog is spec'd as a sequence of expression statements |
| // consisting only of string literals at beginning of a scope. |
| // If one of them is 'use strict' then it indicates strict mode. |
| // Rather than worry about finer points of the grammar we basically |
| // accept any mixture of strings, semicolons and whitespace. |
| while (true) { |
| StringPiece token_text; |
| JsKeywords::Type token_type = tokenizer.NextToken(&token_text); |
| switch (token_type) { |
| case JsKeywords::kComment: |
| case JsKeywords::kWhitespace: |
| case JsKeywords::kLineSeparator: |
| case JsKeywords::kSemiInsert: |
| // All of these can occur in prologue sections (but not quite that |
| // freely). |
| break; |
| case JsKeywords::kOperator: |
| // ; may also be OK, but other stuff isn't. |
| if (token_text != ";") { |
| return false; |
| } |
| break; |
| case JsKeywords::kStringLiteral: |
| if (token_text == "'use strict'" || token_text == "\"use strict\"") { |
| return true; |
| } |
| break; |
| default: |
| return false; |
| } |
| } |
| } |
| |
| void JsCombineFilter::StartDocumentImpl() { |
| } |
| |
| void JsCombineFilter::StartElementImpl(HtmlElement* element) { |
| HtmlElement::Attribute* src = NULL; |
| ScriptTagScanner::ScriptClassification classification = |
| script_scanner_.ParseScriptElement(element, &src); |
| switch (classification) { |
| case ScriptTagScanner::kNonScript: |
| if (script_depth_ > 0) { |
| // We somehow got some tag inside a script. Be conservative --- |
| // it may be meaningful so we don't want to destroy it; |
| // so flush the complete things before us, and call it a day. |
| if (context_->HasElementLast(current_js_script_)) { |
| context_->RemoveLastElement(); |
| } |
| NextCombination(); |
| } |
| break; |
| |
| case ScriptTagScanner::kJavaScript: |
| ConsiderJsForCombination(element, src); |
| ++script_depth_; |
| break; |
| |
| case ScriptTagScanner::kUnknownScript: |
| // We have something like vbscript. Handle this as a barrier |
| NextCombination(); |
| ++script_depth_; |
| break; |
| } |
| } |
| |
| void JsCombineFilter::EndElementImpl(HtmlElement* element) { |
| if (element->keyword() == HtmlName::kScript) { |
| --script_depth_; |
| if (script_depth_ == 0) { |
| current_js_script_ = NULL; |
| } |
| } |
| } |
| |
| void JsCombineFilter::IEDirective(HtmlIEDirectiveNode* directive) { |
| NextCombination(); |
| } |
| |
| void JsCombineFilter::Characters(HtmlCharactersNode* characters) { |
| // If a script has non-whitespace data inside of it, we cannot |
| // replace its contents with a call to eval, as they may be needed. |
| if (script_depth_ > 0 && !OnlyWhitespace(characters->contents())) { |
| if (context_->HasElementLast(current_js_script_)) { |
| context_->RemoveLastElement(); |
| NextCombination(); |
| } |
| } |
| } |
| |
| void JsCombineFilter::Flush() { |
| // We try to combine what we have thus far the moment we see a flush. |
| // This serves two purposes: |
| // 1) Let's us edit elements while they are still rewritable, |
| // but as late as possible. |
| // 2) Ensures we do combine eventually (as we will get a flush at the end of |
| // parsing). |
| NextCombination(); |
| } |
| |
| // Determine if we can add this script to the combination or not. |
| // If not, call NextCombination() to write out what we've got and then |
| // reset. |
| void JsCombineFilter::ConsiderJsForCombination(HtmlElement* element, |
| HtmlElement::Attribute* src) { |
| // Worst-case scenario is if we somehow ended up with nested scripts. |
| // In this case, we just give up entirely. |
| if (script_depth_ > 0) { |
| driver()->WarningHere("Nested <script> elements"); |
| context_->Reset(); |
| return; |
| } |
| |
| // Opening a new script normally... |
| current_js_script_ = element; |
| |
| // Now we may have something that's not combinable; in those cases we would |
| // like to flush as much as possible. |
| // TODO(morlovich): if we stick with the current eval-based strategy, this |
| // is way too conservative, as we keep multiple script elements for |
| // actual execution. |
| |
| // If our current script may be inside a noscript, which means |
| // we should not be making it runnable. |
| if (noscript_element() != NULL) { |
| NextCombination(); |
| return; |
| } |
| |
| // An inline script. |
| if (src == NULL || src->DecodedValueOrNull() == NULL) { |
| NextCombination(); |
| return; |
| } |
| |
| // Don't combine scripts with the data-pagespeed-no-defer attribute. |
| if (element->FindAttribute(HtmlName::kDataPagespeedNoDefer) != NULL || |
| element->FindAttribute(HtmlName::kPagespeedNoDefer) != NULL) { |
| NextCombination(); |
| return; |
| } |
| |
| // We do not try to merge in a <script with async/defer> or for/event. |
| // TODO(morlovich): is it worth combining multiple scripts with |
| // async/defer if the flags are the same? |
| if (script_scanner_.ExecutionMode(element) != script_scanner_.kExecuteSync) { |
| NextCombination(); |
| return; |
| } |
| |
| // Now we see if policy permits us merging this element with previous ones. |
| context_->AddElement(element, src); |
| } |
| |
| GoogleString JsCombineFilter::VarName(const RewriteDriver* driver, |
| const GoogleString& url) { |
| // We want to apply any rewrite mappings, since they can change the directory |
| // and hence affect variable names. |
| GoogleString output_url; |
| |
| GoogleString domain_out; // ignored. |
| GoogleUrl resource_url(url); |
| // We can't generally use the preexisting UrlPartnership in the |
| // ResourceCombiner since during the .pagespeed. resource fetch it's not |
| // filled in. |
| UrlPartnership::FindResourceDomain(driver->base_url(), |
| driver->server_context()->url_namer(), |
| driver->options(), |
| &resource_url, |
| &domain_out, |
| driver->message_handler()); |
| if (resource_url.IsWebValid()) { |
| resource_url.Spec().CopyToString(&output_url); |
| } else { |
| LOG(DFATAL) << "Somehow got invalid URL in JsCombineFilter::VarName:" |
| << resource_url.UncheckedSpec() << " starting from:" |
| << url; |
| output_url = url; |
| } |
| |
| // We hash the non-host portion of URL to keep it consistent when sharding. |
| // This is safe since we never include URLs from different hosts in a single |
| // combination. |
| GoogleString url_hash = |
| JavascriptCodeBlock::JsUrlHash(output_url, |
| driver->server_context()->hasher()); |
| |
| return StrCat("mod_pagespeed_", url_hash); |
| } |
| |
| JsCombineFilter::Context* JsCombineFilter::MakeContext() { |
| return new Context(driver(), this); |
| } |
| |
| RewriteContext* JsCombineFilter::MakeRewriteContext() { |
| return MakeContext(); |
| } |
| |
| JsCombineFilter::JsCombiner* JsCombineFilter::combiner() const { |
| return context_->combiner(); |
| } |
| |
| // In async flow, tell the rewrite_driver to write out the last |
| // combination, and reset our context to a new one. |
| // In sync flow, just write out what we have so far, and then |
| // reset the context. |
| void JsCombineFilter::NextCombination() { |
| if (!context_->empty()) { |
| driver()->InitiateRewrite(context_.release()); |
| context_.reset(MakeContext()); |
| } |
| context_->Reset(); |
| } |
| |
| void JsCombineFilter::DetermineEnabled(GoogleString* disabled_reason) { |
| set_is_enabled(!driver()->flushed_cached_html()); |
| } |
| |
| } // namespace net_instaweb |