blob: 11c7eb952f0aa85b394fe50a0e47cf81a6301bea [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: sligocki@google.com (Shawn Ligocki)
#include "net/instaweb/rewriter/public/js_outline_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/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/script_tag_scanner.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "pagespeed/kernel/base/ref_counted_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.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"
namespace net_instaweb {
class MessageHandler;
const char JsOutlineFilter::kFilterId[] = "jo";
JsOutlineFilter::JsOutlineFilter(RewriteDriver* driver)
: CommonFilter(driver),
inline_element_(NULL),
inline_chars_(NULL),
server_context_(driver->server_context()),
size_threshold_bytes_(driver->options()->js_outline_min_bytes()),
script_tag_scanner_(driver) { }
JsOutlineFilter::~JsOutlineFilter() {}
void JsOutlineFilter::StartDocumentImpl() {
inline_element_ = NULL;
inline_chars_ = NULL;
}
void JsOutlineFilter::StartElementImpl(HtmlElement* element) {
// No tags allowed inside script element.
if (inline_element_ != NULL) {
// TODO(sligocki): Add negative unit tests to hit these errors.
driver()->ErrorHere("Tag '%s' found inside script.",
CEscape(element->name_str()).c_str());
inline_element_ = NULL; // Don't outline what we don't understand.
inline_chars_ = NULL;
}
HtmlElement::Attribute* src;
// We only deal with Javascript
if (script_tag_scanner_.ParseScriptElement(element, &src) ==
ScriptTagScanner::kJavaScript) {
inline_element_ = element;
inline_chars_ = NULL;
// script elements which already have a src should not be outlined.
if (src != NULL) {
inline_element_ = NULL;
}
}
}
void JsOutlineFilter::EndElementImpl(HtmlElement* element) {
if (inline_element_ != NULL) {
if (element != inline_element_) {
// No other tags allowed inside script element.
driver()->ErrorHere("Tag '%s' found inside script.",
CEscape(element->name_str()).c_str());
} else if (inline_chars_ != NULL &&
inline_chars_->contents().size() >= size_threshold_bytes_) {
OutlineScript(inline_element_, inline_chars_->contents());
}
inline_element_ = NULL;
inline_chars_ = NULL;
}
}
void JsOutlineFilter::Flush() {
// If we were flushed in a script element, we cannot outline it.
inline_element_ = NULL;
inline_chars_ = NULL;
}
void JsOutlineFilter::Characters(HtmlCharactersNode* characters) {
if (inline_element_ != NULL) {
inline_chars_ = characters;
}
}
// Try to write content and possibly header to resource.
bool JsOutlineFilter::WriteResource(const GoogleString& content,
OutputResource* resource,
MessageHandler* handler) {
// We don't provide charset here since in generally we can just inherit
// from the page.
// TODO(morlovich) check for proper behavior in case of embedded BOM.
return driver()->Write(
ResourceVector(), content, &kContentTypeJavascript, StringPiece(),
resource);
}
// Create file with script content and remove that element from DOM.
void JsOutlineFilter::OutlineScript(HtmlElement* inline_element,
const GoogleString& content) {
if (driver()->IsRewritable(inline_element)) {
// Create script file from content.
MessageHandler* handler = driver()->message_handler();
// Create outline resource at the document location, not base URL location
GoogleString failure_reason;
OutputResourcePtr resource(
driver()->CreateOutputResourceWithUnmappedUrl(
driver()->google_url(), kFilterId, "_", kOutlinedResource,
&failure_reason));
if (resource.get() == NULL) {
driver()->InsertDebugComment(failure_reason, inline_element);
} else if (WriteResource(content, resource.get(), handler)) {
HtmlElement* outline_element = driver()->CloneElement(inline_element);
driver()->AddAttribute(outline_element, HtmlName::kSrc,
resource->url());
// Add <script src=...> element to DOM.
driver()->InsertNodeBeforeNode(inline_element, outline_element);
// Remove original script element from DOM.
if (!driver()->DeleteNode(inline_element)) {
driver()->FatalErrorHere("Failed to delete inline script element");
}
} else {
driver()->InsertDebugComment("Failed to write outlined script resource.",
inline_element);
driver()->ErrorHere("Failed to write outlined script resource.");
}
}
}
} // namespace net_instaweb