CSS-Minifier-XS-0.09000755001750001750 012234533345 14047 5ustar00grahamgraham000000000000CSS-Minifier-XS-0.09/Build.PL000444001750001750 70612234533345 15463 0ustar00grahamgraham000000000000use strict; use warnings; use Module::Build; Module::Build->new( 'module_name' => 'CSS::Minifier::XS', 'license' => 'perl', 'dist_author' => 'Graham TerMarsch (cpan@howlingfrog.com)', 'create_makefile_pl'=> 'traditional', 'xs_files' => { 'XS.xs' => 'lib/CSS/Minifier/XS.xs', }, 'build_requires' => { 'Test::More' => 0, }, )->create_build_script(); CSS-Minifier-XS-0.09/Changes000444001750001750 437412234533345 15507 0ustar00grahamgraham000000000000Revision history for Perl extension CSS::Minifier::XS. 0.09 Wed Oct 31 13:00 PDT 2013 - RT #85350; retain WS around "+" and "-" operators, so that we don't break the "calc()" function by accident. Thanks to Philipp Soehnlein - RT #60239; remove WS around ">" and "~" selector combinators Thanks to Jacob R. - RT #60238; remove units on zero values (e.g. "0px" -> "0") Thanks to Jacob R. - RT #85896 and #79792; allow builds on Perls older than 5.8.8 Thanks to Michael Robinton 0.08 Tue Nov 2 22:25 PDT 2010 - Bump Perl requirement to 5.8.8; oldest release with Newxz() 0.07 Wed Jul 21 21:09 PDT 2010 - RT #39978; use 'Newxz/Safefree' instead of 'malloc/free' for memory allocation. Thanks to Kenichi Ishigaki. 0.06 Wed Jul 21 20:44 PDT 2010 - RT #59549; '(' should retain leading WS, as needed for @media queries. Thanks to Mike Cardwell. 0.05 Fri Apr 23 22:29 PDT 2010 - Switch to Git 0.04 Thu Aug 6 22:03 PDT 2009 - fix invalid "L" POD sequence 0.03 Wed Jul 16 23:22 PDT 2008 - RT #36557: Nasty segfault on minifying comment-only css input. Turned out to be *any* input that minified to "nothing" caused the segfault, and only in certain Perl versions. 0.02 Tue May 6 00:16 PDT 2008 - rebuild packages; EU::MM borked my META.yml 0.01 Mon May 5 15:15 PDT 2008 - no changes from 0.01_06, but is non-devel 0.01_06 Wed Oct 31 20:39 PDT 2007 - retain whitespace after ")" characters; MSIE needs the whitespace, even though the W3 validator considers the results valid CSS. 0.01_05 Sat Oct 20 22:52 PDT 2007 - don't use "strcasestr()"; not available on Solaris 0.01_04 Wed Oct 17 15:57 PDT 2007 - fix t/02-minify.t so it doesn't try to "use_ok()" before issuing a test plan 0.01_03 Tue Oct 16 19:45 PDT 2007 - don't use "strndup()"; not available on all systems - we require Perl 5.006; update Build.PL and XS.pm to denote this 0.01_02 Tue Oct 16 12:18 PDT 2007 - relocate the XS file so that its picked up properly by EU::MM when running "perl Makefile.PL" to do a build. 0.01_01 Mon Oct 15 22:11 PDT 2007 - initial public version CSS-Minifier-XS-0.09/Makefile.PL000444001750001750 60612234533345 16140 0ustar00grahamgraham000000000000# Note: this file was auto-generated by Module::Build::Compat version 0.4004 use ExtUtils::MakeMaker; WriteMakefile ( 'NAME' => 'CSS::Minifier::XS', 'VERSION_FROM' => 'lib/CSS/Minifier/XS.pm', 'PREREQ_PM' => { 'ExtUtils::CBuilder' => 0, 'Test::More' => 0 }, 'INSTALLDIRS' => 'site', 'EXE_FILES' => [], 'PL_FILES' => {} ) ; CSS-Minifier-XS-0.09/MANIFEST.SKIP000444001750001750 4312234533345 16037 0ustar00grahamgraham000000000000Build _build .git/ \..*\.swp blib/ CSS-Minifier-XS-0.09/README000444001750001750 60712234533345 15047 0ustar00grahamgraham000000000000CSS::Minifier::XS minifies CSS documents by removing un-necessary whitespace Copyright (C) 2007-2010, Graham TerMarsch. All Rights Reserved. This is free software; you can redistribute it and/or modify it under the same terms as Perl itself. To install: perl Build.PL ./Build ./Build test ./Build install or: perl Makefile.PL make make test make install CSS-Minifier-XS-0.09/MANIFEST000444001750001750 202112234533345 15330 0ustar00grahamgraham000000000000Build.PL Makefile.PL Changes MANIFEST MANIFEST.SKIP META.yml META.json README XS.xs lib/CSS/Minifier/XS.pm t/01-loads.t t/02-minify.t t/03-minifies-to-nothing.t t/99-benchmark.t t/pod.t t/pod-coverage.t t/css/calc-operators.css t/css/calc-operators.min t/css/combinators.css t/css/combinators.min t/css/comments.css t/css/comments.min t/css/comments-mac-ie-hack.css t/css/comments-mac-ie-hack.min t/css/leading-whitespace.css t/css/leading-whitespace.min t/css/media.css t/css/media.min t/css/retain-whitespace-after-close-parenthesis.css t/css/retain-whitespace-after-close-parenthesis.min t/css/styles.css t/css/styles.min t/css/trailing-semicolon.css t/css/trailing-semicolon.min t/css/trailing-whitespace.css t/css/trailing-whitespace.min t/css/units-preserved.css t/css/units-preserved.min t/css/zero-units-point-zero.css t/css/zero-units-point-zero.min t/css/zero-units-zero.css t/css/zero-units-zero.min t/css/zero-units-zerooo.css t/css/zero-units-zerooo.min t/css/zero-units-zero-point-zero.css t/css/zero-units-zero-point-zero.min CSS-Minifier-XS-0.09/META.yml000444001750001750 106612234533345 15460 0ustar00grahamgraham000000000000--- abstract: 'XS based CSS minifier' author: - 'Graham TerMarsch (cpan@howlingfrog.com)' build_requires: ExtUtils::CBuilder: 0 Test::More: 0 configure_requires: Module::Build: 0.40 dynamic_config: 1 generated_by: 'Module::Build version 0.4004, CPAN::Meta::Converter version 2.120921' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: 1.4 name: CSS-Minifier-XS provides: CSS::Minifier::XS: file: lib/CSS/Minifier/XS.pm version: 0.09 resources: license: http://dev.perl.org/licenses/ version: 0.09 CSS-Minifier-XS-0.09/META.json000444001750001750 167112234533345 15632 0ustar00grahamgraham000000000000{ "abstract" : "XS based CSS minifier", "author" : [ "Graham TerMarsch (cpan@howlingfrog.com)" ], "dynamic_config" : 1, "generated_by" : "Module::Build version 0.4004, CPAN::Meta::Converter version 2.120921", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : "2" }, "name" : "CSS-Minifier-XS", "prereqs" : { "build" : { "requires" : { "ExtUtils::CBuilder" : "0", "Test::More" : "0" } }, "configure" : { "requires" : { "Module::Build" : "0.40" } } }, "provides" : { "CSS::Minifier::XS" : { "file" : "lib/CSS/Minifier/XS.pm", "version" : "0.09" } }, "release_status" : "stable", "resources" : { "license" : [ "http://dev.perl.org/licenses/" ] }, "version" : "0.09" } CSS-Minifier-XS-0.09/XS.xs000444001750001750 4663012234533345 15143 0ustar00grahamgraham000000000000#include #include #include #include #include #include #include /* **************************************************************************** * CHARACTER CLASS METHODS * **************************************************************************** */ int charIsSpace(char ch) { if (ch == ' ') return 1; if (ch == '\t') return 1; return 0; } int charIsEndspace(char ch) { if (ch == '\n') return 1; if (ch == '\r') return 1; if (ch == '\f') return 1; return 0; } int charIsWhitespace(char ch) { return charIsSpace(ch) || charIsEndspace(ch); } int charIsIdentifier(char ch) { if ((ch >= 'a') && (ch <= 'z')) return 1; if ((ch >= 'A') && (ch <= 'Z')) return 1; if ((ch >= '0') && (ch <= '9')) return 1; if (ch == '_') return 1; if (ch == '.') return 1; if (ch == '#') return 1; if (ch == '@') return 1; if (ch == '%') return 1; return 0; } int charIsInfix(char ch) { /* WS before+after these characters can be removed */ if (ch == '{') return 1; if (ch == '}') return 1; if (ch == ';') return 1; if (ch == ':') return 1; if (ch == ',') return 1; if (ch == '~') return 1; if (ch == '>') return 1; return 0; } int charIsPrefix(char ch) { /* WS after these characters can be removed */ if (ch == '(') return 1; /* requires leading WS when used in @media */ return charIsInfix(ch); } int charIsPostfix(char ch) { /* WS before these characters can be removed */ if (ch == ')') return 1; /* requires trailing WS for MSIE */ return charIsInfix(ch); } /* **************************************************************************** * TYPE DEFINITIONS * **************************************************************************** */ typedef enum { NODE_EMPTY, NODE_WHITESPACE, NODE_BLOCKCOMMENT, NODE_IDENTIFIER, NODE_LITERAL, NODE_SIGIL } NodeType; struct _Node; typedef struct _Node Node; struct _Node { /* linked list pointers */ Node* prev; Node* next; /* node internals */ char* contents; size_t length; NodeType type; int can_prune; }; typedef struct { /* linked list pointers */ Node* head; Node* tail; /* doc internals */ const char* buffer; size_t length; size_t offset; } CssDoc; /* **************************************************************************** * NODE CHECKING MACROS/FUNCTIONS * **************************************************************************** */ /* checks to see if the node is the given string, case INSENSITIVELY */ int nodeEquals(Node* node, const char* string) { return (strcasecmp(node->contents, string) == 0); } /* checks to see if the node contains the given string, case INSENSITIVELY */ int nodeContains(Node* node, const char* string) { const char* haystack = node->contents; size_t len = strlen(string); char ul_start[2] = { tolower(*string), toupper(*string) }; /* if node is shorter we know we're not going to have a match */ if (len > node->length) return 0; /* find the needle in the haystack */ while (haystack && *haystack) { /* find first char of needle */ haystack = strpbrk( haystack, ul_start ); if (haystack == NULL) return 0; /* check if the rest matches */ if (strncasecmp(haystack, string, len) == 0) return 1; /* nope, move onto next character in the haystack */ haystack ++; } /* no match */ return 0; } /* checks to see if the node begins with the given string, case INSENSITIVELY. */ int nodeBeginsWith(Node* node, const char* string) { size_t len = strlen(string); if (len > node->length) return 0; return (strncasecmp(node->contents, string, len) == 0); } /* checks to see if the node ends with the given string, case INSENSITVELY. */ int nodeEndsWith(Node* node, const char* string) { size_t len = strlen(string); size_t off = node->length - len; if (len > node->length) return 0; return (strncasecmp(node->contents+off, string, len) == 0); } /* macros to help see what kind of node we've got */ #define nodeIsWHITESPACE(node) ((node->type == NODE_WHITESPACE)) #define nodeIsBLOCKCOMMENT(node) ((node->type == NODE_BLOCKCOMMENT)) #define nodeIsIDENTIFIER(node) ((node->type == NODE_IDENTIFIER)) #define nodeIsLITERAL(node) ((node->type == NODE_LITERAL)) #define nodeIsSIGIL(node) ((node->type == NODE_SIGIL)) #define nodeIsEMPTY(node) ((node->type == NODE_EMPTY) || ((node->length==0) || (node->contents==NULL))) #define nodeIsMACIECOMMENTHACK(node) (nodeIsBLOCKCOMMENT(node) && nodeEndsWith(node,"\\*/")) #define nodeIsPREFIXSIGIL(node) (nodeIsSIGIL(node) && charIsPrefix(node->contents[0])) #define nodeIsPOSTFIXSIGIL(node) (nodeIsSIGIL(node) && charIsPostfix(node->contents[0])) #define nodeIsCHAR(node,ch) ((node->contents[0]==ch) && (node->length==1)) /* **************************************************************************** * NODE MANIPULATION FUNCTIONS * **************************************************************************** */ /* allocates a new node */ Node* CssAllocNode() { Node* node; Newz(0, node, 1, Node); node->prev = NULL; node->next = NULL; node->contents = NULL; node->length = 0; node->type = NODE_EMPTY; node->can_prune = 1; return node; } /* frees the memory used by a node */ void CssFreeNode(Node* node) { if (node->contents) Safefree(node->contents); Safefree(node); } void CssFreeNodeList(Node* head) { while (head) { Node* tmp = head->next; CssFreeNode(head); head = tmp; } } /* clears the contents of a node */ void CssClearNodeContents(Node* node) { if (node->contents) Safefree(node->contents); node->contents = NULL; node->length = 0; } /* sets the contents of a node */ void CssSetNodeContents(Node* node, const char* string, size_t len) { CssClearNodeContents(node); node->length = len; /* allocate string, fill with NULLs, and copy */ Newz(0, node->contents, (len+1), char); strncpy( node->contents, string, len ); } /* removes the node from the list and discards it entirely */ void CssDiscardNode(Node* node) { if (node->prev) node->prev->next = node->next; if (node->next) node->next->prev = node->prev; CssFreeNode(node); } /* appends the node to the given element */ void CssAppendNode(Node* element, Node* node) { if (element->next) element->next->prev = node; node->next = element->next; node->prev = element; element->next = node; } /* collapses a node to a single whitespace character. If the node contains any * endspace characters, that is what we're collapsed to. */ void CssCollapseNodeToWhitespace(Node* node) { if (node->contents) { char ws = node->contents[0]; size_t idx; for (idx=0; idxlength; idx++) { if (charIsEndspace(node->contents[idx])) { ws = node->contents[idx]; break; } } CssSetNodeContents(node, &ws, 1); } } /* **************************************************************************** * TOKENIZING FUNCTIONS * **************************************************************************** */ /* extracts a quoted literal string */ void _CssExtractLiteral(CssDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; char delimiter = buf[offset]; /* skip start of literal */ offset ++; /* search for end of literal */ while (offset < doc->length) { if (buf[offset] == '\\') { /* escaped character; skip */ offset ++; } else if (buf[offset] == delimiter) { const char* start = buf + doc->offset; size_t length = offset - doc->offset + 1; CssSetNodeContents(node, start, length); node->type = NODE_LITERAL; return; } /* move onto next character */ offset ++; } croak( "unterminated quoted string literal" ); } /* extracts a block comment */ void _CssExtractBlockComment(CssDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; /* skip start of comment */ offset ++; /* skip "/" */ offset ++; /* skip "*" */ /* search for end of comment block */ while (offset < doc->length) { if (buf[offset] == '*') { if (buf[offset+1] == '/') { const char* start = buf + doc->offset; size_t length = offset - doc->offset + 2; CssSetNodeContents(node, start, length); node->type = NODE_BLOCKCOMMENT; return; } } /* move onto next character */ offset ++; } croak( "unterminated block comment" ); } /* extracts a run of whitespace characters */ void _CssExtractWhitespace(CssDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; while ((offset < doc->length) && charIsWhitespace(buf[offset])) offset ++; CssSetNodeContents(node, doc->buffer+doc->offset, offset-doc->offset); node->type = NODE_WHITESPACE; } /* extracts an identifier */ void _CssExtractIdentifier(CssDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; while ((offset < doc->length) && charIsIdentifier(buf[offset])) offset++; CssSetNodeContents(node, doc->buffer+doc->offset, offset-doc->offset); node->type = NODE_IDENTIFIER; } /* extracts a -single- symbol/sigil */ void _CssExtractSigil(CssDoc* doc, Node* node) { CssSetNodeContents(node, doc->buffer+doc->offset, 1); node->type = NODE_SIGIL; } /* tokenizes the given string and returns the list of nodes */ Node* CssTokenizeString(const char* string) { CssDoc doc; /* initialize our CSS document object */ doc.head = NULL; doc.tail = NULL; doc.buffer = string; doc.length = strlen(string); doc.offset = 0; /* parse the CSS */ while ((doc.offset < doc.length) && (doc.buffer[doc.offset])) { /* allocate a new node */ Node* node = CssAllocNode(); if (!doc.head) doc.head = node; if (!doc.tail) doc.tail = node; /* parse the next node out of the CSS */ if ((doc.buffer[doc.offset] == '/') && (doc.buffer[doc.offset+1] == '*')) _CssExtractBlockComment(&doc, node); else if ((doc.buffer[doc.offset] == '"') || (doc.buffer[doc.offset] == '\'')) _CssExtractLiteral(&doc, node); else if (charIsWhitespace(doc.buffer[doc.offset])) _CssExtractWhitespace(&doc, node); else if (charIsIdentifier(doc.buffer[doc.offset])) _CssExtractIdentifier(&doc, node); else _CssExtractSigil(&doc, node); /* move ahead to the end of the parsed node */ doc.offset += node->length; /* add the node to our list of nodes */ if (node != doc.tail) CssAppendNode(doc.tail, node); doc.tail = node; } /* return the node list */ return doc.head; } /* **************************************************************************** * MINIFICATION FUNCTIONS * **************************************************************************** */ /* checks to see if the string represents a "zero unit" */ int CssIsZeroUnit(char* str) { char* ptr = str; int foundZero = 0; /* Find and skip over any leading zero value */ while (*ptr == '0') { /* leading zeros */ foundZero ++; ptr++; } if (*ptr == '.') { /* decimal point */ ptr++; } while (*ptr == '0') { /* following zeros */ foundZero ++; ptr++; } /* If we didn't find a zero, this isn't a Zero Unit */ if (!foundZero) { return 0; } /* If it ends with a known Unit, its a Zero Unit */ if (0 == strcmp(ptr, "em")) { return 1; } if (0 == strcmp(ptr, "ex")) { return 1; } if (0 == strcmp(ptr, "ch")) { return 1; } if (0 == strcmp(ptr, "rem")) { return 1; } if (0 == strcmp(ptr, "vw")) { return 1; } if (0 == strcmp(ptr, "vh")) { return 1; } if (0 == strcmp(ptr, "vmin")) { return 1; } if (0 == strcmp(ptr, "vmax")) { return 1; } if (0 == strcmp(ptr, "cm")) { return 1; } if (0 == strcmp(ptr, "mm")) { return 1; } if (0 == strcmp(ptr, "in")) { return 1; } if (0 == strcmp(ptr, "px")) { return 1; } if (0 == strcmp(ptr, "pt")) { return 1; } if (0 == strcmp(ptr, "pc")) { return 1; } if (0 == strcmp(ptr, "%")) { return 1; } /* Nope, string contains something else; its not a Zero Unit */ return 0; } /* collapses all of the nodes to their shortest possible representation */ void CssCollapseNodes(Node* curr) { int inMacIeCommentHack = 0; while (curr) { Node* next = curr->next; switch (curr->type) { case NODE_WHITESPACE: CssCollapseNodeToWhitespace(curr); break; case NODE_BLOCKCOMMENT: { if (!inMacIeCommentHack && nodeIsMACIECOMMENTHACK(curr)) { /* START of mac/ie hack */ CssSetNodeContents(curr, "/*\\*/", 5); curr->can_prune = 0; inMacIeCommentHack = 1; } else if (inMacIeCommentHack && !nodeIsMACIECOMMENTHACK(curr)) { /* END of mac/ie hack */ CssSetNodeContents(curr, "/**/", 4); curr->can_prune = 0; inMacIeCommentHack = 0; } } break; case NODE_IDENTIFIER: if (CssIsZeroUnit(curr->contents)) { CssSetNodeContents(curr, "0", 1); } default: break; } curr = next; } } /* checks to see whether we can prune the given node from the list. * * THIS is the function that controls the bulk of the minification process. */ enum { PRUNE_NO, PRUNE_PREVIOUS, PRUNE_CURRENT, PRUNE_NEXT }; int CssCanPrune(Node* node) { Node* prev = node->prev; Node* next = node->next; /* only if node is prunable */ if (!node->can_prune) return PRUNE_NO; switch (node->type) { case NODE_EMPTY: /* prune empty nodes */ return PRUNE_CURRENT; case NODE_WHITESPACE: /* remove whitespace before comment blocks */ if (next && nodeIsBLOCKCOMMENT(next)) return PRUNE_CURRENT; /* remove whitespace after comment blocks */ if (prev && nodeIsBLOCKCOMMENT(prev)) return PRUNE_CURRENT; /* leading whitespace gets pruned */ if (!prev) return PRUNE_CURRENT; /* trailing whitespace gets pruned */ if (!next) return PRUNE_CURRENT; /* keep all other whitespace */ return PRUNE_NO; case NODE_BLOCKCOMMENT: /* keep comments that contain the word "copyright" */ if (nodeContains(node,"copyright")) return PRUNE_NO; /* remove comment blocks */ return PRUNE_CURRENT; case NODE_IDENTIFIER: /* keep all identifiers */ return PRUNE_NO; case NODE_LITERAL: /* keep all literals */ return PRUNE_NO; case NODE_SIGIL: /* remove whitespace after "prefix" sigils */ if (nodeIsPREFIXSIGIL(node) && next && nodeIsWHITESPACE(next)) return PRUNE_NEXT; /* remove whitespace before "postfix" sigils */ if (nodeIsPOSTFIXSIGIL(node) && prev && nodeIsWHITESPACE(prev)) return PRUNE_PREVIOUS; /* remove ";" characters at end of selector groups */ if (nodeIsCHAR(node,';') && next && nodeIsSIGIL(next) && nodeIsCHAR(next,'}')) return PRUNE_CURRENT; /* keep all other sigils */ return PRUNE_NO; } /* keep anything else */ return PRUNE_NO; } /* prune nodes from the list */ Node* CssPruneNodes(Node *head) { Node* curr = head; while (curr) { /* see if/how we can prune this node */ int prune = CssCanPrune(curr); /* prune. each block is responsible for moving onto the next node */ Node* prev = curr->prev; Node* next = curr->next; switch (prune) { case PRUNE_PREVIOUS: /* discard previous node */ CssDiscardNode(prev); /* reset "head" if that's what got pruned */ if (prev == head) head = curr; break; case PRUNE_CURRENT: /* discard current node */ CssDiscardNode(curr); /* reset "head" if that's what got pruned */ if (curr == head) head = prev ? prev : next; /* backup and try again if possible */ curr = prev ? prev : next; break; case PRUNE_NEXT: /* discard next node */ CssDiscardNode(next); /* stay on current node, and try again */ break; default: /* move ahead to next node */ curr = next; break; } } /* return the (possibly new) head node back to the caller */ return head; } /* **************************************************************************** * Minifies the given CSS, returning a newly allocated string back to the * caller (YOU'RE responsible for freeing its memory). * **************************************************************************** */ char* CssMinify(const char* string) { char* results; /* PASS 1: tokenize CSS into a list of nodes */ Node* head = CssTokenizeString(string); if (!head) return NULL; /* PASS 2: collapse nodes */ CssCollapseNodes(head); /* PASS 3: prune nodes */ head = CssPruneNodes(head); if (!head) return NULL; /* PASS 4: re-assemble CSS into single string */ { Node* curr; char* ptr; /* allocate the result buffer to the same size as the original CSS; in * a worst case scenario that's how much memory we'll need for it. */ Newz(0, results, (strlen(string)+1), char); ptr = results; /* copy node contents into result buffer */ curr = head; while (curr) { memcpy(ptr, curr->contents, curr->length); ptr += curr->length; curr = curr->next; } *ptr = 0; } /* free memory used by node list */ CssFreeNodeList(head); /* return resulting minified CSS back to caller */ return results; } MODULE = CSS::Minifier::XS PACKAGE = CSS::Minifier::XS PROTOTYPES: disable SV* minify(string) SV* string INIT: char* buffer = NULL; RETVAL = &PL_sv_undef; CODE: /* minify the CSS */ buffer = CssMinify( SvPVX(string) ); /* hand back the minified CSS (if we had any) */ if (buffer != NULL) { RETVAL = newSVpv(buffer, 0); Safefree( buffer ); } OUTPUT: RETVAL CSS-Minifier-XS-0.09/lib000755001750001750 012234533345 14615 5ustar00grahamgraham000000000000CSS-Minifier-XS-0.09/lib/CSS000755001750001750 012234533345 15245 5ustar00grahamgraham000000000000CSS-Minifier-XS-0.09/lib/CSS/Minifier000755001750001750 012234533345 17007 5ustar00grahamgraham000000000000CSS-Minifier-XS-0.09/lib/CSS/Minifier/XS.pm000444001750001750 732212234533345 20040 0ustar00grahamgraham000000000000package CSS::Minifier::XS; use strict; use warnings; require Exporter; require DynaLoader; our @ISA = qw(Exporter DynaLoader); our @EXPORT_OK = qw(minify); our $VERSION = '0.09'; bootstrap CSS::Minifier::XS $VERSION; 1; =head1 NAME CSS::Minifier::XS - XS based CSS minifier =head1 SYNOPSIS use CSS::Minifier::XS qw(minify); $minified = minify($css); =head1 DESCRIPTION C is a CSS "minifier"; its designed to remove un-necessary whitespace and comments from CSS files, while also B breaking the CSS. C is similar in function to C, but is substantially faster as its written in XS and not just pure Perl. =head1 METHODS =over =item minify($css) Minifies the given C<$css>, returning the minified CSS back to the caller. =back =head1 HOW IT WORKS C minifies the CSS by removing un-necessary whitespace from CSS documents. Comment blocks are also removed, I when (a) they contain the word "copyright" in them, or (b) they're needed to implement the "Mac/IE Comment Hack". Internally, the minification is done by taking multiple passes through the CSS document: =head2 Pass 1: Tokenize First, we go through and parse the CSS document into a series of tokens internally. The tokenizing process B check to make sure that you've got syntactically valid CSS, it just breaks up the text into a stream of tokens suitable for processing by the subsequent stages. =head2 Pass 2: Collapse We then march through the token list and collapse certain tokens down to their smallest possible representation. I they're still included in the final results we only want to include them at their shortest. =over =item Whitespace Runs of multiple whitespace characters are reduced down to a single whitespace character. If the whitespace contains any "end of line" (EOL) characters, then the end result is the I EOL character encountered. Otherwise, the result is the first whitespace character in the run. =item Comments Comments implementing the "Mac/IE Comment Hack" are collapsed down to the smallest possible comment that would still implement the hack ("/*\*/" to start the hack, and "/**/" to end it). =item Zero Units Zero Units (e.g. "0px") are reduced down to just "0", as the CSS specification indicates that the unit is not required when its a zero value. =back =head2 Pass 3: Pruning We then go back through the token list and prune and remove un-necessary tokens. =over =item Whitespace Wherever possible, whitespace is removed; before+after comment blocks, and before+after various symbols/sigils. =item Comments Comments that either (a) are needed to implement the "Mac/IE Comment Hack", or that (b) contain the word "copyright" in them are preserved. B other comments are removed. =item Symbols/Sigils Semi-colons that are immediately followed by a closing brace (e.g. ";}") are removed; semi-colons are needed to separate multiple declarations, but aren't required at the end of a group. =item Everything else We keep everything else; identifiers, quoted literal strings, symbols/sigils, etc. =back =head2 Pass 4: Re-assembly Lastly, we go back through the token list and re-assemble it all back into a single CSS string, which is then returned back to the caller. =head1 AUTHOR Graham TerMarsch (cpan@howlingfrog.com) =head1 REPORTING BUGS Please report bugs via RT (L), and be sure to include the CSS that you're having troubles minifying. =head1 COPYRIGHT Copyright (C) 2007-, Graham TerMarsch. All Rights Reserved. This is free software; you can redistribute it and/or modify it under the same license as Perl itself. =head1 SEE ALSO C. =cut CSS-Minifier-XS-0.09/t000755001750001750 012234533345 14312 5ustar00grahamgraham000000000000CSS-Minifier-XS-0.09/t/01-loads.t000444001750001750 12212234533345 16127 0ustar00grahamgraham000000000000use strict; use Test::More tests=>1; BEGIN { use_ok( 'CSS::Minifier::XS' ); } CSS-Minifier-XS-0.09/t/pod.t000444001750001750 20112234533345 15367 0ustar00grahamgraham000000000000use Test::More; eval "use Test::Pod 1.00"; plan skip_all => "Test::Pod 1.00 required for testing POD" if $@; all_pod_files_ok(); CSS-Minifier-XS-0.09/t/02-minify.t000444001750001750 202512234533345 16345 0ustar00grahamgraham000000000000use strict; use warnings; use IO::File; use Test::More; use CSS::Minifier::XS qw(minify); ############################################################################### # figure out how many CSS files we're going to run through for testing my @files = ; plan tests => scalar @files; ############################################################################### # test each of the CSS files in turn foreach my $file (@files) { (my $min_file = $file) =~ s/\.css$/\.min/; my $str = slurp( $file ); my $min = slurp( $min_file ); my $res = minify( $str ); is( $res, $min, $file ); } ############################################################################### # HELPER METHOD: slurp in contents of file to scalar. ############################################################################### sub slurp { my $filename = shift; my $fin = IO::File->new( $filename, '<' ) || die "can't open '$filename'; $!"; my $str = join('', <$fin>); $fin->close(); chomp( $str ); return $str; } CSS-Minifier-XS-0.09/t/pod-coverage.t000444001750001750 23012234533345 17162 0ustar00grahamgraham000000000000use Test::More; eval "use Test::Pod::Coverage 1.00"; plan skip_all => "Test::Pod::Coverage 1.00 required for testing POD" if $@; all_pod_coverage_ok(); CSS-Minifier-XS-0.09/t/03-minifies-to-nothing.t000444001750001750 106412234533345 20744 0ustar00grahamgraham000000000000use strict; use warnings; use Test::More tests => 2; use CSS::Minifier::XS qw(minify); my $results; ############################################################################### # RT #36557: Nasty segfault on minifying comment-only css input # # Actually turns out to be that *anything* that minifies to "nothing" causes # a segfault in Perl-5.8. Perl-5.10 seems immune. $results = minify( q{/* */} ); ok( !defined $results, "minified single block comment to nothing" ); $results = minify( q{} ); ok( !defined $results, "minified empty string to nothing" ); CSS-Minifier-XS-0.09/t/99-benchmark.t000444001750001750 364412234533345 17034 0ustar00grahamgraham000000000000use strict; use warnings; use Test::More; use IO::File; use Benchmark qw(countit); use CSS::Minifier::XS; ############################################################################### # check if CSS::Minifier available, so we can do a benchmark comparison eval { require CSS::Minifier }; if ($@) { plan skip_all => 'CSS::Minifier not available for benchmark comparison'; } plan tests => 1; ############################################################################### # get the list of CSS files we're going to run through for testing my @files = ; ############################################################################### # time test the PurePerl version against the XS version. compare_benchmark: { my $count; my $time = 10; # build a longer CSS document to process; 64KBytes should be suitable my $str = ''; while (1) { foreach my $file (@files) { $str .= slurp( $file ); } last if (length($str) > (64*1024)); } # benchmark the original "pure perl" version $count = countit( $time, sub { CSS::Minifier::minify(input=>$str) } ); my $rate_pp = ($count->iters() / $time) * length($str); # benchmark the "XS" version $count = countit( $time, sub { CSS::Minifier::XS::minify($str) } ); my $rate_xs = ($count->iters() / $time) * length($str); ok( 1, "benchmarking" ); diag( "" ); diag( "Benchmark results:" ); diag( "\tperl\t=> $rate_pp bytes/sec" ); diag( "\txs\t=> $rate_xs bytes/sec" ); } ############################################################################### # HELPER METHOD: slurp in contents of file to scalar. ############################################################################### sub slurp { my $filename = shift; my $fin = IO::File->new( $filename, '<' ) || die "can't open '$filename'; $!"; my $str = join('', <$fin>); $fin->close(); chomp( $str ); return $str; } CSS-Minifier-XS-0.09/t/css000755001750001750 012234533345 15102 5ustar00grahamgraham000000000000CSS-Minifier-XS-0.09/t/css/zero-units-zerooo.css000444001750001750 40012234533345 21355 0ustar00grahamgraham000000000000/* "zeroooooo" can be expressed without a unit */ p.in { width: 000in } p.cm { width: 000cm } p.mm { width: 000mm } p.pt { width: 000pt } p.pc { width: 000pc } p.px { width: 000px } p.em { width: 000em } p.ex { width: 000ex } p.pct { width: 000% } CSS-Minifier-XS-0.09/t/css/zero-units-zero-point-zero.min000444001750001750 16712234533345 23130 0ustar00grahamgraham000000000000p.in{width:0}p.cm{width:0}p.mm{width:0}p.pt{width:0}p.pc{width:0}p.px{width:0}p.em{width:0}p.ex{width:0}p.pct{width:0} CSS-Minifier-XS-0.09/t/css/comments-mac-ie-hack.min000444001750001750 3312234533345 21562 0ustar00grahamgraham000000000000/*\*/ul{font-size:80%}/**/ CSS-Minifier-XS-0.09/t/css/combinators.css000444001750001750 16512234533345 20253 0ustar00grahamgraham000000000000/* CSS Selector Combinators */ h1 > p { background: green } h1 + p { background: yellow } h1 ~ p { background: red } CSS-Minifier-XS-0.09/t/css/trailing-whitespace.css000444001750001750 7212234533345 21653 0ustar00grahamgraham000000000000/* trailing whitespace gets removed */ trailing{} CSS-Minifier-XS-0.09/t/css/calc-operators.min000444001750001750 16712234533345 20646 0ustar00grahamgraham000000000000.plus{width:calc(100px + 10%)}.minus{width:calc(100% - 30px)}.times{width:calc(60px * 2)}.divide{width:calc(100% / 2)} CSS-Minifier-XS-0.09/t/css/units-preserved.min000444001750001750 25212234533345 21062 0ustar00grahamgraham000000000000p.in{width:1.0in}p.cm{width:1.0cm}p.mm{width:1.0mm}p.pt{width:1.0pt}p.pc{width:1.0pc}p.px{width:1.0px}p.em{width:1.0em}p.ex{width:1.0ex}p.pct{width:1.0%}p.pct{width:10%} CSS-Minifier-XS-0.09/t/css/retain-whitespace-after-close-parenthesis.min000444001750001750 17012234533345 26063 0ustar00grahamgraham000000000000.retained{background:url(foo.gif) no-repeat}.minimized{background:url(foo.gif)}.also-minimized{background:url(foo.gif)} CSS-Minifier-XS-0.09/t/css/zero-units-zerooo.min000444001750001750 16712234533345 21362 0ustar00grahamgraham000000000000p.in{width:0}p.cm{width:0}p.mm{width:0}p.pt{width:0}p.pc{width:0}p.px{width:0}p.em{width:0}p.ex{width:0}p.pct{width:0} CSS-Minifier-XS-0.09/t/css/zero-units-point-zero.css000444001750001750 37012234533345 22154 0ustar00grahamgraham000000000000/* "point zero" can be expressed without a unit */ p.in { width: .0in } p.cm { width: .0cm } p.mm { width: .0mm } p.pt { width: .0pt } p.pc { width: .0pc } p.px { width: .0px } p.em { width: .0em } p.ex { width: .0ex } p.pct { width: .0% } CSS-Minifier-XS-0.09/t/css/retain-whitespace-after-close-parenthesis.css000444001750001750 33712234533345 26075 0ustar00grahamgraham000000000000/* we retain whitespace after a ')', -unless- its at the end of a rule */ .retained { background: url(foo.gif) no-repeat; } .minimized { background: url(foo.gif) ; } .also-minimized { background: url(foo.gif) } CSS-Minifier-XS-0.09/t/css/zero-units-zero-point-zero.css000444001750001750 40612234533345 23131 0ustar00grahamgraham000000000000/* "zero point zero" can be expressed without a unit */ p.in { width: 0.0in } p.cm { width: 0.0cm } p.mm { width: 0.0mm } p.pt { width: 0.0pt } p.pc { width: 0.0pc } p.px { width: 0.0px } p.em { width: 0.0em } p.ex { width: 0.0ex } p.pct { width: 0.0% } CSS-Minifier-XS-0.09/t/css/styles.min000444001750001750 34012234533345 17244 0ustar00grahamgraham000000000000@import url(more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid yellow}div#content h1 + p{padding-top:0;margin-top:0} CSS-Minifier-XS-0.09/t/css/zero-units-point-zero.min000444001750001750 16712234533345 22153 0ustar00grahamgraham000000000000p.in{width:0}p.cm{width:0}p.mm{width:0}p.pt{width:0}p.pc{width:0}p.px{width:0}p.em{width:0}p.ex{width:0}p.pct{width:0} CSS-Minifier-XS-0.09/t/css/styles.css000444001750001750 54312234533345 17256 0ustar00grahamgraham000000000000/* some CSS to try to exercise things in general */ @import url( more.css ); body, td, th { font-family: Verdana, "Bitstream Vera Sans", sans-serif; font-size : 12px; } .nav { margin-left: 20%; } #main-nav { background-color: red; border: 1px solid yellow; } div#content h1 + p { padding-top: 0; margin-top: 0; } CSS-Minifier-XS-0.09/t/css/calc-operators.css000444001750001750 22612234533345 20647 0ustar00grahamgraham000000000000.plus { width: calc( 100px + 10% ) } .minus { width: calc( 100% - 30px ) } .times { width: calc( 60px * 2 ) } .divide { width: calc( 100% / 2 ) } CSS-Minifier-XS-0.09/t/css/comments.css000444001750001750 21712234533345 17556 0ustar00grahamgraham000000000000/* block comments get removed */ /* comments containing the word "copyright" are left in, though */ /* but all other comments are removed */ CSS-Minifier-XS-0.09/t/css/trailing-whitespace.min000444001750001750 1312234533345 21641 0ustar00grahamgraham000000000000trailing{} CSS-Minifier-XS-0.09/t/css/trailing-semicolon.css000444001750001750 13312234533345 21525 0ustar00grahamgraham000000000000/* trailing semi-colons at the end of a group are removed */ h1 { font-weight: bold; } CSS-Minifier-XS-0.09/t/css/leading-whitespace.min000444001750001750 1212234533345 21432 0ustar00grahamgraham000000000000leading{} CSS-Minifier-XS-0.09/t/css/zero-units-zero.css000444001750001750 35112234533345 21024 0ustar00grahamgraham000000000000/* "zero" can be expressed without a unit */ p.in { width: 0in } p.cm { width: 0cm } p.mm { width: 0mm } p.pt { width: 0pt } p.pc { width: 0pc } p.px { width: 0px } p.em { width: 0em } p.ex { width: 0ex } p.pct { width: 0% } CSS-Minifier-XS-0.09/t/css/units-preserved.css000444001750001750 46212234533345 21072 0ustar00grahamgraham000000000000/* "something point zero" requires a unit */ p.in { width: 1.0in } p.cm { width: 1.0cm } p.mm { width: 1.0mm } p.pt { width: 1.0pt } p.pc { width: 1.0pc } p.px { width: 1.0px } p.em { width: 1.0em } p.ex { width: 1.0ex } p.pct { width: 1.0% } /* percentages require a unit */ p.pct { width: 10% } CSS-Minifier-XS-0.09/t/css/comments-mac-ie-hack.css000444001750001750 30312234533345 21607 0ustar00grahamgraham000000000000/* comments get removed */ /* except those implementing the "Mac/IE Comment Hack" \*/ ul { /* unless we're already "in" the hack \*/ font-size: 80%; } /* and those that close the hack */ CSS-Minifier-XS-0.09/t/css/trailing-semicolon.min000444001750001750 2512234533345 21500 0ustar00grahamgraham000000000000h1{font-weight:bold} CSS-Minifier-XS-0.09/t/css/leading-whitespace.css000444001750001750 7012234533345 21443 0ustar00grahamgraham000000000000 /* leading whitespace gets removed */ leading{} CSS-Minifier-XS-0.09/t/css/media.css000444001750001750 21512234533345 17006 0ustar00grahamgraham000000000000/* CSS with media selector that requires '(' to have leading WS */ @media all and (max-width:100px) { div { margin: 0px; } } CSS-Minifier-XS-0.09/t/css/zero-units-zero.min000444001750001750 16712234533345 21024 0ustar00grahamgraham000000000000p.in{width:0}p.cm{width:0}p.mm{width:0}p.pt{width:0}p.pc{width:0}p.px{width:0}p.em{width:0}p.ex{width:0}p.pct{width:0} CSS-Minifier-XS-0.09/t/css/media.min000444001750001750 6012234533345 16757 0ustar00grahamgraham000000000000@media all and (max-width:100px){div{margin:0}} CSS-Minifier-XS-0.09/t/css/comments.min000444001750001750 10312234533345 17543 0ustar00grahamgraham000000000000/* comments containing the word "copyright" are left in, though */ CSS-Minifier-XS-0.09/t/css/combinators.min000444001750001750 10412234533345 20237 0ustar00grahamgraham000000000000h1>p{background:green}h1 + p{background:yellow}h1~p{background:red}