JavaScript-Minifier-XS-0.11000755001750001750 012462031340 15445 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/Build.PL000444001750001750 111612462031340 17075 0ustar00grahamgraham000000000000use strict; use warnings; use Module::Build; Module::Build->new( 'module_name' => 'JavaScript::Minifier::XS', 'license' => 'perl', 'dist_author' => 'Graham TerMarsch (cpan@howlingfrog.com)', 'create_makefile_pl'=> 'traditional', 'xs_files' => { 'XS.xs' => 'lib/JavaScript/Minifier/XS.xs', }, 'requires' => { 'perl' => '5.6.0', }, 'build_requires' => { 'Test::More' => 0, 'ExtUtils::CBuilder' => 0, }, )->create_build_script(); JavaScript-Minifier-XS-0.11/Changes000444001750001750 526512462031340 17105 0ustar00grahamgraham000000000000Revision history for Perl extension JavaScript::Minifier::XS. 0.11 Tue Jan 27 16:50 PST 2015 - RT #58416; don't segfault when trying to minify non-javascript 0.10 Mon Jan 26 22:46 PST 2015 - RT #64948; allow builds on older versions of Perl. Thanks to Michael Robinton and Ruslan Zakirov. - Lowered minimum Perl version to v5.6.0, as detected by Perl::MinimumVersion. - RT #51008; allow for minification of JS code that returns regexps from functions. Thanks to James Barton, Robert Krimen, and Randy Stauner. - Added "xt/test-compile.t" to test minification against a wider range of JS. While not a 100% guarantee that the JS still works, it does provide a wider range of JS to test against. 0.09 Tue Nov 2 22:12 PDT 2010 - Bump required Perl version to 5.8.8; oldest release w/Newxz() available. 0.08 Wed Jul 21 21:23 PDT 2010 - use Newxz/Safefree for memory management, instead of malloc/free. Thanks to Kenichi Ishigaki for his patch to CSS-Minifier-XS that prompted this. 0.07 Fri Apr 23 23:44 PDT 2010 - switch to Git 0.06 Thu Aug 6, 22:08 PDT 2009 - fix invalid "L" POD sequences 0.05 Wed Jul 16, 23:35 PDT 2008 - don't segfault w/older Perls if we minify right down to nothing. (similar behaviour as described for CSS::Minifier::XS in RT #36557) 0.04 Wed May 28, 21:58 PDT 2008 - rebuild packages; wrong version number in META.yml 0.03 Wed May 28, 14:46 PDT 2008 - fix minification when a regexp follows a comment that ends with something that looks like code; was treating it as division instead of as a literal - properly clear end of internally allocated buffers - added some debugging output, which could be enabled at compile-time 0.02 Tue May 6 00:16 PDT 2008 - rebuild packages; EU::MM borked my META.yml 0.01 Mon May 5 15:11 PDT 2008 - fix minification of "division of an array subscript". Thanks to Ingy and Dan at Socialtext for the JS! - first NON-devel release 0.01_05 Sat Oct 20 22:48 PDT 2007 - don't use "strcasestr()"; not available on Solaris 0.01_04 Wed Oct 17 15:56 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:47 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:22 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 JavaScript-Minifier-XS-0.11/Makefile.PL000444001750001750 64612462031340 17542 0ustar00grahamgraham000000000000# Note: this file was auto-generated by Module::Build::Compat version 0.4206 require 5.006000; use ExtUtils::MakeMaker; WriteMakefile ( 'NAME' => 'JavaScript::Minifier::XS', 'VERSION_FROM' => 'lib/JavaScript/Minifier/XS.pm', 'PREREQ_PM' => { 'ExtUtils::CBuilder' => 0, 'Test::More' => 0 }, 'INSTALLDIRS' => 'site', 'EXE_FILES' => [], 'PL_FILES' => {} ) ; JavaScript-Minifier-XS-0.11/MANIFEST.SKIP000444001750001750 13612462031340 17460 0ustar00grahamgraham000000000000Build _build .git/ .gitignore \..*\.swp .prove .travis.yml blib/ ^MYMETA\.yml$ ^MYMETA\.json$ JavaScript-Minifier-XS-0.11/README000444001750001750 62012462031340 16440 0ustar00grahamgraham000000000000JavaScript::Minifier::XS minifies JavaScript documents by removing un-necessary whitespace Copyright (C) 2007, 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 JavaScript-Minifier-XS-0.11/MANIFEST000444001750001750 205712462031340 16737 0ustar00grahamgraham000000000000Build.PL Makefile.PL Changes MANIFEST MANIFEST.SKIP META.yml META.json README XS.xs lib/JavaScript/Minifier/XS.pm t/01-loads.t t/02-minify.t t/03-minifies-to-nothing.t t/04-not-javascript.t t/js/comments.js t/js/comments.min t/js/comments-before-a-regex.js t/js/comments-before-a-regex.min t/js/comments-ie-conditional.js t/js/comments-ie-conditional.min t/js/division.js t/js/division.min t/js/division-of-array-subscripts.js t/js/division-of-array-subscripts.min t/js/leading-whitespace.js t/js/leading-whitespace.min t/js/literals-double-quotes.js t/js/literals-double-quotes.min t/js/literals-regexp.js t/js/literals-regexp.min t/js/literals-single-quotes.js t/js/literals-single-quotes.min t/js/postfix-sigil.js t/js/postfix-sigil.min t/js/prefix-sigil.js t/js/prefix-sigil.min t/js/regexp-not-line-comment.js t/js/regexp-not-line-comment.min t/js/return-regex.js t/js/return-regex.min t/js/simple.js t/js/simple.min t/js/trailing-whitespace.js t/js/trailing-whitespace.min xt/benchmark.t xt/leaks.t xt/minimum-version.t xt/pod.t xt/pod-coverage.t xt/test-compile.t JavaScript-Minifier-XS-0.11/META.yml000444001750001750 116712462031340 17060 0ustar00grahamgraham000000000000--- abstract: 'XS based JavaScript minifier' author: - 'Graham TerMarsch (cpan@howlingfrog.com)' build_requires: ExtUtils::CBuilder: '0' Test::More: '0' configure_requires: Module::Build: '0.42' dynamic_config: 1 generated_by: 'Module::Build version 0.4206, CPAN::Meta::Converter version 2.142060' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: JavaScript-Minifier-XS provides: JavaScript::Minifier::XS: file: lib/JavaScript/Minifier/XS.pm version: '0.11' requires: perl: v5.6.0 resources: license: http://dev.perl.org/licenses/ version: '0.11' JavaScript-Minifier-XS-0.11/META.json000444001750001750 201312462031340 17217 0ustar00grahamgraham000000000000{ "abstract" : "XS based JavaScript minifier", "author" : [ "Graham TerMarsch (cpan@howlingfrog.com)" ], "dynamic_config" : 1, "generated_by" : "Module::Build version 0.4206", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : "2" }, "name" : "JavaScript-Minifier-XS", "prereqs" : { "build" : { "requires" : { "ExtUtils::CBuilder" : "0", "Test::More" : "0" } }, "configure" : { "requires" : { "Module::Build" : "0.42" } }, "runtime" : { "requires" : { "perl" : "v5.6.0" } } }, "provides" : { "JavaScript::Minifier::XS" : { "file" : "lib/JavaScript/Minifier/XS.pm", "version" : "0.11" } }, "release_status" : "stable", "resources" : { "license" : [ "http://dev.perl.org/licenses/" ] }, "version" : "0.11" } JavaScript-Minifier-XS-0.11/XS.xs000444001750001750 6156412462031340 16544 0ustar00grahamgraham000000000000#include #include #include #include #include #include #include /* uncomment to enable debugging output */ /* #define DEBUG 1 */ /* **************************************************************************** * 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 > 126) return 1; return 0; } int charIsInfix(char ch) { /* EOL characters 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; if (ch == '<') return 1; if (ch == '>') return 1; if (ch == '?') return 1; if (ch == '|') return 1; if (ch == '\n') return 1; return 0; } int charIsPrefix(char ch) { /* EOL characters after these characters can be removed */ if (ch == '{') return 1; if (ch == '(') return 1; if (ch == '[') return 1; if (ch == '!') return 1; return charIsInfix(ch); } int charIsPostfix(char ch) { /* EOL characters before these characters can be removed */ if (ch == '}') return 1; if (ch == ')') return 1; if (ch == ']') return 1; return charIsInfix(ch); } /* **************************************************************************** * TYPE DEFINITIONS * **************************************************************************** */ typedef enum { NODE_EMPTY, NODE_WHITESPACE, NODE_BLOCKCOMMENT, NODE_LINECOMMENT, NODE_IDENTIFIER, NODE_LITERAL, NODE_SIGIL } NodeType; static char* strNodeTypes[] = { "empty", "whitespace", "block comment", "line comment", "identifier", "literal", "sigil" }; struct _Node; typedef struct _Node Node; struct _Node { /* linked list pointers */ Node* prev; Node* next; /* node internals */ char* contents; size_t length; NodeType type; }; typedef struct { /* linked list pointers */ Node* head; Node* tail; /* doc internals */ const char* buffer; size_t length; size_t offset; } JsDoc; /* **************************************************************************** * 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 INSENSITIVELY */ 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 nodeIsLINECOMMENT(node) ((node->type == NODE_LINECOMMENT)) #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 nodeIsCOMMENT(node) (nodeIsBLOCKCOMMENT(node) || nodeIsLINECOMMENT(node)) #define nodeIsIECONDITIONALBLOCKCOMMENT(node) (nodeIsBLOCKCOMMENT(node) && nodeBeginsWith(node,"/*@") && nodeEndsWith(node,"@*/")) #define nodeIsIECONDITIONALLINECOMMENT(node) (nodeIsLINECOMMENT(node) && nodeBeginsWith(node,"//@")) #define nodeIsIECONDITIONALCOMMENT(node) (nodeIsIECONDITIONALBLOCKCOMMENT(node) || nodeIsIECONDITIONALLINECOMMENT(node)) #define nodeIsPREFIXSIGIL(node) (nodeIsSIGIL(node) && charIsPrefix(node->contents[0])) #define nodeIsPOSTFIXSIGIL(node) (nodeIsSIGIL(node) && charIsPostfix(node->contents[0])) #define nodeIsENDSPACE(node) (nodeIsWHITESPACE(node) && charIsEndspace(node->contents[0])) #define nodeIsCHAR(node,ch) ((node->contents[0]==ch) && (node->length==1)) /* **************************************************************************** * NODE MANIPULATION FUNCTIONS * **************************************************************************** */ /* allocates a new node */ Node* JsAllocNode() { Node* node; Newz(0, node, 1, Node); node->prev = NULL; node->next = NULL; node->contents = NULL; node->length = 0; node->type = NODE_EMPTY; return node; } /* frees the memory used by a node */ void JsFreeNode(Node* node) { if (node->contents) Safefree(node->contents); Safefree(node); } void JsFreeNodeList(Node* head) { while (head) { Node* tmp = head->next; JsFreeNode(head); head = tmp; } } /* clears the contents of a node */ void JsClearNodeContents(Node* node) { if (node->contents) Safefree(node->contents); node->contents = NULL; node->length = 0; } /* sets the contents of a node */ void JsSetNodeContents(Node* node, const char* string, size_t len) { size_t bufSize = len + 1; /* clear node, set new length */ JsClearNodeContents(node); node->length = len; /* allocate string, fill with NULLs, and copy */ Newz(0, node->contents, bufSize, char); strncpy( node->contents, string, len ); } /* removes the node from the list and discards it entirely */ void JsDiscardNode(Node* node) { if (node->prev) node->prev->next = node->next; if (node->next) node->next->prev = node->prev; JsFreeNode(node); } /* appends the node to the given element */ void JsAppendNode(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 JsCollapseNodeToWhitespace(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; } } JsSetNodeContents(node, &ws, 1); } } /* collapses a node to a single endspace character. If the node doesn't * contain any endspace characters, the node is collapsed to an empty string. */ void JsCollapseNodeToEndspace(Node* node) { if (node->contents) { char ws = 0; size_t idx; for (idx=0; idxlength; idx++) { if (charIsEndspace(node->contents[idx])) { ws = node->contents[idx]; break; } } JsClearNodeContents(node); if (ws) JsSetNodeContents(node, &ws, 1); } } /* **************************************************************************** * TOKENIZING FUNCTIONS * **************************************************************************** */ /* extracts a quoted literal string */ void _JsExtractLiteral(JsDoc* 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; JsSetNodeContents(node, start, length); node->type = NODE_LITERAL; return; } /* move onto next character */ offset ++; } croak( "unterminated quoted string literal" ); } /* extracts a block comment */ void _JsExtractBlockComment(JsDoc* 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; JsSetNodeContents(node, start, length); node->type = NODE_BLOCKCOMMENT; return; } } /* move onto next character */ offset ++; } croak( "unterminated block comment" ); } /* extracts a line comment */ void _JsExtractLineComment(JsDoc* 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 line */ while ((offset < doc->length) && !charIsEndspace(buf[offset])) offset ++; /* found it ! */ { const char* start = buf + doc->offset; size_t length = offset - doc->offset; JsSetNodeContents(node, start, length); node->type = NODE_LINECOMMENT; } } /* extracts a run of whitespace characters */ void _JsExtractWhitespace(JsDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; while ((offset < doc->length) && charIsWhitespace(buf[offset])) offset ++; JsSetNodeContents(node, doc->buffer+doc->offset, offset-doc->offset); node->type = NODE_WHITESPACE; } /* extracts an identifier */ void _JsExtractIdentifier(JsDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; while ((offset < doc->length) && charIsIdentifier(buf[offset])) offset ++; JsSetNodeContents(node, doc->buffer+doc->offset, offset-doc->offset); node->type = NODE_IDENTIFIER; } /* extracts a -single- symbol/sigil */ void _JsExtractSigil(JsDoc* doc, Node* node) { JsSetNodeContents(node, doc->buffer+doc->offset, 1); node->type = NODE_SIGIL; } /* tokenizes the given string and returns the list of nodes */ Node* JsTokenizeString(const char* string) { JsDoc doc; /* initialize our JS document object */ doc.head = NULL; doc.tail = NULL; doc.buffer = string; doc.length = strlen(string); doc.offset = 0; /* parse the JS */ while ((doc.offset < doc.length) && (doc.buffer[doc.offset])) { /* allocate a new node */ Node* node = JsAllocNode(); if (!doc.head) doc.head = node; if (!doc.tail) doc.tail = node; /* parse the next node out of the JS */ if (doc.buffer[doc.offset] == '/') { if (doc.buffer[doc.offset+1] == '*') _JsExtractBlockComment(&doc, node); else if (doc.buffer[doc.offset+1] == '/') _JsExtractLineComment(&doc, node); else { /* could be "division" or "regexp", but need to know more about * our context... */ Node* last = doc.tail; char ch = 0; /* find last non-whitespace, non-comment node */ while (nodeIsWHITESPACE(last) || nodeIsCOMMENT(last)) last = last->prev; ch = last->contents[last->length-1]; /* see if we're "division" or "regexp" */ if (nodeIsIDENTIFIER(last) && nodeEquals(last, "return")) { /* returning a regexp from a function */ _JsExtractLiteral(&doc, node); } else if (ch && ((ch == ')') || (ch == '.') || (ch == ']') || (charIsIdentifier(ch)))) { /* looks like an identifier; guess its division */ _JsExtractSigil(&doc, node); } else { /* presume its a regexp */ _JsExtractLiteral(&doc, node); } } } else if ((doc.buffer[doc.offset] == '"') || (doc.buffer[doc.offset] == '\'')) _JsExtractLiteral(&doc, node); else if (charIsWhitespace(doc.buffer[doc.offset])) _JsExtractWhitespace(&doc, node); else if (charIsIdentifier(doc.buffer[doc.offset])) _JsExtractIdentifier(&doc, node); else _JsExtractSigil(&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) JsAppendNode(doc.tail, node); doc.tail = node; /* some debugging info */ #ifdef DEBUG { int idx; printf("----------------------------------------------------------------\n"); printf("%s: %s\n", strNodeTypes[node->type], node->contents); printf("next: '"); for (idx=0; idx<=10; idx++) { if ((doc.offset+idx) >= doc.length) break; if (!doc.buffer[doc.offset+idx]) break; printf("%c", doc.buffer[doc.offset+idx]); } printf("'\n"); } #endif } /* return the node list */ return doc.head; } /* **************************************************************************** * MINIFICATION FUNCTIONS * **************************************************************************** */ /* collapses all of the nodes to their shortest possible representation */ void JsCollapseNodes(Node* curr) { while (curr) { Node* next = curr->next; switch (curr->type) { case NODE_WHITESPACE: /* all WS gets collapsed */ JsCollapseNodeToWhitespace(curr); break; case NODE_BLOCKCOMMENT: /* block comments get collapsed to WS if that's a side-affect * of their placement in the JS document. */ if (!nodeIsIECONDITIONALBLOCKCOMMENT(curr)) { int convert_to_ws = 0; /* find surrounding non-WS nodes */ Node* nonws_prev = curr->prev; Node* nonws_next = curr->next; while (nonws_prev && nodeIsWHITESPACE(nonws_prev)) nonws_prev = nonws_prev->prev; while (nonws_next && nodeIsWHITESPACE(nonws_next)) nonws_next = nonws_next->next; /* check what we're between... */ if (nonws_prev && nonws_next) { /* between identifiers? convert to WS */ if (nodeIsIDENTIFIER(nonws_prev) && nodeIsIDENTIFIER(nonws_next)) convert_to_ws = 1; /* between possible pre/post increment? convert to WS */ if (nodeIsCHAR(nonws_prev,'-') && nodeIsCHAR(nonws_next,'-')) convert_to_ws = 1; if (nodeIsCHAR(nonws_prev,'+') && nodeIsCHAR(nonws_next,'+')) convert_to_ws = 1; } /* convert to WS */ if (convert_to_ws) { JsSetNodeContents(curr," ",1); curr->type = NODE_WHITESPACE; } } break; 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 JsCanPrune(Node* node) { Node* prev = node->prev; Node* next = node->next; switch (node->type) { case NODE_EMPTY: /* prune empty nodes */ return PRUNE_CURRENT; case NODE_WHITESPACE: /* multiple whitespace gets pruned to preserve endspace */ if (prev && nodeIsENDSPACE(prev)) return PRUNE_CURRENT; if (prev && nodeIsWHITESPACE(prev)) return PRUNE_PREVIOUS; /* 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; /* keep comments that are for IE Conditional Compilation */ if (nodeIsIECONDITIONALBLOCKCOMMENT(node)) return PRUNE_NO; /* block comments get pruned */ return PRUNE_CURRENT; case NODE_LINECOMMENT: /* keep comments that contain the word "copyright" */ if (nodeContains(node, "copyright")) return PRUNE_NO; /* keep comments that are for IE Conditional Compilation */ if (nodeIsIECONDITIONALLINECOMMENT(node)) return PRUNE_NO; /* line comments get pruned */ return PRUNE_CURRENT; case NODE_IDENTIFIER: /* remove whitespace (but NOT endspace) after identifiers, provided * that next thing is -NOT- another identifier */ if (next && nodeIsWHITESPACE(next) && !nodeIsENDSPACE(next) && next->next && !nodeIsIDENTIFIER(next->next)) return PRUNE_NEXT; /* 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 whitespace (but NOT endspace) after closing brackets */ if (next && nodeIsWHITESPACE(next) && !nodeIsENDSPACE(next) && (nodeIsCHAR(node,')') || nodeIsCHAR(node,'}') || nodeIsCHAR(node,']'))) return PRUNE_NEXT; /* remove whitespace surrounding "/", EXCEPT where it'd cause "//" */ if (nodeIsCHAR(node,'/') && prev && nodeIsWHITESPACE(prev) && prev->prev && !nodeEndsWith(prev->prev,"/")) return PRUNE_PREVIOUS; if (nodeIsCHAR(node,'/') && next && nodeIsWHITESPACE(next) && next->next && !nodeBeginsWith(next->next,"/")) return PRUNE_NEXT; /* remove whitespace (but NOT endspace) surrounding "-", EXCEPT where it'd cause "--" */ if (nodeIsCHAR(node,'-') && prev && nodeIsWHITESPACE(prev) && !nodeIsENDSPACE(prev) && prev->prev && !nodeIsCHAR(prev->prev,'-')) return PRUNE_PREVIOUS; if (nodeIsCHAR(node,'-') && next && nodeIsWHITESPACE(next) && !nodeIsENDSPACE(next) && next->next && !nodeIsCHAR(next->next,'-')) return PRUNE_NEXT; /* remove whitespace (but NOT endspace) surrounding "+", EXCEPT where it'd cause "++" */ if (nodeIsCHAR(node,'+') && prev && nodeIsWHITESPACE(prev) && !nodeIsENDSPACE(prev) && prev->prev && !nodeIsCHAR(prev->prev,'+')) return PRUNE_PREVIOUS; if (nodeIsCHAR(node,'+') && next && nodeIsWHITESPACE(next) && !nodeIsENDSPACE(next) && next->next && !nodeIsCHAR(next->next,'+')) return PRUNE_NEXT; /* keep all other sigils */ return PRUNE_NO; } /* keep anything else */ return PRUNE_NO; } /* prune nodes from the list */ Node* JsPruneNodes(Node *head) { Node* curr = head; while (curr) { /* see if/howe we can prune this node */ int prune = JsCanPrune(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 */ JsDiscardNode(prev); /* reset "head" if that's what got pruned */ if (prev == head) prev = curr; break; case PRUNE_CURRENT: /* discard current node */ JsDiscardNode(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 */ JsDiscardNode(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 JavaScript, returning a newly allocated string back to * the caller (YOU'RE responsible for freeing its memory). * **************************************************************************** */ char* JsMinify(const char* string) { char* results; /* PASS 1: tokenize JS into a list of nodes */ Node* head = JsTokenizeString(string); if (!head) return NULL; /* PASS 2: collapse nodes */ JsCollapseNodes(head); /* PASS 3: prune nodes */ head = JsPruneNodes(head); if (!head) return NULL; /* PASS 4: re-assemble JS into single string */ { Node* curr; char* ptr; /* allocate the result buffer to the same size as the original JS; 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 */ JsFreeNodeList(head); /* return resulting minified JS back to caller */ return results; } MODULE = JavaScript::Minifier::XS PACKAGE = JavaScript::Minifier::XS PROTOTYPES: disable SV* minify(string) SV* string INIT: char* buffer = NULL; RETVAL = &PL_sv_undef; CODE: /* minify the JavaScript */ buffer = JsMinify( SvPVX(string) ); /* hand back the minified JS (if we had any) */ if (buffer != NULL) { RETVAL = newSVpv(buffer, 0); Safefree( buffer ); } OUTPUT: RETVAL JavaScript-Minifier-XS-0.11/lib000755001750001750 012462031340 16213 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/lib/JavaScript000755001750001750 012462031340 20261 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/lib/JavaScript/Minifier000755001750001750 012462031340 22023 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/lib/JavaScript/Minifier/XS.pm000444001750001750 652612462031340 23061 0ustar00grahamgraham000000000000package JavaScript::Minifier::XS; use 5.6.0; use strict; use warnings; require Exporter; require DynaLoader; our @ISA = qw(Exporter DynaLoader); our @EXPORT_OK = qw(minify); our $VERSION = '0.11'; bootstrap JavaScript::Minifier::XS $VERSION; 1; =head1 NAME JavaScript::Minifier::XS - XS based JavaScript minifier =head1 SYNOPSIS use JavaScript::Minifier::XS qw(minify); $minified = minify($js); =head1 DESCRIPTION C is a JavaScript "minifier"; its designed to remove un-necessary whitespace and comments from JavaScript files, which also B breaking the JavaScript. 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($js) Minifies the given C<$js>, returning the minified JavaScript back to the caller. =back =head1 HOW IT WORKS C minifies the JavaScript by removing un-necessary whitespace from JavaScript documents. Comments (both block and line) are also removed, I when (a) they contain the word "copyright" in them, or (b) they're needed to implement "IE Conditional Compilation". Internally, the minification process is done by taking multiple passes through the JavaScript document: =head2 Pass 1: Tokenize First, we go through and parse the JavaScript document into a series of tokens internally. The tokenizing process B check to make sure you've got syntactically valid JavaScript, 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. =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 are either (a) IE conditional compilation comments, or that (b) contain the word "copyright" in them are preserved. B other comments (line and block) are removed. =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 JavaScript 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 JavaScript that you're having troubles minifying. =head1 COPYRIGHT Copyright (C) 2007-2008, 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 JavaScript-Minifier-XS-0.11/t000755001750001750 012462031340 15710 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/t/01-loads.t000444001750001750 13112462031340 17525 0ustar00grahamgraham000000000000use strict; use Test::More tests=>1; BEGIN { use_ok( 'JavaScript::Minifier::XS' ); } JavaScript-Minifier-XS-0.11/t/02-minify.t000444001750001750 205612462031340 17747 0ustar00grahamgraham000000000000use strict; use warnings; use IO::File; use Test::More; use JavaScript::Minifier::XS qw(minify); ############################################################################### # figure out how many JS files we're going to run through for testing my @files = ; plan tests => scalar @files; ############################################################################### # test each of the JS files in turn foreach my $file (@files) { (my $min_file = $file) =~ s/\.js$/\.min/; my $str = slurp( $file ); my $min = slurp( $min_file ); my $res = eval { minify( $str ) }; is( $res, $min, $file ) or diag ($@); } ############################################################################### # 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; } JavaScript-Minifier-XS-0.11/t/04-not-javascript.t000444001750001750 114012462031340 21413 0ustar00grahamgraham000000000000use strict; use warnings; use Test::More; use JavaScript::Minifier::XS qw(minify); ############################################################################### # RT#58416; don't crash if attempting to minify something that isn't JS # ... while there's no guarantee that what we get back is _sane_, we should at # least not blow up or segfault. subtest "Minifying non-JS shouldn't crash" => sub { my $results = minify("not javascript"); pass "didn't segfault while processing non-javascript"; }; ############################################################################### done_testing(); JavaScript-Minifier-XS-0.11/t/03-minifies-to-nothing.t000444001750001750 113412462031340 22340 0ustar00grahamgraham000000000000use strict; use warnings; use Test::More tests => 3; use JavaScript::Minifier::XS qw(minify); my $results; ############################################################################### # Minifying down to "nothing" shouldn't segfault. # # RT #36557 described this for CSS::Minifier::XS, but we exhibit the same bug # here too. $results = minify( "/* */" ); ok( !defined $results, "minified block comment to nothing" ); $results = minify( "// foo" ); ok( !defined $results, "minified line comment to nothing" ); $results = minify( q{} ); ok( !defined $results, "minified empty string to nothing" ); JavaScript-Minifier-XS-0.11/t/js000755001750001750 012462031340 16324 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/t/js/prefix-sigil.min000444001750001750 3712462031340 21530 0ustar00grahamgraham000000000000function foo(){alert("foo!");} JavaScript-Minifier-XS-0.11/t/js/simple.min000444001750001750 1112462031340 20407 0ustar00grahamgraham000000000000var x=2; JavaScript-Minifier-XS-0.11/t/js/literals-single-quotes.min000444001750001750 10312462031340 23554 0ustar00grahamgraham000000000000var single_quoted=' single quoted strings // with line comments '; JavaScript-Minifier-XS-0.11/t/js/literals-regexp.min000444001750001750 3612462031340 22234 0ustar00grahamgraham000000000000var regexes=/ regexes stay /; JavaScript-Minifier-XS-0.11/t/js/comments-before-a-regex.js000444001750001750 103412462031340 23430 0ustar00grahamgraham000000000000/* comments placed directly before a regex should be skipped, instead of being * used to determine whether the leading '/' of the regexp is actually for * division or not. * * when its not working correctly, the regexes are parsed as division and that * causes the quote matching to get bungled up. */ var foo = [ // trick the engine into thinking we end in an array[] /^'/, // this *should* be parsed as a comment, not a literal /^"/, // isn't this the line with the closing apostrophe in it? /foo/ ]; JavaScript-Minifier-XS-0.11/t/js/simple.js000444001750001750 2712462031340 20247 0ustar00grahamgraham000000000000/* foo */ var x = 2; JavaScript-Minifier-XS-0.11/t/js/prefix-sigil.js000444001750001750 14312462031340 21377 0ustar00grahamgraham000000000000/* whitespace after "prefix" sigils should get removed */ function foo( ){ alert("foo!"); } JavaScript-Minifier-XS-0.11/t/js/comments-ie-conditional.js000444001750001750 52312462031340 23520 0ustar00grahamgraham000000000000/* comments get removed */ /*@ except those that are "IE Conditional Compilation" comments @*/ /*@ we'll remove those that start with the flag but don't end with it. */ /* as well as those that end with it but didn't start with it @*/ // line comments also get removed //@ except those that are "IE Conditional Compilation" line comments JavaScript-Minifier-XS-0.11/t/js/postfix-sigil.js000444001750001750 15412462031340 21600 0ustar00grahamgraham000000000000/* whitespace before "postfix" sigils should get removed */ function foo( ) { alert("foo!") ; } JavaScript-Minifier-XS-0.11/t/js/leading-whitespace.js000444001750001750 6712462031340 22517 0ustar00grahamgraham000000000000 var leading="leading whitespace gets removed"; JavaScript-Minifier-XS-0.11/t/js/division.js000444001750001750 10512462031340 20617 0ustar00grahamgraham000000000000/* use of "/" for division should get compacted */ var foo = 10 / 2; JavaScript-Minifier-XS-0.11/t/js/literals-single-quotes.js000444001750001750 17112462031340 23412 0ustar00grahamgraham000000000000/* quoted literals get preserved, in several forms */ var single_quoted=' single quoted strings // with line comments '; JavaScript-Minifier-XS-0.11/t/js/trailing-whitespace.min000444001750001750 6112462031340 23066 0ustar00grahamgraham000000000000var trailing="trailing whitespace gets removed"; JavaScript-Minifier-XS-0.11/t/js/literals-double-quotes.js000444001750001750 17512462031340 23407 0ustar00grahamgraham000000000000/* quoted literals get preserved, in several forms */ var double_quoted=" double quoted strings /* with block comments */ "; JavaScript-Minifier-XS-0.11/t/js/postfix-sigil.min000444001750001750 4012462031340 21721 0ustar00grahamgraham000000000000function foo() {alert("foo!");} JavaScript-Minifier-XS-0.11/t/js/leading-whitespace.min000444001750001750 5712462031340 22665 0ustar00grahamgraham000000000000var leading="leading whitespace gets removed"; JavaScript-Minifier-XS-0.11/t/js/comments-before-a-regex.min000444001750001750 3312462031340 23535 0ustar00grahamgraham000000000000var foo=[/^'/,/^"/,/foo/]; JavaScript-Minifier-XS-0.11/t/js/regexp-not-line-comment.min000444001750001750 5312462031340 23601 0ustar00grahamgraham000000000000function foo(url){return(/\//).test(url);} JavaScript-Minifier-XS-0.11/t/js/comments-ie-conditional.min000444001750001750 21112462031340 23661 0ustar00grahamgraham000000000000/*@ except those that are "IE Conditional Compilation" comments @*/ //@ except those that are "IE Conditional Compilation" line comments JavaScript-Minifier-XS-0.11/t/js/division.min000444001750001750 1612462031340 20747 0ustar00grahamgraham000000000000var foo=10/2; JavaScript-Minifier-XS-0.11/t/js/division-of-array-subscripts.js000444001750001750 41212462031340 24535 0ustar00grahamgraham000000000000/* * Division of an array subscript should NOT be treated as opening a regexp, * but should be treated as division. */ function foo() { var bar = someArray[2]/2; } function bar() { foo(); // this / is not a regexp close, its just part of a line comment } JavaScript-Minifier-XS-0.11/t/js/trailing-whitespace.js000444001750001750 7112462031340 22720 0ustar00grahamgraham000000000000var trailing="trailing whitespace gets removed"; JavaScript-Minifier-XS-0.11/t/js/comments.min000444001750001750 40112462031340 20766 0ustar00grahamgraham000000000000/* comments containing the word "copyright" are left in, though */ // including line comments, with mixed case cOpYrIgHt var foo=3;var bar=4;var replaced_with_ws=foo+ +bar;var also_replaced=foo- -bar;var removed_outright=foo+-bar;var also_removed=foo-+bar; JavaScript-Minifier-XS-0.11/t/js/literals-regexp.js000444001750001750 12412462031340 22103 0ustar00grahamgraham000000000000/* quoted literals get preserved, in several forms */ var regexes=/ regexes stay /; JavaScript-Minifier-XS-0.11/t/js/regexp-not-line-comment.js000444001750001750 21312462031340 23450 0ustar00grahamgraham000000000000/* RT#80598; regexps containing an escaped "/" should not be treated as comments */ function foo(url) { return ( /\// ).test( url ); } JavaScript-Minifier-XS-0.11/t/js/division-of-array-subscripts.min000444001750001750 7712462031340 24673 0ustar00grahamgraham000000000000function foo(){var bar=someArray[2]/2;} function bar(){foo();} JavaScript-Minifier-XS-0.11/t/js/comments.js000444001750001750 116512462031340 20647 0ustar00grahamgraham000000000000/* block comments get removed */ // as do line comments /* comments containing the word "copyright" are left in, though */ // including line comments, with mixed case cOpYrIgHt /* block comments placed inline get removed too. If they function as providing * whitespace between things that shouldn't be shoved together, though, they're * replaced with some whitespace. */ var foo /* remove */ = /* me too */ 3; var bar = /* and me */ 4; var replaced_with_ws = foo + /* ws */ +bar; var also_replaced = foo - /* ws */ -bar; var removed_outright = foo + /* me gone */ -bar; var also_removed = foo - /* me gone */ +bar; JavaScript-Minifier-XS-0.11/t/js/return-regex.min000444001750001750 4112462031340 21550 0ustar00grahamgraham000000000000function foo(){return/\d{1,2}/;} JavaScript-Minifier-XS-0.11/t/js/literals-double-quotes.min000444001750001750 10712462031340 23551 0ustar00grahamgraham000000000000var double_quoted=" double quoted strings /* with block comments */ "; JavaScript-Minifier-XS-0.11/t/js/return-regex.js000444001750001750 14012462031340 21421 0ustar00grahamgraham000000000000/* should be able to return a regex from a function */ function foo() { return /\d{1,2}/; } JavaScript-Minifier-XS-0.11/xt000755001750001750 012462031340 16100 5ustar00grahamgraham000000000000JavaScript-Minifier-XS-0.11/xt/pod.t000444001750001750 20112462031340 17155 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(); JavaScript-Minifier-XS-0.11/xt/leaks.t000444001750001750 126212462031340 17522 0ustar00grahamgraham000000000000#!/usr/bin/perl use strict; use warnings; use Test::More; use File::Slurp qw(slurp); use JavaScript::Minifier::XS qw(minify); BEGIN { eval "use Test::LeakTrace"; plan skip_all => "Test::LeakTrace required for leak testing" if $@; plan tests => 2; } use Test::LeakTrace; ############################################################################### # Suck in a bunch of JS to use for testing. my $js = ''; $js .= slurp($_) for (); ok length($js), 'got some JS to minify'; ############################################################################### # Make sure we're not leaking memory when we minify no_leaks_ok { minify($js) } 'no leaks when minifying JS'; JavaScript-Minifier-XS-0.11/xt/pod-coverage.t000444001750001750 23012462031340 20750 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(); JavaScript-Minifier-XS-0.11/xt/minimum-version.t000444001750001750 25312462031340 21540 0ustar00grahamgraham000000000000use Test::More; eval "use Test::MinimumVersion"; plan skip_all => "Test::MinimumVersion required for testing version requirements" if $@; all_minimum_version_ok('5.6.0'); JavaScript-Minifier-XS-0.11/xt/benchmark.t000444001750001750 410512462031340 20354 0ustar00grahamgraham000000000000use strict; use warnings; use Test::More; use IO::File; use Benchmark qw(countit); use JavaScript::Minifier::XS; ############################################################################### # check if JavaScript::Minifier available, so we can do comparison testing eval { require JavaScript::Minifier }; if ($@) { plan skip_all => 'JavaScript::Minifier not available for benchmark comparison'; } plan tests => 1; ############################################################################### # get the list of JS files we're going to run through testing # ... but remove "return-regex.js" as JavaScript::Minifier chokes on that one # (we're ok in JS:Min:XS, but JS:Min chokes). my @files = grep { !/return-regex/ } ; ############################################################################### # time test the PurePerl version against the XS version. compare_benchmark: { my $count; my $time = 10; diag "Benchmarking..."; # build a longer JavaScript document to process; 64KBytes should be # suitable my $content = join '', map { slurp($_) } @files; my $str = ''; while (1) { last if (length($str) > (64*1024)); $str .= $content; } # benchmark the original "pure perl" version $count = countit( $time, sub { JavaScript::Minifier::minify(input=>$str) } ); my $rate_pp = ($count->iters() / $time) * length($str); diag "\tperl\t=> $rate_pp bytes/sec"; # benchmark the "XS" version $count = countit( $time, sub { JavaScript::Minifier::XS::minify($str) } ); my $rate_xs = ($count->iters() / $time) * length($str); diag "\txs\t=> $rate_xs bytes/sec"; pass 'benchmarking'; } ############################################################################### # 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; } JavaScript-Minifier-XS-0.11/xt/test-compile.t000444001750001750 437312462031340 21036 0ustar00grahamgraham000000000000#!/usr/bin/perl # # test-compile.t # # Tries to minify a handful of JS libs and verify that they're still able to be # compiled after being minified. While not a 100% guarantee that we haven't # broken anything, it does give us a chance to test minification against a # larger suite of JS. # ############################################################################### use strict; use warnings; use Test::More; use IPC::Run qw(run); use File::Which qw(which); use JavaScript::Minifier::XS qw(minify); ############################################################################### # SKIP on Travis-CI for now (until I find a replacement for "jsl") plan skip_all => "Skipping on Travis-CI.org for now" if ($ENV{TRAVIS_BUILD_ID}); ############################################################################### # Make sure we've got "curl" and "jsl" installed. my $curl = which('curl'); my $jsl = which('jsl'); unless ($curl && $jsl) { plan skip_all => "Test requires 'curl' and 'jsl'"; } ############################################################################### # What JS libraries should we try to minify? my @libs = qw( https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.js https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.js https://raw.githubusercontent.com/christianbach/tablesorter/master/jquery.tablesorter.js ); ############################################################################### # Suck down a bunch of popular JS libraries, and try to minify them all. foreach my $uri (@libs) { subtest $uri => sub { my $content = qx{$curl --silent $uri}; ok defined $content, 'fetched JS'; return unless (defined $content); # try to compile the original JS ok js_compile($content), 'original JS compiles'; # minify the JS my $minified = minify($content); ok $minified, 'minified JS'; # try to compile the minified JS ok js_compile($minified), 'minified JS compiles'; }; } sub js_compile { my $js = shift; my ($rc, $out, $err); run [$jsl, '-stdin', '-nosummary'], \$js, \$out, \$err; $rc = $? >> 8; return $rc <= 2; } ############################################################################### # All done! done_testing();