| /** |
| * 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: sligocki@google.com (Shawn Ligocki) |
| |
| #include "net/instaweb/rewriter/public/js_outline_filter.h" |
| |
| #include "base/scoped_ptr.h" |
| #include "net/instaweb/rewriter/public/output_resource.h" |
| #include "net/instaweb/rewriter/public/resource_manager.h" |
| #include "net/instaweb/htmlparse/public/html_parse.h" |
| #include "net/instaweb/htmlparse/public/html_element.h" |
| #include "net/instaweb/util/public/content_type.h" |
| #include "net/instaweb/util/public/simple_meta_data.h" |
| #include <string> |
| #include "net/instaweb/util/public/string_writer.h" |
| |
| namespace net_instaweb { |
| |
| const char JsOutlineFilter::kFilterId[] = "jo"; |
| |
| JsOutlineFilter::JsOutlineFilter(HtmlParse* html_parse, |
| ResourceManager* resource_manager, |
| size_t size_threshold_bytes) |
| : inline_element_(NULL), |
| html_parse_(html_parse), |
| resource_manager_(resource_manager), |
| size_threshold_bytes_(size_threshold_bytes), |
| s_script_(html_parse->Intern("script")), |
| s_src_(html_parse->Intern("src")), |
| s_type_(html_parse->Intern("type")) { } |
| |
| void JsOutlineFilter::StartDocument() { |
| inline_element_ = NULL; |
| buffer_.clear(); |
| } |
| |
| void JsOutlineFilter::StartElement(HtmlElement* element) { |
| // No tags allowed inside script element. |
| if (inline_element_ != NULL) { |
| // TODO(sligocki): Add negative unit tests to hit these errors. |
| html_parse_->ErrorHere("Tag '%s' found inside script.", |
| element->tag().c_str()); |
| inline_element_ = NULL; // Don't outline what we don't understand. |
| buffer_.clear(); |
| } |
| if (element->tag() == s_script_) { |
| inline_element_ = element; |
| buffer_.clear(); |
| // script elements which already have a src should not be outlined. |
| if (element->FindAttribute(s_src_) != NULL) { |
| inline_element_ = NULL; |
| } |
| } |
| } |
| |
| void JsOutlineFilter::EndElement(HtmlElement* element) { |
| if (inline_element_ != NULL) { |
| if (element != inline_element_) { |
| // No other tags allowed inside script element. |
| html_parse_->ErrorHere("Tag '%s' found inside script.", |
| element->tag().c_str()); |
| |
| } else if (buffer_.size() >= size_threshold_bytes_) { |
| OutlineScript(inline_element_, buffer_); |
| } else { |
| html_parse_->InfoHere("Inline element not outlined because its size %d, " |
| "is below threshold %d", |
| static_cast<int>(buffer_.size()), |
| static_cast<int>(size_threshold_bytes_)); |
| } |
| inline_element_ = NULL; |
| buffer_.clear(); |
| } |
| } |
| |
| void JsOutlineFilter::Flush() { |
| // If we were flushed in a script element, we cannot outline it. |
| inline_element_ = NULL; |
| buffer_.clear(); |
| } |
| |
| void JsOutlineFilter::Characters(HtmlCharactersNode* characters) { |
| if (inline_element_ != NULL) { |
| buffer_ += characters->contents(); |
| } |
| } |
| |
| void JsOutlineFilter::Comment(HtmlCommentNode* comment) { |
| if (inline_element_ != NULL) { |
| html_parse_->ErrorHere("Comment found inside script."); |
| inline_element_ = NULL; // Don't outline what we don't understand. |
| buffer_.clear(); |
| } |
| } |
| |
| void JsOutlineFilter::Cdata(HtmlCdataNode* cdata) { |
| if (inline_element_ != NULL) { |
| html_parse_->ErrorHere("CDATA found inside script."); |
| inline_element_ = NULL; // Don't outline what we don't understand. |
| buffer_.clear(); |
| } |
| } |
| |
| void JsOutlineFilter::IEDirective(HtmlIEDirectiveNode* directive) { |
| if (inline_element_ != NULL) { |
| html_parse_->ErrorHere("IE Directive found inside script."); |
| inline_element_ = NULL; // Don't outline what we don't understand. |
| buffer_.clear(); |
| } |
| } |
| |
| // Try to write content and possibly header to resource. |
| bool JsOutlineFilter::WriteResource(const std::string& content, |
| OutputResource* resource, |
| MessageHandler* handler) { |
| // We set the TTL of the origin->hashed_name map to 0 because this is |
| // derived from the inlined HTML. |
| int64 origin_expire_time_ms = 0; |
| return resource_manager_->Write(HttpStatus::kOK, content, resource, |
| origin_expire_time_ms, handler); |
| } |
| |
| // Create file with script content and remove that element from DOM. |
| // TODO(sligocki): We probably will break any relative URL references here. |
| void JsOutlineFilter::OutlineScript(HtmlElement* inline_element, |
| const std::string& content) { |
| if (html_parse_->IsRewritable(inline_element)) { |
| // Create script file from content. |
| const char* type = inline_element->AttributeValue(s_type_); |
| // We only deal with Javascript. If no type specified, JS is assumed. |
| // See http://www.w3.org/TR/html5/scripting-1.html#script |
| if (type == NULL || |
| strcmp(type, kContentTypeJavascript.mime_type()) == 0) { |
| MessageHandler* handler = html_parse_->message_handler(); |
| // Create outline resource at the document location, not base URL location |
| scoped_ptr<OutputResource> resource( |
| resource_manager_->CreateOutputResourceWithPath( |
| GoogleUrl::AllExceptLeaf(html_parse_->gurl()), kFilterId, "_", |
| &kContentTypeJavascript, handler)); |
| if (WriteResource(content, resource.get(), handler)) { |
| HtmlElement* outline_element = html_parse_->NewElement( |
| inline_element->parent(), s_script_); |
| outline_element->AddAttribute(s_src_, resource->url(), "'"); |
| // Add all atrributes from old script element to new script src element. |
| for (int i = 0; i < inline_element->attribute_size(); ++i) { |
| const HtmlElement::Attribute& attr = inline_element->attribute(i); |
| outline_element->AddAttribute(attr); |
| } |
| // Add <script src=...> element to DOM. |
| html_parse_->InsertElementBeforeElement(inline_element, |
| outline_element); |
| // Remove original script element from DOM. |
| if (!html_parse_->DeleteElement(inline_element)) { |
| html_parse_->FatalErrorHere("Failed to delete inline script element"); |
| } |
| } else { |
| html_parse_->ErrorHere("Failed to write outlined script resource."); |
| } |
| } else { |
| std::string element_string; |
| inline_element->ToString(&element_string); |
| html_parse_->InfoHere("Cannot outline non-javascript script %s", |
| element_string.c_str()); |
| } |
| } |
| } |
| |
| } // namespace net_instaweb |