blob: a7ae4e8c5aa7bf70f5f8421d2fd4962e0a9e79ff [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_minify.h"
#include <algorithm>
#include <vector>
#include "base/logging.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/writer.h"
#include "util/utf8/public/unicodetext.h"
#include "webutil/css/identifier.h"
#include "webutil/css/media.h"
#include "webutil/css/parser.h"
#include "webutil/css/property.h"
#include "webutil/css/selector.h"
#include "webutil/css/tostring.h"
#include "webutil/css/value.h"
#include "webutil/html/htmlcolor.h"
namespace net_instaweb {
bool CssMinify::Stylesheet(const Css::Stylesheet& stylesheet,
Writer* writer,
MessageHandler* handler) {
// Get an object to encapsulate writing.
CssMinify minifier(writer, handler);
minifier.Minify(stylesheet);
return minifier.ok_;
}
bool CssMinify::ParseStylesheet(StringPiece stylesheet_text) {
ok_ = true;
Css::Parser parser(stylesheet_text);
parser.set_preservation_mode(true); // Leave in unparseable regions.
parser.set_quirks_mode(false); // Don't fix badly formatted colors.
scoped_ptr<Css::Stylesheet> stylesheet(parser.ParseRawStylesheet());
// Report error summary.
if (error_writer_ != NULL) {
if (parser.errors_seen_mask() != Css::Parser::kNoError) {
error_writer_->Write(StringPrintf(
"CSS parsing error mask %s\n",
Integer64ToString(parser.errors_seen_mask()).c_str()), handler_);
}
if (parser.unparseable_sections_seen_mask() != Css::Parser::kNoError) {
error_writer_->Write(StringPrintf(
"CSS unparseable sections mask %s\n",
Integer64ToString(parser.unparseable_sections_seen_mask()).c_str()),
handler_);
}
// Report individual errors.
for (int i = 0, n = parser.errors_seen().size(); i < n; ++i) {
Css::Parser::ErrorInfo error = parser.errors_seen()[i];
error_writer_->Write(error.message, handler_);
error_writer_->Write("\n", handler_);
}
}
Minify(*stylesheet);
return ok_ && (parser.errors_seen_mask() == Css::Parser::kNoError);
}
bool CssMinify::Declarations(const Css::Declarations& declarations,
Writer* writer,
MessageHandler* handler) {
// Get an object to encapsulate writing.
CssMinify minifier(writer, handler);
minifier.JoinMinify(declarations, ";");
return minifier.ok_;
}
CssMinify::CssMinify(Writer* writer, MessageHandler* handler)
: writer_(writer), error_writer_(NULL), handler_(handler), ok_(true),
url_collector_(NULL) {
}
CssMinify::~CssMinify() {
}
// Write if we have not encountered write error yet.
void CssMinify::Write(const StringPiece& str) {
if (ok_) {
ok_ &= writer_->Write(str, handler_);
}
}
void CssMinify::WriteURL(const UnicodeText& url) {
StringPiece string_url(url.utf8_data(), url.utf8_length());
if (url_collector_ != NULL) {
string_url.CopyToString(StringVectorAdd(url_collector_));
}
Write(Css::EscapeUrl(string_url));
}
// Write out minified version of each element of vector using supplied function
// seperated by sep.
template<typename Container>
void CssMinify::JoinMinify(const Container& container, const StringPiece& sep) {
JoinMinifyIter(container.begin(), container.end(), sep);
}
template<typename Iterator>
void CssMinify::JoinMinifyIter(const Iterator& begin, const Iterator& end,
const StringPiece& sep) {
for (Iterator iter = begin; iter != end; ++iter) {
if (iter != begin) {
Write(sep);
}
Minify(**iter);
}
}
template<>
void CssMinify::JoinMinifyIter<Css::FontFaces::const_iterator>(
const Css::FontFaces::const_iterator& begin,
const Css::FontFaces::const_iterator& end,
const StringPiece& sep) {
// Go through the list of @font-faces finding the contiguous subsets with the
// same set of media (f.ex [a b b b a a] -> [a] [b b b] [a a]). For each
// such subset, emit the start of the @media rule (if required), then emit
// each @font-face without an @media rule, separating them by the given 'sep',
// then emit the end of the @media rule (if required).
for (Css::FontFaces::const_iterator iter = begin; iter != end; ) {
const Css::MediaQueries& first_media_queries = (*iter)->media_queries();
MinifyMediaStart(first_media_queries);
MinifyFontFaceIgnoringMedia(**iter);
for (++iter; iter != end && Equals(first_media_queries,
(*iter)->media_queries()); ++iter) {
Write(sep);
MinifyFontFaceIgnoringMedia(**iter);
}
MinifyMediaEnd(first_media_queries);
}
}
template<>
void CssMinify::JoinMinifyIter<Css::Rulesets::const_iterator>(
const Css::Rulesets::const_iterator& begin,
const Css::Rulesets::const_iterator& end,
const StringPiece& sep) {
// Go through the list of rulesets finding the contiguous subsets with the
// same set of media (f.ex [a b b b a a] -> [a] [b b b] [a a]). For each
// such subset, emit the start of the @media rule (if required), then emit
// each ruleset without an @media rule, separating them by the given 'sep',
// then emit the end of the @media rule (if required).
for (Css::Rulesets::const_iterator iter = begin; iter != end; ) {
const Css::MediaQueries& first_media_queries = (*iter)->media_queries();
MinifyMediaStart(first_media_queries);
MinifyRulesetIgnoringMedia(**iter);
for (++iter; iter != end && Equals(first_media_queries,
(*iter)->media_queries()); ++iter) {
Write(sep);
MinifyRulesetIgnoringMedia(**iter);
}
MinifyMediaEnd(first_media_queries);
}
}
// Write the minified versions of each type. Most of these are called via
// templated instantiations of JoinMinify (or JoinMinifyIter) so that we can
// abstract the idea of minifying all sub-elements of a vector and joining them
// together.
// Adapted from webutil/css/tostring.cc
void CssMinify::Minify(const Css::Stylesheet& stylesheet) {
// We might want to add in unnecessary newlines between rules and imports
// so that some readability is preserved.
Minify(stylesheet.charsets());
JoinMinify(stylesheet.imports(), "");
// Note: Adjacent @font-face with the same media type are placed in the same
// @media block. The same is true for adjacent Ruelsets. However, we do not
// yet combine @font-face with Rulesets into the same @media block because
// we do not expect this to be worth the trouble.
JoinMinify(stylesheet.font_faces(), "");
JoinMinify(stylesheet.rulesets(), "");
}
void CssMinify::Minify(const Css::Charsets& charsets) {
for (Css::Charsets::const_iterator iter = charsets.begin();
iter != charsets.end(); ++iter) {
Write("@charset \"");
Write(Css::EscapeString(*iter));
Write("\";");
}
}
void CssMinify::Minify(const Css::Import& import) {
Write("@import url(");
WriteURL(import.link());
Write(")");
if (!import.media_queries().empty()) {
Write(" ");
JoinMinify(import.media_queries(), ",");
}
Write(";");
}
void CssMinify::Minify(const Css::MediaQuery& media_query) {
switch (media_query.qualifier()) {
case Css::MediaQuery::ONLY:
Write("only ");
break;
case Css::MediaQuery::NOT:
Write("not ");
break;
case Css::MediaQuery::NO_QUALIFIER:
break;
}
Write(Css::EscapeIdentifier(media_query.media_type()));
if (!media_query.media_type().empty() && !media_query.expressions().empty()) {
Write(" and ");
}
JoinMinify(media_query.expressions(), " and ");
}
void CssMinify::Minify(const Css::MediaExpression& expression) {
Write("(");
Write(Css::EscapeIdentifier(expression.name()));
if (expression.has_value()) {
Write(":");
const UnicodeText& value = expression.value();
// Note: Value is an unparsed region of raw bytes. So don't escape it.
Write(StringPiece(value.utf8_data(), value.utf8_length()));
}
Write(")");
}
void CssMinify::MinifyMediaStart(const Css::MediaQueries& media_queries) {
if (!media_queries.empty()) {
Write("@media ");
JoinMinify(media_queries, ",");
Write("{");
}
}
void CssMinify::MinifyMediaEnd(const Css::MediaQueries& media_queries) {
if (!media_queries.empty()) {
Write("}");
}
}
void CssMinify::MinifyFontFaceIgnoringMedia(const Css::FontFace& font_face) {
Write("@font-face{");
JoinMinify(font_face.declarations(), ";");
Write("}");
}
void CssMinify::MinifyRulesetIgnoringMedia(const Css::Ruleset& ruleset) {
// TODO(sligocki): Only write out ruleset if declarations() is non-empty.
// Note that we should also propagate this up to not print @media rules
// if all their rulesets are empty. Otherwise we'll fail the css_minify_test
// which checks for idempotent minifications.
switch (ruleset.type()) {
case Css::Ruleset::RULESET:
if (ruleset.selectors().is_dummy()) {
Write(ruleset.selectors().bytes_in_original_buffer());
} else {
JoinMinify(ruleset.selectors(), ",");
}
Write("{");
JoinMinify(ruleset.declarations(), ";");
Write("}");
break;
case Css::Ruleset::UNPARSED_REGION:
Minify(*ruleset.unparsed_region());
break;
}
}
void CssMinify::Minify(const Css::Selector& selector) {
// Note Css::Selector == std::vector<Css::SimpleSelectors*>
Css::Selector::const_iterator iter = selector.begin();
if (iter != selector.end()) {
bool isfirst = true;
Minify(**iter, isfirst);
++iter;
JoinMinifyIter(iter, selector.end(), "");
}
}
void CssMinify::Minify(const Css::SimpleSelectors& sselectors, bool isfirst) {
if (sselectors.combinator() == Css::SimpleSelectors::CHILD) {
Write(">");
} else if (sselectors.combinator() == Css::SimpleSelectors::SIBLING) {
Write("+");
} else if (!isfirst) {
Write(" ");
}
// Note Css::SimpleSelectors == std::vector<Css::SimpleSelector*>
JoinMinify(sselectors, "");
}
void CssMinify::Minify(const Css::SimpleSelector& sselector) {
// SimpleSelector::ToString is already basically minified (and is escaped).
Write(sselector.ToString());
}
namespace {
bool IsValueNormalIdentifier(const Css::Value& value) {
return (value.GetLexicalUnitType() == Css::Value::IDENT &&
value.GetIdentifier().ident() == Css::Identifier::NORMAL);
}
// See http://www.w3.org/TR/css3-values/#lengths : Lengths refer to
// distance measurements and are denoted by <length> in the property
// definitions. A length is a dimension. However, for zero lengths the
// unit identifier is optional (i.e. can be syntactically represented
// as the <number> 0).
//
// http://www.w3.org/TR/css3-values/#relative-lengths
// http://www.w3.org/TR/css3-values/#absolute-lengths
const char* kLengths[] = {
"ch",
"cm",
"em",
"ex",
"in",
"mm",
"pc",
"pt",
"px",
"q",
"rem",
"vh",
"vmax",
"vmin",
"vw",
};
bool IsLength(const GoogleString& unit) {
return std::binary_search(kLengths, kLengths + arraysize(kLengths),
unit);
}
bool UnitsRequiredForValueZero(const GoogleString& unit) {
// https://github.com/pagespeed/mod_pagespeed/issues/1164 : Chrome does not
// allow abbreviating 0s or 0% as 0. It only allows that abbreviation for
// lengths.
//
// https://github.com/pagespeed/mod_pagespeed/issues/1261 See
// https://www.w3.org/TR/CSS2/visudet.html#the-height-property
return (unit == "%") || !IsLength(unit);
}
} // namespace
void CssMinify::MinifyFont(const Css::Values& font_values) {
CHECK_LE(5U, font_values.size());
// font-style: defaults to normal
if (!IsValueNormalIdentifier(*font_values.get(0))) {
Minify(*font_values.get(0));
Write(" ");
}
// font-variant: defaults to normal
if (!IsValueNormalIdentifier(*font_values.get(1))) {
Minify(*font_values.get(1));
Write(" ");
}
// font-weight: defaults to normal
if (!IsValueNormalIdentifier(*font_values.get(2))) {
Minify(*font_values.get(2));
Write(" ");
}
// font-size is required
Minify(*font_values.get(3));
// line-height: defaults to normal
if (!IsValueNormalIdentifier(*font_values.get(4))) {
Write("/");
Minify(*font_values.get(4));
}
// font-family:
for (int i = 5, n = font_values.size(); i < n; ++i) {
Write(i == 5 ? " " : ",");
Minify(*font_values.get(i));
}
}
void CssMinify::Minify(const Css::Declaration& declaration) {
if (declaration.prop() == Css::Property::UNPARSEABLE) {
Write(declaration.bytes_in_original_buffer());
} else {
Write(Css::EscapeIdentifier(declaration.prop_text()));
Write(":");
switch (declaration.prop()) {
case Css::Property::FONT_FAMILY:
JoinMinify(*declaration.values(), ",");
break;
case Css::Property::FONT:
// font: menu special case.
if (declaration.values()->size() == 1) {
JoinMinify(*declaration.values(), " ");
// Normal font notation.
} else if (declaration.values()->size() >= 5) {
MinifyFont(*declaration.values());
} else {
handler_->Message(kError, "Unexpected number of values in "
"font declaration: %d",
static_cast<int>(declaration.values()->size()));
ok_ = false;
}
break;
default:
JoinMinify(*declaration.values(), " ");
break;
}
if (declaration.IsImportant()) {
Write("!important");
}
}
}
void CssMinify::Minify(const Css::Value& value) {
switch (value.GetLexicalUnitType()) {
case Css::Value::NUMBER: {
GoogleString buffer;
StringPiece number_string;
if (!value.bytes_in_original_buffer().empty()) {
// All parsed values should have verbatim bytes set and we use them
// to ensure we keep the original precision.
number_string = value.bytes_in_original_buffer();
} else {
// Values added or modified outside of the parsing code need
// to be converted to strings by us.
buffer = StringPrintf("%.16g", value.GetFloatValue());
number_string = buffer;
}
if (number_string.starts_with("0.")) {
// Optimization: Strip "0.25" -> ".25".
Write(number_string.substr(1));
} else if (number_string.starts_with("-0.")) {
// Optimization: Strip "-0.25" -> "-.25".
Write("-");
Write(number_string.substr(2));
} else {
// Otherwise just print the original string.
Write(number_string);
}
// Optimization: Do not print units if value is 0.
GoogleString unit = value.GetDimensionUnitText();
if (!unit.empty() &&
((value.GetFloatValue() != 0) || UnitsRequiredForValueZero(unit))) {
// Unit can be either "%" or an identifier.
if (unit != "%") {
unit = Css::EscapeIdentifier(unit);
}
Write(unit);
}
break;
}
case Css::Value::URI:
Write("url(");
WriteURL(value.GetStringValue());
Write(")");
break;
case Css::Value::FUNCTION:
Write(Css::EscapeIdentifier(value.GetFunctionName()));
Write("(");
Minify(*value.GetParametersWithSeparators());
Write(")");
break;
case Css::Value::RECT:
Write("rect(");
Minify(*value.GetParametersWithSeparators());
Write(")");
break;
case Css::Value::COLOR:
// TODO(sligocki): Can we assert, or might this happen in the wild?
CHECK(value.GetColorValue().IsDefined());
Write(HtmlColorUtils::MaybeConvertToCssShorthand(
value.GetColorValue()));
break;
case Css::Value::STRING:
if (!value.bytes_in_original_buffer().empty()) {
// All parsed strings should have verbatim bytes set.
// Note: bytes_in_original_buffer() contains quote chars.
Write(value.bytes_in_original_buffer());
} else {
// Strings added or modified outside of the parsing code will need
// to be serialized by us.
Write("\"");
Write(Css::EscapeString(value.GetStringValue()));
Write("\"");
}
break;
case Css::Value::IDENT:
Write(Css::EscapeIdentifier(value.GetIdentifierText()));
break;
case Css::Value::COMMA:
// TODO(sligocki): Do not add spaces around COMMA tokens.
Write(",");
break;
case Css::Value::UNKNOWN:
handler_->MessageS(kError, "Unknown attribute");
ok_ = false;
break;
case Css::Value::DEFAULT:
break;
}
}
void CssMinify::Minify(const Css::FunctionParameters& parameters) {
if (parameters.size() >= 1) {
Minify(*parameters.value(0));
}
for (int i = 1, n = parameters.size(); i < n; ++i) {
switch (parameters.separator(i)) {
case Css::FunctionParameters::COMMA_SEPARATED:
Write(",");
break;
case Css::FunctionParameters::SPACE_SEPARATED:
Write(" ");
break;
}
Minify(*parameters.value(i));
}
}
void CssMinify::Minify(const Css::UnparsedRegion& unparsed_region) {
Write(unparsed_region.bytes_in_original_buffer());
}
bool CssMinify::Equals(const Css::MediaQueries& a,
const Css::MediaQueries& b) const {
if (a.size() != b.size()) {
return false;
}
for (int i = 0, n = a.size(); i < n; ++i) {
if (!Equals(*a.at(i), *b.at(i))) {
return false;
}
}
return true;
}
bool CssMinify::Equals(const Css::MediaQuery& a,
const Css::MediaQuery& b) const {
if (a.qualifier() != b.qualifier() ||
a.media_type() != b.media_type() ||
a.expressions().size() != b.expressions().size()) {
return false;
}
for (int i = 0, n = a.expressions().size(); i < n; ++i) {
if (!Equals(a.expression(i), b.expression(i))) {
return false;
}
}
return true;
}
bool CssMinify::Equals(const Css::MediaExpression& a,
const Css::MediaExpression& b) const {
if (a.name() != b.name() ||
a.has_value() != b.has_value()) {
return false;
}
if (a.has_value() && a.value() != b.value()) {
return false;
}
return true;
}
} // namespace net_instaweb