Module:Cssmin
Module documentation
[create]
--[=[
A simple module for Scribunto that minifies CSS
Ported by [[User:Mxn]]
Tweaked to more closely match the output of http://cssminifier.com/
node-cssmin
https://github.com/jbleuzen/node-cssmin
A simple module for Node.js that minify CSS
Author : Johan Bleuzen
cssmin is released under a "BSD License":
http://opensource.org/licenses/bsd-license.php.
cssmin.js
Author: Stoyan Stefanov - http://phpied.com/
This is a JavaScript port of the CSS minification tool
distributed with YUICompressor, itself a port
of the cssmin utility by Isaac Schlueter - http://foohack.com/
Permission is hereby granted to use the JavaScript version under the same
conditions as the YUICompressor (original YUICompressor note below).
YUI Compressor
http://developer.yahoo.com/yui/compressor/
Author: Julien Lecomte - http://www.julienlecomte.net/
Copyright (c) 2011 Yahoo! Inc. All rights reserved.
The copyrights embodied in the content of this file are licensed
by Yahoo! Inc. under the BSD (revised) open source license.
]=]--
local p = {}
local yesno = require "Module:Yesno"
function p.cssmin(css, linebreakpos, pretty)
local frame
if type(css) == "table" then
frame, css, linebreakpos, pretty = css, css.args[1] or "", tonumber(css.args.linebreakpos), yesno(css.args.pretty)
else
frame = mw.getCurrentFrame()
end
local startIndex = 1
local endIndex = 1
local preservedSingleTokens = {}
local numPreservedSingleTokens = 0
local preservedDoubleTokens = {}
local numPreservedDoubleTokens = 0
local comments = {}
local numcomments = 0
local token = ""
local totallen = mw.ustring.len(css)
linebreakpos = linebreakpos or 0
-- collect all comment blocks...
while true do
startIndex = mw.ustring.find(css, "/*", startIndex, true)
if not startIndex then break end
endIndex = mw.ustring.find(css, "*/", startIndex + 2, true)
if not endIndex then
endIndex = totallen - 1
end
token = mw.ustring.sub(css, startIndex + 2, endIndex)
table.insert(comments, token)
numcomments = numcomments + 1
css = mw.ustring.format("%s___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_%i___%s",
mw.ustring.sub(css, 1, startIndex + 1),
numcomments,
mw.ustring.sub(css, endIndex))
startIndex = startIndex + 2
end
-- preserve strings so their content doesn't get accidentally minified
-- TODO: Skip past escaped " and ' in strings.
local preserveStrings = function (match, tokens, oldnumtokens, kind)
local i
local max
local quote = mw.ustring.sub(match, 1, 1)
match = mw.ustring.sub(match, 2, -2)
-- maybe the string contains a comment-like substring?
-- one, maybe more? put'em back then
if mw.ustring.find(match, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") then
for i, comment in ipairs(comments) do
match = mw.ustring.gsub(match, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" .. i .. "___", comment)
end
end
-- minify alpha opacity in filter strings
-- TODO: Ignore case.
match = mw.ustring.gsub(match, "progid:DXImageTransform%.Microsoft%.Alpha%(Opacity=", "alpha(opacity=", 1)
table.insert(tokens, match)
return string.format("%s___YUICSSMIN_PRESERVED_%s_TOKEN_%i___%s", quote, kind:upper(), oldnumtokens + 1, quote)
end
-- TODO: Single-quotes inside double-quoted strings and vice-versa.
css = mw.ustring.gsub(css, "%b''", function (match)
local token = preserveStrings(match, preservedSingleTokens, numPreservedSingleTokens, "single")
numPreservedSingleTokens = numPreservedSingleTokens + 1
return token
end)
css = mw.ustring.gsub(css, '%b""', function (match)
local token = preserveStrings(match, preservedDoubleTokens, numPreservedDoubleTokens, "double")
numPreservedDoubleTokens = numPreservedDoubleTokens + 1
return token
end)
-- strings are safe, now wrestle the comments
for i, token in ipairs(comments) do
-- UNSUPPORTED: ! in the first position of the comment means preserve
-- UNSUPPORTED: \ in the last position looks like hack for Mac/IE5
-- UNSUPPORTED: keep empty comments after child selectors (IE7 hack)
-- e.g. html >/**/ body
-- in all other cases kill the comment
css = mw.ustring.gsub(css, "/%*___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" .. i .. "___%*/", "")
end
-- Normalize all whitespace strings to single spaces. Easier to work with that way.
css = mw.ustring.gsub(css, "%s+", " ")
-- Remove the spaces before the things that should not have spaces before them.
-- But, be careful not to turn "p :link {...}" into "p:link{...}"
-- Swap out any pseudo-class colons with the token, and then swap back.
local insertPseudoClassColons = function (m)
m = mw.ustring.gsub(m, ":", "___YUICSSMIN_PSEUDOCLASSCOLON___")
-- Remove spaces around relational selectors
m = mw.ustring.gsub(m, "%s*([~%+>])%s*", "%1")
return m
end
css = mw.ustring.gsub(css, "^[^{]+", insertPseudoClassColons)
css = mw.ustring.gsub(css, "}[^{]+", insertPseudoClassColons)
-- Preserve spaces in calc expressions
css = mw.ustring.gsub(css, "(calc%s*%(%s*(.-)%s*%))", function (m, c)
return mw.ustring.gsub(m, (mw.ustring.gsub(c, "%%", "%%%%")),
mw.ustring.gsub(c, "%s+", "___YUICSSMIN_SPACE_IN_CALC___"))
end)
css = mw.ustring.gsub(css, "%s+([!{};:>+()%],])", "%1")
css = mw.ustring.gsub(css, "___YUICSSMIN_PSEUDOCLASSCOLON___", ":")
-- retain space for special IE6 cases
css = mw.ustring.gsub(css, ":first%-line([{,])", ":first-line %1")
css = mw.ustring.gsub(css, ":first%-letter([{,])", ":first-letter %1")
-- no space after the end of a preserved comment
css = mw.ustring.gsub(css, "%*/ ", "*/")
-- UNSUPPORTED: If there is a @charset, then only allow one, and push to the top of the file.
-- Put the space back in some cases, to support stuff like
-- @media screen and (-webkit-min-device-pixel-ratio:0){
css = mw.ustring.gsub(css, "%f[%w]and%(", "and (")
-- Remove the spaces after the things that should not have spaces after them.
css = mw.ustring.gsub(css, "([!{}:;>+%(%[,])%s+", "%1")
-- Restore preserved spaces in calc expressions
css = mw.ustring.gsub(css, "___YUICSSMIN_SPACE_IN_CALC___", " ")
-- remove unnecessary semicolons
css = mw.ustring.gsub(css, ";+}", "}")
-- Replace 0(px,em,%) with 0.
css = mw.ustring.gsub(css, "([%s:])(0)(%w%w)", function (pre, zero, unit)
if unit == "px" or unit == "em" or unit == "in" or unit == "cm"
or unit == "mm" or unit == "pc" or unit == "pt"
or unit == "ex" then
return pre .. zero
end
end)
css = mw.ustring.gsub(css, "([%s:])(0)%%", "%1%2")
-- Replace 0 0 0 0; with 0.
css = mw.ustring.gsub(css, ":0 0 0 0([;}])", ":0%1")
css = mw.ustring.gsub(css, ":0 0 0([;}])", ":0%1")
css = mw.ustring.gsub(css, ":0 0([;}])", ":0%1")
-- Replace background-position:0; with background-position:0 0;
-- same for transform-origin
css = mw.ustring.gsub(css, "([%w%-]+):0([;}])", function (prop, tail)
if prop == "background-position" or prop == "transform-origin"
or prop == "webkit-transform-origin"
or prop == "moz-transform-origin"
or prop == "o-transform-origin"
or prop == "ms-transform-origin" then
return string.format("%s:0 0%s", prop:lower(), tail)
end
end)
-- Replace 0.6 to .6, but only when preceded by : or a white-space
css = mw.ustring.gsub(css, "([:%s])0+%.(%d+)", "%1.%2")
-- Shorten font-weight: bold to font-weight: 700
css = mw.ustring.gsub(css, "font%-weight:%s*bold", "font-weight:700")
-- Shorten colors from rgb(51,102,153) to #336699
-- This makes it more likely that it'll get further compressed in the next step.
-- TODO: Ignore case.
css = mw.ustring.gsub(css, "rgb%s*%(%s*([0-9,%s]+)%s*%)", function (rgbcolors)
rgbcolors = mw.text.split(rgbcolors, ",", true)
for i, color in ipairs(rgbcolors) do
rgbcolors[i] = string.format("%x", tonumber(color, 10))
if #rgbcolors[i] == 1 then
rgbcolors[i] = "0" .. rgbcolors[i]
end
end
return "#" .. table.concat(rgbcolors, "")
end)
-- Shorten colors from #AABBCC to #ABC. Note that we want to make sure
-- the color is not preceded by either ", " or =. Indeed, the property
-- filter: chroma(color="#FFFFFF");
-- would become
-- filter: chroma(color="#FFF");
-- which makes the filter break in IE.
css = mw.ustring.gsub(css, "(([^\"'=%s])(%s*)#(%x)(%x)(%x)(%x)(%x)(%x))", function (all, pre, space, r1, r2, g1, g2, b1, b2)
if r1:lower() == r2:lower()
and g1:lower() == g2:lower()
and b1:lower() == b2:lower() then
return string.format("%s%s#%s%s%s", pre, space, r1, g1, b1):lower()
end
return all:lower()
end)
-- border: none -> border:0
-- TODO: Ignore case.
css = mw.ustring.gsub(css, "([%w%-]+):none([;}])", function (prop, tail)
if prop == "border"
or prop == "border-top" or prop == "border-right"
or prop == "border-bottom" or prop == "border-right"
or prop == "outline" or prop == "background" then
return string.format("%s:0%s", prop:lower(), tail)
end
end)
-- shorter opacity IE filter
-- TODO: Ignore case.
css = mw.ustring.gsub(css, "progid:DXImageTransform%.Microsoft%.Alpha%(Opacity=", "alpha(opacity=")
-- Remove empty rules.
css = mw.ustring.gsub(css, "[^%};%{%/]+%{%}", "")
if linebreakpos >= 0 then
-- Some source control tools don't like it when files containing lines longer
-- than, say 8000 characters, are checked in. The linebreak option is used in
-- that case to split long lines after a specific column.
local startIndex = 1
local i = 1
while i <= mw.ustring.len(css) do
i = i + 1
if css[i - 1] == "}" and i - startIndex > linebreakpos then
css = mw.ustring.sub(css, 1, i - 1) .. "\n" .. mw.ustring.sub(css, i)
startIndex = i
end
end
end
-- Replace multiple semi-colons in a row by a single one
-- See SF bug #1980989
css = mw.ustring.gsub(css, ";;+", ";")
-- restore preserved comments and strings
for i, token in ipairs(preservedSingleTokens) do
css = mw.ustring.gsub(css, string.format("___YUICSSMIN_PRESERVED_SINGLE_TOKEN_%i___", i), token)
end
for i, token in ipairs(preservedDoubleTokens) do
css = mw.ustring.gsub(css, string.format("___YUICSSMIN_PRESERVED_DOUBLE_TOKEN_%i___", i), token)
end
-- Unquote url()
css = mw.ustring.gsub(css, "url%('([^']-)'%)", "url(%1)")
css = mw.ustring.gsub(css, "url%(\"([^\"]-)\"%)", "url(%1)")
-- Unquote attribute selectors
local unquoteAttributeSelectors = function (m)
return mw.ustring.gsub(m, "%[(%a[%w_%-]*)([~|%^$%*]?=)(['\"])(%a%w*)%3%]", "[%1%2%4]")
end
css = mw.ustring.gsub(css, "^[^{]+", unquoteAttributeSelectors)
css = mw.ustring.gsub(css, "}[^{]+", unquoteAttributeSelectors)
-- Trim the final string (for any leading or trailing white spaces)
css = mw.text.trim(css)
if pretty then
css = frame:callParserFunction{
name = "#tag",
args = {
"source",
lang = "css",
css
},
}
end
return css
end
return p