blob: dada66ee221d36079b7c724856fde600eea34619 [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/css_outline_filter.h"
#include "base/logging.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/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/base/string_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"
namespace net_instaweb {
class MessageHandler;
const char kStylesheet[] = "stylesheet";
const char CssOutlineFilter::kFilterId[] = "co";
CssOutlineFilter::CssOutlineFilter(RewriteDriver* driver)
: CommonFilter(driver),
inline_element_(NULL),
inline_chars_(NULL),
size_threshold_bytes_(driver->options()->css_outline_min_bytes()) {
}
CssOutlineFilter::~CssOutlineFilter() {}
void CssOutlineFilter::StartDocumentImpl() {
inline_element_ = NULL;
inline_chars_ = NULL;
}
void CssOutlineFilter::StartElementImpl(HtmlElement* element) {
// No tags allowed inside style element.
if (inline_element_ != NULL) {
// TODO(sligocki): Add negative unit tests to hit these errors.
driver()->ErrorHere("Tag '%s' found inside style.",
CEscape(element->name_str()).c_str());
inline_element_ = NULL; // Don't outline what we don't understand.
inline_chars_ = NULL;
}
if (element->keyword() == HtmlName::kStyle &&
element->FindAttribute(HtmlName::kScoped) == NULL) {
// <style scoped> can't be directly converted to a <link>. We could
// theoretically convert it to a <style scoped>@import ... </style>, but
// given the feature has very little browser support, it's probably not
// worth the effort, so we just leave it alone.
// All other <style> blocks are candidates for conversion.
inline_element_ = element;
inline_chars_ = NULL;
}
}
void CssOutlineFilter::EndElementImpl(HtmlElement* element) {
if (inline_element_ != NULL) {
CHECK(element == inline_element_);
if (inline_chars_ != NULL &&
inline_chars_->contents().size() >= size_threshold_bytes_) {
OutlineStyle(inline_element_, inline_chars_->contents());
}
inline_element_ = NULL;
inline_chars_ = NULL;
}
}
void CssOutlineFilter::Flush() {
// If we were flushed in a style element, we cannot outline it.
inline_element_ = NULL;
inline_chars_ = NULL;
}
void CssOutlineFilter::Characters(HtmlCharactersNode* characters) {
if (inline_element_ != NULL) {
CHECK(inline_chars_ == NULL) << "Multiple character blocks in style.";
inline_chars_ = characters;
}
}
// Try to write content and possibly header to resource.
bool CssOutlineFilter::WriteResource(const StringPiece& content,
OutputResource* resource,
MessageHandler* handler) {
// We don't provide charset here since in general we can just inherit
// from the page.
// TODO(morlovich) check for proper behavior in case of embedded BOM.
// TODO(matterbury) but AFAICT you cannot have a BOM in a <style> tag.
return driver()->Write(
ResourceVector(), content, &kContentTypeCss, StringPiece(), resource);
}
// Create file with style content and remove that element from DOM.
void CssOutlineFilter::OutlineStyle(HtmlElement* style_element,
const GoogleString& content_str) {
StringPiece content(content_str);
if (driver()->IsRewritable(style_element)) {
// Create style file from content.
const char* type = style_element->AttributeValue(HtmlName::kType);
// We only deal with CSS styles. If no type specified, CSS is assumed.
// See http://www.w3.org/TR/html5/semantics.html#the-style-element
if (type == NULL || strcmp(type, kContentTypeCss.mime_type()) == 0) {
MessageHandler* handler = driver()->message_handler();
// Create outline resource at the document location,
// not base URL location.
GoogleString failure_reason;
OutputResourcePtr output_resource(
driver()->CreateOutputResourceWithUnmappedUrl(
driver()->google_url(), kFilterId, "_", kOutlinedResource,
&failure_reason));
if (output_resource.get() == NULL) {
driver()->InsertDebugComment(failure_reason, style_element);
} else {
// Rewrite URLs in content.
GoogleString transformed_content;
StringWriter writer(&transformed_content);
bool content_valid = true;
switch (driver()->ResolveCssUrls(base_url(),
output_resource->resolved_base(),
content,
&writer, handler)) {
case RewriteDriver::kNoResolutionNeeded:
break;
case RewriteDriver::kWriteFailed:
content_valid = false;
break;
case RewriteDriver::kSuccess:
content = transformed_content;
break;
}
if (content_valid &&
WriteResource(content, output_resource.get(), handler)) {
HtmlElement* link_element = driver()->NewElement(
style_element->parent(), HtmlName::kLink);
driver()->AddAttribute(link_element, HtmlName::kRel, kStylesheet);
driver()->AddAttribute(
link_element, HtmlName::kHref, output_resource->url());
// Add all style attributes to link.
const HtmlElement::AttributeList& attrs = style_element->attributes();
for (HtmlElement::AttributeConstIterator i(attrs.begin());
i != attrs.end(); ++i) {
const HtmlElement::Attribute& attr = *i;
link_element->AddAttribute(attr);
}
// Add link to DOM.
driver()->InsertNodeAfterNode(style_element, link_element);
// Remove style element from DOM.
if (!driver()->DeleteNode(style_element)) {
driver()->FatalErrorHere("Failed to delete inline style element");
}
}
}
} else {
driver()->InsertDebugComment(StrCat(
"Cannot outline stylesheet with non-CSS type=", type), style_element);
GoogleString element_string = style_element->ToString();
driver()->InfoHere("Cannot outline non-css stylesheet %s",
element_string.c_str());
}
}
}
} // namespace net_instaweb